"Исчерпывающее руководство по написанию всплывающих подсказок" - читать интересную книгу автора (Джек Роджер)

Подсказки домашнего изготовления: TitleTips

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

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

TitleTip – это вид подсказок, которые позволяют полностью увидеть не полностью показанные строки в списковых элементах управления. Например, такие подсказки присутствуют в окне Project Workspace среды разработки Visual C++. Если имя класса не помещается в окно Project Workspace, появляется подсказка, которая показывает нужный текст целиком. Это избавляет пользователя от необходимости горизонтальной прокрутки и увеличении ширины окна. Я написал демо-проект, реализующий TitleTips для элемента управления "список". Однако вы можете использовать сходные приемы для добавления этого вида подсказок и к другим элементам управления. Код, который я написал, может работать как с обычными элементами "список", так и со списками с пользовательской отрисовкой (owner-draw listboxes). Я заполнил оба списка названиями моих любимых книг по программированию (см. рис.9).

Рис.9. Демонстрация элементов TitleTip

Вы, наверное, поинтересуетесь, почему я не использовал возможность пользовательской отрисовки подсказок (появившейся в IE 4.0 Common Controls DLL) для реализации TitleTips. Дело в том, что ширина окна подсказки рассчитывается исходя из ширины показанной части строки в списке. Другими словами, у вас нет прямого контроля над шириной элемента ToolTip. Это мешает реализации подсказок для элементов "список" с пользовательской отрисовкой, потому что вам может понадобиться вывести на экран не только текст. Кроме того, я думаю, нужно уметь создавать подсказки с нуля, потому что всегда может оказаться, что стандартная реализация подсказок не обеспечивает нужной функциональности. Допустим, вы захотите создать анимированную или говорящую подсказку.

На рис.10 показана диаграмма классов, которая показывает отношения между классами нашего примера. Класс CListBox – это стандартный класс MFC, который инкапсулирует функциональность стандартного элемента управления "список". Класс CTitleTipListBox унаследован от класса CListBox и ответственен за создание и управление подсказками для списка. CTitleTipListBox может использоваться напрямую, если вы реализуете обычный элемент "список". Класс CTitleTip унаследован от CWnd и представляет элемент ToolTip. Класс CODListBox – это элемент "список" с пользовательской отрисовкой, он унаследован от CTitleTipListBox. Для создания элемента "список" с пользовательской отрисовкой нужно унаследовать класс от CTitleTipListBox и переопределить функцию CTitleTipListBox::GetIdealItemRect. Мы обсудим детали реализации CTitleTipListBox::GetIdealItemRect позже.


Рис.10. Диаграмма классов для примера использования элементов ToolTip

Класс CTitleTip представляет окно подсказки (см. рис.11). В статической переменной CTitleTip::m_pszWndClass хранится зарегистрированное имя класса окна. Имя хранится в статической переменной, потому что класс окна нужно зарегистрировать только один раз для всех экземпляров CTitleTip. CTitleTip::m_nItemIndex – это индекс строки в списке, для которой в данный момент выводится подсказка. Эта переменная может принимать значение константы CTitleTip::m_nNoIndex, если подсказка не выводится ни для одной из строк. CTitleTip::m_pListBox хранит указатель на родительское окно элемента TitleTip. Родительское окно должно быть элементом "список", чтобы я смог взять оттуда информацию для подсказки.

Рис.11. CTitleTip

/////////////////////////////////////////////////////////////////////////////

// CTitleTip window

class CTitleTip : public CWnd {

public:

 CTitleTip();

 virtual BOOL Create(CListBox* pParentWnd);

 virtual void Show(CRect DisplayRect, int nItemIndex);

 virtual void Hide();

// Overrides

 // ClassWizard generated virtual function overrides

 //{{AFX_VIRTUAL(CTitleTip)

 //}}AFX_VIRTUAL

 // Implementation

public:

 virtual ~CTitleTip();

protected:

 const int m_nNoIndex; // Пустой индекс

 static LPCSTR m_pszWndClass; // Имя зарегистрированного класса

 int m_nItemIndex; // Индекс строки, для которой показывается подсказка

 CListBox* m_pListBox; // Родительское окно

 BOOL IsListBoxOwnerDraw();

 // Generated message map functions

protected:

 //{{AFX_MSG(CTitleTip)

 afx_msg void OnPaint();

 //}}AFX_MSG

 DECLARE_MESSAGE_MAP()

};


/////////////////////////////////////////////////////////////////////////////

// TitleTip.cpp : implementation file //

#include "stdafx.h"

#include "TitleTip.h"


#ifdef _DEBUG

#define new DEBUG_NEW

#undef THIS_FILE

static char THIS_FILE[] = __FILE__;

#endif


/////////////////////////////////////////////////////////////////////////////

// CTitleTip

LPCSTR CTitleTip::m_pszWndClass = NULL;

CTitleTip::CTitleTip() : m_nNoIndex(-1) {

 // Зарегистрировать класс окна, если он еще не зарегистрирован

 // другим экземпляром CTitleTip.

 if (m_pszWndClass == NULL) {

  m_pszWndClass = AfxRegisterWndClass(CS_SAVEBITS | CS_HREDRAW | CS_VREDRAW);

 }

 m_nItemIndex = m_nNoIndex;

 m_pListBox = NULL;

}


CTitleTip::~CTitleTip() { }


