用pyqt原生功能实现自由屏幕截图

一、常用的截图方式

1.图片处理库PIL可以对整个屏幕进行截图,但是没有UI界面不能自由选择截图区域

from PIL import ImageGrab

img = ImageGrab.grab()
img.save('1.jpg')

2.调用QQ、微信等软件里的dll文件

QQ截图插件:CameraDll.dll【2009-05-14】没有截图工具栏按钮
Export:
CameraWindow
CameraSubArea
CameraWindowLikeSpy
调用参数:
rundll32 CameraDll.dll CameraSubArea

邮箱截图插件TXGYMailCamera.dll(X86)【2013-04-25】
Export:
CameraWindow
CameraSubArea
CameraWindowLikeSpy
调用参数:
rundll32 TXGYMailCamera.dll CameraSubArea
rundll32 TXGYMailCamera.dll CameraWindow

微信截图插件PrScrn.dll(X86)【2014-07-16】
Export:
PrScrn
调用参数:
rundll32 PrScrn.dll PrScrn

QQ浏览器截图插件PrScrnNew.dll(X86)【2018】
Export:
PrScrnNew
调用参数:
rundll32 PrScrnNew.dll RunSnap

import os
import ctypes

def func_screenshotByDLL():
    '''调用dll进行屏幕区域截图(仅适用于32位)'''
    reply = (False, "程序运行中出现异常,请联系软件开发者")
    try:
        dllFilePath = os.path.join(os.getcwd(), 'bin', 'PrScrn.dll')
        if os.path.exists(dllFilePath):
            dll = ctypes.cdll.LoadLibrary(dllFilePath)
            dll.PrScrn(0)
            reply = (True, '截图成功(PrScrn.dll)')
        else:
            reply = (False, '文件丢失:%s' % dllFilePath)
    except BaseException:
        try:
            # 如果dll加载失败,则直接运行
            os.system("Rundll32.exe %s, PrScrn" % dllFilePath)
            reply = (True, '截图成功(Rundll32.exe)')
        except BaseException as e:
            reply = (False, repr(e))
    finally:
        return reply

用ctypes调用dll仅支持32位的Python,因为这些dll文件都是32位的,用64位Python调用会报错“OSError: [WinError 193] %1 不是有效的 Win32 应用程序。”改用os.system()也是可以运行dll的,但是会弹出一个dos窗口,截图画面会看到这个dos窗口挡住其他窗口;虽然可以通过time.sleep(1.0)解决,但是点个截图还要等1秒,就是不爽。为了解决这个1秒,我就自己动手实现截图功能了,重复造轮子也要造。

二、用pyqt原生功能实现自由屏幕截图

(一)开发环境:python3.8.10(64位)、pyqt6.1.1

用pyqt原生功能实现自由屏幕截图_第1张图片
主要功能:
1.拖拽鼠标划定截图区域,截图区域可拖拽移动、可在边框8个点拖拽调整大小
2.划定截图区域时、调整截图区域大小时显示辅助放大镜
3.工具栏:
矩形、椭圆、涂鸦:可选线条颜色、粗细;
输入文字:可选线条颜色、字体;
保存:保存截图到本地;
复制:复制截图到粘贴板

(二)思路

1.获取整个屏幕的截图QPixmap,用QWidget的paintEvent事件绘制屏幕截图并全屏显示QWidget.showFullScreen()
2.将整个屏幕划分为九宫格,在屏幕截图上绘制中央截图区域矩形边框,截图区域周边8个区域绘制遮罩层,分区结果保存在ScreenArea类,此类还用于保存工具栏提供的的矩形、椭圆、涂鸦、输入文字等编辑行为的结果。
3.通过状态标志控制截图区域的拖拽和大小调整、放大镜和工具栏的显示隐藏
4.放大镜:以鼠标光标位置为中心截取屏幕截图的子集QPixmap,画上纵横十字线后,绘制到屏幕截图上
5.工具栏:使用QToolBar.move(截图区域右下角)即可,通过状态标志切换不同的编辑工具。

(三)重要概念:设备像素比

devicePixelRatio设备像素比,即电脑、手机等设备设置的字体大小的缩放倍率。
devicePixelRatio = 设备物理像素 / 设备独立像素(device-independent pixels,dips)

1.设备物理像素(physical),比如电脑的分辨率2560x1440,1920x1080,1600x900等
QWidget.rect(),QWidget.size()等返回的都是缩放倍率为1.0的原始尺寸
2.设备独立像素(logical,device-independent pixels,dips)独立于设备的用于逻辑上衡量像素的单位
在屏幕上获取的QtGui.QCursor.pos()、事件event.position()返回的都是缩放后的QPointF
QPixmap.deviceIndependentSize() * QPixmap.devicePixelRatio() = QPixmap.size()
3.例子:在分辨率为2560x1440的屏幕上,设置字体放大125%,则设备像素比为1.25
放大显示后,原本可以显示2560x1440的屏幕,仅能显示2048x1152的屏幕大小,但肉眼看见的字体变大了
在2048x1152屏幕上的QRect(60, 60, 125, 125)等于2560x1440屏幕上的(75, 75, 100, 100)
在2048x1152屏幕上的QPoint(60, 60)等于2560x1440屏幕上的(75, 75)
4.为便于屏幕绘制,ScreenArea类所有的算法都基于设备独立像素计算
保存截图结果、设置主窗口大小时才以原始尺寸计算

(四)代码

# coding=utf-8
import os
import sys
from datetime import datetime
from PyQt6 import QtCore, QtGui
from PyQt6.QtWidgets import *
from PyQt6.QtCore import QRectF, QRect, QSizeF, QPointF, QPoint, QMarginsF
from PyQt6.QtGui import QPainter, QPen, QPixmap, QColor, QAction, QIcon


class TextInputWidget(QTextEdit):
    '''在截图区域内的文本输入框'''

    def __init__(self, god=None):
        super().__init__(god)
        self.god = god
        # 设置背景透明
        # self.setStyleSheet("QTextEdit{background-color: transparent;}")
        palette = self.palette()
        palette.setBrush(QtGui.QPalette.ColorRole.Base, self.god.color_transparent)
        self.setPalette(palette)
        self.setTextColor(self.god.toolbar.curColor())
        self.setCurrentFont(self.god.toolbar.curFont())
        self._doc = self.document()  # QTextDocument
        self.textChanged.connect(self.adjustSizeByContent)
        self.adjustSizeByContent()  # 初始化调整高度为一行
        self.hide()

    def adjustSizeByContent(self, margin=30):
        '''限制宽度不超出截图区域,根据文本内容调整高度,不会出现滚动条'''
        self._doc.setTextWidth(self.viewport().width())
        margins = self.contentsMargins()
        h = int(self._doc.size().height() + margins.top() + margins.bottom())
        self.setFixedHeight(h)

    def beginNewInput(self, pos, endPointF):
        '''开始新的文本输入'''
        self._maxRect = self.god.screenArea.normalizeRectF(pos, endPointF)
        self.waitForInput()

    def waitForInput(self):
        self.setGeometry(self._maxRect.toRect())
        # self.setGeometry(self._maxRect.adjusted(0, 0, -1, 0))  # 宽度-1
        self.setFocus()
        self.show()

    def loadTextInputBy(self, action):
        '''载入修改旧的文本
        action:(type, color, font, rectf, txt)'''
        self.setTextColor(action[1])
        self.setCurrentFont(action[2])
        self._maxRect = action[3]
        self.append(action[4])
        self.god.isDrawing = True
        self.waitForInput()


