Написание приложений Win32 с помощью одних классов C++ (часть 2)

ОГЛАВЛЕНИЕ

Данная статья продолжает первую часть. Здесь к NLib добавляются другие возможности.

•    Скачать исходники - 52.8 Кб
•    Скачать исполняемый файл - 47.1 Кб

Подход

Соблюдается модульный подход: недопущение превращения библиотеки в каркас и использование компонентной модели (не обязательно COM или .NET: применяются собственные возможности языка), позволяющей использовать тот компонент при необходимости, без необходимости включать его или использовать при ненадобности.

Как в предыдущей статье, не делается ничего нового, что не было бы сделано раньше: есть много статей о том, как сделать то же самое с помощью MFC или WTL. Тут лишь испытывается другая техника программирования. Не изобретается ли колесо заново? Да, так и есть!

Но если MFC хорош для одного, а WTL – для другого, то нижеописанное сгодится для третьего!

Отправная точка

Это проект W2 из предыдущей статьи: малозаметное окно "Hello world", но уже с картами сообщений и доставкой команд. Был создан пустой проект W3 Win32, в него помещена копия источников W2, и был сделан ряд настроек (добавление "NLib" к решению, активация RTTI, установка "использовать предварительно скомпилированные заголовки" и "создать предварительно скомпилированный заголовок" для stdafx.cpp, установка зависимости W3 от NLib).

Некоторое изменение NLIB

В ходе разработки второй статьи были изменены определенные части NLIB для устранения ошибок, улучшения и удобства.

Распаковщики сообщений

Макрос карты распаковщика сообщений был изменен, чтобы хранить функции, имеющие bool& bHandled в качестве последнего параметра. Макрос устанавливает это значение в «Истину» перед вызовом переданной функции, но данная схема позволяет установить его обратно в «Ложь». Благодаря установке в «Ложь» связанное действие не прерывается вызовом обработчика и может продолжиться с другим EWnd, впоследствии прикрепленным к тому же самому HWND. Во избежание путаницы этот макрос был переименован в ONMSG_xxx, а не в GETMSG_xxx.

Это очень удобно для отслеживания сообщения без вмешательства в его отправку.

Переделка оберток

При расширении оберток для создания дополнительных возможностей было обнаружено несколько изъянов, которые при отсутствии переделки налагают ряд ограничений на использование. И так как издержки переделки малы, оказалось удобным доработать имеющийся код:

В частности:
•    Были введены новые типы: CRH и COH: обычно они работают как CH и RH, но возвращаются функциями "const". Наоборот, старые типы возвращаются непостоянными функциями.
•    Были введены новые функции в классы traits_wrp_xxx: CAccess и CDereference: они возвращают COH и CRH из IH<CODE>.
•    В WrpBase оператор operator TT::OH() больше не является const и вызывает Access. Вместо него был введен оператор operator TT::COH()const, вызывающий CAccess.
•    В Wrp операторы operator->() и operator*() были переписаны в постоянной и непостоянной версиях и вызывают соответствующие функции типажей.

Хотя представленная реализация типажей делает то же самое, другой интерфейс позволяет при необходимости различать использование обертки для доступа к данным для чтения и использование для изменения хранимых данных.

Строки

Многие Windows API определены через TCHAR. Хотя сравнительно легко сделать класс строк, хранящий TCHAR, путем определения std::basic_string<TCHAR>, реализация std::strings, представленная П.Дж. Плогером (по сути, специализация векторов) не очень эффективна: строки считаются "значениями" и всегда копируются.

Напротив, CString из MFC и ATL использует разделяемые векторы, клонирующиеся только при изменении значения строки. Для реализации похожей схемы было испытано переопределение "WrpBase и traits", позволяющее хранить разделяемые данные между обертками и осуществлять клонирование при попытке непостоянного доступа.

Хотя это работало, были издержки из-за внутренних вызовов между Wrp, WrpBase и traits. Поэтому была выбрана специальная реализация, более эффективная для данной конкретной цели (притом менее гибкая).

