Роджер Джек - Исчерпывающее руководство по написанию всплывающих подсказок
Рис.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->GetSafeHwnd(), NULL, NULL);
}
BOOL CTitleTip::IsListBoxOwnerDraw() {
ASSERT_VALID(m_pListBox);
DWORD dwStyle = m_pListBox->GetStyle();
return (dwStyle & LBS_OWNERDRAWFIXED) || (dwStyle & LBS_OWNERDRAWVARIABLE);
}
void CTitleTip::Show(CRect DisplayRect, int nItemIndex) {
ASSERT_VALID(m_pListBox);
ASSERT(nItemIndex < m_pListBox->GetCount());
ASSERT(nItemIndex >= 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(&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->GetDlgCtrlID();
DrawItemStruct.itemID = m_nItemIndex;
DrawItemStruct.itemAction = ODA_DRAWENTIRE;
DrawItemStruct.hwndItem = m_pListBox->GetSafeHwnd();
DrawItemStruct.hDC = DC.GetSafeHdc();
DrawItemStruct.rcItem = ClientRect;
DrawItemStruct.itemData = m_pListBox->GetItemData(m_nItemIndex);
DrawItemStruct.itemState = (m_pListBox->GetSel(m_nItemIndex) > 0 ? ODS_SELECTED : 0);
if (m_pListBox->GetStyle() & LBS_MULTIPLESEL) {
if (m_pListBox->GetCaretIndex() == m_nItemIndex) {
DrawItemStruct.itemState |= ODS_FOCUS;
}
} else {
DrawItemStruct.itemState |= ODS_FOCUS;
}
m_pListBox->DrawItem(&DrawItemStruct);
} else {
// Рисуем самостоятельно
CFont* pFont = m_pListBox->GetFont();
ASSERT_VALID(pFont);
DC.SelectObject(pFont);
COLORREF clrBackground = RGB(255, 255, 255);
if (m_pListBox->GetSel(m_nItemIndex) > 0) {
DC.SetTextColor(::GetSysColor(COLOR_HIGHLIGHTTEXT));
clrBackground = ::GetSysColor(COLOR_HIGHLIGHT);
}
// Рисуем фон
DC.FillSolidRect(ClientRect, clrBackground);
// Рисуем текст строки
CString strItem;
m_pListBox->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->DrawItem, которой передается адрес заполненной структуры DrawItemStruct. Результатом всех этих действий является то, что элемент "список" рисует одну из своих строк в окне подсказки. Очень умно! Вот в чем преимущество объектно-ориентированного программирования и хорошо продуманных интерфейсов. Элемент управления "список" не знает – или не хочет знать – где он рисует строку, он знает только, как ее нужно рисовать. CTitleTip не умеет рисовать строку списка с пользовательской отрисовкой, но он знает как инициализировать DrawItemStruct и вызвать CListBox::DrawItem. С другой стороны, если родительский список является обычным элементом "список", класс CTitleTip рисует все сам. К счастью, это не так сложно. Функция отрисовки получает нужный текст и шрифт от родительского элемента "список", устанавливает контекст устройства, заполняет фон и рисует текст.