PyQt:桌面程序设计的饕餮盛宴
Posted 天元浪子
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PyQt:桌面程序设计的饕餮盛宴相关的知识,希望对你有一定的参考价值。
文章目录
1. 概述
Qt是一个跨平台的C++图形用户界面应用程序开发框架,目前已成为最强大,最受欢迎的跨平台GUI库之一。或许说Qt是一个GUI库并不恰当,因为Qt已经庞大到可以提供“一站式”服务了:既可开发GUI程序,也可开发非GUI程序,比如控制台工具和服务程序。
PyQt是Qt的Python封装,提供Qt类和函数的API。PyQt的最新版本是PyQt6,本文代码使用的就是这个版本。和上一个版本PyQt5相比,不考虑性能提升仅就使用习惯而言,PyQt6似乎只有以下3个改变:
- QAction类从QtWidgets模块移到了QtGui模块
- QApplication类exec_方法更名为exec,去掉了后面的下划线
- 枚举和常量增加了分组限定,比如QtCore.Qt中的AlignCenter变成了AlignmentFlag.AlignCenter
新用户可以直接使用下面的命令安装最新版。如果此前已经安装了PyQt5也没有关系,PyQt6可以和PyQt5并存,甚至可以混合使用——尽管这不是一个值得推荐的做法。
pip install PyQt6
安装完成后,可以在Python的IDLE中检查Qt和PyQt的版本。
>>> from PyQt6.QtCore import QT_VERSION_STR, PYQT_VERSION_STR
>>> QT_VERSION_STR, PYQT_VERSION_STR
('6.3.0', '6.3.0')
2. PyQt的组织架构
在一个热爱生活的的程序员眼里,诸美皆可食。如果把wxPthon比作是珍馐玉馔,那么Tkinter就好比是肯德基套餐,而PyQt则是传说中的满汉全席了。据说一顿满汉全席要持续三天,倘若被邀赴宴,不事先了解一下宴席的礼仪规矩、菜系菜品和时序流程,恐怕要闹出不少笑话。同样的,既然把PyQt比作满汉全席,那就有必要在使用之前了解一下它的组织架构,以免被代码中导入的数量巨多的模块和类弄得晕头转向,从此产生心理阴影。
PyQt究竟有多么庞大呢?看看下面这张PytQt6的组织架构图就清楚了,PytQt5比这个还要臃肿一点点。
这张图只列出了PyQt6的常用模块,每个模块各自封装了大量的类和函数。其中QtWidgets模块、QtGui模块、QtCore模块是桌面程序开发中使用频率最高的模块,QtWidgets类中QApplication类、QWidget类和QMainWindow类又是使用频率最高的类。根据模块和类在桌面程序开发中使用频率的高低,我给它们标注了红黄绿蓝四种颜色。当然,这是非常主观的,完全是我个人的使用体验。
【小结:几乎每个应用程序都会用到的3个模块】
QtWidgets模块
:包含应用程序类、窗口类、控件类和组件类
QtGui模块
:包含和gui相关的功能,例如用于事件处理、图像处理、字体和颜色类等
QtCore模块
:包含核心的非gui功能,例如线程、定时器、日期时间类、文件类等
3. 快速体验
3.1 桌面应用程序开发的一般流程
用PyQt6写一个桌面应用程序,通常分为五个步骤:
- 创建应用程序
- 创建窗口
- 把需要的控件放到窗口上,并告诉它们当有预期的事件发生时就执行预设的动作
- 显示窗口
- 应用程序进入事件处理主循环
除第3步之外的其它步骤,基本都是一行代码就可以完成,第3步的复杂程度取决于功能需求的多寡和业务逻辑的复杂度。下面这段代码就是这个一般流程的体现。
from PyQt6.QtWidgets import *
app = QApplication([]) # 第1步:创建应用程序
win = QWidget() # 第2步:创建窗口
lab = QLabel('Hello World', win) # 第3步:显示Hello World
win.show() # 第4步:显示窗口
app.exec() # 第5步:应用程序进入事件处理主循环
代码中导入了QtWidgets模块的全部类,但只用到了QApplication类(构建应用程序)、QWidget类(构建窗口)和QLabel类(实例化标签)等3个类。这段代码虽然运行起来没有问题,但不够美观,也没有遵循下面4个约定俗成的规则:
- 由于PyQt太过庞大,应当尽量避免使用星号导入所有项,而是仅导入指定的项
- 应用程序以sys.argv作为来自命令行的参数列表
- 应用程序退出事件处理主循环后调用sys.exit函数清理现场
- 面向对象编程,减少全局变量,将业务逻辑封装在派生的窗口类中
3.2 Hello World
下面这段代码遵循了上述的规则,加上了窗口标题和图标,同时设置了标签大小、文本的字体字号和对齐方式。
import sys
from PyQt6.QtWidgets import QApplication, QWidget, QLabel
from PyQt6.QtGui import QIcon, QFont
from PyQt6.QtCore import Qt
class MyWindow(QWidget):
"""从QWidget类派生的桌面应用程序窗口类"""
def __init__(self):
"""构造函数"""
super().__init__() # 调用基类的构造函数
self.setWindowTitle('Hello World') # 设置标题
self.setWindowIcon(QIcon('res/qt.png')) # 设置图标
lab = QLabel('Hello World', self) # 实例化标签
lab.resize(320,160) # 设置标签大小
lab.setFont(QFont('Arial', 32, QFont.Weight.Bold)) # 设置字体字号
lab.setAlignment(Qt.AlignmentFlag.AlignCenter) # 文本在标签内居中
self.show() # 显示窗口
if __name__ == '__main__':
app = QApplication(sys.argv) # 创建应用程序,接收来自命令行的参数列表
win = MyWindow() # 创建窗口
sys.exit(app.exec()) # 应用程序主循环结束后,调用sys.exit()方法清理现场
代码中用到了一个.png格式的图像文件文件,想要运行这段代码的话,请先替换成本地文件。至于文件格式,setWindowIcon方法没有任何限制,常见的包括.ico在内的图像格式都支持。运行界面如下图所示。
【小结:使用频率最高的类】
QApplication类
:每个应用程序都是该类的实例
QWidget类
:所有窗口和控件的基类,每个应用程序的窗口都是该类或其派生类的实例
QLabel
:出镜率最高的控件之一
4 控件布局
在Hello World例子中设置了标签的大小,窗口自动适应标签。通常的应用场景正好与之相反:控件需要根据窗口大小自动适应,并且多个控件之间的相对关系也需要自动适应。这就是控件布局要实现的功能。
4.1 分区布局
所谓分区布局,就是将一个矩形区域沿水平或垂直方向分割成多个矩形区域,并可嵌套分区。QtWidgets模块提供了QBoxLayout类作为分区布局管理器,其派生的QHBoxLayout类和QVBoxLayout类分别实现水平分区布局和垂直分区布局。
分区布局的诸多方法中有两个极为重要的参数,需要详细说明。
- stretch:拉伸因子,用来设置控件或子项在布局方向上占用剩余空间的额度。例如,有三个控件水平布局,假定三个控件的拉伸因子分别为0、1、2,那么第1个控件除了自身宽度外,不占用剩余空间,第2个控件除了自身宽度外,占用剩余空间的1/3,第3个控件除了自身宽度外,占用剩余空间的2/3。
- alignment:对齐方式,用来设置控件在其可用空间内的放置方式。默认控件填充分配给自身的全部空间,可以选项由QtCore.Qt.AlignmentFlag枚举,包括AlignLeft、AlignRight、AlignTop、AlignBottom、AlignCenter、AlignHCenter、AlignVCenter、AlignBaseline、AlignJustify等。
分区布局QBoxLayout类的方法很多,不过最重要最常用的只有下述这几个:
addWidget
(QWidget, stretch=0, alignment=0) - 添加部件addLayout
(QLayout, stretch=0) - 添加布局管理器addStretch
(stretch) - 添加拉伸因子addSpacing
(spacing) - 添加以像素为单位的空间
下面是一个水平和垂直混合分区布局的例子。
import sys
from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QLineEdit, QPushButton, QHBoxLayout, QVBoxLayout
from PyQt6.QtGui import QIcon
from PyQt6.QtCore import Qt
class MyWindow(QWidget):
"""从QWidget类派生的桌面应用程序窗口类"""
def __init__(self):
"""构造函数"""
super().__init__()
self.setWindowTitle('分区布局')
self.setWindowIcon(QIcon('res/qt.png'))
self.setGeometry(400, 300, 320, 160) # 设置窗位置和大小
lab_acc = QLabel('账号:')
account = QLineEdit()
account.setAlignment(Qt.AlignmentFlag.AlignCenter)
lab_pw = QLabel('密码:')
passwd = QLineEdit()
passwd.setAlignment(Qt.AlignmentFlag.AlignCenter)
passwd.setEchoMode(QLineEdit.EchoMode.Password) # 不显示密码
btn_ok = QPushButton('确定')
btn_cancel = QPushButton('取消')
# 使用水平布局管理器布局lab_acc控件和account控件,左右留白10像素
hbox_acc = QHBoxLayout()
hbox_acc.addSpacing(10)
hbox_acc.addWidget(lab_acc)
hbox_acc.addWidget(account)
hbox_acc.addSpacing(10)
# 使用水平布局管理器布局lab_pw控件和passwd控件,左右留白10像素
hbox_pw = QHBoxLayout()
hbox_pw.addSpacing(10)
hbox_pw.addWidget(lab_pw)
hbox_pw.addWidget(passwd)
hbox_pw.addSpacing(10)
# 使用水平布局管理器布局btn_ok控件和btn_cancel控件
hbox_btn = QHBoxLayout() # 水平布局管理器
hbox_btn.addStretch(5) # 设置左侧拉伸因子
hbox_btn.addWidget(btn_ok) # 添加btn_ok控件
hbox_btn.addWidget(btn_cancel) # 添加btn_cancel控件
hbox_btn.addStretch(1) # 设置右侧拉伸因子
# 使用垂直布局管理器布局上面3个水平布局管理器
vbox = QVBoxLayout()
vbox.addSpacing(10)
vbox.addLayout(hbox_acc)
vbox.addSpacing(5)
vbox.addLayout(hbox_pw)
vbox.addStretch(1)
vbox.addLayout(hbox_btn)
vbox.addSpacing(10)
# 将垂直布局管理器应用到窗口
self.setLayout(vbox)
self.show() # 显示窗口
if __name__ == '__main__':
app = QApplication(sys.argv)
win = MyWindow()
sys.exit(app.exec())
这段代码设计了一个登录界面,演示了水平布局和垂直布局的方法,同时介绍了单行文本编辑框控件QLineEdit和按钮控件QPushButton的用法。运行界面如下图所示。
【小结:除了布局管理类,按钮和单行文本编辑框也是最常用的控件】
QHBoxLayout
:水平布局管理器
QVBoxLayout
:垂直布局管理器
QPushButton
:按钮
QLineEdit
:单行文本编辑框
4.2 栅格布局
顾名思义,栅格布局就是将布局空间划分成网格,将控件放置到不同的网格内。栅格布局比较简单,用起来非常方便。栅格布局QGridLayout类的主要方法有:
addWidget
(QWidget, row, col, alignment) - 在row行col列添加控件,并设置对齐方式addWidget
(QWidget, row, col, r, c, alignment) - 在row行col列添加控件,占r行c列,并设置对齐方式addLayout
(QLayout, row, col, alignment) - 在row行col列添加布局,并设置对齐方式addLayout
(QLayout, row, col, r, c, alignment) - 在row行col列添加布局,占r行c列,并设置对齐方式setRowStretch
(row, stretch) - 设置row行的拉伸因子setColumnStretch
(col, stretch) - 设置col列的拉伸因子
下面是一个栅格布局的例子。
import sys
from PyQt6.QtWidgets import QApplication, QWidget, QPushButton, QGridLayout
from PyQt6.QtGui import QIcon
class MyWindow(QWidget):
"""从QWidget类派生的桌面应用程序窗口类"""
def __init__(self):
"""构造函数"""
super().__init__()
self.setWindowTitle('栅格布局')
self.setWindowIcon(QIcon('res/qt.png'))
self.initUI() # 初始化界面
self.show() # 显示窗口
def initUI(self):
"""初始化界面"""
keys = [
['(', ')', 'Back', 'Clear'],
['7', '8', '9', '/'],
['4', '5', '6', '*'],
['1', '2', '3', '-'],
['0', '.', '=', '+']
]
grid = QGridLayout() # 创建网格布局管理器
self.setLayout(grid) # 将网格布局管理器应用到窗口
for i in range(5):
for j in range(4):
button = QPushButton(keys[i][j])
grid.addWidget(button, i, j)
if __name__ == '__main__':
app = QApplication(sys.argv)
win = MyWindow()
sys.exit(app.exec())
这段代码实现了一个计算器界面,5行4列20个按钮放置在5行4列的网格上。代码运行界面如下图所示。
【小结:栅格布局管理器】
QGridLayout
:使用频率和分区布局管理器不相上下
4.3 表单布局
分区布局的例子是一个登录界面,界面上每一行都是一个标签和一个单行文本编辑器。针对此种情况,PyQt推出了表单布局,专用于登录、注册等标签和单行文本编辑器成对使用的情况。通过下面的代码,很容易掌握表单布局管理器QFormLayout类的用法。
import sys
from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QLineEdit, QPushButton, QVBoxLayout, QFormLayout
from PyQt6.QtGui import QIcon
from PyQt6.QtCore import Qt
class MyWindow(QWidget):
"""从QWidget类派生的桌面应用程序窗口类"""
def __init__(self):
"""构造函数"""
super().__init__()
self.setWindowTitle('表单布局')
self.setWindowIcon(QIcon('res/qt.png'))
self.setGeometry(400, 300, 320, 200) # 设置窗位置和大小
form = QFormLayout() # 创建表单布局管理器
form.setLabelAlignment(Qt.AlignmentFlag.AlignRight) #设置标签右对齐(默认左对齐)
name = QLineEdit()
mobile = QLineEdit()
passwd = QLineEdit()
addr = QLineEdit()
form.addRow('姓名', name)
form.addRow('移动电话', mobile)
form.addRow('密码', passwd)
form.addRow('通讯地址', addr)
name.setPlaceholderText("请输入姓名")
mobile.setPlaceholderText("请输入移动电话")
passwd.setPlaceholderText("请输入密码")
addr.setPlaceholderText("请输入通讯地址")
name.setEchoMode(QLineEdit.EchoMode.Normal)
mobile.setEchoMode(QLineEdit.EchoMode.NoEcho)
passwd.setEchoMode(QLineEdit.EchoMode.Password)
addr.setEchoMode(QLineEdit.EchoMode.PasswordEchoOnEdit)
btn_ok = QPushButton('确定')
vbox = QVBoxLayout()
vbox.addSpacing(10)
vbox.addLayout(form)
vbox.addStretch(1)
vbox.addWidget(btn_ok, alignment=Qt.AlignmentFlag.AlignCenter)
vbox.addSpacing(10)
# 将垂直布局管理器应用到窗口
self.setLayout(vbox)
self.show() # 显示窗口
if __name__ == '__main__':
app = QApplication(sys.argv)
win = MyWindow()
sys.exit(app.exec())
代码运行界面如下图所示。
【小结:表单布局管理器】
QFormLayout
:适用于标签和单行文本编辑框成对使用的场合
5. 事件和事件函数
如果把窗体和控件比作是桌面程序的躯体,那么事件驱动机制就是它的灵魂。同样的,PyQt的GUI程序也是事件驱动的,但不同于Tkinter和wx那样任由用户绑定事件和事件函数,事件在PyQt中是底层的,每个事件都有与之对应的事件函数——也许只是个空函数,用户无法改变它们之间的绑定关系。
5.1 事件模型
PyQt事件大致有键盘事件、鼠标事件、拖放事件、滚轮事件、定时事件、焦点事件、进入和离开事件、窗口关闭事件、窗口移动事件、窗口显示和隐藏事件,窗口选中事件,以及Socket事件、剪贴板事件、文字改变事件,布局改变事件等。
事件有三个要素:事件源、事件对象和事件目标。事件源指向事件的制造者,事件目标指向事件的处理者,事件对象封装了事件的信息,比如事件源的状态变化等。
针对每个事件,窗口和控件的基类QWidget提供与之对应的事件函数。有些事件函数是有内容的,比如和窗口关闭事件对应的closeEvent;有些事件函数则是空的,比如和键盘事件对应的keyPressEvent,需要用户根据业务逻辑重写(Override)。所有的事件函数都以事件对象为参数,事件对象提供了事件的详细信息,比如键盘按下事件的事件对象就包含了被按下的键的信息。
以下是PyQt的全部事件函数。
- actionEvent
- changeEvent
- childEvent
- closeEvent
- contextMenuEvent
- customEvent
- dragEnterEvent
- dragLeaveEvent
- dragMoveEvent
- dropEvent
- enterEvent
- focusInEvent
- focusOutEvent
- hideEvent
- inputMethodEvent
- installEventFilter
- keyPressEvent
- keyReleaseEvent
- leaveEvent
- mouseDoubleClickEvent
- mouseMoveEvent
- mousePressEvent
- mouseReleaseEvent
- moveEvent
- nativeEvent
- paintEvent
- removeEventFilter
- resizeEvent
- showEvent
- tabletEvent
- timerEvent
- wheelEvent
5.2 重写(Override)事件函数
既然事件和事件函数之间的已经建立了对应关系,用户就无需考虑事件和事件函数之间的绑定,只在有必要的时候重写(Override)事件函数即可。
import sys
from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QHBoxLayout
from PyQt6.QtCore import Qt
class MyWindow(QWidget):
"""从QWidget类派生的桌面应用程序窗口类"""
def __init__(self):
"""构造函数"""
super().__init__()
self.setWindowTitle('事件和事件函数')
self.setGeometry(400, 300, 320, 80)
self.initUI() # 初始化界面
self.show()
def initUI(self):
"""初始化界面"""
lab = QLabel('按Esc键关闭窗口')
box = QHBoxLayout()
box.addStretch(1)
box.addWidget(lab)
box.addStretch(1)
self.setLayout(box)
def keyPressEvent(self, evt):
"""重写按键事件函数"""
if evt.key() == Qt.Key.Key_Escape.value:
self.close()
if __name__ == '__main__':
app = QApplication(sys.argv)
win = MyWindow()
sys.exit(app.exec())
这段代码重写了键盘按下事件函数keyPressEvent,实现了Esc键关闭窗口的功能。代码运行界面如下图所示。
下面这段代码在控制台打印了鼠标进入窗口事件对象的全部属性和方法,名字通俗易懂,望文生义基本都不会错。
import sys
from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QHBoxLayout
from PyQt6.QtGui import QIcon, QFont
from PyQt6.QtCore import Qt
class MyWindow(QWidget):
"""从QWidget类派生的桌面应用程序窗口类"""
def __init__(self):
"""构造函数"""
super().__init__()
self.setWindowTitle('事件对象')
self.setWindowIcon(QIcon('res/qt.png'))
self.setGeometry(400, 300, 320, 80)
self.initUI() # 初始化界面
self.show()
def initUI(self):
"""初始化界面"""
self.lab = QLabel('')
self.lab.setFont(QFont('Arial', 32, QFont.Weight.Bold))
self.lab.setAlignment(Qt.AlignmentFlag.AlignCenter)
box = QHBoxLayout()
box.addWidget(self.lab)
self.setLayout(box)
def enterEvent(self, evt):
"""重写进入事件函数"""
for item in dir(evt):
print(item)
pos = evt.position()
self.lab.setText('x=%d, y=%d'%(pos.x(), pos.y()))
if __name__ == '__main__':
app = QApplication(sys.argv)
win = MyWindow()
sys.exit(app.exec())
代码运行界面如下图所示。
鼠标进入窗口时,控制台同时打印了事件对象的属性和方法。
accept
allPointsAccepted
button
buttons
clone
device
deviceType
exclusivePointGrabber
globalPosition
ignore
isAccepted
isBeginEvent
isEndEvent
isInputEvent
isPointerEvent
isSinglePointEvent
isUpdateEvent
modifiers
point
pointById
pointCount
pointerType
pointingDevice
points
position
registerEventType
scenePosition
setAccepted
setExclusivePointGrabber
spontaneous
timestamp
type
【小结:重写(Override)】
重写是子类对父类允许访问的方法的实现过程重新编写代码, 返回值和形参都不能改变,即外壳不变,核心重写。重写不同于与重载(Overload),重载是方法的参数个数或种类或顺序不同,方法名相同。
5.3 事件过滤器
在某些应用特定场景中,需要拦截屏蔽某些事件,或者在某些事件被处理前插入其他操作,此时就需要事件过滤器出场了。事件过滤器的使用有几个要点:一是要重写QObject.eventFilter过滤器,二是要在调用QApplication.installEventFilter安装过滤器。
import sys
from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QHBoxLayout
from PyQt6.QtCore import Qt, QEvent
class MyWindow(QWidget):
以上是关于PyQt:桌面程序设计的饕餮盛宴的主要内容,如果未能解决你的问题,请参考以下文章
荟萃行业领袖的饕餮盛宴,EcoSaaS 生态圈峰会强势登陆魔都!
蜂巢HIVECHAIN2018战略升级蜂巢商学院大会饕餮盛宴