Эффективное использование WTL

Часть 2

Автор: Алексей Ширшов
The RSDN Group

Источник: RSDN Magazine #4-2003
Опубликовано: 03.04.2004
Версия текста: 1.0
Введение
Стандартные классы
Класс CEditCommands
Класс CDragListBox
Дополнительные классы
CInplaceEditTooltipCtrl
COpenFileDialogEx
CRolloutContainerT
CDateTimeCtrl и CDateTimeRangeCtrl
CPrintPreviewWindowEx
CLinkEditCtrl
Заключение

Примеры к статье

Введение

Библиотека WTL развивается и взрослеет. Несмотря на многие мрачные прогнозы довольно видных мужей, количество вопросов в конференциях по WTL растет, библиотека начинает применяться в серьезных проектах, а количество обнаруженных багов в месяц – снижается. Это хорошая тенденция.

На CodeProject появляются уже не поверхностные how to, а подробные и дотошные статьи. И, хотя они предназначены для новичков (находятся в категории beginners), уровень материала довольно высок. На момент создания этой статьи последней официальной версией WTL была седьмая, но возможно, в момент, когда вы читаете это вступление, версия WTL 7.1 уже выйдет. Очень хочется, чтобы она была свободна от тех недочетов и ошибок, которыми изобиловали предыдущие версии. Однако вряд ли туда будут включены некоторые нужные, но не стандартные классы, которым, в основном, и посвящена статья.

Стандартные классы

Большинство стандартных классов очень хорошо рассматриваются во всевозможных статьях и обучалках (tutorials), однако некоторые по удивительным причинам опускаются. К счастью, с каждой статьей их становится все меньше. Надеюсь, после этой статьи их не будет совсем.

Начнем с простого.

Класс CEditCommands

Этот класс предназначен для помощи программисту при обработке команд от edit control-а. К таким командам относятся операции работы с буфером обмена (вставка, удаление с копированием (cut) и проч.), операции запроса состояния (есть ли выделенный текст, можно ли скопировать текст и проч.), которые используются для обновления соответствующих пунктов меню, и некоторые другие команды. Все команды запроса состояния (state helpers) пользуются двумя внутренними функциями: HasSelection и HasText. Первая, посылая полю ввода сообщение EM_GETSEL, проверяет, есть ли выделенный текст, и возвращает соответствующее булево значение. Вторая, с помощью функции GetWindowTextLength, выполняет проверку наличия текста в control-е. Теперь легко разобраться в функциях запроса состояния:

  BOOL CanCut() const
  { return HasSelection(); }
  BOOL CanCopy() const
  { return HasSelection(); }
  BOOL CanClear() const
  { return HasSelection(); }
  BOOL CanSelectAll() const
  { return HasText(); }
  BOOL CanFind() const
  { return HasText(); }
  BOOL CanRepeat() const
  { return HasText(); }
  BOOL CanReplace() const
  { return HasText(); }
  BOOL CanClearAll() const
  { return HasText(); }

Эти команды могут использоваться в операциях обновления пользовательского интерфейса (update UI) в функции OnIdle.

Класс CEditCommands содержит карту сообщений (читай функцию ProcessWindowMessage), которая предназначена для обработки команд пользователя. Эти команды должны иметь стандартные идентификаторы, например ID_EDIT_COPY. Пункт меню или кнопка на тулбаре должны иметь именно такие идентификаторы, для того чтобы класс CEditCommands смог их обработать. Эти идентификаторы определены в файле atlres.h. Обработчики команд от control-ов с такими идентификаторами помещены в карту сообщений за номером 1. Чтобы ее подключить, вы должны воспользоваться макросом CHAIN_MSG_MAP_ALT, например, так:

  BEGIN_MSG_MAP(_thisClass)
    CHAIN_MSG_MAP_ALT(_EditCommandsClass, 1)
  END_MSG_MAP()

Класс представления на основе control-а CEdit может выглядеть так:

        class CEditView : public CWindowImpl<CEditView, CEdit>,
  public CEditCommands<CEditView>
{
  typedef CWindowImpl<CEditView, CEdit> _thisClass;
  typedef CEditCommands<CEditView> EditCommandsClass;

public:
  DECLARE_WND_SUPERCLASS(NULL, CEdit::GetWndClassName())

  BOOL PreTranslateMessage(MSG*)
  {
    return FALSE;
  }

  BEGIN_MSG_MAP(_thisClass)
    CHAIN_MSG_MAP_ALT(_EditCommandsClass, 1)
  END_MSG_MAP()
};

Сообщения от меню и тулбара приходят в главный фрейм приложения, а не в класс представления. Эти команды нужно каким-либо образом передать нашему классу. В зависимости от типа проекта, нужно поступать следующим образом:

Кроме этого, в случае MDI приложения для обновления соответствующих пользовательских элементов следует создать в дочернем фрейме фоновый обработчик (idle handler). Это делается путем наследования от класса CIdleHandler, определения карты обновлений пользовательского интерфейса и реализации обработчика OnIdle.

В библиотеке WTL также имеется класс CRichEditCommands – наследник CEditCommands, предназначенный для работы с rich edit control. Он переопределяет всего три функции, а его использование аналогично описанному сценарию.

Класс CDragListBox

Этот класс предназначен для реализации технологии drag&drop для элементов списка. Он предоставляет три сервисные функции, являющиеся оболочками над соответствующими функциями WinAPI:

Для осуществления операций drag&drop родительское окно должно обрабатывать четыре уведомления от списка:

