Исчерпывающее руководство по написанию всплывающих подсказок

Автор: Роджер Джек
Перевод: Олег Быков
Опубликовано: 11.05.2001
Версия текста: 1.2

Всплывающие подсказки (ToolTips) облегчают использование приложений. Если вам неясно назначение кнопки на панели инструментов (ToolBar), вы просто наводите на нее курсор мыши и ждете появления подсказки. Конечно, с помощью строки состояния можно получить больше информации, но в случае с всплывающими подсказками вам не приходится опускать взгляд вниз окна.

Существуют также другие виды подсказок: TitleTips - для расширения заголовков элементов управления "список" и "древовидный список", и DataTips - для получения дополнительной информации о данных в окне, и всплывающие подсказки для Web-страниц. Эта статья является подробным учебником по использованию подсказок в ваших приложениях, начиная от добавления простых подсказок средствами MFC до написания своих собственных подсказок. По пути я покажу, как добавлять подсказки к вашим Web-страницам, включая всплывающие подсказки для простого ActiveX-элемента "кнопка". Но прежде чем приступать к обсуждению деталей, давайте посмотрим, какую поддержку всплывающих подсказок предоставляют классы MFC.

Поддержка подсказок MFC-классами

Библиотека MFC располагает двумя классами для поддержки всплывающих подсказок: CToolTipCtrl и CWnd. CToolTipCtrl инкапсулирует функциональность стандартного элемента управления ToolTip (из библиотеки элементов управления общего назначения - Common Controls DLL) и может, таким образом, использоваться для создания и управления элементом подсказки напрямую. Один элемент ToolTip может поддерживать много инструментов (tools), которые представляют собой прямоугольники в окне, и могут быть (а могут и не быть) дочерними окнами. Один инструмент также может заполнять все окно. Информация об инструменте в некоторых случаях передается в структуре TOOLINFO со следующими полями: хэндл окна, содержащего инструмент, ID или хэндл окна самого инструмента, координаты инструмента (прямоугольник), и информация о тексте для этого инструмента. Один из самых важных методов - это CToolTipCtrl::RelayEvent, который используется для ретрансляции (relay) сообщений мыши элементу ToolTip для обработки. Передача сообщений мыши элементу необходима для того, чтобы ToolTip смог определить момент, когда следует показать или скрыть подсказку. К сожалению, CToolTipCtrl не полностью инкапсулирует функциональность элемента ToolTip. К примеру, CToolTipCtrl::SetDelayTime не поддерживает все допустимые интервалы задержки. Иногда мне приходилось напрямую использовать сообщения и уведомления Windows® из-за подобных ограничений. [Эта информация несколько устарела. Сейчас в класс CToolTipCtrl входит функция SetDelayTime, чьи возможности эквивалентны возможностям TTM_SETDELAYTIME - прим. перев.] Имена всех сообщений (messages) элемента ToolTip начинаются с префикса "TTM_", а имена всех уведомлений (notifications) - с префикса "TTN_". Далее я буду много использовать этот класс, поэтому пока что не стану заострять на нем внимание.

Не так давно Microsoft расширила DLL, содержащую элемент ToolTip (comctl32.dll), с выпуском Microsoft® Internet Explorer 4.0 (IE 4.0). Статья в MSJ из двух частей - "Предварительный обзор библиотеки элементов управления общего назначения для Microsoft Internet Explorer" (первая часть которой была опубликована в октябре 1996 года) - прекрасно описывает новые возможности библиотеки. В эти возможности входят пользовательская отрисовка подсказок (owner-draw), многострочные подсказки, подсказки произвольного цвета, а также поддержка подсказок, перемещающихся за мышью. Появилось сообщение TTM_GETDELAYTIME для получения различных значений интервалов задержки и сообщение TTM_POP для скрытия элемента ToolTip. Увы, на тот момент, когда я пишу эти строки, Microsoft еще не добавила поддержку новых возможностей в CToolTipCtrl. Поэтому для примеров в этой статье я вынужден использовать CWnd::SendMessage. (В выходящем скоро Visual Studio файл commctl.h должен содержать все необходимые объявления - ред.)

Класс CWnd представляет базовую поддержку добавления подсказок к окну. Рисунок 1 показывает методы CWnd для поддержки подсказок. CWnd::EnableToolTips разрешает или запрещает подсказки для окна, и должна быть вызвана до вызова других методов. Нужно заметить, что в работе CWnd::EnableToolTips есть недостаток: когда вы передаете CWnd::EnableToolTips значение FALSE, этот метод вызывает еще один метод, который посылает сообщение для деактивации элемента ToolTip. Когда же вы вызываете CWnd::EnableToolTip со значением TRUE, он не активирует ToolTip заново.

Рис.1. Поддержка подсказок классом CWnd

МетодОписание
BOOL EnableToolTips(BOOL bEnable) Разрешает или запрещает подсказки для окна
virtual int CWnd::OnToolHitTest(CPoint point, TOOLINFO* pTI ) const Вызывается библиотекой, чтобы определить, не находится ли курсор мыши над инструментом, имеющим подсказку
void FilterToolTipMessage(MSG* pMsg) Проверяет, относится ли сообщение к выводу подсказок
static void PASCAL CancelToolTips(BOOL bKeys) Прячет подсказку, если она показана на экране

CWnd::OnToolHitTest вызывается непосредственно библиотекой, и вы можете переопределить эту функцию для реализации собственного алгоритма определения контура инструмента. Первый аргумент, point, является координатами курсора в клиентских координатах. Используйте его для сравнения позиции курсора с координатами ваших инструментов (или кнопок). Второй параметр - это уже упоминавшаяся структура TOOLINFO. Далее я покажу, как переопределять функцию CWnd::OnToolHitTest.

