1. 程式人生 > >Windows ToolTips簡要介紹

Windows ToolTips簡要介紹

Windows 標準控制元件ToolTips簡要介紹

參考文件 MSDN

https://msdn.microsoft.com/en-us/library/ff486072(v=vs.85).aspx

一,什麼是ToolTips

ToolTips 就是一個類似於一個懸浮的文字框,在滑鼠指標移動上去能顯示特定的文字。

Screen shot showing text in a tooltip that appears over a file on the desktop

Screen shot showing a tooltip containing one line of text, positioned above a button on a dialog box

Screen shot showing a tooltip with an icon, title, and text, positioned below a button on a dialog box

Screen shot showing a tooltip containing a file name positioned next to a file icon in a tree control

各種ToolTips樣式。

二,建立ToolTips

HWND hwndTip = CreateWindowEx(NULL, TOOLTIPS_CLASS, NULL,
                            WS_POPUP | TTS_NOPREFIX | TTS_ALWAYSTIP,
                            CW_USEDEFAULT, CW_USEDEFAULT,
                            CW_USEDEFAULT, CW_USEDEFAULT,
                            hwndParent, NULL, hinstMyDll,
                            NULL);

SetWindowPos(hwndTip, HWND_TOPMOST,0, 0, 0, 0,
             SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);

此後ToolTips的視窗函式自動維護Tooltips的尺寸,位置和顯示隱藏狀態等。 ToolTips的高度基於所設定的字型的高度。

1.啟用ToolTips

Tooltips可以處於啟用和未啟用的狀態。啟用狀態下ToolTips會顯示文字。 當ToolTips未啟用,其文字講不被顯示。即使滑鼠指標放在一個Tools上傳送TTM_ACTIVE可以啟用和關閉啟用一個ToolTips的狀態。

2.將ToolTips關聯Tools

建立一個TOOLINFO的結構體物件,設定uID為關聯工具的ID

設定uFlags為  TTF_IDISHWND

併發送TTM_ADDTOOL訊息給ToolTips的控制代碼。後面的完整的例子。

3.顯示文字

預設使用TOOLINFO的  lpszText為顯示問題。 可以傳送TTM_UPDATETIPTEXT訊息來更新顯示值。

如果將lpszText 設定為 LPSTR_TEXTCALLBACK ToolTips需要顯示文字時候,會Call之前註冊的父視窗控制代碼的視窗函式併發送 TTN_GETDISPINFO通知碼,

該訊息包含了指向NMTTDISPINFO 結構的指標用於修改相應的文字,以供後續顯示使用。

4.訊息和通知

windows預設只發送訊息給包含滑鼠指標的視窗,並不會傳送訊息給ToolTips。因此需要關聯ToolTips和其對應的父視窗活控制元件ID來控制其顯示(恰當的位置和恰當的時間)。

ToolTips會自動處理一下的訊息。

1.通過TOOLINFO繫結過的控制元件或者父視窗的矩形區域。

2.繫結ToolTip的父視窗在同一個執行緒內。

滿足以上兩個條件 將TOOLINFO的uFlags設定為 TTF_SUBCLASS  然後傳送TTM_ADDTOOL訊息給Tooltip的控制代碼。  但是ToolTips和關聯的視窗必須有直接的訊息通路。也就是父視窗和子視窗的關係。 如果你關聯了別的程序的視窗,還是收不到訊息的。可能要使用HOOK。此時你應該傳送TTM_RELAYEVENT訊息給tooltip 參考Tracking Tooltip

當Tooltip要顯示的時候會發送給其擁有者視窗TTN_SHOW通知碼。 TTN_POP表明Tooltip即將要隱藏。  通過WM_NOTIFY訊息傳送。

三,ToolTips應用

1.一個簡單的ToolTips的例子

#include <windows.h>
#include <windowsx.h>
#include <commctrl.h>
#include "resource.h"
#pragma comment(lib, "comctl32.lib")

LRESULT CALLBACK MyDlgProc(HWND, UINT, WPARAM, LPARAM);

HINSTANCE g_hInst;
HWND hTTWnd;

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int)
{
	g_hInst = hInstance;
	INITCOMMONCONTROLSEX cx = { sizeof(INITCOMMONCONTROLSEX), ICC_BAR_CLASSES };
	BOOL ret = InitCommonControlsEx(&cx);
	return DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, (DLGPROC)MyDlgProc);
}


// Description:
//   Creates a tooltip for an item in a dialog box. 
// Parameters:
//   idTool - identifier of an dialog box item.
//   nDlg - window handle of the dialog box.
//   pszText - string to use as the tooltip text.
// Returns:
//   The handle to the tooltip.
//
HWND CreateToolTip(int toolID, HWND hDlg, PTSTR pszText)
{
	if (!toolID || !hDlg || !pszText)
	{
		return FALSE;
	}
	// Get the window of the tool.
	HWND hwndTool = GetDlgItem(hDlg, toolID);

	// Create the tooltip. g_hInst is the global instance handle.
	HWND hwndTip = CreateWindowEx(NULL, TOOLTIPS_CLASS, NULL,
		WS_POPUP | TTS_ALWAYSTIP | TTS_BALLOON,
		CW_USEDEFAULT, CW_USEDEFAULT,
		CW_USEDEFAULT, CW_USEDEFAULT,
		hDlg, NULL,
		g_hInst, NULL);

	if (!hwndTool || !hwndTip)
	{
		return (HWND)NULL;
	}

	// Associate the tooltip with the tool.
	TOOLINFO toolInfo = { 0 };
	toolInfo.cbSize = sizeof(toolInfo);
	toolInfo.hwnd = hDlg;
	toolInfo.uFlags = TTF_IDISHWND | TTF_SUBCLASS;
	toolInfo.uId = (UINT_PTR)hwndTool;
	toolInfo.lpszText = pszText;
	SendMessage(hwndTip, TTM_ADDTOOL, 0, (LPARAM)&toolInfo);

	return hwndTip;
}