Для упрощения жизни программиста (девиз WTL) библиотека содержит стандартный класс CDragListNotifyImpl, который, как и CEditCommands помогает обрабатывать эти уведомления. Он содержит карту сообщений и преобразует приведенные выше четыре сообщения в вызов функций OnBeginDrag, OnCancelDrag, OnDragging и OnDropped соответственно.

Давайте рассмотрим код простого класса диалога, на котором размещен listbox.

        class CDlg1 : public CDialogImpl<CDlg1>,
  public CDragListNotifyImpl<CDlg1>
{
  typedef CDragListNotifyImpl<CDlg1> _dragBase;

public:
  enum { IDD = IDD_DIALOG1 };

  BEGIN_MSG_MAP(CDlg1)
    MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
    COMMAND_ID_HANDLER(IDOK, OnCloseCmd)
    COMMAND_ID_HANDLER(IDCANCEL, OnCloseCmd)
    CHAIN_MSG_MAP(_dragBase)
  END_MSG_MAP()

  LRESULT OnInitDialog(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/,
    BOOL& /*bHandled*/)
  {
    CenterWindow(GetParent());

    CDragListBox dlb;

    //Связываем экземпляр класса dlb с окном. Никакого субклассинга!
    dlb = GetDlgItem(IDC_LIST1);

    //Переводим окно в режим drag&drop
    dlb.MakeDragList();

    //Добавляем элементы
    dlb.AddString(_T("alex"));
    dlb.AddString(_T("rosa"));
    dlb.AddString(_T("dima"));

    return TRUE;
  }

  LRESULT OnCloseCmd(WORD /*wNotifyCode*/, WORD wID, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
  {
    EndDialog(wID);
    return 0;
  }

  void OnDropped(int/*nCtlID*/, HWND /*hWndDragList*/, POINT ptCursor)
  {
    //Получаем дескриптор окна, над которым пользователь отпустил элемент
    CWindow wnd = WindowFromPoint(ptCursor);
    if (wnd.IsWindow()){

      CDragListBox dlb;
      dlb = GetDlgItem(IDC_LIST1);

      constint idx = dlb.GetCurSel();

      if (idx != -1){
        const size_t sz = dlb.GetTextLen(idx)+1;
        PTSTR pBuf = (PTSTR)_alloca(sz*sizeof(TCHAR));
        if (pBuf){
          pBuf[0] = 0;
          dlb.GetText(idx,pBuf);
          wnd.SetWindowText(pBuf);
        }
      }
    }
  }

  int OnDragging(int/*nCtlID*/, HWND /*hWndDragList*/, POINT /*ptCursor*/)
  {
    return DL_COPYCURSOR;
  }
};

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

Дополнительные классы

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

CInplaceEditTooltipCtrl

Inplace tooltip – это всплывающая подсказка, возникающая на месте выходящего за область видимости control-а. Как правило, она встраивается в treeview и listview control-ы. Основное отличие данного вида всплывающей подсказки от остальных – позиционирование и корректировка прямоугольника вывода текста.

Всплывающая подсказка – это обычное всплывающее (popup) окно верхнего уровня (top-level window), следовательно, к нему применимы функции позиционирования наподобие SetWindowPos. Ей мы и будем пользоваться для достижения нашей цели.

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

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

  CRect r(CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT);
  itt.Create(m_hWnd,r,NULL,WS_POPUP|TTS_NOPREFIX|TTS_ALWAYSTIP);
  ATLASSERT(itt.IsWindow());

  //Связываем поле ввода с всплывающей подсказкой
  CRect r;
  ::GetClientRect(hEdit,&r);
  CToolInfo ti(TTF_SUBCLASS,hEdit,iID,&r,_T("Some text"));
  BOOL b = itt.AddTool(ti);
  ATLASSERT(b);

  //Подправляем внутренние координаты вывода текста для подсказки
  CRect rc;
  itt.GetMargin(&rc);
  rc.InflateRect(0,1);
  rc.left = -2;
  itt.SetMargin(&rc);
    
  //Шрифт у подсказки должен быть тем же, что и у поля ввода
  itt.SetFont(::GetFont(hEdit));

При связывании поля ввода с всплывающей подсказкой я использовал флаг TTF_SUBCLASS, который приводит к подмене оконной процедуры поля ввода. Зачем? Подсказка должно перехватывать некоторые сообщения (типа WM_MOUSEMOVE) связанного control-а, и, если не указать этот флаг, необходимо будет вручную перенаправлять все необходимые сообщения в окно подсказки. Это делается с помощью управляющего сообщения TTM_RELAYEVENT, которое вызывается в цикле выборки сообщений (или в PreTranslateMessage) для следующих сообщений:

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

При связывании следует также указать идентификатор подсказки – по нему можно определить дескриптор окна (видимо, подсказку можно несколько раз «привязать» к control-у). По-моему, этот идентификатор создает одни проблемы, и лучше бы, если его не было.

Теперь перейдем к позиционированию, которое будем выполнять в обработчике TTN_SHOW. Так как это уведомление приходит не в родительское окно (строго говоря, у всплывающих окон нет родителей, есть владельцы), а в связанное окно, мы вынуждены субклассировать поле ввода. Слава WTL, это делается очень легко – достаточно использовать класс CContainedWindowT<CEdit> и субклассировать окно с помощью метода SubclassWindow.

_edit.SubclassWindow(hEdit);

Теперь обработчик уведомления TTN_SHOW:

CRect r;
_edit.GetRect(&r);
_edit.ClientToScreen(&r);
r.OffsetRect(-1,-1);
BOOL b = itt.SetWindowPos(NULL,&r, SWP_NOSIZE|SWP_NOZORDER|SWP_NOACTIVATE);
ATLASSERT(b);
return 1;

Неужели все так просто? Конечно, нет! Дело в том, что по определению подсказка всплывает, когда курсор мыши оказывается над связанным control-ом. Когда мы перемещаем подсказку так, чтобы она находилась над полем ввода, курсор мыши оказывается над подсказкой, что приводит к тому, что она тут же скрывается. Как мы уже знаем, предотвратить сокрытие подсказки, когда нам это не нужно, нормальными средствами мы не можем. Остается один выход – субклассировать окно подсказки и перехватывать сообщение WM_WINDOWPOSCHANGING. Алгоритм такой:

Теперь код. Обработчик TTN_SHOW:

LRESULT OnShow(int/*idCtrl*/, LPNMHDR /*pnmh*/, BOOL& /*bHandled*/)
{
  //Установили флаг
  fHide = false;
  //Захватили ввод мыши
  SetCapture();
  //Стандартные действия
  CRect r;
  _edit.GetRect(&r);
  _edit.ClientToScreen(&r);
  r.OffsetRect(-1,-1);
  BOOL b = SetWindowPos(NULL,&r, SWP_NOSIZE|SWP_NOZORDER|SWP_NOACTIVATE);
  ATLASSERT(b);
  return 1;
}

Обработчик WM_WINDOWPOSCHANGING:

LRESULT OnWindowPosChanging(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam,
  BOOL& bHandled)
{
  if (!fHide){
    LPWINDOWPOS wp = (LPWINDOWPOS)lParam;
    wp->flags &= ~SWP_HIDEWINDOW;
  }
  bHandled = FALSE;
  return 1;
}

Обработчик WM_MOUSEMOVE:

LRESULT OnMouseMove(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam,
  BOOL& bHandled)
{
  CPoint p(lParam);
  CRect rc;
  GetClientRect(&rc);
  if (!rc.PtInRect(p)){
    //Если вышли за пределы всплывающей подсказки
    fHide = true;
    ReleaseCapture();
    ShowWindow(SW_HIDE);
  }
  bHandled = FALSE;
  return 1;
}

Осталось реализовать самую малость – устанавливать текст подсказки идентичным тексту поля ввода. Это можно сделать в обработчике EN_CHANGE в родительском классе. При этом нужно учитывать тот факт, что если текст помещается в поле ввода и полностью виден пользователю, подсказку выводить не надо.

        if (fHide)
{
  //Вычисляем длину текстаconstint sz = _edit.GetWindowTextLength()+1;
  //Выделяем буфер на стеке
  PTSTR pBuf = (PTSTR)_alloca(sz*sizeof(TCHAR));
  ATLASSERT(pBuf);
  //Получаем текст
  _edit.GetWindowText(pBuf,sz);
  //Получаем контекст отображения окна
  CWindowDC dc(_edit);
  dc.SelectFont(_edit.GetFont());
  CRect r(0,0,0,0),rc;
  //Вычисляем размер текста
  dc.DrawText(pBuf,-1,&r,DT_CALCRECT|DT_SINGLELINE);
  //Получаем координаты поля ввода
  _edit.GetClientRect(&rc);
  //Если ширина поля ввода меньше, чем ширина текста в контроле,//показываем подсказкуif (rc.Width() < r.Width())
    UpdateTipText(pBuf,_edit,iID);
  else
    UpdateTipText(_T(""),_edit,iID);
  }
}

