Сообщений 11    Оценка 150        Оценить  
Система Orphus

Как добавить всплывающие подсказки для элементов управления диалога?

Авторы: Игорь Вартанов
Александр Шаргин

Версия текста: 1.1

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

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

Win32 API

Ограничимся простейшим (но не самым бесполезным!) набором функций, которые мы хотим получить от подсказок. Чаще всего необходимо добавить появление подсказки для определенных областей окна (будь то контролы или отведенные для этой цели прямоугольники), кроме того необходимо иметь возможность изменять текст подсказок и при определенных обстоятельствах блокировать их вывод. Разобравшись с указанными вопросами, достаточно легко расширить функциональность и вариативность их поведения.

Нам понадобится следующий набор функций:

HWND APIENTRY CreateToolTip(HWND hWndParent); 
void APIENTRY FillInToolInfo(TOOLINFO* ti, HWND hWnd, UINT nIDTool = 0); 
BOOL APIENTRY AddTool(HWND hTip, HWND hWnd, RECT* pr = NULL, UINT nIDTool = 0, LPCTSTR szText = NULL); 
void APIENTRY UpdateTipText(HWND hTip, HWND hWnd, UINT nIDTool = 0, LPCTSTR lpszText = NULL);
void APIENTRY GetTipText(HWND hTip, HWND hWnd, UINT nIDTool, LPSTR szText);
void APIENTRY EnableToolTip(HWND hTip, BOOL activate);

Вот пример их реализации (демонстрация применения в тестовом проекте Tooltip).

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

//-------------------------------------------------------------
HWND APIENTRY CreateToolTip(HWND hWndParent)
{
    InitCommonControls();
    HWND hTip = CreateWindowEx(
        0,TOOLTIPS_CLASS,
        0,0,0,0,0,0,
        hWndParent,0,0,0);
    return hTip;
}

Функция FillInToolInfo( ) играет вспомогательную роль для выполнения рутинных операций со структурой TOOLINFO. Логика поведения функции предусматривает использование в качестве уникального идентификатора области вывода подсказки (которая в MSDN носит название tool) хэндла окна - носителя подсказки в случае, если в нее передан нулевой идентификатор nIDTool. В случае ненулевого значения nIDTool программист сам должен обеспечить уникальность передаваемых значений.

//-------------------------------------------------------------
void APIENTRY FillInToolInfo(TOOLINFO* ti, HWND hWnd, UINT nIDTool)
{
    ZeroMemory(ti,sizeof(TOOLINFO));
    ti->cbSize = sizeof(TOOLINFO);
    if(!nIDTool)
    {
        ti->hwnd   = GetParent(hWnd);
        ti->uFlags = TTF_IDISHWND;
        ti->uId    = (UINT)hWnd;
    }
    else
    {
        ti->hwnd   = hWnd;
        ti->uFlags = 0;
        ti->uId    = nIDTool;
    }
}

Добавить новую область подсказки можно функцией AddTool( ). Данная реализация AddTool( ) предусматривает, что контрол hTip сам обеспечит себе получение системных сообщений о передвижении мыши от окон - носителей подсказки. Для этого при создании области выставляется флаг TTF_SUBCLASS. В этом случае совершенно отпадает необходимость в использованиии механизма TTM_RELAYEVENT. Флаг TTF_TRANSPARENT опционален и означает, что выводимые окна подсказки будут прозрачны для мышиных сообщений.

Существует возможность отложить установку текста подсказки на более позднее время. Для этого просто передается NULL-указатель в качестве указателя на текст подсказки. Вместо NULL в ToolTip контрол будет передано значение LPSTR_TEXTCALLBACK, говорящее контролу, что при необходимости он сможет получить текст подсказки посредством механизма нотификации (через WM_NOTIFY) посылкой TTN_GETDISPINFO (эквивалентное ему TTN_NEEDTEXT).

Кроме того AddTool( ) предусматривает возможность ограничения чувствительной области окна (не только окна диалога, но и окна любого контрола) явно задаваемым прямоугольником (если указатель на него равен NULL, будет использована вся клиентская область окна). Однако, при добавлении области подсказки имеет значение способ идентификации области подсказки - если она основана на использовании хэндла окна в качестве идентификатора (установлен флаг TTF_IDISHWND), то чувствительной областью становится вся клиентская область окна - носителя, а координаты прямоугольника (даже если они указаны явно) будут игнорироваться. Как видно из реализации функции FillInToolInfo( ), это будет происходить для случаев, когда nIDTool равен нулю.

