Как мы элегантно решили проблему динамических прав в 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, капельку математической логики и правильное распределение потоков памяти.

А как вы решаете вопросы динамических прав в десктопных приложениях? Поделитесь в комментариях!

ссылка на оригинал статьи https://habr.com/ru/articles/1081672/