BOOL CTitleTip::Create(CListBox* pParentWnd) {

 ASSERT_VALID(pParentWnd);

 m_pListBox = pParentWnd;

 // Не рисовать рамку для обычных элементов "список", так как

 // строки с пользовательской отрисовкой добавляют рамку автоматически.

 DWORD dwStyle = WS_POPUP;

 if (!IsListBoxOwnerDraw()) {

  dwStyle |= WS_BORDER;

 }

 return CreateEx(0, m_pszWndClass, NULL, dwStyle, 0, 0, 0, 0, pParentWnd-gt;GetSafeHwnd(), NULL, NULL);

}


BOOL CTitleTip::IsListBoxOwnerDraw() {

 ASSERT_VALID(m_pListBox);

 DWORD dwStyle = m_pListBox-gt;GetStyle();

 return (dwStyle amp; LBS_OWNERDRAWFIXED) || (dwStyle amp; LBS_OWNERDRAWVARIABLE);

}


void CTitleTip::Show(CRect DisplayRect, int nItemIndex) {

 ASSERT_VALID(m_pListBox);

 ASSERT(nItemIndex lt; m_pListBox-gt;GetCount());

 ASSERT(nItemIndex gt;= 0);

 ASSERT(::IsWindow(m_hWnd));

 ASSERT(!DisplayRect.IsRectEmpty());

 // Пометить для обновления, если новая строка.

 if (m_nItemIndex != nItemIndex) {

  m_nItemIndex = nItemIndex;

  InvalidateRect(NULL);

 }

 // Установить позицию и видимость окна.

 CRect WindowRect;

 GetWindowRect(WindowRect);

 int nSWPFlags = SWP_SHOWWINDOW | SWP_NOACTIVATE;

 if (WindowRect == DisplayRect) {

  nSWPFlags |= SWP_NOMOVE | SWP_NOSIZE;

 }

 VERIFY(SetWindowPos(amp;wndTopMost, DisplayRect.left, DisplayRect.top, DisplayRect.Width(), DisplayRect.Height(), nSWPFlags));

}


void CTitleTip::Hide() {

 ASSERT(::IsWindow(m_hWnd));

 ShowWindow(SW_HIDE);

}


BEGIN_MESSAGE_MAP(CTitleTip, CWnd)

 //{{AFX_MSG_MAP(CTitleTip)

 ON_WM_PAINT()

 //}}AFX_MSG_MAP

END_MESSAGE_MAP()


/////////////////////////////////////////////////////////////////////////////

// CTitleTip message handlers

void CTitleTip::OnPaint() {

 ASSERT(m_nItemIndex != m_nNoIndex);

 CPaintDC DC(this);

 int nSavedDC = DC.SaveDC();

 CRect ClientRect;

 GetClientRect(ClientRect);

 if (IsListBoxOwnerDraw()) {

  // Доверим рисование элементу "список".

  DRAWITEMSTRUCT DrawItemStruct;

  DrawItemStruct.CtlType = ODT_LISTBOX;

  DrawItemStruct.CtlID = m_pListBox-gt;GetDlgCtrlID();

  DrawItemStruct.itemID = m_nItemIndex;

  DrawItemStruct.itemAction = ODA_DRAWENTIRE;

  DrawItemStruct.hwndItem = m_pListBox-gt;GetSafeHwnd();

  DrawItemStruct.hDC = DC.GetSafeHdc();

  DrawItemStruct.rcItem = ClientRect;

  DrawItemStruct.itemData = m_pListBox-gt;GetItemData(m_nItemIndex);

  DrawItemStruct.itemState = (m_pListBox-gt;GetSel(m_nItemIndex) gt; 0 ? ODS_SELECTED : 0);

  if (m_pListBox-gt;GetStyle() amp; LBS_MULTIPLESEL) {

   if (m_pListBox-gt;GetCaretIndex() == m_nItemIndex) {

    DrawItemStruct.itemState |= ODS_FOCUS;

   }

  } else {

   DrawItemStruct.itemState |= ODS_FOCUS;

  }

  m_pListBox-gt;DrawItem(amp;DrawItemStruct);

 } else {

  // Рисуем самостоятельно

  CFont* pFont = m_pListBox-gt;GetFont();

  ASSERT_VALID(pFont);

  DC.SelectObject(pFont);

  COLORREF clrBackground = RGB(255, 255, 255);

  if (m_pListBox-gt;GetSel(m_nItemIndex) gt; 0) {

   DC.SetTextColor(::GetSysColor(COLOR_HIGHLIGHTTEXT));

   clrBackground = ::GetSysColor(COLOR_HIGHLIGHT);

  }

  // Рисуем фон

  DC.FillSolidRect(ClientRect, clrBackground);

  // Рисуем текст строки

  CString strItem;

  m_pListBox-gt;GetText(m_nItemIndex, strItem);

  ASSERT(!strItem.IsEmpty());

  DC.SetBkMode(TRANSPARENT);

  DC.TextOut(1, –1, strItem);

 }

 DC.RestoreDC(nSavedDC);

 // Не вызываем CWnd::OnPaint() для сообщений отрисовки

}

