当前版本 v2.18.1-qt(版本号只在 version.py 定义;改动记录见
CHANGELOG.md)。
作者 / Author:Walt Liang <Wat.L@outlook.com>
(作者与邮箱同样只在 version.py 定义一处,界面「关于」、帮助、
单文件版文件头都从那里取。)
FSCapture 风格的 Windows 截图与标注工具,专为做操作指引/步骤说明优化。 支持区域截图、全屏截图(可选显示器)、滚动长截图、取色、贴图, 以及带多标签页的标注编辑器(可把多张截图粘贴拼到一张图上编辑)。
零依赖的 Tkinter 实现保留在
tk_version/(备用/对照)。 版本控制与提交历史见 VERSIONING.md。 许可:MIT(可自由使用、修改、再分发)。
pip install PySide6 # 唯一依赖
python main.py # 驻留托盘,按热键截图
python main.py 图片.png # 直接编辑已有图片
python PyShot.py # 单文件版(缺依赖会自动 pip 安装)
python PyShot.py --check-deps # 只检查依赖改完源码后重新生成单文件:python tools/build_single.py
| 方式 | 说明 |
|---|---|
| 区域截图 | 热键 Ctrl+Alt+X(被占用时自动换 Ctrl+Shift+X / Ctrl+Alt+F9 / Ctrl+Shift+F9),或双击托盘图标 |
| 全屏截图 | 热键 Ctrl+Alt+F(自动换 Ctrl+Shift+F / Ctrl+Alt+F10 / Ctrl+Shift+F10)——截鼠标所在的那块显示器 |
| 指定显示器 | 托盘右键 →「选择显示器截图 ▸」→ 列出每块屏(主屏标记/分辨率/缩放比例),点哪块截哪块;末尾还有「所有显示器拼成一张」 |
- 全屏截图会自动最小化编辑器再抓,避免窗口残影进图
- 抓到的画面是空白/纯色(硬件加速、内容保护窗口如 Citrix)时自动回退 PrintWindow
- 全屏冻结遮罩:选区高亮、尺寸标签、对齐参考线、跟随放大镜(含色值)
- Esc / 右键取消;多显示器时每块屏一个覆盖层
- 屏幕取色:单击即把
#RRGGBB复制到剪贴板
- 标签页:多次截图累积在同一个编辑器里,可切换、单独关闭、
+再截一张 - 工具:选择/移动 · 矩形 · 椭圆 · 直线 · 箭头 · 画笔 · 序号(自动递增)· 文字 · 高亮 · 马赛克 · 取色 · 裁剪
- 颜色 9 色调色板 + 自定义取色;线宽 / 字号 / 序号圆大小可调
- 选中图形可拖动、8 点句柄缩放;撤销/重做(每标签独立)
- 缩放
−/+/100%/适应,Ctrl+滚轮;100% 时按物理像素 1:1 显示 - 导出:复制剪贴板、保存 PNG/JPG/BMP、贴图钉在屏幕最上层
- 系统缩放(125%/150%)下画图跟随鼠标 —— 见下方「坐标约定」
关掉软件(或崩溃)之前,编辑器里的标签会悄悄写进缓存;下次启动自动放回来, 连标注都还在(可以继续改,不是一张"焊死"的图)。
- 位置:
~/.pyshot/session/(session.json+ 每个标签一张无损 PNG) - 不需要你保存:不弹对话框、不碰你的文件夹;恢复的标签仍是"未保存"状态, 想留就 Ctrl+S
- 只保留最后一次会话(每次写入先清空,不会越积越多);最多 12 个标签、 总量约 120MB
- 写入是"先写临时目录再整体替换",写到一半崩掉也不会留下坏会话
- 「选项 → 启动时恢复上次的截图」可开关;「清除上次的截图缓存」可手动清掉
- 恢复成功时启动气泡会写"(恢复了 N 张上次的截图)"
图放大到超出窗口时,有三种方式平移画面:
- 抓手工具(工具轨道最下方):左键拖动即可移动画面,光标是张开/握拳的手
- 中键拖动:任何工具下都能用(画图时不用切工具就能挪画面)
- 按住空格 + 左键拖动:临时抓手,松开空格回到原工具(和 Photoshop 一致)
平移是直接驱动滚动条,所以和滚轮缩放、滚动条位置完全一致;拖动时不会误画图形。 (顺带修了个老问题:状态栏一直写着中键拖动滚动,但代码里对非左键直接 return, 中键其实从来没生效过。)
顶栏按钮之外,编辑器还有标准菜单栏(功能更全、也符合习惯):
| 菜单 | 内容 |
|---|---|
| 文件 | 打开图片… (Ctrl+O) · 打开剪贴板图片 · 保存 (Ctrl+S) · 关闭当前标签 (Ctrl+W) · 退出 (Ctrl+Q) |
| 编辑 | 撤销 · 重做 · 复制到剪贴板 · 贴图到屏幕 |
| 视图 | 放大 · 缩小 · 实际像素 (1:1) · 适应窗口 |
| 特效 | 水印… · 边框…(和 FSCapture 的「特效」菜单一致) |
| 选项 | 语言 ▸(简/繁/英/跟随系统)· 编辑默认水印… · 编辑默认边框… |
| 帮助 | 关于 PyShot |
编辑器可以空着直接打开:托盘「显示编辑器」在没有窗口时直接给一个空白编辑器 (以前会弹打开图片对话框,只是想看看编辑器却先被要求选文件)。空白时有提示页 说明怎么开始,依赖图片的菜单项自动置灰,打开图片后自动恢复。
| 模式 | 适用 |
|---|---|
| 自动滚轮 | 普通网页/文档 |
| 拖拽滚动条 | 远程桌面、Citrix 等不吃滚轮的应用 |
| 按键翻页 | 没有滚动条的应用 |
| 手动滚动 | 你自己滚,程序只负责拼接 |
- 能读到系统滚动条状态时用
SetScrollInfo程序化滚动(不动鼠标、绝对精准); 否则真拖滑块并逐步试探校准步长,锚点跟随滑块,滚太多自动减半重试 - 拼接:行哈希投票定位移 + 行级精确比较定胜负(1~2 ms/帧); 自动裁掉固定边缘(标题栏/底部面板),识别"多块独立滚动区域"并给出提示
区域截图 Ctrl+Alt+X ← 高频动作置顶 全屏截图 Ctrl+Alt+F 选择显示器截图 ▸ 每块屏一项 + 所有显示器拼成一张 ────────── 滚动长截图 ▸ 自动滚轮 / 拖拽滚动条 / 按键翻页 / 手动滚动 ────────── 屏幕取色 贴出剪贴板图片 ────────── 打开图片编辑… 打开编辑器 ────────── 退出 PyShot
- 快捷键显示在右侧列(只是显示,不注册 Qt 快捷键 —— 否则会和全局热键 同时触发、一次按键截两张图)
- 「选择显示器截图」在弹出时按当前屏幕列表重建,插拔显示器后自动更新
编辑器顶栏「水印」按钮打开对话框,结构对齐 FSCapture 的「特效 → 水印」:
┌ ☑ 文字水印 ────────────────────────────┐ │ [多行文本 ] │ │ 字体 [Microsoft YaHei 28px 粗体] 颜色[■] │ │ 不透明度 [========|=====] 35% ☑ 描边 │ ├ ☑ 图片水印 ────────────────────────────┤ │ [路径 ] [浏览…] [清除] │ │ 大小 [20] % 图宽 不透明度 [=====|==] │ ├ 位置与排布 ────────────────────────────┤ │ ┌───┬───┬───┐ ☐ 平铺整张图 间距[60]px │ │ │ ○ │ ○ │ ○ │ 旋转 [0]° 边距 [24]px │ │ ├───┼───┼───┤ │ │ │ ○ │ ○ │ ○ │ │ │ ├───┼───┼───┤ │ │ │ ○ │ ○ │ ● │ ← 九宫格点选 │ │ └───┴───┴───┘ │ ├ 预览(实时) ──────────────────────────┤ │ [ 水印效果实时显示 ] │ └ [应用] [应用并设为默认] [取消] ──────────┘
- 文字与图片可以同时启用(FSCapture 的组合水印),上下排列成一组
- 文字/图片各自独立的不透明度;文字可加描边,深浅背景都清晰
- 字体走系统字体对话框(和 FSCapture 的 Font 按钮一致),颜色用取色器
- 位置是九宫格点选(不是下拉框),勾选平铺后位置自动禁用、间距生效
- 支持旋转角度与距边距(margin)
- 「应用并设为默认」后,之后每次新截图自动加水印(可 Ctrl+Z 撤销)
- 配置文件在 ~/.pyshot/watermark.json;旧版配置会自动迁移 (kind → use_text/use_image、�lpha → 两个透明度、 position=9 → ile=true、shadow → outline)
编辑器顶栏「边框」按钮打开对话框,和水印同一个菜单体系:
┌ 边框 / 边缘效果 ────────────────────────┐ │ 样式 [投影阴影 ▼] │ │ 宽度 [16] px 颜色 [■] │ │ 细节 圆角 [16] px 阴影浓度 [====|] 43% │ ├ 预览(实时) ──────────────────────────┤ │ [ 边框效果实时显示 ] │ ├ [应用] [应用并设为默认] [取消] ──────────┘
九种样式(FSCapture 各版本样式名略有出入,这里是常用的一套):
| 样式 | 效果 |
|---|---|
| 单线边框 | 纯色边框,最简洁 |
| 双线边框 | 外粗内细的双线 |
| 虚线边框 | 虚线描边 |
| 圆角边框 | 图片切圆角 + 描边 |
| 投影阴影 | 四周柔和阴影,背景透明(贴到文档里自然) |
| 立体浮雕 | 左上亮、右下暗,做出凹凸感 |
| 边缘渐隐 | 图片四边渐隐到边框色 |
| 拍立得白边 | 下方留宽白边,像拍立得 |
| 手撕纸 | 图片贴在一张撕下来的纸上:不规则撕口 + 纸面渐变 + 跟着撕口走的投影 |
- 边框加在图片外面,所以输出图会变大(和 FSCapture 一致,不是画在图上)
- 已有标注会整体平移跟着走,不会错位
- 全程走撤销栈,加错了 Ctrl+Z 就能回去
- 「应用并设为默认」后,之后每次新截图自动加边框
- 配置在 ~/.pyshot/border.json
手撕纸(torn)的做法:
- 撕口是整圈一次连续随机游走生成的(不是按四条边分段),所以撕痕会自然绕过 四角、不会在角上被掐尖;带惯性 → 起伏连续;10% 概率来一道更深的撕裂
- 用确定性随机(
seed):同一种子下预览与成品逐像素一致; 「换一个撕法」只是把 seed +1,不会每次点开都不一样 - 纸面有极淡的纵向渐变(受光感),撕口描一条极淡灰线 + 随机纤维短须
- 投影是同一撕边路径多次小偏移叠加而成,所以阴影形状跟着撕口走
- 纸边宽度 =
宽度,撕边起伏 =撕边幅度(自动夹取到不超过纸边宽度); 外面再多留一圈撕边幅度,保证撕口不超出画布
托盘右键 →「语言 / Language」可切换,三项勾选,另有一项「跟随系统」。
- 默认跟随系统:zh* → 简体,zh_TW / zh_HK / Hant → 繁體, 其它语言一律英文(首次运行自动判断,之后记住你的选择)
- 选择存在 ~/.pyshot/settings.json;设置 PYSHOT_LANG=zh_CN|zh_TW|en 可强制指定(测试与批处理用)
- 切换后立即生效:托盘菜单当场重建;已打开的编辑器窗口会就地刷新文案 (做法是把当前显示的文本再翻译一次 —— i18n 里有译文→原文的反查, 所以反复切换不会错乱);对话框每次打开都是新的,自动用新语言
实现(i18n.py + i18n_data.py):
- 键就是简体原文(gettext 风格):源码里写 r(区域截图),好读好维护; 占位符用 r(已保存:{}, path)
- 查不到的词条回落原文,所以漏译只是显示原文、不会崩
- 词表由 gen_i18n.py 生成(繁体用字/词映射自动转换,英文手写); wrap_tr.py 用 AST 把 UI 位置的字符串批量包成 r(...)
- 当前覆盖:繁体 257/257,英文 219/257(余下是控制台日志与内部调试片段)
- 托盘常驻,启动不自动截图;托盘菜单可随时「打开编辑器」
- 截图期间编辑器自动最小化,截完/取消都会恢复
理解这三条就不会再出"画图不跟手"的 bug:
- 图形坐标一律用"图像物理像素",与系统显示缩放无关。
- 底图是 dpr 感知绘制的:
QPixmap带devicePixelRatio时 Qt 按逻辑尺寸 (物理尺寸 / dpr)绘制,所以 100% 缩放时按物理像素 1:1 显示、不糊。 - 因此
paintEvent里画图形前必须再scale(1/dpr),而鼠标映射用to_image = 控件坐标 × dpr / zoom;两者必须严格互逆。少了第 3 条里的
1/dpr,dpr≠1(系统缩放非 100%)时图形会整体偏 dpr 倍, 表现就是画图不跟手(100% 缩放下看不出来,所以容易漏)。
test_editor_dpi.py 守这条不变量:在 dpr=1/1.25/1.5/2 与多种缩放下
真实渲染并检查图形像素落点是否等于鼠标位置(误差 <1px)。
pyshot/
├── PyShot.py # ★ 单文件版(tools/build_single.py 生成,可直接分发)
├── main.py # 入口:托盘、全局热键(区域+全屏两组)、截图流程
├── snipper.py # 覆盖层:框选/取色(放大镜、尺寸、多显示器)+ grab_screen
├── editor.py # 编辑器:多标签页、画布、工具、导出
├── shapes.py # 标注图形对象
├── scroller.py # 滚动长截图(四种驱动 + 拼接算法)
├── capture_utils.py # 抓屏工具:空白检测、PrintWindow 回退、原生滚动条 API
├── style.py # 深色主题与矢量图标
├── watermark.py # 水印(对照 FSCapture:文字/图片各自开关、九宫格/平铺、独立透明度)
├── border.py # 加边框(对照 FSCapture「特效 → 边缘」:8 种样式、图会变大)
├── pinboard.py # 贴图窗口
├── help_text.py # 帮助正文(只写简体,繁體/英文由 i18n 词表提供)
├── helpwin.py # 帮助窗口(小节列表 + 搜索 + 正文,非模态)
├── bootstrap.py # 依赖自举(缺库自动 pip 安装,多级降级 + 镜像)
├── version.py # 版本号与作者(唯一来源)
├── tests/ # 全部测试(57 个套件,见下)
├── tools/ # 开发/诊断脚本:构建、词表生成、跑测试、抓屏诊断
└── tk_version/ # 零依赖 Tkinter 实现(备用/对照)
运行时代码都在根目录:
main.py里全是from editor import …这种顶层导入, 而且python main.py/python PyShot.py是用户的常用入口,所以不挪进src/。
python tools/run_tests.py # 跑全部离线套件(推荐;约 80 秒)
python tools/run_tests.py --live # 连需要"真实桌面已解锁"的也一起跑
python tools/run_tests.py test_help # 只跑名字匹配的
python tools/run_tests.py --list # 看看会跑哪些也可以单独跑:python tests/test_editor_dpi.py、python tests/test_flow.py 等 ——
每个测试自己把项目根加进 sys.path,从哪个目录调用都行。
- 顶层同名会静默覆盖:
style.py的图标函数_draw_text(p, c)覆盖了watermark.py里同名的_draw_text(painter, settings, box)—— 多文件版正常, 单文件版一点「水印」就TypeError。 - 本地导入的
as别名会悬空:合并后本地 import 一律被删(名字已在同一命名空间), 但from border import load_border_default as border_load这种写法删掉导入后border_load就不存在了 —— 单文件版一点「边框」就NameError。
两道防线:
- 构建期:检测跨模块重名并拒绝构建(同名常量且赋值完全相同的除外,比如各模块都写
user32 = ctypes.windll.user32);带as的本地导入自动补一条别名 = 真名赋值(保留缩进);本地模块导入(import x)直接报错,要求改成from x import 名字。 - 测试期:
test_single_file.py在合并件上真跑用户路径 —— 直接调EditorWindow.add_watermark()/add_border()(把模态exec打成自动确定), 而不是只测底层函数。
教训:这类"多文件正常、单文件崩"的问题,只有在合并件上跑用户真正点的那条路径 才拦得住;只测底层函数会一直漏。
- ctypes 默认按 32 位返回,x64 下会截断 64 位句柄/指针 → 所有返回句柄的函数
与带句柄参数的函数都显式声明了
restype/argtypes(否则窗口创建会莫名失败) COLORREF是0x00BBGGRR(低位是红),不是 RGB 顺序AlphaBlend在Msimg32.dll,不在gdi32.dll;DrawTextW在user32.dll,TextOutW在gdi32.dllNOTIFYICONDATAW少了guidItem/hBalloonIcon字段会导致结构体过小 → 访问越界崩溃ScrollControlBar可能已被 Qt 回收,直接调方法会抛Internal C++ object already deleted→ 调用前判活(shiboken6.isValid)- 系统缩放下的坐标必须严格互逆(见「坐标约定」):图形用图像物理像素、
底图按 dpr 感知绘制,所以画图形前要
scale(1/dpr)
- Windows 10/11
- Python 3.10+,PySide6(唯一第三方依赖;拼接/图像统计已是纯 Python,不需要 numpy)