CWnd::FilterToolTipMessage обычно вызывается за вас функцией CWnd::PreTranslateMessage. Вы можете вызвать CWnd::FilterToolTipMessage напрямую (обычно из переопределенной PreTranslateMessage), если CWnd::PreTranslateMessage у вас не вызывается. Позже я покажу, как это делается. CWnd::CancelToolTips прячет показанный элемент ToolTip. Параметр bKeys устанавливается в TRUE, чтобы прятать подсказку по нажатию клавиши. Важно осознавать, что, несмотря на статичность функции-члена CWnd::CancelToolTips, она воздействует только на элементы ToolTip, созданные классом CWnd. Другими словами, она не влияет на объекты CToolTipCtrl, которые вы создаете в собственном коде.

На самом деле CWnd реализует подсказки скрытым созданием и манипулированием объектом CToolTipCtrl. CWnd сохраняет указатель на элемент ToolTip в поле m_pToolTip скрытой структуры AFX_THREAD_STATE. Эта структура используется библиотекой MFC для хранения локальной информации потока. CWnd не предоставляет документированного прямого доступа к этому элементу.

Простая реализация подсказок с помощью MFC

Microsoft упростила добавление подсказок к кнопкам на панелях инструментов. Если вы используете AppWizard, этот процесс происходит автоматически. При генерации вашего приложения с помощью AppWizard щелкните флажок "Docking toolbar". После генерации приложения в классе CMainFrame будет присутствовать переменная m_wndToolBar класса CToolBar, которая инициализируется в методе CMainFrame::OnCreate. В класс CToolBar встроена поддержка элементов ToolTip. AppWizard добавляет в файл ресурсов строки, которые CToolBar использует как подсказки для кнопок панели инструментов.

Изменить строки подсказок после генерации приложения просто - найдите панель инструментов в списке ресурсов, откройте двойным щелчком по любой из кнопок на панели диалог свойств кнопки панели инструментов (Toolbar Button Properties) и отредактируйте строку "Prompt" после символа "\n". Например, на рис.2, текстом всплывающей подсказки является "Open". Строка до символа "\n" является текстом, который появляется в строке состояния при наведении на кнопку.


Рис.2. Свойства кнопки на панели инструментов

Как я уже упоминал, текст подсказки хранится в таблице строк. ID строки с текстом подсказки равен ID соответствующей кнопки на панели инструментов. Для рис.2 ID строки будет ID_FILE_OPEN. Добавлять подсказки к панели инструментов так просто, что ошибиться практически негде. Единственной проблемой в моей практике было случайное перезаписывание строк в таблице из другого участка кода.

Добавление подсказок к модальным диалоговым окнам

Вы, вероятно, видели диалоги с подсказками для каждого элемента управления на них. Это очень удобно, если предназначение элемента неочевидно из контекста. Статья Q141758 в базе знаний (Knowledge Base) подробно описывает, как реализовать подсказки в MFC-диалогах, поэтому здесь я лишь кратко перечислю основные пункты. Для MFC версии 4.0 и выше, вам придется выполнить следующие шаги (предполагаем, что диалоговое окно в вашем приложении уже существует):

В MFC версий ниже 4.0 из CDialog::DoModal не вызывается CDialog::PreTranslateMessage, поэтому придется выполнить дополнительные действия для передачи сообщений мыши элементу ToolTip. Нужно переопределить CWinApp::ProcessMessageFilter и в нем передавать сообщения элементу ToolTip. CWinApp::ProcessMessageFilter вызывается хуковой функцией MFC для реакции на определенные сообщения Windows. За деталями, а также рабочими примерами, обратитесь к статье в базе знаний.

Добавление подсказок к Web-страницам

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

Как и в приложениях, подсказки на Web-страницах могут быть очень полезны. Подсказки могут быть использованы в двух очевидных случаях: для картинок и элементов ActiveX. Я написал пример элемента "кнопка", чтобы продемонстрировать, как просто добавлять подсказки к элементам ActiveX. На рис.3 показаны и элемент ActiveX - кнопка с улыбающейся рожицей, и картинка - небольшое "художество" прямо под кнопкой.


Рис.3. Подсказки на Web-странице

Добавить подсказку к картинке проще простого, потому что эта функциональность встроена в язык HTML (см. рис.4). Эта строка:

<img src="Image.gif" height=48 width=48 
alt="Image ToolTip" >

задает имя и размер картинки. Подстрока "Image ToolTip" и есть текст подсказки, которая появляется при подведении курсора мыши к картинке. Также возможно задать для картинки несколько "активных зон" (HotSpots) и определить несколько подсказок, но это выходит за рамки статьи. Я только хочу показать, насколько просто добавлять подсказки на языке HTML.

Рис.4. HTML-код для подсказки

<OBJECT ID="WebButton1" WIDTH=31 HEIGHT=28
 CLASSID="CLSID:381C5023-2FDA-11D0-8BC1-444553540000">
    <PARAM NAME="_Version" VALUE="65536">
    <PARAM NAME="_ExtentX" VALUE="786">
    <PARAM NAME="_ExtentY" VALUE="731">
    <PARAM NAME="_StockProps" VALUE="0">
    <PARAM NAME="ToolTipText" VALUE="WebButton ToolTip Test">
</OBJECT>
</P>

<br><img src="Image.gif" height=48 width=48 alt="Image ToolTip" >

<SCRIPT LANGUAGE="VBScript">
<!--
Sub WebButton1_Click()
MsgBox "WebButton was clicked"
end sub
-->
</SCRIPT>

