Skip to content

feat(win10-taskbar): take over the taskbar app-button context menu (Shell-styled custom menu) - #847

Open
v4806 wants to merge 6 commits into
moudey:mainfrom
v4806:feat/win10-taskbar-button-menu
Open

v4806 wants to merge 6 commits into
moudey:mainfrom
v4806:feat/win10-taskbar-button-menu

Conversation

@v4806

@v4806 v4806 commented Oct 11, 2026 •

Copy link
Copy Markdown

Problem

On Windows 10, right-clicking a taskbar app button still pops the ShellExperienceHost XAML jump
list, even though the taskbar background menu is already owned by Shell.

Root cause

The button menu is not a Win32 menu: no #32768 window, no TrackPopupMenu(Ex), no
IContextMenu - no ordinary hook can see it. The only entry point is to subclass the taskbar button
area window and intercept WM_CONTEXTMENU.

Approach

  • Subclass Shell_TrayWnd -> ReBarWindow32 -> MSTaskSwWClass -> MSTaskListWClass and intercept
    WM_CONTEXTMENU, swallowing it only when the click really lands on an app button; clicks in the gaps
    are passed through so explorer (and Shell) still shows the regular taskbar menu.
  • Identify the button with UIA ElementFromPoint. ControlType must accept Button, MenuItem and
    SplitButton - a merged multi-window button is exposed as MenuItem. Button identity comes from the
    shell:AppsFolder\<AUMID> display name.
  • Hand a custom HMENU to TrackPopupMenuExProc: Shell snapshots and re-themes it, so the menu
    looks native without any custom drawing.
  • Items: app name (clickable - activates the window, or launches the app when it is not running) /
    pin / unpin / close window. Closing uses the UIA WindowPattern::Close() and closes every window of
    a merged button (one "Close all windows" entry); parallel fallbacks are used to bring elevated
    windows to the front.
  • Pin / unpin: taskbarunpin verb on the pinned .lnk -> exe verb -> the official
    IStartMenuPinnedList fallback. After a successful pin the shortcut's AppUserModel property set is
    completed so it merges with the running window.
  • The button menu gets its own context type FSO_TASKBAR_BUTTON: window.is_taskbar_button is true
    only there and window.is_taskbar returns false, so existing type='taskbar' rules (e.g. "Go to")
    do not leak in while .nss items can target the button menu directly.
  • New built-in command.kill_process_tree terminates the process tree of all windows behind the button
    (leaves-first from a Toolhelp snapshot). window.taskbar_button_windows exposes the matched window
    count so configs can hide dead items on buttons whose app is not running.
  • The hook covers the main taskbar and every secondary taskbar, and a background watchdog re-arms it
    every 3 seconds, so a rebuilt taskbar window (DPI change, monitor hotplug, theme switch) does not
    silently drop back to the native menu.

Scope

Windows 10 only - the hook is installed only when IsWindows11OrGreater() is false, so the
Windows 11 path is untouched. The changes are concentrated in Main.cpp; ContextMenu.cpp only adds
2 entry conditions.

Verification

Win10 22H2 + Shell 1.9.19:

Item Result
Button menu Shell-drawn style, no more XAML jump list
Pin / unpin verified for running and not-running pinned items
Close window UWP Settings, Edge, QQ, FlClash, Everything
Merged multi-window button taken over as well; "Close all windows" closes every window
.nss customisation where=window.is_taskbar_button items show up only in the button menu
command.kill_process_tree [KILL] roots=2 processes=4 killed=4 (child processes included)
App-name item brings the window to the front, elevated ones included (ok=1 fg=0x450166); launches the app when it is not running
Taskbar background menu behaviour unchanged
Build VS2026 + PlatformToolset=v145, release x64

v4806 added 6 commits October 12, 2026 02:27
现象
  Win10 上右键任务栏的应用按钮,弹的是 ShellExperienceHost 的 XAML 跳转列表,
  完全绕过 Shell;任务栏空白处却已被 Shell 接管。两者观感不一致。

根因
  任务栏按钮的右键菜单不是 Win32 菜单:弹出时无 #32768 窗口、不经过 TrackPopupMenu(Ex)、
  也不经过 IContextMenu,无法用常规 Hook 拦截(实测埋日志:桌面/托盘/任务栏空白处都有日志,
  只有按钮菜单一条都没有)。

做法
  - subclass 任务栏按钮区窗口链 Shell_TrayWnd → ReBarWindow32 → MSTaskSwWClass → MSTaskListWClass,
    拦 WM_CONTEXTMENU;只有点在真正的应用按钮上才吞掉,空白处放行给 explorer。
  - 用 UIA ElementFromPoint 识别按钮(ControlType 接受 Button / MenuItem / SplitButton,
    多窗口合并按钮是 MenuItem);按钮身份经 shell:AppsFolder 显示名解析。
  - 自建 HMENU 后交给 TrackPopupMenuExProc,Shell 会抓快照并按自己的主题重绘 ——
    菜单因此自动变成 Shell 样式(方案的关键)。
  - 菜单内容:应用名 / 固定或取消固定 / 关闭窗口(仅在有运行窗口时)。
  - 自建菜单期间置 in_taskbar_button_menu,ContextMenu 的两处配置项入口跳过 .nss 规则。

