ПРОГРАММИРОВАНИЕ    НА    V I S U A L   C + +
РАССЫЛКА САЙТА       
RSDN.RU  

    Выпуск No. 96 от 30 июня 2003 г.
   
Подписчиков: 21763 

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

Приветствую!


 CТАТЬЯ

Эффективное использование WTL
Описание стандартных и нестандартных классов

Автор: Алексей Ширшов
Источник: RSDN Magazine #1-2003
Предисловие

Эта статья для тех, кто, как и я, продолжает использовать старую добрую VC 6.0 с библиотеками ATL 3.0 и WTL 7.0. Наступает эра .NET. У .NET много преимуществ, но, как мне кажется, и много недостатков. Самый распространенный лозунг бывшего С++’ника, перешедшего на .NET: «Когда у меня непонятно где и почему возникала ошибка нарушения доступа (Access Violation), я не знал что делать! Сейчас я пользуюсь C#, и подобных проблем не испытываю». Что ж, каждому свое, отмечу только, что в истории имеется немало примеров создания на С и С++ крупных программных продуктов, пользовавшихся популярностью на рынке. Качество конечного программного продукта зависит от многих вещей, но не в первую очередь от языка. Намного больше зависит от выбора библиотеки. Выбор библиотеки влияет на скорость разработки. Чем удобнее библиотека для программиста, тем больше она «весит», и тем больше абстрагируется от реальных механизмов операционной системы. «Хорошие» библиотеки снижают планку требуемой квалификации программиста. За примерами далеко ходить не надо – MFC, VCL, .Net Framework. Погружаясь в мягкий и комфортный мир этих библиотек, программист забывает о некоторых реалиях, пусть они даже имеют глубоко технический характер. Чем лучше библиотека, тем меньше в ней свободы. Вы можете пользоваться только заранее определенными (кем-то) механизмами по заранее определенным схемам. В данном случае свобода приносится в жертву переносимости, удобству, скорости разработки и изучению. Не думаю, что это является для вас новостью. Используя голый WinAPI, можно написать практически все, что угодно, любые самые «навороченные» control-ы а-ля WinForms. Весь вопрос в том, как много времени у вас на это уйдет, и какой квалификацией вы должны для этого обладать.

Библиотека WTL делает маленький шаг в сторону больших библиотек. Он маленький потому, что WTL – это всего лишь тонкая обертка над WinAPI. Однако огромное количество рутинных операций WTL берет на себя, позволяя создавать компактные и эффективные приложения (такие же, как и при использовании голого WinAPI), используя при этом красивые объектно-ориентированные обертки. Ювелирное применение шаблонов делает WTL очень гибкой и элегантной библиотекой. WTL очень компактна и полностью доступна в исходных кодах. Мне многое в ней не нравится, но я понял и научился использовать ее мощь. Она намного сложнее MFC (главным образом из-за тех же шаблонов и большого количества макросов), но те, кто программировал на WinAPI, должны ее усвоить с легкостью.

С помощью стандартных и нестандартных инструментов библиотеки WTL вы можете создавать пользовательский интерфейс, ничем не уступающий WinForms, при этом последняя будет «тянуть» с собой огромный рантайм. Выводы делайте сами, я же предпочитаю гибкость, независимость и прозрачность.

Обзор стандартных классов

В этом разделе рассмотрены стандартные классы библиотеки WTL, которые почему-то не нашли должного отражения в статьях RSDN, CodeProject и др. Здесь представлены их назначение, функциональные возможности и баги.

CAppModule

