Введение
В первой статье цикла мы кратко упомянули <customFilter> в .qhp как механизм пометки контента — в этой статье разберём его подробнее на практическом сценарии: продукт с несколькими тарифами (Basic/Pro/Enterprise), где часть функциональности и, соответственно, часть справочного контента доступна не всем пользователям. Custom filters позволяют скомпилировать единую базу контента, но показывать пользователю только релевантное для его конкретной конфигурации подмножество страниц.
Концепция
Каждая страница (или раздел TOC) может быть помечена набором атрибутов фильтра через filterAttribute внутри соответствующего блока <filterSection> — при наличии нескольких <filterSection> в одном .qhp, каждый описывает своё подмножество TOC/keywords/files для своей комбинации атрибутов. В рантайме приложение указывает QHelpEngine, какой набор атрибутов активен для текущего пользователя (через setCurrentFilter() или, в более новых версиях API, через QHelpFilterEngine), и движок автоматически показывает в TOC и индексе только контент, соответствующий активному фильтру — без необходимости поддерживать несколько физически разных .qch-файлов для разных тарифов.
Пример кода: Custom filters глубже
<!-- myapp.qhp — два набора фильтров для разных тарифов в одном файле -->
<?xml version="1.0" encoding="UTF-8"?>
<QtHelpProject version="1.0">
<namespace>com.mycompany.myapp.1.0</namespace>
<virtualFolder>doc</virtualFolder>
<customFilter name="Basic">
<filterAttribute>tier_basic</filterAttribute>
</customFilter>
<customFilter name="Pro">
<filterAttribute>tier_basic</filterAttribute>
<filterAttribute>tier_pro</filterAttribute>
</customFilter>
<!-- Контент, доступный во всех тарифах -->
<filterSection>
<filterAttribute>tier_basic</filterAttribute>
<toc>
<section title="Начало работы" ref="source/getting_started.html" />
</toc>
<files>
<file>source/getting_started.html</file>
</files>
</filterSection>
<!-- Контент, доступный только в Pro и выше -->
<filterSection>
<filterAttribute>tier_pro</filterAttribute>
<toc>
<section title="Расширенная аналитика (Pro)" ref="source/advanced_analytics.html" />
</toc>
<files>
<file>source/advanced_analytics.html</file>
</files>
</filterSection>
</QtHelpProject>
#include <QHelpEngine>
#include <QHelpFilterEngine>
void configureHelpForUserTier(QHelpEngine &engine, const QString &userTier)
{
auto *filterEngine = engine.filterEngine();
// userTier — "basic" или "pro", определяется лицензией пользователя
const QString filterName = (userTier == "pro") ? "Pro" : "Basic";
if (filterEngine->filters().contains(filterName)) {
filterEngine->setActiveFilter(filterName);
qDebug() << "Активирован фильтр справки:" << filterName;
} else {
qWarning() << "Фильтр" << filterName << "не найден в .qch";
}
}
Пояснения к коду
Каждый <customFilter> объявляет именованный набор атрибутов, доступный для выбора в рантайме — фильтр "Basic" включает только атрибут tier_basic, а "Pro" включает и tier_basic, и tier_pro одновременно, что отражает иерархическую природу тарифов (Pro включает в себя всё из Basic плюс дополнительный контент). Каждый <filterSection> помечен своим набором filterAttribute — раздел «Расширенная аналитика» помечен только tier_pro, и поэтому появится в TOC только когда активен фильтр, включающий этот атрибут (то есть "Pro", но не "Basic").
configureHelpForUserTier() показывает, как приложение в рантайме выбирает активный фильтр на основе реальной лицензии пользователя через QHelpFilterEngine::setActiveFilter() — после этого вызова engine.contentModel() автоматически перестраивается, отражая только контент, релевантный для выбранного фильтра, без какой-либо ручной фильтрации на стороне кода приложения.
Подводные камни
- Имя фильтра, передаваемое в
setActiveFilter(), должно точно совпадать с именем, объявленным в<customFilter name="...">— несовпадение (включая регистр) не выдаёт явной ошибки, а просто оставляет фильтр неактивным, и пользователь увидит либо весь немодифицированный контент, либо вовсе пустую справку, в зависимости от того, как настроено поведение «фильтр не выбран» в конкретной версии Qt. - Усложнение поддержки контента при росте числа тарифов и комбинаций функциональности. Если число различающихся комбинаций атрибутов растёт (не просто линейная иерархия Basic → Pro → Enterprise, а множество независимых фич, доступных в разных сочетаниях), управление пересекающимися
<filterSection>становится сложным для поддержки авторами контента — для таких случаев на практике часто проще ограничиться небольшим числом базовых уровней фильтрации, а не пытаться покрыть фильтрами каждую отдельную фичу. - Один и тот же файл, упомянутый в нескольких
<filterSection>с разным набором атрибутов, физически встраивается в .qch только один раз, но логически «виден» в разных фильтрах — путаница может возникнуть, если автор контента предполагает, что одна и та же физическая страница может иметь разное содержание в разных фильтрах, что не так: фильтрация работает на уровне видимости целых страниц/разделов в навигации, а не на уровне содержания внутри одной и той же страницы. - Полнотекстовый поиск может не учитывать активный фильтр в более старых версиях Qt Help Framework, выдавая результаты по контенту, недоступному пользователю в текущем тарифе через TOC, что создаёт нежелательный «тизер» недоступной функциональности — стоит явно протестировать поведение поиска при включённом нетривиальном фильтре в той версии Qt, на которой работает приложение.