Программирование на Visual С++ - No.96 (Эффективное использование WTL)
Информационный Канал Subscribe.Ru |
|
РАССЫЛКА САЙТА
RSDN.RU |
Приветствую! Эффективное использование WTL Автор: Алексей Ширшов
|
Константа | Значение | Описание |
---|---|---|
SPLIT_PANE_LEFT | 0 | Левая панель. |
SPLIT_PANE_RIGHT | 1 | Правая панель. |
SPLIT_PANE_TOP | 0 | Верхняя панель. |
SPLIT_PANE_BOTTOM | 1 | Нижняя панель. |
SPLIT_PANE_NONE | -1 | Восстанавливает «мульти» режим. |
- Функция GetSinglePaneMode возвращает номер области в режиме работы, заданном SetSinglePaneMode.
- Функция SetSplitterExtendedStyle устанавливает расширенный стиль сплиттера. Расширенных стилей всего три:
Константа | Значение | Описание |
---|---|---|
SPLIT_PROPORTIONAL | 1 | При изменении размеров окна-контейнера размеры разделяемых областей меняются пропорционально. |
SPLIT_NONINTERACTIVE | 2 | Позицию разделителя нельзя изменить интерактивно. |
SPLIT_RIGHTALIGNED/ SPLIT_BOTTOMALIGNED | 4 | При изменении размеров окна-контейнера размер правой/нижней области остается постоянным. |
- Функция GetSplitterExtendedStyle возвращает расширенный стиль сплиттера.
- Член m_bFullDrag, если установлен в true, разрешает перерисовку правой и левой (верхней и нижней) областей в момент изменения позиции разделителя. По умолчанию установлен в true.
- Функция SetSplitterPane позволяет задать окно для одной из областей. Хендл окна сохраняется во внутреннем массиве m_hWndPane для того, чтобы при изменении размеров областей соответствующие окна также изменяли свои размеры. Всем этим занимается функция UpdateSplitterLayout.
- Функция GetSplitterPane возвращает хендл окна, назначенного области.
- Функция SetSplitterPanes позволяет одновременно назначить окна обеим областям (правой/левой, либо верхней/нижней).
- Функция SetActivePane передает фокус окну, назначенному области.
- Функция GetActivePane возвращает номер области, ассоциированное с которой окно владеет фокусом.
- Функция ActivateNextPane, используя GetActivePane и SetActivePane, передает фокус окну следующей области.
- Функция SetDefaultActivePane устанавливает область, которой будет передан фокус при передаче фокуса окну-контейнеру.
- Функция GetDefaultActivePane возвращает область, окно которой первым получает фокус.
Все сообщения, посылаемые контейнеру дочерними окнами, переадресуются родительскому окну контейнера. Делается это с помощью добавления в карту сообщений макроса
FORWARD_NOTIFICATIONS()
который разворачивается в вызов функции Atl3ForwardNotifications. Этот макрос появляется в классе CSplitterWindowT.
Пример переключения из/в одиночный режим
Ниже приводится пример, в котором скрывается левая или верхняя область, а при повторном вызове восстанавливается нормальный режим.
void Toggle(int pane = SPLIT_PANE_RIGHT) { if (m_vSplit.GetSinglePaneMode() != SPLIT_PANE_NONE) pane = SPLIT_PANE_NONE; m_vSplit.SetSinglePaneMode(pane); }
Пример создания контейнера с разделителем
В примере создаются контейнер с разделителем и ListView, и затем ListView назначается правой области контейнера.
CRect rcVert; GetClientRect(&rcVert); // создание вертикального разделителя m_vSplit.Create(m_hWnd, rcVert, NULL, WS_CHILD|WS_VISIBLE|WS_CLIPSIBLINGS|WS_CLIPCHILDREN); // создание ListView m_lv.Create(m_vSplit, rcDefault, 0, WS_CHILD|WS_VISIBLE|LVS_REPORT|WS_CLIPSIBLINGS|WS_CLIPCHILDREN, WS_EX_CLIENTEDGE); // задание правой панели m_vSplit.SetSplitterPane(SPLIT_PANE_RIGHT, m_lv); m_hWndClient = m_vSplit;
Отметим последнюю строчку: если в качестве клиентского окна для главного окна приложения не указать окно-контейнер, вас ждут крупные неприятности! Окно контейнера отрисовываться не будет, как и все его дочерние окна.
Ошибки
Особых ошибок замечено не было, за исключением описанного выше, связанного с подпиской на сообщение WM_SETTINGCHANGE.
Проблемный код:
LRESULT OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled) { GetSystemSettings(false); bHandled = FALSE; return 1; }
Должно быть:
LRESULT OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled) { _Module.AddSettingChangeNotify(m_hWnd); GetSystemSettings(false); bHandled = FALSE; return 1; }
CPaneContainer
Класс предназначен для создания контейнеров, имеющих определенный заголовок и кнопку закрытия. Вид заголовка примерно такой же, как и у стандартной программы «Проводник» (Explorer). Класс реализован в файле atlctrlx.h. Рассмотрим наиболее важные его функции и члены.
- Функция GetPaneContainerExtendedStyle возвращает расширенный стиль контейнера. Одно из следующих значений:
Константа | Значение | Описание |
---|---|---|
PANECNT_NOCLOSEBUTTON | 1 | Кнопка закрытия отсутствует. |
PANECNT_VERTICAL | 2 | Заголовок контейнера расположен вертикально. |
- Функция SetPaneContainerExtendedStyle устанавливает расширенный стиль контейнера.
- Функция SetClient аналогична SetSplitterPane для CSplitterWindowT. После ее вызова дочернее окно будет изменять размеры при изменении размеров контейнера.
- Функция GetClient возвращает хендл окна, связанный с данным контейнером функцией SetClient.
- Функция SetTitle устанавливает текстовую строку заголовка контейнера.
- Функция GetTitle копирует строку заголовка в переданный буфер.
- Функция GetTitleLength возвращает длину строки заголовка в символах.
- Член m_cxyHeader задает высоту заголовка контейнера.
- Функция EnableCloseButton делает доступной или нет, в зависимости от параметра, кнопку закрытия на заголовке.
- Функция UpdateLayout перерисовывает заголовок контейнера и клиентское окно, если оно имеется.
При нажатии на кнопку закрытия окна, родительскому окну посылается сообщение WM_COMMAND с идентификатором ID_PANE_CLOSE.
Пример создания контейнера с вертикальным заголовком
Так как вертикальный заголовок не может иметь названия, второй параметр функции создания контейнера можно было опустить.
// Создание контейнера m_tPane.Create(m_hWnd, _T("Simple pane")); // Задание расшширенного стиля vertical m_tPane.SetPaneContainerExtendedStyle(PANECNT_VERTICAL); m_hWndClient = m_tPane;
Пример создания дочернего окна
// Создание контейнера m_tPane.Create(m_hWnd, _T("Simple pane")); // Создание клиентского окна для контейнера m_edit.Create(m_tPane, rcDefault, NULL, WS_CHILD|WS_VISIBLE|WS_CLIPSIBLINGS|WS_CLIPCHILDREN, WS_EX_CLIENTEDGE); // Задание шрифта GUI по умолчанию m_edit.SetFont(AtlGetDefaultGuiFont()); // Подключение текстового поля к контейнеру m_tPane.SetClient(m_edit); m_hWndClient = m_tPane;
Очень часто бывает необходимо в MDI-приложении размещать статические окна, как, например, ClassView в VisualStudio 6.0. С помощью класса CSplitterWindow этого добиться очень легко. Рассмотрим по шагам необходимые действия:
1. В функции OnCreate класса CMainFrame создаем окно с разделителем.
RECT rcClient; GetClientRect(&rcClient); m_vSplit.Create(m_hWnd, rcClient, NULL, WS_CHILD|WS_VISIBLE|WS_CLIPSIBLINGS|WS_CLIPCHILDREN);
2. Создаем окно CPaneContainer.
m_lPane.Create(m_vSplit.m_hWnd);
3. Подключаем окно контейнера к левой области окна с разделителем.
m_vSplit.SetSplitterPane(SPLIT_PANE_LEFT, m_lPane.m_hWnd);
4. Создаем дочернее окно MDI (MDI Client) и подключаем его к правой области окна с разделителем.
m_hWndMDIClient = CreateMDIClient(); m_vSplit.SetSplitterPane(SPLIT_PANE_RIGHT, m_hWndMDIClient);
5. Делаем окно с разделителем родителем дочерних окон MDI.
::SetParent(m_hWndMDIClient, m_vSplit.m_hWnd);
6. Делаем окно с разделителем клиентским окном главного окна MDI.
m_hWndClient = m_vSplit.m_hWnd;
7. Теперь нужно исправить одну неточность, которую создает мастер при генерации проекта. В функции главного фрейма приложения OnFileNew:
CChildFrame* pChild = new CChildFrame; HWND hWnd = pChild->CreateEx(m_hWndMDIClient); // HWND hWnd = pChild->CreateEx(m_hWndClient);
В принципе, теперь можно запускать проект на компиляцию. Однако после запуска приложения вы увидите, что клиентские окна не активизируются. Это связано с тем, что некоторые сообщения теперь поступают не в главный фрейм приложения, а в окно с разделителем.
8. Для решения этой проблемы нужно добавить обработчик сообщения WM_MDIACTIVATE в MDI-окно:
// Находим родительское окно окна с разделителем (должно быть Main Frame) HWND GetMainFrame() { return ::GetParent(GetMDIFrame()); } // Перегружаем обработчик активации дочернего окна LRESULT OnMDIActivate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { // Дезактивируем дочернее окно ::SendMessage((HWND)wParam, WM_NCACTIVATE, FALSE, 0); // Активируем дочернее окно ::SendMessage((HWND)lParam, WM_NCACTIVATE, TRUE, 0); // Передаем фокус клиентскому окну MDI ::SetFocus(m_hWndMDIClient); // Переключаемся в меню дочернего окна или в меню по умолчанию if((HWND)lParam == m_hWnd && m_hMenu != NULL) { HMENU hWindowMenu = GetStandardWindowMenu(m_hMenu); MDISetMenu(m_hMenu, hWindowMenu); MDIRefreshMenu(); ::DrawMenuBar(GetMainFrame()); return 0; } else if((HWND)lParam == NULL) { // Последнее дочернее окно закрылось ::SendMessage(GetMainFrame(), WM_MDISETMENU, 0, 0); return 0; } bHandled = FALSE; return 1; }
Этот код с небольшими изменениями можно найти здесь.
Результат моей бурной фантазии приведен на рисунке 2. При написании этого пользовательского интерфейса использовалась библиотека Tab Controls, о которой мы поговорим позже.
Обзор дополнительных классов
В этом разделе мы рассмотрим дополнительные классы, которых нет в стандартной библиотеке, но которые часто бывают нужны. Все классы находятся в пространстве имен AWTL или начинаются с префикса AWTL_.
CSplitter
Этот класс не имеет ничего общего с описанным выше классом CSplitterWindow. Основное его достоинство в том, что он может использоваться не как контейнер, а как дополнение к control-у «собственного изготовления». Ниже я приведу пример создания ListBox-control-а с разделителем. Сейчас рассмотрим функции класса:
- SetSplitterRect – устанавливает прямоугольник, который будет разделяться сплиттером.
- GetSplitterRect – возвращает по ссылке прямоугольник, установленный функцией SetSplitterRect.
- GetWidthHeight – возвращает размер правой/верхней области.
- OnBeginTrack – вызывается в момент начала изменения позиции сплиттера.
- OnEndTrack – вызывается в момент окончания изменения позиции сплиттера.
Сплиттер может быть вертикальным или горизонтальным, это определяется параметром в конструкторе, а не параметром шаблона, как в WTL.
Пример использования
Ниже приводится практически полный пример subclassing-а субклассинга ListBox-а со сплиттером.
class CSplitterListCtrl : public CWindowImpl<CSplitterListCtrl, CListBox>, public COwnerDraw<CSplitterListCtrl>, public AWTL::CSplitter<CSplitterListCtrl> { typedef CWindowImpl<CSplitterListCtrl, CListBox> _baseclass; typedef AWTL::CSplitter<CSplitterListCtrl> _baseSplitter; public: CSplitterListCtrl():_baseSplitter(true){}; BOOL SubclassWindow(HWND hWnd) { BOOL b = _baseclass::SubclassWindow(hWnd); Init(); return b; } void OnEndTrack() { middle = GetWidthHeight(); InvalidateRect(0); } //Private-члены private: int middle; DECLARE_WND_CLASS(_T("SplitterListCtrl")) private: BEGIN_MSG_MAP(CPropertyListCtrl) CHAIN_MSG_MAP(_baseSplitter) MESSAGE_HANDLER(WM_CREATE, OnCreate) CHAIN_MSG_MAP_ALT(COwnerDraw<CSplitterListCtrl>, 1) END_MSG_MAP() void Init() { // Проверка стилей listbox-а const int required_styles = LBS_OWNERDRAWVARIABLE|LBS_NOTIFY|LBS_HASSTRINGS; const int stl = GetStyle(); ATLASSERT((stl & required_styles) == required_styles); CRect r; GetClientRect(&r); // Находим середину listbox middle = r.Width()/2; r.left = middle-2; r.right = r.left + 4; SetSplitterRect(r); } LRESULT OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) { Init(); return 0; } void DrawItem(LPDRAWITEMSTRUCT lpdi) { // Если нет элементов списка, пропустите это сообщение. if (lpdi->itemID == -1) return; CDCHandle dc(lpdi->hDC); TCHAR buf[100]; GetText(lpdi->itemID, buf); // Выводим текст dc.DrawText(buf, -1, &lpdi->rcItem, DT_VCENTER); // Рисуем разделитель dc.MoveTo(lpdi->rcItem.left + middle, lpdi->rcItem.top); dc.LineTo(lpdi->rcItem.left + middle, lpdi->rcItem.bottom); } };
ListBox выводит строки элементов и разделитель. Разделитель можно перетаскивать, при окончании изменения его позиции вызывается функция OnEndTrack. В ней запоминается позиция разделителя, который отрисовывается функции DrawItem. Использовать данный класс очень просто. В функции инициализации диалога вставьте следующий код:
list.SubclassWindow(GetDlgItem(IDC_LIST1)); list.AddString(_T("item1")); list.AddString(_T("item2")); list.AddString(_T("item3"));
, где list – это поле экземпляра класса CSplitterListBox.
ПРИМЕЧАНИЕ
ListBox должен иметь стили LBS_OWNERDRAWVARIABLE, LBS_NOTIFY и LBS_HASSTRINGS.
Если вы обратили внимание на карту сообщений класса CSplitterListBox, первый макрос переадресует сообщения в дочерний класс сплиттера. Зачем? Дело в том, что если вы в каком-либо обработчике CSplitterListBox не присвоите параметру bHandled значения FALSE, то последующий обработчик этого же сообщения не вызовется. Таким образом, если макрос переадресации сообщений сплиттеру будет стоять последним в карте сообщений, вполне возможно, он не будет правильно работать. Сам же сплиттер хоть и обрабатывает сообщения, но честно устанавливает параметр bHandled в FALSE, так что они доходят до карты сообщений класса CSplitterListBox.
ПРЕДУПРЕЖДЕНИЕ
Не забудьте включить макрос REFLECT_NOTIFICATIONS() в карту сообщений диалога. Это нужно для того, чтобы сообщение WM_DRAWITEM, посылаемое родительскому окну, то бишь диалогу, передавалось обратно в ListBox.
CDialogResize
Данный класс позволяет автоматически изменять размеры control-ов при изменении размеров диалога. Для того чтобы это стало возможным, вы должны написать специальную карту масштабирования. Более подробную информацию можно найти в [1].
Класс всем хорош, однако у него имеется одно серьезное ограничение: вы не можете изменять размер control-ов, расположенных на диалоге, «собственноручно». Если у вас, например, используется тот же сплиттер, то вам нужно изменять размеры control-ов в функции OnEndTrack, но для control-ов, участвующих в карте масштабирования, это невозможно. Ниже приводится класс, который решает эту проблему:
template<class T> class AWTL_CDialogResize : public CDialogResize<T> { public: void SetControlRect(DWORD dwCtrlId, const RECT& rc) { for(int i = 0; i < m_arrData.GetSize(); i++) { if (m_arrData[i].m_nCtlID == dwCtrlId) { m_arrData[i].m_rect = rc; CRect r; static_cast<T*>(this)->GetClientRect(&r); DlgResize_UpdateLayout(r.Width(), r.Height()); break; } } } RECT GetControlRect(DWORD dwCtrlId) const { for(int i = 0; i < m_arrData.GetSize(); i++) { if (m_arrData[i].m_nCtlID == dwCtrlId) { return m_arrData[i].m_rect; } } CRect r; r.SetRectEmpty(); return r; } }; //end of CDialogResize
У него всего две функции: первая устанавливает размеры заданного control-а, а вторая возвращает их. Если вам необходимо изменить размеры control-а вручную, вы запрашиваете (с помощью GetControlRect) размеры контрола, изменяете их, добавляя или вычитая из соответствующих координат значения, и затем, с помощью функции SetControlRect, устанавливаете новые размеры и позицию control-а.
CParentContainedWindow
Этот класс является наследником класса CContainedWindow, который позволяет обрабатывать сообщения некоторого дочернего окна в родительском, вернее в карте сообщений класса родительского окна. CContainedWindow – стандартный класс библиотеки ATL. Достаточно подробную документацию по нему можно найти в MSDN.
Класс CParentContainedWindow предназначен для использования карты сообщений, отличной от карты сообщений родительского окна. С помощью этого класса можно обрабатывать сообщения control-а (по существу, задавать его поведение) в классе, чье окно не является родительским для данного control-а. Эта схема приведена на рисунке 3:
Даже если вы не хотите изменять поведение control-а, этот класс очень полезен. Использование этого класса позволяет обрабатывать все сообщения WM_COMMAND, WM_NOTIFY и другие в связанном классе, что создает впечатление, будто он является родителем control-а.
Интерфейсная часть у класса изменилась ненамного – добавилось всего две функции:
- SetUnrealParent – устанавливает хендл окна, который будет передаваться с сообщением в карту сообщений.
- GetUnrealParent – возвращает хендл окна, передаваемый в обработчики карты сообщений.
Механизм работы класса: при создании дочернего control-а указывается хендл окна реального, настоящего родителя. Оконная процедура подменяется (вызовом метода SubclassWindow) для того, чтобы перехватывать определенные сообщения, отправляемые данным control-ом. При создании control-а указывается карта сообщений (по существу, процедура ProcessWindowMessage), куда будут поступать перехваченные сообщения. Так как по карте сообщений нельзя определить, к классу какого окна она принадлежит, можно установить хендл псевдородителя с помощью функции SetUnrealParent, чтобы обработчики данной карты сообщений первым параметром получали этот хендл. Если его не установить, в качестве хендла окна в обработчиках будет значение 0.
Самая важная процедура класса – это перехватчик сообщений:
LRESULT CALLBACK ParentWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { if (uMsg == WM_NOTIFY) { LPNMHDR pnmh = (LPNMHDR)lParam; if (pnmh->hwndFrom == m_hWnd) { LRESULT lRes = 0; BOOL bRet = m_pObject->ProcessWindowMessage(m_hWndParent, uMsg, wParam, lParam, lRes, 0); if (bRet || lRes != 0) return lRes; } } else if (uMsg == WM_PARENTNOTIFY && (wParam == WM_CREATE || wParam == WM_DESTROY)) { if (m_hWnd == (HWND)lParam) { LRESULT lRes; BOOL bRet = m_pObject->ProcessWindowMessage(m_hWndParent, uMsg, wParam, lParam, lRes, 0); return lRes; } } else if (uMsg == WM_COMMAND && (HWND)lParam == m_hWnd) { LRESULT lRes = 0; BOOL bRet = m_pObject->ProcessWindowMessage(m_hWndParent, uMsg, wParam, lParam, lRes, 0); return lRes; } else if (uMsg == WM_DRAWITEM) { LPDRAWITEMSTRUCT lpdi = (LPDRAWITEMSTRUCT)lParam; if (lpdi->hwndItem == m_hWnd) { LRESULT lRes = 0; BOOL bRet = m_pObject->ProcessWindowMessage(m_hWndParent, uMsg, wParam, lParam, lRes, 0); if (bRet || lRes) return lRes; } } else if (uMsg == WM_MEASUREITEM) { LPMEASUREITEMSTRUCT lpmi = (LPMEASUREITEMSTRUCT)lParam; if (lpmi->itemID == (UINT)GetWindowLong(GWL_ID)) { LRESULT lRes = 0; BOOL bRet = m_pObject->ProcessWindowMessage(m_hWndParent, uMsg, wParam, lParam, lRes, 0); if (bRet || lRes) return lRes; } } else if (uMsg == WM_COMPAREITEM) { LPCOMPAREITEMSTRUCT ci = (LPCOMPAREITEMSTRUCT)lParam; if (ci->hwndItem == m_hWnd) { LRESULT lRes = 0; BOOL bRet = m_pObject->ProcessWindowMessage(m_hWndParent, uMsg, wParam, lParam, lRes, 0); return lRes; } } else if (uMsg == WM_DELETEITEM) { LPDELETEITEMSTRUCT di = (LPDELETEITEMSTRUCT)lParam; if (di->hwndItem == m_hWnd) { LRESULT lRes = 0; BOOL bRet = m_pObject->ProcessWindowMessage(m_hWndParent, uMsg, wParam, lParam, lRes, 0); if (bRet || lRes) return lRes; } } else if ((uMsg == WM_CTLCOLORBTN || uMsg == WM_CTLCOLOREDIT || uMsg == WM_CTLCOLORLISTBOX || uMsg == WM_CTLCOLORSCROLLBAR || uMsg == WM_CTLCOLORSTATIC) && (HWND)lParam == m_hWnd) { LRESULT lRes = 0; BOOL bRet = m_pObject->ProcessWindowMessage(m_hWndParent, uMsg, wParam, lParam, lRes, 0); if (bRet || lRes) return lRes; } return CallWindowProc((WNDPROC)pParentProc, hWnd, uMsg, wParam, lParam); }
Как видите, процедура довольно большая, но не сложная. Она проверяет определенные сообщения и, если они пришли от «нашего» control-а, передает их в указанную при создании карту сообщений. Если сообщение там не обработано, вызывается функция родительского окна.
ПРИМЕЧАНИЕ
Более точно картина выглядит так: все оконные сообщения данного control-а попадают в его оконную процедуру, которая вызывает функцию ProcessWindowMessage, передающую управление в карту сообщений, указанную при создании control-а. Так как одна карта сообщений может содержать несколько секций, номер нужной секции также задается при создании control-а. Сообщения, предназначенные для родительского окна, попадают в нашу процедуру, где перенаправляются в ту же карту сообщений, только всегда в секцию с номером 0.
Для правильной работы класса нужно исправить кое-какую недоработку CContainedWindow, которая в определенных случаях может приводить к ошибке. Дело в том, что CContainedWindow регистрирует новый оконный класс, но не дерегистрирует его при уничтожении. Проблема и один из способов ее решения описаны в Q248400. Разработчики класса CContainedWindow просто забыли включить вызов функции OnFinalMessage при поступлении сообщения WM_NCDESTROY в оконную процедуру, так как это сделано для класса CWindowImplBase. Это и лишило их возможности проводить освобождение ресурсов при уничтожении окна. Классу CParentContainedWindow такая очистка необходима, поэтому пришлось несколько модифицировать код оконной процедуры WindowProc класса CContainedWindow:
if(uMsg != WM_NCDESTROY) lRes = pThis->DefWindowProc(uMsg, wParam, lParam); else { // восстанавливаем оконную процедуру, если нужно LONG pfnWndProc = ::GetWindowLong(pThis->m_hWnd, GWL_WNDPROC); lRes = pThis->DefWindowProc(uMsg, wParam, lParam); if(pThis->m_pfnSuperWindowProc != ::DefWindowProc && ::GetWindowLong(pThis->m_hWnd, GWL_WNDPROC) == pfnWndProc) ::SetWindowLong(pThis->m_hWnd, GWL_WNDPROC, (LONG)pThis->m_pfnSuperWindowProc); pThis->m_hWnd = NULL; #ifdef FINAL_MESSAGE_IN_CONTAINED_WINDOW pThis->OnFinalMessage(); #endif }
Перед включением файла atlwin.h необходимо определить макрос FINAL_MESSAGE_IN_CONTAINED_WINDOW. Теперь нужно определить функцию OnFinalMessage в классе CContainedWindow:
#ifdef FINAL_MESSAGE_IN_CONTAINED_WINDOW virtual void OnFinalMessage() { // исправление ошибки Q248400 LPTSTR szBuff = (LPTSTR)_alloca((lstrlen(m_lpszClassName) + 14) * sizeof(TCHAR)); lstrcpy(szBuff, _T("ATL:")); lstrcat(szBuff, m_lpszClassName); ::UnregisterClass(szBuff, _Module.GetModuleInstance()); } #endif
Использование данного класса связано с одним ограничением. Дело в том, что CWindowImplRoot содержит внутреннюю переменную "const MSG* m_pCurrentMsg;", в которую перед вызовом ProcessWindowMessage помещается указатель на копию текущей структуры MSG. Это поле используется рядом функций, например DefWindowProc() без параметров. Так как нет никакой возможности установить его из нашей функции перехвата, нельзя пользоваться следующими функциями:
- GetCurrentMessage()
- DefWindowProc()
Пример
Возьмем класс CSplitterListBox и сделаем так, чтобы сообщения от кнопки Cancel обрабатывались в нем, а не в диалоге.
Первым делом нужно добавить в класс диалога поле AWTL::CParentContainedWindowT<CButton> m_btn. Затем, в конструкторе, произвести инициализацию:
CDlg::CDlg() : m_btn((CMessageMap*)&list, 1) { }
Этим мы говорим о том, что будет использоваться карта сообщений control-а list с номером 1.
Теперь нужно подправить карту сообщений класса CSplitterListBox:
BEGIN_MSG_MAP(CPropertyListCtrl) CHAIN_MSG_MAP(_baseSplitter) MESSAGE_HANDLER(WM_CREATE, OnCreate) COMMAND_CODE_HANDLER(BN_CLICKED, OnClick) CHAIN_MSG_MAP_ALT(COwnerDraw<CSplitterListCtrl>, 1) ALT_MSG_MAP(1) // обработчики для кнопки END_MSG_MAP()
Сообщение WM_COMMAND с кодом BN_CLICKED будет обрабатываться в главной карте, а все сообщения от кнопки – в альтернативной карте с номером 1.
ПРИМЕЧАНИЕ
Даже если вы не хотите обрабатывать никакие сообщения от кнопки, необходимо создать альтернативную карту сообщений, так как в противном случае сработает следующий кусок кода функции ProcessWindowMessage:
default: \ ATLTRACE2(atlTraceWindowing, 0, _T("Invalid message map ID (%i)\n"), dwMsgMapID); \ ATLASSERT(FALSE); \ break; \
Для того чтобы заработал класс CParentContainedWindow, осталось только заменить оконную процедуру кнопки в функции инициализации диалога:
m_btn.SubclassWindow(GetDlgItem(IDCANCEL));
Полный пример использования класса можно найти в прилагаемом к статье исходном коде.
Обзор классов сайта CodeProject
Очень сложно создать конкурентоспособный продукт только стандартными средствами. WTL обеспечивает нас мощными инструментами создания «продвинутого» пользовательского интерфейса, но их все-таки недостаточно. В этом разделе я познакомлю вас с несколькими независимыми разработками, которые бесплатно распространяются в Internet, а именно – являются частью портала для программистов CodeProject. Как мне показалось, эти библиотеки очень слабо освещены в оригинальных статьях, поэтому я решил включить их обзор в эту статью.
Библиотека DockWins
Материал данного раздела основан на статье автора библиотеки Сергея Климова [2].
Ниже приведена сильно урезанная диаграмма классов в UML-нотации.
Подготовка проекта
- Скопируйте все файлы библиотеки в директорию, доступную компилятору.
- Подключите к проекту файл dockimpl.cpp, или добавьте запись #include <Dockimpl.cpp> в файл stdafx.cpp.
- Включите синхронную модель обработки исключений (опция /Ehs компилятора) и удалите макрос _ATL_MIN_CRT.
- Если вы используете библиотеку boost, определите макрос USE_BOOST.
- Добавьте в файл stdafx.h запись #include <DockMisc.h>.
Изменение главного фрейма MDI- или SDI-проектов
Включите файл DockingFrame.h в заголовочный файл главного фрейма приложения.
Поменяйте базовый класс главного фрейма приложения на dockwins::CDockingFrameImpl<CMainFrame> для SDI-приложения или на dockwins::CMDIDockingFrameImpl<CMainFrame> для MDI-приложений. Замените все упоминания старого базового класса на новый.
Добавьте в обработчик OnCreate функцию инициализации InitializeDockingFrame(). Ее параметры рассматриваются ниже.
Включите файл ExtDockingWindow.h в заголовочный файл клиентского окна.
Поменяйте базовый класс клиентского фрейма приложения на dockwins::CTitleDockingWindowImpl. В качестве одного из параметров шаблона базового класса укажите специальный класс, производный от CDockingWindowTraits, определяющий стили и свойства прилипающего (docking) окна. Они будут рассмотрены ниже.
Вот и все – компилируйте и запускайте проект. Если вам вдруг захочется, чтобы прилипающие окна автоматически скрывались (как это сделано в VC 7.0) – просто добавьте в stdafx.h заголовочный файл DWAutoHide.h.
Наиболее часто используемые функции класса CDockingFrameImplBase
Практически все функции этого класса в качестве параметра используют структуру DFDOCKPOS. Она объявлена следующим образом:
typedef struct tagDFDOCKPOS { DFMHDR hdr; DWORD dwDockSide; union { struct { unsigned long nBar; float fPctPos; unsigned long nWidth; unsigned long nHeight; }; RECT rcFloat; }; unsigned long nIndex; }DFDOCKPOS;
- dwDockSide может принимать одно из следующих значений:
Название | Назначение |
---|---|
CDockingSide::sSingle | Прилипающее окно занимает всю область, вытесняя другие уже прилипшие окна. Используется в комбинации со следующими значениями. |
CDockingSide::sRight | Окно прилипает к правому краю главного фрейма. |
CDockingSide::sLeft | Окно прилипает к левому краю главного фрейма. |
CDockingSide::sTop | Окно прилипает к верхнему краю главного фрейма. |
CDockingSide::sBottom | Окно прилипает к нижнему краю главного фрейма. |
Остальные параметры не так важны: nBar задает индекс области (начиная с нуля), к которой будет присоединено окно. Атрибут имеет значение, только если не менее двух окон присоединяются к одному краю.
- DockWindow – передаваемое в качестве параметра окно прилипает к задаваемому параметром dwDockSide краю главного фрейма приложения.
- InitializeDockingFrame – производит начальную инициализацию фрейма. В качестве параметра используются следующие значения:
Название | Назначение |
---|---|
CStyle::sUseSysSettings | Параметры определяются системными настройками. |
CStyle::sIgnoreSysSettings | Системные настройки игнорируются. Этот флаг используется совместно с двумя следующими. |
CStyle::sFullDrag | Изменение размеров окна приводит к его перерисовке. |
CStyle::sGhostDrag | Изменение размеров окна не приводит к его перерисовке. |
CStyle::sAnimation | Использовать анимационные эффекты при автоматическом скрытии окна. |
Основные функции класса CTitleDockingWindowImpl
- Undock – «отлепляет окно» от кромки.
- Float – восстанавливает прилепленное окно и делает его «плавающим».
- IsDocking – возвращает true, если окно прилеплено.
- Hide – скрывает окно.
- Show – показывает окно.
- Toggle – скрывает окно, если оно видно на экране, и показывает окно, если оно скрыто.
Если включена поддержка автоматического скрытия окон, функция PinUp скрывает окно за кромкой главного фрейма приложения. В качестве параметра указывается одно из значений, приведенных в предыдущей таблице.
Библиотека довольно сложна. В ней почти невозможно разобраться без помощи автора, так как комментарии практически отсутствуют, а информации в [2] чрезвычайно мало.
Библиотека Tab Controls
В данном разделе рассматривается библиотека tab-controls или, по-русски, библиотека закладок. Автором ее является Daniel Bowen, оригинальный обзор библиотеки можно найти в [3]. Ниже приведена диаграмма классов в UML-нотации. Методы и свойства опущены, так как в противном случае диаграмма не влезла бы ни в какие рамки.
Библиотека реализует различные tab-control-ы (производные от CСustomTabCtrl), а также несколько классов для использования этих control-ов в SDI- и MDI-приложениях.
Список функций класса CCustomTabCtrl:
- CreateNewItem – создает и возвращает закладку.
- DeleteItem – удаляет закладку.
- ScrollLeft – прокрутка закладок влево.
- ScrollRight - прокрутка закладок вправо.
- SubclassWindow – подмена оконной процедуры.
- SetImageList – задает список изображений (ImageList).
- GetImageList - получает список изображений.
- InsertItem – вставляет закладку в заданную позицию.
- MoveItem – передвигает закладку в заданную позицию.
- SwapItemPositions – меняет закладки местами.
- DeleteAllItems – удаляет все закладки.
- GetItem – возвращает закладку с заданной позиции.
- SetCurSel – активизирует закладку, находящуюся в определенной позиции.
- GetCurSel – возвращает позицию активной закладки.
- GetItemCount – возвращает общее количество закладок.
- FindItem – возвращает позицию некоторой закладки.
Ниже приведен список наиболее часто используемых функций закладок (CCustomTabOwnerImpl):
- UpdateTabText - используется для изменения названия закладки.
- UpdateTabToolTip - используется для изменения всплывающей подсказки закладки.
- AddIcon – добавляет иконку во внутренний список (image list).
- AddBitmap – добавляет картинку во внутренний список.
- UpdateTabImage – изменяет изображение для закладки.
- GetTabAreaHeight – возвращает высоту области, где расположены закладки.
- SetTabAreaHeight – устанавливает высоту области, где расположены закладки.
- RemoveTab – удаляет закладку.
- GetTabCtrl – возвращает tab-control.
Класс CTabbedFrameImpl является базовым для CTabbedChildWindow и CTabbedPopupFrame, которые предназначены для дочерних или всплывающих (popup) окон. Список функций класса CTabbedFrameImpl:
- SetReflectNotifications – устанавливает флаг отражения уведомлений.
- GetReflectNotifications – возвращает флаг отражения уведомлений.
- SetTabStyles – устанавливает стили закладок (см. таблицу из примера SDI-приложения).
- GetTabStyles – возвращает стили закладок.
- SetTabAreaHeight – переопределяет функцию базового класса CCustomTabOwnerImpl.
Класс CTabbedMDIFrameWindowImpl переопределяет функциональность базового стандартного класса CMDIFrameWindowImpl для поддержки просмотра дочерних окон как закладок в MDI-приложении. Наиболее часто используемые функции класса:
- CreateMDIClient – переопределяет функцию базового класса.
- SubclassMDIClient – позволяет подменить оконную процедуру клиентского окна MDI-приложения на процедуру, которая делегирует вызов классу CTabbedMDIClient.
- SetTabOwnerParent – делегирует вызов классу CTabbedMDIClient.
- GetTabOwnerParent – делегирует вызов классу CTabbedMDIClient.
Функции класса CTabbedMDIClient, который предназначен для переопределения окна стандартного класса MDIClient. Наиболее часто используемые функции класса:
- SetTabOwnerParent – устанавливает хендл окна родителя.
- GetTabOwnerParent – возвращает хендл окна родителя.
- GetTabOwner – возвращает экземпляр (обычно класса CMDITabOwner) окна владельца.
- UseMDIChildIcon – устанавливает флаг использования иконки дочернего окна.
- SetDrawFlat – устанавливает флаг CTCS_FLATEDGE.
- GetDrawFlat – позволяет узнать, установлен ли флаг CTCS_FLATEDGE.
- SubclassWindow – позволяет подменить оконную процедуру на свою.
Примеры
Начнем с простого примера – создадим MDI-приложение, для дочерних окон которого организуются специальные закладки.
1. Создайте простое MDI-приложение.
2. Добавьте в stdafx.h следующие файлы.
#include <atlwin.h> #include <atlframe.h> #include <atlctrls.h> #include <atlctrlw.h> #include <atlmisc.h> #include <TabbedMDI.h> #include <DotNetTabCtrl.h>
Для того чтобы компилятор нашел последние два файла, зайдите в Tools->Options->Directories и добавьте новую директорию в категорию Include files.
3. Главный фрейм приложения нужно унаследовать не от стандартного CMDIFrameWindowImpl, а от CTabbedMDIFrameWindowImpl.
class CMainFrame : public CTabbedMDIFrameWindowImpl<CMainFrame, CTabbedMDIClient<CDotNetTabCtrl<CTabViewTabItem> > >, ...
4. Вместо стандартного CMDICommandBarCtrl нужно использовать CTabbedMDICommandBarCtrl:
CTabbedMDICommandBarCtrl m_CmdBar; // CMDICommandBarCtrl m_CmdBar;
5. Дочерний фрейм приложения нужно унаследовать от CTabbedMDIChildWindowImpl:
class CChildFrame : public CTabbedMDIChildWindowImpl<CChildFrame> //public CMDIChildWindowImpl<CChildFrame>
Вот и все, компилируйте и запускайте приложение. Оно должно выглядеть примерно так:
Для SDI-приложения необходимо выполнить следующие шаги:
1. Добавьте в stdafx.h следующие файлы:
#include <atlmisc.h> #include <TabbedMDI.h> #include <DotNetTabCtrl.h>
2. Добавьте новое поле в класс CMainFrame – оно будет клиентским окном.
CTabbedChildWindow<CDotNetTabCtrl<CTabViewTabItem> > m_tabbedChildWindow;
3. Функцию OnCreate класса CMainFrame измените так:
m_tabbedChildWindow.SetReflectNotifications(true); m_tabbedChildWindow.SetTabStyles(CTCS_TOOLTIPS); m_tabbedChildWindow.Create(m_hWnd, rcDefault); m_hWndClient = m_tabbedChildWindow; m_view.Create(m_tabbedChildWindow, rcDefault, _T("First view"), WS_CHILD|WS_VISIBLE|WS_CLIPSIBLINGS|WS_CLIPCHILDREN); m_tabbedChildWindow.DisplayTab(m_view, TRUE, TRUE); // m_hWndClient = m_view.Create(m_hWnd, rcDefault, NULL, // WS_CHILD|WS_VISIBLE|WS_CLIPSIBLINGS|WS_CLIPCHILDREN, WS_EX_CLIENTEDGE);
Это все. Теперь можете компилировать и запускать приложение. Для добавления других представлений (view) создайте соответствующий класс, затем в функции OnCreate создайте окно данного представления и добавьте его к окну закладок с помощью функции DisplayTab.
Рассмотрим наиболее часто используемые стили закладок:
Константа | Значение | |
---|---|---|
CTCS_BOTTOM | 0x0002 | Закладки располагаются внизу. |
CTCS_CLOSEBUTTON | 0x0008 | Появляется кнопка закрытия. При ее нажатии приходит сообщение WM_NOTIFY с кодом CTCN_CLOSE. Надо заметить, что класс CTabbedMDIFrameWindowImpl устанавливает этот стиль и обрабатывает соответствующее извещение автоматически. |
CTCS_BOLDSELECTEDTAB | 0x2000 | Название активной закладки выделяется жирным шрифтом. |
CTCS_TOOLTIPS | 0x4000 | Для закладки выводится всплывающая подсказка. |
У любой закладки должно быть имя. Имя обычно берется из названия клиентского окна, добавляемого с помощью функции DisplayTab. DisplayTab также пытается получить иконку окна с помощью сообщения WM_GETICON и функции GetClassLong(GCL_HICONSM). Если это не удается, закладка выводится без иконки. Если окно не имеет текста и иконки, можно воспользоваться функцией AddTabWithIcon. Она позволяет указать для добавляемого окна как текст закладки, так и ее иконку.
Функция SetReflectNotifications управляет тем, будут ли в карте сообщений Tab-а отражаться уведомления от дочерних control-ов. В случае control-а CustomDraw нужно вызвать эту функцию, передав в качестве параметра значение true, в противном случае вы не сможете обрабатывать соответствующие уведомления о прорисовке и проч.
Параметр шаблона CTabbedChildWindow задает тип закладки или ее вид. Более подробную информацию об этом можно получить в [3].
Совместное использование библиотеки TabControls и DockWins
Чтобы создать пользовательский интерфейс, подобный VS 7.0, нужно проделать следующие шаги:
1. Создайте с помощью мастера MDI-проект.
2. Включите в проект (в файл stdafx.h) следующие файлы:
#include <DockMisc.h> #include <TabDockingBox.h> #include <TabbedMDI.h> #include <DotNetTabCtrl.h> #include <ExtDockingWindow.h> #include <TabbedDockingWindow.h>
3. В заголовочный файл главного фрейма приложения (mainfrm.h) включите файл DockingFrame.h.
4. Замените базовый класс главного фрейма CMDIFrameWindowImpl на dockwins::CMDIDockingFrameImpl.
5. Создайте поле в классе главного фрейма приложения типа CTabbedMDIClient<CDotNetTabCtrl<CTabViewTabItem> >.
6. Замените тип поля m_CmdBar с CMDICommandBarCtrl на CTabbedMDICommandBarCtrl. После это класс главного фрейма приложения должен выглядеть примерно так:
class CMainFrame : public dockwins::CMDIDockingFrameImpl<CMainFrame>, public CUpdateUI<CMainFrame>, public CMessageFilter, public CIdleHandler { typedef dockwins::CMDIDockingFrameImpl<CMainFrame> _baseClass; public: DECLARE_FRAME_WND_CLASS(NULL, IDR_MAINFRAME) //CMDICommandBarCtrl m_CmdBar; CTabbedMDIClient<CDotNetTabCtrl<CTabViewTabItem> > m_tabbedClient; CTabbedMDICommandBarCtrl m_CmdBar; ...
7. В обработчик OnCreate главного фрейма приложения вставьте следующий код:
InitializeDockingFrame(); m_tabbedClient.SetTabOwnerParent(m_hWnd); m_tabbedClient.SubclassWindow(m_hWndMDIClient); m_CmdBar.UseMaxChildDocIconAndFrameCaptionButtons(false); m_CmdBar.SetMDIClient(m_hWndMDIClient);
8. Исправьте ошибку, вносимую мастером создания проекта – обработчик OnFileNew главного фрейма приложения:
CChildFrame* pChild = new CChildFrame; pChild->CreateEx(m_hWndClient); //pChild->CreateEx(m_hWndMDIClient);
9. После этого остается только исправить в дочернем фрейме приложения (CChildFrame) базовый класс с CMDIChildWindowImpl на CTabbedMDIChildWindowImpl
Это все! Компилируйте и запускайте проект. У вас должно получится что-то вроде этого
ПРИМЕЧАНИЕ
Если при компиляции проекта у вас возникает ошибка «fatal error C1076: compiler limit : internal heap limit reached;», не пугайтесь. Для компиляции такого огромного количества шаблонов у компилятора просто не хватает памяти. Увеличить объем кучи можно опцией Zm. В Project Options добавьте ключ /Zm200, что расширит объем кучи до двухсот мегабайт.
Еще одно изменение касается простых, не MDI-окон (таких, как окно Output на рисунке). Чтобы они работали в новом проекте, необходимо заменить базовый класс dockwins::CTitleDockingWindowImpl на dockwins::CBoxedDockingWindowImpl, а так же стиль dockwins::COutlookLikeTitleDockingWindowTraits на dockwins::COutlookLikeBoxedDockingWindowTraits. После изменений класс окна должен выглядеть примерно так:
class CSampleDockingWindow : public dockwins::CBoxedDockingWindowImpl<CSampleDockingWindow, CWindow, dockwins::COutlookLikeBoxedDockingWindowTraits > { typedef dockwins::CBoxedDockingWindowImpl<CSampleDockingWindow,CWindow, dockwins::COutlookLikeBoxedDockingWindowTraits> _baseClass; ...
Более продвинутые возможности этих библиотек можно изучить по исходным кодам из [2] и [3], или заходя на наши форумы. :)
CSSFileDialog
С выходом Windows 2000 появился новый стандартный диалог открытия/сохранения файлов. Он отличается от обычного диалога наличием тулбара в левой части, позволяющего быстро переключаться на специальные папки (Рабочий стол, Мои документы и проч.).
Все вы прекрасно знакомы со структурой OPENFILENAME, которая используется для создания диалогов открытия/сохранения в старом стиле. Если открыть файл CommDlg.h, можно увидеть такое определение этой структуры (если в вашем файле ее нет, скачайте новый Platform SDK).
typedef struct tagOFNW { DWORD lStructSize; HWND hwndOwner; ... #if (_WIN32_WINNT >= 0x0500) void * pvReserved; DWORD dwReserved; DWORD FlagsEx; #endif // (_WIN32_WINNT >= 0x0500) } OPENFILENAMEW, *LPOPENFILENAMEW;
Как видите, для использования новых возможностей необходимо объявить макрос _WIN32_WINNT со значением, равным или большим 0x0500 (версия Win2k). Также необходимо правильно заполнить поле lStructSize и указать нужные расширенные флаги (FlagsEx). В данный момент определено только одно значение (файл commdlg.h)
#if (_WIN32_WINNT >= 0x0500) #define OFN_EX_NOPLACESBAR 0x00000001 #endif // (_WIN32_WINNT >= 0x0500)
Таким образом, при указании нулевого значения стандартный диалог представляется в расширенном виде, с тулбаром слева. При указании единицы, диалог появляется в старом стиле.
Более подробную информацию об этом, а также пример реализации на MFC стандартного диалога расширенного стиля можно найти в колонке С++ Q&A августовского номера журнала MSDN Magazine за 2000 год.
Для использования расширенного диалога из WTL можете использовать уже написанный класс CSSFileDialog, который можно скачать здесь. Там имеются кое-какие недоработки и неточности, но, в общем и целом, класс неплохой.
Заключение
Напоследок хочется сказать, что WTL создавалась и поддерживается небольшой группой энтузиастов, официально не поддерживается Microsoft, и вряд ли уже когда-нибудь это изменится. Это объясняет текущие недостатки библиотеки, связанные с проектированием и многочисленными ошибками. Но, несмотря на эти недостатки, я считаю, библиотека имеет будущее, так как является очень гибкой, расширяемой и открытой.
Литература
Ведущий рассылки: Алекс Jenter jenter@rsdn.ru
Публикуемые в рассылке материалы принадлежат сайту RSDN.
http://subscribe.ru/
E-mail: ask@subscribe.ru |
Отписаться
Убрать рекламу |
В избранное | ||