В основе механизма owner draw лежат сообщения WM_DRAWITEM, WM_MEASUREITEM, WM_COMPAREITEM и WM_DELETEITEM. Так, в обработчике WM_DRAWITEM выполняется собственно отрисовка контрола, а в обработчике WM_MEASUREITEM – задание размеров отдельных элементов, содержащихся в контроле (пунктов меню, элементов списка и т.п.). WTL содержит небольшой класс COwnerDraw<>, который помогает вам обрабатывать все эти сообщения (описан в файле atlframe.h). Чтобы им воспользоваться, включите его в список базовых классов окна, которое будет заниматься отрисовкой контролов.
Посмотрим, какие элементы входят в класс COwnerDraw<>. В первую очередь это карта сообщений. Точнее, две карты (вы ещё не забыли, что в WTL окно может иметь несколько карт сообщений?).
BEGIN_MSG_MAP(COwnerDraw<T>)
MESSAGE_HANDLER(WM_DRAWITEM, OnDrawItem)
MESSAGE_HANDLER(WM_MEASUREITEM, OnMeasureItem)
MESSAGE_HANDLER(WM_COMPAREITEM, OnCompareItem)
MESSAGE_HANDLER(WM_DELETEITEM, OnDeleteItem)
ALT_MSG_MAP(1)
MESSAGE_HANDLER(OCM_DRAWITEM, OnDrawItem)
MESSAGE_HANDLER(OCM_MEASUREITEM, OnMeasureItem)
MESSAGE_HANDLER(OCM_COMPAREITEM, OnCompareItem)
MESSAGE_HANDLER(OCM_DELETEITEM, OnDeleteItem)
END_MSG_MAP()
По умолчанию используется карта с номером 0. Она обрабатывает сообщения в родительском окне. Карту с номером 1 можно использовать для перехвата отражённых сообщений, связанных с механизмом owner draw, в самом контроле.
Обработчики сообщений реализованы примерно одинаково. Они распаковывают параметры сообщений и передают управление специальным функциям, которые и выполняют основную работу. Вот прототипы этих функций.
void DrawItem(LPDRAWITEMSTRUCT);
void MeasureItem(LPMEASUREITEMSTRUCT);
int CompareItem(LPCOMPAREITEMSTRUCT);
void DeleteItem(LPDELETEITEMSTRUCT);
Именно эти функции вы можете переопределить в производном классе, чтобы реализовать отрисовку контрола. Это удобнее, чем вручную перехватывать сообщения и вспоминать, каким образом в их параметрах запакована информация. Обратите внимание, что класс COwnerDraw<> содержит стандартную реализацию этих функций. Функции DrawItem, CompareItem и DeleteItem ничего полезного не делают, зато функция MeasureItem возвращает размер пункта меню в зависимости от настроек системы и размер элемента в списке в зависимости от размера стандартного системного фонта, который используется в диалогах и меню. Если такое поведение вас не устраивает, измените его на любое другое.
Рассмотрим пример использования класса COwnerDraw<> для рисования нестандартной кнопки.
class CButtonDemoDlg : public CSimpleDialog<IDD_BUTTON_DIALOG>, public COwnerDraw<CButtonDemoDlg>, ... {
private:
HICON m_hIcon1, m_hIcon2;
...
public:
BEGIN_MSG_MAP(CButtonDemoDlg)
...
CHAIN_MSG_MAP(COwnerDraw<CButtonDemoDlg>)
END_MSG_MAP()
void DrawItem(LPDRAWITEMSTRUCT pDIS) {
if ((pDIS->itemState & ODS_SELECTED) != 0) {
// Кнопка нажата
DrawIcon(pDIS->hDC, 0, 0, m_hIcon2);
} else {
// Кнопка отпущена
DrawIcon(pDIS->hDC, 0, 0, m_hIcon1);
}
}
};
Класс CCustomDraw<>: пользовательское рисование в стиле WTLМеханизм пользовательского рисования (custom draw) иногда путают с owner draw. Он предназначен для той же цели – изменить внешний вид контролов. Однако он появился несколько позже (вместе с набором общих контролов из библиотеки comctl32.dll) и используется для более новых контролов (таких, как ListView и TreeView).
Пользовательское рисование работает следующим образом. Когда контрол перерисовывается, он посылает родительскому окну одно или несколько уведомлений NM_CUSTOMDRAW, упакованных в сообщение WM_NOTIFY. Каждое уведомление соответствует некоторой фазе перерисовки (до/после рисования контрола целиком или отдельного элемента и т. д.). Фазу можно определить по полю dwDrawStage структуры NMCUSTOMDRAW, указатель на которую передаётся вместе с уведомлением. В зависимости от фазы родительское окно может выполнить некоторые действия (например, изменить цвет или фонт отдельного элемента списка). Подробности можно найти в MSDN (см. статью "Customizing a Control's Appearance Using Custom Draw").
В WTL есть класс CCustomDraw<> (описан в файле atlctls.h), который помогает вам перехватывать уведомление NM_CUSTOMDRAW и распаковывать его параметры. Он очень похож на класс COwnerDraw<>, который мы рассмотрели выше. Его реализация выглядит так.
template <class T> class CCustomDraw {
public:
// Message map and handlers
BEGIN_MSG_MAP(CCustomDraw<T>)
NOTIFY_CODE_HANDLER(NM_CUSTOMDRAW, OnCustomDraw)
ALT_MSG_MAP(1)
REFLECTED_NOTIFY_CODE_HANDLER(NM_CUSTOMDRAW, OnCustomDraw)
END_MSG_MAP()
// message handler
LRESULT OnCustomDraw(int idCtrl, LPNMHDR pnmh, BOOL& bHandled) {
T* pT = static_cast<T*>(this);
pT->SetMsgHandled(TRUE);
LPNMCUSTOMDRAW lpNMCustomDraw = (LPNMCUSTOMDRAW)pnmh;
DWORD dwRet = 0;
switch(lpNMCustomDraw->dwDrawStage) {
case CDDS_PREPAINT:
dwRet = pT->OnPrePaint(idCtrl, lpNMCustomDraw);
break;
case CDDS_POSTPAINT:
dwRet = pT->OnPostPaint(idCtrl, lpNMCustomDraw);
break;
// Остальные фазы отрисовки
// ...
default:
pT->SetMsgHandled(FALSE);
break;
}
bHandled = pT->IsMsgHandled();
return dwRet;
}
// Overrideables
DWORD OnPrePaint(int /*idCtrl*/, LPNMCUSTOMDRAW /*lpNMCustomDraw*/) {
return CDRF_DODEFAULT;
}
DWORD OnPostPaint(int /*idCtrl*/, LPNMCUSTOMDRAW /*lpNMCustomDraw*/) {
return CDRF_DODEFAULT;
}
// Остальные функции.
// ...
Как видим, в классе CCustomDraw<> также предусмотрено две карты сообщений – для родительского окна и для самого контрола, если он получает отражённые уведомления. Обработчик OnCustomDraw распаковывает параметры уведомления NM_CUSTOMDRAW и определяет фазу рисования. Каждой фазе соответствует своя функция, которая и вызывается из OnCustomDraw. Вы можете переопределить любую из этих функций в производном классе и включить в неё нужный вам код (реализации из класса CCustomDraw<> не выполняют никой полезной работы). Список фаз рисования и соответствующих им функций приведён в таблице 10.
Фаза Прототип функции CDDS_PREPAINTD WORD OnPrePaint(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw) CDDS_POSTPAINTD WORD OnPostPaint(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw) CDDS_PREERASAED WORD OnPreErase(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw) CDDS_POSTERASED WORD OnPostErase(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw) CDDS_ITEMPREPAINTD WORD OnItemPrePaint(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw) CDDS_ITEMPOSTPAINTD WORD OnItemPostPaint(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw) CDDS_ITEMPREERASED WORD OnItemPreErase(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw) CDDS_ITEMPOSTERASE DWORD OnItemPostErase(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw)
Вот небольшой пример использования класса CCustomDraw<>. Для разнообразия я поручил обработку сообщения NM_CUSTOMDRAW самому контролу. Подразумевается, что родительское окно переправляет ему уведомления, используя механизм отражения.
class CCustomDrawListView : public CWindowImpl<CCustomDrawListView, CListViewCtrl>, public CCustomDraw<CCustomDrawListView> {
public:
BEGIN_MSG_MAP(CCustomDrawListView)
// Направляем сообщения в карту №1 класса CCustomDraw!
CHAIN_MSG_MAP_ALT(CCustomDraw<CCustomDrawListView>, 1)
END_MSG_MAP()
DWORD OnPrePaint(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw) {
// Запрашиваем уведомления NM_CUSTOMDRAW для каждого элемента списка.
return CDRF_NOTIFYITEMDRAW;
}
DWORD OnItemPrePaint(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw) {
// Нам нужны поля, специфичные для ListView.
LPNMLVCUSTOMDRAW pLVCD = (LPNMLVCUSTOMDRAW)lpNMCustomDraw;
if ((lpNMCustomDraw->dwItemSpec & 0x01) != 0) {
// Для нечётных элементов: рисуем белым по чёрному.
pLVCD->clrText = RGB(255,255,255);
pLVCD->clrTextBk = RGB(0,0,0);
} else {
// Для чётных элементов: рисуем красным по серому.
pLVCD->clrText = RGB(255,0,0);
pLVCD->clrTextBk = RGB(200,200,200);
}
return CDRF_NEWFONT;
}
};
От теории к практике
Мы изучили уже целую кучу новых классов, и теперь самое время посмотреть, как они применяются на практике. В этом разделе мы изучим целый ряд демонстрационных программ, иллюстрирующих различные аспекты программирования диалогов и контролов с использованием библиотеки WTL.
WTLErrLook: приложение на базе модального диалогаДемонстрационный проект WTLErrLook
WTLErrLook