首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >PyQt 截图小工具

PyQt 截图小工具

原创
作者头像
繁依Fanyi
发布2025-05-04 20:59:51
发布2025-05-04 20:59:51
1.1K1
举报

前言

时不时要截个屏,圈画重点,再复制给同事或存档,工作效率却总被琐碎的操作拉低。偶然的一次灵感:何不自己动手,做一个 “一键框选截图 + 涂鸦标注 + 复制/保存” 的小工具?这样既能练练 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
复制
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
复制
# 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 确保程序退出时自动结束监听。


六、截图覆盖层 ScreenshotOverlay

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

1. 透明无边框全屏窗口

代码语言:python
复制
# 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
复制
# 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
复制
# 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
复制
# 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 实现互斥选择;
  • 初始默认为“涂鸦”(画笔)模式,切换后进入文本模式。

2. 文本模式绘制

AnnotationCanvas 中添加属性:

代码语言:python
复制
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)

重写鼠标事件:

代码语言:python
复制
    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 绘制到临时层。
  • 可以根据需要拓展“箭头工具”“矩形工具”,同理实现。

九、一键保存与复制到剪贴板

完成标注后,用户最关心的就是“保存本地”或“一键复制”。

1. 保存到文件

save 方法里:

代码语言:python
复制
    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)
  • 保存完成后关闭画布。

2. 复制到剪贴板

copy 方法里:

代码语言:python
复制
    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 打包为单个可执行:

代码语言:bash
复制
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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、动机与需求
  • 二、技术选型
  • 三、整体架构设计
  • 四、项目结构一览
  • 五、全局快捷键监听
  • 六、截图覆盖层 ScreenshotOverlay
    • 1. 透明无边框全屏窗口
    • 2. 鼠标事件处理
  • 七、注释画布 AnnotationCanvas
  • 八、文字标注与多种绘制工具
    • 1. 工具切换 UI
    • 2. 文本模式绘制
  • 九、一键保存与复制到剪贴板
    • 1. 保存到文件
    • 2. 复制到剪贴板
  • 十、打包与发布
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档