CTitleTip::CTitleTip регистрирует класс окна вызовом функции AfxRegisterWndClass и сохраняет имя класса в переменной CTitleTip::m_pszWndClass. Я использую функцию AfxRegisterWndClass, чтобы иметь возможность зарегистрировать класс окна с установленным стилем CS_SAVEBITS. Флаг CS_SAVEBITS используется для оптимизации – Windows сохраняет кусок окна, заслоненного элементом TitleTip, как картинку. В результате, этому окну не нужно посылать сообщение WM_PAINT, когда подсказка убирается с экрана. CTitleTip::Create создает подсказку в виде popup-окна. К окну подсказки рамка добавляется только если элемент "список" является обычным, так как Windows автоматически добавляет рамку к элементам "список" с пользовательской отрисовкой перед посылкой сообщения WM_DRAWITEM. Обратите внимание, что значение переменной CTitleTip::m_pszWndClass передается в качестве имени класса окна в функцию CWnd::CreateEx. CTitleTip::IsListBoxOwnerDraw возвращает TRUE, если родительский элемент "список" является элементом с пользовательской отрисовкой. Функция узнает об этом по стилю элемента "список".

Функция CTitleTip::Show отвечает за показ элемента TitleTip. Ее параметр DisplayRect указывает на координаты и размеры подсказки в клиентской системе координат родительского окна. Параметр nItemIndex указывает индекс отображаемой строки в списке. Я оптимизировал функцию, чтобы она только помечала для отрисовки и устанавливала координаты и размеры подсказки только если она изменилась. Для изменения размеров подсказки используется функция CWnd::SetWindowPos. В качестве ее первого параметра используется wndTopMost, чтобы окно подсказки располагалось поверх всех остальных окон. Чтобы предотвратить получение фокуса ввода этим окном (окну подсказки в любом случае не нужен клавиатурный ввод), используется флаг SWP_NOACTIVATE. Функция CTitleTip::Hide прячет TitleTip вызовом функции CWnd::ShowWindow с параметром SW_HIDE.

CTitleTip::OnPaint по-разному рисует подсказку в зависимости от вида элемента управления "список". Если родительский элемент "список" реализует пользовательскую отрисовку, функция создает и инициализирует структуру DrawItemStruct подобно тому, как это проделывает Windows перед отправкой сообщения WM_DRAWITEM. Разница лишь в том, что вместо того, чтобы установить поле hDC этой структуры равным хэндлу контекста устройства элемента "список", CTitleTip::OnPaint инициализирует это поле значением хэндла контекста устройства окна подсказки. После этого вызывается функция m_pListBox-gt;DrawItem, которой передается адрес заполненной структуры DrawItemStruct. Результатом всех этих действий является то, что элемент "список" рисует одну из своих строк в окне подсказки. Очень умно! Вот в чем преимущество объектно-ориентированного программирования и хорошо продуманных интерфейсов. Элемент управления "список" не знает – или не хочет знать – где он рисует строку, он знает только, как ее нужно рисовать. CTitleTip не умеет рисовать строку списка с пользовательской отрисовкой, но он знает как инициализировать DrawItemStruct и вызвать CListBox::DrawItem. С другой стороны, если родительский список является обычным элементом "список", класс CTitleTip рисует все сам. К счастью, это не так сложно. Функция отрисовки получает нужный текст и шрифт от родительского элемента "список", устанавливает контекст устройства, заполняет фон и рисует текст.

Класс CTitleTipListBox отвечает за управление элементом TitleTip (см. рис.12). В переменной CTitleTipListBox::m_LastMouseMovePoint хранится последняя позиция курсора мыши. CTitleTipListBox::m_bMouseCaptured показывает, производится ли в данный момент захват мыши (mouse capture). CTitleTipListBox::m_TitleTip – это экземпляр класса CTitleTip, указывающий на показываемую подсказку. CTitleTipListBox::m_nNoIndex – это константа, означающая, что в элементе "список" не отображается подсказка ни для одной строки.

Рис.12. CTitleTipListBox

// TitleTipListBox.h : header file

//

/////////////////////////////////////////////////////////////////////////////

// CTitleTipListBox window

#ifndef __TITLETIPLISTBOX_H__

#define __TITLETIPLISTBOX_H__

#include "TitleTip.h"


class CTitleTipListBox : public CListBox { // Construction public:

 CTitleTipListBox();

// Overrides

 // ClassWizard generated virtual function overrides

 //{{AFX_VIRTUAL(CTitleTipListBox)

public:

 virtual BOOL PreTranslateMessage(MSG* pMsg);

 //}}AFX_VIRTUAL

 // Implementation

public:

 virtual ~CTitleTipListBox();

protected:

 const int m_nNoIndex; // Пустой индекс

 CPoint m_LastMouseMovePoint; // Последние координаты курсора мыши

 BOOL m_bMouseCaptured; // Захвачена ли мышь?

 CTitleTip m_TitleTip; // Показываемый элемент TitleTip

 // Этот метод должен быть переопределен элементом "список" с пользовательской отрисовкой.

 virtual int GetIdealItemRect(int nIndex, LPRECT lpRect);

 void AdjustTitleTip(int nNewIndex);

 void CaptureMouse();

 BOOL IsAppActive();

 // Generated message map functions

protected:

 //{{AFX_MSG(CTitleTipListBox)

 afx_msg void OnMouseMove(UINT nFlags, CPoint point);

 afx_msg void OnSelchange();

 afx_msg void OnKillFocus(CWnd* pNewWnd);

 afx_msg void OnDestroy();

 afx_msg void OnLButtonDown(UINT nFlags, CPoint point);

 afx_msg void OnLButtonUp(UINT nFlags, CPoint point);

 //}}AFX_MSG

 afx_msg LONG OnContentChanged(UINT, LONG);

 DECLARE_MESSAGE_MAP()

};

#endif // __TITLETIPLISTBOX_H__


/////////////////////////////////////////////////////////////////////////////

// TitleTipListBox.cpp : implementation file

