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

Часть 2. Диалоги и контролы

Автор: Александр Шаргин
Опубликовано: 03.11.2001
Версия текста: 1.0
Диалоги
Классы WTL для работы с диалогами
Класс CDialogImplBaseT<>
Класс CDialogImpl<>
Класс CAxDialogImpl<>
Класс CSimpleDialog<>
Класс CWinDataExchange<>: механизм DDX в стиле WTL
Класс CUpdateUI<>: обновление дочерних окон в стиле WTL
Класс CDialogResize<>: масштабирование диалогов в стиле WTL
Контролы
Поддержка cтандартных и общих контролов Windows
"Самодельные" контролы
Класс COwnerDraw<>: отрисовка контрола родительским окном в стиле WTL
Класс CCustomDraw<>: пользовательское рисование в стиле WTL
От теории к практике
WTLErrLook: приложение на базе модального диалога
WTLSndVol: приложение на базе немодального диалога
WTLNavigator: использование диалогов с ActiveX-контролами
WTLCalc: обновление дочерних окон
WTLSizeDlg: пример масштабируемого диалога
WTLCtlDemo: использование стандартных и общих контролов
WTLCtlxDemo: использование "самодельных" контролов WTL

Диалоги

Диалоговые окна широко используются в Windows-приложениях, начиная с момента выхода самой операционной системы Windows. Они очень удобны для организации диалога с пользователем (отсюда их название). Кроме того, в несложных приложениях часто удаётся построить на базе диалогов не только вспомогательные окна, но и главное окно приложения (такие приложения иногда называют "dialog-based"). В этом разделе мы рассмотрим классы WTL, предназначенные для работы с диалоговыми окнами.

Классы WTL для работы с диалогами

Классы WTL, относящиеся к диалоговым окнам, показаны на рисунке 1.


Рисунок 1. Диалоговые классы WTL

Обратите внимание, что все диалоговые классы порождаются от базового класса CWindowImplRoot<>, а не от класса CWindowImpl<>. Это сделано потому, что диалоги, в отличие от всех остальных окон, не используют оконную процедуру для обработки сообщений. Вместо этого используется диалоговая процедура, адрес которой задаётся при создании диалога. WTL предоставляет вам свою реализацию диалоговой процедуры в классе CDialogImplBaseT<>. Соответственно, все остальные классы диалогов WTL наследуют эту реализацию.

ПРИМЕЧАНИЕ
Все классы, показанные на рисунке 1, WTL унаследовала от библиотеки ATL. Они описаны в файле atlwin.h

Теперь изучим каждый класс более подробно.

Класс CDialogImplBaseT<>

Итак, класс CDialogImplBaseT<> содержит функциональность, необходимую всем без исключения диалоговым окнам. Это, в первую очередь, поддержка диалоговых процедур, а также пара вспомогательных функций. Обратите внимание, что в класс CDialogImplBaseT<> не встроен механизм создания диалога при помощи функций DialogBox и CreateDialog. Дело в том, что не все диалоги нуждаются в этих функциях. Например, стандартные диалоги создаются при помощи специальных функций (GetOpenFileName, ChooseColor и т. д.).

Диалоговые процедуры в классе CDialogImplBaseT<> реализованы более или менее аналогично оконным процедурам в классе CWindowImplBaseT<>.

template <class TBase>
LRESULT CALLBACK
CDialogImplBaseT< TBase >::StartDialogProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    CDialogImplBaseT< TBase >* pThis = (CDialogImplBaseT< TBase >*)_Module.ExtractCreateWndData();
    ATLASSERT(pThis != NULL);

    pThis->m_hWnd = hWnd;
    pThis->m_thunk.Init(pThis->GetDialogProc(), pThis);

    WNDPROC pProc = (WNDPROC)&(pThis->m_thunk.thunk);
    WNDPROC pOldProc = (WNDPROC)::SetWindowLong(hWnd, DWL_DLGPROC, (LONG)pProc);

#ifdef _DEBUG
    // check if somebody has subclassed us already since we discard it
    if(pOldProc != StartDialogProc)
        ATLTRACE2(atlTraceWindowing, 0, _T("Subclassing through a hook discarded.\n"));
#else
    pOldProc;    // avoid unused warning
#endif

    return pProc(hWnd, uMsg, wParam, lParam);
}
template <class TBase>
LRESULT CALLBACK
CDialogImplBaseT< TBase >::DialogProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    CDialogImplBaseT< TBase >* pThis = (CDialogImplBaseT< TBase >*)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;

    // set result if message was handled
    if(bRet)
    {
        switch (uMsg)
        {
        case WM_COMPAREITEM:
        case WM_VKEYTOITEM:
        case WM_CHARTOITEM:
        case WM_INITDIALOG:
        case WM_QUERYDRAGICON:
        case WM_CTLCOLORMSGBOX:
        case WM_CTLCOLOREDIT:
        case WM_CTLCOLORLISTBOX:
        case WM_CTLCOLORBTN:
        case WM_CTLCOLORDLG:
        case WM_CTLCOLORSCROLLBAR:
        case WM_CTLCOLORSTATIC:
            return lRes;
            break;
        }
        ::SetWindowLong(pThis->m_hWnd, DWL_MSGRESULT, lRes);
        return TRUE;
    }

    if(uMsg == WM_NCDESTROY)
    {
        // clear out window handle
        HWND hWnd = pThis->m_hWnd;
        pThis->m_hWnd = NULL;
        // clean up after dialog is destroyed
        pThis->OnFinalMessage(hWnd);
    }

    return FALSE;
}

Статическая функция StartDialogProc назначается диалогу при его создании. Для этого её адрес передаётся функциям, подобным DialogBox и CreateDialog, или задаётся в качестве хука для стандартных диалогов. Получив управление, эта функция извлекает хэндл диалога из объекта _Module и сохраняет его в переменной m_hWnd, затем инициализирует переходник и передаёт управление штатной диалоговой процедуре DialogProc, которая и выполняет дальнейшее обслуживание диалога. Каждое полученное сообщение она "пропускает" через карту сообщений вызовом ProcessWindowMessage. Возвращаемое после обработки сообщения значение интерпретируется в зависимости от типа сообщения. Тем самым обеспечивается небольшое, но весьма приятное удобство: программист не должен помнить, каким образом нужно передать операционной системе LRESULT из диалоговой процедуры (напрямую или с помощью SetWindowLong). Достаточно вернуть его из функции-обработчика, а об остальном позаботится WTL.

Поскольку диалоги, как и все остальные окна, используют для обработки сообщений функцию ProcessWindowMessage, вы можете использовать для её создания уже знакомые вам макросы карты сообщений.

После уничтожения диалога WTL вызывает виртуальную функцию OnFinalMessage. Вы можете переопределить её в производном классе и возложить на неё "очистительные" работы. Следует только иметь ввиду, что во время работы этой функции диалог уже не существует, и даже переменная m_hWnd содержит NULL. Поэтому в функции OnFinalMessage нельзя, к примеру, загружать данные из контролов диалога в переменные.

Класс CDialogImpl<>

Класс CDialogImpl<> - основное средство для работы с диалогами в WTL. Он используется как с модальными, так и с немодальными диалогами. Соответственно, в нём содержатся обёртки для функций DialogBoxParam, EndDialog, CreateDialogParam и DestroyWindow. Механизм обработки сообщений наследуется от класса CDialogImplBaseT<>.

Для создания модального диалога используется метод DoModal. Уничтожить модальный диалог можно, используя метод EndDialog (можно вызывать этот метод из любого обработчика сообщений, в том числе из обработчика сообщения WM_INITDIALOG). Реализация обоих методов более чем прямолинейна:

    // modal dialogs
    int DoModal(HWND hWndParent = ::GetActiveWindow(), LPARAM dwInitParam = NULL)
    {
        ATLASSERT(m_hWnd == NULL);
        _Module.AddCreateWndData(&m_thunk.cd, (CDialogImplBaseT< TBase >*)this);

#ifdef _DEBUG
        m_bModal = true;
#endif //_DEBUG
        return

        ::DialogBoxParam(_Module.GetResourceInstance(), MAKEINTRESOURCE(T::IDD),
                    hWndParent, (DLGPROC)T::StartDialogProc, dwInitParam);
    }

    BOOL EndDialog(int nRetCode)
    {
        ATLASSERT(::IsWindow(m_hWnd));
        ATLASSERT(m_bModal);    // must be a modal dialog
        return ::EndDialog(m_hWnd, nRetCode);
    }

Здесь следует обратить внимание всего на две вещи. Во-первых, в качестве диалоговой процедуры задаётся StartDialogProc. Благодаря этому к создаваемому диалогу подключается механизм обработки сообщений, рассмотренный в предыдущем разделе. Во-вторых, в качестве идентификатора ресурса диалога используется константа IDD. Вам необходимо определить её в производном классе, чтобы WTL знала, какой диалог требуется создать. В принципе, можно сделать IDD и статической переменной производного класса, но прибегать к этому приёму на практике приходится не часто.

Библиотека MFC не использует функцию DialogBox(Param). Вместо этого она создаёт немодальный диалог, а затем эмулирует поведение модального. Благодаря этому программировать модальные диалоги в MFC гораздо удобнее, чем на "чистом" Win32 API (а значит, и в WTL). Проблема в том, что функция DialogBox(Param) создаёт свой собственный цикл сообщений, до которого не так-то просто добраться. Если нам потребуется, к примеру, внедрить в него трансляцию акселераторов, придётся прибегать к различным неочевидным приёмам.

