PyQt5 学习笔记

本笔记主要参考教程:Python Qt 简介 —— 白月黑羽

目的在于学习使用 PyQt 和 QtDesigner 开发图形化界面

1. PyQt 安装和在 pycharm 中联动

参考这篇博客:PyCharm+PyQt5+QtDesigner配置

2. 第一个案例

首先使用 QtDesigner 画出相应的图形界面,很简单的一个图

其中涉及有关布局 layout 的部分,不在这里细讲,直接参看B站教程:页面布局 layout (主要觉得比较抽象。和前端的布局很像,同样,我也不大会…)

再放一个参考教程 pyqt5 布局 layout 收集
PyQt5 学习笔记_第1张图片
然后使用 pyuic5 生成相应的 py文件,引入到主程序中:

from PyQt5.QtWidgets import QApplication, QMainWindow
import mainWindows


# QMainWindow 为一个控件类,表示主窗口
# 要创建一个控件,就需要实例化这个控件类
# 一般把一个窗口及其控件封装至一个类中
# 我们自定义(封装)一个窗口类,继承自QMainWindow
class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        # 将 ui文件:mainWindows.py 导入定义界面类
        self.ui = mainWindows.Ui_MainWindow()
        # 初始化界面
        self.ui.setupUi(self)
        # 设置窗口标题
        self.setWindowTitle("第一个案例")


if __name__ == '__main__':
    # QApplication 提供了整个图形界面程序的底层管理功能,应首先创建它
    app = QApplication([])
    # 创建主窗口这个控件,需实例化主窗口类
    mainw = MainWindow()
    # 显示主窗口
    mainw.show()
    # 只有最后一个关口关闭,才可以结束程序
    app.exec_()

这里解释说明几点:

① QApplication

QApplication 提供了整个图形界面程序的底层管理功能,所以应首先创建它

app = QApplication([])

② QMainWindow

我们要在窗口中创建一个控件,就需要实例化这个控件类

PyQt5.QtWidgets 中引入的 QMainWindow 表示主窗口,所以需要实例化它

但一个窗口中有很多子控件,所以,为了方便管理,我们通常将一个窗口封装为一个类,即:

# 继承自QMainWindow
class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        # 将 ui文件:mainWindows.py 导入定义界面类
        self.ui = mainWindows.Ui_MainWindow()
        # 初始化界面
        self.ui.setupUi(self)

③ 控件的继承关系

直接上图吧,感觉这一部分用的不多,主要由 QtDesigner 帮我们处理完成
PyQt5 学习笔记_第2张图片

④ 信号(signal)与 槽(slot)

信号可理解为一个控件的动作触发的信号,槽即可理解为处理这个 signal 的函数

他们一般这样联系起来:

button.clicked.connect(handleCalc)

用QT的术语来解释上面这行代码,就是:把 button 被点击(clicked) 的信号(signal), 连接(connect)到了 handleCalc 这样的一个 slot上

⑤ 启动窗口

...
# 显示主窗口
mainw.show()
# 只有最后一个关口关闭,才可以结束程序
app.exec_()

⑥ 总结

创建程序的一般步骤如下:

  1. 创建 app: app = QApplication([])
  2. 定义窗口类,完成信号与槽的相关逻辑:
# 继承自QMainWindow
class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        # 将 ui文件:mainWindows.py 导入定义界面类
        self.ui = mainWindows.Ui_MainWindow()
        # 初始化界面
        self.ui.setupUi(self)
  1. 实例化窗口类:mainw = MainWindow()
  2. 启动窗口:
mainw.show()
app.exec_()

⑦ 踩坑

  1. 主窗口的类名必须是 MainWindow,即:
class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        self.ui = queryScore_ui.Ui_MainWindow()
        self.ui.setupUi(self)


if __name__ == '__main__':
    app = QApplication([])
    mainw = MainWindow()
    mainw.show()
    app.exec_()

