Repository navigation
Releases: yakoye/WindowMark
Release list
WindowMark v0.6.0
这一版做了三件事:给程序一个能看懂的入口,让边框自己从坏状态里爬出来,再把几个真问题修掉。
左键单击托盘图标 = 主面板
右键菜单对会用的人够快,对第一次见它的人不行:四个功能叫什么、各自干什么、怎么用、现在开着没开,全藏在三层子菜单里,而菜单装不下任何一句说明。
现在左键单击托盘图标打开一个面板(菜单最上面也多了「面板...」):
- 四个功能各一块:名字 + 一个勾(当场开关,面板不关)+ 一句「这是什么」+ 一句「怎么用」+「设置...」「排除应用...」;
- 「置顶刚才那个窗口「<标题>」」——按钮上直接写着是哪个窗口,已经钉住时变成「取消置顶」,旁边是准星;
- 「开机时自动启动」勾,以及 配置文件 / 创建桌面快捷方式 / 剪贴板守护 / 诊断报告 / 安装到系统(或卸载)/ 关于 / 退出。
边框:三个真问题
点一下任务栏,所有窗口的边框都变成「非活动」色。 活动窗口跟着系统的前台事件走,而任务栏、托盘、开始菜单、以及 WindowMark 自己的面板和设置窗口拿到前台时照样会报这条事件——它们都不在跟踪列表里,于是没有任何一个窗口匹配得上「活动」。现在「活动」指的是用户最后在用的那个窗口:前台落到跟踪不到的窗口上时不认。
很多窗口干脆没有边框。 边框画布是一张盖满整屏的分层窗口,挂在置顶层末尾。它被挤到普通窗口下面去了(远程桌面会话里见过)——位还在、位置没了,画出来的每一笔都被上面的窗口盖住。现在每 500ms 自查一次,沉下去就自己提回置顶层;看门狗那一问也查一遍(桌面静着的时候不会有重画)。
打字时输入法候选框一出来,那一块的边框就没了。 置顶窗口被当成了「矩形遮挡物」。可画布就挂在置顶层末尾,别人家的置顶窗口统统在它上面,系统合成本来就会按那个窗口实际画出来的像素盖住边框;再拿矩形裁一遍,等于把它没画的地方也抹掉。输入法候选框正是矩形不小、真正画出来只有一条的那种窗口。
看门狗:卡住了自己恢复
有一类故障从外面看都一样:窗口变了,程序毫无反应,重启才好。成因不止一个——Windows 会把回调太慢的钩子摘掉(摘掉不通知任何人),explorer 重启或会话切换之后事件流可能断掉,边框也可能因为会话锁定挂起而再没收到解除挂起的那条事件。
现在每 5 秒自己对一次账:系统上真实存在的窗口和它记着的那份比一遍,连续两次对不上就重新订阅事件并整份重新播种;边框如果还挂着而桌面已经回来了,就自己解除挂起重画。这类情况通常不用再重启——最多 10 秒自己好。
一个 exe 就是完整的绿色版
发布页上除了 zip,还有一个 WindowMark-v0.6.0-portable.exe:单独一个文件,不用解压、不用配套文件,双击就用,设置写在它旁边,删掉它就干净。
双击一个和正在运行的那份不是同一个文件的 exe 时会问一句「要换成这一份吗」——没有这一下,拿到新版双击只会弹「已在运行」然后退出。同一个文件再双击不问,开机自启那次也不问。
诊断
- 四部分(边框、书签、置顶、拖动)现在认同一个开关:配置文件旁边建一个空文件
diag.on,日志写进同一个diag.log。以前书签那套只认环境变量,等于只在开发机上存在。 - 位置跟着配置文件走:绿色版就在 exe 旁边,整个文件夹仍然自带一切。
- 顺手修了一件事:z 序诊断自己太贵(每次都把窗口链从头走到尾再写一行),6.7ms 一次,报出来的数字是诊断自己的。改成按种类计数后,书签 Apply 从 6200–7000µs 降到 164–257µs。
其他
- 设置页「颜色」下面,非活动窗口那一排色卡换成了同色相的减弱版——以前两排一模一样,挑同一格得到的是一模一样的边框,而这个功能的全部意义就是一眼看出哪个窗口是活动的。
- 拖动窗口时书签条跟手(走和边框同一条不节流的几何快路径)。
- 「书签」统一叫「窗口书签」。
- 关于框最下面多了一行「觉得不错?赞赏一下作者~」,点了才弹收款码,不点就当没有。
已有的 settings.conf 不会被改写,升级安装照常保留设置。
WindowMark v0.5.5
这一版只做一件事:把「怎么装、怎么开」这件事收拾干净。
以前下载解压完,根目录里平铺着六个 exe——主程序、安装程序、卸载程序、两个诊断工具、剪贴板守护——第一眼根本不知道该双击哪个。
现在包里顶层只有一个 exe
WindowMark.exe ← 双击这个
portable.on
README.md CHANGELOG.md LICENSE
tools\ ← 诊断工具、剪贴板守护、测量脚本,程序自己去调
双击 WindowMark.exe 就能用,不需要安装。设置写在它旁边的 settings.conf(包里的 portable.on 就是这个意思):整个文件夹拷到 U 盘、换台电脑、挪到别的盘,设置都跟着走;不想要了直接删掉文件夹就是卸干净,系统里不留东西。
删掉 portable.on 再启动,就改回系统默认位置 %LOCALAPPDATA%\WindowMark\settings.conf;托盘的「配置文件...」里也能随时切换,并且会把现有配置搬过去。
安装和卸载是同一个 exe 的两个模式
没有安装程序 exe 了:
- 托盘右键 「安装到系统...」 → 一个确认框(安装位置 + 开机自启勾选)→ 装到
%LOCALAPPDATA%\Programs\WindowMark、建开始菜单快捷方式、注册「设置 - 应用」里的卸载入口 → 启动安装的那份,当前这份退出,解压出来的文件夹就可以删了。 - 安装目录里那份的托盘菜单会把这一项换成 「卸载 WindowMark...」(两项同时只出现一个)。「设置 - 应用」里的卸载入口也在。
- 脚本用:
WindowMark.exe --install /S和WindowMark.exe --uninstall /S。
升级时会删掉安装目录里旧版留下的那几个顶层 exe,不然装完目录里还是一堆。
第一次运行会问一次
第一次启动(配置文件还不存在)弹一个框:图标在托盘里,要不要顺手设好开机自动启动并在桌面放一个图标。答「是」两件一起办好,答「否」什么都不动,以后在托盘右键里随时改;答过就再也不问,开机自启那次不问。
安装程序本来会替用户做这两件事,绿色版没人做,只放在托盘右键里等于默认没有。托盘里也新增了 「创建桌面快捷方式」,随时能补建。
拖动窗口时书签条跟手
书签条明显落在窗口后面。每个窗口位置事件本来就有一条不节流的快路径,但它只给边框用;书签条要等 33ms 的节流、还要重建整组模型才挪。v0.5.4 起它贴在窗口内侧,慢一拍就是直接错开一截,所以它现在也走那条路:纯平移时只重新摆一次窗口位置,代价和边框一样是一次 SetWindowPos。单元测试钉住「快路径和整份重算逐像素一致」。
「书签」改名为「窗口书签」
托盘子菜单、启用项、气泡提示、关于框、设置窗口标题和设置页分组都跟着改,和「窗口边框」「窗口置顶」「窗口拖动」一致。配置文件里的键名不变。
已有的 settings.conf 不会被改写,升级安装照常保留设置。
WindowMark v0.5.4
书签的悬停重做了,外加两个「看起来像坏了」的问题。
磁性书签栏
以前:鼠标命中哪个书签,哪个就展开成一长条、塞进整个名字,其余收起;滑过两个书签之间的间隙,整条栏先塌下去再弹起来。
现在鼠标是一块磁铁,书签是一排铁块:
- 沿书签栏方向,离鼠标越近的书签越大,正对中心时长到固定峰值(绝对像素,激活的书签和别的书签一样大);半径(默认 120px)以外完全不动,中间平滑衰减。
- 放大的书签把邻居连续挤开,鼠标下的那一点始终留在鼠标下,书签不会跑开让鼠标去追。
- 间隙不再是死区,停在两个书签正中间时两边一样大。只有离开整条栏才算离开,还有 60ms 宽限,回来得及时就当没离开过。
书签 → 浮动标题 → 缩略图
- 书签里只放短名,完整名字在独立的浮动标题里,缩略图在标题外侧。从窗口边缘往窗口内容依次排开,任何时刻互不重叠——邻居书签长高时,标题和缩略图被连续推开,间距不变。
- 左右两侧的标题是真正的竖排(中文直立、英文侧转),两侧都从上往下读。超长标题末尾省略号。
- 换一个书签时,标题和缩略图从旧位置滑到新位置,内容交叉淡入淡出,不出现空白。
- 指向当前窗口自己的书签没有缩略图可放,那个位置显示大字「当前窗口」。
- 缩略图第一次出现仍等
preview.delay_ms,之后跟着书签换,不再等;空间不够时按比例缩小,太小就不显示,绝不压到标题上。
左右两侧的书签条也改到了窗口内侧,四个方向是同一套几何、互为镜像。
书签右键菜单的「重命名」「设置」点了没反应
菜单弹得出来,点上面的项什么都不发生。菜单的主人必须是前台窗口,菜单项才点得动;书签条不能成为前台,以前的做法是把前台交给宿主窗口,可宿主是别的程序。现在和托盘菜单同一套做法:由 WindowMark 里一个看不见、能激活的窗口来弹菜单。
托盘开关打着勾,功能却没开
- 托盘里「书签」的对勾初值写死为「开」,启动时从不读配置:配置里关着的书签,托盘照样打勾。现在托盘每次弹出都从当前配置现读四个功能的对勾。
- 设置页里的「启用书签 / 启用边框 / 启用置顶」去掉了,开关只在托盘菜单里,不会再有两份状态对不上。
- 「暂停所有 / 启用所有」显示的文字和点下去真正切换的曾经不是同一组功能(只开着置顶时点「暂停所有」反而把其余三个打开)。现在两边一致。
设置
新增:drawer.magnet_radius(磁场半径)、drawer.magnet_grace_ms(离开宽限)、横排和侧边各自的峰值尺寸,以及 preview.title_gap、preview.thumbnail_gap、preview.crossfade_ms,设置界面「书签」页都有。
退役:drawer.expanded_extent、drawer.bottom_expanded_extent、drawer.attach_overlap——配置里照常读写、不丢值,只是不再生效。preview.enabled 现在只管缩略图,浮动标题总是显示。
已有的 settings.conf 不会被改写,新增的项取默认值。
验证
- 磁场和三段式排布是核心库里的纯函数:单元测试覆盖峰值、按 0.25px 扫过整条栏的连续性、间隙无死区、不动点、主标签滞回、四方向镜像与互不重叠;变异测试 33 个全部被抓住。
- 四个方向、五种场景离屏渲染成图片逐一核对过:竖排方向、省略号、空间不足时缩略图缩小。
- 「像手指滑过钢琴键」的手感只能靠实际用,有不顺手的地方请反馈。
安装:下载 WindowMark-v0.5.4-win64.zip,解压后运行 WindowMarkSetup.exe。不想安装就直接运行解压目录里的 WindowMark.exe。升级会保留现有配置。
完整变更见 CHANGELOG.md。
WindowMark v0.5.3
三件事。
不再需要安装 VC++ 运行库
在全新安装的 Windows 10 上,WindowMarkSetup.exe 和 WindowMark.exe 双击直接报「找不到 VCRUNTIME140.dll / MSVCP140.dll / VCRUNTIME140_1.dll」。
原因是默认动态链接 C/C++ 运行库,这三个 DLL 得由系统提供,全新系统、LTSC、IoT 版上常常没有。用 dumpbin 核对过:0.4.1 到 0.5.2 每一个发布版都有这三项依赖——缺运行库的机器上哪个版本都起不来。
现在六个 exe 全部静态链接,dumpbin 确认已经没有 MSVCP140、VCRUNTIME140*,连 UCRT 的 api-ms-win-crt-* 也没有了。代价是体积:WindowMark.exe 537 KB → 761 KB。
0.5.2 及以前的版本仍然需要运行库,安装方法见仓库里的 WindowMark-依赖安装.md。
书签不再盖住右键菜单
资源管理器里右键,弹出的菜单被书签条压住了一块。
书签条是置顶窗口,而置顶窗口之间谁在上面,看谁最后一次被显示:右键时焦点晃一下,书签条隐藏再显示,就跳回了置顶层的最顶上。现在宿主窗口在前台时,书签条挪到置顶层的末尾——比所有普通窗口高,比右键菜单、输入法候选框、任务栏这些别的程序的置顶窗口低。稳态下不发任何 SetWindowPos。
装了 WGestures 这类软件后,所有边框都消失
用户报告,二分到 v0.4.8 正常、v0.4.9 起不正常。
WGestures 常驻一个铺满全屏、置顶、完全透明的窗口画手势轨迹——眼睛看不见,系统却说它「可见」。v0.4.9 起遮挡由 WindowMark 自己用矩形计算,这种窗口被当成一块不透明的板子,屏幕上所有边框被整圈裁光。
现在鼠标能穿过去的窗口不算遮挡物:
- 带
WS_EX_TRANSPARENT的分层窗口直接判定为可穿透 - 只靠透明像素让鼠标穿过去的,在窗口里取五个点做命中测试,任何一处接得住鼠标就仍然算遮挡
tracking.treat_as_topmost_classes里点名的窗口、被禁用的窗口,照旧算遮挡
没有装 WGestures 本身测,而是用 tools\check-transparent-overlay.py 造同类窗口,数屏幕上边框颜色的像素:
| 测试窗口 | 修复前 | 修复后 |
|---|---|---|
WS_EX_LAYERED + WS_EX_TRANSPARENT 的全屏透明窗口 |
22,868 → 0 | 100% 保住 |
只有 WS_EX_LAYERED、像素全透明 |
22,868 → 0 | 100% 保住 |
| 对照组:同样全屏透明,但正中一块接得住鼠标 | 裁光 | 照样裁光 |
对照组证明修复没有矫枉过正,也证明 WindowMark 确实对测试窗口做出了反应。
WindowMarkDiag.exe 同步:可穿透的窗口不再被报告成「挡住边框的窗口」。
安装:下载 WindowMark-v0.5.3-win64.zip,解压后运行 WindowMarkSetup.exe。不想安装就直接运行解压目录里的 WindowMark.exe。升级会保留现有配置。
完整变更见 CHANGELOG.md。
WindowMark v0.5.2
⚠ 双击没反应?先做这一步
解压之前:右键下载的 zip →「属性」→ 勾选底部的「解除锁定」→ 确定,然后再解压。
已经解压过、双击毫无反应的:先退出 WindowMark,对 zip 做上面的操作后重新解压。
为什么会这样
WindowMark 的 exe 没有数字签名。从网上下载的文件带着「来自网络」标记,双击时要先过 SmartScreen:
- SmartScreen 按下载量积累的「信誉」决定要不要弹「Windows 已保护你的电脑」。新版本刚发布、信誉还没攒起来,就会弹。
- 这个警告弹窗本身是个系统组件。在 LTSC / IoT 企业版(精简掉了这类组件),或者系统更新装了一半、这个组件在崩溃的机器上,连警告都弹不出来 —— 表现就是双击毫无反应,以管理员身份运行也一样。
这也解释了「老版本能用、新版本全都不行」:老版本发布得早、信誉够了,不会触发警告。如果某个新版本已经在后台跑着,老版本启动时发现「已在运行」会把消息转给它然后退出 —— 于是托盘里看到的版本号是新的那个,更让人摸不着头脑。
新增:WindowMarkDiag.exe 诊断工具
程序在跑、但边框没画出来(或者别的不对劲),双击 WindowMarkDiag.exe。报告会存到桌面并复制到剪贴板,直接粘贴发给开发者就行。
报告开头的「发现的问题」通常已经说明了原因,常见的几种:
| 报告里说 | 意思 |
|---|---|
| 前台窗口是最大化的 | 最大化的窗口按设计不画边框(v0.4.9 起) |
| border.enabled=false | 边框功能被关掉了 |
| 每一个的整圈边框都被遮挡计算裁掉了 | 有个看不见的窗口挡在所有窗口前面,报告会点名是谁 |
| 截不到这个会话的屏幕 | 远程桌面最小化或锁屏时运行的,换个能看到桌面的时候再跑一次 |
| 目录里有 N 个 exe 带「来自网络」标记 | 就是上面「双击没反应」那件事 |
报告里凡是「WindowMark 会怎么判断」的部分,调用的都是 WindowMark 自己的代码(配置解析、窗口资格判断、桌面快照、边框规划),不是另写的一套规则,所以不会和程序的实际行为对不上。
只读、不改任何设置;不收集窗口标题;路径里的用户目录会换成 %USERPROFILE%。
修复
- 发布包里的
measure-shadow-inset.ps1在普通中文 Windows 上跑不起来:脚本是无 BOM 的 UTF-8,Windows PowerShell 5.1 按系统代码页读,中文把引号吃掉,整个脚本解析失败。含中文的脚本全部加上了 BOM。 - 从源码构建:在系统代码页为 936 的机器上编译不过(核心库漏了
/utf-8,资源编译器也需要 UTF-8 代码页),改成全局设置。
安装:按上面的方法解除锁定后解压,运行 WindowMarkSetup.exe;不想安装就直接运行解压目录里的 WindowMark.exe。升级会保留现有配置。
SHA256(WindowMark-v0.5.2-win64.zip):E3F607CB1F39FDE6AB6CDC0BC3149702CE36F68F5426C5CFB7AD339ECD3682DD
完整变更见 CHANGELOG.md。
WindowMark v0.5.1
三件事在用户眼里都是「WindowMark 不工作了」,根因互不相干。
托盘图标在 explorer 重启后永久消失
现象是「WindowMark 启动不了」:托盘里没有图标,双击 exe 也毫无反应。其实进程一直好好跑着。
explorer 每次重启都会销毁所有托盘图标,然后广播一条 TaskbarCreated 消息让各程序重新加回去 —— WindowMark 从来没接过这条消息,图标于是再也回不来。explorer 重启比想象中常见:改显示配置、ToDesk / 远程桌面接入、它自己崩一次都会。抓到的现场里进程启动时间对得很死:WindowMark 15:15:44,explorer 18:46:53。
双击 exe 没反应是第二重。 单例检查发现已在运行,就给运行中的实例发个消息、自己退出;而那个消息的处理用 NIM_MODIFY 往图标上挂气泡 —— 图标都没了,NIM_MODIFY 直接失败,而且是静默失败。于是用户最可能采取的动作,恰好是唯一走到这条路的时候,却什么都不发生。
现在接上 TaskbarCreated 重新加图标;气泡那条路也不再假设图标存在。
锁屏之后可能永远停在挂起
只认 WTS_SESSION_UNLOCK 一个恢复事件。机器锁过一次,之后走 ToDesk 之类的远程接入,到达的是 WTS_CONSOLE_CONNECT / WTS_REMOTE_CONNECT,UNLOCK 未必来 —— 于是永远停在挂起:进程跑着、消息循环正常、一条边框也不画。
overlays_.Sync() 在挂起判断后面,所以画布尺寸也跟着冻住。抓到的现场:屏幕已经是 1920×1080,画布还停在 1536×864。
改成只有 LOCK 挂起,其余每一个会话事件都恢复。写成「其余一律恢复」而不是逐个列出恢复事件:新的事件类型只会把它推向恢复,不会把它锁死在挂起里。
tools\check-session-resume.py 盯诊断日志里的状态机验证,不盯屏幕像素 —— 锁屏界面或任何全屏窗口盖在上面时边框本来就该一个不画,那时「屏幕上没有边框」根本区分不出挂起和被遮挡。把恢复分支变异回「只认 UNLOCK」,6 个恢复事件里 5 个立刻失败。
边框往窗口里吃 4px
border.width=3、border.offset=-1,按文档窗口内侧该只盖 1px。实测是 2px 在外、4px 在内,整条线 6px —— border.width 说了不算。像素剖面显示 Windows 自己那条灰边已经完全被盖住,4px 里只有约 1px 是压它需要的。
两处合起来造成的:
corner_width_extra 加的是整圈的宽,不只是四个角。 corners 一开,整圈都由一条抗锯齿的弧矩形画出来,独立的直边段根本不存在了。而加宽方向写死了往窗口内侧长。改成往外长,内沿钉在 -offset,加多少宽都不动它。
屏幕夹取按 outer 算,把外扩的部分又裁了回去。 ClampBorderToScreen 返回的 limit 是从传进去的矩形只缩不放的,随后 ClipToBounds 裁掉超出部分 —— 传 outer 等于把长出去的那截原样裁回。改传 paintOuter,判据也从 reach 换成 reach + grow:窗口离任务栏 4px 时按 reach=2 算「没贴上」,可边框实际往外画 5px,照样会压上去。
corner_width_extra 默认也从 3 改成 1 —— 方向变了之后这个数的含义跟着变了,原来那个 3 是按「往内长」逐档试出来的。现在默认是 3px 在外、1px 在内、整条线 4px;设 0 就是严格的 border.width。
RoundedRingOf 抽成 core 的纯函数配测试,6 个变异体全部被抓住。
升级注意:已有的 settings.conf 不会被改写,corner_width_extra 的新默认值只对全新安装生效。想用新值自己改掉那一行,或者跑 rebuild_and_install.bat -Fresh。
安装:下载 WindowMark-v0.5.1-win64.zip,解压后运行 WindowMarkSetup.exe。不想安装就直接跑解压目录里的 WindowMark.exe。
完整变更见 CHANGELOG.md。
WindowMark v0.5.0 — 窗口拖动
窗口拖动
按住修饰键(默认右 Alt),在窗口内的任意位置按下鼠标就能拖它 —— 不用够到标题栏,也不用瞄准四条边。
主要解决一件事:窗口跑到屏幕外面,只剩一个角还露着。这时候标题栏和拖动区都在屏幕之外,鼠标根本点不到,窗口就再也拉不回来了。
| 手势 | 行为 |
|---|---|
| 修饰键 + 左键拖 | 整窗移动,窗口内任意位置 |
| 修饰键 + 左键单击 | 窗口居中到光标所在那块屏(位移 4px 以内算单击) |
| 修饰键 + 右键拖 | 缩放,按下位置落在九宫格哪一格决定拉动哪几条边 |
默认关闭。托盘 →「窗口拖动」→「启用」。触发键六个预设可多选(左右 Ctrl / Alt / Win),默认只勾右 Alt —— 左 Alt + 拖动被 Photoshop、IDE、Blender 占着,装在左 Alt 上会吞掉那些操作。
单击居中不占任何额外按键:那次按下本来就被拖动吞掉了,应用永远收不到,不接上就是白白浪费。
两个踩过的坑
移动事件不能吞。 WH_MOUSE_LL 返回非零 = 事件不再往下走,对移动来说就是光标不动。真实鼠标报的是相对位移,系统拿「当前光标位置 + 这一跳的位移」算 MSLLHOOKSTRUCT::pt —— 光标被钉死,pt 就永远只在按下点附近晃一个鼠标 tick 的距离,永不累加,表现是「窗口只晃一下就不动了」。
这个 bug 躲过了原来那份合成测试,因为它注入的是绝对坐标:pt 不依赖当前光标位置,照样累加,测试完全绿。改成相对位移注入后,同一份测试立刻抓住了它。判据也跟着改成「窗口位移 == 光标真实位移」,并单独断言光标确实动了 —— 少了这一条,「窗口没动 + 光标也没动」会当成两者一致蒙混过关,而那恰恰就是这个 bug 的样子。
鼠标底下不能拖时,要往 z 序下面找。 系统会留下看不见却铺满整块屏的 topmost 窗口:锁屏的 Windows.UI.Core.CoreWindow(LockApp.exe)和 LockScreenInputOcclusionFrame,解锁之后有时并不消失,IsWindowVisible 仍然是 1。TargetWindowAt 原来只问一次 WindowFromPoint,拿到不能拖的就返回空,于是整个手势在那块屏上彻底失效,而屏幕上什么异常都看不出来。现在继续往下找第一个能拖、且盖住这个点的窗口;只在按下时走一次,不进移动的热路径。
性能
空闲路径只有两行:先看在不在拖动,再看修饰键按没按(GetAsyncKeyState,几十纳秒),不查窗口、不算几何、不碰配置。装与不装钩子的差值落在噪声里。
键盘钩子只在配置含 Win 键时才装 —— 单独按一下 Win 再松开会弹出开始菜单,要压掉被手势用掉的那一次抬起只能靠 WH_KEYBOARD_LL;它比鼠标钩子更敏感,不该让所有人默认承担。
安装:下载 WindowMark-v0.5.0-win64.zip,解压后运行 WindowMarkSetup.exe。不想安装就直接跑解压目录里的 WindowMark.exe。升级会保留现有配置。
完整变更见 CHANGELOG.md。
WindowMark v0.4.10
v0.4.9 之后的三处收尾。
边框不再压在任务栏上
窗口贴着工作区底边(半屏吸附,或者手动拖到贴边)时,边框往外那两像素正好落在任务栏
那条上。实测:窗口内五像素命中 12 个采样点,窗口外命中 6 个。
跨屏那种越界本来就没有——画布只覆盖本监视器,越界的部分根本没地方落笔。压任务栏挡不住,
因为画布覆盖的是整块监视器,含任务栏那一条。
接法上有个坑值得记:不能把夹取后的矩形直接当成边框环的外矩形。圆角的路径以它为
基准算半径和位置,改了弧就错位。外矩形保持不动,只把可见范围限制在夹取后的矩形里——
弧还是原来那条,只是压出去的那截不画。
修完实测:窗口外从 6 个采样点降到 0,窗口内仍是 12,边框本身完好。
重装会把开机自启动弄丢
现象是重启电脑之后 WindowMark 没起来。查下去两边对不上:Run 键里没有启动命令了,而
StartupApproved\Run 里那条「已批准」还留着。
两段代码合起来造成的:卸载器关掉自启动(真卸载之后不该开机启动一个已经不存在的程序,
这是对的),而安装器读「当前状态」来决定装完要不要开——升级是「先卸载再安装」,安装器
读到的正是卸载刚清空的那个状态。每升级一次就关掉一次,直到某次重启才暴露。
如果你之前升级过 WindowMark,开机自启动很可能已经被关掉了。 装上这一版之后,在托盘
菜单里重新勾一次「开机启动」即可;此后升级不会再弄丢。
顺带修了关闭路径的残缺:只删 Run 键的值,不删批准记录,留下的正是「批准了一个不存在
的启动项」这种状态——查「为什么开机没启动」的人会先看到那条已批准,以为一切正常。
锁屏时挂起
锁屏期间 GetForegroundWindow() 返回 LockApp.exe,它会以前台窗口的身份混进快照,边框
就画到锁屏界面上去了;解锁后还要等下一个事件才恢复。而且锁屏后台仍会有窗口在动(播放器、
定时刷新的面板),每一次都白重画一遍谁也看不见的东西。
收到 WTS_SESSION_LOCK 停画并把画布擦干净,WTS_SESSION_UNLOCK 时重新取一次快照。
实测锁定时擦掉 99.92%,解锁后精确回到原值。
诊断工具:删掉失效的,改造还有用的
overlay 重构之后「每个窗口一个边框窗口」那个模型没有了,靠它工作的七个工具全部失效——
它们查的是 WindowMark.WindowBorder 这个已经不存在的窗口类。删掉,check-maximized.py
改用像素判据重写:不再问「这个窗口有没有配对的边框窗口」,直接去看那几条线画出来没有。
包里带的工具换了一批,都是能帮你自己解决具体症状的:
| 工具 | 用途 |
|---|---|
measure_shadow_auto.bat |
量窗口矩形比可见部分大多少,--apply 直接写进配置 |
measure-gap.py |
量边框缺口对不对得上遮挡,--all 扫全桌面 |
diagnose-window.py |
某个窗口为什么没边框、被谁盖了 |
inspect-corner.py |
逐像素看圆角画成什么样 |
watch-line-over.py |
守着抓「边框画到了上层窗口身上」 |
升级直接覆盖安装,配置不受影响。
WindowMark v0.4.9
rc1 说过「z 序没有根治,正式版会换掉底层模型」。换掉了。
边框不再和系统抢 z 序
以前每个窗口配一个边框窗口,各自调 SetWindowPos 往全局 z 序链表里插。那条链是全局
共享的、任何进程都能改的,调整还会静默失败——返回 TRUE、GetLastError() 为 0、状态
纹丝不动。后一个边框的插入打乱前一个刚排好的位置,窗口越多越乱,v0.4.5 到 v0.4.9-rc1
那几轮「边框不全」「切换时闪一下」全出在这里。
现在每块显示器一个透明画布,所有边框画在上面。「边框该排第几」这个时序问题变成了
「哪几段该画」的几何问题:非激活窗口的边框环减去排在它上面的窗口,剩下的才画。几何
计算不会失败,也没有竞争。
rc1 里那一整套 z 序操作——精确锚定、被拒后逐级退让、推不动就重建、守望轮询——全部删掉。
拖动跟手:CPU 从 98% 降到 20%
拖动窗口时边框落后十几个像素,还有近一半时间整条画不出来。量下来是一个核被占满:边框
每秒重画 30 次,每次 34ms。三处修,每处都先量后改:
- 圆角自己栅格化,不再用 Direct2D。 原本每段可见部分都要把整个环的路径栅格化一遍,
一个窗口每帧六段就是六遍,实测每段 2.15ms。改用圆角矩形的距离公式逐像素算,只碰环
那一圈带子——每段降到 0.044ms。 - 脏区只算变了的段。 原本取所有边框段的包围盒,拖一个窗口却要每帧重贴大半个屏幕
(2.41 百万像素,屏幕总共 3.69 百万)。改成和上一帧比出增删——降到 0.68 百万。 - 几何事件不再整取桌面快照。 每秒上百个事件,每个都遍历全部窗口、每个可见窗口两次
跨进程 DWM 查询,实测每次 2.4ms。改成缓存快照、只刷新动了的那一条——每秒 260ms
降到 28ms。
结果:边框重画频率 30 次/秒 → 125 次/秒(跟得上鼠标的报告率),实测滞后从中位 12px
降到 0px,「整条边框没画出来」的采样从 47% 降到 0%。
最大化的窗口不再画边框
它贴着工作区边缘,外面没有画边框的那几像素;夹回屏幕内之后边框和窗口边界完全重合,
实测四条边一个像素都露不出来。而它正是 v0.4.5–v0.4.8 那三轮「溢出到任务栏」「跨屏跑到
另一块屏」的源头——为一个看不见的东西修了三个版本。置顶窗口例外。
圆角
默认圆角,可选直角。直角走原来的像素填充,一个像素没动。圆角自己逐像素栅格化,抗锯齿
的过渡带宽度因此握在自己手里。设置里能调线宽增量和向窗口中心的偏移。
边框不对劲时,自己就能改
设置里「按类名调整」一组三个框,对应三种症状(类名用 WindowMarkInspect.exe 查):
| 症状 | 填哪个 |
|---|---|
| 这个窗口不该有边框却有 | 不画边框 |
| 该有边框却没有 | 强制画 |
| 它浮在上面,边框却盖了过去 | 视为置顶 |
另有一类:有些窗口把阴影画在自己的窗口矩形里,矩形比看得见的部分大一圈(实测 Windows 11
的托盘面板 15px,Qt 的贴图窗 14px,下边 32px),别人的边框就在它旁边多断那么宽。双击
measure_shadow_auto.bat,鼠标停在那个窗口上五秒,它自己量出来;加 --apply 直接写进
配置。
默认值换了一套
装上就有边框(以前默认关闭),线宽 3、偏移 -1、圆角自定义 12、圆角加宽 3。已有的
settings.conf 不受影响。
修复
- 点一下桌面空白处,所有边框消失。 桌面窗口也能当前台,而它铺满整个虚拟桌面却
谁都不挡——「前台盖住的地方不画别人的边框」这条推断遇到它就成了全屏遮挡。 - 快速来回切换两个窗口后,别人的边框盖在最上面那个窗口上。 快照的遍历走的是活的
z 序链表,途中有窗口被提到前面就再也遇不上——实测切七八次就能让前台窗口从快照里整个
消失,而遮挡计算的两道保险都要按 hwnd 找到它。 - MobaXterm 等 Delphi/VCL 程序的对话框被边框线切开。 事件层判断「是不是顶级窗口」
只看WS_CHILD,而这类对话框既带WS_CHILD又直接挂在桌面下,于是拖动它时一个事件
都产生不了,遮挡计算还记着它的旧位置。 - 边框和被它挡住的窗口之间有一像素的缝。 两块颜色正好齐平时,各自的抗锯齿边缘各占
半像素,合起来就是一条缝。遮挡范围往里咬一像素,边框钻到对方边缘底下。 - Word / Notepad++ 的查找对话框被边框横穿。
- 切换窗口时重叠处的边框滞后。
完整清单见 CHANGELOG.md。
v0.4.9-rc1 预发布:边框 z 序与延迟
预发布。 边框的 z 序做了大量修复,但没有根治——已知问题见文末。正式版会换掉底层模型。
最大化的窗口不再画边框
最大化窗口贴着工作区边缘,外面没有画边框的那 3px;夹回屏幕内之后边框和窗口边界完全重合,实测四条边一个像素都露不出来。而它正是 v0.4.5–v0.4.8 那三轮「溢出到任务栏」「跨屏跑到另一块屏」的源头——为一个看不见的东西修了三个版本。
置顶窗口例外:置顶是显式操作,边框是它生效的唯一视觉反馈。
边框 z 序:三个根因
一、SWP_NOSENDCHANGING 不能用于 z 序调整。 带着它设 HWND_TOPMOST 会「假成功」:返回 TRUE、GetLastError() 为 0、topmost 位和 z 序立刻都变了(实测 z 从 106 跳到 12),但两百毫秒内又退回原处。它跳过 WM_WINDOWPOSCHANGING,而那是 z 序变更提交路径的一部分。这个 flag 对移动是合理优化,对改 z 序是错的。
二、精确锚定会被 UIPI 拒绝。 锚点属于完整性更高的进程时直接被拒(任务管理器实测 25 秒内 141 次),而且不区分方向——往它下面插同样不行。
三、判据不能只看自己维护的 active 标记。 它和 GetForegroundWindow() 在焦点切换途中会分家,那时提 topmost 会在别的窗口上留下一条与谁都不搭界的孤立线。
现在前台窗口用一个创建时就带 WS_EX_TOPMOST 的专用边框窗口,只移动、只上色、层内换位,永不执行「从普通层提进 topmost 层」——那正是会卡死的操作。
性能
三处都不是计算量的问题,是顺序和节流:MoveTo 的 15ms 节流与 60Hz 的 16.7ms 打拍;z 序排在重绘后面,而视觉上「边框出现」等的正是 z 序;书签条排在边框前面。
| 指标 | 改前 | 改后 |
|---|---|---|
| 拖动跟随·中位数 | 6.1 ms | 3.0 ms |
| 拖动跟随·p99 | 45.3 ms | 15.7 ms |
| 拖动跟随·一帧内 | 95% | 100% |
| 切换后边框出现·中位数 | 28 ms | 5 ms |
| 切换后边框出现·最慢 | 141 ms | 11 ms |
残影来自尾部而不是中位数:去掉节流对中位数几乎没有影响,p99 却从 27.2ms 掉到 15.7ms。
其他
- MobaXterm 等 Delphi/VCL 程序拿不到边框:它们把主窗体挂在一个隐藏的
TApplication窗口下(0x0 +WS_EX_TOOLWINDOW),被「有 owner 就是附属窗口」的判据滤掉了。改为看 owner 自己像不像个窗口。 - 内缩量缓存键补上 DPI(照机制补的防线,未经复现)。
- 新增一批诊断工具(
tools/),都不抢焦点、不动任何窗口。
已知问题(rc1 未解决)
- 非激活窗口的边框层次在窗口重叠时经常不对:会被与目标重叠的其他窗口盖住。
- 快速来回切换多个重叠窗口时,边框仍可能短暂不完整。
两者同源:现在用全局共享、任何进程都能改、调整还会静默失败的 z 序,去表达十几组局部的归属关系。每个边框的 SetWindowPos 都改动同一条全局链表,后一个的插入打乱前一个刚排好的位置。这不是补丁能修好的。
正式版会改成「Z 序只读不写 + 遮挡裁剪」的单 Overlay 模型:每个显示器一个透明画布,非激活边框按「边框区域 − 上方所有窗口的并集」裁剪后绘制,激活边框最后画、不裁剪。那样「边框排第几」这个和系统抢资源的时序问题,变成「哪几段该画」的纯几何问题。