Роджер Джек - Исчерпывающее руководство по написанию всплывающих подсказок
Функция 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 >= 0);
int nResult = GetItemRect(nIndex, lpRect);
if (nResult != LB_ERR) {
CClientDC DC(this);
CFont* pOldFont = DC.SelectObject(&m_Font);
// Calculate the text length.
CString strItem;
GetText(nIndex, strItem);
CSize TextSize = DC.GetTextExtent(strItem);
// Взять максимум от обычной ширины и идеальной ширины.
lpRect->right = max(lpRect->right, lpRect->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->hDC);
ASSERT_VALID(pDC);
int nSavedDC = pDC->SaveDC();
CString strItem;
if (lpDrawItemStruct->itemID != –1) {
GetText(lpDrawItemStruct->itemID, strItem);
}
COLORREF TextColor;
COLORREF BackColor;
UINT nItemState = lpDrawItemStruct->itemState;
if (nItemState & ODS_SELECTED) {
TextColor = RGB(255, 255, 255); // Белый
BackColor = RGB(255, 0, 0); // Красный
} else {
TextColor = RGB(255, 0, 0); // Красный
BackColor = RGB(255, 255, 255); // Белый
}