//

#include "stdafx.h"

#include "TitleTipListBox.h"


#ifdef _DEBUG

#define new DEBUG_NEW

#undef THIS_FILE static char THIS_FILE[] = __FILE__;

#endif


/////////////////////////////////////////////////////////////////////////////

// CTitleTipListBox

CTitleTipListBox::CTitleTipListBox() : m_LastMouseMovePoint(0, 0) , m_nNoIndex(-1) {

 m_bMouseCaptured = FALSE;

}


CTitleTipListBox::~CTitleTipListBox() {

 ASSERT(!m_bMouseCaptured);

}


int CTitleTipListBox::GetIdealItemRect(int nIndex, LPRECT lpRect) {

 // Вычислить размеры идеальной строки. Размеры зависят

 // от длины строки. Это работает только для обычных элементов

 // "список"(без пользовательской отрисовки)

 ASSERT(lpRect);

 ASSERT(nIndex gt;= 0);

 DWORD dwStyle = GetStyle();

 int nStatus = GetItemRect(nIndex, lpRect);

 if (nStatus != LB_ERR amp;amp; !(dwStyle amp; LBS_OWNERDRAWFIXED) amp;amp; !(dwStyle amp; LBS_OWNERDRAWVARIABLE)) {

  CString strItem;

  GetText(nIndex, strItem);

  if (!strItem.IsEmpty()) {

   // Вычислить длину идеального текста.

   CClientDC DC(this);

   CFont* pOldFont = DC.SelectObject(GetFont());

   CSize ItemSize = DC.GetTextExtent(strItem);

   DC.SelectObject(pOldFont);

   // Взять максимум от обычной ширины и идеальной ширины.

   const int cxEdgeSpace = 2;

   lpRect-gt;right = max(lpRect-gt;right, lpRect-gt;left + ItemSize.cx + (cxEdgeSpace * 2));

  }

 } else {

  TRACE("Owner-draw listbox detected – override CTitleTipListBox::GetIdeaItemRect()\n");

 }

 return nStatus;

}


void CTitleTipListBox::AdjustTitleTip(int nNewIndex) {

 if (!::IsWindow(m_TitleTip.m_hWnd)) {

  VERIFY(m_TitleTip.Create(this));

 }

 if (nNewIndex == m_nNoIndex) {

  m_TitleTip.Hide();

 } else {

  CRect IdealItemRect;

  GetIdealItemRect(nNewIndex, IdealItemRect);

  CRect ItemRect;

  GetItemRect(nNewIndex, ItemRect);

  if (ItemRect == IdealItemRect) {

   m_TitleTip.Hide();

  } else {

   // Поправить координаты рядом с краем экрана.

   ClientToScreen(IdealItemRect);

   int nScreenWidth = ::GetSystemMetrics(SM_CXFULLSCREEN);

   if (IdealItemRect.right gt; nScreenWidth) {

    IdealItemRect.OffsetRect(nScreenWidth – IdealItemRect.right, 0);

   }

   if (IdealItemRect.left lt; 0) {

    IdealItemRect.OffsetRect(-IdealItemRect.left, 0);

   }

   m_TitleTip.Show(IdealItemRect, nNewIndex);

  }

 }

 if (m_TitleTip.IsWindowVisible()) {

  // Удостовериться, что мышь захвачена, чтобы отследить

  // момент отключения подсказки.

  if (!m_bMouseCaptured amp;amp; GetCapture() != this) {

   CaptureMouse();

  }

 } else {

  // Подсказка невидима, поэтому освободить мышь.

  if (m_bMouseCaptured) {

   VERIFY(ReleaseCapture());

   m_bMouseCaptured = FALSE;

  }

 }

}


void CTitleTipListBox::CaptureMouse() {

 ASSERT(!m_bMouseCaptured);

 CPoint Point;

 VERIFY(GetCursorPos(amp;Point));

 ScreenToClient(amp;Point);

 m_LastMouseMovePoint = Point;

 SetCapture();

 m_bMouseCaptured = TRUE;

}


/////////////////////////////////////////////////////////////////////////////

// CTitleTipListBox message handlers

LONG CTitleTipListBox::OnContentChanged(UINT, LONG) {

 // Turn off title tip.

 AdjustTitleTip(m_nNoIndex);

 return Default();

}


void CTitleTipListBox::OnMouseMove(UINT nFlags, CPoint point) {

 if (point != m_LastMouseMovePoint amp;amp; IsAppActive()) {

  m_LastMouseMovePoint = point;

  int nIndexHit = m_nNoIndex;

  CRect ClientRect;

  GetClientRect(ClientRect);

  if (ClientRect.PtInRect(point)) {

   // Hit test.

   for (int n = 0; nIndexHit == m_nNoIndex amp;amp; n lt; GetCount(); n++) {

    CRect ItemRect;

    GetItemRect(n, ItemRect);

    if (ItemRect.PtInRect(point)) {

     nIndexHit = n;

    }

   }

  }

  AdjustTitleTip(nIndexHit);

 }

 CListBox::OnMouseMove(nFlags, point);

}


void CTitleTipListBox::OnSelchange() {

 int nSelIndex;

 if (GetStyle() amp; LBS_MULTIPLESEL) {

  nSelIndex = GetCaretIndex();

 } else {

  nSelIndex = GetCurSel();

 }

 AdjustTitleTip(nSelIndex);

 m_TitleTip.InvalidateRect(NULL);

 m_TitleTip.UpdateWindow();

}


