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

Собственный справочный браузер на QTextBrowser/QWebEngineView поверх QHelpEngine

Введение

QHelpEngine сам по себе не отображает HTML — для полноценного встроенного справочного браузера нужен виджет отображения, подключённый к схеме qthelp://, по которой адресуются страницы внутри справочника. В статье построим минимальный, но полнофункциональный справочный браузер с навигацией по TOC, кнопками «вперёд»/»назад» и поиском, используя QTextBrowser как основу — он легче по весу, чем QWebEngineView, и достаточен для большинства справочных текстов без сложного JavaScript.

Концепция

QTextBrowser поддерживает навигацию по гиперссылкам «из коробки», но по умолчанию умеет загружать только файлы с диска или ресурсы Qt — для поддержки схемы qthelp://, по которой адресуются страницы внутри .qch, нужно унаследовать QTextBrowser и переопределить loadResource(), перенаправляя загрузку данных через QHelpEngine::fileData() вместо стандартной файловой загрузки. После этого обычные <a href="qthelp://..."> ссылки внутри HTML-контента справки начинают работать прозрачно через встроенный браузер.

Пример кода: Собственный справочный браузер

// helpbrowser.h
#pragma once
#include <QTextBrowser>
#include <QHelpEngine>

class HelpBrowser : public QTextBrowser
{
    Q_OBJECT
public:
    explicit HelpBrowser(QHelpEngine *helpEngine, QWidget *parent = nullptr)
        : QTextBrowser(parent), m_helpEngine(helpEngine) {}

protected:
    QVariant loadResource(int type, const QUrl &url) override
    {
        if (url.scheme() == "qthelp") {
            return QVariant(m_helpEngine->fileData(url));
        }
        return QTextBrowser::loadResource(type, url);
    }

private:
    QHelpEngine *m_helpEngine;
};
// helpwindow.h — окно справки с TOC-навигацией и кнопками истории
#pragma once
#include <QMainWindow>
#include <QSplitter>
#include <QTreeView>
#include <QToolBar>
#include "helpbrowser.h"

class HelpWindow : public QMainWindow
{
    Q_OBJECT
public:
    explicit HelpWindow(QHelpEngine *helpEngine, QWidget *parent = nullptr)
        : QMainWindow(parent), m_helpEngine(helpEngine)
    {
        setWindowTitle("Справка MyApp");
        resize(900, 600);

        m_browser = new HelpBrowser(helpEngine, this);

        auto *contentTree = new QTreeView(this);
        contentTree->setModel(helpEngine->contentModel());
        contentTree->setHeaderHidden(true);
        connect(contentTree, &QTreeView::clicked, this, &HelpWindow::onContentItemClicked);

        auto *splitter = new QSplitter(this);
        splitter->addWidget(contentTree);
        splitter->addWidget(m_browser);
        splitter->setStretchFactor(1, 1); // браузер занимает основное пространство
        setCentralWidget(splitter);

        auto *toolbar = addToolBar("Навигация");
        toolbar->addAction("Назад", m_browser, &QTextBrowser::backward);
        toolbar->addAction("Вперёд", m_browser, &QTextBrowser::forward);
        toolbar->addAction("Домой", this, [this]() {
            navigateTo(QUrl("qthelp://com.mycompany.myapp.1.0/doc/source/index.html"));
        });
    }

    void navigateTo(const QUrl &url)
    {
        m_browser->setSource(url);
    }

private slots:
    void onContentItemClicked(const QModelIndex &index)
    {
        auto *model = static_cast<QHelpContentModel *>(m_helpEngine->contentModel());
        QHelpContentItem *item = model->contentItemAt(index);
        if (item) {
            navigateTo(item->url());
        }
    }

private:
    QHelpEngine *m_helpEngine;
    HelpBrowser *m_browser;
};

Пояснения к коду

HelpBrowser::loadResource() — ключевой момент интеграции: при попытке QTextBrowser загрузить ресурс по URL со схемой qthelp://, вместо стандартной обработки (которая не умеет работать с этой схемой и вернула бы пустой результат) вызывается m_helpEngine->fileData(url), который извлекает содержимое страницы непосредственно из скомпилированного .qch — для любых других схем (обычные файлы, ресурсы Qt) поведение делегируется обратно базовой реализации через QTextBrowser::loadResource().

HelpWindow связывает QTreeView с моделью helpEngine->contentModel() — той самой, что Qt Help Framework строит автоматически на основе TOC из .qhp — клик по любому узлу дерева получает соответствующий QHelpContentItem и его URL, который передаётся в navigateTo(). Кнопки «Назад»/»Вперёд» на панели инструментов используют встроенную в QTextBrowser историю навигации (backward()/forward()), не требующую отдельной ручной реализации стека истории.

Подводные камни

  • QTextBrowser поддерживает ограниченное подмножество HTML/CSS — сложная вёрстка с современным CSS (flexbox, grid), JavaScript-интерактивность или сложные таблицы могут отображаться некорректно или не отображаться вовсе; если контент справки сложнее простого текста с изображениями и базовым форматированием, может потребоваться QWebEngineView (с дополнительной интеграцией через QWebEngineUrlSchemeHandler для той же схемы qthelp://, что технически сложнее, чем переопределение loadResource()).
  • Изображения и другие вложенные ресурсы внутри HTML-страниц справки также должны проходить через loadResource(). Если страница ссылается на <img src="qthelp://.../images/logo.png">, этот запрос точно так же обрабатывается переопределённым методом — но стоит явно протестировать загрузку вложенных ресурсов, а не только основных HTML-страниц, поскольку тип ресурса (QTextDocument::ImageResource и так далее) передаётся в type, и иногда логика должна различать типы ресурсов, а не обрабатывать их одинаково.
  • QHelpContentModel обновляется асинхронно после registerDocumentation(). Сразу после регистрации нового .qch модель может быть ещё не готова — для надёжного отображения дерева содержания стоит дождаться сигнала QHelpEngine::setupFinished() перед привязкой модели к QTreeView, а не предполагать её немедленную готовность.
  • Отсутствие обработки ситуации «страница не найдена» при битой ссылке внутри справки (например, после правки .qhp без обновления конкретной страницы) — fileData() в этом случае может вернуть пустой QByteArray, и QTextBrowser просто отобразит пустую страницу без какого-либо сообщения об ошибке, что затрудняет диагностику для конечного пользователя.