ESPNGarcia backs up talk, TKO's Benn in second round to retain WBC titlePunchCharismatic bishops’ president defends endorsement of Tinubu for 2027InquirerBoat operator nabbed for allegedly hiring unlicensed fishers in MindoroThe Jerusalem PostIsrael’s PISA collapse exposes a failing education system threatening its future - opinionCNN TürkKayseri Kültür Yolu Festivali'nde 34 lezzet noktası ziyaret edildiDaily MaverickTHE GATHERING 2026: Zille draws coalition red lines as DA targets Joburg majorityוואלהסוכנות הסחר הימי של בריטניה: הרשויות מפנות צוותים מספינה שהותקפה במצר הורמוזUOLHomem viaja a Apucarana para comprar barco anunciado no Facebook e perde R$ 4 mil em golpe20 MinutenSchwer verletzter Mann (26) entdeckt – was ist passiert?ABC NewsSearch underway for about 130 people after Indonesian passenger ship overturnsCollider7 Mystery Shows Without a Single FlawEuronewsIran claims one killed in US attack on Iranian commercial ship in Hormuz
The Daily Newsstand · Free, Always
Sunday, September 13, 2026

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

Translate

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

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

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.