class LineWidthAction(QAction):

    '''画笔粗细选择器'''

    def __init__(self, text, parent, lineWidth):
        super().__init__(text, parent)
        self._lineWidth = lineWidth
        self.refresh(QtCore.Qt.GlobalColor.red)
        self.triggered.connect(self.onTriggered)
        self.setVisible(False)

    def refresh(self, color):
        painter = self.parent().god.screenArea._painter
        dotRadius = QPointF(self._lineWidth, self._lineWidth)
        centerPoint = self.parent().iconPixmapCenter()
        pixmap = self.parent().iconPixmapCopy()
        painter.begin(pixmap)
        painter.setPen(self.parent().god.pen_transparent)
        painter.setBrush(color)
        painter.drawEllipse(QRectF(centerPoint - dotRadius, centerPoint + dotRadius))
        painter.end()
        self.setIcon(QIcon(pixmap))

    def onTriggered(self):
        self.parent()._curLineWidth = self._lineWidth


class FontAction(QAction):

    '''字体选择器'''

    def __init__(self, text, parent):
        super().__init__(text, parent)
        self.setIcon(QIcon(r"img/sys/font.png"))
        self._curFont = self.parent().god.font_textInput
        self.triggered.connect(self.onTriggered)
        self.setVisible(False)

    def onTriggered(self):
        font, ok = QFontDialog.getFont(self._curFont, self.parent(), caption='选择字体')
        if ok:
            self._curFont = font
            self.parent().god.textInputWg.setCurrentFont(font)


class ColorAction(QAction):

    '''颜色选择器'''

    def __init__(self, text, parent):
        super().__init__(text, parent)
        self._curColor = QtCore.Qt.GlobalColor.red
        self._pixmap = QPixmap(32, 32)
        self.refresh(self._curColor)
        self.triggered.connect(self.onTriggered)

    def refresh(self, color):
        self._curColor = color
        self._pixmap.fill(color)
        self.setIcon(QIcon(self._pixmap))
        self.parent()._at_line_small.refresh(color)
        self.parent()._at_line_normal.refresh(color)
        self.parent()._at_line_big.refresh(color)

    def onTriggered(self):
        col = QColorDialog.getColor(self._curColor, self.parent(), title='选择颜色')
        if col.isValid():
            self.refresh(col)
            self.parent().god.textInputWg.setTextColor(col)


class ScreenShotToolBar(QToolBar):
    '''截图区域工具条'''

    def __init__(self, god):
        super().__init__(god)
        self.god = god
        self.setToolButtonStyle(QtCore.Qt.ToolButtonStyle.ToolButtonTextUnderIcon)
        self.setStyleSheet("QToolBar {border-radius: 5px;padding: 3px;background-color: #eeeeef;}")
        self._style_normal = "QToolBar QToolButton{color: black;}"
        self._style_selected = "QToolBar QToolButton{color: #ff7300;border: 1px solid #BEDAF2;background-color: #D6E4F1}"  # 与鼠标悬停样式一样
        self._iconPixmap = QPixmap(32, 32)
        self._iconPixmap.fill(self.god.color_transparent)
        self._iconPixmapCenter = QPointF(self._iconPixmap.rect().center())
        self._curLineWidth = 3
        self._at_line_small = LineWidthAction('细', self, self._curLineWidth - 2)
        self._at_line_normal = LineWidthAction('中', self, self._curLineWidth)
        self._at_line_big = LineWidthAction('粗', self, self._curLineWidth + 2)
        self._at_font = FontAction('字体', self)
        self._at_color = ColorAction('颜色', self)
        self._at_rectangle = QAction(QIcon(r"img/sys/rectangle.png"), '矩形', self, triggered=self.beforeDrawRectangle)
        self._at_ellipse = QAction(QIcon(r"img/sys/ellipse.png"), '椭圆', self, triggered=self.beforeDrawEllipse)
        self._at_graffiti = QAction(QIcon(r"img/sys/graffiti.png"), '涂鸦', self, triggered=self.beforeDrawGraffiti)
        self._at_textInput = QAction(QIcon(r"img/sys/write.png"), '文字', self, triggered=self.beforeDrawText)
        self.addAction(self._at_line_small)
        self.addAction(self._at_line_normal)
        self.addAction(self._at_line_big)
        self.addAction(self._at_font)
        self.addAction(self._at_color)
        self.addSeparator()
        self.addAction(self._at_rectangle)
        self.addAction(self._at_ellipse)
        self.addAction(self._at_graffiti)
        self.addAction(self._at_textInput)
        self.addAction(QAction(QIcon(r"img/sys/undo.png"), '撤销', self, triggered=self.undo))
        self.addSeparator()
        self.addAction(QAction(QIcon(r"img/sys/logout.png"), '退出', self, triggered=self.god.close))
        self.addAction(QAction(QIcon(r"img/chat/download.png"), '保存', self, triggered=lambda: self.beforeSave('local')))
        self.addAction(QAction(QIcon(r"img/chat/sendImg.png"), '复制', self, triggered=lambda: self.beforeSave('clipboard')))
        self.actionTriggered.connect(self.onActionTriggered)

    def curLineWidth(self):
        return self._curLineWidth

    def curFont(self):
        return self._at_font._curFont

    def curColor(self):
        return self._at_color._curColor
        # return QColor(self._at_color._curColor.toRgb())  # 颜色的副本

    def iconPixmapCopy(self):
        return self._iconPixmap.copy()

    def iconPixmapCenter(self):
        return self._iconPixmapCenter

    def onActionTriggered(self, action):
        '''突出显示已选中的画笔粗细、编辑模式'''
        for at in [self._at_line_small, self._at_line_normal, self._at_line_big]:
            if at._lineWidth == self._curLineWidth:
                self.widgetForAction(at).setStyleSheet(self._style_selected)
            else:
                self.widgetForAction(at).setStyleSheet(self._style_normal)
        if self.god.isDrawRectangle:
            self.widgetForAction(self._at_rectangle).setStyleSheet(self._style_selected)
        else:
            self.widgetForAction(self._at_rectangle).setStyleSheet(self._style_normal)
        if self.god.isDrawEllipse:
            self.widgetForAction(self._at_ellipse).setStyleSheet(self._style_selected)
        else:
            self.widgetForAction(self._at_ellipse).setStyleSheet(self._style_normal)
        if self.god.isDrawGraffiti:
            self.widgetForAction(self._at_graffiti).setStyleSheet(self._style_selected)
        else:
            self.widgetForAction(self._at_graffiti).setStyleSheet(self._style_normal)
        if self.god.isDrawText:
            self.widgetForAction(self._at_textInput).setStyleSheet(self._style_selected)
        else:
            self.widgetForAction(self._at_textInput).setStyleSheet(self._style_normal)

    def setLineWidthActionVisible(self, flag):
        self._at_line_small.setVisible(flag)
        self._at_line_normal.setVisible(flag)
        self._at_line_big.setVisible(flag)

    def beforeDrawRectangle(self):
        self.god.clearEditFlags()
        self.god.isDrawRectangle = True
        self.setLineWidthActionVisible(True)
        self._at_font.setVisible(False)

    def beforeDrawEllipse(self):
        self.god.clearEditFlags()
        self.god.isDrawEllipse = True
        self.setLineWidthActionVisible(True)
        self._at_font.setVisible(False)

    def beforeDrawGraffiti(self):
        self.god.clearEditFlags()
        self.god.isDrawGraffiti = True
        self.setLineWidthActionVisible(True)
        self._at_font.setVisible(False)

    def beforeDrawText(self):
        self.god.clearEditFlags()
        self.god.isDrawText = True
        self.setLineWidthActionVisible(False)
        self._at_font.setVisible(True)

    def undo(self):
        '''撤销上次编辑行为'''
        if self.god.screenArea.undoEditAction():
            self.god.update()

    def beforeSave(self, target):
        # 若正在编辑文本未保存,先完成编辑
        if self.god.isDrawing and self.god.isDrawText:
            self.god.screenArea.saveTextInputAction()
        if target == 'local':
            self.god.save2Local()
        elif target == 'clipboard':
            self.god.save2Clipboard()

    def enterEvent(self, event):
        self.god.setCursor(QtCore.Qt.CursorShape.ArrowCursor)  # 工具条上显示标准箭头cursor

    def leaveEvent(self, event):
        self.god.setCursor(QtCore.Qt.CursorShape.CrossCursor)  # 十字无箭头