void CreateToolTipForRect(HWND hwndParent)
{
	// Create a tooltip.
	HWND hwndTT = CreateWindowEx(WS_EX_TOPMOST, TOOLTIPS_CLASS, NULL,
		WS_POPUP | TTS_NOPREFIX | TTS_ALWAYSTIP,
		CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
		hwndParent, NULL, g_hInst, NULL);

	SetWindowPos(hwndTT, HWND_TOPMOST, 0, 0, 0, 0,
		SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);

	// Set up "tool" information. In this case, the "tool" is the entire parent window.

	TOOLINFO ti = { 0 };
	ti.cbSize = sizeof(TOOLINFO);
	ti.uFlags = TTF_SUBCLASS;
	ti.hwnd = hwndParent;
	ti.hinst = g_hInst;
	ti.lpszText = TEXT("This is your tooltip string.");

	GetClientRect(hwndParent, &ti.rect);

	// Associate the tooltip with the "tool" window.
	SendMessage(hwndTT, TTM_ADDTOOL, 0, (LPARAM)(LPTOOLINFO)&ti);
}

LRESULT CALLBACK MyDlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg)
	{
	case WM_INITDIALOG:
	{
		CreateToolTipForRect(hDlg);

		break;
	}
	case WM_CLOSE:
		EndDialog(hDlg, FALSE);
		break;
	}
	return FALSE;
}


這個程式碼很簡單就是在Windows對話方塊上顯示ToolTips。可是編譯以後死活不顯示,初始化InitCommonControlsEx的呼叫也沒有問題。觀察到自己建立的對話方塊風格非常復古。


和MSDN上的大相徑庭。


後來查閱相關資料。這是由於專案缺少了Manifest定義。在網上找了一個Manifest的定義檔案在專案載入此檔案就解決了此問題。關於Manifest檔案的定義參考此文章

MSDN: Enable Visual Style in your program.

https://msdn.microsoft.com/en-us/library/windows/desktop/bb773175(v=vs.85).aspx#no_extensions

Windows.Manifest定義如下

  <?xml version="1.0" encoding="UTF-8" standalone="yes"?> 
  <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> 
  <assemblyIdentity 
  name="Microsoft.Windows.XXXX" 
  processorArchitecture="x86" 
  version="5.1.0.0" 
  type="win32"/> 
  <description>Windows Shell</description> 
  <dependency> 
  <dependentAssembly> 
  <assemblyIdentity 
  type="win32" 
  name="Microsoft.Windows.Common-Controls" 
  version="6.0.0.0" 
  processorArchitecture="x86" 
  publicKeyToken="6595b64144ccf1df" 
  language="*" 
   /> 
  </dependentAssembly> 
  </dependency> 
  </assembly>


執行結果如下。


關於這個問題深入研究發現,是呼叫TOOLINFOW類的時候如果程式載入Common Control 6.0以下的版本,這個結構體的定義的實際size比6.0少4個位元組。

而ANSI版本無此問題。如果使用Unicode版本必須加入manifest強制讓應用程式載入common Control 6.0才能使用sizeof(TOOLINFOW)的返回值。

否則就要將此值減去4

參考此文章:  https://stackoverflow.com/questions/2545682/unicode-tooltips-not-showing-up/15173051

2. 一個指定位置顯示Tooltip的例子 同時顯示2個Tooltip並且自己定位Toolpis的位置。

#include <windows.h>
#include <windowsx.h>
#include <commctrl.h>
#include "resource.h"
#pragma comment(lib, "comctl32.lib")

LRESULT CALLBACK MyDlgProc(HWND, UINT, WPARAM, LPARAM);

HINSTANCE g_hInst;
HWND hTTWnd;
HWND g_hwndTrackingTT;
HWND g_hwndTrackingTT1;
TOOLINFO g_toolItem;
BOOL g_TrackingMouse;

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int)
{
	g_hInst = hInstance;
	INITCOMMONCONTROLSEX cx = { sizeof(INITCOMMONCONTROLSEX), ICC_BAR_CLASSES };
	BOOL ret = InitCommonControlsEx(&cx);
	return DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, (DLGPROC)MyDlgProc);
}


// Description:
//   Creates a tooltip for an item in a dialog box. 
// Parameters:
//   idTool - identifier of an dialog box item.
//   nDlg - window handle of the dialog box.
//   pszText - string to use as the tooltip text.
// Returns:
//   The handle to the tooltip.
//
HWND CreateToolTip(int toolID, HWND hDlg, PTSTR pszText)
{
	if (!toolID || !hDlg || !pszText)
	{
		return FALSE;
	}
	// Get the window of the tool.
	HWND hwndTool = GetDlgItem(hDlg, toolID);

	// Create the tooltip. g_hInst is the global instance handle.
	HWND hwndTip = CreateWindowEx(NULL, TOOLTIPS_CLASS, NULL,
		WS_POPUP | TTS_ALWAYSTIP | TTS_BALLOON,
		CW_USEDEFAULT, CW_USEDEFAULT,
		CW_USEDEFAULT, CW_USEDEFAULT,
		hDlg, NULL,
		g_hInst, NULL);

	if (!hwndTool || !hwndTip)
	{
		return (HWND)NULL;
	}

	// Associate the tooltip with the tool.
	TOOLINFO toolInfo = { 0 };
	toolInfo.cbSize = sizeof(toolInfo);
	toolInfo.hwnd = hDlg;
	toolInfo.uFlags = TTF_IDISHWND | TTF_SUBCLASS;
	toolInfo.uId = (UINT_PTR)hwndTool;
	toolInfo.lpszText = pszText;
	SendMessage(hwndTip, TTM_ADDTOOL, 0, (LPARAM)&toolInfo);

	return hwndTip;
}

