
时不时要截个屏,圈画重点,再复制给同事或存档,工作效率却总被琐碎的操作拉低。偶然的一次灵感:何不自己动手,做一个 “一键框选截图 + 涂鸦标注 + 复制/保存” 的小工具?这样既能练练 PyQt 的功力,又能打造一个真正好用的小利器。
工作中,我经常需要:
市场上虽有类似软件,但大多臃肿或闭源,不易二次定制。于是,我决定用 PyQt 从零打造一款 轻量、定制化 的截图标注工具。
为什么选择 PyQt?主要原因有三点:
QPainter + QPixmap 组合,可高效实现涂鸦与文字绘制;QClipboard)、快捷键(QShortcut)等友好封装;此外,Python 生态下的标准库和第三方库(如 pynput 或 keyboard)可以补充全局热键监听。
在开始写代码前,我先做了一个模块交互图,理清各部分职责和信号流转。

主要模块:
Ctrl+Shift+S),调用截图流程。QWidget 的画布,承载截图位图与用户涂鸦、文字注释操作。模块职责单一,信号槽衔接清晰,方便后续拓展。
我在项目根目录下组织代码,简要如下:
custom_screenshot/
├── main.py
├── hotkey_listener.py
├── screenshot_overlay.py
├── annotation_canvas.py
├── utils.py
└── resources/
└── style.qsskeyboard 库或 Qt 的本地快捷键方案,触发截图。要实现“任意时刻按快捷键呼出截图”,可以选两种方案:
keyboard:跨平台但需管理员权限;我最终选用 pynput 库监听全局热键,它对 Python3 支持良好。
# hotkey_listener.py
from pynput import keyboard
from PyQt5.QtCore import QObject, pyqtSignal
class HotkeyListener(QObject):
trigger = pyqtSignal()
def __init__(self, combo={keyboard.Key.ctrl_l, keyboard.Key.shift, keyboard.KeyCode(char='s')}):
super().__init__()
self.combo = combo
self.current = set()
self.listener = keyboard.Listener(on_press=self._on_press,
on_release=self._on_release)
self.listener.start()
def _on_press(self, key):
self.current.add(key)
if all(k in self.current for k in self.combo):
self.trigger.emit()
def _on_release(self, key):
if key in self.current:
self.current.remove(key)关键点:
使用
pynput.keyboard.Listener监听全局按键; 当检测到Ctrl+Shift+S同时按下时,发射trigger信号; 在 PyQt 主线程中,可用listener.daemon = True确保程序退出时自动结束监听。
最棘手的部分就是,一个全屏透明窗口,既要拦截所有鼠标事件,又要在选区绘制半透明蒙层、实线矩形框,并准确生成截图。
# screenshot_overlay.py
from PyQt5.QtWidgets import QMainWindow
from PyQt5.QtGui import QPainter, QColor, QPen, QGuiApplication, QPixmap
from PyQt5.QtCore import Qt, QRect
class ScreenshotOverlay(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
# 半透明黑色背景
self.setAttribute(Qt.WA_TranslucentBackground, True)
self.screen = QGuiApplication.primaryScreen()
self.full_pixmap = self.screen.grabWindow(0)
self.origin = None
self.current = None
self.selection = QRect()
def showEvent(self, event):
self.resize(self.screen.size())
self.showFullScreen()
def paintEvent(self, event):
painter = QPainter(self)
# 绘制截图背景
painter.drawPixmap(0, 0, self.full_pixmap)
# 蒙层
painter.fillRect(self.rect(), QColor(0, 0, 0, 100))
# 清除选区蒙层
if not self.selection.isNull():
painter.setCompositionMode(QPainter.CompositionMode_Clear)
painter.fillRect(self.selection, QColor(0,0,0,0))
# 画选区边框
painter.setCompositionMode(QPainter.CompositionMode_SourceOver)
pen = QPen(QColor(255,255,255), 2)
painter.setPen(pen)
painter.drawRect(self.selection)
painter.end()这里的要点:
WA_TranslucentBackground 让窗口支持透明;screen.grabWindow(0) 抓取当前屏幕内容,做为背景;CompositionMode_Clear 清除选区区域;# screenshot_overlay.py 续...
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.origin = event.pos()
self.selection = QRect(self.origin, self.origin)
def mouseMoveEvent(self, event):
if self.origin:
self.current = event.pos()
self.selection = QRect(self.origin, self.current).normalized()
self.update()
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton and not self.selection.isNull():
# 截取选区
cropped = self.full_pixmap.copy(self.selection)
# 进入注释画布
self.open_annotation(cropped)
self.close()origin;current,并用 normalized() 确保矩形正向;full_pixmap.copy(selection) 得到选区截图;open_annotation() 进入下一步。捕获到 cropped 的 QPixmap 后,需要打开一个新的窗口,让用户进行涂鸦和文字标注。
# annotation_canvas.py
from PyQt5.QtWidgets import QWidget, QPushButton
from PyQt5.QtGui import QPainter, QPen, QFont, QPixmap
from PyQt5.QtCore import Qt, QPoint
class AnnotationCanvas(QWidget):
def __init__(self, pixmap):
super().__init__()
self.base = pixmap
self.temp = QPixmap(pixmap.size())
self.temp.fill(Qt.transparent)
self.drawing = False
self.last_point = QPoint()
self.pen = QPen(Qt.red, 3, Qt.SolidLine)
self.init_ui()
def init_ui(self):
self.resize(self.base.size())
self.setWindowFlags(Qt.WindowStaysOnTopHint)
# 保存与复制按钮
self.btn_save = QPushButton("保存", self)
self.btn_copy = QPushButton("复制", self)
self.btn_save.move(10,10)
self.btn_copy.move(90,10)
self.btn_save.clicked.connect(self.save)
self.btn_copy.clicked.connect(self.copy)
self.show()
def paintEvent(self, event):
painter = QPainter(self)
painter.drawPixmap(0,0,self.base)
painter.drawPixmap(0,0,self.temp)
painter.end()
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.drawing = True
self.last_point = event.pos()
def mouseMoveEvent(self, event):
if self.drawing:
painter = QPainter(self.temp)
painter.setPen(self.pen)
painter.drawLine(self.last_point, event.pos())
self.last_point = event.pos()
painter.end()
self.update()
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton:
self.drawing = False核心思路:
self.base 保存原始截图;self.temp 用于实时绘制,最后合并;mouseMoveEvent 中,将线段绘制到 self.temp,再调用 update() 重绘;涂鸦之后,最常用的是在截图上添加文字说明或箭头指示。为此,我在注释画布中增加工具栏,用户可切换“画笔模式”和“文本模式”。
在 AnnotationCanvas 的 init_ui 方法里,加入工具按钮:
# annotation_canvas.py 扩展 init_ui
from PyQt5.QtWidgets import QToolButton, QAction, QButtonGroup
def init_ui(self):
# … 之前的保存/复制按钮 …
# 工具栏按钮
self.btn_pen = QToolButton(self)
self.btn_text = QToolButton(self)
self.btn_pen.setText("✏️")
self.btn_text.setText("🔤")
self.btn_pen.move(170,10)
self.btn_text.move(210,10)
# 分组互斥
self.tool_group = QButtonGroup(self)
self.tool_group.addButton(self.btn_pen, id=0)
self.tool_group.addButton(self.btn_text, id=1)
self.tool_group.setExclusive(True)
self.btn_pen.setCheckable(True)
self.btn_text.setCheckable(True)
self.btn_pen.setChecked(True)
self.tool_group.buttonClicked[int].connect(self.change_tool)QToolButton 展示图标或文字;QButtonGroup 实现互斥选择;在 AnnotationCanvas 中添加属性:
class AnnotationCanvas(QWidget):
def __init__(…):
# …
self.tool = 'pen' # 'pen' 或 'text'
self.text_to_add = "" # 准备输入的文字
def change_tool(self, tool_id):
if tool_id == 0:
self.tool = 'pen'
self.setCursor(Qt.CrossCursor)
else:
self.tool = 'text'
self.setCursor(Qt.IBeamCursor)重写鼠标事件:
def mousePressEvent(self, event):
if self.tool == 'pen':
# 原有涂鸦逻辑
if event.button() == Qt.LeftButton:
self.drawing = True
self.last_point = event.pos()
elif self.tool == 'text':
# 弹出输入对话框
from PyQt5.QtWidgets import QInputDialog
text, ok = QInputDialog.getText(self, "输入标注文字", "文字内容:")
if ok and text:
painter = QPainter(self.temp)
painter.setPen(QPen(Qt.green, 2))
painter.setFont(QFont("Arial", 14))
painter.drawText(event.pos(), text)
painter.end()
self.update()QInputDialog,输入文字后直接用 QPainter.drawText 绘制到临时层。完成标注后,用户最关心的就是“保存本地”或“一键复制”。
在 save 方法里:
def save(self):
from PyQt5.QtWidgets import QFileDialog
# 合并底图与标注
final = QPixmap(self.base.size())
painter = QPainter(final)
painter.drawPixmap(0,0,self.base)
painter.drawPixmap(0,0,self.temp)
painter.end()
# 弹出保存对话框
path, _ = QFileDialog.getSaveFileName(self, "保存截图", "screenshot.png",
"PNG Files (*.png);;JPEG Files (*.jpg)")
if path:
final.save(path)
self.close()QFileDialog.getSaveFileName;final.save(path);在 copy 方法里:
def copy(self):
from PyQt5.QtWidgets import QApplication
final = QPixmap(self.base.size())
painter = QPainter(final)
painter.drawPixmap(0,0,self.base)
painter.drawPixmap(0,0,self.temp)
painter.end()
# 复制到剪贴板
QApplication.clipboard().setPixmap(final)
self.close()QApplication.clipboard().setPixmap;代码完成后,用 PyInstaller 打包为单个可执行:
pyinstaller --noconfirm --clean \
--name CustomScreenshot \
--windowed \
--add-data "resources/style.qss;resources/" \
main.py--windowed:隐藏启动控制台;--add-data:包含样式表等资源;dist/CustomScreenshot/ 下,用户直接运行即可。原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。