Написание приложений Win32 с помощью одних классов C++ (часть 4)
ОГЛАВЛЕНИЕ
Данная статья продолажет предыдущю по теме изучения способа написания приложений Win32 с помощью одних классов C++.
• Скачать проект и демо - 97.7 Кб
Добавленные возможности
Макросы NOTIFY_xxx_HANDLER
Есть макросы карты сообщений типа ATL, применяемые для отправки сообщений WM_NOTIFY. Благодаря их улучшению они принимают дополнительный параметр "type".
Новая форма - GE_NOTIFY_xxx_TYPEDHANDLER( ... , func, type). (Примечание: в зависимости от конкретного макроса, "..." равен code, id, range идентификаторов, или их сочетание). Это позволяет указать в макросе тип, к которому он будет приведен, LPNMHDR, переносимый параметром сообщения LPARAM.
Это также позволяет объявлять обработчики сообщений, имеющие в качестве параметра непосредственную ссылку на нужную структуру (например, NMLISTVIEW&), а не LPNMHDR, приводимый в теле функции.
Типизированные уведомления
Чтобы позволить Windows взаимно оповещать о событиях, Windows предоставляет архитектуру отправки сообщений на базе сообщений (MSG) и некоторые API для отправки (SendMessage), публикации (PostMessage), извлечения (GetMessage, PeekMessage) и передачи (DispatchMessage, WINDOWPROC). В этом каркасе WINDOWPOC всегда является внутренним подклассом оконной процедуры, и отправка осуществляется с помощью карт сообщений. Зато отправка сообщений требует больше внимания.
При отправке уже определенных сообщений вызывается SendMessage API с передачей требуемых параметров. При отправке других видов сообщений нужно хотя бы определить способ их идентификации. Это можно сделать путем определения именованных констант типа #define WM_MYMESSAGE (WM_USER+xxx), но представьте исходник, состоящий их разных модулей библиотек и разных компонентов от разных разработчиков. Нужно очень строгое соглашение о нумерации (во избежание повторного использования одних и тех же идентификаторов в разных исходниках) или какая-то автоматизация этого.
NUtil::XId_base предоставляет статическую функцию (UINT NewVal()), возвращающую значение увеличенного статического счетчика при каждом вызове.
NUtil::SId<T> предоставляет функцию UINT _getval(), возвращающую значение статической переменной, которой присвоено начальное значение XId_base::NewVal() при первом вызове _getval. Также он имеет operator UINT(), возвращающий это значение. Это позволяет привязать столько UINT, сколько нужно,к стольким типам T, сколько нужно использовать с SId.
SNmHdr<N> является структурой struct с первым членом NMHDR, инициализирующим ее член "code" в (UINT)SId<N>(). Windows определяет коды WM_NOTIFY в библиотеке "общего управления" в виде (OU - xxxU) (значит: от 0xFFFFFFFF до ... около 3000 кодов). Так как XId_base сделан начинающимся с 0x2000 и возрастающим ... есть много идентификаторов для использования.
SNmHdr<N> также имеет функцию LRESULT Send(HWND hTo), делающую SendMessage(hTo, WM_NOTIFY, nmhdr.idFrom, (LPARAM)this);. Можно породить структуру struct (скажем, SMyNotification) от SNnHdr<SMyNotification>, заполнив ее члены как требуется, и вызвать Send.
Такое сообщение извлекают макросы карты сообщений (GE_NOTIFY_REGISTREDHANDLER,GE_NOTIFY_CODE_REGISTREDHANDLER, GE_NOTIFY_RANGE_CODE_REGISTREDHANDLER), принимающие параметр "type", проверяющие uMsg == WM_NOTIFY и GE_::NUtil::SId<type><TYPE>() == ((LPNMHDR)lParam)->code), вызывающие функцию в виде lResult = func((int)wParam, *(type*)lParam, bHandled).
Следовательно, можно поместить запись GE_NOTIFY_CODE_REGISTREDHANDLER(OnMyHandler, SMyNotification) в карту сообщений окна, чтобы вызвать функцию-член LRESULT OnMyHandler(int nID, SMyNotification& myntf,bool& bHandled).
Маршрутизация команд
Представьте, что есть окно-рамка с дочерним видом внутри. Меню и панели инструментов обычно принадлежат рамке и отправляют WM_COMMAND и WM_NOTIFY рамке.
Но может понадобиться обрабатывать эти команды из дочернего вида. Этого можно добиться путем привязывания карты сообщений рамки к другой карте сообщений вида (но при этом передаются все сообщения). Или можно пересылать только WM_COMMAND или WM_NOTIFY.
Для этого служат макросы GE_ROUTE_MSG_MAP_xxxx. Можно передать классу, члену, или через указатель (производится проверка указателя NULL).
Есть две разных серии макросов для команд и уведомления. Но при использовании команд автообновления (GE_COMMAND_ID_HANDLER_U) не забывайте, что само автообновление является сообщением WM_NOTIFY. Следовательно, при маршрутизации команд ... маршрутизируйте WM_NOTIFY точно так же. Или используйте макросы, вызывающие обе серии одновременно.
Заметьте, что макросы "ROUTE" вызывают some::ProcessWindowMessage. Это отличается от "отправки" сообщения: если окно имеет несколько оберток, вызов SendMessage позволяет всем оберткам получить сообщение в своей стандартной карте сообщений, тогда как "ROUTE" заставляет только переданную обертку обработать маршрутизированные сообщения или команды.
Чтобы снова отправить команду заданному окну, вместо "маршрутизации" его заданной обертке используйте GE_FORWARD_COMMANDS. Он повторно отправляет WM_COMMAND и WM_NOTIFY.
Пересылка сообщений
Другой способ приказать окну обработать сообщение, изначально отправленное другому окну (не путая его с сообщениями, предназначенными для того окна) - "пересылка путем инкапсуляции". Оригинальное сообщение снова отправляется внутри другого сообщения другому окну. Данная хитрость входит в ATL (ATL_FORWRARD_MESSAGE), но здесь ее обобщили.
GE_FORWARD_MESSAGE(hWndTo, code) отправляет WM_GE_FORWARDMSG, чьими параметрами являются идентификатор кода (как WPARAM) и NWin::XWndProcParams* (как LPARAM: переносит параметры оригинального сообщения).
Можно обработать его в целевой карте сообщений обертки HWND с помощью GE_WM_FORWARDMSG(func), где "func" является aLRESULT func (NWin::XWndProcParams& msg, DWORD nCode, bool& bHandled);.
Или можно извлечь оригинальное сообщение, рекурсивно вызывая ProcessWindowMessage после извлечения параметра из XWndProcParams.
Это можно сделать с помощью:
GE_WM_FORWARDMSG_ALT(msgMapID): извлечь оригинальное сообщение и предоставить его той же карте сообщений в другой секции ALT_MSG_MAP. Это позволяет обработать сообщение распаковщиками GE_WM_xxx.
GE_WM_FORWARDMSG_ALT_CODE(code, msgMapID): как раньше, но только те сообщения, оболочка которых была помечена "кодом" при повторной отправке.
Привязка карт сообщений
Серия GE_CHAIN_MSG_MAP устроена так, чтобы иметь одинаковое число макросов: можно привязать стандартную карту или конкретную "запасную карту" (xxx_ALT(..., msgMapID): тот же принцип, что и в ATL).
Можно привязать карту к:
• Классу – помогает производным классам ссылаться на их базовые классы.
• Члену – полезно для классов, в членах которых хранятся другие классы.
• Указателю – полезно в более сложных структурах, где имеются разные ссылки. Пустые указатели проверяются перед вызовом указанного ProcessWindowMessage.
Соображения по обработке сообщений
Объединив все техники маршрутизации сообщений, можно сделать почти все. И учитывая, что любой класс может быть производным NWin::IMessageMap (нет нужды самому быть оберткой окна), карты сообщений позволяют классам взаимодействовать без необходимости запутывать себя (конструктивно знать свой взаимный интерфейс) статически.
Если нужно больше динамики, правильным решением может быть использование "событий" (NWrp::Event<A> andNWrp::EventRcv<D>: смотрите описание в части 1).
Обратите внимание на главное различие между двумя методами: карты сообщений являются макросами, предоставляющими фрагменты кода. События являются структурами данных. Цепи карт сообщений определяются во время компиляции (при переводе макроса). Отправка события – полностью механизм времени выполнения.
Технически возможно определить взаимодействие между классами посредством карт сообщений и можно преобразовать сообщения Windows в события. Однако приведенный способ поддержки событий нельзя использовать с сообщениями как есть: связывание карт сообщений позволяет передать несколько сообщений от одной карты к другой. На настоящий момент события взаимно-однозначны: получатель должен отдельно зарегистрировать все события, которые он хочет получать.
Нахождение обертки заданного типа (RTTI)
Рассмотрите окно с несколькими прикрепленными обертками. Может понадобиться найти одну заданного типа (или унаследованную от заданного типа). Так как обертки HWND связаны, это легко делается с помощью шаблонной функции и dynamic_cast, путем прохода по цепи, пока приведение не будет непустым. Более общий boolWrpChainBase::DynamicFind<A>(A*&, TT::IH) делает именно это. Но он реализован в WrpChainBase, поэтому работает для каждой связанной обертки.
Так как используется dynamic_cast, RTTI (информация о типе времени исполнения) должна быть активирована.
Модальное диалоговое окно
Модальные диалоговые окна асимметричны в Windows API: функция DialoBox Windows требует DLGPROC, но этот "proc" – не настоящий WNDPROC. Настоящий WNDPROC закрыт в системе. Он вызывает процедуру и – при возврате лжи - вызывает DefDlgProc. Все это происходит в модальном цикле, внутреннем для DialogBox API.
Чтобы принудительно провести это в уже существующей обертке, был реализован EWnd::CreateModalDialog, который, действуя как CreateWnd, устанавливает ловушку и передает внутреннюю скрытую функцию в качестве DLGPROC. Ловушка прикрепляет создание нового окна (в данном случае диалога) к запрашивающей обертке и автоматически отцепляет себя.
Процедура ловушки была пересмотрена, чтобы ловушка была активна в течение наиболее короткого срока (во избежание рекурсии в функции ловушки, где создается несколько вложенных окон – представьте родителя, создающего своих потомков в ходе процесса создания самого себя). Представленный скрытый DLGPROC всегда возвращает ложь, кроме WM_COMMAND с кодом от 1 до 7 включительно (IDOK, IDCANCEL, ..., IDCLOSE: чтобы была стандартная обработка, возвращающая значение, иначе программа застрянет в информационном окне).
Пересмотренная процедура ловушки также исправляет ошибку: в предыдущей версии, если еще создающееся окно обернуто и перехватывает WM_CREATE для создания дополнительных окон (представьте главное окно с потомками), создается несколько экземпляров вложенных ловушек, но при возврате отцепляется только последняя. Хотя это не влияет на функционал (процедуры ловушки только прикрепляют первую обертку), в ряде случаев это влияет на производительность. Теперь такого не случится: отцепление ловушки производится в самой процедуре ловушки, до отправки любого сообщения любой обертки. Рекурсия невозможна.
Как заставить все работать
Нелегко продемонстрировать все вышеназванное в простом приложении, не делающем почти ничего, но позволяющем проверить почти все. С этой целью проект W3 использует Nlib и NGUI. Была создана рамка, обернута EMenuUpdator и EMauImagerIDE, и было добавлено дочернее окно при WM_CREATE.
Команды направлялись потомку, и обрабатывался ID_FILE_EXIT в главном окне и ID_HELP_ABOUT в потомке (это необычно, но нормально для демонстрации маршрутизации команд). Чтобы реагировать на ID_HELP_ABOUT, был создан экземпляр модального DialogBox.
Использование Commoncontrols
Надо подключить библиотеку импорта ComCtrl32.lib и вызвать InitCommonControlsEx. Вся эта неприятная, но всегда нужная начинка была помещена в класс (NUtil::SInitCommonControlsEx). Создается экземпляр временного объекта, вызывающий конструктор с передачей требуемого значения (по умолчанию принято ICC_WIN95_CLASSES) – и всё тут. Библиотека подключается посредством #pragma comment(lib ...) в заголовке NGUI/CommCtrl.h.
Примечание: Эта инициализация не сделана неявной (т.е. через созданный статический экземпляр объекта), потому что не всем приложениям нужны точно такие же общие управляющие элементы.
Диалоговое окно «О» обернуто CAboutBox, и его экземпляр создается при создании таймера, осуществляющего обратный отсчет с помощью индикатора выполнения. При достижении нуля диалоговое окно автоматически закрывается. (Нажатие OK ускоряет его закрытие). Это показывает корректную работу карты сообщений с DLGPROC.
Обработка более сложных компоновок
Цель – позволить рамке манипулировать клиентским окном и набором состыкованных панелей по алгоритму, схожему с IDE. DockMan.h и DockMan.cpp содержат нужную начинку. В частности, два интерфейса определяют взаимодействия между стыкуемыми объектами и рамками.
Диспетчер стыковки
ILayoutManager определяет прототип для функции RedoLayout, тогда как IAutoLayout создает прототипы для функций DoLayout и GetSideAlignment. Как правило, реализация ILayoutManager должна содержать или ссылаться на несколько элементов IAutolayout, чтобы размещать их при перемещении или изменении размера. RedoLayout вызывается с передачей запрашивающего HWND (в случае если его пропустит алгоритм компоновки: обычно этот параметр равен NULL). Он, в свою очередь, должен вызвать DoLayout вложенного IAutoLayout, передав прямоугольник. Вложенный IAutoLayout должен перестроиться на базе конца того прямоугольника, изменив его до части прямоугольника, остающейся открытой.
ILayoutManager определяет порядок компоновки. IAutolayout – размещаемые компоненты. Представленная реализация – во избежание запутывания классов – делит реализацию ILauoutManager на две части.
Внутренний класс (XLayoutProvider) реализует в куче ILayoutManager и получается путем вызова статической функции ILayoutManager::Get(HWND): она извлекает (через DynamicFind) ILayoutManager, связанный с переданным HWND или (если ничего не прикреплено) создает XLayoutProvider и прикрепляет его, сделав его автоматически удаляемым наблюдателем.
XLayoutProvider обрабатывает сообщение WM_SIZE, получая прямоугольник клиента и передавая его в типизированное уведомительное сообщение, чьей структурой данных является ILayoutManager::Autonotify.
Здесь можно прикрепить произвольное число оберток, реализующих описатель этого сообщения, который, получив прямоугольник, переносимый структурой данных, и некоторые принадлежащие данные, решает, что делать с вложенными или указанными элементами IAutoLayout.
В частности, EDockBarManager реализует ILayoutManager::Autonotify, вмещая один EClientWnd и четыре (один на каждую сторону) EDockBar. Каждый EDockBar может принять любое число HWND для встраивания, и при осуществлении этого прикрепляет EDockBar::XElement к переданному HWND. Указанный второй внутренний класс предназначен для работы с EDockBar и является наблюдателем, автоматически удаляющим EWnd. Он существует в куче и уничтожается при уничтожении окна, к которому он прикреплен, и хранит состояние стыковки (размещение), управляя стыковкой и отстыковкой обернутого HWND. Возможность стыковки не встроена в переданное окно, а подключается при прикреплении окна.
Не нужны особенные HWND: всего лишь обычные всплывающие окна, созданные принадлежащими родителю EDockBar (как правило, EDockBarManager). Они становятся потомками панелей при стыковке и снова становятся всплывающими при свободном перемещении.
Конечно, те окна могут быть управляющими элементами, или панелями инструментов, или другими более сложными окнами (это рассмотрено в части 4 статьи).
Команды «О», обе - EDockBar и EdockBar::XElem пересылают принятые команды родителю (или владельцу), тогда как EDockBarManager пересылает их клиентскому окну. Это создает маршрутизацию команд в духе MFC.
Пример приложения
В примере приложения создается CMainWnd, создающий, в свою очередь, 8 по-разному расположенных панелей (смотрите CMainWnd::OnCreate). Некоторые из них перемещаемы, у других можно менять размер.
Некоторые панели снабжены обычной кнопкой закрытия. При закрытии панели она уничтожится (и так как она обернута внутренней автоматически удаляющей наблюдающей оберткой, обертка тоже уничтожается). Отсутствует интерфейс для управления созданием или скрытием панелей, так как это не вошло в функционал классов.
Обратите внимание на NUtil::STrace::_Filter() = 2; в WinMain: это не дает классу STrace отображать в отладочном выводе много сообщений, связанных с отправкой сообщений и с операциями обертывания и распаковки описателя GDI(интерфейс графического устройства).