PyQt开发完整指南
PyQt开发完整指南
目录
- PyQt简介
- 环境搭建
- 第一个PyQt应用
- Qt Designer使用指南
- 常用控件详解
- 布局管理
- 信号与槽机制
- 事件处理
- 对话框
- 菜单栏、工具栏和状态栏
- 多线程编程
- 数据库操作
- 样式表(QSS)
- 打包部署
- 最佳实践
- 常见问题与解决方案
PyQt简介
什么是PyQt
PyQt是Python编程语言和Qt应用程序框架的绑定。它允许Python开发者使用Qt库创建功能丰富的桌面应用程序。PyQt结合了Python的简洁性和Qt的强大功能,成为开发跨平台GUI应用的优秀选择。
PyQt的版本
- PyQt5: 目前最稳定和广泛使用的版本
- PyQt6: 最新版本,基于Qt6,提供了更现代的特性
PyQt vs PySide
环境搭建
安装Python
确保系统已安装Python 3.6或更高版本:
python --version
安装PyQt5
使用pip安装PyQt5:
pip install PyQt5
安装PyQt5工具(包含Qt Designer):
pip install pyqt5-tools
安装PyQt6(可选)
pip install PyQt6
pip install pyqt6-tools
验证安装
创建一个简单的测试脚本:
import sys
from PyQt5.QtWidgets import QApplication, QLabelapp = QApplication(sys.argv)
label = QLabel("PyQt5 安装成功!")
label.show()
sys.exit(app.exec_())
第一个PyQt应用
基本窗口应用
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel
from PyQt5.QtCore import Qtclass MainWindow(QMainWindow):def __init__(self):super().__init__()self.setWindowTitle("我的第一个PyQt应用")self.setGeometry(100, 100, 600, 400)# 创建中心控件label = QLabel("Hello PyQt!", self)label.setAlignment(Qt.AlignCenter)self.setCentralWidget(label)if __name__ == "__main__":app = QApplication(sys.argv)window = MainWindow()window.show()sys.exit(app.exec_())
应用程序结构解析
- QApplication: 管理GUI应用程序的控制流和主要设置
- QMainWindow: 提供主应用程序窗口的框架
- 事件循环:
app.exec_()
启动应用程序的事件循环
Qt Designer使用指南
启动Qt Designer
# Windows
python -m PyQt5.uic.pyuic5designer# Linux/Mac
designer
创建UI文件
- 新建窗体(选择Main Window)
- 拖拽控件到窗体
- 设置控件属性
- 保存为.ui文件
将UI文件转换为Python代码
pyuic5 -x design.ui -o design.py
在代码中使用UI文件
from PyQt5 import uic
from PyQt5.QtWidgets import QApplication, QMainWindowclass MainWindow(QMainWindow):def __init__(self):super().__init__()uic.loadUi('design.ui', self)
常用控件详解
按钮控件
QPushButton(普通按钮)
from PyQt5.QtWidgets import QPushButtonbutton = QPushButton("点击我", self)
button.clicked.connect(self.on_button_clicked)
button.setGeometry(50, 50, 100, 30)def on_button_clicked(self):print("按钮被点击了!")
QRadioButton(单选按钮)
from PyQt5.QtWidgets import QRadioButtonradio1 = QRadioButton("选项1", self)
radio2 = QRadioButton("选项2", self)
radio1.toggled.connect(lambda: self.on_radio_toggled(radio1))
QCheckBox(复选框)
from PyQt5.QtWidgets import QCheckBoxcheckbox = QCheckBox("同意条款", self)
checkbox.stateChanged.connect(self.on_checkbox_changed)def on_checkbox_changed(self, state):if state == 2: # Qt.Checkedprint("已勾选")else:print("未勾选")
输入控件
QLineEdit(单行文本框)
from PyQt5.QtWidgets import QLineEditline_edit = QLineEdit(self)
line_edit.setPlaceholderText("请输入文本")
line_edit.textChanged.connect(self.on_text_changed)
line_edit.setMaxLength(20)
QTextEdit(多行文本框)
from PyQt5.QtWidgets import QTextEdittext_edit = QTextEdit(self)
text_edit.setPlainText("初始文本")
text_edit.textChanged.connect(self.on_text_changed)
QSpinBox(数字输入框)
from PyQt5.QtWidgets import QSpinBoxspin_box = QSpinBox(self)
spin_box.setMinimum(0)
spin_box.setMaximum(100)
spin_box.setValue(50)
spin_box.valueChanged.connect(self.on_value_changed)
显示控件
QLabel(标签)
from PyQt5.QtWidgets import QLabel
from PyQt5.QtGui import QPixmap# 文本标签
label = QLabel("这是一个标签", self)
label.setWordWrap(True) # 自动换行# 图片标签
image_label = QLabel(self)
pixmap = QPixmap("image.png")
image_label.setPixmap(pixmap)
image_label.setScaledContents(True)
QProgressBar(进度条)
from PyQt5.QtWidgets import QProgressBarprogress = QProgressBar(self)
progress.setMinimum(0)
progress.setMaximum(100)
progress.setValue(75)
列表控件
QListWidget(列表控件)
from PyQt5.QtWidgets import QListWidget, QListWidgetItemlist_widget = QListWidget(self)
list_widget.addItem("项目1")
list_widget.addItem("项目2")# 添加自定义项目
item = QListWidgetItem("自定义项目")
list_widget.addItem(item)list_widget.itemClicked.connect(self.on_item_clicked)
QComboBox(下拉框)
from PyQt5.QtWidgets import QComboBoxcombo = QComboBox(self)
combo.addItems(["选项1", "选项2", "选项3"])
combo.currentTextChanged.connect(self.on_selection_changed)
QTableWidget(表格控件)
from PyQt5.QtWidgets import QTableWidget, QTableWidgetItemtable = QTableWidget(self)
table.setRowCount(3)
table.setColumnCount(2)
table.setHorizontalHeaderLabels(["列1", "列2"])# 设置单元格内容
table.setItem(0, 0, QTableWidgetItem("数据1"))
table.setItem(0, 1, QTableWidgetItem("数据2"))
布局管理
QHBoxLayout(水平布局)
from PyQt5.QtWidgets import QHBoxLayout, QPushButtonlayout = QHBoxLayout()
layout.addWidget(QPushButton("按钮1"))
layout.addWidget(QPushButton("按钮2"))
layout.addWidget(QPushButton("按钮3"))widget = QWidget()
widget.setLayout(layout)
QVBoxLayout(垂直布局)
from PyQt5.QtWidgets import QVBoxLayout, QLabellayout = QVBoxLayout()
layout.addWidget(QLabel("标签1"))
layout.addWidget(QLabel("标签2"))
layout.addWidget(QLabel("标签3"))
QGridLayout(网格布局)
from PyQt5.QtWidgets import QGridLayout, QPushButtonlayout = QGridLayout()
layout.addWidget(QPushButton("按钮1"), 0, 0)
layout.addWidget(QPushButton("按钮2"), 0, 1)
layout.addWidget(QPushButton("按钮3"), 1, 0, 1, 2) # 跨列
QFormLayout(表单布局)
from PyQt5.QtWidgets import QFormLayout, QLineEditlayout = QFormLayout()
layout.addRow("姓名:", QLineEdit())
layout.addRow("邮箱:", QLineEdit())
layout.addRow("电话:", QLineEdit())
嵌套布局
# 主垂直布局
main_layout = QVBoxLayout()# 顶部水平布局
top_layout = QHBoxLayout()
top_layout.addWidget(QPushButton("左"))
top_layout.addWidget(QPushButton("中"))
top_layout.addWidget(QPushButton("右"))# 添加到主布局
main_layout.addLayout(top_layout)
main_layout.addWidget(QTextEdit())
信号与槽机制
基本概念
信号与槽是Qt的核心机制,用于对象之间的通信:
- 信号(Signal): 当特定事件发生时发出
- 槽(Slot): 响应信号的函数
连接信号与槽
# 基本连接
button.clicked.connect(self.on_button_clicked)# 带参数的连接
slider.valueChanged.connect(lambda value: self.update_label(value))# 断开连接
button.clicked.disconnect(self.on_button_clicked)
自定义信号
from PyQt5.QtCore import pyqtSignal, QObjectclass CustomObject(QObject):# 定义信号custom_signal = pyqtSignal(str)data_signal = pyqtSignal(int, str)def trigger_signal(self):self.custom_signal.emit("Hello")self.data_signal.emit(42, "数据")# 使用自定义信号
obj = CustomObject()
obj.custom_signal.connect(lambda msg: print(f"收到信号: {msg}"))
装饰器方式
from PyQt5.QtCore import pyqtSlotclass MainWindow(QMainWindow):@pyqtSlot()def on_button_clicked(self):print("按钮被点击")@pyqtSlot(int)def on_value_changed(self, value):print(f"值改变为: {value}")
事件处理
重写事件处理方法
class MainWindow(QMainWindow):def closeEvent(self, event):reply = QMessageBox.question(self, '确认', '确定要退出吗?',QMessageBox.Yes | QMessageBox.No)if reply == QMessageBox.Yes:event.accept()else:event.ignore()def keyPressEvent(self, event):if event.key() == Qt.Key_Escape:self.close()def mousePressEvent(self, event):if event.button() == Qt.LeftButton:print(f"鼠标左键点击位置: {event.pos()}")
事件过滤器
class EventFilter(QObject):def eventFilter(self, obj, event):if event.type() == QEvent.KeyPress:print(f"按键: {event.key()}")return Truereturn False# 安装事件过滤器
filter = EventFilter()
widget.installEventFilter(filter)
对话框
QMessageBox(消息框)
from PyQt5.QtWidgets import QMessageBox# 信息框
QMessageBox.information(self, "标题", "这是一条信息")# 警告框
QMessageBox.warning(self, "警告", "这是一条警告")# 错误框
QMessageBox.critical(self, "错误", "发生了错误")# 询问框
reply = QMessageBox.question(self, "询问", "是否继续?",QMessageBox.Yes | QMessageBox.No)
QFileDialog(文件对话框)
from PyQt5.QtWidgets import QFileDialog# 打开文件
file_name, _ = QFileDialog.getOpenFileName(self, "打开文件", "","文本文件 (*.txt);;所有文件 (*)")# 保存文件
file_name, _ = QFileDialog.getSaveFileName(self, "保存文件", "","文本文件 (*.txt)")# 选择目录
directory = QFileDialog.getExistingDirectory(self, "选择目录")
QInputDialog(输入对话框)
from PyQt5.QtWidgets import QInputDialog# 文本输入
text, ok = QInputDialog.getText(self, "输入", "请输入文本:")# 整数输入
num, ok = QInputDialog.getInt(self, "输入", "请输入数字:", value=0, min=-100, max=100)# 选项输入
items = ["选项1", "选项2", "选项3"]
item, ok = QInputDialog.getItem(self, "选择", "请选择:", items)
自定义对话框
class CustomDialog(QDialog):def __init__(self, parent=None):super().__init__(parent)self.setWindowTitle("自定义对话框")self.setModal(True)layout = QVBoxLayout()# 添加控件self.input = QLineEdit()layout.addWidget(QLabel("请输入:"))layout.addWidget(self.input)# 按钮buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)buttons.accepted.connect(self.accept)buttons.rejected.connect(self.reject)layout.addWidget(buttons)self.setLayout(layout)def get_input(self):return self.input.text()# 使用自定义对话框
dialog = CustomDialog(self)
if dialog.exec_() == QDialog.Accepted:result = dialog.get_input()
菜单栏、工具栏和状态栏
菜单栏
def create_menu_bar(self):menubar = self.menuBar()# 文件菜单file_menu = menubar.addMenu("文件(&F)")# 新建动作new_action = QAction("新建(&N)", self)new_action.setShortcut("Ctrl+N")new_action.triggered.connect(self.new_file)file_menu.addAction(new_action)# 打开动作open_action = QAction("打开(&O)", self)open_action.setShortcut("Ctrl+O")open_action.triggered.connect(self.open_file)file_menu.addAction(open_action)# 分隔符file_menu.addSeparator()# 退出动作exit_action = QAction("退出(&X)", self)exit_action.setShortcut("Ctrl+Q")exit_action.triggered.connect(self.close)file_menu.addAction(exit_action)
工具栏
def create_toolbar(self):toolbar = self.addToolBar("工具栏")# 添加带图标的动作new_action = QAction(QIcon("new.png"), "新建", self)new_action.triggered.connect(self.new_file)toolbar.addAction(new_action)open_action = QAction(QIcon("open.png"), "打开", self)open_action.triggered.connect(self.open_file)toolbar.addAction(open_action)# 添加分隔符toolbar.addSeparator()# 添加控件toolbar.addWidget(QLabel("搜索:"))toolbar.addWidget(QLineEdit())
状态栏
def create_status_bar(self):self.status_bar = self.statusBar()# 显示临时消息(5秒)self.status_bar.showMessage("准备就绪", 5000)# 添加永久控件self.progress = QProgressBar()self.progress.setMaximumWidth(200)self.status_bar.addPermanentWidget(self.progress)self.label = QLabel("状态: 正常")self.status_bar.addPermanentWidget(self.label)
右键菜单
def contextMenuEvent(self, event):menu = QMenu(self)copy_action = menu.addAction("复制")paste_action = menu.addAction("粘贴")action = menu.exec_(self.mapToGlobal(event.pos()))if action == copy_action:self.copy()elif action == paste_action:self.paste()
多线程编程
使用QThread
from PyQt5.QtCore import QThread, pyqtSignal
import timeclass WorkerThread(QThread):# 定义信号progress = pyqtSignal(int)finished = pyqtSignal()def __init__(self):super().__init__()self.is_running = Truedef run(self):for i in range(101):if not self.is_running:breakself.progress.emit(i)time.sleep(0.1)self.finished.emit()def stop(self):self.is_running = False# 使用线程
class MainWindow(QMainWindow):def __init__(self):super().__init__()self.worker = WorkerThread()self.worker.progress.connect(self.update_progress)self.worker.finished.connect(self.on_finished)def start_work(self):self.worker.start()def update_progress(self, value):self.progress_bar.setValue(value)def on_finished(self):QMessageBox.information(self, "完成", "任务已完成!")
使用QRunnable和QThreadPool
from PyQt5.QtCore import QRunnable, QThreadPool, pyqtSignal, QObjectclass WorkerSignals(QObject):finished = pyqtSignal()error = pyqtSignal(str)result = pyqtSignal(object)class Worker(QRunnable):def __init__(self, fn, *args, **kwargs):super().__init__()self.fn = fnself.args = argsself.kwargs = kwargsself.signals = WorkerSignals()def run(self):try:result = self.fn(*self.args, **self.kwargs)self.signals.result.emit(result)except Exception as e:self.signals.error.emit(str(e))finally:self.signals.finished.emit()# 使用线程池
threadpool = QThreadPool()
worker = Worker(self.long_running_task, param1, param2)
worker.signals.result.connect(self.handle_result)
threadpool.start(worker)
数据库操作
使用QSqlDatabase
from PyQt5.QtSql import QSqlDatabase, QSqlQuery, QSqlTableModel# 创建数据库连接
db = QSqlDatabase.addDatabase('QSQLITE')
db.setDatabaseName('database.db')if not db.open():print("无法打开数据库")return# 创建表
query = QSqlQuery()
query.exec_("""CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT,name TEXT NOT NULL,email TEXT NOT NULL)
""")# 插入数据
query.prepare("INSERT INTO users (name, email) VALUES (?, ?)")
query.addBindValue("张三")
query.addBindValue("zhangsan@example.com")
query.exec_()# 查询数据
query.exec_("SELECT * FROM users")
while query.next():id = query.value(0)name = query.value(1)email = query.value(2)print(f"{id}: {name} - {email}")
使用QSqlTableModel
# 创建模型
model = QSqlTableModel()
model.setTable('users')
model.setEditStrategy(QSqlTableModel.OnFieldChange)
model.select()# 设置表头
model.setHeaderData(1, Qt.Horizontal, "姓名")
model.setHeaderData(2, Qt.Horizontal, "邮箱")# 与视图绑定
table_view = QTableView()
table_view.setModel(model)
table_view.hideColumn(0) # 隐藏ID列# 添加新记录
record = model.record()
record.setValue("name", "李四")
record.setValue("email", "lisi@example.com")
model.insertRecord(-1, record)
样式表(QSS)
基本语法
# 设置单个控件样式
button.setStyleSheet("""QPushButton {background-color: #4CAF50;color: white;border: none;padding: 8px 16px;border-radius: 4px;font-size: 14px;}QPushButton:hover {background-color: #45a049;}QPushButton:pressed {background-color: #3d8b40;}
""")# 设置应用程序全局样式
app.setStyleSheet("""QMainWindow {background-color: #f0f0f0;}QLabel {color: #333;font-size: 12px;}
""")
常用样式示例
/* 按钮样式 */
QPushButton {background-color: #007BFF;color: white;border: none;padding: 10px 20px;border-radius: 5px;font-weight: bold;
}QPushButton:hover {background-color: #0056b3;
}QPushButton:disabled {background-color: #cccccc;color: #666666;
}/* 输入框样式 */
QLineEdit {border: 2px solid #ddd;border-radius: 4px;padding: 8px;font-size: 14px;
}QLineEdit:focus {border-color: #007BFF;
}/* 列表样式 */
QListWidget {border: 1px solid #ddd;border-radius: 4px;background-color: white;
}QListWidget::item {padding: 8px;border-bottom: 1px solid #eee;
}QListWidget::item:selected {background-color: #007BFF;color: white;
}/* 菜单样式 */
QMenuBar {background-color: #f8f9fa;border-bottom: 1px solid #dee2e6;
}QMenuBar::item:selected {background-color: #007BFF;color: white;
}
从文件加载样式
def load_stylesheet(file_path):with open(file_path, 'r', encoding='utf-8') as file:return file.read()# 应用样式
app.setStyleSheet(load_stylesheet('style.qss'))
打包部署
使用PyInstaller
安装PyInstaller
pip install pyinstaller
基本打包命令
# 打包为单个可执行文件
pyinstaller --onefile --windowed main.py# 打包为文件夹
pyinstaller --onedir --windowed main.py# 指定图标
pyinstaller --onefile --windowed --icon=app.ico main.py# 包含额外文件
pyinstaller --onefile --windowed --add-data "assets;assets" main.py
配置文件(.spec)
# main.spec
a = Analysis(['main.py'],pathex=[],binaries=[],datas=[('assets', 'assets')],hiddenimports=['PyQt5.QtPrintSupport'],hookspath=[],runtime_hooks=[],excludes=[],win_no_prefer_redirects=False,win_private_assemblies=False,cipher=None,noarchive=False)pyz = PYZ(a.pure, a.zipped_data, cipher=None)exe = EXE(pyz,a.scripts,a.binaries,a.zipfiles,a.datas,[],name='MyApp',debug=False,bootloader_ignore_signals=False,strip=False,upx=True,console=False,icon='app.ico')
资源文件处理
import sys
import osdef resource_path(relative_path):"""获取资源文件的绝对路径"""try:# PyInstaller创建临时文件夹并将路径存储在_MEIPASS中base_path = sys._MEIPASSexcept Exception:base_path = os.path.abspath(".")return os.path.join(base_path, relative_path)# 使用资源文件
icon_path = resource_path('icons/app.png')
pixmap = QPixmap(icon_path)
最佳实践
项目结构
project/
├── main.py # 主入口文件
├── ui/ # UI文件目录
│ ├── __init__.py
│ ├── main_window.ui
│ └── dialogs/
├── widgets/ # 自定义控件
│ ├── __init__.py
│ └── custom_widget.py
├── resources/ # 资源文件
│ ├── icons/
│ ├── images/
│ └── styles/
├── utils/ # 工具函数
│ ├── __init__.py
│ └── helpers.py
├── models/ # 数据模型
│ ├── __init__.py
│ └── data_model.py
└── requirements.txt # 依赖列表
代码组织
# main_window.py
class MainWindow(QMainWindow):def __init__(self):super().__init__()self.init_ui()self.init_connections()self.load_settings()def init_ui(self):"""初始化UI"""self.setup_window()self.create_widgets()self.create_layouts()self.create_menus()def init_connections(self):"""初始化信号连接"""self.button.clicked.connect(self.on_button_clicked)def load_settings(self):"""加载配置"""settings = QSettings('MyCompany', 'MyApp')geometry = settings.value('geometry')if geometry:self.restoreGeometry(geometry)def save_settings(self):"""保存配置"""settings = QSettings('MyCompany', 'MyApp')settings.setValue('geometry', self.saveGeometry())
内存管理
# 正确删除控件
widget.deleteLater()# 清理布局
def clear_layout(layout):while layout.count():child = layout.takeAt(0)if child.widget():child.widget().deleteLater()# 使用弱引用
import weakref
self.widget_ref = weakref.ref(widget)
异常处理
def safe_operation(self):try:# 危险操作result = self.risky_operation()except FileNotFoundError:QMessageBox.warning(self, "警告", "文件未找到")except Exception as e:QMessageBox.critical(self, "错误", f"发生错误: {str(e)}")# 记录日志logging.error(f"操作失败: {e}", exc_info=True)
性能优化
# 使用QTimer延迟操作
def delayed_operation(self):QTimer.singleShot(100, self.heavy_operation)# 批量更新UI
self.setUpdatesEnabled(False)
# 执行大量UI更新
for i in range(1000):self.add_item(i)
self.setUpdatesEnabled(True)# 使用模型/视图架构
model = QStandardItemModel()
view = QListView()
view.setModel(model)
常见问题与解决方案
1. 中文显示问题
# 设置编码
import sys
if sys.platform == 'win32':import ctypesctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID('myapp')# 设置字体
font = QFont("Microsoft YaHei", 10)
app.setFont(font)
2. 高DPI显示问题
# 在创建QApplication之前设置
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)app = QApplication(sys.argv)
3. 线程中更新UI
# 错误方式
class Worker(QThread):def run(self):self.label.setText("更新") # 错误!# 正确方式
class Worker(QThread):update_signal = pyqtSignal(str)def run(self):self.update_signal.emit("更新")# 主线程中
worker.update_signal.connect(self.label.setText)
4. 资源文件未找到
# 使用Qt资源系统
import resources_rc # pyrcc5生成的资源文件# 或使用绝对路径
import os
current_dir = os.path.dirname(os.path.abspath(__file__))
icon_path = os.path.join(current_dir, 'icons', 'app.png')
5. 窗口关闭后程序未退出
# 确保最后一个窗口关闭时退出
app.setQuitOnLastWindowClosed(True)# 或在关闭事件中
def closeEvent(self, event):QApplication.quit()
6. 内存泄漏
# 使用parent参数
button = QPushButton("按钮", parent=self)# 断开不需要的信号连接
try:self.signal.disconnect()
except TypeError:pass# 清理定时器
if hasattr(self, 'timer'):self.timer.stop()self.timer.deleteLater()
7. 打包后找不到模块
# 在.spec文件中添加隐藏导入
hiddenimports=['PyQt5.QtPrintSupport', 'PyQt5.QtSql']# 或在命令行中
pyinstaller --hidden-import=PyQt5.QtPrintSupport main.py
学习资源
官方文档
- Qt Documentation
- PyQt5 Reference Guide