Как мы элегантно решили проблему динамических прав в MFC Ribbon, или Наш ответ Чемберлену из Microsoft

Привет, Хабр! Сегодня я хочу поделиться небольшим, но очень практичным ноу-хау, к которому мы пришли при разработке крупной распределенной SCADA/HMI-системы для реального завода.
Если вы когда-нибудь работали с классическим каркасом CMFCRibbonBar в C++/MFC, то наверняка знаете официальную позицию Microsoft по поводу разграничения прав доступа. Документация MSDN и тонны учебников в один голос твердят: «Используйте стандартные обработчики ON_UPDATE_COMMAND_UI! Если у пользователя нет прав на действие — делайте кнопку серой и недоступной».
Но давайте спустимся с небес теоретического программирования на землю реального производства. Представьте интерфейс диспетчера, в котором больше 10 вкладок («Цех варки», «Цех упаковки», «Справочники», «Отчеты», «Администрирование пользователей») и сотни кнопок. Если к терминалу подходит обычный рабочий линии, перед ним открывается гигантское «кладбище мертвых серых иконок». Интерфейс перегружен, оператор путается в визуальном шуме, а полезная площадь экрана (которая в промышленных мониторах на вес золота) расходуется вхолостую.
Мы решили пойти другим путем: полностью вырезать интерфейс на основе ролевой модели пользователя прямо «на лету». То есть директор завода видит все 10 вкладок, а варщик у котла — строго одну вкладку «Котлы». Никакого лишнего шума.
В чем коварство стандартного удаления в MFC?
Казалось бы, задача тривиальная. Загружаем Ribbon из XML-ресурса, бежим циклом по категориям и удаляем ненужные через метод RemoveCategory(index).
Но здесь неопытных разработчиков поджидает классическая капкан-ловушка инвалидации индексов при удалении элементов из динамического контейнера. Представьте, что вы бежите по категориям обычным циклом:
// ТАК ДЕЛАТЬ НЕЛЬЗЯ!
for (int i = 0; i < m_wndRibbonBar.GetCategoryCount(); i++) {
if (НужноУдалить(i)) {
m_wndRibbonBar.RemoveCategory(i);
}
}Как только этот код удаляет, например, 2-ю категорию, стоявшая за ней 3-я категория автоматически сдвигается влево и становится 2-й! Ваш цикл for инкрементирует счетчик i++, переходит к индексу 3 и… благополучно проскакивает сместившуюся вкладку. В лучшем случае интерфейс удалится наполовину, в худшем — приложение упадет с ошибкой Out of Range (Access Violation).
В тяжелых коммерческих системах эту проблему часто решают созданием монструозных XML-интерпретаторов, которые по одной кнопочке собирают весь Ribbon в памяти с нуля. Мы же нашли куда более элегантное, быстрое и лаконичное решение.
Наше Ноу-Хау: Паттерн "Dynamic Ribbon Pruning"
Мы объединили динамическую перезагрузку RibbonBar из родных .rc ресурсов с хитрым «затвором» на базе цикла do-while.
Алгоритм работает по принципу «умного шага»: если категория удаляется, индекс обхода остается на месте, а верхняя граница контейнера пересчитывается заново. Если категория сохраняется — индекс делает шаг вперед.

Вот как выглядит этот отказоустойчивый конвейер:
void CMainFrame::UpdateUserRibbonPermissions()
{
// 1. Полностью сбрасываем и перерождаем Ribbon из базовых ресурсов .exe
m_wndRibbonBar.RemoveAllCategories();
m_wndRibbonBar.RemoveAllFromTabs();
m_wndRibbonBar.LoadFromResource(IDR_RIBBON);
m_wndRibbonBar.RecalcLayout();
// 2. Входим в многопоточную критическую секцию
// Атомарно копируем маску прав текущего пользователя в локальный буфер.
// Нам нельзя держать указатель на m_pCurrentUser на свободе, чтобы исключить 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. Запускаем наш конвейер точечного вырезания вкладок
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. Эстетический WinAPI-хак: Полностью убираем круглую кнопку меню (Application Button)
// Она перерождается при каждом LoadFromResource, поэтому гасим её прямо здесь.
CMFCRibbonApplicationButton* pAppBtn = m_wndRibbonBar.GetApplicationButton();
if (pAppBtn != nullptr)
{
pAppBtn->SetVisible(FALSE);
m_wndRibbonBar.SetApplicationButton(NULL, 0); // Передача пустой строки схлопывает левые отступы
}
// Финальный пересчет геометрии окон. Вкладки доступных цехов плавно сдвигаются в самый левый край.
m_wndRibbonBar.ForceRecalcLayout();
}Архитектурные итоги
Что мы получили в результате внедрения этого подхода на 30–50 параллельных пользователях в наших проектах?
Идеальный UX/UI: Интерфейс приложения мгновенно адаптируется под конкретного человека в момент логина. Рабочий видит только свои кнопки управления, технолог — свои графики варок, бухгалтер — свои отчеты. Никаких «серых кладбищ».
Абсолютная потокобезопасность: Благодаря выносу проверки прав из-под мьютекса через локальный примитив
uiLocalPermission, графический интерфейс переключается плавно, не вызывая дедлоков (Deadlock) и не мешая фоновым потокам SCADA ежесекундно опрашивать контроллеры по сети.Скорость работы: Операция пересчета всего Риббона занимает доли миллисекунды, так как выполняется в ОЗУ, не нагружая процессор и сетевые сокеты СУБД.
Иногда, чтобы сделать интерфейс промышленной программы по-настоящему удобным и надежным, не нужно внедрять тяжелые сторонние движки. Достаточно использовать базовые возможности WinAPI, капельку математической логики и правильное распределение потоков памяти.
А как вы решаете вопросы динамических прав в десктопных приложениях? Поделитесь в комментариях!
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.