Введение
Весь цикл статей про Qt Help до этого момента ориентировался на Qt Widgets (QTextBrowser, QTreeView) — но множество современных Qt-приложений построены полностью на Qt Quick/QML, без единого виджета. В статье разберём, как встроить ту же справочную систему на основе QHelpEngine в чисто QML-приложение, используя WebEngineView для отображения контента и кастомный QML-делегат для навигации по TOC вместо QTreeView.
Концепция
QHelpEngine — C++ класс, и для использования в QML его нужно либо зарегистрировать как QML-тип (через QML_ELEMENT, как в статье про синглтоны), либо обернуть в промежуточный класс, экспортирующий нужную функциональность через Q_INVOKABLE-методы и Q_PROPERTY. Контент справки отображается через WebEngineView (из модуля Qt WebEngine) — для него, в отличие от QTextBrowser, нужна интеграция через QWebEngineUrlSchemeHandler, чтобы обрабатывать схему qthelp://, поскольку WebEngineView не предоставляет аналог простого переопределения loadResource().
Пример кода: Справка в Qt Quick/QML-приложениях
// qthelpurlschemehandler.h — обработчик схемы qthelp:// для QWebEngine
#pragma once
#include <QWebEngineUrlSchemeHandler>
#include <QWebEngineUrlRequestJob>
#include <QHelpEngine>
#include <QBuffer>
class QtHelpUrlSchemeHandler : public QWebEngineUrlSchemeHandler
{
Q_OBJECT
public:
explicit QtHelpUrlSchemeHandler(QHelpEngine *helpEngine, QObject *parent = nullptr)
: QWebEngineUrlSchemeHandler(parent), m_helpEngine(helpEngine) {}
void requestStarted(QWebEngineUrlRequestJob *job) override
{
const QUrl url = job->requestUrl();
const QByteArray data = m_helpEngine->fileData(url);
if (data.isEmpty()) {
job->fail(QWebEngineUrlRequestJob::UrlNotFound);
return;
}
auto *buffer = new QBuffer(job);
buffer->setData(data);
buffer->open(QIODevice::ReadOnly);
const QString mimeType = url.path().endsWith(".png") ? "image/png" : "text/html";
job->reply(mimeType.toUtf8(), buffer);
}
private:
QHelpEngine *m_helpEngine;
};
// helpbridge.h — мост между QHelpEngine и QML
#pragma once
#include <QObject>
#include <QHelpEngine>
#include <QQmlEngine>
class HelpBridge : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
public:
explicit HelpBridge(QObject *parent = nullptr) : QObject(parent) {}
Q_INVOKABLE QVariantList contentTree()
{
QVariantList result;
auto *model = m_helpEngine.contentModel();
buildTreeRecursive(model->contentItemAt(model->index(0, 0)), result);
return result;
}
Q_INVOKABLE QString homeUrl() const
{
return "qthelp://com.mycompany.myapp.1.0/doc/source/index.html";
}
private:
void buildTreeRecursive(QHelpContentItem *item, QVariantList &out)
{
if (!item) return;
QVariantMap node;
node["title"] = item->title();
node["url"] = item->url().toString();
out.append(node);
for (int i = 0; i < item->childCount(); ++i) {
buildTreeRecursive(item->child(i), out);
}
}
QHelpEngine m_helpEngine{"help_collection.qhc"};
};
// HelpPage.qml — отображение справки в QML
import QtQuick
import QtQuick.Controls
import QtWebEngine
import App.Help 1.0
Item {
anchors.fill: parent
RowLayout {
anchors.fill: parent
// Кастомный TOC-делегат вместо QTreeView
ListView {
Layout.preferredWidth: 250
Layout.fillHeight: true
model: HelpBridge.contentTree()
delegate: ItemDelegate {
width: parent.width
text: modelData.title
onClicked: webView.url = modelData.url
}
}
WebEngineView {
id: webView
Layout.fillWidth: true
Layout.fillHeight: true
url: HelpBridge.homeUrl()
}
}
}
Пояснения к коду
QtHelpUrlSchemeHandler регистрируется в QWebEngineProfile (регистрация показана в реальном проекте отдельным вызовом installUrlSchemeHandler("qthelp", handler), не включённым в листинг для краткости) и перехватывает каждый запрос к схеме qthelp://, перенаправляя его через m_helpEngine->fileData() — это концептуальный аналог переопределения loadResource() у QTextBrowser, но реализованный через отдельный, более многословный API QWebEngineUrlSchemeHandler, специфичный для движка WebEngine.
HelpBridge — мост, регистрирующий себя как QML-синглтон (паттерн из статьи 88) и предоставляющий contentTree() — упрощённое плоское представление иерархии TOC в виде QVariantList, которое легко потребляется обычным ListView в QML. В реальном проекте плоское представление обычно дополняется явным указанием уровня вложенности для визуального отступа в делегате, но базовая идея — конвертация C++-специфичной модели QHelpContentModel в простые, «плоские» QML-совместимые структуры данных — продемонстрирована полностью.
Подводные камни
QWebEngineUrlSchemeHandlerдолжен быть зарегистрирован до создания первогоWebEngineViewв приложении — регистрация схем выполняется на уровне профиля и должна происходить как можно раньше в жизненном цикле приложения (обычно вmain(), до запуска QML-движка), иначе первая загрузкаqthelp://-ссылки завершится ошибкой, поскольку схема ещё не была известна движку.- Плоское представление дерева TOC в
contentTree()без сохранения иерархии теряет визуальную вложенность подразделов — для полноценной заменыQTreeViewнужен либо вложенныйRepeater/TreeView(доступный в Qt Quick Controls с версии 6.3+), либо явное добавление поля «уровень вложенности» в каждый элемент плоского списка, используемое делегатом для визуального отступа. WebEngineView— значительно более тяжёлая зависимость, чемQTextBrowser. Модуль Qt WebEngine основан на Chromium и существенно увеличивает размер дистрибутива приложения и время запуска — для справки с относительно простым HTML-контентом без необходимости в полной поддержке современного CSS/JS этот выбор стоит сопоставить с более лёгкимTextArea/кастомным QML-рендерером простого HTML, если такой компромисс приемлем для конкретного продукта.- Несоответствие жизненного цикла
QHelpEngineвHelpBridgeмодели владения QML. ПосколькуHelpBridgeзарегистрирован какQML_SINGLETON, аQHelpEngineсоздаётся как его член с захардкоженным путём к файлу коллекции прямо в объявлении класса, инициализация привязана к моменту первого обращения к синглтону из QML — для более гибкой настройки (например, разный путь к коллекции в зависимости от конфигурации) нужна отдельная фабрика или явная инициализация перед первым использованием.