Элемент ActiveX представляет собой кнопку с подсказкой. Для чего мне возиться с созданием кнопки, когда можно добавить на Web-страницу трехмерную картинку, которая будет выглядеть как кнопка? На это есть две причины. Во-первых, кнопка выглядит реалистичнее - она нажимается и отжимается по щелчку пользователя, как и положено настоящей кнопке. Во-вторых, я хотел показать добавление подсказок к элементам ActiveX, а кнопка - это простейший элемент, который я мог использовать в демонстрационных целях.

Для генерации кода я использовал AppWizard. Я установил флажок "Activate when visible" и отключил все остальные флажки. В опции "Which window class, if any, should this control subclass?" я выбрал BUTTON. AppWizard генерирует массу дополнительного кода, не относящегося к данной статье. В основном, я остановлюсь на коде, добавленном мной в класс CWebButtonCtrl (см. рис.5). Давайте для начала взглянем на пару переменных класса. CWebButtonCtrl::m_bToolTipEnabled устанавливается в TRUE, если подсказки разрешены. В CWebButtonCtrl::m_strToolTipText хранится текст подсказки. Я добавил обе переменные через ClassWizard и они представляют OLE-свойства, автоматические обновляемые библиотекой MFC при их изменении.

Рис.5. CWebButtonCtrl

// WebButtonCtl.cpp : Implementation of the CWebButtonCtrl OLE control class.


/////////////////////////////////////////////////////////////////////////////
// CWebButtonCtrl::RelayToolTipEvent - Pass mouse messages to ToolTip

void CWebButtonCtrl::RelayToolTipEvent(const MSG* pMsg)
{
    MSG MsgCopy;
    ::memcpy(&MsgCopy, pMsg, sizeof(MSG));
    FilterToolTipMessage(&MsgCopy);
}


int CWebButtonCtrl::OnToolHitTest(CPoint point, TOOLINFO* pTI) const
{
    if (m_bToolTipEnabled && pTI != NULL && pTI->cbSize >= sizeof(TOOLINFO))
    {
        // setup the TOOLINFO structure
        pTI->hwnd = m_hWnd;
        pTI->uId = 0;
        pTI->uFlags = 0;
        GetClientRect(&(pTI->rect));
        pTI->lpszText = LPSTR_TEXTCALLBACK;
    }

    return (m_bToolTipEnabled ? 1 : -1);
}


/////////////////////////////////////////////////////////////////////////////
// CWebButtonCtrl message handlers

int CWebButtonCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct) 
{
    if (COleControl::OnCreate(lpCreateStruct) == -1)
    {
        return -1;
    }
    
    if (m_Bitmap.LoadBitmap(IDB_WEBBUTTON))
    {
        SendMessage(BM_SETIMAGE, IMAGE_BITMAP, 
            (LPARAM)m_Bitmap.GetSafeHandle());
    }
    else
    {
        TRACE("Unable to load bitmap for button.");
    }

    EnableToolTips(TRUE);
    return 0;
}

void CWebButtonCtrl::OnMouseMove(UINT nFlags, CPoint point) 
{
    RelayToolTipEvent(GetCurrentMessage());
    COleControl::OnMouseMove(nFlags, point);
}

void CWebButtonCtrl::OnLButtonDown(UINT nFlags, CPoint point) 
{
    RelayToolTipEvent(GetCurrentMessage());
    COleControl::OnLButtonDown(nFlags, point);
}

void CWebButtonCtrl::OnLButtonUp(UINT nFlags, CPoint point) 
{
    RelayToolTipEvent(GetCurrentMessage());
    COleControl::OnLButtonUp(nFlags, point);
}

BOOL CWebButtonCtrl::OnToolNeedText(UINT id, NMHDR * pNMHDR, LRESULT * pResult)
{
    TOOLTIPTEXT *pTTT = (TOOLTIPTEXT *)pNMHDR;
    ::strcpy(pTTT->szText, m_strToolTipText);
    return TRUE;
}


 
/////////////////////////////////////////////////////////////////////////////
// Property changed handlers

void CWebButtonCtrl::OnToolTipEnabledChanged() 
{
    SetModifiedFlag();
}

void CWebButtonCtrl::OnToolTipTextChanged() 
{
    SetModifiedFlag();
}

CWebButtonCtrl::PreCreateWindow манипулирует передаваемой ему структурой CREATESTRUCT. Я задал для кнопки стиль пользовательской отрисовки (owner-draw) - BS_OWNERDRAW - для того, чтобы не рисовалась рамка фокуса (focus rect) при активизации кнопки. В противном случае, рамка была бы все время видна. Как побочный эффект задания такого стиля приходится переопределять функцию CWebButtonCtrl::OnOcmDrawItem для рисования кнопки. CWebButtonCtrl::OnCreate загружает и устанавливает картинку для кнопки посылкой сообщения BM_SETIMAGE. Она также вызывает CWebButton::EnableToolTips, чтобы задействовать поддержку подсказок классом CWnd.

Функции CWebButtonCtrl::OnMouseMove, CWebButtonCtrl::OnLButtonDown, и CWebButtonCtrl::OnLButtonUp делают одно и то же - они все вызывают CWnd::RelayToolTipEvent. Метод CWebButtonCtrl::RelayToolTipEvent делает неконстантную копию переданного ему сообщения и вызывает CWnd::FilterToolTipMessage. Копия сообщения делается из-за того, что CWnd::FilterToolTipMessage требует неконстантного указателя на сообщение. Я мог бы, конечно, привести указатель к неконстантному, но это небезопасно, потому что в этом случае CWnd::FilterToolTipMessage могла бы изменить исходное сообщение. Обычно CWnd автоматически вызывает CWnd::FilterToolTipMessage в функции CWnd::PreTranslateMessage. Однако, в элементе ActiveX сообщения мыши никогда не попадают в CWnd::PreTranslateMessage, она вызывается только как результат клавиатурного ввода (CWnd::PreTranslateMessage в основном используется для работы с клавиатурными акселераторами). В обычном MFC-приложении CWnd::PreTranslateMessage вызывается в результате работы функции CWinThread::PumpMessage.