Немодальный диалог создаётся с использованием функции Create и разрушается вызовом DestroyWindow. Реализация обоих методов также достаточно очевидна.

    // modeless dialogs
    HWND Create(HWND hWndParent, LPARAM dwInitParam = NULL)
    {
        ATLASSERT(m_hWnd == NULL);
        _Module.AddCreateWndData(&m_thunk.cd, (CDialogImplBaseT< TBase >*)this);

#ifdef _DEBUG
        m_bModal = false;
#endif //_DEBUG

        HWND hWnd = ::CreateDialogParam(_Module.GetResourceInstance(), MAKEINTRESOURCE(T::IDD),
                    hWndParent, (DLGPROC)T::StartDialogProc, dwInitParam);
        ATLASSERT(m_hWnd == hWnd);
        return hWnd;
    }

    BOOL DestroyWindow()
    {
        ATLASSERT(::IsWindow(m_hWnd));
        ATLASSERT(!m_bModal);    // must not be a modal dialog
        return ::DestroyWindow(m_hWnd);
    }

С учётом всего сказанного, типичный класс диалога, порождённый от CDialogImpl<>, выглядит так (в качестве параметра шаблона задаётся имя класса, который вы порождаете).

class CMyDialog : public CDialogImpl<CMyDialog>
{
public:
    enum { IDD = IDD_MY_DIALOG };

    BEGIN_MSG_MAP(CMyDialog)
        // Карта сообщений
    END_MSG_MAP()
};

Обратите внимание, что константа IDD описывается в секции public. Если описать её в private-секции, функция базового класса CDialogImpl<>::DoModal не сможет к ней обратиться, что приведёт к ошибке.

Далее полученный класс можно использовать для создания как модальных, так и немодальных диалогов, например:

// Создаём модальный диалог
CMyDialog modal;
modal.DoModal();

// Создаём немодальный диалог
CMyDialog modeless;
modeless.Create(HWND_DESKTOP);

Класс CAxDialogImpl<>

Класс CAxDialogImpl<> очень похож на предыдущий. Вся разница в том, что вместо функции DialogBoxParam он использует функцию AtlAxDialogBox, а вместо функции CreateDialogParam - функцию AtlAxCreateDialog:

    // modal dialogs
    int DoModal(HWND hWndParent = ::GetActiveWindow(), LPARAM dwInitParam = NULL)
    {
        ATLASSERT(m_hWnd == NULL);
        _Module.AddCreateWndData(&m_thunk.cd, (CDialogImplBaseT< TBase >*)this);

#ifdef _DEBUG
        m_bModal = true;
#endif //_DEBUG

        return AtlAxDialogBox(_Module.GetResourceInstance(), MAKEINTRESOURCE(T::IDD),
                    hWndParent, (DLGPROC)T::StartDialogProc, dwInitParam);
    }

    ...

    // modeless dialogs
    HWND Create(HWND hWndParent, LPARAM dwInitParam = NULL)
    {
        ATLASSERT(m_hWnd == NULL);
        _Module.AddCreateWndData(&m_thunk.cd, (CDialogImplBaseT< TBase >*)this);

#ifdef _DEBUG
        m_bModal = false;
#endif //_DEBUG

        HWND hWnd = AtlAxCreateDialog(_Module.GetResourceInstance(), MAKEINTRESOURCE(T::IDD),
                    hWndParent, (DLGPROC)T::StartDialogProc, dwInitParam);
        ATLASSERT(m_hWnd == hWnd);
        return hWnd;
    }

Эти функции, в отличие от своих аналогов из Win32 API, могут создавать диалоги, содержащие ActiveX-контролы. Мы не будем рассматривать их реализацию, поскольку тема использования ActiveX-контролов выходит за рамки данной статьи.

Класс CSimpleDialog<>

Чтобы создавать диалоги на базе класса CDialogImpl<>, необходимо каждый раз порождать от него собственные классы. Это довольно утомительно. Класс CSimpleDialog<> предназначен для отображения простейших модальных диалогов, содержащих только статическую информацию и стандартные кнопки, такие как "OK" и "Отмена". Кроме функции DoModal, которая реализована почти так же, как в классе CDialogImpl<>, этот класс предоставляет собственную карту сообщений и обработчики OnInitDialog и OnCloseCmd. Последний вызывается в ответ на нажатие любой кнопки со стандартным идентификатором (IDOK, IDCANCEL, IDABORT, IDRETRY, IDIGNORE, IDYES или IDNO) и закрывает диалог.

Обратите внимание, что идентификатор ресурса диалога в классе CSimpleDialog<> задаётся не как константа, а как первый параметр шаблона. Благодаря этому класс можно использовать, не порождая от него собственных классов. Если, к примеру, вы нарисовали в редакторе диалоговое окно About и назначили ему идентификатор IDD_ABOUT, отобразить его можно, используя класс CSimpleDialog<> напрямую:

CSimpleDialog<IDD_ABOUT> dlg;
dlg.DoModal();

Ещё раз подчеркну, что класс CSimpleDialog<> не содержит реализации метода Create, а поэтому не позволяет создавать немодальные диалоги. Методы EndDialog и DestroyWindow также отсутствуют.

Класс CWinDataExchange<>: механизм DDX в стиле WTL

Механизм динамического обмена данными (DDX - Dynamic Data eXchange) используется для обмена данными между контролами и переменными вашей программы. Термин DDX был введён в MFC, хотя сам механизм под разными названиями существует и в других библиотеках. В WTL он также присутствует. Его реализация содержится в классе CWinDataExchange<>.

Прежде чем рассказывать про класс CWinDataExchange<>, скажу несколько слов об общих принципах реализации дополнительной функциональности в WTL.

Обычно дополнительные возможности WTL реализуются в отдельных классах. Чтобы получить доступ к этим возможностям, необходимо произвести свой класс от всех классов WTL, содержащих нужную нам функциональность. Далее каждый из базовых классов конфигурируется с помощью соответствующей карты (map), которая составляется из специально предусмотренных для этой цели макросов. Обычно карта начинается макросом BEGIN_XXX_MAP и заканчивается макросом END_XXX_MAP (XXX обозначает некоторый идентификатор, разъясняющий назначение карты). Между ними располагаются все остальные макросы карты.

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

Настроив нужные нам механизмы WTL, мы можем использовать их, вызывая или переопределяя предусмотренные для этой цели методы.

Вернёмся к механизму DDX. Чтобы использовать его, включите в список базовых классов вашего диалога (или другого окна, содержащего контролы) класс CWinDataExchange<> (описан в файле atlddx.h). В качестве параметра шаблона задаётся имя вашего производного класса. Например:

class CMyDialog : public CDialogImpl<CMyDialog>, public CWinDataExchange<CMyDialog>
{
    ...
};

Следующий шаг - включить в public-секцию вашего класса карту DDX. Каждая строчка в этой карте связывает идентификатор контрола с некоторой переменной в вашей программе. Обычно это переменная-член класса, но она может быть и глобальной/статической. В обмене могут участвовать числовые или текстовые данные с ограничениями или без них. Список макросов, из которых строится карта DDX, приведён в таблице 1.

Макрос Описание
BEGIN_DDX_MAP(thisClass)

Начало карты DDX. thisClass - имя класса, в котором содержится карта.

DDX_TEXT(nID, var)

Связывает строковую переменную var с контролом nID (здесь и далее nID - это идентификатор контрола). Переменная var может иметь тип TCHAR*, BSTR, CComBSTR или CString. Обмен данными осуществляется при помощи функций SetWindowText и GetWindowText. Чаще всего макрос используется для статических контролов и полей ввода, хотя может применяться и с другими окнами.

DDX_TEXT_LEN(nID, var, len)

Аналогичен предыдущему, но длина строки ограничивается значением len. Попытка передать строку, длина которой превышает len, приведёт к ошибке валидации (об ошибках немного позже).

DDX_INT(nID, var)

Связывает целочисленную переменную var с контролом nID.

DDX_INT_RANGE(nID, var, min, max)

Аналогичен предыдущему, но передаваемое значение должно лежать в диапазоне от min до max. Невыполнение этого условия приведёт к ошибке валидации.

DDX_UINT(nID, var)

Связывает целочисленную беззнаковую переменную var с контролом nID.

DDX_UINT_RANGE(nID, var, min, max)

Аналогичен предыдущему, но передаваемое значение должно лежать в диапазоне от min до max. Невыполнение этого условия приведёт к ошибке валидации.

DDX_FLOAT(nID, var)

Связывает переменную с плавающей точкой var с контролом nID. var может иметь тип float или double. Макрос DDX_FLOAT будет доступен, только если вы определите макрос _ATL_USE_DDX_FLOAT перед включением заголовочного файла atlddx.h.

DDX_FLOAT_RANGE(nID, var, min, max)

Аналогичен предыдущему, но передаваемое значение должно лежать в диапазоне от min до max. Невыполнение этого условия приведёт к ошибке валидации.

DDX_CONTROL(nID, obj)

Связывает объект obj с контролом nID. Для связывания используется метод obj.SubclassWindow, поэтому объект должен принадлежать классу CWindowImplBaseT<> или производному от него.

DDX_CHECK(nID, var)

Привязывает переменную var типа int к флагу checked кнопки nID. Для обмена данными используются сообщения BM_SETCHECK и BM_GETCHECK.

DDX_RADIO(nID, var)

Связывает переменную var типа int с группой переключателей. Контрол nID должен быть первым в группе.

END_DDX_MAP()

Этот макрос завершает карту DDX. Не имеет параметров.

Таблица 1. Макросы карты DDX

Рассмотрим пример карты DDX для диалога, который позволяет вводить имя, адрес и номер телефона.

    BEGIN_DDX_MAP(CMyDialog)
        DDX_TEXT_LEN(IDC_NAME, m_name, 20)
        DDX_TEXT(IDC_ADDRESS, m_address)
        DDX_UINT_RANGE(IDC_PHONE, m_phone, 0, 9999999)
    END_DDX_MAP()