void CreateToolTipForRect(HWND hwndParent)
{
	// Create a tooltip.
	HWND hwndTT = CreateWindowEx(WS_EX_TOPMOST, TOOLTIPS_CLASS, NULL,
		WS_POPUP | TTS_NOPREFIX | TTS_ALWAYSTIP,
		CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
		hwndParent, NULL, g_hInst, NULL);

	SetWindowPos(hwndTT, HWND_TOPMOST, 0, 0, 0, 0,
		SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);

	// Set up "tool" information. In this case, the "tool" is the entire parent window.

	TOOLINFO ti = { 0 };
	ti.cbSize = sizeof(TOOLINFO);
	ti.uFlags = TTF_SUBCLASS;
	ti.hwnd = hwndParent;
	ti.hinst = g_hInst;
	ti.lpszText = TEXT("This is your tooltip string.");

	GetClientRect(hwndParent, &ti.rect);

	// Associate the tooltip with the "tool" window.
	SendMessage(hwndTT, TTM_ADDTOOL, 0, (LPARAM)(LPTOOLINFO)&ti);
}

HWND CreateTrackingToolTip(int toolID, HWND hDlg, WCHAR* pText)
{
	// Create a tooltip.
	HWND hwndTT = CreateWindowEx(WS_EX_TOPMOST, TOOLTIPS_CLASS, NULL,
		WS_POPUP | TTS_NOPREFIX | TTS_ALWAYSTIP | TTS_BALLOON,
		CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
		hDlg, NULL, g_hInst, NULL);

	if (!hwndTT)
	{
		return NULL;
	}

	// Set up the tool information. In this case, the "tool" is the entire parent window.

	g_toolItem.cbSize = sizeof(TOOLINFO);
	g_toolItem.uFlags = TTF_IDISHWND | TTF_TRACK | TTF_ABSOLUTE;
	g_toolItem.hwnd = hDlg;
	g_toolItem.hinst = g_hInst;
	g_toolItem.lpszText = pText;
	g_toolItem.uId = (UINT_PTR)hDlg;

	GetClientRect(hDlg, &g_toolItem.rect);

	// Associate the tooltip with the tool window.

	SendMessage(hwndTT, TTM_ADDTOOL, 0, (LPARAM)(LPTOOLINFO)&g_toolItem);

	return hwndTT;
}

LRESULT CALLBACK MyDlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg)
	{
	case WM_INITDIALOG:
	{
		g_hwndTrackingTT = CreateTrackingToolTip(0, hDlg, L"");
		g_hwndTrackingTT1 = CreateTrackingToolTip(0, hDlg, L"");
		break;
	}

	case WM_MOUSELEAVE:
		SendMessage(g_hwndTrackingTT, TTM_TRACKACTIVATE, (WPARAM)FALSE, (LPARAM)&g_toolItem);
		SendMessage(g_hwndTrackingTT1, TTM_TRACKACTIVATE, (WPARAM)FALSE, (LPARAM)&g_toolItem);
		g_TrackingMouse = FALSE;
		return FALSE;

	case WM_MOUSEMOVE:
		static int oldX, oldY;
		int newX, newY;

		if (!g_TrackingMouse)   // The mouse has just entered the window.
		{                       // Request notification when the mouse leaves.

			TRACKMOUSEEVENT tme = { sizeof(TRACKMOUSEEVENT) };
			tme.hwndTrack = hDlg;
			tme.dwFlags = TME_LEAVE;

			TrackMouseEvent(&tme);

			// Activate the tooltip.
			SendMessage(g_hwndTrackingTT, TTM_TRACKACTIVATE, (WPARAM)TRUE, (LPARAM)&g_toolItem);
			SendMessage(g_hwndTrackingTT1, TTM_TRACKACTIVATE, (WPARAM)TRUE, (LPARAM)&g_toolItem);

			g_TrackingMouse = TRUE;
		}

		newX = GET_X_LPARAM(lParam);
		newY = GET_Y_LPARAM(lParam);

		// Make sure the mouse has actually moved. The presence of the tooltip 
		// causes Windows to send the message continuously.

		if ((newX != oldX) || (newY != oldY))
		{
			oldX = newX;
			oldY = newY;

			// Update the text.
			WCHAR coords[12];
			wsprintf(coords, TEXT("%d, %d"), newX, newY);

			g_toolItem.lpszText = coords;
			SendMessage(g_hwndTrackingTT, TTM_SETTOOLINFO, 0, (LPARAM)&g_toolItem);
			SendMessage(g_hwndTrackingTT1, TTM_SETTOOLINFO, 0, (LPARAM)&g_toolItem);

			// Position the tooltip. The coordinates are adjusted so that the tooltip does not overlap the mouse pointer.

			//POINT pt = { newX, newY };
			POINT pt = { 50, 50 };
			ClientToScreen(hDlg, &pt);
			SendMessage(g_hwndTrackingTT, TTM_TRACKPOSITION, 0, (LPARAM)MAKELONG(pt.x + 10, pt.y - 20));
			SendMessage(g_hwndTrackingTT1, TTM_TRACKPOSITION, 0, (LPARAM)MAKELONG(pt.x + 100, pt.y - 20));
		}
		return FALSE;
	case WM_CLOSE:
		EndDialog(hDlg, FALSE);
		break;
	}
	return FALSE;
}


執行結果,

可以自行設定ToolTips的位置TTM_TRACKPOSITION,修改ToolTips的顯示值TTM_SETTOOLINFO, 控制Tooltips的顯示TTM_TRACKACTIVE.

3. 顯示多行文字的ToolTips

多行文字ToolTips參考圖


使用TTM_SETMAXTIPWIDTH 訊息來建立一個多行文字的ToolTips。設定每行的寬度,超過此寬度的文字會自動換行。也可以使用\r\n 強制換行。

注意NMTTDISPINFO 的szText成員最多隻能儲存80個字元。如果要顯示長字串,請用NMTTDISPINFO的lpszText指向一個長文字的字元。