Не показывать подсказку можно просто путем установки пустого текста.

Вот и все. Остались кое-какие мелочи, типа обновления текста поля подсказки в обработчике WM_SETTEXT или скрытия подсказки при щелчке по ней (пользователь ведь думает, что курсор находится над полем ввода, тогда как на самом деле он находится над подсказкой). Все они реализованы в специальном классе – CInplaceEditTooltipCtrl.

ПРИМЕЧАНИЕ

Класс CInplaceEditTooltipCtrl находится в пространстве имен AWTL, как и другие специальные классы, рассматриваемые в статье.

Вот пример его использования:

        //член класса окна или диалога
AWTL::CInplaceEditTooltipCtrl itt;
...
//код создания подсказки в OnInitDialog или в OnCreate
CRect r(CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT);
itt.Create(m_hWnd,r,NULL,WS_POPUP|TTS_NOPREFIX|TTS_ALWAYSTIP);
ATLASSERT(itt.IsWindow());
itt.AddTool(GetDlgItem(IDC_EDIT1),ID_TOOLTIP);

Я думаю, вам совсем несложно будет реализовать подобную всплывающую подсказку для своих control-ов или стандартных treeview и listview.

COpenFileDialogEx

Основная идея этого класса позаимствована из великолепной статьи в MSDN Magazine за март 2003 года. Для тех, кто не читал ее, вкратце приведу суть.