CWebButtonCtrl::OnToolHitTest вызывается функцией CWnd::FilterToolTipMessage, и я переопределил ее реализацию по умолчанию, чтобы заполнить передаваемую ей структуру TOOLINFO. Заполнение структуры происходит только в том случае, если для элемента разрешены подсказки. Подсказка (элемент ToolTip) будет показана на экране только при заполненных полях структуры TOOLINFO. Остальные проверки на NULL и размер структуры - избыточные проверки входных параметров на корректность. После заполнения структуры TOOLINFO функция устанавливает поле rect равным размеру клиентской части кнопки. Другими словами, вся кнопка задается как один инструмент. Полю lpszText присваивается значение LPSTR_CALLBACK, в результате чего элемент ToolTip посылает уведомление TTN_NEEDTEXT, чтобы получить текст подсказки. CWebButtonCtrl::OnToolNeedText обрабатывает это уведомление от элемента ToolTip, копируя строку из m_strToolTipText в поле szText переданной структуры TOOLTIPTEXT.

Как вы видите, эта реализации элемента управления ActiveX основывается на поддержке подсказок классом CWnd. Статья Q141871 базы знаний описывает еще один метод добавления подсказок к элементам ActiveX путем создания объекта класса CToolTipCtrl и вызовом его функций AddTool и UpdateTipText. Версия элемента ActiveX, использующего эту технику, прилагается вместе с исходным кодом (см. статью Q165577). В этом примере размер кода для обоих подходов практически одинаков. Так как далее используется второй вариант реализации подсказок, здесь я хочу полнее раскрыть детали поддержки подсказок классом CWnd.

Хочу предупредить вас об ограниченности моей реализации. Во-первых, проблемы появятся при изменении размеров элемента ActiveX. Размер инструмента не изменится. Эта проблема немного надумана, потому что вы вряд ли будете изменять размеры кнопки после загрузки Web-страницы. Во-вторых, реальному элементу ActiveX понадобится подписать код для создания сертификата аутентификации, иначе любой посетитель этой Web-страницы увидит окно с предупреждением. Наш демонстрационный элемент ActiveX неподписан. В третьих, в кнопку жестко зашита одна картинка. Настоящий элемент ActiveX должен уметь динамически менять картинки на кнопке.

Для добавления ActiveX-элемента на страницу я использовал ActiveX Control Pad, который доступен для бесплатного скачивания по адресу http://www.microsoft.com/workshop/author/cpad/cpad.htm. На рисунке 4 показан сгенерированный этой утилитой HTML-код. В этом коде определяются значения OBJECT ID, WIDTH, HEIGHT, и CLASSID. Также у элемента ActiveX имеется список параметров, или свойств. Параметр ToolTipText (имеющий значение "WebButton ToolTip Test") задает текст подсказки для нашей кнопки. Строка

<SCRIPT LANGUAGE="VBScript">

является началом короткой процедуры на языке VBScript, которую я написал для обработки нажатия на кнопку. При щелчке на кнопке появляется информационное окно с сообщением "WebButton was clicked".

Добавление DataTips

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

Элементы DataTips используются для предоставления детальной информации о данных, отображаемых в окне. Например, Microsoft использует DataTips в Visual C++® для показа значений переменных. Вы наводите курсор мыши на переменную во время отладки и видите текущее значение этой переменной в появившейся подсказке. DataTips полезны в любой ситуации, когда об объекте имеется больше информации, чем можно разместить в окне.

В этом примере я использую новые возможности элементов ToolTip, ставшие доступными с появлением IE 4.0 Common Controls DLL, для создания элементов DataTips, в которых содержится информация о нарисованных кругах (см. рис.6). Я создаю круги различных размеров и цветов в случайных местах окна. Когда курсор мыши находится над кругом, появляется многострочный DataTip, в котором указаны координаты центра, радиус и цвет круга. Цвет подсказки соответствует цвету круга. Многострочные подсказки с возможностью изменения цвета текста в них доступны только при инсталлированном IE 4.0.


Рис.6. Пример использования элемента DataTip

С помощью AppWizard я сгенерировал приложение с однодокументным интерфейсом (SDI), отключив опцию "предварительный просмотр при печати" и оставив остальные по умолчанию. Для реализации нужной функциональности я создал или изменил три класса: CCircle, CDTDocument, и CDTView.

CCircle - простой класс, реализующий рисование круга и определение принадлежности точки кругу (hit-testing). Код определения принадлежности точки кругу показан на рис.7. В переменных CCircle::m_CenterPoint, CCircle::m_nRadius, и CCircle::m_Color хранятся, соответственно, координаты центра, радиус и цвет круга. CCircle::Initialize принимает в качестве параметров координаты центра, радиус и цвет и использует их для инициализации соответствующих переменных класса круга. Я предпочел инициализировать переменные класса через функцию CCircle::Initialize, а не через конструктор класса, потому что так легче создавать массив кругов в классе CDTDocument. Все станет понятно, когда мы будем рассматривать класс CDTDocument.

Рис.7. Реализация алгоритма hit-testing в классе CCircle

/////////////////////////////////////////////////////////////////////////////
// CCircle hittesting

inline double Square(int n) { return (double(n) * double(n)); }

BOOL CCircle::HitTest(const CPoint& Point) const
{
    CPoint Diff = m_CenterPoint - Point;
    return ((Square(Diff.x) + Square(Diff.y)) <= Square(m_nRadius));
}