Класс приложения, пришедший на смену CComModule, предназначен для хранения циклов сообщений (CMessageLoop) потоков приложения, а также для рассылки сообщения WM_SETTINGCHANGE всем подписавшимся окнам. Внутренние члены класса CAppModule не защищены критическими секциями, что можно рассматривать как баг.

  • Init – Вызывает метод Init базового класса и производит инициализацию данных класса, а именно: сохраняет идентификатор текущего потока – главного потока приложения, и создает массив для циклов сообщений.
  • Term – удаляет окно, рассылающее сообщение WM_SETTINGCHANGE, а также удаляет массивы сообщений и подписчиков WM_SETTINGCHANGE. Затем вызывается функция Term базового класса.
  • AddMessageLoop – добавляет цикл сообщений, используя идентификатор текущего потока. Если цикл сообщений с данным идентификатором потока уже существует, срабатывает ATLASSERT.
  • RemoveMessageLoop – удаляет цикл сообщений, используя идентификатор текущего потока.
  • AddSettingChangeNotify – добавляет окно-подписчик для сообщения WM_SETTINGCHANGE. Если массив подписчиков не создан – он создается, если окно верхнего уровня (top-level window) не создано – оно создается.
  • RemoveSettingChangeNotify – удаляет окно-подписчик сообщения WM_SETTINGCHANGE.

Для чего нужны несколько циклов обработки сообщений? Для того, чтобы каждый желающий класс мог создать и обрабатывать свои Idle-обработчики и фильтры. Для чего нужно создавать подписку на сообщение WM_SETTINGCHANGE? Дело в том, что это сообщение рассылается только окнам верхнего уровня (окна, не имеющие родителей) и не рассылается дочерним. Для того, чтобы дочернее окно могло обрабатывать сообщение WM_SETTINGCHANGE, его нужно подписать на это сообщение с помощью функции AddSettingChangeNotify. Механизм рассылки следующий: класс CAppModule создает скрытое окно верхнего уровня, и в его оконной процедуре обрабатывает сообщение WM_SETTINGCHANGE, перебирая массив подписчиков и посылая им это сообщение. Вот часть кода:


for(int i = 1; i < pModule->m_pSettingChangeNotify->GetSize(); i++)
  ::SendMessageTimeout((*pModule->m_pSettingChangeNotify)[i], uMsg, 
      wParam, lParam, SMTO_ABORTIFHUNG, 1500, NULL);

На этом можно завершить обзор класса CAppModule, но хочется сказать об еще одной то ли ошибке, то ли особенности. Ни один из стандартных классов WTL, а это CCommandBar, CScrollImpl, CSplitterImpl и др., имея в карте сообщений обработчик WM_SETTINGCHANGE, не подписывается на это сообщение. То ли забыли ребята из Microsoft подписаться на WM_SETTINGCHANGE, то ли забыли, как работает сообщение, но факт остается фактом: при приходе WM_SETTINGCHANGE окнам верхнего уровня ни один из стандартных control-ов WTL его не получает.

CServerAppModule

Этот класс предназначен для использования в приложениях, являющихся одновременно Automation-серверами. Он включается в проект вместо CAppModule, если вы установите галочку «Create as COM server» при работе мастера создания проектов. CAppModule является базовым классом для CServerAppModule. Вот список переопределенных и новых функций класса:

  • Init – Вызывает метод Init базового класса и устанавливает переменную m_dwTimeOut в 5000, а m_dwPause в 1000.
  • Term – удаляет объект ядра Event m_hEventShutdown, если он был создан, и вызывает функцию Term базового класса.
  • MonitorShutdown – ожидает события m_hEventShutdown и, при его возникновении, останавливает фабрику класса (CoSuspendClassObjects), удаляет событие и посылает главному потоку приложения сообщение WM_QUIT.
  • StartMonitor – создает неименованный объект-событие с автоматическим сбросом m_hEventShutdown и создает поток, функцией которого является MonitorProc.
  • MonitorProc вызывает MonitorShutdown.
  • Две версии ParseCommandLine, обрабатывающие командную строку на наличие строк «UnregServer» и «RegServer». Отличаются они тем, что одной передается идентификатор ресурса, который она передает функции UpdateRegistryFromResource, а второй передается строка AppID, которую она передает в функции-помощники UnregisterAppId и RegisterAppId.
  • RegisterAppId – создает раздел в реестре в HKCL\AppID
  • UnregisterAppId – удаляет раздел в реестре в HKCL\AppID
  • FindOneOf – используется функциями ParseCommandLine для разбора командной строки.
  • Unlock – переопределяет функцию базового класса CComModule. Устанавливает событие в сигнальное состояние, если функция CComModule::Unlock вернула значение, равное нулю.