一下例子使用了TTN_GETDISPINFO通知碼來修改tooltips的文字。

    case WM_NOTIFY:
    {
        switch (((LPNMHDR)lParam)->code)
        {
        case TTN_GETDISPINFO:
            LPNMTTDISPINFO pInfo = (LPNMTTDISPINFO)lParam;
            SendMessage(pInfo->hdr.hwndFrom, TTM_SETMAXTIPWIDTH, 0, 150);
            wcscpy_s(pInfo->szText, ARRAYSIZE(pInfo->szText), 
                L"This\nis a very long text string " \
                L"that must be broken into several lines.");
            break;
        }
        break;
    }

測試用例

顯示兩個固定位置的多行提示框 若視窗處於非啟用狀態則隱藏。

提示框的位置會隨著窗的移動而移動。

#include <windows.h>
#include <windowsx.h>
#include <commctrl.h>
#include <stdio.h>
#include "resource.h"
#pragma comment(lib, "comctl32.lib")

LRESULT CALLBACK MyDlgProc(HWND, UINT, WPARAM, LPARAM);

HINSTANCE g_hInst;
HWND hTTWnd;
HWND g_hwndTrackingTT;
HWND g_hwndTrackingTT1;
TOOLINFO g_toolItem;
BOOL g_TrackingMouse = FALSE;

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE, PSTR, int)
{
	g_hInst = hInstance;
	INITCOMMONCONTROLSEX cx = { sizeof(INITCOMMONCONTROLSEX), ICC_BAR_CLASSES };
	BOOL ret = InitCommonControlsEx(&cx);
	return DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, (DLGPROC)MyDlgProc);
}


// Description:
//   Creates a tooltip for an item in a dialog box. 
// Parameters:
//   idTool - identifier of an dialog box item.
//   nDlg - window handle of the dialog box.
//   pszText - string to use as the tooltip text.
// Returns:
//   The handle to the tooltip.
//
HWND CreateToolTip(int toolID, HWND hDlg, PTSTR pszText)
{
	if (!toolID || !hDlg || !pszText)
	{
		return FALSE;
	}
	// Get the window of the tool.
	HWND hwndTool = GetDlgItem(hDlg, toolID);

	// Create the tooltip. g_hInst is the global instance handle.
	HWND hwndTip = CreateWindowEx(NULL, TOOLTIPS_CLASS, NULL,
		WS_POPUP | TTS_ALWAYSTIP | TTS_BALLOON,
		CW_USEDEFAULT, CW_USEDEFAULT,
		CW_USEDEFAULT, CW_USEDEFAULT,
		hDlg, NULL,
		g_hInst, NULL);

	if (!hwndTool || !hwndTip)
	{
		return (HWND)NULL;
	}

	SetWindowPos(hwndTip, HWND_TOPMOST, 0, 0, 0, 0,
		SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
	// Associate the tooltip with the tool.
	TOOLINFO toolInfo = { 0 };
	toolInfo.cbSize = sizeof(toolInfo);
	toolInfo.hwnd = hDlg;
	toolInfo.uFlags = TTF_IDISHWND | TTF_SUBCLASS;
	toolInfo.uId = (UINT_PTR)hwndTool;
	toolInfo.lpszText = LPSTR_TEXTCALLBACK; // pszText;
	SendMessage(hwndTip, TTM_ADDTOOL, 0, (LPARAM)&toolInfo);

	return hwndTip;
}

void CreateToolTipForRect(HWND hwndParent)
{
	// Create a tooltip.
	HWND hwndTT = CreateWindowEx(WS_EX_TOPMOST, TOOLTIPS_CLASS, NULL,
		WS_POPUP | TTS_NOPREFIX | TTS_ALWAYSTIP,
		CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
		hwndParent, NULL, g_hInst, NULL);

	SetWindowPos(hwndTT, HWND_TOPMOST, 0, 0, 0, 0,
		SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);

	// Set up "tool" information. In this case, the "tool" is the entire parent window.

	TOOLINFO ti = { 0 };
	ti.cbSize = sizeof(TOOLINFO);
	ti.uFlags = TTF_SUBCLASS;
	ti.hwnd = hwndParent;
	ti.hinst = g_hInst;
	ti.lpszText = TEXT("This is your tooltip string.");

	GetClientRect(hwndParent, &ti.rect);

	// Associate the tooltip with the "tool" window.
	SendMessage(hwndTT, TTM_ADDTOOL, 0, (LPARAM)(LPTOOLINFO)&ti);
}

HWND CreateTrackingToolTip(int toolID, HWND hDlg, WCHAR* pText)
{
	// Create a tooltip.
	HWND hwndTT = CreateWindowEx(WS_EX_TOPMOST, TOOLTIPS_CLASS, NULL,
		WS_POPUP | TTS_NOPREFIX | TTS_ALWAYSTIP | TTS_BALLOON,
		CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
		hDlg, NULL, g_hInst, NULL);

	if (!hwndTT)
	{
		return NULL;
	}

	// Set up the tool information. In this case, the "tool" is the entire parent window.

	g_toolItem.cbSize = sizeof(TOOLINFO);
	g_toolItem.uFlags = TTF_IDISHWND | TTF_TRACK | TTF_ABSOLUTE;
	g_toolItem.hwnd = hDlg;
	g_toolItem.hinst = g_hInst;
	g_toolItem.lpszText = LPSTR_TEXTCALLBACK;//pText;
	g_toolItem.uId = (UINT_PTR)hDlg;

	GetClientRect(hDlg, &g_toolItem.rect);

	// Associate the tooltip with the tool window.

	SendMessage(hwndTT, TTM_ADDTOOL, 0, (LPARAM)(LPTOOLINFO)&g_toolItem);
	SendMessage(hwndTT, TTM_SETDELAYTIME, (WPARAM)TTDT_AUTOPOP, (LPARAM)MAKELONG(30 * 1000, 0));

	return hwndTT;
}

LRESULT CALLBACK MyDlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static BOOL bActive = FALSE;
	switch (uMsg)
	{
	case WM_INITDIALOG:
	{
		g_hwndTrackingTT = CreateTrackingToolTip(0, hDlg, L"");
		g_hwndTrackingTT1 = CreateTrackingToolTip(0, hDlg, L"");
		//hTTWnd = CreateToolTip(IDCANCEL, hDlg, TEXT("IDCANCEL String \r\n nextline"));
		//CreateToolTipForRect(hDlg);

		break;
	}

	case WM_NOTIFY:
	{
		switch (((LPNMHDR)lParam)->code)
		{
		case TTN_GETDISPINFO: /*TTN_NEEDTEXT:*/
			LPNMTTDISPINFO pInfo = (LPNMTTDISPINFO)lParam;
			SendMessage(pInfo->hdr.hwndFrom, TTM_SETMAXTIPWIDTH, 0, 150);
			wcscpy_s(pInfo->szText, ARRAYSIZE(pInfo->szText),
				L"This\nis a very long text string " \
				L"that must be broken into several lines.");
			break;
		}

		break;
	}
	case WM_MOVE:
	{
		POINT pt = { 50, 50 };
		ClientToScreen(hDlg, &pt);
		SendMessage(g_hwndTrackingTT, TTM_TRACKPOSITION, 0, (LPARAM)MAKELONG(pt.x + 10, pt.y - 20));
		SendMessage(g_hwndTrackingTT1, TTM_TRACKPOSITION, 0, (LPARAM)MAKELONG(pt.x + 10, pt.y + 80));
	}


		break;
	case WM_ACTIVATE:
		// if the main windows is inactive ,disappear the tooltips.
		if (LOWORD(wParam) == WA_INACTIVE)
		{
			SendMessage(g_hwndTrackingTT, TTM_TRACKACTIVATE, (WPARAM)FALSE, (LPARAM)&g_toolItem);
			SendMessage(g_hwndTrackingTT1, TTM_TRACKACTIVATE, (WPARAM)FALSE, (LPARAM)&g_toolItem);
			bActive = FALSE;
		}
		else
		{
			SendMessage(g_hwndTrackingTT, TTM_TRACKACTIVATE, (WPARAM)TRUE, (LPARAM)&g_toolItem);
			SendMessage(g_hwndTrackingTT1, TTM_TRACKACTIVATE, (WPARAM)TRUE, (LPARAM)&g_toolItem);
			POINT pt = { 50, 50 };
			ClientToScreen(hDlg, &pt);
			SendMessage(g_hwndTrackingTT, TTM_TRACKPOSITION, 0, (LPARAM)MAKELONG(pt.x + 10, pt.y - 20));
			SendMessage(g_hwndTrackingTT1, TTM_TRACKPOSITION, 0, (LPARAM)MAKELONG(pt.x + 10, pt.y + 80));
			bActive = TRUE;
		}

		break;

	case WM_MOUSEMOVE:
	{
		if (!bActive)
			break;

		SendMessage(g_hwndTrackingTT, TTM_TRACKACTIVATE, (WPARAM)TRUE, (LPARAM)&g_toolItem);
		SendMessage(g_hwndTrackingTT1, TTM_TRACKACTIVATE, (WPARAM)TRUE, (LPARAM)&g_toolItem);

		POINT pt = { 50, 50 };
		ClientToScreen(hDlg, &pt);
		SendMessage(g_hwndTrackingTT, TTM_TRACKPOSITION, 0, (LPARAM)MAKELONG(pt.x + 10, pt.y - 20));
		SendMessage(g_hwndTrackingTT1, TTM_TRACKPOSITION, 0, (LPARAM)MAKELONG(pt.x + 10, pt.y + 80));

	}
		break;
	case WM_CLOSE:
		EndDialog(hDlg, FALSE);
		break;
	}
	return FALSE;
}