CCircle::Draw принимает указатель на контекст устройства, в котором круг должен себя нарисовать. Функция вычисляет координаты квадрата, в который будет вписан круг, создает кисть нужного цвета, после чего использует CDC::Ellipse, чтобы нарисовать круг в контексте устройства. CCircle::HitTest принимает координаты тестируемой точки и использует теорему Пифагора (a2 + b2 = c2) для определения принадлежности точки окну, сравнивая c2 с радиусом круга. Я определил встроенную функцию Square (возведение в квадрат), чтобы повысить читабельность кода. Встроенные методы CCircle::GetColor, CCircle::GetCenter, и CCircle::GetRadius возвращают, соответственно, переменные класса m_Color, m_CenterPoint, и m_nRadius. Я описал эти функции, а также функции CCircle::Draw и CCircle::HitTest как константные, поскольку они не изменяют внутреннее состояние класса. Другими словами, они сохраняют константность класса. Это хороший стиль программирования, потому что он позволяет вам использовать константные экземпляры CCircle.

CDTDemoDoc унаследован от CDocument. В этом классе хранится массив объектов CCircle, а также определены функции CDTDemoDoc::GetCircleCount и CDTDemoDoc::GetCircle для доступа к информации об этом массиве. CDTDemoDoc::GetCircle принимает целочисленный индекс, указывающий на смещение в массиве. Объекты CCircle хранятся в переменной CDTDemoDoc::m_CircleArray размером CIRCLECOUNT. Я мог бы объявить этот массив как открытый (public), но не сделал этого по двум причинам. Во-первых, реализацию легче менять, если ее детали скрыты от внешнего мира. Допустим, я захочу в будущем использовать для реализации массива переменного размера шаблон CArray. Во-вторых, я хочу возвращать константные ссылки пользователям CDTDemoDoc::GetCircle, чтобы они не смогли случайно изменить объекты кругов в массиве. CDTDemoDoc::CDTDemoDoc вызывает CCircle::Initialize для каждого круга в массиве. CCircle::Initialize облегчает создание массивов фиксированного размера, потому что мне не нужно передавать параметры конструктору CCircle. В противном случае мне пришлось бы создавать массив динамически. Я использовал функцию rand для установки координат кругов. Генератор случайных чисел инициализируется текущим значением времени, поэтому довольно высока вероятность того, что позиция каждого круга будет разной каждый раз, когда вы запускаете приложение.

Класс CDTDemoView ответственен за отображение кругов и реализацию элементов DataTip (см. рис.8). В переменной CDTDemoView::m_ToolTip хранится элемент ToolTip, который выступает в качестве DataTip. Я хотел, чтобы этот пример показал вам, как использовать класс CToolTipCtrl напрямую, вместо использования функций класса CWnd. Однако, в данном случае, даже если я захотел использовать CWnd, я бы не смог, потому что мне нужен прямой доступ к элементу ToolTip, чтобы посылать ему сообщения. CWnd не предоставляет документированных способов доступа к создаваемому им элементу ToolTip, а я не хотел бы полагаться на детали конкретной реализации. CDTDemoView::m_pCircleHit хранит круг под курсором мыши. Если ни один из кругов не попадает под курсор мыши, CDTDemoView::m_pCircleHit может быть NULL.

Рис.8. DTDemoView

/////////////////////////////////////////////////////////////////////////////
// DTDemoView.cpp : implementation of the CDTDemoView class
.
.
.
/////////////////////////////////////////////////////////////////////////////
// CDTDemoView HitTest

const CCircle* CDTDemoView::HitTest(const CPoint& Point)
{
    CDTDemoDoc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);

    // Проверяем в обратном порядке, чтобы корректно обработать 
    // перекрывающиеся круги
    const CCircle *pCircleHit = NULL;
    for (int n = pDoc->GetCircleCount() - 1; n >= 0 && pCircleHit == NULL; n--)
    {
        if (pDoc->GetCircle(n).HitTest(Point))
        {
            pCircleHit = &(pDoc->GetCircle(n));
        }
    }
    return pCircleHit;
}

/////////////////////////////////////////////////////////////////////////////
// CDTDemoView drawing

void CDTDemoView::OnDraw(CDC* pDC)
{
    CDTDemoDoc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);

    for (int n = 0; n < pDoc->GetCircleCount(); n++)
    {
        pDoc->GetCircle(n).Draw(pDC);
    }
}

/////////////////////////////////////////////////////////////////////////////
// CDTDemoView diagnostics

#ifdef _DEBUG
void CDTDemoView::AssertValid() const
{
    CView::AssertValid();
}
void CDTDemoView::Dump(CDumpContext& dc) const
{
    CView::Dump(dc);
}

CDTDemoDoc* CDTDemoView::GetDocument() // non-debug version is inline
{
    ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CDTDemoDoc)));
    return (CDTDemoDoc*)m_pDocument;
}
#endif //_DEBUG

/////////////////////////////////////////////////////////////////////////////
// CDTDemoView message handlers

void CDTDemoView::OnInitialUpdate() 
{
    CView::OnInitialUpdate();
    CRect ClientRect(0, 0, 1000, 1000);
    if (m_ToolTip.Create(this, TTS_ALWAYSTIP) && m_ToolTip.AddTool(this))
    {
        m_ToolTip.SendMessage(TTM_SETMAXTIPWIDTH, 0, SHRT_MAX);
        m_ToolTip.SendMessage(TTM_SETDELAYTIME, TTDT_AUTOPOP, SHRT_MAX);
        m_ToolTip.SendMessage(TTM_SETDELAYTIME, TTDT_INITIAL, 200);
        m_ToolTip.SendMessage(TTM_SETDELAYTIME, TTDT_RESHOW, 200);
    }
    else
    {
        TRACE("Error in creating ToolTip");
    }
}