Был введен новый тип обертки (NWrp::SShare<T, nullval>) для автоматической генерации, автоматического клонирования и разделения буферов T.

Затем NWin::SString порождается с помощью NWrp::SShare<TCHAR, _T('\0')>, добавляя Left, Mid, Right, конструкторы и операторы.

Заметьте, что SString сам не является полноценным объектно-ориентированным классом (ни один из определенных тут классов не является таковым): это всего лишь диспетчер памяти. Строками манипулируют с помощью Windows API или строковой функции, взятой из tchar.h.

SString неявно преобразует в и из LPCTSTR. Для изменения буфера используется знак “-“ вместо GetBuffer(intwantedsize), при этом передавая требуемый размер или -1, чтобы фактический размер не изменился. GetBuffer производит копирование буфера, если фактический буфер разделяемый, затем соответственно изменяет его размер и возвращает его адрес.


Работа с меню, командами и идентификаторами

Для добавления иконки или меню к окну не нужна обертка: идентификатор иконки можно задать во время регистрации класса окна, а меню можно загрузить во время создания окна. Пока HMENU остается связанным с окном, Windows обеспечивает его уничтожение.

Ниже приведена измененная winmain.

int APIENTRY _tWinMain(
    HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPTSTR lpCmdLine, int nCmdShow)
{
    NWin::SMsgLoop loop(true);  //создать экземпляр цикла
    NUtil::SResID resWapp(IDR_WApp);

    LPCTSTR wcname = _T("W1MainWnd");
    //создать экземпляр класса окна
    NWin::SWndClassExReg wc(true, hInstance, wcname, resWapp);    
    CMainWnd wnd;
    wnd.CreateWnd(hInstance, wcname, NULL, WS_VISIBLE|WS_OVERLAPPEDWINDOW,
        NWin::SRect(NWin::SPoint(CW_USEDEFAULT,CW_USEDEFAULT),
        NWin::SSize(600,400)), NULL,
        LoadMenu(hInstance, resWapp), NULL);

    loop.Loop();

    return 0;
}

Сложней управлять обновлениями пользовательского интерфейса команд (например: отключить неиспользуемые команды).

Два распространенных подхода следующие:
•    подход WTL: с командой связано состояние, управляемое приложением. Элементы GUI умеют представлять это состояние при отображении. Меню обычно делают это при WM_INITMENUPOPUP, панели инструментов – при обработке во время простоя.
•    подход MFC: элемент GUI (меню при WM_INITENUPOPUP, панели инструментов при обработке во время простоя) запрашивает у приложения свое состояние. Приложение отвечает путем установки своего состояния интерфейса.

Два подхода "двойственны": нет лучшего, но один может быть лучше другого в зависимости от ситуаций: если состояние команды зависит от множества факторов, распределенных по множеству мест, то подход MFC проще для программирования, тогда как если состояние является строго определенным условием, то проще подход WTL.

Подход MFC более общий, поэтому был реализован аналогичный подход.

Состояние команды

Нужен объект, абстрагирующий поведение элемента GUI (пункт меню, кнопка панели инструментов или что угодно), могущий принимать ряд состояний: активно/отключено, недоступно для выбора, выбрано, выбран переключатель, промежуточное, есть текст, есть изображение.

Это делается посредством интерфейса: ICmdState, предоставляющего набор абстрактных функций.
Указанные функции могут быть реализованы в разных классах, управляющих конкретным компонентом пользовательского интерфейса. Для стандартного меню Windows этим занимается SCmdMnu.
Для управления обновлением команды использовалось два закрытых сообщения GE_QUERYCOMMANDHANLER и GE_UPDATECOMMANDUI.

В частности, для обновления меню:
•    При WM_INITMENUPOPUP отправляется сообщение GE_QUERYCOMMANDHANLER. LPARAM является SCmdMnu*, а WPARAM является идентификатором команды.
•    GE_QUERYCOMMANDHANLER обрабатывается макросом COMMAND_xxxx_U (должен использоваться вместо COMMAND_xxxx, того же, который обрабатывает WM_COMMAND) путем вызова SetHandled.