//-------------------------------------------------------------
BOOL APIENTRY AddTool(HWND hTip, HWND hWnd, RECT* pr, UINT nIDTool, LPCTSTR szText)
{
    TOOLINFO ti;
    RECT     r = {0,0,0,0};

    FillInToolInfo(&ti, hWnd, nIDTool);
    ti.hinst  = (HINSTANCE)GetModuleHandle(NULL);
    ti.uFlags |= TTF_SUBCLASS | TTF_TRANSPARENT;
    ti.lpszText = LPSTR( szText ? szText : LPSTR_TEXTCALLBACK );
    if(!(ti.uFlags & TTF_IDISHWND))
    {
        if(!pr)
        {
            pr = &r;
            GetClientRect(hWnd, pr);
        }
        memcpy(&ti.rect, pr, sizeof(RECT));
    }
    BOOL res = SendMessage(hTip, TTM_ADDTOOL, 0, (LPARAM)&ti);
    return res;
}

После того, как область зарегистрирована, можно управлять ее текстом посредством UpdateTipText( ). Можно заметить, что в ней может быть использован тот же механизм обратного вызова текста подсказки, что и в AddTool( ). Т.е. в том случае, если указатель lpszText будет установлен в NULL, то будет задействован механизм обратного вызова текста подсказки. А как же поступить в случае, если нужно просто прекратить вывод какой-либо одной подсказки, если установка lpszText в NULL задействует альтернативный способ? В этом случае нужно, чтобы lpszText указывал на пустую строку "".

//-------------------------------------------------------------
void APIENTRY UpdateTipText(HWND hTip, HWND hWnd, UINT nIDTool, LPCTSTR lpszText)
{
    TOOLINFO ti;
    FillInToolInfo(&ti, hWnd, nIDTool);
    ti.lpszText = LPSTR( lpszText ? lpszText : LPSTR_TEXTCALLBACK );
    SendMessage(hTip, TTM_UPDATETIPTEXT, 0, (LPARAM)&ti);
}

Получить текст конкретной подсказки можно посредством GetTipText( ).

//-------------------------------------------------------------
void APIENTRY GetTipText(HWND hTip, HWND hWnd, UINT nIDTool, LPSTR szText)
{
    TOOLINFO ti;
    if(!szText)
        return;
    *szText = 0;
    FillInToolInfo(&ti, hWnd, nIDTool);
    ti.lpszText = szText;
    SendMessage(hTip, TTM_GETTEXT, 0, (LPARAM)&ti);
}

Включить/выключить вывод всех подсказок, зарегистрированных данным tooltip-контролом, можно функцией EnableToolTip( ).

//-------------------------------------------------------------
void APIENTRY EnableToolTip(HWND hTip, BOOL activate)
{
    SendMessage(hTip, TTM_ACTIVATE, activate, 0);
}
ПРИМЕЧАНИЕ
Необходимо отметить, что в данной реализации способа работы с областями подсказки имеется одно ограничение - если программист явным образом задает идентификаторы областей подсказки (флаг TTF_IDISHWND в этом случае не установлен), то механизм обратного вызова текста подсказки не работает, поскольку нотификационные сообщения обратного вызова приходят не диалогу, а окну-носителю области подсказки, которое не умеет их обрабатывать (в данной реализации).

MFC

В MFC для работы с всплывающими подсказками предназначен класс CToolTipCtrl. Рассмотрим, как им пользоваться.

Первым делом необходимо добавить объект класса CToolTipCtrl в класс диалогового окна, которое вы хотите снабдить всплывающими подсказками. Тем самым мы гарантируем, что этот объект будет существовать ровно столько, сколько сам диалог. Например:

class CMFCTipsDlg : public CDialog
{
...
protected:
    CToolTipCtrl m_tt;
...
};

Хотя большую часть времени всплывающая подсказка не видна на экране, это обыкновенное окно, и прежде чем работать с ним, его необходимо создать и связать с уже имеющимся у нас объектом m_tt. Для этого используется функция CToolTipCtrl::Create, которая получает указатель на объект родительского окна и стиль подсказки. Обычно её вызывают из обработчика WM_INITDIALOG родительского окна подсказки, например:

BOOL CMFCTipsDlg::OnInitDialog()
{
...

    m_tt.Create(this);
...
};

