一、常用的截图方式
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
主要功能:
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).setS