PySide2/PySide6、PyQt5/PyQt6:都是基于Qt 的Python库,可以形象地这样说,PySide2 是Qt的 亲儿子(Qt官方开发的) , PyQt5 是Qt还没有亲儿子之前的收的 义子 (Riverbank Computing这个公司开发的,有商业版权限制)。
两个库的使用 对程序员来说,差别很小:它们的调用接口几乎一模一样。如果你的程序是PyQt5开发的,通常只要略作修改,比如把导入的名字从 PyQt5 换成 PySide2 就行了。反之亦然。
对应:PySide2-PyQt5
、PySide6-PyQt6
安装pyside2:(推荐使用)
pip install pyside2 -i https://pypi.douban.com/simple/`
安装pyside6:
pip install pyside6
安装pyqt5:
pip install pyqt5-tools
安装pyqt6:
pip install pyqt6-tools
问题:运行pyside2代码时发生初始化问题
解决:
方法1 :在代码头中加入环境变量
import sys, os
import PySide2
from PySide2.QtWidgets import *
dirname = os.path.dirname(PySide2.__file__)
plugin_path = os.path.join(dirname, 'plugins', 'platforms')
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = plugin_path
print(plugin_path)
方法2:
把 PySide2 或者 PyQt5 安装在解释器目录下的 \plugins\platforms 目录添加到环境变量Path中。
比如,我的环境就是把这个路径加到 环境变量 Path 中
D:\Anaconda\envs\virtual_pytorch\Library\plugins\platforms
1 基础—(控件、动作、封装)
1.1 控件 (QtWidgets)
QApplication
提供了整个图形界面程序的底层管理功能,比如:初始化、程序入口参数的处理,用户事件(对界面的点击、输入、拖拽)分发给各个对应的控件等等。所以,我们必须在任何界面控件对象创建前,先创建它。
app = QApplication([])
QMainWindow
、QPlainTextEdit
、QPushButton
是3个控件类,分别对应界面的主窗口、文本框、按钮。他们都是控件基类对象QWidget的子类。要在界面上 创建一个控件 ,就需要在程序代码中 创建 这个 控件对应类 的一个 实例对象。
window = QMainWindow()
window.resize(500, 400)
window.move(300, 310)
window.setWindowTitle('窗口名')
textEdit = QPlainTextEdit(window)
textEdit.setPlaceholderText("文本框名")
textEdit.move(10,25)
textEdit.resize(300,350)
button = QPushButton('按钮名', window)
button.move(380,80)
window.show() # 显示窗口
app.exec_() # 进入QApplication的事件处理循环,接收用户的输入事件
在 Qt 系统中,控件(widget)是 层层嵌套
的,除了最顶层的控件,其他的控件都有父控件。QPlainTextEdit、QPushButton 实例化时,都有一个参数window
,就是指定它的父控件对象 是 window 对应的QMainWindow 主窗口。而 实例化 QMainWindow 主窗口时,却没有指定 父控件, 因为它就是最上层的控件了。
控件对象的 move 方法
决定了这个控件显示的位置
。
控件对象的 resize 方法
决定了这个控件显示的大小
。
放在主窗口的控件,要能全部显示在界面上, 必须使用window.show()
最后 ,通过app.exec_()
,进入QApplication的事件处理循环,接收用户的输入事件,并且分配给相应的对象去处理。
常用控件介绍:https://www.byhy.net/tut/py/gui/qt_05_1/
1.2 动作 (signal 和 slot)
在 Qt 系统中, 当界面上一个控件被操作
时,比如 被点击、被输入文本、被鼠标拖拽等, 就会发出 信号signal
。就是表明一个事件发生
了(比如被点击、被输入文本)。
我们可以预先在代码中指定处理这个 signal 的函数,这个处理 signal 的函数
叫做 槽slot
。
比如上节例子中,我们可以像下面这样定义一个slot函数:
def handleCalc():
QMessageBox.about(window, '关于', '点击按钮1次')
QMessageBox
是的信息提示框对象,他的about方法可以单独弹出一个提示框。
然后, 指定 如果 发生了button 按钮被点击 的事情,需要让 handleCalc 来处理,像这样
button.clicked.connect(handleCalc)
用QT的术语来解释上面这行代码,就是:把 button
被点击(clicked)
产生的信号signal
, 连接(connect)
到了 handleCalc
这样的一个处理函数slot
上。
大白话就是:让 handleCalc 来 处理 button 被 点击的操作。
1.3 封装
上面的代码把控件对应的变量名全部作为全局变量。
如果要设计稍微复杂一些的程序,就会出现太多的控件对应的变量名。
而且这样也不利于 代码的模块化。
所以,我们通常应该把窗口和其包含的控件(self)
以及处理函数
,对应的代码 全部封装到类中
:
import sys, os
import PySide2
from PySide2.QtWidgets import *
dirname = os.path.dirname(PySide2.__file__)
plugin_path = os.path.join(dirname, 'plugins', 'platforms')
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = plugin_path
print(plugin_path)
class Stats():
def __init__(self):
self.window = QMainWindow()
self.window.resize(500, 400)
self.window.move(300, 300)
self.window.setWindowTitle('窗口名')
self.textEdit = QPlainTextEdit(self.window)
self.textEdit.setPlaceholderText("文本框默认内容")
self.textEdit.move(10, 25)
self.textEdit.resize(300, 350)
self.button = QPushButton('按钮名', self.window)
self.button.move(380, 80)
self.button.clicked.connect(self.handleCalc)
def handleCalc(self):
QMessageBox.about(self.window, '关于', '点击按钮1次')
app = QApplication([])
stats = Stats()
stats.window.show()
app.exec_()
2 界面布局
2.1 Qt Designer
我们可以用QT界面生成器
Qt Designer ,拖拖拽拽
就可以直观的创建出程序大体的界面。
怎么运行这个工具呢?
Windows下,运行 Python安装目录下 Scripts\pyside2-designer.exe
这个可执行文件
如果你安装的是pyqt5, 运行 Python安装目录下 Scripts\pyqt5designer.exe 这个可执行文件。
主界面区域介绍:
工具箱 区域
:提供GUI界面开发使用的各种基本控件,如单选框、文本框等。可以拖动到新创建的主程序界面。
主界面 区域
:用户放置各种从工具箱拖过来的各种控件。模板选项中最常用的就是Widget(通用窗口)和MainWindow(主窗口)。二者区别主要是Widget窗口不包含菜单栏、工具栏等。可以分别创建对比看看。
对象查看器 区域
:查看主窗口放置的对象列表。
属性编辑器 区域
: 提供对窗口、控件、布局的属性编辑功能。比如修改控件的显示文本、对象名、大小等。
信号/槽编辑器 区域
:编辑控件的信号和槽函数,也可以添加自定义的信号和槽函数。
Widget Box控件工具箱介绍:
窗体(Window)
:
Mian Window(包含菜单栏的窗体)
Widget(不包含菜单栏的窗体)
显示控件(Display Widget)
:
Lable:文本标签,显示文本,可以用来标记控件。PySide2 中QLabel类可以显示文字和图片,选择了QLabel加Timer播放。QLabel显示的格式必须是Qimage,但是用cv2读取的帧格式是numpy array,必须把它转换成为Qimage。
Text Browser:显示文本控件。用于后台命令执行结果显示。
输入控件(Input Widget)
:提供与用户输入交互
Line Edit:单行文本框,输入单行字符串。控件对象常用函数为Text() 返回文本框内容,用于获取输入。setText() 用于设置文本框显示。
Text Edit:多行文本框,输入多行字符串。控件 对象常用函数同Line Edit控件。
Combo Box:下拉框列表。用于输入指定枚举值。
控件按钮(Buttons)
:供用户选择与执行
Push Button:命令按钮。常见的确认、取消、关闭等按钮就是这个控件。clicked信号一定要记住。clicked信号就是指鼠标左键按下然后释放时会发送信号,从而触发相应操作。
Radio Button:单选框按钮。
Check Box:多选框按钮。
布局(Layout)
:不同布局方式可以嵌套;比如A是水平布局,B是垂直布局 ,可以选中AB再做一种布局方式;参考讲解Layout
布局有三种方法:
1、先在左侧空间栏Layouts中的某种布局方式拖动到主窗口;再将组件拖入布局窗中;
2、先将组件拖入主窗中并选中;再点击工具栏中的某种布局方式,如下图。
3、先将组件拖入主窗中并选中;鼠标右键>布局,选择其中一种布局方式。
所有组件的布局完成之后,如下图红色框位置,顶层还未布局;选中顶层,然后对其进行布局。可以进行水平、垂直、网格的任意一种(效果一样)。至此已经完成了所有布局,按CTRL+R预览的时候拉大窗口,所有组件会随着窗口的变化而变化。
间隔(Spacers)
:实现部件布局排列美观,如自动靠左对齐或者靠右对齐的方式来显示等。Spacers部件除了名字之外只有三个属性,分别是orientation、sizeType和sizeHint。
属性编辑器介绍:
有从父类Widget继承而来的属性,也有自己控件独特的属性,比较方便。
常用的属性介绍:
名称 | 含义 |
---|---|
objectName | 控件对象名称 |
geometry | 相应左上角为原点的坐标与宽和高(x,y),x,y |
sizePolicy | 控件大小的策略 |
minimumSize | 最小的宽和高 |
maximumSize | 最大的宽和高 |
font | 字体 |
cursor | 光标 |
…… | …… |
导出设计:
通过 Qt Designer 设计的界面,最终是生成XML格式
的ui界面定义文件
。
2.2 动态加载UI文件(推荐)
有了界面定义文件,我们的Python程序就可以从文件中加载UI定义,并且动态 创建一个相应的窗口对象。
如下:
from PySide2.QtUiTools import QUiLoader
from PySide2.QtWidgets import *
dirname = os.path.dirname(PySide2.__file__)
plugin_path = os.path.join(dirname, 'plugins', 'platforms')
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = plugin_path
print(plugin_path)
class Stats:
def __init__(self):
# 从文件中加载UI定义
self.ui = QUiLoader().load('你的UI文件名.ui')
# 从 UI 定义中动态 创建一个相应的窗口对象
# 注意:里面的控件对象也成为窗口对象self.ui的属性了
# 比如 self.ui.button , self.ui.textEdit
self.ui.button.clicked.connect(self.handleCalc)
def handleCalc(self):
QMessageBox.about(self.window, '关于', '点击按钮1次')
app = QApplication([])
stats = Stats()
stats.ui.show()
app.exec_()
2.3 转化UI文件为Python代码(不推荐)
先把UI文件直接转化为包含界面定义的Python代码文件,然后在你的程序中使用定义界面的类,执行如下的命令 把UI文件直接转化为包含界面定义的Python代码文件。
pyside2-uic main.ui > ui_main.py
然后在你的代码文件中这样使用定义界面的类
from PySide2.QtWidgets import QApplication,QMainWindow
from ui_main import Ui_MainWindow
# 注意 这里选择的父类 要和你UI文件窗体一样的类型
# 主窗口是 QMainWindow, 表单是 QWidget, 对话框是 QDialog
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# 使用ui文件导入定义界面类
self.ui = Ui_MainWindow()
# 初始化界面
self.ui.setupUi(self)
# 使用界面定义的控件,也是从ui里面访问
self.ui.webview.load('http://www.baidu.com')
app = QApplication([])
mainw = MainWindow()
mainw.show()
app.exec_()
通常采用动态加载比较方便,因为改动界面后,不需要转化,直接运行,特别方便。
3 显示样式
要让产品更好看一些,通常就是指定界面元素的 显示样式 。比如指定颜色、字体、间距。Qt有种定义界面显示样式的方法,称之为 Qt Style Sheet ,简称 QSS。
如果在设计师界面上 最上层的 MainWindow
对象 styleSheet
属性指定如下的内容
QPushButton {
color: red ;
font-size:15px;
}
就会发现,所有的按钮上的文字都变成了红色的,并且字体变大了。注意这个指定界面元素 显示样式的 语法,由 selector
和 declaration
组成。
- 花括号前面的 部分,比如示例中的 QPushButton 称之为 selector。
- 花括号后面的 部分,称之为 Properties样式属性。
选择器selector:花括号前面的 部分称之为 selector,用来 告诉Qt 哪些特征的元素
是你要设定显示效果的。
样式属性declaration:在Qt Designer
中设置样式。
背景
:可以指定某些元素的背景色,像这样
QTextEdit { background-color: yellow }
颜色可以使用红绿蓝数字,像这样
QTextEdit { background-color: #e7d8d8 }
也可以像这样指定背景图片
QTextEdit {
background-image: url(gg03.png);
}
边框
:可以像这样指定边框
border:1px solid #1d649c;
其中,1px 是边框宽度,solid 是边框线为实线, 也可以是 dashed(虚线) 和 dotted(点)
比如
*[myclass=bar2btn]:hover{
border:1px solid #1d649c;
}
边框可以指定为无边框
border:none
字体、大小、颜色
:可以这样指定元素的 文字字体、大小、颜色
*{
font-family:微软雅黑;
font-size:15px;
color: #1d649c;
}
宽度、高度
:可以这样指定元素的 宽度、高度
QPushButton {
width:50px;
height:20px;
}
margin、padding
:
可以这样指定元素的 元素的 margin,分别指定了元素的上右下左margin。
QTextEdit {
margin:10px 11px 12px 13px
}
也可以使用 margin-top, margin-right, margin-bottom, margin-left 单独指定 元素的上右下左margin。
QTextEdit {
margin:10px 50px;
padding:10px 50px;
}
4 后台线程 与 信号
4.1 界面阻塞问题
我们 点击按钮
执行槽函数,如果服务端接收处理的比较慢,就会导致槽函数要比较长的时间才能返回。假设10秒钟后,才接收到响应消息,这时候,界面就会 僵死
10秒钟。
这是因为,我们现在的代码都是在主线程中执行
的。其中最末尾的代码:
app.exec_()
其实会让主线程进入一个死循环
,循环不断的处理 用户操作的事件。
当我们点击按钮后,Qt的 核心代码就会接受到这个 点击事件,并且调用相应的 slot函数去处理。
因为我们代码做了这样的设置
# 信号处理
self.ui.buttonSend.clicked.connect(self.槽函数)
如果这个槽函数
很快能接收到 服务端的响应,那么 槽函数 就可以很快的返回。
返回后, 整个程序又进入到 app.exec_() 里面接收各种 事件,并且调用相应的函数去处理。界面就不会僵死,因为所有的操作界面的事件,都能得到及时的处理。
但是,如果这个 槽函数 要很长时间才能返回,这段时间内,整个程序就停在 槽函数内的某段代码处
,自然就没有机会去处理其他的用户操作界面的事件了,当然程序就僵死了。
4.2 子线程处理
典型的一种解决方法就是使用多线程
去处理。
为了防止槽函数
卡在 发送请求 或 处理数据的操作
上,我们可以使用Python多线程,将发送请求 或 处理数据的操作
封装成一个函数,在槽函数中创建新线程
,然后调用封装好的函数。
这样,通过创建新的线程去执行[发送请求 或 处理数据]的方法,服务器响应再慢,也只会在新线程中阻塞
,主线程启动新线程后,主线程就继续执行后面的代码
,返回继续运行Qt的事件循环处理 ,可以响应用户的操作
,就不会僵死了。
from threading import Thread
def 槽函数(self):
正常操作;
# 创建新的线程去执行发送方法,
# 服务器慢,只会在新线程中阻塞
# 不影响主线程
thread = Thread(target = self.新线程请求或处理函数,
args= (参数列表)
)
thread.start()
# 新线程入口函数
def 新线程请求或处理函数(self,参数列表):
原来槽函数中 发送请求 或 处理数据的操作;
4.3 子线程发信号更新界面
Qt建议: 只在主线程中操作界面 。
在另外一个线程直接操作界面,可能会导致意想不到的问题,比如:输出显示不全,甚至程序崩溃。
但是,我们确实经常需要在子线程中 更新界面。比如子线程是个爬虫,爬取到数据显示在界面上。
怎么办呢?
这时,推荐的方法是使用信号
。
前面我们曾经看到过 各种 Qt 控件可以发出信号,比如 被点击、被输入等。
我们也可以自定义信号
,只要这个类继承QObject类
,就能发出自己定义的各种Qt信号,具体做法如下:
-
自定义一个
Qt 的 QObject类
,里面封装一些自定义的 Signal信号
,一种信号定义为 该类的 一个静态属性
,值为Signal 实例对象即可。可以定义 多个 Signal静态属性,对应这种类型的对象可以发出的 多种 信号。
(如下MySignals类有text_print 信号 和 update_table 信号)
注意:Signal实例对象的信号参数类型
,就是 发出信号对象时,传递的参数数据类型。因为Qt底层是C++开发的,必须指定类型。(如下的QTextBrowser 和 str类型) -
定义
主线程
执行的函数处理Signal信号
(通过connect方法) -
在
新线程需要操作界面的时候
,就通过自定义对象
发出 信号
通过该信号对象
的emit方法
发出信号, emit方法的参数 传递必要的数据。参数类型 遵循 定义Signal时,指定的类型。 -
主线程
信号处理函数,被触发执行,获取Signal里面的参数,执行必要的更新界面操作
from PySide2.QtWidgets import QApplication, QTextBrowser
from PySide2.QtUiTools import QUiLoader
from threading import Thread
from PySide2.QtCore import Signal,QObject
# 自定义信号源对象类型,一定要继承自 QObject
class MySignals(QObject):
# 定义一种text_print信号,两个参数 类型分别是: QTextBrowser 和 字符串
# 调用 emit方法 发信号时,传入参数 必须是这里指定的 参数类型
text_print = Signal(QTextBrowser,str)
# 还可以定义其他种类的update_table信号
update_table = Signal(str)
# 实例化
global_ms = MySignals()
# GUI类
class Stats:
def __init__(self):
self.ui = QUiLoader().load('main.ui')
# 自定义信号MySignals类的处理函数
global_ms.text_print.connect(self.printToGui)
# 自定义信号处理函数
def printToGui(self,fb,text):
fb.append(str(text))
fb.ensureCursorVisible()
# 产生text_print信号时的界面操作函数
def task1(self):
def threadFunc():
# 通过Signal 的 emit 触发执行 主线程里面的处理函数
# emit参数和定义Signal的数量、类型必须一致
global_ms.text_print.emit(self.ui.infoBox1, '输出内容')
thread = Thread(target = threadFunc )
thread.start()
5 智云平台实战
智云平台是一个集成活体检测、人脸识别、关键点检测、人像分割、疲劳检测、视线追踪、微表情识别、危险驾驶动作识别、火灾检测、越狱检测、打架检测的开放算法平台。
待更…
程序图标
添加主窗口图标
我们程序运行的窗口,需要显示自己的窗口图标
,这样才更像一个正式的产品。
通过如下代码,我们可以把一个png图片
文件作为 程序窗口图标。
from PySide2.QtGui import QIcon
app = QApplication([])
# 加载 icon
app.setWindowIcon(QIcon('logo.png'))
注意:这些图标png文件,在使用PyInstaller创建可执行程序时,也要拷贝到程序所在目录。否则可执行程序运行后不会显示图标。
应用程序图标
应用程序图标是放在可执行程序里面的资源。可以在PyInstaller创建可执行程序时,通过参数 --icon=“logo.ico” 指定。
pyinstaller main_ui.py --noconsole --hidden-import PySide2.QtXml --icon="logo.ico"
注意参数一定是存在的ico文件
,不能是png等图片文件。
如果你只有png文件,可以通过在线的png转ico文件网站,生成ico,比如下面的网站:IOC生成
发布程序
我们前面开发的QT界面程序,在Windows 上只需要执行pyinstaller
的命令,即可制作独立exe程序。
pyinstaller main_ui.py --noconsole --hidden-import PySide2.QtXml
这样就会在当前目录下产生一个名为 dist
的目录,dist目录
里面就有一个名为 main_ui的目录
,我们的可执行程序 main_ui.exe
就在里面。
main_ui.py
dis____
|
main_ui____
|
main_ui.exe
其中:
-
main_ui.py
是包含QApplication()
的GUI程序,可以自己命名。 -
noconsole
指定不要命令行窗口,否则我们的程序运行的时候,还会多一个黑窗口。 但是我建议大家可以先去掉这个参数,等确定运行成功后,再加上参数重新制作exe。因为这个黑窗口可以显示出程序的报错,这样我们容易找到问题的线索。 -
hidden-import PySide2.QtXml
参数是因为这个 QtXml库是动态导入,PyInstaller没法分析出来,需要我们告诉它。
最后,如果使用的是动态加载UI文件,别忘了,把程序所需要的ui文件拷贝到main_ui.exe同级目录中。
因为PyInstaller只能分析出需要哪些代码文件。 而你的程序动态打开的资源文件,比如 图片、excel、ui这些,它是不会帮你打包的。