[Python]用Qt6和Pillow实现截图小工具

news2024/11/26 22:43:29

        本文章主要讲述的内容是,使用python语言借助PyQt6和Pillow库进行简单截图工具的开发,含义一个简单的范围裁剪和软件界面。

        主要解决的问题是,在高DPI显示屏下,坐标点的偏差导致QWidget显示图片不全、剪裁范围偏差问题。

        适合有一点点基础的朋友来看,使用的工具有:Qt Designer、PyUIC、Qt6、Pillow

截图与剪裁功能设计思路

一般截图功能的步骤是:

  1. 启用截图功能
  2. 将整个屏幕进行截取,保存截取的全屏图片
  3. 呈现出刚刚截取的全屏,由用户选择截取的范围。并对所选的范围进行剪裁
  4. 保存剪裁的图片,删除截取的全屏

利用QtDesigner对软件前端的简单制作

mainWindow-主界面

这里不是重点,就新建一个Main Window后放置一个pushButton就好了。

并使用PyUIC对保存后的ui转换成.py格式

 minorWindow-副界面

创建一个简单的Widget就好了,副界面主要是作用是:呈现原图,提供剪裁的平台。

并使用PyUIC对保存后的ui转换成.py格式

主界面代码编写

主要是作用是:

  1. 为截图功能提供一个启动方法
  2. 保存截取的全屏幕截图。
import time

from PIL import ImageGrab
from PyQt6 import QtWidgets

from shDemo import mainWindow
from shDemo import minorWindow


  # 继承我们前面编写的主界面的前端.py,以及对应的QMainWindow