使用 QtDesigner 是这样,原因暂且不明

3. 窗口跳转与弹窗

① 窗口跳转

即:实例化另外一个窗口,显示新窗口,关闭(隐藏)老窗口

在窗口类中定义信号处理函数 和 进行信号与槽的绑定

class Windows1(QMainWindow):

    def __init__(self):
        super(Windows1, self).__init__()
        self.ui = windows1.Ui_MainWindow()
        self.ui.setupUi(self)

class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        # 使用ui文件导入定义界面类
        self.ui = mainWindows.Ui_MainWindow()
        # 初始化界面
        self.ui.setupUi(self)
        # 绑定信号与槽
        self.ui.pushButton.clicked.connect(self.open_windows1)

    def open_windows1(self):
        # 实例化 windows1
        self.windows1 = Windows1()
        # 显示 windows1
        self.windows1.show()
        # 隐藏自己
        self.hide()

实例化,显示,隐藏

记录一个报错

在写从 windows1 返回 mainWindow 时,进行信号与槽的绑定时有如下写法:

self.ui.pushButton.clicked.connect(self.return_mainWindows())

然后报这个错:

Process finished with exit code -1073740791 (0xC0000409)

原因在于绑定时传入的参数应是函数名,不能带有括号,正确写法如下:

self.ui.pushButton.clicked.connect(self.return_mainWindows)     # 注意这里不能有括号,传入的是参数名

这一长串中的名称都没有括号,在 PyCharm 中运用 代码补全 需注意

② 弹出模式对话框

所谓模式对话框,就是弹出此对话框后, 原窗口就处于不可操作的状态,只有当模式对话框关闭才能继续

但教程中的方法会报错:

class class MainWindow(QMainWindow):
	...
    def show_dialog_box(self):
        # 实例化 dialogBox
        self.dialogBox = DialogBox()
        # 显示弹窗
        self.dialogBox.show()
        # 阻塞进程,不关闭对话框则无法操作主窗口
        self.dialogBox.exec_()

这里采用 PyQt5各种常用对话框总结 中提到的方法

from PyQt5.QtCore import Qt

class DialogBox(QMainWindow):

    def __init__(self):
        super(DialogBox, self).__init__()
        self.ui = dialogBox.Ui_MainWindow()
        self.ui.setupUi(self)
        # 设置窗口为非模态
        # self.setWindowModality(Qt.NonModal)
        # 设置窗口为窗口模态,程序在未处理完当前对话框时将阻止和对话框的父窗口进行交互
        # self.setWindowModality(Qt.WindowModal)
        # 设置窗口为应用程序模态,阻止和人任何其他窗口进行交互
        self.setWindowModality(Qt.ApplicationModal)

在 QtDesigner 中设定信号与槽

PyQt5 学习笔记_第3张图片
PyQt5 学习笔记_第4张图片

总结

设置窗口模态 self.setWindowModality()

# 非模态
self.setWindowModality(Qt.NonModal)
# 窗口模态
self.setWindowModality(Qt.WindowModal)
# 应用程序模态
self.setWindowModality(Qt.ApplicationModal)

4. 打包程序

① 打包

使用 pyinsatller

pyinstaller -F -w main.py

其中:

-F 指定只生成一个exe文件

-w 指定不显示命令行窗口

--icon 可选项,用于指定 exe 文件的图标 (在线ICO转换)

② 添加主窗口图标

即右上角的图标
在这里插入图片描述

from PyQt5.QtGui import QIcon

...

if __name__ == '__main__':
    # QApplication 提供了整个图形界面程序的底层管理功能,应首先创建它
    app = QApplication([])
    # 加载 icon
    app.setWindowIcon(QIcon('logo.png'))
    ...

注意图片要放在同一目录下

5. 自定义信号

PyQt 信号与槽的讲解可参考这篇:关于PyQt5中自定义信号的几点理解

下面为自己的一些理解与总结:

① 自定义一个信号

