使用PySide6图形界面框架制作GUI

4/5/2022 PySide6QtDesignerQSS美化样式可执行文件打包

本文演示了 PySide6 的安装、配置,以一个OCR识别翻译工具为示例演示了开发流程,对期间碰到的问题进行了梳理和总结。

# 1. PySide基本介绍

# 1.1 PySide简介

在介绍 PySide 之前,要先了解下 Qt。Qt是一个跨平台的C++开发库,很适合用于开发GUI程序。但其除了GUI库之外,同时也为用户提供了数据库、图像处理、音视频处理、网络通信、文件操作甚至并发操作等相关API,这为客户端界面的开发提供了极大的便利。

PySide与PyQt都是Qt界面框架对Python语言的绑定,目前最新的版本是PySide6,对标PyQt6,而上一代是PySide2对标PyQt5,PySide跳过了3-5的版本就是为了与其进行对应。

# 1.2 PySide与PyQt对比

PyQt

  • PyQt由Riverbank Computing公司开发,推出的时间比较早,所以现在相对来说比较成熟,最新版本为PyQt6。

  • PyQt采用GPLv3许可证和商业许可证发布,GPLv3许可表示如果你使用PyQt,则必须将程序开源,否则可能收到律师函,如果选择闭源,则需购买商用许可证。

PySide

  • PySide的推出时间较PyQt要晚得多,但它有一优势,PySide是Qt官方提供的库,目前PySide最新版为PySide6。

  • PySide采用LGPL协议发布,使用该协议,只要你以调用动态链接库的形式使用Qt,你可以以任何形式发布你的程序。

# 2. PySide6安装及环境配置

# 2.1 安装pyside6

# 2.1.1 安装pyside6依赖

PySide6 是来自于Qt for Python项目的官方Python模块,它提供了对完整Qt 6.0+框架的访问。

  • Qt Designer:拖拽式的界面设计工具,通过拖拽的方式放置控件,并实时查看控件效果进行快速UI设计。
  • PyUIC:主要是把Qt Designer生成的.ui文件换成.py文件。
  • PyRCC:主要是把编写的.qrc资源文件换成.py文件。
$ pip install PySide6     
1

安装的是最新版:6.2.4,已经包含了Qt Designer、PyUIC、PyRCC等工具。

# 2.1.2 配置环境变量

将 PySide6 路径下的 plugins\platforms 添加到系统环境变量中用户变量里。

  • 变量名:QT_QPA_PLATFORM_PLUGIN_PATH

  • 变量值:你的安装路径\Lib\site-packages\PySide6\plugins\platforms

保存变量后,一定要重启电脑,因为新的环境变量要重启后才能被系统识别!

注:这里如果没配置好的话,后续跑代码的时候会出现如下弹框报错。

This application failed to start because not Qt platform plugin could be initialized.Reinstalling the application may fix this problem.
1

# 2.2 在PyCharm中配置External Tools

为了后续方便使用,建议在 PyCharm 的 External Tools 中,配置下图的三个工具。配置好之后可以在 Tools——External Tools 处方便打开这三个工具,就不用去安装目录里找了。

配置External-Tools

注意事项:MacOS的PySide6里没有PyUIC工具,可使用PyQt5里的pyuic5工具代替,详见我的另一篇博客:Win更换MacOS的入门指南 (opens new window)。其实即便是Win,我也更倾向于使用pyuic5工具进行转换,PySide6转换完的比较乱(大量空行、没用到的包被import),而且最后的中文部分会转义(能直接用,但是看起来不直接)

# 2.2.1 配置QtDesigner

Name: QtDesigner

Program : 你的安装路径\Scripts\pyside6-designer.exe

Working directory: $ProjectFileDir$

配置QtDesigner

# 2.2.2 配置PyUIC

Name: PyUIC

Program : 你的安装路径\Scripts\pyside6-uic.exe

Arguments:$FileName$ -o $FileNameWithoutExtension$.py

Working directory: $FileDir$

配置PyUIC

# 2.2.3 配置PyRCC

Name: PyRCC

Program : 你的安装路径\Scripts\pyside6-rcc.exe

Arguments:$FileName$ -o $FileNameWithoutExtension$_rc.py

Working directory: $FileDir$

配置PyRCC

# 3. 使用QtDesigner进行界面设计

# 3.1 QtDesigner界面构成

Tools——External Tools——QtDesigner 打开界面设计器,整个工作界面的构成:

  • 左侧的“Widget Box”就是各种可以自由拖动的组件
  • 中间的“MainWindow – weather.ui”窗体就是画布
  • 右上方的”Object Inspector”可以查看当前ui的结构
  • 右侧中部的”Property Editor”可以设置当前选中组件的属性
  • 右下方的”Resource Browser”可以添加各种素材,比如图片,背景等

QtDesigner界面

最终生成.ui文件(实质上就是XML格式的文件),可直接使用,也可以通过PyUIC工具转换成.py文件。

# 3.2 信号与槽基本概念解释

在Qt中使用信号和槽(Signals and Slots)来实现其他编程工具包的“回调”功能。信号和槽机制是 Qt 的主要特性并且也很有可能是它与其他框架特性区别最大的部分。当一个特定的事件发生时,信号会被发送出去,而槽则被用来接收信号。

# 3.3 设计GUI界面并导出ui文件

在QtDesigner里,文件——新建——Widget,根据自己的需求设计并配置界面,这里不太好描述,不会的话就自行查阅视频资料学习一下吧。Ctrl+R快捷键可以预览效果,设计完成后导出一个ui文件。

YoyoOCR界面设计

注:自动适应窗口变化可通过“在窗体空白处右键——布局——栅格布局”来实现。

# 3.4 将ui文件转换成py文件

在Pycharm里,右键ui文件——External Tools——PyUIC,可以将ui文件转换成py文件,转换后根据自己需要对代码进行一些修改。

转换并修改后的Ui_YoyoOCR.py:

# -*- coding: utf-8 -*-

from PySide6.QtCore import (QCoreApplication, QMetaObject, QSize)
from PySide6.QtWidgets import (QComboBox, QHBoxLayout, QLabel, QPushButton, QRadioButton, QTextEdit, QGridLayout)


class Ui_YoyoOCR(object):
    def setupUi(self, main):
        if not main.objectName():
            main.setObjectName(u"main")
        main.resize(800, 400)
        self.gridLayout = QGridLayout(main)
        self.gridLayout.setObjectName(u"gridLayout")
        self.titleHorizontalLayout = QHBoxLayout()
        self.titleHorizontalLayout.setObjectName(u"titleHorizontalLayout")
        self.languageTab = QHBoxLayout()
        self.languageTab.setSpacing(6)
        self.languageTab.setObjectName(u"languageTab")
        self.languageTab.setContentsMargins(0, -1, 0, -1)
        self.languageLabel = QLabel(main)
        self.languageLabel.setObjectName(u"languageLabel")
        self.languageTab.addWidget(self.languageLabel)
        self.languageComboBox = QComboBox(main)
        self.languageComboBox.setObjectName(u"languageComboBox")
        self.languageComboBox.setSizeIncrement(QSize(0, 0))
        self.languageComboBox.setBaseSize(QSize(0, 0))
        self.languageTab.addWidget(self.languageComboBox)
        self.titleHorizontalLayout.addLayout(self.languageTab)
        self.functionTab = QHBoxLayout()
        self.functionTab.setObjectName(u"functionTab")
        self.functionTab.setContentsMargins(0, -1, 0, -1)
        self.functionLabel = QLabel(main)
        self.functionLabel.setObjectName(u"functionLabel")
        self.functionTab.addWidget(self.functionLabel)
        self.translateRadioButton = QRadioButton(main)
        self.translateRadioButton.setObjectName(u"translateRadioButton")
        self.functionTab.addWidget(self.translateRadioButton)
        self.translateRadioButton.setChecked(True)
        self.voiceRadioButton = QRadioButton(main)
        self.voiceRadioButton.setObjectName(u"voiceRadioButton")
        self.functionTab.addWidget(self.voiceRadioButton)
        self.ocrRadioButton = QRadioButton(main)
        self.ocrRadioButton.setObjectName(u"ocrRadioButton")
        self.functionTab.addWidget(self.ocrRadioButton)
        self.keywordRadioButton = QRadioButton(main)
        self.keywordRadioButton.setObjectName(u"keywordRadioButton")
        self.functionTab.addWidget(self.keywordRadioButton)
        self.sentenceRadioButton = QRadioButton(main)
        self.sentenceRadioButton.setObjectName(u"sentenceRadioButton")
        self.functionTab.addWidget(self.sentenceRadioButton)
        self.pushButton = QPushButton(main)
        self.pushButton.setObjectName(u"pushButton")
        self.pushButton.setEnabled(True)
        self.pushButton.setAutoRepeatDelay(300)
        self.functionTab.addWidget(self.pushButton)
        self.titleHorizontalLayout.addLayout(self.functionTab)
        self.titleHorizontalLayout.setStretch(0, 1)
        self.titleHorizontalLayout.setStretch(1, 4)
        self.gridLayout.addLayout(self.titleHorizontalLayout, 0, 0, 1, 1)
        self.textHorizontalLayout = QHBoxLayout()
        self.textHorizontalLayout.setSpacing(0)
        self.textHorizontalLayout.setObjectName(u"textHorizontalLayout")
        self.textHorizontalLayout.setContentsMargins(-1, -1, 9, -1)
        self.input = QTextEdit(main)
        self.input.setObjectName(u"input")
        self.textHorizontalLayout.addWidget(self.input)
        self.output = QTextEdit(main)
        self.output.setObjectName(u"output")
        self.textHorizontalLayout.addWidget(self.output)
        self.gridLayout.addLayout(self.textHorizontalLayout, 1, 0, 1, 1)
        self.retranslateUi(main)
        QMetaObject.connectSlotsByName(main)


    def retranslateUi(self, main):
        main.setWindowTitle(QCoreApplication.translate("main", u"OCR识别翻译工具", None))
        self.languageLabel.setText(QCoreApplication.translate("main", u"1.目标语言", None))
        self.functionLabel.setText(QCoreApplication.translate("main", u"2.功能选型", None))
        self.translateRadioButton.setText(QCoreApplication.translate("main", u"文本翻译", None))
        self.voiceRadioButton.setText(QCoreApplication.translate("main", u"语音朗读", None))
        self.ocrRadioButton.setText(QCoreApplication.translate("main", u"OCR识别", None))
        self.keywordRadioButton.setText(QCoreApplication.translate("main", u"关键词提取", None))
        self.sentenceRadioButton.setText(QCoreApplication.translate("main", u"概要提取", None))
        self.pushButton.setText(QCoreApplication.translate("main", u"开始", None))

        # 语言下拉框内容
        self.languageComboBox.addItem("中文简体")
        self.languageComboBox.addItem("中文繁体")
        self.languageComboBox.addItem("英语")
        self.languageComboBox.addItem("日语")
        self.languageComboBox.addItem("韩语")
        self.languageComboBox.addItem("德语")
        self.languageComboBox.addItem("法语")
        self.languageComboBox.addItem("西班牙语")
        self.languageComboBox.addItem("意大利语")
        self.languageComboBox.addItem("俄语")

        # 设置默认值
        self.languageComboBox.setCurrentIndex(2)
        self.languageComboBox.currentText()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100