Класс работает следующим образом: в функции WinMain после инициализации разбирается командная строка (при этом почему-то не используются функции ParseCommandLine). Если приложение запущено не для регистрации/дерегистрации, вызывается функция StartMonitor. Она создает поток, в котором, в конечном счете, вызывается функция класса приложения MonitorShutdown. Этот поток «засыпает» в ожидании события. После вызова StartMonitor вызывается функция регистрации фабрики класса в ROT (RegisterClassObjects). Если все прошло успешно, вызывается либо глобальная функция Run, приводящая к созданию главного окна приложения, либо функция Run локально созданного цикла сообщений (CMessageLoop). При уменьшении счетчика ссылок модуля до нуля, событие устанавливается в сигнальное состояние, что приводит к завершению дополнительного потока и посылке главному потоку сообщения WM_QUIT. Если приложение запущено интерактивно (в командной строке не присутствуют «Automation» или «Embedding»), после создания главного окна приложения счетчик ссылок модуля увеличивается, что препятствует завершению приложения после удаления последнего объекта.

После выхода из главного цикла главного потока приложения, производится очистка: фабрика класса удаляется из ROT и вызывается функция класса приложения Term.

Видимых багов в классе замечено не было, однако код написан очень посредственно. Например:


HRESULT RegisterAppId(LPCTSTR pAppId)
{
  CRegKey keyAppID;
  ...
}

Я бы поставил здесь ATLASSERT для проверки входного параметра. То же касается функций ParseCommandLine и RegisterAppId. Внутренние члены не защищены критической секцией.

CThreadManager

Этот класс создается мастером создания проектов, если вы выбираете тип Multi SDI Application. Такое приложение отличается от обычного SDI тем, что оно поддерживает несколько окон (как MDI), но для каждого создается свой элемент в таскбаре (TaskBar). Так работает, например, Internet Explorer. Все появляющиеся в таскбаре окна принадлежат одному процессу, однако исполняются в разных потоках.

  • AddThread – создает новый поток и добавляет его хендл в массив хендлов потоков. В качестве процедуры потока указывается функция RunThread.
  • RemoveThread – закрывает хендл потока. Поиск хендла производится по индексу – порядковому номеру создания потока.
  • Run – вызывается главным потоком приложения. Эта функция входит в бесконечный цикл, вызывая на каждой итерации функцию MsgWaitForMultipleObjects, куда в качестве параметра передается массив хендлов потоков. Функция ждет, пока не придет любое оконное сообщение, или один из потоков не завершится. При завершении одного из потоков вызывается функция RemoveThread. Если пришло оконное сообщение WM_USER, создается новый поток (AddThread).
  • RunThread – функция потока, которая создает локально цикл сообщений, добавляет его в класс приложения, создает окно и входит в цикл выборки сообщений.

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


::PostThreadMessage(_Module.m_dwMainThreadID, WM_USER, 0, 0L);

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

Баги
  • AddThread

Проблемный код:


_RunData* pData = new _RunData;
...
DWORD dwThreadID;
HANDLE hThread = ::CreateThread(NULL, 0, RunThread, pData, 0, &dwThreadID);
if(hThread == NULL)
{
  ::MessageBox(NULL, _T("ERROR: Cannot create thread!!!"), 
      _T("ProjectName"), MB_OK);
  return 0;
}

Так как память, выделенная под _RunData, освобождается только в функции потока RunThread, ошибка при создании потока приводит к потере этой памяти. Должно быть:


if(hThread == NULL)
{
  ::MessageBox(NULL, _T("ERROR: Cannot create thread!!!"), 
      _T("ProjectName"), MB_OK);
  delete pData;
  return 0;
}

  • RunThread

Проблемный код:


_RunData* pData = (_RunData*)lpData;
CMainFrame wndFrame;

