【PyQt6] 框选截图功能

news2024/11/26 12:14:25

1 简介

书接上回, 全屏截图实现起来很简单, 来点稍微复杂点的, 框选截图
原理很简单, 弄个控件实现全屏半透视, 在全屏控件上画一个选框或者再弄一个几乎全透的子控件,实现鼠标拖动,缩放,移动, 键盘wasd 微调

用一个控件实现起来会很完美, 但是逻辑全部堆砌在一起,看代码会很累, 用一个子控件分开来写,逻辑清晰, 看着也舒服点,有机会以后在组合在一起,关键实现了一个独立的橡皮框控件, 想复用也容易.

Qt6 自身有个橡皮筋控件, 尝试了下, 可以当简单的框选工具, 比如选择多个item之类的,如果功能不需要复杂,倒是可以直接使用.

原本打算扩展这个控件[QRubberBand]的,想想又没必要, 干脆就直接扩展 QWidget

2 Demo 代码

2.1 Rubber 控件

框选控件, 双击鼠标左键 发出选中的区域
在这里插入图片描述

from PyQt6.QtCore import QEvent, Qt, QPoint, QPointF, QRectF, QRect, QTimer, pyqtSignal, QLineF
from PyQt6.QtGui import QEnterEvent, QMouseEvent, QPainter, QPaintEvent, QPen, QVector2D, QKeyEvent, QCursor, QColor
from PyQt6.QtWidgets import QWidget, QApplication


