Skip to content

Releases: yakoye/WindowMark

WindowMark v0.6.0

Choose a tag to compare

@yakoye yakoye released this 08 Oct 09:52

这一版做了三件事:给程序一个能看懂的入口,让边框自己从坏状态里爬出来,再把几个真问题修掉。

左键单击托盘图标 = 主面板

右键菜单对会用的人够快,对第一次见它的人不行:四个功能叫什么、各自干什么、怎么用、现在开着没开,全藏在三层子菜单里,而菜单装不下任何一句说明。

现在左键单击托盘图标打开一个面板(菜单最上面也多了「面板...」):

  • 四个功能各一块:名字 + 一个勾(当场开关,面板不关)+ 一句「这是什么」+ 一句「怎么用」+「设置...」「排除应用...」;
  • 「置顶刚才那个窗口「<标题>」」——按钮上直接写着是哪个窗口,已经钉住时变成「取消置顶」,旁边是准星;
  • 「开机时自动启动」勾,以及 配置文件 / 创建桌面快捷方式 / 剪贴板守护 / 诊断报告 / 安装到系统(或卸载)/ 关于 / 退出。

边框:三个真问题

点一下任务栏,所有窗口的边框都变成「非活动」色。 活动窗口跟着系统的前台事件走,而任务栏、托盘、开始菜单、以及 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

Choose a tag to compare

@yakoye yakoye released this 01 Oct 15:45

这一版只做一件事:把「怎么装、怎么开」这件事收拾干净。

以前下载解压完,根目录里平铺着六个 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

Choose a tag to compare

@yakoye yakoye released this 18 Sep 11:39

书签的悬停重做了,外加两个「看起来像坏了」的问题。

磁性书签栏

以前:鼠标命中哪个书签,哪个就展开成一长条、塞进整个名字,其余收起;滑过两个书签之间的间隙,整条栏先塌下去再弹起来。

现在鼠标是一块磁铁,书签是一排铁块:

  • 沿书签栏方向,离鼠标越近的书签越大,正对中心时长到固定峰值(绝对像素,激活的书签和别的书签一样大);半径(默认 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

Choose a tag to compare

@yakoye yakoye released this 17 Sep 09:20

三件事。

不再需要安装 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

Choose a tag to compare

@yakoye yakoye released this 14 Sep 12:30

⚠ 双击没反应?先做这一步

解压之前:右键下载的 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

Choose a tag to compare

@yakoye yakoye released this 11 Sep 23:26

三件事在用户眼里都是「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 — 窗口拖动

Choose a tag to compare

@yakoye yakoye released this 11 Sep 02:16

窗口拖动

按住修饰键(默认右 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

Choose a tag to compare

@yakoye yakoye released this 09 Sep 08:07

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

Choose a tag to compare

@yakoye yakoye released this 08 Sep 08:35

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 序与延迟

Choose a tag to compare

@yakoye yakoye released this 05 Sep 00:41

预发布。 边框的 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 模型:每个显示器一个透明画布,非激活边框按「边框区域 − 上方所有窗口的并集」裁剪后绘制,激活边框最后画、不裁剪。那样「边框排第几」这个和系统抢资源的时序问题,变成「哪几段该画」的纯几何问题。