BOOL CDTDemoView::OnToolTipNeedText(UINT id, NMHDR * pNMHDR, LRESULT * pResult)
{
    BOOL bHandledNotify = FALSE;

    CPoint CursorPos;
    VERIFY(::GetCursorPos(&CursorPos));
    ScreenToClient(&CursorPos);

    CRect ClientRect;
    GetClientRect(ClientRect);

    // Удостовериться, что курсор попадает в клиентскую область окна,
    // потому что библиотека тоже хочет получать эти сообщения для
    // показа подсказок на панели инструментов.
    if (ClientRect.PtInRect(CursorPos))
    {
        TOOLTIPTEXT *pTTT = (TOOLTIPTEXT *)pNMHDR;
        m_pCircleHit = HitTest(CursorPos);

        if (m_pCircleHit)
        {
            // Adjust the text by filling in TOOLTIPTEXT
            CString strTip;
            const CPoint& Center = m_pCircleHit->GetCenter();
            COLORREF Color = m_pCircleHit->GetColor();
            strTip.Format("Center:  (%d, %d)\nRadius:  %d\nColor:  (%d, %d, %d)", 
                          Center.x, Center.y, m_pCircleHit->GetRadius(), 
                          (int)GetRValue(Color), (int)GetGValue(Color),  
                          (int)GetBValue(Color));
            ASSERT(strTip.GetLength() < sizeof(pTTT->szText));
            ::strcpy(pTTT->szText, strTip);

            // Set the text color to same color as circle
            m_ToolTip.SendMessage(TTM_SETTIPTEXTCOLOR, Color, L);
        }
        else
        {
            pTTT->szText[0] = 0;
        }
        bHandledNotify = TRUE;
    }
    return bHandledNotify;
}

BOOL CDTDemoView::PreTranslateMessage(MSG* pMsg) 
{
    if (::IsWindow(m_ToolTip.m_hWnd) && pMsg->hwnd == m_hWnd)
    {
        switch(pMsg->message)
        {
        case WM_LBUTTONDOWN:    
        case WM_MOUSEMOVE:
        case WM_LBUTTONUP:    
        case WM_RBUTTONDOWN:
        case WM_MBUTTONDOWN:    
        case WM_RBUTTONUP:
        case WM_MBUTTONUP:
            m_ToolTip.RelayEvent(pMsg);
            break;
        }
    }
    return CView::PreTranslateMessage(pMsg);
}

void CDTDemoView::OnMouseMove(UINT nFlags, CPoint point) 
{
    if (::IsWindow(m_ToolTip.m_hWnd))
    {
        const CCircle* pCircleHit = HitTest(point);

        if (!pCircleHit || pCircleHit != m_pCircleHit)
        {
            // Activate() скрывает подсказку.
            m_ToolTip.Activate(FALSE);        
        }

        if (pCircleHit)
        {
            m_ToolTip.Activate(TRUE);
            m_pCircleHit = pCircleHit;
        }
    }
    CView::OnMouseMove(nFlags, point);
}

CDTDemoView::OnInitialUpdate создает DataTip и подготавливает его к использованию. Я вызываю m_ToolTip.Create и передаю ей TTS_ALWAYSTIP, чтобы DataTip показывался независимо от того, активно приложение или нет. Затем я вызываю m_ToolTip.AddTool и передаю ей CDTDemoView, как окно, содержащее инструмент. Из-за того, что последние параметры в CToolTipCtrl::AddTool имеют значения по умолчанию (LPSTR_TEXTCALLBACK как текст подсказки, NULL для параметра lpRectTool и 0 для nIDTool), все окно будет считаться инструментом и нотификационное сообщение TTN_NEEDTEXT будет послано CDTDemoView. Эта сообщение позволяет мне установить текст подсказки в функции CDTDemoView::OnToolTipNeedText.

Я посылаю несколько сообщений элементу DataTip, чтобы подготовить его к использованию. TTM_SETMAXTIPWIDTH посылается с заведомо большим значением (SHRT_MAX) в параметре lParam для установки максимальной ширины подсказки. [для этой цели можно также использовать CToolTipCtrl::SetMaxTipWidth - прим.пер.] Это заставляет элемент ToolTip распознавать символы новой строки в строке подсказки (новая возможность IE 4.0 Common Controls DLL). Затем я три раза посылаю сообщение TTM_SETDELAYTIME. [что эквивалентно CToolTipCtrl::SetDelayTime - прим.пер.] В первый раз параметр wParam имеет значение TTDT_AUTOPOP, для установки времени, которое должно пройти до того, как ToolTip исчезнет. Время инициализируется в параметре lParam заведомо большим значением (SHRT_MAX), чтобы отключить автоисчезание подсказок. [справедливости ради нужно сказать, что значение SHRT_MAX дает задержку порядка 30 секунд, а не отключает автоисчезание полностью - прим.пер.] Теперь я могу контролировать их время жизни. Во второй раз в wParam передается TTDT_INITIAL, что задает интервал между моментом остановки курсора мыши и первым появлением подсказки. Этот интервал устанавливается в 200 миллисекунд, поэтому подсказка появляется довольно быстро. И, наконец, TTDT_RESHOW устанавливает временной интервал между появлением следующей подсказки при перемещении курсора в другой круг. Этот интервал также установлен в 200 миллисекунд. Почему я не использовал CToolTipCtrl::SetDelayTime для установки интервалов? К сожалению, CToolTipCtrl::SetDelayTime позволяет задать только интервал для TTDT_AUTOMATIC.