创建一个主函数,调用该UI,查看一下效果。

新增YoyoOCR.py:

# -*- coding: utf-8 -*-

import sys

from PySide6.QtWidgets import QApplication, QWidget
from Ui_YoyoOCR import Ui_YoyoOCR


class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.ui = Ui_YoyoOCR()
        self.ui.setupUi(self)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

注:这里查看一下实际显示效果,有时候实际显示效果与OtDesigner里看到的不符,有问题及时调整。在这里我踩过一个坑儿,就是对整体用了栅格布局之后,组件内容全部挤到左上角了,原因是栅格布局只与QWidget兼容,这里我把 class MainWindow(QMainWindow)改成class MainWindow(QWidget)就正常了。

# 3.5 使用QSS美化样式

QSS 全称 Qt Style Sheets(Qt样式表),用于美化Qt程序界面,类似于CSS,但不如CSS强大,选择器和属性较少。

为降低耦合,往往把QSS写在一个单独的style.qss文件中,然后在main.py的QApplication或QMainWindow中加载样式。

if __name__ == '__main__':
    # 初始化界面
    app = QApplication(sys.argv)
    window = MainWindow()
    with open('./style.qss', 'r') as f:  # 导入qss样式
        qss_style = f.read()
    app.setStyleSheet(qss_style)
    window.show()
    sys.exit(app.exec())
1
2
3
4
5
6
7
8
9

注:此处有很多开源的QSS样式,可以根据实际需要进行修改,https://github.com/satchelwu/QSS-Skin-Builder/tree/master/other/qss (opens new window)

我这里直接使用开源的 QSS 样式表,下面介绍 qt-material 和 QDarkStyleSheet 两种。由于qt-material样式表对我所需要的组件支持还不够完善,我实际使用的是QDarkStyleSheet。

# 3.5.1 qt-material样式表

项目介绍:适用于PySide2、PySide6、PyQt5 和 PyQt6的主题样式表,有深色和浅色两套主题,可以对它进行自定义。

项目地址:https://github.com/UN-GCPDS/qt-material (opens new window)

依赖安装:

$ pip install qt-material
1

样式风格:

qt-material主题样式表

基本用法:

import sys
from PySide6 import QtWidgets
from qt_material import apply_stylesheet

# create the application and the main window
app = QtWidgets.QApplication(sys.argv)
window = QtWidgets.QMainWindow()

# setup stylesheet
apply_stylesheet(app, theme='dark_teal.xml')

# run
window.show()
app.exec_()
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 3.5.2 QDarkStyleSheet样式表

项目介绍:最完整的深色/浅色Qt主题,适用于PySide2、PySide6、PyQt5 和 PyQt6。

项目地址:https://github.com/ColinDuquesnoy/QDarkStyleSheet (opens new window)

官方文档:https://qdarkstylesheet.readthedocs.io/en/latest/ (opens new window)

依赖安装:

$ pip install qdarkstyle
1

样式风格:

QDarkStyleSheet样式风格

基本用法:

import sys
import qdarkstyle
from PySide6 import QtWidgets

# create the application and the main window
app = QtWidgets.QApplication(sys.argv)
window = QtWidgets.QMainWindow()

# setup stylesheet
app.setStyleSheet(qdarkstyle.load_stylesheet_pyside2())
# or in new API
app.setStyleSheet(qdarkstyle.load_stylesheet(qt_api='pyside6'))

# run
window.show()
app.exec_()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

如果需要该样式表的浅色模式,可以通过以下方式引入

from qdarkstyle.light.palette import LightPalette

app.setStyleSheet(qdarkstyle.load_stylesheet(qt_api='PySide6', palette=LightPalette()))
1
2
3

# 3.6 使用PyQt-Fluent-Widgets开源组件库

项目描述:Python 图形界面框架 PyQt5 的组件库。如果使用的是 PySide2、PySide6、PyQt6,去对应分支拉取代码。

项目地址:https://github.com/zhiyiYo/PyQt-Fluent-Widgets (opens new window)

PyQt-Fluent-Widgets

基于该组件库的项目应用:

# 4. 编写应用的业务逻辑

本项目只是一个演示开发流程的简单demo,当时做的时候用的 QDarkStyleSheet 样式表。如果要做的好看,我更推荐使用 PyQt-Fluent-Widgets 组件库,这个开源的比较晚,我也是后来才发现的。

示例的代码已在Github上开源,项目地址:https://github.com/Logistic98/yoyo-ocr-gui (opens new window)

yoyo-ocr-gui
 ├── config.ini
 ├── logo
 │   ├── logo.ico
 │   └── logo.png
 ├── PrScrn.dll
 ├── PrScrn.py
 ├── ui
 │   └── Ui_YoyoOCR.ui
 ├── gol.py
 ├── Ui_YoyoOCR.py
 └── YoyoOCR.py
1
2
3
4
5
6
7
8
9
10
11
12

下面开始真正编写应用的业务逻辑,封装的PaddleOCR识别、破解Google翻译、gTTS文本合成语音、提取关键词、提取文本句子概要的接口我是部署在自己的服务器上,算法接口的代码我已在Github上开源,https://github.com/Logistic98/yoyo-algorithm (opens new window),要注意的是破解Google翻译、gTTS文本合成语音必须部署在境外服务器或者配置代理,否则无法使用。

为了后续打包exe的纯净性,强烈建议对项目单独建一个conda环境进行开发,不然最后打包进去很多没用的,导致文件很大。

# 4.1 创建配置文件

本项目需要自部署五个算法服务,配置示例如下:

新增config.ini:

[FastTextRank]
keyword_url = http://ip:port/fastTextRank/getKeyWord
sentence_url = http://ip:port/fastTextRank/getSentence

[PaddleOCR]
paddleocr_url = http://ip:port/paddle/paddleOcr

[GoogleTranslate]
google_translate_url = http://ip:port/googleTranslate/getTranslateResult

[gTTS]
gtts_url = http://ip:port/gtts/textToVoice

[ImageTempDir]
tmpDir = ./tmp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 4.2 调用微信截图功能的技术实现

调用微信的截图功能较为美观,技术上也容易实现,天若OCR工具也是用的它的截图功能。

Step1:用Everything工具全局搜索 PrScrn.dll 文件,把它放到项目根目录下

Step2:创建截图工具类

新增PrScrn.py:

# -*- coding: utf-8 -*-

import os
from time import sleep
from PIL import Image, ImageGrab
from ctypes import *


def screenshot(img_path):

    if windll.user32.OpenClipboard(None):
        windll.user32.EmptyClipboard()
        windll.user32.CloseClipboard()

    os.system('start /B rundll32 PrScrn.dll PrScrn')

    # 等待截图后放到剪切板
    index = 0
    im = ImageGrab.grabclipboard()
    while not im:
        if index < 500:
            im = ImageGrab.grabclipboard()
            sleep(0.01)
            index = index + 1
        else:
            break

    if isinstance(im, Image.Image):
        im.save(img_path)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

说明:

  • 调用PrScrn.dll时,加上start /B前缀就不会弹出dos窗口,程序不会阻塞。
  • 程序不会阻塞,我们就需要根据剪切板里面是否有数据来判断有没有截图完成,所以需要先清空剪切板。
  • 添加 index 超出次数跳出循环是为了防止点了关闭按钮后始终监听不到剪贴板导致主界面卡死。

# 4.3 主程序编写业务逻辑

翻译功能直接调用破解后的Google翻译接口;OCR识别功能先进行截图转base64,再调用封装的PaddleOCR接口;文本朗读功能是先调用的gTTS接口得到mp3语音,再播放该文件;文本关键词提取和句子概要提取直接调用相应的FastTextRank接口。

修改YoyoOCR.py:

# -*- coding: utf-8 -*-

import base64
import json
import logging
import os
import sys
import uuid

from configparser import ConfigParser
import qdarkstyle
import requests
from PySide6 import QtWidgets
from PySide6.QtCore import QThread, Signal
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QApplication, QWidget
from playsound import playsound

import gol
from Ui_YoyoOCR import Ui_YoyoOCR
from PrScrn import screenshot

logging.basicConfig(filename='logging_yoyo_ocr.log', level=logging.INFO,
                    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)


# 读取配置文件
def read_config(config_path):
    cfg = ConfigParser()
    cfg.read(config_path, encoding='utf-8')
    section_list = cfg.sections()
    config_dict = {}
    for section in section_list:
        section_item = cfg.items(section)
        for item in section_item:
            config_dict[item[0]] = item[1]
    return config_dict


# 请求GoogleTranslateCrack接口
def google_translate_crack(url, text, to_lang):
    # 传输的数据格式
    data = {'text': text, 'to_lang': to_lang}
    # post传递数据
    res = requests.post(url, data=json.dumps(data))
    # 返回结果
    return res


# 请求PaddleOCR接口
def paddle_ocr(url, imgPath):
    # 测试请求
    f = open(imgPath, 'rb')
    # base64编码
    base64_data = base64.b64encode(f.read())
    f.close()
    base64_data = base64_data.decode()
    # 传输的数据格式
    data = {'img': base64_data}
    # post传递数据
    res = requests.post(url, data=json.dumps(data))
    # 删除临时图片文件
    os.remove(imgPath)
    # 返回结果
    return res