Если вы использовали MFC, вы, вероятно, заметили сходство карты DDX с виртуальной функцией CWnd::DoDataExchange. Они действительно похожи. Одно отличие состоит в том, что макросы WTL не используют вспомогательную структуру, аналогичную CDataExchange из MFC. Это несколько упрощает их использование. Второе отличие - в WTL собственно обмен и валидация объединены вместе, а в MFC им соответствуют различные функции (DDX_* для обмена данными и DDV_* для валидации).

Вот и всё. Никакой дополнительной инициализации механизм DDX в WTL не требует. Чтобы выполнить обмен данными, используйте функцию DoDataExchange. Вот её прототип:

BOOL DoDataExchange(BOOL bSaveAndValidate = FALSE, UINT nCtlID = (UINT)-1)

Параметр bSaveAndValidate задаёт направление обмена (FALSE или DDX_LOAD соответствует записи значений из переменных в контролы, а TRUE или DDX_SAVE - из контролов в переменные). Второй параметр задаёт идентификатор контрола, с которым необходимо произвести обмен. Значение по умолчанию (-1) соответствует всем контролам, упомянутым в карте DDX. Функция DoDataExchange возвращает TRUE, если обмен данными был успешным, или FALSE в противном случае.

В MFC обмен данными осуществляет функция CWnd::UpdateData, похожая на DoDataExchange из WTL. Отличие в том, что функция UpdateData не позволяет задавать идентификатор контрола. Вместо этого она всегда воздействует на все контролы, прописанные в функции CWnd::DoDataExchange. Реализация в WTL несколько гибче, но было бы ещё лучше, если бы разработчики WTL предусмотрели разбиение карты DDX на подкарты (как это сделано для карт сообщений). Часто в реальной программе требуется выполнить обмен данными не с одним контролом и не со всеми контролами, а с некоторым их подмножеством.

Иногда в процессе обмена данными возникают ошибки. Их делят на две разновидности: ошибки обмена (data exchange errors) и ошибки валидации (data validation errors). Ошибки обмена возникают, когда контрол не содержит значения, соответствующего типу связанной с ним переменной (например, поле ввода, связанное с переменной типа int, содержит пробелы или другие нецифровые символы). Ошибки валидации фиксируются в случае несоответствия передаваемого значения и наложенных на него ограничений (максимальная длина строки, минимальное и максимальное значение числа). В случае возникновения ошибки обмена вызывается виртуальная функция OnDataExchangeError, а при возникновении ошибки валидации - виртуальная функция OnDataValidateError. Дальнейший процесс обмена данными прерывается, а DoDataExchange возвращает FALSE, сигнализируя о неуспехе операции.

Класс CWinDataExchange<> предоставляет свои реализации функций OnDataExchangeError и OnDataValidateError. Они обе совершенно одинаковы.

// Overrideables
    void OnDataExchangeError(UINT nCtrlID, BOOL /*bSave*/)
    {
        // Override to display an error message
        ::MessageBeep((UINT)-1);
        T* pT = static_cast<T*>(this);
        ::SetFocus(pT->GetDlgItem(nCtrlID));
    }

    void OnDataValidateError(UINT nCtrlID, BOOL /*bSave*/, _XData& /*data*/)
    {
        // Override to display an error message
        ::MessageBeep((UINT)-1);
        T* pT = static_cast<T*>(this);
        ::SetFocus(pT->GetDlgItem(nCtrlID));
    }

Как видим, эти функции издают звуковой сигнал и устанавливают фокус ввода на контрол, в котором содержится неверное значение. Вы можете изменить это поведение на любое другое. Обратите внимание на структуру _XData, которая передаётся в функцию OnDataValidateError. Она содержит информацию об ограничении, которое было нарушено. Вот как описана эта структура в файле atlddx.h.

// Helpers for validation error reporting
    enum _XDataType
    {
        ddxDataNull = 0,
        ddxDataText = 1,
        ddxDataInt = 2,
        ddxDataFloat = 3,
        ddxDataDouble = 4
    };

    struct _XTextData
    {
        int nLength;
        int nMaxLength;
    };

    struct _XIntData
    {
        long nVal;
        long nMin;
        long nMax;
    };

    struct _XFloatData
    {
        double nVal;
        double nMin;
        double nMax;
    };

    struct _XData
    {
        _XDataType nDataType;
        union
        {
            _XTextData textData;
            _XIntData intData;
            _XFloatData floatData;
        };
    };

Соответственно, в функции OnDataValidateError нужно проанализировать значение поля nDataType и выбрать в зависимости от него структуру textData, intData или floatData, которая и будет содержать информацию о нарушенном ограничении.

MFC не позволяет повлиять на отображение ошибки валидации. Если вы используете функции DDV_*, вы всегда будете получать сообщение об ошибке валидации в виде message box'а. Изменить это поведение нельзя, можно только отказаться от DDV_* и использовать для валидации функции "собственного изготовления".

Как это всё работает

Теперь посмотрим, как механизм DDX выглядит "изнутри". К счастью, в его реализации нет ничего сложного. От класса CWinDataExchange<> ваш класс наследует функции DDX_Text, DDX_Int, DDX_Float, DDX_Control, DDX_Check и DDX_Radio, которые и выполняют собственно обмен данными. Некоторые из них перегружены, а DDX_Int и вовсе оформлена как шаблон, что позволяет работать с самыми разными целыми типами.

После обработки препроцессором карта DDX превращается в функцию DoDataExchange. Макросы BEGIN_DDX_MAP и END_DDX_MAP создают пролог и эпилог этой функции. "Заготовка" карты:

    BEGIN_DDX_MAP(CMyDialog)
        // Другие макросы карты DDX
    END_DDX_MAP()

превращается в:

    BOOL DoDataExchange(BOOL bSaveAndValidate = FALSE, UINT nCtlID = (UINT)-1)
    {
        bSaveAndValidate;
        nCtlID;

        // Другие макросы карты DDX

        return TRUE;
    }

Что касается остальных макросов DDX_*, то все они реализованы примерно одинаково. Сначала они сравнивают свой идентификатор контрола nID с идентификатором nCtlID, который был передан в функцию DoDataExchange. Если идентификаторы равны или nCtlID равен -1, макрос вызывает соответствующую функцию DDX_*. Далее проверяется возвращаемое значение, и если оно равно FALSE, обмен данными прекращается. Рассмотрим для примера макросы DDX_TEXT и DDX_TEXT_LEN. Обратите внимание, что они используют одну и ту же функцию DDX_Text, но передают ей разные параметры.

#define DDX_TEXT(nID, var) \
        if(nCtlID == (UINT)-1 || nCtlID == nID) \
        { \
            if(!DDX_Text(nID, var, sizeof(var), bSaveAndValidate)) \
                return FALSE; \
        }

#define DDX_TEXT_LEN(nID, var, len) \
        if(nCtlID == (UINT)-1 || nCtlID == nID) \
        { \
            if(!DDX_Text(nID, var, sizeof(var), bSaveAndValidate, TRUE, len)) \
                return FALSE; \
        }

Теперь мы знаем, как устроены карты DDX. Это может помочь нам писать их более эффективно. Например, мы можем написать в карте DDX следующее:

    BEGIN_DDX_MAP(CMyDialog)
        ...
        for(int i=0; i<100; i++)
            DDX_INT(IDC_BASE+i, m_numbers[i]);
        ...
    END_DDX_MAP()

Это гораздо удобнее, чем вставлять в карту 100 записей.

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

Если с макросами DDX_INT, DDX_UINT и DDX_FLOAT проблем обычно не возникает, то макрос DDX_TEXT может стать источником неприятностей. Чтобы с ними разобраться, рассмотрим реализацию функции DDX_Text.

    BOOL DDX_Text(UINT nID, LPTSTR lpstrText, int nSize, BOOL bSave, BOOL bValidate = FALSE, int nLength = 0)
    {
        T* pT = static_cast<T*>(this);
        BOOL bSuccess = TRUE;

        if(bSave)
        {
            HWND hWndCtrl = pT->GetDlgItem(nID);
            int nRetLen = ::GetWindowText(hWndCtrl, lpstrText, nSize);
            if(nRetLen < ::GetWindowTextLength(hWndCtrl))
                bSuccess = FALSE;
        }

        ...

        return bSuccess;
    }

Как видим, размер буфера задаётся параметром nSize. Но рассчитывается этот размер по меньшей мере странно:

#define DDX_TEXT(nID, var) \
        if(nCtlID == (UINT)-1 || nCtlID == nID) \
        { \
            if(!DDX_Text(nID, var, sizeof(var), bSaveAndValidate)) \

                return FALSE; \
        }

#define DDX_TEXT_LEN(nID, var, len) \
        if(nCtlID == (UINT)-1 || nCtlID == nID) \
        { \
            if(!DDX_Text(nID, var, sizeof(var), bSaveAndValidate, TRUE, len)) \
                return FALSE; \
        }

Другими словами, за размер буфера принимается размер переменной var, которая связывается с контролом. Отсюда следует два вывода. Во-первых, переменная var может быть только статическим массивом, а динамическим - нет. Во-вторых, в программе, использующей набор символов Unicode, этот размер будет всегда определяться неправильно. Выход в том и в другом случае - отказаться от макроса DDX_TEXT и обратиться к функции DDX_Text напрямую, передав ей правильный размер. Замечу также, что при передаче строки из переменной в контрол размер буфера значения не имеет, так что если вы передаёте данные только в этом направлении, DDX_TEXT использовать можно.