Я лично очень редко пользуюсь панельками (кнопками быстрой навигации) в диалоге открытия и сохранения файла (функции GetOpenFileName/GetSaveFileName). По умолчанию их значения выбраны на редкость бестолково. Чтобы изменить положение вещей, необходимо прибегнуть к кое-какому трюку. Дело в том, что информация о панелях (places) хранится (где бы вы думали?) в реестре, в ветке Software\Microsoft\Windows\CurrentVersion\Policies\comdlg32\PlacesBar. Я больше чем уверен, что на вашей системе такого ключа нет. Это значит, что во всех диалогах используется значение по умолчанию. Создайте ключ PlacesBar и добавьте в него значение Place0=”c:\”. Теперь все стандартные диалоги в системе будут содержать всего одну панельку, которая позволит выполнять быструю навигацию к корню диска с. Замечательно, но хочется, чтобы для каждого приложения этот диалог имел собственную настройку. К сожалению, под семейством операционных систем Windows 9х этого сделать невозможно, однако для операционных систем семейства NT версии 5.0 и старше существует функция RegOverridePredefKey. Она позволяет отобразить в контексте данного процесса один из стандартных предопределенных ключей, типа HKEY_CURRENT_USER, на произвольный другой ключ. Некая разновидность hook-а.

Класс CFileDialogEx наследуется от класса CFileDialog и переопределяет функцию DoModal, в которой и разворачиваются основные действия. Давайте рассмотрим ее код:

  INT_PTR DoModal(HWND hWndParent = ::GetActiveWindow())
  {
    ATLASSERT(m_ofn.Flags & OFN_ENABLEHOOK);
    ATLASSERT(m_ofn.lpfnHook != NULL);  // can still be a user hook

    ATLASSERT(m_ofn.Flags & OFN_EXPLORER);

    if(m_ofn.hwndOwner == NULL)    // set only if not specified before
      m_ofn.hwndOwner = hWndParent;

    ATLASSERT(m_hWnd == NULL);
    _Module.AddCreateWndData(&m_thunk.cd, (CDialogImplBase*)this);

    BOOL bRet;

#if (_WIN32_WINNT >= 0x0500)
    //Даже если мы скомпилировались под NT 5.0 и выше//нужно выполнить проверку на версии ОСif (fShowPlaceBar && !AtlIsOldWindows()){      
      m_ofn.FlagsEx = 0;
      if (_places.GetSize() > 0){
        //Панельки есть, но их должно быть не больше 5
        ATLASSERT(_places.GetSize() < 6);
        CRegKey reg;
        if (reg.Create(HKEY_CURRENT_USER, _T("AWTL")) == ERROR_SUCCESS){
          LONG lErr = ::RegOverridePredefKey(HKEY_CURRENT_USER,reg);
          ATLASSERT(lErr == ERROR_SUCCESS);
          if (lErr == ERROR_SUCCESS){
            CRegKey places;
            lErr = places.Create(HKEY_CURRENT_USER,
              _T("Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\comdlg32\\PlacesBar"));
            if (lErr == ERROR_SUCCESS){
              for(int i = 0;i < _places.GetSize();i++){
                const UINT iPlace = PtrToUint(_places[i]._pStr);
                ATLASSERT(iPlace <= 20 || iPlace > 0x0FFFF);

                TCHAR buf[100];
                wsprintf(buf,_T("Place%d"),i);
                
                if (iPlace <= 20)
                  places.SetValue(iPlace,buf);
                else
                  places.SetValue(_places[i]._pStr,buf);
              }
            }
          }
        }
      }
    }
#endifif (m_bOpenFileDialog)
      bRet = ::GetOpenFileName(&m_ofn);
    else
      bRet = ::GetSaveFileName(&m_ofn);

#if (_WIN32_WINNT >= 0x0500)
    if (fShowPlaceBar && !AtlIsOldWindows() && _places.GetSize() > 0){
      //отчистка
      LONG lErr = ::RegOverridePredefKey(HKEY_CURRENT_USER,NULL);
      ATLASSERT(lErr == ERROR_SUCCESS);
      SHDeleteKey(HKEY_CURRENT_USER,_T("AWTL"));
    }
#endif    
    m_hWnd = NULL;

    return bRet ? IDOK : IDCANCEL;
  }

Последовательность действий такая:

  1. Выполняются проверки на версию операционной системы и наличия панелей;
  2. Создается ключ HKCU\AWTL и выполняется мапинг на него ключа HKEY_CURRENT_USER;
  3. Создается ключ HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\comdlg32\PlacesBar, который фактически расположен в HKCU\AWTL\Software\Microsoft\Windows\CurrentVersion\Policies\comdlg32\PlacesBar;
  4. В этот подключ добавляются значения PlaceX, где Х – число от 0 до 4;
  5. Вызывается стандартный диалог;
  6. Размапливается ключ HKEY_CURRENT_USER;
  7. Содержимое HKCU\AWTL удаляется.

Теперь рассмотрим интерфейс класса CFileDialogEx, благо он несложен:

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

        enum
{
  Desktop = 0,
  StartMenuPrograms = 2,
  ControlPanel = 3,
  Printers = 4,
  MyDocuments = 5,
  Favorites = 6,
  ProgramsStartup = 7,
  RecentFiles = 8,
  sendto = 9,
  Recycle = 10,
  StartMenu = 12,
  MyComputer = 17,
  NetworkPlaces = 18,
  Fonts = 20
};

Более подробную информацию о значениях можно найти в уже упоминавшейся статье.

Пример использования:

LRESULT OnFileOpen(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
{
  AWTL::CFileDialogEx dlg(true);
  dlg.Add(_T("c:\\"));
  dlg.Add(_T("c:\\work"));
  dlg.Add(AWTL::CFileDialogEx::MyDocuments);
  dlg.Add(AWTL::CFileDialogEx::ControlPanel);
  dlg.Add(AWTL::CFileDialogEx::Favorites);
  if (dlg.DoModal() == IDOK){
    //выполняем полезную работу
  }
  return 0;
}

Вот что должно получиться:


Рисунок 1.

CRolloutContainerT

Этот control родился в результате осмысления и переработки одноименного контрола с www.codeproject.com. Если некоторые классы и переменные совпадают – это случайность, весь код написан с нуля и лишь небольшие фрагменты могут отдаленно напоминать его папу.

Назначение: позволяет располагать большое количество форм на сравнительно маленьком участке с помощью их сворачивания (см. рисунок 3). Это несколько похоже на интерфейс диалогов 3D Studio MAX.

Дизайн представлен на рисунке:


Рисунок 2

Класс CRolloutCtrlButton реализует кнопку, которая появляется над каждой формой и предназначена для сворачивания/разворачивания это формы.

ПРИМЕЧАНИЕ

Под формой или панелью в данном разделе я подразумеваю любое дочернее окно, имеющее стиль WS_OVERLAPPED. Если вы выбрали в качестве форм диалоги, то учтите – в редакторе ресурсов по умолчанию устанавливаются несколько другие стили, которые будет необходимо сменить. Нужно снять галочку с кнопки Title bar, System Menu, поставить Style – Child, Border – None, а на закладке Extended Styles выставить галочку Transparent. Почему я сам не занимаюсь установкой этих стилей? Дело в том, что главный стиль, который нужно установить – Child вместо Popup, после создания окна уже не изменить. Либо окно создано всплывающим, либо дочерним. Преобразования типа окна после его создания невозможны.

К конкретной форме этот класс не привязан, поэтому его можно использовать где угодно. Это просто специфическая кнопка.

Класс Rollout содержит один экземпляр класса CRolloutCtrlButton и дескриптор окна (формы), к которой эта кнопка привязывается. Это чрезвычайно простой класс, объявляющий конструктор, деструктор и единственную функцию:

        bool CreateButton(HWND hParent,CRect rc,HFONT hFont,PCTSTR szTitle);

которая создает кнопку.

Класс CRolloutHolder – это массив классов Rollout, реализованный с помощью стандартного класса ATL CSimpleArray. Он отвечает за добавление, удаление, поиск формы и другие функции над множеством форм, к которым уже привязана кнопка. Вот наиболее важные функции интерфейса:

CRolloutHolder инкапсулирует данные, но не реализует их отображение. За отображение отвечает обобщенный класс CRolloutContainerT, который реализует самые примитивные, общие операции пользовательского интерфейса, которые будут присущи всем «настоящим» реализациям. CRolloutContainerT – «не настоящий» класс потому, что он не является классом окна и не содержит карты сообщений. Настоящие виды строятся с помощью производных классов. Рассмотрим интерфейс данного класса.

Конкретный вид контрола определяется производными классами. Например, чтобы сделать контейнер прокручиваемым (scrollable), я написал вот такую простую реализацию:

        class CRolloutContainer:
  public CScrollWindowImpl<CRolloutContainer>,
  public CRolloutContainerT
{

  typedef CRolloutContainer _thisClass;
  typedef CScrollWindowImpl<CRolloutContainer> _baseClass;
public:
  DECLARE_WND_CLASS_EX(_T("AWTL_RolloutView"),0,COLOR_BTNFACE);
  
  BOOL PreTranslateMessage(PMSG pMsg)
  {
    return ::IsDialogMessage(m_hWnd, pMsg);
  }

  void DoPaint(CDCHandle dc)
  {
    _DoPaint(dc);
  }

protected:

  void Init(){}
  
  BOOL _Invalidate()
  {
    return Invalidate();
  }

  HWND _GetThisWindow()
  {
    return m_hWnd;
  }

  int RecalcButtons() const
  {
    int top = CRolloutContainerT::RecalcButtons();

    SIZE sz = {
      GetWidth() + outer_margins.cx*2+inner_margins.cx*2,
      top
    };
    const_cast<_thisClass*>(this)->SetScrollSize(sz,TRUE);

    return top;
  }

private:  
// Message map and handlers
  BEGIN_MSG_MAP(_thisClass)
    CHAIN_MSG_MAP(_baseClass)
    MESSAGE_HANDLER(WM_CREATE, OnCreate)
    COMMAND_CODE_HANDLER(BN_CLICKED, OnRolloutCtrlClicked)
  END_MSG_MAP()

  LRESULT OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled)
  {
    Init();
    return 1;
  }

  LRESULT OnRolloutCtrlClicked(WORD /*wNotifyCode*/, WORD /*wID*/, HWND hWndCtl, BOOL& bHandled)
  {
    constint idx = FindByBtnHandle(hWndCtl);
    if (idx != -1){
      if (!::SendMessage(m_RolloutCtrls[idx]->hWnd, RCM_ISEXPANDED, 0, 0)){
        if (IsRolloutCollapsed(idx))  
          Expand(idx);
        else
          Collapse(idx);
      }
    }
    return 1;
  }
};  //end of CRolloutContainer

Использовать его очень просто:

  AWTL::CRolloutContainer m_view;
  CDlg1 dlg1;
  CDlg2 dlg2;
        //Создание rollout-control в качестве окна представления
  m_hWndClient = m_view.Create(m_hWnd, rcDefault, NULL,
    WS_CHILD|WS_VISIBLE|WS_CLIPSIBLINGS|WS_CLIPCHILDREN,
    WS_EX_CLIENTEDGE);

  //Создание диалога (не модально)
  dlg1.Create(m_view);
  //Добавление его к rollout-control
  m_view.AddDialog(dlg1,_T("First pane"),true);

  dlg2.Create(m_view);
  m_view.AddDialog(dlg2,_T("Second pane"),false);

Вот примерно такой вид должно иметь приложение:


