本文演示了 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
安装的是最新版: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.
# 2.2 在PyCharm中配置External Tools
为了后续方便使用,建议在 PyCharm 的 External Tools 中,配置下图的三个工具。配置好之后可以在 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$
# 2.2.2 配置PyUIC
Name: PyUIC
Program : 你的安装路径\Scripts\pyside6-uic.exe
Arguments:$FileName$ -o $FileNameWithoutExtension$.py
Working directory: $FileDir$
# 2.2.3 配置PyRCC
Name: PyRCC
Program : 你的安装路径\Scripts\pyside6-rcc.exe
Arguments:$FileName$ -o $FileNameWithoutExtension$_rc.py
Working directory: $FileDir$
# 3. 使用QtDesigner进行界面设计
# 3.1 QtDesigner界面构成
Tools——External Tools——QtDesigner 打开界面设计器,整个工作界面的构成:
- 左侧的“Widget Box”就是各种可以自由拖动的组件
- 中间的“MainWindow – weather.ui”窗体就是画布
- 右上方的”Object Inspector”可以查看当前ui的结构
- 右侧中部的”Property Editor”可以设置当前选中组件的属性
- 右下方的”Resource Browser”可以添加各种素材,比如图片,背景等
最终生成.ui文件(实质上就是XML格式的文件),可直接使用,也可以通过PyUIC工具转换成.py文件。
# 3.2 信号与槽基本概念解释
在Qt中使用信号和槽(Signals and Slots)来实现其他编程工具包的“回调”功能。信号和槽机制是 Qt 的主要特性并且也很有可能是它与其他框架特性区别最大的部分。当一个特定的事件发生时,信号会被发送出去,而槽则被用来接收信号。
# 3.3 设计GUI界面并导出ui文件
在QtDesigner里,文件——新建——Widget,根据自己的需求设计并配置界面,这里不太好描述,不会的话就自行查阅视频资料学习一下吧。Ctrl+R快捷键可以预览效果,设计完成后导出一个ui文件。
注:自动适应窗口变化可通过“在窗体空白处右键——布局——栅格布局”来实现。
# 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()
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())
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())
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
样式风格:
基本用法:
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_()
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
样式风格:
基本用法:
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_()
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()))
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组件库的模板,实现了通用的基本功能:https://github.com/Cheukfung/pyqt-fluent-widgets-template (opens new window)
- 基于 PyQt5 的跨平台音乐播放器:https://github.com/zhiyiYo/Groove (opens new window)
- 基于强化学习的五子棋机器人:https://github.com/zhiyiYo/Alpha-Gobang-Zero (opens new window)
# 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
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
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)
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('./config.ini')
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())
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
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
示例代码如下:
from playsound import playsound
playsound('demo.mp3')
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
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
属性后,需要调用QApplication
的processEvents()
方法才会立刻生效。
[6] 如果要禁止文本编辑框的编辑功能
给文本编辑框设置setReadOnly(True)
即可。
# 4.4 工具效果展示
工具目前支持文本翻译、语音朗读、OCR识别、关键词提取、概要提取五个功能。
# 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:打包时要包含的搜索目录,一般不用指定该项(依赖缺失的话再指定)
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
打包后,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)
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文件
2
3
4
5
# 5.2.2 使用Nuitka打包示例程序
$ nuitka --standalone --windows-disable-console --windows-icon-from-ico=./logo/logo.ico --low-memory YoyoOCR.py
打包后,*.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()
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()
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
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环境里安装好,一起拷贝过来
然后进入安装路径\Lib\site-packages\pywin32_system32
寻找pythoncom38.dll
和pywintypes38.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系统
# 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)
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())
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
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())
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
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())
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())
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())
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'}]
]
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体"))
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())
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)") # 正常颜色
2
[2] 可使用currentIndex()方法获取当前处于哪个模块,拖拽获取文件路径的时候可能会用到。
self.ui.stackedWidget.currentIndex()
# 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())
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())
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")
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")
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())
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())
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())
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()
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的缓存是没用的,可以考虑上述注释掉的代码禁用浏览器环境。
- 这种方式嵌入的网页无法处理页面跳转,当这个页面存在跳转时,点击跳转按钮时没反应的。
# 7. Orange工具的二次开发
# 7.1 Orange工具简介
# 7.1.1 基本介绍
Orange3 是一款基于Python的数据挖掘和可视化工具,它提供了丰富的数据分析、机器学习和数据挖掘算法,同时也支持可视化分析和交互式数据探索。它提供了友好的界面和丰富的示例工程,使得新手用户也可以快速上手,同时也支持Python脚本,可以满足高级用户的需求。
- 项目地址:https://github.com/biolab/orange3 (opens new window)
- 官方网站:https://orangedatamining.com (opens new window)
- 组件目录:https://orangedatamining.com/widget-catalog (opens new window)
- 组件使用:https://orange3.readthedocs.io/projects/orange-visual-programming/en/latest/index.html (opens new window)
# 7.1.2 重要概念
Orange中的重要概念包括:
- 组件(widget)将界面显示、交互控制、逻辑处理等封装为一体的独立功能单元。
- 类别(category)将具有相似功能的组件组织在一起的逻辑分组。
- 节点(node)组件的运行时实例,一个组件可以有多个不同的实例。
- 输入(input)组件可接收处理的不同类型数据。
- 输出(output)组件对输入数据完成处理后向外发布的不同类型数据。
- 连接(link)将一个组件的输出连接到另一个组件的输入,类似于Unix的管道机制,输出和输入必须是同种或兼容的数据类型。
- 工作流(workflow)将组件按照数据的上下游关系串联起来实现特定任务目标的计算模型。
- 画布(canvas)组件布局和工作流设置的绘图区域。
- 控制区域(controlArea)用于对组件的运行参数进行配置。
- 内容区域(mainArea)用于展示该组件对输入数据的处理结果,通常为图表方式。
# 7.2 运行项目
实验环境:Macbook Pro 2021,M1 pro芯片,16G内存,macOS Ventura13.3.1系统、Conda 22.11.1
由于我这里需要二次开发,因此采用了从源码启动的方式,如果只是为了使用,可以直接从 官网 (opens new window) 下载 release 版本。
# 7.2.1 准备代码与环境
从Github上拉取项目代码,创建并激活Conda虚拟环境。
$ git clone https://github.com/biolab/orange3.git
$ conda create -n conda_orange_env python=3.8
$ conda activate conda_orange_env
2
3
安装项目依赖:
$ pip3 install PyQt6 PyQt6-WebEngine
$ pip3 install -r requirements.txt
2
其中 requirements-core.txt 里的 openTSNE 依赖安装失败,暂时先把它注释掉,之后单独手动安装。
ERROR: Failed building wheel for openTSNE
Failed to build openTSNE
ERROR: Could not build wheels for openTSNE, which is required to install pyproject.toml-based projects
2
3
使用源码自己构建 openTSNE 依赖。
$ git clone https://github.com/pavlin-policar/openTSNE.git
$ cd openTSNE
$ pip3 install .
2
3
# 7.2.2 编译动态链接库
在项目里的Orange目录内,有一些 .pyx、.c、.cpp 的文件,我们需要把它编译成动态链接库(Linux/MacOS是.so,Win是.pyd)才可以使用。
$ find "./Orange" -type f \( -name "*.pyx" -o -name "*.c" -o -name "*.cpp" \) -print
./Orange/data/_valuecount.pyx
./Orange/data/_contingency.pyx
./Orange/data/_variable.pyx
./Orange/data/_io.pyx
./Orange/classification/_tree_scorers.pyx
./Orange/classification/_simple_tree.c
./Orange/distance/_distance.pyx
./Orange/preprocess/_discretize.pyx
./Orange/preprocess/_relieff.pyx
./Orange/projection/_som.pyx
./Orange/widgets/utils/_grid_density.cpp
2
3
4
5
6
7
8
9
10
11
12
13
这里我在根目录写了一个 compile.py 脚本统一进行编译。
# -*- coding: utf-8 -*-
import os
import platform
import shutil
import sys
from setuptools import setup, Extension
from Cython.Build import cythonize
import numpy as np
# 在运行前模拟命令行参数
sys.argv = [sys.argv[0], 'build_ext', '--inplace']
# 定义扩展
extensions = [
Extension("_valuecount", ["./Orange/data/_valuecount.pyx"], include_dirs=[np.get_include()]),
Extension("_contingency", ["./Orange/data/_contingency.pyx"], include_dirs=[np.get_include()]),
Extension("_variable", ["./Orange/data/_variable.pyx"], include_dirs=[np.get_include()]),
Extension("_io", ["./Orange/data/_io.pyx"], include_dirs=[np.get_include()]),
Extension("_tree_scorers", ["./Orange/classification/_tree_scorers.pyx"], include_dirs=[np.get_include()]),
Extension("_simple_tree", ["./Orange/classification/_simple_tree.c"], include_dirs=[np.get_include()]),
Extension("_distance", ["./Orange/distance/_distance.pyx"], include_dirs=[np.get_include()]),
Extension("_discretize", ["./Orange/preprocess/_discretize.pyx"], include_dirs=[np.get_include()]),
Extension("_relieff", ["./Orange/preprocess/_relieff.pyx"], include_dirs=[np.get_include()]),
Extension("_som", ["./Orange/projection/_som.pyx"], include_dirs=[np.get_include()]),
Extension("_grid_density", ["./Orange/widgets/utils/_grid_density.cpp"], include_dirs=[np.get_include()])
]
# 运行setup
setup(
ext_modules=cythonize(extensions)
)
# 构建目录和扩展映射
build_base = './build'
lib_directories = [d for d in os.listdir(build_base) if d.startswith("lib") and os.path.isdir(os.path.join(build_base, d))]
build_directory = build_base + "/" + lib_directories[0]
# 确定操作系统类型
ext = ".so" if platform.system() != "Windows" else ".pyd"
# 获取所有编译的文件
compile_files = [f for f in os.listdir(build_directory) if f.endswith(ext)]
# 定义目标目录映射
target_dict = {
"_valuecount": "./Orange/data/",
"_contingency": "./Orange/data/",
"_variable": "./Orange/data/",
"_io": "./Orange/data/",
"_tree_scorers": "./Orange/classification/",
"_simple_tree": "./Orange/classification/",
"_distance": "./Orange/distance/",
"_discretize": "./Orange/preprocess/",
"_relieff": "./Orange/preprocess/",
"_som": "./Orange/projection/",
"_grid_density": "./Orange/widgets/utils/"
}
# 移动动态链接库文件
for compile_file in compile_files:
# 去掉文件扩展名,并切分
split_name = compile_file.split('.')[0].split('_')
# 重建基础名称,考虑可能的多个下划线部分
base_name = '_' + '_'.join(split_name[1:])
# 根据字典获取目标目录
target_directory = target_dict.get(base_name)
if target_directory:
source_path = os.path.join(build_directory, compile_file)
target_path = os.path.join(target_directory, compile_file)
shutil.move(source_path, target_path)
else:
print(f"未找到目标目录: {base_name}")
# 递归删除build目录
shutil.rmtree("./build")
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
安装Cython和numpy依赖,然后将其编译成动态链接库。
$ pip3 install Cython numpy
$ python3 compile.py
2
注:如果没有编译动态链接库,或者与系统版本不匹配,启动时会有类似于如下的报错。
from Orange.data import _variable
ImportError: cannot import name '_variable' from partially initialized module 'Orange.data' (most likely due to a circular import)
2
# 7.2.3 启动Orange3项目
首次启动项目建议输出调试信息,以排查解决组件报错。
$ python3 -m Orange.canvas // 启动项目
$ python3 -m Orange.canvas -l 4 --no-splash // 启动项目,跳过启动屏幕窗口,并输出更多调试信息
2
若想要使用Python文件进行启动,可以在根目录新建一个main.py。
# -*- coding: utf-8 -*-
from Orange.canvas.__main__ import main
if __name__ == "__main__":
main()
2
3
4
5
6
注意事项:
[1] 如果启动时,控制台报错 ModuleNotFoundError: No module named 'Orange.version',执行setup.py文件,即可在./Orange
目录下创建出 version.py。
# THIS FILE IS GENERATED FROM ORANGE SETUP.PY
short_version = '3.36.0'
version = '3.36.0'
full_version = '3.36.0.dev0+'
git_revision = ''
release = False
if not release:
version = full_version
short_version += ".dev"
2
3
4
5
6
7
8
9
10
[2] 如果启动后,左侧工具栏不显示,并且控制台出现如下报错,执行 pip3 install --upgrade scikit-learn
命令即可。
ImportError: cannot import name 'METRIC_MAPPING64' from 'sklearn.metrics._dist_metrics' (/Users/xxx/miniforge3/envs/conda_orange_env/lib/python3.8/site-packages/sklearn/metrics/_dist_metrics.cpython-38-darwin.so)
首次启动较慢,耐心等待一会儿,启动成功后的界面如下:
# 7.3 组件概述及使用
# 7.3.1 组件功能概述
从 Orange 主界面图上可见,Orange 左边栏默认提供了 6 个组件集,组件的图标也很直观地展示了组件的功能。
- 数据( Data ):常见格式数据的导入、数据库数据读取、数据保存、抽样、创建透视表、转换、设置相关系数等。
- 转换( Transform ):主要用于数据预处理和特征工程,包括数据清洗、标准化、特征选择等功能。
- 可视化( Visualize ):树状图、箱体图、散点图、直方图、热图等。
- 模型( Model ):各类机器学习模型,如 KNN、随机森林、SVM、逻辑回归、神经网络、贝叶斯,模型加载和保存。
- 评估( Evaluate ):交叉验证、抽样程序、ROC 曲线等。
- 无监督算法( Unsupervised ):各类数据降维算法,如 PCA、t-SNE;各类无监督算法模型,如 K-Means 分析、层次聚类分析等。
在菜单栏的Options——Add-ons...处,可以添加更多的组件。
# 7.3.2 官方使用示例
项目启动时,在欢迎页处有Examples,点开之后有官方提供的使用示例,加载后直接就能用。
更多的应用实例,可见官网的 Screenshots 页面。
# 7.4 自定义组件开发
除了使用Orange自带的数据导入、预处理、建模、评估、可视化等一系列基础组件之外,用户还可以自己开发组件并集成进Orange平台。
# 7.4.1 以插件形式动态集成
Orange采用了PyQt的技术架构,自定义组件详见 开发文档 (opens new window),示例工程详见 orange3-example-addon (opens new window),其项目结构如下所示:
.
├── MANIFEST.in
├── README.md
├── README.pypi
├── doc
│ ├── Makefile
│ ├── conf.py
│ ├── index.rst
│ ├── make.bat
│ └── widgets
│ ├── icons
│ │ └── mywidget.png
│ └── mywidget.md
├── orangecontrib
│ ├── __init__.py
│ └── example
│ ├── __init__.py
│ ├── tests
│ │ ├── __init__.py
│ │ └── test_example.py
│ ├── tutorials
│ │ ├── __init__.py
│ │ └── example_tutorial.ows
│ └── widgets
│ ├── __init__.py
│ ├── icons
│ │ ├── category.svg
│ │ └── mywidget.svg
│ └── mywidget.py
├── screenshot.png
└── setup.py
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
orangecontrib 是 Orange 官方建议的包的顶级名称,可采用也可不采用,example 是用户创建的包的名称,包含两个目录,widgets 中放置组件的源代码文件,tutorials 中放置该组件示例教程的 ows 文件。
自定义组件并不强制放在Orange项目工程内部,可以做成独立的安装包发布并安装。通过Python提供的插件机制,Orange启动时自动发现自定义组件并装载,其原理可参见 该文档 (opens new window)。在setup.py中对应的插件部分的配置代码如下所示,在ENTRY_POINTS
中设置了3个插件注入点,最重要的就是orange.widgets
用于装载自定义组件。
ENTRY_POINTS = {
# Entry points that marks this package as an orange add-on. If set, addon will
# be shown in the add-ons manager even if not published on PyPi.
'orange3.addon': (
'example = orangecontrib.example',
),
# Entry point used to specify packages containing tutorials accessible
# from welcome screen. Tutorials are saved Orange Workflows (.ows files).
'orange.widgets.tutorials': (
# Syntax: any_text = path.to.package.containing.tutorials
'exampletutorials = orangecontrib.example.tutorials',
),
# Entry point used to specify packages containing widgets.
'orange.widgets': (
# Syntax: category name = path.to.package.containing.widgets
# Widget category specification can be seen in
# orangecontrib/example/widgets/__init__.py
'Examples = orangecontrib.example.widgets',
),
# Register widget help
"orange.canvas.help": (
'html-index = orangecontrib.example.widgets:WIDGET_HELP_PATH',)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
组件源文件mywidget.py的代码如下所示:
from Orange.data import Table
from Orange.widgets import gui
from Orange.widgets.settings import Setting
from Orange.widgets.widget import OWWidget, Input, Output, Msg
class MyWidget(OWWidget):
# Widget needs a name, or it is considered an abstract widget
# and not shown in the menu.
name = "Hello World"
description = "Tell me more about yourself."
icon = "icons/mywidget.svg"
priority = 100 # where in the widget order it will appear
keywords = ["widget", "data"]
want_main_area = False
resizing_enabled = False
label = Setting("")
class Inputs:
# specify the name of the input and the type
data = Input("Data", Table)
class Outputs:
# if there are two or more outputs, default=True marks the default output
data = Output("Data", Table, default=True)
# same class can be initiated for Error and Information messages
class Warning(OWWidget.Warning):
warning = Msg("My warning!")
def __init__(self):
super().__init__()
self.data = None
self.label_box = gui.lineEdit(
self.controlArea, self, "label", box="Text", callback=self.commit)
@Inputs.data
def set_data(self, data):
if data:
self.data = data
else:
self.data = None
def commit(self):
self.Outputs.data.send(self.data)
def send_report(self):
# self.report_plot() includes visualizations in the report
self.report_caption(self.label)
if __name__ == "__main__":
from Orange.widgets.utils.widgetpreview import WidgetPreview # since Orange 3.20.0
WidgetPreview(MyWidget).run()
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
注:mywidget.py 可以脱离平台单独运行及调试,也可以添加进 Orange 平台里运行。
把之前项目编译了动态链接库的Orange目录拷贝到项目根目录,使用如下命令将组件添加进去。
$ pip3 install -e .
之后重新启动项目,即可看到自定义组件被添加进去了。
$ python3 -m Orange.canvas -l 4 --no-splash
注:如果要卸载组件,也是在菜单栏的Options——Add-ons...处取消勾选进行卸载。
# 7.4.2 在平台里固定集成
Step1:在./Orange/widgets
目录新建一个 example 文件夹,将 orange3-example-addon (opens new window) 示例工程的 widgets 部分整个拷贝进来,修改__init__.py
如下:
NAME = "example"
ID = "orange.widgets.example"
DESCRIPTION = """示例工具模块。"""
ICON = "icons/category.svg"
BACKGROUND = "#FFD39F"
PRIORITY = 0
2
3
4
5
6
7
8
9
10
11
Step2:再在 ./Orange/widgets/__init__.py
文件里引入 orange.widgets.example 即可。
import os
import sysconfig
import pkg_resources
from orangecanvas.registry import CategoryDescription
from orangecanvas.registry.utils import category_from_package_globals
import orangewidget.workflow.discovery
# Entry point for main Orange categories/widgets discovery
def widget_discovery(discovery):
# type: (orangewidget.workflow.discovery.WidgetDiscovery) -> None
dist = pkg_resources.get_distribution("Orange3")
pkgs = [
"Orange.widgets.data",
"Orange.widgets.example", # 添加这行代码
"Orange.widgets.visualize",
"Orange.widgets.model",
"Orange.widgets.evaluate",
"Orange.widgets.unsupervised",
]
for pkg in pkgs:
discovery.handle_category(category_from_package_globals(pkg))
# manually described category (without 'package' definition)
discovery.handle_category(
CategoryDescription(
name="Transform",
priority=1,
background="#FF9D5E",
icon="data/icons/Transform.svg",
package=__package__,
)
)
discovery.handle_category(
CategoryDescription(
name="Orange Obsolete",
package=__package__,
hidden=True,
)
)
for pkg in pkgs:
discovery.process_category_package(pkg, distribution=dist)
discovery.process_widget_module("Orange.widgets.obsolete.owtable")
WIDGET_HELP_PATH = (
("{DEVELOP_ROOT}/doc/visual-programming/build/htmlhelp/index.html", None),
(os.path.join(sysconfig.get_path("data"),
"share/help/en/orange3/htmlhelp/index.html"),
None),
("https://docs.biolab.si/orange/3/visual-programming/", ""),
)
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
之后重新启动项目,即可看到自定义组件被添加进去了。
$ python3 -m Orange.canvas -l 4 --no-splash
# 7.5 橙现智能Orange3汉化版
# 7.5.1 汉化版项目简介
橙现智能基于Orange3汉化及二次开发,仍然免费开源,是一个简单易用的可视化人工智能应用工具。
# 7.5.2 启动Orange3汉化版
拉取项目代码,依旧使用之前的 conda_orange_env 环境
$ git clone https://github.com/szzyiit/orange3.git
按理说下载下来之后,编译完动态链接库,再使用 pip3 install orange3-zh
命令安装汉化依赖就行了。
但我这里安不上这个依赖,就把它下载、解压、重命名为 Orange3-zh 放到项目根目录里了。
$ wget http://mirrors.aliyun.com/pypi/packages/17/46/c8dd43aad6835b5cb458bc2e099ef8ef182cf2c1d0997f99aa23a38a330e/Orange3-zh-3.33.1.tar.gz
修改 ./Orange/widgets/__init__.py
文件里对 Orange3-zh 的引用。
# Entry point for main Orange categories/widgets discovery
def widget_discovery(discovery):
# type: (orangewidget.workflow.discovery.WidgetDiscovery) -> None
# dist = pkg_resources.get_distribution("Orange3-zh")
dist = pkg_resources.working_set.add_entry('Orange3-zh') # 修改成这样
pkgs = [
"Orange.widgets.data",
"Orange.widgets.visualize",
"Orange.widgets.model",
"Orange.widgets.evaluate",
"Orange.widgets.unsupervised",
"Orange.widgets.reinforcement",
"Orange.widgets.deepLearning",
]
for pkg in pkgs:
discovery.handle_category(category_from_package_globals(pkg))
# manually described category (without 'package' definition)
# discovery.handle_category(
# CategoryDescription(
# name="变换(Transform)",
# priority=1,
# background="#FF9D5E",
# icon="data/icons/Transform.svg",
# package=__package__,
# )
# )
for pkg in pkgs:
discovery.process_category_package(pkg, distribution=dist)
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
按照之前的流程进行启动,把缺失的依赖补全即可。
# 8. 参考资料
[1] Python Qt库PySide和PyQt哪个好? from 完美代码 (opens new window)
[2] Python 图形界面框架 PySide6 使用及避坑指南 from 左小米z (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)
[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
[34] 易上手的数据挖掘、可视化与机器学习工具:Orange介绍 from 夜行人 (opens new window)
[35] Orange3-二:使用Orange3对图片进行数据挖掘 from 知乎 (opens new window)
[36] Orange-自定义组件开发(一) from 知乎 (opens new window)
[37] 使用Orange3做差异化表达分析 from 知乎 (opens new window)
[38] Orange3的Distributions、Rank和Sieve Diagram from 简书 (opens new window)
[39] Python | Orange:不用写代码,也能做数字化审计(一) from 墨天轮 (opens new window)
[40] Orange3-三: 用Orange3处理文本数据 from 知乎 (opens new window)
[41] Orange数据挖掘工具介绍 from 台部落 (opens new window)
[42] 使用orange进行聚类分析 from CSDN (opens new window)
[43] pyinstaller打包Orange3 from CSDN (opens new window)
[44] 用于构建Orange应用程序安装程序的脚本 from Github (opens new window)
[45] Orange:一个基于 Python 的数据挖掘和机器学习平台 from CSDN (opens new window)