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