PyQt5从入门学习:Python桌面应用开发学习笔记
目录
- 一、引言
- 二、环境配置与安装
- 三、第一个PyQt5程序
- 四、核心概念深入理解
- 五、常用控件详解
- 六、对话框与窗口管理
- 七、高级主题
- 八、项目实战:学生信息管理系统
- 九、最佳实践与性能优化
- 十、总结与进阶学习
1. 引言
在Python生态中,图形用户界面(GUI)开发一直是重要的应用领域。无论是桌面工具、数据可视化软件还是企业内部管理系统,GUI应用都扮演着不可或缺的角色。在众多Python GUI框架中,PyQt5以其强大的功能、丰富的组件库和成熟的生态系统,成为开发者构建桌面应用的首选方案之一。PyQt5是Qt框架的Python bindings,继承了Qt在跨平台桌面应用开发领域的卓越表现,同时充分利用了Python语言的简洁性和易用性。本技术文档将系统性地介绍PyQt5的核心概念、开发方法和最佳实践,帮助读者从零基础逐步成长为能够独立完成复杂桌面应用的开发人员。
PyQt5的应用场景极为广泛。在桌面工具开发方面,它能够创建功能完善的文件管理器、图像处理工具和文本编辑器;在数据可视化领域,它可以开发交互式的图表展示程序和数据监控系统;在企业应用方面,PyQt5能够构建复杂的业务管理系统和办公自动化软件。此外,PyQt5还支持移动端开发(通过Qt for Android),这意味着使用PyQt5开发的技能具有很强的可移植性。掌握PyQt5不仅能够满足当前的项目需求,还为开发者打开了通往更广阔技术领域的大门。
本技术文档的结构设计遵循循序渐进的原则,从环境配置和基础概念入手,逐步深入到高级主题和实战项目。第一部分涵盖PyQt5的简介、环境搭建和第一个程序的创建,帮助读者快速入门;第二部分深入讲解PyQt5的核心概念,包括信号与槽机制、布局管理系统和事件处理;第三部分详细介绍常用控件的属性、方法和使用场景;第四部分探讨对话框、窗口管理和样式定制;第五部分涉及多线程编程、数据库操作和网络通信等高级主题;最后通过一个完整的项目实例,展示PyQt5开发的具体流程和技巧。每个部分都配有详细的代码示例和效果说明,确保读者能够理论联系实践,真正掌握PyQt5的开发技能。
2. 环境配置与安装
2.1 Python环境准备
在开始PyQt5开发之前,需要确保系统中已经安装了Python环境。建议使用Python 3.7或更高版本,因为PyQt5对Python 3有更好的支持和优化。可以通过访问Python官方网站(python.org)下载并安装最新版本的Python。安装过程中需要注意勾选"Add Python to PATH"选项,这样可以方便地在命令行中直接调用Python命令。安装完成后,打开终端或命令提示符,输入"python --version"命令,如果能够正确显示Python版本号,说明安装成功。
对于PyQt5开发,建议使用虚拟环境来管理项目依赖,这样可以避免不同项目之间的包版本冲突。Python 3.3及更高版本内置了venv模块,可以直接使用它创建虚拟环境。在项目目录下执行"python -m venv venv"命令,即可创建一个名为venv的虚拟环境。激活虚拟环境的方式因操作系统而异:在Windows系统中使用"venv\Scripts\activate"命令;在Linux和macOS系统中使用"source venv/bin/activate"命令。激活虚拟环境后,安装的包将只在该环境中生效,不会影响系统Python或其他项目。
pip是Python的包管理工具,用于安装和管理Python包。确保pip是最新版本可以获得更好的安装体验和依赖解析能力。可以使用"python -m pip install --upgrade pip"命令升级pip。在安装PyQt5之前,更新pip是一个良好的实践。pip会从Python Package Index(PyPI)获取包信息并完成安装过程。了解pip的基本使用方法对于管理项目依赖非常重要,包括使用requirements.txt文件记录依赖列表、使用"pip list"查看已安装的包、使用"pip freeze"导出依赖等操作。
2.2 PyQt5安装详解
PyQt5的安装过程相对简单,但需要根据不同的操作系统和需求选择合适的安装方式。最直接的安装方法是通过pip命令,在激活的虚拟环境中执行"pip install PyQt5"命令即可。pip会自动下载PyQt5及其依赖包并完成安装。这个命令安装的是PyQt5的核心库,包含了Qt框架的主要功能。如果需要额外的高级功能,可以安装扩展包,例如"pip install PyQt5-sip"提供了更深入的SIP绑定支持。
在某些情况下,使用pip安装可能会遇到依赖问题或版本兼容性问题。这时可以尝试使用conda进行安装。Anaconda是一个流行的Python发行版,自带了conda包管理器。如果已经安装了Anaconda或Miniconda,可以使用"conda install pyqt"命令安装PyQt5。conda会自动处理依赖关系,并确保安装的版本与已安装的其他包兼容。conda还提供了创建和管理不同环境的功能,可以为不同的项目创建独立的工作环境。
安装完成后,需要验证PyQt5是否正确安装。可以编写一个简单的测试脚本来检查:导入PyQt5的模块并创建一个基本的窗口。如果脚本能够正常运行并显示窗口,说明安装成功。以下是一个简单的验证脚本:首先导入必要的模块,然后创建一个QApplication实例,接着创建一个QWidget作为主窗口,设置窗口标题和尺寸,最后调用show方法显示窗口并启动应用的事件循环。如果一切正常,应该能够看到一个空白窗口出现在屏幕上。
需要注意的是,PyQt5依赖于Qt库,而Qt库在某些系统上可能需要额外的配置。在Windows系统上,通常不需要额外安装任何东西,PyQt5的pip包中包含了必要的Qt库文件。在Linux系统上,可能需要安装一些系统级的依赖,例如libgl1-mesa-dev等OpenGL开发库。在macOS系统上,如果遇到问题,可以尝试安装Xcode命令行工具。在后续的开发过程中,如果遇到与Qt库相关的错误,首先应该检查是否缺少系统级的依赖包。
2.3 开发工具选择
选择合适的开发工具可以显著提升开发效率。对于PyQt5开发,有多种IDE和编辑器可供选择。PyCharm是JetBrains公司出品的专业Python IDE,提供了优秀的代码补全、调试功能和项目管理能力。PyCharm的社区版免费开源,足以满足PyQt5开发的需求。在PyCharm中,可以方便地配置Python解释器、管理项目依赖,并使用内置的调试器逐步执行代码、检查变量值。
VS Code是微软推出的轻量级代码编辑器,通过安装Python扩展可以支持PyQt5开发。VS Code具有启动快速、插件生态丰富的特点,受到许多开发者的喜爱。配置好Python环境后,VS Code可以提供语法高亮、代码补全、 linting等功能。对于习惯使用轻量级编辑器的开发者来说,VS Code是一个很好的选择。
除了专业的IDE,还可以使用更轻量的文本编辑器如Sublime Text或Atom。这些编辑器启动速度更快,但需要手动配置更多的开发环境。如果只是进行简单的PyQt5开发或学习,使用任何顺手的编辑器都可以。需要注意的是,无论选择哪种开发工具,都应该熟悉其调试功能,因为GUI应用的调试往往比命令行程序更加复杂。
3. 第一个PyQt5程序
3.1 程序结构解析
创建一个PyQt5应用程序需要遵循一定的结构和模式。最基本的PyQt5程序包含几个关键要素:QApplication对象、QWidget窗口、事件循环。以下是一个最简单的PyQt5程序示例,通过分析这个示例,可以了解PyQt5程序的基本结构和工作原理。
import sys
from PyQt5.QtWidgets import QApplication, QWidget
if __name__ == '__main__':
app = QApplication(sys.argv)
w = QWidget()
w.resize(400, 300)
w.setWindowTitle('第一个PyQt5程序')
w.show()
sys.exit(app.exec_())
这个程序虽然简短,但包含了PyQt5应用的所有核心组件。QApplication是整个应用程序的管理类,负责处理应用程序级别的设置和事件。每个PyQt5应用程序有且仅有一个QApplication实例,它管理应用程序的主事件循环,处理用户输入和系统事件。创建QApplication时需要传入sys.argv参数,这是因为Qt需要处理命令行参数,虽然在简单的脚本中这些参数通常不涉及,但传递sys.argv是一个良好的习惯。
QWidget是所有用户界面对象的基类,构成了PyQt5应用程序的用户界面基础。在这个示例中,我们创建了一个简单的QWidget作为主窗口。QWidget类本身提供了基本的窗口功能,包括显示、隐藏、移动和调整大小等。通过调用resize方法可以设置窗口的初始尺寸,通过setWindowTitle方法可以设置窗口标题栏显示的文字。show方法将窗口设置为可见状态,在此之前窗口是不可见的。
app.exec_()启动了应用程序的事件循环,这是PyQt5程序运行的核心机制。事件循环是一个无限循环,它持续运行以处理用户操作、系统事件和重绘请求等。当用户关闭窗口时,事件循环会结束,sys.exit()返回退出代码。在PyQt5中,推荐使用exec_()而不是exec(),这是因为exec是Python 2中的关键字,在某些情况下可能会导致兼容性问题。
3.2 程序运行原理
理解PyQt5程序的运行原理对于开发更复杂的应用至关重要。程序启动后,QApplication会创建并进入事件循环。进入事件循环后,程序的控制权移交给Qt框架,程序不再继续执行后续代码,而是等待各种事件的发生。当用户点击鼠标、按下键盘、窗口需要重绘或其他事件发生时,Qt会捕获这些事件并将其转换为QEvent对象,然后分发给相应的对象处理。
信号与槽是PyQt5中处理事件的核心机制。当某个事件发生时,对象会发出一个信号(Signal),信号可以连接到槽(Slot)函数,槽函数会被自动调用以响应这个事件。例如,当用户点击按钮时,按钮会发出clicked信号,这个信号可以连接到自定义的处理函数。这种机制实现了对象之间的松耦合通信,使得程序的结构更加清晰和易于维护。
事件循环的退出发生在多种情况下:用户关闭主窗口、按下了特定的退出快捷键、调用了quit或exit方法等。当事件循环结束时,应用程序会返回一个退出代码,这个代码通常表示程序是正常退出还是发生了错误。在上面的示例中,sys.exit(app.exec_())确保了应用程序以正确的方式退出,并返回适当的退出代码。
3.3 常用开发模式
在实际开发中,通常会将窗口代码封装成类,这比使用简单的函数更加规范和可维护。下面是一个使用类封装窗口的示例:
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QMessageBox
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
self.setWindowTitle('窗口类示例')
self.setGeometry(100, 100, 400, 300)
self.button = QPushButton('点击我', self)
self.button.move(150, 120)
self.button.clicked.connect(self.show_message)
def show_message(self):
QMessageBox.information(self, '提示', '你点击了按钮!')
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
这个示例展示了更规范的开发模式:创建自定义的窗口类继承自QWidget,在类的__init__方法中进行初始化工作,将UI初始化代码放在单独的init_ui方法中。这种结构使得代码更加清晰,也便于后续的扩展和维护。按钮的clicked信号连接到自定义的槽函数show_message,当按钮被点击时会弹出消息框。
4. 核心概念深入理解
4.1 信号与槽机制
信号与槽是PyQt5中最核心和独特的机制,它是Qt框架引入的一种强大的事件处理和对象通信方式。理解信号与槽的工作原理对于掌握PyQt5至关重要。信号(Signal)是一个对象发出的消息,表示某个动作或状态变化已经发生;槽(Slot)是一个可调用的函数,用于响应信号。当信号被发出时,与之连接的槽函数会被自动调用。
信号可以携带参数,这使得信号传递信息更加灵活。例如,QSlider的valueChanged信号会携带当前滑块的值,接收者可以获取这个值并进行处理。一个信号可以连接多个槽函数,一个槽函数也可以被多个信号连接,这种多对多的关系提供了极大的灵活性。信号与信号之间也可以连接,形成信号的链式传递。
信号的连接使用connect方法完成。基本的连接语法是:signal.connect(slot_function)。当信号被发出时,指定的槽函数会被调用。槽函数可以是普通的Python函数、lambda表达式或者其他可调用对象。以下是一些信号与槽的使用示例:
from PyQt5.QtWidgets import *
import sys
class SignalSlotDemo(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
self.setWindowTitle('信号与槽示例')
self.setGeometry(100, 100, 300, 200)
# 创建滑块
self.slider = QSlider(Qt.Horizontal, self)
self.slider.setMinimum(0)
self.slider.setMaximum(100)
self.slider.setValue(50)
self.slider.move(50, 50)
# 创建标签显示滑块值
self.label = QLabel('当前值: 50', self)
self.label.move(50, 100)
# 连接信号与槽
self.slider.valueChanged.connect(self.on_value_changed)
def on_value_changed(self, value):
self.label.setText(f'当前值: {value}')
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = SignalSlotDemo()
demo.show()
sys.exit(app.exec_())
在这个示例中,滑块的valueChanged信号被连接到on_value_changed槽函数。当用户拖动滑块时,信号会携带当前值发出,槽函数接收这个值并更新标签的显示。这种模式在PyQt5开发中非常常见,几乎所有的交互都是通过信号与槽来实现的。
除了用户自定义信号,PyQt5中的控件本身也提供了大量的内置信号。例如,QPushButton有clicked、pressed、released等信号;QCheckBox有toggled信号;QLineEdit有textChanged、editingFinished等信号。熟悉这些内置信号可以大大提高开发效率。在实际应用中,可以根据需要选择合适的信号来响应用户的操作。
4.2 布局管理系统
布局管理是PyQt5中组织用户界面的核心机制。Qt提供了多种布局管理器,每种布局都有其特定的用途和特点。正确使用布局管理器可以创建出美观、响应式的用户界面,并且能够自动适应窗口大小的变化。PyQt5中主要的布局管理器包括QVBoxLayout(垂直布局)、QHBoxLayout(水平布局)、QGridLayout(网格布局)和QFormLayout(表单布局)。
QVBoxLayout按照垂直方向依次排列子控件,控件会占据布局的整个宽度,高度由控件自身决定。这种布局适合创建简单的垂直列表界面,例如工具栏、按钮组等。QHBoxLayout则按照水平方向排列子控件,适合创建水平排列的元素,如工具栏中的按钮。QVBoxLayout和QHBoxLayout可以嵌套使用,创建复杂的界面结构。以下是使用垂直布局的示例:
from PyQt5.QtWidgets import *
class LayoutDemo(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
self.setWindowTitle('布局管理示例')
# 创建垂直布局
layout = QVBoxLayout()
# 添加标签和按钮
layout.addWidget(QLabel('这是一个垂直布局示例'))
layout.addWidget(QPushButton('按钮1'))
layout.addWidget(QPushButton('按钮2'))
layout.addWidget(QPushButton('按钮3'))
# 添加伸展项,使控件靠上对齐
layout.addStretch()
self.setLayout(layout)
self.setMinimumSize(300, 200)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
demo = LayoutDemo()
demo.show()
sys.exit(app.exec_())
QGridLayout将布局区域划分为网格,控件可以放置在指定的网格位置。这种布局适合创建表格形式的界面,例如计算器的按键布局。QGridLayout使用行和列的索引来定位控件,一个网格位置可以放置一个控件,也可以让一个控件跨越多个网格。以下是网格布局的示例:
from PyQt5.QtWidgets import *
class GridLayoutDemo(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
self.setWindowTitle('网格布局示例')
layout = QGridLayout()
# 添加网格式排列的按钮
names = ['7', '8', '9', '/',
'4', '5', '6', '*',
'1', '2', '3', '-',
'0', '.', '=', '+']
positions = [(i, j) for i in range(4) for j in range(4)]
for position, name in zip(positions, names):
button = QPushButton(name)
layout.addWidget(button, *position)
self.setLayout(layout)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
demo = GridLayoutDemo()
demo.show()
sys.exit(app.exec_())
QFormLayout是一种专门用于创建表单界面的布局管理器,它有两列:左列用于标签,右列用于输入控件。这种布局非常适合创建设置对话框、用户信息表单等界面。使用QFormLayout可以轻松创建对齐的标签-控件对,并且支持自动生成行号标签。
4.3 事件处理机制
PyQt5中的事件处理是理解应用程序行为的关键。每个Qt应用程序都有一个事件循环,它持续不断地处理各种事件并将其分发给相应的对象。事件可以是用户输入(鼠标点击、键盘按键)、系统事件(窗口重绘、定时器触发)或者自定义事件。理解事件的传播和处理方式,可以帮助开发者实现复杂的交互逻辑。
在PyQt5中,事件处理主要通过重写事件处理方法来实现。QObject的event方法可以接收所有类型的事件,然后根据事件类型分发给特定的处理方法。常见的事件处理方法包括:mousePressEvent(鼠标按下)、mouseReleaseEvent(鼠标释放)、mouseMoveEvent(鼠标移动)、keyPressEvent(键盘按下)、keyReleaseEvent(键盘释放)、paintEvent(重绘)、resizeEvent(调整大小)等。
以下是一个重写鼠标事件处理方法的示例:
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtCore import Qt, QPoint
import sys
class MouseEventDemo(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle('鼠标事件示例')
self.setGeometry(100, 100, 400, 300)
self.last_pos = QPoint()
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.last_pos = event.pos()
print(f'鼠标左键按下: ({event.x()}, {event.y()})')
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton:
print(f'鼠标左键释放: ({event.x()}, {event.y()})')
def mouseMoveEvent(self, event):
if event.buttons() & Qt.LeftButton:
current_pos = event.pos()
dx = current_pos.x() - self.last_pos.x()
dy = current_pos.y() - self.last_pos.y()
self.move(self.x() + dx, self.y() + dy)
self.last_pos = current_pos
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = MouseEventDemo()
demo.show()
sys.exit(app.exec_())
在这个示例中,我们重写了鼠标事件处理方法来实现一个简单的窗口拖动功能。mousePressEvent在鼠标按下时记录当前位置,mouseMoveEvent在鼠标移动时计算位移并移动窗口,mouseReleaseEvent在鼠标释放时结束拖动。这种直接重写事件处理方法的方式比使用信号槽更加底层和灵活,适用于需要精细控制交互的场景。
事件过滤是另一个强大的事件处理机制。通过安装事件过滤器,一个对象可以拦截并处理另一个对象的事件,而不需要继承该对象类。这种方式可以在不修改原有类的情况下,为对象添加新的行为。事件过滤器需要重写eventFilter方法,并在目标对象上安装过滤器。事件过滤器的使用场景包括:监控特定对象的事件、修改事件的行为、为多个对象添加相同的处理逻辑等。
5. 常用控件详解
5.1 基础显示控件
PyQt5提供了丰富的基础显示控件,这些控件是构建用户界面的基本元素。QLabel用于显示文本或图像,是最常用的显示控件之一。QLabel可以显示普通文本、HTML格式的富文本、图像以及动画。可以通过setText方法设置显示的文本,使用setAlignment方法设置文本对齐方式。QLabel还支持文本选择,这在使用户能够复制标签中的信息时非常有用。
QLabel的用法示例:
from PyQt5.QtWidgets import QLabel, QWidget, QVBoxLayout, QApplication
from PyQt5.QtCore import Qt
import sys
class LabelDemo(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
self.setWindowTitle('QLabel示例')
layout = QVBoxLayout()
# 普通文本标签
label1 = QLabel('这是一个普通文本标签')
layout.addWidget(label1)
# 富文本标签
label2 = QLabel('<h2>这是富文本标签</h2><p>支持<b>加粗</b>、<i>斜体</i>等格式</p>')
label2.setAlignment(Qt.AlignCenter)
layout.addWidget(label2)
# 设置样式的标签
label3 = QLabel('带有背景色的标签')
label3.setStyleSheet('background-color: yellow; padding: 10px;')
label3.setAlignment(Qt.AlignCenter)
layout.addWidget(label3)
self.setLayout(layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = LabelDemo()
demo.show()
sys.exit(app.exec_())
QLCDNumber用于显示数字,模拟液晶显示器的效果。它可以显示整数或浮点数,支持多种显示模式,如十进制、十六进制、八进制和二进制。QLCDNumber还可以显示时间或日期,适用于需要电子钟表显示效果的场景。设置显示值使用display方法,设置位数使用setDigitCount方法。
QProgressBar用于显示进度信息,常用于长时间操作的进度指示。可以设置为确定模式(显示具体进度百分比)或不确定模式(显示动画表示正在处理)。确定模式下使用setValue方法设置当前进度值,不确定模式下使用setRange和setValue设置范围。QProgressBar可以水平或垂直显示,还可以设置文本显示格式。
5.2 输入控件
输入控件允许用户输入和编辑数据。QLineEdit是最基本的文本输入控件,用于获取单行文本输入。它支持多种功能:输入验证(通过validator属性)、输入掩码(通过inputMask属性)、自动补全(通过completer属性)、密码模式(通过setEchoMode方法)等。QLineEdit还提供了textChanged和editingFinished等信号,用于响应文本变化和完成编辑。
QLineEdit的用法示例:
from PyQt5.QtWidgets import QLineEdit, QWidget, QVBoxLayout, QLabel, QApplication
from PyQt5.QtGui import QIntValidator, QDoubleValidator, QRegExpValidator
from PyQt5.QtCore import QRegExp
import sys
class LineEditDemo(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
self.setWindowTitle('QLineEdit示例')
layout = QVBoxLayout()
# 普通文本输入
self.normal_input = QLineEdit()
self.normal_input.setPlaceholderText('请输入文本')
layout.addWidget(QLabel('普通输入框:'))
layout.addWidget(self.normal_input)
# 整数输入(限制范围0-100)
self.int_input = QLineEdit()
int_validator = QIntValidator(0, 100, self)
self.int_input.setValidator(int_validator)
self.int_input.setPlaceholderText('请输入0-100的整数')
layout.addWidget(QLabel('整数输入:'))
layout.addWidget(self.int_input)
# 浮点数输入
self.double_input = QLineEdit()
double_validator = QDoubleValidator(0.0, 100.0, 2, self)
self.double_input.setValidator(double_validator)
self.double_input.setPlaceholderText('请输入0-100的浮点数')
layout.addWidget(QLabel('浮点数输入:'))
layout.addWidget(self.double_input)
# 密码输入
self.password_input = QLineEdit()
self.password_input.setEchoMode(QLineEdit.Password)
self.password_input.setPlaceholderText('请输入密码')
layout.addWidget(QLabel('密码输入:'))
layout.addWidget(self.password_input)
# 只读输入
self.readonly_input = QLineEdit('只读文本')
self.readonly_input.setReadOnly(True)
layout.addWidget(QLabel('只读输入:'))
layout.addWidget(self.readonly_input)
# 信号连接
self.normal_input.textChanged.connect(self.on_text_changed)
self.normal_input.editingFinished.connect(self.on_editing_finished)
self.setLayout(layout)
def on_text_changed(self, text):
print(f'文本变化: {text}')
def on_editing_finished(self):
print('编辑完成')
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = LineEditDemo()
demo.show()
sys.exit(app.exec_())
QTextEdit用于多行文本编辑,支持富文本格式(HTML)和纯文本。它提供了更多的格式化选项,适合需要编辑大量文本或需要文本格式化功能的场景。QTextEdit还可用作文本浏览器,通过setHtml或setPlainText方法设置显示内容。QTextEdit支持自动换行,可以设置只读模式,并且提供了丰富的文本操作方法。
QSpinBox和QDoubleSpinBox用于数值输入。QSpinBox处理整数,QDoubleSpinBox处理浮点数。它们提供了微调按钮(上下箭头),用户可以通过点击按钮或直接输入来调整数值。可以设置数值范围、步长、前缀和后缀等属性。QSpinBox和QDoubleSpinBox都支持键盘输入和鼠标滚轮调整,使用非常方便。
QComboBox是下拉列表控件,允许用户从预定义的选项中选择一个值。它可以设置为可编辑的,允许用户输入自定义值。QComboBox支持添加多个选项(使用addItem或addItems方法)、设置当前选项(setCurrentIndex或setCurrentText)、获取选中值(currentText或currentIndex)等操作。QComboBox还可以设置为只读或可编辑,支持图标显示。
5.3 按钮和选择控件
QPushButton是最常用的按钮控件,用于触发特定的操作。它可以显示文本、图标,或者同时显示两者。QPushButton支持多种样式,可以通过setCheckable方法设置为可切换状态( toggled button)。按钮可以设置为默认按钮(按回车触发)和自动默认按钮。常用的信号是clicked,在按钮被点击时发出。
QPushButton的用法示例:
from PyQt5.QtWidgets import (QPushButton, QWidget, QVBoxLayout,
QLabel, QApplication, QMessageBox)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon
import sys
class ButtonDemo(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
self.setWindowTitle('QPushButton示例')
self.setMinimumWidth(300)
layout = QVBoxLayout()
# 普通按钮
self.normal_btn = QPushButton('普通按钮')
self.normal_btn.clicked.connect(self.on_click)
layout.addWidget(self.normal_btn)
# 带有图标的按钮
self.icon_btn = QPushButton('带图标按钮')
# self.icon_btn.setIcon(QIcon('icon.png')) # 需要实际的图标文件
self.icon_btn.clicked.connect(self.on_click)
layout.addWidget(self.icon_btn)
# 可切换按钮
self.toggle_btn = QPushButton('切换按钮')
self.toggle_btn.setCheckable(True)
self.toggle_btn.clicked.connect(self.on_toggle)
layout.addWidget(self.toggle_btn)
# 扁平化按钮
self.flat_btn = QPushButton('扁平化按钮')
self.flat_btn.setFlat(True)
layout.addWidget(self.flat_btn)
# 信息标签
self.info_label = QLabel('点击按钮查看效果')
layout.addWidget(self.info_label)
self.setLayout(layout)
def on_click(self):
sender = self.sender()
self.info_label.setText(f'你点击了: {sender.text()}')
QMessageBox.information(self, '提示', f'按钮 "{sender.text()}" 被点击')
def on_toggle(self, checked):
state = '选中' if checked else '未选中'
self.info_label.setText(f'切换按钮状态: {state}')
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = ButtonDemo()
demo.show()
sys.exit(app.exec_())
QCheckBox是复选框控件,用于提供多项选择的功能。每个QCheckBox都是独立的,可以单独选中或取消选中。QCheckBox支持三种状态:未选中(off)、选中(on)和半选中(partially checked,半选中状态需要通过setTristate方法启用)。常用信号是stateChanged,在状态改变时发出。QCheckBox可以与QRadioButton结合使用,QRadioButton用于单选功能,同一组内的单选按钮只能选中一个。
QSlider是滑块控件,用于在一定范围内选择一个值。它支持水平或垂直方向,可以通过setOrientation方法设置。QSlider常用于音量控制、亮度调节、数值选择等场景。它提供了valueChanged信号,在值变化时发出;setSingleStep和setPageStep方法可以设置键盘操作时的步长。滑块还可以设置为可选的,显示勾号标记。
6. 对话框与窗口管理
6.1 标准对话框
PyQt5提供了多种标准对话框,用于常见的用户交互场景。这些对话框都是Modal的,即会阻塞当前窗口的操作,直到用户做出选择。标准对话框简化了常见操作的处理,提供了统一的用户体验。常用的标准对话框包括:QMessageBox(消息框)、QInputDialog(输入对话框)、QFileDialog(文件对话框)、QColorDialog(颜色对话框)和QFontDialog(字体对话框)。
QMessageBox用于显示信息和获取简单反馈。它支持多种预定义的图标和按钮组合,如Information、Warning、Critical、Question等。QMessageBox提供静态方法快速创建各种类型的消息框,也可以通过创建QMessageBox实例进行自定义。以下是QMessageBox的用法示例:
from PyQt5.QtWidgets import QMessageBox, QPushButton, QWidget, QVBoxLayout, QApplication
import sys
class MessageBoxDemo(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
self.setWindowTitle('QMessageBox示例')
layout = QVBoxLayout()
# 信息消息框
info_btn = QPushButton('信息消息框')
info_btn.clicked.connect(self.show_info)
layout.addWidget(info_btn)
# 警告消息框
warn_btn = QPushButton('警告消息框')
warn_btn.clicked.connect(self.show_warning)
layout.addWidget(warn_btn)
# 错误消息框
error_btn = QPushButton('错误消息框')
error_btn.clicked.connect(self.show_error)
layout.addWidget(error_btn)
# 问题消息框
question_btn = QPushButton('问题消息框')
question_btn.clicked.connect(self.show_question)
layout.addWidget(question_btn)
self.setLayout(layout)
def show_info(self):
QMessageBox.information(self, '信息', '这是一个信息提示框')
def show_warning(self):
QMessageBox.warning(self, '警告', '这是一个警告提示框')
def show_error(self):
QMessageBox.critical(self, '错误', '这是一个错误提示框')
def show_question(self):
reply = QMessageBox.question(
self, '确认', '确定要执行此操作吗?',
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
print('用户选择了是')
else:
print('用户选择了否')
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = MessageBoxDemo()
demo.show()
sys.exit(app.exec_())
QInputDialog用于获取用户输入,支持多种输入类型。getText方法获取文本输入,getInt和getDouble获取数值输入,getItem从列表中选择一项。这些方法都是静态方法,直接返回一个值或(值、是否确认)的元组。QInputDialog的使用非常简单,适合需要用户输入少量数据的场景。
QFileDialog用于打开或保存文件。它提供了getOpenFileName(打开单个文件)、getOpenFileNames(打开多个文件)、getSaveFileName(保存文件)和getExistingDirectory(选择目录)等方法。这些方法会弹出系统原生的文件对话框,返回选中的文件路径或路径列表。QFileDialog支持设置文件过滤器,只显示特定类型的文件。
6.2 自定义对话框
除了使用标准对话框,有时还需要创建自定义的对话框来满足特定的需求。自定义对话框通过继承QDialog类来实现,可以在对话框中放置任意的控件,并定义按钮来确定对话框的关闭方式和返回值。以下是创建自定义对话框的示例:
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QApplication)
import sys
class LoginDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle('用户登录')
self.setModal(True)
self.init_ui()
def init_ui(self):
layout = QVBoxLayout()
# 用户名输入
username_layout = QHBoxLayout()
username_layout.addWidget(QLabel('用户名:'))
self.username_input = QLineEdit()
username_layout.addWidget(self.username_input)
layout.addLayout(username_layout)
# 密码输入
password_layout = QHBoxLayout()
password_layout.addWidget(QLabel('密码:'))
self.password_input = QLineEdit()
self.password_input.setEchoMode(QLineEdit.Password)
password_layout.addWidget(self.password_input)
layout.addLayout(password_layout)
# 按钮
button_layout = QHBoxLayout()
ok_btn = QPushButton('登录')
cancel_btn = QPushButton('取消')
ok_btn.clicked.connect(self.accept)
cancel_btn.clicked.connect(self.reject)
button_layout.addWidget(ok_btn)
button_layout.addWidget(cancel_btn)
layout.addLayout(button_layout)
self.setLayout(layout)
def get_credentials(self):
return self.username_input.text(), self.password_input.text()
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle('主窗口')
self.setGeometry(100, 100, 300, 200)
from PyQt5.QtWidgets import QPushButton, QVBoxLayout
layout = QVBoxLayout()
login_btn = QPushButton('打开登录对话框')
login_btn.clicked.connect(self.show_login)
layout.addWidget(login_btn)
self.setLayout(layout)
def show_login(self):
dialog = LoginDialog(self)
if dialog.exec_() == QDialog.Accepted:
username, password = dialog.get_credentials()
print(f'用户名: {username}, 密码: {password}')
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
自定义对话框的关键是重写accept和reject方法,或者使用按钮的clicked信号连接这两个方法。当用户点击确定按钮时调用accept,点击取消按钮时调用reject。调用exec_方法会显示对话框并等待用户关闭,返回值是Accepted或Rejected,根据返回值可以判断用户的操作。自定义对话框可以像标准对话框一样使用,但提供了更大的灵活性。
6.3 主窗口结构
复杂的PyQt5应用程序通常使用QMainWindow作为主窗口。QMainWindow提供了标准的应用程序框架,包括菜单栏、工具栏、停靠窗口和状态栏等元素。与QWidget不同,QMainWindow有预定义的布局结构,开发者只需要将各个部分放入对应的区域即可。
菜单栏通过menubar方法创建,使用QMenu添加菜单项和子菜单。菜单项可以设置快捷键、图标和复选状态。QAction表示一个可执行的操作,可以在菜单、工具栏和键盘快捷键中使用。以下是创建菜单栏的示例:
from PyQt5.QtWidgets import (QMainWindow, QAction, QMenuBar, QMenu,
QApplication, QTextEdit, QToolBar)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QKeySequence
import sys
class MainWindowDemo(QMainWindow):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
self.setWindowTitle('QMainWindow示例')
self.setGeometry(100, 100, 600, 400)
# 创建中心部件
self.text_edit = QTextEdit()
self.setCentralWidget(self.text_edit)
# 创建菜单栏
menubar = self.menuBar()
# 文件菜单
file_menu = menubar.addMenu('文件')
new_action = QAction('新建', self)
new_action.setShortcut(QKeySequence.New)
new_action.triggered.connect(self.new_file)
file_menu.addAction(new_action)
open_action = QAction('打开', self)
open_action.setShortcut(QKeySequence.Open)
open_action.triggered.connect(self.open_file)
file_menu.addAction(open_action)
file_menu.addSeparator()
save_action = QAction('保存', self)
save_action.setShortcut(QKeySequence.Save)
save_action.triggered.connect(self.save_file)
file_menu.addAction(save_action)
file_menu.addSeparator()
exit_action = QAction('退出', self)
exit_action.setShortcut(QKeySequence.Quit)
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
# 编辑菜单
edit_menu = menubar.addMenu('编辑')
copy_action = QAction('复制', self)
copy_action.setShortcut(QKeySequence.Copy)
copy_action.triggered.connect(self.text_edit.copy)
edit_menu.addAction(copy_action)
paste_action = QAction('粘贴', self)
paste_action.setShortcut(QKeySequence.Paste)
paste_action.triggered.connect(self.text_edit.paste)
edit_menu.addAction(paste_action)
# 创建工具栏
toolbar = QToolBar('工具栏')
self.addToolBar(toolbar)
toolbar.addAction(new_action)
toolbar.addAction(open_action)
toolbar.addAction(save_action)
# 创建状态栏
self.statusBar().showMessage('就绪')
def new_file(self):
self.text_edit.clear()
self.statusBar().showMessage('新建文件')
def open_file(self):
self.statusBar().showMessage('打开文件')
def save_file(self):
self.statusBar().showMessage('保存文件')
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindowDemo()
window.show()
sys.exit(app.exec_())
工具栏通过addToolBar方法添加,可以放置常用的操作。工具栏可以移动、停靠,允许用户自定义布局。状态栏通过statusBar方法获取,用于显示状态信息和提示。QMainWindow还支持停靠窗口(通过addDockWidget方法添加),可以创建多面板的复杂界面。
7. 高级主题
7.1 多线程编程
在GUI应用程序中,执行耗时操作时如果阻塞主线程,会导致界面无响应。解决这个问题的方法是将耗时操作放到单独的线程中执行。PyQt5提供了QThread类来支持多线程编程。QThread是Qt的线程实现,可以创建多个线程,每个线程可以执行独立的任务。需要注意的是,Qt的GUI操作必须在主线程中执行,从其他线程直接操作GUI控件是不安全的。
QThread的使用方法有两种:一种是继承QThread并重写run方法,另一种是将QThread与QObject配合使用(worker对象模式)。第一种方法更加面向对象,适合需要封装线程逻辑的场景;第二种方法更加灵活,适合需要在线程和主线程之间传递数据的场景。以下是继承QThread的方法示例:
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout,
QPushButton, QLabel, QProgressBar)
from PyQt5.QtCore import QThread, pyqtSignal
import time
import sys
class WorkerThread(QThread):
# 定义信号,线程完成时发出
finished = pyqtSignal()
# 定义信号,进度更新时发出
progress = pyqtSignal(int)
def run(self):
for i in range(100):
time.sleep(0.1)
self.progress.emit(i + 1)
self.finished.emit()
class ThreadDemo(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
self.setWindowTitle('多线程示例')
layout = QVBoxLayout()
self.progress = QProgressBar()
self.progress.setRange(0, 100)
layout.addWidget(self.progress)
self.status_label = QLabel('就绪')
layout.addWidget(self.status_label)
self.start_btn = QPushButton('开始任务')
self.start_btn.clicked.connect(self.start_task)
layout.addWidget(self.start_btn)
self.setLayout(layout)
def start_task(self):
self.start_btn.setEnabled(False)
self.status_label.setText('任务进行中...')
# 创建并启动工作线程
self.worker = WorkerThread()
self.worker.progress.connect(self.update_progress)
self.worker.finished.connect(self.task_finished)
self.worker.start()
def update_progress(self, value):
self.progress.setValue(value)
def task_finished(self):
self.status_label.setText('任务完成')
self.start_btn.setEnabled(True)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = ThreadDemo()
demo.show()
sys.exit(app.exec_())
在这个示例中,我们创建了一个WorkerThread类继承自QThread,重写了run方法来执行耗时操作。线程通过emit方法发出信号,主线程通过连接信号来接收线程的状态更新。这种模式实现了线程与主线程的安全通信,避免了直接在子线程中操作GUI控件的问题。
信号与槽机制在多线程编程中尤为重要。pyqtSignal是PyQt5提供的线程安全信号,可以跨线程使用。当信号在另一个线程中发出时,会自动使用Qt的元对象系统将调用调度到主线程,这就是为什么我们在子线程中发出信号,但槽函数可以在主线程中执行的原因。
7.2 数据库操作
PyQt5内置了对SQL数据库的支持,通过Qt SQL模块可以连接和操作各种数据库,包括SQLite、MySQL、PostgreSQL等。Qt SQL使用统一的API操作不同的数据库,开发者不需要编写数据库特定的代码。PyQt5还提供了模型/视图架构来处理数据库查询结果,可以轻松地将数据库表显示在表格控件中。
SQLite是Python内置的轻量级数据库,适合桌面应用程序的数据存储。PyQt5可以直接使用SQLite,不需要额外的数据库服务器。以下是使用PyQt5操作SQLite数据库的示例:
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QTableView, QPushButton, QMessageBox, QAction)
from PyQt5.QtCore import Qt
from PyQt5.QtSql import QSqlDatabase, QSqlQuery, QSqlTableModel, QSqlQueryModel
import sys
class DatabaseDemo(QMainWindow):
def __init__(self):
super().__init__()
self.init_ui()
self.init_database()
def init_ui(self):
self.setWindowTitle('数据库操作示例')
self.setGeometry(100, 100, 600, 400)
# 创建菜单
menubar = self.menuBar()
file_menu = menubar.addMenu('文件')
refresh_action = QAction('刷新', self)
refresh_action.triggered.connect(self.load_data)
file_menu.addAction(refresh_action)
# 创建表格视图
self.table_view = QTableView()
self.setCentralWidget(self.table_view)
# 创建工具栏
toolbar = self.addToolBar('工具栏')
add_btn = QPushButton('添加记录')
add_btn.clicked.connect(self.add_record)
toolbar.addWidget(add_btn)
del_btn = QPushButton('删除记录')
del_btn.clicked.connect(self.delete_record)
toolbar.addWidget(del_btn)
def init_database(self):
# 创建数据库连接
db = QSqlDatabase.addDatabase('QSQLITE')
db.setDatabaseName('demo.db')
if not db.open():
QMessageBox.critical(self, '错误', '无法打开数据库')
return
# 创建表
query = QSqlQuery()
query.exec_('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL,
age INTEGER
)
''')
# 插入初始数据
query.exec_("SELECT COUNT(*) FROM users")
if query.next() and query.value(0) == 0:
query.exec_("INSERT INTO users (name, email, age) VALUES ('张三', 'zhangsan@example.com', 25)")
query.exec_("INSERT INTO users (name, email, age) VALUES ('李四', 'lisi@example.com', 30)")
query.exec_("INSERT INTO users (name, email, age) VALUES ('王五', 'wangwu@example.com', 28)")
self.load_data()
def load_data(self):
# 创建模型
self.model = QSqlTableModel(self)
self.model.setTable('users')
self.model.select()
# 设置表头
self.model.setHeaderData(0, Qt.Horizontal, 'ID')
self.model.setHeaderData(1, Qt.Horizontal, '姓名')
self.model.setHeaderData(2, Qt.Horizontal, '邮箱')
self.model.setHeaderData(3, Qt.Horizontal, '年龄')
# 设置视图
self.table_view.setModel(self.model)
self.table_view.setColumnHidden(0, True)
def add_record(self):
# 添加新记录
row = self.model.rowCount()
self.model.insertRow(row)
self.model.setData(self.model.index(row, 1), '新用户')
self.model.setData(self.model.index(row, 2), 'new@example.com')
self.model.setData(self.model.index(row, 3), 20)
self.model.submitAll()
def delete_record(self):
# 删除选中的行
current_row = self.table_view.currentIndex().row()
if current_row >= 0:
self.model.removeRow(current_row)
self.model.submitAll()
if __name__ == '__main__':
app = QApplication(sys.argv)
window = DatabaseDemo()
window.show()
sys.exit(app.exec_())
这个示例展示了PyQt5数据库操作的主要功能。首先通过QSqlDatabase创建数据库连接并打开SQLite数据库文件;然后使用QSqlQuery执行SQL语句创建表和插入初始数据;使用QSqlTableModel作为数据模型,可以直接与QTableView绑定实现数据的显示和编辑;通过模型的insertRow和removeRow方法可以实现记录的添加和删除,submitAll方法将更改提交到数据库。
7.3 网络通信
PyQt5提供了网络通信功能,通过Qt Network模块可以创建网络应用程序。常用的功能包括:使用QNetworkAccessManager进行HTTP请求、使用QTcpSocket和QUdpSocket进行TCP/UDP通信、使用QNetworkReply处理网络响应等。这些类提供了异步操作接口,不会阻塞主线程,适合GUI应用使用。
使用QNetworkAccessManager可以方便地发送HTTP请求并获取响应。它支持GET、POST等请求方法,可以设置请求头和上传数据。以下是使用QNetworkAccessManager的示例:
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout,
QPushButton, QTextEdit, QLabel)
from PyQt5.QtCore import QUrl, pyqtSlot
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
import sys
class NetworkDemo(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
self.setWindowTitle('网络请求示例')
layout = QVBoxLayout()
self.url_input = QLineEdit('https://httpbin.org/get')
self.url_input.setPlaceholderText('输入URL')
layout.addWidget(self.url_input)
self.fetch_btn = QPushButton('获取数据')
self.fetch_btn.clicked.connect(self.fetch_data)
layout.addWidget(self.fetch_btn)
self.status_label = QLabel('')
layout.addWidget(self.status_label)
self.result_text = QTextEdit()
self.result_text.setReadOnly(True)
layout.addWidget(self.result_text)
self.setLayout(layout)
# 创建网络管理器
self.manager = QNetworkAccessManager(self)
self.manager.finished.connect(self.on_finished)
def fetch_data(self):
url = QUrl(self.url_input.text())
request = QNetworkRequest(url)
self.status_label.setText('请求中...')
self.fetch_btn.setEnabled(False)
self.manager.get(request)
@pyqtSlot(QNetworkReply)
def on_finished(self, reply):
self.fetch_btn.setEnabled(True)
if reply.error() == QNetworkReply.NoError:
data = reply.readAll()
self.result_text.setText(str(data, encoding='utf-8'))
self.status_label.setText('请求成功')
else:
self.result_text.setText(f'错误: {reply.errorString()}')
self.status_label.setText('请求失败')
reply.deleteLater()
class QLineEdit(QWidget):
pass
import sys
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
self.setWindowTitle('网络请求示例')
layout = QVBoxLayout()
self.url_input = QLineEdit()
layout.addWidget(QLabel('URL:'))
layout.addWidget(self.url_input)
self.fetch_btn = QPushButton('获取数据')
layout.addWidget(self.fetch_btn)
self.status_label = QLabel('')
layout.addWidget(self.status_label)
self.result_text = QTextEdit()
self.result_text.setReadOnly(True)
layout.addWidget(self.result_text)
self.setLayout(layout)
self.manager = QNetworkAccessManager(self)
self.fetch_btn.clicked.connect(self.fetch_data)
def fetch_data(self):
url = QUrl(self.url_input.text())
request = QNetworkRequest(url)
self.manager.get(request)
self.manager.finished.connect(lambda reply: self.on_finished(reply))
def on_finished(self, reply):
if reply.error() == QNetworkReply.NoError:
data = reply.readAll()
self.result_text.setText(str(data, encoding='utf-8'))
reply.deleteLater()
if __name__ == '__main__':
from PyQt5.QtWidgets import QLineEdit as _QLineEdit
# 修复导入
import PyQt5.QtWidgets
class NetworkDemo(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
from PyQt5.QtWidgets import QVBoxLayout, QPushButton, QTextEdit, QLabel, QLineEdit
self.setWindowTitle('网络请求示例')
layout = QVBoxLayout()
from PyQt5.QtCore import QUrl
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
self.url_input = QLineEdit('https://httpbin.org/get')
layout.addWidget(QLabel('URL:'))
layout.addWidget(self.url_input)
self.fetch_btn = QPushButton('获取数据')
layout.addWidget(self.fetch_btn)
self.status_label = QLabel('')
layout.addWidget(self.status_label)
self.result_text = QTextEdit()
self.result_text.setReadOnly(True)
layout.addWidget(self.result_text)
self.setLayout(layout)
self.manager = QNetworkAccessManager(self)
self.fetch_btn.clicked.connect(self.fetch_data)
self.manager.finished.connect(self.on_finished)
def fetch_data(self):
url = QUrl(self.url_input.text())
request = QNetworkRequest(url)
self.status_label.setText('请求中...')
self.fetch_btn.setEnabled(False)
self.manager.get(request)
def on_finished(self, reply):
self.fetch_btn.setEnabled(True)
if reply.error() == QNetworkReply.NoError:
data = reply.readAll()
self.result_text.setText(str(data, encoding='utf-8'))
self.status_label.setText('请求成功')
else:
self.result_text.setText(f'错误: {reply.errorString()}')
self.status_label.setText('请求失败')
reply.deleteLater()
app = QApplication(sys.argv)
window = NetworkDemo()
window.show()
sys.exit(app.exec_())
网络请求是异步进行的,不会阻塞主界面。当请求完成时,finished信号会被发出,我们可以连接这个信号来处理响应结果。在on_finished方法中,首先检查是否有错误,然后读取响应数据。readAll方法返回一个QByteArray,需要将其转换为字符串显示。注意最后要调用deleteLater方法来释放reply对象,避免内存泄漏。
8. 项目实战:学生信息管理系统
8.1 项目概述与设计
为了综合运用PyQt5的知识,本节将开发一个完整的学生信息管理系统。该系统将包含以下功能:学生信息的增删改查、数据持久化存储、图形化界面展示。项目的目标是创建一个功能完整、界面美观、易于使用的桌面应用程序。
从功能层面分析,系统需要实现以下模块:学生列表展示模块,用于以表格形式显示所有学生的信息;学生信息编辑模块,用于添加和修改学生信息;查询模块,用于按照姓名或学号搜索学生;数据持久化模块,用于将数据保存到本地数据库。
从架构设计角度,我们采用典型的MVC(模型-视图-控制器)模式:模型层负责数据操作,包括数据库访问和数据验证;视图层负责界面展示,包括各种窗口和控件;控制器层负责业务逻辑,协调模型和视图之间的交互。这种分层架构可以提高代码的可维护性和可扩展性。
8.2 完整代码实现
以下是学生信息管理系统的完整实现代码。这个示例涵盖了前面介绍的大部分知识点,包括数据库操作、表格视图、自定义对话框、信号槽机制等:
import sys
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QTableWidget, QTableWidgetItem,
QPushButton, QLineEdit, QLabel, QMessageBox,
QDialog, QFormLayout, QSpinBox, QComboBox,
QAction, QToolBar, QStatusBar, QHeaderView)
from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtSql import QSqlDatabase, QSqlQuery, QSqlTableModel
class DatabaseManager:
_instance = None
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = DatabaseManager()
return cls._instance
def __init__(self):
self.db = QSqlDatabase.addDatabase('QSQLITE')
self.db.setDatabaseName('students.db')
if not self.db.open():
raise Exception('无法打开数据库')
self.create_tables()
def create_tables(self):
query = QSqlQuery()
query.exec_('''
CREATE TABLE IF NOT EXISTS students (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
student_id TEXT NOT NULL UNIQUE,
age INTEGER NOT NULL,
gender TEXT NOT NULL,
grade TEXT NOT NULL,
phone TEXT,
email TEXT
)
''')
def add_student(self, data):
query = QSqlQuery()
query.prepare('''
INSERT INTO students (name, student_id, age, gender, grade, phone, email)
VALUES (?, ?, ?, ?, ?, ?, ?)
''')
query.addBindValue(data['name'])
query.addBindValue(data['student_id'])
query.addBindValue(data['age'])
query.addBindValue(data['gender'])
query.addBindValue(data['grade'])
query.addBindValue(data['phone'])
query.addBindValue(data['email'])
return query.exec_()
def update_student(self, data):
query = QSqlQuery()
query.prepare('''
UPDATE students SET name=?, age=?, gender=?, grade=?, phone=?, email=?
WHERE student_id=?
''')
query.addBindValue(data['name'])
query.addBindValue(data['age'])
query.addBindValue(data['gender'])
query.addBindValue(data['grade'])
query.addBindValue(data['phone'])
query.addBindValue(data['email'])
query.addBindValue(data['student_id'])
return query.exec_()
def delete_student(self, student_id):
query = QSqlQuery()
return query.exec_(f"DELETE FROM students WHERE student_id='{student_id}'")
def search_students(self, keyword):
query = QSqlQuery()
query.exec_(f'''
SELECT * FROM students
WHERE name LIKE '%{keyword}%' OR student_id LIKE '%{keyword}%'
''')
results = []
while query.next():
results.append({
'id': query.value(0),
'name': query.value(1),
'student_id': query.value(2),
'age': query.value(3),
'gender': query.value(4),
'grade': query.value(5),
'phone': query.value(6),
'email': query.value(7)
})
return results
class StudentDialog(QDialog):
def __init__(self, parent=None, student_data=None):
super().__init__(parent)
self.student_data = student_data or {}
self.setWindowTitle('学生信息')
self.init_ui()
def init_ui(self):
layout = QFormLayout()
self.name_input = QLineEdit(self.student_data.get('name', ''))
self.id_input = QLineEdit(self.student_data.get('student_id', ''))
self.age_input = QSpinBox()
self.age_input.setRange(10, 100)
self.age_input.setValue(self.student_data.get('age', 18))
self.gender_combo = QComboBox()
self.gender_combo.addItems(['男', '女'])
self.grade_input = QLineEdit(self.student_data.get('grade', ''))
self.phone_input = QLineEdit(self.student_data.get('phone', ''))
self.email_input = QLineEdit(self.student_data.get('email', ''))
if self.student_data.get('gender') == '女':
self.gender_combo.setCurrentIndex(1)
layout.addRow('姓名:', self.name_input)
layout.addRow('学号:', self.id_input)
layout.addRow('年龄:', self.age_input)
layout.addRow('性别:', self.gender_combo)
layout.addRow('班级:', self.grade_input)
layout.addRow('电话:', self.phone_input)
layout.addRow('邮箱:', self.email_input)
btn_layout = QHBoxLayout()
ok_btn = QPushButton('确定')
cancel_btn = QPushButton('取消')
ok_btn.clicked.connect(self.accept)
cancel_btn.clicked.connect(self.reject)
btn_layout.addWidget(ok_btn)
btn_layout.addWidget(cancel_btn)
layout.addRow(btn_layout)
self.setLayout(layout)
if self.student_data:
self.id_input.setEnabled(False)
def get_data(self):
return {
'name': self.name_input.text(),
'student_id': self.id_input.text(),
'age': self.age_input.value(),
'gender': self.gender_combo.currentText(),
'grade': self.grade_input.text(),
'phone': self.phone_input.text(),
'email': self.email_input.text()
}
class StudentManagerWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle('学生信息管理系统')
self.setGeometry(100, 100, 800, 600)
self.db_manager = DatabaseManager.get_instance()
self.init_ui()
self.load_data()
def init_ui(self):
menubar = self.menuBar()
file_menu = menubar.addMenu('文件')
refresh_action = QAction('刷新', self)
refresh_action.triggered.connect(self.load_data)
file_menu.addAction(refresh_action)
file_menu.addSeparator()
exit_action = QAction('退出', self)
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
help_menu = menubar.addMenu('帮助')
about_action = QAction('关于', self)
about_action.triggered.connect(self.show_about)
help_menu.addAction(about_action)
toolbar = QToolBar()
self.addToolBar(toolbar)
add_btn = QPushButton('添加')
add_btn.clicked.connect(self.add_student)
toolbar.addWidget(add_btn)
edit_btn = QPushButton('编辑')
edit_btn.clicked.connect(self.edit_student)
toolbar.addWidget(edit_btn)
delete_btn = QPushButton('删除')
delete_btn.clicked.connect(self.delete_student)
toolbar.addWidget(delete_btn)
toolbar.addSeparator()
self.search_input = QLineEdit()
self.search_input.setPlaceholderText('搜索姓名或学号')
self.search_input.setMaximumWidth(200)
self.search_input.returnPressed.connect(self.search_students)
toolbar.addWidget(self.search_input)
search_btn = QPushButton('搜索')
search_btn.clicked.connect(self.search_students)
toolbar.addWidget(search_btn)
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout()
self.table = QTableWidget()
self.table.setColumnCount(7)
self.table.setHorizontalHeaderLabels(['学号', '姓名', '年龄', '性别', '班级', '电话', '邮箱'])
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.table.setSelectionBehavior(QTableWidget.SelectRows)
self.table.setEditTriggers(QTableWidget.NoEditTriggers)
self.table.setAlternatingRowColors(True)
layout.addWidget(self.table)
central_widget.setLayout(layout)
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
def load_data(self):
query = QSqlQuery()
query.exec_('SELECT * FROM students')
self.table.setRowCount(0)
row = 0
while query.next():
self.table.insertRow(row)
db_indices = [2, 1, 3, 4, 5, 6, 7]
for col, db_idx in enumerate(db_indices):
item = QTableWidgetItem(str(query.value(db_idx)))
item.setTextAlignment(Qt.AlignCenter)
self.table.setItem(row, col, item)
row += 1
self.status_bar.showMessage(f'共 {row} 名学生')
def add_student(self):
dialog = StudentDialog(self)
if dialog.exec_() == QDialog.Accepted:
data = dialog.get_data()
if not data['name'] or not data['student_id']:
QMessageBox.warning(self, '错误', '姓名和学号不能为空')
return
if self.db_manager.add_student(data):
QMessageBox.information(self, '成功', '添加成功')
self.load_data()
else:
QMessageBox.critical(self, '错误', '学号已存在或添加失败')
def edit_student(self):
current_row = self.table.currentRow()
if current_row < 0:
QMessageBox.warning(self, '提示', '请选择要编辑的学生')
return
student_id = self.table.item(current_row, 0).text()
query = QSqlQuery()
query.exec_(f"SELECT * FROM students WHERE student_id='{student_id}'")
if query.next():
student_data = {
'name': query.value(1),
'student_id': query.value(2),
'age': query.value(3),
'gender': query.value(4),
'grade': query.value(5),
'phone': query.value(6),
'email': query.value(7)
}
dialog = StudentDialog(self, student_data)
if dialog.exec_() == QDialog.Accepted:
data = dialog.get_data()
if self.db_manager.update_student(data):
QMessageBox.information(self, '成功', '修改成功')
self.load_data()
else:
QMessageBox.critical(self, '错误', '修改失败')
def delete_student(self):
current_row = self.table.currentRow()
if current_row < 0:
QMessageBox.warning(self, '提示', '请选择要删除的学生')
return
student_id = self.table.item(current_row, 0).text()
name = self.table.item(current_row, 1).text()
reply = QMessageBox.question(
self, '确认', f'确定要删除学生 "{name}" 吗?',
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
if self.db_manager.delete_student(student_id):
QMessageBox.information(self, '成功', '删除成功')
self.load_data()
else:
QMessageBox.critical(self, '错误', '删除失败')
def search_students(self):
keyword = self.search_input.text()
if not keyword:
self.load_data()
return
results = self.db_manager.search_students(keyword)
self.table.setRowCount(0)
row = 0
for data in results:
self.table.insertRow(row)
items = [
data['student_id'], data['name'], str(data['age']),
data['gender'], data['grade'], data['phone'], data['email']
]
for col, text in enumerate(items):
item = QTableWidgetItem(text)
item.setTextAlignment(Qt.AlignCenter)
self.table.setItem(row, col, item)
row += 1
self.status_bar.showMessage(f'找到 {row} 个结果')
def show_about(self):
QMessageBox.about(
self, '关于',
'学生信息管理系统 v1.0\n\n基于 PyQt5 开发'
)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = StudentManagerWindow()
window.show()
sys.exit(app.exec_())
8.3 运行与测试
将上述代码保存为student_manager.py,确保已经安装了PyQt5,然后运行程序。程序启动后会显示一个包含菜单栏、工具栏和表格的主窗口。可以通过工具栏的按钮添加、编辑和删除学生信息,使用搜索框可以根据姓名或学号快速查找学生。所有数据会自动保存到本地SQLite数据库中,重启程序后数据仍然存在。
在测试过程中,可以按照以下步骤进行验证:点击"添加"按钮,在弹出的对话框中填写学生信息,点击确定后学生信息会显示在表格中;选中表格中的一行,点击"编辑"按钮可以修改学生信息;选中表格中的一行,点击"删除"按钮会弹出确认对话框,确认后删除选中的学生;在搜索框中输入关键字,按回车或点击"搜索"按钮,可以过滤显示符合条件的学生。
9. 最佳实践与性能优化
9.1 代码组织与架构
随着项目规模的增长,良好的代码组织变得尤为重要。建议将不同功能的代码放在不同的模块中,使用面向对象的编程方式,合理封装数据和行为。以下是一些推荐的代码组织方式:窗口类应该继承自QMainWindow或QWidget,将相关的控件和逻辑封装在类中;数据访问代码应该封装在独立的类或模块中;使用常量类或配置文件管理应用程序的设置和参数。
资源管理也是需要关注的问题。PyQt5对象有父子层次关系,适当的父子关系可以帮助自动管理内存。创建控件时可以传入父对象,这样父对象被销毁时,子对象也会被自动销毁。另外,对于打开的资源(如文件、数据库连接),应该在适当的时机显式关闭。
9.2 样式与主题定制
PyQt5支持使用Qt Style Sheets(QSS)进行界面样式定制,这与CSS类似但有Qt特定的属性和选择器。通过setStyleSheet方法可以为控件设置样式,实现自定义的外观。以下是一些样式设置的示例:
# 设置按钮样式
button.setStyleSheet('''
QPushButton {
background-color: #0078D7;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
}
QPushButton:hover {
background-color: #106EBE;
}
QPushButton:pressed {
background-color: #005A9E;
}
QPushButton:disabled {
background-color: #CCCCCC;
color: #888888;
}
''')
# 设置表格样式
table.setStyleSheet('''
QTableWidget {
border: 1px solid #CCCCCC;
background-color: white;
gridline-color: #E0E0E0;
}
QTableWidget::item {
padding: 5px;
}
QTableWidget::item:selected {
background-color: #0078D7;
color: white;
}
QHeaderView::section {
background-color: #F5F5F5;
padding: 8px;
border: none;
border-bottom: 1px solid #CCCCCC;
font-weight: bold;
}
''')
除了手动设置样式,还可以使用Qt内置的主题或第三方的主题库。QStyleFactory提供了几个内置主题,如Fusion、Windows、WindowsVista。选择不同的主题可以改变应用程序的整体外观。
9.3 性能优化建议
在开发PyQt5应用时,需要注意一些性能相关的最佳实践。首先,避免在主线程中执行耗时操作,这会导致界面卡顿。耗时操作应该放到工作线程中执行,通过信号槽机制与主线程通信。其次,合理使用模型/视图架构,特别是在处理大量数据时,直接操作控件可能会导致性能问题。
对于频繁更新的界面,应该考虑使用Qt的增量更新机制,而不是每次都完全重绘。在需要显示大量数据时,可以使用QTableView或QTreeView的虚拟滚动功能,只渲染可见区域的内容。另外,注意及时释放不再使用的资源,避免内存泄漏。
10. 总结与进阶学习
本技术文档系统性地介绍了PyQt5桌面应用开发的各个方面,从环境配置和基础概念开始,逐步深入到高级主题和实战项目。读者应该已经掌握了PyQt5的核心概念,包括信号与槽机制、布局管理系统和事件处理;熟悉了常用控件的使用方法,包括显示控件、输入控件和按钮控件;了解了对话框、窗口管理、多线程编程、数据库操作和网络通信等高级主题;通过学生信息管理系统的实战项目,体验了PyQt5开发的完整流程。
学习PyQt5只是桌面应用开发的起点,还有很多方向可以进一步探索。在图形和可视化方面,可以学习QGraphicsView框架进行自定义图形绘制,使用QCharts或PyQtGraph创建数据可视化图表。在高级UI设计方面,可以研究Qt Quick和QML进行现代化UI开发,实现更丰富的动画和交互效果。在跨平台部署方面,可以了解PyInstaller等工具将Python应用打包为可执行文件,实现应用程序的分发。
Qt框架本身是一个功能极为丰富的跨平台开发框架,PyQt5只是其Python绑定。深入学习Qt的底层机制可以帮助更好地理解PyQt5的工作原理,也可以为进一步学习Qt C++开发打下基础。此外,PyQt6是PyQt5的升级版本,在命名空间和API上有一些变化,如果有兴趣也可以了解两者的差异和迁移方法。
PyQt5是一个成熟、稳定且功能强大的桌面应用开发框架。无论是开发小型工具还是复杂的企业级应用,PyQt5都能提供良好的支持。希望本技术文档能够帮助读者快速入门PyQt5开发,并在实践中不断提升技能,最终能够独立完成各种桌面应用项目的开发工作。持续练习和实践是掌握任何技术的关键,祝愿每一位读者在PyQt5开发道路上都能取得丰硕的成果。
更多推荐


所有评论(0)