本笔记主要参考教程:Python Qt 简介 —— 白月黑羽
目的在于学习使用 PyQt 和 QtDesigner 开发图形化界面
参考这篇博客:PyCharm+PyQt5+QtDesigner配置
首先使用 QtDesigner 画出相应的图形界面,很简单的一个图
其中涉及有关布局 layout 的部分,不在这里细讲,直接参看B站教程:页面布局 layout (主要觉得比较抽象。和前端的布局很像,同样,我也不大会…)
再放一个参考教程 pyqt5 布局 layout 收集
然后使用 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 提供了整个图形界面程序的底层管理功能,所以应首先创建它
app = QApplication([])
我们要在窗口中创建一个控件,就需要实例化这个控件类
从 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 帮我们处理完成
信号可理解为一个控件的动作触发的信号,槽即可理解为处理这个 signal 的函数
他们一般这样联系起来:
button.clicked.connect(handleCalc)
用QT的术语来解释上面这行代码,就是:把 button 被点击(clicked) 的信号(signal), 连接(connect)到了 handleCalc 这样的一个 slot上
...
# 显示主窗口
mainw.show()
# 只有最后一个关口关闭,才可以结束程序
app.exec_()
创建程序的一般步骤如下:
app = QApplication([])
# 继承自QMainWindow
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# 将 ui文件:mainWindows.py 导入定义界面类
self.ui = mainWindows.Ui_MainWindow()
# 初始化界面
self.ui.setupUi(self)
mainw = MainWindow()
mainw.show()
app.exec_()
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 是这样,原因暂且不明
即:实例化另外一个窗口,显示新窗口,关闭(隐藏)老窗口
在窗口类中定义信号处理函数 和 进行信号与槽的绑定
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)
设置窗口模态 self.setWindowModality()
# 非模态
self.setWindowModality(Qt.NonModal)
# 窗口模态
self.setWindowModality(Qt.WindowModal)
# 应用程序模态
self.setWindowModality(Qt.ApplicationModal)
使用 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'))
...
注意图片要放在同一目录下
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)
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()
在槽中创建一个线程进行处理:
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 持续更新,效果如下
循环不能放在主线程中,必须新开一个线程
为了在接受信号后终止这个循环,我们设置了一个flag self.flag
def printToGui(self, data):
# 终止 shower 的循环
self.flag = False
...
Thread(target=Func, args=(arg_1,)
如下所示:
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)
参考文章: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
有时我们会发现,纵使我们结束了主线程,但子线程仍在运行
这是因为,在Python程序中,只有当所有的 非daemon线程
结束了,整个程序才会结束
主线程是 非daemon线程
,而新建的子线程默认也是 非daemon线程
,所以为了解决上面的问题,需将子线程设为 daemon线程
:
thread_1 = Thread(target=self.threadFunc_query, args=(username, password, xnm_1, xqm_1), daemon=True)
与 CSS 很像
QtDesigner 中这样进入样式表编辑:
以下是对 显示样式——白月黑羽 的总结提炼
QLabel#label_13{
font-family:微软雅黑;
font-size:15px;
color: red
}
一些常用的选择器:
说明 | 选择器 |
---|---|
选择所有 对象名为 okButton 的QPushButton 类型 |
QPushButton#okButton |
选择所有 QDialog 内部 QPushButton 类型 |
QDialog QPushButton |
选择所有 QDialog 直接子节点 QPushButton 类型 |
QDialog > QPushButton |
指定当鼠标移动到一个元素上方
的时候,元素的显示样式
QPushButton:hover { color: red }
再比如,指定一个元素是disable状态
的显示样式
QPushButton:disabled { color: red }
再比如,指定一个元素是鼠标悬浮,并且处于勾选(checked)状态
的显示样式
QCheckBox:hover:checked { color: white }