Когда GUI требуется, (WM_INITMEMUPOPUP, обработчик простоя, или что угодно), он должен отправить сообщение GE_UPDATECOMMANDUI (тот же параметр, что и предыдущий). Приложение обрабатывает это сообщение путем вызова функций-членов ICmdState. Их реализация управляет установкой состояния GUI.
Для меню этим управляет производный класс EWnd, прикрепленный, например, к главному окну. Этот класс - EMenuUpdator: он обрабатывает WM_INITMENUPOPUP, как показано.

С целью применения он создается и прикрепляется к главному окну.

int APIENTRY _tWinMain(
    HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPTSTR lpCmdLine, int nCmdShow)
{
    NWin::SMsgLoop loop(true);  //создать экземпляр цикла
    NUtil::SResID resWapp(IDR_WApp);

    LPCTSTR wcname = _T("W1MainWnd");
    //создать экземпляр класса окна
    NWin::SWndClassExReg wc(true, hInstance, wcname, resWapp);    
    CMainWnd wnd;
    wnd.CreateWnd(hInstance, wcname, NULL, WS_VISIBLE|WS_OVERLAPPEDWINDOW,
        NWin::SRect(NWin::SPoint(CW_USEDEFAULT,CW_USEDEFAULT),
        NWin::SSize(600,400)), NULL,
        LoadMenu(hInstance, resWapp), NULL);
    NWin::EMenuUpdator mnuupdt;
    mnuupdt.Attach(wnd);

    loop.Loop();

    return 0;
}

Вот и все.

Для обработки GE_UPDATECOMMANDUI были предоставлены макросы ONMSG_GE_UPDATECOMMANDUI,ONMSG_GE_UPDATECOMMANDUI_RANGE и ONMSG_GE_UPDATECOMMANDUI_RANGE_CODE (они находятся в CmdUpdt_macros.h), применяемые в картах сообщений.

Все они вызывают следующий прототип функции:

LRESULT function(
  UINT nID,             //идентификатор команды
  WORD nCode,           //код уведомления (если от управляющего элемента)
  ICmdState* pCmdState, //ICmdState, чьи функции меняют состояние GUI
  bool& bHandled      //установлен в «Истину» перед вызовом функции.
  );      //возвращаемое значение: всегда должно быть 0.

Ложные ошибки исправлены

Было исправлено несколько ложных ошибок из предыдущей статьи. Они названы ложными, поскольку являются не "ошибками" (ошибками программирования), а "ошибками проектирования", показывая отсутствие нужного функционала.


Типы обертки

Функция WrpBase::Value изначально возвращает OH, но на самом деле она должна возвращать тип функции Access, унаследованный от типажей и делающий функцию Value бесполезной копией. По сути, Value должна возвращать значение так, как оно хранится, только для чтения, без преобразования. Следовательно, ее возвращаемый тип был изменен на const SH&.

Это не влияет на нормальные обертки (SH является H, а OH является const HANDLE&, так что это одно и то же) или указатели (SH является H*, а OH является H*, что значит - "копировать при возврате", следовательно, const SH& становится const H*&, точно так же присваиваемым H*), но важно в более сложных случаях, когда возвращаемые типы не идентичны значениям, хранящимся в обертке.

Ниже приведен реальный случай.

Чтение пользовательских ресурсов

Для чтения пользовательского ресурса windows API требует следующих шагов:
1.    Вызвать FindResource, передав HMODLULE и ID, чтобы получить HRSRC.
2.    Вызвать LoadResource, передав HRSRC, чтобы получить ложный HGLOBAL.
3.    Вызвать LockResource, чтобы получить LPVOID для надлежащего приведения типа.
4.    Теперь стоит использовать данные
5.    Затем вызвать UnlockResource, передав HGLOBAL, и затем ...
6.    Снова вызвать FreeResource с HGLOBAL.

