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