void CTitleTipListBox::OnKillFocus(CWnd* pNewWnd) {

 CListBox::OnKillFocus(pNewWnd);

 if (pNewWnd != amp;m_TitleTip) {

  AdjustTitleTip(m_nNoIndex);

 }

}


void CTitleTipListBox::OnDestroy() {

 AdjustTitleTip(m_nNoIndex);

 m_TitleTip.DestroyWindow();

 CListBox::OnDestroy();

}


void CTitleTipListBox::OnLButtonDown(UINT nFlags, CPoint point) {

 // Временно отключить захват мыши, так как базовый класс может

 // захватить мышь.

 if (m_bMouseCaptured) {

  ReleaseCapture();

  m_bMouseCaptured = FALSE;

 }

 CListBox::OnLButtonDown(nFlags, point);

 if (m_TitleTip.IsWindowVisible()) {

  m_TitleTip.InvalidateRect(NULL);

  if (this != GetCapture()) {

   CaptureMouse();

  }

 }

}


void CTitleTipListBox::OnLButtonUp(UINT nFlags, CPoint point) {

 CListBox::OnLButtonUp(nFlags, point);

 if (this != GetCapture() amp;amp; m_TitleTip.IsWindowVisible()) {

  CaptureMouse();

 }

}


BOOL CTitleTipListBox::PreTranslateMessage(MSG* pMsg) {

 switch (pMsg-gt;message) {

 case WM_RBUTTONDOWN:

 case WM_RBUTTONUP:

 case WM_LBUTTONDBLCLK:

 case WM_RBUTTONDBLCLK:

 // Активизировать окно представления, потому что такое

 // поведение подразумевается по сообщению WM_MOUSEACTIVATE,

 // когда над окном нет никаких подсказок.

  AdjustTitleTip(m_nNoIndex);

  CFrameWnd* pFrameWnd = GetParentFrame();

  if (pFrameWnd) {

   BOOL bDone = FALSE;

   CWnd* pWnd = this;

   while (!bDone) {

    pWnd = pWnd-gt;GetParent();

    if (!pWnd || pWnd == pFrameWnd) {

     bDone = TRUE;

    }

    else if (pWnd-gt;IsKindOf(RUNTIME_CLASS(CView))) {

     pFrameWnd-gt;SetActiveView((CView*)pWnd);

     bDone = TRUE;

    }

   }

  }

  break;

 }

 return CListBox::PreTranslateMessage(pMsg);

}

Функция CTitleTipListBox::GetIdealItemRect вычисляет размер и координаты идеальной строки списка. Параметр nIndex – это индекс нужной строки. Параметр lpRect используется для того, чтобы вернуть идеальный размер и координаты в клиентской системе координат. Вы должны переопределить этот метод для элемента "список" с пользовательской отрисовкой, и далее я покажу, как с этим справляется CODListBox. Если не переопределить этот метод для элемента "список" с пользовательской отрисовкой, то метод CTitleTipListBox::GetIdealItemRect выдаст TRACE-сообщение об ошибке. Однако для обычных элементов "список" этот метод автоматически вычисляет размер и координаты идеальной строки списка. Сначала он вызывает функцию CListBox::GetItemRect для вычисления высоты и ширины строки. Ширина строки, возвращенная CListBox::GetItemRect является шириной самого элемента "список", а не шириной текста. Чтобы вычислить настоящую ширину текста подсказки, я получаю текст и шрифт для строки и вызываю CDC::GetTextExtent. Затем в lpRect подставляется максимум от ширины строки и вычисленной ширины строки (плюс немного места по краям из эстетических соображений).

Функция CTitleTipListBox::AdjustTitleTip показывает или прячет элемент TitleTip. Параметр nNewIndex является индексом строки для отображения. Он может принимать значение константы m_nNoIndex, если подсказка не нужна ни для одной строки. Функция создает элемент ToolTip, если он еще не создан. Если в качестве индекса строки передается m_nNoIndex, функция прячет текущую подсказку. В противном случае функция получает размеры идеальной строки вызовом CTitleTipListBox::GetIdealItemRect. Если размеры идеальной строки совпадают с размерами, возвращенными функцией CListBox::GetItemRect, то в подсказке нет необходимости, и подсказка прячется. Если размеры отличаются, то функция изменяет размеры идеальной строки таким образом, чтобы она поместилась на экране и показывает подсказку. Если элемент TitleTip видима, делается захват мыши, чтобы узнать момент, когда подсказку следует скрыть. Другими словами, если курсор мыши не находится ни над одной строкой, функция должна скрыть подсказку; если элемент TitleTip невидим, то функция освобождает мышь. Для захвата курсора мыши используется функция CTitleTipListBox::CaptureMouse. Она сохраняет позицию курсора в клиентской системе координат в переменной CTitleTipListBox::m_LastMouseMovePoint, а также устанавливает флаг m_bMouseCaptured в значение TRUE для индикации того, что курсор мыши теперь захвачен.

Метод CTitleTipListBox::IsAppActive возвращает TRUE, если приложение, в котором находится элемент "список", активно. Активность приложения определяется получением активного окна и проверкой, является ли оно окном верхнего уровня приложения (или одним из его дочерних окон). Этот метод используется в CTitleTipListBox::OnMouseMove для того, чтобы удостовериться, что подсказка отображается только при активном приложении.