Рисунок 3

Реализация rollout-control-а в стиле 3D Studio не так тривиальна. В ней самому приходится отрисовывать полосу прокрутки, и учитывать смещение содержимого относительно начала координат. Реализацию этого класса можно найти в исходных файлах, прилагаемых к статье. У меня еще есть идея создать масштабируемый rollout-control, который содержит диалоги, умеющие изменять положение своих дочерних окон в зависимости от размера. Диалог может быть унаследован от класса CDialogResize. Для того чтобы он изменял свой размер, ему необходимо посылать сообщение WM_SIZE. Это можно сделать из обработчика WM_SIZE класса контейнера – код не должен быть сложным. Оставляю вам его реализовать самим в качестве упражнения.

CDateTimeCtrl и CDateTimeRangeCtrl

Зачем я создал эти классы, если у нас есть DateTimePicker? Дело в том, что DateTimePicker имеет очень мало рычагов настройки его внешнего вида и содержания. Например, нельзя стандартными средствами выводить секунды (приходиться использовать DTN_FORMATQUERY) и невозможно изменить стиль выпадающего окна с выбором дат. Кроме этого, я не нашел способа уменьшить высоту самого control-а – уж очень громоздко он смотрится.

При реализации я воспользовался прекрасным control-ом Mini-calendar в XP-стиле (http://www.codeproject.com/wtl/WtlMiniCal.asp). Он, правда, написан с использованием классов ATL 7.0 и не компилируется под ATL 3.0. Но я решил эту проблему. Этот control использует два класса из седьмой ATL: COleDateTime и COleDateTimeSpan, которые являются клонами классов MFC. Использование control-ом этих классов ограничено небольшим количеством функций и операторов, так что мне не составило большого труда клонировать эти классы очередной раз для использования в ATL 3.0. Кроме этого, в сам control пришлось внести несколько мелких изменений и поправить интерфейс. С автором control-а я еще не общался, но думаю, он не сильно обидится, к тому же все его авторские права остаются в силе. Итак, для использования этого класса под ATL 3.0 понадобятся файлы atl3comtime.h, MiniCalendarCtrl.cpp и MiniCalendarCtrl.h. Все их можно найти среди прилагаемых к статье файлов.

Назначение: просмотр и редактирование дат и диапазонов дат.

Дизайн:


Рисунок 4

Класс CDateTimeCtrl является owner-draw статическим control-ом (static control). Я унаследовался от класса COwnerDraw, чтобы упростить работу по обработке owner-draw сообщений. Класс в качестве членов содержит экземпляры классов CMiniCalendarCtrl и CContainedWindowT<CEdit>. Каждая ячейка control-а CDateTimeCtrl представлена классом CMaskItem. Массив экземпляров этого типа является членом класса CDateTimeCtrl.

Логика работы контрола следующая. Создается или субклассируется control, после чего задается маска. Маска может содержать только стандартные символы обозначения года, месяца, часа и минуты. Допустимые символы являются набором символов для функций GetDateFormat и GetTimeFormat. Указанная маска разбивается на элементы, каждый их которых представлен экземпляром класса CMaskItem. Этот класс содержит информацию о формате, самих данных, координатах прямоугольника для поля ввода и разделителе между данным элементом и следующим за ним. Из этого следует, что первые символы в формате не могут быть разделителями. В качестве разделителей могут использоваться:

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

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

Вот интерфейс класса CDateTimeCtrl:

Использовать control можно либо субклассировав окно static control-a, либо создав новое окно. При этом не забудьте включить макрос REFLECT_NOTIFICATIONS в карту сообщений родителя.

Пример использования:

        //Субклассируем static
dt.SubclassWindow(GetDlgItem(IDC_DATETIME));
//Устанавливаем формат
dt.SetFormat(_T("yyyy-MM-dd HH:mm"));
//Устанавливаем текущее время
dt.SetDateTime();

Control должен выглядеть примерно так:


Рисунок 5

Класс CDateTimeRangeCtrl наследуется от CDateTimeCtrl, задает строго определенный формат (маску) и несколько меняет интерфейс класса.

Использовать control CDateTimeRangeCtrl еще проще – просто субклассируйте static control и все!

Контрол выглядит примерно так:


Рисунок 6

CPrintPreviewWindowEx

В своей предыдущей статье о WTL я затрагивал тему печати. Однако из-за небольшого цейтнота рассмотреть удалось не все. В частности, я не рассказал о классе предварительного просмотра с возможностью масштабирования и обещал, что обязательно рассмотрю его в следующий раз. Этот следующий раз настал.

Идея проста, как сама WTL – создать класс, производный от классов CPrintPreviewWindow и CScrollImpl. Одновременно при этом убиваются два зайца: масштабирование и скроллинг. Основную трудность составляют две функции: DoPaint, в которой будет производиться вывод графической информации, и SetZoom, которая устанавливает коэффициент масштабирования.

Давайте сначала рассмотрим интерфейс класса, а затем реализацию этих двух функций.

        class CPrintPreviewWindowEx :
  public CPrintPreviewWindow,
  public CScrollImpl<CPrintPreviewWindowEx>
{
  typedef CScrollImpl<CPrintPreviewWindowEx> _baseScrollClass;
  typedef CPrintPreviewWindow _basePreviewClass;

  //Private attributesprivate:
  float m_iZoom;

  //Protected functionsprotected:
  BEGIN_MSG_MAP(CPrintPreviewWindowEx)
    CHAIN_MSG_MAP(_baseScrollClass)
    CHAIN_MSG_MAP(_basePreviewClass)
    MESSAGE_HANDLER(WM_CREATE,OnCreate)
    MESSAGE_HANDLER(WM_KEYDOWN,OnKeyDown)
  END_MSG_MAP()

  LRESULT OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled);

  LRESULT OnKeyDown(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& bHandled);

  //Called by _baseScrollClass::OnPaintvoid DoPaint(CDCHandle dc);

public:
  CPrintPreviewWindowEx();

  //Public interfacepublic:
  float GetZoom() const;

  float SetZoom(float zoom);
};  //end of CPrintPreviewEx

