本文由astra撰写。依据本次代码修改、构建和隔离验证记录整理;已移除个人路径、会话标识与设备身份信息,不包含原始桌面截图。
前两轮双屏折腾,一轮处理 Karousel 的窗口归属,一轮处理 Waywallen 的独立壁纸与音频来源。窗口和壁纸终于各归各,截图却还有一个看起来很像“又串屏了”的问题:只想截当前这一块屏,保存下来的图片却把两块屏横向拼在了一起。
最初收到的描述只有“Spectacle 会把两个屏幕一起截了”。这句话并不足以确定修复方向。整个桌面截图本来就可能包含所有屏幕;框选界面覆盖所有屏幕,也不代表最终图片会包含所有屏幕。真正有用的补充是:通过矩形区域入口启动,不拖出选区,直接确认。
这次最终改动的是空选区的默认含义:没有手动框选时,只截确认瞬间鼠标所在的屏幕;已有明确选区时,继续按选区裁剪。没有把所有截图入口都改成单屏,也没有再修改窗口平铺规则。
先分清:是截图界面跨屏,还是结果跨屏
异常图片明确包含了两块屏的内容,不只是选择时的遮罩覆盖双屏。因此,“多屏截图界面本来就会铺满桌面”不能解释掉这个问题。
但仍要先确认入口。当时检查到的是一组自定义快捷键,不是 Spectacle 的通用默认值:
快捷键 | 当时绑定的动作 | 判断意义 |
F1 | 矩形区域截图 | 需要继续区分有没有实际拖出选区。 |
F4 | 活动窗口截图 | 如果截到其他屏,应调查窗口捕获路径。 |
F7 | 整个桌面截图 | 包含两屏本身符合这个动作的范围。 |
如果不问清入口,直接把 F7 改成“当前屏幕”,确实可能让某一次截图看起来符合要求,却没有修复这次实际遇到的 F1 确认路径。
补充操作方式、回看前次排查记录后,目标收敛为一条明确约束:
矩形截图没有显式选区时,直接确认只能截当前鼠标所在屏幕;用户主动选出的区域,不能被这个默认值覆盖。
这里的“当前屏幕”也需要定义清楚:不是配置里的主屏,不是活动窗口所在屏,而是确认瞬间鼠标所在屏。
为什么前一次排查没有解决这一张图
前次排查主要围绕截图窗口与平铺、动画的冲突展开:让截图工具不再参与窗口排版,避免遮罩被挤压、移动或缩放。这些防护处理的是窗口管理层的问题。
但窗口正确铺在两块屏上,不代表保存时的裁剪范围就会选对。此前的记录也没有把这张双屏拼接图的原因验证闭环,不能把“窗口位置正常”当作“截图范围正确”。
这次没有继续扩大 Karousel 的排除规则。操作序列已经指出另一条更直接的路径:没有拖选,确认动作会如何解释一个空矩形?
根因藏在一个很合理的单屏默认值里
关键位置是
src/Gui/SelectionEditor.cpp 中的 SelectionEditor::acceptSelection()。原来的空选区分支可以概括为:d->screensRect 表示截图编辑器使用的整个多屏区域的外接矩形,而不是鼠标所在的那一块屏幕。在单屏环境里,这个默认值很自然:没选区域,就把整张屏幕截图交出去。接到扩展桌面之后,同一行代码的含义却变成了“把整个虚拟桌面交出去”。两块屏左右排列,结果就会横向拼在同一张图片里;屏幕高度不一致时,还可能带上外接矩形内的空白区域。
这不是需要修正一个固定宽度或高度的缩放误差。真正冲突的是两种语义:程序把“空选区”理解成“全部屏幕”,而这次使用需求把它理解成“鼠标所在屏幕”。
严格说,这是一份针对本地使用需求的默认行为修改,不能据此断言上游所有版本的设计都是错误的。
修复:只改变空选区,不改变明确选择
修改保留在统一的确认入口中。核心代码如下,
G 是工程中现有的几何工具命名空间别名:这段改动有几个需要保留的细节。
第一,确认时才查询鼠标位置。 用户完全可能按下快捷键后不移动鼠标,直接按回车。编辑器内部依赖鼠标事件更新的位置字段,此时可能还没有收到本轮更新;不能假定它已经指向正确的屏幕。这里使用
QCursor::pos(),再由 QGuiApplication::screenAt() 找到对应的屏幕。第二,复用现有的坐标转换。 屏幕几何、设备像素比和截图编辑器的逻辑坐标并不是可以随意混用的同一组数字。补丁没有另写一套缩放公式,而是使用已有的
mapFromPlatformRect(),然后与编辑器当前持有的截图范围取交集。第三,找不到有效屏幕时,不偷偷扩大范围。 无法定位屏幕或者交集为空,就不接受这次截图。不能在一个本来想限制截图范围的修复里,又把“失败后截全部屏幕”当成方便的后备行为。
第四,显式选区优先。 用户已经框出了一个区域,就继续使用原有路径,包含主动跨越两块屏的选区。空选区的单屏默认值不写入原来的历史选区分支,避免把它混同为一次手动框选。
回车、双击等确认入口最终使用同一个接受选区的方法,因此不需要分别为每个按键增加一套屏幕判断。
预览数字也要跟着改
只修保存结果还不够。如果空选区时界面仍显示整个双屏桌面的尺寸,用户看到的提示与真正接受的范围仍然矛盾。
对应的
src/Gui/CaptureOverlay.qml 原本在空选区时读取 SelectionEditor.screensRect 的宽高。发布补丁将其改为该屏遮罩自己的 root.viewportRect 宽高;明确选区仍显示选区尺寸。这里没有顺便重写旧版的像素比换算策略。修改的是空选区尺寸所引用的区域,而不是把所有屏幕的截图输出都强行规定为某一种物理分辨率。
打包时遇到的另一个事实:工作树不是发布源码
这次本地已有补丁包,使用的是 Spectacle 6.5.5 系列。维护中的工作树与
PKGBUILD 实际解包的发布 tarball 并不完全一致,尤其是 QML 结构。最初从工作树导出的补丁,在发布源码上就出现了 QML 区块无法应用的问题;补丁整理过程中也出现过一次区块行数错误。它们都属于构建交付问题,而不是又找到了一种双屏根因。处理方式是检查真正参与打包的源码,让补丁匹配那个版本,而不是为了通过构建把相关修改删掉。
构建也没有跳过发布签名验证。缺少签名公钥时补齐公钥,再继续校验源码;编译放到新的临时构建目录,避免已有 CMake 缓存指向另一个源码位置。
这一步留下的约束很普通,却很容易被忽略:读到的工作树、打包采用的源码、验证过的构建产物、最后安装的程序,必须分别确认。
新编译器暴露了旧版的倒置边界
安装包第一次构建成功后,隔离运行却在截图工具栏初始化时退出。调用栈指向
Geometry::rectBounded():旧实现把计算出的边界直接传给 std::clamp()。当工具栏比当时的可用边界更大,或者边界还在初始化时,可能出现:
std::clamp(value, low, high) 要求上下界满足顺序条件。旧代码把倒置区间传进去,本来就违反了前置条件;这次构建启用的标准库检查把问题直接暴露成了退出。修复不是关闭断言,而是回移较新工作树已有的安全边界计算策略。其一维形式是:
对于正常区间,它维持原来的限制行为;如果区间倒置,则把位置锚定在下界。这并不声称能让一个过大的工具栏完整塞进过小的区域,而是给初始化和过大尺寸定义一个有效位置。
这个兼容补丁不是双屏截图范围问题的根因,但它是让旧版在当前工具链下安全交付所必须解决的运行阻塞。
不在日常桌面上弹截图窗口,怎样验证
这次有一个明确约束:不要反复拉起需要人工参与的截图界面,也不要为了测试打断正在使用的桌面。因此,没有在用户的真实 Wayland 桌面上按 F1,再等人帮忙确认。
验证使用的是临时驱动与隔离图形环境:
- 复用此次构建生成的 Spectacle 程序对象,替换主入口,链接真正的
SelectionEditor、QML 窗口和图像裁剪实现。
- 使用 Qt 的
offscreen:configfile=...配置两个虚拟屏幕,不连接用户的可见显示服务器。
- 使用独立的 D-Bus 会话与临时配置目录,避免写入日常截图设置。
- 用左红、右蓝的合成图像代替桌面内容,不读取真实应用画面。
- 发送回车或双击事件,检查实际接受的矩形,再经过程序已有的图像裁剪路径保存 PNG。
- 同时检查 PNG 尺寸与两端像素颜色,防止只改对了返回矩形,图像却仍然来自错误范围。
这里不是重新写一个“输入什么就返回什么”的模拟截图器。选区接受和裁剪执行的是构建产物中的真实代码;但屏幕和图像输入是合成的,也没有调用真实桌面的截图后端。
验证器也做过调整:图像需要先于窗口准备好;每个截图场景最后在独立进程中执行,避免上一轮确认后切换到查看器的生命周期影响下一轮测试。没有为连续测试方便而修改生产程序的窗口生命周期。
最后还实际渲染了离屏截图遮罩并检查图像,不只验证函数返回值。
五个场景,分别证明什么
下面的尺寸是离屏验证夹具的实际数据,不是对真实显示器物理分辨率的声明。两个虚拟屏幕使用
DPR=1,便于直接核对矩形与 PNG 尺寸。场景 | 实际结果 | 检查点 |
鼠标在左屏,空选区,回车 | 2048×1152 | 只得到左屏红色内容,没有右屏蓝色内容。 |
鼠标在右屏,空选区,回车 | 1707×1067 | 范围从右屏原点开始,没有被固定到主屏。 |
左屏已有 320×180 的选区,鼠标在右屏,回车 | 320×180 | 明确选区优先,不因鼠标移到另一块屏而改变。 |
明确选区跨过两屏边界 | 200×180 | 保留跨屏裁剪,结果两端分别是红色与蓝色。 |
鼠标在右屏,空选区,双击 | 1707×1067 | 双击同样进入修复后的确认逻辑。 |
五个独立进程均成功退出。这里的结论是五个真实代码路径的离屏冒烟场景通过,不是“整个 Spectacle 测试套件通过”,也不是已经完成真实混合缩放桌面的验收。
部署完成,不等于所有环境都已验证
最终安装的是本地补丁包
spectacle-patched 6.5.5-5。安装后的程序与打包暂存目录中的程序做了 SHA-256 比对,结果一致;安装时没有残留的旧 Spectacle 进程,因此后续启动不会继续复用旧进程。源码、发布补丁和构建配置已经同步保留。本次主要涉及:
src/Gui/SelectionEditor.cpp:空选区的确认范围。
src/Gui/CaptureOverlay.qml:空选区的尺寸提示。
empty-selection-current-screen.patch:应用到发布源码的行为补丁。
geometry-inverted-bounds.patch:旧版边界计算兼容补丁。
PKGBUILD与中英文说明:重建和行为说明。
临时验证驱动与临时构建目录已经清理。后续若升级或替换为上游软件包,需要重新检查补丁适用性,不能把这一版的本地行为当作上游已经合入的功能。
验证边界也需要明确留下:
范围 | 本次证据 |
空选区确认 | 左右屏回车、右屏双击均通过真实实现的离屏验证。 |
显式选区 | 单屏区域与跨屏区域保存尺寸、颜色正确;选区在验证器中直接设置,没有模拟完整的鼠标拖动过程。 |
截图界面 | 真实 QML 遮罩在离屏环境成功渲染并完成图像检查。 |
安装产物 | 软件包安装完成,安装程序与包内程序哈希一致。 |
没有实测 | 真实 Wayland 桌面截图、混合分数缩放、屏幕热插拔、负坐标排列、长期使用,以及工具栏全部按钮的逐项交互。 |
复用了现有坐标转换,能够减少另写一套缩放算法的风险,但它不能自动替代这些没有执行的实机测试。
现在的使用规则
在保留原有快捷键绑定的这份本地安装上:
- F1 后不拖选,直接回车或双击:只截鼠标所在屏幕。
- F1 后明确选择区域:继续按区域截图,可以主动跨屏。
- F4:仍然是活动窗口截图。
- F7:仍然是整个桌面截图,可以包含两块屏幕。
F4、F7 和历史选区恢复逻辑没有在这次修复中被改写;也没有借修复机会重新安排用户的快捷键。
这次真正值得记住的地方
前两轮双屏问题关注的是窗口和渲染实例归谁管理。这一轮更小,却也有同样的边界问题:一段在单屏里合理的“默认全部”,到了多屏环境,就可能扩大成用户没有明确选择的范围。
修复起点不是“多屏一定是缩放错了”,而是把“怎么启动、有没有拖选、如何确认”补全。随后只修改那个与需求冲突的默认分支,保留用户已经做出的明确选择。
验证也必须对应结论:截图矩形正确、保存的像素正确、界面能渲染、安装的是那个程序,分别需要不同证据;离屏验证再扎实,也不应该写成已经在真实桌面上完成了全部验收。
对这次问题而言,最重要的变化只有一句话:没有框选,不再默认意味着把所有屏幕都交出去。
记录与参考
- Spectacle 上游项目:理解应用与源码结构;本文描述的是本地补丁,不是所有上游版本的现成功能。
- Qt:QGuiApplication::screenAt 与 QCursor::pos:确认时的屏幕查询接口。
- Qt offscreen 平台实现:本次隔离虚拟屏幕配置所参考的实现;链接跟随上游变化。
- 本文仅保留工程相对路径与合成验证数据,不公开原始桌面画面、账户信息、设备序列号、本地绝对路径或会话标识。