class ScreenArea(QtCore.QObject):
    '''屏幕区域(提供各种算法的核心类),划分为9个子区域:
    TopLeft,Top,TopRight
    Left,Center,Right
    BottomLeft,Bottom,BottomRight
    其中Center根据start、end两个QPointF确定
    '''

    def __init__(self, god):
        super().__init__()
        self.god = god
        self._pt_start = QPointF()  # 划定截图区域时鼠标左键按下的位置(topLeft)
        self._pt_end = QPointF()  # 划定截图区域时鼠标左键松开的位置(bottomRight)
        self._rt_toolbar = QRectF()  # 工具条的矩形
        self._actions = []  # 在截图区域上的所有编辑行为(矩形、椭圆、涂鸦、文本输入等)
        self._pt_startEdit = QPointF()  # 在截图区域上绘制矩形、椭圆时鼠标左键按下的位置(topLeft)
        self._pt_endEdit = QPointF()  # 在截图区域上绘制矩形、椭圆时鼠标左键松开的位置(bottomRight)
        self._pointfs = []  # 涂鸦经过的所有点
        self._painter = QPainter()  # 独立于ScreenShotWidget之外的画家类
        self._textOption = QtGui.QTextOption(QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignTop)
        self._textOption.setWrapMode(QtGui.QTextOption.WrapMode.WrapAnywhere)  # 文本在矩形内自动换行
        # self._textOption.setWrapMode(QtGui.QTextOption.WrapMode.WrapAtWordBoundaryOrAnywhere)
        self.captureScreen()

    def captureScreen(self):
        '''抓取整个屏幕的截图'''
        # screen = QtGui.QGuiApplication.primaryScreen()
        self._screenPixmap = QApplication.primaryScreen().grabWindow()
        self._pixelRatio = self._screenPixmap.devicePixelRatio()  # 设备像素比
        self._rt_screen = self.screenLogicalRectF()
        self.remakeNightArea()

    def normalizeRectF(self, topLeftPoint, bottomRightPoint):
        '''根据起止点生成宽高非负数的QRectF,通常用于bottomRightPoint比topLeftPoint更左更上的情况
        入参可以是QPoint或QPointF'''
        rectf = QRectF(topLeftPoint, bottomRightPoint)
        x = rectf.x()
        y = rectf.y()
        w = rectf.width()
        h = rectf.height()
        if w < 0:  # bottomRightPoint在topLeftPoint左侧时,topLeftPoint往左移动
            x = x + w
            w = -w
        if h < 0:  # bottomRightPoint在topLeftPoint上侧时,topLeftPoint往上移动
            y = y + h
            h = -h
        return QRectF(x, y, w, h)

    def physicalRectF(self, rectf):
        '''计算划定的截图区域的(缩放倍率1.0的)原始矩形(会变大)
        rectf:划定的截图区域的矩形。可为QRect或QRectF'''
        return QRectF(rectf.x() * self._pixelRatio, rectf.y() * self._pixelRatio,
                      rectf.width() * self._pixelRatio, rectf.height() * self._pixelRatio)

    def logicalRectF(self, physicalRectF):
        '''根据原始矩形计算缩放后的矩形(会变小)
        physicalRectF:缩放倍率1.0的原始矩形。可为QRect或QRectF'''
        return QRectF(physicalRectF.x() / self._pixelRatio, physicalRectF.y() / self._pixelRatio,
                      physicalRectF.width() / self._pixelRatio, physicalRectF.height() / self._pixelRatio)

    def physicalPixmap(self, rectf, editAction=False):
        '''根据指定区域获取其原始大小的(缩放倍率1.0的)QPixmap
        rectf:指定区域。可为QRect或QRectF
        editAction:是否带上编辑结果'''
        if editAction:
            canvasPixmap = self.screenPhysicalPixmapCopy()
            self._painter.begin(canvasPixmap)
            self.paintEachEditAction(self._painter, textBorder=False)
            self._painter.end()
            return canvasPixmap.copy(self.physicalRectF(rectf).toRect())
        else:
            return self._screenPixmap.copy(self.physicalRectF(rectf).toRect())

    def screenPhysicalRectF(self):
        return QRectF(self._screenPixmap.rect())

    def screenLogicalRectF(self):
        return QRectF(QPointF(0, 0), self.screenLogicalSizeF())  # 即当前屏幕显示的大小

    def screenPhysicalSizeF(self):
        return QSizeF(self._screenPixmap.size())

    def screenLogicalSizeF(self):
        return QSizeF(self._screenPixmap.width() / self._pixelRatio, self._screenPixmap.height() / self._pixelRatio)

    def screenPhysicalPixmapCopy(self):
        return self._screenPixmap.copy()

    def screenLogicalPixmapCopy(self):
        return self._screenPixmap.scaled(self.screenLogicalSizeF().toSize())

    def centerPhysicalRectF(self):
        return self.physicalRectF(self._rt_center)

    def centerLogicalRectF(self):
        '''根据屏幕上的start、end两个QPointF确定'''
        return self._rt_center

    def centerPhysicalPixmap(self, editAction=True):
        '''截图区域的QPixmap
        editAction:是否带上编辑结果'''
        return self.physicalPixmap(self._rt_center + QMarginsF(-1, -1, 1, 1), editAction=editAction)

    def centerTopMid(self):
        return self._pt_centerTopMid

    def centerBottomMid(self):
        return self._pt_centerBottomMid

    def centerLeftMid(self):
        return self._pt_centerLeftMid

    def centerRightMid(self):
        return self._pt_centerRightMid

    def setStartPoint(self, pointf, remake=False):
        self._pt_start = pointf
        if remake:
            self.remakeNightArea()

    def setEndPoint(self, pointf, remake=False):
        self._pt_end = pointf
        if remake:
            self.remakeNightArea()

    def setCenterArea(self, start, end):
        self._pt_start = start
        self._pt_end = end
        self.remakeNightArea()

    def remakeNightArea(self):
        '''重新划分九宫格区域。根据中央截图区域计算出来的其他8个区域、截图区域四个边框中点坐标等都是logical的'''
        self._rt_center = self.normalizeRectF(self._pt_start, self._pt_end)
        # 中央区域上下左右边框的中点,用于调整大小
        self._pt_centerTopMid = (self._rt_center.topLeft() + self._rt_center.topRight()) / 2
        self._pt_centerBottomMid = (self._rt_center.bottomLeft() + self._rt_center.bottomRight()) / 2
        self._pt_centerLeftMid = (self._rt_center.topLeft() + self._rt_center.bottomLeft()) / 2
        self._pt_centerRightMid = (self._rt_center.topRight() + self._rt_center.bottomRight()) / 2
        # 以截图区域左上、上中、右上、左中、右中、左下、下中、右下为中心的正方形区域,用于调整大小
        self._square_topLeft = self.squareAreaByCenter(self._rt_center.topLeft())
        self._square_topRight = self.squareAreaByCenter(self._rt_center.topRight())
        self._square_bottomLeft = self.squareAreaByCenter(self._rt_center.bottomLeft())
        self._square_bottomRight = self.squareAreaByCenter(self._rt_center.bottomRight())
        self._square_topMid = self.squareAreaByCenter(self._pt_centerTopMid)
        self._square_bottomMid = self.squareAreaByCenter(self._pt_centerBottomMid)
        self._square_leftMid = self.squareAreaByCenter(self._pt_centerLeftMid)
        self._square_rightMid = self.squareAreaByCenter(self._pt_centerRightMid)
        # 除中央截图区域外的8个区域
        self._rt_topLeft = QRectF(self._rt_screen.topLeft(), self._rt_center.topLeft())
        self._rt_top = QRectF(QPointF(self._rt_center.topLeft().x(), 0), self._rt_center.topRight())
        self._rt_topRight = QRectF(QPointF(self._rt_center.topRight().x(), 0), QPointF(self._rt_screen.width(), self._rt_center.topRight().y()))
        self._rt_left = QRectF(QPointF(0, self._rt_center.topLeft().y()), self._rt_center.bottomLeft())
        self._rt_right = QRectF(self._rt_center.topRight(), QPointF(self._rt_screen.width(), self._rt_center.bottomRight().y()))
        self._rt_bottomLeft = QRectF(QPointF(0, self._rt_center.bottomLeft().y()), QPointF(self._rt_center.bottomLeft().x(), self._rt_screen.height()))
        self._rt_bottom = QRectF(self._rt_center.bottomLeft(), QPointF(self._rt_center.bottomRight().x(), self._rt_screen.height()))
        self._rt_bottomRight = QRectF(self._rt_center.bottomRight(), self._rt_screen.bottomRight())

    def squareAreaByCenter(self, pointf):
        '''以QPointF为中心的正方形QRectF'''
        rectf = QRectF(0, 0, 15, 15)
        rectf.moveCenter(pointf)
        return rectf

    def aroundAreaIn8Direction(self):
        '''中央区域周边的8个方向的区域(无交集)'''
        return [self._rt_topLeft, self._rt_top, self._rt_topRight,
                self._rt_left, self._rt_right,
                self._rt_bottomLeft, self._rt_bottom, self._rt_bottomRight]

    def aroundAreaIn4Direction(self):
        '''中央区域周边的4个方向的区域(有交集)
        上区域(左上、上、右上):0, 0, maxX, topRight.y
        下区域(左下、下、右下):0, bottomLeft.y, maxX, maxY-bottomLeft.y
        左区域(左上、左、左下):0, 0, bottomLeft.x, maxY
        右区域(右上、右、右下):topRight.x, 0, maxX - topRight.x, maxY'''
        screenSizeF = self.screenLogicalSizeF()
        pt_topRight = self._rt_center.topRight()
        pt_bottomLeft = self._rt_center.bottomLeft()
        return [QRectF(0, 0, screenSizeF.width(), pt_topRight.y()),
                QRectF(0, pt_bottomLeft.y(), screenSizeF.width(), screenSizeF.height() - pt_bottomLeft.y()),
                QRectF(0, 0, pt_bottomLeft.x(), screenSizeF.height()),
                QRectF(pt_topRight.x(), 0, screenSizeF.width() - pt_topRight.x(), screenSizeF.height())]

    def aroundAreaWithoutIntersection(self):
        '''中央区域周边的4个方向的区域(无交集)
        上区域(左上、上、右上):0, 0, maxX, topRight.y
        下区域(左下、下、右下):0, bottomLeft.y, maxX, maxY-bottomLeft.y
        左区域(左):0, topRight.y, bottomLeft.x-1, center.height
        右区域(右):topRight.x+1, topRight.y, maxX - topRight.x, center.height'''
        screenSizeF = self.screenLogicalSizeF()
        pt_topRight = self._rt_center.topRight()
        pt_bottomLeft = self._rt_center.bottomLeft()
        centerHeight = pt_bottomLeft.y() - pt_topRight.y()
        return [QRectF(0, 0, screenSizeF.width(), pt_topRight.y()),
                QRectF(0, pt_bottomLeft.y(), screenSizeF.width(), screenSizeF.height() - pt_bottomLeft.y()),
                QRectF(0, pt_topRight.y(), pt_bottomLeft.x() - 1, centerHeight),
                QRectF(pt_topRight.x() + 1, pt_topRight.y(), screenSizeF.width() - pt_topRight.x(), centerHeight)]

    def setBeginDragPoint(self, pointf):
        '''计算开始拖拽位置距离截图区域左上角的向量'''
        self._drag_vector = pointf - self._rt_center.topLeft()

    def getNewPosAfterDrag(self, pointf):
        '''计算拖拽后截图区域左上角的新位置'''
        return pointf - self._drag_vector

    def moveCenterAreaTo(self, pointf):
        '''限制拖拽不能超出屏幕范围'''
        self._rt_center.moveTo(self.getNewPosAfterDrag(pointf))
        startPointF = self._rt_center.topLeft()
        if startPointF.x() < 0:
            self._rt_center.moveTo(0, startPointF.y())
            startPointF = self._rt_center.topLeft()
        if startPointF.y() < 0:
            self._rt_center.moveTo(startPointF.x(), 0)
        screenSizeF = self.screenLogicalSizeF()
        endPointF = self._rt_center.bottomRight()
        if endPointF.x() > screenSizeF.width():
            self._rt_center.moveBottomRight(QPointF(screenSizeF.width(), endPointF.y()))
            endPointF = self._rt_center.bottomRight()
        if endPointF.y() > screenSizeF.height():
            self._rt_center.moveBottomRight(QPointF(endPointF.x(), screenSizeF.height()))
        self.setCenterArea(self._rt_center.topLeft(), self._rt_center.bottomRight())

    def setBeginAdjustPoint(self, pointf):
        '''判断开始调整截图区域大小时鼠标左键在哪个区(不可能是中央区域),用于判断调整大小的意图方向'''
        self._mousePos = self.getMousePosBy(pointf)

    def getMousePosBy(self, pointf):
        if self._square_topLeft.contains(pointf):
            return 'TL'
        elif self._square_topMid.contains(pointf):
            return 'T'
        elif self._square_topRight.contains(pointf):
            return 'TR'
        elif self._square_leftMid.contains(pointf):
            return 'L'
        elif self._rt_center.contains(pointf):
            return 'CENTER'
        elif self._square_rightMid.contains(pointf):
            return 'R'
        elif self._square_bottomLeft.contains(pointf):
            return 'BL'
        elif self._square_bottomMid.contains(pointf):
            return 'B'
        elif self._square_bottomRight.contains(pointf):
            return 'BR'
        else:
            return 'ERROR'

    def adjustCenterAreaBy(self, pointf):
        '''根据开始调整截图区域大小时鼠标左键在哪个区(不可能是中央区域),判断调整大小的意图方向,判定新的开始、结束位置'''
        startPointF = self._rt_center.topLeft()
        endPointF = self._rt_center.bottomRight()
        if self._mousePos == 'TL':
            startPointF = pointf
        elif self._mousePos == 'T':
            startPointF = QPointF(startPointF.x(), pointf.y())
        elif self._mousePos == 'TR':
            startPointF = QPointF(startPointF.x(), pointf.y())
            endPointF = QPointF(pointf.x(), endPointF.y())
        elif self._mousePos == 'L':
            startPointF = QPointF(pointf.x(), startPointF.y())
        elif self._mousePos == 'R':
            endPointF = QPointF(pointf.x(), endPointF.y())
        elif self._mousePos == 'BL':
            startPointF = QPointF(pointf.x(), startPointF.y())
            endPointF = QPointF(endPointF.x(), pointf.y())
        elif self._mousePos == 'B':
            endPointF = QPointF(endPointF.x(), pointf.y())
        elif self._mousePos == 'BR':
            endPointF = pointf
        else:  # 'ERROR'
            return
        newRectF = self.normalizeRectF(startPointF, endPointF)
        self.setCenterArea(newRectF.topLeft(), newRectF.bottomRight())

    def getMouseShapeBy(self, pointf):
        '''根据鼠标位置返回对应的鼠标样式'''
        if self._rt_center.contains(pointf):
            if self.god.isDrawRectangle or self.god.isDrawEllipse:
                return QtCore.Qt.CursorShape.ArrowCursor
            elif self.god.isDrawGraffiti:
                return QtCore.Qt.CursorShape.PointingHandCursor  # 超链接上的手势
            elif self.god.isDrawText:
                return QtCore.Qt.CursorShape.IBeamCursor  # 工字
            else:
                return QtCore.Qt.CursorShape.SizeAllCursor  # 十字有箭头
                # return QtCore.Qt.CursorShape.OpenHandCursor  # 打开的手,表示可拖拽
        elif self._square_topLeft.contains(pointf) or self._square_bottomRight.contains(pointf):
            return QtCore.Qt.CursorShape.SizeFDiagCursor  # ↖↘
        elif self._square_topMid.contains(pointf) or self._square_bottomMid.contains(pointf):
            return QtCore.Qt.CursorShape.SizeVerCursor  # ↑↓
        elif self._square_topRight.contains(pointf) or self._square_bottomLeft.contains(pointf):
            return QtCore.Qt.CursorShape.SizeBDiagCursor  # ↙↗
        elif self._square_leftMid.contains(pointf) or self._square_rightMid.contains(pointf):
            return QtCore.Qt.CursorShape.SizeHorCursor  # ←→
        else:
            return QtCore.Qt.CursorShape.CrossCursor  # 十字无箭头

    def isMousePosInCenterRectF(self, pointf):
        return self._rt_center.contains(pointf)

    def paintMagnifyingGlassPixmap(self, pos, glassSize):
        '''绘制放大镜内的图像(含纵横十字线)
        pos:鼠标光标位置
        glassSize:放大镜边框大小'''
        pixmapRect = QRect(0, 0, 20, 20)  # 以鼠标光标为中心的正方形区域,最好是偶数
        pixmapRect.moveCenter(pos)
        glassPixmap = self.physicalPixmap(pixmapRect)
        glassPixmap.setDevicePixelRatio(1.0)
        glassPixmap = glassPixmap.scaled(glassSize, glassSize, QtCore.Qt.AspectRatioMode.KeepAspectRatio)
        # 在放大后的QPixmap上画纵横十字线
        self._painter.begin(glassPixmap)
        halfWidth = glassPixmap.width() / 2
        halfHeight = glassPixmap.height() / 2
        self._painter.setPen(self.god.pen_SolidLine_lightBlue)
        self._painter.drawLine(QPointF(0, halfHeight), QPointF(glassPixmap.width(), halfHeight))
        self._painter.drawLine(QPointF(halfWidth, 0), QPointF(halfWidth, glassPixmap.height()))
        self._painter.end()
        return glassPixmap

    def paintEachEditAction(self, painter, textBorder=True):
        '''绘制所有已保存的编辑行为。编辑行为超出截图区域也无所谓,保存图像时只截取截图区域内
        textBorder:是否绘制文本边框'''
        for action in self.getEditActions():
            if action[0] == 'rectangle':  # (type, color, lineWidth, startPoint, endPoint)
                self.paintRectangle(painter, action[1], action[2], action[3], action[4])
            elif action[0] == 'ellipse':  # (type, color, lineWidth, startPoint, endPoint)
                self.paintEllipse(painter, action[1], action[2], action[3], action[4])
            elif action[0] == 'graffiti':  # (type, color, lineWidth, points)
                self.paintGraffiti(painter, action[1], action[2], action[3])
            elif action[0] == 'text':  # (type, color, font, rectf, txt)
                self.paintTextInput(painter, action[1], action[2], action[3], action[4], textBorder=textBorder)

    def paintRectangle(self, painter, color, lineWidth, startPoint=None, endPoint=None):
        if not startPoint:
            startPoint = self._pt_startEdit
        if not endPoint:
            endPoint = self._pt_endEdit
        qrectf = self.normalizeRectF(startPoint, endPoint)
        if qrectf.isValid():
            pen = QPen(color)
            pen.setWidth(lineWidth)
            painter.setPen(pen)
            painter.setBrush(self.god.color_transparent)
            painter.drawRect(qrectf)

    def paintEllipse(self, painter, color, lineWidth, startPoint=None, endPoint=None):
        if not startPoint:
            startPoint = self._pt_startEdit
        if not endPoint:
            endPoint = self._pt_endEdit
        qrectf = self.normalizeRectF(startPoint, endPoint)
        if qrectf.isValid():
            pen = QPen(color)
            pen.setWidth(lineWidth)
            painter.setPen(pen)
            painter.setBrush(self.god.color_transparent)
            painter.drawEllipse(qrectf)

    def paintGraffiti(self, painter, color, lineWidth, pointfs=None):
        if not pointfs:
            pointfs = self.getGraffitiPointFs()
        pen = QPen(color)
        pen.setWidth(lineWidth)
        painter.setPen(pen)
        total = len(pointfs)
        if total == 0:
            return
        elif total == 1:
            painter.drawPoint(pointfs[0])
        else:
            previousPoint = pointfs[0]
            for i in range(1, total):
                nextPoint = pointfs[i]
                painter.drawLine(previousPoint, nextPoint)
                previousPoint = nextPoint

    def paintTextInput(self, painter, color, font, rectf, txt, textBorder=True):
        painter.setPen(color)
        painter.setFont(font)
        painter.drawText(rectf, txt, self._textOption)
        if textBorder:
            painter.setPen(QtCore.Qt.PenStyle.DotLine)  # 点线
            painter.setBrush(self.god.color_transparent)
            painter.drawRect(rectf)

    def getEditActions(self):
        return self._actions.copy()

    def takeTextInputActionAt(self, pointf):
        '''根据鼠标位置查找已保存的文本输入结果,找到后取出'''
        for i in range(len(self._actions)):
            action = self._actions[i]
            if action[0] == 'text' and action[3].contains(pointf):
                return self._actions.pop(i)
        return None

    def undoEditAction(self):
        reply = False
        if self._actions:
            reply = self._actions.pop()
            if not self._actions:  # 所有编辑行为都被撤销后退出编辑模式
                self.god.exitEditMode()
        else:
            self.god.exitEditMode()
        return reply

    def clearEditActions(self):
        self._actions.clear()

    def setBeginEditPoint(self, pointf):
        '''在截图区域上绘制矩形、椭圆时鼠标左键按下的位置(topLeft)'''
        self._pt_startEdit = pointf
        self.god.isDrawing = True

    def setEndEditPoint(self, pointf):
        '''在截图区域上绘制矩形、椭圆时鼠标左键松开的位置(bottomRight)'''
        self._pt_endEdit = pointf

    def saveRectangleAction(self):
        rectf = self.normalizeRectF(self._pt_startEdit, self._pt_endEdit)
        self._actions.append(('rectangle', self.god.toolbar.curColor(), self.god.toolbar.curLineWidth(),
                              rectf.topLeft(), rectf.bottomRight()))
        self._pt_startEdit = QPointF()
        self._pt_endEdit = QPointF()
        self.god.isDrawing = False

    def saveEllipseleAction(self):
        rectf = self.normalizeRectF(self._pt_startEdit, self._pt_endEdit)
        self._actions.append(('ellipse', self.god.toolbar.curColor(), self.god.toolbar.curLineWidth(),
                              rectf.topLeft(), rectf.bottomRight()))
        self._pt_startEdit = QPointF()
        self._pt_endEdit = QPointF()
        self.god.isDrawing = False

    def saveGraffitiPointF(self, pointf, first=False):
        self._pointfs.append(pointf)
        if first:
            self.god.isDrawing = True

    def getGraffitiPointFs(self):
        return self._pointfs.copy()

    def saveGraffitiAction(self):
        if self._pointfs:
            self._actions.append(('graffiti', self.god.toolbar.curColor(), self.god.toolbar.curLineWidth(), self._pointfs.copy()))
            self._pointfs.clear()
            self.god.isDrawing = False

    def setBeginInputTextPoint(self, pointf):
        '''在截图区域上输入文字时鼠标左键按下的位置(topLeft)'''
        self.god.isDrawing = True
        self.god.textInputWg.beginNewInput(pointf, self._pt_end)

    def saveTextInputAction(self):
        txt = self.god.textInputWg.toPlainText()
        if txt:
            rectf = self.god.textInputWg._maxRect  # 取最大矩形的topLeft
            rectf.setSize(QRectF(self.god.textInputWg.rect()).size())  # 取实际矩形的宽高
            self._actions.append(('text', self.god.toolbar.curColor(), self.god.toolbar.curFont(),
                                  rectf, txt))
            self.god.textInputWg.clear()
        self.god.textInputWg.hide()  # 不管保存成功与否都取消编辑
        self.god.isDrawing = False

    def saveNightAreaImg(self):
        '''将九宫格区域保存为本地图片,仅用于开发测试'''
        screenPixmap = self.screenPhysicalPixmapCopy()
        self._painter.begin(screenPixmap)
        self._painter.setPen(self.pen_SolidLine_lightBlue)
        self._painter.setFont(self.god.font_normal)
        self._painter.drawRect(self._rt_center)
        for area in self.aroundAreaIn8Direction():
            self._painter.drawRect(area)
        for pointf in [self._rt_center.topLeft(), self._rt_center.topRight(),
                       self._rt_center.bottomLeft(), self._rt_center.bottomRight(),
                       self._pt_centerTopMid, self._pt_centerBottomMid,
                       self._pt_centerLeftMid, self._pt_centerRightMid]:
            self._painter.drawText(pointf + QPointF(5, -5), '(%s, %s)' % (pointf.x(), pointf.y()))
        self._painter.end()
        screenPixmap.save('1.jpg', quality=100)
        self.centerPhysicalPixmap().save('2.jpg', quality=100)