# 定义一个信号
from PyQt5.QtCore import pyqtSignal, QObject

class MySignals(QObject):
    # 定义一种信号,参数类型为:dict
    data_print = pyqtSignal(dict)
    error_print = pyqtSignal(str)

自定义的信号类继承自 QObject

在信号类中声明信号:信号名 = pyqtSignal(类型),声明中的参数为数据类型,而不是变量名,表示这个信号传递的数据类型

注意这里声明的信号的参数数目,需与处理该信号的槽一致:
在这里插入图片描述

② 触发自定义信号

同样,需要首先实例化这个信号类

class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        ...
        # 实例化信号
        self.global_ms = MySignals()
        ...

使用 emit 触发:
实例化信号对象.所声明信号名称.emit(需传递的消息)

    def threadFunc(self):
      	self.global_ms.data_print.emit(data)

③ 捕获信号并绑定槽

与前面一样:
实例化信号对象.所声明信号名称.connect(槽)

对比自带的信号:button.clicked.connect(handleCalc)

class MainWindow(QMainWindow):
	def __init__(self):
	        super().__init__()
	        ...
			self.global_ms.data_print.connect(self.printToGui)
    def printToGui(self, data):
        self.ui.GPA.setText(data["GPA"])
        self.ui.score.setText(data["SCORE"])
        self.ui.shower.setText(self.xnm + " " + self.xqm)

6. 多线程

python 多线程可参考这篇:多线程 和 多进程

大致可总结如下:

from threading import Thread
from time import sleep

def threadFunc(arg1,arg2):
    print('子线程 开始')
    print(f'线程函数参数是:{arg1}, {arg2}')
    sleep(5)
    print('子线程 结束')

# 创建 Thread 类的实例对象, 并且指定新线程的入口函数
thread = Thread(target=threadFunc, args=('参数1', '参数2'))

# 执行start 方法,就会创建新线程,新线程会去执行入口函数里面的代码。
# 这时候 这个进程 有两个线程了。
thread.start()

# 主线程的代码执行 子线程对象的join方法,
# 就会等待子线程结束,才继续执行下面的代码
thread.join()

PyQt5 学习笔记_第5张图片
PyQt5 学习笔记_第6张图片
在这里插入图片描述
(感谢 白月黑羽 大大)

① 在PyQt 中创建线程

在槽中创建一个线程进行处理:

    def queryScore(self):
        self.ui.shower.setText("正在查询")
       ...
        # 创建新线程
        thread_1 = Thread(target=self.threadFunc_query, args=(username, password, xnm_1, xqm_1), daemon=True)
        thread_1.start()
        thread_2 = Thread(target=self.threadFunc_shower, daemon=True)
        thread_2.start()

处理界面阻塞问题

def threadFunc_query(self, username, password, xnm, xqm):
	try:
	    data = getScore.GetScore(username, password, xnm, xqm).workOutScore()
	    # 使用 emit 发出信号
	    self.global_ms.data_print.emit(data)
	except requests.exceptions.ConnectionError:
	    self.global_ms.error_print.emit("网络错误,校外用户请检查是否连接VPN")
	except getScore.LoginException:
	    self.global_ms.error_print.emit("登录错误,请检查用户名或密码")

thread_1 用于处理界面阻塞问题

我们点击按钮发送一个HTTP请求。若服务端接收处理的比较慢,界面就会僵死一段时间。在这期间,点击界面将没有任何反应

这是由于HTTP请求是在主线程中请求的,在请求的时间内,整个程序就停在请求代码处

为了解决这个问题,我们新开一个线程,让请求在新线程中请求,不再妨碍主线程的运行,这样主窗口就不会僵死,仍可操作

放一个循环

def threadFunc_shower(self):
	   # shower 的循环处理
	   self.flag = True
	   i = 1
	   # 循环不能放在主线程中,必须新开一个线程
	   while self.flag:
	       msg = "正在查询" + "." * i
	       i = i + 1
	       if i == 4:
	           i = 0
	       self.ui.shower.setText(msg)
	       sleep(1)