В то время как шаг 1 не требует выделения памяти, шаги 5 и 6 обратны шагам 3 и 2 соответственно.
Следовательно, удобная обертка прикрепляется к HRSRC, загружая данные, делает их доступными путем разыменования и освобождает данные при отцеплении. Если с одним и тем же HRSRC связано несколько оберток, загрузка должна производиться первой, а освобождение - последней. Данные должны быть шаблонным параметром.

Обертка ресурса для ресурса панели инструментов может быть (кроме пространства имен) Wrp<SResourceData<XToolBarData> >. Если wrpTbrRes является переменной указанного типа, operator-> вернет XToolbarData*.

Затем SResourcedata порождается от WrpBase, замыкая наследование (передавая себя в качестве параметра "W") и имея держатель конкретных типажей и счетчиков ссылок.

Поскольку для загрузки ресурса нужен не только HRSRC, но и HMODULE, приходится сохранять HMODULE как "начальный параметр" в SResourceData, прописывая функцию инициализации (void SetHModule(HMODULE hMod)). Надо хранить HGLOBAL и указатель Data. Так как эти значения должны сохраняться, пока у заданного ресурса не появятся обертки, удобней хранить данные значения в разделяемом держателе счетчиков ссылок.

Итак:
•    был определен XRefCountRes<Data>, порожденный от XRefCount<SResourceData<Data> >, хранящий еще два дополнительных члена: HGLOBAL и Data*.
•    были определены типы для получения const HRSRC& (являющегося IH), хранения HRSRC(являющегося SH), доступа к Data*(OH) и разыменования Data& (как RH)
•    изменены Access и Dereference для доступа к _pRC (держатель счетчика ссылок) и возврата Data* или Data& соответственно. Эти функции определены как статические в типажах и унаследованы в WrpBase. Нельзя переделать типажи, потому что на этом уровне не определен ни один указатель на объект счетчика ссылок. Они переопределены SResourceData не как статические, а как постоянные функции-члены. Поскольку они всегда вызываются через pThis() (преобразующего в W*, если W - обертка: SResourceData, в данном случае), будут вызваны переопределения.
•    OnFirstAttach и OnLastDetach переопределены для выполнения тяжелой работы: Load и Lock – сначала, Unlock и Free – позже.

Например, следующий код считывает ресурс RT_TOOLBAR в XToolBarData:

    // Примечание: допустим, уже имеется
    //  HINSTANCE hInst; LPCTSTR lpszResourceName;
    //
    struct XToolBarData
    {
        WORD wVersion;
        WORD wWidth;
        WORD wHeight;
        WORD wItemCount;
        //WORD aItems[wItemCount]

        WORD* items()
            { return (WORD*)(this+1); }
    };
   
    typedef NWrp::Wrp<NWrp::SResourceData<XToolBarData> > TTbrData;
    TTbrData TbrData;
    TbrData.SetHModule((HMODULE)hInst);
    TbrData.Attach(FindResource(hInst, lpszResourceName, RT_TOOLBAR));

    XToolBarData* pData = &*TbrData;
    ASSERT(pData && pData->wVersion == 1);
    // использовать pData при необходимости.
    // ресурсы будут освобождены при уходе из области видимости

Добавление изображений к меню

Команды привязываются к изображениям с помощью редактора панели инструментов Visual Studio путем создания битового массива и ресурса "панели инструментов".

При инициализации меню можно преобразовать все пункты в "прорисовку владельцем" (задача, выполняемая с помощью производного EWnd, вызывающего EMenuImager), но ... где хранить данные?

Для команд можно придумать глобальную карту, индексируемую идентификаторами команд: идентификаторы команд глобальные. Значением карты будет SCmdDraw::XCmdChara, хранящий идентификатор ресурса, из которого взято изображение, и относительный индекс изображения.

