Сообщений 13 Оценка 632 [+1/-0] Оценить |
"If you only knew the power of the Dark Side..."
Lord Vader
WTL (Windows Template Library) - это ещё одна библиотека классов от Микрософт, которую вы можете использовать при создании ваших программ. "Ещё одна", потому что на сегодняшний день существуют различные библиотеки классов (MFC и ATL фирмы Microsoft, OWL и VCL фирмы Borland и т. д.). WTL изначально создавалась как пример расширения ATL, и предназначалась для упрощения процесса создания графического интерфейса в программах, использующих эту библиотеку. Как и ATL, WTL базируется на шаблонах языка C++ и позволяет создавать очень быстрые компактные программы.
WTL стала чем-то вроде "побочного продукта" деятельности Микрософт, и до сих пор для неё не нашлось места в официальных планах компании. Поддержка этого продукта и официальная документация на него отсутствуют. Тем не менее, WTL продолжает развиваться, и сейчас разработчикам доступна уже версия 3.1 этой библиотеки. Всё больше программистов использует её в своих приложениях. Давайте рассмотрим, как WTL соотносится с другими технологиями фирмы Микрософт и какие преимущества может дать её использование.
WTL является "надстройкой" над оконной подсистемой Win32 API и существенно упрощает работу с функциями этой подсистемы, а также облегчает задачу написания повторно используемого кода. В WTL поддерживает работу с окнами и диалогами, окнами-рамками, полным набором стандартных элементов управления, стандартными диалогами, GDI и другими неотъемлемыми элементами пользовательского интерфейса. Изучить WTL можно сравнительно быстро, после чего она позволит вам существенно ускорить разработку приложений, которые раньше писались на "чистом" API.
ATL и WTL очень тесно интегрированы между собой. WTL использует базовый механизм поддержки работы с окнами, предоставляемый ATL, для реализации более "продвинутых" возможностей. Фактически, многие файлы WTL (такие как atlbase.h и atlwin.h) даже не входят в комплект поставки, так как распространяются вместе с Visual C++ в составе библиотеки ATL. Если вы уже программируете на ATL, WTL несомненно придётся вам по вкусу.
Самую интересную и животрепещущую тему я приберёг под конец. Вопрос, что же лучше, WTL или MFC, занимает умы программистов уже некоторое время. За 8 лет своего существования MFC прошла долгий путь развития и успела обзавестись "тяжёлой наследственностью". В ней сосуществуют (не всегда мирно) как старые классы (типа CToolBar), так и более новые (такие, как CToolBarCtrl). Для поддержки совместимости с уже существующим кодом Микрософт была вынуждена вносить в MFC всё новые и новые неоптимальные решения и "заплатки", которые сделали внутреннее устройство MFC запутанным и ненадёжным. Сейчас уже практически невозможно что-то изменить в архитектуре MFC, не получив при этом неприятных побочных эффектов. Косность и монолитность MFC порой приводит программистов в бешенство. Наконец, MFC создавалась в то время, когда Windows не поддерживала многопоточное программирование, и с тех пор остаётся недостаточно потокобезопасной. Другими словами, в своём развитии MFC зашла в тупик, и теперь её отмирание - это вопрос времени.
Однако, использование MFC имеет не только недостатки, но и определённые преимущества. Во-первых, на данный момент накоплен поистине громадный опыт использования этой библиотеки. Её используют многие программисты, по ней написаны горы документации, книг и статей. Проблемы, возникающие при её использовании, хорошо изучены. Не следует забывать и про мощные библиотеки классов, расширяющие MFC, как коммерческие (продукты Stingray, Dundas), так и свободно доступные в Интернете. Во-вторых, MFC остаётся самым быстрым средством разработки крупномасштабных приложений на Visual C++, а скорость разработки - немаловажный фактор, который следует учитывать при выборе той или иной библиотеки. В-третьих, MFC предоставляет гораздо более полный набор классов, чем WTL. В неё встроена поддержка файлов, сокетов, классов WinInet, технологий ODBC и DAO, OLE-серверов, ISAPI и многое другое, чего нет в других библиотеках. Более того, в WTL эти средства никогда не будут встроены, так как она создавалась исключительно для поддержки GUI.
Резюмируя сказанное выше, можно утверждать, что вопрос о том, что лучше, невозможно решить однозначно. В зависимости от ситуации следует предпочесть или мощь MFC, или эффективность и гибкость WTL.
В этой статье я буду часто вставлять в текст примечания, сопоставляющие решения, реализованные в WTL, с аналогичными решениями в MFC. Если вы знакомы с MFC, это позволит вам лучше понять сходства и различия двух библиотек.
Будущее WTL предсказать достаточно сложно. Вероятно, эта библиотека появилась слишком поздно. Сейчас всё более актуальной становится проблема интеграции самых различных платформ, операционных систем, языков программирования и т. д. Поэтому всё внимание Микрософт сосредоточено на разработке .NET, "платформы будущего", которая, по мнению её создателей, должна дать ответ на многие вопросы. В этих планах фирмы Микрософт для библиотеки WTL скорее всего не найдётся места. Она будет и дальше развиваться усилиями энтузиастов, создавших её; программисты будут использовать её там, где использование MFC менее удобно. Но она не станет официальным средством разработки, которое придёт на смену MFC. Что будет с ней дальше, покажет время.
Эту статью могут читать все, кого интересует библиотека WTL. При её написании я предполагал, что вы уже достаточно хорошо знаете язык C++ и платформу Win32. Если эти темы вам не знакомы, вам скорее всего не следует браться за WTL, так как изучение этой библиотеки часто сводится к чтению её исходных текстов.
Если это предостережение вас не очень напугало, давайте закончим разговоры и приступим непосредственно к изучению WTL.
Итак, вы решили использовать WTL. Прежде всего, нужно установить эту библиотеку, так как она не входит в состав Visual C++ 6.0 и или более ранних версий. Файлы, входящие в состав, WTL можно загрузить с сервера Microsoft. Самораспаковывающийся архив лежит по адресу:
http://msdn.microsoft.com/msdn-files/027/001/586/wtl31.exe
После того, как вы загрузили и распаковали архив с файлами WTL, дальнейший процесс установки распадается на два этапа.
В архиве, который вы получили от Микрософт, находится три каталога: Include, AppWiz и Samples. Каталог Include содержит файлы, составляющие собственно библиотеку WTL. Поскольку она широко использует шаблоны языка C++, вся её реализация содержится в заголовочных файлах. Скопируйте этот каталог, куда вам нравится. Мне представляется логичным поместить его в том же каталоге, где уже находятся библиотеки MFC и ATL, - в %Visual Studio%\Vc98\ (%Visual Studio% обозначает базовый каталог, в который установлен пакет Visual Studio).
В каталоге AppWiz содержится WTL App Wizard, предназначенный для создания заготовок приложений на базе WTL. Местонахождение файла визарда более критично: чтобы он был успешно обнаружен интегрированной средой Visual C++, его необходимо разместить в каталоге %Visual Studio%\Common\MSDev98\Template.
Что касается примеров из каталога Samples, вы можете разместить их где угодно, например, в папке, где хранятся ваши проекты.
Чтобы компилятор языка C++ мог найти заголовочные файлы библиотеки WTL, нужно указать ему каталог, в котором они лежат. Можно, конечно, указывать в директиве #include полный путь к каждому файлу, но это менее удобно и ухудшит переносимость вашей программы (на другом компьютере файлы WTL могут оказаться в совсем другом каталоге, и ваше приложение откажется компилироваться).
Чтобы указать путь к файлам WTL, запустите Visual C++, откройте диалог Tools->Options и перейдите на вкладку Directories. Затем выберите из выпадающего списка "Show directories for:" пункт Include files и добавьте в конец списка каталог, в который вы скопировали файлы WTL. После выполнения этой операции на моём компьютере окно выглядело так.
Рисунок 1. Окно Tools->Options после добаления пути к файлам библиотеки WTL
Вот и всё! WTL установлена, и теперь вы можете использовать её при разработке собственных программ.
Давайте посмотрим, что же содержится в файлах, которые мы только что установили. Их полный список приведён в таблице 1.
Файл | Что содержит |
---|---|
atlapp.h | Классы модуля и цикла сообщений, интерфейсы фоновых обработчиков и фильтров сообщений. |
atlcrack.h | Набор дополнительных макросов для карты сообщений. |
atlctrls.h | Классы для всех основных контролов Windows. |
atlctrlw.h | Класс командной панели (command bar). |
atlctrlx.h | Набор "самодельных" контролов, не входящих в стандартный комплект Windows. |
atlddx.h | Поддержка механизма обмена данными с диалогом. |
atldlgs.h | Классы для стандартных диалогов и страниц свойств. |
atlframe.h | Класс окна-рамки, классы окон интерфейса MDI, поддержка механизма обновления объектов пользовательского интерфейса. |
atlgdi.h | Классы контекста устройства и объектов GDI (перья, кисти). |
atlmisc.h | Набор вспомогательных классов: CPoint и CRect, CString и т. д. |
atlprint.h | Поддержка печати и предварительного просмотра. |
atlres.h | Набор описаний идентификаторов ресурсов, используемых WTL. |
atlscrl.h | Классы окон с поддержкой прокрутки. |
atlsplit.h | Класс окна-разделителя (splitter window). |
atluser.h | Класс меню. |
Ещё раз подчеркну, что некоторые важные файлы, без которых WTL не может работать в принципе, распространяются в составе библиотеки ATL и находятся в каталоге %Visual Studio%\Vc98\Atl. В первую очередь к таким файлам относятся atlbase.h и atlwin.h.
В книгах по программированию для Windows можно найти множество вариаций на тему приложения "Hello, World!", с которого принято изучать программирование на любом новом языке или в любой новой среде. Вот один из возможных вариантов такого приложения.
#include <windows.h> LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch(msg) { case WM_PAINT: { // Рисуем в окне. PAINTSTRUCT ps; RECT rc; GetClientRect(hWnd, &rc); HDC hdc = BeginPaint(hWnd, &ps); DrawText(hdc, "Hello, Win32!", -1, &rc, DT_SINGLELINE|DT_CENTER|DT_VCENTER); EndPaint(hWnd, &ps); return 0; } case WM_DESTROY: { // Завершаем приложение. PostQuitMessage(0); } } return DefWindowProc(hWnd, msg, wParam, lParam); } int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int) { // Регистрируем класс главного окна. WNDCLASS wc; ZeroMemory(&wc, sizeof(wc)); wc.lpszClassName = "HelloClass"; wc.lpfnWndProc = WndProc; wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); wc.hInstance = hInstance; RegisterClass(&wc); // Создаём главное окно. CreateWindow( "HelloClass", "Hello, Win32!", WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, 0, hInstance, NULL); // Входим в цикл обработки сообщений. MSG msg; while(GetMessage(&msg, 0, 0, 0)) { DispatchMessage(&msg); } return 0; } |
Хотя это приложение не назовёшь многофункциональным, оно содержит все основные элементы, присущие программе для Windows: регистрацию оконного класса, создание главного окна, цикл обработки сообщений и оконную процедуру с обработчиками сообщений WM_PAINT и WM_DESTROY.
Теперь посмотрим, как выглядит та же самая программа, написанная с использованием WTL. Для её создания мы не будем пользоваться визардом. Безусловно, он ускоряет разработку приложений "в реальной жизни", но нам с вами торопиться некуда. Наша задача - как можно глубже понять, как работает WTL.
#include <atlbase.h> #include <atlapp.h> extern CAppModule _Module; #include <atlwin.h> #include <atlgdi.h> #include <atlmisc.h> CAppModule _Module; class CMainWindow : public CWindowImpl<CMainWindow, CWindow, CFrameWinTraits> { // Карта сообщений направляет сообщения в нужные обработчики. BEGIN_MSG_MAP(CMainWindow) MESSAGE_HANDLER(WM_PAINT, OnPaint) MESSAGE_HANDLER(WM_DESTROY, OnDestroy) END_MSG_MAP() LRESULT OnPaint(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { CPaintDC dc(m_hWnd); CRect rect; GetClientRect(rect); dc.DrawText("Hello, Wtl!", -1, rect, DT_SINGLELINE|DT_CENTER|DT_VCENTER); return 0; } LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { PostQuitMessage(0); return 0; } }; int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int) { // Инициализируем модуль _Module.Init(0, hInstance, 0); // Создаём главное окно приложения. CMainWindow wnd; wnd.Create(NULL, CWindow::rcDefault, "Hello, Wtl!"); wnd.ShowWindow(SW_SHOW); // Запускаем цикл сообщений CMessageLoop loop; int res = loop.Run(); // Завершаем программу. _Module.Term(); return res; } |
Как видим, функция WinMain никуда не исчезла (как известно, в MFC эта функция спрятана от программиста). Вместо оконной процедуры появился класс CMainWindow, содержащий обработчики интересующих нас сообщений, а также карту сообщений, сопоставляющую сообщения соответствующим обработчикам. Что касается цикла обработки сообщений, он реализован как объект класса CMessageLoop.
В последующих разделах мы подробно рассмотрим, что происходит внутри WTL и каким образом она выполняет базовые операции, которые мы видели в программе "Hello, Win32!".
Как видно из нашего примера, для работы с окнами в WTL предназначен класс CWindowImpl<>. Однако, под этим классом лежит целая иерархия классов, каждый из которых добавляет в CWindowImpl<> определённую часть функциональности. Классы, входящие в эту иерархию, показаны на рисунке 2. Рассмотрим их по порядку.
Рисунок 2. Иерархия оконных классов библиотеки WTL
Класс CWindow представляет собой тонкую обёртку вокруг хэндла HWND, который используется для работы с окнами в Windows API, и упрощает вызов функций, требующих указания этого хэндла, например:
class CWindow { ... HWND m_hWnd; ... HWND GetDlgItem(int nID) const { ATLASSERT(::IsWindow(m_hWnd)); return ::GetDlgItem(m_hWnd, nID); } ... HICON SetIcon(HICON hIcon, BOOL bBigIcon = TRUE) { ATLASSERT(::IsWindow(m_hWnd)); return (HICON)::SendMessage(m_hWnd, WM_SETICON, bBigIcon, (LPARAM)hIcon); } ... int SetHotKey(WORD wVirtualKeyCode, WORD wModifiers) { ATLASSERT(::IsWindow(m_hWnd)); return (int)::SendMessage(m_hWnd, WM_SETHOTKEY, MAKEWORD(wVirtualKeyCode, wModifiers), 0); } ... }; |
Как видим, класс CWindow хранит хэндл связанного с ним окна в переменной-члене m_hWnd и передаёт его функциям Win32. Использование класса CWindow избавляет нас от необходимости помнить, какие сообщения нужно посылать окну для выполнения с ним определённых действий и как упаковывать параметры этих сообщений в WPARAM и LPARAM. Кроме того, каждая функция содержит строчку ATLASSERT(::IsWindow(m_hWnd)). Попытавшись выполнить какую-либо операцию с несуществующим окном, мы тут же получим предупреждение в отладочной версии программы. В финальной версии проверки исчезнут, избавляя программу от лишней нагрузки.
Класс CWindow никак не зависит от остальных классов WTL. Это означает, что вы легко можете использовать только его в программе на "чистом" API. Для этого достаточно включить в программу строчки:
#include <atlbase.h> extern CComModule _Module; #include <atlwin.h> |
Замечу, что описывать переменную _Module в этом случае не нужно. Достаточно декларации, чтобы умиротворить компилятор языка C++.
Если вы когда-нибудь пытались использовать в программе на "чистом" API класс CWnd из библиотеки MFC, вы знаете, что сделать это практически невозможно. Сказывается "монолитность" MFC, в которой все классы зависят друг от друга. |
Обратите внимание на ещё один важный момент: функции типа SetFocus и GetDlgItem возвращают HWND, а не объект класса CWindow. Тем не менее, класс CWindow имеет полный набор конструкторов, операторов присваивания и приведения типа, что позволяет вам использовать HWND и CWindow как один и тот же тип. Например, вы можете "на лету" преобразовать возвращаемый вам HWND в CWindow:
CWindow wnd = ::GetFocus(); |
Функции MFC-класса CWnd никогда не возвращают HWND. Вместо этого возвращается указатель на объект CWnd. Если в программе не существует объекта класса CWnd, соответствующего нужному окну, MFC создаёт временный объект. При этом происходит динамическое распределение памяти, поиск объекта в постоянной и временной карте окон и множество других подобный операций. Кроме того, MFC уничтожает временные объекты класса CWnd в цикле фоновой обработки. При этом возвращённые нам указатели становятся недействительными, что вносит дополнительную путаницу и может стать причиной ошибки. |
Класс CMessageMap является абстракцией карты сообщений. Посмотрим, как этот класс описан в atlwin.h.
class ATL_NO_VTABLE CMessageMap { public: virtual BOOL ProcessWindowMessage(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lResult, DWORD dwMsgMapID) = 0; }; |
Картой сообщений в WTL является любой класс, который может обрабатывать сообщения Windows. Для этого он порождается от CMessageMap и предоставляет свою реализацию виртуальной функции ProcessWindowMessage. В разделе "Маршрутизация сообщений" мы подробно рассмотрим, как эта функция создаётся с помощью макросов карты сообщений, предоставляемых нам библиотекой WTL.
А сейчас ещё раз внимательно посмотрите на описание класса CMessageMap. Он очень похож на интерфейсы, которые используются в COM и Java. И это сходство не случайно. Любой объект в вашей программе (не обязательно оконный!) может реализовать интерфейс, задаваемый классом CMessageMap, и обрабатывать сообщения. Благодаря этому вы без труда сможете распределить обработку сообщений по объектам, в которых осуществлять её наиболее удобно. Этот простой и элегантный подход нам ещё не раз встретится при изучении WTL.
Знатоки MFC несомненно заметят здесь сходство с классом CCmdTarget. Однако, есть и два важных различия. Во-первых, неоконные классы, порождённые от CCmdTarget, могут обрабатывать только команды (WM_COMMAND) и уведомления (WM_NOTIFY), а все остальные сообщения - нет. Во вторых, MFC накладывает существенные ограничения на множественное наследование: вы не можете произвести класс от двух классов, в свою очередь порождённых от CObject. Это означает, что класс, порождённый от CFile, CDC или CRecordset, уже не может наследоваться ещё и от CCmdTarget. С другой стороны, WTL не накладывает ограничений на множественное наследование, и вы можете без труда "прикрутить" класс CMessageMap к любому производному классу. |
CWinTraits - это очень маленький и лёгкий класс, предназначенный для работы с обычными и расширенными стилями окна. Он описан следующим образом.
template <DWORD t_dwStyle = 0, DWORD t_dwExStyle = 0> class CWinTraits { public: static DWORD GetWndStyle(DWORD dwStyle) { return dwStyle == 0 ? t_dwStyle : dwStyle; } static DWORD GetWndExStyle(DWORD dwExStyle) { return dwExStyle == 0 ? t_dwExStyle : dwExStyle; } }; |
Обратите внимание, что этот класс вообще не содержит переменных-членов. Стили, которые возвращают функции GetWndStyle и GetWndExStyle, статически задаются на этапе компиляции. Классы, порождаемые от CWinTraits, используют эти стили при создании окна.
Чтобы сделать нашу работу более комфортной, разработчики WTL предоставили нам несколько специализаций шаблона CWinTraits<>, задав в них наиболее часто используемые комбинации стилей:
typedef CWinTraits<WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, 0> CControlWinTraits; typedef CWinTraits<WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, WS_EX_APPWINDOW | WS_EX_WINDOWEDGE> CFrameWinTraits; typedef CWinTraits<WS_OVERLAPPEDWINDOW | WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, WS_EX_MDICHILD> CMDIChildWinTraits; typedef CWinTraits<0, 0> CNullTraits; |
Класс CControlWinTraits удобнее всего применять для элементов управления, CFrameWinTraits - для главного окна приложения (именно его я использовал в программе "Hello, Wtl!"), а CMDIChildWinTraits - для дочерних окон главного окна в приложении с интерфейсом MDI. CNullTraits вообще не определяет никаких стилей.
CWindowImplRoot - очень важный класс. Именно с него начинается "ветвление" иерархии оконных классов WTL. Одна ветвь, отходящая от него - это обычные окна, вторая ветвь - диалоги. Соответственно, класс CWindowImplRoot как бы "резюмирует" все свойства, необходимые и тем, и другим.
Описание класса CWindowImplRoot выглядит так.
template <class TBase = CWindow> class ATL_NO_VTABLE CWindowImplRoot : public TBase, public CMessageMap { public: CWndProcThunk m_thunk; // Несущественные детали опущены. ... // Message reflection support LRESULT ReflectNotifications(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); static BOOL DefaultReflectionHandler(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lResult); }; |
Как видим, класс CWindowImplRoot, произведён от двух других классов, TBase и CMessageMap. В связи с этим возникает интересный вопрос: а почему нельзя было сразу подставить вместо TBase класс CWindow? Дело в том, что CWindow далеко не всегда отвечает всем нашим нуждам. Если, к примеру, мы работаем с полем ввода (edit box), нам будет удобно породить от CWindow новый класс (например, CEdit) и добавить в него обёртки сообщений типа EM_SETSEL и EM_SETLIMITTEXT. Как мы узнаем позже, WTL предлагает нам целый набор подобных классов, описанных в файле atlctrls.h. Благодаря предусмотрительности WTL мы сможем подставить любой из них в шаблон CWindowImplRoot, наделив результирующий класс необходимыми свойствами.
Кроме поддержки работы с HWND в объектно-ориентированном стиле и карт сообщений класс CWindowImplRoot обеспечивает поддержку ещё двух важных механизмов: переходников оконной процедуры (window proc thunks) и отражения уведомлений (notification reflection). Переходники используются для эффективного отображения хэндла окна на адрес соответствующего ему объекта в нашей программе. Отражение сообщений позволяет контролам обрабатывать собственные уведомления. Мы увидим, как работают эти механизмы, в разделе "Маршрутизация сообщений".
Класс CWindowImplBaseT реализует практически все функции, необходимые для работы с окном. Именно здесь появляются оконные процедуры StartWindowProc и WindowProc, которые обеспечивают обработку сообщений через карты сообщений. Кроме того, класс CWindowImplBaseT содержит функции SubclassWindow и UnsubclassWindow, позволяющие классу "подключиться" к уже существующему окну и обработать часть поступающих в него сообщений через карту сообщений (остальные будут по-прежнему поступать в оконную процедуру окна).
template <class TBase = CWindow, class TWinTraits = CControlWinTraits> class ATL_NO_VTABLE CWindowImplBaseT : public CWindowImplRoot< TBase > { public: WNDPROC m_pfnSuperWindowProc; ... static LRESULT CALLBACK StartWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); ... BOOL SubclassWindow(HWND hWnd); HWND UnsubclassWindow(BOOL bForce = FALSE); ... LRESULT DefWindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam) { #ifdef STRICT return ::CallWindowProc(m_pfnSuperWindowProc, m_hWnd, uMsg, wParam, lParam); #else return ::CallWindowProc((FARPROC)m_pfnSuperWindowProc, m_hWnd, uMsg, wParam, lParam); #endif } }; |
Обратите внимание на переменную-член m_pfnSuperWindowProc. Именно в ней сохраняется указатель на оконную процедуру, которую окно имело до сабклассинга с помощью функции SubclassWindow. Если карта сообщений не содержит записи для некоторого сообщения, оно будет передано в изначальную оконную процедуру (как мы видим, это происходит в функции DefWindowProc).
Для работы со стилями окна класс CWindowImplBaseT использует уже рассмотренный нами шаблон CWinTraits.
Ну, вот мы и добрались до класса CWindowImpl. Это совсем маленький класс, который наследует большую часть функциональности от CWindowImplBaseT. Единственное, что в нём добавляется - это поддержка работы с классами окна. Как мы знаем, в Windows каждое окно имеет свой класс. Классы регистрируются с помощью функции RegisterClass(Ex) и передаются в функцию, создающую окно (CreateWindow(Ex)).
Класс CWindowImpl скрывает от нас работу по регистрации класса окна. Посмотрим, как он описан в atlwin.h.
template <class T, class TBase = CWindow, class TWinTraits = CControlWinTraits> class ATL_NO_VTABLE CWindowImpl : public CWindowImplBaseT< TBase, TWinTraits > { public: DECLARE_WND_CLASS(NULL) HWND Create(HWND hWndParent, RECT& rcPos, LPCTSTR szWindowName = NULL, DWORD dwStyle = 0, DWORD dwExStyle = 0, UINT nID = 0, LPVOID lpCreateParam = NULL) { if (T::GetWndClassInfo().m_lpszOrigName == NULL) T::GetWndClassInfo().m_lpszOrigName = GetWndClassName(); ATOM atom = T::GetWndClassInfo().Register(&m_pfnSuperWindowProc); dwStyle = T::GetWndStyle(dwStyle); dwExStyle = T::GetWndExStyle(dwExStyle); return CWindowImplBaseT< TBase, TWinTraits >::Create(hWndParent, rcPos, szWindowName, dwStyle, dwExStyle, nID, atom, lpCreateParam); } }; |
В WTL информация о классе окна содержится в объекте класса CWndClassInfo, который в зависимости от значения макроса _UNICODE разворачивается либо в структуру _ATL_WNDCLASSINFOA, либо в структуру _ATL_WNDCLASSINFOW. Для примера рассмотрим структуру _ATL_WNDCLASSINFOA.
struct _ATL_WNDCLASSINFOA { WNDCLASSEXA m_wc; LPCSTR m_lpszOrigName; WNDPROC pWndProc; LPCSTR m_lpszCursorID; BOOL m_bSystemCursor; ATOM m_atom; CHAR m_szAutoName[13]; ATOM Register(WNDPROC* p) { return AtlModuleRegisterWndClassInfoA(&_Module, this, p); } }; |
Как видим, самое первое поле этой структуры имеет тип WNDCLASSEX - это структура, которая используется при регистрации класса в Win32 API. Функция Register выполняет регистрацию оконного класса, информация о котором хранится в объекте класса CWndClassInfo.
Макрос DECLARE_WND_CLASS добавляет в класс окна (языка C++) статический экземпляр объекта класса CWndClassInfo, а также функцию для доступа к нему:
#define DECLARE_WND_CLASS(WndClassName) \ static CWndClassInfo& GetWndClassInfo() \ { \ static CWndClassInfo wc = \ { \ { sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, StartWindowProc, \ 0, 0, NULL, NULL, NULL, (HBRUSH)(COLOR_WINDOW + 1), NULL, WndClassName, NULL }, \ NULL, NULL, IDC_ARROW, TRUE, 0, _T("") \ }; \ return wc; \ } |
При создании окна функция CWindowImpl::Create использует информацию, добавленную с помощью макроса DECLARE_WND_CLASS, для автоматической регистрации оконного класса (с помощью функции CWndClassInfo::Register). Замечу, что в классе CWindowImpl класс окна объявлен с именем NULL. Для таких "безымянных" классов WTL выбирает имя сама. Это имя имеет вид "ATL:XXXXXXXX", где XXXXXXXX - адрес структуры m_wc в объекте CWndClassInfo. Вы можете изменить это имя на любое другое, добавив в секцию public произведённого от CWindowImpl класса строчку:
DECLARE_WND_CLASS("<имя_класса>")
|
Вы можете также изменить любые параметры, которые выбрал за вас макрос DECLARE_WND_CLASS. Для этого измените соответствующие поля структуры m_wc, содержащейся в классе CWndClassInfo. Например, сменить иконку приложения можно так:
CMainWindow wnd; // Задаём иконку "перечёркнутый круг". wnd.GetWndClassInfo().m_wc.hIcon = LoadIcon(NULL, IDI_ERROR); wnd.Create(NULL, CWindow::rcDefault, "Hello, Wtl!"); |
Иерархию оконных классов, которую мы только что рассмотрели, WTL унаследовала от библиотеки ATL. Зато классы CAppModule и CMessageLoop в WTL появились впервые. Они описаны в файле atlapp.h. Давайте посмотрим, как они вписываются в общую картину.
Класс CMessageLoop реализует цикл обработки сообщений Windows. Концептуально он ничем не отличается от простейшего цикла сообщений, который мы видели в программе "Hello, Win32", но предоставляет несколько новых возможностей - фильтрацию сообщений и фоновую обработку. Вот как цикл сообщений описан в функции CMessageLoop::Run.
// message loop int Run() { BOOL bDoIdle = TRUE; int nIdleCount = 0; BOOL bRet; for(;;) { while(!::PeekMessage(&m_msg, NULL, 0, 0, PM_NOREMOVE) && bDoIdle) { if(!OnIdle(nIdleCount++)) bDoIdle = FALSE; } bRet = ::GetMessage(&m_msg, NULL, 0, 0); if(bRet == -1) { ATLTRACE2(atlTraceUI, 0, _T("::GetMessage returned -1 (error)\n")); continue; // error, don't process } else if(!bRet) { ATLTRACE2(atlTraceUI, 0, _T("CMessageLoop::Run - exiting\n")); break; // WM_QUIT, exit message loop } if(!PreTranslateMessage(&m_msg)) { ::TranslateMessage(&m_msg); ::DispatchMessage(&m_msg); } if(IsIdleMessage(&m_msg)) { bDoIdle = TRUE; nIdleCount = 0; } } return (int)m_msg.wParam; } |
Работа цикла сообщений распадается на нескольких этапов. Сначала он проверяет, нет ли в очереди необработанных сообщений. Если сообщений нет, вызывается виртуальная функция OnIdle, в которой выполняется фоновая обработка. После того как фоновая обработка закончена, вызывается функция GetMessage. Эта функция извлекает следующее сообщение из очереди. Если сообщений нет, Windows немедленно заберёт у нашей программы управление и вернёт его только тогда, когда сообщения появятся. Рано или поздно это произойдёт, и GetMessage вернёт управление. Если к нам пришло сообщение WM_QUIT, цикл сообщений немедленно завершается. В противном случае вызывается виртуальная функция PreTranslateMessage, поддерживающая механизм фильтров сообщений. В процессе работы фильтров сообщение может быть преобразовано, а то и вовсе отброшено (в этом случае PreTranslateMessage вернёт TRUE, и цикл сообщений перейдёт к следующей итерации). Если этого не произошло, сообщение направляется в целевую оконную процедуру вызовом DispatchMessage.
Посмотрим, каким образом выполняется фоновая обработка.
Каждый объект класса CMessageMap поддерживает список фоновых обработчиков. Добавить объект в этот список можно, используя функцию CMessageLoop::AddIdleHandler. Функция CMessageLoop::RemoveIdleHandler выполняет обратную операцию - удаляет обработчик из списка. Когда в цикле сообщений вызывается функция OnIdle, она просматривает список фоновых обработчиков и вызывает их по очереди, пока список не будет исчерпан:
virtual BOOL OnIdle(int /*nIdleCount*/) { for(int i = 0; i < m_aIdleHandler.GetSize(); i++) { CIdleHandler* pIdleHandler = m_aIdleHandler[i]; if(pIdleHandler != NULL) pIdleHandler->OnIdle(); } return FALSE; // don't continue } |
Обратите внимание, что существующая реализация функции OnIdle никак не использует счётчик nIdleCount и всегда возвращает FALSE. Поэтому фоновая обработка всегда выполняется за одну итерацию. Вы можете изменить это поведение, переопределив виртуальную функцию OnIdle в классе, производном от CMessageLoop.
ПРИМЕЧАНИЕ Как видим, WTL, в отличие от MFC, сама по себе не выполняет никакой фоновой обработки. Если вам требуется обновлять объекты пользовательского интерфейса или выполнять ещё какие-либо операции "в фоне", вы должны явно добавить в цикл сообщений один или несколько фоновых обработчиков. |
Осталось выяснить, что представляет собой объект, который может стать фоновым обработчиком. Такой объект обязан наследовать от класса CIdleHandler и предоставлять собственную реализацию чисто виртуальной функции OnIdle, объявленной в этом классе.
Класс CIdleHandler определяет интерфейс, единый для всех фоновых обработчиков. Его описание, как и в случае с CMessageMap, совсем простое.
class CIdleHandler { public: virtual BOOL OnIdle() = 0; }; |
Таким образом, в WTL объект совершенно любого класса может стать фоновым обработчиком. Всё, что для этого нужно, - произвести этот класс от CIdleHandler и добавить в него функцию OnIdle.
Мы не будем долго задерживаться на фильтрах сообщений, поскольку они реализованы полностью аналогично фоновым обработчикам. Класс CMessageLoop поддерживает список фильтров сообщений, работа с которым ведётся через функции CMessageLoop::AddMessageFilter и CMessageLoop::RemoveMessageFilter. Когда из цикла сообщений вызывается функция CMessageLoop::PreTranslateMessage, эта функция просматривает список фильтров и вызывает их по очереди. Фильтр получает указатель на структуру MSG, содержащую текущее сообщение, и может делать с ней всё, что угодно (например, подменять параметры сообщения или окно-адресата). Как только один из фильтров вернул TRUE, функция немедленно завершается, также возвращая TRUE. Оставшиеся фильтры при этом не вызываются. Если ни один фильтр не отбросил сообщение, PreTranslateMessage возвращает FALSE, и сообщение, как мы уже видели, попадает в оконную процедуру.
Из сказанного следует, что порядок добавления фильтров, в отличие от порядка добавления фоновых обработчиков, может иметь значение. Чем раньше фильтр был добавлен, тем раньше он будет вызываться в процессе работы CMessageLoop::PreTranslateMessage.
Интерфейс, общий для всех фильтров сообщений, определяет класс CMessageFilter. В этом классе содержится чисто виртуальная функция PreTranslateMessage, которая должна быть реализована в производном классе. Если эта функция возвращает TRUE, дальнейшая обработка сообщения не производится. Класс CMessageFilter выглядит так.
class CMessageFilter { public: virtual BOOL PreTranslateMessage(MSG* pMsg) = 0; }; |
Если вы программировали с использованием библиотеки MFC, вам должно быть известно, что MFC реализует похожий механизм фильтрации сообщений: перед вызовом TranslateMessage и DispatchMessage MFC вызывает функцию PreTranslateMessage, которая может преобразовывать или отбрасывать сообщения. Однако есть и существенное различие: MFC сама решает, для каких объектов вызывать PreTranslateMessage, а для каких нет. Это избавляет вас от дополнительной работы, но зато лишает вас "свободы действий" и может приводить к весьма странным ошибкам. У меня был случай, когда сообщения WM_KEYDOWN, адресованные окну верхнего уровня, попадали в диалог (который являлся главным окном приложения), после чего программа входила в бесконечный цикл. Искать такие ошибки довольно сложно. Если же попытаться изменить стандартный механизм фильтров в MFC (перегрузив CWinThread::PreTranslateMessage), это может привести к непредсказуемым последствиям. |
Класс CAppModule является чем-то вроде центрального репозитория данных о приложении. Так, в нём хранится hInstance, который ОС передаёт в функцию WinMain. Объект класса CAppModule должен иметь имя _Module, так как некоторые заголовочные файлы WTL рассчитывают именно на это название (обратите внимание, что декларация объекта _Module в нашем примере предшествует включению файла atlwin.h, - функции, описанные в этом файле, активно используют _Module).
На самом деле, класс CAppModule порождён от ATL-класса CComModule и наследует от него не только функции для поддержки работы с окнами, но и функции, отвечающие за манипулирование COM-объектами (добавление информации об объектах в реестр Windows, создание объектов через фабрики классов и т. д.). |
Объект _Module является глобальным и доступен в любой точке приложения. Целостность его внутренних структур обеспечивается с помощью применения критических секций, поэтому вы можете спокойно обращаться к нему из разных потоков. Инициализация объекта _Module выполняется с помощью функции Init. Советую вызывать эту функцию как можно раньше, так как иначе могут возникнуть ошибки. Например, именно в ней инициализируются все критические секции объекта, а попытка использовать хотя бы одну из них без предварительной инициализации приведёт к аварийному завершению программы.
В примерах, поставляемых вместе с WTL, можно увидеть примерно следующий код.
CMessageLoop theLoop;
_Module.AddMessageLoop(&theLoop);
...
int nRet = theLoop.Run();
...
_Module.RemoveMessageLoop();
|
Как видим, в объекте _Module можно регистрировать циклы сообщений. Благодаря этому доступ к ним можно получить из любого места в вашей программе. Хотя я не стал регистрировать цикл сообщений в примере "Hello, Wtl!", это может быть очень удобно для последующей регистрации фоновых обработчиков и фильтров сообщений. Получить цикл сообщений, соответствующий текущему потоку, можно с помощью вызова:
CMessageLoop *pLoop = _Module.GetMessageLoop(); |
Получить цикл любого другого потока можно, передав идентификатор этого потока функции GetMessageLoop.
ПРЕДУПРЕЖДЕНИЕ Попытка добавить два цикла сообщений, принадлежащих одному и тому же потоку, приведёт к ошибке. |
В следующем разделе мы увидим, как объект _Module используется в процессе создания окна.
Маршрутизация сообщений - важная тема. Без понимания механизмов обработки сообщений можно легко зайти в тупик, когда программа начинает вести себя не так, как предполагалось. Давайте попытаемся проследить путь сообщений, которые операционная система передаёт нашему приложению.
Для обработки сообщений в Windows используются оконные процедуры. Но прежде чем туда попасть, сообщение добавляется в очередь сообщений потока. Каждый поток имеет свою очередь. Сообщение поступает в поток, создавший окно, которому это сообщение адресовано.
Кроме того, есть сообщения, адресованные потоку, а не окну; в этом случае поле hwnd структуры MSG устанавливается в NULL. |
Мы уже видели, как сообщения выбираются из очереди с помощью функции GetMessage. Это происходит в цикле сообщений. Полученное сообщение направляется в фильтры, которые мы зарегистрировали. Любой фильтр может изменить сообщение или запретить его дальнейшую обработку. Если ни один фильтр не отбросил сообщение, оно передаётся функции DispatchMessage, которая направляет его в соответствующую оконную процедуру.
Используя фильтры сообщений, необходимо всегда помнить важный момент: не все сообщения ставятся в очередь. Многие сообщения направляются прямиком в оконную процедуру. К их числу относятся такие сообщения, как WM_CREATE, WM_SIZE, WM_COPY, любые сообщения, отправленные с помощью SendMessage и т. д. Попытка перехватить любое из этих сообщений в фильтре закончится неудачей, так как оно туда просто не попадёт. |
Итак, сообщение попадает в оконную процедуру. Если окно имеет связанный с ним класс CWindowImpl, то эта процедура - CWindowImplBaseT<>::WindowProc. Посмотрим, как она реализована.
template <class TBase, class TWinTraits> LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)hWnd; // set a ptr to this message and save the old value MSG msg = { pThis->m_hWnd, uMsg, wParam, lParam, 0, { 0, 0 } }; const MSG* pOldMsg = pThis->m_pCurrentMsg; pThis->m_pCurrentMsg = &msg; // pass to the message map to process LRESULT lRes; BOOL bRet = pThis->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam, lRes, 0); // restore saved value for the current message ATLASSERT(pThis->m_pCurrentMsg == &msg); pThis->m_pCurrentMsg = pOldMsg; // do the default processing if message was not handled if(!bRet) { if(uMsg != WM_NCDESTROY) lRes = pThis->DefWindowProc(uMsg, wParam, lParam); else { // unsubclass, if needed ... } } return lRes; } |
Для нас здесь важно то, что функция WindowProc определяет по хэндлу окна адрес связанного с ним объекта класса, а затем вызывает функцию ProcessWindowMessage. Реализация этой функции целиком лежит на совести программиста. Можно, как в старые добрые времена, написать её в стиле огромного switch'а. Но проще воспользоваться специальными макросами карты сообщений, которые предоставляет нам WTL.
Заготовка функции ProcessWindowMessage создаётся макросами BEGIN_MSG_MAP и END_MSG_MAP:
BEGIN_MSG_MAP(CMainWindow)
// Сюда вставляются другие макросы.
END_MSG_MAP()
|
После обработки препроцессором этот фрагмент примет следующий вид.
public: BOOL ProcessWindowMessage(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lResult, DWORD dwMsgMapID = 0) { BOOL bHandled = TRUE; hWnd; uMsg; wParam; lParam; lResult; bHandled; switch(dwMsgMapID) { case 0: // Сюда вставляются другие макросы. break; default: ATLTRACE2(atlTraceWindowing, 0, _T("Invalid message map ID (%i)\n"), dwMsgMapID); ATLASSERT(FALSE); break; } return FALSE; |
Возможно, вы подумали, что оператор switch, который для нас приготовила WTL, предназначен для выбора сообщения. Но это не так. Этот оператор предназначен для выбора карты сообщений. По умолчанию используется карта с номером 0. Функция ProcessWindowMessage узнаёт, какую карту использовать, по параметру dwMsgMapID. Как видим, объект может иметь несколько карт сообщений и использовать разные карты в различных обстоятельствах. Гибкость WTL впечатляет, не так ли?
ПРИМЕЧАНИЕ В MFC карты сообщений реализованы совершенно иначе, чем в WTL. Там карта сообщений представляет собой не функцию, а массив структур, каждая из которых сопоставляет сообщению нужный обработчик. Каждый раз, когда сообщение нужно обработать, карта сообщений сканируется в поисках нужного обработчика. Если обработчик не найден, процедура повторяется для базового класса и так далее до самого верхнего уровня иерархии наследования. |
Другие макросы карты сообщений делают именно то, что можно было предположить. Например, макрос MESSAGE_HANDLER связывает сообщение Windows с соответствующим обработчиком:
#define MESSAGE_HANDLER(msg, func) \ if(uMsg == msg) \ { \ bHandled = TRUE; \ lResult = func(uMsg, wParam, lParam, bHandled); \ if(bHandled) \ return TRUE; \ } |
Макрос ATL_MSG_MAP служит для начала новой карты сообщений; для этого он просто вставляет ещё одну ветку оператора switch:
#define ALT_MSG_MAP(msgMapID) \ break; \ case msgMapID: |
Макрос CHAIN_MSG_MAP позволяет направить сообщение в карту сообщений базового класса.
#define CHAIN_MSG_MAP(theChainClass) \ { \ if(theChainClass::ProcessWindowMessage(hWnd, uMsg, wParam, lParam, lResult)) \ return TRUE; \ } |
ПРИМЕЧАНИЕ В отличие от MFC, в WTL карты сообщений не наследуются автоматически. Направлять сообщение в базовый класс вам придётся самостоятельно. |
Макрос CHAIN_MSG_MAP_MEMBER позволяет передать сообщение на обработку другому объекту.
#define CHAIN_MSG_MAP_MEMBER(theChainMember) \ { \ if(theChainMember.ProcessWindowMessage(hWnd, uMsg, wParam, lParam, lResult)) \ return TRUE; \ } |
Из описания макроса видно, что он может передавать сообщение не только объекту-члену нашего класса, но и глобальному объекту.
Полный список макросов можно найти в MSDN. Однако я призываю вас не доверять документации, а обратиться к описаниям этих макросов в файле atlwin.h. В этом случае вы будете уверенными, что не упустили что-то важное. Кроме того, вы можете перемежать макросы карты сообщений с обычным кодом на C++. Допустим, вы хотите направить сообщения WM_LBUTTONUP и WM_RBUTTONUP в один и тот же обработчик. Можно добавить в карту сообщений два макроса MESSAGE_HANDLER, а можно написать просто:
BEGIN_MSG_MAP(CMainWindow) ... if(uMsg == WM_LBUTTONUP || uMsg == WM_RBUTTONUP) { OnButtonUp(); return 1; } ... END_MSG_MAP() |
Если вы долго писали на MFC и чувствуете себя неуютно без макросов типа ON_WM_CREATE и ON_WM_PAINT, вам будет приятно узнать, что WTL предоставляет вам набор похожих макросов. Они описаны в файле atlcrack.h. Так как они не входят в библиотеку ATL, документацию на них вы не найдёте. Тем не менее, вы сможете без труда разобраться в работе этих макросов, посмотрев на их описания.
Все макросы из atlcrack.h имеют вид:
MSG_сообщение_Windows(<имя обработчика>) |
Если вы решите использовать их в вашей программе, помните, что в этом случае вам следует использовать вместо BEGIN_MSG_MAP макрос BEGIN_MSG_MAP_EX. Этот макрос тоже вставляет пролог функции ProcessWindowMessage, но предваряет его описаниями нескольких вспомогательных функций.
Вот небольшой пример использования макросов из atlcrack.h:
BEGIN_MSG_MAP_EX(CMainWindow) MSG_WM_PAINT(OnPaint) MSG_WM_DESTROY(OnDestroy) END_MSG_MAP() void OnPaint(HDC) { CPaintDC dc(m_hWnd); CRect rect; GetClientRect(rect); dc.DrawText("Hello, Wtl!", -1, rect, DT_SINGLELINE|DT_CENTER|DT_VCENTER); } void OnDestroy() { PostQuitMessage(0); } |
Важный случай обработки сообщений в WTL - отражение уведомлений. Контролы посылают уведомления родительскому окну, когда происходит что-то важное. Но очень часто уведомления удобнее обрабатывать в классе самого контрола, а не родительского окна. Механизм отражения позволяет окну переправлять уведомления обратно дочерним контролам, которые их послали. Чтобы это происходило, следует добавить в карту сообщений этого окна макрос REFLECT_NOTIFICATIONS.
#define REFLECT_NOTIFICATIONS() \ { \ bHandled = TRUE; \ lResult = ReflectNotifications(uMsg, wParam, lParam, bHandled); \ if(bHandled) \ return TRUE; \ } |
ПРИМЕЧАНИЕ В MFC механизм отражения реализован иначе: для перенаправления уведомлений обратно в контрол там не требуется модифицировать карту сообщений родительского окна, так как библиотека сама заботится о корректной маршрутизации. |
Как видим, макрос REFLECT_NOTIFICATIONS просто передаёт сообщение в функцию ReflectNotifications, которая описана в классе CWindowImplRoot. Эта функция определяет, является ли сообщение уведомлением, и если является, направляет его обратно отправителю.
Мы уже знаем, что в WTL любой класс может иметь карту сообщений. Однако, включение макроса REFLECT_NOTIFICATIONS в карту объекта, у которого нет функции ReflectNotifications, приведёт к ошибке. Используя макросы карты сообщений, следует всегда помнить, какой код за ними стоит. |
Отражённые сообщения обрабатываются точно так же, как и все остальные. Чтобы их можно было отличить от нормальных сообщений, функция ReflectNotifications добавляет к их коду константу OCM__BASE. Коды сообщений с прибавленной константой описаны в файле olectrl.h и имеют префикс OCM_. WM_COMMAND превращается в OCM_COMMAND, WM_DRAWITEM - в OCM_DRAWITEM и т. д. Таким образом, для обработки отражённого сообщения WM_COMMAND нужно вставить в карту сообщений контрола макрос вида:
MESSAGE_HANDLER(OCM_COMMAND, OnReflectedCommand) |
Чтобы перенаправить все необработанные отражённые сообщения в обработчик по умолчанию, добавьте в конец карты сообщений контрола макрос DEFAULT_REFLECTION_HANDLER, который вызовет функцию CWindowImplRoot::DefaultReflectionHandler. Функция DefaultReflectionHandler реализована более чем прямолинейно:
template <class TBase> BOOL CWindowImplRoot< TBase >::DefaultReflectionHandler(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lResult) { switch(uMsg) { case OCM_COMMAND: case OCM_NOTIFY: case OCM_PARENTNOTIFY: case OCM_DRAWITEM: // И т. д. ещё десяток сообщений ... lResult = ::DefWindowProc(hWnd, uMsg - OCM__BASE, wParam, lParam); return TRUE; default: break; } return FALSE; } |
В этом разделе мы рассмотрим два тонких момента, связанных с оконными процедурами в WTL. Вы вполне можете пропустить его, удовлетворившись тем, что "это всё работает". Но самых любопытных я приглашаю читать дальше.
В предыдущим разделе мы познакомились с оконной функцией WindowProc. Однако в ней есть строчка, заслуживающая более пристального внимания. Посмотрим на неё ещё раз.
template <class TBase, class TWinTraits> LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)hWnd; ... } |
Как видим, переход от хэндла окна к указателю на объект класса, связанный с ним, осуществляется прямым приведением типа. Как же так? Ведь мы не можем заказать Windows нужный нам хэндл, и просмотр в Spy++ действительно показывает, что он не совпадает с адресом объекта. Функция WindowProc вызывается непосредственно операционной системой, и никакие другие функции WTL в этот процесс не вмешиваются. Если вы подумали о перегрузке операторов, то опять не угадали. WTL не перегружает оператор приведения типа, и преобразование осуществляется по обычным правилам языка C++.
Так в чём же дело? Оказывается, при вызове оконной процедуры управление попадает в неё не сразу. Вместо этого операционная система вызывает переходник, который имеет следующий вид.
mov dword ptr [esp+4],<адрес объекта> jmp <адрес оконной процедуры> |
Этот код подменяет хэндл, лежащий в стеке, на адрес объекта, а затем делает безусловный переход по адресу оконной процедуры. Вот как получается, что она получает адрес вместо хэндла.
Каждый объект класса, производного от CWindowImplRoot, имеет свой собственный переходник. Он находится в переменной класса m_thunk:
CWndProcThunk m_thunk; |
Посмотрим, как описан класс CWndProcThunk для процессора Intel x86.
#pragma pack(push,1) struct _WndProcThunk { DWORD m_mov; // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd) DWORD m_this; // BYTE m_jmp; // jmp WndProc DWORD m_relproc; // relative jmp }; #pragma pack(pop) class CWndProcThunk { public: union { _AtlCreateWndData cd; _WndProcThunk thunk; }; void Init(WNDPROC proc, void* pThis) { thunk.m_mov = 0x042444C7; //C7 44 24 0C thunk.m_this = (DWORD)pThis; thunk.m_jmp = 0xe9; thunk.m_relproc = (int)proc - ((int)this+sizeof(_WndProcThunk)); ... } }; |
Сейчас для нас не важно, что такое _AtlCreateWndData. Важно, что после записи в поля m_mov и m_jmp предопределённых значений, а в поля m_this и m_relproc соответствующих адресов в классе CWndProcThunk образуется код переходника, который мы рассмотрели выше. Обратите внимание на директиву #pragma pack, которая отключает выравнивание полей структуры по границам, отличным от байта. Без неё компилятор мог бы добавить в структуру _WndProcThunk незаполненные пространства, нарушая содержащийся в ней код.
Надо признать, что механизм переходников оконных процедур весьма элегантен. Сколько бы окон не создала наша программа, определение указателя на объект класса по хэндлу окна будет происходить практически моментально.
В MFC для определения указателя на объект класса по хэндлу окна используются карта хэндлов - класс, похожий на map из STL. Такой подход существенно проигрывает по скорости способу, применяемому в WTL. |
Теперь мы знаем, как работают переходники, и нам осталось выяснить, когда WTL подменяет адрес настоящей оконной процедуры на адрес переходника. Если мы подключаемся к уже существующему окну, ответ на этот вопрос достаточно прост - это происходит в функции CWindowImplBaseT::SubclassWindow. Но в случае создания окна "с нуля" функцией CWindowImpl::Create процесс усложняется. Класс окна, который регистрирует эта функция, имеет в качестве оконной процедуры функцию CWindowImplBaseT::StartWindowProc:
static CWndClassInfo& GetWndClassInfo() \ { \ static CWndClassInfo wc = \ { \ { sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, StartWindowProc, \ 0, 0, NULL, NULL, NULL, (HBRUSH)(COLOR_WINDOW + 1), NULL, WndClassName, NULL }, \ NULL, NULL, IDC_ARROW, TRUE, 0, _T("") \ }; \ return wc; \ } |
Функция StartWindowProc назначается окну "временно". Её задача - создать для окна переходник к функции WindowProc и задать его адрес в качестве новой оконной процедуры. Когда окно создаётся (внутри функции CreateWindowEx из Win32 API), ему посылается одно или несколько сообщений (WM_CREATE, WM_NCCREATE и т. д.). Как только первое из них достигает StartWindowProc, она выполняет необходимые операции и передаёт все полномочия функции WindowProc. Кроме того, она записывает в поле m_hWnd реальное значение хэндла окна, чтобы ваши обработчики сообщений WM_CREATE, WM_NCCREATE и т. д. могли использовать это поле до фактического возврата из функции ::CreateWindowEx.
Но откуда StartWindowProc узнаёт адрес объекта, связанного с окном? Эта информация записывается в объект _Module непосредственно перед вызовом CreateWindowEx:
template <class TBase, class TWinTraits> HWND CWindowImplBaseT< TBase, TWinTraits >::Create(HWND hWndParent, RECT& rcPos, LPCTSTR szWindowName, DWORD dwStyle, DWORD dwExStyle, UINT nID, ATOM atom, LPVOID lpCreateParam) { ... _Module.AddCreateWndData(&m_thunk.cd, this); ... HWND hWnd = ::CreateWindowEx(dwExStyle, (LPCTSTR)MAKELONG(atom, 0), szWindowName, dwStyle, rcPos.left, rcPos.top, rcPos.right - rcPos.left, rcPos.bottom - rcPos.top, hWndParent, (HMENU)nID, _Module.GetModuleInstance(), lpCreateParam); ATLASSERT(m_hWnd == hWnd); return hWnd; } |
Для каждого потока создаётся отдельная запись, поэтому неприятностей с многопоточной программой не возникает. Получив управление от операционной системы, StartWindowProc извлекает нужную информацию из объекта _Module:
template <class TBase, class TWinTraits> LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::StartWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)_Module.ExtractCreateWndData(); ... } |
Кстати, только что рассмотренные фрагменты кода отвечают на вопрос, зачем нужно объявлять объект CAppModule _Module до включения файла atlwin.h: как мы только что видели, описанные в нём функции активно используют этот объект в процессе своей работы.
В MFC механизм подмены оригинальной оконной процедуры на AfxWndProc происходит похожим образом. Перед вызовом функции ::CreateWindowEx информация об объекте класса CWnd записывается в структуру состояния потока. Разница в том, что в MFC эту информацию извлекает и использует локальный хук типа WH_CBT, а не временная оконная процедура. |
В первой части статьи мы изучили основные механизмы, лежащие в основе поддержки оконного интерфейса библиотекой WTL. Тем самым мы заложили прочный фундамент для дальнейших исследований, и теперь сможем двигаться вперёд гораздо быстрее. В следующей части мы изучим использование диалогов и окон-рамок, меню, панелей инструментов и строк состояния в программах, написанных с использованием библиотеки WTL.
Сообщений 13 Оценка 632 [+1/-0] Оценить |