熟悉了ToolTips的用法可以對其用C++做一個封裝方便呼叫。

自己實現了一個可以自行設定位置的基於Windows ToolTips的類。並且使用了SetWindowsLongPtr自行處理了WM_NOTIFY訊息。(主視窗不必關心內部訊息處理)

CToolTips.h  標頭檔案

/*					CToolTips - CToolTips.h
*
*		Author: Sesiria <[email protected]>
*		Copyright (c) 2017 Sesiria.
*
*	CToolTips module header file
*	This module is designed to to encapsulation the behavior of the standard Windows ToolTips control.
*/
#ifndef _CTOOLTIPS_H_
#define _CTOOLTIPS_H_

#include <windows.h>
#include <CommCtrl.h>
#include <map>
#include <list>
#include <string>



class CToolTips  //Based on the Track style of the ToolTips control.
{
	// member function.
public:
	CToolTips(HWND hParentWnd, HINSTANCE hInstance, bool MultiLine = false); //Normal Style and Multiline
	~CToolTips();

	void setText(LPCTSTR szText);
	void setMultiLineText(LPCTSTR szText, const LONG nWidth);
	void initToolTips();
	void setPosition(const POINT& pt);
	void setVisible(bool bVisible);
	void setUpdate();

	static LRESULT CALLBACK tooltipWndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);
	static LRESULT CALLBACK parentWndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);
	WNDPROC getParentWndProc()
	{
		return m_parentWndProc;
	};


private:
	//
	void setActive(bool bActive);
	void _registerWndProc();
	void _unregisterWndProc();

	bool _createToolTips();
	void _destroyToolTips();
	void _addtoInstanceTable();
	void _removeFromInstanceTable();

	// member variable.
private:
	LPTSTR m_szText;
	LPTSTR m_multiText;
	LONG m_nWidth;
	bool m_bMultiLine;
	bool m_bActive;
	bool m_bVisible;
	POINT m_pos;

	HWND m_hParent;
	HWND m_hToolTips;
	HINSTANCE m_hInst;
	TOOLINFO m_toolInfo;

	WNDPROC m_parentWndProc;
	WNDPROC m_tooltipWndProc;

};