class Rubber(QWidget):
    confirm_selected = pyqtSignal(QRect)

    # ===============  构造函数 ==============================
    def __init__(self, parent=None) -> None:
        super().__init__(parent)
        self.hide()  # 默认隐藏

        # self.corners = [QRectF]   # 保存4个角的rect

        self.corner_i = -1  # 保存角的索引

        self.orgin = QPoint()   # 保存鼠标按下的坐标

        self.select_rect = QRectF()  # 保存调整后的区域,即实线区
        self.txt_height = 10        # 留出高度 显示字符串

        self.isLeftButton = False       # 鼠标是否为移动状态

    # ===============  绘制事件 ==============================
    def paintEvent(self, a0: QPaintEvent | None) -> None:
        """绘制事件
        """
        painter = QPainter(self)
        painter.setClipRect(a0.rect())

        # 填充背景
        painter.fillRect(self.rect(), QColor(255, 255, 255, 1))

        pen = QPen()
        color = Qt.GlobalColor.red
        pen.setColor(color)
        pen.setWidth(0)
        painter.setPen(pen)

        # 绘制 4 个角区域 以及 选区框
        self.drawCornors(painter)

        w = int(self.select_rect.width() * self.devicePixelRatio())
        h = int(self.select_rect.height() * self.devicePixelRatio())
        txt = f'{w}x{h}'
        painter.drawText(0, self.txt_height, txt)
    # ===============  鼠标进入事件 ==============================

    def enterEvent(self, event: QEnterEvent | None) -> None:
        self.setMouseTracking(True)
        self.setFocus()
        # self.__setCursorShape(Qt.CursorShape.SizeAllCursor)

    # ===============  鼠标离开事件 ==============================
    def leaveEvent(self, a0: QEvent | None) -> None:
        self.setMouseTracking(False)
        self.clearFocus()
        # return super().leaveEvent(a0)

    # ===============  鼠标按下事件 ==============================
    def mousePressEvent(self, a0: QMouseEvent | None) -> None:
        self.orgin = a0.pos()
        self.orgin1 = a0.pos()
        # print(self.orgin1 == self.orgin, id(self.orgin1), id(self.orgin))
        if a0.buttons() == Qt.MouseButton.LeftButton:
            self.isLeftButton = True

    # ===============  鼠标移动事件 ==============================
    def mouseMoveEvent(self, a0: QMouseEvent | None) -> None:
        pos_c = a0.pos()    # 当前鼠标位置

        # 鼠标左键按下后, 可以调整 大小
        if self.isLeftButton:
            self.set_geometry_mouse(pos_c)
            return

        # 判断4个角是否包含鼠标位置, 设置相应的光标形状
        self.changeCursor(pos_c)

    # ===============  鼠标释放事件 ==============================

    def mouseReleaseEvent(self, a0: QMouseEvent | None) -> None:
        self.orgin = QPoint()
        self.corner_i = -1
        self.isLeftButton = False

    def mouseDoubleClickEvent(self, a0: QMouseEvent | None) -> None:

        self.hide()
        QTimer.singleShot(200, self.__sendSign)

        # return super().mouseDoubleClickEvent(a0)

    # ===============  按键事件 ===============
    def keyPressEvent(self, a0: QKeyEvent | None) -> None:
        match a0.key():
            # ESC 键
            case Qt.Key.Key_Escape:
                self.hide()
            case Qt.Key.Key_A:
                self.set_geometry_key('a')
            case Qt.Key.Key_S:
                self.set_geometry_key('s')
            case Qt.Key.Key_D:
                self.set_geometry_key('d')
            case Qt.Key.Key_W:
                self.set_geometry_key('w')

    # ===============  设置光标形状方法 ==============================
    def __setCursorShape(self, shape: Qt.CursorShape):
        cursor = self.cursor()
        cursor.setShape(shape)
        self.setCursor(cursor)

    def __sendSign(self):
        global_pos = self.mapToGlobal(QPoint(0, 0))
        rect = self.select_rect.adjusted(
            global_pos.x(), global_pos.y(), global_pos.x(), global_pos.y()).toRect()

        self.confirm_selected.emit(rect)

    def set_geometry_key(self, cmd: str):
        """通过按键调整 geometry
        """
        if self.isLeftButton is False:
            return

        cur_pos = QCursor.pos()
        sp = self.cursor().shape()

        if cmd == 'a' and sp != Qt.CursorShape.SizeVerCursor:
            cur_pos += QPoint(-1, 0)
        elif cmd == 's' and sp != Qt.CursorShape.SizeHorCursor:
            cur_pos += QPoint(0, 1)
        elif cmd == 'd' and sp != Qt.CursorShape.SizeVerCursor:
            cur_pos += QPoint(1, 0)
        elif cmd == 'w' and sp != Qt.CursorShape.SizeHorCursor:
            cur_pos += QPoint(0, -1)
        QCursor.setPos(cur_pos)

    def drawCornors(self, painter: QPainter):
        """ 绘制4个角 以及 选区框线

        Note:
            self.corners.append : 4个角的添加顺序为:  上左0 上右1 下右2 下左3
        """
        # 4个角 的顺序  上左 上右 下右 下左
        off = 2.0
        # self.corners.clear()
        corners = []
        geometricF = self.rect().toRectF()
        corners.append(
            QRectF(tl := geometricF.topLeft() + QPointF(0, self.txt_height), tl + QPointF(off, off)).normalized())
        corners.append(
            QRectF(tr := geometricF.topRight() + QPointF(0, self.txt_height), tr + QPointF(-off, off)).normalized())
        corners.append(
            QRectF(br := geometricF.bottomRight(), br + QPointF(-off, -off)).normalized())
        corners.append(
            QRectF(bl := geometricF.bottomLeft(), bl + QPointF(off, -off)).normalized())

        # print(self.corners)
        # print('='*30, '>')
        for rect in corners:
            # print(rect)
            painter.fillRect(rect, painter.pen().color())
        # print('='*30, '<')

        self.select_rect = QRectF(
            corners[0].center(), corners[2].center())

        painter.drawRect(self.select_rect)

    def set_geometry_mouse(self, pos: QPoint):
        """ 通过鼠标的位置调整 geometry
        Args:
            pos: 鼠标的当前位置
        """
        offset = pos - self.orgin     # 左上角坐标有变化,不需要更新
        offset1 = pos - self.orgin1   # 其他需要更新
        rect = self.geometry()
        # x1, y1, x2, y2 = self.geometry().getCoords()

        sp = self.cursor().shape()
        # 根据光标形状来实现相应方法
        match sp:
            case Qt.CursorShape.SizeFDiagCursor:    # \
                if self.corner_i == 0:
                    rect.adjust(offset.x(), offset.y(), 0, 0)

                elif self.corner_i == 2:
                    rect.adjust(0, 0, offset1.x(), offset1.y())

            case Qt.CursorShape.SizeBDiagCursor:    # /
                if self.corner_i == 1:
                    rect.adjust(0, offset.y(), offset1.x(), 0)

                elif self.corner_i == 3:
                    rect.adjust(offset.x(), 0, 0, offset1.y())

            case Qt.CursorShape.SizeHorCursor:  # -

                if self.corner_i == 1:
                    rect.adjust(0, 0, offset1.x(), 0)

                if self.corner_i == 3:
                    rect.adjust(offset.x(), 0, 0, 0)

            case Qt.CursorShape.SizeVerCursor:  # +
                if self.corner_i == 0:
                    rect.adjust(0, offset.y(), 0, 0)
                if self.corner_i == 2:
                    rect.adjust(0, 0, 0, offset1.y())

            case Qt.CursorShape.SizeAllCursor:
                rect.adjust(offset.x(), offset.y(), offset.x(), offset.y())

        self.setGeometry(rect.normalized())
        self.orgin1 = pos  # 更新数据

    def changeCursor(self, pos: QPoint):
        r"""根据鼠标的位置确定光标的形状
        SizeFDiagCursor:     /
        SizeBDiagCursor:     \\
        SizeVerCursor:       |
        SizeHorCursor:       -
        SizeAllCursor:       +
        """
        # 在 4 个 边角区域  distance < 5 ,认为相交

        cors = []
        cors.append(self.select_rect.topLeft())
        cors.append(self.select_rect.topRight())
        cors.append(self.select_rect.bottomRight())
        cors.append(self.select_rect.bottomLeft())

        idx = self.corner_i = -1

        for i, p in enumerate(cors):
            distance = QLineF(pos.toPointF(), p).length()
            if distance < 5:
                if i == 0 or i == 2:
                    self.__setCursorShape(Qt.CursorShape.SizeFDiagCursor)
                elif i == 1 or i == 3:
                    self.__setCursorShape(Qt.CursorShape.SizeBDiagCursor)
                self.corner_i = i
                return

        # 判断4 条边是否 和 鼠标相交 如果 distance < 5 ,认为相交
        # idx = -1
        for i in range(4):
            s_2d = QVector2D(cors[i])          # 起点
            e_2d = QVector2D(cors[(i+1) % 4])  # 终点

            pos_2d = QVector2D(pos)
            distance = pos_2d.distanceToLine(s_2d, (e_2d-s_2d).normalized())
            if distance < 5:
                idx = i
                break

        if idx == 0 or idx == 2:
            self.__setCursorShape(Qt.CursorShape.SizeVerCursor)  # |
        elif idx == 1 or idx == 3:
            self.__setCursorShape(Qt.CursorShape.SizeHorCursor)  # -
        else:
            self.__setCursorShape(Qt.CursorShape.SizeAllCursor)  # +

        self.corner_i = idx  # 更新属性