在 thread_2 中放一个循环,让某个 lable 持续更新,效果如下PyQt5 学习笔记_第7张图片
循环不能放在主线程中,必须新开一个线程

为了在接受信号后终止这个循环,我们设置了一个flag self.flag

def printToGui(self, data):
    # 终止 shower 的循环
    self.flag = False
    ...

记录一个错误

Thread(target=Func, args=(arg_1,)

args 参数必须传入一个元组,而:
PyQt5 学习笔记_第8张图片
故逗号不可省略

② 捕获子线程中的异常

如下所示:

def threadFunc_query(self, username, password, xnm, xqm):
	try:
	    data = getScore.GetScore(username, password, xnm, xqm).workOutScore()
	    # 使用 emit 发出信号
	    self.global_ms.data_print.emit(data)
	except requests.exceptions.ConnectionError:
	    self.global_ms.error_print.emit("网络错误,校外用户请检查是否连接VPN")
	except getScore.LoginException:
	    self.global_ms.error_print.emit("登录错误,请检查用户名或密码")

捕获异常后发出信号,然后接受信号并交给槽处理:

self.global_ms.error_print.connect(self.showMessageBox_critical)

    def showMessageBox_critical(self, msg):
        # 显示错误弹窗
        self.msgBox.critical(self, '错误', msg)

showMessageBox 弹窗的使用

参考文章:PyQt5消息框QMessageBox

from PyQt5.QtWidgets import QMessageBox
# 首先实例化
self.msgBox = QMessageBox()
# 然后调用弹窗
self.msgBox.critical(self, '错误', msg)

msg_box.question(self, '窗口标题', '提示信息', msg_box.Ok | msg_box.Cancel | msg_box.Yes, msg_box.Cancel)

  • 第一个参数:父窗口
  • 第二个参数:消息框的窗口标题
  • 第三个参数:提示信息
  • 第四个参数:显示的按钮,多个按钮用 | 分开
  • 第五个参数:默认高亮的按钮
信息 QMessageBox.information
问答 QMessageBox.question
警告 QMessageBox.warning
错误 QMessageBox.critical
关于 QMessageBox.about

③ 守护线程 daemon

有时我们会发现,纵使我们结束了主线程,但子线程仍在运行

这是因为,在Python程序中,只有当所有的 非daemon线程 结束了,整个程序才会结束

主线程是 非daemon线程,而新建的子线程默认也是 非daemon线程,所以为了解决上面的问题,需将子线程设为 daemon线程

thread_1 = Thread(target=self.threadFunc_query, args=(username, password, xnm_1, xqm_1), daemon=True)

7. 控件显示样式 QSS

与 CSS 很像

QtDesigner 中这样进入样式表编辑:
PyQt5 学习笔记_第9张图片
以下是对 显示样式——白月黑羽 的总结提炼

① selector 选择器

QLabel#label_13{
	font-family:微软雅黑;
	font-size:15px;
	color: red
}

一些常用的选择器:

说明 选择器
选择所有 对象名为 okButtonQPushButton类型 QPushButton#okButton
选择所有 QDialog 内部 QPushButton类型 QDialog QPushButton
选择所有 QDialog 直接子节点 QPushButton类型 QDialog > QPushButton

② Pseudo-States 伪状态

指定当鼠标移动到一个元素上方的时候,元素的显示样式

QPushButton:hover { color: red }

再比如,指定一个元素是disable状态的显示样式

QPushButton:disabled { color: red }

再比如,指定一个元素是鼠标悬浮,并且处于勾选(checked)状态的显示样式

QCheckBox:hover:checked { color: white }

8. 一些杂七杂八的小问题

① 把 QLineEdit 设为 密码样式

PyQt5 学习笔记_第10张图片

你可能感兴趣的:(Python,学习,pyqt5)