# 请求gTTS文本合成语音接口
def gtts(url, text, lang):
    # 传输的数据格式
    data = {'text': text, 'lang': lang}
    # post传递数据
    r = requests.post(url, data=json.dumps(data))
    # 写入文件
    file_path = './tmp/{}.mp3'.format(uuid.uuid1())
    with open(file_path, 'ab') as file:
        file.write(r.content)
        file.flush()
    return file_path


# 请求FastTextRank的关键词提取接口
def get_keyword(url, text, type):
    # 传输的数据格式
    data = {'text': text, 'type': type}
    # post传递数据
    res = requests.post(url, data=json.dumps(data))
    # 返回结果
    return res


# 请求FastTextRank的句子概要提取接口
def get_sentence(url, text):
    # 传输的数据格式
    data = {'text': text}
    # post传递数据
    res = requests.post(url, data=json.dumps(data))
    # 返回结果
    return res


# OCR识别的多线程执行
class WorkThreadOcr(QThread):

    # 自定义信号对象。参数str就代表这个信号可以传一个字符串
    ocrSignal = Signal(str)
    ocrButtonSignal = Signal(str)
    ocrErrorSignal = Signal(str)

    # 初始化函数
    def __int__(self):
        super(WorkThreadOcr, self).__init__()

    # 重写线程执行的run函数,触发自定义信号
    def run(self):
        imgPath = gol.get_value('imgPath')
        # 请求OCR识别接口
        try:
            res = paddle_ocr(config_dict['paddleocr_url'], imgPath)
            if json.loads(res.text)['code'] == 200:
                content_list = json.loads(res.text)['data']
                result = "\n".join(str(i) for i in content_list)
                self.ocrSignal.emit(result)
            else:
                error_text = "请求PaddleOCR接口失败!"
                logging.error(error_text)
                self.ocrErrorSignal.emit(error_text)
        except Exception as e:
            error_text = "请求PaddleOCR接口失败!"
            logger.error(e)
            self.ocrErrorSignal.emit(error_text)

        self.ocrButtonSignal.emit("开始")
        gol.set_value('isRuning', False)


# 文本翻译的多线程执行
class WorkThreadTranslate(QThread):

    # 自定义信号对象。参数str就代表这个信号可以传一个字符串
    translateSignal = Signal(str)
    translateButtonSignal = Signal(str)
    translateErrorSignal = Signal(str)

    # 初始化函数
    def __int__(self):
        super(WorkThreadTranslate, self).__init__()

    # 重写线程执行的run函数,触发自定义信号
    def run(self):
        inputText = gol.get_value('inputText')
        languageCode = gol.get_value('languageCode')
        # 请求Google翻译接口
        try:
            res = google_translate_crack(config_dict['google_translate_url'], inputText, languageCode)
            if json.loads(res.text)['code'] == 200:
                result = json.loads(res.text)['data']
                self.translateSignal.emit(result)
            else:
                error_text = "请求Google翻译接口失败!"
                logging.error(error_text)
                self.translateErrorSignal.emit(error_text)
        except Exception as e:
            error_text = "请求Google翻译接口失败!"
            logger.error(e)
            self.translateErrorSignal.emit(error_text)

        self.translateButtonSignal.emit("开始")
        gol.set_value('isRuning', False)


# 文本合成语音的多线程执行
class WorkThreadVoice(QThread):

    # 自定义信号对象。参数str就代表这个信号可以传一个字符串
    voiceButtonSignal = Signal(str)
    voiceErrorSignal = Signal(str)

    # 初始化函数
    def __int__(self):
        super(WorkThreadVoice, self).__init__()

    # 重写线程执行的run函数,触发自定义信号
    def run(self):
        selectedText = gol.get_value('selectedText')
        languageCode = gol.get_value('languageCode')
        # 对languageCode进行处理
        if languageCode == "zh-cn" or "zh-tw":
            languageCode = "zh-CN"
        # 请求gTTS接口
        try:
            file_path = gtts(config_dict['gtts_url'], selectedText, languageCode)
            if os.path.exists(file_path):
                # 播放语音
                try:
                    # playsound调用时可能出现“指定的设备未打开,或不被 MCI 所识别”报错,需要修改源码
                    playsound(file_path)
                except Exception as e:
                    logger.error(e)
                # 删除临时语音文件
                os.remove(file_path)
            else:
                error_text = "请求gTTS文本合成语音接口失败!"
                logger.error(error_text)
                self.voiceErrorSignal.emit(error_text)
        except Exception as e:
            error_text = "请求gTTS文本合成语音接口失败!"
            logger.error(e)
            self.voiceErrorSignal.emit(error_text)

        self.voiceButtonSignal.emit("开始")
        gol.set_value('isRuning', False)


# 文本关键词提取的多线程执行
class WorkThreadKeyword(QThread):

    # 自定义信号对象。参数str就代表这个信号可以传一个字符串
    keywordSignal = Signal(str)
    keywordButtonSignal = Signal(str)
    keywordErrorSignal = Signal(str)

    # 初始化函数
    def __int__(self):
        super(WorkThreadKeyword, self).__init__()

    # 重写线程执行的run函数,触发自定义信号
    def run(self):
        inputText = gol.get_value('inputText')
        # 请求文本关键词提取接口
        try:
            res = get_keyword(config_dict['keyword_url'], inputText, "array")
            if json.loads(res.text)['code'] == 200:
                result_array = json.loads(res.text)['data']
                result = ""
                for item in result_array:
                    result = result + item[0] + ": " +str(item[1]) + "\n"
                self.keywordSignal.emit(result)
            else:
                error_text = "请求文本关键词提取接口失败!"
                logging.error(error_text)
                self.keywordErrorSignal.emit(error_text)
        except Exception as e:
            error_text = "请求文本关键词提取接口失败!"
            logger.error(e)
            self.keywordErrorSignal.emit(error_text)

        self.keywordButtonSignal.emit("开始")
        gol.set_value('isRuning', False)


# 句子概要提取的多线程执行
class WorkThreadSentence(QThread):

    # 自定义信号对象。参数str就代表这个信号可以传一个字符串
    sentenceSignal = Signal(str)
    sentenceButtonSignal = Signal(str)
    sentenceErrorSignal = Signal(str)

    # 初始化函数
    def __int__(self):
        super(WorkThreadSentence, self).__init__()

    # 重写线程执行的run函数,触发自定义信号
    def run(self):
        inputText = gol.get_value('inputText')
        # 请求句子概要提取接口
        try:
            res = get_sentence(config_dict['sentence_url'], inputText)
            if json.loads(res.text)['code'] == 200:
                result_array = json.loads(res.text)['data']
                result = ""
                for item in result_array:
                    result = result + item + "\n"
                self.sentenceSignal.emit(result)
            else:
                error_text = "请求句子概要提取接口失败!"
                logging.error(error_text)
                self.sentenceErrorSignal.emit(error_text)
        except Exception as e:
            error_text = "请求句子概要提取接口失败!"
            logger.error(e)
            self.sentenceErrorSignal.emit(error_text)

        self.sentenceButtonSignal.emit("开始")
        gol.set_value('isRuning', False)


class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.ui = Ui_YoyoOCR()
        self.ui.setupUi(self)
        self.langDict = {
            "中文简体": "zh-cn",
            "中文繁体": "zh-tw",
            "英语": "en",
            "日语": "ja",
            "韩语": "ko",
            "德语": "de",
            "法语": "fr",
            "西班牙语": "es",
            "意大利语": "it",
            "俄语": "ru"
        }
        self.setWindowIcon(QIcon('./logo/logo.png'))
        # 将信号与槽函数绑定
        self.ui.pushButton.clicked.connect(self.queryContent)

    # 按钮变化
    def buttonStatusDisplay(self, status):
        self.ui.pushButton.setText(status)
        app.processEvents()

    # 错误弹框
    def errorMessageBoxDisplay(self, error_text):
        QtWidgets.QMessageBox.information(self, '提示信息', error_text, QtWidgets.QMessageBox.Ok)

    # 输入框内容输出
    def inputResultDisplay(self, result):
        if result != "":
            self.ui.input.setText(result)

    # 输出框内容输出
    def outputResultDisplay(self, result):
        if result != "":
            self.ui.output.setText(result)

    # 获取下拉框选择的语言
    def getCode(self, lang):
        return self.langDict.get(lang)

    # 与开始按钮绑定的槽函数
    def queryContent(self):

        # 判断是否处在处理中状态
        isRuning = gol.get_value('isRuning')
        if not isRuning:

            gol.set_value('isRuning', True)
            ocrRadio = self.ui.ocrRadioButton.isChecked()
            translateRadio = self.ui.translateRadioButton.isChecked()
            voiceRadio = self.ui.voiceRadioButton.isChecked()
            keywordRadio = self.ui.keywordRadioButton.isChecked()
            sentenceRadio = self.ui.sentenceRadioButton.isChecked()

            if ocrRadio:
                self.ui.input.clear()
                self.ui.output.clear()
                imgPath = str(config_dict['image_temp_dir']) + '/' + str(uuid.uuid1()) + '.jpg'
                screenshot(imgPath)
                if os.path.exists(imgPath):
                    gol.set_value('imgPath', imgPath)
                    # 多线程处理OCR识别接口请求
                    self.ocr_work = WorkThreadOcr(parent=self)  # 实例化线程对象
                    self.ocr_work.start()  # 启动线程
                    self.ocr_work.ocrSignal.connect(self.buttonStatusDisplay('处理中'))
                    self.ocr_work.ocrSignal.connect(self.inputResultDisplay)
                    self.ocr_work.ocrButtonSignal.connect(self.buttonStatusDisplay)
                    self.ocr_work.ocrErrorSignal.connect(self.errorMessageBoxDisplay)
                else:
                    gol.set_value('isRuning', False)

            if translateRadio:
                self.ui.output.clear()
                languageName = self.ui.languageComboBox.currentText()
                languageCode = self.getCode(languageName)
                inputText = self.ui.input.toPlainText()
                if inputText != "":
                    gol.set_value('languageCode', languageCode)
                    gol.set_value('inputText', inputText)
                    # 多线程处理Google翻译接口请求
                    self.translate_work = WorkThreadTranslate(parent=self)  # 实例化线程对象
                    self.translate_work.start()  # 启动线程
                    self.translate_work.translateSignal.connect(self.buttonStatusDisplay('处理中'))
                    self.translate_work.translateSignal.connect(self.outputResultDisplay)
                    self.translate_work.translateButtonSignal.connect(self.buttonStatusDisplay)
                    self.translate_work.translateErrorSignal.connect(self.errorMessageBoxDisplay)
                else:
                    gol.set_value('isRuning', False)

            if voiceRadio:
                languageName = self.ui.languageComboBox.currentText()
                languageCode = self.getCode(languageName)
                # 按照输出选中、输入选中、输出区域的优先级顺序获取文本
                selectedText = self.ui.output.textCursor().selectedText()
                if selectedText == "":
                    selectedText = self.ui.input.textCursor().selectedText()
                if selectedText == "":
                    selectedText = self.ui.output.toPlainText()
                if selectedText != "":
                    gol.set_value('languageCode', languageCode)
                    gol.set_value('selectedText', selectedText)
                    # 多线程处理文本合成语音接口请求
                    self.voice_work = WorkThreadVoice(parent=self)  # 实例化线程对象
                    self.voice_work.start()  # 启动线程
                    self.voice_work.voiceButtonSignal.connect(self.buttonStatusDisplay('处理中'))
                    self.voice_work.voiceButtonSignal.connect(self.buttonStatusDisplay)
                    self.voice_work.voiceErrorSignal.connect(self.errorMessageBoxDisplay)
                else:
                    gol.set_value('isRuning', False)

            if keywordRadio:
                self.ui.output.clear()
                inputText = self.ui.input.toPlainText()
                if inputText != "":
                    gol.set_value('inputText', inputText)
                    # 多线程处理关键词提取接口请求
                    self.keyword_work = WorkThreadKeyword(parent=self)  # 实例化线程对象
                    self.keyword_work.start()  # 启动线程
                    self.keyword_work.keywordSignal.connect(self.buttonStatusDisplay('处理中'))
                    self.keyword_work.keywordSignal.connect(self.outputResultDisplay)
                    self.keyword_work.keywordButtonSignal.connect(self.buttonStatusDisplay)
                    self.keyword_work.keywordErrorSignal.connect(self.errorMessageBoxDisplay)
                else:
                    gol.set_value('isRuning', False)

            if sentenceRadio:
                self.ui.output.clear()
                inputText = self.ui.input.toPlainText()
                if inputText != "":
                    gol.set_value('inputText', inputText)
                    # 多线程处理句子概要提取接口请求
                    self.sentence_work = WorkThreadSentence(parent=self)  # 实例化线程对象
                    self.sentence_work.start()  # 启动线程
                    self.sentence_work.sentenceSignal.connect(self.buttonStatusDisplay('处理中'))
                    self.sentence_work.sentenceSignal.connect(self.outputResultDisplay)
                    self.sentence_work.sentenceButtonSignal.connect(self.buttonStatusDisplay)
                    self.sentence_work.sentenceErrorSignal.connect(self.errorMessageBoxDisplay)
                else:
                    gol.set_value('isRuning', False)


if __name__ == '__main__':
    # 读取配置文件
    config_dict = read_config()
    if not os.path.exists(config_dict['image_temp_dir']):
        os.makedirs(config_dict['image_temp_dir'])
    # 全局变量初始化
    gol._init()
    gol.set_value('isRuning', False)
    # 初始化界面
    app = QApplication(sys.argv)
    window = MainWindow()
    app.setStyleSheet(qdarkstyle.load_stylesheet(qt_api='pyside6'))
    window.show()
    sys.exit(app.exec())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448

新增gol.py:

# -*- coding: utf-8 -*-

def _init():
    global _global_dict
    _global_dict = {}


def set_value(key, value):
    """ 定义一个全局变量 """
    _global_dict[key] = value


def get_value(key, defValue=None):
    """ 获得一个全局变量,不存在则返回默认值 """
    try:
        return _global_dict[key]
    except KeyError:
        return defValue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

注意事项:

[1] 执行耗时长的任务时主界面假死【重要】

问题情境:由于我的服务器没有GPU,执行OCR识别接口时请求比较慢,主界面出现未响应的问题。

问题分析:在GUI程序中,主线程也叫GUI线程,因为它是唯一被允许执行GUI相关操作的线程。对于一些耗时的操作,如果放在主线程中,就会出现界面无法响应的问题。

问题解决:使用多线程模块QThread来解决主界面假死问题,主线程向其他线程传参使用global全局变量来实现,其他线程向主线程返回结果数据通过自定义信号Signal来解决。

[2] 在子线程仍在运行时被销毁的问题【重要】

在实例化线程的时候,带上parent=self参数,例如:self.ocr_work = WorkThreadOcr(parent=self)

[3] 语音文件播放

playsound 声明它已经在WAV和MP3文件上进行了测试,但是它可能也适用于其他文件格式。

$ pip install playsound
1

示例代码如下:

from playsound import playsound
playsound('demo.mp3')
1
2

注意事项:调用时可能出现“指定的设备未打开,或不被 MCI 所识别”报错。原因是windows不支持utf-16编码,需修改playsound源码。

修改\Lib\site-packages\playsound.py文件的源码如下:

def winCommand(*command):
        bufLen = 600
        buf = c_buffer(bufLen)
        #command = ' '.join(command).encode('utf-16') # 1.修改前
        command = ' '.join(command) # 1.修改后
        errorCode = int(windll.winmm.mciSendStringW(command, buf, bufLen - 1, 0))  # use widestring version of the function
        if errorCode:
            errorBuffer = c_buffer(bufLen)
            windll.winmm.mciGetErrorStringW(errorCode, errorBuffer, bufLen - 1)  # use widestring version of the function
            exceptionMessage = ('\n    Error ' + str(errorCode) + ' for command:'
                                #'\n        ' + command.decode('utf-16') + # 2.修改前
                                '\n        ' + command + # 2.修改后
                                '\n    ' + errorBuffer.raw.decode('utf-16').rstrip('\0'))
            logger.error(exceptionMessage)
            raise PlaysoundException(exceptionMessage)
        return buf.value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

注:就playsound.py一个文件,可以把修改后的这个文件也放到项目目录里去。

[4] 工具Logo未正常显示的问题

使用self.setWindowIcon(QIcon('./logo/logo.png'))设置图标,图片格式我就png试验成功了,svg、ico、jpg我这里都没有正常显示。

[5] 修改按钮文字不生效的问题

修改完QPushButton属性后,需要调用QApplicationprocessEvents()方法才会立刻生效。

[6] 如果要禁止文本编辑框的编辑功能

给文本编辑框设置setReadOnly(True)即可。

# 4.4 工具效果展示

工具目前支持文本翻译、语音朗读、OCR识别、关键词提取、概要提取五个功能。

YoyoOCR工具效果展示

# 5. 将GUI工具打包成可执行文件

Pyinstaller与Nuitka比较

速度方面:对于简单的程序,两者的打包速度都是差不多的。不过碰到一个相对来说较大的库时,Nuitka会去编译这个库,导致它的打包速度非常地慢。第三方库的代码是不需要加密的,我们可以通过--nofollow-imports命令不编译引入的库,通过--include-data-dir命令直接复制进来(当然你也可以后期手动复制)。PyInstaller默认是直接复制进来的,不会进行编译。

安全方面:PyInstaller在打包时将py文件编译成pyc文件,这个可以直接被反编译。你也可以通过--key命令在打包时进行加密,但也不安全。如果要做到更高程度的安全性,可以结合Cython来加密打包。Nutka是直接将py文件编译成c语言级别的文件,安全性更胜一筹。

跨平台性:PyInstaller和Nuitka都能在Windows、MacOS和Linxu系统上进行打包操作,并生成相应平台的可执行文件。

我对于打包工具的选择:

对于一些比较简单的项目,使用Nuitka进行打包,安全性好,包也比较小,Pyinstaller留作备用。对于一些比较复杂的项目,比如内部离线封装了深度学习算法,则使用QPT进行打包,虽然包比较大且代码没有加密,但打包方便,专治疑难杂症,Pyinstaller一大堆的依赖缺失问题,Nuitka跑的极慢还可能失败,心态炸裂。

# 5.1 使用Pyinstaller进行打包

# 5.1.1 Pyinstaller简介

项目介绍:将 Python 程序打包成独立的可执行文件。

项目地址:https://github.com/pyinstaller/pyinstaller (opens new window)

依赖安装:pip install pyinstaller

基本用法:pyinstaller main.py -F -w -i logo.ico -n 测试工具 -p D:\Conda4.10.1\envs\conda_env\Lib\site-packages

 -F:打包为单文件可执行程序,没有此参数的话会有很多其他文件跟可执行文件在一起
 -w:是否是窗口程序,不指定的话,程序运行的时候有个控制台黑窗口
 -i:可执行文件的图标
 -n:可执行文件的名称
 -p:打包时要包含的搜索目录,一般不用指定该项(依赖缺失的话再指定)
1
2
3
4
5

# 5.1.2 使用Pyinstaller打包示例程序

使用pyinstaller打包时,经常会出现依赖缺失问题,加--hidden-import PySide6.QtXml就是解决Pyside6依赖找不到的问题。

$ pyinstaller YoyoOCR.py -i ./logo/logo.ico --noconsole --hidden-import PySide6.QtXml 
1

打包后,build文件夹是构建文件,没用删了就行,dist里是输出的打包文件,找到里面的exe即可执行。

注:打包后不能正常使用,需要手动将logo目录、config.ini文件、PrScrn.dll文件挪到打包目录里。

存在的坑:打包时的图标应为16x16分辨率的ico文件,否则可能会出错,可通过以下代码进行转换。

# -*- coding: utf-8 -*-

from PIL import Image


def transfer(input_file, output_file):
    im = Image.open(input_file)
    reim=im.resize((16, 16))  # 宽*高
    reim.save(output_file, dpi=(200.0, 200.0))  # 200.0,200.0分别为想要设定的dpi值


if __name__ == '__main__':
    input_file = "logo.ico"
    output_file = "logo_16x16.ico"
    transfer(input_file, output_file)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 5.2 使用Nuitka进行打包

# 5.2.1 Nuitka简介

项目介绍:Nuitka 是一个用 Python 编写的 Python 编译器。它与 Python 2.6、2.7、3.3、3.4、3.5、3.6、3.7、3.8、3.9 和 3.10 完全兼容。它可以对你的 Python 应用程序进行打包,输出一个可执行文件或扩展模块。

项目地址:https://github.com/Nuitka/Nuitka (opens new window)

依赖安装:pip install nuitka

基本用法:nuitka --standalone --windows-disable-console --low-memory --windows-icon=logo.ico main.py

--standalone    附带依赖环境进行编译     
--windows-disable-console   关闭控制台的黑窗口
--low-memory    使用更少的内存进行编译
--windows-icon-from-ico  指定打包后的图标
--onefile   打包成单个exe文件
1
2
3
4
5

# 5.2.2 使用Nuitka打包示例程序

$ nuitka --standalone --windows-disable-console --windows-icon-from-ico=./logo/logo.ico --low-memory YoyoOCR.py   
1

打包后,*.build文件夹是构建文件,没用删了就行,*.dist里是输出的打包文件,找到里面的exe即可执行。

说明:使用Nuitka不怎么会出现依赖缺失的问题,但打包一些较大的项目时会出现内存问题和编译器错误,导致打包失败,命令里加上--low-memory就是为了解决该问题。

注意事项:打包后不能正常使用,需要手动将logo目录、config.ini文件、PrScrn.dll文件挪到打包目录里。

# 5.3 使用QPT进行打包

# 5.3.1 QPT简介

项目简介:前向式Python环境快捷封装工具,快速将Python打包为EXE并添加CUDA、NoAVX等支持。

项目地址:https://github.com/QPT-Family/QPT (opens new window)

依赖安装:pip install qpt

基本使用:支持撰写打包脚本、使用命令打包两种方式,我这里只介绍第一种。

# -*- coding: utf-8 -*-

# 导入QPT
from qpt.executor import CreateExecutableModule as CEM
from qpt.smart_opt import set_default_pip_source
from qpt.modules.python_env import Python38

# 自定义指定镜像源(默认是清华源:https://pypi.tuna.tsinghua.edu.cn/simple)
set_default_pip_source("http://mirrors.aliyun.com/pypi/simple/")

#                                                    -----关于路径的部分,强烈建议使用绝对路径避免出现问题-----
module = CEM(work_dir="./project",                   # [项目文件夹]待打包的目录,并且该目录下需要有↓下方提到的py文件
             launcher_py_path="./project/main.py",   # [主程序文件]用户启动EXE文件后,QPT要执行的py文件
             save_path="./output",                   # [输出目录]打包后相关文件的输出目录
             requirements_file="./project/requirements.txt", # [Python依赖]此处可填入依赖文件路径,也可设置为auto自动搜索依赖
             hidden_terminal=True,                    # [终端窗口]设置为True后,运行时将不会展示黑色终端窗口
             interpreter_module=Python38(),           # [跨版本编译] 需要预先from qpt.modules.python_env import Python38
             icon="./project/logo/logo.ico")    # [自定义图标文件]支持将exe文件设置为ico/JPG/PNG等格式的自定义图标
# 开始打包
module.make()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

注:其中只有work_dir、launcher_py_path、save_path是必填项。

# 5.3.2 使用QPT打包示例程序

在项目目录外新建打包程序build.py,内容如下:

# -*- coding: utf-8 -*-

# 导入QPT
from qpt.executor import CreateExecutableModule as CEM
from qpt.smart_opt import set_default_pip_source
from qpt.modules.python_env import Python38

# 自定义指定镜像源(默认是清华源:https://pypi.tuna.tsinghua.edu.cn/simple)
set_default_pip_source("http://mirrors.aliyun.com/pypi/simple/")

#                                                    -----关于路径的部分,强烈建议使用绝对路径避免出现问题-----
module = CEM(work_dir="./yoyo-ocr-gui",                   # [项目文件夹]待打包的目录,并且该目录下需要有↓下方提到的py文件
             launcher_py_path="./yoyo-ocr-gui/YoyoOCR.py",   # [主程序文件]用户启动EXE文件后,QPT要执行的py文件
             save_path="./output",                   # [输出目录]打包后相关文件的输出目录
             requirements_file="./yoyo-ocr-gui/requirements.txt", # [Python依赖]此处可填入依赖文件路径,也可设置为auto自动搜索依赖
             hidden_terminal=True,                    # [终端窗口]设置为True后,运行时将不会展示黑色终端窗口
             interpreter_module=Python38(),           # [跨版本编译] 需要预先from qpt.modules.python_env import Python38
             icon="./yoyo-ocr-gui/logo/logo.ico")    # [自定义图标文件]支持将exe文件设置为ico/JPG/PNG等格式的自定义图标
# 开始打包
module.make()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

编写好后python build.py执行程序,在output目录输出Debug和Release两个目录,目录结构如下:

output
 ├── Debug
 │   ├── configs
 │   ├── Debug-打印已安装的软件包列表.cmd
 │   ├── Debug-收集Debug信息.cmd
 │   ├── Debug-进入Python环境.cmd
 │   ├── Debug-进入半虚拟环境.cmd
 │   ├── Debug.exe
 │   ├── opt
 │   ├── Python
 │   ├── resources
 │   └── 使用兼容模式运行.cmd
 └── Release
     ├── configs
     ├── opt
     ├── Python
     ├── resources
     ├── 使用兼容模式运行.cmd
     └── 启动程序.exe
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

其中resources里是源码,Python里是依赖环境,Debug.exe是启动程序。我们先进入Debug目录,启动Debug.exe,第一次执行会比较慢,cmd控制台会显示输出信息,有报错的话排查修复即可(本示例比较简单,并没有乱七八糟的问题,直接就成功了),调试成功后即出现我们的GUI界面,测试一下功能,测试成功后进入Release,如果对依赖环境进行过调试则把Python目录也整个替换过来,之后点击启动程序.exe打包即可,最后交付只需要提供Release。

注意事项:[1] 目录路径里不要包含中文 [2] 如果exe点击后长时间没反应,则需要点击“使用兼容模式运行.cmd”,新设备首次使用时需要用一下,它会自动配置运行环境。

# 5.3.3 QPT打包离线深度学习项目的问题

[1] 依赖类库打包问题

离线深度学习项目涉及的依赖比较多,直接打包会出现很多问题,我这里是选择建一个纯净的conda环境,把依赖都安到conda环境里调试好,注释掉打包脚本里的requirements_file选项,最后把整个conda环境替换掉打包后的Python目录。

[2] 解决“ImportError: DLL load failed while importing win32api: 找不到指定的程序”的问题

$ pip install pypiwin32==223      // 建议在conda环境里安装好,一起拷贝过来
1

然后进入安装路径\Lib\site-packages\pywin32_system32寻找pythoncom38.dllpywintypes38.dll两个文件,拷贝到Python根目录下。

[3] 解决“ImportError: DLL load failed while importing _ssl: The specified module could not be found.”的问题

Step1:确保 libcrypto-1_1.dll 和 libssl-1_1.dll 与 _ssl.pyd 位于同一目录中。--用Everything搜一下,前两者也放到 _ssl.pyd 同一目录里。

Step2:使用与该Python版本对应的_ssl.pyd,我那个conda环境里的_ssl.pyd与Release里的那个不一致,可能是版本没完全对应上,拷贝过来替换掉即可。

# 5.4 对Windows7系统的兼容性适配

以上是没有理会旧版本系统兼容性的,如果要兼容Windows7,需要解决以下等问题,再老的版本不考虑适配。

Step1:需要将用于打包的Python版本降低至3.7.3及以下版本。

Step2:需要将Pyside6降级至Pyside2,代码改下引用库包名即可。

在使用层面上,PySide2/PyQt5和PySide6/PyQt6并无过多的差异,只有一点需要注意,使用PySide6/PyQt6开发的程序在默认情况下,不兼容Windows7系统
1

# 6. Pyside6代码模板

下面整理一下这个示例项目没用到,但又比较常用的Pyside6代码模板及常见情形的解决方案。

# 6.1 功能性代码模板

# 6.1.1 获取文件与目录路径

    # 获取文件路径
    def selectFilePath(self):
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        filePathList, _ = QFileDialog.getOpenFileNames(self, "请选择文件", "", "All Files (*);;Python Files (*.py)", options=options)
        filePath = ",".join(str(i) for i in filePathList)
        self.ui.lineEdit.setText(filePath)

    # 获取目录路径
    def selectDirPath(self):
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        dirPath = QFileDialog.getExistingDirectory(self, "请选择目录", "", options=options)
        self.ui.lineEdit.setText(dirPath)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 6.1.2 拖拽文件获取文件路径

QLineEdit拖拽获取文件路径,支持多文件拖拽。

# -*- coding: utf-8 -*-

import sys
from PySide6.QtWidgets import QLineEdit, QApplication


class MLineEdit(QLineEdit):
    def __init__(self):
        super().__init__()
        self.setAcceptDrops(True)

    def setupUi(self):
        self.lineEdit = MLineEdit()

    def dragEnterEvent(self, e):
        if e.mimeData().hasText():
            e.accept()
        else:
            e.ignore()

    def dropEvent(self, e):
        # 获取文件路径列表
        filePathList = e.mimeData().text().split('\n')
        # 多选文件时,获取的文件路径列表有一个空字符串
        if len(filePathList) >= 2:
            filePathList = filePathList[0:-1]
        # 将文件路径列表转成逗号分隔的字符串,并去除文件地址前缀的特定字符
        filePath = ",".join(str(item).replace('file:///', '', 1) for item in filePathList)
        self.setText(filePath)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MLineEdit()
    window.show()
    sys.exit(app.exec())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

补充说明:对获取的路径常搭配如下方法将其转成符合条件的文件列表,如果这个列表为空,给出弹框提示。

# -*- coding: utf-8 -*-

import os

# 级联遍历⽬录,获取⽬录下的所有⽂件路径
def find_filepaths(dir):
    result = []
    for root, dirs, files in os.walk(dir):
        for name in files:
            filepath = os.path.join(root, name)
            if os.path.exists(filepath):
                result.append(filepath)
    return result


# 筛选出扩展名符合条件的文件路径列表(扩展名不带通配符,extList例如['.jpg', '.png'])
def checkDirOrFilePath(path, extList):

    file_path_list = []
    if os.path.isdir(path):  # 判断路径是否是目录
        file_path_list = find_filepaths(path)
    elif os.path.isfile(path):  # 判断路径是否是文件
        file_path_list.append(path)
    elif path.find(",") != -1:  # 判断路径是否是逗号分隔的多选文件
        file_path_list = path.split(",")
    elif path.find(";") != -1:  # 判断路径是否是分号分隔的多选文件
        file_path_list = path.split(";")

    result_list = []
    for file_path in file_path_list:
        file_dir, file_full_name = os.path.split(file_path)
        file_name, file_ext = os.path.splitext(file_full_name)
        if file_ext in extList:
            result_list.append(file_path)

    return result_list
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 6.1.3 常用的各类信息提示框

包含信息提示对话框、提问对话框、警告对话框、严重错误对话框、关于对话框。

# -*- coding: utf-8 -*-

import sys
from PySide6 import QtCore, QtGui, QtWidgets
from PySide6.QtWidgets import QApplication, QMainWindow, QMessageBox


class Ui_Form(object):

    def setupUi(self, Form):
        Form.setObjectName("Form")
        Form.resize(431, 166)
        self.pushButton = QtWidgets.QPushButton(Form)
        self.pushButton.setGeometry(QtCore.QRect(160, 50, 91, 41))
        font = QtGui.QFont()
        font.setFamily("YaHei Consolas Hybrid")
        font.setPointSize(10)
        self.pushButton.setFont(font)
        self.pushButton.setObjectName("pushButton")
        self.retranslateUi(Form)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "对话框"))
        self.pushButton.setText(_translate("Form", "弹出对话框"))


class MyMainForm(QMainWindow, Ui_Form):
    def __init__(self, parent=None):
        super(MyMainForm, self).__init__(parent)
        self.setupUi(self)
        self.pushButton.clicked.connect(self.showMsg)

    def showMsg(self):
        QMessageBox.information(self, '信息提示对话框', '前方右拐到达目的地')
        QMessageBox.question(self, "提问对话框", "你要继续测试吗?", QMessageBox.Yes | QMessageBox.No)
        QMessageBox.warning(self, "警告对话框", "继续执行会导致系统重启,你确定要继续吗?", QMessageBox.Yes | QMessageBox.No)
        QMessageBox.critical(self, "严重错误对话框", "数组越界,程序异常退出")
        QMessageBox.about(self, "关于对话框", "你的系统是MacOS")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    myWin = MyMainForm()
    myWin.show()
    sys.exit(app.exec())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

关于信息提示框的返回值(Yes == 16384, No == 65536)

# 提示信息对话框--INFO级别
def showInfoMsg(self, message):
    QMessageBox.information(self, '执行信息', message)

# 提示警告对话框--Warn级别
def showWarnMsg(self, message):
    return QMessageBox.warning(self, "执行警告", message, QMessageBox.Yes | QMessageBox.No)

# 提示报错对话框--Error级别
def showErrorMsg(self, message):
    QMessageBox.critical(self, "执行出错", message)

...

flag = self.showWarnMsg("该操作存在xx风险警告")
# Yes == 16384, No == 65536
if flag == 16384:
    # 执行xx操作
    pass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 6.1.4 QProgressBar进度条的实现

进度条选用 QProgressBar 组件来实现,实时进度情况的解决方案之一是通过读写文件来获取。

# -*- coding: utf-8 -*-

import sys
import time
from PySide6 import QtCore, QtWidgets
from PySide6.QtWidgets import *
from PySide6.QtCore import QThread, Signal


class Window(QWidget):

    def __init__(self):
        super(Window, self).__init__()

    def setupUi(self):
        self.setFixedSize(500, 90)
        self.main_widget = QtWidgets.QWidget(self)
        self.progressBar = QtWidgets.QProgressBar(self.main_widget)
        self.progressBar.setGeometry(QtCore.QRect(20, 20, 450, 50))
        # 创建并启用子线程
        self.thread_1 = Worker()
        self.thread_1.progressBarValue.connect(self.copy_file)
        self.thread_1.start()

    def copy_file(self, i):
        self.progressBar.setValue(i)


class Worker(QThread):

    progressBarValue = Signal(int)  # 更新进度条

    def __init__(self):
        super(Worker, self).__init__()

    def run(self):
        for i in range(101):
            time.sleep(0.1)
            self.progressBarValue.emit(i)  # 发送进度条的值 信号


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    testIns = Window()
    testIns.setupUi()
    testIns.show()
    sys.exit(app.exec())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

补充说明:执行进度情况可以通过文件记录(不要使用JSON文件,同时读写时会出问题,可以用ini文件)的方式进行传递,执行模块写入进度,然后主界面给这个进度条单独开一个子线程,轮询读取,达到执行完毕的条件时break。

# 6.1.5 QThread线程的操作

QThread线程的开启、挂起、恢复与停止,配合进度条进行使用。

# -*- coding: utf-8 -*-

import sys
import time

from PySide6.QtCore import QThread, Signal
from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QProgressBar, QPushButton


class Worker(QThread):
    valueChanged = Signal(int)  # 值变化信号
    status = Signal(int)

    def run(self):
        # 循环发送信号
        break_flag = False  # break标志量
        for i in range(1, 101):
            if thread_status == 1:  # 停止信号1
                self.valueChanged.emit(0)
                # 这个地方如果有进程锁,需要在这里解锁,当然也可以直接用break,但是break会执行后面的语句
                print('程序结束')
                return
            elif thread_status == 2:  # 暂停信号2
                while 1:
                    if thread_status == 2:
                        time.sleep(0.1)  # 必须有time.sleep 可以有效降低cpu消耗
                        continue
                    elif thread_status == 1:  # 停止信号1
                        break_flag = True  # break标志量,外层继续执行break
                        break  # 跳出无限循环
                    elif thread_status == 0:  # 恢复信号0
                        break
            else:
                pass
            if (break_flag):  # 跳出外层for循环
                break
            print('value', i)
            self.valueChanged.emit(i)
            time.sleep(0.5)
        self.status.emit(0)


class Window(QWidget):

    def __init__(self, *args, **kwargs):
        super(Window, self).__init__(*args, **kwargs)

        # 垂直布局
        layout = QVBoxLayout(self)
        self.progressBar = QProgressBar(self)
        self.progressBar.setRange(0, 100)
        layout.addWidget(self.progressBar)
        self.startButton = QPushButton('开启线程', self, clicked=self.onStart)
        layout.addWidget(self.startButton)
        self.suspendButton = QPushButton('挂起线程', self, clicked=self.onSuspendThread, enabled=False)
        layout.addWidget(self.suspendButton)
        self.resumeButton = QPushButton('恢复线程', self, clicked=self.onResumeThread, enabled=False)
        layout.addWidget(self.resumeButton)
        self.stopButton = QPushButton('终止线程', self, clicked=self.onStopThread, enabled=False)
        layout.addWidget(self.stopButton)

        # 子线程
        self._thread = Worker(self)
        self._thread.valueChanged.connect(self.progressBar.setValue)

    def onStart(self):
        global thread_status
        thread_status = 0
        self._thread.start()  # 启动线程
        self.startButton.setEnabled(False)
        self.suspendButton.setEnabled(True)
        self.stopButton.setEnabled(True)

    def onSuspendThread(self):
        global thread_status
        thread_status = 2
        print('挂起线程')
        self.suspendButton.setEnabled(False)
        self.resumeButton.setEnabled(True)

    def onResumeThread(self):
        global thread_status
        thread_status = 0
        print('恢复线程')
        self.suspendButton.setEnabled(True)
        self.resumeButton.setEnabled(False)

    def onStopThread(self):
        global thread_status
        thread_status = 1
        self.startButton.setEnabled(True)
        self.resumeButton.setEnabled(False)
        self.progressBar.setRange(0, 100)
        print('终止线程')
        self.stopButton.setEnabled(False)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = Window()
    w.show()
    sys.exit(app.exec())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102

# 6.2 展示性代码模板

# 6.2.1 登录页面与主页面的跳转

login.py

# -*- coding: utf-8 -*-

import qdarkstyle
from PySide6 import QtCore, QtWidgets
import sys
from PySide6.QtGui import QIcon
from qdarkstyle import LightPalette

from account import account_list
from entrance import MainWindow


class LoginWidget(QtWidgets.QMainWindow):
    def __init__(self):
        super(LoginWidget, self).__init__()
        # 设定登录页面大小
        self.resize(433, 300)
        self.centralwidget = QtWidgets.QWidget(self)
        self.setCentralWidget(self.centralwidget)
        _translate = QtCore.QCoreApplication.translate
        self.setWindowTitle(_translate("centralwidget", "测试工具"))
        # 添加组控件
        self.groupBox = QtWidgets.QGroupBox(self.centralwidget)
        self.groupBox.setGeometry(QtCore.QRect(50, 60, 361, 135))
        self.groupBox.setTitle('用户登录')
        self.usernameLabel = QtWidgets.QLabel(self.groupBox)
        self.usernameLabel.setGeometry(QtCore.QRect(30, 30, 48, 16))
        self.usernameLabel.setMaximumSize(QtCore.QSize(16777215, 20))
        self.usernameLabel.setText('用户')
        self.passwordLabel = QtWidgets.QLabel(self.groupBox)
        self.passwordLabel.setGeometry(QtCore.QRect(30, 80, 48, 16))
        self.passwordLabel.setMaximumSize(QtCore.QSize(16777215, 20))
        self.passwordLabel.setText('密码')
        self.usernameEdit = QtWidgets.QLineEdit(self.groupBox)
        self.usernameEdit.setGeometry(QtCore.QRect(120, 30, 200, 20))
        self.usernameEdit.setMaximumSize(QtCore.QSize(200, 20))
        self.passwordEdit = QtWidgets.QLineEdit(self.groupBox)
        self.passwordEdit.setGeometry(QtCore.QRect(120, 80, 200, 20))
        self.passwordEdit.setMaximumSize(QtCore.QSize(200, 20))
        self.pushButton = QtWidgets.QPushButton(self.centralwidget)
        self.pushButton.setGeometry(QtCore.QRect(110, 220, 75, 25))
        self.pushButton.setMaximumSize(QtCore.QSize(16777215, 25))
        self.pushButton.setText('确定')
        # 确定按钮绑定回车快捷键
        self.pushButton.setShortcut('Enter')
        self.pushButton_2 = QtWidgets.QPushButton(self.centralwidget)
        self.pushButton_2.setGeometry(QtCore.QRect(240, 220, 75, 25))
        self.pushButton_2.setMaximumSize(QtCore.QSize(16777215, 25))
        self.pushButton_2.setText('取消')

        # 禁止窗口最大最小化
        self.setWindowFlags(QtCore.Qt.WindowCloseButtonHint)
        # 禁止拉伸窗口
        self.setFixedSize(self.width(), self.height())
        # 密码隐藏
        self.passwordEdit.setEchoMode(QtWidgets.QLineEdit.Password)
        # 连接信号和槽函数,实现功能:点击取消按钮,退出应用
        self.connect(self.pushButton_2, QtCore.SIGNAL('clicked()'), self.closeWin)
        # 连接信号和函数,实现功能:点击确定按钮,进入主窗口
        self.pushButton.clicked.connect(self.openMain)

        # 设置图标
        self.setWindowIcon(QIcon('./logo/logo.png'))

    def openMain(self):
        account_exist_flag = False
        flag = False
        for account in account_list:
            username = account[0]['username']
            password = account[1]['password']
            if self.usernameEdit.text() == username and self.passwordEdit.text() == password:
                flag = True
            if self.usernameEdit.text() == username:
                account_exist_flag = True
        if flag:
            self.mw = MainWindow()
            self.mw.show()
            self.hide()
        else:
            if account_exist_flag:
                # 密码错误,弹出提示框
                QtWidgets.QMessageBox.information(self, u'提示', u'密码错误,请重新输入', QtWidgets.QMessageBox.Ok)
                print('密码错误,请重新输入')
            else:
                # 账号不存在错误,弹出提示框
                QtWidgets.QMessageBox.information(self, u'提示', u'账号不存在,请重新输入', QtWidgets.QMessageBox.Ok)
                print('账号不存在,请重新输入')

    def closeWin(self):
        self.close()


if __name__ == '__main__':
    # 初始化界面
    app = QtWidgets.QApplication(sys.argv)
    gui = LoginWidget()
    app.setStyleSheet(qdarkstyle.load_stylesheet(qt_api='PySide6', palette=LightPalette()))
    gui.show()
    sys.exit(app.exec())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99

account.py

# -*- coding: utf-8 -*-

account_list = [
    [{'username': 'admin'}, {'password': '123456'}],
    [{'username': 'user'}, {'password': '654321'}]
]
1
2
3
4
5
6

# 6.2.2 QStackedWidget按钮选项卡切换

添加若干个用于切换的按钮,在body处选用 QStackedWidget 组件,然后在各个 QStackedWidget 层分别配置界面(在QtDesigner里有切换箭头),最后给按钮绑定上切换属性即可。

UI_ButtonTab.py

# -*- coding: utf-8 -*-

from PySide6 import QtCore, QtWidgets


class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        Form.resize(308, 282)
        self.gridLayout = QtWidgets.QGridLayout(Form)
        self.gridLayout.setObjectName("gridLayout")
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.modelOnePushButton = QtWidgets.QPushButton(Form)
        self.modelOnePushButton.setObjectName("modelOnePushButton")
        self.horizontalLayout.addWidget(self.modelOnePushButton)
        self.modelTwoPushButton = QtWidgets.QPushButton(Form)
        self.modelTwoPushButton.setObjectName("modelTwoPushButton")
        self.horizontalLayout.addWidget(self.modelTwoPushButton)
        self.gridLayout.addLayout(self.horizontalLayout, 0, 0, 1, 1)
        self.stackedWidget = QtWidgets.QStackedWidget(Form)
        self.stackedWidget.setObjectName("stackedWidget")
        self.modelOnePage = QtWidgets.QWidget()
        self.modelOnePage.setObjectName("modelOnePage")
        self.modelOneLabel = QtWidgets.QLabel(self.modelOnePage)
        self.modelOneLabel.setGeometry(QtCore.QRect(100, 50, 100, 100))
        self.modelOneLabel.setObjectName("modelOneLabel")
        self.stackedWidget.addWidget(self.modelOnePage)
        self.modelTwoPage = QtWidgets.QWidget()
        self.modelTwoPage.setObjectName("modelTwoPage")
        self.modelTwoLabel = QtWidgets.QLabel(self.modelTwoPage)
        self.modelTwoLabel.setGeometry(QtCore.QRect(100, 50, 100, 100))
        self.modelTwoLabel.setObjectName("modelTwoLabel")
        self.stackedWidget.addWidget(self.modelTwoPage)
        self.gridLayout.addWidget(self.stackedWidget, 1, 0, 1, 1)

        self.retranslateUi(Form)
        self.stackedWidget.setCurrentIndex(1)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "测试按钮选项卡切换"))
        self.modelOnePushButton.setText(_translate("Form", "模块1"))
        self.modelTwoPushButton.setText(_translate("Form", "模块2"))
        self.modelOneLabel.setText(_translate("Form", "模块2的body体"))
        self.modelTwoLabel.setText(_translate("Form", "模块1的body体"))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

main.py

# -*- coding: utf-8 -*-

import sys
from PySide6.QtWidgets import QApplication, QWidget

from UI_ButtonTab import Ui_Form


class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.ui = Ui_Form()
        self.ui.setupUi(self)

        # 将信号与槽函数绑定
        self.ui.modelOnePushButton.clicked.connect(self.displayModelOnePage)
        self.ui.modelTwoPushButton.clicked.connect(self.displayModelTwoPage)

    # 选项卡切换--模块1
    def displayModelOnePage(self):
        self.ui.stackedWidget.setCurrentIndex(1)

    # 选项卡切换--模块2
    def displayModelTwoPage(self):
        self.ui.stackedWidget.setCurrentIndex(0)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

补充说明:

[1] 如果需要选中时高亮,可在切换函数里添加按钮背景色,例如:

self.ui.modelOnePushButton.setStyleSheet("background-color:rgb(84, 104, 122)")   # 高亮颜色
self.ui.modelTwoPushButton.setStyleSheet("background-color:rgb(69, 83, 100)")    # 正常颜色
1
2

[2] 可使用currentIndex()方法获取当前处于哪个模块,拖拽获取文件路径的时候可能会用到。

self.ui.stackedWidget.currentIndex()
1

# 6.2.3 QTableWidget表格组件展示数据

可直接使用QTableWidget组件实现表格形式展示数据。

# -*- coding: utf-8 -*-

import sys
from PySide6.QtGui import QColor
from PySide6.QtWidgets import (QApplication, QWidget, QTableWidget, QTableWidgetItem, QVBoxLayout)

colors = [("Red", "#FF0000"),
          ("Green", "#00FF00"),
          ("Blue", "#0000FF"),
          ("Black", "#000000"),
          ("White", "#FFFFFF"),
          ("Electric Green", "#41CD52"),
          ("Dark Blue", "#222840"),
          ("Yellow", "#F9E56d")]


def get_rgb_from_hex(code):
    code_hex = code.replace("#", "")
    rgb = tuple(int(code_hex[i:i+2], 16) for i in (0, 2, 4))
    return QColor.fromRgb(rgb[0], rgb[1], rgb[2])


class tableData(QWidget):
    def __init__(self, parent=None):
        super(tableData, self).__init__(parent)
        self.setWindowTitle("My TableData")

        self.table = QTableWidget()
        self.table.setRowCount(len(colors))
        self.table.setColumnCount(len(colors[0]) + 1)
        self.table.setHorizontalHeaderLabels(["Name", "Hex Code", "Color"])

        for i, (name, code) in enumerate(colors):
            item_name = QTableWidgetItem(name)
            item_code = QTableWidgetItem(code)
            item_color = QTableWidgetItem()
            item_color.setBackground(get_rgb_from_hex(code))
            self.table.setItem(i, 0, item_name)
            self.table.setItem(i, 1, item_code)
            self.table.setItem(i, 2, item_color)

        layout = QVBoxLayout(self)
        layout.addWidget(self.table)


if __name__ == "__main__":
    app = QApplication([])
    window = tableData()
    window.show()
    sys.exit(app.exec())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

# 6.2.4 QLabel展示图片文件

可以使用QLabel组件展示图片文件,由于该组件没有边框,因此我一般习惯于在这个的上层添加个QFrame组件。

# -*- coding: utf-8 -*-

import sys
from PySide6 import QtGui, QtWidgets
from PySide6.QtWidgets import QApplication


class PicLabel(QtWidgets.QLabel):
    def __init__(self, imgfile, parent=None):
        super(PicLabel, self).__init__(parent)
        # 得到图片标签,并加载图片
        pix = QtGui.QPixmap(imgfile)
        self.setPixmap(pix)
        # 调整控件到图片大小
        self.resize(pix.width(), pix.height())


if __name__ == '__main__':
    imgfile = './images/test.png'
    app = QApplication(sys.argv)
    a = PicLabel(imgfile)
    a.show()
    sys.exit(app.exec())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 6.2.5 给界面添加背景图片

将图片作为背景之前,建议先调整一下图片的透明度

# -*- coding: UTF-8 -*-

from PIL import Image


# 调整图片的透明度,用于制作背景
def addTransparency(img, factor):
    img = img.convert('RGBA')
    img_blender = Image.new('RGBA', img.size, (0, 0, 0, 0))
    img = Image.blend(img_blender, img, factor)
    return img


if __name__ == '__main__':
    img = Image.open("original_background.jpeg")
    img = addTransparency(img, factor=0.7)
    img.save("background.png")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

如果需要根据单一图标平铺来制作背景,可以使用如下脚本(大小缩放、倾斜度、透明度、间隔、数量都是可以调的):

# -*- coding: utf-8 -*-

from PIL import Image
import math


def modify_transparency(img, factor):
    img = img.convert('RGBA')
    img_blender = Image.new('RGBA', img.size, (0, 0, 0, 0))
    img = Image.blend(img_blender, img, factor)
    return img


def skew_image(img, skew_angle):
    width, height = img.size
    xshift = abs(skew_angle) * width
    new_width = width + int(round(xshift))
    img = img.transform((new_width, height), Image.AFFINE, (1, skew_angle, -xshift if skew_angle > 0 else 0, 0, 1, 0), Image.BICUBIC)
    return img


def create_collage_single_image(image_path, total_images, per_row, gap, scale_factor, transparency_factor, skew_angle):
    if total_images <= 0:
        raise ValueError("Total images must be a positive number.")

    # Load and modify the image
    image = Image.open(image_path)

    # Resize, adjust transparency, and skew
    img_width, img_height = image.size
    new_size = (int(img_width * scale_factor), int(img_height * scale_factor))
    image = image.resize(new_size)
    image = modify_transparency(image, transparency_factor)
    image = skew_image(image, skew_angle)

    # Recalculate the image size after adjustments
    img_width, img_height = image.size

    # Calculate total rows and columns
    rows = math.ceil(total_images / per_row)
    columns = min(per_row, total_images)

    # Calculate the size of the collage
    collage_width = columns * img_width + (columns - 1) * gap
    collage_height = rows * img_height + (rows - 1) * gap

    # Create a new blank image for the collage with transparent background
    collage = Image.new('RGBA', (collage_width, collage_height), (0, 0, 0, 0))

    # Paste images into the collage
    x_offset, y_offset = 0, 0
    for i in range(total_images):
        collage.paste(image, (x_offset, y_offset), image)
        x_offset += img_width + gap
        if (i + 1) % per_row == 0:
            x_offset = 0
            y_offset += img_height + gap

    return collage


if __name__ == '__main__':
    single_image_path = "bg_item.jpg"
    collage = create_collage_single_image(single_image_path, total_images=42, per_row=7, gap=180,
                                          scale_factor=0.1, transparency_factor=0.2, skew_angle=-0.3)
    collage.save("background.png", "PNG")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

以下介绍两种在Pyside里设置背景图片的方式

[1] 使用 QSS 设置窗口背景

import sys
from PySide6.QtWidgets import QMainWindow, QApplication

app = QApplication(sys.argv)
win = QMainWindow()
win.setWindowTitle("界面背景图片设置")
win.resize(350,  250)
win.setObjectName("MainWindow")
win.setStyleSheet("#MainWindow{border-image:url(./background/background.png);}")
# win.setStyleSheet("#MainWindow{background-color: blue}")
win.show()
sys.exit(app.exec())
1
2
3
4
5
6
7
8
9
10
11
12

[2] 实现 paintEvent,使用 QPainter 绘制背景

# -*- coding: UTF-8 -*-

import sys
from PySide6.QtWidgets import QApplication, QWidget
from PySide6.QtGui import QPixmap, QPainter


class Winform(QWidget):
    def __init__(self, parent=None):
        super(Winform, self).__init__(parent)
        self.setWindowTitle("paintEvent设置背景图片")

    def paintEvent(self, event):
        painter = QPainter(self)
        pixmap = QPixmap("./background/background.png")
        # 绘制窗口背景,平铺到整个窗口,随着窗口改变而改变
        painter.drawPixmap(self.rect(), pixmap)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    form = Winform()
    form.show()
    sys.exit(app.exec())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 6.2.6 嵌入pyecharts绘制的动态图像

pyecharts是python对echarts的封装,用于绘制可动态交互的图表,生成的图表可以嵌入到pyside中的QWebEngineView组件中。

Faker是用于生成模拟数据作为演示,实际绘图把其换成实际数据列表即可。

# -*- coding: utf-8 -*-

import sys
from PySide6.QtWidgets import *
from PySide6.QtWebEngineWidgets import QWebEngineView
from pyecharts.charts import Bar, Line
from pyecharts.faker import Faker
import pyecharts.options as opts


class stats(QWidget):
    def __init__(self):
        super(stats, self).__init__()
        self.initUI()

    def initUI(self):
        self.setWindowTitle("pyecharts绘图示例")
        self.setGeometry(100, 100, 800, 600)
        layout = QHBoxLayout()
        self.scrolla = QScrollArea()
        self.button = QPushButton('绘图')
        layout.addWidget(self.scrolla)
        layout.addWidget(self.button)
        self.setLayout(layout)
        self.button.clicked.connect(self.drawImage)

    # 绘制图像,Faker是用于生成模拟数据作为演示,实际绘图把其换成实际数据列表即可。
    def drawImage(self):
        bro = QWebEngineView()

        # 绘制条形图
        # c = (Bar()
        #      .add_xaxis(Faker.days_attrs)
        #      .add_yaxis("商家", Faker.days_values)
        #      .set_global_opts(title_opts=opts.TitleOpts(title="Bar-DataZoom(slider-水平)"), datazoom_opts=opts.DataZoomOpts())
        # )

        # 绘制折线图
        c = (
            Line()  # 生成line类型图表
            .add_xaxis(Faker.choose())
            .add_yaxis('数据1', Faker.values())
            .add_yaxis('数据2', Faker.values())
            .set_global_opts(title_opts=opts.TitleOpts(title='折线图基本示例'))
        )

        bro.setHtml(c.render_embed())
        self.scrolla.setWidget(bro)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    main = stats()
    main.show()
    exit(app.exec())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

# 6.2.7 QtWebEngineWidgets嵌入网页

可以使用 PyQtWebEngine 嵌入外部网页。

# -*- coding: utf-8 -*-

import sys
from PySide6.QtCore import QUrl
from PySide6.QtWidgets import QApplication, QMainWindow
from PySide6.QtWebEngineWidgets import QWebEngineView


class Browser(QMainWindow):
    def __init__(self):
        super().__init__()
        self.browser = QWebEngineView()
        
        # 禁用浏览器缓存
        # profile = QWebEngineProfile.defaultProfile()
        # profile.setHttpCacheType(QWebEngineProfile.NoCache)
        # profile.setPersistentCookiesPolicy(QWebEngineProfile.NoPersistentCookies)
        # profile.setPersistentStoragePath('')
        
        self.browser.setUrl(QUrl("http://your.test.domain"))
        self.setCentralWidget(self.browser)
        self.showMaximized()
        self.resize(1080, 720)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    QApplication.setApplicationName('Open Browser')
    window = Browser()
    app.exec()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

注意事项:

  • 这种方式会有浏览器缓存,你的页面修改后,集成进来的却没有更新,清理Chrome的缓存是没用的,可以考虑上述注释掉的代码禁用浏览器环境。
  • 这种方式嵌入的网页无法处理页面跳转,当这个页面存在跳转时,点击跳转按钮时没反应的。

QtWebEngineWidgets嵌入网页

# 7. 参考资料

[1] Python Qt库PySide和PyQt哪个好? from 完美代码 (opens new window)

[2] Python 图形界面框架 PySide6 使用及避坑指南 from 左小米z (opens new window)

[3] 亲测有效,一招解决错误:This application failed to start because not Qt platform plugin could be initialized. from CSDN (opens new window)

[4] PySide2学习总结(八)Qt的信号(Signal)和槽(Slot) from CSDN (opens new window)

[5] Python 打包成 exe,太大了该怎么解决?from 知乎 (opens new window)

[6] PySide6:专业的 Python GUI 库 from 程序员灯塔 (opens new window)

[7] 用Python写了一个图像文字识别OCR工具 from 腾讯云 (opens new window)

[8] 使用QSS美化PyQt5界面 from muzing's (opens new window)

[9] pyqt截图功能 from 简书 (opens new window)

[10] python调用微信截图功能 from 程序员宝宝 (opens new window)

[11] PyQt5中多线程模块QThread使用方法 from 1024搜 (opens new window)

[12] PyQt之科学使用线程处理耗时任务以及多线程通信注意事项 from 代码先锋网 (opens new window)

[13] python QPushButton 点击后修改文本不生效问题 from CSDN (opens new window)

[14] Linux之Python代码打包工具Nuitka使用说明 from CSDN (opens new window)

[15] qt自适应窗口到方法,qt layout适应变化,qt界面控件自动拉伸 from CSDN (opens new window)

[16] Qt栅格化布局问题,所有显示全跑到左上角 from CSDN (opens new window)

[17] pyside2登录页面与主页面 from CSDN (opens new window)

[18] Python 截图库,替代 Linux 上的 Pillow ImageGrab 模块 from Github (opens new window)

[19] 比较PyInstaller和Nuitka from ICode9 (opens new window)

[20] ImportError: DLL load failed while importing win32api: 找不到指定的程序。 from CodeTD (opens new window)

[21] ImportError: DLL load failed while importing _ssl: 找不到指定的模块。from 程序员宝宝 (opens new window)

[22] ImportError: DLL load failed while importing _ssl: The specified module could not be found. from Python Issues (opens new window)

[23] PyQt5如何实现实时更新的进度条 from 腾讯云 (opens new window)

[24] pyqt5线程的启动停止终止的两种方法 from CSDN (opens new window)

[25] Python如何安全地挂起、恢复、终止Qthread线程 from 代码先锋网 (opens new window)

[26] 使用Qt实现从资源管理器中拖动文件到应用程序的功能 from 老鱼的博客 (opens new window)

[27] PySide6使用表格小部件显示数据 from CodeAntenna (opens new window)

[28] PySide2&6快速入门一 from 知乎 (opens new window)

[29] PyQt5 - QThread:在线程仍在运行时被销毁 from stackoverflow (opens new window)

[30] 使用PyArmor保护Python代码 from Escape (opens new window)

[31] QMessageBox的返回值 from CSDN (opens new window)

[32] PyQt5 系统化学习: 设置窗口背景 from 简书 (opens new window)

[33] Pyinstaller打包时提示Unable to open icon file xx.ico from CSDN

Last Updated: 4/3/2024, 10:13:17 AM