if __name__ == '__main__':
    app = QApplication([])
    w = QWidget()
    w.setWindowOpacity(0.5)
    w.resize(400, 300)
    c = Rubber(w)
    c.setVisible(True)
    w.show()
    app.exec()
    # sys.exit(app.exec())

2.2 全屏选区截图

再来一个全屏的半透明控件,整体效果如图:
样式和操作参考了 ABBYY Screenshot 以及 Snipaste

只是可惜了在橡皮筋控件捕捉到鼠标移动事件时, 就只能正常正选,不能反选, 应该把这一部分逻辑挪到 全屏控件里的!!
在这里插入图片描述

from PyQt6.QtCore import Qt, QRect, QPoint
from PyQt6.QtGui import QKeyEvent, QMouseEvent, QPainter, QPaintEvent, QColor, QRegion
from PyQt6.QtWidgets import QApplication, QWidget
from Rubber import Rubber


class FullScreen(QWidget):
    # ===============  构造函数 =============================================
    def __init__(self):
        super(FullScreen, self).__init__()
        # ------------- 窗口标志设置  -------------
        self.setWindowFlags(
            Qt.WindowType.WindowStaysOnTopHint   # 置顶
            | Qt.WindowType.FramelessWindowHint  # 无框
            | Qt.WindowType.Tool)         # 无任务栏图标

        self.setAttribute(
            Qt.WidgetAttribute.WA_TranslucentBackground, True)  # 设置窗口背景透明

        # ------------- 橡皮筋  -------------
        self.rubber = Rubber(self)
        self.rubber.confirm_selected.connect(self.__grab)

    # ===============  绘制事件 =============================================
    def paintEvent(self, a0: QPaintEvent | None) -> None:
        painter = QPainter(self)

        # 设置绘制区域 = 全部区域 - 选中区域
        region = QRegion(self.rect())

        slt_r = self.rubber.rect()
        rubber_p = self.rubber.pos()
        if self.rubber.isVisible():
            slt_r.adjust(
                rubber_p.x(), rubber_p.y() + self.rubber.txt_height,
                rubber_p.x(), rubber_p.y()
            )
            region -= QRegion(slt_r)

        painter.setClipRegion(region)

        painter.fillRect(self.rect(), QColor(0, 0, 127, 100))
        return super().paintEvent(a0)

    # ===============  按键事件 =============================================
    def keyPressEvent(self, a0: QKeyEvent | None) -> None:
        if a0.key() == Qt.Key.Key_Escape:   # ESC 键
            app.quit()
        return super().keyPressEvent(a0)

    # ===============  鼠标按下事件 =============================================
    def mousePressEvent(self, a0: QMouseEvent | None) -> None:
        self.orgin = a0.pos()
        self.rubber.setGeometry(
            QRect(self.orgin, QPoint(0, 0))
        )
        self.rubber.show()
        return super().mousePressEvent(a0)

    # ===============  鼠标移动事件 =============================================
    def mouseMoveEvent(self, a0: QMouseEvent | None) -> None:
        self.rubber.setGeometry(
            QRect(self.orgin, a0.pos()).normalized()
        )
        return super().mouseMoveEvent(a0)

    # ===============  抓取选区截图 =============================================
    def __grab(self, rect: QRect):
        screen = QApplication.primaryScreen()
        pixmap = screen.grabWindow(0,
                                   rect.x(),
                                   rect.y(),
                                   rect.width(),
                                   rect.height())
        pixmap.save("CWidget.png")
        self.close()
        # clipboard = QApplication.clipboard()
        # clipboard.setImage(self.pixmap.toImage())