CTitleTipListBox::OnContentChanged прячет подсказку и вызывается по наступлению различных событий, которые могут изменить содержимое элемента "список". Например, сообщение LB_INSERTSTRING, которое вставляет строку в список, может сделать подсказку неактуальной, потому что после вставки курсор мыши может оказаться над совсем другой строкой. Список таких событий можно увидеть в карте сообщений (message map) по макросам ON_MESSAGE. Вы спросите, почему я не использовал CWnd::PreTranslateMessage для перехвата этих сообщений? Честно говоря, я пытался так и сделать, но CWnd::PreTranslateMessage перехватывает только сообщения из очереди сообщений, а интересующие нас сообщения являются результатом вызова самой Windows функции SendMessage, которая минует очередь сообщений.

CTitleTipListBox::OnMouseMove проверяет, не попадает ли курсор мыши на какую-нибудь строку, чтобы показать подсказку для этой строки. Эта проверка осуществляется только когда приложение с элементом "список" активно и курсор мыши действительно изменил свое положение. Я выяснил, что Windows иногда посылает несколько сообщений WM_MOUSEMOVE для одной и той же позиции курсора, поэтому я использую переменную m_LastMouseMovePosition для фильтрации этих лишних сообщений. Далее CTitleTipListBox::OnMouseMove проверяет, находится ли курсор мыши в клиентской области списка. Курсор вполне может оказаться за пределами клиентской области из-за захвата курсора мыши. Забавный побочный эффект наблюдается, если не делать такой проверки – подсказка может появиться для строк, невидимых в списке в данный момент. Если же курсор мыши находится в клиентской области списка, CTitleTipListBox::OnMouseMove проходит по списку и выясняет, над какой именно строкой находится курсор. Если это так, функция использует этот индекс для передачи CTitleTipListBox::AdjustTitleTip.

CTitleTipListBox::OnSelchange обрабатывает нотификационное сообщение LBN_SELCHANGE. Если была выбрана другая строка в списке, то может понадобиться изменить подсказку. Например, если выбрана та же строка, которая отражается элементом TitleTip, то TitleTip нужно обновить для показа выбранной строки. Заметьте, что CTitleTipListBox::OnSelchange различает списки с одиночным и множественным выделением. Для списков с множественным выделением она вызывает CListBox::GetCaretIndex, а для списков с одиночным выделением – CListBox::GetCurSel. Обработка нотификационного сообщения LBN_SELCHANGE также позволяет корректно отображать подсказку, когда пользователь выбирает строки клавиатурой, а не мышью.

CTitleTipListBox::OnKillFocus и CTitleTipListBox::OnDestroy относительно просты. CTitleTipListBox::OnKillFocus прячет подсказку, если только окно, получающее фокус, не является окном подсказки. Это нужно для того, чтобы автоматически прятать подсказку, когда пользователь переключается со списка клавишей Tab. CTitleTipListBox::OnDestroy скрывает и уничтожает элемент TitleTip.

CTitleTipListBox::OnLButtonDown помечает элемент TitleTip для перерисовки при смене строки. Я временно отключаю захват мыши перед вызовом функции базового класса, потому что, как выяснилось, если этого не сделать, нарушается механизм выбора нескольких строк (когда вы перемещаете курсор мыши по строкам, удерживая левую кнопку). Поскольку я не посвящен в тайны внутреннего устройства стандартного элемента "список", я могу лишь догадываться о причинах проблемы. Возможно, список сам захватывает мышь при нажатии левой кнопки мыши для отслеживания перемещений курсора.

CTitleTipListBox::OnLButtonUp захватывает курсор мыши, если окно подсказки показано на экране и CTitleTipListBox еще не захватил мышь. CTitleTipListBox::PreTranslateMessage следит за другими сообщениями от мыши и делает окно представления активным, если список находится в этом окне. Я реализовал это для имитации поведения MFC-окна представления, когда оно получает сообщение WM_MOUSEACTIVATE. Иначе окно может пропустить сообщение об активации мышью, когда пользователь щелкает на окне подсказки.

CODListBox представляет собой пример реализации подсказок TitleTips для элемента "список" с пользовательской отрисовкой (см. рис.13). Константа CODListBox::m_nEdgeSpace используется для добавления пространства по краям текста. Константа CODListBox::m_nFontHeight представляет желаемую высоту шрифта для отображения строк. В переменной CODListBox::m_Font хранится шрифт для отображения строк. CODListBox::CODListBox создает шрифт (m_Font) и использует его при отрисовке элемента "список".

Рис.13. CODListBox

// ODListBox.h : header file

//


/////////////////////////////////////////////////////////////////////////////

// CODListBox window

#include "TitleTipListBox.h"


class CODListBox : public CTitleTipListBox { // Construction public:

 CODListBox();

// Overrides

 // ClassWizard generated virtual function overrides

 //{{AFX_VIRTUAL(CODListBox)

public:

 virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);

 virtual void MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct);

 //}}AFX_VIRTUAL

 // Implementation

public:

 virtual ~CODListBox();

protected:

 const int m_nEdgeSpace; // Дополнительное пространство вокруг текста

 const int m_nFontHeight; // Высота шрифта

 CFont m_Font; // Шрифт для отображения строк

 virtual int GetIdealItemRect(int nIndex, LPRECT lpRect);

 // Generated message map functions protected:

 //{{AFX_MSG(CODListBox)

 // NOTE – the ClassWizard will add and remove member functions here.

 //}}AFX_MSG

 DECLARE_MESSAGE_MAP()

};


///////////////////////////////////////////////////////////////////////////// // ODListBox.cpp : implementation file //

#include "stdafx.h"

#include "TTDemo.h"