С набором символов Unicode связана ещё одна интересная проблема. Посмотрим на следующую карту DDX:

    LPTSTR m_msg;


    BEGIN_DDX_MAP(CMyDialog)
        ...
        DDX_Text(IDC_MESSAGE, m_msg, ...)
        ...
    END_DDX_MAP()

Если попытаться откомпилировать этот код, задав макрос UNICODE, компилятор выдаст следующую ошибку: 'DDX_Text' : ambiguous call to overloaded function (неоднозначность при обращении к перегруженной функции). Дело в том, что в классе CWinDataExchange<> существует несколько перегруженных версий DDX_Text. Вот две из них:

// Text exchange
    BOOL DDX_Text(UINT nID, LPTSTR lpstrText, int nSize, BOOL bSave, BOOL bValidate = FALSE, int nLength = 0)
    {
        ...
    }

    BOOL DDX_Text(UINT nID, BSTR& bstrText, int /*nSize*/, BOOL bSave, BOOL bValidate = FALSE, int nLength = 0)
    {
        ...
    }

Если макрос UNICODE определён, LPTSTR превращается в wchar_t*, а BSTR& - в wchar_t*&. Получается неоднозначность. Чтобы решить эту проблему, можно переписать карту DDX так:

    BEGIN_DDX_MAP(CMyDialog)
        ...
        DDX_Text(IDC_MESSAGE, (TCHAR * const)m_msg, ...)
        ...
    END_DDX_MAP()

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

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

Макрос DDX_CONTROL связывает контрол с объектом класса, порождённого от CWindowImplBaseT<>. Если вы знакомы с MFC, вы знаете, что там обычной практикой является связывание объекта класса CWnd (или его потомка) с контролом, даже если вам не нужно подключать его к карте сообщений, а просто вызвать несколько обёрток типа CWnd::GetWindowText или CListCtrl::GetItem. Это создаёт значительный, причём совершенно ненужный, перерасход ресурсов. Не используйте макрос DDX_CONTROL из WTL подобным образом. Он используется, если вам действительно необходимо заменить оконную процедуру контрола и обрабатывать его сообщения через карту сообщений.

Если же вам нужно просто использовать функции-обёртки из класса CWindow для работы с контролом, достаточно получить хэндл этого контрола с помощью GetDlgItem, а затем присвоить его объекту класса. Удобно делать это в обработчике WM_INITDIALOG. Например:

class CMyDialog : public CDialogImpl<CMyDialog>,
                  public CWinDataExchange<CMyDialog>,
{
private:
    CWindow m_control;

    ...

public:
    BEGIN_MSG_MAP_EX(CMyDialog)
        MSG_WM_INITDIALOG(OnInitDialog)
        ...
    END_MSG_MAP()

    LRESULT OnInitDialog(HWND, LPARAM)
    {
        m_control = GetDlgItem(IDC_SOME_CONTROL);
        ...
    }

    ...
};

Ниже в этой статье мы увидим, что кроме CWindow в WTL существует целый набор классов для работы с контролами - CStatic, CButton, CEdit и т. д. Их можно использовать так же, как и CWindow в приведённом выше примере.

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

Макрос DDX_RADIO используется для работы сразу с целой группой переключателей. При этом переменная var, связанная с группой, содержит порядковый номер выбранного переключателя в группе (нумерация начинается с нуля). Значение -1 соответствует состоянию группы, в котором ни один из переключателей не выбран.

А что, если нам нужно связать переменную не со всей группой, а с конкретным переключателем из неё? В этом случае нужно просто воспользоваться макросом DDX_CHECK вместо DDX_RADIO.

Класс CUpdateUI<>: обновление дочерних окон в стиле WTL

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

Чтобы подключить механизм обновления дочерних контролов к вашему диалогу, добавьте в список базовых классов класс CUpdateUI<>, который описан в файле atlframe.h. Кроме этого, необходимо написать карту обновления пользовательского интерфейса (далее карта UI). Набор макросов, из которых составляется карта UI, минимален. Их всего 3 штуки. Все они описаны в таблице 2.

Макрос Описание
BEGIN_UPDATE_UI_MAP(thisClass)

Начало карты UI. thisClass - имя класса, в котором содержится карта.

UPDATE_ELEMENT(nID, wType)

Определяет, какие типы элементов пользовательского интерфейса с идентификатором nID должны обновляться. Нужные типы объединяются с помощью операции "ИЛИ" и передаются в качестве второго параметра макроса wType. WTL распознаёт следующие типы: UPDUI_MENUPOPUP (пункт всплывающего меню), UPDUI_MENUBAR (пункт полоски меню), UPDUI_CHILDWINDOW (дочернее окно, контрол), UPDUI_TOOLBAR (кнопка на панели инструментов) и UPDUI_STATUSBAR (панель на строке состояния). В этой статье мы сосредоточимся на контролах, а об остальных элементах поговорим, когда доберёмся до окон-рамок.

END_UPDATE_UI_MAP()

Этот макрос завершает карту UI. Не имеет параметров.

Таблица 2. Макросы карты UI

После того, как карта UI добавлена в класс, остаётся один завершающий штрих. Вы должны зарегистрировать все контейнеры элементов пользовательского интерфейса, которые нужно обновлять. В случае с контролами в качестве контейнера выступает сам диалог. В случае с меню это окно, содержащее меню. И так далее. Для каждого контейнера существует своя функция регистрации: UIAddMenuBar для меню, UIAddToolBar для панелей иснтрументов, UIAddStatusBar для строк состояния и UIAddChildWindowContainer для контейнеров дочерних окон. Все перечисленные функции принимают хэндл окна-контейнера и позвращают BOOL, сигнализирующий об успехе или неуспехе регистрации. В случае с диалогом регистрировать контейнер удобно в обработчике сообщения WM_INITDIALOG.

Итак, инициализация закончена. Теперь мы можем изменять состояния контролов, идентификаторы которых включены в карту UI. Для этого используется ещё один набор функций с префиксом UI. Все они перечислены в таблице 3.

Функция Описание
BOOL UIEnable(int nID, BOOL bEnable, BOOL bForceUpdate = FALSE)

Изменяет состояние доступности элементов с идентификатором nID в соответствии со значением bEnable. Флаг принудительного обновления bForceUpdate задаётся, когда нужно фактически обновить элемент, даже если его текущее состояние соответствует желаемому и менять ничего не надо. Поскольку все функции используют этот флаг одинаково, я больше не буду на нём останавливаться.

BOOL UISetCheck(int nID, int nCheck, BOOL bForceUpdate = FALSE)

Изменяет состояние флага "checked" элементов с идентификатором nID в соответствии со значением nCheck. Из контролов эту функцию имеет смысл применять только к кнопкам, так как другие контролы не имеют такого флага. nCheck может принимать одно из трёх значений: 0, если кнопка "отжата", 1, если нажата и 2, если она находится в "третьем состоянии" (кнопка активна, но отрисовывается серым цветом). Третье состояние имеется только у конпок со стилями BS_3STATE или BS_AUTO3STATE.

BOOL UISetRadio(int nID, BOOL bRadio, BOOL bForceUpdate = FALSE)

Изменяет состояние флага "radio" элементов с идентификатором nID в соответствии со значением nRadio. Для контролов эта функция работает аналогично предыдущей.

BOOL UISetText(int nID, LPCTSTR lpstrText, BOOL bForceUpdate = FALSE)

Изменяет текст элементов с идентификатором nID на заданный в параметре lpstrText.

BOOL UISetState(int nID, DWORD dwState)

Эта функция позволяет изменить сразу несколько флагов, связанных с элементами nID. Эти флаги объединяются операцией "ИЛИ" и передаются в качестве параметра dwState. Можно использовать флаги UPDUI_DISABLED, UPDUI_CHECKED, UPDUI_CHECKED2 (этот флаг соответствует "третьему состоянию" кнопки), UPDUI_RADIO и UPDUI_DEFAULT. Замечу, что флаг UPDUI_DEFAULT можно менять, только используя функцию UISetState. Специальной функции для его изменения нет. Этот флаг позволяет сделать элемент используемым по умолчанию.

Обратите внимание, что функция UISetState не использует флаг bForceUpdate и всегда обновляет элемент, вне зависимости от его текущего состояния.

DWORD UIGetState(int nID)

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

Таблица 3. Функции обновления UI

Функции, которые мы только что рассмотрели, не изменяют фактическое состояние элементов. Они только записывают новые значения во внутренние структуры класса CUpdateUI<>. Чтобы внесённые изменения вступили в силу, нужно вызвать специальную функцию. Для каждого типа контейнеров существует своя функция: UIUpdateMenuBar для меню, UIUpdateToolBar для панели инструментов, UIUpdateStatusBar для строки состояния и UIUpdateChildWindows для контейнера дочерних окон. Каждая из этих функций принимает флаг bForceUpdate. Используйте его, чтобы принудительно обновить все элементы, прописанные в карте UI.

Как это всё работает

Посмотрим, как устроен класс CUpdateUI<>. Карта UI, которую вы создаёте, превращается в массив структур _AtlUpdateUIMap.

    struct _AtlUpdateUIMap
    {
        WORD m_nID;
        WORD m_wType;
    };

Каждая структура содержит в точности те значения, которые вы передаёте макросу UPDATE_ELEMENT в качестве параметров. Массив завершается структурой со значениями {(WORD)-1, 0}. Для обращения к нему используется функция GetUpdateUIMap, внутри которой он описывается как статическая переменная. Этот массив один на все объекты класса, порождённого от CUpdateUI<>. Кроме этого, каждый объект класса наследует от CUpdateUI<> переменные m_UIElements, m_pUIData и m_wDirtyType.

m_UIElements - это массив контейнеров, для редактирования которого и используется семейство функций UIAddXXX. Кстати, странно, что разработчики WTL не предусмотрели средства для удаления контейнеров из этого массива. Но тут уже ничего не поделаешь.

