Различные статьи по Qt и c++

Справка в Qt Quick/QML-приложениях: WebView и кастомный TOC-делегат

Введение

Весь цикл статей про 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 — для более гибкой настройки (например, разный путь к коллекции в зависимости от конфигурации) нужна отдельная фабрика или явная инициализация перед первым использованием.