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

一、常用的截图方式

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

from PIL import ImageGrab

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

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

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

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

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

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

import os
import ctypes

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

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

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

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

结果图
主要功能:
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
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值