Использование WTL

Часть 1

Автор: Александр Шаргин
Опубликовано: 21.04.2001
Версия текста: 0.9
Введение
Что такое WTL?
WTL и Win32
WTL и ATL
WTL и MFC
Будущее WTL
Для кого предназначена эта статья
Установка WTL
Копирование файлов
Настройка путей
Обзор WTL
Hello, WTL!
Классы WTL для работы с окнами
Класс CWindow
Класс CMessageMap
Класс CWinTraits<>
Класс CWindowImplRoot<>
Класс CWindowImplBaseT<>
Класс CWindowImpl
Модули и циклы сообщений
Класс CMessageLoop
Класс CAppModule
Маршрутизация сообщений в WTL
Предварительная обработка
Карты сообщений
Переходники и процесс создания окна
Продолжение следует
        
          
"If you only knew the power of the Dark Side..."
Lord Vader

Введение

Что такое WTL?

WTL (Windows Template Library) - это ещё одна библиотека классов от Микрософт, которую вы можете использовать при создании ваших программ. "Ещё одна", потому что на сегодняшний день существуют различные библиотеки классов (MFC и ATL фирмы Microsoft, OWL и VCL фирмы Borland и т. д.). WTL изначально создавалась как пример расширения ATL, и предназначалась для упрощения процесса создания графического интерфейса в программах, использующих эту библиотеку. Как и ATL, WTL базируется на шаблонах языка C++ и позволяет создавать очень быстрые компактные программы.

WTL стала чем-то вроде "побочного продукта" деятельности Микрософт, и до сих пор для неё не нашлось места в официальных планах компании. Поддержка этого продукта и официальная документация на него отсутствуют. Тем не менее, WTL продолжает развиваться, и сейчас разработчикам доступна уже версия 3.1 этой библиотеки. Всё больше программистов использует её в своих приложениях. Давайте рассмотрим, как WTL соотносится с другими технологиями фирмы Микрософт и какие преимущества может дать её использование.

WTL и Win32

WTL является "надстройкой" над оконной подсистемой Win32 API и существенно упрощает работу с функциями этой подсистемы, а также облегчает задачу написания повторно используемого кода. В WTL поддерживает работу с окнами и диалогами, окнами-рамками, полным набором стандартных элементов управления, стандартными диалогами, GDI и другими неотъемлемыми элементами пользовательского интерфейса. Изучить WTL можно сравнительно быстро, после чего она позволит вам существенно ускорить разработку приложений, которые раньше писались на "чистом" API.

WTL и ATL

ATL и WTL очень тесно интегрированы между собой. WTL использует базовый механизм поддержки работы с окнами, предоставляемый ATL, для реализации более "продвинутых" возможностей. Фактически, многие файлы WTL (такие как atlbase.h и atlwin.h) даже не входят в комплект поставки, так как распространяются вместе с Visual C++ в составе библиотеки ATL. Если вы уже программируете на ATL, WTL несомненно придётся вам по вкусу.

WTL и MFC

Самую интересную и животрепещущую тему я приберёг под конец. Вопрос, что же лучше, 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

Будущее WTL предсказать достаточно сложно. Вероятно, эта библиотека появилась слишком поздно. Сейчас всё более актуальной становится проблема интеграции самых различных платформ, операционных систем, языков программирования и т. д. Поэтому всё внимание Микрософт сосредоточено на разработке .NET, "платформы будущего", которая, по мнению её создателей, должна дать ответ на многие вопросы. В этих планах фирмы Микрософт для библиотеки WTL скорее всего не найдётся места. Она будет и дальше развиваться усилиями энтузиастов, создавших её; программисты будут использовать её там, где использование MFC менее удобно. Но она не станет официальным средством разработки, которое придёт на смену MFC. Что будет с ней дальше, покажет время.

Для кого предназначена эта статья

Эту статью могут читать все, кого интересует библиотека WTL. При её написании я предполагал, что вы уже достаточно хорошо знаете язык C++ и платформу Win32. Если эти темы вам не знакомы, вам скорее всего не следует браться за WTL, так как изучение этой библиотеки часто сводится к чтению её исходных текстов.

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

Обзор 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 Класс меню.
Таблица 1. Заголовочные файлы WTL

Ещё раз подчеркну, что некоторые важные файлы, без которых WTL не может работать в принципе, распространяются в составе библиотеки ATL и находятся в каталоге %Visual Studio%\Vc98\Atl. В первую очередь к таким файлам относятся atlbase.h и atlwin.h.

Hello, WTL!

В книгах по программированию для 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 для работы с окнами

Как видно из нашего примера, для работы с окнами в WTL предназначен класс CWindowImpl<>. Однако, под этим классом лежит целая иерархия классов, каждый из которых добавляет в CWindowImpl<> определённую часть функциональности. Классы, входящие в эту иерархию, показаны на рисунке 2. Рассмотрим их по порядку.


Рисунок 2. Иерархия оконных классов библиотеки WTL

Класс CWindow

Класс 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

Класс 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<>

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<>

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<>

Класс 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

Ну, вот мы и добрались до класса 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

Класс 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

Класс 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 используется в процессе создания окна.

Маршрутизация сообщений в WTL

Маршрутизация сообщений - важная тема. Без понимания механизмов обработки сообщений можно легко зайти в тупик, когда программа начинает вести себя не так, как предполагалось. Давайте попытаемся проследить путь сообщений, которые операционная система передаёт нашему приложению.

Предварительная обработка

Для обработки сообщений в 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.


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