一、常用的截图方式

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

from PIL import ImageGrabimg = 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 ctypesdef 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


主要功能:
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, QIconclass 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()  # QTextDocumentself.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))  # 宽度-1self.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 = Trueself.waitForInput()class LineWidthAction(QAction):'''画笔粗细选择器'''def __init__(self, text, parent, lineWidth):super().__init__(text, parent)self._lineWidth = lineWidthself.refresh(QtCore.Qt.GlobalColor.red)self.triggered.connect(self.onTriggered)self.setVisible(False)def refresh(self, color):painter = self.parent().god.screenArea._painterdotRadius = 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._lineWidthclass 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_textInputself.triggered.connect(self.onTriggered)self.setVisible(False)def onTriggered(self):font, ok = QFontDialog.getFont(self._curFont, self.parent(), caption='选择字体')if ok:self._curFont = fontself.parent().god.textInputWg.setCurrentFont(font)class ColorAction(QAction):'''颜色选择器'''def __init__(self, text, parent):super().__init__(text, parent)self._curColor = QtCore.Qt.GlobalColor.redself._pixmap = QPixmap(32, 32)self.refresh(self._curColor)self.triggered.connect(self.onTriggered)def refresh(self, color):self._curColor = colorself._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 = godself.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 = 3self._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._curLineWidthdef curFont(self):return self._at_font._curFontdef 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._iconPixmapCenterdef 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 = Trueself.setLineWidthActionVisible(True)self._at_font.setVisible(False)def beforeDrawEllipse(self):self.god.clearEditFlags()self.god.isDrawEllipse = Trueself.setLineWidthActionVisible(True)self._at_font.setVisible(False)def beforeDrawGraffiti(self):self.god.clearEditFlags()self.god.isDrawGraffiti = Trueself.setLineWidthActionVisible(True)self._at_font.setVisible(False)def beforeDrawText(self):self.god.clearEditFlags()self.god.isDrawText = Trueself.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)  # 工具条上显示标准箭头cursordef leaveEvent(self, event):self.god.setCursor(QtCore.Qt.CursorShape.CrossCursor)  # 十字无箭头class ScreenArea(QtCore.QObject):'''屏幕区域(提供各种算法的核心类),划分为9个子区域:TopLeft,Top,TopRightLeft,Center,RightBottomLeft,Bottom,BottomRight其中Center根据start、end两个QPointF确定'''def __init__(self, god):super().__init__()self.god = godself._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 + ww = -wif h < 0:  # bottomRightPoint在topLeftPoint上侧时,topLeftPoint往上移动y = y + hh = -hreturn 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的)QPixmaprectf:指定区域。可为QRect或QRectFeditAction:是否带上编辑结果'''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_centerdef centerPhysicalPixmap(self, editAction=True):'''截图区域的QPixmapeditAction:是否带上编辑结果'''return self.physicalPixmap(self._rt_center + QMarginsF(-1, -1, 1, 1), editAction=editAction)def centerTopMid(self):return self._pt_centerTopMiddef centerBottomMid(self):return self._pt_centerBottomMiddef centerLeftMid(self):return self._pt_centerLeftMiddef centerRightMid(self):return self._pt_centerRightMiddef setStartPoint(self, pointf, remake=False):self._pt_start = pointfif remake:self.remakeNightArea()def setEndPoint(self, pointf, remake=False):self._pt_end = pointfif remake:self.remakeNightArea()def setCenterArea(self, start, end):self._pt_start = startself._pt_end = endself.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()) / 2self._pt_centerBottomMid = (self._rt_center.bottomLeft() + self._rt_center.bottomRight()) / 2self._pt_centerLeftMid = (self._rt_center.topLeft() + self._rt_center.bottomLeft()) / 2self._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 rectfdef 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_vectordef 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 = pointfelif 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 = pointfelse:  # 'ERROR'returnnewRectF = 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.ArrowCursorelif 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() / 2halfHeight = glassPixmap.height() / 2self._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 glassPixmapdef 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_startEditif not endPoint:endPoint = self._pt_endEditqrectf = 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_startEditif not endPoint:endPoint = self._pt_endEditqrectf = 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:returnelif total == 1:painter.drawPoint(pointfs[0])else:previousPoint = pointfs[0]for i in range(1, total):nextPoint = pointfs[i]painter.drawLine(previousPoint, nextPoint)previousPoint = nextPointdef 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 Nonedef undoEditAction(self):reply = Falseif self._actions:reply = self._actions.pop()if not self._actions:  # 所有编辑行为都被撤销后退出编辑模式self.god.exitEditMode()else:self.god.exitEditMode()return replydef clearEditActions(self):self._actions.clear()def setBeginEditPoint(self, pointf):'''在截图区域上绘制矩形、椭圆时鼠标左键按下的位置(topLeft)'''self._pt_startEdit = pointfself.god.isDrawing = Truedef setEndEditPoint(self, pointf):'''在截图区域上绘制矩形、椭圆时鼠标左键松开的位置(bottomRight)'''self._pt_endEdit = pointfdef 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 = Falsedef 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 = Falsedef saveGraffitiPointF(self, pointf, first=False):self._pointfs.append(pointf)if first:self.god.isDrawing = Truedef 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 = Falsedef setBeginInputTextPoint(self, pointf):'''在截图区域上输入文字时鼠标左键按下的位置(topLeft)'''self.god.isDrawing = Trueself.god.textInputWg.beginNewInput(pointf, self._pt_end)def saveTextInputAction(self):txt = self.god.textInputWg.toPlainText()if txt:rectf = self.god.textInputWg._maxRect  # 取最大矩形的topLeftrectf.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 = Falsedef 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.transparentself.color_black = QColor(0, 0, 0, 64)  # 黑色背景self.color_lightBlue = QColor(30, 120, 255)  # 浅蓝色。深蓝色QtCore.Qt.GlobalColor.blueself.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,点线DotLineself.pen_SolidLine_lightBlue.setWidthF(0)  # 0表示线宽为1self.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):returnpos = 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 = Falseself.isDrawRectangle = Falseself.isDrawEllipse = Falseself.isDrawGraffiti = Falseself.isDrawText = Falsedef exitEditMode(self):'''退出编辑模式'''self.clearEditFlags()self.toolbar.onActionTriggered(None)  # 清空工具条工具按钮选中状态self.textInputWg.hide()def clearScreenShotArea(self):'''清空已划定的截取区域'''self.screenArea.clearEditActions()  # 清除已保存的编辑行为self.exitEditMode()self.hasScreenShot = Falseself.isCapturing = Falsepos = 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 = Falseself.isMoving = Falseself.isAdjusting = Falseself.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 = Trueself.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_imgfilePath, 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.ReadOnlyif saveFileName == '':saveFileName = self.sys_getCurTime(defaultFileFmt)if not saveFileDir:saveFileDir = self.dir_lastAccesssaveFilePath = os.path.join(saveFileDir, saveFileName)if not fileType:fileType = self.fileType_allfilePath, 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())

