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

Контекстная справка по F1 и What’s This: привязка тем справки к виджетам

Введение

Контекстная справка — когда пользователь нажимает F1 на конкретном элементе интерфейса (поле формы, кнопке, диалоге) и сразу попадает на релевантную страницу справки, а не на общий стартовый экран — значительно повышает воспринимаемое качество справочной системы. В Qt это реализуется через сочетание системы событий (перехват нажатия F1) и сопоставления конкретных виджетов с конкретными URL внутри справочника. В статье разберём оба механизма — собственно контекстную справку по F1, и схожий, но самостоятельный механизм What’s This.

Концепция

Привязка виджета к конкретной странице справки делается через присвоение виджету произвольного идентификатора (например, через setProperty("helpId", "settings.network") или через objectName()) и последующую обработку события QHelpEvent (для What’s This) или глобального перехвата клавиши F1 в QMainWindow/диалоге. При нажатии F1 приложение определяет, какой виджет находится в фокусе (или под курсором), сопоставляет его идентификатор с URL в справочнике (через таблицу соответствий, которую разработчик ведёт сам — Qt не делает это автоматически) и открывает соответствующую страницу справки.

QWhatsThis — отдельный, более лёгковесный механизм: каждый виджет может иметь короткий текст подсказки (setWhatsThis()), отображаемый во всплывающем окне при активации специального режима «Что это?» (курсор со знаком вопроса) — это не полноценная справка, а краткая встроенная подсказка, иногда дополнительно содержащая ссылку на полную статью справки.

Пример кода: Контекстная справка по F1 и What’s This

#pragma once
#include <QWidget>
#include <QHash>
#include <QString>
#include <QKeyEvent>
#include "helpsystem.h"

class ContextHelpManager : public QObject
{
    Q_OBJECT
public:
    explicit ContextHelpManager(HelpSystem *helpSystem, QObject *parent = nullptr)
        : QObject(parent), m_helpSystem(helpSystem) {}

    // Связывает идентификатор контекстной справки с конкретным виджетом
    void registerHelpTopic(QWidget *widget, const QString &helpKeyword)
    {
        widget->setProperty("helpKeyword", helpKeyword);
        widget->installEventFilter(this);
    }

protected:
    bool eventFilter(QObject *watched, QEvent *event) override
    {
        if (event->type() == QEvent::KeyPress) {
            auto *keyEvent = static_cast<QKeyEvent *>(event);
            if (keyEvent->key() == Qt::Key_F1) {
                auto *widget = qobject_cast<QWidget *>(watched);
                if (widget) {
                    showHelpForWidget(widget);
                    return true; // событие обработано, не передаём дальше
                }
            }
        }
        return QObject::eventFilter(watched, event);
    }

private:
    void showHelpForWidget(QWidget *widget)
    {
        const QString keyword = widget->property("helpKeyword").toString();
        if (keyword.isEmpty()) {
            qDebug() << "Для этого элемента справка не настроена, открываем общую страницу";
            emit helpRequested(QUrl("qthelp://com.mycompany.myapp.1.0/doc/index.html"));
            return;
        }

        const QUrl page = m_helpSystem->findPageByKeyword(keyword);
        emit helpRequested(page);
    }

signals:
    void helpRequested(const QUrl &page);

private:
    HelpSystem *m_helpSystem;
};
// Использование при построении диалога настроек
#include "contexthelpmanager.h"
#include <QDialog>
#include <QLineEdit>
#include <QFormLayout>

class NetworkSettingsDialog : public QDialog
{
    Q_OBJECT
public:
    NetworkSettingsDialog(ContextHelpManager *helpManager, QWidget *parent = nullptr)
        : QDialog(parent)
    {
        auto *proxyField = new QLineEdit(this);
        proxyField->setWhatsThis("Адрес прокси-сервера в формате host:port. "
                                  "Оставьте пустым для прямого подключения.");

        helpManager->registerHelpTopic(proxyField, "прокси-сервер");
        helpManager->registerHelpTopic(this, "настройки.сеть");

        auto *layout = new QFormLayout(this);
        layout->addRow("Прокси-сервер:", proxyField);
    }
};

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

ContextHelpManager::registerHelpTopic() сохраняет ключевое слово справки прямо как динамическое свойство (setProperty) на самом виджете и устанавливает на него фильтр событий — это позволяет не вести отдельную, синхронизируемую вручную глобальную таблицу «виджет → URL справки» где-то на стороне, а хранить эту связь непосредственно вместе с виджетом, к которому она относится.

eventFilter() перехватывает нажатие F1 на уровне любого виджета, для которого был вызван registerHelpTopic(), и если у виджета не задано собственное ключевое слово (как для this — самого диалога, зарегистрированного с общей темой «настройки.сеть»), F1, нажатая где-то внутри диалога, но не на специфичном поле, откроет общую страницу темы диалога — это реализует разумное поведение по умолчанию для частей интерфейса без детальной привязки. setWhatsThis() на поле proxyField работает независимо от системы F1-справки — это отдельный, более лёгкий механизм всплывающей подсказки.

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

  • Несинхронизированные ключевые слова между кодом интерфейса и .qhp-индексом справки. Если ключевое слово, зарегистрированное в коде ("прокси-сервер"), не совпадает точно (включая регистр и форму слова) с ключевым словом, объявленным в <keywords> .qhp-файла, поиск через findPageByKeyword() не найдёт страницу — это рассинхронизация между двумя независимо поддерживаемыми артефактами (код и контент справки), требующая дисциплины или автоматизированной проверки соответствия.
  • Перехват F1 на уровне eventFilter конкурирует с глобальными или платформенными обработчиками F1. Если приложение уже использует F1 для чего-то другого (стандартный shortcut для справки в QMenuBar или платформенная обработка), нужно явно решить порядок приоритета — несогласованная настройка может привести к тому, что F1 откроет не ту справку, что ожидается, либо не сработает вовсе из-за конфликта обработчиков.
  • Жёсткая привязка по setProperty/objectName() без проверки её сохранности при пересоздании виджетов (например, при динамическом построении формы из конфигурации в рантайме) — если виджет с привязкой к теме справки регулярно пересоздаётся, регистрацию через registerHelpTopic() нужно повторять при каждом пересоздании, что легко упустить и обнаружить только при ручном тестировании F1 на конкретном экране.
  • What’s This текст, дублирующий или, наоборот, противоречащий содержанию полной статьи справки по той же теме — поскольку это два независимо поддерживаемых источника текста (короткая встроенная подсказка и полная HTML-страница), их легко рассинхронизировать при обновлении одного без обновления другого, что создаёт противоречивую информацию для пользователя, обратившегося к обоим источникам.