Большое значение имеет порядок подключения базовых классов с помощью макроса CHAIN_MSG_MAP, так как оба они обрабатывают одни и те же сообщения. Так как нам важнее скроллинг, первым в карте стоит класс CScrollImpl, а часть кода обработчика сообщения WM_PAINT класса CPrintPreviewWindow дублируется в методе DoPaint.

Что должен делать метод DoPaint? Он вызывается из обработчика WM_PAINT класса CScrollImpl и должен выполнить некоторую прорисовку. Эта прорисовка состоит в том, чтобы подготовить контекст и ограничивающий прямоугольник в соответствии с коэффициентом масштабирования. После этого можно вызывать метод DoPaint базового класса CPrintPreviewWindow, который и будет выполнять реальный вывод. Алгоритм работы метода DoPaint таков:

  1. Если коэффициент больше нуля (что говорит о наличии масштабирования), вычислить координаты прямоугольника rcArea, из которого в дальнейшем будет получен ограничивающий страницу прямоугольник. Прямоугольник центрируется в зависимости от ориентации листа: по вертикали – если альбомная, по горизонтали – если портретная.
  2. Если коэффициент меньше или равен нулю, мы в режиме «fit to window», в котором страница масштабируется в зависимости от размеров окна, а прокрутка отсутствует. В этом случае координаты прямоугольника rcArea равны координатам клиентской области.
  3. Прямоугольник смещается на величину m_cxOffset по горизонтальной оси и m_cyOffset – по вертикальной. Эти переменные принадлежат классу CScrollImpl.
  4. Рассчитывается прямоугольник страницы с помощью функции GetPageRect. Она принадлежит классу CPrintPreviewWindow.
  5. Производится заливка области за пределами прямоугольника страницы.
  6. Вызывается функция CPrintPreviewWindow::DoPaint

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

Алгоритм функции SetZoom намного проще, но код посложнее (относительно).

  1. Если коэффициент меньше или равен нулю, установить размер прокручиваемой области равным клиентскому размеру окна.
  2. Если коэффициент больше нуля, но меньше 8, вычислить физические размеры страницы (с помощью функции GetDeviceCaps).
  3. Умножить полученный прямоугольник на коэффициент масштабирования.
  4. Установить размеры прокручиваемой области.

Вот код:

        float SetZoom(float zoom)
  {
    constfloat oldZ = m_iZoom;
    if (zoom <= 8){
      m_iZoom = zoom;
      RECT rect;
      GetClientRect(&rect);
      if (zoom == 0){  //fit to window
        SetScrollSize(rect.right, rect.bottom, FALSE);                            
        SetScrollOffset(0, 0);
        ScrollWindowEx(0, 0, SW_SCROLLCHILDREN|SW_ERASE|SW_INVALIDATE);
        return oldZ;
      }

      CClientDC dc(m_hWnd);
      CDC pdc = GetPrinterDC();
      constint mmx = pdc.GetDeviceCaps(HORZSIZE);
      constint mmy = pdc.GetDeviceCaps(VERTSIZE);
      constint pix_per_inchx = dc.GetDeviceCaps(LOGPIXELSX);
      constint pix_per_inchy = dc.GetDeviceCaps(LOGPIXELSY);
      SIZE sz = {
        LONG(MulDiv(mmx,pix_per_inchx*10,254)*m_iZoom),
        LONG(MulDiv(mmy,pix_per_inchy*10,254)*m_iZoom)
      };

      int new_size_y;
      int new_size_x;
      if (sz.cx < sz.cy){
        new_size_y = sz.cy+20;
        new_size_x = sz.cx+sz.cx/4;
      }
      else{
        new_size_x = sz.cx+20;
        new_size_y = sz.cy+sz.cy/4;
      }

      GetScrollSize(sz);
      int x = 0;
      int y = 0;
      if (new_size_x > rect.right)
        x = (new_size_x-rect.right)/2;

      SetScrollSize(new_size_x, new_size_y, FALSE);                            
      SetScrollOffset(x, y);
      ScrollWindowEx(-x, -y, SW_SCROLLCHILDREN|SW_ERASE|SW_INVALIDATE);
      Invalidate();
    }
    return oldZ;
  }

Вот как может выглядеть окно предварительного просмотра с использованием этого класса.


Рисунок 7

CLinkEditCtrl

Этот класс предназначен для ввода и редактирования URL-ссылок и поддерживает следующие возможности:

Control построен на основе стандартного поля ввода и представляет следующий скромный интерфейс:

Определение того, что пользователь ввел действительную URL-ссылку, осуществляется в обработчике EN_CHANGE с помощью функции PathIsUrl. Вот код:

  LRESULT OnEditChange(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& bHandled)
  {
    const size_t sz = GetWindowTextLength()+1;
    PTSTR pBuf = (PTSTR)_alloca(sz);
    if (pBuf){
      GetWindowText(pBuf,sz);
      PTSTR buf = new TCHAR[sz+10*sizeof(TCHAR)];
      buf[0] = 0;
      if (_tcsncicmp(pBuf,_T("www."),4) == 0){
        lstrcpy(buf,_T("http:://"));
      }
      lstrcat(buf,pBuf);
      bool t = PathIsURL(buf) == TRUE;
      delete[] buf;

      if (t ^ f_url)
        Invalidate();
      
      f_url = t;

      UpdateText();
    }
    bHandled = FALSE;
    return 1;
  }

Нечего особо сложного здесь нет. После того, как обнаружено соответствие вводимого текста и URL, внутренняя переменная f_url устанавливается в true. Она используется во многих внутренних функциях, в том числе и в функции отрисовки подчеркивания ссылки, код которой намного интереснее.

        static
        void PreDrawLine(HDC hdc,PTSTR pBuf,HFONT hFont, CRect& rc)
  {
    CDCHandle dc(hdc);
    HFONT hOldFont = 0;
    if (hFont)
      hOldFont = dc.SelectFont(hFont);

    CRect rc1(0,0,0,0);
    dc.DrawText(pBuf,-1,&rc1,DT_EDITCONTROL|DT_CALCRECT);
    if (hOldFont && hFont)
      dc.SelectFont(hOldFont);

    rc.top = ++rc.bottom;
    rc.left += 2;
    rc.right = rc.left + min(rc1.Width(),rc.Width());
  }
  
  staticvoid PostDrawLine(HDC hdc,COLORREF hyper_color, const CRect& rc)
  {
    CDCHandle dc(hdc);
    CPen pen;
    pen.CreatePen(PS_SOLID,1,hyper_color);
    HPEN hOldPen = dc.SelectPen(pen);
    dc.MoveTo(rc.left,rc.bottom);
    dc.LineTo(rc.right,rc.bottom);
    dc.SelectPen(hOldPen);
  }
...
  LRESULT OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled)
  {
    if (f_url){
      const size_t sz = GetWindowTextLength()+1;
      PTSTR pBuf = (PTSTR)_alloca(sz);
      if (pBuf && GetWindowText(pBuf,sz)){
        CRect rc;
        GetRect(&rc);
        PreDrawLine(GetDC(),pBuf,GetFont(),rc);
        ValidateRect(&rc);
        LRESULT lRes = DefWindowProc();
        HideCaret();
        PostDrawLine(GetDC(),hyper_color,rc);
        ShowCaret();
        return 0;//lRes;
      }
    }
    bHandled = FALSE;
    return 1;
  }

Здесь есть несколько тонких моментов:

Изменение цвета текста в поле ввода достигается путем обработки сообщения WM_CTLCOLOREDIT, в котором, в зависимости от флага f_url, в контекст выбирается заранее определенный цвет текста.

Использовать control так же просто, как и все остальные, приведенные в статье – просто субклассируйте его в обработчике WM_INITDIALOG или WM_CREATE.

Вот так он может выглядеть:


Рисунок 8

Заключение

В заключение я бы хотел рассмотреть текущее состояния дел с библиотекой WTL.

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

WTL задумана как библиотека, упрощающая и ускоряющая процесс создания добротного и современного пользовательского интерфейса. Она предназначена для придания многочисленным WinAPI-функциям и управляющим сообщениям объектно-ориентированного вида. Кроме этого, она обобщает некоторые (если не все) реализации с целью достижения максимальной эффективности при проектировании новых классов. При этом, естественно, очень широко используются шаблоны С++ (C++ templates) и макросы. Это дает большую гибкость в плане использования многочисленных возможностей WinAPI и их расширения.

Да, объективно, это сложно, но не сложнее, чем использование голого WinAPI для достижения тех же результатов. К тому же, это дает значительный выигрыш в скорости и позволяет избежать утомительного написания рутинных оберток над WinAPI control-ами. Переход на использование WTL с WinAPI практически не сопряжен ни с какими трудностями. Единственно, что может служить камнем преткновения для многих – шаблоны. Но, по-моему, время и силы, потраченные на их изучение, окупятся втройне.

Все это вовсе не значит, что WTL – нечто сложное для понимания и использования. Это просто новое. До выхода этой библиотеки практически не было попыток объединить ориентированный на объекты WinAPI и объектно-ориентированный С++, как говорится, по полной программе. Естественно, каждый программист создавал свои обертки, классы-помощники, но такого масштаба и подхода не было. Сейчас мы его имеем, и он доказывает свою эффективность.

Что касается будущего. Не хотелось бы, чтобы в библиотеке появлялись классы для работы с базами данных, файлами и интернетом. Я хочу, чтобы она оставалась такой, какой ее задумывали, и не превращалась в монстра наподобие MFC или ATL 7.0. Я думаю, что только в случае наличия единой концепции и развития не вширь, а вглубь, WTL обеспечена дорога в будущее. Сегодня можно выделить следующие направления развития:

WTL должна стать своеобразным boost-ом для прикладных программистов, занимающихся пользовательским интерфейсом. В последнее время в WTL-сообществе поднимаются вопросы о переводе библиотеки в русло open source, что может привести к улучшению качества кода и объединению многих разрозненных наработок. Однако пока все авторские права на WTL принадлежат Microsoft. Учитывая то, что библиотека с их стороны не поддерживается и не входит ни в какие планы, позиция корпорации схожа с позицией собаки на сене.

Будем надеяться на лучшее и ждать выхода WTL 7.1!


Эта статья опубликована в журнале RSDN Magazine #4-2003. Информацию о журнале можно найти здесь