文章目录
- pip install PyQt5
- uv add pyqt5==5.15.11 pyqt5-qt5==5.15.2 这里限定版本是因为pyqt5的最新版本不适配于Windows uv库,如果是其他设备可能不需要限定版本。
- PyQt5 5.15.x 是PyQt5的最后一个系列版本 支持Python 3.5及以上版本 建议使用Python 3.8+以获得最佳体验
- 安装完成后,可以运行以下代码验证: import sys from PyQt5.QtWidgets import QApplication, QLabel app = QApplication(sys.argv) label = QLabel(“Hello PyQt5!”) label.setMinimumSize(400, 50) label.show() app.exec_() 运行效果: (由于电脑分辨率不同,具体的大小效果可能不同) 我运行完代码之后会打印这个信息: Can't find filter elementCan't find filter element 不知道是为什么,但是好像也不影响GUI应用的展示,我就先不管了……
- QApplication是PyQt5应用的"大脑"和"心脏",负责: 管理应用程序的控制流:协调各个窗口和控件的工作 处理系统级事件:接收和分发鼠标点击、键盘输入、窗口重绘等事件 提供全局设置:管理应用程序的字体、样式、调色板等 重要规则:一个应用只能有一个QApplication实例! app = QApplication(sys.argv) # sys.argv用于接收命令行参数 事件循环:GUI程序的"心脏跳动" 当我们运行一个PyQt5程序时,到底发生了什么?让我用一个比喻来解释:想象一下,你的GUI程序就像一个餐厅: QApplication是餐厅经理 事件循环(event loop)是餐厅的服务流程 用户操作(点击、输入)就是顾客的订单 为什么需要事件循环? # 启动事件循环 app.exec_() app.exec_() 启动了一个无限循环,这个循环不断地做三件事: 监听系统事件(鼠标点击、键盘输入等) 将事件分发给对应的控件处理 处理完事件后,等待下一个事件 这个循环会一直运行,直到你关闭所有窗口或调用app.quit()。 sys.exit(app.exec_())到底是什么意思? 这是一个初学者常见的困惑点。让我们分解来看: sys.exit(app.exec_()) 分解理解: app.exec_(): 启动事件循环 返回一个退出状态码(通常是0表示成功,非0表示错误) 事件循环会阻塞在这里,直到程序退出 sys.exit(): Python标准库函数 用于退出Python程序 可以接收一个退出状态码作为参数 为什么要这样写? # 不推荐的写法 app.exec_() # 程序会在这里卡住,但退出时可能不会清理资源 # 推荐的写法 sys.exit(app.exec_()) # 确保程序正确退出,并返回状态码 工作原理示意图: 开始 ↓创建 QApplication ↓创建窗口和控件 ↓sys.exit(app.exec_()) ├── 启动事件循环(app.exec_()) │ ├── 监听用户操作 │ ├── 处理事件 │ └── 等待下一个事件… │ └── 事件循环结束时返回状态码 ↓ sys.exit(状态码) 退出程序 完整示例:理解程序执行流程 import sys from PyQt5.QtWidgets import QApplication, QWidget, QPushButton class MyApp(QWidget): def __init__(self): super().__init__() self.initUI() def initUI(self): self.setWindowTitle(‘QApplication示例’) self.setGeometry(300, 300, 300, 200) btn = QPushButton(‘退出程序’, self) btn.clicked.connect(self.close) # 点击按钮关闭窗口 btn.move(100, 80) self.show() if __name__ == ‘__main__’: print(“1. 程序开始”) app = QApplication(sys.argv) print(“2. QApplication实例已创建”) window = MyApp() print(“3. 窗口已创建并显示”) print(“4. 进入事件循环…”) exit_code = app.exec_() # 在这里程序会”阻塞”,等待事件 print(f”5. 事件循环结束,退出码: {exit_code}”) sys.exit(exit_code) 运行这个程序,你会看到: 控制台先打印1-4步的信息 然后程序进入事件循环,GUI界面出现 当你关闭窗口时,事件循环结束 最后打印第5步的信息,程序退出 常见问题解答 Q: 为什么要用sys.exit()包装app.exec_()? A: 为了确保程序正确退出,并返回合适的退出状态码给操作系统。 Q: 可以不用sys.exit()吗? A: 在简单程序中可能可以,但在复杂程序中,不使用sys.exit()可能导致资源未正确释放。 Q: 什么时候事件循环会结束? A: 当调用app.quit()或关闭所有窗口时,事件循环会结束。 Q: 事件循环期间,我的代码还能运行吗? A: 不能直接运行。所有代码都必须在事件处理函数中执行。如果需要执行长时间任务,应该使用多线程。 记住这个模式 几乎所有PyQt5程序都遵循这个模式: import sys from PyQt5.QtWidgets import QApplication, QWidget def main(): app = QApplication(sys.argv) # 1. 创建应用 window = QWidget() # 2. 创建窗口 window.show() # 3. 显示窗口 return app.exec_() # 4. 进入事件循环 if __name__ == ‘__main__’: sys.exit(main()) # 5. 确保正确退出 掌握了QApplication和事件循环的概念,你就理解了PyQt5程序运行的基础机制。这是构建更复杂应用的基石!
- QWidget:所有可视化组件的基类 按钮、标签、输入框等都是QWidget的子类 可以单独作为窗口使用 QMainWindow:带有菜单栏、工具栏、状态栏的主窗口 适用于复杂的应用程序 提供了标准的应用程序框架 QMainWindow是QWidget的"加强版",但它们的定位和用途有很大区别。 核心关系:继承与扩展 # PyQt5中的继承关系object ↓QObject ↓QWidget ← 所有可视化元素的基类 ↓QMainWindow ← 专门用于主窗口的特殊QWidget 简单来说: QWidget是所有可视控件的"爷爷辈"基类 QMainWindow是QWidget的一个"大儿子",专门为应用程序主窗口设计 按钮、标签、输入框等都是QWidget的其他"儿子孙子" QWidget:万能的基础窗口 QWidget是PyQt5中最基础的窗口类,它可以扮演两种角色: 1. 作为独立的简单窗口 from PyQt5.QtWidgets import QWidget, QLabel, QPushButton, QVBoxLayout, QApplication import sys class SimpleWindow(QWidget): def __init__(self): super().__init__() self.setWindowTitle(“我是一个QWidget窗口”) self.resize(300, 200) # 创建布局和控件 layout = QVBoxLayout() label = QLabel(“这是一个简单的对话框”) button = QPushButton(“确定”) layout.addWidget(label) layout.addWidget(button) self.setLayout(layout) if __name__ == “__main__”: app = QApplication(sys.argv) window = SimpleWindow() window.show() sys.exit(app.exec_()) 运行效果: 2. 作为其他控件的容器 # QLabel、QPushButton、QLineEdit等都是QWidget的子类 # 它们都继承了QWidget的所有功能 label = QLabel(“我是QWidget的子类”) button = QPushButton(“我也是QWidget的子类”) QWidget的特点: 轻量级,内存占用小 灵活,可以自由布局 适合对话框、弹窗、简单工具窗口 QMainWindow:专业的应用程序主窗口 QMainWindow是专门为应用程序主窗口设计的,它提供了"开箱即用"的标准主窗口结构: from PyQt5.QtWidgets import QApplication, QMainWindow, QTextEdit, QAction, QLabel import sys class MainAppWindow(QMainWindow): def __init__(self): super().__init__() self.initUI() def initUI(self): # 1. 设置中央部件(必需) text_edit = QTextEdit() self.setCentralWidget(text_edit) # 2. 创建菜单栏(可选) menubar = self.menuBar() file_menu = menubar.addMenu(“文件(&F)”) # 向菜单添加具体的动作 open_action = QAction(“打开”, self) save_action = QAction(“保存”, self) exit_action = QAction(“退出”, self) exit_action.triggered.connect(self.close) file_menu.addAction(open_action) file_menu.addAction(save_action) file_menu.addSeparator() file_menu.addAction(exit_action) # 3. 创建工具栏并添加工具按钮(必需,否则工具栏为空) toolbar = self.addToolBar(“标准工具栏”) # 添加工具栏按钮(使用文本) toolbar.addAction(“新建”) toolbar.addAction(“打开”) toolbar.addAction(“保存”) toolbar.addSeparator() toolbar.addAction(“打印”) # 4. 创建状态栏(可选) status_bar = self.statusBar() status_bar.showMessage(“就绪”, 3000) # 显示3秒 # 添加永久显示的状态栏部件 permanent_label = QLabel(“永久状态信息”) status_bar.addPermanentWidget(permanent_label) # 5. 设置窗口属性 self.setWindowTitle(“文本编辑器”) self.setGeometry(100, 100, 800, 600) self.show() if __name__ == “__main__”: app = QApplication(sys.argv) window = MainAppWindow() sys.exit(app.exec_()) 运行效果(前3秒会显示就绪): 展开菜单”文件“,过3秒: QMainWindow的标准结构 +—————————————————+| 菜单栏 (Menu Bar) |+—————————————————+| 工具栏 (Tool Bar) |+—————————————————+| || 中央部件 (Central Widget) || || +—————————————–+ || | | || | 你的主要内容在这里 | || | | || +—————————————–+ || |+—————————————————+| 状态栏 (Status Bar) |+—————————————————+ 我们很容易发现QMainWindow里面又有菜单,又有工具栏,那我们很容易就会提问,不对啊,现在的软件不是要么只有菜单(如VSCode),要么只有标签形式切换的工具栏(如Word)吗? 这是一个相当复杂的问题,首先Word老版(1984-2006)其实真的是又有菜单栏又有工具栏的: ┌─────────────────────────────────────┐│ 文件(F) 编辑(E) 视图(V) 帮助(H) ← 固定菜单栏├─────────────────────────────────────┤│ [📄] [📂] [💾] [🖨️] [B] [I] [U] ← 浮动工具栏└─────────────────────────────────────┘ 特点: 菜单栏是固定的、层级式的 工具栏是独立的、可拖动的 功能隐藏在多层菜单中 空间利用率较低 但2007年后改为了采用这样的Ribbon界面: ┌─────────────────────────────────────┐│ 🏠 插入 设计 布局 引用 邮件 审阅 视图 ← 情境化标签页├─────────────────────────────────────┤│ 当前任务相关功能分组展示 ← 自适应功能区│ ┌─────────┐ ┌─────────┐ ┌─────────┐│ │ 剪贴板 │ │ 字体 │ │ 段落 ││ │ │ │ │ │ ││ └─────────┘ └─────────┘ └─────────┘└─────────────────────────────────────┘ 核心创新: 情境化标签页:根据当前任务切换功能组 功能分组可视化:图标+文字,直观易懂 自适应空间:根据窗口大小调整显示 减少菜单层级:80%常用功能在第一层 而VSCode则采用的是侧边活动栏: # VS Code的界面结构┌─────────────────────────────────────────────────────┐│ File Edit View Go Run Terminal Help │ ← 顶部菜单栏(简洁)├─────────────────────────────────────────────────────┤│ 🏠 🔍 💾 🐙 ⏹️ │ ← 活动栏(侧边图标栏)│ ││ 侧边面板区域 ││ (资源管理器、搜索、Git等) ││ │├─────────────────────────────────────────────────────┤│ [main.py] │ ← 编辑器标签页│ ││ def main(): │ ← 主编辑区域│ print("Hello World") ││ │├─────────────────────────────────────────────────────┤│ Python 3.12.4 • UTF-8 • LF • 2 spaces │ ← 状态栏(多信息显示)└─────────────────────────────────────────────────────┘ 关键区别对比表 特性 QWidget QMainWindow 定位 基础窗口/控件 应用程序主窗口 内存占用 较小 较大(包含更多组件) 内置结构 无 有标准菜单栏、工具栏、状态栏区域 布局管理 需要手动设置布局 中央部件区域可设置布局 灵活性 高,完全自定义 有一定结构限制 典型用途 对话框、弹窗、简单窗口 软件主窗口、复杂应用 是否能使用布局管理器 可以,直接设置 只能对中央部件使用布局 是否能有多个实例 可以 通常只有一个 关键注意事项 1. QMainWindow不能直接设置布局! (布局的介绍见本文第6节) # ❌ 错误做法 class WrongWindow(QMainWindow): def __init__(self): super().__init__() layout = QVBoxLayout() layout.addWidget(QLabel(“测试”)) self.setLayout(layout) # 这不会工作! # ✅ 正确做法 class CorrectWindow(QMainWindow): def __init__(self): super().__init__() # 创建一个QWidget作为中央部件的容器 central_widget = QWidget() self.setCentralWidget(central_widget) # 在容器上设置布局 layout = QVBoxLayout() layout.addWidget(QLabel(“测试”)) central_widget.setLayout(layout) # 在中央部件上设置布局 2. 实际应用中的选择策略 # 场景1:需要标准菜单栏/工具栏的软件 → 选择QMainWindow class TextEditor(QMainWindow): “””文本编辑器,需要菜单栏保存文件””” pass # 场景2:简单的配置对话框 → 选择QWidget class SettingsDialog(QWidget): “””设置对话框,不需要复杂的菜单结构””” pass # 场景3:复杂应用的子窗口 → 选择QWidget class PreviewWindow(QWidget): “””预览窗口,作为主窗口的子窗口””” pass 3. 混合使用示例 from PyQt5.QtWidgets import QApplication, QMainWindow, QTextEdit, QWidget import sys class MainApp(QMainWindow): def __init__(self): super().__init__() # 主窗口使用QMainWindow self.setWindowTitle(“主应用程序”) self.setCentralWidget(QTextEdit()) # 但点击按钮可以弹出QWidget对话框 self.settings_dialog = SettingsDialog(self) def show_settings(self): self.settings_dialog.exec_() # 显示模态对话框 class SettingsDialog(QWidget): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle(“设置”) self.resize(300, 200) # 使用QWidget作为对话框 if __name__ == “__main__”: app = QApplication(sys.argv) window = MainApp() window.show() sys.exit(app.exec_()) 生成效果(我手动拖拽了一下界面大小): 实践建议 什么时候用QWidget? 对话框和弹窗:确认框、消息框、设置窗口 简单工具:计算器、单位转换器等小工具 自定义控件:创建可重用的界面组件 子窗口:主应用中的浮动面板 什么时候用QMainWindow? 应用程序主窗口:编辑器、浏览器、IDE等 复杂应用:需要标准菜单和工具栏的软件 专业工具:图像处理、数据分析等专业软件 一个有用的技巧:从简单开始 # 初期:从QWidget开始,快速原型 class SimpleApp(QWidget): def __init__(self): super().__init__() # 简单布局和功能 # 后期:需要更多功能时,轻松迁移到QMainWindow class EnhancedApp(QMainWindow): def __init__(self): super().__init__() # 将原来的QWidget内容设为中央部件 old_widget = SimpleApp() self.setCentralWidget(old_widget) # 添加菜单栏、工具栏等 总结 QWidget和QMainWindow不是"基础版"和"高级版"的关系,而是不同用途的工具: QWidget像是白纸:给你最大自由度,想画什么就画什么 QMainWindow像是已经画好框架的画布:提供了标准结构,你只需要填充内容 记住这个简单的选择原则: 如果只需要一个简单的窗口或对话框 → 选QWidget 如果要创建有标准菜单/工具栏的应用程序主窗口 → 选QMainWindow 理解它们的区别后,你就能根据实际需求做出合适的选择,写出更专业、更高效的PyQt5代码!
- 这是Qt最强大的特性之一!想象一下: 信号(Signal):像电灯的开关 槽(Slot):像电灯本身 连接(Connect):像连接开关和灯的电线 工作原理:事件发生 → 发出信号 → 连接到槽 → 执行函数 import sys from PyQt5.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QLabel, QPushButton, QTextEdit, ) from PyQt5.QtCore import pyqtSignal class CustomButton(QPushButton): “”” 自定义按钮类 演示如何创建和使用自定义信号 “”” # 自定义信号 – 可以发送一个字符串 message_signal = pyqtSignal(str) # 另一个自定义信号 – 可以发送两个整数 number_signal = pyqtSignal(int, int) def __init__(self, text, parent=None): super().__init__(text, parent) self.click_count = 0 def mousePressEvent(self, event): “”” 重写鼠标按下事件 每次点击都会发出两个信号 “”” self.click_count += 1 # 发出第一个信号 – 带字符串消息 self.message_signal.emit(f”第{self.click_count}次点击!”) # 发出第二个信号 – 带两个数字 x, y = event.x(), event.y() self.number_signal.emit(x, y) # 重要:调用父类方法确保正常行为 super().mousePressEvent(event) class ExampleApp(QWidget): def __init__(self): super().__init__() self.setup_ui() def setup_ui(self): self.setWindowTitle(“自定义信号详细示例”) self.resize(500, 400) # 创建布局 layout = QVBoxLayout() # 1. 信息显示区域 self.info_label = QLabel(“点击下面的按钮查看效果”, self) layout.addWidget(self.info_label) # 2. 文本显示区域(用于显示详细信息) self.text_display = QTextEdit(self) self.text_display.setReadOnly(True) layout.addWidget(self.text_display) # 3. 创建自定义按钮 self.custom_btn = CustomButton(“自定义按钮 – 点击我”, self) layout.addWidget(self.custom_btn) # 4. 重置按钮 self.reset_btn = QPushButton(“重置计数”, self) layout.addWidget(self.reset_btn) self.setLayout(layout) # 连接信号 self.connect_signals() def connect_signals(self): “””连接所有信号到槽函数””” # 连接自定义按钮的第一个信号 self.custom_btn.message_signal.connect(self.update_info) # 连接自定义按钮的第二个信号 self.custom_btn.number_signal.connect(self.show_click_position) # 连接重置按钮 self.reset_btn.clicked.connect(self.reset_counter) def update_info(self, message): “””更新信息标签””” self.info_label.setText(f”自定义信号: {message}”) self.text_display.append(f”收到消息: {message}”) def show_click_position(self, x, y): “””显示点击位置””” self.text_display.append(f”点击位置: x={x}, y={y}”) def reset_counter(self): “””重置计数器””” self.custom_btn.click_count = 0 self.info_label.setText(“计数器已重置”) self.text_display.append(“— 计数器重置 —“) # 运行示例 if __name__ == “__main__”: app = QApplication(sys.argv) window = ExampleApp() window.show() sys.exit(app.exec_()) 首先设置带信号(代码里设置了两种)的按钮,然后将按钮放到组件上,把信号连接到槽函数上(槽函数也可以是一个lambda函数)。按钮被触发时,信号发射(emit),槽函数接受并处理信号,在代码中就将处理结果展示在界面上:
- from PyQt5.QtWidgets import ( QLabel, # 标签 – 显示文本或图片 QPushButton, # 按钮 – 点击触发动作 QLineEdit, # 单行输入框 QTextEdit, # 多行文本编辑 QCheckBox, # 复选框 QRadioButton, # 单选按钮 QComboBox, # 下拉框 QSpinBox, # 数字输入框 QProgressBar, # 进度条 QSlider, # 滑块 )
- 自动调整控件位置和大小 适应不同分辨率和窗口大小 简化界面设计
- QVBoxLayout – 垂直排列 from PyQt5.QtWidgets import QVBoxLayout, QPushButton, QLabel layout = QVBoxLayout() layout.addWidget(QLabel(“第一行”)) layout.addWidget(QPushButton(“第二行”)) layout.addWidget(QLabel(“第三行”)) self.setLayout(layout) # 应用到窗口 QHBoxLayout – 水平排列 from PyQt5.QtWidgets import QHBoxLayout layout = QHBoxLayout() layout.addWidget(QPushButton(“左”)) layout.addWidget(QPushButton(“中”)) layout.addWidget(QPushButton(“右”)) 布局嵌套 – 创建复杂界面 # 创建主垂直布局 main_layout = QVBoxLayout() # 创建水平布局并添加控件 top_layout = QHBoxLayout() top_layout.addWidget(QLabel(“姓名:”)) top_layout.addWidget(QLineEdit()) # 将水平布局添加到垂直布局 main_layout.addLayout(top_layout) main_layout.addWidget(QPushButton(“提交”)) self.setLayout(main_layout)
- # 错误写法 window = QWidget() window.show() # 正确写法 app = QApplication(sys.argv) window = QWidget() window.show() app.exec_()
- 在PyQt5(以及大多数GUI框架)中,有一个黄金规则:所有GUI操作都必须在主线程(也称为GUI线程)中进行! 为什么有这个限制? 简化的解释: PyQt5的GUI组件不是"线程安全"的 想象一下两个线程同时修改同一个控件: 线程A: label.setText("Hello") 线程B: label.setText("World") ↓ ↓同时写入同一个内存区域 → 数据竞争 → 程序崩溃! 错误示例分析 错误代码: import sys import time from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QPushButton from PyQt5.QtCore import QThread, pyqtSignal class WorkerThread(QThread): “””工作线程””” def run(self): time.sleep(2) # 模拟耗时操作 # ❌ 危险!在子线程中直接更新GUI label.setText(“处理完成!”) # 可能导致崩溃 # 在主线程中创建窗口 app = QApplication(sys.argv) window = QWidget() label = QLabel(“等待中…”) button = QPushButton(“开始任务”) layout = QVBoxLayout() layout.addWidget(label) layout.addWidget(button) window.setLayout(layout) # 创建工作线程 worker = WorkerThread() def start_task(): worker.start() button.clicked.connect(start_task) window.show() sys.exit(app.exec_()) 可能的结果: 程序可能直接崩溃 界面可能不更新 可能偶尔正常工作,但不可靠(最危险的情况!) 正确解决方案:使用信号 方案1:QThread + moveToThread import sys import time from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QPushButton from PyQt5.QtCore import QThread, pyqtSignal, QObject class Worker(QObject): “””工作对象,使用信号通信””” # 定义信号 progress_signal = pyqtSignal(int) # 传递进度百分比 result_signal = pyqtSignal(str) # 传递结果字符串 finished_signal = pyqtSignal() # 完成信号(无参数) def do_work(self): “””执行耗时任务””” for i in range(1, 11): time.sleep(0.5) # 模拟耗时操作 self.progress_signal.emit(i * 10) # 发射进度信号 self.result_signal.emit(“处理完成!”) # 发射结果信号 self.finished_signal.emit() # 发射完成信号 class MainWindow(QWidget): def __init__(self): super().__init__() self.init_ui() self.setup_worker() def init_ui(self): self.setWindowTitle(“线程安全更新GUI示例”) self.resize(400, 300) layout = QVBoxLayout() self.label = QLabel(“准备开始任务…”) self.progress_label = QLabel(“进度: 0%”) self.button = QPushButton(“开始任务”) self.status_label = QLabel(“状态: 空闲”) layout.addWidget(self.label) layout.addWidget(self.progress_label) layout.addWidget(self.status_label) layout.addWidget(self.button) self.setLayout(layout) self.button.clicked.connect(self.start_work) def setup_worker(self): “””设置工作线程和信号连接””” # 创建工作对象和线程 self.worker = Worker() self.thread = QThread() # 将工作对象移动到线程中 self.worker.moveToThread(self.thread) # 连接信号 self.worker.progress_signal.connect(self.update_progress) self.worker.result_signal.connect(self.update_result) self.worker.finished_signal.connect(self.work_finished) # 线程开始后,连接do_work方法 self.thread.started.connect(self.worker.do_work) # 线程结束时,清理资源 self.worker.finished_signal.connect(self.thread.quit) self.worker.finished_signal.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) def start_work(self): “””开始工作””” self.button.setEnabled(False) self.status_label.setText(“状态: 处理中…”) self.thread.start() def update_progress(self, progress): “””更新进度(在主线程中执行)””” self.progress_label.setText(f”进度: {progress}%”) def update_result(self, result): “””更新结果(在主线程中执行)””” self.label.setText(result) def work_finished(self): “””任务完成(在主线程中执行)””” self.button.setEnabled(True) self.status_label.setText(“状态: 完成”) if __name__ == “__main__”: app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_()) 方案2:QRunnable 需要管理生命周期: import sys import time from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QPushButton from PyQt5.QtCore import QThreadPool, QRunnable, pyqtSignal, QObject, pyqtSlot, QMutex class WorkerSignals(QObject): “””定义工作线程的信号””” finished = pyqtSignal() progress = pyqtSignal(int) result = pyqtSignal(str) def __init__(self): super().__init__() self.is_valid = True # 添加有效性标志 def delete_later(self): self.is_valid = False self.deleteLater() class Worker(QRunnable): “””工作线程类””” def __init__(self): super().__init__() self.signals = WorkerSignals() self.mutex = QMutex() # 互斥锁保护信号对象 def run(self): “””执行耗时任务””” for i in range(10): time.sleep(0.3) progress = (i + 1) * 10 # 使用互斥锁保护信号对象 self.mutex.lock() try: if ( self.signals and hasattr(self.signals, “is_valid”) and self.signals.is_valid ): self.signals.progress.emit(progress) except RuntimeError: # 信号对象已被删除 pass finally: self.mutex.unlock() self.mutex.lock() try: if ( self.signals and hasattr(self.signals, “is_valid”) and self.signals.is_valid ): self.signals.result.emit(“任务完成!”) self.signals.finished.emit() except RuntimeError: pass finally: self.mutex.unlock() class SimpleThreadExample(QWidget): def __init__(self): super().__init__() self.init_ui() self.threadpool = QThreadPool() self.workers = [] # 保持对worker的引用 def init_ui(self): self.setWindowTitle(“简单多线程示例”) self.resize(300, 200) layout = QVBoxLayout() self.label = QLabel(“点击按钮开始任务”) self.button = QPushButton(“开始耗时任务”) layout.addWidget(self.label) layout.addWidget(self.button) self.setLayout(layout) self.button.clicked.connect(self.start_task) def start_task(self): “””启动工作线程””” # 禁用按钮,防止重复点击 self.button.setEnabled(False) # 创建工作对象 worker = Worker() self.workers.append(worker) # 保持引用 # 连接信号 worker.signals.progress.connect(self.on_progress) worker.signals.result.connect(self.on_result) worker.signals.finished.connect(lambda: self.on_finished(worker)) # 在线程池中执行 self.threadpool.start(worker) def on_progress(self, progress): “””更新进度(自动在主线程中执行)””” self.label.setText(f”处理中… {progress}%”) def on_result(self, result): “””显示结果(自动在主线程中执行)””” self.label.setText(result) def on_finished(self, worker): “””任务完成(自动在主线程中执行)””” self.button.setEnabled(True) # 清理worker if worker in self.workers: if worker.signals: worker.signals.delete_later() self.workers.remove(worker) if __name__ == “__main__”: app = QApplication(sys.argv) window = SimpleThreadExample() window.show() sys.exit(app.exec_()) 为什么信号能安全地更新GUI? 信号与槽的线程安全机制: PyQt5的内部机制: 当信号从子线程发射时,PyQt5会自动: 将信号放入主线程的事件队列 主线程在适当的时候处理这个事件 调用连接的槽函数(在主线程中!) 所以: worker.signals.result.emit("数据") # 子线程中发射信号 ↓PyQt5内部:跨线程传递信号 ↓label.setText("数据") # 在主线程中执行槽函数 其他安全更新GUI的方法 1. 使用QTimer在主线程中轮询 from PyQt5.QtCore import QTimer class SafeUpdateExample: def __init__(self): self.results_queue = [] # 线程安全的数据结构 # 定时器在主线程中运行 self.timer = QTimer() self.timer.timeout.connect(self.check_results) self.timer.start(100) # 每100毫秒检查一次 def check_results(self): “””在主线程中检查并更新GUI””” if self.results_queue: result = self.results_queue.pop(0) label.setText(result) 2. 使用QMetaObject.invokeMethod from PyQt5.QtCore import QMetaObject, Qt, pyqtSlot class WorkerThread(QThread): result_ready = pyqtSignal(str) def run(self): # 耗时任务 result = “处理完成” # 安全地调用主线程的方法 QMetaObject.invokeMethod( main_window, # 目标对象 “update_label”, # 方法名 Qt.QueuedConnection, # 异步连接 result # 参数 ) class MainWindow: @pyqtSlot(str) def update_label(self, text): label.setText(text) # 在主线程中执行 实际应用场景 场景1:网络请求 class DownloadWorker(QThread): progress = pyqtSignal(int) finished = pyqtSignal(bytes) error = pyqtSignal(str) def run(self): try: # 下载文件(耗时操作) for chunk in download_file(): self.progress.emit(chunk.progress) self.finished.emit(file_data) except Exception as e: self.error.emit(str(e)) # 发送错误信号,而不是直接弹窗 场景2:数据处理 class DataProcessorWorker(QObject): data_processed = pyqtSignal(pd.DataFrame) # 发送处理后的数据 error_occurred = pyqtSignal(str) def process_large_data(self, data): try: # 复杂的数据处理 result = heavy_computation(data) self.data_processed.emit(result) except Exception as e: self.error_occurred.emit(f”处理失败: {e}”) 常见错误模式 错误1:忘记moveToThread worker = Worker() thread = QThread() # ❌ 忘记移动对象到线程 thread.start() worker.do_work() # 还在主线程中执行! 错误2:直接调用GUI方法 def worker_function(): # 各种计算… window.update_ui(data) # ❌ 危险!在子线程中调用GUI方法 错误3:忽略异常处理 def worker_function(): try: # 可能失败的操作 except Exception as e: # ❌ 不要直接显示错误对话框 QMessageBox.critical(None, “错误”, str(e)) # 可能崩溃! # ✅ 应该发送信号 self.error_signal.emit(str(e)) 最佳实践总结 永远不在子线程中直接操作GUI控件 使用信号/槽进行线程间通信 复杂的计算放在工作线程中 GUI更新只在主线程中进行 使用合适的错误处理机制 使用moveToThread()确保对象在正确的线程中 简单记忆法则 记住这句话:“信号发射是自由的,但槽函数执行总是在主线程的怀抱中。” 这样,您就能安全地在PyQt5中使用多线程了!
- # 可能的内存泄漏 def create_widget(): widget = QWidget() # 没有父对象,需要手动管理 return widget # 更好的做法 def create_widget(parent=None): widget = QWidget(parent) # 指定父对象,自动管理内存 return widget
- 所有GUI操作必须在主线程 使用布局管理器,而不是固定坐标 合理使用信号与槽,避免过度耦合 学习使用Qt Designer进行可视化设计
目录
- 1. 引言:为什么选择PyQt5开发桌面应用
- 2. PyQt5与PySide,以及PyQt6简介
- 3. 快速开始:环境搭建
- 使用pip安装(推荐大多数用户)
- 使用uv安装(新一代Python包管理工具)
- 版本说明
- 验证安装
- 4. 第一个PyQt5窗口:Hello World
- 5. 理解PyQt5的核心"积木"
- 5.1 QApplication:应用程序的心脏
- 5.2 QWidget与QMainWindow:窗口的两大家族
- 5.3 信号与槽:Qt的"事件通信系统"
- 5.4 常见控件快速上手
- 6. 让界面自动排列:布局管理
- 为什么需要布局管理器
- 常用布局管理器
- 7. 初学者常见错误与注意事项
- 错误1:忘记创建QApplication实例
- 错误2:在子线程中直接更新GUI
- 错误3:内存泄漏(忘记设置父对象)
- 重要注意事项
- 9. 总结
当你掌握了Python基础语法后,自然会想要创造一些有界面的实用工具。这时候,GUI(图形用户界面)开发就成为了必经之路。在众多Python GUI框架中,PyQt5以其强大的功能和优雅的设计脱颖而出。
PyQt5是Qt框架的Python绑定,Qt本身是一个久经考验的跨平台C++应用程序框架。通过PyQt5,我们可以用Python语言享受到Qt的所有功能,而无需与C++的复杂性打交道。
PyQt5的核心优势
- 功能全面:从简单的窗口到复杂的3D图形,应有尽有
- 真正的跨平台:同一份代码可在Windows、macOS、Linux上运行
- Pythonic的优雅:既有Qt的强大,又有Python的简洁
- 丰富的控件库:按钮、表格、树形视图等上百种控件
- 活跃的社区:遇到问题容易找到解决方案
适合的应用场景
- 数据分析可视化工具
- 日常工作效率工具
- 原型快速开发
- 中小型企业应用
在开始之前,你可能会遇到几个相似的名字,这里简单澄清一下:
PyQt5 vs PySide2:
两者都是Qt的Python绑定,功能几乎完全相同。主要区别在于:
- 许可证:PyQt5使用GPL/commercial,PySide2使用LGPL(对商业应用更友好)
- 历史:PyQt出现较早,PySide是Qt官方后来推出的
- API细微差别:方法名略有不同,如信号连接方式
PyQt6的登场:
PyQt6是PyQt5的下一代版本,主要变化包括:
- 默认使用Qt6库(PyQt5使用Qt5)
- 一些API发生了变化和优化
- 移除了对Python 2的支持
为什么本文选择PyQt5?
对于初学者,PyQt5有更丰富的教程资源、更稳定的环境,而且目前大多数项目仍在使用PyQt5。掌握了PyQt5,迁移到PyQt6或PySide6也会很容易。
pip install PyQt5
uv add pyqt5==5.15.11 pyqt5-qt5==5.15.2
这里限定版本是因为pyqt5的最新版本不适配于Windows uv库,如果是其他设备可能不需要限定版本。
- PyQt5 5.15.x 是PyQt5的最后一个系列版本
- 支持Python 3.5及以上版本
- 建议使用Python 3.8+以获得最佳体验
安装完成后,可以运行以下代码验证:
import sys
from PyQt5.QtWidgets import QApplication, QLabel
app = QApplication(sys.argv)
label = QLabel("Hello PyQt5!")
label.setMinimumSize(400, 50)
label.show()
app.exec_()
运行效果:

(由于电脑分辨率不同,具体的大小效果可能不同)
我运行完代码之后会打印这个信息:
Can't find filter element
Can't find filter element
不知道是为什么,但是好像也不影响GUI应用的展示,我就先不管了……
让我们从一个最简单的完整示例开始:
import sys
from PyQt5.QtWidgets import QApplication, QWidget
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
# 设置窗口位置和大小
self.setGeometry(300, 300, 750, 500)
# 设置窗口标题
self.setWindowTitle('我的第一个PyQt5应用')
# 显示窗口
self.show()
if __name__ == '__main__':
# 创建应用实例
app = QApplication(sys.argv)
# 创建主窗口
window = MainWindow()
# 进入主循环
sys.exit(app.exec_())
运行效果:

代码解析:
import sys:提供对系统相关功能的访问QApplication:每个PyQt5应用都需要一个QApplication实例QWidget:所有用户界面对象的基类self.setGeometry(x, y, width, height):设置窗口位置和大小app.exec_():启动应用的事件循环
QApplication是PyQt5应用的"大脑"和"心脏",负责:
- 管理应用程序的控制流:协调各个窗口和控件的工作
- 处理系统级事件:接收和分发鼠标点击、键盘输入、窗口重绘等事件
- 提供全局设置:管理应用程序的字体、样式、调色板等
重要规则:一个应用只能有一个QApplication实例!
app = QApplication(sys.argv) # sys.argv用于接收命令行参数
事件循环:GUI程序的"心脏跳动"
当我们运行一个PyQt5程序时,到底发生了什么?让我用一个比喻来解释:
想象一下,你的GUI程序就像一个餐厅:
- QApplication是餐厅经理
- 事件循环(event loop)是餐厅的服务流程
- 用户操作(点击、输入)就是顾客的订单
为什么需要事件循环?
# 启动事件循环 app.exec_()
app.exec_() 启动了一个无限循环,这个循环不断地做三件事:
- 监听系统事件(鼠标点击、键盘输入等)
- 将事件分发给对应的控件处理
- 处理完事件后,等待下一个事件
这个循环会一直运行,直到你关闭所有窗口或调用app.quit()。
sys.exit(app.exec_())到底是什么意思?
这是一个初学者常见的困惑点。让我们分解来看:
sys.exit(app.exec_())
分解理解:
app.exec_():
- 启动事件循环
- 返回一个退出状态码(通常是0表示成功,非0表示错误)
- 事件循环会阻塞在这里,直到程序退出
sys.exit():
- Python标准库函数
- 用于退出Python程序
- 可以接收一个退出状态码作为参数
为什么要这样写?
# 不推荐的写法 app.exec_() # 程序会在这里卡住,但退出时可能不会清理资源 # 推荐的写法 sys.exit(app.exec_()) # 确保程序正确退出,并返回状态码
工作原理示意图:
开始
↓
创建 QApplication
↓
创建窗口和控件
↓
sys.exit(app.exec_())
├── 启动事件循环(app.exec_())
│ ├── 监听用户操作
│ ├── 处理事件
│ └── 等待下一个事件…
│
└── 事件循环结束时返回状态码
↓
sys.exit(状态码) 退出程序
完整示例:理解程序执行流程
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton
class MyApp(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle('QApplication示例')
self.setGeometry(300, 300, 300, 200)
btn = QPushButton('退出程序', self)
btn.clicked.connect(self.close) # 点击按钮关闭窗口
btn.move(100, 80)
self.show()
if __name__ == '__main__':
print("1. 程序开始")
app = QApplication(sys.argv)
print("2. QApplication实例已创建")
window = MyApp()
print("3. 窗口已创建并显示")
print("4. 进入事件循环...")
exit_code = app.exec_() # 在这里程序会"阻塞",等待事件
print(f"5. 事件循环结束,退出码: {exit_code}")
sys.exit(exit_code)
运行这个程序,你会看到:
- 控制台先打印1-4步的信息
- 然后程序进入事件循环,GUI界面出现
- 当你关闭窗口时,事件循环结束
- 最后打印第5步的信息,程序退出
常见问题解答
Q: 为什么要用sys.exit()包装app.exec_()?
A: 为了确保程序正确退出,并返回合适的退出状态码给操作系统。
Q: 可以不用sys.exit()吗?
A: 在简单程序中可能可以,但在复杂程序中,不使用sys.exit()可能导致资源未正确释放。
Q: 什么时候事件循环会结束?
A: 当调用app.quit()或关闭所有窗口时,事件循环会结束。
Q: 事件循环期间,我的代码还能运行吗?
A: 不能直接运行。所有代码都必须在事件处理函数中执行。如果需要执行长时间任务,应该使用多线程。
记住这个模式
几乎所有PyQt5程序都遵循这个模式:
import sys
from PyQt5.QtWidgets import QApplication, QWidget
def main():
app = QApplication(sys.argv) # 1. 创建应用
window = QWidget() # 2. 创建窗口
window.show() # 3. 显示窗口
return app.exec_() # 4. 进入事件循环
if __name__ == '__main__':
sys.exit(main()) # 5. 确保正确退出
掌握了QApplication和事件循环的概念,你就理解了PyQt5程序运行的基础机制。这是构建更复杂应用的基石!
QWidget:所有可视化组件的基类
- 按钮、标签、输入框等都是QWidget的子类
- 可以单独作为窗口使用
QMainWindow:带有菜单栏、工具栏、状态栏的主窗口
- 适用于复杂的应用程序
- 提供了标准的应用程序框架
QMainWindow是QWidget的"加强版",但它们的定位和用途有很大区别。
核心关系:继承与扩展
# PyQt5中的继承关系
object
↓
QObject
↓
QWidget ← 所有可视化元素的基类
↓
QMainWindow ← 专门用于主窗口的特殊QWidget
简单来说:
- QWidget是所有可视控件的"爷爷辈"基类
- QMainWindow是QWidget的一个"大儿子",专门为应用程序主窗口设计
- 按钮、标签、输入框等都是QWidget的其他"儿子孙子"
QWidget:万能的基础窗口
QWidget是PyQt5中最基础的窗口类,它可以扮演两种角色:
1. 作为独立的简单窗口
from PyQt5.QtWidgets import QWidget, QLabel, QPushButton, QVBoxLayout, QApplication
import sys
class SimpleWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("我是一个QWidget窗口")
self.resize(300, 200)
# 创建布局和控件
layout = QVBoxLayout()
label = QLabel("这是一个简单的对话框")
button = QPushButton("确定")
layout.addWidget(label)
layout.addWidget(button)
self.setLayout(layout)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = SimpleWindow()
window.show()
sys.exit(app.exec_())
运行效果:

2. 作为其他控件的容器
# QLabel、QPushButton、QLineEdit等都是QWidget的子类
# 它们都继承了QWidget的所有功能
label = QLabel("我是QWidget的子类")
button = QPushButton("我也是QWidget的子类")
QWidget的特点:
- 轻量级,内存占用小
- 灵活,可以自由布局
- 适合对话框、弹窗、简单工具窗口
QMainWindow:专业的应用程序主窗口
QMainWindow是专门为应用程序主窗口设计的,它提供了"开箱即用"的标准主窗口结构:
from PyQt5.QtWidgets import QApplication, QMainWindow, QTextEdit, QAction, QLabel
import sys
class MainAppWindow(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
# 1. 设置中央部件(必需)
text_edit = QTextEdit()
self.setCentralWidget(text_edit)
# 2. 创建菜单栏(可选)
menubar = self.menuBar()
file_menu = menubar.addMenu("文件(&F)")
# 向菜单添加具体的动作
open_action = QAction("打开", self)
save_action = QAction("保存", self)
exit_action = QAction("退出", self)
exit_action.triggered.connect(self.close)
file_menu.addAction(open_action)
file_menu.addAction(save_action)
file_menu.addSeparator()
file_menu.addAction(exit_action)
# 3. 创建工具栏并添加工具按钮(必需,否则工具栏为空)
toolbar = self.addToolBar("标准工具栏")
# 添加工具栏按钮(使用文本)
toolbar.addAction("新建")
toolbar.addAction("打开")
toolbar.addAction("保存")
toolbar.addSeparator()
toolbar.addAction("打印")
# 4. 创建状态栏(可选)
status_bar = self.statusBar()
status_bar.showMessage("就绪", 3000) # 显示3秒
# 添加永久显示的状态栏部件
permanent_label = QLabel("永久状态信息")
status_bar.addPermanentWidget(permanent_label)
# 5. 设置窗口属性
self.setWindowTitle("文本编辑器")
self.setGeometry(100, 100, 800, 600)
self.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainAppWindow()
sys.exit(app.exec_())
运行效果(前3秒会显示就绪):

展开菜单”文件“,过3秒:

QMainWindow的标准结构
+—————————————————+
| 菜单栏 (Menu Bar) |
+—————————————————+
| 工具栏 (Tool Bar) |
+—————————————————+
| |
| 中央部件 (Central Widget) |
| |
| +—————————————–+ |
| | | |
| | 你的主要内容在这里 | |
| | | |
| +—————————————–+ |
| |
+—————————————————+
| 状态栏 (Status Bar) |
+—————————————————+
我们很容易发现QMainWindow里面又有菜单,又有工具栏,那我们很容易就会提问,不对啊,现在的软件不是要么只有菜单(如VSCode),要么只有标签形式切换的工具栏(如Word)吗?
这是一个相当复杂的问题,首先Word老版(1984-2006)其实真的是又有菜单栏又有工具栏的:
┌─────────────────────────────────────┐
│ 文件(F) 编辑(E) 视图(V) 帮助(H) ← 固定菜单栏
├─────────────────────────────────────┤
│ [📄] [📂] [💾] [🖨️] [B] [I] [U] ← 浮动工具栏
└─────────────────────────────────────┘
特点:
- 菜单栏是固定的、层级式的
- 工具栏是独立的、可拖动的
- 功能隐藏在多层菜单中
- 空间利用率较低
但2007年后改为了采用这样的Ribbon界面:
┌─────────────────────────────────────┐
│ 🏠 插入 设计 布局 引用 邮件 审阅 视图 ← 情境化标签页
├─────────────────────────────────────┤
│ 当前任务相关功能分组展示 ← 自适应功能区
│ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ │ 剪贴板 │ │ 字体 │ │ 段落 │
│ │ │ │ │ │ │
│ └─────────┘ └─────────┘ └─────────┘
└─────────────────────────────────────┘
核心创新:
- 情境化标签页:根据当前任务切换功能组
- 功能分组可视化:图标+文字,直观易懂
- 自适应空间:根据窗口大小调整显示
- 减少菜单层级:80%常用功能在第一层
而VSCode则采用的是侧边活动栏:
# VS Code的界面结构
┌─────────────────────────────────────────────────────┐
│ File Edit View Go Run Terminal Help │ ← 顶部菜单栏(简洁)
├─────────────────────────────────────────────────────┤
│ 🏠 🔍 💾 🐙 ⏹️ │ ← 活动栏(侧边图标栏)
│ │
│ 侧边面板区域 │
│ (资源管理器、搜索、Git等) │
│ │
├─────────────────────────────────────────────────────┤
│ [main.py] │ ← 编辑器标签页
│ │
│ def main(): │ ← 主编辑区域
│ print("Hello World") │
│ │
├─────────────────────────────────────────────────────┤
│ Python 3.12.4 • UTF-8 • LF • 2 spaces │ ← 状态栏(多信息显示)
└─────────────────────────────────────────────────────┘
关键区别对比表
| 特性 | QWidget | QMainWindow |
|---|---|---|
| 定位 | 基础窗口/控件 | 应用程序主窗口 |
| 内存占用 | 较小 | 较大(包含更多组件) |
| 内置结构 | 无 | 有标准菜单栏、工具栏、状态栏区域 |
| 布局管理 | 需要手动设置布局 | 中央部件区域可设置布局 |
| 灵活性 | 高,完全自定义 | 有一定结构限制 |
| 典型用途 | 对话框、弹窗、简单窗口 | 软件主窗口、复杂应用 |
| 是否能使用布局管理器 | 可以,直接设置 | 只能对中央部件使用布局 |
| 是否能有多个实例 | 可以 | 通常只有一个 |
关键注意事项
1. QMainWindow不能直接设置布局!
(布局的介绍见本文第6节)
# ❌ 错误做法
class WrongWindow(QMainWindow):
def __init__(self):
super().__init__()
layout = QVBoxLayout()
layout.addWidget(QLabel("测试"))
self.setLayout(layout) # 这不会工作!
# ✅ 正确做法
class CorrectWindow(QMainWindow):
def __init__(self):
super().__init__()
# 创建一个QWidget作为中央部件的容器
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 在容器上设置布局
layout = QVBoxLayout()
layout.addWidget(QLabel("测试"))
central_widget.setLayout(layout) # 在中央部件上设置布局
2. 实际应用中的选择策略
# 场景1:需要标准菜单栏/工具栏的软件 → 选择QMainWindow
class TextEditor(QMainWindow):
"""文本编辑器,需要菜单栏保存文件"""
pass
# 场景2:简单的配置对话框 → 选择QWidget
class SettingsDialog(QWidget):
"""设置对话框,不需要复杂的菜单结构"""
pass
# 场景3:复杂应用的子窗口 → 选择QWidget
class PreviewWindow(QWidget):
"""预览窗口,作为主窗口的子窗口"""
pass
3. 混合使用示例
from PyQt5.QtWidgets import QApplication, QMainWindow, QTextEdit, QWidget
import sys
class MainApp(QMainWindow):
def __init__(self):
super().__init__()
# 主窗口使用QMainWindow
self.setWindowTitle("主应用程序")
self.setCentralWidget(QTextEdit())
# 但点击按钮可以弹出QWidget对话框
self.settings_dialog = SettingsDialog(self)
def show_settings(self):
self.settings_dialog.exec_() # 显示模态对话框
class SettingsDialog(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("设置")
self.resize(300, 200)
# 使用QWidget作为对话框
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainApp()
window.show()
sys.exit(app.exec_())
生成效果(我手动拖拽了一下界面大小):

实践建议
什么时候用QWidget?
- 对话框和弹窗:确认框、消息框、设置窗口
- 简单工具:计算器、单位转换器等小工具
- 自定义控件:创建可重用的界面组件
- 子窗口:主应用中的浮动面板
什么时候用QMainWindow?
- 应用程序主窗口:编辑器、浏览器、IDE等
- 复杂应用:需要标准菜单和工具栏的软件
- 专业工具:图像处理、数据分析等专业软件
一个有用的技巧:从简单开始
# 初期:从QWidget开始,快速原型
class SimpleApp(QWidget):
def __init__(self):
super().__init__()
# 简单布局和功能
# 后期:需要更多功能时,轻松迁移到QMainWindow
class EnhancedApp(QMainWindow):
def __init__(self):
super().__init__()
# 将原来的QWidget内容设为中央部件
old_widget = SimpleApp()
self.setCentralWidget(old_widget)
# 添加菜单栏、工具栏等
总结
QWidget和QMainWindow不是"基础版"和"高级版"的关系,而是不同用途的工具:
- QWidget像是白纸:给你最大自由度,想画什么就画什么
- QMainWindow像是已经画好框架的画布:提供了标准结构,你只需要填充内容
记住这个简单的选择原则:
- 如果只需要一个简单的窗口或对话框 → 选QWidget
- 如果要创建有标准菜单/工具栏的应用程序主窗口 → 选QMainWindow
理解它们的区别后,你就能根据实际需求做出合适的选择,写出更专业、更高效的PyQt5代码!
这是Qt最强大的特性之一!想象一下:
- 信号(Signal):像电灯的开关
- 槽(Slot):像电灯本身
- 连接(Connect):像连接开关和灯的电线
工作原理:事件发生 → 发出信号 → 连接到槽 → 执行函数
import sys
from PyQt5.QtWidgets import (
QApplication,
QWidget,
QVBoxLayout,
QLabel,
QPushButton,
QTextEdit,
)
from PyQt5.QtCore import pyqtSignal
class CustomButton(QPushButton):
"""
自定义按钮类
演示如何创建和使用自定义信号
"""
# 自定义信号 - 可以发送一个字符串
message_signal = pyqtSignal(str)
# 另一个自定义信号 - 可以发送两个整数
number_signal = pyqtSignal(int, int)
def __init__(self, text, parent=None):
super().__init__(text, parent)
self.click_count = 0
def mousePressEvent(self, event):
"""
重写鼠标按下事件
每次点击都会发出两个信号
"""
self.click_count += 1
# 发出第一个信号 - 带字符串消息
self.message_signal.emit(f"第{self.click_count}次点击!")
# 发出第二个信号 - 带两个数字
x, y = event.x(), event.y()
self.number_signal.emit(x, y)
# 重要:调用父类方法确保正常行为
super().mousePressEvent(event)
class ExampleApp(QWidget):
def __init__(self):
super().__init__()
self.setup_ui()
def setup_ui(self):
self.setWindowTitle("自定义信号详细示例")
self.resize(500, 400)
# 创建布局
layout = QVBoxLayout()
# 1. 信息显示区域
self.info_label = QLabel("点击下面的按钮查看效果", self)
layout.addWidget(self.info_label)
# 2. 文本显示区域(用于显示详细信息)
self.text_display = QTextEdit(self)
self.text_display.setReadOnly(True)
layout.addWidget(self.text_display)
# 3. 创建自定义按钮
self.custom_btn = CustomButton("自定义按钮 - 点击我", self)
layout.addWidget(self.custom_btn)
# 4. 重置按钮
self.reset_btn = QPushButton("重置计数", self)
layout.addWidget(self.reset_btn)
self.setLayout(layout)
# 连接信号
self.connect_signals()
def connect_signals(self):
"""连接所有信号到槽函数"""
# 连接自定义按钮的第一个信号
self.custom_btn.message_signal.connect(self.update_info)
# 连接自定义按钮的第二个信号
self.custom_btn.number_signal.connect(self.show_click_position)
# 连接重置按钮
self.reset_btn.clicked.connect(self.reset_counter)
def update_info(self, message):
"""更新信息标签"""
self.info_label.setText(f"自定义信号: {message}")
self.text_display.append(f"收到消息: {message}")
def show_click_position(self, x, y):
"""显示点击位置"""
self.text_display.append(f"点击位置: x={x}, y={y}")
def reset_counter(self):
"""重置计数器"""
self.custom_btn.click_count = 0
self.info_label.setText("计数器已重置")
self.text_display.append("--- 计数器重置 ---")
# 运行示例
if __name__ == "__main__":
app = QApplication(sys.argv)
window = ExampleApp()
window.show()
sys.exit(app.exec_())
首先设置带信号(代码里设置了两种)的按钮,然后将按钮放到组件上,把信号连接到槽函数上(槽函数也可以是一个lambda函数)。按钮被触发时,信号发射(emit),槽函数接受并处理信号,在代码中就将处理结果展示在界面上:


from PyQt5.QtWidgets import (
QLabel, # 标签 - 显示文本或图片
QPushButton, # 按钮 - 点击触发动作
QLineEdit, # 单行输入框
QTextEdit, # 多行文本编辑
QCheckBox, # 复选框
QRadioButton, # 单选按钮
QComboBox, # 下拉框
QSpinBox, # 数字输入框
QProgressBar, # 进度条
QSlider, # 滑块
)
没有布局管理器的GUI就像没有CSS的HTML——元素会堆叠在一起。
布局管理器其实就是把一堆空间按布局组合到一起。
- 自动调整控件位置和大小
- 适应不同分辨率和窗口大小
- 简化界面设计
QVBoxLayout – 垂直排列
from PyQt5.QtWidgets import QVBoxLayout, QPushButton, QLabel
layout = QVBoxLayout()
layout.addWidget(QLabel("第一行"))
layout.addWidget(QPushButton("第二行"))
layout.addWidget(QLabel("第三行"))
self.setLayout(layout) # 应用到窗口
QHBoxLayout – 水平排列
from PyQt5.QtWidgets import QHBoxLayout
layout = QHBoxLayout()
layout.addWidget(QPushButton("左"))
layout.addWidget(QPushButton("中"))
layout.addWidget(QPushButton("右"))
布局嵌套 – 创建复杂界面
# 创建主垂直布局
main_layout = QVBoxLayout()
# 创建水平布局并添加控件
top_layout = QHBoxLayout()
top_layout.addWidget(QLabel("姓名:"))
top_layout.addWidget(QLineEdit())
# 将水平布局添加到垂直布局
main_layout.addLayout(top_layout)
main_layout.addWidget(QPushButton("提交"))
self.setLayout(main_layout)
# 错误写法
window = QWidget()
window.show()
# 正确写法
app = QApplication(sys.argv)
window = QWidget()
window.show()
app.exec_()
在PyQt5(以及大多数GUI框架)中,有一个黄金规则:所有GUI操作都必须在主线程(也称为GUI线程)中进行!
为什么有这个限制?
简化的解释:
PyQt5的GUI组件不是"线程安全"的
想象一下两个线程同时修改同一个控件:
线程A: label.setText("Hello") 线程B: label.setText("World")
↓ ↓
同时写入同一个内存区域 → 数据竞争 → 程序崩溃!
错误示例分析
错误代码:
import sys
import time
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QPushButton
from PyQt5.QtCore import QThread, pyqtSignal
class WorkerThread(QThread):
"""工作线程"""
def run(self):
time.sleep(2) # 模拟耗时操作
# ❌ 危险!在子线程中直接更新GUI
label.setText("处理完成!") # 可能导致崩溃
# 在主线程中创建窗口
app = QApplication(sys.argv)
window = QWidget()
label = QLabel("等待中...")
button = QPushButton("开始任务")
layout = QVBoxLayout()
layout.addWidget(label)
layout.addWidget(button)
window.setLayout(layout)
# 创建工作线程
worker = WorkerThread()
def start_task():
worker.start()
button.clicked.connect(start_task)
window.show()
sys.exit(app.exec_())
可能的结果:
- 程序可能直接崩溃
- 界面可能不更新
- 可能偶尔正常工作,但不可靠(最危险的情况!)
正确解决方案:使用信号
方案1:QThread + moveToThread
import sys
import time
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QPushButton
from PyQt5.QtCore import QThread, pyqtSignal, QObject
class Worker(QObject):
"""工作对象,使用信号通信"""
# 定义信号
progress_signal = pyqtSignal(int) # 传递进度百分比
result_signal = pyqtSignal(str) # 传递结果字符串
finished_signal = pyqtSignal() # 完成信号(无参数)
def do_work(self):
"""执行耗时任务"""
for i in range(1, 11):
time.sleep(0.5) # 模拟耗时操作
self.progress_signal.emit(i * 10) # 发射进度信号
self.result_signal.emit("处理完成!") # 发射结果信号
self.finished_signal.emit() # 发射完成信号
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
self.setup_worker()
def init_ui(self):
self.setWindowTitle("线程安全更新GUI示例")
self.resize(400, 300)
layout = QVBoxLayout()
self.label = QLabel("准备开始任务...")
self.progress_label = QLabel("进度: 0%")
self.button = QPushButton("开始任务")
self.status_label = QLabel("状态: 空闲")
layout.addWidget(self.label)
layout.addWidget(self.progress_label)
layout.addWidget(self.status_label)
layout.addWidget(self.button)
self.setLayout(layout)
self.button.clicked.connect(self.start_work)
def setup_worker(self):
"""设置工作线程和信号连接"""
# 创建工作对象和线程
self.worker = Worker()
self.thread = QThread()
# 将工作对象移动到线程中
self.worker.moveToThread(self.thread)
# 连接信号
self.worker.progress_signal.connect(self.update_progress)
self.worker.result_signal.connect(self.update_result)
self.worker.finished_signal.connect(self.work_finished)
# 线程开始后,连接do_work方法
self.thread.started.connect(self.worker.do_work)
# 线程结束时,清理资源
self.worker.finished_signal.connect(self.thread.quit)
self.worker.finished_signal.connect(self.worker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
def start_work(self):
"""开始工作"""
self.button.setEnabled(False)
self.status_label.setText("状态: 处理中...")
self.thread.start()
def update_progress(self, progress):
"""更新进度(在主线程中执行)"""
self.progress_label.setText(f"进度: {progress}%")
def update_result(self, result):
"""更新结果(在主线程中执行)"""
self.label.setText(result)
def work_finished(self):
"""任务完成(在主线程中执行)"""
self.button.setEnabled(True)
self.status_label.setText("状态: 完成")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
方案2:QRunnable
需要管理生命周期:
import sys
import time
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QPushButton
from PyQt5.QtCore import QThreadPool, QRunnable, pyqtSignal, QObject, pyqtSlot, QMutex
class WorkerSignals(QObject):
"""定义工作线程的信号"""
finished = pyqtSignal()
progress = pyqtSignal(int)
result = pyqtSignal(str)
def __init__(self):
super().__init__()
self.is_valid = True # 添加有效性标志
def delete_later(self):
self.is_valid = False
self.deleteLater()
class Worker(QRunnable):
"""工作线程类"""
def __init__(self):
super().__init__()
self.signals = WorkerSignals()
self.mutex = QMutex() # 互斥锁保护信号对象
def run(self):
"""执行耗时任务"""
for i in range(10):
time.sleep(0.3)
progress = (i + 1) * 10
# 使用互斥锁保护信号对象
self.mutex.lock()
try:
if (
self.signals
and hasattr(self.signals, "is_valid")
and self.signals.is_valid
):
self.signals.progress.emit(progress)
except RuntimeError:
# 信号对象已被删除
pass
finally:
self.mutex.unlock()
self.mutex.lock()
try:
if (
self.signals
and hasattr(self.signals, "is_valid")
and self.signals.is_valid
):
self.signals.result.emit("任务完成!")
self.signals.finished.emit()
except RuntimeError:
pass
finally:
self.mutex.unlock()
class SimpleThreadExample(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
self.threadpool = QThreadPool()
self.workers = [] # 保持对worker的引用
def init_ui(self):
self.setWindowTitle("简单多线程示例")
self.resize(300, 200)
layout = QVBoxLayout()
self.label = QLabel("点击按钮开始任务")
self.button = QPushButton("开始耗时任务")
layout.addWidget(self.label)
layout.addWidget(self.button)
self.setLayout(layout)
self.button.clicked.connect(self.start_task)
def start_task(self):
"""启动工作线程"""
# 禁用按钮,防止重复点击
self.button.setEnabled(False)
# 创建工作对象
worker = Worker()
self.workers.append(worker) # 保持引用
# 连接信号
worker.signals.progress.connect(self.on_progress)
worker.signals.result.connect(self.on_result)
worker.signals.finished.connect(lambda: self.on_finished(worker))
# 在线程池中执行
self.threadpool.start(worker)
def on_progress(self, progress):
"""更新进度(自动在主线程中执行)"""
self.label.setText(f"处理中... {progress}%")
def on_result(self, result):
"""显示结果(自动在主线程中执行)"""
self.label.setText(result)
def on_finished(self, worker):
"""任务完成(自动在主线程中执行)"""
self.button.setEnabled(True)
# 清理worker
if worker in self.workers:
if worker.signals:
worker.signals.delete_later()
self.workers.remove(worker)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = SimpleThreadExample()
window.show()
sys.exit(app.exec_())
为什么信号能安全地更新GUI?
信号与槽的线程安全机制:
PyQt5的内部机制:
当信号从子线程发射时,PyQt5会自动:
- 将信号放入主线程的事件队列
- 主线程在适当的时候处理这个事件
- 调用连接的槽函数(在主线程中!)
所以:
worker.signals.result.emit("数据") # 子线程中发射信号
↓
PyQt5内部:跨线程传递信号
↓
label.setText("数据") # 在主线程中执行槽函数
其他安全更新GUI的方法
1. 使用QTimer在主线程中轮询
from PyQt5.QtCore import QTimer
class SafeUpdateExample:
def __init__(self):
self.results_queue = [] # 线程安全的数据结构
# 定时器在主线程中运行
self.timer = QTimer()
self.timer.timeout.connect(self.check_results)
self.timer.start(100) # 每100毫秒检查一次
def check_results(self):
"""在主线程中检查并更新GUI"""
if self.results_queue:
result = self.results_queue.pop(0)
label.setText(result)
2. 使用QMetaObject.invokeMethod
from PyQt5.QtCore import QMetaObject, Qt, pyqtSlot
class WorkerThread(QThread):
result_ready = pyqtSignal(str)
def run(self):
# 耗时任务
result = "处理完成"
# 安全地调用主线程的方法
QMetaObject.invokeMethod(
main_window, # 目标对象
"update_label", # 方法名
Qt.QueuedConnection, # 异步连接
result # 参数
)
class MainWindow:
@pyqtSlot(str)
def update_label(self, text):
label.setText(text) # 在主线程中执行
实际应用场景
场景1:网络请求
class DownloadWorker(QThread):
progress = pyqtSignal(int)
finished = pyqtSignal(bytes)
error = pyqtSignal(str)
def run(self):
try:
# 下载文件(耗时操作)
for chunk in download_file():
self.progress.emit(chunk.progress)
self.finished.emit(file_data)
except Exception as e:
self.error.emit(str(e)) # 发送错误信号,而不是直接弹窗
场景2:数据处理
class DataProcessorWorker(QObject):
data_processed = pyqtSignal(pd.DataFrame) # 发送处理后的数据
error_occurred = pyqtSignal(str)
def process_large_data(self, data):
try:
# 复杂的数据处理
result = heavy_computation(data)
self.data_processed.emit(result)
except Exception as e:
self.error_occurred.emit(f"处理失败: {e}")
常见错误模式
错误1:忘记moveToThread
worker = Worker() thread = QThread() # ❌ 忘记移动对象到线程 thread.start() worker.do_work() # 还在主线程中执行!
错误2:直接调用GUI方法
def worker_function():
# 各种计算...
window.update_ui(data) # ❌ 危险!在子线程中调用GUI方法
错误3:忽略异常处理
def worker_function():
try:
# 可能失败的操作
except Exception as e:
# ❌ 不要直接显示错误对话框
QMessageBox.critical(None, "错误", str(e)) # 可能崩溃!
# ✅ 应该发送信号
self.error_signal.emit(str(e))
最佳实践总结
- 永远不在子线程中直接操作GUI控件
- 使用信号/槽进行线程间通信
- 复杂的计算放在工作线程中
- GUI更新只在主线程中进行
- 使用合适的错误处理机制
- 使用
moveToThread()确保对象在正确的线程中
简单记忆法则
记住这句话:“信号发射是自由的,但槽函数执行总是在主线程的怀抱中。”
这样,您就能安全地在PyQt5中使用多线程了!
# 可能的内存泄漏
def create_widget():
widget = QWidget() # 没有父对象,需要手动管理
return widget
# 更好的做法
def create_widget(parent=None):
widget = QWidget(parent) # 指定父对象,自动管理内存
return widget
- 所有GUI操作必须在主线程
- 使用布局管理器,而不是固定坐标
- 合理使用信号与槽,避免过度耦合
- 学习使用Qt Designer进行可视化设计
通过本文,你已经掌握了PyQt5的基础知识:
- 理解了PyQt5的基本架构
- 学会了创建窗口和基本控件
- 掌握了信号与槽机制
- 能够使用布局管理器排列控件
- 创建了第一个交互式应用
PyQt5的学习曲线可能有些陡峭,但一旦掌握,你将拥有创建强大桌面应用的能力。记住,最好的学习方式就是动手实践。从今天开始,尝试用PyQt5解决你遇到的实际问题吧!
到此这篇关于Python使用PyQt5打造桌面应用的入门指南的文章就介绍到这了,更多相关Python PyQt5桌面应用开发内容请搜索风君子博客以前的文章或继续浏览下面的相关文章希望大家以后多多支持风君子博客!
您可能感兴趣的文章:
- Python结合PyQt5编写一个批量目录重命名工具并打包成EXE
- 使用Python和PyQt5打造一个JSON快速查看器
- 使用Python和PyQt5开发一个多功能音乐播放器
- Python结合PyQt5模拟实现微信程序聊天功能
- 基于Python+PyQt5开发一个AI智能截图解答工具
- Python+pyqt5实现一个图像变形工具
- Python+PyQt5打造一个代码行数统计工具