Реализации CDTDemoView::OnDraw и CDTDemoView::HitTest довольно прозрачны. CDTDemoView::OnDraw проходит по массиву кругов, вызывая для каждого круга CCircle::Draw. Функция CDTDemoView::HitTest проверяет, находится ли переданная ему точка над каким-нибудь кругом, вызывая для каждого круга в массиве CCircle::HitTest. Обратите внимание, что эта проверка выполняется в порядке, обратном используемому в CDTDemoView::OnDraw. Это делается для учитывания z-сортировки (z-order) кругов. Например, если круг Б нарисован поверх круга A, тогда сначала должна вызваться функция проверки круга Б.

Функция CDTDemoView::OnToolTipNeedText перехватывает нотификационные сообщения TTN_NEEDTEXT от элемента, на который указывает переменная m_ToolTip. Главное предназначение этой функции - определить, над каким кругом находится курсор и определить текст подсказки. Сначала она получает координаты курсора мыши и конвертирует их в клиентскую систему координат. Затем идет проверка на принадлежность координат клиентской части представления. Если этого не сделать, пропадут подсказки на панели инструментов, так как нотификационное сообщение TTN_NEEDTEXT используется также панелью инструментов. CDTDemoView::OnToolTipNeedText автоматически будет получать все сообщения TTN_NEEDTEXT при активном окне представления. Переменная bHandledNotify показывает, должно ли сообщение передаваться дальше главному окну. Если курсор находится в клиентской части окна, я проверяю его координаты на принадлежность какому-нибудь кругу и сохраняю результаты проверки в переменной m_pCircleHit. Эта переменная также используется в функции CDTDemoView::OnMouseMove. Если курсор попадает в один из кругов, я создаю строку подсказки, в которую заношу координаты центра, радиус и цвет круга. Эта строка копируется в поле szText переданной структуры TOOLTIPTEXT. И, наконец, я устанавливаю цвет текста подсказки с помощью сообщения TTM_SETTIPTEXTCOLOR. [что эквивалентно CToolTipCtrl::SetTipTextColor - прим.пер.] Параметр wParam этого сообщения содержит новое значение цвета текста, и устанавливается равным цвету круга (еще одна новая возможность IE 4.0 Common Controls DLL). Если курсор не попадает ни в один круг, поле szText указывает на пустую строку.

Функция CDTDemoView::PreTranslateMessage ретранслирует некоторые сообщения мыши элементу DataTip с помощью функции CToolTipCtrl::RelayEvent. CDTDemoView::PreTranslateMessage вызывается для каждого сообщения, посылаемого окну. Проще всего вызывать CToolTipCtrl::RelayEvent здесь, потому что в этом случае мне не нужно переопределять перехватчики каждого сообщения мыши для передачи этих сообщений элементу DataTip. Подобным же образом работает и поддержка подсказок классом CWnd.

Функция CDTDemoView::OnMouseMove прячет и показывает подсказку в зависимости от принадлежности координат курсора какому-нибудь кругу, что определяется функцией HitTest. Если курсор мыши не находится над кругом либо если он находится не над тем кругом, над которым он находился во время последнего показа подсказки, то CDTDemoView::OnMouseMove прячет подсказку вызовом m_ToolTip.Activate(FALSE). Значение FALSE показывает, что элемент DataTip должен быть спрятан. Далее, если курсор находится над другим кругом, подсказка снова выводится на экран вызовом m_ToolTip.Activate(TRUE), а в переменную CDTDemoView::m_pCircleHit заносится указатель на новый круг. Таким образом, при перемещении курсора с одного круга на другой, я выключаю подсказку и сразу включаю ее. Это делается для того, чтобы элемент DataTip запросил новый текст подсказки через сообщение TTN_NEEDTEXT, что позволяет инициализировать подсказку новой информацией о круге. В IE 4.0 для скрытия подсказки предназначено сообщение TTM_POP, но это сообщение отсутствовало в той версии файла commctrl.h, которая у меня была.

Подсказки домашнего изготовления: 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->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 рисует все сам. К счастью, это не так сложно. Функция отрисовки получает нужный текст и шрифт от родительского элемента "список", устанавливает контекст устройства, заполняет фон и рисует текст.

Класс 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 >= 0);
    DWORD dwStyle = GetStyle();
    int    nStatus = GetItemRect(nIndex, lpRect);    
    if (nStatus != LB_ERR && !(dwStyle & LBS_OWNERDRAWFIXED) && 
        !(dwStyle & 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->right = max(lpRect->right, 
                lpRect->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 > nScreenWidth)
            {
                IdealItemRect.OffsetRect(nScreenWidth - IdealItemRect.right, 0);
            }
            if (IdealItemRect.left < 0)
            {
                IdealItemRect.OffsetRect(-IdealItemRect.left, 0);
            }

            m_TitleTip.Show(IdealItemRect, nNewIndex);  
        }
    }

    if (m_TitleTip.IsWindowVisible())
    {
        // Удостовериться, что мышь захвачена, чтобы отследить 
        // момент отключения подсказки.
        if (!m_bMouseCaptured && GetCapture() != this)
        {
            CaptureMouse();
        }
    }
    else
    {
        // Подсказка невидима, поэтому освободить мышь.
        if (m_bMouseCaptured)
        {
            VERIFY(ReleaseCapture());
            m_bMouseCaptured = FALSE;
        }
    }
}

void CTitleTipListBox::CaptureMouse()
{
    ASSERT(!m_bMouseCaptured);
    CPoint Point;
    VERIFY(GetCursorPos(&Point));
    ScreenToClient(&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 && 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 && n < 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() & 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 != &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() && m_TitleTip.IsWindowVisible())
    {
        CaptureMouse();
    }
}


