Мой блог
Динамическое разграничение прав в MFC Ribbon: паттерн Dynamic Ribbon Pruning
При разработке крупных промышленно-ориентированных десктопных приложений, таких как системы SCADA или HMI для технологических линий, инженер неизбежно сталкивается с задачей разграничения прав доступа к органам управления. Если ваш графический стек построен на классической библиотеке C++/MFC и компоненте CMFCRibbonBar, то первое, на что вы наткнетесь в стандартной документации MSDN, — это совет использовать механизмы ON_UPDATE_COMMAND_UI.
Официальный подход Microsoft звучит просто: если у текущего пользователя нет прав на выполнение той или иной команды, соответствующую кнопку на ленте Ribbon следует просто деактивировать, превратив в серую и неактивную иконку. Однако в условиях реального производства этот подход создает колоссальные проблемы для операторов.

Официальные рекомендации Microsoft и суровая реальность UX
Представьте себе рабочее место диспетчера или оператора цеха. В крупном проекте лента управления может содержать больше десятка тематических вкладок: «Цех варки», «Упаковочная линия», «Справочники ингредиентов», «Аналитические отчеты», «Администрирование пользователей» и так далее. Каждая вкладка включает десятки команд и регуляторов.
Если за терминал садится обычный линейный рабочий, согласно подходу Microsoft перед ним разворачивается огромное «кладбище серых иконок». Полезная площадь дисплея расходуется крайне неэффективно, а сам пользователь вынужден тратить время на визуальный поиск двух-трех нужных ему активных кнопок среди сотен заблокированных. На моем опыте такой подход ведет к операторским ошибкам и замедляет работу.
Мы решили пойти более радикальным, но правильным с точки зрения UX путем: полностью удалять недоступные вкладки и категории прямо «на лету». Если системе авторизовался директор или главный инженер, он видит весь массив инструментов. Если же за консолью находится варщик котла, интерфейс автоматического перестраивается и оставляет исключительно вкладку «Управление котлами».
Коварство прямого удаления: трагедия смещения индексов в MFC
Первая мысль, которая приходит в голову разработчику при взгляде на API CMFCRibbonBar, — загрузить структуру ленты из ресурсов, пробежать по массиву категорий обычным циклом и вызвать метод RemoveCategory(index) для всех лишних элементов.
Однако здесь программиста поджидает типовая ловушка работы с динамическими контейнерами — инвалидация и смещение индексов в процессе их обхода. Посмотрим на классическую ошибку:
// ОПАСНО: Пример некорректной реализации удалений категорий
for (int i = 0; i < m_wndRibbonBar.GetCategoryCount(); i++)
{
if (NeedToRemoveCategory(i))
{
m_wndRibbonBar.RemoveCategory(i);
}
}
В чем заключается деструктивность этого кода? Представим, что цикл находится на индексе i = 1 и метод NeedToRemoveCategory(1) возвращает true. Мы вызываем RemoveCategory(1). Элемент под индексом 1 удаляется, а находившийся за ним элемент под индексом 2 мгновенно сдвигается влево, занимая освободившуюся позицию 1.
После этого итератор цикла выполняет шаг i++, переходя к i = 2. В результате сдвинувшийся элемент (бывший 2-й, ставший 1-м) оказывается полностью пропущенным! В лучшем случае часть защищенных вкладок останется на экране, в худшем — при выходе индекса за физические границы пересчитанного массива приложение завершится критическим сбоем с ошибкой Access Violation (Out of Range).
В некоторых промышленных проектах для обхода этой проблемы создают тяжеловесные внешние XML-парсеры, которые при каждом входе пользователя генерируют структуру Ribbon в памяти с нуля. Я считаю такой подход избыточным. Для решения задачи мы применили куда более чистое и лаконичное архитектурное решение.
Паттерн Dynamic Ribbon Pruning: элегантная фильтрация вкладок
Разработанный нами метод основан на полном сбросе ленты к исходному состоянию из .rc-ресурса исполняемого файла с последующим обходом контейнера категорий с помощью цикла do-while с условным шагом (умным сдвигом индекса).
Суть алгоритма проста: если текущая категория подлежит удалению, мы удаляем ее, но оставляем текущий индекс обхода на месте, одновременно обновляя верхнюю границу общего счетчика элементов. Если же категория проходит проверку прав, мы сохраняем ее и только в этом случае инкрементируем индекс.
Ниже приведен полностью рабочий C++ фрагмент метода, который я успешно применяю в производственных системах:
void CMainFrame::UpdateUserRibbonPermissions()
{
// 1. Полный сброс и повторная инициализация структуры RibbonBar из ресурсов
m_wndRibbonBar.RemoveAllCategories();
m_wndRibbonBar.RemoveAllFromTabs();
m_wndRibbonBar.LoadFromResource(IDR_RIBBON);
m_wndRibbonBar.RecalcLayout();
// 2. Безопасная извлечение маски прав в многопоточной среде
// Используем мьютекс и создаем локальный примитив прав, исключая Data Race
UINT uiLocalPermission = 0;
bool bUserIsLogged = false;
{
std::lock_guard<std::mutex> mapLock(m_mtxDB);
if (m_pCurrentUser)
{
bUserIsLogged = true;
uiLocalPermission = m_pCurrentUser->iPermition;
}
}
int nCategoryCount = m_wndRibbonBar.GetCategoryCount();
int nCategoryNdx = 0;
bool bCategoryWasRemoved;
// 3. Отказоустойчивый конвейер динамической обрезки (Dynamic Ribbon Pruning)
do
{
bCategoryWasRemoved = false;
CMFCRibbonCategory* pmrc = m_wndRibbonBar.GetCategory(nCategoryNdx);
if (!pmrc)
break;
CString strCategoryName = pmrc->GetName();
// Побитовая проверка прав на основе локальной маски
if (strCategoryName == TAB_TABLES) // Вкладка "Таблицы"
{
if (bUserIsLogged && (((uiLocalPermission >> 4) & 1) == false))
{
m_wndRibbonBar.RemoveCategory(nCategoryNdx);
bCategoryWasRemoved = true;
}
}
else if (strCategoryName == TAB_GRAPHS) // Вкладка "Графики"
{
if (bUserIsLogged && (((uiLocalPermission >> 5) & 1) == false))
{
m_wndRibbonBar.RemoveCategory(nCategoryNdx);
bCategoryWasRemoved = true;
}
}
else if (strCategoryName == TAB_SPRAV) // Вкладка "Справочники"
{
if (bUserIsLogged && (((uiLocalPermission >> 6) & 1) == false))
{
m_wndRibbonBar.RemoveCategory(nCategoryNdx);
bCategoryWasRemoved = true;
}
}
// ... Аналогичные проверки для остальных технологических категорий ...
// КЛЮЧЕВАЯ ЛОГИКА ИНДЕКСАЦИИ:
if (!bCategoryWasRemoved)
nCategoryNdx++; // Продвигаемся вперед только при СОХРАНЕНИИ категории
else
nCategoryCount = m_wndRibbonBar.GetCategoryCount(); // Пересчитываем длину при УДАЛЕНИИ
} while (nCategoryNdx < nCategoryCount);
// 4. Корректное скрытие кнопки меню (Application Button) через WinAPI/MFC
CMFCRibbonApplicationButton* pAppBtn = m_wndRibbonBar.GetApplicationButton();
if (pAppBtn != nullptr)
{
pAppBtn->SetVisible(FALSE);
m_wndRibbonBar.SetApplicationButton(NULL, 0); // Обнуление убирает сдвиг влево
}
// Финальный пересчет геометрии оконных элементов
m_wndRibbonBar.ForceRecalcLayout();
}
Разбор ключевых этапов алгоритма
1. Пересборка каркаса и выгрузка базовых ресурсов
Каждый раз при смене пользователя мы полностью очищаем CMFCRibbonBar с помощью RemoveAllCategories() и RemoveAllFromTabs(), после чего заново загружаем исходное полное дерево элементов из IDR_RIBBON. Это гарантирует, что мы всегда стартуем с чистого листа и не зависим от предыдущих модификаций интерфейса.
2. Потокобезопасная выборка прав
В распределенных SCADA-системах данные пользователя могут параллельно обновляться сетевыми потоками обмена с БД или контроллерами. Если обращаться к объекту пользователя непосредственно из потока UI во время обхода вкладок, можно легко получить Data Race или взаимную блокировку (Deadlock).
Чтобы избежать этого, я захватываю мьютекс std::lock_guard на минимально необходимый срок и атомарно копирую маску прав в локальную переменную uiLocalPermission. Вся дальнейшая проверка элементов ленты идет с использованием этой локальной переменной без блокировки фоновых потоков.
3. Цикл обхода с динамическим шагом
Вместо стандартного for применяется конструкция do-while. Переменная bCategoryWasRemoved отслеживает, был ли извлечен текущий элемент. Если отсечение произошло, индекс nCategoryNdx остаётся неизменным — ведь следующий элемент сам сдвинулся на освободившееся место. Мы лишь актуализируем новое общее количество элементов через GetCategoryCount().
4. Эстетическая доработка через WinAPI
Вызов LoadFromResource() автоматически восстанавливает крупную круглую кнопку меню приложения (Application Button). Если в вашей концепции интерфейса она не используется, недостаточно просто вызвать SetVisible(FALSE) — слева останется пустой отступ. Для полного схлопывания пространства необходимо передать NULL в SetApplicationButton, как показано в примере.
Архитектурные плюсы и результаты тестов
Внедрение паттерна Dynamic Ribbon Pruning в наших системах, работающих с десятками одновременных операторских сессий, дало отличные результаты:
- Улучшение UX/UI: Полное отсутствие визуального шума. Пользователи получают минималистичный лаконичный интерфейс, содержащий только те инструменты, на которые у них есть прямые допуски.
- Потокобезопасность и стабильность: Использование локального снимка прав предотвращает задержки отрисовки и полностью исключает состояние гонки при взаимодействии с потоками опроса оборудования.
- Высочайшее быстродействие: Вся операция сброса, фильтрации и пересчета геометрии занимает доли милисекунды в ОЗУ, не нагружая процессор и сеть.
Для создания чистого и надежного интерфейса в C++ вовсе не обязательно усложнять систему сторонними фреймворками. Часто достаточно грамотной работы с базовыми контейнерами WinAPI и MFC. А какими способами динамического управления правами в десктопе пользуетесь вы?
Источник: habr.com