改动范围
  仅 Win10 分支生效(IsWindows11OrGreater() 为假时才挂 hook);Win11 路径不变。

验证
  Win10 22H2 + Shell 1.9.19:按钮菜单已是 Shell 样式;固定/取消固定、关闭窗口(含 UWP)、
  多窗口合并按钮均实测通过;任务栏空白处行为不变。
- 按钮菜单的上下文类型改成独立的 FSO_TASKBAR_BUTTON:用 type='taskbar' / window.is_taskbar
  写的既有项(例如「转到」)不会再涌进按钮菜单;配置里用 where=window.is_taskbar_button
  可以精确命中按钮菜单(任务栏空白处、桌面、文件菜单都是 false)。
- 新增内置命令 command.kill_process_tree:按 Toolhelp 快照展开按钮下**所有**窗口的进程树,
  再从叶子往根终止(先杀父进程的话子进程会被系统改挂,杀树会漏)。
- 修好控制台窗口(cmd.exe)认不出来:AppsFolder 里 cmd.exe 的键是
  {1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\cmd.exe,拿 exe 全路径查显示名必然失败。
  增加「按 exe 文件名查 AppsFolder」兜底(整表只建一次 + 缓存)。
  cmd 按钮因此恢复:结束进程 / 关闭窗口 / 固定到任务栏。
- hook 重构成「主任务栏 + 每个副屏任务栏(Shell_SecondaryTrayWnd)」,并加长期巡检:
  任务栏窗口被就地重建(改 DPI / 插拔显示器 / 切主题)时 subclass 会跟着丢,
  原先只重试 15 次就结束,之后按钮菜单会静默退回原生 XAML 跳转列表 —— 现在每 3 秒巡检一次,
  发现丢了就重新挂上(只有真的新挂上才写日志)。顺手去掉 4 行 [PROBE] 启动日志。
- 新增 window.taskbar_button_windows(按钮菜单当前匹配到的窗口数),用来做死项治理:
  未运行 / 窗口认不出来的按钮没有可操作目标,配置项可用它把「结束进程」这类死项藏掉。
以前应用名项的 id 写 0,点了完全没有反应。现在点击 =
- 有运行窗口:切到前台(最小化先 SW_RESTORE;SetForegroundWindow 被前台锁拒绝时
  用 AttachThreadInput 接上当前前台线程再试一次);
- 没在运行 / 没有可见窗口(例如只留在托盘的 Everything):启动程序,
  优先走固定项 .lnk(能带上它原有的工作目录与参数),否则用窗口进程的 exe 路径。
任务管理器这类以管理员运行(高完整性)的窗口,从 explorer(中完整性)切前台时会失败:
- SetForegroundWindow 被 Windows 的前台锁拒绝;
- 常规兜底 AttachThreadInput 要访问目标线程的输入队列,**跨完整性被 UIPI 挡掉**;
  → 结果就是「点了完全没反应」(用户实测连点 4 次,日志里 4 行 activate 但窗口没动)。

现在按代价递增依次尝试,并把结果写进日志:
1) IsIconic → ShowWindow(SW_RESTORE)(窗口最小化时这条就够,不碰输入队列)
2) SetForegroundWindow
3) AttachThreadInput + SetForegroundWindow(同完整性时有效)
4) 模拟一次 ALT 键解开前台锁 + SetForegroundWindow(对提权窗口有效)
5) SwitchToThisWindow(hwnd, TRUE)(任务栏点击内部用的老 API)

日志改成 [BTN] activate window=0x.. ok=.. fg=0x..,以后「点了没反应」一眼能看出卡在哪一步。
实测:对提权的任务管理器连续两次 ok=1 且 fg 就是目标窗口。
为便于上游审核,把开发过程中留下的噪声还原为官方状态,行为零变化:

- 删除全仓无调用点的 find_app_window() / is_pinned_to_taskbar(),
  以及只写不读的 taskbar_window_finder::hwnd 字段与其赋值
- 新增注释统一改写为英文(Main.cpp 188 行、ContextMenu.cpp 11 行、
  FuncExpression.cpp 10 行、FileSystemObjects.h 2 行)
- 删除 [PROBE] / 无条件 IAT hook 试验等历史诊断注释 6 行
- Main.cpp / FuncExpression.cpp 恢复与官方一致的 UTF-8 BOM,去掉 1 处多余空行

验证:剥掉注释与空行后逐行比对,代码序列仅少上述死代码(added = 0);
编译 EXIT=0(v145 release x64,无 C4505);已部署并实机验证菜单功能正常。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant