admin管理员组

文章数量:1436834

PyQt 截图小工具

前言

时不时要截个屏,圈画重点,再复制给同事或存档,工作效率却总被琐碎的操作拉低。偶然的一次灵感:何不自己动手,做一个 “一键框选截图 + 涂鸦标注 + 复制/保存” 的小工具?这样既能练练 PyQt 的功力,又能打造一个真正好用的小利器。


一、动机与需求

工作中,我经常需要:

  • 用鼠标框选任意屏幕区域截图;
  • 对截图进行自由涂鸦、箭头、文本标注;
  • 一键点击即可保存到本地,或复制到系统剪贴板,方便粘贴到聊天窗口;
  • 通过快捷键(如 Ctrl+Shift+S)随时呼出截图界面,不打断当前窗口。

市场上虽有类似软件,但大多臃肿或闭源,不易二次定制。于是,我决定用 PyQt 从零打造一款 轻量、定制化 的截图标注工具。


二、技术选型

为什么选择 PyQt?主要原因有三点:

  1. 窗口透明与事件拦截:Qt 支持透明窗口、鼠标穿透和拦截,可自定义截图蒙层;
  2. 强大的绘图 APIQPainter + QPixmap 组合,可高效实现涂鸦与文字绘制;
  3. 系统交互:Qt 提供对剪贴板(QClipboard)、快捷键(QShortcut)等友好封装;

此外,Python 生态下的标准库和第三方库(如 pynputkeyboard)可以补充全局热键监听。


三、整体架构设计

在开始写代码前,我先做了一个模块交互图,理清各部分职责和信号流转。

在这里插入图片描述

主要模块:

  • HotkeyListener:全局监听截图快捷键(例如 Ctrl+Shift+S),调用截图流程。
  • ScreenshotOverlay:全屏透明窗口,拦截鼠标事件,绘制选区框并捕获所选区域。
  • AnnotationCanvas:基于 QWidget 的画布,承载截图位图与用户涂鸦、文字注释操作。
  • FileSaver:将最终图像保存到指定路径;
  • ClipboardManager:将图像复制到系统剪贴板;

模块职责单一,信号槽衔接清晰,方便后续拓展。


四、项目结构一览

我在项目根目录下组织代码,简要如下:

代码语言:python代码运行次数:0运行复制
custom_screenshot/
├── main.py
├── hotkey_listener.py
├── screenshot_overlay.py
├── annotation_canvas.py
├── utils.py
└── resources/
    └── style.qss
  • main.py:程序入口,加载 QSS 样式,初始化全局热键监听和隐藏主窗口。
  • hotkey_listener.py:可选使用 keyboard 库或 Qt 的本地快捷键方案,触发截图。
  • screenshot_overlay.py:实现透明截图蒙层与鼠标框选捕获逻辑。
  • annotation_canvas.py:实现对截图结果的涂鸦、文字、保存与复制功能。
  • utils.py:封装剪贴板操作、文件对话框、图像格式处理等公共逻辑。
  • resources/style.qss:简洁现代的 UI 样式表。

五、全局快捷键监听

要实现“任意时刻按快捷键呼出截图”,可以选两种方案:

  1. 第三方库 keyboard:跨平台但需管理员权限;
  2. Qt 本地热键:只在应用有焦点时生效,不够“全局”。

我最终选用 pynput 库监听全局热键,它对 Python3 支持良好。

代码语言:python代码运行次数:0运行复制
# 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__()
        selfbo = 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 selfbo):
            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 确保程序退出时自动结束监听。


六、截图覆盖层 ScreenshotOverlay

最棘手的部分就是,一个全屏透明窗口,既要拦截所有鼠标事件,又要在选区绘制半透明蒙层、实线矩形框,并准确生成截图。

1. 透明无边框全屏窗口

代码语言:python代码运行次数:0运行复制
# 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 清除选区区域;
  • 绘制白色矩形框,高亮边界。

2. 鼠标事件处理

代码语言:python代码运行次数:0运行复制
# 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() 进入下一步。

七、注释画布 AnnotationCanvas

捕获到 croppedQPixmap 后,需要打开一个新的窗口,让用户进行涂鸦和文字标注。

代码语言:python代码运行次数:0运行复制
# 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

核心思路:

  1. 双图层设计
  • self.base 保存原始截图;
  • self.temp 用于实时绘制,最后合并;
  • 涂鸦逻辑:在 mouseMoveEvent 中,将线段绘制到 self.temp,再调用 update() 重绘;
  • 按钮交互:用户可点击“保存”或“复制”进行后续操作。

八、文字标注与多种绘制工具

涂鸦之后,最常用的是在截图上添加文字说明箭头指示。为此,我在注释画布中增加工具栏,用户可切换“画笔模式”和“文本模式”。

1. 工具切换 UI

AnnotationCanvasinit_ui 方法里,加入工具按钮:

代码语言:python代码运行次数:0运行复制
# 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("

本文标签: PyQt 截图小工具