【PyQt6] 框选截图功能

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())
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用PyQt可以很容易地实现鼠标框选图片的功能。下面是一个简单的示例代码: ```python import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel from PyQt5.QtGui import QPixmap, QPainter, QPen, QColor from PyQt5.QtCore import Qt, QPoint class MainWindow(QMainWindow): def __init__(self): super().__init__() self.image_path = 'image.jpg' # 图片的路径 self.selected_area = None # 选中区域的坐标 self.start_pos = None # 框选起始点的坐标 self.end_pos = None # 框选结束点的坐标 self.image_label = QLabel(self) self.pixmap = QPixmap(self.image_path) self.image_label.setPixmap(self.pixmap) def paintEvent(self, event): super().paintEvent(event) if self.selected_area: painter = QPainter(self) painter.setPen(QPen(QColor(255, 0, 0), 2, Qt.SolidLine)) painter.drawRect(self.selected_area) def mousePressEvent(self, event): if event.buttons() == Qt.LeftButton: self.start_pos = event.pos() def mouseMoveEvent(self, event): if event.buttons() == Qt.LeftButton and self.start_pos: self.end_pos = event.pos() self.update() def mouseReleaseEvent(self, event): if event.button() == Qt.LeftButton and self.start_pos and self.end_pos: self.selected_area = self.calculate_selected_area() self.start_pos = None self.end_pos = None self.update() def calculate_selected_area(self): x = min(self.start_pos.x(), self.end_pos.x()) y = min(self.start_pos.y(), self.end_pos.y()) width = abs(self.start_pos.x() - self.end_pos.x()) height = abs(self.start_pos.y() - self.end_pos.y()) return QRect(x, y, width, height) if __name__ == '__main__': app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_()) ``` 在这个示例中,我们创建了一个名为MainWindow的类,继承自QMainWindow类,并重写了几个函数。其中,paintEvent函数用于绘制选中的区域,mousePressEvent函数用于记录鼠标框选起始点的坐标,mouseMoveEvent函数用于记录鼠标框选结束点的坐标并进行更新,mouseReleaseEvent函数用于计算并记录选中区域的坐标。 通过运行这段代码,我们可以看到一张图片窗口,可以使用鼠标来进行框选操作。当用鼠标左键按下并移动时,会实时显示出框选区域的红色矩形框。当释放鼠标左键时,selected_area变量会保留框选区域的坐标信息,并将其绘制在图片上。需要注意的是,要使用正确的图片路径,以显示您自己的图片。 这就是使用PyQt实现鼠标框选图片的一个简单示例。您可以根据实际需要进行修改和扩展。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值