class ScreenShotWidget(QWidget):

    fileType_all = '所有文件 (*);;Excel文件 (*.xls *.xlsx);;图片文件 (*.jpg *.jpeg *.gif *.png *.bmp)'
    fileType_img = '图片文件 (*.jpg *.jpeg *.gif *.png *.bmp)'
    dir_lastAccess = os.getcwd()  # 最后访问目录

    def __init__(self):
        super().__init__()
        self.setMouseTracking(True)
        self.setWindowFlags(QtCore.Qt.WindowType.FramelessWindowHint | QtCore.Qt.WindowType.WindowStaysOnTopHint)
        self.initPainterTool()
        self.initFunctionalFlag()
        self.screenArea = ScreenArea(self)
        self.toolbar = ScreenShotToolBar(self)
        self.textInputWg = TextInputWidget(self)
        # 设置 screenPixmap 为窗口背景
        # palette = QtGui.QPalette()
        # palette.setBrush(QtGui.QPalette.ColorRole.Window, QtGui.QBrush(self.screenArea.screenPhysicalPixmapCopy()))
        # self.setPalette(palette)

    def start(self):
        self.screenArea.captureScreen()
        self.setGeometry(self.screenArea.screenPhysicalRectF().toRect())
        self.clearScreenShotArea()
        self.showFullScreen()

    def initPainterTool(self):
        self.painter = QPainter()
        self.color_transparent = QtCore.Qt.GlobalColor.transparent
        self.color_black = QColor(0, 0, 0, 64)  # 黑色背景
        self.color_lightBlue = QColor(30, 120, 255)  # 浅蓝色。深蓝色QtCore.Qt.GlobalColor.blue
        self.font_normal = QtGui.QFont('Times New Roman', 11, QtGui.QFont.Weight.Normal)
        self.font_textInput = QtGui.QFont('微软雅黑', 16, QtGui.QFont.Weight.Normal)  # 工具条文字工具默认字体
        self.pen_transparent = QPen(QtCore.Qt.PenStyle.NoPen)  # 没有笔迹,画不出线条
        self.pen_white = QPen(QtCore.Qt.GlobalColor.white)
        self.pen_SolidLine_lightBlue = QPen(self.color_lightBlue)  # 实线,浅蓝色
        self.pen_SolidLine_lightBlue.setStyle(QtCore.Qt.PenStyle.DashLine)  # 实线SolidLine,虚线DashLine,点线DotLine
        self.pen_SolidLine_lightBlue.setWidthF(0)  # 0表示线宽为1
        self.pen_DashLine_lightBlue = QPen(self.color_lightBlue)  # 虚线,浅蓝色
        self.pen_DashLine_lightBlue.setStyle(QtCore.Qt.PenStyle.DashLine)

    def initFunctionalFlag(self):
        self.hasScreenShot = False  # 是否已通过拖动鼠标左键划定截图区域
        self.isCapturing = False  # 正在拖动鼠标左键选定截图区域时
        self.isMoving = False  # 在截图区域内拖动时
        self.isAdjusting = False  # 在截图区域的边框按住鼠标左键调整大小时
        self.isDrawing = False  # 是否已在截图区域内开始绘制
        self.isDrawRectangle = False  # 正在截图区域内画矩形
        self.isDrawEllipse = False  # 正在截图区域内画椭圆
        self.isDrawGraffiti = False  # 正在截图区域内进行涂鸦
        self.isDrawText = False  # 正在截图区域内画文字
        self.setCursor(QtCore.Qt.CursorShape.CrossCursor)  # 设置鼠标样式 十字

    def paintEvent(self, event):
        centerRectF = self.screenArea.centerLogicalRectF()
        screenSizeF = self.screenArea.screenLogicalSizeF()
        canvasPixmap = self.screenArea.screenPhysicalPixmapCopy()
        # canvasPixmap = QPixmap(screenSizeF.toSize())
        # canvasPixmap.fill(self.color_transparent)
        # 在屏幕截图的副本上绘制已选定的截图区域
        self.painter.begin(canvasPixmap)
        if self.hasScreenShot:
            self.paintCenterArea(centerRectF)  # 绘制中央截图区域
            self.paintMaskLayer(screenSizeF, fullScreen=False)  # 绘制截图区域的周边区域遮罩层
        else:
            self.paintMaskLayer(screenSizeF)
        self.paintMagnifyingGlass(screenSizeF)  # 在鼠标光标右下角显示放大镜
        self.paintToolbar(centerRectF, screenSizeF)  # 在截图区域右下角显示工具条
        self.paintEditActions()  # 在截图区域绘制编辑行为结果
        self.painter.end()
        # 把画好的绘制结果显示到窗口上
        self.painter.begin(self)
        self.painter.drawPixmap(0, 0, canvasPixmap)  # 从坐标(0, 0)开始绘制
        self.painter.end()

    def paintCenterArea(self, centerRectF):
        '''绘制已选定的截图区域'''
        self.painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)  # 反走样
        # 1.绘制矩形线框
        self.painter.setPen(self.pen_DashLine_lightBlue)
        self.painter.drawRect(centerRectF)
        # 2.绘制矩形线框4个端点和4条边框的中间点
        if centerRectF.width() >= 100 and centerRectF.height() >= 100:
            points = [  # 点坐标
                centerRectF.topLeft(), centerRectF.topRight(), centerRectF.bottomLeft(), centerRectF.bottomRight(),
                self.screenArea.centerLeftMid(), self.screenArea.centerRightMid(),
                self.screenArea.centerTopMid(), self.screenArea.centerBottomMid()
            ]
            blueDotRadius = QPointF(2, 2)  # 椭圆蓝点
            self.painter.setBrush(self.color_lightBlue)
            for point in points:
                self.painter.drawEllipse(QRectF(point - blueDotRadius, point + blueDotRadius))
        # 3.在截图区域左上角显示截图区域宽高
        if centerRectF.topLeft().y() > 20:
            labelPos = centerRectF.topLeft() + QPointF(5, -5)
        else:  # 拖拽截图区域到贴近屏幕上边缘时“宽x高”移动到截图区域左上角的下侧
            labelPos = centerRectF.topLeft() + QPointF(5, 15)
        centerPhysicalRect = self.screenArea.centerPhysicalRectF().toRect()
        self.painter.setPen(self.pen_white)
        self.painter.setFont(self.font_normal)
        self.painter.drawText(labelPos, '%s x %s' % (centerPhysicalRect.width(), centerPhysicalRect.height()))
        # 4.在屏幕左上角预览截图结果
        # self.painter.drawPixmap(0, 0, self.screenArea.centerPhysicalPixmap())  # 从坐标(0, 0)开始绘制

    def paintMaskLayer(self, screenSizeF, fullScreen=True):
        if fullScreen:  # 全屏遮罩层
            maskPixmap = QPixmap(screenSizeF.toSize())
            maskPixmap.fill(self.color_black)
            self.painter.drawPixmap(0, 0, maskPixmap)
        else:  # 绘制截图区域的周边区域遮罩层,以凸显截图区域
            # 方法一:截图区域以外的8个方向区域
            # for area in self.screenArea.aroundAreaIn8Direction():
            #     area = area.normalized()
            #     maskPixmap = QPixmap(area.size().toSize())  # 由于float转int的精度问题,可能会存在黑线条缝隙
            #     maskPixmap.fill(self.color_black)
            #     self.painter.drawPixmap(area.topLeft(), maskPixmap)
            # 方法二:截图区域以外的上下左右区域(有交集,交集部分颜色加深,有明显的纵横效果)
            # for area in self.screenArea.aroundAreaIn4Direction():
            #     maskPixmap = QPixmap(area.size().toSize())
            #     maskPixmap.fill(self.color_black)
            #     self.painter.drawPixmap(area.topLeft(), maskPixmap)
            # 方法三:截图区域以外的上下左右区域(无交集)
            for area in self.screenArea.aroundAreaWithoutIntersection():
                maskPixmap = QPixmap(area.size().toSize())
                maskPixmap.fill(self.color_black)
                self.painter.drawPixmap(area.topLeft(), maskPixmap)

    def paintMagnifyingGlass(self, screenSizeF, glassSize=150, offset=30, labelHeight=30):
        '''未划定截图区域模式时、正在划定截取区域时、调整截取区域大小时在鼠标光标右下角显示放大镜
        glassSize:放大镜正方形边长
        offset:放大镜任意一个端点距离鼠标光标位置的最近距离
        labelHeight:pos和rgb两行文字的高度'''
        if self.hasScreenShot and (not self.isCapturing) and (not self.isAdjusting):
            return
        pos = QtGui.QCursor.pos()
        glassPixmap = self.screenArea.paintMagnifyingGlassPixmap(pos, glassSize)  # 画好纵横十字线后的放大镜内QPixmap
        # 限制放大镜显示不超出屏幕外
        glassRect = glassPixmap.rect()
        if (pos.x() + glassSize + offset) < screenSizeF.width():
            if (pos.y() + offset + glassSize + labelHeight) < screenSizeF.height():
                glassRect.moveTo(pos + QPoint(offset, offset))
            else:
                glassRect.moveBottomLeft(pos + QPoint(offset, -offset))
        else:
            if (pos.y() + offset + glassSize + labelHeight) < screenSizeF.height():
                glassRect.moveTopRight(pos + QPoint(-offset, offset))
            else:
                glassRect.moveBottomRight(pos + QPoint(-offset, -offset))
        self.painter.drawPixmap(glassRect.topLeft(), glassPixmap)
        # 显示pos:(x, y)、rgb:(255,255,255)
        qrgb = QtGui.QRgba64.fromArgb32(glassPixmap.toImage().pixel(glassPixmap.rect().center()))
        labelRectF = QRectF(glassRect.bottomLeft().x(), glassRect.bottomLeft().y(), glassSize, labelHeight)
        self.painter.setPen(self.pen_transparent)
        self.painter.setBrush(self.color_black)  # 黑底
        self.painter.drawRect(labelRectF)
        self.painter.setPen(self.pen_white)
        self.painter.setFont(self.font_normal)
        self.painter.drawText(labelRectF,
                              QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignVCenter,
                              'pos:(%s, %s)\nrgb:(%s, %s, %s)' % (pos.x(), pos.y(), qrgb.red8(), qrgb.green8(), qrgb.blue8()))

    def paintToolbar(self, centerRectF, screenSizeF):
        '''在截图区域右下角显示工具条'''
        if self.hasScreenShot:
            if self.isCapturing or self.isAdjusting:
                self.toolbar.hide()  # 正在划定截取区域时、调整截图区域大小时不显示工具条
            else:
                self.toolbar.adjustSize()
                toolbarRectF = QRectF(self.toolbar.rect())
                # 工具条位置优先顺序:右下角下侧,右上角上侧,右下角上侧
                if (screenSizeF.height() - centerRectF.bottomRight().y()) > toolbarRectF.height():
                    toolbarRectF.moveTopRight(centerRectF.bottomRight() + QPointF(-5, 5))
                elif centerRectF.topRight().y() > toolbarRectF.height():
                    toolbarRectF.moveBottomRight(centerRectF.topRight() + QPointF(-5, -5))
                else:
                    toolbarRectF.moveBottomRight(centerRectF.bottomRight() + QPointF(-5, -5))
                # 限制工具条的x坐标不为负数,不能移出屏幕外
                if toolbarRectF.x() < 0:
                    pos = toolbarRectF.topLeft()
                    pos.setX(centerRectF.x() + 5)
                    toolbarRectF.moveTo(pos)
                self.toolbar.move(toolbarRectF.topLeft().toPoint())
                self.toolbar.show()
        else:
            self.toolbar.hide()

    def paintEditActions(self):
        '''在截图区域绘制编辑行为结果。编辑行为超出截图区域也无所谓,保存图像时只截取截图区域内'''
        # 1.绘制正在拖拽编辑中的矩形、椭圆、涂鸦
        if self.isDrawRectangle:
            self.screenArea.paintRectangle(self.painter, self.toolbar.curColor(), self.toolbar.curLineWidth())
        elif self.isDrawEllipse:
            self.screenArea.paintEllipse(self.painter, self.toolbar.curColor(), self.toolbar.curLineWidth())
        elif self.isDrawGraffiti:
            self.screenArea.paintGraffiti(self.painter, self.toolbar.curColor(), self.toolbar.curLineWidth())
        # 2.绘制所有已保存的编辑行为
        self.screenArea.paintEachEditAction(self.painter)

    def clearEditFlags(self):
        self.isDrawing = False
        self.isDrawRectangle = False
        self.isDrawEllipse = False
        self.isDrawGraffiti = False
        self.isDrawText = False

    def exitEditMode(self):
        '''退出编辑模式'''
        self.clearEditFlags()
        self.toolbar.onActionTriggered(None)  # 清空工具条工具按钮选中状态
        self.textInputWg.hide()

    def clearScreenShotArea(self):
        '''清空已划定的截取区域'''
        self.screenArea.clearEditActions()  # 清除已保存的编辑行为
        self.exitEditMode()
        self.hasScreenShot = False
        self.isCapturing = False
        pos = QPointF()
        self.screenArea.setCenterArea(pos, pos)
        self.update()
        self.setCursor(QtCore.Qt.CursorShape.CrossCursor)  # 设置鼠标样式 十字

    def mousePressEvent(self, event):
        if event.button() == QtCore.Qt.MouseButton.LeftButton:
            pos = event.position()
            if self.hasScreenShot:
                if self.isDrawRectangle or self.isDrawEllipse:
                    self.screenArea.setBeginEditPoint(pos)
                elif self.isDrawGraffiti:  # 保存涂鸦经过的每一个点
                    self.screenArea.saveGraffitiPointF(pos, first=True)
                elif self.isDrawText:
                    if self.isDrawing:
                        if QRectF(self.textInputWg.rect()).contains(pos):
                            pass  # 在输入框内调整光标位置,忽略
                        else:  # 鼠标点到输入框之外,完成编辑
                            self.screenArea.saveTextInputAction()
                    else:  # 未开始编辑时(暂不支持文本拖拽)
                        action = self.screenArea.takeTextInputActionAt(pos)
                        if action:  # 鼠标点到输入框之内,修改旧的文本输入
                            self.textInputWg.loadTextInputBy(action)
                        else:  # 鼠标点到输入框之外,开始新的文本输入
                            self.screenArea.setBeginInputTextPoint(pos)
                elif self.screenArea.isMousePosInCenterRectF(pos):
                    self.isMoving = True  # 进入拖拽移动模式
                    self.screenArea.setBeginDragPoint(pos)
                else:
                    self.isAdjusting = True  # 进入调整大小模式
                    self.screenArea.setBeginAdjustPoint(pos)
            else:
                self.screenArea.setCenterArea(pos, pos)
                self.isCapturing = True  # 进入划定截图区域模式
        if event.button() == QtCore.Qt.MouseButton.RightButton:
            if self.hasScreenShot or self.isCapturing:  # 清空已划定的的截图区域
                self.clearScreenShotArea()
            else:
                self.close()

    def mouseReleaseEvent(self, event):
        if event.button() == QtCore.Qt.MouseButton.LeftButton:
            if self.isDrawRectangle:
                self.screenArea.saveRectangleAction()
            elif self.isDrawEllipse:
                self.screenArea.saveEllipseleAction()
            elif self.isDrawGraffiti:
                self.screenArea.saveGraffitiAction()
            self.isCapturing = False
            self.isMoving = False
            self.isAdjusting = False
            self.toolbar.show()

    def mouseMoveEvent(self, event):
        pos = event.position()
        if self.isDrawing:
            if self.isDrawRectangle or self.isDrawEllipse:
                self.screenArea.setEndEditPoint(pos)
            elif self.isDrawGraffiti:
                self.screenArea.saveGraffitiPointF(pos)
        elif self.isCapturing:
            self.hasScreenShot = True
            self.screenArea.setEndPoint(pos, remake=True)
        elif self.isMoving:
            self.screenArea.moveCenterAreaTo(pos)
        elif self.isAdjusting:
            self.screenArea.adjustCenterAreaBy(pos)
        self.update()
        if self.hasScreenShot:
            self.setCursor(self.screenArea.getMouseShapeBy(pos))
        else:
            self.setCursor(QtCore.Qt.CursorShape.CrossCursor)  # 设置鼠标样式 十字

    def mouseDoubleClickEvent(self, event):
        if event.button() == QtCore.Qt.MouseButton.LeftButton:
            if self.screenArea.isMousePosInCenterRectF(event.position()):
                self.save2Clipboard()
                self.close()

    def keyPressEvent(self, QKeyEvent):
        if QKeyEvent.key() == QtCore.Qt.Key.Key_Escape:
            self.close()
        if QKeyEvent.key() in (QtCore.Qt.Key.Key_Return, QtCore.Qt.Key.Key_Enter):  # 大键盘、小键盘回车
            self.save2Clipboard()
            self.close()

    def save2Clipboard(self):
        '''将截图区域复制到剪贴板'''
        if self.hasScreenShot:
            mimData = QtCore.QMimeData()
            mimData.setImageData(self.screenArea.centerPhysicalPixmap().toImage())
            QApplication.clipboard().setMimeData(mimData)
            # self.screenArea.saveNightAreaImg()
            self.close()

    def save2Local(self):
        fileType = self.fileType_img
        filePath, fileFormat = self.sys_selectSaveFilePath(self, fileType=fileType)
        if filePath:
            self.screenArea.centerPhysicalPixmap().save(filePath, quality=100)
            self.close()

    def sys_getCurTime(self, fmt='%Y-%m-%d %H:%M:%S'):
        '''获取字符串格式的当前时间'''
        # return QtCore.QDateTime.currentDateTime().toString('yyyy-MM-dd hh:mm:ss')
        return datetime.now().strftime(fmt)

    def sys_selectSaveFilePath(self, widget, title='选择文件保存路径', saveFileDir=None,
                               saveFileName='', defaultFileFmt='%Y%m%d%H%M%S', fileType=None):
        '''选择文件保存路径
        title:选择窗口标题
        saveFileDir:指定保存目录
        saveFileName:默认保存文件名
        defaultFileFmt:不指定saveFileName时,自动以此格式的时间字符串命名文件
        fileType:可以选择的文件类型
        return:(所选的文件保存路径, 文件的类型)
        '''
        options = QFileDialog.Option.ReadOnly
        if saveFileName == '':
            saveFileName = self.sys_getCurTime(defaultFileFmt)
        if not saveFileDir:
            saveFileDir = self.dir_lastAccess
        saveFilePath = os.path.join(saveFileDir, saveFileName)
        if not fileType:
            fileType = self.fileType_all
        filePath, fileFormat = QFileDialog.getSaveFileName(widget, title, saveFilePath, fileType, options=options)
        if filePath:
            self.dir_lastAccess = os.path.dirname(filePath)
        return (filePath, fileFormat)


