# TaskbarMetrics **Repository Path**: jason1012/taskbar-metrics ## Basic Information - **Project Name**: TaskbarMetrics - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-02 - **Last Updated**: 2026-10-02 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # TaskbarMetrics Windows 任务栏部件,显示网速、CPU、GPU、内存占用。 纯 C++17 / Win32 / GDI,无 MFC、无 ATL、无 GDI+、无 Direct2D。编译产物单个 exe,约 210 KB, 除系统 DLL 外无外部依赖(连 VC++ Runtime 都不需要)。 部件出现在任务栏通知区左侧,显示网速(上行/下行)、CPU、GPU、内存占用: ``` ↑ 1.0 MB/s ↓ 31 KB/s CPU 68% GPU 19% 内存 62% ( 9.8 GB) ``` > 作者:Jason201012 · 版本 0.2.0 ## 下载 到仓库的 [Releases](../../releases) 页下载 `TaskbarMetrics-0.2.0.zip`,解压后双击 `TaskbarMetrics.exe`。 **请把 exe 放在任意可写目录,不要放 `C:\Program Files`** —— 配置文件与日志都写在 exe 同目录,那里需要管理员权限,程序会启动失败。 未签名程序,Windows 可能提示「Windows 已保护你的电脑」,点**更多信息 → 仍要运行**即可。 想自己编译的话见[构建](#构建),一条 `build.bat` 的事。 ## 目录 - [特性](#特性) - [构建](#构建) - [使用](#使用) - [设置项](#设置项) - [架构](#架构) - [窗口模式:为什么有两种](#窗口模式为什么有两种) - [踩坑清单](#踩坑清单) - [调试](#调试) - [已知限制](#已知限制) --- ## 特性 - 逐像素透明,融进任务栏背景(32 位预乘 alpha,不是猜一个背景色去色键) - 网速(上行/下行)、CPU、GPU、内存百分比,内存用量可选 - 单行 / 双行两种布局,每个显示项都能单独开关 - 文字颜色自动跟随任务栏明暗主题(采样任务栏像素亮度,不是读注册表) - 字体大小、刷新间隔可配 - 开机自启(`HKCU\...\Run`,不需要管理员权限) - Explorer 崩溃重启后自动恢复 - 全部显示项在设置窗口里改,改完立即生效,无需重启 - 右键「卸载清理」一键移除自启项与配置 **当前环境**:Windows 10 / 11,任务栏在顶部或底部。竖直任务栏与 Win11 右对齐 的定位尚未适配,见[已知限制](#已知限制)。 --- ## 构建 需要 Visual Studio Build Tools(MSVC v14.51+,C++17)。项目自带 `build.bat`: ```bat build.bat ``` 产物是 `TaskbarMetrics.exe`,中间文件在 `build\`。 `build.bat` 做了三件有先后依赖的事,顺序不能改: ``` rc.exe settings.rc -> build\settings.res cvtres.exe .res -> build\settings_res.obj cl.exe *.cpp + app.manifest -> TaskbarMetrics.exe ``` 两个非显然的地方,都写在脚本注释里了: - **`cvtres` 的输出名不能是 `settings.obj`。** 下面 `cl` 编译 `settings.cpp` 也会生成 `build\settings.obj`,后写的会把资源对象覆盖掉,链接器拿到的 `.obj` 里没有对话框资源。 表现是 `DialogBoxParamW` **静默失败**(返回 0 且不弹窗)——极难定位。 - **`cvtres.exe` 在 MSVC 工具集里,不在 Windows Kits\bin 下。** `app.manifest` 通过 `/link /MANIFEST:EMBED` 嵌进 exe,用来声明 `PerMonitorV2` DPI 感知。 这一条是硬性的,原因见[踩坑清单](#dpi)。 --- ## 使用 启动后部件出现在任务栏通知区左侧。右键: | 菜单项 | 作用 | |---|---| | 隐藏 / 显示 | 临时收起部件 | | 设置 | 打开设置窗口(含「关于」查看版本号) | | 卸载清理 | 删掉自启项与配置,然后退出 | | 退出 | 结束进程 | 部分透明像素区域鼠标穿透到底下的任务栏 —— 点击图标会直接打开对应程序。 **卸载**:右键选「卸载清理」。它会删掉注册表自启项、`settings.ini` 和日志。 `TaskbarMetrics.exe` 需要你手动删除 —— 进程运行期间 Windows 不允许程序删除自己。 ### 开发时的实测参数 以下是 `tools\probe-taskbar.ps1` 探针在开发机(2560×1440 / 125% 缩放 / Win11 24H2) 上测得的,写在这里是为了解释代码里那些看起来像魔数的常量: ``` 物理屏幕 2560 x 1440 任务栏 (0,1380)-(2560,1440) 高 60px DPI 120 (96 * 1.25, 125% 缩放) TrayNotifyWnd 左边缘 x = 1887~2127 (随托盘图标数量浮动) GetIfTable2 返回 61 行,但物理网卡只有 5 张 GPU \GPU Engine(*) 返回约 320~400 个实例,有效 4~7 个 ``` **换一台分辨率或缩放不同的机器时这些值都会变**,但代码是运行时取的,不需要改常量。 --- ## 设置项 | 项 | 取值 | |---|---| | 刷新间隔 | 500 ms / 1 s / 2 s / 5 s | | 布局 | 单行 / 双行 | | 字号 | 8 / 9 / 10 / 11 pt | | 显示项目 | 上行、下行、CPU、GPU、内存百分比、内存用量(各自独立开关) | | 开机自动启动 | 写 `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` | | 启用诊断日志 | 写 `TaskbarMetrics.log` | | 嵌入任务栏(实验) | 见[窗口模式](#窗口模式为什么有两种) | 部件固定贴在系统托盘(通知区)左侧。原先还有个「贴开始按钮左侧」的选项,已删除 —— 两种位置实测没有明显优劣,而多一个选项就多一份「为什么它跑到那边去了」的支持成本。 配置存 `settings.ini`(UTF-16,与 exe 同目录)。 --- ## 架构 ``` main.cpp 采集 + 窗口过程 + 渲染 + Z 序 + 上下文菜单 settings.cpp 设置模型 / INI 持久化 / 对话框过程 settings.h 控件 ID + SettingsData + 对外接口 settings.rc 对话框模板(资源脚本) logger.cpp/h Log(),单独抽出因为 settings.cpp 也要链它 app.manifest PerMonitorV2 DPI 感知 ``` ### 数据流 定时器(默认 1 Hz)触发: ``` CollectCpu / CollectGpu / CollectMemory / CollectNetwork ↓ RefreshMetricsAndText() 生成两行文本,按最宽模板测定尺寸 ↓ UpdatePosition() 贴到任务栏(坐标基准随模式而定) ↓ InvalidateRect() → WM_PAINT → PaintLayeredWindow() 逐像素重建 alpha 通道 UpdateLayeredWindow(ULW_ALPHA) ``` ### 关键设计决定 **固定宽度槽位。** 窗口尺寸在 `WM_CREATE` 时按「最宽模板」一次测定,此后恒定。 不是为了省事 —— 是为了**窗口尺寸永远不变**,否则通知区宽度一变,部件宽度就跳一下, 视觉上抖得很明显。 **布局尺寸是上界,不是实测。** 文本长度会变,但窗口不动。多出来的空间留空。 **两套坐标系,由一个函数转换。** 定位算法一律产出**屏幕坐标**,由 `PositionToWindow()` 决定最终传给 `SetWindowPos` / `UpdateLayeredWindow` 的是屏幕坐标还是父窗口客户区坐标。 这样切换嵌入模式只影响这一个函数。 --- ## 窗口模式:为什么有两种 | | 顶层窗口(默认) | 嵌入模式(实验) | |---|---|---| | 机制 | `HWND_TOPMOST` 覆盖在任务栏上方 | `SetParent` 成为 `Shell_TrayWnd` 子窗口 | | Z 序 | 与整个桌面的顶层窗口竞争 | 只与任务栏自己的子窗口竞争 | | 定位坐标 | 屏幕坐标 | 父窗口客户区坐标 | | Explorer 崩溃 | 存活,只是锚点句柄失效 | 被父窗口连带销毁,需重建 | | 默认 | ✅ | ❌ | 嵌入模式与 [TrafficMonitor](https://github.com/zhongyang219/TrafficMonitor) 的做法一致。 它的好处是「真正属于任务栏」,不必每秒和桌面抢 Z 序;代价是引入了恢复逻辑的复杂度。 **默认保持顶层模式**,因为嵌入模式依赖任务栏的内部结构,而这个结构不是稳定契约。 切换:`settings.ini` 里 `[Display]` 段的 `EmbedMode=1`,或设置窗口里勾「嵌入任务栏(实验)」。 嵌入式下有两个必须遵守的约束,都来自实测: 1. **必须用 `SetWindowPos(HWND_TOP)` 主动置顶。** `Windows.UI.Composition.DesktopWindowContentBridge`(Win11 的 XAML island)是任务栏的 子窗口,铺满整条任务栏,且位于子窗口 Z 序**最顶**。不主动置顶就被它整块盖住。 2. **`SetParent` 之后必须立刻清 `WS_POPUP` / 加 `WS_CHILD`。** `GetParent` 对 `WS_POPUP` 窗口返回的是 **owner** 而不是父窗口,不改样式会误判成嵌入失败。 迁移过程与踩坑见 [`docs/embed-migration.md`](docs/embed-migration.md)。 --- ## 踩坑清单 按类组织。每条的教训都是**当时无法从代码或文档看出来**的。 ### 窗口与 Z 序 **① 部件会被任务栏盖住后永久消失** 现象:跑一会儿部件不见了,不隐藏(`IsWindowVisible` 仍为 true),只是被压在下面。 原因:任务栏的 XAML 内容窗口是持续存在的顶层窗口,随时可能重新置顶。一旦盖住我们, 而我们没把自己顶回去,就永久消失。 教训:**顶层覆盖方案必须自带「被盖住就自愈」的能力**,这是采用这种架构的固有成本, 不是可以后期补的小问题。 **② 每秒无条件 `SetWindowPos(HWND_TOPMOST)` 会让下拉框点一下就收回** 现象:设置窗口里的下拉框点一下立刻收起,要连点很多次才停得住。 原因:下拉框的弹出列表是 combo 拥有的 popup 窗口,也在 topmost 带里。 Z 序被反复搅动时弹出列表丢失激活状态。 教训:**无条件置顶是有代价的**。修法是改成「位置没变且没被盖住就不动 Z 序」。 **③ `WM_DESTROY` 在父窗口被连带销毁时不送达** 现象:Explorer 崩溃后进程活着,但部件窗口**完全不存在**,日志里 `widgetAlive=1`。 原因:父窗口被销毁时子窗口被系统直接回收,我们的 `WM_DESTROY` 没进来,标志位成了过期数据。 之后所有逻辑都以为一切正常。 教训:**标志位不可信,`IsWindow()` 才是 ground truth**。不要依赖「应该会收到」的消息, 用常驻心跳主动核对。 **④ 一层窗口不够,需要哨兵窗口** 现象:部件窗口是任务栏的子窗口,Explorer 一崩它就跟着死, 而 `TaskbarCreated` 是广播消息、需要顶层窗口才能收到 —— 那一刻已无窗口可收。 教训:把「进程该不该退出」和「部件窗口还在不在」拆成两层窗口(哨兵 + 部件), 两者生命周期独立。 **⑤ Explorer 初始化时会先建一个临时任务栏再替换** 现象:收到 `TaskbarCreated` 就重建,进程活着但部件永远不出现。 原因:抢着挂到了临时任务栏上,跟着一起死,而不会再有第二次广播。 教训:**收到广播 ≠ 目标已就绪**。要轮询重试,且就绪判据必须包含「通知区有真实尺寸」 (临时窗口的 `TrayNotifyWnd` 可能宽为 0)。 **⑥ 分层窗口的命中测试按 alpha,且发生在 `WM_NCHITTEST` 之前** 现象:右键菜单收不到点击,反而弹出了「任务栏设置」。 原因:alpha == 0 的像素会把鼠标消息**无条件**透传给下层窗口,而这个判断在 `WM_NCHITTEST` 之前,所以在窗口过程里返回 `HTCLIENT` 根本拦不住。 **⑦ 背景像素的 alpha 必须给 1,不能给 0** 现象:给 0 时透明区域点不到;给 1 时透明正常。 原因:预乘 alpha 下 `结果 = src.rgb + dst*(1-a/255)`。背景 `rgb=0, a=1` 时结果 `= dst*0.996`,四舍五入后视觉上与全透明等价,但像素不再「透明」,能吃到点击。 给 0 会被判为完全透明。 ### DPI **⑧ DPI 感知必须在 manifest 里声明,运行时调用会被忽略** 现象:运行时 `SetProcessDpiAwarenessContext` 调了没用,仍是 `SYSTEM_DPI_AWARE`。 原因:窗口已经创建,感知模式就定了。 教训:**DPI 感知是进程级、创建前决定的**,只能靠 `app.manifest` + `/link /MANIFEST:EMBED`。 **⑨ 外部工具读 `GetWindowRect` 会被 DPI 虚拟化** 现象:外部脚本读到窗口 `y=2766`,屏幕上其实在 `y=1386`。差值恰好 1380 = 任务栏高度。 教训:**跨进程读窗口矩形不一定可信**。子窗口 + DPI 感知的组合下尤其危险。 我因此误判过一次「是测量误差」,实际那是 bug 本身。宁可假设程序错了, 不要先归咎测量工具。 **⑩ `UpdateLayeredWindow` 的 `pptDst` 坐标系取决于窗口类型** 现象:嵌入模式下部件完全不显示。`UpdateLayeredWindow` 返回**成功**(`err=0`), 但窗口被移到屏幕外 1380px。定时器每秒挪回来一次,下一帧绘制又推出去,形成拉锯。 原因:`pptDst` 对顶层窗口是屏幕坐标,对**子窗口是父窗口客户区坐标**。 顶层模式下两者恰好一致,所以这个 bug 一直藏着。 教训:**这个 API 返回成功不代表结果正确**。必须比对调用前后的窗口矩形。 现在 `ULW ok=… dst=… before=… after=…` 那行 trace 日志(前 4 帧)就是为此保留的。 **⑪ `GetParent` 对 `WS_POPUP` 返回 owner,不是父窗口** 现象:`SetParent` 后立刻 `GetParent` 拿到 NULL,误判「本机不支持嵌入」。 原因:`GetParent` 对 `WS_CHILD` 返回父窗口,对 `WS_POPUP` 返回 owner。 `SetParent` 不会顺手改样式。 教训:**`SetParent` 的返回值也不能单独当判据**(失败与「本来就没有父窗口」都返回 NULL)。 顺序必须是:`SetParent` → 修样式 → 此时 `GetParent` 才是父窗口。 ### 采集 **⑫ `GetIfTable2` 返回的行数远多于物理网卡数** 现象:5 张网卡,返回 61 行;流量显示成真实的二十多倍。 原因:每张网卡的每个协议绑定(IPv4/IPv6/Teredo/ISATAP)都生成一条子接口行, 且与父网卡**共享同一份字节计数器**。 教训:必须过滤 `row.InterfaceAndOperStatusFlags.FilterInterface`。 **⑬ 只判 `OperStatus` 不够** 现象:跑虚拟机时流量莫名升高。 原因:`vEthernet (Default Switch)`(Hyper-V)与 `以太网 2`(VirtualBox) 状态同样是 `Up`。还需要 `MediaConnectState == Connected` 和 `Type` 白名单。 **⑭ PDH 计数器有两处易错** - `PDH_NOCAP100` 不存在,正确是 `PDH_FMT_NOCAP100`。 - 通配符计数器的元素类型是 `PDH_FMT_COUNTERVALUE_ITEM_W`(带 `_ITEM_W` 后缀), 要读它的 `.FmtValue` 成员,直接把元素当 `PDH_FMT_COUNTERVALUE` 会拿到垃圾。 另外 `\GPU Engine(*)` 有 300~400 个实例,全量遍历每帧有成本,所以降频到每 3 拍采一次。 **⑮ 累计值必须自己做差** `InOctets` / `OutOctets` 是累计值。时间间隔用 `QueryPerformanceCounter`, 避免 1 秒定时器的累积漂移。计数器回绕或网卡重置会产生负差,要忽略该网卡。 ### 构建 **⑯ 源文件必须写在 `/link` 之前** 否则 `cl` 把它们转发给链接器,报 `fatal error D8003: missing source filename`。 **⑯b `cvtres` 的输出名不能与源码目标文件冲突** `cvtres` 输出 `settings.obj`,`cl` 编译 `settings.cpp` 也输出 `build\settings.obj`, 后者把前者覆盖掉,链接器拿到的 `.obj` 里没有对话框资源。 表现是 `DialogBoxParamW` **静默失败** —— 返回 0 且不弹窗,没有任何报错。 详见 `build.bat` 里的注释。 **⑰ `wWinMain` 不能放在匿名 namespace 里** 匿名 namespace 内的名字有内部链接,链接器找不到入口点: `LNK2019: unresolved external symbol WinMain`。`main.cpp` 几乎全部在匿名 namespace 内, 所以 `wWinMain` 是全局作用域。 **⑱ `build.bat` 必须是 CRLF** 批处理里 `^` 的续行在 LF 行尾下行为不同。 **⑲ `.rc` 需要 `#pragma code_page(65001)`** 否则中文资源按系统 ANSI 代码页(这里是 GBK)解释。对话框模板里的静态控件 ID 必须是 `-1`, 不能是 `IDC_STATIC`。 **⑳ `.gitignore` 里的 `*.manifest` 会吃掉源文件** 现象:`app.manifest` 没进版本控制,`git clone` 出来的副本编译失败 (`/link /MANIFESTINPUT:app.manifest` 找不到文件),DPI 感知随之失效。 原因:`.gitignore` 里的 `*.manifest` 本意是排除 `/MANIFEST:EMBED` 生成的中间产物 `TaskbarMetrics.exe.manifest`,但 glob 把源文件 `app.manifest` 一起忽略了。 教训:**glob 模式同时匹配源文件和生成物时必须写全名**: ```gitignore TaskbarMetrics.exe.manifest # 而不是 *.manifest ``` 这类问题很隐蔽 —— 本地一切正常,只有干净检出时才暴露。 `git ls-files` 里有没有构建必需文件,是个值得偶尔检查的事。 ### 持久化 / 对话框 **㉑ INI 的区段必须读写的两边一致** 表现:勾了「开机自动启动」,重启后又没勾。 原因:`SaveSettings` 把 `Autostart` / `Logging` 写进 `[General]`, 而 `LoadSettings` 统一从 `[Display]` 读,于是这两项永远读不回来。 **㉑ 清理废弃键要放在保存路径,不能放在读取路径** 现象:在 `LoadSettings` 里 `WritePrivateProfileStringW(..., nullptr, ...)` 删除废弃键, 保存后 ini 里那个键还在。 教训:**读路径上做完删除不会落盘**。必须跟着「保存设置」这次明确的写操作一起提交。 **㉒ 运行时改设置必须重算窗口尺寸** 表现:开启「内存用量」后文字被裁掉。 原因:设置改动后只重绘没重算尺寸,而尺寸是按最宽模板一次测定的常量。 **㉓ `SegmentW` 的格式串要带 `%`** 漏掉会导致宽度计算偏差,槽位溢出。这条和下面一条是同一个 bug 的两半。 **㉔ GB → TB 的阈值判断有 off-by-one** `FormatSize` 在字节数超过某个界时切换单位,但判断写成了 `>` 而不是 `>=`, 导致某一档宽度算出来比预期小。排查时表现为「槽位莫名不够用」。 ### PowerShell 脚本自身 这项目的一半时间花在调试脚本上。以下是纯脚本层面的坑。 **㉕ 不要用 `Set-Content` / `-replace` 编辑源文件** PowerShell 5.1 在中文 Windows 上默认按 ANSI(GBK)读写,会**破坏 UTF-8 中文注释**。 本项目因此踩坑 3 次,每次都靠 `git diff` 才发现。用编辑器工具改文件。 转换文件编码时要显式指定编码,且读的时候也要显式指定: ```powershell $t = [System.IO.File]::ReadAllText($p, [System.Text.Encoding]::UTF8) [System.IO.File]::WriteAllText($p, $t, (New-Object System.Text.UTF8Encoding($true))) ``` **㉖ `.ps1` 文件需要 UTF-8 BOM + CRLF** PS 5.1 没有 BOM 时按 ANSI 读,中文字符串会变乱码并导致语法错误。 输出乱码也会让「字符串缺少引号终止符」这种看不懂的报错出现。 最省事的做法是让 `.ps1` 源文件**只含 ASCII**,中文改用 `Write-Host` 输出。 **㉗ `EnumWindows` 在非交互 window station 上枚举不到 `Shell_TrayWnd`** `FindWindowW` 却能正常拿到。这两个 API 的行为不一致(`FindWindowW` 还会遍历 其他 window station,`EnumWindows` 不会)。 教训:**窗口查找不能只依赖一种 API**,两者都不行时在应用内部查询最可靠 —— 应用自己一直靠 `FindWindowW` 定位,从没出过问题。 **㉘ `$pid` 是 PowerShell 的只读自动变量** 用作函数参数名会报「无法覆盖变量 pid」,整个脚本挂掉。改用 `$procId`。 ### 最阴险的几条 共 30 条。其中这几条属于**「症状与原因毫无关联」**类型 —— 光看现象绝不可能猜到方向, 也是这个项目最花时间的部分: | 症状 | 真实原因 | |---|---| | 右键弹出的是「任务栏设置」而非我们的菜单 | 背景 alpha 是 0,鼠标穿透发生在 `WM_NCHITTEST` **之前**,返回 `HTCLIENT` 拦不住 | | 设置窗口不弹窗,无任何报错 | `cvtres` 产物被 `cl` 的 `.obj` 覆盖,对话框资源没链接进去 | | `dpiAwareness` 是 1 而不是 2 | `app.manifest` 被 `.gitignore` 的 `*.manifest` 一起忽略了 | | 外部脚本读到的窗口位置差 1380px | 外部 `GetWindowRect` 被 DPI 虚拟化,程序自报的才对 | | 部件永久消失但不是隐藏 | 是被压在下面。`IsWindowVisible` 仍为 true | | 部件完全不显示,但所有日志都正常 | `UpdateLayeredWindow` 返回**成功**却把窗口移到了屏幕外 | | 进程活着但部件窗口完全不存在 | 父窗口连带销毁时 `WM_DESTROY` 不送达,标志位成了过期数据 | --- ## 调试 ### 诊断日志 设置里勾「启用诊断日志」,日志写在 `TaskbarMetrics.log`(UTF-16)。 日志里最值得盯的几行(原文照抄,方便 grep): | 日志 | 含义 | |---|---| | `START dpiAwareness=2 taskbar=... notifyLeft=...` | 启动状态。`dpiAwareness` 必须是 2 | | `ATTACH OK parent=0x... style=[CHILD]` | 嵌入成功。`FAILED` 表示退回顶层模式 | | `ATTACH-RECT screen=(...)` | 嵌入后实际落点。只在启动时记一次 | | `POS dpi=120 mode=embedded anchor=2087 (src=TrayNotifyWnd) gap=5 -> screen(1764,1386) win(1764,6) 318x48` | 定位。`mode` 两种取值 | | `ULW ok=1 dst=(...) before=... after=...` | 分层窗口上屏,只记前 4 帧 | | `HEALTH: 部件窗口已不存在,进入恢复` | 自愈触发 | | `RECOVERY OK: 部件已重新挂到任务栏 ...` | 自愈完成 | | `RAISE: 被其他顶层窗口盖住,已重新置顶` | 顶层模式下的置顶 | | `RECOVERY GIVE UP: 重试 30 次仍未成功` | 自愈放弃,程序会退出 | `POS` 行同时记屏幕坐标和窗口坐标 —— 嵌入式下两者差 `(0, -1380)`(任务栏客户区原点不在屏幕原点)。 **这一行是排查定位问题的主要依据**,`win` 值和 `screen` 值对不上就说明坐标基准错了。 其他可 grep 的前缀:`WIDGET destroyed`(部件窗口销毁)、`SENTINEL destroyed`(哨兵销毁)、 `DETACH`(退回顶层)、`PROBE`(`--probe-embed` 的输出)、`ERROR` / `FATAL` / `WARN`。 ### 验收脚本 ```powershell powershell -ExecutionPolicy Bypass -File tools\verify.ps1 ``` 启动一个实例,检查 9 项:DPI 感知、内存占用、窗口尺寸合理性、垂直居中、 水平在任务栏内、活动网卡数、PDH 计数器、尺寸稳定性,并和应用日志对账。 嵌入式下脚本会自动改用 `EnumChildWindows` 找窗口(`EnumWindows` 只枚举顶层窗口), 并改用应用自报的矩形(外部读会被 DPI 虚拟化)。 ### 任务栏结构探针 ```powershell powershell -ExecutionPolicy Bypass -File tools\probe-taskbar.ps1 ``` 枚举任务栏结构、通知区、按钮、各显示器 DPI。本 README 里的「本机实测参数」就是它测的。 ### 嵌入可行性探针 ```powershell TaskbarMetrics.exe --probe-embed ``` 在**进程内部**查询任务栏子窗口树、测试 `SetParent`、做 3×3 命中测试, 结果写进日志。必须在应用内跑 —— 外部脚本枚举不到 `Shell_TrayWnd`(见坑 ㉗)。 --- ## 已知限制 前四条是**当前配置下未验证**的用法,不是已知 bug —— 发给别人之前请留意。 - **任务栏在左侧/右侧时定位会失败。** 定位公式 `y = taskbar.top + (tbH - height)/2` 假设任务栏在顶部或底部。竖直任务栏下它会把 部件放到屏幕垂直中点,而不是贴着任务栏。任务栏朝向判断没实现 (TrafficMonitor 有 `CheckTaskbarOnTopOrBottom()`)。 - **Win11 任务栏右对齐时定位错误。** 会盖住系统托盘。 - **界面文字是硬编码中文。** 给非中文用户会显示乱码。 - **配置写在 exe 同目录。** 放在需要管理员权限的目录(如 `Program Files`)会写不进去。 - **副屏未验证。** 代码里保留了 `Shell_SecondaryTrayWnd` 分支,但本机无副屏。 - **DPI 缩放切换未测试。** `WM_DPICHANGED` 处理写了但从未执行过。 嵌入模式下坐标基准从屏幕变成父客户区,对 DPI 变化更敏感。 - **嵌入模式依赖任务栏内部结构。** `DesktopWindowContentBridge` 的类名和 Z 序位置 不是公开契约,Windows 更新后可能变化。`--probe-embed` 就是为这个准备的。 ### 排查线索 | 现象 | 先查 | |---|---| | 开机后没看到部件 | 注册表 `HKCU\...\Run` 里 `TaskbarMetrics` 的路径是否还指向当前 exe。exe 移动过就会失效,且不报错 | | 勾了自启但没生效 | 同上。勾选框只反映 ini 的值,不反映注册表是否写成功 | | 尺寸全部偏小/偏大 | `START` 行里的 `dpiAwareness` 必须是 2。为 1 说明 manifest 没生效 —— 见坑 ⑧ | | 改装系统后编译失败 | 确认 `app.manifest` 在版本控制里(`git ls-files`)。见坑 ㉚ | | 部件卡在屏幕左上角或消失 | 看 `POS` 行的 `win` 与 `screen` 是否差 `(0, -1380)`。差值不对说明坐标基准错了 | | 幽灵进程,任务栏上没东西 | 看日志有没有 `RECOVERY GIVE UP`。自愈失败会干净退出,不会留僵尸 | - **网卡为全部网卡合计。** 不支持单独选择某张网卡。 - **无温度监控。** 需要引入 LibreHardwareMonitor 这个额外依赖,与「保持零依赖」的目标冲突。