class screenshot(QtWidgets.QMainWindow, mainWindow.Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.setupUi(self)  # 调用主界面的setupUI

        self.pushButton.clicked.connect(self.screenshot)  # 绑定pushButton按钮到screenshot事件上

  # 按钮被点击后触发此方法
    def screenshot(self):
        # 把当前窗口最小化
        self.showMinimized()
        # 等待1秒,给窗口最小化的时间
        time.sleep(1)
        # 截取全屏
        img = ImageGrab.grab()
        # 暂存全屏图片 保存到本地
        img.save('屏幕快照.png')
        # 生成副窗口
        self.childWidget = minorWindow.Ui_jieping()
        # 展示副窗口
        self.childWidget.show()
        # 完成剪裁工作,恢复主窗口
        self.showNormal()

if __name__ == '__main__':
    app = QtWidgets.QApplication([])
    window = screenshot()
    window.show()
    app.exec()

副界面代码编写

因为此处的副界面是被调用的,我们直接在其ui转换后的.py文件上进行编写,拓展其方法

继承一下QWidget,调用一下setupUi

class Ui_jieping(QtWidgets.QWidget):

    def __init__(self):
        super().__init__()
        self.setupUi(self)

原截图呈现、范围绘制与范围截取

要注意的就是,呈现的像素比率,截取的坐标点

主要思路是,将保存好的原截图,呈现到一个QWidget(副界面)上进行显示。

这里有一个问题,就是关于屏幕DPI不同

重写一下paintEvent方法,这是一个QWidget类中原有方法,是一个绘制组件的事件。被调用的情况有如下:

  1. 窗口初始化和显示
  2. 部件大小或位置发生变化
  3. 强制重绘,使用update()或repaint()时
  4. 系统事件触发,如窗口激活

像素比率 

像素比率 = 物理像素尺寸 / 逻辑像素尺寸

         为了适应不同应用,获得更好的视觉感官,一般可以调整缩放与布局。调整到比较高的DPI,获得一个更好体验。

        屏幕缩放比例为125%,意味着逻辑像素将比物理像素更大,以便内容在屏幕上看起来更大。缩放比例125%可以表示为1.25的倍数。

        在缩放比例为125%的情况下,1920*1080的显示屏中逻辑像素的分辨率将变为1536x864。 

        显示图片时需要转换为逻辑尺寸,以确保在不同DPI的显示器上图像显示的尺寸一致。然而,截图抓取的坐标点是物理像素坐标的,因为截图本质上是对屏幕上实际像素的捕捉。

        所以在显示的时候,按照屏幕的逻辑尺寸进行展示。实际抓取的时候,要转成物理尺寸进行截取,根据像素比率对图片显示进行对应调整后就不影响图片的显示或坐标点的偏差

import typing

from PIL import ImageGrab
from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtGui import QPainter, QPixmap, QPen, QColor


class Ui_jieping(QtWidgets.QWidget):

    def __init__(self):
        super().__init__()
        self.setupUi(self)
        # 记录截取的第一个坐标点
        self.firstPoint = QtCore.QPoint()
        # 记录截取的第二个坐标点
        self.endPoint = QtCore.QPoint()
        # 将子窗口设置在屏幕最上层
        # self.setWindowFlag(QtCore.Qt.WindowType.WindowStaysOnTopHint)
        # 让其全屏显示
        self.setWindowState(QtCore.Qt.WindowState.WindowFullScreen)

    # 重写QWidget的painEvent方法,这个在初始启动的时候会调用
    def paintEvent(self, a0: typing.Optional[QtGui.QPaintEvent]) -> None:
        # 生成一个画板
        painter = QPainter(self)
        # 读取本地先前在主界面截图的图像
        # 在QT中图片放到组件上一般要转成pixmap
        pixmap = QPixmap('./屏幕快照.png')
        # 获取主屏幕对象
        screen = QtGui.QGuiApplication.primaryScreen()
        # 获取设备像素比率  物理像素与逻辑像素之间的比率
        self.device_pixel_ratio = screen.devicePixelRatio()
        # 计算实际绘制尺寸
        # 在显示和编程的时候,是按照逻辑像素取进行展示与设计
        # 逻辑尺寸= 物理尺寸 / 像素比  计算出符合当前屏幕的尺寸
        actual_width = pixmap.width() / self.device_pixel_ratio
        actual_height = pixmap.height() / self.device_pixel_ratio
        # 绘制图片
        # 0,0的意思是,从屏幕左上角作为起始点,如果此时的逻辑尺寸与屏幕的一致,就作为全屏展示
        painter.drawPixmap(0, 0, int(actual_width), int(actual_height), pixmap)
        # 将截图画框显示为红色
        pen = QPen(QColor(255, 0, 0))
        painter.setPen(pen)
        # 绘制矩形的方法,其中的参数来自鼠标事件 显示要截图的范围 在绘制的时候还会调用update来触发paintEvent方法
        # 从第一个记录点开始
        # 记住0,0是屏幕最坐上角
        # 向右self.endPoint.x() - self.firstPoint.x()个像素 作为长
        # 向下self.endPoint.y() - self.firstPoint.y()个像素 作为高
        # 得到负数也没关系噢,x方向上负数就是往左, y方向上负数是向上
        painter.drawRect(self.firstPoint.x(), self.firstPoint.y(), self.endPoint.x() - self.firstPoint.x(),
                         self.endPoint.y() - self.firstPoint.y())

    # 在鼠标按下的时候触发此事件
    def mousePressEvent(self, a0: typing.Optional[QtGui.QMouseEvent]) -> None:
        # 记录按下的第一个坐标点
        self.firstPoint = a0.pos()

    # 在鼠标移动的时候触发此事件
    def mouseMoveEvent(self, a0: typing.Optional[QtGui.QMouseEvent]) -> None:
        # 记录移动过程中的当前鼠标的坐标点
        self.endPoint = a0.pos()
        self.update()  # 触发paintEvent,在移动鼠标的时候不断重绘截图边框

    # 在鼠标松开的时候触发此事件
    def mouseReleaseEvent(self, a0: typing.Optional[QtGui.QMouseEvent]) -> None:
        self.endPoint = a0.pos()  # 锁定最后松开的坐标
        self.update()  # 更新在Widget上的所选范围矩形
        # 在原图上进行对所选区域的截取
        # 此处截图的时候,也要记得调整一下 从逻辑像素转换成为物理像素进行抓取
        # 不然截图出来会有偏差
        # 物理像素 = 逻辑像素 * 像素比率
        self.firstPoint.setX(int(self.firstPoint.x() * self.device_pixel_ratio))
        self.firstPoint.setY(int(self.firstPoint.y() * self.device_pixel_ratio))
        self.endPoint.setX(int(self.endPoint.x() * self.device_pixel_ratio))
        self.endPoint.setY(int(self.endPoint.y() * self.device_pixel_ratio))
        # 最后借助PIL进行对屏幕固定范围进行抓取
        # 这里有一个坑 在从右向左,从下到上进行画范围截图的时候,会有一个报错
        # 因为grab的参数是,左上角和右下角坐标点的x和y值
        # firstPoint和endPoint又是一开始写死的
        # 可以比较一下两者的位置,如果endPoint比firstPoint小,就可以互换一下
        if self.firstPoint.x() > self.endPoint.x() and self.firstPoint.y() > self.endPoint.y():
            self.firstPoint, self.endPoint = self.endPoint, self.firstPoint
        image = ImageGrab.grab(
            bbox=(self.firstPoint.x() + 1, self.firstPoint.y() + 1, self.endPoint.x() - 1, self.endPoint.y() - 1))
        # 将范围截取下来的进行保存
        image.save('hello.png')
        # 就可以将先前截的全屏删掉了
        # os.remove('./屏幕快照.png')
        # 关闭全屏显示的子窗口
        self.close()

    def setupUi(self, jieping):
        jieping.setObjectName("jieping")
        jieping.resize(400, 300)

        self.retranslateUi(jieping)
        QtCore.QMetaObject.connectSlotsByName(jieping)

    def retranslateUi(self, jieping):
        _translate = QtCore.QCoreApplication.translate
        jieping.setWindowTitle(_translate("jieping", "Form"))

在不同的DPI下截取出来的图片都是一样滴,大家可以去试一下

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1795282.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spark MLlib 机器学习详解

目录 🍉引言 🍉Spark MLlib 简介 🍈 主要特点 🍈常见应用场景 🍉安装与配置 🍉数据处理与准备 🍈加载数据 🍈数据预处理 🍉分类模型 🍈逻辑回归 &a…

【Linux网络】传输层协议 - UDP

文章目录 一、传输层(运输层)运输层的特点复用和分用再谈端口号端口号范围划分认识知名端口号(Well-Know Port Number)两个问题① 一个进程是否可以绑定多个端口号?② 一个端口号是否可以被多个进程绑定? n…

Java Web学习笔记15——DOM对象

DOM: 概念:Document Object Model: 文档对象模型 将标记语言的各个组成部分封装为对应的对象: Document: 整个文档对象 Element:元素对象 Attribute: 属性对象 Text:文本对象 Comment&a…

logback删除日志文件和文件夹

​​​​​一,事由和源码 logback版本1.2.11 网上找了很多都是无法删除文件夹的,原先使用的TimeBasedRollingPolicy无法删除日志的文件夹,有很多空的日期文件夹,于是查看TimeBasedRollingPolicy源码发现有校验不删除文件夹&#x…

docker-compose部署 kafka 3.7 集群(3台服务器)并启用账号密码认证

文章目录 1. 规划2. 服务部署2.1 kafka-012.2 kafka-022.3 kafka-032.4 启动服务 3. 测试3.1 kafkamap搭建(测试工具)3.2 测试 1. 规划 服务IPkafka-0110.10.xxx.199kafka-0210.10.xxx.198kafka-0310.10.xxx.197kafkamp10.10.xxx.199 2. 服务部署 2.1…

MySQL报ERROR 2002 (HY000)解决

今天在连接客户服务器时MySQL的时候报: ERROR 2002 (HY000): Can’t connect to local MySQL server through socket ‘/tmp/mysql/mysql.sock’ (2) [rootXXX ~]# mysql -uroot -p Enter password: ERROR 2002 (HY000): Can’t connect to local MySQL server through socket…

香港服务器无法访问是什么情况?

香港服务器无法访问是什么情况?简单来说,这意味着香港服务器没有响应请求,客户端无法访问。此错误可能由于多种原因而发生,包括网络连接问题、服务器停机、防火墙限制和 DNS 错误。当发生服务器无法访问错误时,它会影响您网站的性…

【Linux】进程切换环境变量

目录 一.进程切换 1.进程特性 2.进程切换 1.进程切换的现象 2.如何实现 3.现实例子 2.环境变量 一.基本概念 二.常见环境变量 三.查询常见环境变量的方法 四.和环境变量相关的命令 五.环境变量表的组织方式 六.使用系统调用接口方式查询环境变量 1.getenv 2.反思 …

【自然语言处理】【Scaling Law】语言模型物理学 第3.3部分:知识容量Scaling Laws

语言模型物理学3.3:知识容量Scaling Laws 论文名称:Physics of Language Models: Part 3.3, Knowledge Capacity Scaling Laws 论文地址:https://arxiv.org/pdf/2404.05405 相关博客 【自然语言处理】【Scaling Law】Observational Scaling …

RTA_OS基础功能讲解 2.8-Tick计数器

RTA_OS基础功能讲解 2.8-Tick计数器 文章目录 RTA_OS基础功能讲解 2.8-Tick计数器一、计数器简介二、计数器配置三、计数器驱动3.1 软件计数器驱动3.1.1 递增软件计数器3.1.2 静态计数器接口3.2 硬件计数器驱动3.2.1 Advancing硬件计数器3.2.2 回调函数四、在运行时访问计数器属…

Xcode 打包报错Command PhaseScriptExecution failed with a nonzero exit code

解决办法: 1、在Xcode项目中 Pods -> Targets Support Files -> Pods-项目名 -> Pods-项目名-frameworks 中(大约在第44行) 加上 -f 2、CocoaPods版本太旧了,可以尝试升级CocoaPods版本 使用sudo gem update cocoapods更新cocoapods,问题将在1.12.1版本已…

lua vm 二: 查看字节码、看懂字节码

本文讲一讲如何查看 lua 的字节码(bytecode),以及如何看懂字节码。 以下分析基于 lua-5.4.6,下载地址:https://lua.org/ftp/ 。 1. 查看字节码 1.1 方法一:使用 luac luac 是 lua 自带的编译程序&#x…

无线和移动网络

背景 两个重要的挑战 无线:通过无线链路通信移动:需要网络处理移动(不同变换所接入的网络)用户 无线网络中的组件 无线主机(无线并不总是意味着移动的)基站(base station 或者叫AP&#xff0…

芝麻IP好用吗?来测试了!

作为老牌代理IP服务厂商,芝麻IP和青果网络代理IP都做的不错,市场上几乎可以是有口皆碑了,上次测试了青果网络的代理IP,效果表现得还挺不错,和他们自己宣传的以及客户对他们的评价大差不差。 总的来说,他们家…

纷享销客安全体系:物理与环境安全

纷享销客的物理设备托管在经过严格准入制度授权的TIER3级别以上的专业数据中心,这些数据中心均通过了等保三级与IS027001安全认证,确保电力、制冷等基础设施提供相应级别的冗余,以增强IDC环境的安全性。 业务操作系统平台采用当前广泛使用的…

解决 iOS 端小程序「saveVideoToPhotosAlbum:fail invalid video」问题

场景复现: const url https://mobvoi-digitalhuman-video-public.weta365.com/1788148372310446080.mp4uni.downloadFile({url,success: (res) > {uni.saveVideoToPhotosAlbum({filePath: res.tempFilePath,success: (res) > {console.log("res > &…

NocoDB开源的智能表格详解-腾讯文档本地替代品

文章目录 一、介绍二、docker-compose部署三、登录NocoDB四、NocoDB手册1. 创建项目2. 收集统计表2.1 添加字段2.2 编辑字段2.3 字段类型2.4 发布表格 3.创建表单3.1 创建表单3.2 分享表单3.3 填写检测单 4.创建看板5.创建画廊 一、介绍 可作为腾讯文档的本地电子表格替代品&a…

VS2019 QT无法打开 源 文件 “QTcpSocket“

VS2019 QT无法打开 源 文件 "QTcpSocket" QT5.15.2_msvc2019_64 严重性 代码 说明 项目 文件 行 禁止显示状态 错误(活动) E1696 无法打开 源 文件 "QTcpSocket" auto_pack_line_demo D:\vs_qt_project\auto_pack_line_de…

OZON快蜗牛数据工具,OZON数据分析工具

在当今的电商时代,数据已经成为了商家们最宝贵的资产之一。无论是产品选品、市场定位,还是营销策略的制定,都离不开对数据的深入分析和精准把握。而在众多电商平台中,OZON以其独特的商业模式和庞大的用户群体,吸引了众…

Vue3项目准备:utils工具插件文件夹中封装request.js配置axios请求基地址及超时时间、请求拦截器、响应拦截器

token介绍 概念:访问权限的令牌,本质上是一串字符串 创建:正确登录后,由后端签发并返回 作用:判断是否有登录状态等,控制访问权限 注意:前端只能判断token有无,而后端才能判断tok…