#endif // _CTOOLTIPS_H_


CToolTips.cpp 原始檔

/*					CToolTips - CToolTips.cpp
*
*		Author: Sesiria <[email protected]>
*		Copyright (c) 2017 Sesiria.
*
*	CToolTips module source file
*	This module is designed to to encapsulation the behavior of the standard Windows ToolTips control.
*/
#include "CToolTips.h"

#define DELETEP(p)      do { if (p) { delete(p); (p)=NULL; } } while (0)
#define DELETEPV(pa)    do { if (pa) { delete [] (pa); (pa)=NULL; } } while (0)

typedef std::list<HWND> ListInstance;

// this data struct is used to support different parent dialog bind with the tooltips instance.
// the HWND is the parent dialog HWND
// ListInstance is a list container to store all the handle of the tooltips relative the same
// parent dialog.
typedef std::map<HWND, ListInstance*> TableInstance;

/////////////////////////////////////////////////////////////

static TableInstance g_tblInstance;

bool isInTable(HWND hParent)
{
	TableInstance::iterator iter = g_tblInstance.find(hParent);
	if (iter == g_tblInstance.end())
		return false;
	return true;
}

bool isInTable(HWND hParent, HWND hToolTips)
{
	ListInstance * pList = NULL;
	TableInstance::iterator iter = g_tblInstance.find(hParent);
	if (iter == g_tblInstance.end()) // the parent window has not been register.
	{
		return false;
	}
	else // the parent windows has been registered we just get the parent wndproc from the other nodes.
	{
		pList = iter->second;
		HWND hToolTips = *pList->begin();
		ListInstance::const_iterator iterList = std::find(pList->begin(), pList->end(), hToolTips);
		if (iterList == pList->end())
			return false;
	}
	return true;
}

HWND getFirstToolTips(HWND hParent)
{
	if (!isInTable(hParent))
		return NULL;
	ListInstance * pList = NULL;
	TableInstance::iterator iter = g_tblInstance.find(hParent);

	return *iter->second->begin();
}

CToolTips::CToolTips(HWND hParentWnd, HINSTANCE hInstance, bool MultiLine /*= false*/)
	:m_szText(NULL),
	m_multiText(NULL),
	m_nWidth(0),
	m_bMultiLine(MultiLine),
	m_bActive(false),
	m_bVisible(false),
	m_hParent(hParentWnd),
	m_hToolTips(NULL),
	m_parentWndProc(NULL),
	m_hInst(hInstance)
{
	m_pos.x = 0;
	m_pos.y = 0;
	memset(&m_toolInfo, 0, sizeof(TOOLINFO));

	if (_createToolTips())
	{
		_registerWndProc();
		_addtoInstanceTable();
	}
}

CToolTips::~CToolTips()
{
	_removeFromInstanceTable();
	_unregisterWndProc();
	if (m_hToolTips)
	{
		DestroyWindow(m_hToolTips);
		m_hToolTips = NULL;
	}

	DELETEPV(m_szText);
	DELETEPV(m_multiText);
}

bool CToolTips::_createToolTips()
{
	if (!m_hParent || !m_hInst)
		return false;

	// Create the Handle for the ToolTips control
	m_hToolTips = CreateWindowEx(NULL, TOOLTIPS_CLASS, NULL,
		WS_POPUP | TTS_NOPREFIX | TTS_ALWAYSTIP | TTS_BALLOON,
		CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
		m_hParent, NULL, m_hInst, NULL);

	return (m_hToolTips != NULL);
}

void CToolTips::initToolTips()
{
	if (!m_hToolTips || !m_hInst)
		return;

	SetWindowPos(m_hToolTips, HWND_TOPMOST, 0, 0, 0, 0,
		SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);

	//Init the TOOLINFO
	m_toolInfo.cbSize = sizeof(TOOLINFO);
	m_toolInfo.uFlags = TTF_IDISHWND | TTF_TRACK | TTF_ABSOLUTE;
	m_toolInfo.hwnd = m_hParent;
	m_toolInfo.hinst = m_hInst;
	m_toolInfo.lpszText = LPSTR_TEXTCALLBACK;
	m_toolInfo.uId = (UINT_PTR)m_hParent;

	// Associate the tooltip with the tool window.
	SendMessage(m_hToolTips, TTM_ADDTOOL, 0, (LPARAM)(LPTOOLINFO)&m_toolInfo);

	// Set the DelayTime of the ToolTips
	SendMessage(m_hToolTips, TTM_SETDELAYTIME, (WPARAM)TTDT_AUTOPOP, (LPARAM)MAKELONG(30 * 1000, 0));

	// By default, we just set the tooltips to inactive.
	SendMessage(m_hToolTips, TTM_TRACKACTIVATE, (WPARAM)FALSE, (LPARAM)&m_toolInfo);

	m_bActive = true;
}

void CToolTips::_addtoInstanceTable()
{
	ListInstance * pList = NULL;
	TableInstance::iterator iter = g_tblInstance.find(m_hParent);
	if (iter == g_tblInstance.end()) // the parent window has not been register.
	{
		pList = new ListInstance;
		pList->push_back(m_hToolTips);
		g_tblInstance.insert(std::make_pair(m_hParent, pList));
	}
	else
	{
		pList = iter->second;
		ListInstance::const_iterator iterSet = std::find(pList->begin(), pList->end(), m_hToolTips);
		if (iterSet == pList->end())
			pList->push_back(m_hToolTips);
	}
	
}

void CToolTips::_removeFromInstanceTable()
{
	TableInstance::iterator iter = g_tblInstance.find(m_hParent);
	if (iter == g_tblInstance.end())
		return;

	ListInstance * pSet = iter->second;
	pSet->remove(m_hToolTips);

}