BOOL CTitleTipListBox::PreTranslateMessage(MSG* pMsg) 
{
    switch (pMsg->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->GetParent();
                    if (!pWnd || pWnd == pFrameWnd)
                    {
                        bDone = TRUE;
                    }
                    else if (pWnd->IsKindOf(RUNTIME_CLASS(CView)))
                    {
                        pFrameWnd->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 >= 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); // Белый
    }
    
    CRect ItemRect(lpDrawItemStruct->rcItem);

    // Нарисовать фон
    pDC->FillSolidRect(ItemRect, BackColor);

    // Нарисовать текст
    pDC->SetTextColor(TextColor);
    pDC->SetBkMode(TRANSPARENT);
    pDC->SelectObject(&m_Font);
    ItemRect.left += m_nEdgeSpace;
    pDC->DrawText(strItem, ItemRect, 
        DT_LEFT | DT_SINGLELINE | DT_VCENTER);
    ItemRect.left -= m_nEdgeSpace;

    // Нарисовать по необходимости рамку фокуса
    if (nItemState & ODS_FOCUS)
    {
        pDC->DrawFocusRect(ItemRect);
    }

    pDC->RestoreDC(nSavedDC);
}

void CODListBox::MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct) 
{
    lpMeasureItemStruct->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()->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 < 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(&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;
}

Заключение

Итак, я дал вам пять советов по использованию подсказок. Надеюсь, они вдохновят вас на более широкое применение подсказок ToolTips, TitleTips и DataTips в ваших программах и Web-страницах. А может быть, вы даже придумаете новый вид подсказок, которым поделитесь со всеми нами!

Автор выражает благодарность Биллу Кинсли и другим из компании AM Communications, Inc.

Добавление задержек к подсказкам

Элемент TitleTip, созданный мной, не реализует возможностей добавления задержки перед показом подсказки, предоставляемых стандартным элементом ToolTip с помощью сообщения TTM_SETDELAYTIME. Я не стал добавлять задержек по примеру Microsoft, которая также не реализовала задержки в своих TitleTip'ах. Однако, в большинстве других видов подсказок эта задержка присутствует, поэтому если вы желаете добавить задержку перед показом ваших самописных подсказок, я объясню, как это сделать.

В общем случае, задержки реализуются через Windows-таймеры. В классе CWnd реализованы две функции, которые относятся к таймерам: CWnd::SetTimer и CWnd::KillTimer. CWnd::SetTimer устанавливает таймер, и принимает в качестве параметров ID таймера, временной интервал в миллисекундах и указатель на функцию, вызываемую по истечению указанного интервала. Если указатель на функцию равен NULL, то таймер использует сообщение WM_TIMER для уведомления окна о наступившем событии. CWnd::KillTimer останавливает таймер, принимая в качестве параметра ID таймера.

Заручившись такой поддержкой, давайте посмотрим, как можно добавить задержки к написанным вами подсказкам. Я покажу, как реализовать эквивалент TTDT_AUTOPOP сообщения TTM_SETDELAYTIME. Сообщение с этим параметром устанавливает время задержки перед скрытием подсказки, если курсор мыши неподвижен.

Во-первых, добавим protected- или private-переменные в наш класс, чтобы хранить ID таймера и координаты курсора мыши, когда подсказка видна на экране. Также добавим константу, в которой будет находиться время задержки до автоматического скрытия подсказки. Код будет выглядеть следующим образом:

class CCustomTip: public CWnd
{
.
.
.
protected:
    const int m_nAutoPopDelay; // В миллисекундах.
    UINT m_nTimerId;
    CPoint m_LastMousePosition;
.
.
.
};

В конструкторе инициализируем константу m_nAutoPopDelay значением 10000 (десять секунд), а m_nTimerId установим в 0.

CCustomTip::CCustomTip
: m_nAutoPopDelay(10000), m_LastMousePosition(0, 0)
{
.
.
.
m_nTimerId = 0;
.
.
.
}

Вы можете установить любой интервал - я задал 10 секунд только в целях демонстрации задержки.

Добавьте код, который запускает таймер, когда показывается подсказка. Например, если бы существовала функция CCustomTip::Show, вы бы добавили следующий код в метод, показывающий окно:

void CCustomTip::Show
{
.
.
.
    if (m_nTimerId > 0)
    {
        VERIFY(KillTimer(m_nTimerId));
        m_nTimerId = 0;
    }
    VERIFY(::GetCursorPos(&m_LastMousePosition));
    m_nTimerId = SetTimer(1, m_nAutoPopDelay, NULL);
    ASSERT(m_nTimerId != 0);
.
.
.
}

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

void CTitleTip::OnTimer(UINT nIDEvent)
{
    CPoint CurrentMousePosition;

    VERIFY(::GetCursorPos(&CurrentMousePosition));
    if (CurrentMousePosition == m_LastMousePosition)
    {
        Hide();
    }
    else
    {
        m_LastMousePosition = CurrentMousePosition;
    }
}

Наконец, добавьте в CCustomTip::Hide код, останавливающий таймер:

void CCustomTip::Hide()
{
    if (m_nTimerId > 0)
    {
        VERIFY(KillTimer(m_nTimerId));
        m_nTimerId = 0;
    }
    ShowWindow(SW_HIDE);
}

Таким же образом можно реализовать другие типы задержек стандартных элементов ToolTip, но придется отслеживать больше изменений, чем координаты курсора мыши. Например, для реализации эквивалента TTDT_RESHOW (интервал времени перед показом следующей подсказки при перемещении курсора мыши на другую строку, или инструмент) нужно будет создать переменные для хранения времени скрытия последней подсказки и ее содержимого.


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