三、结语

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

用pyqt原生功能实现自由屏幕截图相关推荐

  1. PyQt - 维基百科,自由的百科全书

    PyQt - 维基百科,自由的百科全书 PyQt 维基百科,自由的百科全书 跳转至: 导航. 搜索 PyQt 開發者 Riverbank Computing 穩定版本 4.9.5/ 2012年9月29 ...

  2. 开源项目推荐:C++/MFC/Qt开发的功能齐全的屏幕截图软件ScreenCatch/Snipaste

    声明:项目非本人原创,仅仅分享链接! 声明:项目非本人原创,仅仅分享链接! 声明:项目非本人原创,仅仅分享链接! ScreenCatch(基于MFC) 详情参见 功能齐全的屏幕截图C++实现详解_ch ...

  3. Visual Studio 2022 c#中很实用的VS默认快捷键和原生功能

    常常使用VS感觉还是有必要掌握其默认的快捷键,我这个人比较懒,不喜欢动不动就去设置快捷键,系统有就用,记住了就可以到处用,问题是像我们这种有很多个工作场所的人不可能每台电脑都去配置一下快键键.实际上我 ...

  4. wap2app实现打包点微同城APP集成扫一扫,微信分享支付等原生功能

    DZ有个点微同城的插件,发现这个插件挺好用的,但是它没有APP,于是就自己动手研究了一段时间点微同城打包成APP 于是便集成了点微同城原生微信登录,原生微信支付,原生支付宝支付以及扫一扫,长按保存海报 ...

  5. HBuilder原生功能概述

    http://www.jianshu.com/p/aee82dc9742e 界面: mui的风格样式是最接近原生样式的,支持html CSS javascript编写UI代码,代码提示风格比较快.可以 ...

  6. linux下截图软件,Flameshot - Linux下功能强大的屏幕截图软件

    今天推荐大家使用Flameshot(火焰),因为它是我个人最喜欢屏幕截图软件.如果你不知道,Flameshot是一个Linux可用的开源截屏工具. 在本文中,我将重点介绍"Flameshot ...

  7. 功能齐全的屏幕截图C++实现详解

    点击蓝字 关注我们 1.概述 要使用屏幕截图,其实很容易,装一款聊天软件或者办公软件就可以了,比如QQ.企业微信.钉钉.飞书等.但要开发出类似这些软件的屏幕截图模块,则没那么容易.其实实现屏幕截图的技 ...

  8. FastStone Capture v9.7 功能强悍的屏幕截图工具

    前言 FSCapture是小编做公众号一直在用的一款截图录屏软件,体积小巧.功能强大.简单易用,集屏幕截图.图像编辑.视频录制等功能于一身.它可以捕捉屏幕上的任何区域,提供多种捕获方式(如:活动窗口. ...

  9. 功能最强大的屏幕截图屏幕录制:Snagit

    Snagit简单,强大的屏幕捕获软件! 省去创建图像和视频的麻烦.捕获屏幕,编辑图像并提供结果. TechSmith snagit Mac 破解版是Mac平台上一款强大的屏幕捕获软件,snagit 2 ...

  10. 利用 Chrome 原生功能截图网页全图

    打开你想截图的网页了,然后按下 F12(macOS 是 option + command + i)调出开发者工具,接着按「Ctrl + Shift + P」(macOS 是 command + Shi ...

最新文章

  1. 2022-2028年中国绝热隔音材料行业投资分析及前景预测报告
  2. 30年前过气老论文,为何能催生革命全球的CNN框架?
  3. python在代码里查看获取模块信息
  4. Spring Boot系列——7步集成RabbitMQ
  5. 《商务旅行》解题报告
  6. PCL:PCL1.9.0更新
  7. csvn(apache+svn)管理工具搭建
  8. linux驱动文件操作简单介绍
  9. Doc2Vec训练相似文章识别模型
  10. 如何在下一个网页设计项目中使用Google字体
  11. Jedis的使用及SpringBoot整合Redis
  12. UDP与TCP报文格式,字段意义
  13. 红米怎么打开USB调试(Android4.2.2),系统中隐藏开发者选项(红米,MTK6589,三星i9500 /S4打开开发者选项)
  14. Netty In Action中文版 - 第一章:Netty介绍
  15. 作为SLAM中最常用的闭环检测方法,视觉词袋模型技术详解来了
  16. 如何计算802.11 PHY Data Rate (11ac/11ax)
  17. 一个好的浏览器是多么重要,强烈推荐一个好用的浏览器.........
  18. tableau-客户留存分析模型
  19. 台式计算机硬盘的安装位置,台式主机扩大存储,7步教你完美安装机械硬盘
  20. 书蕴——基于书评的人工智能推荐系统

热门文章

  1. ArcGIS Runtime API for Android100.13.0加载TPK包、Runtime包、WMS地图服务、三维模式
  2. 木讷的程序员需要知道的事情 (六)
  3. java.util.concurrent.ThreadFactory 实例讲解
  4. 发票查验API给财务工作带来了哪些便利?
  5. 如何在工作中设定和使用 SMART 目标
  6. C# 获取 ARP 映射
  7. 【周赛266】leetcode2062.统计字符串中的的元音子字符串
  8. 流量分析和强制执行ntopng
  9. 2022牛客多校9 BTwo Frogs(概率DP)
  10. linux删除网卡网卡驱动命令,Linux系统如何查看网卡驱动