void CToolTips::_registerWndProc()
{
	// bind the this pointer to the handle of the tooltips
	SetWindowLongPtr(m_hToolTips, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this)); 

	// Register the windows proc for the tooltip dialog.
	m_tooltipWndProc = (WNDPROC)SetWindowLongPtr(m_hToolTips,
		GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&CToolTips::tooltipWndProc));

	// Register the windows proc for the parents dialog.
	if (!isInTable(m_hParent) && !m_parentWndProc)
	{
		m_parentWndProc = (WNDPROC)SetWindowLongPtr(m_hParent,
			GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&CToolTips::parentWndProc));
	}
	else
	{
		HWND hToolTips;
		if (!(hToolTips = getFirstToolTips(m_hParent)))
			return;
		LONG_PTR user_data = GetWindowLongPtr(hToolTips, GWLP_USERDATA);
		CToolTips *pToolTips = reinterpret_cast<CToolTips*>(user_data);
		m_parentWndProc = pToolTips->getParentWndProc();
	}
}

void CToolTips::_unregisterWndProc()
{
	// if it is the last element relative to the parent dialog just unregister the wndproc.
	TableInstance::iterator iter = g_tblInstance.find(m_hParent);
	if (iter != g_tblInstance.end() && m_parentWndProc != NULL)
	{
		ListInstance *pSet = iter->second;
		if (pSet->size() == 0)// it is the empty set.
		{
			(WNDPROC)SetWindowLongPtr(m_hParent,
				GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(m_parentWndProc));

			g_tblInstance.erase(iter);
			DELETEP(pSet);
		}
		m_parentWndProc = NULL;
	}

	// unregister the window procedure and restore to the default procedure.
	if (m_tooltipWndProc)
	{
		SetWindowLongPtr(m_hToolTips,
			GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(m_tooltipWndProc));
		m_tooltipWndProc = NULL;
	}

	// unregister the this pointer to the hwnd GWL_USERDATA 
	SetWindowLongPtr(m_hToolTips, GWLP_USERDATA, NULL);
}

LRESULT CALLBACK CToolTips::tooltipWndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
	LONG_PTR user_data = GetWindowLongPtr(hWnd, GWLP_USERDATA);
	CToolTips *this_window = reinterpret_cast<CToolTips*>(user_data);
	if (!this_window || !this_window->m_tooltipWndProc)
		return DefWindowProc(hWnd, Msg, wParam, lParam);

	static bool g_TrackingMouse = false;

	switch (Msg)
	{
	case WM_MOUSELEAVE:
		g_TrackingMouse = false;
		return DefWindowProc(hWnd, Msg, wParam, lParam);
		break;
	case WM_MOUSEMOVE:
		if (!g_TrackingMouse)
		{
			TRACKMOUSEEVENT tme = { sizeof(TRACKMOUSEEVENT) };
			tme.hwndTrack = hWnd;
			tme.dwFlags = TME_LEAVE;
			TrackMouseEvent(&tme);
			this_window->setUpdate();
			g_TrackingMouse = true;
		}
		break;
	}

	return CallWindowProc(this_window->m_tooltipWndProc, hWnd, Msg, wParam, lParam);
}

// hook for the parent window procedure
LRESULT CALLBACK CToolTips::parentWndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
	LONG_PTR user_data = GetWindowLongPtr(getFirstToolTips(hWnd), GWLP_USERDATA);
	CToolTips *this_window = reinterpret_cast<CToolTips*>(user_data);

	if (!this_window || !this_window->getParentWndProc())
		return DefWindowProcW(hWnd, Msg, wParam, lParam);

	switch (Msg)
	{
	case WM_NOTIFY:
		{
			switch (((LPNMHDR)lParam)->code)
			{
			case TTN_GETDISPINFO: /*TTN_NEEDTEXT:*/
				LPNMTTDISPINFO pInfo = (LPNMTTDISPINFO)lParam;
				SendMessage(pInfo->hdr.hwndFrom, TTM_SETMAXTIPWIDTH, 0, this_window->m_nWidth);
				lstrcpyn(pInfo->szText, this_window->m_multiText, ARRAYSIZE(pInfo->szText));
				break;
			}
		}
		break;

	case WM_MOVE:
		{
			TableInstance::iterator iter = g_tblInstance.find(hWnd);
			if (iter == g_tblInstance.end())
				break;
			ListInstance *pList = iter->second;
			ListInstance::iterator  iterlist;
			for (iterlist = pList->begin(); iterlist != pList->end(); ++iterlist)
			{
				LONG_PTR user_data = GetWindowLongPtr((HWND)(*iterlist), GWLP_USERDATA);
				CToolTips *tooltips_window = reinterpret_cast<CToolTips*>(user_data);
				if (!tooltips_window)
					continue;
				tooltips_window->setPosition(tooltips_window->m_pos);
			}		
		}
		break;

	case WM_ACTIVATE:
	{
		TableInstance::iterator iter = g_tblInstance.find(hWnd);
		if (iter == g_tblInstance.end())
			break;
		ListInstance *pList = iter->second;
		ListInstance::iterator  iterlist;
		for (iterlist = pList->begin(); iterlist != pList->end(); ++iterlist)
		{
			LONG_PTR user_data = GetWindowLongPtr((HWND)(*iterlist), GWLP_USERDATA);
			CToolTips *tooltips_window = reinterpret_cast<CToolTips*>(user_data);
			if (!tooltips_window)
				continue;
			if (LOWORD(wParam) == WA_INACTIVE)
			{
				tooltips_window->setActive(false);
			}
			else
			{
				tooltips_window->setActive(true);
			}
		}
	}
	}

	return CallWindowProc(this_window->m_parentWndProc, hWnd, Msg, wParam, lParam);
}

void CToolTips::setText(LPCTSTR szText)
{
	DELETEPV(m_szText);
	m_szText = new TCHAR[lstrlen(szText) + 1];
	lstrcpy(m_szText, szText);
}

