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

Джек Роджер

 

Всплывающие подсказки (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® из-за подобных ограничений. Имена всех сообщений (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 и выше, вам придется выполнить следующие шаги (предполагаем, что диалоговое окно в вашем приложении уже существует):

• Добавить private или protected переменную типа CToolTipCtrl в класс вашего диалога.

• Добавить в класс управляющую переменную (control member variable), для каждого элемента, у которого будет подсказка. Это можно сделать с помощью ClassWizard (на закладке Member Variable).

• Переопределить CDialog::OnInitDialog и вызвать в нем CToolTipCtrl::Create. Затем вызвать CToolTipCtrl::AddTool для каждого элемента с подсказкой, передавая адрес управляющей переменной и текст подсказки в качестве параметров.

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

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

 

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

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

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

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

Image ToolTip

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

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

 

 

 

 

 

 


Image ToolTip

Элемент 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, который доступен для бесплатного скачивания по адресу . На рисунке 4 показан сгенерированный этой утилитой HTML-код. В этом коде определяются значения OBJECT ID, WIDTH, HEIGHT, и CLASSID. Также у элемента ActiveX имеется список параметров, или свойств. Параметр ToolTipText (имеющий значение "WebButton ToolTip Test") задает текст подсказки для нашей кнопки. Строка