Затем можно преобразовать в прорисовку владельцем все пункты меню при WM_INITMENUPOPUP и преобразовать обратно в нормальные при WM_UNINITMENUPOPUP. Возможно, это не так эффективно, но безопасно в плане управления памятью (не хранятся все строки без идентификаторов из меню, например, в подменю).

Важно, чтобы те сообщения обрабатывались после возможного изменения состояния меню, вызванного другими обертками. Чтобы гарантировать это, обработчик сообщения помечает сообщения как обработанные (без последующей обработки), но Default() вызывается в начале обработчика.

Поэтому :
•    Если вы оборачиваете сначала (например) посредством EMenuUpdator и затем с помощью EMenuImager, подкласс оконной процедуры вызовет EMenuImager, вызывающий Default, заставляющий обработать EMenuUpdator первым.
•    Если вы оборачиваете сначала посредством EMenuImager и затем с помощью EMenuUpdator, подкласс оконной процедуры вызовет EMenuUpdator первым и (потому что она оставила сообщения необработанными) затем EMenuImager.

В обоих случаях последовательность сначала обновляет пункты, затем приспосабливает их к прорисовке.

Прорисовка пунктов

Поскольку есть разные способы прорисовки пунктов меню (в плане прорисовки эффектов и состояния: например, Win2K или WinXP или VS-IDE), можно придумать разные стратегии реализации обработки WM_DRAWITEM.

A.    Сделать шаблон EMenuImager и предоставить параметрический класс типажей, предоставляющий функцию OnDrawItem: его недостаток в статичности кода: пользователь не может переключаться между разными типажами: приходится обеспечить создание разных экземпляров шаблонов, что означает создание множественных экземпляров статической или глобальной структуры данных.
B.    Объявить OnDrawItem как абстрактный и реализовать его в разных производных классах.
C.    Не реагировать на WM_DRAWITEM и обработать его в отдельном производном EWnd, оставив осуществление полиморфизма на ядро Windows.
D.    Породить EMenuManager, дать ему новую карту сообщений, связанную с базовой (стиль WTL).

Тогда как A не подходит для таких реализаций, B налагает ряд ограничений на использование функций.
C означает - объявить EMenuImager_xxx, чтобы прикрепить к тому же самому HWND, обернутому EMenuImager, где xxx – составляющая, которую надо предоставить. Вероятно, это самое гибкое решение, но склонное к генерации сильно запутанных классов: определяется Exxx, кажущийся оберткой HWND, но не работающий сам по себе и зависящий от данных, генерируемых (и определяемых) другим независимым классом.

D наиболее традиционна, но кажется оптимальной в данном случае. Представленная реализация - NGDI::EMenuImagerIDE.

Отображение быстрых клавиш

В программах с применением быстрых клавиш обычно отображают быстрые клавиши для команд справа от текста меню.

Но писать названия быстрых клавиш в ресурсе MENU не рекомендуется: перевод сообщений основан на HACCEL, загружаемом из "таблицы быстрых клавиш", самой не являющейся меню. Быстрые клавиши меняются путем редактирования таблицы быстрых клавиш, но надо изменить и текст меню. И если одна и та же команда присутствует во множестве разных меню ... это непросто отследить.

Другой выход – не помещать текст в ресурс меню, а обеспечить возможность изменения текста меню перед его отображением. Это легко сделать в EMenuUpdator. Известив этот класс о таблице быстрых клавиш, и дав ему текстовые описания для всех клавиш, можно непосредственно перед отображением команды просмотреть заданную таблицу быстрых клавиш, найти быструю клавишу для заданной команды, извлечь ее текст и затем добавить его справа от меню.

Связывание таблицы быстрых клавиш с окном позволяет прицепиться к синглтону SMsgFiltrerEvent при активации (и отцепиться при дезактивации) и вызвать TranslateAccelerator API. Так как это событие генерируется циклом обработки сообщений сразу перед отправкой сообщения, это заставит работать быстрые клавиши, связанные с окном.