void CToolTips::setMultiLineText(LPCTSTR szText, const LONG nWidth)
{
	DELETEPV(m_multiText);
	m_multiText = new TCHAR[lstrlen(szText) + 1];
	lstrcpy(m_multiText, szText);

	m_nWidth = nWidth;
	if (m_bActive)
	{
		setUpdate();
	}
}

void CToolTips::setPosition(const POINT& pt)
{
	m_pos.x = pt.x;
	m_pos.y = pt.y;
	POINT newPt = { m_pos.x, m_pos.y};
	ClientToScreen(m_hParent, &newPt);
	SendMessage(m_hToolTips, TTM_TRACKPOSITION, 0, (LPARAM)MAKELONG(newPt.x, newPt.y));
}

void CToolTips::setActive(bool bActive)
{
	m_bActive = bActive;
	if (m_bActive && m_bVisible)
	{
		SendMessage(m_hToolTips, TTM_TRACKACTIVATE, (WPARAM)TRUE, (LPARAM)&m_toolInfo);
		setPosition(m_pos);
	}
	else
	{
		SendMessage(m_hToolTips, TTM_TRACKACTIVATE, (WPARAM)FALSE, (LPARAM)&m_toolInfo);
	}	
}

void CToolTips::setVisible(bool bVisible)
{
	m_bVisible = bVisible;
	if (m_bVisible && m_bActive)
	{
		SendMessage(m_hToolTips, TTM_TRACKACTIVATE, (WPARAM)TRUE, (LPARAM)&m_toolInfo);
		setPosition(m_pos);
	}
	else
	{
		SendMessage(m_hToolTips, TTM_TRACKACTIVATE, (WPARAM)FALSE, (LPARAM)&m_toolInfo);
	}
}


void CToolTips::setUpdate()
{
	SendMessage(m_hToolTips, TTM_UPDATE, 0, 0);
}


main.cpp 測試程式碼

#include <windows.h>
#include <windowsx.h>
#include <commctrl.h>
#include <stdio.h>
#include <assert.h>
#include "resource.h"
#include "CToolTips.h"
#pragma comment(lib, "comctl32.lib")


#define MULTILINE_TEXT	TEXT("This\nis a very long text string that must be broken into several lines.")
#define MULTILINE_TEXT_TIME	TEXT("This\nis a very long text string that must be broken into several lines. %d")

LRESULT CALLBACK MyDlgProc(HWND, UINT, WPARAM, LPARAM);
LRESULT CALLBACK MyDlgProc1(HWND, UINT, WPARAM, LPARAM);

HINSTANCE g_hInst;
CToolTips *g_pToolTips = NULL;
CToolTips *g_pToolTips1 = NULL;

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE, PSTR, int)
{
	g_hInst = hInstance;
	INITCOMMONCONTROLSEX cx = { sizeof(INITCOMMONCONTROLSEX), ICC_BAR_CLASSES };
	BOOL ret = InitCommonControlsEx(&cx);
	return DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, (DLGPROC)MyDlgProc1);
}

void TEST_Constructor(HWND hDlg)
{
	g_pToolTips = new CToolTips(hDlg, g_hInst, true);
	assert(g_pToolTips != NULL);
	g_pToolTips1 = new CToolTips(hDlg, g_hInst, true);
}

void TEST_Destructor()
{
	if (g_pToolTips)
	{
		delete g_pToolTips;
		g_pToolTips = NULL;
	}

	if (g_pToolTips1)
	{
		delete g_pToolTips1;
		g_pToolTips1 = NULL;
	}
}

void TEST_MultilineToolTips()
{
	if (!g_pToolTips)
		return;
	g_pToolTips->setMultiLineText(MULTILINE_TEXT, 150);
	g_pToolTips->initToolTips();
	POINT pt = { 50, 50 };
	g_pToolTips->setPosition(pt);
	g_pToolTips->setVisible(true);

	if (!g_pToolTips1)
		return;
	g_pToolTips1->setMultiLineText(MULTILINE_TEXT, 150);
	g_pToolTips1->initToolTips();
	POINT pt1 = { 100, 50 };
	g_pToolTips1->setPosition(pt1);
	g_pToolTips1->setVisible(true);
}

void TEST_StartDynamicUpdate(HWND HDlg)
{
	SetTimer(HDlg, 1, 5000, NULL);
}
void TEST_DynamicUpdateToolTips(HWND hDlg)
{
	TCHAR buf[255] = { 0 };
	wsprintf(buf, MULTILINE_TEXT_TIME, GetCurrentTime());
	//
	//
	static int i = 0;
	if (i % 2 == 0)
		g_pToolTips1->setMultiLineText(buf, 150);
	else
		g_pToolTips->setMultiLineText(buf, 150);
	i++;
}

void TEST_KillDynamicUpdate(HWND hDlg)
{
	KillTimer(hDlg, 1);
}

LRESULT CALLBACK MyDlgProc1(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static CToolTips * pToolTips;
	switch (uMsg)
	{
	case WM_INITDIALOG:
		TEST_Constructor(hDlg);
		TEST_MultilineToolTips();
		TEST_StartDynamicUpdate(hDlg);
		break;
	case WM_TIMER:
		TEST_DynamicUpdateToolTips(hDlg);
		break;

	case WM_CLOSE:
		TEST_KillDynamicUpdate(hDlg);
		TEST_Destructor();
		EndDialog(hDlg, FALSE);
		break;
	}
	return FALSE;

}


執行結果如下

建立了兩個多行氣泡的ToolTips,  定時器每間隔5秒會更新氣泡的內容,氣泡的位置會隨著視窗被拖動而自行 調整, 當視窗處於非啟用狀態時候氣泡自動隱藏。

單滑鼠指向某一個氣泡的時候,該氣泡預設會顯示在視窗最頂端。