m_pUIData - массив структур _AtlUpdateData. Количество элементов в этом массиве в точности соответствует количеству записей в карте UI. Каждая структура _AtlUpdateData содержит флаги состояния (те самые, которые меняет функция UISetState) и указатель на строку, которые должны быть назначены элементу. Место для строк распределяется динамически. Вот как описана структура _AtlUpdateUIData.

    struct _AtlUpdateUIData
    {
        WORD m_wState;
        void* m_lpData;
    };

Теперь понятно, что делают функции типа UIEnable и UISetCheck. Они просто изменяют поля структуры _AtlUpdateUIData, соответствующей заданному элементу. Что касается семейства функций UIUpdateXXX, то они используют данные из m_pUIData, чтобы обновить элементы управления.

Наконец, переменная m_wDirtyType используется в целях оптимизации. В ней содержатся типы тех элементов, состояние которых было изменено с момента последнего обновления. Когда вы вызываете функцию UIUpdateXXX, WTL проверяет соответствующий флаг в m_wDirtyType и обновляет элементы, только если он установлен. После обновления m_wDirtyType сбрасывается в ноль.

Где обновлять элементы

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

Альтернативный вариант, который, кстати, используется в MFC, заключается в обновлении элементов в фоне, то есть когда очередь сообщений пуста. Если вы используете немодальный диалог, вам будет нетрудно реализовать эту идею и в WTL: для этого достаточно зарегистрировать объект диалога в цикле сообщений как фоновый обработчик, а затем обновлять элементы в функции OnIdle. Однако если диалог модальный, цикл сообщений скрыт внутри функции DialogBoxParam и фоновая обработка в стиле WTL недоступна. В этом случае можно использовать сообщение WM_ENTERIDLE (модальный диалог посылает его родительскому окну, когда очередь сообщений исчерпана) или вообще отказаться от фоновой обработки.

Класс CDialogResize<>: масштабирование диалогов в стиле WTL

Как известно, обычные диалоги не позволяют себя масштабировать. С точки зрения пользователя это довольно неудобно. Часть информации не помещается в маленьких контролах, и их приходится прокручивать, чтобы просмотреть всё целиком. В то же время часть экрана монитора всё равно остаётся незанятой, и диалог вполне мог бы её занять. Возникает вопрос: как реализовать масштабируемые диалоги в вашем приложении?

Обычно эта проблема решается так. Диалогу назначается стиль WS_THICKFRAME (Border: resizing в редакторе ресурсов). Затем в программе перехватывается сообщение WM_SIZE, сигнализирующее об изменении размеров диалога. В ответ на него программа соответствующим образом изменяет размеры контролов в диалоге. Этот подход универсален и достаточно прост в реализации, но требует написания большого количества кода, связанного с пересчётом координат. Поэтому в WTL введён класс, который в ряде случаев избавит вас от рутинной работы по масштабированию контролов. Этот класс называется CDialogResize<>. Он описан в файле atlframe.h. Хотя этот класс не является универсальным, он подойдёт в большинстве случаев. Замечу, что его можно применять с любым окном, содержащим дочерние окна, но чаще всего он применяется именно с диалогами.

Итак, чтобы воспользоваться поддержкой масштабирования, которую предоставляет WTL, нужно включить в число базовых классов вашего диалога класс CDialogResize<>, задав в качестве параметра шаблона имя порождаемого класса. После этого вам, как обычно, потребуется написать карту - на этот раз карту масштабирования. Макросы, из которых она формируется, приведены в таблице 4.

Макрос Описание
BEGIN_DLGRESIZE_MAP(thisClass)

Начало карты масштабирования. thisClass - имя класса, в котором содержится карта.

END_DLGRESIZE_MAP()

Этот макрос завершает карту масштабирования.

DLGRESIZE_CONTROL(id, flags)

Этот макрос определяет, каким образом должен масштабироваться контрол с идентификатором id. Для этого в WTL определено несколько флагов, которые нужно объединить операцией логического "ИЛИ" и передать в качестве второго параметра макроса flags. Вы можете использовать флаги DLSZ_MOVE_X и DLSZ_MOVE_Y (перемещение вдоль осей X и Y соответственно), DLSZ_SIZE_X и DLSZ_SIZE_Y (изменение ширины и высоты контрола), а также флаг DLSZ_REPAINT, если после масштабирования контрола его нужно перерисовывать (то есть вызывать для него функцию Invalidate).

BEGIN_DLGRESIZE_GROUP()

Контролы, включённые в карту масштабирования, можно группировать. Об эффектах группировки мы поговорим позже. Макрос BEGIN_DLGRESIZE_GROUP начинает группу контролов. Группы не могут быть вложенными.

END_DLGRESIZE_GROUP()

Завершает группу контролов. Каждому макросу BEGIN_DLGRESIZE_GROUP должен соответствовать ровно один макрос END_DLGRESIZE_GROUP.

Таблица 4. Макросы карты масштабирования

Кроме написания карты масштабирования, необходимо выполнить ещё два действия. Во-первых, класс CDialogResize<> имеет свою собственную карту сообщений. В частности, она содержит обработчик сообщения WM_SIZE, который инициирует перемасштабирование контролов при каждом изменении размеров диалога. Эту карту сообщений следует подключить к карте сообщений вашего диалога, используя макрос CHAIN_MSG_MAP:

    BEGIN_MSG_MAP(CMyDialog)
        ...
        CHAIN_MSG_MAP(CDialogResize<CMyDialog>)
        ...
    END_MSG_MAP()

Во вторых, после того, как ваш диалог создан, необходимо инициализировать внутренние структуры WTL, связанные с масштабированием. Это делается при помощи функции DlgResize_Init. Удобно вызывать её из обработчика сообщения WM_INITDIALOG. Функция DlgResize_Init имеет следующий прототип:

void DlgResize_Init(bool bAddGripper = true,
                    bool bUseMinTrackSize = true,
                    DWORD dwForceStyle = WS_THICKFRAME | WS_CLIPCHILDREN)

В большинстве случаев на параметры можно не обращать внимание, так как значения по умолчанию вполне удовлетворяют всем нуждам. Параметр bAddGripper указывает, нужно ли добавить к диалогу "гриппер" - маленький уголок, за который можно ухватиться курсором и изменить размеры диалога. Флаг bUseMinTrackSize определяет, нужно ли ограничивать минимальные размеры диалога. В большинстве случаев это хорошая идея, так как сильно уменьшенный диалог всё равно плохо выглядит и не удобен для работы с ним. Минимальный размер диалога хранится в переменной m_ptMinTrackSize, которую ваш класс диалога наследует от класса CDialogResize<>. По умолчанию в неё записывается первоначальный размер диалога (тот, который установлен в момент вызова функции DlgResize_Init). Вы можете записать туда любое другое значение. Что касается параметра dwForceStyle, то это просто стиль, который принудительно назначается диалогу в функции DlgResize_Init.

Ещё одна функция из класса CDialogResize<>, о которой следует упомянуть, - DlgResize_UpdateLayout. Эта функция принудительно пересчитывает координаты всех контролов в зависимости от переданных ей размеров диалога (cx и cy). Именно она вызывается из обработчика сообщения WM_SIZE, но при необходимости вы можете вызывать её в любом другом месте.

Как составлять карту масштабирования

На самом деле, единственная проблема с классом CDialogResize<> состоит в том, чтобы правильно составить карту масштабирования. Для этого нужно чётко понимать, как работают флаги DLSZ_XXX. Эти флаги по-разному действуют на контрол в группе или без неё.

Сначала посмотрим, как флаги DLSZ_XXX действуют на контрол, не включённый в группу. Допустим, размеры диалога изменились на dx и dy соответственно. Тогда:

Как видно из этого описания, задавать одновременно флаги DLSZ_MOVE_X и DLSZ_SIZE_X (а также DLSZ_MOVE_Y и DLSZ_SIZE_Y) бессмысленно, так как в этом случае будут учитываться только флаги DLSZ_MOVE_*.

Описанная схема масштабирования довольно примитивна. Так, очевидно, что к двум расположенным рядом контролам нельзя применять флаг DLSZ_SIZE_*, так как они оба увеличат размер и "заедут" друг на друга. И всё-таки во многих случаях такого механизма оказывается достаточно. Для примера рассмотрим типичный диалог выбора файла (рисунок 2).


Рисунок 2. Схема диалога открытия файла

При масштабировании логично изменять размер контролов диалога следующим образом: растягивать IDC_LEFT_PANE на всю высоту диалога, растягивать IDC_COMBO по горизонтали, отодвигая IDC_TOOLBAR до предела вправо, отодвигать IDC_NAME и IDC_FILTER вниз и растягивать по горизонтали, перемещать кнопки IDOK и IDCANCEL в правый нижний угол и занимать списком файлов IDC_FILE_LIST всё оставшееся место. Чтобы воплотить в жизнь эту схему, следует записать карту масштабирования следующим образом:

    BEGIN_DLGRESIZE_MAP(COpenFileDialog)
        DLGRESIZE_CONTROL(IDC_LEFT_PANE, DLSZ_SIZE_Y)
        DLGRESIZE_CONTROL(IDC_COMBO, DLSZ_SIZE_X)
        DLGRESIZE_CONTROL(IDC_TOOLBAR, DLSZ_MOVE_X)
        DLGRESIZE_CONTROL(IDC_FILE_LIST, DLSZ_SIZE_X | DLSZ_SIZE_Y)
        DLGRESIZE_CONTROL(IDC_NAME, DLSZ_MOVE_Y | DLSZ_SIZE_X)
        DLGRESIZE_CONTROL(IDC_FILTER, DLSZ_MOVE_Y | DLSZ_SIZE_X)
        DLGRESIZE_CONTROL(IDOK, DLSZ_MOVE_Y | DLSZ_MOVE_X)
        DLGRESIZE_CONTROL(IDCANCEL, DLSZ_MOVE_Y | DLSZ_MOVE_X)
    END_DLGRESIZE_MAP()