if(wndFrame.CreateEx() == NULL)
{
  ATLTRACE(_T("Frame window creation failed!\n"));
  return 0;
}

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


_RunData* pData = (_RunData*)lpData;
CMainFrame wndFrame;

if(wndFrame.CreateEx() == NULL)
{
  ATLTRACE(_T("Frame window creation failed!\n"));
  delete pData;
  return 0;
}

  • Run

Проблема впервые описана в http://groups.google.com/groups?q=CTheardManager&hl=ru&lr=&ie=UTF-8&oe=UTF-8&scoring=d&selm=ehrCNPukAHA.2176%40tkmsftngp03&rnum=1. Вкратце суть ее в том, что, если перед запуском главного потока приложения (CThreadManager::Run) вызвать модальный диалог, то процесс не завершается после закрытия всех окон.

Проблемный код:


else if(dwRet == (WAIT_OBJECT_0 + m_dwCount))
{
  ::GetMessage(&msg, NULL, 0, 0);
  if(msg.message == WM_USER)
    AddThread("", SW_SHOWNORMAL);
  else
    ::MessageBeep((UINT)-1);
}

Если честно, то я не понял, почему возникает проблема, но решил ее. Дело в том, что после закрытия диалогового окошка и вызова CThreadManager::Run функция ожидания сообщений и хендлов потоков:


dwRet = ::MsgWaitForMultipleObjects(m_dwCount, m_arrThreadHandles, FALSE, INFINITE, QS_ALLINPUT);

возвращает управление сразу, dwRet при этом равен 1. Управление передается проблемному коду, описанному выше, и главный поток «зависает» на функции GetMessage().

После закрытия всех окон соответствующие дочерние потоки, созданные с помощью AddThread, завершаются, но возвращения из функции GetMessage для главного потока так и не происходит, поэтому процесс не завершается. Кроме этого, функция AddThread принимает первым параметром аргумент типа LPTSTR, в проблемном коде передается LPSTR. Вот как может выглядеть решение:


else if(dwRet == (WAIT_OBJECT_0 + m_dwCount))
{
#ifdef _DEBUG
  static int h = 0;
  TCHAR buf[100];
  ATLTRACE(_itot(h++, buf, 10));
  ATLTRACE(_T("\n"));
#endif
  BOOL ms = ::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE);
  if(ms && msg.message == WM_USER)
    AddThread(_T(""), SW_SHOWNORMAL);
  else
    ::MessageBeep((UINT)-1);
}

CSplitterWindow и CHorSplitterWindow

Классы реализуют окна-контейнеры с разделителями. Это дочерние окна от базового, шаблонного класса CSplitterWindowT, реализованные в файле atlsplit.h. Иерархия классов представлена на рисунке 1.

Рисунок

Рассмотрим наиболее важные функции и члены класса CSplitterWindowImpl.

  • Функция SetSplitterPos устанавливает позицию разделителя. Если передано значение –1, разделитель устанавливается посередине.
  • Функция GetSplitterPos возвращает позицию разделителя.
  • Поле m_cxyMin устанавливает минимальные размеры областей справа и слева для вертикального режима, и сверху и снизу – для горизонтального сплиттера. По умолчанию устанавливается в 0.
  • Функция SetSinglePaneMode устанавливает режим, в котором контейнер содержит только одну область. Разделитель удаляется, и в окне-контейнере остается только одна из областей, задаваемая в параметре. Вот полный список значений:
Константа Значение Описание
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, и вряд ли уже когда-нибудь это изменится. Это объясняет текущие недостатки библиотеки, связанные с проектированием и многочисленными ошибками. Но, несмотря на эти недостатки, я считаю, библиотека имеет будущее, так как является очень гибкой, расширяемой и открытой.

Литература
  1. Использование WTL. Часть 2.
  2. Библиотека DockWins.
  3. Библиотека закладок.


Ведущий рассылки: Алекс Jenter   jenter@rsdn.ru
Публикуемые в рассылке материалы принадлежат сайту RSDN.

| Предыдущие выпуски     | Статистика рассылки