在当今的软件开发世界中,桌面应用程序依然占据着重要地位。虽然Web应用和移动应用发展迅猛,但桌面应用在性能、用户体验和功能丰富性方面仍有其独特优势。PyQt5作为Python生态系统中最强大的GUI开发框架之一,为开发者提供了构建专业级跨平台桌面应用的完整解决方案。
本教程将带您从零开始,系统性地掌握PyQt5开发技术,最终能够独立构建功能完整的桌面应用程序。
一、PyQt5 概览与环境搭建
1.1 PyQt5 简介
什么是PyQt5
PyQt5是一个功能强大的Python GUI工具包,它是Qt框架的Python绑定。Qt是由挪威Trolltech公司开发的跨平台C++图形用户界面应用程序开发框架,被广泛应用于开发GUI程序。
PyQt5的主要特点:
- 跨平台:支持Windows、macOS、Linux等主流操作系统
- 功能丰富:提供完整的GUI控件集合
- 高性能:基于C++的Qt框架,性能优异
- 专业外观:原生外观,用户体验佳
- 开源免费:GPL和商业双重许可
PyQt5 vs 其他GUI框架对比
特性 | PyQt5 | Tkinter | wxPython |
---|---|---|---|
学习曲线 | 中等 | 简单 | 中等 |
控件丰富度 | 非常丰富 | 基础 | 丰富 |
外观美观度 | 优秀 | 一般 | 良好 |
跨平台性 | 优秀 | 良好 | 优秀 |
文档完整度 | 优秀 | 良好 | 一般 |
社区活跃度 | 高 | 高 | 中等 |
1.2 开发环境准备
Python环境要求
PyQt5支持Python 3.5及以上版本,推荐使用Python 3.7或更高版本:
bash
# 检查Python版本
python --version
# 或
python3 --version
PyQt5安装方法
使用pip安装PyQt5是最简单的方法:
bash
# 安装PyQt5
pip install PyQt5
# 安装PyQt5开发工具(包含Qt Designer)
pip install PyQt5-tools
# 验证安装
python -c "import PyQt5; print(PyQt5.Qt.PYQT_VERSION_STR)"
Qt Designer安装与配置
Qt Designer是PyQt5的可视化界面设计工具:
bash
# 查找Qt Designer位置
python -c "from PyQt5 import Qt; print(Qt.QLibraryInfo.location(Qt.QLibraryInfo.BinariesPath))"
# Windows系统通常在:
# Python安装目录/Lib/site-packages/qt5_applications/Qt/bin/designer.exe
IDE推荐与配置
PyCharm配置:
- 打开File → Settings → Tools → External Tools
- 添加Qt Designer工具:
- Name: Qt Designer
- Program: designer.exe的完整路径
- Working directory: <math xmlns="http://www.w3.org/1998/Math/MathML"> P r o j e c t F i l e D i r ProjectFileDir </math>ProjectFileDir
VS Code配置: 安装Python扩展和Qt for Python扩展,配置tasks.json文件以便快速启动Qt Designer。
1.3 第一个PyQt5程序
让我们从一个简单的"Hello World"程序开始:
python
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QVBoxLayout
from PyQt5.QtCore import Qt
class HelloWorldWindow(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
# 设置窗口属性
self.setWindowTitle('我的第一个PyQt5程序')
self.setGeometry(300, 300, 400, 200)
# 创建标签
label = QLabel('Hello, PyQt5!')
label.setAlignment(Qt.AlignCenter)
label.setStyleSheet("font-size: 18px; color: blue;")
# 创建布局
layout = QVBoxLayout()
layout.addWidget(label)
# 设置布局
self.setLayout(layout)
def main():
# 创建应用程序对象
app = QApplication(sys.argv)
# 创建窗口
window = HelloWorldWindow()
window.show()
# 进入应用程序主循环
sys.exit(app.exec_())
if __name__ == '__main__':
main()
程序结构分析
- 导入模块:导入必要的PyQt5模块
- 创建应用程序对象:QApplication管理整个应用程序
- 创建窗口类:继承自QWidget,定义窗口行为
- 初始化界面:设置窗口属性和控件
- 显示窗口:调用show()方法显示窗口
- 进入主循环:app.exec_()启动事件循环
二、PyQt5 核心概念与基础组件
2.1 核心架构理解
信号与槽机制
信号与槽是PyQt5最重要的特性之一,它提供了对象间通信的机制:
python
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QLabel
class SignalSlotDemo(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle('信号与槽示例')
self.setGeometry(300, 300, 300, 150)
# 创建控件
self.label = QLabel('点击次数: 0')
self.button = QPushButton('点击我')
self.counter = 0
# 连接信号与槽
self.button.clicked.connect(self.on_button_clicked)
# 布局
layout = QVBoxLayout()
layout.addWidget(self.label)
layout.addWidget(self.button)
self.setLayout(layout)
def on_button_clicked(self):
"""按钮点击事件处理函数(槽函数)"""
self.counter += 1
self.label.setText(f'点击次数: {self.counter}')
def main():
app = QApplication(sys.argv)
window = SignalSlotDemo()
window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
自定义信号
您也可以创建自定义信号:
python
from PyQt5.QtCore import pyqtSignal, QObject
class CustomSignalDemo(QWidget):
# 定义自定义信号
custom_signal = pyqtSignal(str)
def __init__(self):
super().__init__()
self.initUI()
# 连接自定义信号
self.custom_signal.connect(self.handle_custom_signal)
def initUI(self):
self.setWindowTitle('自定义信号示例')
self.button = QPushButton('发送自定义信号')
self.button.clicked.connect(self.emit_custom_signal)
self.label = QLabel('等待信号...')
layout = QVBoxLayout()
layout.addWidget(self.button)
layout.addWidget(self.label)
self.setLayout(layout)
def emit_custom_signal(self):
"""发送自定义信号"""
self.custom_signal.emit("自定义信号被触发!")
def handle_custom_signal(self, message):
"""处理自定义信号"""
self.label.setText(message)
2.2 基础窗口组件
QMainWindow主窗口
QMainWindow是最常用的顶级窗口类,提供了菜单栏、工具栏、状态栏等:
python
from PyQt5.QtWidgets import (QMainWindow, QMenuBar, QStatusBar,
QToolBar, QAction, QTextEdit)
from PyQt5.QtCore import Qt
class MainWindowDemo(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle('主窗口示例')
self.setGeometry(100, 100, 800, 600)
# 创建中央控件
central_widget = QTextEdit()
central_widget.setPlainText('这是主窗口的中央区域')
self.setCentralWidget(central_widget)
# 创建菜单栏
self.create_menu_bar()
# 创建工具栏
self.create_tool_bar()
# 创建状态栏
self.create_status_bar()
def create_menu_bar(self):
"""创建菜单栏"""
menubar = self.menuBar()
# 文件菜单
file_menu = menubar.addMenu('文件')
new_action = QAction('新建', self)
new_action.setShortcut('Ctrl+N')
new_action.triggered.connect(self.new_file)
file_menu.addAction(new_action)
open_action = QAction('打开', self)
open_action.setShortcut('Ctrl+O')
open_action.triggered.connect(self.open_file)
file_menu.addAction(open_action)
file_menu.addSeparator()
exit_action = QAction('退出', self)
exit_action.setShortcut('Ctrl+Q')
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
def create_tool_bar(self):
"""创建工具栏"""
toolbar = self.addToolBar('主工具栏')
new_action = QAction('新建', self)
new_action.triggered.connect(self.new_file)
toolbar.addAction(new_action)
open_action = QAction('打开', self)
open_action.triggered.connect(self.open_file)
toolbar.addAction(open_action)
def create_status_bar(self):
"""创建状态栏"""
status_bar = self.statusBar()
status_bar.showMessage('就绪')
def new_file(self):
self.statusBar().showMessage('新建文件', 2000)
self.centralWidget().clear()
def open_file(self):
self.statusBar().showMessage('打开文件', 2000)
2.3 常用控件详解
按钮类控件
python
from PyQt5.QtWidgets import (QWidget, QPushButton, QRadioButton,
QCheckBox, QVBoxLayout, QHBoxLayout,
QButtonGroup, QLabel)
class ButtonDemo(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle('按钮控件示例')
self.setGeometry(300, 300, 400, 300)
main_layout = QVBoxLayout()
# QPushButton 普通按钮
self.push_button = QPushButton('普通按钮')
self.push_button.clicked.connect(self.on_push_button_clicked)
main_layout.addWidget(self.push_button)
# QRadioButton 单选按钮
radio_layout = QHBoxLayout()
radio_layout.addWidget(QLabel('选择性别:'))
self.radio_male = QRadioButton('男')
self.radio_female = QRadioButton('女')
self.radio_male.setChecked(True) # 默认选中
# 创建按钮组确保单选
self.gender_group = QButtonGroup()
self.gender_group.addButton(self.radio_male)
self.gender_group.addButton(self.radio_female)
radio_layout.addWidget(self.radio_male)
radio_layout.addWidget(self.radio_female)
main_layout.addLayout(radio_layout)
# QCheckBox 复选框
main_layout.addWidget(QLabel('兴趣爱好:'))
self.check_reading = QCheckBox('阅读')
self.check_music = QCheckBox('音乐')
self.check_sports = QCheckBox('运动')
main_layout.addWidget(self.check_reading)
main_layout.addWidget(self.check_music)
main_layout.addWidget(self.check_sports)
# 结果显示
self.result_label = QLabel('结果显示区域')
main_layout.addWidget(self.result_label)
# 提交按钮
submit_button = QPushButton('提交')
submit_button.clicked.connect(self.on_submit)
main_layout.addWidget(submit_button)
self.setLayout(main_layout)
def on_push_button_clicked(self):
self.result_label.setText('普通按钮被点击了!')
def on_submit(self):
# 获取单选按钮选择
gender = '男' if self.radio_male.isChecked() else '女'
# 获取复选框选择
hobbies = []
if self.check_reading.isChecked():
hobbies.append('阅读')
if self.check_music.isChecked():
hobbies.append('音乐')
if self.check_sports.isChecked():
hobbies.append('运动')
result = f'性别: {gender}, 兴趣: {", ".join(hobbies) if hobbies else "无"}'
self.result_label.setText(result)
输入类控件
python
from PyQt5.QtWidgets import (QWidget, QLineEdit, QTextEdit, QSpinBox,
QDoubleSpinBox, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QSlider)
from PyQt5.QtCore import Qt
class InputDemo(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle('输入控件示例')
self.setGeometry(300, 300, 500, 400)
layout = QVBoxLayout()
# QLineEdit 单行文本输入
layout.addWidget(QLabel('姓名:'))
self.name_edit = QLineEdit()
self.name_edit.setPlaceholderText('请输入您的姓名')
self.name_edit.textChanged.connect(self.on_name_changed)
layout.addWidget(self.name_edit)
# QTextEdit 多行文本输入
layout.addWidget(QLabel('个人简介:'))
self.intro_edit = QTextEdit()
self.intro_edit.setPlaceholderText('请输入个人简介...')
self.intro_edit.setMaximumHeight(100)
layout.addWidget(self.intro_edit)
# QSpinBox 整数输入
age_layout = QHBoxLayout()
age_layout.addWidget(QLabel('年龄:'))
self.age_spin = QSpinBox()
self.age_spin.setRange(0, 120)
self.age_spin.setValue(25)
self.age_spin.valueChanged.connect(self.on_age_changed)
age_layout.addWidget(self.age_spin)
layout.addLayout(age_layout)
# QDoubleSpinBox 浮点数输入
salary_layout = QHBoxLayout()
salary_layout.addWidget(QLabel('薪资(K):'))
self.salary_spin = QDoubleSpinBox()
self.salary_spin.setRange(0.0, 999.9)
self.salary_spin.setDecimals(1)
self.salary_spin.setValue(10.0)
salary_layout.addWidget(self.salary_spin)
layout.addLayout(salary_layout)
# QSlider 滑块
slider_layout = QVBoxLayout()
slider_layout.addWidget(QLabel('满意度评分:'))
self.satisfaction_slider = QSlider(Qt.Horizontal)
self.satisfaction_slider.setRange(0, 10)
self.satisfaction_slider.setValue(5)
self.satisfaction_slider.valueChanged.connect(self.on_slider_changed)
self.slider_label = QLabel('5')
self.slider_label.setAlignment(Qt.AlignCenter)
slider_layout.addWidget(self.satisfaction_slider)
slider_layout.addWidget(self.slider_label)
layout.addLayout(slider_layout)
# 显示结果
self.result_label = QLabel('输入信息将在这里显示')
layout.addWidget(self.result_label)
# 提交按钮
submit_button = QPushButton('获取所有输入')
submit_button.clicked.connect(self.get_all_inputs)
layout.addWidget(submit_button)
self.setLayout(layout)
def on_name_changed(self, text):
if len(text) > 10:
self.name_edit.setText(text[:10])
def on_age_changed(self, value):
if value >= 60:
self.result_label.setText('注意:已达到退休年龄')
else:
self.result_label.setText('')
def on_slider_changed(self, value):
self.slider_label.setText(str(value))
def get_all_inputs(self):
name = self.name_edit.text()
intro = self.intro_edit.toPlainText()
age = self.age_spin.value()
salary = self.salary_spin.value()
satisfaction = self.satisfaction_slider.value()
result = f"""
输入信息汇总:
姓名: {name}
年龄: {age}
薪资: {salary}K
满意度: {satisfaction}/10
简介: {intro[:50]}{'...' if len(intro) > 50 else ''}
"""
self.result_label.setText(result)
三、布局管理与界面设计
3.1 布局管理器
布局管理器是PyQt5中控制控件位置和大小的重要工具。
QHBoxLayout水平布局
python
from PyQt5.QtWidgets import (QWidget, QHBoxLayout, QPushButton,
QVBoxLayout, QLabel)
class HBoxLayoutDemo(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle('水平布局示例')
self.setGeometry(300, 300, 400, 100)
# 创建水平布局
hbox = QHBoxLayout()
# 添加按钮
btn1 = QPushButton('按钮1')
btn2 = QPushButton('按钮2')
btn3 = QPushButton('按钮3')
hbox.addWidget(btn1)
hbox.addWidget(btn2)
hbox.addWidget(btn3)
# 设置拉伸因子
hbox.setStretchFactor(btn1, 1) # 按钮1占1份
hbox.setStretchFactor(btn2, 2) # 按钮2占2份
hbox.setStretchFactor(btn3, 1) # 按钮3占1份
self.setLayout(hbox)
QVBoxLayout垂直布局
python
class VBoxLayoutDemo(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle('垂直布局示例')
self.setGeometry(300, 300, 200, 300)
# 创建垂直布局
vbox = QVBoxLayout()
# 添加控件
label = QLabel('垂直布局示例')
btn1 = QPushButton('第一个按钮')
btn2 = QPushButton('第二个按钮')
btn3 = QPushButton('第三个按钮')
vbox.addWidget(label)
vbox.addWidget(btn1)
vbox.addWidget(btn2)
vbox.addWidget(btn3)
# 添加弹性空间
vbox.addStretch()
self.setLayout(vbox)
QGridLayout网格布局
python
from PyQt5.QtWidgets import QGridLayout
class GridLayoutDemo(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle('网格布局示例 - 计算器')
self.setGeometry(300, 300, 300, 400)
# 创建网格布局
grid = QGridLayout()
# 显示屏
display = QLineEdit()
display.setReadOnly(True)
display.setStyleSheet("font-size: 18px; padding: 10px;")
grid.addWidget(display, 0, 0, 1, 4) # 跨4列
# 按钮数据
buttons = [
('C', 1, 0), ('±', 1, 1), ('%', 1, 2), ('÷', 1, 3),
('7', 2, 0), ('8', 2, 1), ('9', 2, 2), ('×', 2, 3),
('4', 3, 0), ('5', 3, 1), ('6', 3, 2), ('-', 3, 3),
('1', 4, 0), ('2', 4, 1), ('3', 4, 2), ('+', 4, 3),
('0', 5, 0, 1, 2), ('.', 5, 2), ('=', 5, 3)
]
# 创建按钮
for btn_data in buttons:
text = btn_data[0]
row = btn_data[1]
col = btn_data[2]
row_span = btn_data[3] if len(btn_data) > 3 else 1
col_span = btn_data[4] if len(btn_data) > 4 else 1
button = QPushButton(text)
button.setMinimumHeight(50)
button.setStyleSheet("font-size: 16px;")
grid.addWidget(button, row, col, row_span, col_span)
self.setLayout(grid)
嵌套布局技巧
python
class NestedLayoutDemo(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle('嵌套布局示例')
self.setGeometry(300, 300, 500, 400)
# 主垂直布局
main_layout = QVBoxLayout()
# 标题
title = QLabel('用户注册表单')
title.setStyleSheet("font-size: 18px; font-weight: bold; padding: 10px;")
title.setAlignment(Qt.AlignCenter)
main_layout.addWidget(title)
# 表单区域(网格布局)
form_layout = QGridLayout()
# 基本信息
form_layout.addWidget(QLabel('姓名:'), 0, 0)
form_layout.addWidget(QLineEdit(), 0, 1)
form_layout.addWidget(QLabel('邮箱:'), 1, 0)
form_layout.addWidget(QLineEdit(), 1, 1)
form_layout.addWidget(QLabel('电话:'), 2, 0)
form_layout.addWidget(QLineEdit(), 2, 1)
# 性别选择(水平布局)
gender_layout = QHBoxLayout()
gender_layout.addWidget(QRadioButton('男'))
gender_layout.addWidget(QRadioButton('女'))
gender_layout.addStretch()
form_layout.addWidget(QLabel('性别:'), 3, 0)
form_layout.addLayout(gender_layout, 3, 1)
main_layout.addLayout(form_layout)
# 按钮区域(水平布局)
button_layout = QHBoxLayout()
button_layout.addStretch()
button_layout.addWidget(QPushButton('取消'))
button_layout.addWidget(QPushButton('注册'))
main_layout.addLayout(button_layout)
self.setLayout(main_layout)
3.2 Qt Designer可视化设计
Qt Designer是PyQt5提供的可视化界面设计工具,可以大大提高开发效率。
Qt Designer基本使用
-
启动Qt Designer:
bash# Windows designer.exe # 或通过Python启动 python -m PyQt5.uic.pyuic
-
创建新窗体:
- File → New → Widget/MainWindow/Dialog
- 选择合适的窗体类型
-
设计界面:
- 从左侧控件面板拖拽控件到窗体
- 使用右侧属性面板设置控件属性
- 使用布局管理器组织控件
.ui文件转换与使用
方法一:使用pyuic5工具转换
bash
# 将.ui文件转换为.py文件
pyuic5 -o main_window.py main_window.ui
方法二:动态加载.ui文件
python
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.uic import loadUi
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# 动态加载.ui文件
loadUi('main_window.ui', self)
# 连接信号与槽
self.pushButton.clicked.connect(self.on_button_clicked)
def on_button_clicked(self):
self.label.setText('按钮被点击了!')
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
3.3 样式表与美化
QSS样式表语法
QSS(Qt Style Sheets)类似于CSS,用于美化PyQt5应用程序:
python
class StyledWidgetDemo(QWidget):
def __init__(self):
super().__init__()
self.initUI()
self.setStyleSheet(self.get_style())
def initUI(self):
self.setWindowTitle('样式表示例')
self.setGeometry(300, 300, 400, 300)
layout = QVBoxLayout()
# 标题标签
title = QLabel('现代化界面设计')
title.setObjectName('title')
layout.addWidget(title)
# 主要按钮
primary_btn = QPushButton('主要按钮')
primary_btn.setObjectName('primary-btn')
layout.addWidget(primary_btn)
# 次要按钮
secondary_btn = QPushButton('次要按钮')
secondary_btn.setObjectName('secondary-btn')
layout.addWidget(secondary_btn)
# 危险按钮
danger_btn = QPushButton('危险按钮')
danger_btn.setObjectName('danger-btn')
layout.addWidget(danger_btn)
# 输入框
input_field = QLineEdit()
input_field.setPlaceholderText('请输入内容...')
input_field.setObjectName('input-field')
layout.addWidget(input_field)
self.setLayout(layout)
def get_style(self):
return """
QWidget {
background-color: #f0f0f0;
font-family: 'Microsoft YaHei', Arial, sans-serif;
}
#title {
font-size: 24px;
font-weight: bold;
color: #333;
padding: 20px;
text-align: center;
}
QPushButton {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
margin: 5px;
}
#primary-btn {
background-color: #007bff;
color: white;
}
#primary-btn:hover {
background-color: #0056b3;
}
#primary-btn:pressed {
background-color: #004085;
}
#secondary-btn {
background-color: #6c757d;
color: white;
}
#secondary-btn:hover {
background-color: #545b62;
}
#danger-btn {
background-color: #dc3545;
color: white;
}
#danger-btn:hover {
background-color: #c82333;
}
#input-field {
padding: 12px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 14px;
margin: 5px;
}
#input-field:focus {
border-color: #007bff;
outline: none;
}
"""
主题切换实现
python
from PyQt5.QtWidgets import QComboBox
class ThemeDemo(QWidget):
def __init__(self):
super().__init__()
self.themes = {
'默认主题': self.get_default_theme(),
'深色主题': self.get_dark_theme(),
'绿色主题': self.get_green_theme()
}
self.initUI()
def initUI(self):
self.setWindowTitle('主题切换示例')
self.setGeometry(300, 300, 400, 300)
layout = QVBoxLayout()
# 主题选择器
theme_layout = QHBoxLayout()
theme_layout.addWidget(QLabel('选择主题:'))
self.theme_combo = QComboBox()
self.theme_combo.addItems(self.themes.keys())
self.theme_combo.currentTextChanged.connect(self.change_theme)
theme_layout.addWidget(self.theme_combo)
layout.addLayout(theme_layout)
# 示例控件
layout.addWidget(QLabel('这是一个标签'))
layout.addWidget(QPushButton('这是一个按钮'))
layout.addWidget(QLineEdit('这是一个输入框'))
self.setLayout(layout)
# 应用默认主题
self.change_theme('默认主题')
def change_theme(self, theme_name):
if theme_name in self.themes:
self.setStyleSheet(self.themes[theme_name])
def get_default_theme(self):
return """
QWidget {
background-color: white;
color: black;
}
QPushButton {
background-color: #e7e7e7;
border: 1px solid #ccc;
padding: 8px;
border-radius: 4px;
}
QPushButton:hover {
background-color: #d4edda;
}
"""
def get_dark_theme(self):
return """
QWidget {
background-color: #2b2b2b;
color: white;
}
QPushButton {
background-color: #404040;
border: 1px solid #555;
padding: 8px;
border-radius: 4px;
color: white;
}
QPushButton:hover {
background-color: #505050;
}
QLineEdit {
background-color: #404040;
border: 1px solid #555;
padding: 8px;
border-radius: 4px;
color: white;
}
"""
def get_green_theme(self):
return """
QWidget {
background-color: #f0f8f0;
color: #2d5a2d;
}
QPushButton {
background-color: #28a745;
border: none;
padding: 8px;
border-radius: 4px;
color: white;
font-weight: bold;
}
QPushButton:hover {
background-color: #218838;
}
QLineEdit {
border: 2px solid #28a745;
padding: 8px;
border-radius: 4px;
background-color: white;
}
"""
四、事件处理与交互逻辑
4.1 信号与槽深入
连接方式详解
PyQt5提供了多种信号槽连接方式:
python
from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtWidgets import QWidget, QPushButton, QVBoxLayout, QLabel
class AdvancedSignalSlotDemo(QWidget):
def __init__(self):
super().__init__()
self.initUI()
self.setup_connections()
def initUI(self):
self.setWindowTitle('高级信号槽示例')
layout = QVBoxLayout()
self.button1 = QPushButton('按钮1')
self.button2 = QPushButton('按钮2')
self.label = QLabel('状态显示')
layout.addWidget(self.button1)
layout.addWidget(self.button2)
layout.addWidget(self.label)
self.setLayout(layout)
def setup_connections(self):
# 方式1: 连接到成员函数
self.button1.clicked.connect(self.on_button1_clicked)
# 方式2: 连接到lambda函数
self.button2.clicked.connect(lambda: self.label.setText('按钮2被点击'))
# 方式3: 带参数的连接
self.button1.clicked.connect(lambda: self.show_message('来自按钮1'))
# 方式4: 一个信号连接多个槽
self.button1.clicked.connect(self.update_status)
# 方式5: 信号链接(信号连接信号)
# self.button2.clicked.connect(self.button1.clicked)
def on_button1_clicked(self):
self.label.setText('按钮1被点击了')
def show_message(self, message):
print(f"消息: {message}")
def update_status(self):
import datetime
now = datetime.datetime.now().strftime("%H:%M:%S")
self.label.setText(f"最后点击时间: {now}")
自定义信号高级用法
python
from PyQt5.QtCore import QObject, pyqtSignal, QTimer
import random
class DataProcessor(QObject):
"""数据处理器,演示自定义信号"""
# 定义各种类型的信号
progress_updated = pyqtSignal(int) # 进度更新信号
data_processed = pyqtSignal(str, dict) # 数据处理完成信号
error_occurred = pyqtSignal(str) # 错误信号
def __init__(self):
super().__init__()
self.timer = QTimer()
self.timer.timeout.connect(self.process_data)
self.progress = 0
def start_processing(self):
"""开始数据处理"""
self.progress = 0
self.timer.start(100) # 每100ms更新一次
def process_data(self):
"""模拟数据处理"""
self.progress += random.randint(1, 10)
self.progress_updated.emit(self.progress)
if self.progress >= 100:
self.timer.stop()
# 发送处理完成信号
result_data = {
'records_processed': 1000,
'time_taken': '5.2s',
'success_rate': '98.5%'
}
self.data_processed.emit("处理完成", result_data)
# 随机模拟错误
if random.random() < 0.05: # 5%概率出错
self.error_occurred.emit("随机处理错误")
class CustomSignalDemo(QWidget):
def __init__(self):
super().__init__()
self.processor = DataProcessor()
self.initUI()
self.setup_connections()
def initUI(self):
self.setWindowTitle('自定义信号高级示例')
layout = QVBoxLayout()
self.start_button = QPushButton('开始处理数据')
self.progress_label = QLabel('进度: 0%')
self.status_label = QLabel('状态: 待机')
self.result_label = QLabel('结果: 无')
layout.addWidget(self.start_button)
layout.addWidget(self.progress_label)
layout.addWidget(self.status_label)
layout.addWidget(self.result_label)
self.setLayout(layout)
def setup_connections(self):
# 连接按钮信号
self.start_button.clicked.connect(self.start_processing)
# 连接自定义信号
self.processor.progress_updated.connect(self.update_progress)
self.processor.data_processed.connect(self.on_data_processed)
self.processor.error_occurred.connect(self.on_error)
def start_processing(self):
self.start_button.setEnabled(False)
self.status_label.setText('状态: 处理中...')
self.processor.start_processing()
def update_progress(self, progress):
self.progress_label.setText(f'进度: {progress}%')
def on_data_processed(self, message, data):
self.status_label.setText(f'状态: {message}')
result_text = f"结果: 处理了{data['records_processed']}条记录," \
f"耗时{data['time_taken']},成功率{data['success_rate']}"
self.result_label.setText(result_text)
self.start_button.setEnabled(True)
def on_error(self, error_message):
self.status_label.setText(f'错误: {error_message}')
4.2 事件处理机制
鼠标事件处理
python
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter, QPen
from PyQt5.QtWidgets import QWidget
class MouseEventDemo(QWidget):
def __init__(self):
super().__init__()
self.initUI()
self.drawing = False
self.brush_size = 3
self.brush_color = Qt.black
self.last_point = None
self.points = []
def initUI(self):
self.setWindowTitle('鼠标事件示例 - 简单画板')
self.setGeometry(300, 300, 600, 400)
self.setMouseTracking(True) # 启用鼠标跟踪
def mousePressEvent(self, event):
"""鼠标按下事件"""
if event.button() == Qt.LeftButton:
self.drawing = True
self.last_point = event.pos()
self.points = [event.pos()]
def mouseMoveEvent(self, event):
"""鼠标移动事件"""
if event.buttons() & Qt.LeftButton and self.drawing:
self.points.append(event.pos())
self.update() # 触发重绘
def mouseReleaseEvent(self, event):
"""鼠标释放事件"""
if event.button() == Qt.LeftButton:
self.drawing = False
def mouseDoubleClickEvent(self, event):
"""鼠标双击事件"""
if event.button() == Qt.LeftButton:
self.points.clear()
self.update()
def wheelEvent(self, event):
"""鼠标滚轮事件"""
# 改变画笔大小
delta = event.angleDelta().y()
if delta > 0:
self.brush_size = min(20, self.brush_size + 1)
else:
self.brush_size = max(1, self.brush_size - 1)
self.setWindowTitle(f'画板 - 画笔大小: {self.brush_size}')
def paintEvent(self, event):
"""绘制事件"""
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
pen = QPen(self.brush_color, self.brush_size,
Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
painter.setPen(pen)
# 绘制线条
for i in range(1, len(self.points)):
painter.drawLine(self.points[i-1], self.points[i])
键盘事件处理
python
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QTextEdit, QVBoxLayout, QLabel
class KeyboardEventDemo(QWidget):
def __init__(self):
super().__init__()
self.initUI()
self.key_combinations = []
def initUI(self):
self.setWindowTitle('键盘事件示例')
self.setGeometry(300, 300, 500, 400)
layout = QVBoxLayout()
self.info_label = QLabel('按下任意键或组合键试试...')
layout.addWidget(self.info_label)
self.text_edit = QTextEdit()
self.text_edit.setPlaceholderText('在这里输入文本,支持快捷键操作')
layout.addWidget(self.text_edit)
self.status_label = QLabel('状态: 就绪')
layout.addWidget(self.status_label)
self.setLayout(layout)
# 设置焦点策略
self.setFocusPolicy(Qt.StrongFocus)
def keyPressEvent(self, event):
"""键盘按下事件"""
key = event.key()
modifiers = event.modifiers()
# 检测修饰键
ctrl_pressed = modifiers & Qt.ControlModifier
shift_pressed = modifiers & Qt.ShiftModifier
alt_pressed = modifiers & Qt.AltModifier
# 构建按键信息
key_info = []
if ctrl_pressed:
key_info.append('Ctrl')
if shift_pressed:
key_info.append('Shift')
if alt_pressed:
key_info.append('Alt')
# 获取按键名称
key_name = self.get_key_name(key)
if key_name:
key_info.append(key_name)
key_combination = '+'.join(key_info)
self.info_label.setText(f'按键: {key_combination}')
# 处理特殊快捷键
if ctrl_pressed:
if key == Qt.Key_S:
self.save_text()
event.accept()
return
elif key == Qt.Key_O:
self.open_text()
event.accept()
return
elif key == Qt.Key_N:
self.new_text()
event.accept()
return
# 处理功能键
if key == Qt.Key_F1:
self.show_help()
event.accept()
return
elif key == Qt.Key_Escape:
self.text_edit.clear()
event.accept()
return
# 传递事件给父类处理
super().keyPressEvent(event)
def get_key_name(self, key):
"""获取按键名称"""
key_map = {
Qt.Key_A: 'A', Qt.Key_B: 'B', Qt.Key_C: 'C', Qt.Key_D: 'D',
Qt.Key_E: 'E', Qt.Key_F: 'F', Qt.Key_G: 'G', Qt.Key_H: 'H',
Qt.Key_I: 'I', Qt.Key_J: 'J', Qt.Key_K: 'K', Qt.Key_L: 'L',
Qt.Key_M: 'M', Qt.Key_N: 'N', Qt.Key_O: 'O', Qt.Key_P: 'P',
Qt.Key_Q: 'Q', Qt.Key_R: 'R', Qt.Key_S: 'S', Qt.Key_T: 'T',
Qt.Key_U: 'U', Qt.Key_V: 'V', Qt.Key_W: 'W', Qt.Key_X: 'X',
Qt.Key_Y: 'Y', Qt.Key_Z: 'Z',
Qt.Key_Return: 'Enter', Qt.Key_Space: 'Space',
Qt.Key_Backspace: 'Backspace', Qt.Key_Delete: 'Delete',
Qt.Key_F1: 'F1', Qt.Key_F2: 'F2', Qt.Key_F3: 'F3',
Qt.Key_Escape: 'Esc', Qt.Key_Tab: 'Tab'
}
return key_map.get(key, f'Key_{key}')
def save_text(self):
self.status_label.setText('状态: 保存文本 (Ctrl+S)')
def open_text(self):
self.status_label.setText('状态: 打开文本 (Ctrl+O)')
def new_text(self):
self.text_edit.clear()
self.status_label.setText('状态: 新建文本 (Ctrl+N)')
def show_help(self):
help_text = """
快捷键帮助:
Ctrl+S - 保存
Ctrl+O - 打开
Ctrl+N - 新建
F1 - 显示帮助
Esc - 清空文本
"""
self.text_edit.setPlainText(help_text)
self.status_label.setText('状态: 显示帮助 (F1)')
4.3 用户交互实现
菜单栏与工具栏
python
from PyQt5.QtWidgets import (QMainWindow, QMenuBar, QToolBar, QAction,
QTextEdit, QFileDialog, QMessageBox, QStatusBar)
from PyQt5.QtGui import QIcon, QKeySequence
from PyQt5.QtCore import Qt
class MenuToolbarDemo(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
self.create_actions()
self.create_menus()
self.create_toolbars()
self.create_statusbar()
def initUI(self):
self.setWindowTitle('菜单栏与工具栏示例')
self.setGeometry(100, 100, 800, 600)
# 创建中央控件
self.text_edit = QTextEdit()
self.setCentralWidget(self.text_edit)
# 文件状态
self.current_file = None
self.is_modified = False
# 监听文本变化
self.text_edit.textChanged.connect(self.text_changed)
def create_actions(self):
"""创建动作"""
# 文件操作
self.new_action = QAction('新建(&N)', self)
self.new_action.setShortcut(QKeySequence.New)
self.new_action.setStatusTip('创建新文档')
self.new_action.triggered.connect(self.new_file)
self.open_action = QAction('打开(&O)', self)
self.open_action.setShortcut(QKeySequence.Open)
self.open_action.setStatusTip('打开文档')
self.open_action.triggered.connect(self.open_file)
self.save_action = QAction('保存(&S)', self)
self.save_action.setShortcut(QKeySequence.Save)
self.save_action.setStatusTip('保存文档')
self.save_action.triggered.connect(self.save_file)
self.save_as_action = QAction('另存为(&A)', self)
self.save_as_action.setShortcut(QKeySequence.SaveAs)
self.save_as_action.setStatusTip('另存为文档')
self.save_as_action.triggered.connect(self.save_as_file)
self.exit_action = QAction('退出(&X)', self)
self.exit_action.setShortcut(QKeySequence.Quit)
self.exit_action.setStatusTip('退出应用程序')
self.exit_action.triggered.connect(self.close)
# 编辑操作
self.undo_action = QAction('撤销(&U)', self)
self.undo_action.setShortcut(QKeySequence.Undo)
self.undo_action.setStatusTip('撤销上一步操作')
self.undo_action.triggered.connect(self.text_edit.undo)
self.redo_action = QAction('重做(&R)', self)
self.redo_action.setShortcut(QKeySequence.Redo)
self.redo_action.setStatusTip('重做操作')
self.redo_action.triggered.connect(self.text_edit.redo)
self.cut_action = QAction('剪切(&T)', self)
self.cut_action.setShortcut(QKeySequence.Cut)
self.cut_action.setStatusTip('剪切选中文本')
self.cut_action.triggered.connect(self.text_edit.cut)
self.copy_action = QAction('复制(&C)', self)
self.copy_action.setShortcut(QKeySequence.Copy)
self.copy_action.setStatusTip('复制选中文本')
self.copy_action.triggered.connect(self.text_edit.copy)
self.paste_action = QAction('粘贴(&P)', self)
self.paste_action.setShortcut(QKeySequence.Paste)
self.paste_action.setStatusTip('粘贴文本')
self.paste_action.triggered.connect(self.text_edit.paste)
# 帮助操作
self.about_action = QAction('关于(&A)', self)
self.about_action.setStatusTip('关于此应用程序')
self.about_action.triggered.connect(self.about)
def create_menus(self):
"""创建菜单栏"""
menubar = self.menuBar()
# 文件菜单
file_menu = menubar.addMenu('文件(&F)')
file_menu.addAction(self.new_action)
file_menu.addAction(self.open_action)
file_menu.addSeparator()
file_menu.addAction(self.save_action)
file_menu.addAction(self.save_as_action)
file_menu.addSeparator()
file_menu.addAction(self.exit_action)
# 编辑菜单
edit_menu = menubar.addMenu('编辑(&E)')
edit_menu.addAction(self.undo_action)
edit_menu.addAction(self.redo_action)
edit_menu.addSeparator()
edit_menu.addAction(self.cut_action)
edit_menu.addAction(self.copy_action)
edit_menu.addAction(self.paste_action)
# 帮助菜单
help_menu = menubar.addMenu('帮助(&H)')
help_menu.addAction(self.about_action)
def create_toolbars(self):
"""创建工具栏"""
# 主工具栏
main_toolbar = self.addToolBar('主工具栏')
main_toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
main_toolbar.addAction(self.new_action)
main_toolbar.addAction(self.open_action)
main_toolbar.addAction(self.save_action)
main_toolbar.addSeparator()
main_toolbar.addAction(self.cut_action)
main_toolbar.addAction(self.copy_action)
main_toolbar.addAction(self.paste_action)
# 编辑工具栏
edit_toolbar = self.addToolBar('编辑工具栏')
edit_toolbar.addAction(self.undo_action)
edit_toolbar.addAction(self.redo_action)
def create_statusbar(self):
"""创建状态栏"""
self.status_bar = self.statusBar()
self.status_bar.showMessage('就绪')
# 添加永久组件
self.char_count_label = QLabel('字符数: 0')
self.status_bar.addPermanentWidget(self.char_count_label)
def new_file(self):
"""新建文件"""
if self.check_save():
self.text_edit.clear()
self.current_file = None
self.is_modified = False
self.update_title()
self.status_bar.showMessage('新建文档', 2000)
def open_file(self):
"""打开文件"""
if self.check_save():
file_path, _ = QFileDialog.getOpenFileName(
self, '打开文件', '', 'Text Files (*.txt);;All Files (*)')
if file_path:
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
self.text_edit.setPlainText(content)
self.current_file = file_path
self.is_modified = False
self.update_title()
self.status_bar.showMessage(f'打开文件: {file_path}', 2000)
except Exception as e:
QMessageBox.warning(self, '错误', f'无法打开文件:\n{str(e)}')
def save_file(self):
"""保存文件"""
if self.current_file:
self.save_to_file(self.current_file)
else:
self.save_as_file()
def save_as_file(self):
"""另存为文件"""
file_path, _ = QFileDialog.getSaveFileName(
self, '保存文件', '', 'Text Files (*.txt);;All Files (*)')
if file_path:
self.save_to_file(file_path)
def save_to_file(self, file_path):
"""保存到指定文件"""
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(self.text_edit.toPlainText())
self.current_file = file_path
self.is_modified = False
self.update_title()
self.status_bar.showMessage(f'保存文件: {file_path}', 2000)
except Exception as e:
QMessageBox.warning(self, '错误', f'无法保存文件:\n{str(e)}')
def check_save(self):
"""检查是否需要保存"""
if self.is_modified:
reply = QMessageBox.question(
self, '保存确认',
'文档已修改,是否保存?',
QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
if reply == QMessageBox.Yes:
self.save_file()
return not self.is_modified # 如果保存失败,返回False
elif reply == QMessageBox.Cancel:
return False
return True
def text_changed(self):
"""文本变化处理"""
self.is_modified = True
self.update_title()
# 更新字符计数
char_count = len(self.text_edit.toPlainText())
self.char_count_label.setText(f'字符数: {char_count}')
def update_title(self):
"""更新窗口标题"""
title = '文本编辑器'
if self.current_file:
title += f' - {self.current_file}'
if self.is_modified:
title += ' *'
self.setWindowTitle(title)
def about(self):
"""关于对话框"""
QMessageBox.about(self, '关于',
'这是一个使用PyQt5开发的简单文本编辑器\n'
'演示了菜单栏、工具栏和状态栏的使用')
def closeEvent(self, event):
"""窗口关闭事件"""
if self.check_save():
event.accept()
else:
event.ignore()
五、高级控件与复杂界面
5.1 表格与树形控件
QTableWidget表格操作
python
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout,
QTableWidget, QTableWidgetItem, QPushButton,
QHeaderView, QAbstractItemView, QMessageBox,
QInputDialog, QLineEdit)
from PyQt5.QtCore import Qt
import random
class TableDemo(QWidget):
def __init__(self):
super().__init__()
self.initUI()
self.setup_table()
self.load_sample_data()
def initUI(self):
self.setWindowTitle('表格控件示例')
self.setGeometry(200, 200, 800, 600)
layout = QVBoxLayout()
# 按钮区域
button_layout = QHBoxLayout()
self.add_btn = QPushButton('添加行')
self.add_btn.clicked.connect(self.add_row)
button_layout.addWidget(self.add_btn)
self.delete_btn = QPushButton('删除行')
self.delete_btn.clicked.connect(self.delete_row)
button_layout.addWidget(self.delete_btn)
self.edit_btn = QPushButton('编辑')
self.edit_btn.clicked.connect(self.edit_item)
button_layout.addWidget(self.edit_btn)
self.search_btn = QPushButton('搜索')
self.search_btn.clicked.connect(self.search_items)
button_layout.addWidget(self.search_btn)
button_layout.addStretch()
layout.addLayout(button_layout)
# 表格
self.table = QTableWidget()
layout.addWidget(self.table)
self.setLayout(layout)
def setup_table(self):
"""设置表格"""
# 设置列数和列标题
columns = ['ID', '姓名', '年龄', '部门', '薪资', '入职日期']
self.table.setColumnCount(len(columns))
self.table.setHorizontalHeaderLabels(columns)
# 设置表格属性
self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
self.table.setAlternatingRowColors(True)
# 设置列宽
header = self.table.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeToContents) # ID列
header.setSectionResizeMode(1, QHeaderView.Stretch) # 姓名列
header.setSectionResizeMode(2, QHeaderView.ResizeToContents) # 年龄列
header.setSectionResizeMode(3, QHeaderView.Stretch) # 部门列
header.setSectionResizeMode(4, QHeaderView.ResizeToContents) # 薪资列
header.setSectionResizeMode(5, QHeaderView.ResizeToContents) # 入职日期列
# 连接信号
self.table.cellDoubleClicked.connect(self.on_cell_double_clicked)
self.table.itemSelectionChanged.connect(self.on_selection_changed)
def load_sample_data(self):
"""加载示例数据"""
sample_data = [
[1, '张三', 28, '技术部', 15000, '2020-01-15'],
[2, '李四', 32, '销售部', 12000, '2019-05-20'],
[3, '王五', 26, '技术部', 13000, '2021-03-10'],
[4, '赵六', 35, '人事部', 11000, '2018-08-05'],
[5, '钱七', 29, '财务部', 14000, '2020-11-12'],
[6, '孙八', 31, '技术部', 16000, '2019-02-28'],
[7, '周九', 27, '销售部', 11500, '2021-06-15'],
[8, '吴十', 33, '技术部', 17000, '2018-12-01']
]
self.table.setRowCount(len(sample_data))
for row, data in enumerate(sample_data):
for col, value in enumerate(data):
item = QTableWidgetItem(str(value))
# 设置ID列不可编辑
if col == 0:
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
# 设置数值列对齐方式
if col in [0, 2, 4]: # ID、年龄、薪资列
item.setTextAlignment(Qt.AlignCenter)
self.table.setItem(row, col, item)
def add_row(self):
"""添加行"""
dialog_data = self.get_employee_data()
if dialog_data:
row_count = self.table.rowCount()
self.table.insertRow(row_count)
# 生成新ID
new_id = self.get_next_id()
# 设置数据
data = [new_id] + dialog_data
for col, value in enumerate(data):
item = QTableWidgetItem(str(value))
if col == 0: # ID列不可编辑
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
if col in [0, 2, 4]: # 数值列居中
item.setTextAlignment(Qt.AlignCenter)
self.table.setItem(row_count, col, item)
def delete_row(self):
"""删除选中行"""
current_row = self.table.currentRow()
if current_row >= 0:
reply = QMessageBox.question(
self, '确认删除',
f'确定要删除第{current_row + 1}行数据吗?',
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
self.table.removeRow(current_row)
def edit_item(self):
"""编辑选中项"""
current_row = self.table.currentRow()
if current_row >= 0:
# 获取当前行数据
current_data = []
for col in range(1, self.table.columnCount()): # 跳过ID列
item = self.table.item(current_row, col)
current_data.append(item.text() if item else '')
# 显示编辑对话框
dialog_data = self.get_employee_data(current_data)
if dialog_data:
# 更新数据
for col, value in enumerate(dialog_data, 1):
item = QTableWidgetItem(str(value))
if col in [2, 4]: # 年龄、薪资列居中
item.setTextAlignment(Qt.AlignCenter)
self.table.setItem(current_row, col, item)
def search_items(self):
"""搜索功能"""
search_text, ok = QInputDialog.getText(self, '搜索', '请输入搜索关键词:')
if ok and search_text:
# 清除之前的选择
self.table.clearSelection()
# 搜索匹配项
found_items = self.table.findItems(search_text, Qt.MatchContains)
if found_items:
# 选中找到的项
for item in found_items:
item.setSelected(True)
# 滚动到第一个匹配项
self.table.scrollToItem(found_items[0])
QMessageBox.information(self, '搜索结果',
f'找到 {len(found_items)} 个匹配项')
else:
QMessageBox.information(self, '搜索结果', '未找到匹配项')
def get_employee_data(self, current_data=None):
"""获取员工数据对话框"""
from PyQt5.QtWidgets import QDialog, QFormLayout, QDialogButtonBox
dialog = QDialog(self)
dialog.setWindowTitle('员工信息')
dialog.setModal(True)
layout = QFormLayout()
# 创建输入控件
name_edit = QLineEdit()
age_edit = QLineEdit()
dept_edit = QLineEdit()
salary_edit = QLineEdit()
date_edit = QLineEdit()
# 如果有当前数据,填充到输入框
if current_data:
name_edit.setText(current_data[0])
age_edit.setText(current_data[1])
dept_edit.setText(current_data[2])
salary_edit.setText(current_data[3])
date_edit.setText(current_data[4])
# 添加到布局
layout.addRow('姓名:', name_edit)
layout.addRow('年龄:', age_edit)
layout.addRow('部门:', dept_edit)
layout.addRow('薪资:', salary_edit)
layout.addRow('入职日期:', date_edit)
# 按钮
buttons = QDialogButtonBox(
QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
buttons.accepted.connect(dialog.accept)
buttons.rejected.connect(dialog.reject)
layout.addRow(buttons)
dialog.setLayout(layout)
if dialog.exec_() == QDialog.Accepted:
return [
name_edit.text(),
age_edit.text(),
dept_edit.text(),
salary_edit.text(),
date_edit.text()
]
return None
def get_next_id(self):
"""获取下一个ID"""
max_id = 0
for row in range(self.table.rowCount()):
item = self.table.item(row, 0)
if item:
try:
id_value = int(item.text())
max_id = max(max_id, id_value)
except ValueError:
pass
return max_id + 1
def on_cell_double_clicked(self, row, column):
"""单元格双击事件"""
if column != 0: # 非ID列才能编辑
self.edit_item()
def on_selection_changed(self):
"""选择变化事件"""
has_selection = len(self.table.selectedItems()) > 0
self.delete_btn.setEnabled(has_selection)
self.edit_btn.setEnabled(has_selection)
QTreeWidget树形结构
python
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout,
QTreeWidget, QTreeWidgetItem, QPushButton,
QInputDialog, QMessageBox, QMenu, QAction)
from PyQt5.QtCore import Qt
class TreeDemo(QWidget):
def __init__(self):
super().__init__()
self.initUI()
self.setup_tree()
self.load_sample_data()
def initUI(self):
self.setWindowTitle('树形控件示例 - 组织结构')
self.setGeometry(200, 200, 600, 500)
layout = QVBoxLayout()
# 按钮区域
button_layout = QHBoxLayout()
self.add_root_btn = QPushButton('添加根节点')
self.add_root_btn.clicked.connect(self.add_root_node)
button_layout.addWidget(self.add_root_btn)
self.add_child_btn = QPushButton('添加子节点')
self.add_child_btn.clicked.connect(self.add_child_node)
button_layout.addWidget(self.add_child_btn)
self.delete_btn = QPushButton('删除节点')
self.delete_btn.clicked.connect(self.delete_node)
button_layout.addWidget(self.delete_btn)
self.expand_all_btn = QPushButton('展开所有')
self.expand_all_btn.clicked.connect(self.tree.expandAll)
button_layout.addWidget(self.expand_all_btn)
self.collapse_all_btn = QPushButton('收起所有')
self.collapse_all_btn.clicked.connect(self.tree.collapseAll)
button_layout.addWidget(self.collapse_all_btn)
button_layout.addStretch()
layout.addLayout(button_layout)
# 树形控件
self.tree = QTreeWidget()
layout.addWidget(self.tree)
self.setLayout(layout)
def setup_tree(self):
"""设置树形控件"""
# 设置列标题
self.tree.setHeaderLabels(['部门/员工', '职位', '人数/工号'])
# 设置列宽
self.tree.setColumnWidth(0, 200)
self.tree.setColumnWidth(1, 150)
self.tree.setColumnWidth(2, 100)
# 连接信号
self.tree.itemClicked.connect(self.on_item_clicked)
self.tree.itemDoubleClicked.connect(self.on_item_double_clicked)
self.tree.itemSelectionChanged.connect(self.on_selection_changed)
# 设置右键菜单
self.tree.setContextMenuPolicy(Qt.CustomContextMenu)
self.tree.customContextMenuRequested.connect(self.show_context_menu)
def load_sample_data(self):
"""加载示例数据"""
# 创建公司根节点
company = QTreeWidgetItem(self.tree)
company.setText(0, 'ABC科技公司')
company.setText(1, '公司')
company.setText(2, '156人')
company.setExpanded(True)
# 技术部
tech_dept = QTreeWidgetItem(company)
tech_dept.setText(0, '技术部')
tech_dept.setText(1, '部门')
tech_dept.setText(2, '45人')
tech_dept.setExpanded(True)
# 技术部员工
tech_employees = [
('张三', '技术总监', 'T001'),
('李四', '高级工程师', 'T002'),
('王五', '前端工程师', 'T003'),
('赵六', '后端工程师', 'T004'),
('钱七', '测试工程师', 'T005')
]
for name, position, emp_id in tech_employees:
employee = QTreeWidgetItem(tech_dept)
employee.setText(0, name)
employee.setText(1, position)
employee.setText(2, emp_id)
# 销售部
sales_dept = QTreeWidgetItem(company)
sales_dept.setText(0, '销售部')
sales_dept.setText(1, '部门')
sales_dept.setText(2, '32人')
sales_employees = [
('孙八', '销售经理', 'S001'),
('周九', '销售代表', 'S002'),
('吴十', '客户经理', 'S003')
]
for name, position, emp_id in sales_employees:
employee = QTreeWidgetItem(sales_dept)
employee.setText(0, name)
employee.setText(1, position)
employee.setText(2, emp_id)
# 人事部
hr_dept = QTreeWidgetItem(company)
hr_dept.setText(0, '人事部')
hr_dept.setText(1, '部门')
hr_dept.setText(2, '8人')
hr_employees = [
('郑十一', '人事经理', 'H001'),
('王十二', '招聘专员', 'H002')
]
for name, position, emp_id in hr_employees:
employee = QTreeWidgetItem(hr_dept)
employee.setText(0, name)
employee.setText(1, position)
employee.setText(2, emp_id)
def add_root_node(self):
"""添加根节点"""
name, ok = QInputDialog.getText(self, '添加根节点', '请输入节点名称:')
if ok and name:
root_item = QTreeWidgetItem(self.tree)
root_item.setText(0, name)
root_item.setText(1, '根节点')
root_item.setText(2, '0')
self.tree.setCurrentItem(root_item)
def add_child_node(self):
"""添加子节点"""
current_item = self.tree.currentItem()
if current_item is None:
QMessageBox.warning(self, '警告', '请先选择一个父节点')
return
name, ok = QInputDialog.getText(self, '添加子节点', '请输入节点名称:')
if ok and name:
child_item = QTreeWidgetItem(current_item)
child_item.setText(0, name)
child_item.setText(1, '子节点')
child_item.setText(2, '1')
# 展开父节点
current_item.setExpanded(True)
self.tree.setCurrentItem(child_item)
def delete_node(self):
"""删除节点"""
current_item = self.tree.currentItem()
if current_item is None:
QMessageBox.warning(self, '警告', '请先选择要删除的节点')
return
reply = QMessageBox.question(
self, '确认删除',
f'确定要删除节点 "{current_item.text(0)}" 及其所有子节点吗?',
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
parent = current_item.parent()
if parent:
parent.removeChild(current_item)
else:
index = self.tree.indexOfTopLevelItem(current_item)
self.tree.takeTopLevelItem(index)
def show_context_menu(self, position):
"""显示右键菜单"""
item = self.tree.itemAt(position)
if item is None:
return
menu = QMenu()
add_child_action = QAction('添加子节点', self)
add_child_action.triggered.connect(self.add_child_node)
menu.addAction(add_child_action)
if item.parent() is not None: # 不是根节点
delete_action = QAction('删除节点', self)
delete_action.triggered.connect(self.delete_node)
menu.addAction(delete_action)
menu.addSeparator()
expand_action = QAction('展开节点', self)
expand_action.triggered.connect(lambda: item.setExpanded(True))
menu.addAction(expand_action)
collapse_action = QAction('收起节点', self)
collapse_action.triggered.connect(lambda: item.setExpanded(False))
menu.addAction(collapse_action)
menu.exec_(self.tree.mapToGlobal(position))
def on_item_clicked(self, item, column):
"""项目点击事件"""
print(f"点击了: {item.text(0)}")
def on_item_double_clicked(self, item, column):
"""项目双击事件"""
if column == 0: # 只允许编辑第一列
# 启用编辑模式
self.tree.editItem(item, column)
def on_selection_changed(self):
"""选择变化事件"""
current_item = self.tree.currentItem()
has_selection = current_item is not None
self.add_child_btn.setEnabled(has_selection)
self.delete_btn.setEnabled(has_selection and current_item.parent() is not None)
六、文件处理与数据管理
6.1 文件操作
python
import os
import json
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QTextEdit, QFileDialog, QMessageBox, QLabel,
QListWidget, QSplitter)
from PyQt5.QtCore import Qt, QSettings
class FileManagerDemo(QWidget):
def __init__(self):
super().__init__()
self.current_file = None
self.recent_files = []
self.settings = QSettings('FileManager', 'Demo')
self.initUI()
self.load_recent_files()
def initUI(self):
self.setWindowTitle('文件管理器')
self.setGeometry(200, 200, 800, 600)
# 创建分割器
splitter = QSplitter(Qt.Horizontal)
# 左侧:最近文件列表
left_widget = QWidget()
left_layout = QVBoxLayout()
left_layout.addWidget(QLabel('最近文件:'))
self.recent_list = QListWidget()
self.recent_list.itemDoubleClicked.connect(self.open_recent_file)
left_layout.addWidget(self.recent_list)
clear_recent_btn = QPushButton('清空最近文件')
clear_recent_btn.clicked.connect(self.clear_recent_files)
left_layout.addWidget(clear_recent_btn)
left_widget.setLayout(left_layout)
left_widget.setMaximumWidth(250)
# 右侧:主编辑区域
right_widget = QWidget()
right_layout = QVBoxLayout()
# 工具栏
toolbar_layout = QHBoxLayout()
new_btn = QPushButton('新建')
new_btn.clicked.connect(self.new_file)
toolbar_layout.addWidget(new_btn)
open_btn = QPushButton('打开')
open_btn.clicked.connect(self.open_file)
toolbar_layout.addWidget(open_btn)
save_btn = QPushButton('保存')
save_btn.clicked.connect(self.save_file)
toolbar_layout.addWidget(save_btn)
save_as_btn = QPushButton('另存为')
save_as_btn.clicked.connect(self.save_as_file)
toolbar_layout.addWidget(save_as_btn)
toolbar_layout.addStretch()
# 文件信息
self.file_info_label = QLabel('无文件打开')
toolbar_layout.addWidget(self.file_info_label)
right_layout.addLayout(toolbar_layout)
# 文本编辑器
self.text_edit = QTextEdit()
self.text_edit.textChanged.connect(self.on_text_changed)
right_layout.addWidget(self.text_edit)
right_widget.setLayout(right_layout)
# 添加到分割器
splitter.addWidget(left_widget)
splitter.addWidget(right_widget)
splitter.setSizes([200, 600])
# 主布局
main_layout = QVBoxLayout()
main_layout.addWidget(splitter)
self.setLayout(main_layout)
def new_file(self):
"""新建文件"""
if self.check_save_changes():
self.text_edit.clear()
self.current_file = None
self.update_file_info()
def open_file(self):
"""打开文件"""
if self.check_save_changes():
file_path, _ = QFileDialog.getOpenFileName(
self, '打开文件', '',
'Text Files (*.txt);;Python Files (*.py);;All Files (*)')
if file_path:
self.load_file(file_path)
def load_file(self, file_path):
"""加载文件"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
self.text_edit.setPlainText(content)
self.current_file = file_path
self.add_to_recent_files(file_path)
self.update_file_info()
except Exception as e:
QMessageBox.warning(self, '错误', f'无法打开文件:\n{str(e)}')
def save_file(self):
"""保存文件"""
if self.current_file:
self.save_to_file(self.current_file)
else:
self.save_as_file()
def save_as_file(self):
"""另存为文件"""
file_path, _ = QFileDialog.getSaveFileName(
self, '保存文件', '',
'Text Files (*.txt);;Python Files (*.py);;All Files (*)')
if file_path:
self.save_to_file(file_path)
def save_to_file(self, file_path):
"""保存到指定文件"""
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(self.text_edit.toPlainText())
self.current_file = file_path
self.add_to_recent_files(file_path)
self.update_file_info()
QMessageBox.information(self, '成功', '文件保存成功!')
except Exception as e:
QMessageBox.warning(self, '错误', f'无法保存文件:\n{str(e)}')
def add_to_recent_files(self, file_path):
"""添加到最近文件列表"""
if file_path in self.recent_files:
self.recent_files.remove(file_path)
self.recent_files.insert(0, file_path)
# 最多保存10个最近文件
if len(self.recent_files) > 10:
self.recent_files = self.recent_files[:10]
self.update_recent_list()
self.save_recent_files()
def update_recent_list(self):
"""更新最近文件列表显示"""
self.recent_list.clear()
for file_path in self.recent_files:
if os.path.exists(file_path):
self.recent_list.addItem(os.path.basename(file_path))
def open_recent_file(self, item):
"""打开最近文件"""
index = self.recent_list.row(item)
if 0 <= index < len(self.recent_files):
file_path = self.recent_files[index]
if os.path.exists(file_path):
if self.check_save_changes():
self.load_file(file_path)
else:
QMessageBox.warning(self, '错误', '文件不存在')
self.recent_files.remove(file_path)
self.update_recent_list()
self.save_recent_files()
def clear_recent_files(self):
"""清空最近文件"""
self.recent_files.clear()
self.update_recent_list()
self.save_recent_files()
def load_recent_files(self):
"""从设置中加载最近文件"""
self.recent_files = self.settings.value('recent_files', [])
if not isinstance(self.recent_files, list):
self.recent_files = []
self.update_recent_list()
def save_recent_files(self):
"""保存最近文件到设置"""
self.settings.setValue('recent_files', self.recent_files)
def update_file_info(self):
"""更新文件信息显示"""
if self.current_file:
file_name = os.path.basename(self.current_file)
file_size = os.path.getsize(self.current_file)
self.file_info_label.setText(f'{file_name} ({file_size} bytes)')
else:
self.file_info_label.setText('无文件打开')
def on_text_changed(self):
"""文本变化事件"""
# 可以在这里添加自动保存等功能
pass
def check_save_changes(self):
"""检查是否需要保存更改"""
# 简化版本,实际应用中应该检查文本是否已修改
if self.text_edit.toPlainText().strip():
reply = QMessageBox.question(
self, '保存确认',
'当前文档可能有未保存的更改,是否继续?',
QMessageBox.Yes | QMessageBox.No)
return reply == QMessageBox.Yes
return True
def closeEvent(self, event):
"""窗口关闭事件"""
if self.check_save_changes():
self.save_recent_files()
event.accept()
else:
event.ignore()
6.2 配置文件管理
python
import json
import configparser
from PyQt5.QtCore import QSettings
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QFormLayout,
QLineEdit, QSpinBox, QCheckBox, QComboBox,
QPushButton, QTabWidget, QGroupBox, QMessageBox)
class ConfigManagerDemo(QWidget):
def __init__(self):
super().__init__()
self.config_methods = {
'QSettings': self.load_qsettings,
'JSON': self.load_json_config,
'INI': self.load_ini_config
}
self.initUI()
self.load_config()
def initUI(self):
self.setWindowTitle('配置管理示例')
self.setGeometry(300, 300, 500, 600)
layout = QVBoxLayout()
# 配置方式选择
method_layout = QHBoxLayout()
method_layout.addWidget(QLabel('配置方式:'))
self.method_combo = QComboBox()
self.method_combo.addItems(list(self.config_methods.keys()))
self.method_combo.currentTextChanged.connect(self.on_method_changed)
method_layout.addWidget(self.method_combo)
method_layout.addStretch()
layout.addLayout(method_layout)
# 创建选项卡
self.tab_widget = QTabWidget()
# 通用设置选项卡
self.create_general_tab()
# 界面设置选项卡
self.create_ui_tab()
# 高级设置选项卡
self.create_advanced_tab()
layout.addWidget(self.tab_widget)
# 按钮区域
button_layout = QHBoxLayout()
load_btn = QPushButton('加载配置')
load_btn.clicked.connect(self.load_config)
button_layout.addWidget(load_btn)
save_btn = QPushButton('保存配置')
save_btn.clicked.connect(self.save_config)
button_layout.addWidget(save_btn)
reset_btn = QPushButton('重置默认')
reset_btn.clicked.connect(self.reset_to_defaults)
button_layout.addWidget(reset_btn)
button_layout.addStretch()
layout.addLayout(button_layout)
self.setLayout(layout)
def create_general_tab(self):
"""创建通用设置选项卡"""
general_widget = QWidget()
layout = QVBoxLayout()
# 基本信息组
basic_group = QGroupBox('基本信息')
basic_layout = QFormLayout()
self.username_edit = QLineEdit()
basic_layout.addRow('用户名:', self.username_edit)
self.email_edit = QLineEdit()
basic_layout.addRow('邮箱:', self.email_edit)
self.auto_save_check = QCheckBox('自动保存')
basic_layout.addRow('', self.auto_save_check)
basic_group.setLayout(basic_layout)
layout.addWidget(basic_group)
# 文件设置组
file_group = QGroupBox('文件设置')
file_layout = QFormLayout()
self.max_recent_spin = QSpinBox()
self.max_recent_spin.setRange(1, 20)
file_layout.addRow('最近文件数量:', self.max_recent_spin)
self.default_encoding_combo = QComboBox()
self.default_encoding_combo.addItems(['UTF-8', 'GBK', 'ASCII'])
file_layout.addRow('默认编码:', self.default_encoding_combo)
file_group.setLayout(file_layout)
layout.addWidget(file_group)
layout.addStretch()
general_widget.setLayout(layout)
self.tab_widget.addTab(general_widget, '通用')
def create_ui_tab(self):
"""创建界面设置选项卡"""
ui_widget = QWidget()
layout = QVBoxLayout()
# 外观设置组
appearance_group = QGroupBox('外观设置')
appearance_layout = QFormLayout()
self.theme_combo = QComboBox()
self.theme_combo.addItems(['默认', '深色', '浅色'])
appearance_layout.addRow('主题:', self.theme_combo)
self.font_size_spin = QSpinBox()
self.font_size_spin.setRange(8, 24)
appearance_layout.addRow('字体大小:', self.font_size_spin)
self.show_toolbar_check = QCheckBox('显示工具栏')
appearance_layout.addRow('', self.show_toolbar_check)
self.show_statusbar_check = QCheckBox('显示状态栏')
appearance_layout.addRow('', self.show_statusbar_check)
appearance_group.setLayout(appearance_layout)
layout.addWidget(appearance_group)
# 窗口设置组
window_group = QGroupBox('窗口设置')
window_layout = QFormLayout()
self.window_width_spin = QSpinBox()
self.window_width_spin.setRange(400, 2000)
window_layout.addRow('窗口宽度:', self.window_width_spin)
self.window_height_spin = QSpinBox()
self.window_height_spin.setRange(300, 1500)
window_layout.addRow('窗口高度:', self.window_height_spin)
self.remember_size_check = QCheckBox('记住窗口大小')
window_layout.addRow('', self.remember_size_check)
window_group.setLayout(window_layout)
layout.addWidget(window_group)
layout.addStretch()
ui_widget.setLayout(layout)
self.tab_widget.addTab(ui_widget, '界面')
def create_advanced_tab(self):
"""创建高级设置选项卡"""
advanced_widget = QWidget()
layout = QVBoxLayout()
# 性能设置组
performance_group = QGroupBox('性能设置')
performance_layout = QFormLayout()
self.cache_size_spin = QSpinBox()
self.cache_size_spin.setRange(10, 1000)
self.cache_size_spin.setSuffix(' MB')
performance_layout.addRow('缓存大小:', self.cache_size_spin)
self.thread_count_spin = QSpinBox()
self.thread_count_spin.setRange(1, 16)
performance_layout.addRow('线程数量:', self.thread_count_spin)
self.enable_logging_check = QCheckBox('启用日志')
performance_layout.addRow('', self.enable_logging_check)
performance_group.setLayout(performance_layout)
layout.addWidget(performance_group)
# 网络设置组
network_group = QGroupBox('网络设置')
network_layout = QFormLayout()
self.proxy_host_edit = QLineEdit()
network_layout.addRow('代理主机:', self.proxy_host_edit)
self.proxy_port_spin = QSpinBox()
self.proxy_port_spin.setRange(1, 65535)
network_layout.addRow('代理端口:', self.proxy_port_spin)
self.timeout_spin = QSpinBox()
self.timeout_spin.setRange(1, 300)
self.timeout_spin.setSuffix(' 秒')
network_layout.addRow('超时时间:', self.timeout_spin)
network_group.setLayout(network_layout)
layout.addWidget(network_group)
layout.addStretch()
advanced_widget.setLayout(layout)
self.tab_widget.addTab(advanced_widget, '高级')
def get_default_config(self):
"""获取默认配置"""
return {
# 通用设置
'username': 'User',
'email': 'user@example.com',
'auto_save': True,
'max_recent_files': 10,
'default_encoding': 'UTF-8',
# 界面设置
'theme': '默认',
'font_size': 12,
'show_toolbar': True,
'show_statusbar': True,
'window_width': 800,
'window_height': 600,
'remember_window_size': True,
# 高级设置
'cache_size': 100,
'thread_count': 4,
'enable_logging': False,
'proxy_host': '',
'proxy_port': 8080,
'timeout': 30
}
def load_config(self):
"""加载配置"""
method = self.method_combo.currentText()
if method in self.config_methods:
config = self.config_methods[method]()
self.apply_config(config)
def load_qsettings(self):
"""使用QSettings加载配置"""
settings = QSettings('ConfigDemo', 'Application')
config = {}
defaults = self.get_default_config()
for key, default_value in defaults.items():
config[key] = settings.value(key, default_value)
# QSettings的类型转换
if isinstance(default_value, bool):
config[key] = str(config[key]).lower() == 'true'
elif isinstance(default_value, int):
config[key] = int(config[key])
return config
def load_json_config(self):
"""从JSON文件加载配置"""
try:
with open('config.json', 'r', encoding='utf-8') as f:
config = json.load(f)
return {**self.get_default_config(), **config}
except FileNotFoundError:
return self.get_default_config()
except Exception as e:
QMessageBox.warning(self, '错误', f'加载JSON配置失败:\n{str(e)}')
return self.get_default_config()
def load_ini_config(self):
"""从INI文件加载配置"""
config = configparser.ConfigParser()
defaults = self.get_default_config()
try:
config.read('config.ini', encoding='utf-8')
result = {}
for key, default_value in defaults.items():
section = 'General'
if key in ['theme', 'font_size', 'show_toolbar', 'show_statusbar',
'window_width', 'window_height', 'remember_window_size']:
section = 'UI'
elif key in ['cache_size', 'thread_count', 'enable_logging',
'proxy_host', 'proxy_port', 'timeout']:
section = 'Advanced'
if config.has_option(section, key):
value = config.get(section, key)
if isinstance(default_value, bool):
result[key] = value.lower() == 'true'
elif isinstance(default_value, int):
result[key] = int(value)
else:
result[key] = value
else:
result[key] = default_value
return result
except Exception as e:
QMessageBox.warning(self, '错误', f'加载INI配置失败:\n{str(e)}')
return defaults
def apply_config(self, config):
"""应用配置到界面"""
# 通用设置
self.username_edit.setText(config.get('username', ''))
self.email_edit.setText(config.get('email', ''))
self.auto_save_check.setChecked(config.get('auto_save', True))
self.max_recent_spin.setValue(config.get('max_recent_files', 10))
encoding = config.get('default_encoding', 'UTF-8')
index = self.default_encoding_combo.findText(encoding)
if index >= 0:
self.default_encoding_combo.setCurrentIndex(index)
# 界面设置
theme = config.get('theme', '默认')
index = self.theme_combo.findText(theme)
if index >= 0:
self.theme_combo.setCurrentIndex(index)
self.font_size_spin.setValue(config.get('font_size', 12))
self.show_toolbar_check.setChecked(config.get('show_toolbar', True))
self.show_statusbar_check.setChecked(config.get('show_statusbar', True))
self.window_width_spin.setValue(config.get('window_width', 800))
self.window_height_spin.setValue(config.get('window_height', 600))
self.remember_size_check.setChecked(config.get('remember_window_size', True))
# 高级设置
self.cache_size_spin.setValue(config.get('cache_size', 100))
self.thread_count_spin.setValue(config.get('thread_count', 4))
self.enable_logging_check.setChecked(config.get('enable_logging', False))
self.proxy_host_edit.setText(config.get('proxy_host', ''))
self.proxy_port_spin.setValue(config.get('proxy_port', 8080))
self.timeout_spin.setValue(config.get('timeout', 30))
def get_current_config(self):
"""获取当前界面配置"""
return {
# 通用设置
'username': self.username_edit.text(),
'email': self.email_edit.text(),
'auto_save': self.auto_save_check.isChecked(),
'max_recent_files': self.max_recent_spin.value(),
'default_encoding': self.default_encoding_combo.currentText(),
# 界面设置
'theme': self.theme_combo.currentText(),
'font_size': self.font_size_spin.value(),
'show_toolbar': self.show_toolbar_check.isChecked(),
'show_statusbar': self.show_statusbar_check.isChecked(),
'window_width': self.window_width_spin.value(),
'window_height': self.window_height_spin.value(),
'remember_window_size': self.remember_size_check.isChecked(),
# 高级设置
'cache_size': self.cache_size_spin.value(),
'thread_count': self.thread_count_spin.value(),
'enable_logging': self.enable_logging_check.isChecked(),
'proxy_host': self.proxy_host_edit.text(),
'proxy_port': self.proxy_port_spin.value(),
'timeout': self.timeout_spin.value()
}
def save_config(self):
"""保存配置"""
method = self.method_combo.currentText()
config = self.get_current_config()
try:
if method == 'QSettings':
self.save_qsettings(config)
elif method == 'JSON':
self.save_json_config(config)
elif method == 'INI':
self.save_ini_config(config)
QMessageBox.information(self, '成功', '配置保存成功!')
except Exception as e:
QMessageBox.warning(self, '错误', f'保存配置失败:\n{str(e)}')
def save_qsettings(self, config):
"""使用QSettings保存配置"""
settings = QSettings('ConfigDemo', 'Application')
for key, value in config.items():
settings.setValue(key, value)
settings.sync()
def save_json_config(self, config):
"""保存到JSON文件"""
with open('config.json', 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
def save_ini_config(self, config):
"""保存到INI文件"""
ini_config = configparser.ConfigParser()
# 分组保存
ini_config.add_section('General')
ini_config.add_section('UI')
ini_config.add_section('Advanced')
# 通用设置
general_keys = ['username', 'email', 'auto_save', 'max_recent_files', 'default_encoding']
for key in general_keys:
ini_config.set('General', key, str(config[key]))
# 界面设置
ui_keys = ['theme', 'font_size', 'show_toolbar', 'show_statusbar',
'window_width', 'window_height', 'remember_window_size']
for key in ui_keys:
ini_config.set('UI', key, str(config[key]))
# 高级设置
advanced_keys = ['cache_size', 'thread_count', 'enable_logging',
'proxy_host', 'proxy_port', 'timeout']
for key in advanced_keys:
ini_config.set('Advanced', key, str(config[key]))
with open('config.ini', 'w', encoding='utf-8') as f:
ini_config.write(f)
def reset_to_defaults(self):
"""重置为默认配置"""
reply = QMessageBox.question(
self, '确认重置',
'确定要重置为默认配置吗?',
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
defaults = self.get_default_config()
self.apply_config(defaults)
def on_method_changed(self, method):
"""配置方式改变"""
self.load_config()
七、多线程与性能优化
7.1 多线程编程
python
import time
import requests
from PyQt5.QtCore import QThread, pyqtSignal, QMutex, QWaitCondition
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QProgressBar, QTextEdit, QLabel, QSpinBox,
QListWidget, QGroupBox)
class WorkerThread(QThread):
"""工作线程示例"""
# 定义信号
progress_updated = pyqtSignal(int)
status_updated = pyqtSignal(str)
result_ready = pyqtSignal(str)
error_occurred = pyqtSignal(str)
def __init__(self, task_type='download', urls=None):
super().__init__()
self.task_type = task_type
self.urls = urls or []
self.is_running = False
self.is_paused = False
self.mutex = QMutex()
self.condition = QWaitCondition()
def run(self):
"""线程主函数"""
self.is_running = True
try:
if self.task_type == 'download':
self.download_files()
elif self.task_type == 'process':
self.process_data()
elif self.task_type == 'calculate':
self.heavy_calculation()
except Exception as e:
self.error_occurred.emit(str(e))
finally:
self.is_running = False
def download_files(self):
"""下载文件示例"""
total_files = len(self.urls)
for i, url in enumerate(self.urls):
if not self.is_running:
break
# 检查暂停状态
self.check_pause()
self.status_updated.emit(f"正在下载: {url}")
try:
# 模拟下载
response = requests.get(url, timeout=10)
if response.status_code == 200:
self.result_ready.emit(f"下载成功: {url}")
else:
self.result_ready.emit(f"下载失败: {url} (状态码: {response.status_code})")
except Exception as e:
self.result_ready.emit(f"下载错误: {url} ({str(e)})")
# 更新进度
progress = int((i + 1) / total_files * 100)
self.progress_updated.emit(progress)
# 模拟下载时间
self.msleep(1000)
def process_data(self):
"""数据处理示例"""
total_steps = 100
for i in range(total_steps):
if not self.is_running:
break
self.check_pause()
self.status_updated.emit(f"处理步骤 {i + 1}/{total_steps}")
# 模拟耗时操作
self.msleep(50)
# 更新进度
progress = int((i + 1) / total_steps * 100)
self.progress_updated.emit(progress)
if self.is_running:
self.result_ready.emit("数据处理完成")
def heavy_calculation(self):
"""重计算示例"""
n = 1000000
total = 0
for i in range(n):
if not self.is_running:
break
if i % 10000 == 0:
self.check_pause()
progress = int(i / n * 100)
self.progress_updated.emit(progress)
self.status_updated.emit(f"计算进度: {i}/{n}")
# 模拟重计算
total += i * i
if self.is_running:
self.result_ready.emit(f"计算完成,结果: {total}")
def check_pause(self):
"""检查暂停状态"""
self.mutex.lock()
try:
while self.is_paused and self.is_running:
self.condition.wait(self.mutex)
finally:
self.mutex.unlock()
def pause(self):
"""暂停线程"""
self.is_paused = True
def resume(self):
"""恢复线程"""
self.mutex.lock()
try:
self.is_paused = False
self.condition.wakeAll()
finally:
self.mutex.unlock()
def stop(self):
"""停止线程"""
self.is_running = False
self.resume() # 确保线程能够退出
class DownloadManagerThread(QThread):
"""下载管理器线程"""
download_started = pyqtSignal(str)
download_finished = pyqtSignal(str, bool)
progress_updated = pyqtSignal(str, int)
def __init__(self):
super().__init__()
self.download_queue = []
self.current_downloads = {}
self.max_concurrent = 3
self.mutex = QMutex()
def add_download(self, url):
"""添加下载任务"""
self.mutex.lock()
try:
self.download_queue.append(url)
finally:
self.mutex.unlock()
def run(self):
"""运行下载管理器"""
while True:
self.mutex.lock()
try:
# 检查是否有新任务且当前下载数未达到上限
if (self.download_queue and
len(self.current_downloads) < self.max_concurrent):
url = self.download_queue.pop(0)
self.start_download(url)
# 检查已完成的下载
completed = []
for url, thread in self.current_downloads.items():
if not thread.isRunning():
completed.append(url)
for url in completed:
thread = self.current_downloads.pop(url)
self.download_finished.emit(url, True)
finally:
self.mutex.unlock()
self.msleep(100) # 避免过度消耗CPU
def start_download(self, url):
"""开始单个下载"""
thread = SingleDownloadThread(url)
thread.progress_updated.connect(
lambda progress: self.progress_updated.emit(url, progress))
self.current_downloads[url] = thread
self.download_started.emit(url)
thread.start()
class SingleDownloadThread(QThread):
"""单个下载线程"""
progress_updated = pyqtSignal(int)
def __init__(self, url):
super().__init__()
self.url = url
def run(self):
"""执行下载"""
try:
# 模拟下载过程
for i in range(101):
self.progress_updated.emit(i)
self.msleep(50)
except Exception:
pass
class ThreadDemo(QWidget):
def __init__(self):
super().__init__()
self.worker_thread = None
self.download_manager = None
self.initUI()
def initUI(self):
self.setWindowTitle('多线程示例')
self.setGeometry(200, 200, 700, 600)
layout = QVBoxLayout()
# 基本线程操作组
basic_group = QGroupBox('基本线程操作')
basic_layout = QVBoxLayout()
# 任务类型选择
task_layout = QHBoxLayout()
task_layout.addWidget(QLabel('任务类型:'))
self.download_btn = QPushButton('文件下载')
self.download_btn.clicked.connect(lambda: self.start_task('download'))
task_layout.addWidget(self.download_btn)
self.process_btn = QPushButton('数据处理')
self.process_btn.clicked.connect(lambda: self.start_task('process'))
task_layout.addWidget(self.process_btn)
self.calc_btn = QPushButton('重计算')
self.calc_btn.clicked.connect(lambda: self.start_task('calculate'))
task_layout.addWidget(self.calc_btn)
task_layout.addStretch()
basic_layout.addLayout(task_layout)
# 控制按钮
control_layout = QHBoxLayout()
self.pause_btn = QPushButton('暂停')
self.pause_btn.clicked.connect(self.pause_task)
self.pause_btn.setEnabled(False)
control_layout.addWidget(self.pause_btn)
self.resume_btn = QPushButton('继续')
self.resume_btn.clicked.connect(self.resume_task)
self.resume_btn.setEnabled(False)
control_layout.addWidget(self.resume_btn)
self.stop_btn = QPushButton('停止')
self.stop_btn.clicked.connect(self.stop_task)
self.stop_btn.setEnabled(False)
control_layout.addWidget(self.stop_btn)
control_layout.addStretch()
basic_layout.addLayout(control_layout)
# 进度显示
self.progress_bar = QProgressBar()
basic_layout.addWidget(self.progress_bar)
self.status_label = QLabel('就绪')
basic_layout.addWidget(self.status_label)
basic_group.setLayout(basic_layout)
layout.addWidget(basic_group)
# 下载管理器组
download_group = QGroupBox('下载管理器')
download_layout = QVBoxLayout()
# 添加下载
add_layout = QHBoxLayout()
add_layout.addWidget(QLabel('并发数
self.concurrent_spin = QSpinBox()
self.concurrent_spin.setRange(1, 10)
self.concurrent_spin.setValue(3)
add_layout.addWidget(self.concurrent_spin)
self.add_download_btn = QPushButton('添加下载任务')
self.add_download_btn.clicked.connect(self.add_download_task)
add_layout.addWidget(self.add_download_btn)
self.start_manager_btn = QPushButton('启动管理器')
self.start_manager_btn.clicked.connect(self.start_download_manager)
add_layout.addWidget(self.start_manager_btn)
add_layout.addStretch()
download_layout.addLayout(add_layout)
# 下载列表
self.download_list = QListWidget()
download_layout.addWidget(self.download_list)
download_group.setLayout(download_layout)
layout.addWidget(download_group)
# 结果显示
result_group = QGroupBox('执行结果')
result_layout = QVBoxLayout()
self.result_text = QTextEdit()
self.result_text.setMaximumHeight(150)
result_layout.addWidget(self.result_text)
result_group.setLayout(result_layout)
layout.addWidget(result_group)
self.setLayout(layout)
def start_task(self, task_type):
"""启动任务"""
if self.worker_thread and self.worker_thread.isRunning():
self.result_text.append("任务正在运行中...")
return
# 准备测试URL
test_urls = [
'https://httpbin.org/delay/1',
'https://httpbin.org/delay/2',
'https://httpbin.org/delay/1',
'https://httpbin.org/status/200',
'https://httpbin.org/json'
]
self.worker_thread = WorkerThread(task_type, test_urls)
# 连接信号
self.worker_thread.progress_updated.connect(self.update_progress)
self.worker_thread.status_updated.connect(self.update_status)
self.worker_thread.result_ready.connect(self.show_result)
self.worker_thread.error_occurred.connect(self.show_error)
self.worker_thread.finished.connect(self.task_finished)
# 更新UI状态
self.set_task_running(True)
# 启动线程
self.worker_thread.start()
self.result_text.append(f"开始执行{task_type}任务...")
def pause_task(self):
"""暂停任务"""
if self.worker_thread:
self.worker_thread.pause()
self.pause_btn.setEnabled(False)
self.resume_btn.setEnabled(True)
self.status_label.setText("任务已暂停")
def resume_task(self):
"""恢复任务"""
if self.worker_thread:
self.worker_thread.resume()
self.pause_btn.setEnabled(True)
self.resume_btn.setEnabled(False)
self.status_label.setText("任务已恢复")
def stop_task(self):
"""停止任务"""
if self.worker_thread:
self.worker_thread.stop()
self.worker_thread.wait() # 等待线程结束
self.task_finished()
self.status_label.setText("任务已停止")
def task_finished(self):
"""任务完成"""
self.set_task_running(False)
self.progress_bar.setValue(0)
self.status_label.setText("任务完成")
def set_task_running(self, running):
"""设置任务运行状态"""
self.download_btn.setEnabled(not running)
self.process_btn.setEnabled(not running)
self.calc_btn.setEnabled(not running)
self.pause_btn.setEnabled(running)
self.stop_btn.setEnabled(running)
self.resume_btn.setEnabled(False)
def update_progress(self, value):
"""更新进度"""
self.progress_bar.setValue(value)
def update_status(self, status):
"""更新状态"""
self.status_label.setText(status)
def show_result(self, result):
"""显示结果"""
self.result_text.append(result)
def show_error(self, error):
"""显示错误"""
self.result_text.append(f"错误: {error}")
def start_download_manager(self):
"""启动下载管理器"""
if self.download_manager and self.download_manager.isRunning():
return
self.download_manager = DownloadManagerThread()
self.download_manager.max_concurrent = self.concurrent_spin.value()
# 连接信号
self.download_manager.download_started.connect(self.on_download_started)
self.download_manager.download_finished.connect(self.on_download_finished)
self.download_manager.progress_updated.connect(self.on_download_progress)
self.download_manager.start()
self.start_manager_btn.setText('管理器运行中')
self.start_manager_btn.setEnabled(False)
def add_download_task(self):
"""添加下载任务"""
test_urls = [
'https://httpbin.org/delay/2',
'https://httpbin.org/delay/3',
'https://httpbin.org/delay/1',
'https://httpbin.org/json',
'https://httpbin.org/status/200'
]
if not self.download_manager:
self.start_download_manager()
for url in test_urls[:2]: # 添加前两个URL
self.download_manager.add_download(url)
self.download_list.addItem(f"队列中: {url}")
def on_download_started(self, url):
"""下载开始"""
for i in range(self.download_list.count()):
item = self.download_list.item(i)
if url in item.text():
item.setText(f"下载中: {url} (0%)")
break
def on_download_finished(self, url, success):
"""下载完成"""
for i in range(self.download_list.count()):
item = self.download_list.item(i)
if url in item.text():
status = "完成" if success else "失败"
item.setText(f"{status}: {url}")
break
def on_download_progress(self, url, progress):
"""下载进度更新"""
for i in range(self.download_list.count()):
item = self.download_list.item(i)
if url in item.text() and "下载中" in item.text():
item.setText(f"下载中: {url} ({progress}%)")
break
def closeEvent(self, event):
"""窗口关闭事件"""
# 停止所有线程
if self.worker_thread and self.worker_thread.isRunning():
self.worker_thread.stop()
self.worker_thread.wait()
if self.download_manager and self.download_manager.isRunning():
self.download_manager.terminate()
self.download_manager.wait()
event.accept()