# ===============  Main =============================================
if __name__ == '__main__':

    app = QApplication([])
    screen = FullScreen()
    screen.resize(400, 300)
    screen.show()
    screen.showFullScreen()
    app.exec()
    # sys.exit(app.exec())

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1456818.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PWM功能介绍 和配置

泰山派默认提供了3组PWM的GPIO &#xff0c; 为了检测PWM的输出&#xff0c;我们可以配合逻辑分析仪来查看效果&#xff0c;或者搭配STC8的LED灯 PWM 测试 列举所有的PWM设备&#xff1a; # 查找所有有pwm名称的文件 find / -name "pwm" # pwm4: pwmfe6e0000 edp屏幕…

VPX信号处理卡设计原理图:9-基于DSP TMS320C6678+FPGA XC7V690T的6U VPX信号处理卡 信号处理 无线电通信

一、概述 本板卡基于标准6U VPX 架构&#xff0c;为通用高性能信号处理平台&#xff0c;系我公司自主研发。板卡采用一片TI DSP TMS320C6678和一片Xilinx公司Virtex 7系列的FPGA XC7V690T-2FFG1761I作为主处理器&#xff0c;Xilinx 的Aritex XC7A200T作为辅助处理器。XC7A2…

OpenAI视频生成模型Sora的全面解析:从ViViT、扩散Transformer到NaViT、VideoPoet

前言 真没想到&#xff0c;距离视频生成上一轮的集中爆发(详见《视频生成发展史&#xff1a;从Gen2、Emu Video到PixelDance、SVD、Pika 1.0、W.A.L.T》)才过去三个月&#xff0c;没想OpenAI一出手&#xff0c;该领域又直接变天了 自打2.16日OpenAI发布sora以来(其开发团队包…

30分钟快速上手LaTex

文章目录 30 分钟快速上手 LATEX1.什么是LATEX?2.为什么学习LATEX?3.编写第一个LATEX程序4.LATEX文档的序言5.LATEX文档的标题、作者和日期信息6.LATEX文档的注释7.LATEX文档的粗体、斜体和下划线8.LATEX文档中添加图片9.LATEX中对图像进行标注、标签化和引用10.在LATEX中创建…

105.网游逆向分析与插件开发-网络通信封包解析-分析接收到的对话数据包

内容参考于&#xff1a;易道云信息技术研究院VIP课 上一个内容&#xff1a;接收数据的初步逆向分析 通过上一个内容&#xff0c;找到了数据包出现的一个很重要的位置&#xff0c;只要hook之后就能很好的得到这个数据了 然后来到明文数据的位置&#xff0c;把数据包复制出来&…

跨境电商独立站是什么?为什么要做独立站?

独立站在近两年被推上风口&#xff0c;很多人跟风涌入赛道&#xff0c;但并不知道做独立网站的根本原因是什么&#xff1f;为什么跨境电商要做独立站&#xff1f; 今天分享这篇文章&#xff0c;希望能帮助正在建站或想要建站的朋友们建立起对独立站的优劣势、未来发展空间的一…

《剑指Offer》笔记题解思路技巧优化 Java版本——新版leetcode_Part_4

《剑指Offer》笔记&题解&思路&技巧&优化_Part_4 &#x1f60d;&#x1f60d;&#x1f60d; 相知&#x1f64c;&#x1f64c;&#x1f64c; 相识&#x1f622;&#x1f622;&#x1f622; 开始刷题1. LCR 148. 验证图书取出顺序——栈的压入、弹出序列2. LCR 14…

Linux:grep进阶(11)

Linux&#xff1a;shell脚本&#xff1a;基础使用&#xff08;4&#xff09;《正则表达式-grep工具》_shell grep 全角字符串-CSDN博客https://blog.csdn.net/w14768855/article/details/132338954?ops_request_misc%257B%2522request%255Fid%2522%253A%252217083360171680022…

