本文最初发表于个人博客。发布日期 12/03/2026.
近日我在开发一款美化软件,以实现覆盖 StartIsBack / StartAllBack 的功能。目前已完成低级钩子的注入与任务栏的子类化。仓库如下:
https://github.com/Bruce225/anabiosis
开发途中遇到一个我自昨日下午起开始着手解决的 Bug; 在历经数小时令人烦躁的修改与调试后,终得以于深更半夜修复。下简要分享思路与调试过程。
前情提要
想要在 Windows 11 上实现类似 StartAllBack 的开始按钮与开始菜单修改,大多数人的第一想法应该是在获取开始按钮的句柄 hStartBtn 后,使用 ShowWindow(hStartBtn, SW_HIDE) 语句隐藏它,并在任务栏中 “嵌入” 自己的开始按钮。
然而 Windows 11 将许多的任务栏窗口组件使用了 XAML 重绘。即使在 Spy++ 等软件上能够看到 Shell_TrayWnd 下存在 “Start” 类,但他已实质上变成不受属性控制的 “代理句柄” ,导致操作 hStartBtn 对开始按钮不构成任何影响。故开发时,选择「覆盖」而不是「隐藏并添加」是更好的思路。关于「覆盖」部分,我的代码如下:
HWND CreateOrbWindow(HINSTANCE hInstance)
{
/* 此处略去一些类定义 */
HWND hTaskbar = FindWindow(L"Shell_TrayWnd", NULL);
// 获取原 Start 按钮坐标以覆盖
RECT r = {};
if (g_hStartBtn) GetWindowRe ct(g_hStartBtn, &r);
int w = r.right - r.left;
int h = r.bottom - r.top;
HWND hWnd = CreateWindowExW(
WS_EX_TOPMOST | WS_EX_TOOLWINDOW, // 置顶 & 不进任务栏
className, L"Orb",
WS_POPUP,
r.left, r.top, w, h,
hTaskbar,
NULL, hInstance, NULL
);
if (hWnd) ShowWindow(hWnd, SW_SHOW);
return hWnd;
}
这相当于在原开始按钮的位置上覆盖了一个自定义 Orb 按钮。其中,指定 hTaskbar 为其父窗口是为了防止点按任务栏时自定义按钮消失。
Bug 的发现
如上的逻辑在 90% 的测试情况下都没有任何问题。快速点按开始按钮、不间断敲打 Win 键或在各种情形下点按任务栏。然而当点击任务栏最右侧 “显示桌面” 按钮,或按下 Win+D 组合键时,该 Orb 按钮会突然消失;再次单击 “显示桌面” ,当左键按下时 (WM_LBUTTONDOWN) 该 Orb 按钮重新显现,松开时 (WM_LBUTTONUP) Orb 按钮仍然消失。若继续点击 “显示桌面” ,上述过程循环往复。
我起初的解决方案是在该 Orb 按钮窗口的过程中,判定 uMsg 时加入如下定时刷新器:
LRESULT CALLBACK OrbWndProc(HWND hwnd, UINT uMsg,
WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_CREATE:
SetTimer(hwnd, 1, 50, NULL); // 50ms 响应
return 0;
case WM_TIMER:
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
break;
/* 其他 case 略去 */
}
return DefWindowProcW(hwnd, uMsg, wParam, lParam);
}
当快速点按 “显示桌面” 时,虽然 Orb 按钮不断重绘并显现,但仍然会在极短的间隙内闪过原版开始按钮,即使将设定的 50ms 减小也无济于事。况且,该种 Timer 轮询的方法极为消耗 CPU 性能。
调试过程
使用定时器显然不是一个好方法,于是我将他移除了。我试图通过将 CreateWindowExW 中的 WS_POPUP 改为 WS_CHILD 来防止 Orb 按钮随 hTaskbar 一起隐藏,又试图通过拦截 Win+D 可能在执行 “显示桌面” 逻辑时向我的 Orb 按钮窗口发送的隐藏信号。
这两个方法都没有解决 Bug.
偶然间,我关注到了 CreateWindowExW 中的 r.left, r.top, w, h 这些坐标与大小参数,于是将 r.left 与 r.top 修改为了 (0, 0) 以及 (0, 1000) 分别进行调试 (我使用的是 1920 x 1080 的显示器)。最终发现,当按下 “显示桌面” 时,我的 Orb 按钮完全不会被它隐藏!
尤其是 (0, 1000), 我注意到按下 “显示桌面” 时它位于任务栏上方的部分未被隐藏,本身覆盖在开始按钮的那部分消失了。也就是说,这个 Bug 本质上是一个 Z-Order 排列竞争的问题。
我猜测是如下的逻辑:在 Windows 11 中,所有置顶窗口 (WM_EX_TOPMOST 属性) 都在同一个平面的 Z 轴序列里。由于任务栏是系统的核心组件,点击 “显示桌面” 时,Explorer.exe 会为了确保任务栏不被其他窗口 (比如可能存在的全屏置顶广告或工具栏) 遮挡,强制刷新一次自己的 Z 轴排序。于是,刚好把我的 Orb 按钮压在了下面。
彻底修复
我的思路是检测按下 “显示桌面” 后窗口的位置变化 (WM_WINDOWPOSCHANGED). 原本的想法是监控开始按钮 (因为我本身代码中对任务栏进行过子类化并寻过开始菜单的句柄 hStartBtn)。
事实上,通过向 DbgView 输出调试字符串的验证方式后,我发现此时开始按钮并没有发生 WM_WINDOWPOSCHANGED. 因此需要转向检测任务栏的位置变化。
首先是在 dll 劫持函数中将劫持目标选为任务栏类 Shell_TrayWnd, 对其进行子类化,并让其处理函数变为我们编写的 NewTaskbarProc. 代码如下:
void StartHijack(HMODULE hModule)
{
HWND hTaskbar = FindWindow(L"Shell_TrayWnd", NULL);
/* 调试信息略去 */
LONG_PTR oldProc = SetWindowLongPtr(hTaskbar,
GWLP_WNDPROC, (LONG_PTR)NewTaskbarProc);
// 子类化
if (oldProc)
{
OldTaskbarProc = (WNDPROC)oldProc;
g_hookedWnd = hTaskbar;
}
/* 启动线程略去 */
}
其中 hModule 与专用线程相关,与本文内容无关。接着在新逻辑 NewTaskbarProc 函数中,拦截任务栏的位置变化消息 WM_WINDOWPOSCHANGED 并通过 SetWindowPos 函数将自定义 Orb 提前。代码如下:
LRESULT CALLBACK NewTaskbarProc(HWND hwnd, UINT uMsg,
WPARAM wParam, LPARAM lParam)
{
// 拦截窗口位置变化消息
if (uMsg == WM_WINDOWPOSCHANGED)
{
// 置顶任务栏
LRESULT ret = CallWindowProc(OldTaskbarProc,
hwnd, uMsg, wParam, lParam);
// 将 Orb 提到最顶层
if (g_hOrbWnd)
{
RECT r = {0};
if (g_hStartBtn) GetWindowRect(g_hStartBtn, &r);
SetWindowPos(g_hOrbWnd, HWND_TOPMOST, r.left, r.top, 0, 0,
SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE);
}
return ret;
}
return CallWindowProc(OldTaskbarProc, hwnd, uMsg, wParam, lParam);
}
这样,每次点按 “显示桌面” 或 Win+D 时,我们的任务栏行为都能使得自定义 Orb 窗口被提到顶层,中间没有任何间隙 (或实则极小,肉眼无法察觉), 达到目的。由此 Bug 修复。
IDE: Microsoft Visual Studio 2026
OS: Windows 11 Enterprise LTSC 2024 (24H2)
Hardware: Intel Core i9-10940X / RTX 3080 / 32GB RAM / Optane P4800X