class MainWindow(QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()
        self.setWindowTitle('自定义截图工具展示')
        self.screenShotWg = ScreenShotWidget()
        centralLayout = QVBoxLayout()
        centralLayout.addWidget(QPushButton('开始截图', clicked=lambda: self.screenShot()))
        centralLayout.addWidget(QPushButton('隐藏本窗口后截图', clicked=lambda: self.screenShot(True)))
        centralWidget = QWidget()
        centralWidget.setLayout(centralLayout)
        self.setCentralWidget(centralWidget)

    def screenShot(self, hide=False):
        if hide:
            self.showMinimized()
        self.screenShotWg.start()


if __name__ == '__main__':
    app = QApplication(sys.argv)
    translator = QtCore.QTranslator()  # 颜色选取窗口、字体选择窗口中文化
    if translator.load("D:/dev/Python38/Lib/site-packages/PyQt6/Qt6/translations/qtbase_zh_CN.qm"):
        app.installTranslator(translator)
    main = MainWindow()
    main.show()
    sys.exit(app.exec())

三、结语

其实,实现了涂鸦就相当于实现了矩形、椭圆、箭头、马赛克、文字等功能,只不过你要有用鼠标精细化描线的能力。其他诸如插入表情、序号笔、长截图、鼠标右键拖拽自由区域截图、获取已打开窗口的句柄感应截图、识别图中文字、翻译图中文字等都是可以实现的,只不过坑太深了,不想挖了。
很多时候我们感到迷茫,是因为我们走得太远了,以至于忘记了为何而出发。
而我,我只是想截个图而已。

你可能感兴趣的:(pyqt6,python,pyqt)