Теперь поговорим о контролах, объединённых в группу.

ПРЕДУПРЕЖДЕНИЕ
Реализация групп в WTL подразумевает, что все контролы в группе должны располагаться рядом друг с другом по горизонтали или по вертикали. Флаги, которые вы задаёте для контролов в группе, должны относиться только к одному направлению (или X, или Y), но не к обоим сразу. Несоблюдение этих условий приведёт к странным эффектам. Кроме того, напомню ещё раз, что группы не могут быть вложенными.

Группы обрабатываются следующим образом. Сначала вычисляются координаты огибающего прямоугольника группы, то есть минимального прямоугольника, содержащего все контролы в ней. Далее размеры этого прямоугольника увеличиваются на dx и dy соответственно (dx и dy имеют то же значение, что и в обсуждении выше). После этого к каждому контролу в группе применяются следующие правила:

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

    BEGIN_DLGRESIZE_MAP(CMyDialog)
        BEGIN_DLGRESIZE_GROUP()
            DLGRESIZE_CONTROL(IDC_BUTTON1, DLSZ_SIZE_X)
            DLGRESIZE_CONTROL(IDC_BUTTON2, DLSZ_SIZE_X)
            DLGRESIZE_CONTROL(IDC_BUTTON3, DLSZ_SIZE_X)
        END_DLGRESIZE_GROUP()
    END_DLGRESIZE_MAP()

то этот диалог будет масштабироваться следующим образом:


Рисунок 3. Масштабирование с использованием групп

Как это всё работает

Мы не будем надолго задерживаться на внутренней реализации класса CDialogResize<>, так как там нет почти ничего интересного. Когда вы вызываете функцию DlgResize_Init, начальные положения всех контролов в диалоге запоминаются во внутренних структурах WTL. Функция DlgResize_UpdateLayout использует новые размеры диалога и сохранённые ранее координаты контролов, чтобы назначить им новое положение в соответствии с заданными флагами. Что касается карты масштабирования, она просто превращается в статический массив структур _AtlDlgResizeMap, для доступа к которому используется функция GetDlgResizeMap. Структура _AtlDlgResizeMap хранит заданные вами в карте значения:

    struct _AtlDlgResizeMap
    {
        int m_nCtlID;
        DWORD m_dwResizeFlags;
    };

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

  1. Элемент может встречаться в карте масштабирования более одного раза.
  2. Элемент, не включённый в группу, двигается относительно его текущего положения.
  3. Элемент, включённый в группу, масштабируется с учётом его начального положения (но без учёта его текущей позиции).

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

    BEGIN_DLGRESIZE_MAP(CMyDialog)
        BEGIN_DLGRESIZE_GROUP()
            DLGRESIZE_CONTROL(IDC_BUTTON1, DLSZ_SIZE_X)
            DLGRESIZE_CONTROL(IDC_BUTTON2, DLSZ_SIZE_X)
            DLGRESIZE_CONTROL(IDC_BUTTON3, DLSZ_SIZE_X)
        END_DLGRESIZE_GROUP()

        DLGRESIZE_CONTROL(IDC_BUTTON1, DLSZ_SIZE_Y)
        DLGRESIZE_CONTROL(IDC_BUTTON2, DLSZ_SIZE_Y)
        DLGRESIZE_CONTROL(IDC_BUTTON3, DLSZ_SIZE_Y)
    END_DLGRESIZE_MAP()

Кроме этого, можно включить элемент в несколько групп. Хотя на его местоположение повлияет только последняя группа, этот приём позволит сложным образом влиять на другие контролы. Но не стоит забывать о чувстве меры. Такие приёмы делают вашу программу более запутанной и более медлительной. Нетривиальное масштабирование контролов в диалоге лучше реализовать вручную, а не заниматься неочевидными фокусами с CDialogResize<>.

Контролы

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

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

Поддержка cтандартных и общих контролов Windows

Мы с вами уже изучили класс CWindow, который предоставляет целый набор обёрток для функций Win32 API, предназначенных для работы с окнами. При работе с контролами этот класс также можно использовать. Но гораздо удобнее использовать специальные классы контролов, которые описаны в файле atlctrls.h. Полный список этих классов приведён в таблице 5.

Контрол Соответствующий класс
Статический текст или изображение (static control) CStatic
Кнопка (button) CButton
Простой список (list box) CListBox
Комбинированный список (combo box) CComboBox
Поле ввода (edit box) CEdit
Полоса прокрутки (scroll bar) CScrollBar
Список изображений (image list) CImageList
Расширенный список (list view) CListViewCtrl
Дерево (tree view) CTreeViewCtrl,
CTreeViewCtrlEx
Заголовок (header) CHeaderCtrl
Панель инструментов (toolbar) CToolBarCtrl
Строка состояния (status bar) CStatusBarCtrl
Окно с закладками (tab control) CTabCtrl
Всплывающая подсказка (tooltip) CToolTipCtrl
Ползунок (trackbar) CTrackBarCtrl
Регулятор (up-down control) CUpDownCtrl
Индикатор прогресса (progress bar) CProgressBarCtrl
Горячая клавиша (hot key) CHotKeyCtrl
Окно с анимацией (animate control) CAnimateCtrl
Расширенное поле ввода (rich edit) CRichEditCtrl
Список с возможностью перетаскивания (drag list box) CDragListBox
Полоска-контейнер (rebar control) CReBarCtrl
Комбинированный список с картинками (ComboBoxEx control) CComboBoxEx
Выбор даты/времени (date and time picker) CDateTimePickerCtrl
Календарь на меcяц (month calendar) CMonthCalendarCtrl
"Плоская" полоса прокрутки (flat scroll bar) CFlatScrollBar
IP-адрес (IP address control) CIPAddressCtrl
Пейджер (pager control) CPagerCtrl
Таблица 5. Классы стандартных и общих контролов в WTL

Каждый из этих классов порождён от CWindow и содержит все его методы. В дополнение каждый класс предоставляет:

Обратите внимание, что функциональность всех классов из atlctrls.h регулируется макросами WINVER, _WIN32_IE и _RICHEDIT_VER. Например, функции, специфичные для контролов из Internet Explorer 4.0 и выше, оформлены так:

#if (_WIN32_IE >= 0x0400)
    ...
#endif //(_WIN32_IE >= 0x0400)

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

Полное описание всех функций и классов из atlctrls.h выходит за рамки данной статьи.

"Самодельные" контролы

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

Создавать контролы "с нуля" мы уже умеем. Для этого нужно породить новый класс от CWindowImpl<> и написать обработчики нужных сообщений. Чаще других обрабатываются сообщения WM_CREATE и WM_PAINT, а также клавиатурные и мышиные сообщения. Кроме того, нужно предусмотреть средства для взаимодействия программы с вашим контролом. Для этой цели можно ввести нестандартные сообщения, которые будет понимать ваш контрол, или предусмотреть соответствующие методы в вашем классе.

Если вы решили построить свой контрол на базе существующего, вам также следует использовать класс CWindowImpl<>. Нужно только учесть два момента. Во-первых, базовым классом для вашего контрола должен быть не CWindow, а класс контрола, который вы модифицируете. Базовый класс задаётся во втором параметре шаблона CWindowImpl<> (по умолчанию этот параметр равен CWindow). А во-вторых, для обработки сообщений по умолчанию должна использоваться не функция DefWindowProc (как для обычных окон), а оконная функция соответствующего контрола. Чтобы этого добиться, следует использовать макрос DECLARE_WND_SUPERCLASS вместо DECLARE_WND_CLASS. Этот макрос объявлен так.

#define DECLARE_WND_SUPERCLASS(WndClassName, OrigWndClassName) \
static CWndClassInfo& GetWndClassInfo() \
{ \
    static CWndClassInfo wc = \
    { \
        { sizeof(WNDCLASSEX), 0, StartWindowProc, \
          0, 0, NULL, NULL, NULL, NULL, NULL, WndClassName, NULL }, \
        OrigWndClassName, NULL, NULL, TRUE, 0, _T("") \
    }; \
    return wc; \
}

Параметр WndClassName определяет имя класса вашего нового контрола. В качестве второго параметра OrigWndClassName следует указать имя класса контрола, который вы взяли за основу. При регистрации вашего класса WndClassName WTL скопирует для него параметры из класса с именем OrigWndClassName, а также сохранит адрес оконной процедуры, связанной с этим классом, в переменной CWindowImplBaseT<>::m_pfnSuperWindowProc и будет обращаться к ней для обработки сообщений, которые не были обработаны через карту сообщений.

С учётом всего сказанного, типичный класс контрола выглядит так.

class CMyCoolControl : public CWindowImpl<CMyCoolControl, CEdit>
{
public:
    DECLARE_WND_SUPERCLASS(NULL, CEdit::GetWndClassName())


    BEGIN_MSG_MAP(CMyCoolControl)
        // Карта сообщений
    END_MSG_MAP()

    ...
};

В этом примере новый контрол создаётся на базе поля ввода (которому соответствует класс CEdit). Аналогично используется любой другой контрол.

Мы уже изучили макрос DDX_CONTROL, входящий в набор макросов DDX. Именно его следует использовать, чтобы связать существующий стандартный контрол (например, нарисованный в редакторе ресурсов) с объектом класса и наделить его дополнительными возможностями.

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

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

Класс CBitmapButton