Все вышесказанное реализовано в EMenuUpdator. В частности, текст, используемый для описания быстрых клавиш, создается с помощью ресурса RC_DATA, являющегося последовательностью пар, состоящих из WORD с последующей C-строкой. WORD является константой VK_xxx, обозначающей функциональную клавишу клавиатуры, а C-строка является ее описанием. Последние три записи ресурса должны иметь идентификаторы, равные 0, и описывать "Shift+", "Ctrl+" и "Alt+" (или любое описание, имеющееся у них в данной локализации) соответственно.

В EMenuUpdator структура XKeyNames помогает читать этот ресурс: она используется обернутой в SResourceData<>, прикрепленный к HRSRC, заданный FindResource. В структуре XKeyTexts хранятся описания, индексируемые идентификатором VK_xxx. (Смотрите реализацию его функции Load.)
Чтобы уложиться в функционал IDE(интегрированная среда разработки), для проекта NLIB был предоставлен файл ресурсов NLib.rc и файл Accels.rc2, а также файл NLib_res.h, содержащий определения именованных констант.

NLib.rc сконфигурирован так, чтобы включить NLib_res.h в качестве своего собственного заголовка и Accels.rc2 в качестве включенного файла ресурсов. Поскольку Nlib - библиотека, NLib.rc сам не должен компилироваться. Наоборот, он должен включиться в файл ресурсов проекта "exe": в данном случае проект "W3" имеет файл ресурсов (W3.rc), включающий традиционный resouce.h и Nlib_res.h, а также включающий в свое тело файл NLib.rc.


Тестовый проект

Было создано несколько проектов для тестирования примеров применения.

Исполняемый файл – чистый проект Win32, использующий NLib как статическую библиотеку.
Файл stdafx.h включает NLib/StdAfx.h, и – для неотладочных версий – определяет символ GE_FORCEINLINE: это приводит к включению файлов NLib CPP в конец их соответствующих файлов .h с объявлением всех функций как "inline".

Поскольку решение содержит два проекта, и поскольку W3 зависит от NLib, приходится подключать Nlib.lib, даже если весь код встроенный. Это достигается путем определения в исходнике emptylib.cpp локального скрытого символа (бессмысленного, чтобы что-то компилировалось, или никакой библиотеки не генерируется), и путем видоизменения конфигурации отладки (где все файлы, кроме emptylib.cpp, включены в процесс генерации) и выпускаемой версии (где правило генерации изменено на обратное). emptylib.cpp также сконфигурирован на неиспользование предварительно скомпилированных заголовков.

Функция WinMain очень простая:

int APIENTRY _tWinMain(
    HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPTSTR lpCmdLine,
    int nCmdShow)
{
    NUtil::STrace::_Filter() = 2;
   
    NUtil::SWinMain winmainparams(hInstance);
    NWin::SMsgLoop loop(true);  //создать экземпляр цикла

    CMainWnd wnd;
    wnd.Create(hInstance);

    loop.Loop();

    return 0;
}

Создается CMainWnd, и выполняется цикл обработки сообщений. CMainWnd объявляется так:

class CMainWnd: public NWin::EWnd
{
protected:
    NWin::EMenuUpdator _mnuupdt;
    NWin::EMenuImagerIDE _mnuimg;
public:
    CMainWnd() {}
    bool Create(HINSTANCE hInstance);
protected:
    LRESULT OnFileExit(WORD wNotifyCode,
           WORD wID, HWND hWndCtl, bool& bHandled);
    LRESULT OnExecSomeCommand(WORD wNotifyCode,
           WORD wID, HWND hWndCtl, bool& bHandled);
    LRESULT OnCreate(LPCREATESTRUCT lpcs, bool& bHandled);
    LRESULT CMainWnd::OnPaint(bool& bHandled);