状压dp,HDU1074.Doing Homework

目录 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 二、解题报告 1、思路分析 2、复杂度 3、代码详解 一、题目 1、题目描述 Ignatius has just come back school from the 30th ACM/ICPC. Now he has a lot of homework to do. Every teacher giv…

【JavaEE】_HTTP响应

目录 1. 首行 2. 报头header 3.空行 4. 正文body 1. 首行 响应首行&#xff1a;版本号状态码状态码描述&#xff1b; HTTP状态码描述了这次响应的结果&#xff08;比如成功、失败&#xff0c;以及失败原因等&#xff09;&#xff1b; 1. HTTP状态码有&#xff1a; &#…

【EI会议征稿通知】第三届先进制造技术与制造系统国际学术会议(ICAMTMS 2024)

第三届先进制造技术与制造系统国际学术会议&#xff08;ICAMTMS 2024&#xff09; 2024 3rd International Conference on Advanced Manufacturing Technology and Manufacturing System 随着工业技术的发展&#xff0c;先进制造技术日益成为未来制造业发展的重大趋势和核心内…

Maven属性scope

参考&#xff1a; maven 中 scope标签的作用&#xff08;runtime、provided、test、compile 的作用&#xff09; 【Maven】属性scope依赖作用范围详解 scope为provided

VNCTF2024misc方向部分wp

文章目录 sqlsharkLearnOpenGLez_msbOnlyLocalSql sqlshark tshark -r sqlshark.pcap -Y "http" -T fields -e frame.len -e http.file_data > data.txt不太像常规的盲注&#xff0c;一次性发送两条很类似的payload&#xff0c;比常规的多了一个least在判断passw…

不懂咱就学,记不住多看几遍(二)

一、Redis分布式锁中加锁与解锁、过期如何续命 实现要点&#xff1a; 互斥性&#xff0c;同一时刻&#xff0c;只能有一个客户端持有锁。防止死锁发生&#xff0c;如果持有锁的客户端因崩溃而没有主动释放锁&#xff0c;也要保证锁可以释放并且其他客户端可以正常加锁。加锁和…

TypeScript(二):TypeScript的细节

TypeScript语法细节 联合类型&#xff08;满足其中一个即可&#xff09; 可以使用多种运算符&#xff0c;从现有的类型中构建新类型 const number|string 123 可以是这些类型中的任何值但是使用的时候需要小心 let virable: number | string 123function getData(id: numb…

金山WPS下的word,如何删除表格下面的大段空白

在csdn&#xff0c;你甚至可以学习到wps小技巧。 如题&#xff0c;这种大段空白怎么设置文本格式&#xff0c;表格格式都没用。正常的backspace删除也没用。 解决方式如下&#xff1a; 长按鼠标左键拖拽选中空白区域&#xff08;可能没有选中成功的特效没关系&#xff09;&am…

Golang - 使用CentOS 7 安装Golang环境

文章目录 操作步骤 操作步骤 为在CentOS 7上安装Go语言环境&#xff0c;可以按照以下步骤进行操作&#xff1a; 下载Go语言包&#xff1a; 从官方网站 https://golang.org/dl/ 下载适用于Linux的Go语言包。 解压缩Go语言包&#xff1a; 使用以下命令解压缩下载的Go语言包 […

洛谷 P6546 [COCI2010-2011#2] PUŽ

讲解&#xff1a; 首先还是正常输入&#xff1a; int a,b,v; cin>>a>>b>>v; 然后经入一个函数num&#xff1a; cout<<num(1.0*(v-a),(a-b))1<<endl; 之所以要乘以1.0是因为要向上取整&#xff01;而这个num函数的两个参数则是“蜗牛白天爬了多…

Asymmetric Temperature Scaling(NeurIPS 2022)论文速读

paper&#xff1a;Asymmetric Temperature Scaling Makes Larger Networks Teach Well Again official implementation&#xff1a;https://gitee.com/mindspore/models/tree/master/research/cv/ats 本文的创新点 在知识蒸馏中&#xff0c;一个奇怪的现象是大的教师模型未必…

网络原理(5)--HTTPS是如何进行加密的

&#x1f495;"Echo"&#x1f495; 作者&#xff1a;Mylvzi 文章主要内容&#xff1a;网络原理(5)–HTTPS是如何进行加密的 在网络原理(4)中介绍了HTTP协议的相关内容,HTTP协议在传输的过程中存在着安全问题,实际上现在的网络中基本不再使用HTTP,而是使用一种更加安…