#include "ODListBox.h"


#ifdef _DEBUG

#define new DEBUG_NEW

#undef THIS_FILE

static char THIS_FILE[] = __FILE__;

#endif


/////////////////////////////////////////////////////////////////////////////

// CODListBox

CODListBox::CODListBox() : m_nEdgeSpace(4), m_nFontHeight(20) {

 VERIFY(m_Font.CreateFont(m_nFontHeight, 0, 0, 0, FW_BOLD, 0, 0, 0, ANSI_CHARSET, OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, "Arial"));

}


CODListBox::~CODListBox() { }


int CODListBox::GetIdealItemRect(int nIndex, LPRECT lpRect) {

 ASSERT(nIndex gt;= 0);

 int nResult = GetItemRect(nIndex, lpRect);

 if (nResult != LB_ERR) {

  CClientDC DC(this);

  CFont* pOldFont = DC.SelectObject(amp;m_Font);

  // Calculate the text length.

  CString strItem;

  GetText(nIndex, strItem);

  CSize TextSize = DC.GetTextExtent(strItem);

  // Взять максимум от обычной ширины и идеальной ширины.

  lpRect-gt;right = max(lpRect-gt;right, lpRect-gt;left + TextSize.cx + (m_nEdgeSpace * 2));

  DC.SelectObject(pOldFont);

 }

 return nResult;

}


BEGIN_MESSAGE_MAP(CODListBox, CTitleTipListBox)

 //{{AFX_MSG_MAP(CODListBox)

 // NOTE – the ClassWizard will add and remove mapping macros here.

 //}}AFX_MSG_MAP

END_MESSAGE_MAP()


/////////////////////////////////////////////////////////////////////////////

// CODListBox message handlers

void CODListBox::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) {

 CDC* pDC = CDC::FromHandle(lpDrawItemStruct-gt;hDC);

 ASSERT_VALID(pDC);

 int nSavedDC = pDC-gt;SaveDC();

 CString strItem;

 if (lpDrawItemStruct-gt;itemID != –1) {

  GetText(lpDrawItemStruct-gt;itemID, strItem);

 }

 COLORREF TextColor;

 COLORREF BackColor;

 UINT nItemState = lpDrawItemStruct-gt;itemState;

 if (nItemState amp; ODS_SELECTED) {

  TextColor = RGB(255, 255, 255); // Белый

  BackColor = RGB(255, 0, 0); // Красный

 } else {

  TextColor = RGB(255, 0, 0); // Красный

  BackColor = RGB(255, 255, 255); // Белый

 }

 CRect ItemRect(lpDrawItemStruct-gt;rcItem);

 // Нарисовать фон

 pDC-gt;FillSolidRect(ItemRect, BackColor);

 // Нарисовать текст

 pDC-gt;SetTextColor(TextColor);

 pDC-gt;SetBkMode(TRANSPARENT);

 pDC-gt;SelectObject(amp;m_Font);

 ItemRect.left += m_nEdgeSpace;

 pDC-gt;DrawText(strItem, ItemRect, DT_LEFT | DT_SINGLELINE | DT_VCENTER);

 ItemRect.left –= m_nEdgeSpace;

 // Нарисовать по необходимости рамку фокуса

 if (nItemState amp; ODS_FOCUS) {

  pDC-gt;DrawFocusRect(ItemRect);

 }

 pDC-gt;RestoreDC(nSavedDC);

}


void CODListBox::MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct) {

 lpMeasureItemStruct-gt;itemHeight = m_nFontHeight + (m_nEdgeSpace * 2);

}

CODListBox::GetIdealItemRect перекрывает такой же метод в классе CTitleTipListBox. Как вы видите, его реализация похожа на реализацию метода в базовом классе, за исключением того, что новый метод использует для шрифта переменную m_Font. Конечно, я мог бы добиться результата и без переопределения метода базового класса, если бы воспользовался CWnd::SetFont для установки шрифта для списка. Однако я хотел показать, как нужно перекрывать этот метод в других случаях. Например, вам придется переопределить CTitleTipListBox::GetIdealItemRect, если вы захотите показывать в списке картинки.

CODListBox::DrawItem рисует строку по информации из структуры DrawItemStruct. Этот код аналогичен коду в функции CTitleTip::OnPaint, за исключением того, что вместо цветов по умолчанию используются красный и белый цвета. Помните, что этот метод может вызываться из класса CTitleTip для рисования внутри его окна.

CODListBox::MeasureItem вычисляет высоту строки на основе шрифта и заданного пустого пространства вокруг текста. Этот метод вызывается Windows только один раз, потому что у этого элемента "список" установлен стиль LBS_OWNERDRAWFIXED. В случае со стилем LBS_OWNERDRAWVARIABLE метод будет вызываться для каждой строки.

В диалоге CTTDemoDlg присутствуют оба рассмотренных элемента "список", и большая часть кода была сгенерирована AppWizard'ом (см. рис.14). Я добавил в класс переменные m_RegListBox и m_ODListBox для обычного списка и списка с пользовательской отрисовкой, соответственно. Еще я добавил код в функцию CTTDemoDlg::OnInitDialog, где производится сабклассинг обоих элементов "список" вызовом CWnd::SubclassWindow. Я загружаю оба списка из статического массива pszItemArray.

Рис.14. CTTDemoDlg

// TTDemoDlg.h : header file /////////////////////////////////////////////////////////////////////////////

// CTTDemoDlg dialog


#include "TitleTipListBox.h"

#include "ODListBox.h"

