|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Программирование на Visual C++Выпуск №54 от 11 ноября 2001 г. Здравствуйте, уважаемые подписчики! Я наконец разобрался с CHM-файлом архива рассылки, так что теперь там поиск должен работать нормально (правда, как и раньше, только для латиницы). Новый файл, в который я заодно добавил вышедшие за это время выпуски, можно скачать здесь. Сегодня в выпуске – долгожданное продолжение руководства по WTL Александра Шаргина. К сожалению из-за большого объема статьи, которую даже пришлось разбить на две части, остальных рубрик сегодня не будет. Но статья того без сомнения стоит! CТАТЬЯИспользование WTL Часть 2. Диалоги и контролы Диалоги Диалоговые окна широко используются в Windows-приложениях, начиная с момента выхода самой операционной системы Windows. Они очень удобны для организации диалога с пользователем (отсюда их название). Кроме того, в несложных приложениях часто удаётся построить на базе диалогов не только вспомогательные окна, но и главное окно приложения (такие приложения иногда называют "dialog-based"). В этом разделе мы рассмотрим классы WTL, предназначенные для работы с диалоговыми окнами. Классы WTL для работы с диалогамиКлассы WTL, относящиеся к диалоговым окнам, показаны на рисунке 1. Рисунок 1. Диалоговые классы WTL Обратите внимание, что все диалоговые классы порождаются от базового класса CWindowImplRoot<>, а не от класса CWindowImpl<>. Это сделано потому, что диалоги, в отличие от всех остальных окон, не используют оконную процедуру для обработки сообщений. Вместо этого используется диалоговая процедура, адрес которой задаётся при создании диалога. WTL предоставляет вам свою реализацию диалоговой процедуры в классе CDialogImplBaseT<>. Соответственно, все остальные классы диалогов WTL наследуют эту реализацию.
Теперь изучим каждый класс более подробно. Класс 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 и статической переменной производного класса, но прибегать к этому приёму на практике приходится не часто.
Немодальный диалог создаётся с использованием функции 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 = IDIDD_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.
Рассмотрим пример карты 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()
Вот и всё. Никакой дополнительной инициализации механизм DDX в WTL не требует. Чтобы выполнить обмен данными, используйте функцию DoDataExchange. Вот её прототип: BOOL DoDataExchange(BOOL bSaveAndValidate = FALSE, UINT nCtlID = (UINT)-1) Параметр bSaveAndValidate задаёт направление обмена (FALSE или DDX_LOAD соответствует записи значений из переменных в контролы, а TRUE или DDX_SAVE – из контролов в переменные). Второй параметр задаёт идентификатор контрола, с которым необходимо произвести обмен. Значение по умолчанию (-1) соответствует всем контролам, упомянутым в карте DDX. Функция DoDataExchange возвращает TRUE, если обмен данными был успешным, или FALSE в противном случае.
Иногда в процессе обмена данными возникают ошибки. Их делят на две разновидности: ошибки обмена (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, которая и будет содержать информацию о нарушенном ограничении. Как это всё работает Теперь посмотрим, как механизм 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.
После того, как карта UI добавлена в класс, остаётся один завершающий штрих. Вы должны зарегистрировать все контейнеры элементов пользовательского интерфейса, которые нужно обновлять. В случае с контролами в качестве контейнера выступает сам диалог. В случае с меню это окно, содержащее меню. И так далее. Для каждого контейнера существует своя функция регистрации: UIAddMenuBar для меню, UIAddToolBar для панелей иснтрументов, UIAddStatusBar для строк состояния и UIAddChildWindowContainer для контейнеров дочерних окон. Все перечисленные функции принимают хэндл окна-контейнера и позвращают BOOL, сигнализирующий об успехе или неуспехе регистрации. В случае с диалогом регистрировать контейнер удобно в обработчике сообщения WM_INITDIALOG. Итак, инициализация закончена. Теперь мы можем изменять состояния контролов, идентификаторы которых включены в карту UI. Для этого используется ещё один набор функций с префиксом UI. Все они перечислены в таблице 3.
Функции, которые мы только что рассмотрели, не изменяют фактическое состояние элементов. Они только записывают новые значения во внутренние структуры класса 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 (модальный диалог посылает его родительскому окну, когда очередь сообщений исчерпана) или вообще отказаться от фоновой обработки. [ ПРОДОЛЖЕНИЕ СЛЕДУЕТ ] Это все на сегодня. Пока! (Алекс Jenter jenter@rsdn.ru) (Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN. ) |
|
||||||||||||||||||||||||||||||||||||||||||||||||||
Главная | В избранное | Наш E-MAIL | Прислать материал | Нашёл ошибку | Наверх |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|