Следующая наша задача - сообщить всплывающей подсказке, над какими контролами она должна появляться и какой текст при этом выдавать. Для этого нужно зарегистрировать каждый контрол в подсказке. Это выполняется с помощью функции CToolTipCtrl::AddTool.

BOOL AddTool(
    CWnd* pWnd,
    LPCTSTR lpszText = LPSTR_TEXTCALLBACK,
    LPCRECT lpRectTool = NULL,
    UINT nIDTool = 0
);

Параметры pWnd и lpRectTool задают окно и прямоугольную область внутри этого окна, над которой будет появляться подсказка, а в nIDTool записывается уникальный идентификатор этой области. Если задать lpRectTool равным NULL, создаётся область, занимающая окно целиком. Именно это нам и требуется, поскольку мы хотим добавить подсказки для контролов в диалоге. В этом случае nIDTool должен быть равен нулю (значение по умолчанию). Параметр lpszText содержит указатель на текст подсказки. Если передать вместо текста значение LPSTR_TEXTCALLBACK, подсказка будет запрашивать его непосредственно перед отображением, посылая окну, содержащему контрол (или прямоугольную область), сообщение TTN_GETDISPINFO. О том, как обрабатывать это сообщение, мы поговорим немного позже.

Обычно подсказки для контролов также назначают в обработчике WM_INITDIALOG. Поступим так и мы. Например:

BOOL CMFCTipsDlg::OnInitDialog()
{
...

    static int ID[] =
    {
        IDC_PICTURE,
        IDC_TEXT,
        IDC_EDIT,
        IDC_COMBO,
        IDC_RADIO1,
        IDC_RADIO2,
        IDC_RADIO3,
        IDC_CHECK,
        IDC_LIST,
        IDC_TREE,
        IDOK,
        IDCANCEL
    };

    static const char *szTipText[] =
    {
        "Picture",
        "Text",
        "Edit",
        "Combo box",
        "Radio button 1",
        "Radio button 2",
        LPSTR_TEXTCALLBACK,
        "Check box",
        "List view",
        "Tree view",
        "OK",
        "Cancel"
    };

    for(int i=0; i<sizeof(ID)/sizeof(int); i++)
        m_tt.AddTool(GetDlgItem(ID[i]), szTipText[i]);
...
}

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

BOOL CMFCTipsDlg::PreTranslateMessage(MSG* pMsg) 
{
    if(pMsg->message >= WM_MOUSEFIRST && pMsg->message <= WM_MOUSELAST)
        m_tt.RelayEvent(pMsg);
    
    return CDialog::PreTranslateMessage(pMsg);
}

Вот и всё. Проделанных действий достаточно, чтобы подсказки начали появляться. Осталось рассмотреть, как обрабатывать сообщение TTN_GETSIDPINFO. Как уже говорилось, оно посылается, перед отображением подсказки, в случае если текст подсказки не был задан заранее. Сообщение TTN_GETDISPINFO обрабатывается по обычной схеме (при помощи макроса ON_NOTIFY или ON_NOTIFY_RANGE). Если вы решите использовать макрос ON_NOTIFY, вам понадобится значение идентификатора подсказки. В текущей версии MFC этот идентификатор равен NULL, но учтите, что это значение нигде не документировано. Используя ON_NOTIFY_RANGE, вы не попадёте в зависимость от недокументированных параметров. Например:

class CMFCTipsDlg : public CDialog
{
...
    afx_msg void OnGetDispInfo(UINT id, NMTTDISPINFO *pNMHDR, LRESULT *pResult);
...
};

BEGIN_MESSAGE_MAP(CMFCTipsDlg, CDialog)
...
    ON_NOTIFY_RANGE(TTN_GETDISPINFO, 0, 0xFFFFFFFF, OnGetDispInfo)
...
END_MESSAGE_MAP()

void CMFCTipsDlg::OnGetDispInfo(UINT id, NMTTDISPINFO *pNMHDR, LRESULT *pResult)
{
    pNMHDR->lpszText = "Radio button 3";
    *pResult = 0;
}

В этом фрагменте мы просто возвращаем предопределённую строку "Radio button 3", так как ранее мы не задали текст подсказки всего для одного контрола. Если же таких контролов несколько, вам придётся сначала проанализировать значения hwnd, uId и rect структуры NMTTDISPINFO, а затем вернуть соответствующую им строку.


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