class CTTDemoDlg : public CDialog { // Construction public:

 CTTDemoDlg(CWnd* pParent = NULL); // standard constructor

// Dialog Data

 //{{AFX_DATA(CTTDemoDlg)

 enum { IDD = IDD_TTDEMO_DIALOG };

 // NOTE: the ClassWizard will add data members here

 //}}AFX_DATA

 // ClassWizard generated virtual function overrides

 //{{AFX_VIRTUAL(CTTDemoDlg)

protected:

 virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support

 //}}AFX_VIRTUAL

 // Implementation

protected:

 HICON m_hIcon;

 CTitleTipListBox m_RegListBox; // Обычный список

 CODListBox m_ODListBox; // Список с пользовательской отрисовкой

 // Generated message map functions

 //{{AFX_MSG(CTTDemoDlg)

 virtual BOOL OnInitDialog();

 afx_msg void OnPaint();

 afx_msg HCURSOR OnQueryDragIcon();

 //}}AFX_MSG

 DECLARE_MESSAGE_MAP()

};


// TTDemoDlg.cpp : implementation file

#include "stdafx.h"

#include "TTDemo.h"

#include "TTDemoDlg.h"


#ifdef _DEBUG

#define new DEBUG_NEW

#undef THIS_FILE

static char THIS_FILE[] = __FILE__;

#endif


/////////////////////////////////////////////////////////////////////////////

// CTTDemoDlg dialog

CTTDemoDlg::CTTDemoDlg(CWnd* pParent /*=NULL*/) : CDialog(CTTDemoDlg::IDD, pParent) {

 //{{AFX_DATA_INIT(CTTDemoDlg)

 // NOTE: the ClassWizard will add member initialization here

 //}}AFX_DATA_INIT

 // Note that LoadIcon does not require a subsequent DestroyIcon in Win32

 m_hIcon = AfxGetApp()-gt;LoadIcon(IDR_MAINFRAME);

}


void CTTDemoDlg::DoDataExchange(CDataExchange* pDX) {

 CDialog::DoDataExchange(pDX);

 //{{AFX_DATA_MAP(CTTDemoDlg)

 // NOTE: the ClassWizard will add DDX and DDV calls here

 //}}AFX_DATA_MAP

}


BEGIN_MESSAGE_MAP(CTTDemoDlg, CDialog)

 //{{AFX_MSG_MAP(CTTDemoDlg)

 ON_WM_PAINT()

 ON_WM_QUERYDRAGICON()

 //}}AFX_MSG_MAP

END_MESSAGE_MAP()


/////////////////////////////////////////////////////////////////////////////

// CTTDemoDlg message handlers

BOOL CTTDemoDlg::OnInitDialog() {

 CDialog::OnInitDialog();

 // Set the icon for this dialog. The framework does this automatically

 // when the application's main window is not a dialog

 SetIcon(m_hIcon, TRUE); // Set big icon

 SetIcon(m_hIcon, FALSE); // Set small icon

 // Сабклассинг обычного элемента "список"

 HWND hwndRegListBox = ::GetDlgItem(GetSafeHwnd(), IDC_REGLISTBOX);

 ASSERT(hwndRegListBox);

 VERIFY(m_RegListBox.SubclassWindow(hwndRegListBox));

 // Сабклассинг списка с пользовательской отрисовкой

 HWND hwndODListBox = ::GetDlgItem(GetSafeHwnd(), IDC_ODLISTBOX);

 ASSERT(hwndODListBox);

 VERIFY(m_ODListBox.SubclassWindow(hwndODListBox));

 // Заполнение обоих списков строками

 static char* pszItemArray[] = {

  "The C++ Programming Language",

  "C++ Primer",

  "OLE Controls Inside Out",

  "Inside OLE 2nd Edition",

  "Inside ODBC",

  "Code Complete",

  "Rapid Software Development",

  "The Design Of Everyday Things",

  "Object-Oriented Analysis And Design",

  "MFC Internals",

  "Animation Techniques In Win32",

  "Inside Visual C++",

  "Writing Solid Code",

  "Learn Java Now"

 };

 static int nItemArrayCount = sizeof(pszItemArray) / sizeof(pszItemArray[0]);

 for (int n = 0; n lt; nItemArrayCount; n++) {

  VERIFY(m_RegListBox.AddString(pszItemArray[n]) != LB_ERR);

  VERIFY(m_ODListBox.AddString(pszItemArray[n]) != LB_ERR);

 }

 return TRUE; // return TRUE unless you set the focus to a control

}


// If you add a minimize button to your dialog, you will need the code below

// to draw the icon. For MFC applications using the document/view model,

// this is automatically done for you by the framework.

void CTTDemoDlg::OnPaint() {

 if (IsIconic()) {

  CPaintDC dc(this); // device context for painting

  SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0);

  // Center icon in client rectangle

  int cxIcon = GetSystemMetrics(SM_CXICON);

  int cyIcon = GetSystemMetrics(SM_CYICON);

  CRect rect;

  GetClientRect(amp;rect);

  int x = (rect.Width() – cxIcon + 1) / 2;

  int y = (rect.Height() – cyIcon + 1) / 2;

  // Draw the icon

  dc.DrawIcon(x, y, m_hIcon);

 } else {

  CDialog::OnPaint();

 }

}


// The system calls this to obtain the cursor to display while the user drags

// the minimized window.

HCURSOR CTTDemoDlg::OnQueryDragIcon() {

 return (HCURSOR) m_hIcon;

}