为什么我的程序在按钮上没有显示工具提示?
我需要创建一个带有工具提示的按钮。程序会显示按钮,但不会显示工具提示。
我已经创建了一个用于演示问题的最小可复现示例(MRE)。按钮和工具提示是在一个 CreateButton() 函数中创建的。该程序是一个没有资源的WinAPI应用程序。
问题出在哪里?
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdlib.h>
#include <malloc.h>
#include <memory.h>
#include <tchar.h>
#include "assert.h"
#include "commctrl.h"
#pragma comment( lib, "comctl32" )
#define MAX_LOADSTRING 100
#define ID_MY_BUTTON1 11
HINSTANCE hInst;
WCHAR szTitle[MAX_LOADSTRING] = L"Window title";
WCHAR szWindowClass[MAX_LOADSTRING] = L"WindowClass";
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
INITCOMMONCONTROLSEX icex;
icex.dwSize = sizeof(INITCOMMONCONTROLSEX);
icex.dwICC = ICC_BAR_CLASSES | ICC_TAB_CLASSES;
bool ok = InitCommonControlsEx(&icex);
assert(ok == TRUE);
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
MyRegisterClass(hInstance);
if (!InitInstance(hInstance, nCmdShow))
return FALSE;
MSG msg;
while (GetMessage(&msg, nullptr, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (int)msg.wParam;
}
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEXW wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = NULL;
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = NULL;
return RegisterClassExW(&wcex);
}
bool CreateButton(HINSTANCE hInstance, HWND hMainWnd) {
bool ok = true;
int StartX = 0;
const int ButtonWidth = 120;
RECT rect;
HMENU ButtonID = (HMENU) 1;
HWND hButton = CreateWindow(L"BUTTON", L"My button 1",
WS_VISIBLE | WS_CHILD,
StartX, 0, ButtonWidth, 30,
hMainWnd, ButtonID, hInst, NULL);
// Create the tooltip. g_hInst is the global instance handle.
HWND hwndTip = CreateWindowEx(WS_EX_TOPMOST, TOOLTIPS_CLASS, NULL,
TTS_ALWAYSTIP,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
hButton, NULL,
hInstance, NULL);
assert(hwndTip != NULL);
// Associate the tooltip with the tool.
TOOLINFO toolInfo = { 0 };
toolInfo.cbSize = sizeof(toolInfo);
toolInfo.uFlags = TTF_IDISHWND | TTF_SUBCLASS;
toolInfo.hwnd = hMainWnd;
toolInfo.uId = (UINT_PTR) hButton;
toolInfo.lpszText = (LPWSTR)L"tool tip";
toolInfo.hinst = hInstance;
auto res = SendMessage(hwndTip, TTM_ADDTOOL, 0, (LPARAM)&toolInfo);
return true;
}
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
hInst = hInstance;
HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
assert(hWnd != NULL);
CreateButton(hInst, hWnd);
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_COMMAND:
{
int wmId = LOWORD(wParam);
// Разобрать выбор в меню:
switch (wmId) {
case ID_MY_BUTTON1: {
break;
}
*/
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
EndPaint(hWnd, &ps);
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
解决方案
简要结论:附加一个 应用程序清单,声明对Common Controls v6程序集的依赖。同时,最好声明 DPI感知、 系统功能兼容性 和 UAC(用户账户控制)合规性 等。
详细回答:程序之所以不显示工具提示,是因为发送 TTM_ADDTOOL 消息返回了 FALSE,表示失败。这个失败的原因是因为 TOOLINFO::cbSize 字段填写不正确。处理公共控件时要记住底层实现至少有四种变体:ANSI与 UNICODE、DLL版本5与 DLL版本6。所讨论的代码显然是为UNICODE变体定制的,在没有清单的情况下它将使用DLL版本5。关键在于不同DLL版本对结构体的大小有些差异:[不同公共控件版本的结构体大小] 的说明。仅仅包含Windows头文件就会将 TOOLINFO 结构声明为最大大小,对应于可以被DLL版本6处理的 TTTOOLINFOW_V3_SIZE 值。ANSI的 DLL版本5能接受比预期更大的结构体大小,而UNICODE的 DLL版本5会拒绝它(这实际上可能是大小检查中的一个bug)。
为了让两者都“满意”,可以使用 TTTOOLINFOW_V2_SIZE 值。不过这会产生Win95风格的工具提示。
更好的做法是使用 TTTOOLINFOW_V3_SIZE 值(应该等于 sizeof(toolInfo))并声明对DLL v6的依赖。这将产生看起来更现代的控件。
可以同时以ANSI和 UNICODE构建的MCVE,取消注释 COMM_CTRL_V6 以使用DLL v6:
#define WIN32_LEAN_AND_MEAN
#include <tchar.h>
#include <Windows.h>
#include <CommCtrl.h>
#pragma comment(lib, "comctl32")
//#define COMM_CTRL_V6
#ifdef COMM_CTRL_V6
#if defined _M_IX86
#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='x86' publicKeyToken='6595b64144ccf1df' language='*'\"")
#elif defined _M_X64
#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='amd64' publicKeyToken='6595b64144ccf1df' language='*'\"")
#else
#error Unknown platform
#endif
#endif
LRESULT CALLBACK
WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_DESTROY: PostQuitMessage(0); return 0;
default: return DefWindowProc(hWnd, message, wParam, lParam);
}
}
int APIENTRY
#ifdef _UNICODE
wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow)
#else
WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
#endif
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
INITCOMMONCONTROLSEX icex = {};
icex.dwSize = sizeof(INITCOMMONCONTROLSEX);
icex.dwICC = ICC_BAR_CLASSES;
if (FALSE == InitCommonControlsEx(&icex))
{
return 1;
}
WNDCLASSEX wcex = {};
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = NULL;
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = _T("WindowClass");
wcex.hIconSm = NULL;
if (ATOM(0) == RegisterClassEx(&wcex))
{
return 2;
}
HWND hWnd = CreateWindow(wcex.lpszClassName, _T("Window title"), WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
if (HWND() == hWnd)
{
return 3;
}
HWND hButton = CreateWindow(WC_BUTTON, _T("My button 1"),
WS_VISIBLE | WS_CHILD,
0, 0, 120, 30,
hWnd, (HMENU) 11, hInstance, NULL);
if (HWND(0) == hButton)
{
return 4;
}
// Create the tooltip. g_hInst is the global instance handle.
HWND hwndTip = CreateWindowEx(WS_EX_TOPMOST, TOOLTIPS_CLASS, NULL,
WS_POPUP | TTS_NOPREFIX | TTS_ALWAYSTIP,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
hWnd, NULL,
hInstance, NULL);
if (HWND(0) == hwndTip)
{
return 5;
}
// Associate the tooltip with the tool.
TOOLINFO toolInfo = {};
#ifdef COMM_CTRL_V6
toolInfo.cbSize = TTTOOLINFOW_V3_SIZE;
static_assert(TTTOOLINFOW_V3_SIZE <= sizeof(toolInfo));
#else
toolInfo.cbSize = TTTOOLINFOW_V2_SIZE;
static_assert(TTTOOLINFOW_V2_SIZE <= sizeof(toolInfo));
#endif
// toolInfo.cbSize = sizeof(toolInfo);
toolInfo.uFlags = TTF_IDISHWND | TTF_SUBCLASS;
toolInfo.hwnd = hWnd;
toolInfo.uId = (UINT_PTR) hButton;
toolInfo.lpszText = const_cast<decltype(toolInfo.lpszText)>(_T("tool tip"));
if (FALSE == SendMessage(hwndTip, TTM_ADDTOOL, 0, (LPARAM)&toolInfo))
{
return 6;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
if (FALSE == UnregisterClass(wcex.lpszClassName, hInstance))
{
return 7;
}
return (int)msg.wParam;
}