Класс CBitmapButton реализует кнопку, с каждым состоянием которой (нажата/отпущена/выключена/в фокусе) связано изображение. Кроме того, с кнопкой связывается всплывающая подсказка, поясняющая её назначение, и набор расширенных стилей (эти стили не имеют ничего общего с расширенным стилями обычного окна). Каждому стилю соответствует битовый флаг. Полный список флагов приведён в таблице 6.

Флаг Описание
BMPBTN_HOVER

Кнопка с этим стилем реагирует на присутствие курсора мыши: если он расположен над кнопкой, она переводится в состояние "в фокусе". Если этот стиль не задан, состояние "в фокусе" присваивается кнопке, на которую установлен клавиатурный фокус ввода. Замечу также, что кнопка со стилем BMPBTN_HOVER не реагирует на клавиатуру, то есть нажать на неё можно только мышью.

BMPBTN_AUTO3D_SINGLE

К кнопке принудительно добавляется трёхмерная рамка толщиной в 1 пиксель. Используя этот стиль, вы можете избавиться от необходимости рисовать трёхмерные рамки на всех изображениях, связанных с состояниями кнопки.

BMPBTN_AUTO3D_DOUBLE

Аналогичен предыдущему, но к кнопке добавляется рамка толщиной в 2 пикселя (такая, как у всех стандартных кнопок Windows).

BMPBTN_AUTOSIZE

Кнопка автоматически масштабируется под размер изображений, которые с ней связаны.

BMPBTN_SHAREIMAGELISTS

Кнопка использует разделяемый список изображений. Это означает, что он не будет уничтожен в деструкторе класса CBitmapButton.

BMPBTN_AUTOFIRE

Этот стиль имеет отношение к клавиатурному интерфейсу кнопки. Если вы нажали на кнопку, используя клавишу Space, и удерживаете эту клавишу, то кнопка со стилем BMPBTN_AUTOFIRE будет через заданные в системе промежутки времени посылать уведомление BN_CLICKED родительскому окну. Если же этот стиль не задать, уведомление отправится ровно 1 раз - в тот момент, когда вы нажали Space.

Таблица 6. Расширенные стили для кнопки с картинками

Стиль кнопки, а также связанный с ней список изображений, задаются в конструкторе класса CBitmapButton, хотя можно установить/изменить их и позже, используя соответствующие методы. Для задания текста всплывающей подсказки также существуют соответствующий метод. Полный список методов класса CBitmapButton приведён в таблице 7.

Метод Описание
CBitmapButtonImpl(DWORD dwExtendedStyle = BMPBTN_AUTOSIZE, HIMAGELIST hImageList = NULL)

Конструктор. Параметры - набор расширенных стилей и хэндл списка изображений, который следует связать с кнопкой.

~CBitmapButtonImpl()

Деструктор. Напоминаю, что внутри деструктора будет разрушен список изображений, связанный с кнопкой в данный момент. Чтобы этого не произошло, следует назначить кнопке расширенный стиль BMPBTN_SHAREIMAGELISTS.

BOOL SubclassWindow(HWND hWnd)

Метод для подмены оконной процедуры (т. н. сабклассинга) существующего контрола. Используется макросом DDX_CONTROL, но вы можете вызывать этот метод и сами.

DWORD GetBitmapButtonExtendedStyle()

Возвращает набор расширенных стилей.

DWORD SetBitmapButtonExtendedStyle(DWORD dwExtendedStyle, DWORD dwMask = 0)

Устанавливает набор расширенных стилей.

HIMAGELIST GetImageList()

Возвращает хэндл списка изображений.

HIMAGELIST SetImageList(HIMAGELIST hImageList)

Устанавливает список изображений.

bool GetToolTipText(LPTSTR lpstrText, int nLength)

Возвращает текст всплывающей подсказки.

bool SetToolTipText(LPCTSTR lpstrText)

Устанавливает текст всплывающей подсказки (память для него динамически распределяется внутри класса CBitmapButton).

void SetImages(int nNormal, int nPushed = -1, int nFocusOrHover = -1, int nDisabled = -1)

Устанавливает соответствие состояний кнопки и изображений в списке. Используйте -1, чтобы оставить соответствующее состояние без изменений. Вы должны задать, по меньшей мере, изображение для состояния nNormal (отпущена), иначе программа не будет работать корректно. Замечу, что вы можете назначить нескольким состояниям одно и то же изображение.

BOOL SizeToImage()

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

void DoPaint(CDCHandle dc)

Эта функция рисует кнопку в одном из четырёх состояний. Вам вряд ли понадобится вызывать её напрямую; зато её можно переопределить в производном классе и придать кнопке нестандартный вид, сохранив при этом остальную функциональность, которую предоставляет класс CBitmapButton.

Таблица 7. Методы класса CBitmapButton

Класс CCheckListViewCtrl

Из названия может показаться, что этот класс реализует список с «галочками» (check boxes), но это не совсем так. Стандартный контрол ListView уже поддерживает галочки. Достаточно задать ему расширенный стиль LVS_EX_CHECKBOXES. Что касается класса CCheckListViewCtrl, то он позволяет манипулировать несколькими галочками одновременно. Для этого пользователь выделяет несколько элементов в списке (используя Shift и Ctrl, в списке можно довольно быстро пометить нужную группу элементов). После этого щелчок по галочке любого элемента (или нажатие на Space) будет приводить к изменению состояния галочек у всех выделенных элементов. При необходимости такое поведение контрола можно подавить, удерживая Ctrl (при этом список будет вести себя, как обычный ListView).

Реализация класса CCheckListViewCtrl достаточно очевидна. Метод SubclassWindow подменяет оконную процедуру списка и принудительно устанавливает ему стиль LVS_EX_CHECKBOXES. Всю остальную работу делают обработчики сообщений WM_LBUTTONDOWN, WM_LBUTTONDBLCLK и WM_KEYDOWN. Все они используют для переключения галочек вспомогательную функцию CheckSelectedItems. Вы можете вызывать эту функцию и сами, хотя такая необходимость возникает нечасто. Функция CheckSelectedItems получает единственный параметр - номер элемента (этот элемент должен быть выделен). Она считывает состояние его галочки, инвертирует это состояние и применяет ко всем выделенным элементам в списке.

Резюмируя сказанное выше, для применения класса CCheckListViewCtrl в большинстве случаев достаточно просто связать объект этого класса с контролом, используя макрос DDX_CONTROL.

Класс CHyperLink

Класс CHyperLink предназначен для создания гиперссылок. На самом деле, большую часть функциональности он наследует от базового класса CHyperLinkImpl. Гиперссылка создаётся на основе статического элемента управления.

Класс CHyperLink наглядно демонстрирует, что иногда для решения самых простых задач приходится написать множество строк кода. Если, конечно, учесть разные «мелочи», о которых задумываются далеко не все. Вот список основных возможностей класса.

Список методов класса CHyperLink приведён в таблице 8.

Метод Описание
CHyperLinkImpl()

Конструктор. Записывает в переменные объекта значения по умолчанию.

~CHyperLinkImpl()

Деструктор. Освобождает ресурсы, распределённые в процессе инициализации.

bool GetLabel(LPTSTR lpstrBuffer, int nLength) const

Возвращает метку гиперссылки (то есть строку, которую пользователь видит на экране).

bool SetLabel(LPCTSTR lpstrLabel)

Устанавливает метку гиперссылки. Текст сохраняется внутри объекта класса. Память для него распределяется динамически. По умолчанию в качестве метки используется содержимое статического контрола, который связывается с объектом класса. Поэтому часто удаётся обойтись и без метода SetLabel.

bool GetHyperLink(LPTSTR lpstrBuffer, int nLength) const

Возвращает адрес гиперссылки, по которому осуществляется фактический переход.

bool SetHyperLink(LPCTSTR lpstrLink)

Устанавливает адрес гиперссылки. Адрес также сохраняется внутри объекта класса в динамически распределяемой памяти. Иногда адрес совпадает с меткой. В этом случае устанавливать его вызовом SetHyperLink необязательно, так как класс CHyperLink сделает это за вас.

BOOL SubclassWindow(HWND hWnd)

Подменяет оконную процедуру окна, подключая к нему объект класса.

bool Navigate()

Осуществляет переход по ссылке.

void Init()

Инициализирует объект класса: создаёт или загружает курсор в виде руки, создаёт «подчёркнутый» фонт и тултип для ссылки, загружает из реестра цвета, которые использует IE.

void DoPaint(CDCHandle dc)

Рисует ссылку. Вы можете переопределить этот метод в производном классе, чтобы изменить её внешний вид.

Таблица 8. Методы класса CHyperLink
Обратите внимание: если метка и адрес гиперссылки у вас отличаются, метод SetHyperLink следует вызывать до связывания объекта класса с контролом. Дело в том, что в момент связывания (в функции Init, которая вызывается из SubclassWindow) для ссылки создаётся тултип, в который записывается адрес ссылки. Если адрес ещё не задан, в тултип запишется метка.

Класс CMultiPaneStatusBarCtrlImpl

Класс CMultiPaneStatusBar призван облегчить вашу жизнь при работе со строками состояния. Стандартный контрол status bar из набора общих контролов Windows позволяет создать на строке состояния до 256 панелей, в которых можно отображать текст и иконки. Но он не предоставляет никаких средств для автоматического перемещения этих панелей. Программисту на «чистом» API приходится передвигать их вручную всякий раз, когда строка состояния изменяет свой размер. В MFC эту работу берёт на себя класс CStatusBar. А в WTL вам поможет класс CMultiPaneStatusBar.