    BEGIN_MSG_MAP(CMainWnd)
        ONMSG_WM_CREATE(OnCreate)
        ONMSG_WM_PAINT(OnPaint)
        COMMAND_ID_HANDLER_U(ID_FILE_EXIT, OnFileExit)
        COMMAND_ID_HANDLER_U(ID_HELP_ABOUT, OnExecSomeCommand)
        COMMAND_ID_HANDLER_U(ID_FILE_NEW, OnExecSomeCommand)
        COMMAND_ID_HANDLER_U(ID_FILE_OPEN, OnExecSomeCommand)
        COMMAND_ID_HANDLER_U(ID_FILE_SAVE, OnExecSomeCommand)
    END_MSG_MAP()
};

Обратите внимание на макрос COMMAND_ID_HANDLER_U в карте сообщений, и заметьте, что некоторые команды были реализованы посредством той же функции-заполнителя OnExecSomeCommand. Функция Create является сокращением для обработки регистрации WNDCLASSEX и созданием настоящего окна через EWnd::CreateWnd(...).

Функция OnCreate (обработчик для WM_CREATE) инициализирует обертки других членов и прикрепляет их к тому же самому CWnd. Те обертки не обязаны быть членами, но заключение их в один и тот же класс упорядочивает структуру данных.

Ниже приведено два тела:

bool CMainWnd::Create(HINSTANCE hInstance)
{
    LPCTSTR wcname = _T("W3MainWnd");
    NUtil::SResID resWapp(IDR_WApp);
    static NWin::SWndClassExReg clsrg(true, hInstance, wcname, resWapp);
    //instantiate a window class
    if(!CreateWnd(hInstance, wcname, _T("W3 test"),
        WS_VISIBLE|WS_OVERLAPPEDWINDOW|WS_CLIPCHILDREN,
        NWin::SRect(NWin::SPoint(CW_USEDEFAULT,CW_USEDEFAULT),
        NWin::SSize(600,400)), NULL,
        LoadMenu(hInstance, resWapp), NULL))
        return false;
   
    return true;
}

LRESULT CMainWnd::OnCreate(LPCREATESTRUCT lpcs, bool& bHandled)
{
    Default();
    NUtil::SResID resWapp(IDR_WApp);
   
    _mnuupdt.LoadAccelerators(HInstance(), resWapp);
    _mnuupdt.Attach(*this);
    _mnuimg.GetCmdImgs().LoadCmdImages(HInstance(), resWapp);
    _mnuimg.Attach(*this);
   
    return 0;
}

В момент прикрепления дополнительной обертки окно уже существует. В конце OnCreate на окно ссылаются три обертки. Отцепление и связывание осуществляется в карте сообщений: это автоматически делается процессами прикрепления EWnd::Attach и отцепления Detach.

Команды меню Файл/Новый, Файл/Открыть, Файл/Сохранить и Помощь/О были реализованы с помощью пустой функции.

Команды Меню/Автоматически обновить и Меню/Изображения являются переключателями. Они выбраны, если прикреплены _mnuupdt и _mnuimg соответственно. Команды реализованы путем поочередного прикрепления/отцепления внутренних оберток.

При отцеплении _mnuupdt меню перестают обновлять свое собственное состояние. Если отцепление производится сразу после запуска приложения, без отображения других меню, меню появятся полностью активными и без описаний быстрых клавиш. Если отцепление происходит позже –  меню сохраняют последнее установленное состояние.

Подробность отладки

В самом начале WinMain есть строка "NUtil::STrace::_Filter() = 2;". Она сокращает вывод отладочной информации, генерируемой STrace и STRACE.

В коде извлечение всех сообщений установлено на "уровень 0", а отправка сообщений / выделение памяти установлены на "уровень 1". Если нужен более подробный вывод отладочной информации, установите значение в 1 или в 0. Неудобство в том, что управление меню замедляется из-за операции обертывания / распаковки объектов GDI и из-за всех сообщений, генерируемых операцией.

Заключение

Возможно, заново изобретается колесо, но, как ни странно, улучшать описанные возможности было приятней, чем писать код с помощью WTL.

Вряд ли это станет новой библиотекой для мощных приложений, но поможет кому-то научиться новому.