Привет, Хабр! Сегодня я хочу поделиться небольшим, но очень практичным ноу-хау, к которому мы пришли при разработке крупной распределенной 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, капельку математической логики и правильное распределение потоков памяти.
А как вы решаете вопросы динамических прав в десктопных приложениях? Поделитесь в комментариях!