Посмотрим, каким образом используется класс CMultiPaneStatusBar. Сначала объект класса связывается с существующей строкой состояния при помощи DDX_CONTROL. Можно и создать строку состояния с нуля, используя метод Create. Затем задаётся набор панелей для строки состояния. Для этого предназначен метод SetPanes. Он принимает количество панелей и массив с их идентификаторами. Идентификаторы используются для последующего обращения к панелям. Одной из панелей можно назначить стандартный идентификатор ID_DEFAULT_PANE. Панель с таким идентификатором растягивается, занимая всё свободное пространство в строке состояния. Остальные панели имеют фиксированный размер (который всегда можно изменить, используя метод SetPaneWidth). О корректном перемещении панелей заботится WTL. Вам остаётся только изменять текст панелей, их иконки и всплывающие подсказки в соответствии с вашими нуждами.

Полный список методов класса CMultiPaneStatusBar приведён в таблице 9.

Метод Описание
CMultiPaneStatusBarCtrlImpl()

Конструктор. Не выполняет никакой полезной работы.

~CMultiPaneStatusBarCtrlImpl()

Деструктор. Освобождает память, занятую списком идентификаторов панелей.

HWND Create(HWND hWndParent, LPCTSTR lpstrText, DWORD dwStyle, UINT nID)

HWND Create(HWND hWndParent, UINT nTextID, DWORD dwStyle, UINT nID)

Создают строку состояния.

BOOL SetPanes(int* pPanes, int nPanes, bool bSetText = true)

Задаёт набор панелей для строки состояния. При этом предыдущий набор полностью теряется. Массив pPanes содержит идентификаторы панелей. Начальный размер панели подгоняется под строку из ресурсов, идентификатор которой совпадает с идентификатором панели. Если задан флаг bSetText, текст из ресурсов будет сразу же вставлен в соответствующие панели.

bool GetPaneTextLength(int nPaneID, int* pcchLength = NULL, int* pnType = NULL) const

Возвращает длину текста в панели nPaneID через указатель pcchLength. По адресу pnType записывается тип панели. В Windows определены следующие типы: SBT_NOBORDERS (панель не имеет видимой рамки), SBT_OWNERDRAW (панель отрисовывается родительским окном), SBT_POPOUT (панель выглядит выпуклой на строке состояния) и SBT_RTLREADING (изменяет направление текста на противоположное). Нулевой тип соответствует обычной панели, которая «вдавлена» в строку состояния.

BOOL GetPaneText(int nPaneID, LPTSTR lpstrText, int* pcchLength = NULL, int* pnType = NULL) const

Аналогичен предыдущему, но извлекает также текст панели в буфер lpstrText.

BOOL SetPaneText(int nPaneID, LPCTSTR lpstrText, int nType = 0)

Задаёт текст (параметр lpstrText) и тип (параметр nType) для панели nPaneID.

BOOL SetPaneWidth(int nPaneID, int cxWidth)

Устанавливает ширину панели nPaneID равной cxWidth.

BOOL GetPaneTipText(int nPaneID, LPTSTR lpstrText, int nSize) const

Извлекает текст всплывающей подсказки для панели nPaneID.

BOOL SetPaneTipText(int nPaneID, LPCTSTR lpstrText)

Устанавливает текст всплывающей подсказки для панели nPaneID.

BOOL GetPaneIcon(int nPaneID, HICON& hIcon) const

Извлекает хэндл иконки, назначенной панели nPaneID.

BOOL SetPaneIcon(int nPaneID, HICON hIcon)

Задаёт иконку для панели nPaneID.

BOOL UpdatePanesLayout()

Пересчитывает расположение панелей. Вызывайте этот метод всякий раз, когда вы изменяете размеры панелей с помощью методов SetPanes и SetPaneWidth.

int GetPaneIndexFromID(int nPaneID) const

Определяет индекс панели по её идентификатору. Как известно, стандартный status bar использует для работы с панелями индексы. Вам вряд ли потребуется этот метод, поскольку класс CMultiPaneStatusBar позволяет вам выполнять все необходимые операции по идентификатору панели. Но для полноты картины стоит упомянуть и его.

Таблица 9. Методы класса CMultiPaneStatusBar
Методы GetPaneTipText, SetPaneTipText, GetPaneIcon и SetPaneIcon доступны, только если макрос _WIN32_IE имеет значение 0x0400 или выше.

И последний важный момент. Всякий раз, когда окно изменяет размер, вы должны посылать строке состояния сообщение WM_SIZE, чтобы она могла скорректировать своё местоположение и размер.

Класс CWaitCursor

Класс CWaitCursor - это простенькая обёртка вокруг метода SetCursor из Win32 API. При помощи этого класса вы можете временно изменить вид курсора мыши. Чаще всего класс CWaitCursor применяют, чтобы "выплюнуть" песочные часы на время выполнения длительной операции. Отсюда и название класса.

Полный список методов класса CWaitCursor приведён в таблице 8.

Метод Описание
CWaitCursor(bool bSet = true, LPCTSTR lpstrCursor = IDC_WAIT, bool bSys = true)

Конструктор. Параметр lpstrCursor задаёт имя ресурса, из которого следует грузить курсор. Если вы собираетесь использовать системный курсор, параметр bSys устанавливается в true. Наконец, флаг bSet определяет, следует ли вызывать из конструктора метод Set (см. ниже).

~CWaitCursor()

Деструктор. Из него принудительно вызывается метод Restore (см. ниже).

bool Set()

Заменяет текущий курсор курсором, заданным в конструкторе.

bool Restore()

Восстанавливает старый курсор, который был изменён методом Set.

Таблица 8. Методы класса CWaitCursor

Предлагаемые по умолчанию параметры конструктора "заточены" для индикации длительной операции. Использование класса CWaitCursor в этом случае тривиально:

void LengthyOperation()
{
    // Конструктор объекта waitCur вызовет метод Set, и курсор поменяется на "песочные часы".
    CWaitCursor waitCur;

    // Выполняем длительную операцию.
    ...

    // Здесь вызывается деструктор для объекта waitCur, и курсор восстанавливается.
}

Класс COwnerDraw<>: отрисовка контрола родительским окном в стиле WTL

Механизм отрисовки контрола родительским окном (owner draw) появился довольно давно – ещё в Windows 3.0. Он позволяет придать контролу совершенно произвольный внешний вид. Его поддерживают такие стандартные элементы управления, как кнопка, меню, простой список и комбинированный список.

В основе механизма 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_PREPAINT DWORD OnPrePaint(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw)
CDDS_POSTPAINT DWORD OnPostPaint(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw)
CDDS_PREERASAE DWORD OnPreErase(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw)
CDDS_POSTERASE DWORD OnPostErase(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw)
CDDS_ITEMPREPAINT DWORD OnItemPrePaint(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw)
CDDS_ITEMPOSTPAINT DWORD OnItemPostPaint(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw)
CDDS_ITEMPREERASE DWORD OnItemPreErase(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw)
CDDS_ITEMPOSTERASE DWORD OnItemPostErase(int idCtrl, LPNMCUSTOMDRAW lpNMCustomDraw)
Таблица 10. Фазы рисования и соответствующие им функции класса CCustomDraw

Вот небольшой пример использования класса 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

Приложение WTLErrLook – это упрощённый вариант программы Error Lookup, которая входит в Visual Studio 6. Главное окно программы выполнено в виде модельного диалога. Обмен данными с полями ввода осуществляется с помощью DDX_TEXT.

WTLSndVol: приложение на базе немодального диалога

Демонстрационный проект WTLSndVol


WTLSndVol

WTLSndVol – это упрощённая версия регулятора громкости (sndvol32.exe), который входит в комплект Windows. При запуске программы она не показывает главное окно (которое выполнено в виде немодального дмалога), а размещает иконку в системном трее (Shell_NotifyIcon). Чтобы она отличалась от иконки стандартного регулятора, я сделал её зелёной. Щелчок по иконке приводит к появлению окна регулятора. Для изменения громкости используется класс CSimpleMixer. Рассматривать его устройство мы не будем, так как это тема для отдельной статьи. Чтобы закрыть WTLSndVol, щёлкните правой кнопкой на иконке в трее и выберите из меню команду Exit.

WTLNavigator: использование диалогов с ActiveX-контролами

Демонстрационный проект WTLNavigator


WTLNavigator

WTLNavigator – это примитивный броузер, построенный на основе ActiveX-контрола "Web Browser". Класс главного окна приложения унаследован от класса CAxDialogImpl.

WTLCalc: обновление дочерних окон

Демонстрационный проект WTLCalc


WTLCalc

WTLCalc – это простенький калькулятор. Доступность математических операций в калькуляторе зависит от введённого числа: логарифм может применяться только к положительным числам, факториал – только к натуральным и т. д. Соответственно, для включения и выключения кнопок используется механизм CUpdateUI.

WTLSizeDlg: пример масштабируемого диалога

Демонстрационный проект WTLSizeDlg


WTLSizeDlg

Программа WTLSizeDlg не выполняет никакой полезной работы. Она просто рисует диалог и позволяет его масштабировать. Для поддержки масштабирования используется класс CDialogResize. Обратите внимание, что корректное масштабирование контролов обеспечивается благодаря наличию невидимого контрола.

WTLCtlDemo: использование стандартных и общих контролов

Демонстрационный проект WTLCtlDemo


WTLCtlDemo

Программа WTLCtlDemo показывает, как можно работать со стандартными контролами – static, button, edit box, list box, combo box, list view и tree view.

WTLCtlxDemo: использование "самодельных" контролов WTL

Демонстрационный проект WTLCtlxDemo


WTLCtlxDemo

Программа WTLCtlxDemo демонстрирует применение «самодельных» контролов, предоставляемых библиотекой WTL - CBitmapButton, CHyperLink, CCheckListViewCtrl и CMultiPaneStatusBarCtrl.


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