# TaskbarTint **Repository Path**: bigant/TaskbarTint ## Basic Information - **Project Name**: TaskbarTint - **Description**: 通过检测打开应用的颜色,来改变任务栏的颜色,让任务栏和应用能融为一体. - **Primary Language**: C++ - **License**: AGPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-11 - **Last Updated**: 2026-09-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: taskbar, Windows11 ## README # TaskbarTint 让 Windows 11 的任务栏跟随最上层应用的标题栏颜色。 常驻系统托盘,右键菜单可调。默认只在应用**全屏或最大化**时才跟随,切走即恢复。 | | | |---|---| | 版本 | 1.2.0.1 | | 作者 | Kevin | | 仓库 | https://gitee.com/bigant/TaskbarTint | | 许可 | GNU Affero General Public License v3.0(见 [LICENSE](LICENSE)) | ``` Copyright (C) 2026 Kevin This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. ``` 改版本号时要同步两处:`src/common.h` 里的宏,和 `res/resource.rc` 里的 `VERSIONINFO`(exe 属性页读的是后者)。 --- ## 原理 Windows 没有公开 API 可以设置任务栏颜色(任务栏由 explorer 用 XAML/Composition 自绘)。 本程序走的是未文档化接口 `SetWindowCompositionAttribute`,给任务栏窗口 (`Shell_TrayWnd`)设置合成属性: ```cpp ACCENT_POLICY pol = {}; pol.AccentState = ACCENT_ENABLE_GRADIENT; // 1 pol.AccentFlags = 2; pol.GradientColor = 0xFF000000 | (B << 16) | (G << 8) | R; // ABGR ``` 好处是**全程不碰注册表、不需要管理员权限、不改变系统主题色**,退出时把任务栏 画回原来的颜色即可。 ### 为什么是 `ACCENT_ENABLE_GRADIENT` 在 Windows 11 build 26200 上实测了四种合成状态,把任务栏刷成已知颜色后再抓屏 对比实际显示值: | 状态 | 7 个测试色的最大偏差 | 结论 | |---|---|---| | `ACCENT_ENABLE_GRADIENT` | **≤ 1/255** | ✅ 采用,肉眼不可辨,无需补偿 | | `ACCENT_ENABLE_ACRYLICBLURBEHIND` | 最大 ±76,且随壁纸内容变化 | ❌ 不可补偿 | | `ACCENT_ENABLE_TRANSPARENTGRADIENT` | 偏差大 | ❌ | | `ACCENT_ENABLE_BLURBEHIND` | 偏差大 | ❌ | 另外实测 `ACCENT_ENABLE_GRADIENT` **忽略 alpha 通道**(`0xFF` 和 `0xC0` 结果完全一致), 所以"混合强度"是在颜色本身上做插值,不是靠透明度实现的。 --- ## 使用 运行 `TaskbarTint.exe`,托盘出现图标(Win11 默认会把它收进"隐藏的图标"溢出区, 可以拖出来固定)。**右键**图标打开菜单: | 菜单项 | 说明 | |---|---| | 启用跟随 | 总开关。取消勾选会**立即**把任务栏画回本色 | | 跟随模式 | `仅全屏/最大化时跟随`(默认)/`始终跟随前台窗口` | | 采集部位 | `标题栏`(窗口顶部,默认)/`状态栏`(窗口底部)。任务栏在屏幕顶部时采标题栏最贴合视觉,在底部时采状态栏更贴合 | | 混合强度 | 30% / 50% / 70% / 100%。100% = 完全使用标题栏原色;50% = 和任务栏本色取中间值 | | 兜底轮询间隔 | 除了前台窗口切换事件之外的定时检查频率 | | 平滑过渡 | 变色时约 170ms 渐变,而不是硬切 | | 开机自启 | 写 `HKCU\...\CurrentVersion\Run` | | 关于 | 版本、作者、许可证、仓库地址 | | 退出 | 退出前会自动把任务栏还原 | 配置存在 `%APPDATA%\TaskbarTint\config.ini`。 --- ## 已知限制 - **只处理主显示器的任务栏**,副屏不动。 - **任务栏会变成不透明色块**,失去 Win11 Mica 材质的轻微透明感。因为 `ACCENT_ENABLE_GRADIENT` 是不透明填充。静态壁纸下与原来肉眼无差别。 - **采样盲区**(这些情况下会保持上一次的颜色不动,不会误刷): - DRM 保护内容(Netflix 等)在屏幕上会读成纯黑 - 独占全屏的 D3D 游戏(此时任务栏本来也看不见) - 标题栏被其它窗口遮挡时(见下) - 有些应用的标题栏颜色**恰好等于任务栏本色**(比如深色主题的 VS Code 和深色任务栏 都是 `#191A1B`),这时看不出变化——这是正确行为,不是故障。 - 托盘图标在 Win11 上默认藏在溢出区里。 - **任务栏一旦被染过色,就一直是我们刷的平面色**,直到资源管理器重启或重启电脑。 因为没有任何 API 能把任务栏交还给系统的 Mica 渲染(原因见下)。由于这个平面色就是 采样自任务栏本色的,肉眼看不出区别,只是失去了 Mica 那点轻微透明感。 ### 换了壁纸之后想重采任务栏本色 Win11 的任务栏用 Mica,本色会跟着壁纸变。本程序只在**首次启动**记录一次本色并存入 `config.ini`,之后不再重采——因为任务栏一旦被染过色就无法还原成自然状态,重采只会 把当前刷上去的颜色又采一遍。 确实需要重采时,手动走一遍: 1. 退出 TaskbarTint 2. **让任务栏回到自然外观**(否则采到的是残留色):在「设置 → 个性化 → 颜色」里 随便切换一项,或者重启资源管理器 3. 打开 `%APPDATA%\TaskbarTint\config.ini`,删掉 `baselineColor` 那一行 4. 重新启动 TaskbarTint,它会重新采样并写回配置 (实测:只做第 2 步的"切换颜色设置"就够了,不必真重启 explorer。) --- ## 实现上踩过的坑 这些都是实测出来的,不是推测,记下来免得以后重新踩: 1. **颜色量化误差**。抓屏后为了做直方图把颜色量化到每通道 5 位,如果直接返回档位中心, 会引入最大 ±4 的误差(`#191A1B` 被报成 `#1C1C1C`,`#F7F7F7` 报成 `#F4F4F4`)。 正确做法是先用量化值找出主色调档位,**再对该档位内的像素求精确平均**。 2. **`ACCENT_DISABLED` 会让任务栏永久变黑,绝对不能用来"还原"**。 实测(`tools/disabletest.cpp`):先把任务栏刷成纯红确认染色通路正常,再施加 `ACCENT_DISABLED`,之后每 300ms 采样一次连续观察 **9 秒,全程都是 `#000000`, 没有任何自行恢复的迹象**。它不会把任务栏交还给系统。 所以"还原"只能改用 **基准色 + `ACCENT_ENABLE_GRADIENT`** 画回去。 这个坑也直接导致"重新采样任务栏本色"这个功能原理上无法实现,已从菜单移除。 3. **首次采样本色时,任务栏必须处于自然状态 —— 这是最隐蔽的一个坑**。 任务栏一旦被任何工具染过色,就没有 API 能让它回到系统的自然外观,此时采到的 只是**残留色**,而不是本色。开发过程中就真的踩到了:某次采到的 `#191A1B` 其实是上一轮测试留下的残留,一直被当成"本色"用,直到用户发现任务栏本该是 强调色的淡蓝 `#DDE4F1` 才暴露出来。 让任务栏回到自然外观的办法(会清掉残留的合成属性): - 重启资源管理器,或 - 在「设置 → 个性化 → 颜色」里切换任意一项(explorer 会重绘任务栏) 所以基准色除了要持久化(见下条),首次采样前也应该确认任务栏是干净的。 4. **必须做遮挡检测**。采样假设"前台窗口标题栏那块区域显示的就是它的标题栏", 但任何盖在上面的东西(通知气泡、控制台窗口、输入法候选框)都会让采样结果跑偏。 用 `GetAncestor(WindowFromPoint(...), GA_ROOT)` 校验采样点上的顶层窗口 是否就是前台窗口本身。 5. **`DwmGetWindowAttribute(DWMWA_EXTENDED_FRAME_BOUNDS)` 而不是 `GetWindowRect`**。 后者包含一圈不可见的拖拽边框,会让采样位置整体偏移。 6. **必须设置 DPI 感知**(`PerMonitorV2`)。否则在缩放显示器上窗口坐标被虚拟化, 抓屏会抓错位置。 7. **不要碰 `HKCU\Software\Microsoft\Windows\DWM\ColorPrevalence`**。那是"在标题栏和 窗口边框上显示强调色",改它会让标题栏自己变色,和采样形成反馈环。 8. **`.bat` 文件里不要写中文**。cmd.exe 在中文 Windows 上按 GBK 解析 .bat, UTF-8 的中文会被拆成乱码字节并被当成命令执行。构建脚本保持纯 ASCII。 9. **菜单忘了处理 `WM_COMMAND`**。`TrackPopupMenu` 选中的项是以 `WM_COMMAND` 投递到窗口的。`WndProc` 里少了这一条,菜单能弹出、点了却毫无反应——而且不会有 任何报错,很容易漏。 10. **进程被强杀时来不及还原**。程序正常退出会画回本色,但被任务管理器结束任务、 崩溃、断电时不会,任务栏就停在被染的颜色上。用配置里的 `dirty` 标记解决: 进入染色状态时置 1,还原时置 0,启动时若发现是 1 就先恢复本色。 11. **explorer 会悄悄把任务栏的合成属性重置掉,重置后任务栏渲染成纯黑**。 触发场景包括与任务栏交互(右键、点图标、点开始菜单)以及各种重绘。 光靠"发现状态变了才改"是不够的——一旦程序进入"已还原"状态, `RestoreIfNeeded()` 里的 `if (!g_applied) return;` 会让它**再也不碰任务栏**, 黑色就一直留着。 解决办法是**每个轮询周期无条件把"当前应该是什么状态"重贴一遍** (染色时贴当前色,未染色时贴回本色),默认 1 秒一次。 用 `tools/blank.cpp`(施加一次 `ACCENT_DISABLED` 模拟这个重置)验证过: 任务栏被打成 `#000000` 后,程序在 0.5 秒内纠正回本色。 12. **前台是外壳窗口时,要回退去问"上次跟随的那个窗口",而不是干等着或还原**。 这条踩了三轮才走对,值得完整记下来: - 点任务栏、开开始菜单、拉出跳转列表 / 右键菜单时,前台会变成 `Shell_TrayWnd`、`SIBJumpView`、`#32768` 这类窗口。如果按"不是应用窗口 就还原"处理,手一碰任务栏颜色就跳回本色——但底下那个应用一点没变。 - 第一版做法是"保持现状 + 安全阀":只有上次跟随的窗口仍满足当前模式的条件 才保持。这解决了点任务栏的场景,但**没解决"改设置"的场景**—— 菜单命令是在菜单还开着的时候处理的,那一刻前台是本程序自己的隐藏窗口 (`TrackPopupMenu` 要求先 `SetForegroundWindow`),于是重新勾选"启用跟随" 什么都不会发生,要等你点了别的应用才跟上。 - 第二版想靠"菜单关掉后把前台还回去"来救,**不可靠**:归还的时机、前台是否 真的回到原窗口,都不受我们控制。 - **最终做法**:前台是外壳 / 临时浮层 / 本程序自己的窗口时,一律 **回退到"上次跟随的那个窗口"重新采样**(`SampleSpecificWindow`)。 用户只是在动任务栏或改设置,直接问那个应用就行。 这个回退顺带替代了安全阀:窗口被关掉 / 最小化 / 不再满足当前模式时, 回退采样自然会失败或判定为"不跟随",照常还原。 另外两个配套判断:菜单/浮层这类窗口普遍带 `WS_EX_TOOLWINDOW` (意为"不进任务栏、不进 Alt+Tab"),非全屏的这类窗口一律按临时浮层处理; 本程序自己的隐藏窗口则靠 `IsWindowVisible` 识别。 13. **应用把内部 Tab 条紧贴标题栏时,不能整条取众数**。 WPS(类名 `XLMAIN`)就是这样:标题栏只有顶部 **6px**,往下 38px 全是 Tab 条, 两者颜色不同(实测标题栏 `#E5E8EE`、Tab 条 `#D9DBE1`)。按固定高度 `min(窗口高/4, 40)` 采样,Tab 条面积大得多,取到的就成了 Tab 的颜色。 想靠 `DWMWA_CAPTION_BUTTON_BOUNDS` 推算标题栏高度也不行 —— 自绘标题栏的 应用(WPS、Chrome 这类)返回空矩形,DWM 根本不知道按钮在哪。 可用的规律是:**标题栏是"满宽成片的纯色"(行内众数占比接近 100%), Tab 条 / 工具栏因为里面是形状和文字,占比明显更低**(实测 41~69%); 而且标题栏一定最靠窗口边缘。 于是改成逐行求众数,从靠边的一侧连续接受"占比 ≥ 90% 且颜色一致"的行, 这段就是标题栏;遇到第一行不成片就停。一行都没接受就退回整条取众数 (比如标题栏本身是渐变的情况)。 验证(来回切换、每次都确认前台确实切换成功): WPS 得到 `#E5E8EE`(标题栏,成片 4 行),VS Code 得到 `#191A1B`(成片 5 行), 资源管理器得到 `#191A1B` —— 普通窗口的结果与改前一致,没有回归。 排查时用 `tools/topbar.cpp` 打印窗口顶部逐行颜色剖面,一眼就能看出 标题栏和 Tab 的分界在哪。 14. **采样区必须夹进显示器工作区**。 最大化窗口的 DWM 扩展边界会伸到任务栏底下 —— 实测资源管理器的 `frame.top = 29`,而任务栏占到 38,于是采样区最上面 9 行采到的是**任务栏本身**。 而任务栏的颜色正是本程序刷上去的,等于把自己的输出当成输入,形成**自我循环**。 旧算法取整条 40 行、任务栏只占少数,歪打正着取对了;改成"从边缘找成片纯色" 之后会优先咬住这几行任务栏,问题才暴露出来(表现为资源管理器、i讯飞取色错误)。 修法:把采样区夹进 `MONITORINFO.rcWork`。工作区天生不含任务栏,而且它是系统 按**当前任务栏的实际高度和位置**算出来的,所以任务栏调高调矮、挪到哪一边都能 自动适配,不用写死数值。真正的全屏窗口(占满显示器、任务栏已被盖住)不夹, 否则会把内容顶部白切掉一截。 ### 采集部位为什么可选 任务栏在屏幕顶部时,跟它视觉相邻的是窗口的**标题栏**;任务栏在底部时,相邻的是 窗口的**状态栏**。所以"采哪一条"取决于任务栏在屏幕的哪一边。 判定用测试窗 `tools/testwin.cpp` 做过验证:把 DWM 标题栏强制设成纯红、客户区底部 涂纯绿,两种模式分别得到 `#FE0000` 和 `#00FF00`,互不串扰。 --- ## 构建 需要 MinGW-w64 g++(含 windres)。无需 CMake,不依赖任何第三方库。 ``` build.bat ``` 脚本顶部的 `GXX` / `WINDRES` 是默认查找路径;**如果那里不存在,会自动退回 PATH 上的 `g++` / `windres`**,所以装了 MinGW-w64 并加进 PATH 的话无需改这个文件。 产物是单文件绿色的 `TaskbarTint.exe`(静态链接,不依赖 libstdc++/libgcc DLL)。 ### 打包安装程序 需要 [Inno Setup](https://jrsoftware.org/isdl.php) 6 或 7。 ``` build-installer.bat ``` 脚本会在几个常见位置找 `ISCC.exe`(找不到时退回 PATH),`TaskbarTint.exe` 不存在的话会先调 `build.bat`。产物是 `dist\TaskbarTint-<版本>-setup.exe`。 安装包的设计取舍: - **按用户安装,不弹 UAC**(`PrivilegesRequired=lowest`),默认装到 `%LOCALAPPDATA%\Programs\TaskbarTint`。程序本身只写 HKCU,要管理员权限没有道理。 - 桌面图标和开机自启都是可选项(自启等价于程序菜单里的那一项,都写 `HKCU\...\Run`)。 - 安装和卸载前都会先结束正在运行的实例,否则 exe 被占用会导致复制失败; 用完的是强杀,没走程序的正常退出流程,但 `dirty` 标记会在下次启动时把任务栏 恢复成本色(见上文)。 - 向导有中文(用 Inno 自带的 `ChineseSimplified.isl`),系统非中文时自动切英文。 - **第一页是自定义的欢迎页**(`[Code]` 里用 `CreateOutputMsgPage` 生成), 说明这个程序做什么、为什么开源、遵循什么协议。Inno 的 `DisableWelcomePage` 默认就是 `yes`,内置那个空白欢迎页是隐藏的,所以这页就是向导首页。 - 仓库里放了 `LICENSE` 就自动显示许可页,没放就跳过(不会编译失败)。 `installer.iss` 里含中文,**必须以 UTF-8 带 BOM 保存** —— Inno 对没有 BOM 的 脚本按 ANSI 解析,中文会全成乱码。用编辑器改完记得确认编码。 --- ## 文件结构 ``` src\main.cpp 托盘图标、右键菜单、前台窗口钩子、平滑过渡 src\sampler.h/.cpp 前台窗口检测 → 全屏判定 → 抓屏 → 直方图取色 src\tinter.h/.cpp 任务栏染色后端(合成属性 API + 基准色管理) src\config.h/.cpp 配置读写、开机自启 src\about.h/.cpp "关于"窗口(自绘,含 GDI+ 解码的 PNG 头图) src\common.h/.cpp 日志、字符串、颜色插值、DPI 感知、版本与署名宏 res\app.ico 程序图标(多尺寸) res\about.png "关于"窗口头图,由 app.png 压到 128x128 生成 res\resource.rc 图标、头图资源、版本信息 spike\ Phase 1 的技术验证程序(热键驱动,已从版本控制排除) tools\probe.cpp 抓屏诊断工具(排查取色问题时用) tools\disabletest.cpp 验证 ACCENT_DISABLED 行为的实验程序 tools\testwin.cpp 采集部位判定用的红/绿测试窗 tools\blank.cpp 把任务栏打成纯黑,用来验证上面的自愈逻辑 tools\topbar.cpp 打印窗口顶部逐行颜色剖面,排查标题栏/Tab 分界用 (用法: topbar.exe <标题子串>) ``` ### 调试 程序会把运行日志写到 **exe 同目录**的 `taskbar-tint.log`,超过 1MB 自动清空。 启动时会输出一段抓屏环境诊断(分辨率、DPI 感知级别、任务栏矩形、各区域取样结果), 排查取色异常时先看这个。 `tools\probe.exe` 是一个独立的抓屏探针,用来对比"外部视角"和"程序内部视角" 的取色是否一致: ``` probe.exe 2 taskbar # dpiMode=2(PerMonitorV2), 抓任务栏区域 probe.exe 0 desktop # dpiMode=0(不设置), 抓桌面区域 ```