一天搞定《AI工程师的PySide2 PyQt5实战开发手册》

PySide2/PySide6、PyQt5/PyQt6:都是基于Qt 的Python库,可以形象地这样说,PySide2 是Qt的 亲儿子(Qt官方开发的) , PyQt5 是Qt还没有亲儿子之前的收的 义子 (Riverbank Computing这个公司开发的,有商业版权限制)。

两个库的使用 对程序员来说,差别很小:它们的调用接口几乎一模一样。如果你的程序是PyQt5开发的,通常只要略作修改,比如把导入的名字从 PyQt5 换成 PySide2 就行了。反之亦然。
对应:PySide2-PyQt5PySide6-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([])

QMainWindowQPlainTextEditQPushButton 是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;
}

就会发现,所有的按钮上的文字都变成了红色的,并且字体变大了。注意这个指定界面元素 显示样式的 语法,由 selectordeclaration 组成。

  • 花括号前面的 部分,比如示例中的 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这些,它是不会帮你打包的。

猜你喜欢

转载自blog.csdn.net/weixin_54338498/article/details/129457070