找到
16
篇与
pyqt5
相关的结果
-
PyQt5阶段三实战项目:多线程文件下载工具(仿迅雷核心功能) 第15篇:阶段三实战项目:仿照下载器实现多线程文件下载工具(完整代码) 哈喽~ 欢迎来到PyQt5系列的第15篇,也是阶段三的收官实战项目!经过前几章的学习,我们已经掌握了信号与槽、自定义信号、事件处理和多线程编程的核心知识。今天我们将这些知识点整合起来,开发一个仿照主流下载器的多线程文件下载工具,实现多任务下载、进度实时显示、暂停/继续/取消下载、下载速度计算等核心功能,彻底解决单线程下载卡顿、效率低的问题! mke2owv3.png图片 一、项目需求分析:对标主流下载器核心功能 我们聚焦下载器的核心实用功能,本次项目实现以下需求: 基础功能:支持输入下载链接、自定义保存路径、启动下载、暂停/继续下载、取消下载; 多任务管理:支持同时添加多个下载任务,每个任务独立运行,互不干扰; 进度展示:实时显示每个任务的下载进度、已下载大小、总大小、下载速度; 交互体验:任务列表清晰展示所有任务状态(等待中/下载中/已暂停/已完成/下载失败),操作按钮状态随任务状态动态切换; 线程安全:严格遵循“子线程不操作UI”原则,通过自定义信号传递数据,避免界面卡顿和程序崩溃; 异常处理:捕获网络异常、文件读写异常、链接无效等问题,给出明确的错误提示。 二、技术选型:整合阶段三核心知识点 本次项目严格基于阶段三所学内容,技术栈如下: 技术点应用场景QThread多线程每个下载任务对应一个子线程,避免阻塞主线程自定义信号子线程向主线程传递下载进度、速度、状态等数据信号与槽绑定按钮点击触发下载控制,子线程信号触发UI更新事件处理窗口关闭时安全停止所有下载线程,释放资源QTableWidget展示多下载任务的详细信息(链接、进度、状态等)QFileDialog选择文件保存路径,提升用户体验网络请求(requests库)分块下载文件,支持断点续传基础逻辑三、项目架构设计:模块化分工 为了让代码结构清晰、易于维护,我们采用模块化设计,将项目分为3个核心部分: 下载线程类(DownloadThread):继承QThread,负责单个任务的下载逻辑,通过自定义信号传递进度和状态; 任务管理类(TaskManager):管理所有下载任务,实现任务的添加、删除、暂停/继续等统一控制; 主窗口类(DownloaderWindow):负责界面布局、用户交互、接收子线程信号并更新UI。 核心架构逻辑:用户在主窗口添加下载任务 → 任务管理类创建对应的下载线程 → 子线程执行下载并发射信号 → 主窗口接收信号更新任务列表 → 用户通过按钮操作任务状态。 四、完整代码实现(可直接运行) 注意:需提前安装requests库(用于网络请求),执行命令:pip install requestsimport sys import time import requests from datetime import datetime from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, QLineEdit, QPushButton, QFileDialog, QMessageBox, QHeaderView ) from PyQt5.QtCore import QThread, pyqtSignal, Qt, QTimer from PyQt5.QtGui import QColor, QFont # -------------------------- 1. 下载线程类:单个任务的下载逻辑 -------------------------- class DownloadThread(QThread): # 自定义信号:传递下载进度、速度、状态等数据 progress_signal = pyqtSignal(int, str, str) # 进度(0-100)、已下载大小、速度 status_signal = pyqtSignal(str) # 任务状态(下载中/已暂停/已完成/失败) finish_signal = pyqtSignal() # 任务完成信号 def __init__(self, url, save_path): super().__init__() self.url = url # 下载链接 self.save_path = save_path # 保存路径 self.is_paused = False # 暂停标志位 self.is_canceled = False # 取消标志位 self.chunk_size = 1024 * 1024 # 分块大小:1MB/块 self.downloaded_size = 0 # 已下载大小(字节) self.total_size = 0 # 文件总大小(字节) def run(self): """子线程核心执行方法:文件下载逻辑""" try: # 发送请求,获取文件大小(支持断点续传的基础) headers = {} if self.downloaded_size > 0: headers["Range"] = f"bytes={self.downloaded_size}-" # 断点续传:从已下载位置开始 response = requests.get(self.url, headers=headers, stream=True, timeout=15) self.total_size = int(response.headers.get("content-length", 0)) + self.downloaded_size # 打开文件,追加模式写入(支持断点续传) with open(self.save_path, "ab") as f: self.status_signal.emit("下载中") start_time = time.time() # 计时:用于计算下载速度 for chunk in response.iter_content(chunk_size=self.chunk_size): # 处理暂停:暂停时阻塞循环,直到取消暂停 while self.is_paused: time.sleep(0.1) if self.is_canceled: self.status_signal.emit("已取消") self.finish_signal.emit() return # 处理取消:直接终止下载 if self.is_canceled: self.status_signal.emit("已取消") self.finish_signal.emit() return # 写入分块数据 f.write(chunk) self.downloaded_size += len(chunk) # 计算进度、已下载大小、速度 progress = int((self.downloaded_size / self.total_size) * 100) if self.total_size > 0 else 0 downloaded_str = self.format_size(self.downloaded_size) total_str = self.format_size(self.total_size) speed_str = self.calculate_speed(self.downloaded_size, start_time) # 发射进度信号(每块发射一次,避免UI刷新太频繁) self.progress_signal.emit(progress, f"{downloaded_str}/{total_str}", speed_str) # 下载完成判断 if self.downloaded_size >= self.total_size and not self.is_canceled: self.status_signal.emit("已完成") elif self.is_canceled: self.status_signal.emit("已取消") else: self.status_signal.emit("已暂停") except requests.exceptions.RequestException as e: self.status_signal.emit(f"失败:{str(e)}") except Exception as e: self.status_signal.emit(f"失败:{str(e)}") finally: self.finish_signal.emit() def pause(self): """暂停下载""" self.is_paused = not self.is_paused status = "已暂停" if self.is_paused else "下载中" self.status_signal.emit(status) def cancel(self): """取消下载""" self.is_canceled = True self.is_paused = False # 取消暂停,让循环退出 def format_size(self, size): """字节大小格式化:转换为KB/MB/GB""" units = ["B", "KB", "MB", "GB"] index = 0 while size >= 1024 and index < len(units) - 1: size /= 1024 index += 1 return f"{size:.2f} {units[index]}" def calculate_speed(self, downloaded_size, start_time): """计算下载速度:单位 KB/s 或 MB/s""" elapsed_time = time.time() - start_time if elapsed_time <= 0: return "0 B/s" speed = downloaded_size / elapsed_time return self.format_size(speed) + "/s" # -------------------------- 2. 主窗口类:界面与交互逻辑 -------------------------- class DownloaderWindow(QMainWindow): def __init__(self): super().__init__() self.init_ui() self.download_tasks = {} # 存储下载任务:{任务索引: (线程实例, 链接, 保存路径)} self.current_task_index = 0 # 任务索引计数器 def init_ui(self): """初始化界面布局""" self.setWindowTitle("多线程文件下载工具") self.resize(900, 600) self.setMinimumSize(800, 500) # 中心控件 central_widget = QWidget() self.setCentralWidget(central_widget) # 主布局:垂直布局 main_layout = QVBoxLayout(central_widget) main_layout.setSpacing(15) main_layout.setContentsMargins(20, 20, 20, 20) # ---------- 顶部:下载任务添加区 ---------- add_task_layout = QHBoxLayout() add_task_layout.setSpacing(10) self.url_edit = QLineEdit() self.url_edit.setPlaceholderText("请输入下载链接(如:https://xxx.xxx/file.zip)") self.url_edit.setStyleSheet("padding: 8px; font-size: 14px;") self.path_edit = QLineEdit() self.path_edit.setPlaceholderText("请选择保存路径") self.path_edit.setStyleSheet("padding: 8px; font-size: 14px;") self.browse_btn = QPushButton("浏览") self.add_btn = QPushButton("添加任务") self.start_all_btn = QPushButton("开始所有任务") for btn in [self.browse_btn, self.add_btn, self.start_all_btn]: btn.setFixedSize(80, 35) btn.setStyleSheet("font-size: 14px;") add_task_layout.addWidget(self.url_edit) add_task_layout.addWidget(self.path_edit) add_task_layout.addWidget(self.browse_btn) add_task_layout.addWidget(self.add_btn) add_task_layout.addWidget(self.start_all_btn) # ---------- 中部:下载任务列表(QTableWidget) ---------- self.task_table = QTableWidget() # 列标题:索引、链接、保存路径、进度、大小、速度、状态、操作 self.task_table.setColumnCount(8) self.task_table.setHorizontalHeaderLabels([ "任务ID", "下载链接", "保存路径", "进度", "大小", "速度", "状态", "操作" ]) # 表格样式优化 self.task_table.horizontalHeader().setStretchLastSection(True) self.task_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.task_table.verticalHeader().setVisible(False) self.task_table.setEditTriggers(QTableWidget.NoEditTriggers) # 禁止编辑单元格 # ---------- 底部:状态栏 ---------- self.status_label = QLabel("就绪:可添加下载任务") self.status_label.setStyleSheet("font-size: 12px; color: #666;") # ---------- 添加所有控件到主布局 ---------- main_layout.addLayout(add_task_layout) main_layout.addWidget(self.task_table) main_layout.addWidget(self.status_label) # ---------- 绑定信号与槽 ---------- self.browse_btn.clicked.connect(self.choose_save_path) self.add_btn.clicked.connect(self.add_download_task) self.start_all_btn.clicked.connect(self.start_all_tasks) def choose_save_path(self): """选择文件保存路径""" file_path, _ = QFileDialog.getSaveFileName( self, "选择保存路径", "", "All Files (*.*)" ) if file_path: self.path_edit.setText(file_path) def add_download_task(self): """添加下载任务到列表""" url = self.url_edit.text().strip() save_path = self.path_edit.text().strip() # 输入验证 if not url: QMessageBox.warning(self, "提示", "请输入下载链接!") return if not save_path: QMessageBox.warning(self, "提示", "请选择保存路径!") return # 添加任务到表格 row = self.task_table.rowCount() self.task_table.insertRow(row) # 填充任务基本信息 self.task_table.setItem(row, 0, QTableWidgetItem(str(self.current_task_index))) self.task_table.setItem(row, 1, QTableWidgetItem(url)) self.task_table.setItem(row, 2, QTableWidgetItem(save_path)) self.task_table.setItem(row, 3, QTableWidgetItem("0%")) self.task_table.setItem(row, 4, QTableWidgetItem("0 B/未知")) self.task_table.setItem(row, 5, QTableWidgetItem("0 B/s")) self.task_table.setItem(row, 6, QTableWidgetItem("等待中")) # 添加操作按钮(暂停/继续/取消) btn_layout = QHBoxLayout() start_btn = QPushButton("开始") pause_btn = QPushButton("暂停") cancel_btn = QPushButton("取消") for btn in [start_btn, pause_btn, cancel_btn]: btn.setFixedSize(60, 30) btn_layout.addWidget(btn) btn_widget = QWidget() btn_widget.setLayout(btn_layout) self.task_table.setCellWidget(row, 7, btn_widget) # 绑定按钮信号 start_btn.clicked.connect(lambda: self.start_single_task(row)) pause_btn.clicked.connect(lambda: self.pause_single_task(row)) cancel_btn.clicked.connect(lambda: self.cancel_single_task(row)) # 存储任务信息 self.download_tasks[row] = { "thread": None, "url": url, "save_path": save_path, "start_btn": start_btn, "pause_btn": pause_btn, "cancel_btn": cancel_btn } # 更新状态 self.status_label.setText(f"已添加任务 {self.current_task_index}:{url}") self.current_task_index += 1 # 清空输入框 self.url_edit.clear() self.path_edit.clear() def start_single_task(self, row): """启动单个下载任务""" task = self.download_tasks.get(row) if not task: return # 创建下载线程 if task["thread"] is None: task["thread"] = DownloadThread(task["url"], task["save_path"]) # 绑定线程信号 task["thread"].progress_signal.connect(lambda p, s, sp: self.update_task_progress(row, p, s, sp)) task["thread"].status_signal.connect(lambda st: self.update_task_status(row, st)) task["thread"].finish_signal.connect(lambda: self.on_task_finished(row)) # 启动线程 task["thread"].start() task["start_btn"].setEnabled(False) task["pause_btn"].setEnabled(True) task["cancel_btn"].setEnabled(True) self.status_label.setText(f"任务 {row} 开始下载...") def pause_single_task(self, row): """暂停/继续单个任务""" task = self.download_tasks.get(row) if task and task["thread"] and task["thread"].isRunning(): task["thread"].pause() def cancel_single_task(self, row): """取消单个任务""" task = self.download_tasks.get(row) if task and task["thread"]: task["thread"].cancel() task["start_btn"].setEnabled(False) task["pause_btn"].setEnabled(False) task["cancel_btn"].setEnabled(False) def start_all_tasks(self): """启动所有等待中的任务""" for row in range(self.task_table.rowCount()): status = self.task_table.item(row, 6).text() if status == "等待中": self.start_single_task(row) def update_task_progress(self, row, progress, size_str, speed_str): """更新单个任务的进度、大小、速度""" self.task_table.setItem(row, 3, QTableWidgetItem(f"{progress}%")) self.task_table.setItem(row, 4, QTableWidgetItem(size_str)) self.task_table.setItem(row, 5, QTableWidgetItem(speed_str)) # 进度条样式(可选:设置单元格背景色) progress_item = self.task_table.item(row, 3) if progress < 50: progress_item.setBackground(QColor(255, 240, 240)) else: progress_item.setBackground(QColor(240, 255, 240)) def update_task_status(self, row, status): """更新单个任务的状态""" self.task_table.setItem(row, 6, QTableWidgetItem(status)) status_item = self.task_table.item(row, 6) if status == "下载中": status_item.setForeground(QColor(34, 139, 34)) elif status == "已暂停": status_item.setForeground(QColor(255, 165, 0)) elif status == "已完成": status_item.setForeground(QColor(0, 128, 0)) elif status == "已取消": status_item.setForeground(QColor(128, 128, 128)) else: status_item.setForeground(QColor(220, 20, 60)) def on_task_finished(self, row): """任务完成后的清理工作""" task = self.download_tasks.get(row) if task: task["start_btn"].setEnabled(False) task["pause_btn"].setEnabled(False) self.status_label.setText(f"任务 {row} 已完成!") def closeEvent(self, event): """窗口关闭时安全停止所有线程""" reply = QMessageBox.question( self, "关闭确认", "确定要关闭下载工具吗?正在下载的任务将被取消!", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: # 停止所有正在运行的线程 for row, task in self.download_tasks.items(): if task["thread"] and task["thread"].isRunning(): task["thread"].cancel() task["thread"].wait() event.accept() else: event.ignore() if __name__ == "__main__": app = QApplication(sys.argv) window = DownloaderWindow() window.show() sys.exit(app.exec_())五、核心功能解析 1. 多线程下载核心逻辑 分块下载:通过response.iter_content(chunk_size=1MB)分块读取文件,避免一次性加载大文件到内存; 断点续传基础:利用HTTP的Range请求头,记录已下载大小,暂停后重新下载时从断点开始; 线程安全控制:通过is_paused和is_canceled两个标志位控制下载循环,避免强制终止线程导致资源泄漏。 2. 自定义信号的应用 子线程定义了3个核心信号,实现与主线程的安全通信: progress_signal:传递下载进度、已下载大小、下载速度,用于实时更新表格; status_signal:传递任务状态,用于更新状态列的文字和颜色; finish_signal:任务完成信号,用于禁用操作按钮,清理资源。 3. 任务列表的动态管理 表格布局:用QTableWidget展示所有任务的详细信息,支持多列拉伸,适配不同窗口大小; 操作按钮:每个任务行都有独立的“开始/暂停/取消”按钮,通过lambda表达式传递任务索引,实现精准控制; 样式优化:根据任务状态设置不同的文字颜色(如完成绿色、失败红色),进度单元格根据进度设置背景色,提升视觉体验。 4. 异常处理机制 网络异常:捕获requests.exceptions.RequestException,包括超时、链接无效、服务器错误等; 文件异常:捕获文件读写时的权限错误、路径不存在等问题; 用户操作异常:避免重复点击开始按钮、暂停非运行中的任务等无效操作。 六、运行与测试步骤 环境准备:确保已安装PyQt5和requests库; 运行代码:将代码保存为multi_thread_downloader.py,执行命令:python multi_thread_downloader.py; 添加任务: 输入一个有效的下载链接(如Python官网的安装包:https://www.python.org/ftp/python/3.12.0/python-3.12.0-amd64.exe); 点击“浏览”选择保存路径(如D:/python.exe); 点击“添加任务”,任务将出现在列表中,状态为“等待中”; 测试功能: 点击“开始”按钮启动下载,观察进度、大小、速度是否实时更新; 点击“暂停”按钮,任务状态变为“已暂停”,再次点击恢复下载; 点击“取消”按钮,任务状态变为“已取消”,停止下载; 测试多任务下载:添加多个任务,点击“开始所有任务”,观察多个任务是否并行运行。 七、常见问题排查 1. 下载失败:提示“ConnectionError” 原因:下载链接无效、网络断开或服务器拒绝访问; 解决方案:检查链接是否正确,确保网络通畅,尝试更换下载链接(如使用公开的文件下载链接)。 2. 进度不更新:任务状态一直显示“下载中” 原因:文件过小,下载速度过快,进度直接跳到100%;或服务器不支持分块下载; 解决方案:尝试下载较大的文件(如几百MB的安装包),观察进度变化;检查response.headers是否包含content-length字段。 3. 暂停后继续下载,文件损坏 原因:未使用追加模式写入文件,暂停后重新下载覆盖了原有文件; 解决方案:确保文件打开模式为ab(追加二进制模式),而非wb(写入二进制模式)。 4. 窗口关闭后,下载线程仍在运行 原因:窗口关闭时未停止所有线程,子线程后台继续运行; 解决方案:在closeEvent中遍历所有任务,调用cancel()方法停止线程,并调用wait()等待线程结束。 八、功能拓展思路(进阶方向) 基于当前代码,可拓展以下实用功能,进一步提升工具的实用性: 断点续传优化:将已下载大小保存到本地配置文件,工具重启后可继续未完成的任务; 多线程分块下载:将一个文件分成多个块,每个块用一个子线程下载,提升下载速度; 任务排序与筛选:支持按状态(如“已完成”“下载中”)筛选任务,按下载速度排序; 下载限速:通过控制分块下载的间隔时间,实现下载速度限制; 文件校验:下载完成后,计算文件的MD5值,与官方提供的MD5对比,验证文件完整性。 总结 本次实战项目完美整合了阶段三的核心知识点——多线程编程、自定义信号、信号与槽绑定、事件处理,实现了一个功能完整的多线程下载工具。通过这个项目,你不仅掌握了PyQt5多线程开发的核心逻辑,还理解了“主线程管UI,子线程做任务”的分工原则,以及如何通过信号与槽实现线程间的安全通信。 下一章我们将进入阶段四的学习——PyQt5样式表(QSS),通过类似CSS的语法,给我们的界面进行美化,打造出高颜值的现代化GUI程序!如果在项目实操中遇到问题,或者有拓展功能的想法,欢迎在评论区留言讨论~ -
PyQt5多线程编程:解决界面卡顿的核心方案(附下载工具实战) 第13篇:PyQt5多线程编程:避免界面卡顿的核心方案(完整代码) 哈喽~ 欢迎来到PyQt5系列的第14篇!前面我们学了信号与槽和事件处理,但在实际开发中,你可能会遇到一个头疼的问题——当执行耗时操作(比如文件批量处理、网络请求、大数据计算)时,界面会直接卡死,无法点击、无法缩放,甚至被系统判定为“无响应”。 mkckots2.png图片 这一切的根源,在于PyQt5的单线程模型。今天我们就来学习多线程编程,通过QThread实现“主线程管界面,子线程做耗时任务”的分工,彻底解决界面卡顿问题!全程搭配完整可运行代码,帮你掌握多线程的核心逻辑和避坑技巧。 一、先搞懂:为什么单线程会导致界面卡顿? PyQt5的主线程(也叫UI线程) 有两个核心职责: 渲染界面:绘制窗口、控件、文字、图片等; 处理交互:响应按钮点击、鼠标移动、键盘输入等事件。 主线程是一个单线程循环——它会按顺序处理事件队列中的任务,一次只能做一件事。如果此时你在主线程中执行耗时操作(比如time.sleep(10)、读取1000个文件),主线程就会被“阻塞”,无法处理界面渲染和交互请求,表现为界面卡死。 多线程的核心解决方案 开启子线程(也叫工作线程),将所有耗时操作交给子线程执行,主线程只负责界面交互和数据展示。两者各司其职,互不干扰: 主线程:专注于界面渲染、用户交互、接收子线程传递的结果并更新界面; 子线程:专注于执行耗时操作,不涉及任何界面控件的直接操作。 核心铁律(必记!) PyQt5中,子线程绝对不能直接操作主线程的界面控件(比如在子线程中label.setText()、progressBar.setValue())。 违反这条规则,轻则界面卡顿,重则程序崩溃,且错误难以排查! 子线程与主线程的通信,必须通过信号与槽——子线程定义信号,耗时操作中发射信号传递数据;主线程绑定信号到槽函数,在槽函数中更新界面。 二、多线程的核心实现:继承QThread类(新手首选) PyQt5提供了QThread类实现多线程,继承QThread并重写run()方法是最常用、最容易理解的方式,适合新手入门。 核心步骤 自定义子线程类:继承QThread; 定义信号:用于子线程向主线程传递数据(如进度、结果、状态); 重写run()方法:将耗时操作写在run()中,这是子线程的执行入口; 主线程中使用子线程: 创建子线程实例; 绑定子线程的信号到主线程的槽函数; 调用start()方法启动子线程(注意:不是直接调用run()); 线程控制:通过标志位实现线程的安全停止(避免强制终止导致资源泄漏)。 三、实战1:基础多线程——进度条更新(解决卡顿核心案例) 我们以“模拟耗时任务+进度条更新”为例,演示多线程的核心用法。子线程负责计算进度,主线程负责更新进度条,全程界面流畅无卡顿。 完整代码 import sys import time from PyQt5.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QProgressBar, QPushButton, QLabel, QMessageBox ) from PyQt5.QtCore import QThread, pyqtSignal, Qt # -------------------------- 1. 自定义子线程类 -------------------------- class WorkThread(QThread): # 定义信号:传递进度值(int)和状态信息(str) progress_signal = pyqtSignal(int) status_signal = pyqtSignal(str) def __init__(self, total_steps=100): super().__init__() self.total_steps = total_steps # 总任务数 self.is_running = True # 线程运行标志位,用于安全停止 def run(self): """子线程执行的核心方法:耗时操作写在这里""" # 发射初始状态 self.status_signal.emit("任务开始...") for step in range(1, self.total_steps + 1): # 安全停止判断:如果标志位为False,终止循环 if not self.is_running: self.status_signal.emit("任务已停止!") break # 模拟耗时操作(比如文件下载、数据计算) time.sleep(0.05) # 发射进度信号(传递给主线程更新进度条) self.progress_signal.emit(step) # 每完成20%,发射一次状态信息 if step % 20 == 0: self.status_signal.emit(f"已完成{step}%...") # 任务正常完成(未被停止) if self.is_running: self.status_signal.emit("任务完成!") def stop(self): """安全停止线程:设置标志位为False""" self.is_running = False # -------------------------- 2. 主线程(界面类) -------------------------- class MainWindow(QWidget): def __init__(self): super().__init__() self.init_ui() self.thread = None # 子线程实例,初始化为None def init_ui(self): self.setWindowTitle("多线程进度条演示(无卡顿)") self.resize(400, 250) self.setStyleSheet("font-size: 14px;") # 布局 layout = QVBoxLayout() layout.setSpacing(20) layout.setContentsMargins(40, 30, 40, 30) # 进度条 self.progress_bar = QProgressBar() self.progress_bar.setRange(0, 100) self.progress_bar.setValue(0) # 状态标签 self.status_label = QLabel("状态:未开始", alignment=Qt.AlignCenter) self.status_label.setStyleSheet("color: #2c3e50;") # 按钮 self.start_btn = QPushButton("开始任务") self.stop_btn = QPushButton("停止任务") self.stop_btn.setEnabled(False) # 初始禁用停止按钮 # 添加控件到布局 layout.addWidget(self.progress_bar) layout.addWidget(self.status_label) layout.addWidget(self.start_btn) layout.addWidget(self.stop_btn) self.setLayout(layout) # 绑定按钮信号 self.start_btn.clicked.connect(self.start_task) self.stop_btn.clicked.connect(self.stop_task) def start_task(self): """启动子线程""" # 1. 创建子线程实例 self.thread = WorkThread(total_steps=100) # 2. 绑定子线程的信号到主线程的槽函数 self.thread.progress_signal.connect(self.update_progress) self.thread.status_signal.connect(self.update_status) # 3. 绑定线程结束信号(可选:任务完成后启用按钮) self.thread.finished.connect(self.on_thread_finished) # 4. 启动子线程(关键:调用start(),会自动执行run()方法) self.thread.start() # 5. 更新界面状态 self.start_btn.setEnabled(False) self.stop_btn.setEnabled(True) self.status_label.setText("状态:任务执行中...") def stop_task(self): """停止子线程""" if self.thread and self.thread.isRunning(): self.thread.stop() self.stop_btn.setEnabled(False) self.status_label.setText("状态:任务停止中...") def update_progress(self, step): """主线程槽函数:更新进度条(接收子线程的进度信号)""" self.progress_bar.setValue(step) def update_status(self, msg): """主线程槽函数:更新状态标签(接收子线程的状态信号)""" self.status_label.setText(f"状态:{msg}") def on_thread_finished(self): """线程结束后恢复界面状态""" self.start_btn.setEnabled(True) self.stop_btn.setEnabled(False) # 释放线程资源 self.thread = None if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_())核心亮点解析 线程安全通信:子线程WorkThread定义了progress_signal(传递进度)和status_signal(传递状态),主线程通过槽函数update_progress和update_status接收数据并更新界面,完全遵守“子线程不操作UI”的铁律。 安全停止机制:用is_running标志位控制run()方法中的循环,避免调用terminate()强制终止线程(强制终止会导致资源泄漏,比如文件未关闭、网络连接未断开)。 线程状态管理:通过thread.isRunning()判断线程是否正在运行,通过thread.finished信号监听线程结束事件,及时恢复界面状态和释放资源。 四、进阶用法:moveToThread实现多任务复用(适合复杂场景) 除了继承QThread,PyQt5还提供了moveToThread方法——将普通的工作类移动到子线程中执行。这种方式更灵活,适合一个子线程处理多个任务的场景。 核心步骤 定义工作类:继承QObject,定义耗时任务的方法和信号; 创建子线程:实例化QThread; 移动工作类到子线程:调用work_obj.moveToThread(thread); 绑定信号:将工作类的信号绑定到主线程槽函数,将主线程的信号绑定到工作类的任务方法; 启动线程:调用thread.start()。 完整代码:多任务复用的多线程 import sys import time from PyQt5.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QPushButton, QLabel ) from PyQt5.QtCore import QThread, QObject, pyqtSignal, pyqtSlot, Qt # -------------------------- 1. 定义工作类(包含多个耗时任务) -------------------------- class Worker(QObject): # 定义信号 result_signal = pyqtSignal(str) # 传递任务结果 finished_signal = pyqtSignal() # 任务完成信号 def __init__(self): super().__init__() self.is_running = True @pyqtSlot(str) # 标记为槽函数,接收主线程的任务指令 def do_task(self, task_name): """执行耗时任务:支持多个任务类型""" if not self.is_running: return self.result_signal.emit(f"开始执行任务:{task_name}") # 模拟不同任务的耗时操作 if task_name == "文件批量处理": for i in range(5): time.sleep(0.8) self.result_signal.emit(f"文件处理进度:{i+1}/5") elif task_name == "网络数据请求": for i in range(3): time.sleep(1) self.result_signal.emit(f"请求数据进度:{i+1}/3") elif task_name == "大数据计算": for i in range(4): time.sleep(0.5) self.result_signal.emit(f"计算进度:{i+1}/4") self.result_signal.emit(f"任务 {task_name} 执行完成!") self.finished_signal.emit() def stop(self): """停止所有任务""" self.is_running = False # -------------------------- 2. 主线程(界面类) -------------------------- class MoveToThreadDemo(QWidget): # 定义主线程的信号:用于向工作类发送任务指令 task_signal = pyqtSignal(str) def __init__(self): super().__init__() self.init_ui() self.init_thread() def init_ui(self): self.setWindowTitle("moveToThread 多任务演示") self.resize(400, 300) self.setStyleSheet("font-size: 14px;") layout = QVBoxLayout() layout.setSpacing(15) layout.setContentsMargins(40, 30, 40, 30) # 按钮:三个不同的任务 self.btn1 = QPushButton("执行文件批量处理") self.btn2 = QPushButton("执行网络数据请求") self.btn3 = QPushButton("执行大数据计算") self.stop_btn = QPushButton("停止所有任务") # 结果标签 self.result_label = QLabel("等待执行任务...", alignment=Qt.AlignCenter) self.result_label.setStyleSheet("color: #e74c3c;") # 添加控件 for btn in [self.btn1, self.btn2, self.btn3, self.stop_btn]: layout.addWidget(btn) layout.addWidget(self.result_label) self.setLayout(layout) # 绑定按钮信号 self.btn1.clicked.connect(lambda: self.start_task("文件批量处理")) self.btn2.clicked.connect(lambda: self.start_task("网络数据请求")) self.btn3.clicked.connect(lambda: self.start_task("大数据计算")) self.stop_btn.clicked.connect(self.stop_all_tasks) def init_thread(self): """初始化子线程和工作类""" # 1. 创建子线程 self.thread = QThread() # 2. 创建工作类实例 self.worker = Worker() # 3. 将工作类移动到子线程 self.worker.moveToThread(self.thread) # 4. 绑定信号 # 主线程 → 工作类:发送任务指令 self.task_signal.connect(self.worker.do_task) # 工作类 → 主线程:传递任务结果 self.worker.result_signal.connect(self.update_result) # 工作类 → 主线程:任务完成 self.worker.finished_signal.connect(self.on_task_finished) # 线程结束时释放资源 self.thread.finished.connect(self.thread.deleteLater) # 5. 启动子线程 self.thread.start() def start_task(self, task_name): """发送任务指令到工作类""" if self.thread.isRunning(): self.worker.is_running = True self.task_signal.emit(task_name) # 禁用按钮避免重复点击 for btn in [self.btn1, self.btn2, self.btn3]: btn.setEnabled(False) def stop_all_tasks(self): """停止所有任务""" self.worker.stop() self.update_result("已停止所有任务!") def update_result(self, msg): """更新任务结果标签""" self.result_label.setText(msg) def on_task_finished(self): """任务完成后启用按钮""" for btn in [self.btn1, self.btn2, self.btn3]: btn.setEnabled(True) def closeEvent(self, event): """窗口关闭时安全停止线程""" self.worker.stop() self.thread.quit() self.thread.wait() event.accept() if __name__ == "__main__": app = QApplication(sys.argv) window = MoveToThreadDemo() window.show() sys.exit(app.exec_())两种多线程方式对比 实现方式优点缺点适用场景继承QThread代码简洁,逻辑清晰,新手易上手一个线程只能处理一个任务,复用性差简单耗时任务(如单一进度条更新、单个文件下载)moveToThread一个线程可处理多个任务,灵活性高,资源利用率高代码稍复杂,需要手动管理信号绑定复杂多任务场景(如同时处理文件、网络、计算任务)五、综合实战:多线程文件下载工具(贴近实际开发) 我们结合网络请求和进度条更新,实现一个简单的多线程文件下载工具。子线程负责下载文件并传递进度,主线程负责显示进度和下载状态。 完整代码(需安装requests库:pip install requests) import sys import requests from PyQt5.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QLineEdit, QPushButton, QProgressBar, QLabel, QMessageBox ) from PyQt5.QtCore import QThread, pyqtSignal, Qt, QFile, QIODevice # -------------------------- 子线程:文件下载线程 -------------------------- class DownloadThread(QThread): progress_signal = pyqtSignal(int) # 下载进度(0-100) status_signal = pyqtSignal(str) # 下载状态 def __init__(self, url, save_path): super().__init__() self.url = url self.save_path = save_path self.is_paused = False self.is_canceled = False self.chunk_size = 1024 * 1024 # 每次下载1MB def run(self): """下载文件的核心逻辑""" try: # 发送请求获取文件大小 response = requests.get(self.url, stream=True, timeout=10) total_size = int(response.headers.get("content-length", 0)) downloaded_size = 0 # 打开文件准备写入 file = QFile(self.save_path) if not file.open(QIODevice.WriteOnly): self.status_signal.emit(f"文件打开失败:{file.errorString()}") return self.status_signal.emit("开始下载...") # 分块下载 for chunk in response.iter_content(chunk_size=self.chunk_size): # 暂停判断 while self.is_paused: self.msleep(100) # 取消判断 if self.is_canceled: self.status_signal.emit("下载已取消") file.close() return # 写入文件 file.write(chunk) downloaded_size += len(chunk) # 计算进度并发射信号 if total_size > 0: progress = int((downloaded_size / total_size) * 100) self.progress_signal.emit(progress) file.close() if not self.is_canceled: self.status_signal.emit("下载完成!") self.progress_signal.emit(100) except requests.exceptions.RequestException as e: self.status_signal.emit(f"下载失败:{str(e)}") except Exception as e: self.status_signal.emit(f"未知错误:{str(e)}") def pause(self): """暂停下载""" self.is_paused = not self.is_paused status = "已暂停" if self.is_paused else "已继续" self.status_signal.emit(status) def cancel(self): """取消下载""" self.is_canceled = True # -------------------------- 主线程:下载工具界面 -------------------------- class DownloadTool(QWidget): def __init__(self): super().__init__() self.init_ui() self.download_thread = None def init_ui(self): self.setWindowTitle("多线程文件下载工具") self.resize(500, 300) self.setStyleSheet("font-size: 14px;") layout = QVBoxLayout() layout.setSpacing(20) layout.setContentsMargins(40, 30, 40, 30) # 输入框:下载链接 self.url_edit = QLineEdit() self.url_edit.setPlaceholderText("请输入文件下载链接(如图片、压缩包)") self.url_edit.setText("https://www.python.org/static/img/python-logo.png") # 输入框:保存路径 self.path_edit = QLineEdit() self.path_edit.setPlaceholderText("请输入保存路径(如 D:/python.png)") self.path_edit.setText("python.png") # 进度条 self.progress_bar = QProgressBar() self.progress_bar.setRange(0, 100) self.progress_bar.setValue(0) # 状态标签 self.status_label = QLabel("状态:未开始", alignment=Qt.AlignCenter) # 按钮 self.start_btn = QPushButton("开始下载") self.pause_btn = QPushButton("暂停/继续") self.cancel_btn = QPushButton("取消下载") self.pause_btn.setEnabled(False) self.cancel_btn.setEnabled(False) # 添加控件 layout.addWidget(QLabel("下载链接:")) layout.addWidget(self.url_edit) layout.addWidget(QLabel("保存路径:")) layout.addWidget(self.path_edit) layout.addWidget(self.progress_bar) layout.addWidget(self.status_label) layout.addWidget(self.start_btn) layout.addWidget(self.pause_btn) layout.addWidget(self.cancel_btn) self.setLayout(layout) # 绑定信号 self.start_btn.clicked.connect(self.start_download) self.pause_btn.clicked.connect(self.pause_download) self.cancel_btn.clicked.connect(self.cancel_download) def start_download(self): """启动下载线程""" url = self.url_edit.text().strip() save_path = self.path_edit.text().strip() if not url or not save_path: QMessageBox.warning(self, "提示", "请输入下载链接和保存路径!") return # 创建下载线程 self.download_thread = DownloadThread(url, save_path) # 绑定信号 self.download_thread.progress_signal.connect(self.update_progress) self.download_thread.status_signal.connect(self.update_status) self.download_thread.finished.connect(self.on_download_finished) # 启动线程 self.download_thread.start() # 更新界面状态 self.start_btn.setEnabled(False) self.pause_btn.setEnabled(True) self.cancel_btn.setEnabled(True) def pause_download(self): """暂停/继续下载""" if self.download_thread and self.download_thread.isRunning(): self.download_thread.pause() def cancel_download(self): """取消下载""" if self.download_thread and self.download_thread.isRunning(): self.download_thread.cancel() self.pause_btn.setEnabled(False) def update_progress(self, progress): """更新下载进度""" self.progress_bar.setValue(progress) def update_status(self, msg): """更新下载状态""" self.status_label.setText(f"状态:{msg}") def on_download_finished(self): """下载完成后恢复界面状态""" self.start_btn.setEnabled(True) self.pause_btn.setEnabled(False) self.cancel_btn.setEnabled(False) self.download_thread = None if __name__ == "__main__": app = QApplication(sys.argv) window = DownloadTool() window.show() sys.exit(app.exec_())核心功能说明 分块下载:通过response.iter_content()分块读取文件,避免一次性加载大文件到内存; 暂停/继续功能:用is_paused标志位实现暂停,通过msleep(100)减少CPU占用; 异常处理:捕获网络请求异常和文件操作异常,通过status_signal反馈给用户; 界面状态管理:下载过程中禁用开始按钮,避免重复下载,下载完成后恢复状态。 六、多线程开发的常见问题与避坑指南 1. 子线程直接操作UI导致崩溃 问题原因:违反“子线程不能操作UI”的铁律; 解决方案:严格通过信号与槽实现子线程到主线程的通信,所有UI更新操作必须在主线程的槽函数中执行。 2. 线程启动后被垃圾回收 问题原因:子线程实例是局部变量,函数执行完后被Python垃圾回收机制销毁; 解决方案:将子线程实例设为主线程类的属性(如self.thread = DownloadThread()),确保生命周期与主线程一致。 3. 线程无法停止或停止后资源泄漏 问题原因:使用terminate()强制终止线程,或未正确处理标志位; 解决方案:用标志位(如is_running、is_canceled)控制run()方法中的循环,线程结束前确保关闭文件、断开网络连接等资源释放操作。 4. 多个线程同时操作同一数据导致混乱 问题原因:多线程共享数据时,可能出现“竞争条件”(比如一个线程写数据,另一个线程读数据); 解决方案:使用QMutex(互斥锁)保护共享数据,确保同一时间只有一个线程能访问数据。 总结 多线程核心目的:将耗时操作从主线程分离到子线程,实现“界面流畅+任务并行”; 核心铁律:子线程绝对不能直接操作UI控件,必须通过信号与槽通信; 两种实现方式: 继承QThread:适合简单单任务场景,新手首选; moveToThread:适合复杂多任务场景,灵活性高; 安全要点:用标志位实现线程的安全停止,避免强制终止;将线程实例设为类属性,防止被垃圾回收。 下一章我们将学习PyQt5样式表(QSS)——通过类似CSS的语法,给你的界面穿上“漂亮的衣服”,实现现代化的界面美化!如果在多线程开发中遇到卡顿、线程停止、资源泄漏等问题,欢迎在评论区留言讨论~ -
PyQt5事件处理:重写鼠标/键盘/窗口事件函数(附绘图实战) 第13篇:PyQt5事件处理:重写事件函数实现灵活交互(完整代码) 哈喽~ 欢迎来到PyQt5系列的第13篇!前面我们学了信号与槽和自定义信号,这两种方式能解决大部分交互需求,但它们是基于控件预设的“高级通知”(比如按钮点击、输入框文本变化)。而在实际开发中,我们还需要捕获更底层的用户操作——比如鼠标点击的具体位置、键盘按下的快捷键、窗口缩放的实时大小等,这就需要用到 PyQt5事件处理。 mk9obcxb.png图片 今天我们就来学习如何通过重写事件函数,捕获并处理鼠标、键盘、窗口等底层事件,实现更灵活的界面交互逻辑,全程搭配完整可运行代码,新手也能轻松掌握! 一、先搞懂:事件与信号的区别(核心概念) 很多同学会混淆事件(Event) 和信号(Signal),其实它们是两个不同层级的概念,核心区别如下: 对比维度事件(Event)信号(Signal)本质底层的输入/系统消息(如鼠标移动、键盘按下、窗口缩放)控件发出的高级通知(如按钮点击、文本变化)触发源操作系统/用户直接操作(如鼠标点一下、按键盘)控件状态变化(如按钮被点击后发出clicked信号)处理方式重写控件的事件函数(如mousePressEvent)绑定信号到槽函数(如btn.clicked.connect(func))灵活性极高,可捕获最细节的操作(如鼠标坐标、按键类型)中等,只能响应控件预设的信号关系信号是基于事件封装的(比如按钮的clicked信号,底层就是鼠标点击事件)-举个例子:点击按钮时,操作系统会先发送鼠标点击事件给按钮,按钮接收到事件后,会触发clicked信号,最终执行我们绑定的槽函数。 二、事件处理的核心:重写事件函数 PyQt5中所有控件都继承自QWidget,而QWidget内置了大量事件函数(如mousePressEvent、keyPressEvent)。我们只需在自定义控件/窗口类中重写这些函数,就能捕获并处理对应的事件。 核心规则 事件函数是固定名称的(比如处理鼠标点击的函数必须叫mousePressEvent),不能自定义名称; 事件函数的参数固定,第一个参数是事件对象(如QMouseEvent、QKeyEvent),包含事件的详细信息; 重写事件函数时,若需要保留控件的原有行为,需调用父类的同名事件函数(如super().mousePressEvent(event))。 三、实战1:鼠标事件处理(最常用) 鼠标事件是最常见的底层事件,包括按下、释放、双击、移动四种,对应的事件函数如下: 事件函数作用事件对象核心方法mousePressEvent(event)鼠标按下时触发QMouseEventevent.pos():获取鼠标位置;event.button():判断鼠标键(左/右/中)mouseReleaseEvent(event)鼠标释放时触发QMouseEvent同上mouseDoubleClickEvent(event)鼠标双击时触发QMouseEvent同上mouseMoveEvent(event)鼠标移动时触发QMouseEvent同上完整代码:鼠标事件捕获演示 import sys from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel from PyQt5.QtCore import Qt from PyQt5.QtGui import QFont class MouseEventDemo(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("鼠标事件处理演示") self.resize(500, 400) self.setMouseTracking(True) # 关键:开启鼠标追踪(不按下也能捕获移动事件) # 布局与标签(显示事件信息) layout = QVBoxLayout() self.info_label = QLabel("鼠标状态:未操作\n位置:(0, 0)\n按键:无", alignment=Qt.AlignCenter) self.info_label.setFont(QFont("微软雅黑", 12)) layout.addWidget(self.info_label) self.setLayout(layout) # ---------- 1. 鼠标按下事件 ---------- def mousePressEvent(self, event): # 获取鼠标位置(相对于当前窗口) x = event.x() y = event.y() # 判断按下的鼠标键 if event.button() == Qt.LeftButton: btn_text = "左键" elif event.button() == Qt.RightButton: btn_text = "右键" elif event.button() == Qt.MiddleButton: btn_text = "中键" else: btn_text = "未知" # 更新标签信息 self.info_label.setText(f"鼠标状态:按下\n位置:( , {y})\n按键:{btn_text}") # 保留父类的原有行为(可选,比如让窗口能被拖动) super().mousePressEvent(event) # ---------- 2. 鼠标释放事件 ---------- def mouseReleaseEvent(self, event): x = event.x() y = event.y() self.info_label.setText(f"鼠标状态:释放\n位置:( , {y})\n按键:无") super().mouseReleaseEvent(event) # ---------- 3. 鼠标双击事件 ---------- def mouseDoubleClickEvent(self, event): x = event.x() y = event.y() self.info_label.setText(f"鼠标状态:双击\n位置:( , {y})\n按键:左键") # 双击窗口标题栏可以最大化,这里保留该行为 super().mouseDoubleClickEvent(event) # ---------- 4. 鼠标移动事件 ---------- def mouseMoveEvent(self, event): x = event.x() y = event.y() # 实时显示鼠标位置(需开启setMouseTracking(True)) self.info_label.setText(f"鼠标状态:移动\n位置:( , {y})\n按键:无") super().mouseMoveEvent(event) if __name__ == "__main__": app = QApplication(sys.argv) window = MouseEventDemo() window.show() sys.exit(app.exec_())关键要点 鼠标追踪:默认情况下,mouseMoveEvent只有在鼠标按下时才会触发。调用self.setMouseTracking(True)后,不按下鼠标也能捕获移动事件; 鼠标位置:event.x()/event.y()获取的是相对于当前窗口的坐标,event.globalX()/event.globalY()获取的是相对于屏幕的坐标; 鼠标键判断:通过event.button()配合Qt.LeftButton/Qt.RightButton/Qt.MiddleButton判断按下的按键。 四、实战2:键盘事件处理(快捷键实现) 键盘事件用于捕获按键操作,比如实现快捷键(如Ctrl+S保存、ESC关闭窗口),核心事件函数是keyPressEvent和keyReleaseEvent。 事件函数作用事件对象核心方法keyPressEvent(event)按键按下时触发QKeyEventevent.key():获取按键;event.modifiers():判断组合键(如Ctrl/Shift)keyReleaseEvent(event)按键释放时触发QKeyEvent同上完整代码:键盘事件与快捷键实现 import sys from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QMessageBox from PyQt5.QtCore import Qt from PyQt5.QtGui import QFont class KeyEventDemo(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("键盘事件与快捷键演示") self.resize(400, 300) layout = QVBoxLayout() self.info_label = QLabel(""" 快捷键说明: 1. 按ESC键:关闭窗口 2. 按Ctrl+S:触发保存提示 3. 按方向键:移动标签位置 """, alignment=Qt.AlignCenter) self.info_label.setFont(QFont("微软雅黑", 11)) # 可移动的标签 self.move_label = QLabel("我是可移动的标签", alignment=Qt.AlignCenter) self.move_label.setStyleSheet("color: #e74c3c; font-size: 14px;") self.move_label.setFixedSize(150, 30) layout.addWidget(self.info_label) layout.addWidget(self.move_label) self.setLayout(layout) # 让窗口获得焦点,否则无法捕获键盘事件 self.setFocusPolicy(Qt.StrongFocus) # ---------- 键盘按下事件 ---------- def keyPressEvent(self, event): # 获取当前标签位置 x = self.move_label.x() y = self.move_label.y() # 1. 处理方向键:移动标签 if event.key() == Qt.Key_Left: self.move_label.move(x - 10, y) elif event.key() == Qt.Key_Right: self.move_label.move(x + 10, y) elif event.key() == Qt.Key_Up: self.move_label.move(x, y - 10) elif event.key() == Qt.Key_Down: self.move_label.move(x, y + 10) # 2. 处理ESC键:关闭窗口 elif event.key() == Qt.Key_Escape: self.close() # 3. 处理组合键:Ctrl+S elif event.key() == Qt.Key_S and event.modifiers() == Qt.ControlModifier: QMessageBox.information(self, "快捷键触发", "Ctrl+S:模拟保存成功!") # 保留父类的原有行为 super().keyPressEvent(event) if __name__ == "__main__": app = QApplication(sys.argv) window = KeyEventDemo() window.show() sys.exit(app.exec_())关键要点 焦点问题:控件必须获得焦点才能捕获键盘事件。调用self.setFocusPolicy(Qt.StrongFocus)让窗口强制获得焦点; 组合键判断:通过event.modifiers()判断组合键,常用的有Qt.ControlModifier(Ctrl键)、Qt.ShiftModifier(Shift键)、Qt.AltModifier(Alt键); 常用按键常量:Qt.Key_Escape(ESC)、Qt.Key_Enter(回车)、Qt.Key_A-Qt.Key_Z(字母键)。 五、实战3:窗口事件处理(关闭/缩放/移动) 窗口事件用于捕获窗口的状态变化,比如关闭、缩放、移动,最常用的是closeEvent(关闭窗口时触发,用于确认是否关闭)。 事件函数作用事件对象核心方法closeEvent(event)窗口关闭时触发QCloseEventevent.accept():允许关闭;event.ignore():阻止关闭resizeEvent(event)窗口缩放时触发QResizeEventevent.size():获取新大小;event.oldSize():获取旧大小moveEvent(event)窗口移动时触发QMoveEventevent.pos():获取新位置;event.oldPos():获取旧位置完整代码:窗口事件处理演示 import sys from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QMessageBox from PyQt5.QtCore import Qt from PyQt5.QtGui import QFont class WindowEventDemo(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("窗口事件处理演示") self.resize(400, 300) layout = QVBoxLayout() self.info_label = QLabel("窗口状态:正常\n大小:400×300\n位置:(0, 0)", alignment=Qt.AlignCenter) self.info_label.setFont(QFont("微软雅黑", 12)) layout.addWidget(self.info_label) self.setLayout(layout) # ---------- 1. 窗口关闭事件(最常用) ---------- def closeEvent(self, event): # 弹出确认框,询问是否关闭 reply = QMessageBox.question( self, "关闭确认", "确定要关闭窗口吗?未保存的内容可能会丢失!", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: event.accept() # 允许关闭窗口 else: event.ignore() # 阻止关闭窗口 # ---------- 2. 窗口缩放事件 ---------- def resizeEvent(self, event): # 获取窗口新大小 new_size = event.size() width = new_size.width() height = new_size.height() # 更新状态信息 self.info_label.setText(f"窗口状态:缩放\n大小:{width}×{height}\n位置:({self.x()}, {self.y()})") super().resizeEvent(event) # ---------- 3. 窗口移动事件 ---------- def moveEvent(self, event): # 获取窗口新位置 new_pos = event.pos() x = new_pos.x() y = new_pos.y() # 更新状态信息 self.info_label.setText(f"窗口状态:移动\n大小:{self.width()}×{self.height()}\n位置:( , {y})") super().moveEvent(event) if __name__ == "__main__": app = QApplication(sys.argv) window = WindowEventDemo() window.show() sys.exit(app.exec_())关键要点 关闭事件控制:closeEvent中,event.accept()允许窗口关闭,event.ignore()阻止关闭。这是实现“关闭确认”的核心方法; 大小/位置获取:resizeEvent中用event.size()获取新大小,moveEvent中用event.pos()获取新位置; 父类方法调用:重写窗口事件时,必须调用父类的同名方法,否则窗口的正常功能会被破坏(比如无法缩放、移动)。 六、综合案例:简易绘图工具(鼠标事件实战) 结合鼠标的按下、移动、释放事件,实现一个简易的绘图工具——按下鼠标拖动时绘制线条,释放鼠标时结束绘制。 完整代码:简易绘图工具 import sys from PyQt5.QtWidgets import QApplication, QWidget from PyQt5.QtCore import Qt, QPoint from PyQt5.QtGui import QPainter, QPen class PaintTool(QWidget): def __init__(self): super().__init__() self.init_ui() # 绘图相关变量 self.is_drawing = False # 是否正在绘图 self.start_point = QPoint() # 绘图起始点 self.end_point = QPoint() # 绘图结束点 def init_ui(self): self.setWindowTitle("简易绘图工具(鼠标拖动绘制线条)") self.resize(600, 500) self.setMouseTracking(True) # ---------- 鼠标按下:开始绘图 ---------- def mousePressEvent(self, event): if event.button() == Qt.LeftButton: self.is_drawing = True self.start_point = event.pos() # 记录起始点 self.end_point = self.start_point # 初始结束点等于起始点 super().mousePressEvent(event) # ---------- 鼠标移动:更新绘图 ---------- def mouseMoveEvent(self, event): if self.is_drawing: self.end_point = event.pos() # 更新结束点 self.update() # 触发重绘(调用paintEvent) super().mouseMoveEvent(event) # ---------- 鼠标释放:结束绘图 ---------- def mouseReleaseEvent(self, event): if event.button() == Qt.LeftButton: self.is_drawing = False self.end_point = event.pos() self.update() # 绘制最后一条线 super().mouseReleaseEvent(event) # ---------- 重绘事件:实际绘制线条 ---------- def paintEvent(self, event): # QPainter是PyQt5的绘图工具 painter = QPainter(self) # 设置画笔样式(颜色、宽度、线条类型) pen = QPen(Qt.red, 3, Qt.SolidLine) painter.setPen(pen) # 绘制线条:从起始点到结束点 painter.drawLine(self.start_point, self.end_point) if __name__ == "__main__": app = QApplication(sys.argv) window = PaintTool() window.show() sys.exit(app.exec_())核心逻辑 绘图状态控制:用is_drawing标记是否正在绘图,鼠标按下时设为True,释放时设为False; 重绘触发:self.update()会强制触发paintEvent方法,实现实时绘图; QPainter绘图:QPainter是PyQt5的绘图核心类,drawLine方法用于绘制线条,还支持绘制矩形、圆形等图形。 七、常见问题排查 1. 鼠标移动事件不触发 问题原因:未开启鼠标追踪,默认只有鼠标按下时才会触发mouseMoveEvent; 解决方案:调用self.setMouseTracking(True)开启鼠标追踪。 2. 键盘事件不响应 问题原因1:控件没有获得焦点; 解决方案:调用self.setFocusPolicy(Qt.StrongFocus)让控件获得焦点; 问题原因2:重写事件函数时忘记调用父类方法; 解决方案:在函数末尾添加super().keyPressEvent(event)。 3. 窗口无法关闭/缩放 问题原因:重写closeEvent/resizeEvent时忘记调用父类方法,破坏了窗口的原有功能; 解决方案:必须调用super().closeEvent(event)或super().resizeEvent(event)。 4. 绘图时线条闪烁 问题原因:每次update()都会重绘整个窗口,导致闪烁; 解决方案:开启双缓冲绘图(PyQt5默认开启,无需手动设置),或只重绘需要更新的区域。 总结 事件与信号的区别:事件是底层操作,信号是控件的高级通知,信号基于事件封装; 事件处理核心:重写固定名称的事件函数,调用父类方法保留原有功能; 常用事件: 鼠标事件:mousePressEvent/mouseMoveEvent(需开启追踪); 键盘事件:keyPressEvent(需获得焦点); 窗口事件:closeEvent(用于关闭确认); 实战价值:事件处理能实现信号与槽无法覆盖的交互(如绘图、快捷键、窗口状态监控)。 下一章我们将学习PyQt5多线程编程——解决耗时操作导致界面卡顿的问题,这是开发高性能GUI程序的必备技能!如果在事件处理中遇到绘图、快捷键实现的问题,欢迎在评论区留言讨论~ -
PyQt5自定义信号:跨窗口通信与多线程进度传递(完整代码) 第12篇:PyQt5自定义信号:满足复杂交互与跨组件通信需求(完整代码) 哈喽~ 欢迎来到PyQt5系列的第12篇!上一章我们吃透了内置信号与槽的基础用法和进阶技巧,但在实际开发中,内置信号往往无法满足复杂场景(比如两个窗口之间传递数据、自定义控件的状态变化、多线程间的进度通知)。今天我们就来学习自定义信号——PyQt5中实现跨组件、跨线程通信的核心武器,全程搭配完整可运行代码,帮你彻底掌握自定义信号的定义、发射与绑定逻辑! mk7oiwng.png图片 一、先明确:自定义信号的核心使用场景 当以下场景出现时,内置信号就不够用了,必须用自定义信号: 跨窗口通信:主窗口和子窗口之间传递数据(如子窗口输入的内容同步到主窗口); 自定义控件:开发自己的控件(如自定义进度条),需要向外发送状态变化信号; 多线程通信:子线程不能直接操作主界面,需通过自定义信号将数据传递给主线程; 复杂业务逻辑:业务状态变化(如支付成功、数据加载完成)需要触发多个槽函数响应。 自定义信号的核心规则(必记!) 自定义信号必须定义在继承自QObject的类中(PyQt5所有控件都继承了QObject,所以自定义窗口类也可以); 自定义信号是类属性,需用pyqtSignal()方法创建,不能在__init__中定义; 信号通过emit()方法发射,发射时的参数必须与信号定义的参数类型一致; 自定义信号同样支持connect()绑定槽函数、disconnect()断开绑定。 二、自定义信号的基础用法:定义、发射与绑定 1. 无参数自定义信号(基础入门) 先从最简单的无参数自定义信号入手,掌握“定义→发射→绑定”的核心流程。 完整代码:无参数自定义信号 import sys from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel from PyQt5.QtCore import QObject, pyqtSignal # 自定义信号的载体类(必须继承QObject) # 注意:如果是自定义窗口类,本身继承QWidget(已继承QObject),可直接在窗口类中定义信号 class MySignal(QObject): # 定义无参数的自定义信号,类属性 custom_signal = pyqtSignal() class SignalDemo(QWidget): def __init__(self): super().__init__() self.init_ui() self.init_signal() def init_ui(self): self.setWindowTitle("无参数自定义信号演示") self.resize(350, 200) layout = QVBoxLayout() self.label = QLabel("信号未触发", alignment=1) self.trigger_btn = QPushButton("点击发射自定义信号") layout.addWidget(self.label) layout.addWidget(self.trigger_btn) self.setLayout(layout) def init_signal(self): # 1. 创建自定义信号的实例 self.my_signal = MySignal() # 2. 绑定自定义信号到槽函数 self.my_signal.custom_signal.connect(self.on_custom_signal_trigger) # 3. 绑定按钮点击信号,触发自定义信号的发射 self.trigger_btn.clicked.connect(self.emit_custom_signal) def emit_custom_signal(self): """发射自定义信号""" print("按钮点击,准备发射自定义信号...") # 核心方法:emit() 发射信号 self.my_signal.custom_signal.emit() def on_custom_signal_trigger(self): """自定义信号的槽函数""" self.label.setText("自定义信号触发成功!") print("自定义信号槽函数执行完成") if __name__ == "__main__": app = QApplication(sys.argv) window = SignalDemo() window.show() sys.exit(app.exec_())核心步骤解析 定义信号:在继承QObject的类中,用pyqtSignal()创建类属性custom_signal,这就是自定义信号; 创建信号实例:在窗口类中实例化MySignal,得到信号载体; 绑定槽函数:用custom_signal.connect(槽函数)将信号与响应逻辑绑定; 发射信号:通过custom_signal.emit()发射信号,触发槽函数执行。 2. 带参数的自定义信号(高频实战) 大多数场景下,信号需要传递数据(如文本、数值、对象),这就需要定义带参数的自定义信号。pyqtSignal()支持指定参数类型(如int、str、tuple等),发射时必须传递对应类型的参数。 完整代码:带参数的自定义信号 import sys from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel, QLineEdit from PyQt5.QtCore import pyqtSignal # 窗口类已继承QObject,可直接定义信号 class ParamSignalDemo(QWidget): # 定义带参数的自定义信号:支持多种参数类型 # 格式:pyqtSignal(参数类型1, 参数类型2, ...) text_signal = pyqtSignal(str) # 传递字符串 num_signal = pyqtSignal(int, str) # 传递整数+字符串 dict_signal = pyqtSignal(dict) # 传递字典 def __init__(self): super().__init__() self.init_ui() self.bind_signals() def init_ui(self): self.setWindowTitle("带参数自定义信号演示") self.resize(400, 300) layout = QVBoxLayout() self.edit = QLineEdit() self.edit.setPlaceholderText("输入文本,点击按钮发射信号") self.label1 = QLabel("字符串信号:未触发", alignment=1) self.label2 = QLabel("整数+字符串信号:未触发", alignment=1) self.label3 = QLabel("字典信号:未触发", alignment=1) self.btn = QPushButton("发射所有带参数信号") layout.addWidget(self.edit) layout.addWidget(self.label1) layout.addWidget(self.label2) layout.addWidget(self.label3) layout.addWidget(self.btn) self.setLayout(layout) def bind_signals(self): """绑定自定义信号到槽函数""" self.text_signal.connect(self.on_text_signal) self.num_signal.connect(self.on_num_signal) self.dict_signal.connect(self.on_dict_signal) self.btn.clicked.connect(self.emit_param_signals) def emit_param_signals(self): """发射带参数的自定义信号""" input_text = self.edit.text().strip() or "默认文本" # 发射信号:参数类型必须与定义一致 self.text_signal.emit(input_text) # 传递字符串 self.num_signal.emit(2026, input_text) # 传递整数+字符串 self.dict_signal.emit({"name": input_text, "year": 2026}) # 传递字典 # 对应不同参数的槽函数 def on_text_signal(self, text): self.label1.setText(f"字符串信号:{text}") def on_num_signal(self, num, text): self.label2.setText(f"整数+字符串信号:{num} | {text}") def on_dict_signal(self, data): self.label3.setText(f"字典信号:{data}") if __name__ == "__main__": app = QApplication(sys.argv) window = ParamSignalDemo() window.show() sys.exit(app.exec_())关键要点 信号参数定义:pyqtSignal(str)表示信号传递字符串,pyqtSignal(int, str)表示传递两个参数(整数+字符串),支持Python基本数据类型和自定义对象; 发射参数匹配:emit()的参数数量和类型必须与信号定义完全一致,否则会触发TypeError; 槽函数接收参数:槽函数的参数数量要与信号传递的参数数量一致,顺序对应。 三、实战案例1:主窗口与子窗口的跨窗口通信 跨窗口通信是自定义信号最常用的场景之一。比如点击主窗口按钮弹出子窗口,子窗口输入内容后,通过自定义信号将数据传递回主窗口并显示。 步骤1:定义子窗口类(含自定义信号) 子窗口负责接收用户输入,输入完成后发射自定义信号传递数据。 # 子窗口类 from PyQt5.QtWidgets import QDialog, QVBoxLayout, QLineEdit, QPushButton, QLabel from PyQt5.QtCore import pyqtSignal class ChildWindow(QDialog): # 定义自定义信号:传递用户输入的文本 child_signal = pyqtSignal(str) def __init__(self, parent=None): super().__init__(parent) self.init_ui() def init_ui(self): self.setWindowTitle("子窗口(输入数据)") self.setFixedSize(300, 200) layout = QVBoxLayout() self.edit = QLineEdit() self.edit.setPlaceholderText("请输入要传递的内容") self.confirm_btn = QPushButton("确认并传递给主窗口") layout.addWidget(self.edit) layout.addWidget(self.confirm_btn) self.setLayout(layout) # 绑定按钮点击信号,发射自定义信号 self.confirm_btn.clicked.connect(self.on_confirm) def on_confirm(self): input_text = self.edit.text().strip() if input_text: # 发射信号,传递输入内容 self.child_signal.emit(input_text) self.close() # 关闭子窗口 else: QLabel("请输入内容!").show()步骤2:定义主窗口类(接收子窗口信号) 主窗口点击按钮弹出子窗口,绑定子窗口的自定义信号,接收数据并显示。 import sys from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel from PyQt5.QtCore import Qt class MainWindow(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("主窗口(接收子窗口数据)") self.resize(400, 250) layout = QVBoxLayout() self.result_label = QLabel("子窗口传递的数据:无", alignment=Qt.AlignCenter) self.result_label.setStyleSheet("font-size: 16px; color: #2ecc71;") self.open_child_btn = QPushButton("打开子窗口") layout.addWidget(self.result_label) layout.addWidget(self.open_child_btn) self.setLayout(layout) # 绑定按钮点击信号,打开子窗口 self.open_child_btn.clicked.connect(self.open_child_window) def open_child_window(self): # 创建子窗口实例 self.child_win = ChildWindow(self) # 绑定子窗口的自定义信号到槽函数 self.child_win.child_signal.connect(self.on_child_data_received) # 显示子窗口(模态) self.child_win.exec_() def on_child_data_received(self, data): """接收子窗口传递的数据""" self.result_label.setText(f"子窗口传递的数据:{data}") if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_())案例核心亮点 子窗口信号定义:子窗口类中定义child_signal = pyqtSignal(str),用于传递输入文本; 跨窗口信号绑定:主窗口创建子窗口后,通过child_win.child_signal.connect()绑定槽函数,实现数据监听; 模态窗口通信:子窗口用exec_()显示为模态窗口,用户操作完子窗口后,数据同步回主窗口。 四、实战案例2:多线程中用自定义信号传递进度(避免界面卡顿) PyQt5中子线程不能直接操作主界面控件,否则会导致界面卡顿甚至崩溃。正确的做法是:子线程执行耗时任务,通过自定义信号将进度传递给主线程,主线程更新界面。 完整代码:多线程进度传递 import sys import time from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QProgressBar, QLabel from PyQt5.QtCore import pyqtSignal, QThread, Qt # 自定义子线程类:执行耗时任务,发射进度信号 class WorkThread(QThread): # 定义自定义信号:传递进度值(整数) progress_signal = pyqtSignal(int) # 定义任务完成信号 finish_signal = pyqtSignal(str) def __init__(self, total_steps=100): super().__init__() self.total_steps = total_steps self.is_running = True def run(self): """线程执行的核心方法:耗时任务""" for step in range(1, self.total_steps + 1): if not self.is_running: break # 模拟耗时操作(如文件下载、数据处理) time.sleep(0.05) # 发射进度信号 self.progress_signal.emit(step) # 任务完成,发射完成信号 self.finish_signal.emit("任务执行完成!" if self.is_running else "任务被取消!") def stop(self): """停止线程""" self.is_running = False # 主窗口类:显示进度条,控制线程 class ThreadSignalDemo(QWidget): def __init__(self): super().__init__() self.init_ui() self.thread = None # 线程实例 def init_ui(self): self.setWindowTitle("多线程自定义信号传递进度") self.resize(400, 250) layout = QVBoxLayout() self.progress_bar = QProgressBar() self.progress_bar.setRange(0, 100) self.status_label = QLabel("状态:未开始", alignment=Qt.AlignCenter) self.start_btn = QPushButton("开始任务") self.stop_btn = QPushButton("停止任务") self.stop_btn.setEnabled(False) layout.addWidget(self.progress_bar) layout.addWidget(self.status_label) layout.addWidget(self.start_btn) layout.addWidget(self.stop_btn) self.setLayout(layout) # 绑定按钮信号 self.start_btn.clicked.connect(self.start_task) self.stop_btn.clicked.connect(self.stop_task) def start_task(self): """启动子线程""" self.start_btn.setEnabled(False) self.stop_btn.setEnabled(True) self.status_label.setText("状态:任务执行中...") # 创建线程实例 self.thread = WorkThread(total_steps=100) # 绑定线程的自定义信号到槽函数 self.thread.progress_signal.connect(self.update_progress) self.thread.finish_signal.connect(self.on_task_finish) # 启动线程 self.thread.start() def stop_task(self): """停止子线程""" if self.thread and self.thread.isRunning(): self.thread.stop() self.status_label.setText("状态:任务停止中...") def update_progress(self, step): """更新进度条(主线程执行)""" self.progress_bar.setValue(step) def on_task_finish(self, msg): """任务完成回调""" self.status_label.setText(f"状态:{msg}") self.progress_bar.setValue(0) self.start_btn.setEnabled(True) self.stop_btn.setEnabled(False) # 释放线程资源 self.thread = None if __name__ == "__main__": app = QApplication(sys.argv) window = ThreadSignalDemo() window.show() sys.exit(app.exec_())案例核心知识点 线程信号定义:子线程类WorkThread中定义progress_signal = pyqtSignal(int),用于传递进度值; 线程安全通信:子线程在run()方法中执行耗时任务,通过emit()发射进度,主线程的槽函数update_progress()更新进度条,避免了子线程直接操作界面; 线程控制:通过is_running标志控制线程是否继续执行,stop()方法安全停止线程,避免强制终止导致的资源泄漏。 五、自定义信号的高级用法:重载信号与信号断开 1. 重载信号(支持多种参数类型) 重载信号指同一个信号名支持多种参数类型,比如data_signal既可以传递int,也可以传递str。定义时用元组包裹不同的参数类型组合。 完整代码:重载信号 import sys from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel from PyQt5.QtCore import pyqtSignal class OverloadSignalDemo(QWidget): # 定义重载信号:支持两种参数类型(int)或(str) data_signal = pyqtSignal([int], [str]) def __init__(self): super().__init__() self.init_ui() self.bind_signals() def init_ui(self): self.setWindowTitle("重载自定义信号演示") self.resize(350, 200) layout = QVBoxLayout() self.label = QLabel("信号未触发", alignment=1) self.btn1 = QPushButton("发射整数信号") self.btn2 = QPushButton("发射字符串信号") layout.addWidget(self.label) layout.addWidget(self.btn1) layout.addWidget(self.btn2) self.setLayout(layout) def bind_signals(self): # 绑定重载信号:指定参数类型,对应不同槽函数 self.data_signal[int].connect(self.on_int_signal) self.data_signal[str].connect(self.on_str_signal) self.btn1.clicked.connect(lambda: self.data_signal[int].emit(2026)) self.btn2.clicked.connect(lambda: self.data_signal[str].emit("重载信号演示")) def on_int_signal(self, num): self.label.setText(f"整数信号:{num}") def on_str_signal(self, text): self.label.setText(f"字符串信号:{text}") if __name__ == "__main__": app = QApplication(sys.argv) window = OverloadSignalDemo() window.show() sys.exit(app.exec_())2. 自定义信号的断开(disconnect) 和内置信号一样,自定义信号也可以用disconnect()方法断开绑定,适用于动态控制信号是否生效的场景。 关键代码片段 # 断开指定槽函数的绑定 self.data_signal.disconnect(self.on_int_signal) # 断开该信号的所有绑定槽函数 self.data_signal.disconnect()六、自定义信号常见问题排查 1. 信号定义后无法发射(最常见!) 问题原因:自定义信号定义在__init__方法中,而非类属性; 解决方案:信号必须是类属性,直接在类中定义(如class MyClass(QObject): signal = pyqtSignal()),不能在__init__里用self.signal = pyqtSignal()。 2. 发射信号时报错TypeError: emit() takes 1 positional argument but 2 were given 问题原因:发射信号的参数数量与信号定义的不一致; 解决方案:检查pyqtSignal()的参数类型和emit()的参数数量,确保完全匹配。 3. 子线程发射信号,主线程槽函数不执行 问题原因1:线程实例被垃圾回收(比如线程是局部变量); 解决方案:将线程实例设为窗口类的属性(如self.thread = WorkThread()); 问题原因2:信号未绑定成功(线程启动后才绑定信号); 解决方案:先绑定信号,再启动线程(connect()要在start()之前)。 4. 跨窗口信号绑定后无响应 问题原因:子窗口实例被销毁(比如子窗口是局部变量,函数执行完后被回收); 解决方案:将子窗口实例设为主窗口的属性(如self.child_win = ChildWindow())。 总结 自定义信号核心流程:定义类属性信号(pyqtSignal())→ 绑定槽函数(connect())→ 发射信号(emit()); 核心使用场景:跨窗口通信、多线程进度传递、自定义控件状态通知; 关键注意事项:信号必须是类属性、参数类型要匹配、子线程不能直接操作界面; 下一章预告:我们将学习PyQt5事件处理——重写事件函数(如鼠标事件、键盘事件),实现更灵活的界面交互逻辑。 如果在自定义信号的使用中遇到跨窗口通信、多线程同步的问题,或者想了解更复杂的信号应用场景,欢迎在评论区留言讨论~ -
PyQt5信号与槽原理:从基础到进阶(附参数传递代码) 第11篇:PyQt5信号与槽原理:从基础到进阶(完整代码) 哈喽~ 欢迎来到PyQt5系列的第11篇!进入阶段三,我们将聚焦PyQt5的核心交互机制——信号与槽(Signal & Slot)。前两阶段我们已经用信号与槽实现了按钮点击、控件状态变化等基础交互,但很多同学可能只知其然不知其所以然。这一章我们从原理入手,深入讲解信号与槽的本质、多种绑定方式、参数传递、信号断开等进阶用法,帮你彻底掌握PyQt5交互的核心逻辑! mk237ph4.png图片 一、先搞懂:信号与槽的核心概念(事件驱动模型) 在学习进阶用法前,必须先明确信号与槽的本质,这是理解所有复杂交互的基础: 1. 核心定义 信号(Signal):控件的某个动作或状态变化(如按钮点击clicked、输入框内容变化textChanged、窗口关闭close),是“事件的触发者”; 槽(Slot):信号触发后执行的函数/方法(如按钮点击后执行on_btn_click),是“事件的响应者”; 绑定(connect):将信号与槽关联起来,形成“触发动作→执行响应”的逻辑链,是PyQt5事件驱动模型的核心。 2. 事件驱动模型图解 用户操作(如点击按钮)→ 控件发出信号(clicked)→ 信号触发绑定的槽函数 → 执行槽函数逻辑(如修改界面、处理数据) 特点:无需主动轮询事件,信号触发时自动执行槽函数,效率高、逻辑清晰; 优势:解耦——控件只负责发出信号,不关心哪个槽函数响应;槽函数只负责处理逻辑,不关心哪个信号触发,灵活度极高。 二、信号与槽的基础绑定方式(3种常用) 前两阶段我们主要用了控件.信号.connect(槽函数)的基础绑定方式,这一章我们拓展另外两种常用绑定方式,并对比各自的适用场景。 1. 方式1:代码手动绑定(最常用,灵活度最高) 这是我们之前一直使用的方式,直接通过代码将信号与槽函数关联,支持所有场景。 效果大概就是这样只 mk22u820.png图片 mk22u9e1.png图片 mk22uafw.png图片 完整代码:基础手动绑定 import sys from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QLabel class BasicSignalSlot(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("信号与槽基础绑定演示") self.resize(300, 200) layout = QVBoxLayout() self.label = QLabel("未点击按钮", alignment=1) self.btn = QPushButton("点击触发信号") layout.addWidget(self.label) layout.addWidget(self.btn) self.setLayout(layout) # 手动绑定:按钮clicked信号 → on_btn_click槽函数 self.btn.clicked.connect(self.on_btn_click) def on_btn_click(self): """槽函数:响应按钮点击信号""" self.label.setText("按钮被点击!信号触发成功") if __name__ == "__main__": app = QApplication(sys.argv) window = BasicSignalSlot() window.show() sys.exit(app.exec_())2. 方式2:Qt Designer可视化绑定(快速开发) 对于复杂界面,用Qt Designer(PyQt5-tools自带)拖拽控件后,可直接在界面中绑定信号与槽,无需手动写connect代码,适合快速开发。 操作步骤(核心流程): 打开Qt Designer:终端输入designer(Windows)/ 手动查找designer.exe(macOS/Linux需手动查找路径); 新建QWidget项目,拖拽一个QPushButton和QLabel; 点击菜单栏「Edit → Edit Signals/Slots」,进入信号槽编辑模式; 鼠标点击按钮并拖拽到标签上,松开后弹出绑定窗口; 左侧选择按钮的clicked()信号,右侧选择标签的setText(QString)槽函数,点击「OK」完成绑定; 保存为.ui文件,用pyuic5 -o ui_main.py main.ui转换为Python代码,直接运行即可。 优点:可视化操作,无需记忆信号/槽函数名称;缺点:灵活性不足,复杂逻辑仍需手动写代码。 3. 方式3:装饰器绑定(PyQt5.4+支持,简洁优雅) 用@pyqtSlot()装饰器标记槽函数,无需显式调用connect,代码更简洁,适合小型项目。 完整代码:装饰器绑定 import sys from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QLabel from PyQt5.QtCore import pyqtSlot class DecoratorSignalSlot(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("装饰器绑定信号与槽") self.resize(300, 200) layout = QVBoxLayout() self.label = QLabel("未点击按钮", alignment=1) self.btn = QPushButton("点击触发信号") layout.addWidget(self.label) layout.addWidget(self.btn) self.setLayout(layout) # 装饰器绑定:无需显式connect,通过@pyqtSlot关联 self.btn.clicked.connect(self.on_btn_click) @pyqtSlot() # 标记该函数为槽函数 def on_btn_click(self): self.label.setText("装饰器绑定:信号触发成功") if __name__ == "__main__": app = QApplication(sys.argv) window = DecoratorSignalSlot() window.show() sys.exit(app.exec_())三种绑定方式对比 绑定方式优点缺点适用场景代码手动绑定灵活度最高,支持复杂逻辑需手动写connect代码大多数项目(推荐)Qt Designer绑定可视化操作,快速开发复杂逻辑不支持,灵活性差简单界面、快速原型装饰器绑定代码简洁,无需显式connect功能有限,不支持动态绑定/解绑小型项目、简单交互三、进阶用法1:带参数的信号与槽(核心难点) 很多场景下,信号需要传递参数给槽函数(如输入框内容变化时传递文本、滑块拖动时传递数值),这是信号与槽的核心进阶用法,需掌握3种参数传递方式。 1. 方式1:信号自带参数(直接接收) PyQt5很多内置信号自带参数(如textChanged(str)、valueChanged(int)),槽函数直接定义对应参数即可接收。 完整代码:接收信号自带参数 mk233i3x.png图片 import sys from PyQt5.QtWidgets import QApplication, QWidget, QLineEdit, QVBoxLayout, QLabel class SignalWithParam(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("带参数的信号与槽(自带参数)") self.resize(350, 200) layout = QVBoxLayout() self.edit = QLineEdit() self.edit.setPlaceholderText("输入文本,实时显示") self.label = QLabel("输入的文本:", alignment=1) layout.addWidget(self.edit) layout.addWidget(self.label) self.setLayout(layout) # 输入框textChanged信号(自带str参数)→ on_text_change槽函数 self.edit.textChanged.connect(self.on_text_change) def on_text_change(self, text): """槽函数:接收信号自带的text参数""" self.label.setText(f"输入的文本:{text}") if __name__ == "__main__": app = QApplication(sys.argv) window = SignalWithParam() window.show() sys.exit(app.exec_())2. 方式2:lambda表达式传递自定义参数 当需要给槽函数传递自定义参数(而非信号自带参数)时,用lambda表达式作为中间桥梁,灵活传递多参数。 完整代码:lambda传递自定义参数 import sys from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QHBoxLayout, QLabel class LambdaParamSignalSlot(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("lambda传递自定义参数") self.resize(400, 200) layout = QHBoxLayout() self.label = QLabel("未点击任何按钮", alignment=1) # 创建3个按钮,传递不同参数 btn1 = QPushButton("按钮1") btn2 = QPushButton("按钮2") btn3 = QPushButton("按钮3") # lambda传递自定义参数:信号触发时,将参数传递给槽函数 btn1.clicked.connect(lambda: self.on_btn_click(1, "按钮1被点击")) btn2.clicked.connect(lambda: self.on_btn_click(2, "按钮2被点击")) btn3.clicked.connect(lambda: self.on_btn_click(3, "按钮3被点击")) layout.addWidget(btn1) layout.addWidget(btn2) layout.addWidget(btn3) layout.addWidget(self.label) self.setLayout(layout) def on_btn_click(self, btn_id, msg): """槽函数:接收自定义参数(按钮ID和提示信息)""" self.label.setText(f"ID:{btn_id} | {msg}") if __name__ == "__main__": app = QApplication(sys.argv) window = LambdaParamSignalSlot() window.show() sys.exit(app.exec_())3. 方式3:functools.partial传递参数(多参数更优雅) 当需要传递多个参数且逻辑复杂时,用functools.partial比lambda更优雅,支持默认参数、关键字参数。 完整代码:partial传递参数 import sys from functools import partial from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QLabel class PartialParamSignalSlot(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("partial传递多参数") self.resize(350, 250) layout = QVBoxLayout() self.label = QLabel("未点击按钮", alignment=1) # 创建按钮,用partial传递多参数(按钮ID、名称、颜色) btn1 = QPushButton("红色按钮") btn2 = QPushButton("蓝色按钮") btn3 = QPushButton("绿色按钮") # partial传递参数:第一个参数是槽函数,后续是自定义参数 btn1.clicked.connect(partial(self.on_btn_click, 1, "红色按钮", "#e74c3c")) btn2.clicked.connect(partial(self.on_btn_click, 2, "蓝色按钮", "#3498db")) btn3.clicked.connect(partial(self.on_btn_click, 3, "绿色按钮", "#2ecc71")) layout.addWidget(btn1) layout.addWidget(btn2) layout.addWidget(btn3) layout.addWidget(self.label) self.setLayout(layout) def on_btn_click(self, btn_id, btn_name, color): """槽函数:接收多个自定义参数""" self.label.setText(f"ID:{btn_id} | 选中:{btn_name}") self.label.setStyleSheet(f"color: {color}; font-size: 16px;") if __name__ == "__main__": app = QApplication(sys.argv) window = PartialParamSignalSlot() window.show() sys.exit(app.exec_())四、进阶用法2:信号与槽的断开(disconnect) 有时需要动态解除信号与槽的绑定(如按钮禁用时停止响应点击),用disconnect()方法实现,支持3种断开方式。 完整代码:信号与槽的断开 import sys from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QLabel class DisconnectSignalSlot(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("信号与槽的断开(disconnect)") self.resize(350, 250) layout = QVBoxLayout() self.label = QLabel("状态:未绑定信号", alignment=1) self.bind_btn = QPushButton("绑定信号") self.unbind_btn = QPushButton("断开信号") self.test_btn = QPushButton("测试信号(点击无响应)") # 绑定“绑定/断开”按钮的信号 self.bind_btn.clicked.connect(self.on_bind) self.unbind_btn.clicked.connect(self.on_unbind) layout.addWidget(self.label) layout.addWidget(self.bind_btn) layout.addWidget(self.unbind_btn) layout.addWidget(self.test_btn) self.setLayout(layout) # 记录信号是否绑定 self.is_bound = False def on_bind(self): """绑定信号""" if not self.is_bound: self.test_btn.clicked.connect(self.on_test_click) self.is_bound = True self.label.setText("状态:信号已绑定(点击测试按钮有响应)") self.test_btn.setText("测试信号(点击有响应)") def on_unbind(self): """断开信号""" if self.is_bound: self.test_btn.clicked.disconnect(self.on_test_click) self.is_bound = False self.label.setText("状态:信号已断开(点击测试按钮无响应)") self.test_btn.setText("测试信号(点击无响应)") def on_test_click(self): """测试信号的槽函数""" self.label.setText("测试信号触发成功!") if __name__ == "__main__": app = QApplication(sys.argv) window = DisconnectSignalSlot() window.show() sys.exit(app.exec_())三种断开方式说明 控件.信号.disconnect(槽函数):断开指定信号与指定槽函数的绑定(最常用); 控件.信号.disconnect():断开该信号的所有绑定槽函数; 控件.disconnect():断开该控件的所有信号与槽的绑定(慎用,可能误删必要绑定)。 五、进阶用法3:同一信号绑定多个槽函数 一个信号可以同时绑定多个槽函数,信号触发时,槽函数会按绑定顺序依次执行,适合复杂逻辑拆分。 完整代码:同一信号绑定多槽函数 import sys from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QLabel class MultiSlotSignal(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("同一信号绑定多个槽函数") self.resize(350, 250) layout = QVBoxLayout() self.label1 = QLabel("槽函数1:未执行", alignment=1) self.label2 = QLabel("槽函数2:未执行", alignment=1) self.label3 = QLabel("槽函数3:未执行", alignment=1) self.btn = QPushButton("点击触发所有槽函数") # 同一信号(btn.clicked)绑定3个槽函数 self.btn.clicked.connect(self.slot1) self.btn.clicked.connect(self.slot2) self.btn.clicked.connect(self.slot3) layout.addWidget(self.label1) layout.addWidget(self.label2) layout.addWidget(self.label3) layout.addWidget(self.btn) self.setLayout(layout) def slot1(self): self.label1.setText("槽函数1:执行成功(顺序1)") def slot2(self): self.label2.setText("槽函数2:执行成功(顺序2)") def slot3(self): self.label3.setText("槽函数3:执行成功(顺序3)") if __name__ == "__main__": app = QApplication(sys.argv) window = MultiSlotSignal() window.show() sys.exit(app.exec_())六、信号与槽的核心原理补充 1. 信号与槽的匹配规则 信号无参数 → 槽函数可无参数,或有默认参数; 信号有参数 → 槽函数必须接收所有参数(或用*args接收任意参数); 槽函数参数不能多于信号参数(否则会报错)。 2. 常用内置信号(高频汇总) 控件常用信号信号含义自带参数QPushButtonclicked()按钮点击无QLineEdittextChanged(str)文本内容变化str(当前文本)QSlidervalueChanged(int)滑块值变化int(当前值)QCheckBoxstateChanged(int)选中状态变化int(2=选中,0=未选中)QComboBoxcurrentIndexChanged(int)选中索引变化int(当前索引)QTableWidgetitemClicked(QTableWidgetItem)单元格点击QTableWidgetItem(单元格对象)七、常见问题排查 1. 信号绑定后槽函数不执行 问题原因1:信号/槽函数名称写错(如clicked写成click,on_btn_click写成on_btn_click1); 问题原因2:控件实例名错误(如self.btn写成btn,未绑定到当前对象); 问题原因3:槽函数参数不匹配(信号有参数但槽函数未接收); 问题原因4:控件被禁用(setDisabled(True)),无法发出信号。 2. 传递参数时报错(TypeError) 问题原因1:lambda表达式语法错误(如参数传递格式错误); 问题原因2:partial传递的参数数量与槽函数不匹配; 问题原因3:信号自带参数与自定义参数冲突(如同时接收信号参数和自定义参数,需用lambda整合)。 3. 断开信号时报错(RuntimeError) 问题原因1:信号未绑定该槽函数,强行断开; 问题原因2:同一信号绑定多次同一槽函数,断开时只需要断开一次; 解决方案:断开前先判断是否绑定(如用is_bound标记)。 总结 核心本质:信号与槽是PyQt5事件驱动模型的核心,实现“触发动作→响应逻辑”的解耦关联; 绑定方式:代码手动绑定(推荐)、Qt Designer可视化绑定(快速开发)、装饰器绑定(简洁); 进阶重点:带参数的信号与槽(lambda/partial传递参数)、信号断开、多槽函数绑定; 关键规则:槽函数参数数量不能多于信号参数,参数类型需匹配; 下一章我们将学习自定义信号——当内置信号无法满足需求时,如何自己定义信号并传递任意参数,实现更复杂的交互逻辑。 如果在实操中遇到信号与槽绑定、参数传递的问题,或者想了解某个复杂场景的信号槽用法,欢迎在评论区留言讨论~ -
PyQt5实战项目:简易Excel表格数据管理器(完整可运行代码) 第10篇:阶段二实战项目:简易Excel表格数据管理器(完整代码) 哈喽~ 欢迎来到PyQt5系列的第10篇!这是阶段二的收官实战项目——我们将整合前9章的核心知识点(布局管理器、QTableWidget、容器控件、标准对话框、自定义交互),开发一个“仿照Excel的简易表格数据管理器”。这个项目覆盖Excel最常用的基础功能(新建/打开/保存、行列增删、数据排序、单元格格式设置等),全程代码可直接运行,帮你把零散的知识点串联成完整的项目开发能力! mk0p53ij.png图片 一、项目核心目标与知识点整合 1. 项目目标 开发一个轻量级表格管理器,实现Excel的核心基础功能,满足日常简单的数据编辑/管理需求,界面风格贴近Excel,操作逻辑直观。 mk0ohoys.png图片 2. 整合的核心知识点 知识点应用场景QTableWidget核心表格控件,承载数据展示/编辑QTabWidget多工作表切换(模拟Excel的多sheet)QGroupBox功能按钮分组(编辑区/格式区/数据区)布局管理器(网格/线性)整体界面排版,保证自适应标准对话框(文件/字体/颜色/消息)打开/保存文件、设置单元格格式、操作提示信号与槽按钮交互、表格事件响应3. 核心功能清单 ✅ 多工作表管理(新建/删除sheet、切换sheet); ✅ 文件操作(新建空白表格、打开CSV文件、保存CSV文件); ✅ 行列管理(插入/删除行/列、清空表格); ✅ 数据编辑(单元格编辑、选中行/列高亮); ✅ 格式设置(单元格字体、颜色、居中对齐); ✅ 数据操作(按列排序、获取选中单元格数据); ✅ 操作提示(成功/失败/警告类消息框)。 二、项目整体架构 1. 界面布局设计 整体界面分为3个区域: 顶部功能区:文件操作按钮(新建/打开/保存) + 工作表切换(QTabWidget); 中部功能按钮区:用QGroupBox分为“编辑区”“格式区”“数据区”,摆放功能按钮; 底部表格区:QTableWidget核心表格,占界面主要空间,支持自适应缩放。 2. 核心功能模块 文件模块:处理CSV文件的新建/打开/保存; 工作表模块:管理多sheet的增删改查; mk0ois8v.png图片 编辑模块:行列增删、清空表格; mk0ojcv5.png图片 格式模块:单元格字体、颜色、对齐方式设置; mk0ojroj.png图片 数据模块:数据排序、选中数据获取。 mk0p33q1.png图片 三、完整代码实现 import sys import os import csv from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QGroupBox, QPushButton, QTableWidget, QTableWidgetItem, QTabWidget, QFileDialog, QFontDialog, QColorDialog, QMessageBox ) from PyQt5.QtGui import QFont, QColor, QIcon from PyQt5.QtCore import Qt, pyqtSlot # 主窗口(继承QMainWindow,支持菜单栏/工具栏,更贴近Excel) class SimpleExcel(QMainWindow): def __init__(self): super().__init__() self.init_ui() self.current_file_path = None # 记录当前打开的文件路径 def init_ui(self): # 基础窗口设置 self.setWindowTitle("简易Excel表格管理器") self.resize(1000, 700) self.setMinimumSize(800, 500) # 设置最小尺寸,避免缩放过小 # ---------- 中心控件(所有内容放在中心控件中) ---------- central_widget = QWidget() self.setCentralWidget(central_widget) # ---------- 主布局(垂直) ---------- main_layout = QVBoxLayout(central_widget) main_layout.setSpacing(15) main_layout.setContentsMargins(15, 15, 15, 15) # ---------- 1. 顶部功能区:文件操作 + 工作表切换 ---------- top_layout = QVBoxLayout() # 1.1 文件操作按钮(水平布局) file_btn_layout = QHBoxLayout() self.new_file_btn = QPushButton("新建") self.open_file_btn = QPushButton("打开") self.save_file_btn = QPushButton("保存") # 按钮样式 for btn in [self.new_file_btn, self.open_file_btn, self.save_file_btn]: btn.setFixedSize(80, 35) btn.setStyleSheet("QPushButton { font-size: 14px; }") file_btn_layout.addWidget(self.new_file_btn) file_btn_layout.addWidget(self.open_file_btn) file_btn_layout.addWidget(self.save_file_btn) file_btn_layout.addStretch() # 右侧留白 # 1.2 工作表切换(QTabWidget) self.sheet_tab = QTabWidget() self.sheet_tab.setTabPosition(QTabWidget.South) # 标签在下方(贴近Excel) self.sheet_tab.setTabsClosable(True) # 显示关闭按钮 # 添加默认工作表 self.add_new_sheet("Sheet1") # 顶部布局组合 top_layout.addLayout(file_btn_layout) top_layout.addWidget(self.sheet_tab) # ---------- 2. 中部功能按钮区(分组) ---------- btn_group_layout = QGridLayout() btn_group_layout.setSpacing(10) # 2.1 编辑区分组 edit_group = QGroupBox("编辑区") edit_layout = QVBoxLayout(edit_group) # 编辑区按钮 self.add_row_btn = QPushButton("插入行") self.del_row_btn = QPushButton("删除行") self.add_col_btn = QPushButton("插入列") self.del_col_btn = QPushButton("删除列") self.clear_btn = QPushButton("清空表格") edit_btns = [self.add_row_btn, self.del_row_btn, self.add_col_btn, self.del_col_btn, self.clear_btn] for btn in edit_btns: btn.setFixedSize(80, 30) edit_layout.addWidget(btn) btn_group_layout.addWidget(edit_group, 0, 0) # 2.2 格式区分组 format_group = QGroupBox("格式区") format_layout = QVBoxLayout(format_group) # 格式区按钮 self.font_btn = QPushButton("字体设置") self.color_btn = QPushButton("字体颜色") self.align_btn = QPushButton("居中对齐") format_btns = [self.font_btn, self.color_btn, self.align_btn] for btn in format_btns: btn.setFixedSize(80, 30) format_layout.addWidget(btn) btn_group_layout.addWidget(format_group, 0, 1) # 2.3 数据区分组 data_group = QGroupBox("数据区") data_layout = QVBoxLayout(data_group) # 数据区按钮 self.sort_asc_btn = QPushButton("升序排序") self.sort_desc_btn = QPushButton("降序排序") self.get_data_btn = QPushButton("选中数据") self.add_sheet_btn = QPushButton("新建Sheet") data_btns = [self.sort_asc_btn, self.sort_desc_btn, self.get_data_btn, self.add_sheet_btn] for btn in data_btns: btn.setFixedSize(80, 30) data_layout.addWidget(btn) btn_group_layout.addWidget(data_group, 0, 2) # ---------- 3. 底部表格区 ---------- # 表格区占主要空间,设置拉伸权重 table_layout = QVBoxLayout() table_layout.setStretchFactor(table_layout, 1) # ---------- 组合所有布局 ---------- main_layout.addLayout(top_layout) main_layout.addLayout(btn_group_layout) main_layout.addLayout(table_layout, stretch=1) # 表格区拉伸权重1,占更多空间 # ---------- 信号绑定 ---------- self.bind_signals() # ---------- 核心方法:添加新工作表 ---------- def add_new_sheet(self, sheet_name): """添加新的工作表(QTableWidget)""" # 创建表格控件 table = QTableWidget() table.setRowCount(10) # 默认10行 table.setColumnCount(5) # 默认5列 # 设置表格样式(贴近Excel) table.setAlternatingRowColors(True) # 隔行变色 table.horizontalHeader().setStretchLastSection(True) # 最后一列拉伸 table.setSelectionBehavior(QTableWidget.SelectRows) # 选中整行 # 添加到标签页 self.sheet_tab.addTab(table, sheet_name) # 切换到新工作表 self.sheet_tab.setCurrentWidget(table) return table # ---------- 信号绑定 ---------- def bind_signals(self): # 文件操作 self.new_file_btn.clicked.connect(self.on_new_file) self.open_file_btn.clicked.connect(self.on_open_file) self.save_file_btn.clicked.connect(self.on_save_file) # 工作表操作 self.add_sheet_btn.clicked.connect(self.on_add_sheet) self.sheet_tab.tabCloseRequested.connect(self.on_close_sheet) # 编辑操作 self.add_row_btn.clicked.connect(self.on_add_row) self.del_row_btn.clicked.connect(self.on_del_row) self.add_col_btn.clicked.connect(self.on_add_col) self.del_col_btn.clicked.connect(self.on_del_col) self.clear_btn.clicked.connect(self.on_clear_table) # 格式操作 self.font_btn.clicked.connect(self.on_set_font) self.color_btn.clicked.connect(self.on_set_color) self.align_btn.clicked.connect(self.on_set_align) # 数据操作 self.sort_asc_btn.clicked.connect(lambda: self.on_sort_data(Qt.AscendingOrder)) self.sort_desc_btn.clicked.connect(lambda: self.on_sort_data(Qt.DescendingOrder)) self.get_data_btn.clicked.connect(self.on_get_selected_data) # ---------- 文件操作槽函数 ---------- def on_new_file(self): """新建空白表格""" # 清空所有工作表 while self.sheet_tab.count() > 0: self.sheet_tab.removeTab(0) # 添加默认工作表 self.add_new_sheet("Sheet1") self.current_file_path = None QMessageBox.information(self, "成功", "新建空白表格完成!") def on_open_file(self): """打开CSV文件""" file_path, _ = QFileDialog.getOpenFileName( self, "打开CSV文件", "", "CSV Files (*.csv);;All Files (*.*)" ) if not file_path: return try: # 清空现有工作表 while self.sheet_tab.count() > 0: self.sheet_tab.removeTab(0) # 创建新工作表并加载数据 table = self.add_new_sheet(os.path.basename(file_path)) # 读取CSV文件 with open(file_path, "r", encoding="utf-8") as f: reader = csv.reader(f) # 获取所有行数据 rows = list(reader) if not rows: QMessageBox.warning(self, "提示", "文件为空!") return # 设置表格行列数 table.setRowCount(len(rows)) table.setColumnCount(len(rows[0])) # 填充数据 for row_idx, row_data in enumerate(rows): for col_idx, col_data in enumerate(row_data): item = QTableWidgetItem(str(col_data)) item.setTextAlignment(Qt.AlignCenter) table.setItem(row_idx, col_idx, item) self.current_file_path = file_path QMessageBox.information(self, "成功", f"已打开文件:{os.path.basename(file_path)}") except Exception as e: QMessageBox.critical(self, "错误", f"打开文件失败:{str(e)}") def on_save_file(self): """保存CSV文件""" # 获取当前表格 current_table = self.sheet_tab.currentWidget() if not isinstance(current_table, QTableWidget): QMessageBox.warning(self, "提示", "无有效表格可保存!") return # 获取保存路径 if self.current_file_path: save_path = self.current_file_path else: save_path, _ = QFileDialog.getSaveFileName( self, "保存CSV文件", "", "CSV Files (*.csv)" ) if not save_path: return # 补充后缀 if not save_path.endswith(".csv"): save_path += ".csv" try: # 写入CSV文件 with open(save_path, "w", encoding="utf-8", newline="") as f: writer = csv.writer(f) # 遍历表格所有行 for row in range(current_table.rowCount()): row_data = [] for col in range(current_table.columnCount()): item = current_table.item(row, col) row_data.append(item.text() if item else "") writer.writerow(row_data) self.current_file_path = save_path QMessageBox.information(self, "成功", f"文件已保存到:{save_path}") except Exception as e: QMessageBox.critical(self, "错误", f"保存文件失败:{str(e)}") # ---------- 工作表操作槽函数 ---------- def on_add_sheet(self): """新建工作表""" sheet_count = self.sheet_tab.count() + 1 self.add_new_sheet(f"Sheet{sheet_count}") QMessageBox.information(self, "成功", f"新建工作表Sheet{sheet_count}完成!") def on_close_sheet(self, index): """关闭指定工作表""" if self.sheet_tab.count() <= 1: QMessageBox.warning(self, "提示", "至少保留一个工作表!") return self.sheet_tab.removeTab(index) QMessageBox.information(self, "成功", "工作表已删除!") # ---------- 编辑操作槽函数 ---------- def get_current_table(self): """获取当前选中的表格,辅助函数""" table = self.sheet_tab.currentWidget() if not isinstance(table, QTableWidget): QMessageBox.warning(self, "提示", "无有效表格!") return None return table def on_add_row(self): """插入行""" table = self.get_current_table() if not table: return # 在选中行下方插入,无选中则在最后插入 current_row = table.currentRow() insert_row = current_row + 1 if current_row >= 0 else table.rowCount() table.insertRow(insert_row) QMessageBox.information(self, "成功", f"在第{insert_row+1}行插入新行!") def on_del_row(self): """删除行""" table = self.get_current_table() if not table: return current_row = table.currentRow() if current_row < 0: QMessageBox.warning(self, "提示", "请先选中要删除的行!") return table.removeRow(current_row) QMessageBox.information(self, "成功", f"第{current_row+1}行已删除!") def on_add_col(self): """插入列""" table = self.get_current_table() if not table: return current_col = table.currentColumn() insert_col = current_col + 1 if current_col >= 0 else table.columnCount() table.insertColumn(insert_col) QMessageBox.information(self, "成功", f"在第{insert_col+1}列插入新列!") def on_del_col(self): """删除列""" table = self.get_current_table() if not table: return current_col = table.currentColumn() if current_col < 0: QMessageBox.warning(self, "提示", "请先选中要删除的列!") return table.removeColumn(current_col) QMessageBox.information(self, "成功", f"第{current_col+1}列已删除!") def on_clear_table(self): """清空表格(保留行列数)""" table = self.get_current_table() if not table: return # 确认清空 reply = QMessageBox.question(self, "确认", "确定要清空表格所有数据吗?", QMessageBox.Yes | QMessageBox.No) if reply != QMessageBox.Yes: return # 清空所有单元格内容 for row in range(table.rowCount()): for col in range(table.columnCount()): table.setItem(row, col, QTableWidgetItem("")) QMessageBox.information(self, "成功", "表格数据已清空!") # ---------- 格式操作槽函数 ---------- def on_set_font(self): """设置选中单元格字体""" table = self.get_current_table() if not table: return # 获取选中单元格 selected_items = table.selectedItems() if not selected_items: QMessageBox.warning(self, "提示", "请先选中要设置格式的单元格!") return # 弹出字体选择对话框 font, ok = QFontDialog.getFont(QFont("微软雅黑", 12), self, "选择字体") if ok: for item in selected_items: item.setFont(font) QMessageBox.information(self, "成功", "字体设置完成!") def on_set_color(self): """设置选中单元格字体颜色""" table = self.get_current_table() if not table: return selected_items = table.selectedItems() if not selected_items: QMessageBox.warning(self, "提示", "请先选中要设置格式的单元格!") return # 弹出颜色选择对话框 color = QColorDialog.getColor(Qt.black, self, "选择字体颜色") if color.isValid(): for item in selected_items: item.setForeground(color) QMessageBox.information(self, "成功", "字体颜色设置完成!") def on_set_align(self): """设置选中单元格居中对齐""" table = self.get_current_table() if not table: return selected_items = table.selectedItems() if not selected_items: QMessageBox.warning(self, "提示", "请先选中要设置格式的单元格!") return for item in selected_items: item.setTextAlignment(Qt.AlignCenter) QMessageBox.information(self, "成功", "居中对齐设置完成!") # ---------- 数据操作槽函数 ---------- def on_sort_data(self, order): """数据排序(按选中列)""" table = self.get_current_table() if not table: return current_col = table.currentColumn() if current_col < 0: QMessageBox.warning(self, "提示", "请先选中要排序的列!") return # 排序 table.sortItems(current_col, order) sort_type = "升序" if order == Qt.AscendingOrder else "降序" QMessageBox.information(self, "成功", f"按第{current_col+1}列{sort_type}排序完成!") def on_get_selected_data(self): """获取选中单元格数据""" table = self.get_current_table() if not table: return selected_items = table.selectedItems() if not selected_items: QMessageBox.warning(self, "提示", "请先选中单元格!") return # 整理选中数据 data_text = "选中数据:\n" for item in selected_items: data_text += f"行{item.row()+1}列{item.column()+1}:{item.text()}\n" QMessageBox.information(self, "选中数据", data_text) # ---------- 程序入口 ---------- if __name__ == "__main__": # 解决中文显示问题(可选) QApplication.setStyle("Fusion") app = QApplication(sys.argv) window = SimpleExcel() window.show() sys.exit(app.exec_())四、核心功能解析 1. 多工作表管理 核心方法:add_new_sheet() 创建新的QTableWidget并添加到QTabWidget; 关闭保护:on_close_sheet() 中判断工作表数量,确保至少保留1个; 标签位置:setTabPosition(QTabWidget.South) 把标签放在下方,贴近Excel的sheet位置。 2. CSV文件读写 读取:用csv.reader()读取所有行,再逐行填充到QTableWidget; 写入:遍历表格所有单元格,收集数据后用csv.writer()写入; 编码:统一用encoding="utf-8",避免中文乱码(Windows若乱码可尝试gbk)。 3. 单元格格式设置 选中单元格:selectedItems() 获取所有选中的单元格项; 字体设置:QFontDialog.getFont() 弹出字体选择框,返回选中的字体对象; 颜色设置:QColorDialog.getColor() 返回颜色对象,用setForeground()应用; 对齐方式:setTextAlignment(Qt.AlignCenter) 设置居中,支持左/右对齐。 4. 行列操作 插入行/列:insertRow()/insertColumn(),优先在选中位置插入,无选中则在末尾; 删除行/列:removeRow()/removeColumn(),需先判断是否选中; 清空表格:遍历所有单元格,设置为空字符串(保留行列数)。 五、常见问题排查 1. 文件相关问题 CSV打开乱码:读写时确保encoding="utf-8",Windows系统可替换为encoding="gbk"; 保存文件失败:检查文件路径是否有权限(如系统盘根目录),建议保存到用户目录; 文件为空提示:读取CSV时判断rows是否为空,避免表格无数据。 2. 表格操作问题 无有效表格提示:所有表格操作前调用get_current_table(),判断是否为QTableWidget; 选中行/列无响应:确保表格setSelectionBehavior(QTableWidget.SelectRows),支持整行选中; 排序无效果:排序需指定列(currentColumn()),确保选中了有效列。 3. 界面适配问题 窗口缩放过小:设置setMinimumSize(800, 500),避免控件重叠; 表格不自适应:horizontalHeader().setStretchLastSection(True) 让最后一列拉伸; 按钮排版混乱:用QGridLayout管理分组按钮,固定按钮大小,避免拉伸变形。 六、功能拓展建议(进阶方向) 这个基础版本可拓展更多Excel功能,适合进阶学习: 单元格合并:setSpan() 实现单元格合并/拆分; 公式计算:支持简单公式(如求和=SUM(A1:A5)); 数据筛选:按条件筛选表格数据; 样式保存:支持单元格背景色、边框设置; Excel格式支持:用openpyxl/pandas库支持.xlsx文件(需额外安装)。 总结 项目核心价值:这是阶段二的收官项目,整合了前9章的所有核心知识点,从“零散知识点”到“完整项目”,帮你建立PyQt5项目开发的整体思维; 核心技术点:QTableWidget是表格开发的核心,QTabWidget实现多页面,QGroupBox规整界面,标准对话框处理交互,信号与槽串联所有功能; 实战思维:开发GUI项目需先设计界面布局→拆分功能模块→实现核心功能→处理异常→优化体验; 后续方向:阶段三将进入PyQt5进阶内容(信号与槽深入、自定义控件、多线程、数据库交互),进一步提升项目开发能力。 如果在运行代码时遇到问题,或者想拓展某个进阶功能(如单元格合并、Excel格式支持),欢迎在评论区留言讨论~ -
PyQt5容器控件:QTabWidget与QGroupBox(附多功能窗口实战) 第9篇:PyQt5容器控件:QTabWidget与QGroupBox(完整代码) 哈喽~ 欢迎来到PyQt5系列的第9篇!上一章我们掌握了QDialog和各类标准对话框,解决了界面的交互弹窗问题。今天我们聚焦容器控件——这类控件的核心作用是“管理其他控件”,把功能相关的控件分组/分页面摆放,让复杂界面更规整、层次感更强。我们会详细讲解两种高频容器控件:QGroupBox(分组框)和QTabWidget(标签页控件),全程搭配完整可运行代码,新手也能轻松掌握! mk0o8rmi.png图片 一、先明确:容器控件的核心定位 容器控件本身不实现业务功能,而是作为“控件的容器”存在,核心价值是提升界面的结构化和可读性: QGroupBox(分组框):把功能相关的控件(如设置项、表单字段)“分组”展示,带标题和可选边框,用户能快速识别控件的功能归属; QTabWidget(标签页控件):把不同功能模块(如文本编辑、数据表格、系统设置)“分页面”展示,通过点击标签切换页面,极大节省界面空间,适合多功能集成的窗口。 二、QGroupBox(分组框)详解:控件分组管理 QGroupBox是最基础的容器控件,核心是“给控件加分组标题+边框”,支持“可勾选分组”(勾选后才启用内部控件),是规整表单/设置界面的必备工具。 1. QGroupBox基础用法(完整代码) 实现两个分组框(“界面设置”和“数据设置”),演示分组框的基础属性和可勾选功能: mk0ns3om.png图片 import sys from PyQt5.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QLabel, QCheckBox, QComboBox, QPushButton ) from PyQt5.QtCore import Qt class GroupBoxDemo(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("QGroupBox分组框基础演示") self.resize(500, 400) # 主布局(垂直):两个分组框 + 确认按钮 main_layout = QVBoxLayout() main_layout.setSpacing(20) main_layout.setContentsMargins(30, 30, 30, 30) # ---------- 分组框1:界面设置(普通分组,不可勾选) ---------- ui_group = QGroupBox("界面设置") # 设置分组标题 ui_group.setAlignment(Qt.AlignCenter) # 标题居中对齐 # 分组框内部布局(网格布局) ui_layout = QHBoxLayout() ui_layout.setSpacing(15) # 添加内部控件 theme_label = QLabel("主题:") theme_combo = QComboBox() theme_combo.addItems(["浅色主题", "深色主题", "系统主题"]) font_size_label = QLabel("字体大小:") font_size_combo = QComboBox() font_size_combo.addItems(["12px", "14px", "16px"]) # 添加到分组框布局 ui_layout.addWidget(theme_label) ui_layout.addWidget(theme_combo) ui_layout.addWidget(font_size_label) ui_layout.addWidget(font_size_combo) # 绑定布局到分组框 ui_group.setLayout(ui_layout) # ---------- 分组框2:数据设置(可勾选分组,勾选才启用内部控件) ---------- data_group = QGroupBox("数据设置") data_group.setCheckable(True) # 开启勾选功能 data_group.setChecked(False) # 默认未勾选(内部控件禁用) # 分组框内部布局(水平) data_layout = QHBoxLayout() data_layout.setSpacing(15) # 添加内部控件 auto_save_check = QCheckBox("自动保存") auto_sync_check = QCheckBox("自动同步") # 添加到分组框布局 data_layout.addWidget(auto_save_check) data_layout.addWidget(auto_sync_check) # 绑定布局到分组框 data_group.setLayout(data_layout) # 确认按钮 confirm_btn = QPushButton("保存设置") confirm_btn.setFixedSize(100, 30) # 将分组框和按钮添加到主布局 main_layout.addWidget(ui_group) main_layout.addWidget(data_group) main_layout.addWidget(confirm_btn, alignment=Qt.AlignCenter) # 绑定主布局到窗口 self.setLayout(main_layout) if __name__ == "__main__": app = QApplication(sys.argv) window = GroupBoxDemo() window.show() sys.exit(app.exec_())2. QGroupBox核心方法解析 QGroupBox的核心围绕“分组样式”和“交互控制”,重点掌握: 方法作用setTitle(标题文本)设置分组框的标题setAlignment(对齐方式)设置标题对齐(如Qt.AlignCenter居中、Qt.AlignLeft左对齐)setCheckable(True/False)是否开启“勾选功能”(勾选后内部控件启用,未勾选禁用)setChecked(True/False)设置勾选状态(仅当setCheckable(True)时生效)setStyleSheet(样式)自定义分组框样式(如边框颜色、标题字体)3. QGroupBox实战:表单字段分组 结合之前学的表单布局,用QGroupBox将用户表单分为“基础信息”和“扩展信息”两组,提升表单可读性: mk0nukbw.png图片 import sys from PyQt5.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QFormLayout, QGroupBox, QLineEdit, QComboBox, QTextEdit, QPushButton ) from PyQt5.QtCore import Qt class GroupBoxFormDemo(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("QGroupBox表单分组实战") self.resize(500, 450) # 主布局 main_layout = QVBoxLayout() main_layout.setSpacing(20) main_layout.setContentsMargins(30, 30, 30, 30) # ---------- 分组1:基础信息 ---------- basic_group = QGroupBox("基础信息(必填)") basic_layout = QFormLayout() basic_layout.setSpacing(15) # 添加表单控件 basic_layout.addRow("用户名:", QLineEdit()) basic_layout.addRow("密码:", QLineEdit()) basic_layout.addRow("性别:", QComboBox()) basic_layout.addRow("手机号:", QLineEdit()) # 绑定布局 basic_group.setLayout(basic_layout) # ---------- 分组2:扩展信息 ---------- extend_group = QGroupBox("扩展信息(选填)") extend_layout = QFormLayout() extend_layout.setSpacing(15) # 添加表单控件 extend_layout.addRow("邮箱:", QLineEdit()) extend_layout.addRow("地址:", QLineEdit()) extend_layout.addRow("备注:", QTextEdit()) # 绑定布局 extend_group.setLayout(extend_layout) # 提交按钮 submit_btn = QPushButton("提交表单") submit_btn.setFixedSize(100, 30) # 添加到主布局 main_layout.addWidget(basic_group) main_layout.addWidget(extend_group) main_layout.addWidget(submit_btn, alignment=Qt.AlignCenter) self.setLayout(main_layout) if __name__ == "__main__": app = QApplication(sys.argv) window = GroupBoxFormDemo() window.show() sys.exit(app.exec_())三、QTabWidget(标签页控件)详解:多页面切换 QTabWidget是“多页面”界面的核心控件,比如浏览器的标签页、软件的功能面板,核心是“一个窗口承载多个功能页面”,极大节省界面空间。 1. QTabWidget基础用法(完整代码) 实现包含“文本编辑页”“数据表格页”“设置页”的多标签窗口,演示标签页的添加、切换、删除等核心操作: mk0nwa5q.png图片 import sys from PyQt5.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QHBoxLayout, QTabWidget, QTextEdit, QTableWidget, QTableWidgetItem, QPushButton, QLabel, QComboBox ) from PyQt5.QtGui import QIcon from PyQt5.QtCore import Qt class TabWidgetDemo(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("QTabWidget标签页基础演示") self.resize(800, 500) # 主布局(垂直):标签页控件 + 操作按钮 main_layout = QVBoxLayout() main_layout.setSpacing(15) main_layout.setContentsMargins(15, 15, 15, 15) # ---------- 创建QTabWidget核心控件 ---------- self.tab_widget = QTabWidget() # 设置标签位置(可选:North/南South/东East/西West,默认North) self.tab_widget.setTabPosition(QTabWidget.North) # 允许关闭标签页(显示×按钮) self.tab_widget.setTabsClosable(True) # 标签栏可滚动(标签过多时显示左右箭头) self.tab_widget.setTabBarAutoHide(False) # ---------- 添加标签页 ---------- # 页1:文本编辑页 text_page = QWidget() text_layout = QVBoxLayout(text_page) text_edit = QTextEdit() text_edit.setPlaceholderText("文本编辑页:在这里输入内容...") text_layout.addWidget(text_edit) # 添加标签页(参数:页面控件,标签文本,可选:图标) self.tab_widget.addTab(text_page, "文本编辑") # 页2:数据表格页 table_page = QWidget() table_layout = QVBoxLayout(table_page) table = QTableWidget() table.setRowCount(5) table.setColumnCount(4) table.setHorizontalHeaderLabels(["ID", "姓名", "性别", "年龄"]) # 填充测试数据 test_data = [ [1, "张三", "男", 25], [2, "李四", "女", 28], [3, "王五", "男", 30], [4, "赵六", "女", 22], [5, "钱七", "男", 27] ] for row in range(5): for col in range(4): item = QTableWidgetItem(str(test_data[row][col])) item.setTextAlignment(Qt.AlignCenter) table.setItem(row, col, item) table_layout.addWidget(table) self.tab_widget.addTab(table_page, "数据表格") # 页3:设置页 setting_page = QWidget() setting_layout = QVBoxLayout(setting_page) # 设置页用水平布局放控件 setting_h_layout = QHBoxLayout() setting_h_layout.addWidget(QLabel("主题:")) setting_h_layout.addWidget(QComboBox()) setting_layout.addLayout(setting_h_layout) setting_layout.addWidget(QLabel("设置页:在这里配置系统参数...")) self.tab_widget.addTab(setting_page, "系统设置") # ---------- 操作按钮区 ---------- btn_layout = QHBoxLayout() add_tab_btn = QPushButton("添加新标签页") close_tab_btn = QPushButton("关闭当前标签页") switch_tab_btn = QPushButton("切换到设置页") for btn in [add_tab_btn, close_tab_btn, switch_tab_btn]: btn.setFixedSize(120, 30) btn_layout.addWidget(add_tab_btn) btn_layout.addWidget(close_tab_btn) btn_layout.addWidget(switch_tab_btn) btn_layout.addStretch() # ---------- 绑定布局和信号 ---------- main_layout.addWidget(self.tab_widget) main_layout.addLayout(btn_layout) self.setLayout(main_layout) # 信号绑定 add_tab_btn.clicked.connect(self.add_new_tab) close_tab_btn.clicked.connect(self.close_current_tab) switch_tab_btn.clicked.connect(self.switch_to_setting_tab) # 标签页关闭信号(点击×按钮触发) self.tab_widget.tabCloseRequested.connect(self.close_specified_tab) # 标签页切换信号 self.tab_widget.currentChanged.connect(self.on_tab_changed) # ---------- 标签页操作槽函数 ---------- def add_new_tab(self): """添加新的空白标签页""" new_page = QWidget() new_layout = QVBoxLayout(new_page) new_layout.addWidget(QLabel("这是新添加的空白标签页")) # 添加标签页(带自定义文本) tab_index = self.tab_widget.addTab(new_page, f"新标签{self.tab_widget.count()+1}") # 切换到新添加的标签页 self.tab_widget.setCurrentIndex(tab_index) def close_current_tab(self): """关闭当前选中的标签页""" current_index = self.tab_widget.currentIndex() if current_index >= 0: # 确保有标签页可关闭 self.tab_widget.removeTab(current_index) def close_specified_tab(self, index): """关闭指定索引的标签页(点击×按钮触发)""" self.tab_widget.removeTab(index) def switch_to_setting_tab(self): """切换到设置页(通过索引,设置页是第3个,索引为2)""" self.tab_widget.setCurrentIndex(2) def on_tab_changed(self, index): """标签页切换时触发,打印当前标签页名称""" tab_text = self.tab_widget.tabText(index) print(f"当前切换到:{tab_text}页") if __name__ == "__main__": app = QApplication(sys.argv) window = TabWidgetDemo() window.show() sys.exit(app.exec_())2. QTabWidget核心方法解析 QTabWidget的核心围绕“标签页的增删改查和切换”,重点掌握: 方法作用addTab(页面控件, 标签文本, 图标)添加标签页(返回新标签页的索引)insertTab(索引, 页面控件, 标签文本)在指定索引位置插入标签页removeTab(索引)删除指定索引的标签页setCurrentIndex(索引)切换到指定索引的标签页currentIndex()获取当前选中的标签页索引tabText(索引)获取指定索引标签页的文本setTabPosition(位置)设置标签位置(North/South/East/West)setTabsClosable(True)显示标签页的关闭按钮(×)setTabIcon(索引, QIcon)给指定标签页设置图标3. QTabWidget实战:带图标的标签页 给标签页添加图标(提升界面美观度),并实现“标签页内容自适应”: mk0o39iv.png图片 图片素材 mk0o4oaf.png图片 mk0o4v3i.png图片 mk0o50fc.png图片 import sys from PyQt5.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QTabWidget, QTextEdit, QTableWidget, QTableWidgetItem ) from PyQt5.QtGui import QIcon from PyQt5.QtCore import Qt # 注意:需提前准备3个图标文件(text.png、table.png、setting.png)放在同级目录 # 若无图标,可注释掉setTabIcon相关代码 # 本站提供素材图片下载 class TabWidgetIconDemo(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("QTabWidget带图标标签页实战") self.resize(800, 500) # 主布局 main_layout = QVBoxLayout() self.tab_widget = QTabWidget() # 页1:文本编辑页(带图标) text_page = QWidget() text_layout = QVBoxLayout(text_page) text_layout.addWidget(QTextEdit("文本编辑页")) self.tab_widget.addTab(text_page, "文本编辑") # 设置图标 self.tab_widget.setTabIcon(0, QIcon("text.png")) # 页2:数据表格页(带图标) table_page = QWidget() table_layout = QVBoxLayout(table_page) table = QTableWidget(5, 4) table.setHorizontalHeaderLabels(["ID", "姓名", "性别", "年龄"]) table_layout.addWidget(table) self.tab_widget.addTab(table_page, "数据表格") self.tab_widget.setTabIcon(1, QIcon("table.png")) # 页3:设置页(带图标) setting_page = QWidget() setting_layout = QVBoxLayout(setting_page) setting_layout.addWidget(QTextEdit("系统设置页")) self.tab_widget.addTab(setting_page, "系统设置") self.tab_widget.setTabIcon(2, QIcon("setting.png")) # 标签页内容自适应 self.tab_widget.setSizePolicy( self.tab_widget.sizePolicy().horizontalPolicy(), self.tab_widget.sizePolicy().verticalPolicy() ) main_layout.addWidget(self.tab_widget) self.setLayout(main_layout) if __name__ == "__main__": app = QApplication(sys.argv) window = TabWidgetIconDemo() window.show() sys.exit(app.exec_())四、综合案例:多功能工具窗口(QTabWidget+QGroupBox) 整合QTabWidget(多页面)和QGroupBox(分组),实现一个“多功能工具窗口”——包含文本编辑页、数据表格页、设置页(设置页用QGroupBox分组),贴合实际项目的界面风格: mk0o7d0r.png图片 import sys from PyQt5.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QHBoxLayout, QTabWidget, QTextEdit, QTableWidget, QTableWidgetItem, QGroupBox, QLabel, QComboBox, QCheckBox, QPushButton, QFileDialog, QMessageBox ) from PyQt5.QtCore import Qt class MultiToolWindow(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("多功能工具窗口(QTabWidget+QGroupBox)") self.resize(800, 600) # 主布局 main_layout = QVBoxLayout() self.tab_widget = QTabWidget() # ---------- 页1:文本编辑页 ---------- text_page = QWidget() text_layout = QVBoxLayout(text_page) # 文本编辑区 self.text_edit = QTextEdit() self.text_edit.setPlaceholderText("请输入文本...") # 操作按钮 text_btn_layout = QHBoxLayout() open_btn = QPushButton("打开文件") save_btn = QPushButton("保存文件") for btn in [open_btn, save_btn]: btn.setFixedSize(80, 30) text_btn_layout.addWidget(open_btn) text_btn_layout.addWidget(save_btn) text_btn_layout.addStretch() # 添加到文本页布局 text_layout.addLayout(text_btn_layout) text_layout.addWidget(self.text_edit) self.tab_widget.addTab(text_page, "文本编辑") # ---------- 页2:数据表格页 ---------- table_page = QWidget() table_layout = QVBoxLayout(table_page) self.table = QTableWidget() self.table.setRowCount(0) self.table.setColumnCount(4) self.table.setHorizontalHeaderLabels(["ID", "姓名", "性别", "年龄"]) table_layout.addWidget(self.table) self.tab_widget.addTab(table_page, "数据表格") # ---------- 页3:设置页(用QGroupBox分组) ---------- setting_page = QWidget() setting_layout = QVBoxLayout(setting_page) # 分组1:界面设置 ui_group = QGroupBox("界面设置") ui_layout = QHBoxLayout() ui_layout.addWidget(QLabel("主题:")) ui_layout.addWidget(QComboBox()) ui_group.setLayout(ui_layout) # 分组2:数据设置 data_group = QGroupBox("数据设置") data_group.setCheckable(True) data_layout = QHBoxLayout() data_layout.addWidget(QCheckBox("自动保存")) data_layout.addWidget(QCheckBox("自动同步")) data_group.setLayout(data_layout) # 保存设置按钮 save_setting_btn = QPushButton("保存设置") save_setting_btn.setFixedSize(100, 30) # 添加到设置页布局 setting_layout.addWidget(ui_group) setting_layout.addWidget(data_group) setting_layout.addWidget(save_setting_btn, alignment=Qt.AlignCenter) self.tab_widget.addTab(setting_page, "系统设置") # ---------- 绑定布局和信号 ---------- main_layout.addWidget(self.tab_widget) self.setLayout(main_layout) # 信号绑定 open_btn.clicked.connect(self.open_file) save_btn.clicked.connect(self.save_file) save_setting_btn.clicked.connect(self.save_setting) # ---------- 文本页功能 ---------- def open_file(self): file_path, _ = QFileDialog.getOpenFileName(self, "打开文件", "", "Text Files (*.txt)") if file_path: with open(file_path, "r", encoding="utf-8") as f: self.text_edit.setText(f.read()) QMessageBox.information(self, "成功", "文件打开成功!") def save_file(self): file_path, _ = QFileDialog.getSaveFileName(self, "保存文件", "", "Text Files (*.txt)") if file_path: with open(file_path, "w", encoding="utf-8") as f: f.write(self.text_edit.toPlainText()) QMessageBox.information(self, "成功", "文件保存成功!") # ---------- 设置页功能 ---------- def save_setting(self): QMessageBox.information(self, "成功", "设置保存成功!") if __name__ == "__main__": app = QApplication(sys.argv) window = MultiToolWindow() window.show() sys.exit(app.exec_())五、常见问题排查 1. QGroupBox相关问题 问题1:分组框内控件排列混乱 → 解决:分组框必须绑定布局(如QVBoxLayout/QHBoxLayout),再将控件添加到布局,而非直接添加到分组框; 问题2:可勾选分组框勾选后内部控件仍禁用 → 解决:确保内部控件未手动设置setEnabled(False),分组框的setCheckable(True)仅控制“是否可勾选”,setChecked(True)才会启用内部控件; 问题3:分组框标题不显示 → 解决:检查setTitle()是否传入空字符串,或样式表覆盖了标题显示(可重置样式表测试)。 2. QTabWidget相关问题 问题1:标签页内容不自适应窗口缩放 → 解决:标签页的页面控件(如QWidget)必须绑定布局,且布局内的控件未设置固定大小; 问题2:标签页关闭按钮不显示 → 解决:调用setTabsClosable(True),且确保Qt版本支持(PyQt5 5.6+均支持); 问题3:添加图标后标签页不显示图标 → 解决:检查图标路径是否正确(绝对路径/相对路径),图标格式是否支持(png/jpg/ico); 问题4:标签页切换无响应 → 解决:currentChanged信号绑定的槽函数需检查参数(索引)是否正确,避免索引越界。 总结 QGroupBox:核心用于控件分组,提升界面可读性,支持“可勾选分组”(勾选启用内部控件),是表单/设置界面的必备工具; QTabWidget:核心用于多页面切换,节省界面空间,支持标签页增删改查、图标设置、关闭按钮等,是多功能窗口的核心控件; 组合使用:实际项目中常将QTabWidget(多页面)和QGroupBox(分组)结合,比如“设置页用QGroupBox分组,多个功能页用QTabWidget切换”; 下一章我们将进入阶段二的收尾——阶段二实战项目:仿照Excel简易表格数据管理器,整合布局管理器、表格控件、容器控件的核心知识点,完成一个完整的实战项目。 如果在容器控件开发中遇到界面排版、标签页切换的问题,或者想拓展更复杂的多标签功能(如拖拽标签页、标签页右键菜单),欢迎在评论区留言讨论~ -
PyQt5 对话框控件:QDialog 与标准对话框(附登录实战代码) 第8篇:PyQt5对话框控件:QDialog与标准对话框(完整代码) 哈喽~ 欢迎来到PyQt5系列的第8篇!上一章我们掌握了QTextEdit(富文本)和QTableWidget(表格)的核心用法,今天聚焦对话框控件——对话框是PyQt5界面交互的“重要桥梁”,比如提示信息、选择文件、用户登录验证等场景都离不开它。我们会详细讲解两类对话框:标准对话框(系统预制,如消息框、文件选择框)和自定义对话框(基于QDialog开发专属交互窗口),全程搭配完整可运行代码,新手也能轻松掌握! mjzpc9v3.png图片 一、先明确:对话框的核心概念 在学习具体用法前,先理清对话框的两个核心属性,避免用错场景: 1. 模态(Modal)vs 非模态(Non-Modal) 模态对话框:弹出后阻塞主窗口操作,必须先处理对话框才能回到主窗口(如登录弹窗、确认删除提示),是最常用的类型; 非模态对话框:弹出后不阻塞主窗口,可同时操作主窗口和对话框(如悬浮的工具面板)。 2. 标准对话框 vs 自定义对话框 标准对话框:PyQt5预制的通用对话框(QMessageBox、QFileDialog、QColorDialog等),无需自定义界面,直接调用即可,开发效率高; 自定义对话框:基于QDialog类开发的专属对话框(如登录窗口、高级设置窗口),可自由设计界面和交互逻辑。 二、标准对话框详解:直接调用的通用交互窗口 PyQt5提供了一系列开箱即用的标准对话框,覆盖80%的通用交互场景,重点掌握以下5类: 1. QMessageBox:消息提示对话框(最常用) 用于显示提示、警告、错误、确认等信息,支持自定义按钮和交互逻辑。 效果图 信息提示框 - mjzoqu7y.png图片 警告提示框 - mjzos4d4.png图片 错误提示框 - mjzosqpr.png图片 确认提示框 - mjzot5q2.png图片 自定义提示框 - mjzp1z0h.png图片 完整代码:QMessageBox常用类型 import sys from PyQt5.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QPushButton, QMessageBox ) class MessageBoxDemo(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("QMessageBox标准消息框演示") self.resize(400, 300) # 布局与按钮 layout = QVBoxLayout() layout.setSpacing(15) layout.setContentsMargins(50, 50, 50, 50) # 不同类型的消息框按钮 info_btn = QPushButton("信息提示框") warn_btn = QPushButton("警告提示框") error_btn = QPushButton("错误提示框") question_btn = QPushButton("确认对话框") custom_btn = QPushButton("自定义按钮对话框") # 绑定信号 info_btn.clicked.connect(self.show_info) warn_btn.clicked.connect(self.show_warn) error_btn.clicked.connect(self.show_error) question_btn.clicked.connect(self.show_question) custom_btn.clicked.connect(self.show_custom) # 添加到布局 for btn in [info_btn, warn_btn, error_btn, question_btn, custom_btn]: layout.addWidget(btn) self.setLayout(layout) # 1. 信息提示框(仅提示,无返回值) def show_info(self): QMessageBox.information( self, # 父窗口 "信息", # 标题 "操作成功!这是一个信息提示框。", # 内容 QMessageBox.Ok # 按钮(默认Ok) ) # 2. 警告提示框 def show_warn(self): QMessageBox.warning( self, "警告", "数据未保存,关闭窗口将丢失内容!", QMessageBox.Ok | QMessageBox.Cancel # 多个按钮 ) # 3. 错误提示框 def show_error(self): QMessageBox.critical( self, "错误", "文件读取失败,请检查文件路径是否正确!", QMessageBox.Retry | QMessageBox.Abort ) # 4. 确认对话框(带返回值,判断用户选择) def show_question(self): reply = QMessageBox.question( self, "确认", "确定要删除这条数据吗?", QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel, # 按钮组合 QMessageBox.No # 默认选中No按钮 ) # 判断用户点击的按钮 if reply == QMessageBox.Yes: print("用户选择:删除") elif reply == QMessageBox.No: print("用户选择:不删除") else: print("用户选择:取消") # 5. 自定义按钮对话框 def show_custom(self): # 创建自定义消息框 msg_box = QMessageBox(self) msg_box.setWindowTitle("自定义对话框") msg_box.setText("请选择操作:") # 设置自定义按钮 btn1 = msg_box.addButton("导出", QMessageBox.ActionRole) btn2 = msg_box.addButton("导入", QMessageBox.ActionRole) btn3 = msg_box.addButton("取消", QMessageBox.CancelRole) # 显示对话框并获取返回值 msg_box.exec_() # 判断用户点击的按钮 if msg_box.clickedButton() == btn1: print("用户选择:导出") elif msg_box.clickedButton() == btn2: print("用户选择:导入") else: print("用户选择:取消") if __name__ == "__main__": app = QApplication(sys.argv) window = MessageBoxDemo() window.show() sys.exit(app.exec_())QMessageBox核心要点 常用类型:information()(信息)、warning()(警告)、critical()(错误)、question()(确认); 按钮组合:用|拼接(如QMessageBox.Yes | QMessageBox.No); 返回值判断:通过返回的枚举值(如QMessageBox.Yes)判断用户操作; 自定义按钮:创建QMessageBox实例,用addButton()添加自定义按钮。 2. 其他标准对话框:文件/颜色/字体选择 除了消息框,PyQt5还提供了文件、颜色、字体选择的标准对话框,用法统一且简单: 完整代码:文件/颜色/字体对话框 效果如图 mjzp4re8.png图片 import sys from PyQt5.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QPushButton, QFileDialog, QColorDialog, QFontDialog, QLabel ) from PyQt5.QtGui import QColor, QFont from PyQt5.QtCore import Qt class StandardDialogsDemo(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("标准对话框(文件/颜色/字体)演示") self.resize(500, 300) # 布局 layout = QVBoxLayout() layout.setSpacing(15) layout.setContentsMargins(50, 50, 50, 50) # 按钮 file_btn = QPushButton("选择文件") color_btn = QPushButton("选择颜色") font_btn = QPushButton("选择字体") # 结果显示标签 self.result_label = QLabel("选择结果将显示在这里...") self.result_label.setAlignment(Qt.AlignCenter) # 绑定信号 file_btn.clicked.connect(self.choose_file) color_btn.clicked.connect(self.choose_color) font_btn.clicked.connect(self.choose_font) # 添加到布局 layout.addWidget(file_btn) layout.addWidget(color_btn) layout.addWidget(font_btn) layout.addWidget(self.result_label) self.setLayout(layout) # 1. 文件选择对话框(打开文件) def choose_file(self): # getOpenFileName:选择单个文件 # getOpenFileNames:选择多个文件 # getSaveFileName:保存文件 file_path, _ = QFileDialog.getOpenFileName( self, # 父窗口 "选择文件", # 标题 "", # 默认路径(当前目录) "Text Files (*.txt);;Image Files (*.png *.jpg);;All Files (*.*)" # 文件筛选器 ) if file_path: self.result_label.setText(f"选中文件:{file_path}") else: self.result_label.setText("未选择任何文件") # 2. 颜色选择对话框 def choose_color(self): # 获取当前标签颜色,作为默认颜色 current_color = self.result_label.textColor() # 弹出颜色选择框 color = QColorDialog.getColor( current_color, # 默认颜色 self, "选择颜色" ) if color.isValid(): # 用户选择了有效颜色 self.result_label.setText(f"选中颜色:{color.name()}") self.result_label.setStyleSheet(f"color: {color.name()}; font-size: 16px;") # 3. 字体选择对话框 def choose_font(self): # 获取当前标签字体,作为默认字体 current_font = self.result_label.font() # 弹出字体选择框 font, ok = QFontDialog.getFont( current_font, # 默认字体 self, "选择字体" ) if ok: # 用户确认选择 self.result_label.setText(f"选中字体:{font.family()},大小:{font.pointSize()}") self.result_label.setFont(font) if __name__ == "__main__": app = QApplication(sys.argv) window = StandardDialogsDemo() window.show() sys.exit(app.exec_())核心方法总结 对话框类型核心方法关键参数/返回值QFileDialoggetOpenFileName()返回(选中路径, 筛选器),支持多文件选择(getOpenFileNames)QColorDialoggetColor()返回QColor对象,isValid()判断是否选择有效颜色QFontDialoggetFont()返回(字体对象, 是否确认),ok为True时表示用户确认选择三、自定义对话框:基于QDialog开发专属交互窗口 当标准对话框无法满足需求时(如登录验证、高级参数设置),需要基于QDialog开发自定义对话框,核心是“设计界面+处理返回值+设置模态”。 1. 自定义对话框基础:登录窗口案例 实现一个带“用户名/密码输入+登录/取消按钮”的登录对话框,支持模态显示和返回值判断: 步骤1:自定义登录对话框类 # login_dialog.py(可单独文件,也可写在主文件) from PyQt5.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QMessageBox ) from PyQt5.QtCore import Qt class LoginDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.init_ui() # 设置模态(默认模态,也可显式设置) self.setModal(True) def init_ui(self): # 对话框基础设置 self.setWindowTitle("用户登录") self.setFixedSize(350, 200) # 固定大小,避免缩放 # 布局 layout = QVBoxLayout() layout.setSpacing(15) layout.setContentsMargins(40, 30, 40, 30) # 用户名输入 user_layout = QHBoxLayout() user_layout.addWidget(QLabel("用户名:")) self.user_edit = QLineEdit() self.user_edit.setPlaceholderText("请输入用户名") user_layout.addWidget(self.user_edit) # 密码输入 pwd_layout = QHBoxLayout() pwd_layout.addWidget(QLabel("密 码:")) self.pwd_edit = QLineEdit() self.pwd_edit.setEchoMode(QLineEdit.Password) # 密码隐藏 self.pwd_edit.setPlaceholderText("请输入密码") pwd_layout.addWidget(self.pwd_edit) # 按钮区 btn_layout = QHBoxLayout() login_btn = QPushButton("登录") cancel_btn = QPushButton("取消") login_btn.clicked.connect(self.check_login) cancel_btn.clicked.connect(self.reject) # 关闭对话框,返回Rejected btn_layout.addWidget(login_btn) btn_layout.addWidget(cancel_btn) # 添加到布局 layout.addLayout(user_layout) layout.addLayout(pwd_layout) layout.addLayout(btn_layout) self.setLayout(layout) def check_login(self): """验证登录信息""" username = self.user_edit.text().strip() password = self.pwd_edit.text().strip() # 简单验证(实际项目中对接数据库/接口) if username == "admin" and password == "123456": # 验证通过,关闭对话框并返回Accepted self.accept() else: QMessageBox.warning( self, "登录失败", "用户名或密码错误!", QMessageBox.Ok ) # 可选:获取用户输入的信息(供主窗口调用) def get_user_info(self): return { "username": self.user_edit.text().strip(), "password": self.pwd_edit.text().strip() }步骤2:主窗口调用自定义登录对话框 import sys from PyQt5.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QPushButton, QLabel ) from login_dialog import LoginDialog # 若在同一文件,无需导入 class MainWindow(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("主窗口(调用自定义登录对话框)") self.resize(400, 300) # 布局 layout = QVBoxLayout() layout.setSpacing(20) layout.setContentsMargins(50, 50, 50, 50) # 按钮和标签 login_btn = QPushButton("点击登录") self.status_label = QLabel("当前状态:未登录") self.status_label.setAlignment(Qt.AlignCenter) # 绑定信号 login_btn.clicked.connect(self.show_login_dialog) # 添加到布局 layout.addWidget(login_btn) layout.addWidget(self.status_label) self.setLayout(layout) def show_login_dialog(self): """显示登录对话框并处理返回值""" # 创建登录对话框实例 login_dlg = LoginDialog(self) # 显示对话框并获取返回值 result = login_dlg.exec_() # 判断返回值:Accepted(登录成功)/ Rejected(取消/关闭) if result == login_dlg.Accepted: user_info = login_dlg.get_user_info() self.status_label.setText(f"当前状态:已登录(用户名:{user_info['username']})") else: self.status_label.setText("当前状态:取消登录") if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_())2. 自定义对话框核心要点 继承QDialog:自定义对话框必须继承QDialog类; 模态设置:setModal(True)设置模态(默认模态),非模态用setModal(False); 返回值处理: self.accept():关闭对话框,返回QDialog.Accepted(表示操作成功); self.reject():关闭对话框,返回QDialog.Rejected(表示取消/失败); 调用exec_()显示对话框,返回值为Accepted/Rejected; 数据传递:通过自定义方法(如get_user_info())将对话框内的输入数据传递给主窗口; 窗口大小:自定义对话框建议用setFixedSize()固定大小,避免用户缩放导致界面混乱。 四、综合案例:整合标准对话框与自定义对话框 实现一个“简易文本编辑器”,整合: 自定义登录对话框(启动时要求登录); 标准文件对话框(打开/保存文件); 标准颜色/字体对话框(设置文本样式); 标准消息框(提示操作结果)。 完整代码 mjzp8xp5.png图片 mjzp9112.png图片 import sys import os from PyQt5.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QTextEdit, QFileDialog, QColorDialog, QFontDialog, QMessageBox, QDialog, QLabel, QLineEdit ) from PyQt5.QtGui import QColor, QFont from PyQt5.QtCore import Qt # 自定义登录对话框 class LoginDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.init_ui() self.setModal(True) def init_ui(self): self.setWindowTitle("用户登录") self.setFixedSize(350, 200) layout = QVBoxLayout() layout.setSpacing(15) layout.setContentsMargins(40, 30, 40, 30) # 用户名 user_layout = QHBoxLayout() user_layout.addWidget(QLabel("用户名:")) self.user_edit = QLineEdit() self.user_edit.setPlaceholderText("admin") user_layout.addWidget(self.user_edit) layout.addLayout(user_layout) # 密码 pwd_layout = QHBoxLayout() pwd_layout.addWidget(QLabel("密 码:")) self.pwd_edit = QLineEdit() self.pwd_edit.setEchoMode(QLineEdit.Password) self.pwd_edit.setPlaceholderText("123456") pwd_layout.addWidget(self.pwd_edit) layout.addLayout(pwd_layout) # 按钮 btn_layout = QHBoxLayout() login_btn = QPushButton("登录") cancel_btn = QPushButton("取消") login_btn.clicked.connect(self.check_login) cancel_btn.clicked.connect(self.reject) btn_layout.addWidget(login_btn) btn_layout.addWidget(cancel_btn) layout.addLayout(btn_layout) self.setLayout(layout) def check_login(self): if self.user_edit.text().strip() == "admin" and self.pwd_edit.text().strip() == "123456": self.accept() else: QMessageBox.warning(self, "失败", "用户名/密码错误!") # 主编辑器窗口 class EditorWindow(QWidget): def __init__(self): super().__init__() # 先显示登录对话框,验证失败则退出 if not self.check_login(): sys.exit() # 初始化主界面 self.init_ui() self.current_file = None def check_login(self): """显示登录对话框,返回是否登录成功""" login_dlg = LoginDialog(self) return login_dlg.exec_() == login_dlg.Accepted def init_ui(self): self.setWindowTitle("简易编辑器(整合对话框)") self.resize(800, 600) # 布局 main_layout = QVBoxLayout() main_layout.setSpacing(10) main_layout.setContentsMargins(15, 15, 15, 15) # 功能按钮区 btn_layout = QHBoxLayout() open_btn = QPushButton("打开文件") save_btn = QPushButton("保存文件") color_btn = QPushButton("字体颜色") font_btn = QPushButton("字体设置") for btn in [open_btn, save_btn, color_btn, font_btn]: btn.setFixedSize(100, 30) btn_layout.addWidget(open_btn) btn_layout.addWidget(save_btn) btn_layout.addStretch() btn_layout.addWidget(color_btn) btn_layout.addWidget(font_btn) # 文本编辑区 self.text_edit = QTextEdit() self.text_edit.setFont(QFont("微软雅黑", 12)) # 添加到布局 main_layout.addLayout(btn_layout) main_layout.addWidget(self.text_edit) self.setLayout(main_layout) # 绑定信号 open_btn.clicked.connect(self.open_file) save_btn.clicked.connect(self.save_file) color_btn.clicked.connect(self.set_font_color) font_btn.clicked.connect(self.set_font) # 打开文件(标准文件对话框) def open_file(self): file_path, _ = QFileDialog.getOpenFileName( self, "打开文件", "", "Text Files (*.txt);;All Files (*.*)" ) if file_path and os.path.exists(file_path): with open(file_path, "r", encoding="utf-8") as f: self.text_edit.setText(f.read()) self.current_file = file_path QMessageBox.information(self, "成功", f"已打开文件:{os.path.basename(file_path)}") # 保存文件(标准文件对话框) def save_file(self): if self.current_file: with open(self.current_file, "w", encoding="utf-8") as f: f.write(self.text_edit.toPlainText()) QMessageBox.information(self, "成功", "文件保存成功!") else: file_path, _ = QFileDialog.getSaveFileName( self, "保存文件", "", "Text Files (*.txt)" ) if file_path: if not file_path.endswith(".txt"): file_path += ".txt" with open(file_path, "w", encoding="utf-8") as f: f.write(self.text_edit.toPlainText()) self.current_file = file_path QMessageBox.information(self, "成功", f"文件已保存到:{file_path}") # 设置字体颜色(标准颜色对话框) def set_font_color(self): color = QColorDialog.getColor(Qt.black, self, "选择字体颜色") if color.isValid(): fmt = self.text_edit.currentCharFormat() fmt.setForeground(color) self.text_edit.mergeCurrentCharFormat(fmt) # 设置字体(标准字体对话框) def set_font(self): font, ok = QFontDialog.getFont(self.text_edit.font(), self, "选择字体") if ok: self.text_edit.setCurrentFont(font) if __name__ == "__main__": app = QApplication(sys.argv) window = EditorWindow() window.show() sys.exit(app.exec_())五、常见问题排查 1. 对话框相关问题 问题1:自定义对话框非模态,主窗口可操作 → 解决:调用setModal(True),或用exec_()显示(show()显示为非模态); 问题2:文件对话框选择中文路径乱码 → 解决:读写文件时指定encoding="utf-8",Windows系统可尝试encoding="gbk"; 问题3:登录对话框关闭后主窗口也退出 → 解决:主窗口初始化时,登录失败调用sys.exit()需确保是在app创建后; 问题4:标准对话框标题/按钮显示英文 → 解决:PyQt5默认是英文,可通过设置Qt的语言环境(需额外配置翻译文件),或使用自定义对话框替换。 2. 模态对话框的坑 exec_():显示模态对话框,阻塞主线程,返回值为Accepted/Rejected; show():显示非模态对话框,不阻塞主线程,无返回值; 自定义对话框若用show()显示,需手动设置setModal(True)才能变为模态。 总结 标准对话框:PyQt5预制的通用交互窗口(消息框、文件选择框等),直接调用即可,覆盖80%通用场景; 自定义对话框:继承QDialog开发专属窗口,核心是setModal()设置模态、accept()/reject()处理返回值; 模态vs非模态:模态对话框阻塞主窗口(常用),非模态不阻塞(适合悬浮面板); 综合应用:实际项目中常结合标准对话框和自定义对话框,比如登录验证+文件操作+消息提示。 下一章我们将学习PyQt5的容器控件(QTabWidget、QGroupBox),实现多标签页、分组管理的复杂界面,进一步提升界面的层次感和实用性。如果在对话框开发中遇到问题,或者想拓展更复杂的自定义对话框(如带验证码的登录窗口),欢迎在评论区留言讨论~ -
PyQt5文本与表格控件:QTextEdit与QTableWidget(附实战代码) 第7篇:PyQt5文本与表格控件:QTextEdit与QTableWidget(完整代码) 哈喽~ 欢迎来到PyQt5系列的第7篇!上一章我们掌握了网格布局和表单布局,解决了复杂界面的排版问题。今天我们聚焦两个高频实用控件:QTextEdit(多行富文本编辑控件)和QTableWidget(表格控件)——这两个控件是处理“大段文本”和“结构化数据”的核心,比如文本编辑器、数据管理系统、报表展示等场景都离不开它们。全程搭配完整可运行代码,新手也能轻松掌握! mjxm4a2v.png图片 一、先明确:两个控件的核心定位 在学习具体用法前,先理清这两个控件的适用场景,避免用错: QTextEdit:多行文本编辑/展示控件,支持富文本(字体、颜色、图片、超链接),比QLineEdit(单行)功能强大,核心用于“大段文本处理”(如记事本、富文本编辑器); QTableWidget:表格控件,支持行/列的增删改查、单元格编辑、数据排序,核心用于“结构化数据展示/编辑”(如Excel简易表格、用户信息列表)。 二、QTextEdit详解:从基础文本到富文本编辑 QTextEdit是阶段一“简易记事本”中用到的核心控件,但当时只用到了基础文本功能,这一节我们深挖它的富文本能力。 1. QTextEdit基础用法(完整代码) 先回顾基础功能,再拓展富文本设置: import sys from PyQt5.QtWidgets import ( QApplication, QWidget, QTextEdit, QVBoxLayout, QPushButton, QHBoxLayout, QColorDialog, QFontDialog ) from PyQt5.QtGui import QTextCharFormat, QFont, QColor from PyQt5.QtCore import Qt class TextEditDemo(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("QTextEdit基础与富文本演示") self.resize(600, 400) # 1. 主布局(垂直):按钮区 + 文本编辑区 main_layout = QVBoxLayout() main_layout.setSpacing(10) main_layout.setContentsMargins(15, 15, 15, 15) # 2. 功能按钮区(水平布局) btn_layout = QHBoxLayout() btn_layout.setSpacing(8) # 富文本功能按钮 self.bold_btn = QPushButton("加粗") self.italic_btn = QPushButton("斜体") self.color_btn = QPushButton("字体颜色") self.font_btn = QPushButton("选择字体") self.clear_btn = QPushButton("清空文本") # 按钮样式 btn_size = (80, 30) for btn in [self.bold_btn, self.italic_btn, self.color_btn, self.font_btn, self.clear_btn]: btn.setFixedSize(*btn_size) # 添加到按钮布局 btn_layout.addWidget(self.bold_btn) btn_layout.addWidget(self.italic_btn) btn_layout.addWidget(self.color_btn) btn_layout.addWidget(self.font_btn) btn_layout.addStretch() # 伸缩空间 btn_layout.addWidget(self.clear_btn) # 3. 创建QTextEdit控件 self.text_edit = QTextEdit() # 基础设置:默认提示文本、字体 self.text_edit.setPlaceholderText("请输入文本(支持富文本格式:加粗、斜体、改颜色等)") self.text_edit.setFont(QFont("微软雅黑", 12)) # 允许自动换行 self.text_edit.setLineWrapMode(QTextEdit.WidgetWidth) # 4. 添加到主布局 main_layout.addLayout(btn_layout) main_layout.addWidget(self.text_edit) # 5. 绑定布局和信号 self.setLayout(main_layout) self.bind_signals() def bind_signals(self): """绑定信号与槽""" self.bold_btn.clicked.connect(self.set_bold) self.italic_btn.clicked.connect(self.set_italic) self.color_btn.clicked.connect(self.set_font_color) self.font_btn.clicked.connect(self.choose_font) self.clear_btn.clicked.connect(self.text_edit.clear) def set_bold(self): """设置选中文本加粗/取消加粗""" # 获取当前光标选中的文本格式 fmt = QTextCharFormat() # 切换加粗状态:当前加粗则取消,反之则加粗 fmt.setFontWeight(QFont.Bold if self.text_edit.fontWeight() != QFont.Bold else QFont.Normal) # 应用格式到选中文本 self.text_edit.mergeCurrentCharFormat(fmt) def set_italic(self): """设置选中文本斜体/取消斜体""" fmt = QTextCharFormat() fmt.setFontItalic(not self.text_edit.fontItalic()) self.text_edit.mergeCurrentCharFormat(fmt) def set_font_color(self): """选择字体颜色并应用到选中文本""" # 弹出颜色选择对话框 color = QColorDialog.getColor(Qt.black, self, "选择字体颜色") if color.isValid(): # 用户选择了有效颜色 fmt = QTextCharFormat() fmt.setForeground(color) self.text_edit.mergeCurrentCharFormat(fmt) def choose_font(self): """选择字体(字体名、大小、样式)""" # 弹出字体选择对话框 font, ok = QFontDialog.getFont(self.text_edit.font(), self, "选择字体") if ok: # 用户确认选择 self.text_edit.setCurrentFont(font) if __name__ == "__main__": app = QApplication(sys.argv) window = TextEditDemo() window.show() sys.exit(app.exec_())2. QTextEdit核心方法解析 QTextEdit的核心分为“文本操作”和“格式设置”两类,重点掌握以下方法: (1)基础文本操作 方法作用toPlainText()获取纯文本内容(忽略富文本格式)setPlainText(文本)设置纯文本内容(覆盖原有内容)toHtml()获取富文本内容(HTML格式)setHtml(HTML文本)设置富文本内容(支持HTML标签)append(文本)在末尾追加文本(保留原有格式)clear()清空所有文本undo()/redo()撤销/重做操作(2)富文本格式设置 方法作用mergeCurrentCharFormat(格式对象)将格式应用到选中的文本setCurrentFont(字体对象)设置当前光标位置/选中文本的字体setFontWeight(QFont.Bold/Normal)设置加粗/取消加粗setFontItalic(True/False)设置斜体/取消斜体setForeground(QColor)设置字体颜色3. QTextEdit实战:简易富文本编辑器 基于基础用法,拓展一个带“打开/保存富文本”功能的编辑器,支持保存为HTML格式(保留富文本样式): import sys import os from PyQt5.QtWidgets import ( QApplication, QWidget, QTextEdit, QVBoxLayout, QPushButton, QHBoxLayout, QColorDialog, QFontDialog, QFileDialog ) from PyQt5.QtGui import QTextCharFormat, QFont, QColor from PyQt5.QtCore import Qt class RichTextEditor(QWidget): def __init__(self): super().__init__() self.init_ui() self.current_file_path = None # 记录当前文件路径 def init_ui(self): self.setWindowTitle("简易富文本编辑器") self.resize(800, 600) # 1. 主布局 main_layout = QVBoxLayout() main_layout.setSpacing(10) main_layout.setContentsMargins(15, 15, 15, 15) # 2. 顶部功能按钮区 top_btn_layout = QHBoxLayout() self.new_btn = QPushButton("新建") self.open_btn = QPushButton("打开") self.save_btn = QPushButton("保存") for btn in [self.new_btn, self.open_btn, self.save_btn]: btn.setFixedSize(80, 30) top_btn_layout.addWidget(self.new_btn) top_btn_layout.addWidget(self.open_btn) top_btn_layout.addWidget(self.save_btn) top_btn_layout.addStretch() # 3. 格式设置按钮区 fmt_btn_layout = QHBoxLayout() self.bold_btn = QPushButton("加粗") self.italic_btn = QPushButton("斜体") self.color_btn = QPushButton("字体颜色") self.font_btn = QPushButton("选择字体") for btn in [self.bold_btn, self.italic_btn, self.color_btn, self.font_btn]: btn.setFixedSize(80, 30) fmt_btn_layout.addWidget(self.bold_btn) fmt_btn_layout.addWidget(self.italic_btn) fmt_btn_layout.addWidget(self.color_btn) fmt_btn_layout.addWidget(self.font_btn) fmt_btn_layout.addStretch() # 4. 文本编辑区 self.text_edit = QTextEdit() self.text_edit.setFont(QFont("微软雅黑", 12)) # 5. 添加到主布局 main_layout.addLayout(top_btn_layout) main_layout.addLayout(fmt_btn_layout) main_layout.addWidget(self.text_edit) # 6. 绑定布局和信号 self.setLayout(main_layout) self.bind_signals() def bind_signals(self): """绑定所有信号与槽""" # 文件操作 self.new_btn.clicked.connect(self.on_new) self.open_btn.clicked.connect(self.on_open) self.save_btn.clicked.connect(self.on_save) # 格式操作 self.bold_btn.clicked.connect(self.set_bold) self.italic_btn.clicked.connect(self.set_italic) self.color_btn.clicked.connect(self.set_font_color) self.font_btn.clicked.connect(self.choose_font) # ---------- 文件操作槽函数 ---------- def on_new(self): """新建文件:清空文本,重置路径""" self.text_edit.clear() self.current_file_path = None self.setWindowTitle("简易富文本编辑器 - 未保存文件") def on_open(self): """打开文件:支持txt(纯文本)和html(富文本)""" file_path, _ = QFileDialog.getOpenFileName( self, "打开文件", "", "HTML Files (*.html);;Text Files (*.txt);;All Files (*.*)" ) if file_path and os.path.exists(file_path): self.current_file_path = file_path # 根据后缀选择读取方式 if file_path.endswith(".html"): with open(file_path, "r", encoding="utf-8") as f: content = f.read() self.text_edit.setHtml(content) else: with open(file_path, "r", encoding="utf-8") as f: content = f.read() self.text_edit.setPlainText(content) # 更新窗口标题 self.setWindowTitle(f"简易富文本编辑器 - {os.path.basename(file_path)}") def on_save(self): """保存文件:默认保存为HTML(保留富文本格式)""" if self.current_file_path: # 直接保存 self.save_file(self.current_file_path) else: # 弹出保存对话框 file_path, _ = QFileDialog.getSaveFileName( self, "保存文件", "", "HTML Files (*.html);;Text Files (*.txt)" ) if file_path: # 补充后缀 if not (file_path.endswith(".html") or file_path.endswith(".txt")): file_path += ".html" self.save_file(file_path) self.current_file_path = file_path self.setWindowTitle(f"简易富文本编辑器 - {os.path.basename(file_path)}") def save_file(self, file_path): """辅助函数:保存文件""" if file_path.endswith(".html"): content = self.text_edit.toHtml() else: content = self.text_edit.toPlainText() with open(file_path, "w", encoding="utf-8") as f: f.write(content) # ---------- 格式设置槽函数 ---------- def set_bold(self): fmt = QTextCharFormat() fmt.setFontWeight(QFont.Bold if self.text_edit.fontWeight() != QFont.Bold else QFont.Normal) self.text_edit.mergeCurrentCharFormat(fmt) def set_italic(self): fmt = QTextCharFormat() fmt.setFontItalic(not self.text_edit.fontItalic()) self.text_edit.mergeCurrentCharFormat(fmt) def set_font_color(self): color = QColorDialog.getColor(Qt.black, self, "选择字体颜色") if color.isValid(): fmt = QTextCharFormat() fmt.setForeground(color) self.text_edit.mergeCurrentCharFormat(fmt) def choose_font(self): font, ok = QFontDialog.getFont(self.text_edit.font(), self, "选择字体") if ok: self.text_edit.setCurrentFont(font) if __name__ == "__main__": app = QApplication(sys.argv) editor = RichTextEditor() editor.show() sys.exit(app.exec_())三、QTableWidget详解:结构化数据的展示与编辑 QTableWidget是处理表格数据的核心控件,支持行/列管理、单元格编辑、数据排序,是实现“数据表格”的首选。 1. QTableWidget基础用法(完整代码) 先实现一个基础表格,演示行/列添加、单元格赋值、选中行获取等核心操作: import sys from PyQt5.QtWidgets import ( QApplication, QWidget, QTableWidget, QTableWidgetItem, QVBoxLayout, QHBoxLayout, QPushButton, QMessageBox ) from PyQt5.QtCore import Qt class TableWidgetDemo(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("QTableWidget基础演示") self.resize(600, 400) # 1. 主布局:按钮区 + 表格区 main_layout = QVBoxLayout() main_layout.setSpacing(15) main_layout.setContentsMargins(15, 15, 15, 15) # 2. 功能按钮区 btn_layout = QHBoxLayout() self.add_row_btn = QPushButton("添加行") self.del_row_btn = QPushButton("删除选中行") self.get_data_btn = QPushButton("获取选中行数据") self.clear_btn = QPushButton("清空表格") for btn in [self.add_row_btn, self.del_row_btn, self.get_data_btn, self.clear_btn]: btn.setFixedSize(100, 30) btn_layout.addWidget(self.add_row_btn) btn_layout.addWidget(self.del_row_btn) btn_layout.addWidget(self.get_data_btn) btn_layout.addWidget(self.clear_btn) btn_layout.addStretch() # 3. 创建QTableWidget控件 self.table = QTableWidget() # 设置表格行列数:5行4列 self.table.setRowCount(5) self.table.setColumnCount(4) # 设置列标题 self.table.setHorizontalHeaderLabels(["ID", "姓名", "性别", "年龄"]) # 可选设置:列宽自适应、行高自适应、禁止编辑(默认允许) self.table.horizontalHeader().setStretchLastSection(True) # 最后一列拉伸 self.table.verticalHeader().setVisible(False) # 隐藏行号 # self.table.setEditTriggers(QTableWidget.NoEditTriggers) # 禁止编辑单元格 # 4. 填充初始数据 init_data = [ [1, "张三", "男", 25], [2, "李四", "女", 28], [3, "王五", "男", 30], [4, "赵六", "女", 22], [5, "钱七", "男", 27] ] for row in range(len(init_data)): for col in range(len(init_data[row])): # 创建表格项(设置居中对齐) item = QTableWidgetItem(str(init_data[row][col])) item.setTextAlignment(Qt.AlignCenter) # 将项添加到表格 self.table.setItem(row, col, item) # 5. 添加到主布局 main_layout.addLayout(btn_layout) main_layout.addWidget(self.table) # 6. 绑定布局和信号 self.setLayout(main_layout) self.bind_signals() def bind_signals(self): """绑定信号与槽""" self.add_row_btn.clicked.connect(self.add_row) self.del_row_btn.clicked.connect(self.del_selected_row) self.get_data_btn.clicked.connect(self.get_selected_data) self.clear_btn.clicked.connect(self.clear_table) def add_row(self): """添加一行空数据""" # 获取当前行数,在末尾添加新行 current_row = self.table.rowCount() self.table.insertRow(current_row) # 给新行ID列赋值(自增) id_item = QTableWidgetItem(str(current_row + 1)) id_item.setTextAlignment(Qt.AlignCenter) self.table.setItem(current_row, 0, id_item) def del_selected_row(self): """删除选中的行""" # 获取选中的行号(单个选中) selected_rows = self.table.selectedItems() if not selected_rows: QMessageBox.warning(self, "提示", "请先选中要删除的行!") return # 获取选中行的行号(取第一个选中项的行号) row = selected_rows[0].row() self.table.removeRow(row) def get_selected_data(self): """获取选中行的所有数据""" selected_rows = self.table.selectedItems() if not selected_rows: QMessageBox.warning(self, "提示", "请先选中一行!") return # 提取选中行的所有列数据 row = selected_rows[0].row() data = [] for col in range(self.table.columnCount()): item = self.table.item(row, col) data.append(item.text() if item else "") # 显示数据 QMessageBox.information(self, "选中行数据", f"ID:{data[0]}\n姓名:{data[1]}\n性别:{data[2]}\n年龄:{data[3]}") def clear_table(self): """清空表格(保留列标题)""" self.table.setRowCount(0) if __name__ == "__main__": app = QApplication(sys.argv) window = TableWidgetDemo() window.show() sys.exit(app.exec_())2. QTableWidget核心方法解析 QTableWidget的核心围绕“行/列管理”和“单元格操作”,重点掌握: (1)行列管理 方法作用setRowCount(行数)设置表格行数setColumnCount(列数)设置表格列数setHorizontalHeaderLabels([列标题列表])设置列标题insertRow(行号)在指定行号位置插入新行removeRow(行号)删除指定行setRowCount(0)清空所有行(保留列标题)(2)单元格操作 方法作用setItem(行号, 列号, QTableWidgetItem)给指定单元格设置内容item(行号, 列号)获取指定单元格的项selectedItems()获取所有选中的单元格项setEditTriggers(触发方式)设置单元格编辑触发方式(如NoEditTriggers禁止编辑)setTextAlignment(对齐方式)设置单元格文本对齐(如Qt.AlignCenter居中)(3)样式/布局优化 方法作用horizontalHeader().setStretchLastSection(True)最后一列自适应拉伸verticalHeader().setVisible(False)隐藏行号setColumnWidth(列号, 宽度)设置指定列的宽度setRowHeight(行号, 高度)设置指定行的高度3. QTableWidget实战:简易数据表格管理器 拓展一个带“导入/导出数据”(CSV格式)、“数据排序”功能的表格管理器,贴合实际数据管理场景: import sys import os import csv from PyQt5.QtWidgets import ( QApplication, QWidget, QTableWidget, QTableWidgetItem, QVBoxLayout, QHBoxLayout, QPushButton, QMessageBox, QFileDialog, QComboBox ) from PyQt5.QtCore import Qt class TableManager(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("简易数据表格管理器") self.resize(700, 500) # 1. 主布局 main_layout = QVBoxLayout() main_layout.setSpacing(15) main_layout.setContentsMargins(15, 15, 15, 15) # 2. 顶部功能区(导入/导出 + 排序) top_layout = QHBoxLayout() # 导入/导出按钮 self.import_btn = QPushButton("导入CSV") self.export_btn = QPushButton("导出CSV") # 排序下拉框 self.sort_combo = QComboBox() self.sort_combo.addItems(["按ID升序", "按年龄升序", "按年龄降序"]) self.sort_btn = QPushButton("排序") # 样式设置 for btn in [self.import_btn, self.export_btn, self.sort_btn]: btn.setFixedSize(100, 30) self.sort_combo.setFixedWidth(120) # 添加到布局 top_layout.addWidget(self.import_btn) top_layout.addWidget(self.export_btn) top_layout.addStretch() top_layout.addWidget(self.sort_combo) top_layout.addWidget(self.sort_btn) # 3. 表格区 self.table = QTableWidget() # 初始列标题 self.table.setColumnCount(4) self.table.setHorizontalHeaderLabels(["ID", "姓名", "性别", "年龄"]) # 布局优化 self.table.horizontalHeader().setStretchLastSection(True) self.table.verticalHeader().setVisible(False) # 4. 添加到主布局 main_layout.addLayout(top_layout) main_layout.addWidget(self.table) # 5. 绑定信号 self.setLayout(main_layout) self.bind_signals() def bind_signals(self): self.import_btn.clicked.connect(self.import_csv) self.export_btn.clicked.connect(self.export_csv) self.sort_btn.clicked.connect(self.sort_data) # ---------- CSV导入导出 ---------- def import_csv(self): """导入CSV文件到表格""" file_path, _ = QFileDialog.getOpenFileName( self, "导入CSV文件", "", "CSV Files (*.csv);;All Files (*.*)" ) if not file_path: return try: # 清空原有数据 self.table.setRowCount(0) # 读取CSV文件 with open(file_path, "r", encoding="utf-8") as f: reader = csv.reader(f) # 跳过表头(如果CSV有表头) # next(reader) # 逐行添加数据 for row_idx, row_data in enumerate(reader): self.table.insertRow(row_idx) for col_idx, col_data in enumerate(row_data): item = QTableWidgetItem(col_data) item.setTextAlignment(Qt.AlignCenter) self.table.setItem(row_idx, col_idx, item) QMessageBox.information(self, "成功", "CSV文件导入成功!") except Exception as e: QMessageBox.critical(self, "错误", f"导入失败:{str(e)}") def export_csv(self): """导出表格数据到CSV""" file_path, _ = QFileDialog.getSaveFileName( self, "导出CSV文件", "", "CSV Files (*.csv)" ) if not file_path: return # 补充.csv后缀 if not file_path.endswith(".csv"): file_path += ".csv" try: # 写入CSV文件 with open(file_path, "w", encoding="utf-8", newline="") as f: writer = csv.writer(f) # 写入表头 headers = [self.table.horizontalHeaderItem(col).text() for col in range(self.table.columnCount())] writer.writerow(headers) # 写入数据 for row in range(self.table.rowCount()): row_data = [] for col in range(self.table.columnCount()): item = self.table.item(row, col) row_data.append(item.text() if item else "") writer.writerow(row_data) QMessageBox.information(self, "成功", "CSV文件导出成功!") except Exception as e: QMessageBox.critical(self, "错误", f"导出失败:{str(e)}") # ---------- 数据排序 ---------- def sort_data(self): """按选择的规则排序""" sort_type = self.sort_combo.currentText() if sort_type == "按ID升序": # 按第0列(ID)升序排序 self.table.sortItems(0, Qt.AscendingOrder) elif sort_type == "按年龄升序": # 按第3列(年龄)升序排序 self.table.sortItems(3, Qt.AscendingOrder) elif sort_type == "按年龄降序": # 按第3列(年龄)降序排序 self.table.sortItems(3, Qt.DescendingOrder) if __name__ == "__main__": app = QApplication(sys.argv) manager = TableManager() manager.show() sys.exit(app.exec_())四、综合案例:文本+表格的信息管理窗口 整合QTextEdit和QTableWidget,实现一个“用户信息管理窗口”——表格展示用户列表,选中用户后在文本区显示详情,支持编辑详情并保存: import sys from PyQt5.QtWidgets import ( QApplication, QWidget, QTableWidget, QTableWidgetItem, QVBoxLayout, QHBoxLayout, QPushButton, QTextEdit, QMessageBox, QSplitter ) from PyQt5.QtCore import Qt, QSize class InfoManager(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("用户信息管理窗口(文本+表格综合)") self.resize(800, 500) # 1. 创建分割器(左右布局,可拖动调整宽度) splitter = QSplitter(Qt.Horizontal) # 2. 左侧表格区 self.table = QTableWidget() self.table.setColumnCount(3) self.table.setHorizontalHeaderLabels(["ID", "姓名", "性别"]) self.table.horizontalHeader().setStretchLastSection(True) self.table.verticalHeader().setVisible(False) # 填充测试数据 test_data = [[1, "张三", "男"], [2, "李四", "女"], [3, "王五", "男"]] for row in range(len(test_data)): self.table.insertRow(row) for col in range(len(test_data[row])): item = QTableWidgetItem(str(test_data[row][col])) item.setTextAlignment(Qt.AlignCenter) self.table.setItem(row, col, item) # 3. 右侧文本区(详情编辑) right_widget = QWidget() right_layout = QVBoxLayout(right_widget) # 文本编辑区 self.info_edit = QTextEdit() self.info_edit.setPlaceholderText("选中表格中的用户,查看/编辑详情...") self.info_edit.setFont(QtGui.QFont("微软雅黑", 11)) # 保存按钮 save_btn = QPushButton("保存详情") save_btn.setFixedSize(100, 30) # 添加到右侧布局 right_layout.addWidget(self.info_edit) right_layout.addWidget(save_btn, alignment=Qt.AlignCenter) # 4. 将表格和右侧控件添加到分割器 splitter.addWidget(self.table) splitter.addWidget(right_widget) # 设置分割器初始宽度比例 splitter.setSizes([300, 500]) # 5. 主布局 main_layout = QVBoxLayout() main_layout.addWidget(splitter) self.setLayout(main_layout) # 6. 绑定信号 self.table.clicked.connect(self.show_user_info) save_btn.clicked.connect(self.save_user_info) def show_user_info(self): """选中表格行,显示用户详情""" selected_items = self.table.selectedItems() if not selected_items: return row = selected_items[0].row() # 获取用户基础信息 user_id = self.table.item(row, 0).text() name = self.table.item(row, 1).text() gender = self.table.item(row, 2).text() # 构造详情文本 info = f"""用户详情(ID:{user_id}) 姓名:{name} 性别:{gender} 年龄:暂未填写 电话:暂未填写 邮箱:暂未填写 备注:无 """ self.info_edit.setPlainText(info) def save_user_info(self): """保存编辑后的详情""" if not self.info_edit.toPlainText(): QMessageBox.warning(self, "提示", "详情不能为空!") return QMessageBox.information(self, "成功", "用户详情保存成功!") # 实际项目中可将详情写入数据库/文件 if __name__ == "__main__": app = QApplication(sys.argv) manager = InfoManager() manager.show() sys.exit(app.exec_())五、常见问题排查 1. QTextEdit相关问题 问题1:富文本格式设置不生效 → 解决:确保先选中文本再点击格式按钮;检查mergeCurrentCharFormat是否正确传入格式对象; 问题2:保存的HTML文件打开乱码 → 解决:保存时指定encoding="utf-8"; 问题3:文本编辑区不自适应窗口缩放 → 解决:确保文本编辑区直接添加到垂直布局,未设置固定大小。 2. QTableWidget相关问题 问题1:表格数据显示不全 → 解决:用horizontalHeader().setStretchLastSection(True)让最后一列拉伸;或手动设置列宽setColumnWidth; 问题2:选中行获取不到数据 → 解决:selectedItems()返回的是选中的单元格列表,需通过item.row()获取行号,再遍历列获取整行数据; 问题3:CSV导入/导出乱码 → 解决:读写CSV时指定encoding="utf-8";Windows系统可尝试encoding="gbk"; 问题4:单元格无法编辑 → 解决:检查setEditTriggers是否设置为NoEditTriggers,如需编辑改为DoubleClicked或AllEditTriggers。 总结 QTextEdit:核心用于多行文本/富文本处理,支持纯文本和HTML格式,mergeCurrentCharFormat是设置富文本格式的核心方法; QTableWidget:核心用于结构化数据展示/编辑,重点掌握行列管理、单元格操作、数据导入导出; 两个控件常结合使用(如表格展示列表+文本区展示详情),是数据管理类应用的核心组合; 下一章我们将学习PyQt5的对话框控件(QDialog),包括标准对话框和自定义对话框,进一步完善界面的交互能力。 如果在实操中遇到文本/表格处理的问题,或者想拓展更复杂的功能(如表格单元格合并、富文本插入图片),欢迎在评论区留言讨论~ -
PyQt5布局管理器进阶:网格布局与表单布局(附实战代码) 第6篇:PyQt5布局管理器进阶:网格布局与表单布局(完整代码) mjw558rv.png图片 哈喽~ 欢迎来到PyQt5系列的第6篇!上一章我们通过“简易文本编辑器”实战,巩固了线性布局(QVBoxLayout/QHBoxLayout)的用法。但在实际开发中,很多复杂界面(比如计算器、用户信息表单、数据展示表格)无法只用线性布局满足——要么控件排列混乱,要么自适应效果差。今天我们就来学习两种进阶布局管理器:网格布局(QGridLayout)和表单布局(QFormLayout),彻底解决复杂界面的排版难题! 一、先明确:进阶布局的核心作用 在学习具体布局前,先搞清楚两种布局的适用场景,避免用错地方: 网格布局(QGridLayout):控件按“行×列”的网格排列,适合需要精准控制控件位置的场景(如计算器按钮、表格数据展示);支持控件跨多行/多列,灵活性极高; 表单布局(QFormLayout):专门用于“标签+输入框”的表单场景(如用户注册/登录表单、信息填写窗口),自动对齐标签和输入框,界面规整且开发高效。 核心优势:两种布局都支持自适应——窗口缩放时,控件会按预设规则自动调整大小和位置,无需手动计算坐标。 二、网格布局(QGridLayout)详解:从基础到实战 网格布局的核心逻辑是“划分网格、给控件分配行和列”,比如将界面划分为3行3列,每个控件占1个“格子”,也可以让控件占2行1列(跨行吗)、1行2列(跨列)。 1. 网格布局基础用法(完整代码) 先实现一个简单的3×3网格,放置9个按钮,演示基础的行、列分配: import sys from PyQt5.QtWidgets import ( QApplication, QWidget, QPushButton, QGridLayout, QLabel ) from PyQt5.QtCore import Qt class GridLayoutDemo(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("QGridLayout网格布局基础演示") self.resize(400, 300) # 1. 创建网格布局实例 grid_layout = QGridLayout() # 设置控件间距(格子之间的距离) grid_layout.setSpacing(10) # 设置边距(布局与窗口边缘的距离) grid_layout.setContentsMargins(20, 20, 20, 20) # 2. 创建控件并添加到网格布局 # 核心方法:addWidget(控件, 行号, 列号, 占用行数, 占用列数) # 行号、列号从0开始;占用行数/列数默认是1(可不写) for i in range(3): # 行:0、1、2 for j in range(3): # 列:0、1、2 btn = QPushButton(f"按钮({i},{j})") # 添加到网格的(i,j)位置,占1行1列 grid_layout.addWidget(btn, i, j) # 3. 额外添加一个跨列控件(占1行2列) cross_col_btn = QPushButton("跨2列按钮") grid_layout.addWidget(cross_col_btn, 3, 0, 1, 2) # 第4行(索引3),第0列,占1行2列 # 4. 额外添加一个跨行控件(占2行1列) cross_row_btn = QPushButton("跨2行按钮") grid_layout.addWidget(cross_row_btn, 0, 3, 2, 1) # 第0行,第4列(索引3),占2行1列 # 5. 将布局绑定到窗口 self.setLayout(grid_layout) if __name__ == "__main__": app = QApplication(sys.argv) window = GridLayoutDemo() window.show() sys.exit(app.exec_())mjw55cag.png图片 2. 网格布局关键方法解析 网格布局的核心是addWidget()方法,参数含义必须记牢: addWidget(控件, row, column, rowSpan, columnSpan) # row:控件所在的行号(从0开始) # column:控件所在的列号(从0开始) # rowSpan:控件占用的行数(默认1,即不跨行) # columnSpan:控件占用的列数(默认1,即不跨列)其他常用方法: setSpacing(像素值):设置相邻控件之间的间距,避免控件挤在一起; setContentsMargins(左, 上, 右, 下):设置布局与窗口边缘的距离,提升界面美观度; setRowStretch(行号, 拉伸系数):设置某一行的拉伸权重(窗口缩放时,拉伸系数大的行占更多空间); setColumnStretch(列号, 拉伸系数):设置某一列的拉伸权重,同理。 3. 网格布局实战:简易计算器界面(核心场景) 计算器是网格布局的典型应用——按钮按固定网格排列,部分按钮(如“0”“=”)跨列。我们实现一个简易计算器的界面(含输入框+按钮区): import sys from PyQt5.QtWidgets import ( QApplication, QWidget, QPushButton, QGridLayout, QLineEdit ) from PyQt5.QtCore import Qt class SimpleCalculator(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("简易计算器(网格布局实战)") self.resize(350, 400) # 1. 创建主布局(垂直布局:输入框在上,按钮区在下) main_layout = QGridLayout() main_layout.setSpacing(10) main_layout.setContentsMargins(15, 15, 15, 15) # 2. 创建输入框(占1行4列) self.input_edit = QLineEdit() self.input_edit.setAlignment(Qt.AlignRight) # 文本右对齐(符合计算器习惯) self.input_edit.setStyleSheet("font-size: 20px; padding: 10px;") self.input_edit.setReadOnly(True) # 输入框只读,通过按钮输入 main_layout.addWidget(self.input_edit, 0, 0, 1, 4) # 第0行,第0列,占1行4列 # 3. 定义计算器按钮文本(按网格顺序排列) btn_texts = [ '7', '8', '9', '/', '4', '5', '6', '*', '1', '2', '3', '-', '0', '.', '=', '+', 'C' # 清空按钮 ] # 4. 给按钮分配行和列,添加到网格布局 row = 1 # 从第1行开始(第0行是输入框) col = 0 for text in btn_texts: btn = QPushButton(text) btn.setStyleSheet("font-size: 16px; padding: 15px;") # 特殊处理:0按钮跨2列,C按钮跨2列 if text == '0': main_layout.addWidget(btn, row, col, 1, 2) # 占1行2列 col += 2 # 列号+2(跳过一个格子) elif text == 'C': main_layout.addWidget(btn, row, col, 1, 2) # 占1行2列 col += 2 else: main_layout.addWidget(btn, row, col) # 默认占1行1列 col += 1 # 每4列换行(计算器是4列按钮) if col >= 4: col = 0 row += 1 # 5. 绑定布局到窗口 self.setLayout(main_layout) # 6. 绑定按钮点击信号(简单演示:点击按钮显示文本到输入框) for i in range(main_layout.count()): widget = main_layout.itemAt(i).widget() if isinstance(widget, QPushButton): widget.clicked.connect(self.on_btn_click) def on_btn_click(self): """按钮点击槽函数:将按钮文本显示到输入框""" sender = self.sender() text = sender.text() if text == 'C': # 清空输入框 self.input_edit.clear() elif text == '=': # 简单计算(实际项目需处理异常,此处简化) try: result = eval(self.input_edit.text()) self.input_edit.setText(str(result)) except: self.input_edit.setText("错误") else: # 拼接文本 current_text = self.input_edit.text() self.input_edit.setText(current_text + text) if __name__ == "__main__": app = QApplication(sys.argv) calculator = SimpleCalculator() calculator.show() sys.exit(app.exec_())4. 计算器界面亮点 用网格布局精准还原计算器的按钮排列,“0”和“C”按钮跨列,符合实际计算器的交互习惯; 结合垂直布局的思路,将输入框和按钮区整合,界面层次清晰; 实现了基础的计算逻辑(数字拼接、清空、结果计算),信号与槽绑定简洁高效。 三、表单布局(QFormLayout)详解:高效实现表单界面 表单布局是“标签+输入框”的专用布局,无需手动调整对齐方式——它会自动将标签放在左侧、输入控件放在右侧,且所有标签和输入框分别对齐,开发效率极高。 1. 表单布局基础用法(完整代码) 实现一个简单的用户注册表单,包含“用户名、密码、邮箱、电话”四个字段: import sys from PyQt5.QtWidgets import ( QApplication, QWidget, QLabel, QLineEdit, QPushButton, QFormLayout, QVBoxLayout, QComboBox ) from PyQt5.QtCore import Qt class FormLayoutDemo(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("QFormLayout表单布局基础演示") self.resize(400, 300) # 1. 创建主布局(垂直布局:表单在上,按钮在下) main_layout = QVBoxLayout() main_layout.setSpacing(20) main_layout.setContentsMargins(30, 30, 30, 30) # 2. 创建表单布局实例 form_layout = QFormLayout() form_layout.setSpacing(15) # 标签与输入框、行与行之间的间距 # 3. 创建标签和输入控件,添加到表单布局 # 核心方法:addRow(标签文本/标签控件, 输入控件) # 方式1:直接传标签文本(自动生成QLabel) form_layout.addRow("用户名:", QLineEdit()) # 方式2:传QLabel控件(可自定义标签样式) pwd_label = QLabel("密码:") pwd_label.setStyleSheet("color: #e74c3c;") pwd_edit = QLineEdit() pwd_edit.setEchoMode(QLineEdit.Password) # 密码隐藏 form_layout.addRow(pwd_label, pwd_edit) # 方式3:输入控件可以是其他类型(如下拉框) form_layout.addRow("性别:", QComboBox()) # 方式4:添加提示标签(跨两列) tip_label = QLabel("* 带星号的为必填项") tip_label.setStyleSheet("color: #95a5a6; font-size: 12px;") form_layout.addRow(tip_label) # 无输入控件时,标签跨两列 # 4. 调整标签对齐方式(默认左对齐,可改为右对齐) form_layout.setLabelAlignment(Qt.AlignRight) # 5. 创建提交按钮 submit_btn = QPushButton("提交表单") submit_btn.setStyleSheet("background-color: #3498db; color: white; padding: 8px;") # 6. 将表单布局和按钮添加到主布局 main_layout.addLayout(form_layout) main_layout.addWidget(submit_btn, alignment=Qt.AlignCenter) # 7. 绑定布局到窗口 self.setLayout(main_layout) if __name__ == "__main__": app = QApplication(sys.argv) window = FormLayoutDemo() window.show() sys.exit(app.exec_())2. 表单布局关键方法解析 addRow(标签, 输入控件):核心方法,添加一行表单;标签可以是字符串(自动生成QLabel)或QLabel实例,输入控件可以是QLineEdit、QComboBox等任意交互控件; setLabelAlignment(对齐方式):设置标签的对齐方式(如Qt.AlignRight让标签右对齐,与输入框间距更紧凑); setSpacing(像素值):设置“标签与输入框之间”和“相邻行之间”的间距; setFieldGrowthPolicy(策略):设置输入控件的拉伸策略(如QFormLayout.ExpandingFieldsGrow让输入控件随窗口缩放而拉伸)。 3. 表单布局实战:完整用户信息登记表单 整合表单布局和之前学的控件,实现一个完整的用户信息登记表单,包含输入框、下拉框、复选框,添加表单验证逻辑: import sys from PyQt5.QtWidgets import ( QApplication, QWidget, QLabel, QLineEdit, QPushButton, QFormLayout, QVBoxLayout, QComboBox, QCheckBox, QMessageBox ) from PyQt5.QtCore import Qt class UserInfoForm(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("用户信息登记表单(表单布局实战)") self.resize(450, 350) self.setStyleSheet("font-size: 14px; color: #2c3e50;") # 1. 主布局 main_layout = QVBoxLayout() main_layout.setSpacing(20) main_layout.setContentsMargins(30, 30, 30, 30) # 2. 表单布局 form_layout = QFormLayout() form_layout.setSpacing(15) form_layout.setLabelAlignment(Qt.AlignRight) # 设置输入控件拉伸(随窗口缩放) form_layout.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow) # 3. 表单控件初始化 self.user_edit = QLineEdit() self.user_edit.setPlaceholderText("请输入用户名(3-10个字符)") self.pwd_edit = QLineEdit() self.pwd_edit.setEchoMode(QLineEdit.Password) self.pwd_edit.setPlaceholderText("请输入密码(6-16个字符)") self.email_edit = QLineEdit() self.email_edit.setPlaceholderText("请输入邮箱(如xxx@xxx.com)") self.gender_combo = QComboBox() self.gender_combo.addItems(["男", "女", "保密"]) self.phone_edit = QLineEdit() self.phone_edit.setPlaceholderText("请输入手机号(11位数字)") # 4. 添加表单行 form_layout.addRow("用户名*:", self.user_edit) form_layout.addRow("密码*:", self.pwd_edit) form_layout.addRow("邮箱*:", self.email_edit) form_layout.addRow("性别:", self.gender_combo) form_layout.addRow("手机号:", self.phone_edit) # 同意条款复选框(跨两列) self.agree_check = QCheckBox("我已阅读并同意《用户服务条款》") form_layout.addRow(self.agree_check) # 5. 提交按钮 self.submit_btn = QPushButton("提交信息") self.submit_btn.setStyleSheet(""" QPushButton { background-color: #27ae60; color: white; padding: 10px; border: none; border-radius: 4px; } QPushButton:hover { background-color: #219653; } """) # 6. 结果显示标签 self.result_label = QLabel("") self.result_label.setAlignment(Qt.AlignCenter) # 7. 添加到主布局 main_layout.addLayout(form_layout) main_layout.addWidget(self.submit_btn, alignment=Qt.AlignCenter) main_layout.addWidget(self.result_label) # 8. 绑定布局和信号 self.setLayout(main_layout) self.submit_btn.clicked.connect(self.check_form) def check_form(self): """表单验证逻辑:检查必填项、格式正确性""" # 1. 获取表单内容 username = self.user_edit.text().strip() password = self.pwd_edit.text().strip() email = self.email_edit.text().strip() gender = self.gender_combo.currentText() phone = self.phone_edit.text().strip() # 2. 验证必填项 if not username: QMessageBox.warning(self, "验证失败", "用户名不能为空!") return if not password: QMessageBox.warning(self, "验证失败", "密码不能为空!") return if not email: QMessageBox.warning(self, "验证失败", "邮箱不能为空!") return if not self.agree_check.isChecked(): QMessageBox.warning(self, "验证失败", "请同意用户服务条款!") return # 3. 验证格式(简化版) if len(username) < 3 or len(username) > 10: QMessageBox.warning(self, "验证失败", "用户名长度需在3-10个字符之间!") return if len(password) < 6 or len(password) > 16: QMessageBox.warning(self, "验证失败", "密码长度需在6-16个字符之间!") return if "@" not in email: QMessageBox.warning(self, "验证失败", "邮箱格式不正确(需包含@)!") return if phone and len(phone) != 11: QMessageBox.warning(self, "验证失败", "手机号需为11位数字!") return # 4. 验证通过,显示结果 result = f""" 信息提交成功! 用户名:{username} 性别:{gender} 邮箱:{email} 手机号:{phone if phone else "未填写"} """ self.result_label.setText(result) self.result_label.setStyleSheet("color: #27ae60;") if __name__ == "__main__": app = QApplication(sys.argv) form = UserInfoForm() form.show() sys.exit(app.exec_())5. 实战表单亮点 用表单布局快速实现规整的表单界面,标签右对齐,输入框自适应拉伸,开发效率高; 添加了完整的表单验证逻辑(必填项检查、格式验证),用QMessageBox弹出提示,提升用户体验; 结合了多种控件(输入框、下拉框、复选框),覆盖实际表单的常见场景; 美化了按钮样式(悬停效果、圆角),界面更美观。 四、布局嵌套技巧:复杂界面的核心思路 实际开发中,很少用单一布局完成复杂界面,而是通过“布局嵌套”组合使用——比如“主布局(垂直)”包含“表单布局”和“网格布局”,再搭配“水平布局”排列按钮。 嵌套核心原则: 先划分界面大区域(如“顶部标题区、中间内容区、底部按钮区”),用主布局(垂直/水平)管理; 每个小区域内部,根据控件类型选择合适的子布局(网格/表单/线性); 用addLayout()方法将子布局添加到主布局,实现层次化管理。 五、常见问题排查 问题1:网格布局控件重叠/位置错乱 → 解决:检查行号、列号是否正确,避免多个控件分配到同一个格子;跨行列时注意rowSpan和columnSpan的数值; 问题2:表单布局标签和输入框不对齐 → 解决:用setLabelAlignment()统一标签对齐方式;避免手动设置输入控件的固定宽度(让布局自动适配); 问题3:窗口缩放时控件不拉伸 → 解决:给布局添加拉伸系数(setRowStretch/setColumnStretch),或设置输入控件的拉伸策略(如表单布局的setFieldGrowthPolicy); 问题4:布局嵌套后界面混乱 → 解决:先画界面草图,明确大区域和子区域的划分;给每个布局添加setSpacing和setContentsMargins,避免间距混乱。 总结 本章我们掌握了两种进阶布局管理器的核心用法: 网格布局(QGridLayout):适合“行×列”的规整排列场景(如计算器),支持控件跨行列,灵活性极高; 表单布局(QFormLayout):专门用于“标签+输入框”的表单场景,自动对齐,开发效率高; 布局嵌套是复杂界面的核心思路,先划分大区域,再用子布局管理小区域。 下一章我们将学习PyQt5的文本与表格控件(QTextEdit与QTableWidget),进一步拓展界面的功能边界。如果在布局实操中遇到问题,或者有复杂界面的排版需求,欢迎在评论区留言讨论~ -
Python 微信自动回复工具 | 带 PyQt5 图形界面 支持 Excel 关键词配置 微信自动回复工具(带PyQt界面) 最近帮朋友处理微信客服消息,重复回复太费时间,干脆写了个带图形界面的自动回复工具。不用记命令,填个Excel路径点按钮就能跑,还能实时看日志,日常用着挺顺手。下面把完整代码和用法贴出来,有需要的可以直接拿去改。 mjr3m247.png图片 先说说要准备的东西 环境:Python 3.8+(版本太高可能和pywin32不兼容) 要装的库:直接复制下面的命令到cmd里跑 pip install pywin32 pandas pyqt5 openpyxlmjr3dd3v.png图片 Excel回复表:建个Excel文件,第一列叫“关键词”,第二列叫“回复内容”,比如这样: 关键词回复内容你好您好~有什么可以帮您?下班时间我们18点下班,急事可留言~价格具体报价请发需求给我哦~mjr3ggkk.png图片 没有的朋友们不要急,我提供了默认数据文件下载,方便测试 微信自动回复表 下载地址:https://pan.quark.cn/s/75a06eed928c 提取码: 完整代码 import sys import time import win32gui import win32api import win32con import pandas as pd from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QLabel, QLineEdit, QPushButton, QTextEdit, QVBoxLayout, QHBoxLayout, QFileDialog) from PyQt5.QtCore import QThread, pyqtSignal, Qt from PyQt5.QtGui import QFont, QPalette, QColor # 消息监听线程:单独开线程是为了不让界面卡住 class ReplyWorker(QThread): # 用来给界面发日志的信号 log_send = pyqtSignal(str) # 用来告诉界面线程是否正常启动 thread_status = pyqtSignal(bool) def __init__(self, excel_path): super().__init__() self.excel_path = excel_path self.reply_rules = {} # 存关键词和回复的对应关系 self.wechat_handle = 0 # 微信窗口的句柄 self.running = False # 控制线程运行的开关 def load_excel_rules(self): """加载Excel里的关键词和回复""" try: # 用openpyxl读xlsx格式,避免编码问题 df = pd.read_excel(self.excel_path, engine='openpyxl') # 转成字典,查起来快 self.reply_rules = dict(zip(df['关键词'], df['回复内容'])) self.log_send.emit(f"回复表加载成功!共{len(self.reply_rules)}条规则") return True except Exception as e: # 捕获各种错误:文件没找到、格式不对、列名错 err_msg = f"加载Excel失败:{str(e)}" self.log_send.emit(err_msg) return False def find_wechat(self): """找到微信窗口,返回是否找到""" # 微信PC端的窗口名一般是“微信”,类名留空不限制 self.wechat_handle = win32gui.FindWindow(None, "微信") if self.wechat_handle == 0: self.log_send.emit("没找到微信窗口!请先打开微信并登录") return False # 把微信窗口提到最前面 win32gui.SetForegroundWindow(self.wechat_handle) self.log_send.emit(f"找到微信啦!窗口句柄:{self.wechat_handle}") return True def find_child(self, parent_handle, class_name=None): """找窗口里的子控件(比如输入框、发送按钮)""" child_handles = [] # 递归遍历所有子控件 def _enum_child(hwnd, extra): if class_name is None or win32gui.GetClassName(hwnd) == class_name: extra.append(hwnd) return True win32gui.EnumChildWindows(parent_handle, _enum_child, child_handles) # 返回第一个找到的控件(一般够用) return child_handles[0] if child_handles else 0 def get_last_msg(self, chat_panel): """获取聊天面板里最后一条消息""" # 微信聊天面板的文本就是所有消息,按换行分割取最后一行 all_msg = win32gui.GetWindowText(chat_panel) if all_msg: return all_msg.split("\n")[-1].strip() return "" def send_reply(self, input_box, send_btn, reply_content): """模拟输入并发送回复""" # 先清空输入框:选中所有文本再删除 win32gui.SendMessage(input_box, win32con.EM_SETSEL, 0, -1) win32gui.SendMessage(input_box, win32con.WM_CLEAR, 0, 0) # 逐个字符输入(太快会乱码,加个小延迟) for char in reply_content: win32gui.SendMessage(input_box, win32con.WM_CHAR, ord(char), 0) time.sleep(0.02) # 点击发送按钮 win32gui.SendMessage(send_btn, win32con.BM_CLICK, 0, 0) def run(self): """线程主逻辑:启动后一直监听""" self.running = True # 先加载规则和找微信,有一个失败就退出 if not self.load_excel_rules() or not self.find_wechat(): self.thread_status.emit(False) self.running = False return self.thread_status.emit(True) # 定位微信的核心控件(不同版本可能要改class_name,用Spy++看) session_list = self.find_child(self.wechat_handle, "ContactPanel") # 会话列表 input_box = self.find_child(self.wechat_handle, "Edit") # 输入框 send_btn = self.find_child(self.wechat_handle, "Button") # 发送按钮 chat_panel = self.find_child(self.wechat_handle, "ChatPanel") # 聊天面板 # 检查控件是否都找到 if not all([session_list, input_box, send_btn, chat_panel]): self.log_send.emit("没找到微信的核心控件!可能版本不兼容") self.running = False return self.log_send.emit("所有控件已定位,开始监听消息...") # 循环监听未读消息 while self.running: # 遍历所有会话找带“未读”的 sessions = [] win32gui.EnumChildWindows(session_list, lambda hwnd, extra: extra.append(hwnd), sessions) for session in sessions: session_text = win32gui.GetWindowText(session) if "未读" in session_text: # 点击未读会话,切换到聊天界面 win32gui.SendMessage(session, win32con.BM_CLICK, 0, 0) time.sleep(0.5) # 等消息加载出来 # 提取联系人(去掉“未读”字样) contact = session_text.replace("未读", "").strip() # 提取最后一条消息 last_msg = self.get_last_msg(chat_panel) if not last_msg: continue self.log_send.emit(f"\n收到[{contact}]的消息:{last_msg}") # 匹配关键词找回复 reply = "抱歉呀,我暂时没理解你的意思~" for keyword, content in self.reply_rules.items(): if keyword in last_msg: reply = content break self.log_send.emit(f"准备回复:{reply}") # 发送回复 self.send_reply(input_box, send_btn, reply) self.log_send.emit("回复发送成功!") # 每2秒查一次,别占太多CPU time.sleep(2) def stop(self): """停止线程""" self.running = False self.log_send.emit("\n监听已停止") # 主界面窗口 class ReplyWindow(QMainWindow): def __init__(self): super().__init__() self.worker = None # 监听线程对象 self.init_ui() # 初始化界面 def init_ui(self): """画界面:布局、按钮、输入框这些""" # 窗口基本设置 self.setWindowTitle("微信自动回复工具 v1.0 - 作者:寒烟似雪 2025/12/29发布在字节曜www.ziyeyao.com博客") self.setFixedSize(1200, 1000) # 固定大小,不允许拉伸 self.setStyleSheet("background-color: #f5f5f5;") # 中心部件(主窗口必须有个中心部件才能放内容) central_widget = QWidget() self.setCentralWidget(central_widget) # 主布局(垂直排列:路径输入→按钮→日志) main_layout = QVBoxLayout(central_widget) main_layout.setSpacing(15) # 控件之间的间距 main_layout.setContentsMargins(25, 25, 25, 25) # 边距 # 1. Excel路径选择区域(水平排列:标签→输入框→选择按钮) path_layout = QHBoxLayout() # 路径标签 path_label = QLabel("回复表路径:") path_label.setFont(QFont("微软雅黑", 10)) # 路径输入框 self.path_edit = QLineEdit() self.path_edit.setFont(QFont("微软雅黑", 9)) self.path_edit.setPlaceholderText("点击右侧按钮选择Excel文件") self.path_edit.setStyleSheet(""" QLineEdit { padding: 6px; border: 1px solid #ddd; border-radius: 4px; background-color: white; } """) # 路径选择按钮 self.select_btn = QPushButton("选择文件") self.select_btn.setFont(QFont("微软雅黑", 9)) self.select_btn.setStyleSheet(""" QPushButton { padding: 6px 12px; border: none; border-radius: 4px; background-color: #4285f4; color: white; } QPushButton:hover { background-color: #3367d6; } """) self.select_btn.clicked.connect(self.select_excel) # 绑定选择文件事件 # 把三个控件加到水平布局里 path_layout.addWidget(path_label) path_layout.addWidget(self.path_edit, stretch=1) # 输入框占满剩余空间 path_layout.addWidget(self.select_btn, stretch=0) main_layout.addLayout(path_layout) # 2. 启动/停止按钮区域(水平排列) btn_layout = QHBoxLayout() # 启动按钮 self.start_btn = QPushButton("启动监听") self.start_btn.setFont(QFont("微软雅黑", 10)) self.start_btn.setStyleSheet(""" QPushButton { padding: 8px 0; border: none; border-radius: 4px; background-color: #34a853; color: white; } QPushButton:disabled { background-color: #a0d9a0; } QPushButton:hover:enabled { background-color: #2d8d46; } """) self.start_btn.clicked.connect(self.start_listen) # 绑定启动事件 # 停止按钮 self.stop_btn = QPushButton("停止监听") self.stop_btn.setFont(QFont("微软雅黑", 10)) self.stop_btn.setStyleSheet(""" QPushButton { padding: 8px 0; border: none; border-radius: 4px; background-color: #ea4335; color: white; } QPushButton:disabled { background-color: #e8a098; } QPushButton:hover:enabled { background-color: #d33526; } """) self.stop_btn.clicked.connect(self.stop_listen) # 绑定停止事件 self.stop_btn.setEnabled(False) # 初始状态:停止按钮禁用 # 两个按钮平分宽度 btn_layout.addWidget(self.start_btn, stretch=1) btn_layout.addSpacing(10) # 按钮之间的间距 btn_layout.addWidget(self.stop_btn, stretch=1) main_layout.addLayout(btn_layout) # 3. 日志显示区域 # 日志标签 log_label = QLabel("运行日志:") log_label.setFont(QFont("微软雅黑", 10)) main_layout.addWidget(log_label) # 日志文本框(只读) self.log_edit = QTextEdit() self.log_edit.setFont(QFont("Consolas", 9)) self.log_edit.setReadOnly(True) self.log_edit.setStyleSheet(""" QTextEdit { padding: 8px; border: 1px solid #ddd; border-radius: 4px; background-color: white; color: #333; } """) main_layout.addWidget(self.log_edit, stretch=1) # 日志框占满剩余空间 def select_excel(self): """选择Excel文件,把路径填到输入框""" # 打开文件选择对话框,只显示Excel文件 file_path, _ = QFileDialog.getOpenFileName( self, "选择回复表", "", "Excel Files (*.xlsx; *.xls)" ) if file_path: self.path_edit.setText(file_path) def start_listen(self): """启动监听线程""" # 先检查路径是否填了 excel_path = self.path_edit.text().strip() if not excel_path: self.add_log("请先选择Excel回复表!") return # 检查线程是否已经在跑了 if self.worker and self.worker.isRunning(): self.add_log(" 监听已经在运行啦,不用重复启动") return # 创建线程对象,绑定信号 self.worker = ReplyWorker(excel_path) self.worker.log_send.connect(self.add_log) # 接收日志信号 self.worker.thread_status.connect(self.set_btn_status) # 接收线程状态信号 self.worker.finished.connect(self.thread_finished) # 线程结束时的信号 # 启动线程 self.worker.start() # 暂时禁用启动按钮 self.start_btn.setEnabled(False) self.add_log("正在初始化监听...") def stop_listen(self): """停止监听线程""" if self.worker and self.worker.isRunning(): self.worker.stop() # 禁用停止按钮,启用启动按钮 self.stop_btn.setEnabled(False) self.start_btn.setEnabled(True) else: self.add_log(" 监听还没启动呢,不用停止") def add_log(self, msg): """往日志框里加内容,自动滚到最下面""" # 加个时间戳,方便看什么时候发生的 time_str = time.strftime("[%H:%M:%S]", time.localtime()) self.log_edit.append(f"{time_str} {msg}") # 自动滚动到最后一行 self.log_edit.moveCursor(self.log_edit.textCursor().End) def set_btn_status(self, is_running): """根据线程状态设置按钮是否可用""" self.start_btn.setEnabled(not is_running) self.stop_btn.setEnabled(is_running) def thread_finished(self): """线程结束时的处理""" self.set_btn_status(False) self.add_log("监听线程已结束") def closeEvent(self, event): """窗口关闭时,确保线程也停了""" if self.worker and self.worker.isRunning(): self.worker.stop() self.worker.wait() # 等线程彻底结束 event.accept() def add_log(self, msg): """往日志里加内容(单独写个方法,方便调用)""" time_str = time.strftime("[%H:%M:%S]", time.localtime()) self.log_edit.append(f"{time_str} {msg}") self.log_edit.ensureCursorVisible() # 自动滚屏 # 程序入口 if __name__ == "__main__": app = QApplication(sys.argv) window = ReplyWindow() window.show() sys.exit(app.exec_()) 怎么用? 先建好转发的Excel表(列名必须是“关键词”和“回复内容”) 运行代码,会弹出一个窗口 点击“选择文件”,找到你建的Excel表 点击“启动监听”,然后打开微信(必须是PC端,登录状态) 有人发消息含关键词,就会自动回复了,日志里能看到过程 不想用了就点“停止监听”,或者直接关窗口 mjr3et38.png图片 注意事项 微信版本兼容问题 这个工具是靠pywin32识别微信窗口控件来实现的,不同版本的微信,控件的类名(比如ContactPanel、ChatPanel)可能不一样。如果运行时提示“没找到核心控件”,可以用Spy++(VS自带工具)查看你电脑上微信的控件类名,然后修改代码里find_child方法传入的class_name参数。 必须打开微信PC端 工具没法模拟微信登录,运行前一定要手动打开微信PC端并登录账号,而且最好不要最小化微信窗口,否则可能识别不到控件。 Excel格式要求 Excel文件里必须有两列,列名严格对应“关键词”和“回复内容”,建议保存为.xlsx格式,避免编码问题。如果提示“加载Excel失败”,检查一下文件路径有没有中文或特殊字符,或者是不是被其他软件占用了。 避免重复回复 工具默认每2秒扫描一次未读消息,如果你担心同一条消息被重复回复,可以在代码里加一个“已处理消息列表”,把已经回复过的消息内容存进去,下次扫描时先判断是否在列表里。 权限问题 运行程序时,如果遇到“权限不足”的提示,右键点击Python.exe,选择“以管理员身份运行”,或者给当前用户授予窗口控制的权限。 常见问题解决 问题1:启动后提示“没找到微信窗口” 解决:确认微信PC端已经打开,并且窗口标题是“微信”(不是其他自定义标题);如果微信在任务栏隐藏,先点击显示出来。 问题2:能找到微信,但提示“没找到核心控件” 解决:用Spy++查看微信的会话列表、输入框、发送按钮的类名,替换代码里对应的class_name;比如有些版本的微信输入框类名是RichEdit20W。 问题3:回复内容发送乱码 解决:在send_reply方法里,延长字符输入的延迟时间,把time.sleep(0.02)改成time.sleep(0.05);同时确保Excel文件的编码是UTF-8。 问题4:线程启动后,界面卡死 解决:检查是不是把监听逻辑写在了主线程里——这个工具的监听代码在ReplyWorker线程里,和界面线程分离,不会卡界面;如果还是卡,大概率是控件识别耗时太长,可以减少扫描频率,把time.sleep(2)改成time.sleep(5)。 结语 这便是我制作的微信自动回复工具啦,有bug可以在评论区留言。有动手能力的朋友们可以尝试添加以下功能: 添加黑白名单:可以在Excel里加一列“是否启用”,或者单独建一个黑白名单文件,指定哪些联系人需要自动回复,哪些不需要。 支持多关键词匹配:现在是匹配到第一个关键词就回复,可以改成支持多个关键词同时匹配,比如“价格”和“优惠”同时出现时,回复特定内容。 定时启停:添加一个时间选择控件,设置每天的监听时间段,比如只在9:00-18:00运行,其他时间自动停止。 回复记录导出:把收到的消息和发送的回复记录到本地文件(比如CSV),方便后续查看和统计。 -
阶段一实战项目:仿照记事本开发简易文本编辑器(PyQt5完整代码) 阶段一实战项目:仿照记事本界面开发简易文本编辑器 哈喽~ 欢迎来到PyQt5系列的第5篇——阶段一实战项目!经过前4篇的学习,我们已经掌握了QWidget基础窗口、线性布局(QVBoxLayout/QHBoxLayout)、核心基础控件(标签、按钮、输入框、复选框等)以及信号与槽的基础用法。今天我们将把这些知识点整合起来,仿照Windows记事本的核心界面与基础功能,开发一个简易文本编辑器,实现“新建、打开、保存文本”“文本编辑”“字体加粗”等核心功能,让你快速掌握知识点的综合应用! mjr0cjri.png图片 一、项目需求分析:仿照记事本核心功能 我们聚焦Windows记事本的核心功能,本次项目实现以下需求: 界面需求:仿照记事本布局,包含“功能按钮区”(新建、打开、保存、字体加粗)和“文本编辑区”(多行文本输入/显示); 核心功能:新建空白文本、打开本地文本文件、保存文本到本地、文本加粗编辑; 交互需求:按钮点击反馈、打开/保存文件弹窗提示、文本编辑实时响应; 适配需求:窗口缩放时,文本编辑区自适应调整大小。 【界面参考】Windows记事本核心布局:顶部功能按钮区 + 中间大面积文本编辑区,我们简化实现核心按钮,保证界面简洁且功能完整。 mjr06wos.png图片 二、技术选型:贴合阶段一知识点 本次项目严格基于阶段一所学知识点,不引入新的复杂组件,技术栈如下: 窗口组件:QWidget(主窗口); 布局管理器:QVBoxLayout(垂直布局,管理按钮区和编辑区)、QHBoxLayout(水平布局,排列功能按钮); 核心控件:QPushButton(功能按钮)、QTextEdit(多行文本编辑区)、QCheckBox(字体加粗选择框); 交互核心:信号与槽(按钮点击、复选框状态变化绑定对应功能); 文件操作:基础文件读写(结合Python内置open函数)。 三、界面设计与实现步骤 我们采用“先搭框架,再填功能”的思路,分3步实现: 搭建主窗口与布局(垂直布局+水平布局组合); 添加控件(功能按钮、复选框、文本编辑区)并绑定布局; 实现控件信号与槽绑定,编写功能逻辑。 四、完整代码实现(可直接运行) mjr0a9z8.png图片 import sys import os from PyQt5.QtWidgets import ( QApplication, QWidget, QPushButton, QTextEdit, QCheckBox, QVBoxLayout, QHBoxLayout, QFileDialog ) from PyQt5.QtGui import QFont from PyQt5.QtCore import Qt class SimpleNotepad(QWidget): def __init__(self): super().__init__() # 初始化窗口基础属性 self.init_window() # 初始化控件与布局结构 self.init_widgets_layout() # 绑定信号与槽函数 self.init_signals_slots() # 记录当前打开的文件路径,初始为None表示新文件 self.current_file_path = None def init_window(self): """初始化窗口的标题、大小和位置""" self.setWindowTitle("简易文本编辑器(仿照记事本)") # 设置窗口初始尺寸 self.resize(800, 600) # 计算并设置窗口居中显示 screen_geometry = QApplication.desktop().availableGeometry() x = (screen_geometry.width() - self.width()) // 2 y = (screen_geometry.height() - self.height()) // 2 self.move(x, y) def init_widgets_layout(self): """创建所有界面控件并设置布局""" # 创建主垂直布局,设置控件间距和窗口内边距 self.main_layout = QVBoxLayout() self.main_layout.setSpacing(10) self.main_layout.setContentsMargins(15, 15, 15, 15) # 创建按钮区水平布局 self.button_layout = QHBoxLayout() self.button_layout.setSpacing(10) # 创建功能按钮并设置固定尺寸 self.new_btn = QPushButton("新建") self.open_btn = QPushButton("打开") self.save_btn = QPushButton("保存") btn_size = (80, 30) self.new_btn.setFixedSize(*btn_size) self.open_btn.setFixedSize(*btn_size) self.save_btn.setFixedSize(*btn_size) # 创建字体加粗复选框,设置文本居中显示 self.bold_check = QCheckBox("字体加粗") self.bold_check.setStyleSheet("text-align: center;") # 将按钮添加到水平布局 self.button_layout.addWidget(self.new_btn) self.button_layout.addWidget(self.open_btn) self.button_layout.addWidget(self.save_btn) # 添加伸缩项,将复选框推至布局右侧 self.button_layout.addStretch() self.button_layout.addWidget(self.bold_check) # 创建文本编辑区域,设置默认字体和占位提示文本 self.text_edit = QTextEdit() self.text_edit.setFont(QFont("微软雅黑", 12)) self.text_edit.setPlaceholderText("请输入文本内容...(支持新建、打开、保存文件)") # 将按钮布局和文本编辑区添加到主布局 self.main_layout.addLayout(self.button_layout) self.main_layout.addWidget(self.text_edit) # 设置窗口的主布局 self.setLayout(self.main_layout) def init_signals_slots(self): """绑定控件的信号与对应的槽函数""" self.new_btn.clicked.connect(self.on_new_click) self.open_btn.clicked.connect(self.on_open_click) self.save_btn.clicked.connect(self.on_save_click) self.bold_check.stateChanged.connect(self.on_bold_check_change) def on_new_click(self): """新建文件:清空编辑区,重置文件路径""" # 若有未保存内容,先提示保存 if self.text_edit.toPlainText() and not self.current_file_path: reply = QFileDialog.getSaveFileName(self, "保存当前内容", "", "Text Files (*.txt)") if reply[0]: self.save_text_to_file(reply[0]) # 清空编辑区内容 self.text_edit.clear() # 重置当前文件路径 self.current_file_path = None # 更新窗口标题 self.setWindowTitle("简易文本编辑器(仿照记事本)- 未保存文件") def on_open_click(self): """打开文件:选择txt文件并读取内容到编辑区""" # 弹出文件选择对话框,筛选文本文件 file_path, _ = QFileDialog.getOpenFileName( self, "打开文本文件", "", "Text Files (*.txt);;All Files (*.*)" ) # 验证文件路径有效性并读取内容 if file_path and os.path.exists(file_path): with open(file_path, "r", encoding="utf-8") as f: content = f.read() self.text_edit.setText(content) self.current_file_path = file_path # 更新窗口标题显示当前文件名 self.setWindowTitle(f"简易文本编辑器(仿照记事本)- {os.path.basename(file_path)}") def on_save_click(self): """保存文件:已有路径则直接保存,无路径则弹出保存对话框""" if self.current_file_path: # 直接保存到当前路径 self.save_text_to_file(self.current_file_path) else: # 弹出保存对话框选择路径 file_path, _ = QFileDialog.getSaveFileName( self, "保存文本文件", "", "Text Files (*.txt)" ) if file_path: # 自动补充txt后缀 if not file_path.endswith(".txt"): file_path += ".txt" self.save_text_to_file(file_path) self.current_file_path = file_path self.setWindowTitle(f"简易文本编辑器(仿照记事本)- {os.path.basename(file_path)}") def save_text_to_file(self, file_path): """将编辑区内容写入指定路径的文件""" content = self.text_edit.toPlainText() with open(file_path, "w", encoding="utf-8") as f: f.write(content) def on_bold_check_change(self, state): """根据复选框状态切换编辑区文本的加粗样式""" current_font = self.text_edit.font() # Qt.Checked对应值为2,Qt.Unchecked对应值为0 current_font.setBold(state == Qt.Checked) self.text_edit.setFont(current_font) if __name__ == "__main__": # 创建应用程序实例 app = QApplication(sys.argv) # 创建记事本窗口实例 notepad = SimpleNotepad() # 显示窗口 notepad.show() # 启动应用程序主循环 sys.exit(app.exec_())五、代码逐行解析(核心部分) 1. 类结构与初始化流程 我们将所有功能封装到SimpleNotepad类(继承QWidget),初始化流程分3步: init_window():设置窗口标题、大小、居中显示,提升用户体验; init_widgets_layout():核心布局搭建,用“垂直布局+水平布局”组合实现记事本风格,按钮区在上、编辑区在下,保证窗口缩放时编辑区自适应; init_signals_slots():绑定所有控件的信号与槽,实现“点击按钮触发功能”“复选框变化触发字体调整”。 2. 布局核心逻辑 采用“嵌套布局”思路,解决控件排列问题: 主布局(QVBoxLayout):垂直方向排列“按钮布局”和“文本编辑区”,addStretch()未使用,让编辑区占满剩余空间; 按钮布局(QHBoxLayout):水平方向排列3个功能按钮,添加addStretch()伸缩空间将“字体加粗”复选框推到右侧,让布局更美观。 3. 核心功能实现 (1)新建文件(on_new_click) 逻辑:先判断当前是否有未保存的内容,如果有则弹出保存对话框;清空编辑区,重置current_file_path(标记为新文件),更新窗口标题。 (2)打开文件(on_open_click) 逻辑:用QFileDialog.getOpenFileName()弹出文件选择框,筛选txt文件;读取选中文件的内容并显示到QTextEdit,更新current_file_path和窗口标题(显示文件名)。 (3)保存文件(on_save_click) 逻辑:如果已打开文件(current_file_path不为None),直接保存;否则弹出保存对话框,让用户选择路径,补充.txt后缀,保存内容后更新路径和标题。 (4)字体加粗(on_bold_check_change) 逻辑:监听复选框状态变化,用QFont.setBold()切换字体加粗状态,直接作用于QTextEdit的当前字体。 六、运行效果与测试步骤 1. 运行方式 将代码保存为simple_notepad.py,确保已安装PyQt5,终端运行命令: python simple_notepad.py # Windows python3 simple_notepad.py # macOS/Linux2. 测试步骤 测试新建:点击“新建”,编辑区清空,标题显示“未保存文件”; 测试打开:点击“打开”,选择本地txt文件,内容正常显示,标题显示文件名; 测试保存:编辑内容后点击“保存”,如果是新文件则弹出保存对话框,保存后可在对应路径找到txt文件; 测试字体加粗:勾选“字体加粗”,编辑区文本变为加粗;取消勾选则恢复正常。 七、常见问题排查 问题1:打开文件后中文乱码 → 解决:读取文件时指定encoding="utf-8",保存时也用utf-8编码; 问题2:窗口缩放时编辑区不自适应 → 解决:确保主布局是QVBoxLayout,且QTextEdit直接添加到主布局(未设置固定大小); 问题3:保存文件后没有.txt后缀 → 解决:代码中已添加判断,自动补充.txt后缀,无需手动输入; 问题4:按钮点击无反应 → 解决:检查信号与槽绑定是否正确(如self.new_btn.clicked.connect(self.on_new_click)是否写错函数名)。 八、功能拓展思路(阶段一知识点范围内) 如果想进一步练习,可以基于当前代码拓展以下功能: 添加“撤销/重做”按钮:利用QTextEdit.undo()和QTextEdit.redo()实现; 添加“清空”按钮:绑定self.text_edit.clear(); 添加“字体大小调整”:用QComboBox下拉选择字体大小,绑定信号修改QFont的setPointSize(); 添加“换行/不换行”复选框:用QTextEdit.setLineWrapMode()控制换行模式。 总结 本次项目完美整合了阶段一的核心知识点:窗口设置、线性布局、基础控件使用、信号与槽绑定。通过仿照记事本界面开发,你应该能深刻理解“布局管理器解决控件排列”“信号与槽实现交互”的核心逻辑。 下一篇我们将进入阶段二,学习网格布局、表单布局等进阶布局管理器,为更复杂的界面开发打基础。如果在项目实操中遇到问题,或者有拓展功能的想法,欢迎在评论区留言讨论~ -
PyQt5常用控件(二):复选框单选框下拉框 PyQt5常用基础控件(二):复选框、单选框与下拉框 哈喽~ 欢迎来到PyQt5从入门到精通的第四篇!上一篇我们掌握了标签、按钮、输入框这三个核心基础控件,这一篇聚焦进阶基础控件——QCheckBox复选框、QRadioButton单选框、QComboBox下拉框,这三类控件是“选择类交互”的核心(比如选择性别、爱好、职业),全程代码完整可直接运行,还会做一个“个人信息选择”综合案例,新手也能轻松掌握! mjqzsti2.png图片 一、核心控件详解:每个控件的用法+完整代码 这三类控件的核心作用是让用户做选择,但适用场景不同:单选框(二选一/多选一)、复选框(多选多)、下拉框(大量选项的精简选择),我们逐个拆解。 1. QCheckBox:复选框(多选多) QCheckBox用于允许多选的场景(比如选择爱好:读书、运动、听歌),支持“选中/未选中”状态,还能监听状态变化。 完整代码:QCheckBox常用用法 mjqzttv2.png图片 import sys from PyQt5.QtWidgets import ( QApplication, QWidget, QCheckBox, QVBoxLayout, QLabel, QPushButton ) from PyQt5.QtCore import Qt class CheckBoxDemo(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("QCheckBox复选框演示") self.resize(350, 250) layout = QVBoxLayout() layout.setSpacing(15) layout.setContentsMargins(50, 40, 50, 40) # 提示标签 self.tip_label = QLabel("请选择你的爱好(可多选):") layout.addWidget(self.tip_label) # 1. 创建复选框 self.check1 = QCheckBox("读书") self.check2 = QCheckBox("运动") self.check3 = QCheckBox("听歌") self.check4 = QCheckBox("编程") # 可选:设置默认选中状态 self.check4.setChecked(True) # 绑定状态变化信号(选中/取消选中时触发) self.check1.stateChanged.connect(self.on_check_change) self.check2.stateChanged.connect(self.on_check_change) self.check3.stateChanged.connect(self.on_check_change) self.check4.stateChanged.connect(self.on_check_change) # 添加到布局 layout.addWidget(self.check1) layout.addWidget(self.check2) layout.addWidget(self.check3) layout.addWidget(self.check4) # 按钮:获取选中的爱好 get_btn = QPushButton("获取选中的爱好") get_btn.clicked.connect(self.get_checked_hobbies) layout.addWidget(get_btn) # 结果显示标签 self.result_label = QLabel("已选中:编程") layout.addWidget(self.result_label) self.setLayout(layout) # 复选框状态变化槽函数 def on_check_change(self, state): # state:2=选中,0=未选中 sender = self.sender() # 获取触发信号的复选框 if state == Qt.Checked: print(f"选中了:{sender.text()}") else: print(f"取消选中:{sender.text()}") # 获取所有选中的爱好 def get_checked_hobbies(self): hobbies = [] if self.check1.isChecked(): hobbies.append(self.check1.text()) if self.check2.isChecked(): hobbies.append(self.check2.text()) if self.check3.isChecked(): hobbies.append(self.check3.text()) if self.check4.isChecked(): hobbies.append(self.check4.text()) if hobbies: self.result_label.setText(f"已选中:{', '.join(hobbies)}") else: self.result_label.setText("未选中任何爱好") if __name__ == "__main__": app = QApplication(sys.argv) window = CheckBoxDemo() window.show() sys.exit(app.exec_())QCheckBox关键方法解析 方法作用setChecked(True)设置默认选中状态(False为未选中)isChecked()判断是否选中(返回True/False)stateChanged.connect(槽函数)监听状态变化(选中/取消选中)text()获取复选框的文本内容sender()在槽函数中获取触发信号的控件(区分多个复选框)2. QRadioButton:单选框(多选一) QRadioButton用于只能选一个的场景(比如性别:男/女),核心是必须分组(QButtonGroup),否则多个单选框不会互斥(能同时选中)。 完整代码:QRadioButton常用用法 mjqzusvk.png图片 import sys from PyQt5.QtWidgets import ( QApplication, QWidget, QRadioButton, QVBoxLayout, QLabel, QPushButton, QButtonGroup ) class RadioButtonDemo(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("QRadioButton单选框演示") self.resize(350, 200) layout = QVBoxLayout() layout.setSpacing(15) layout.setContentsMargins(50, 40, 50, 40) # 提示标签 self.tip_label = QLabel("请选择你的性别(只能选一个):") layout.addWidget(self.tip_label) # 1. 创建单选框分组(核心!否则单选框不互斥) self.radio_group = QButtonGroup(self) # 2. 创建单选框 self.radio1 = QRadioButton("男") self.radio2 = QRadioButton("女") self.radio3 = QRadioButton("保密") # 3. 将单选框加入分组 self.radio_group.addButton(self.radio1, 1) # 第二个参数是自定义ID,可选 self.radio_group.addButton(self.radio2, 2) self.radio_group.addButton(self.radio3, 3) # 可选:设置默认选中 self.radio3.setChecked(True) # 绑定选中变化信号 self.radio_group.buttonClicked.connect(self.on_radio_click) # 添加到布局 layout.addWidget(self.radio1) layout.addWidget(self.radio2) layout.addWidget(self.radio3) # 按钮:获取选中的性别 get_btn = QPushButton("获取选中的性别") get_btn.clicked.connect(self.get_checked_gender) layout.addWidget(get_btn) # 结果显示标签 self.result_label = QLabel("已选中:保密") layout.addWidget(self.result_label) self.setLayout(layout) # 单选框点击槽函数 def on_radio_click(self, radio_btn): # radio_btn是被点击的单选框对象 print(f"选中了性别:{radio_btn.text()}") # 获取选中的性别 def get_checked_gender(self): # 方式1:通过分组获取选中的按钮 checked_btn = self.radio_group.checkedButton() if checked_btn: self.result_label.setText(f"已选中:{checked_btn.text()}") else: self.result_label.setText("未选中任何性别") # 方式2:逐个判断(不推荐,分组更高效) # if self.radio1.isChecked(): # self.result_label.setText("已选中:男") # elif self.radio2.isChecked(): # self.result_label.setText("已选中:女") # elif self.radio3.isChecked(): # self.result_label.setText("已选中:保密") if __name__ == "__main__": app = QApplication(sys.argv) window = RadioButtonDemo() window.show() sys.exit(app.exec_())QRadioButton关键要点 必须分组:用QButtonGroup管理单选框,否则多个单选框可同时选中; 核心方法: 方法作用QButtonGroup.addButton(单选框, ID)将单选框加入分组radio_group.checkedButton()获取分组中选中的单选框setChecked(True)设置默认选中isChecked()判断是否选中 3. QComboBox:下拉选择框(精简多选一) QComboBox用于选项较多、需要精简界面的场景(比如选择职业、城市),支持下拉展开选择,也能设置可编辑(允许用户输入)。 完整代码:QComboBox常用用法 mjqzvsjb.png图片 import sys from PyQt5.QtWidgets import ( QApplication, QWidget, QComboBox, QVBoxLayout, QLabel, QPushButton ) class ComboBoxDemo(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("QComboBox下拉框演示") self.resize(350, 200) layout = QVBoxLayout() layout.setSpacing(15) layout.setContentsMargins(50, 40, 50, 40) # 提示标签 self.tip_label = QLabel("请选择你的职业:") layout.addWidget(self.tip_label) # 1. 创建下拉框 self.combo = QComboBox() # 2. 添加选项(三种方式) # 方式1:逐个添加 self.combo.addItem("学生") self.combo.addItem("程序员") self.combo.addItem("教师") # 方式2:批量添加 self.combo.addItems(["设计师", "医生", "自由职业者"]) # 方式3:添加带自定义数据的选项(文本+值) self.combo.addItem("其他", "other") # 可选设置 self.combo.setCurrentIndex(1) # 设置默认选中第2个选项(索引从0开始) # self.combo.setEditable(True) # 允许用户输入自定义内容 # 绑定选中变化信号 self.combo.currentIndexChanged.connect(self.on_combo_change) # 索引变化 # self.combo.currentTextChanged.connect(self.on_text_change) # 文本变化 layout.addWidget(self.combo) # 按钮:获取选中的职业 get_btn = QPushButton("获取选中的职业") get_btn.clicked.connect(self.get_checked_job) layout.addWidget(get_btn) # 结果显示标签 self.result_label = QLabel("已选中:程序员") layout.addWidget(self.result_label) self.setLayout(layout) # 下拉框索引变化槽函数 def on_combo_change(self, index): # index是选中项的索引 text = self.combo.itemText(index) # 通过索引获取文本 data = self.combo.itemData(index) # 获取自定义数据(没有则返回None) print(f"选中索引:{index},职业:{text},自定义数据:{data}") # # 文本变化槽函数(当setEditable=True时常用) # def on_text_change(self, text): # print(f"选中/输入的职业:{text}") # 获取选中的职业 def get_checked_job(self): # 方式1:获取选中的文本 text = self.combo.currentText() # 方式2:获取选中的索引 index = self.combo.currentIndex() # 方式3:获取自定义数据 data = self.combo.currentData() self.result_label.setText(f"职业:{text}(索引:{index},自定义数据:{data})") if __name__ == "__main__": app = QApplication(sys.argv) window = ComboBoxDemo() window.show() sys.exit(app.exec_())QComboBox关键方法 方法作用addItem("文本", 自定义数据)添加单个选项addItems(["选项1", "选项2"])批量添加选项setCurrentIndex(索引)设置默认选中项(索引从0开始)currentText()获取选中的文本currentIndex()获取选中的索引setEditable(True)允许用户输入自定义内容二、综合案例:个人信息选择窗口(完整代码) 结合复选框、单选框、下拉框,做一个实用的“个人信息填写窗口”——包含性别单选、爱好复选、职业下拉,点击“提交”按钮后显示所有选中的信息,界面美化且逻辑完整。 mjqzxjc5.png图片 import sys from PyQt5.QtWidgets import ( QApplication, QWidget, QLabel, QRadioButton, QCheckBox, QComboBox, QPushButton, QVBoxLayout, QHBoxLayout, QGroupBox, QButtonGroup ) from PyQt5.QtGui import QFont from PyQt5.QtCore import Qt class InfoWindow(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): # 窗口基础设置 self.setWindowTitle("个人信息选择窗口") self.resize(450, 400) self.setStyleSheet(""" QWidget { font-size: 14px; color: #2c3e50; } QGroupBox { font-weight: bold; border: 1px solid #bdc3c7; border-radius: 6px; margin-top: 10px; padding-top: 10px; } QPushButton { background-color: #27ae60; color: white; padding: 8px 16px; border: none; border-radius: 4px; } QPushButton:hover { background-color: #219653; } """) # 主布局(垂直) main_layout = QVBoxLayout() main_layout.setSpacing(20) main_layout.setContentsMargins(40, 30, 40, 30) # 1. 性别分组(QGroupBox美化分组) gender_group = QGroupBox("性别") gender_layout = QHBoxLayout() # 单选框分组 self.gender_btn_group = QButtonGroup() self.radio_male = QRadioButton("男") self.radio_female = QRadioButton("女") self.radio_secret = QRadioButton("保密") self.gender_btn_group.addButton(self.radio_male) self.gender_btn_group.addButton(self.radio_female) self.gender_btn_group.addButton(self.radio_secret) # 默认选中保密 self.radio_secret.setChecked(True) # 添加到性别布局 gender_layout.addWidget(self.radio_male) gender_layout.addWidget(self.radio_female) gender_layout.addWidget(self.radio_secret) gender_group.setLayout(gender_layout) main_layout.addWidget(gender_group) # 2. 爱好分组 hobby_group = QGroupBox("爱好(可多选)") hobby_layout = QHBoxLayout() # 复选框 self.check_read = QCheckBox("读书") self.check_sport = QCheckBox("运动") self.check_music = QCheckBox("听歌") self.check_code = QCheckBox("编程") # 添加到爱好布局 hobby_layout.addWidget(self.check_read) hobby_layout.addWidget(self.check_sport) hobby_layout.addWidget(self.check_music) hobby_layout.addWidget(self.check_code) hobby_group.setLayout(hobby_layout) main_layout.addWidget(hobby_group) # 3. 职业分组 job_group = QGroupBox("职业") job_layout = QHBoxLayout() # 下拉框 self.combo_job = QComboBox() self.combo_job.addItems(["学生", "程序员", "教师", "设计师", "医生", "其他"]) job_layout.addWidget(QLabel("选择:")) job_layout.addWidget(self.combo_job) job_group.setLayout(job_layout) main_layout.addWidget(job_group) # 4. 提交按钮 submit_btn = QPushButton("提交信息") submit_btn.clicked.connect(self.submit_info) main_layout.addWidget(submit_btn, alignment=Qt.AlignCenter) # 5. 结果显示标签 self.result_label = QLabel("") self.result_label.setStyleSheet("color: #e67e22; margin-top: 10px;") self.result_label.setAlignment(Qt.AlignCenter) main_layout.addWidget(self.result_label) # 绑定主布局 self.setLayout(main_layout) # 提交信息槽函数 def submit_info(self): # 1. 获取性别 gender = self.gender_btn_group.checkedButton().text() # 2. 获取爱好 hobbies = [] if self.check_read.isChecked(): hobbies.append(self.check_read.text()) if self.check_sport.isChecked(): hobbies.append(self.check_sport.text()) if self.check_music.isChecked(): hobbies.append(self.check_music.text()) if self.check_code.isChecked(): hobbies.append(self.check_code.text()) hobby_text = ", ".join(hobbies) if hobbies else "无" # 3. 获取职业 job = self.combo_job.currentText() # 4. 显示结果 result = f""" 提交的信息: 性别:{gender} 爱好:{hobby_text} 职业:{job} """ self.result_label.setText(result) if __name__ == "__main__": app = QApplication(sys.argv) window = InfoWindow() window.show() sys.exit(app.exec_())综合案例亮点 用QGroupBox对控件分组,界面更规整、易读; 结合了三类选择控件,覆盖“单选、多选、下拉选”所有常用选择场景; 加入样式美化(按钮悬停、分组边框、字体颜色); 逻辑完整:提交后整合所有选中信息并显示,新手可直接复用。 三、常见问题排查 单选框不互斥: 未使用QButtonGroup分组,只需将所有单选框加入同一个分组即可; 分组时误将单选框加入不同分组(比如创建了多个QButtonGroup)。 复选框无法获取选中状态: 槽函数中未正确调用isChecked(),或控件实例名写错(比如check1写成check_1); 复选框被禁用(setDisabled(True)),导致无法选中。 下拉框选项不显示: 忘记调用addItem()/addItems()添加选项; 下拉框尺寸太小(可通过setMinimumWidth(100)设置最小宽度)。 提交后结果不显示: 槽函数未绑定到按钮的clicked信号; 结果标签被布局遮挡(可调整setContentsMargins或spacing)。 总结 三类选择控件的核心场景:QRadioButton(单选)、QCheckBox(多选)、QComboBox(精简单选); 单选框必须用QButtonGroup分组,否则无法实现互斥; 复选框通过isChecked()判断状态,下拉框通过currentText()获取选中内容; 实际开发中用QGroupBox分组控件,可提升界面可读性; 下一篇我们会讲解阶段一实战项目:仿照记事本界面开发简易文本编辑器,记得关注字节曜博客哦~ 如果在实操中遇到问题,欢迎在评论区留言讨论! -
PyQt5常用控件(一):标签按钮输入框+信号与槽入门(完整代码) PyQt5常用基础控件(一):标签、按钮与输入框+信号与槽入门(附完整代码) 哈喽~ 欢迎来到PyQt5从入门到精通的第三篇!上一篇我们搞定了QWidget窗口属性和线性布局,这一篇聚焦最常用的3个基础控件(QLabel标签、QPushButton按钮、QLineEdit输入框),再加上PyQt5交互的核心——信号与槽(Signal & Slot) 基础,手把手教你实现控件之间的互动,全程代码完整可直接运行,新手也能轻松拿捏! mjqzambw.png图片 一、核心控件详解:每个控件的用法+完整代码 这三个控件是PyQt5开发中最基础也最常用的,几乎所有桌面应用都会用到(比如登录窗口的用户名输入框、确认按钮、提示标签)。我们逐个拆解,先讲用法,再上代码,最后看效果。 1. QLabel:标签控件(显示文本/图片) QLabel的核心作用是显示内容,支持文本、图片、超链接等,是界面中的“信息展示员”。 完整代码:QLabel常用用法 import sys from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QVBoxLayout from PyQt5.QtGui import QPixmap, QFont from PyQt5.QtCore import Qt class LabelDemo(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("QLabel标签控件演示") self.resize(400, 350) # 创建布局(垂直布局,让控件从上到下排列) layout = QVBoxLayout() # 1. 显示普通文本 label1 = QLabel("这是普通文本标签") # 设置文本居中对齐 label1.setAlignment(Qt.AlignCenter) layout.addWidget(label1) # 2. 显示带样式的文本(字体大小、颜色) label2 = QLabel("这是带样式的文本") # 用StyleSheet设置样式(类似CSS) label2.setStyleSheet("font-size: 16px; color: #e74c3c; font-weight: bold;") label2.setAlignment(Qt.AlignCenter) layout.addWidget(label2) # 3. 显示图片(替换为你的图片路径,支持png/jpg等格式) label3 = QLabel() # 加载图片并缩放(保持比例) pixmap = QPixmap("test.png") # 图片放在代码同目录,直接写文件名 label3.setPixmap(pixmap.scaled(200, 200, Qt.KeepAspectRatio)) label3.setAlignment(Qt.AlignCenter) # 图片居中 layout.addWidget(label3) # 4. 显示超链接(可点击跳转) label4 = QLabel('<a href="https://www.ziyeyao.com">点击访问字节曜博客</a>') label4.setAlignment(Qt.AlignCenter) label4.setOpenExternalLinks(True) # 允许打开外部链接 layout.addWidget(label4) # 绑定布局到窗口 self.setLayout(layout) if __name__ == "__main__": app = QApplication(sys.argv) window = LabelDemo() window.show() sys.exit(app.exec_())mjqxgqbv.png图片 QLabel关键方法解析 方法作用setAlignment(Qt.AlignCenter)设置内容对齐(居中/左对齐/右对齐)setStyleSheet("样式")自定义样式(字体、颜色、背景等)setPixmap(QPixmap("图片路径"))显示图片,scaled() 用于缩放图片setOpenExternalLinks(True)启用超链接点击跳转2. QPushButton:按钮控件(触发交互) QPushButton是交互核心控件,用户点击按钮后会触发特定操作(比如登录、保存、关闭窗口),必须结合“信号与槽”使用才能实现交互。 完整代码:QPushButton常用用法+信号与槽基础 import sys from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QLabel from PyQt5.QtCore import Qt class ButtonDemo(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("QPushButton按钮控件演示") self.resize(300, 200) layout = QVBoxLayout() # 创建标签(用于显示按钮点击状态) self.status_label = QLabel("未点击按钮", alignment=Qt.AlignCenter) layout.addWidget(self.status_label) # 1. 普通按钮 btn1 = QPushButton("普通按钮") # 绑定信号与槽:按钮点击(信号)→ 执行on_btn1_click函数(槽) btn1.clicked.connect(self.on_btn1_click) layout.addWidget(btn1) # 2. 带图标+文本的按钮(图标路径替换为你的文件) btn2 = QPushButton("带图标按钮") btn2.setIcon(QPixmap("icon.png").scaled(20, 20, Qt.KeepAspectRatio)) btn2.clicked.connect(self.on_btn2_click) layout.addWidget(btn2) # 3. 禁用状态的按钮(无法点击) btn3 = QPushButton("禁用按钮") btn3.setDisabled(True) # 禁用按钮 layout.addWidget(btn3) self.setLayout(layout) # 槽函数:btn1点击后执行 def on_btn1_click(self): self.status_label.setText("点击了普通按钮!") self.status_label.setStyleSheet("color: #2ecc71;") # 槽函数:btn2点击后执行 def on_btn2_click(self): self.status_label.setText("点击了带图标按钮!") self.status_label.setStyleSheet("color: #3498db;") if __name__ == "__main__": app = QApplication(sys.argv) window = ButtonDemo() window.show() sys.exit(app.exec_())mjqz3lir.png图片 关键解析:信号与槽(核心!) 信号(Signal):控件的某个动作(比如按钮点击clicked、输入框内容变化textChanged); 槽(Slot):信号触发后执行的函数(比如on_btn1_click); 绑定方式:控件.信号.connect(槽函数),这是PyQt5交互的核心逻辑,记住这个公式! QPushButton关键方法 方法作用clicked.connect(槽函数)绑定点击信号与槽函数setIcon(QPixmap("图标路径"))设置按钮图标setDisabled(True)禁用按钮(False为启用)setText("按钮文本")动态修改按钮文本3. QLineEdit:单行输入框(获取用户输入) QLineEdit用于获取用户单行输入(比如用户名、密码、验证码),支持限制输入长度、密码隐藏、提示文本等功能。 完整代码:QLineEdit常用用法 import sys from PyQt5.QtWidgets import QApplication, QWidget, QLineEdit, QVBoxLayout, QLabel, QPushButton class LineEditDemo(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("QLineEdit输入框演示") self.resize(350, 250) layout = QVBoxLayout() layout.setSpacing(20) layout.setContentsMargins(50, 50, 50, 50) # 1. 普通输入框(带提示文本) self.edit1 = QLineEdit() self.edit1.setPlaceholderText("请输入用户名(最多10个字符)") self.edit1.setMaxLength(10) # 限制输入长度 layout.addWidget(self.edit1) # 2. 密码输入框(输入内容隐藏) self.edit2 = QLineEdit() self.edit2.setPlaceholderText("请输入密码") self.edit2.setEchoMode(QLineEdit.Password) # 密码隐藏模式 layout.addWidget(self.edit2) # 3. 只读输入框(无法编辑) edit3 = QLineEdit("只读文本,无法修改") edit3.setReadOnly(True) layout.addWidget(edit3) # 按钮:获取输入框内容 btn = QPushButton("获取输入内容") btn.clicked.connect(self.get_input_value) layout.addWidget(btn) # 标签:显示获取到的内容 self.result_label = QLabel("") layout.addWidget(self.result_label) self.setLayout(layout) # 槽函数:获取输入框内容并显示 def get_input_value(self): username = self.edit1.text() # 获取输入框1的内容 password = self.edit2.text() # 获取输入框2的内容 self.result_label.setText(f"用户名:{username},密码:{password}") # 清空输入框(可选) self.edit1.clear() self.edit2.clear() if __name__ == "__main__": app = QApplication(sys.argv) window = LineEditDemo() window.show() sys.exit(app.exec_())mjqz66bs.png图片 QLineEdit关键方法 方法作用setPlaceholderText("提示文本")设置输入提示(未输入时显示)setMaxLength(数字)限制最大输入长度setEchoMode(QLineEdit.Password)密码模式(输入内容显示为圆点)setReadOnly(True)设置为只读(无法编辑)text()获取输入框中的内容clear()清空输入框内容二、综合案例:用户名密码输入验证窗口(完整代码) 结合上面三个控件和信号与槽,做一个实用的“登录验证窗口”——输入用户名和密码后,点击按钮验证是否正确(模拟登录逻辑)。 import sys from PyQt5.QtWidgets import ( QApplication, QWidget, QLabel, QLineEdit, QPushButton, QVBoxLayout, QHBoxLayout ) from PyQt5.QtGui import QFont, QPixmap from PyQt5.QtCore import Qt class LoginWindow(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): # 窗口基础设置 self.setWindowTitle("模拟登录窗口") self.resize(400, 300) self.setStyleSheet("background-color: #f8f9fa;") # 主布局(垂直布局) main_layout = QVBoxLayout() main_layout.setSpacing(25) main_layout.setContentsMargins(60, 50, 60, 50) # 1. 标题区域(图标+文本) title_layout = QHBoxLayout() # 标题图标 title_icon = QLabel() title_icon.setPixmap(QPixmap("login_icon.png").scaled(30, 30, Qt.KeepAspectRatio)) # 标题文本 title_label = QLabel("用户登录") title_label.setStyleSheet("font-size: 20px; font-weight: bold; color: #2c3e50;") # 添加到标题布局 title_layout.addWidget(title_icon) title_layout.addWidget(title_label) title_layout.setAlignment(Qt.AlignCenter) main_layout.addLayout(title_layout) # 2. 用户名输入区域 user_layout = QHBoxLayout() user_label = QLabel("用户名:") user_label.setStyleSheet("font-size: 14px; color: #34495e;") self.user_edit = QLineEdit() self.user_edit.setPlaceholderText("请输入用户名") self.user_edit.setStyleSheet("padding: 6px; border: 1px solid #bdc3c7; border-radius: 4px;") user_layout.addWidget(user_label) user_layout.addWidget(self.user_edit) main_layout.addLayout(user_layout) # 3. 密码输入区域 pwd_layout = QHBoxLayout() pwd_label = QLabel("密 码:") pwd_label.setStyleSheet("font-size: 14px; color: #34495e;") self.pwd_edit = QLineEdit() self.pwd_edit.setPlaceholderText("请输入密码") self.pwd_edit.setEchoMode(QLineEdit.Password) self.pwd_edit.setStyleSheet("padding: 6px; border: 1px solid #bdc3c7; border-radius: 4px;") pwd_layout.addWidget(pwd_label) pwd_layout.addWidget(self.pwd_edit) main_layout.addLayout(pwd_layout) # 4. 验证结果标签 self.result_label = QLabel("") self.result_label.setAlignment(Qt.AlignCenter) main_layout.addWidget(self.result_label) # 5. 登录按钮 login_btn = QPushButton("登录") login_btn.setStyleSheet(""" background-color: #3498db; color: white; padding: 8px; border: none; border-radius: 4px; font-size: 14px; """) # 鼠标悬停时改变颜色 login_btn.setStyleSheet(""" QPushButton { background-color: #3498db; color: white; padding: 8px; border: none; border-radius: 4px; font-size: 14px; } QPushButton:hover { background-color: #2980b9; } """) login_btn.clicked.connect(self.check_login) main_layout.addWidget(login_btn) # 绑定主布局 self.setLayout(main_layout) # 登录验证槽函数 def check_login(self): # 模拟正确的用户名和密码 correct_user = "admin" correct_pwd = "123456" # 获取用户输入 input_user = self.user_edit.text().strip() # strip()去除前后空格 input_pwd = self.pwd_edit.text().strip() # 验证逻辑 if not input_user or not input_pwd: self.result_label.setText("用户名或密码不能为空!") self.result_label.setStyleSheet("color: #e74c3c; font-size: 12px;") elif input_user == correct_user and input_pwd == correct_pwd: self.result_label.setText("登录成功!") self.result_label.setStyleSheet("color: #2ecc71; font-size: 12px;") else: self.result_label.setText("用户名或密码错误!") self.result_label.setStyleSheet("color: #e74c3c; font-size: 12px;") # 清空密码输入框 self.pwd_edit.clear() if __name__ == "__main__": app = QApplication(sys.argv) window = LoginWindow() window.show() sys.exit(app.exec_())mjqz6yqo.png图片 综合案例亮点 结合了QLabel(标题、提示)、QLineEdit(输入)、QPushButton(登录)三个核心控件; 使用嵌套布局(QVBoxLayout+QHBoxLayout),界面更规整; 加入了样式美化(背景色、边框、按钮悬停效果); 实现了完整的登录验证逻辑(非空判断、正确/错误提示)。 三、常见问题排查 按钮点击后没反应: 忘记绑定clicked.connect(槽函数); 槽函数名称写错(比如on_btn_click写成on_btn_click1); 按钮被设置为禁用状态(setDisabled(True))。 输入框无法获取内容: 没有用text()方法获取内容,或获取的是其他输入框的实例; 输入内容有前后空格,可加strip()方法去除(如self.user_edit.text().strip())。 图片/图标不显示: 图片路径错误(建议放在代码同目录,直接写文件名); 图片尺寸太大,未用scaled()缩放导致超出窗口范围。 样式设置不生效: StyleSheet语法错误(比如少写分号、引号不匹配); 控件样式被布局或父控件样式覆盖,可针对性调整。 总结 三个核心控件的核心用途:QLabel显示、QPushButton触发、QLineEdit输入; 信号与槽是PyQt5交互的核心,记住控件.信号.connect(槽函数)的绑定方式; 实际开发中建议用布局管理器组织控件,配合StyleSheet美化界面; 下一篇我们会讲解复选框、单选框、下拉框等进阶基础控件,以及更复杂的信号与槽用法,记得关注字节曜博客哦~ 如果在实操中遇到问题,欢迎在评论区留言讨论! -
PyQt5核心基础:QWidget窗口属性与线性布局入门 PyQt5核心基础:QWidget基础窗口与布局入门(附完整可运行代码) 哈喽~欢迎来到PyQt5从入门到精通的第二篇!上一篇我们搞定了环境搭建和第一个空白窗口,这一篇我们聚焦QWidget基础窗口的进阶属性和布局管理器入门——解决新手最头疼的“控件位置混乱、窗口缩放后控件错位”问题,全程代码完整可直接运行,新手也能轻松跟上! mjqx9i5t.png图片 一、先回顾:QWidget是什么? QWidget是PyQt5中所有可视化控件的基类(可以理解为“所有窗口/控件的老祖宗”),我们上一篇创建的空白窗口就是QWidget的实例。它不仅能作为独立窗口使用,还能作为其他控件的容器,掌握它的属性设置是PyQt5界面开发的核心基础。 二、QWidget窗口进阶属性设置(完整代码) 上一篇我们只设置了窗口标题、大小、位置,这一节我们拓展更多实用属性:设置窗口图标、固定窗口大小、窗口置顶、自定义背景色、监听关闭事件等,先上完整代码,再逐行解析。 完整代码:QWidget窗口进阶属性 import sys # 导入必要控件:QWidget(窗口)、QApplication(应用)、QIcon(图标) from PyQt5.QtWidgets import QApplication, QWidget from PyQt5.QtGui import QIcon from PyQt5.QtCore import Qt class MyWindow(QWidget): def __init__(self): # 继承QWidget的初始化方法 super().__init__() # 调用自定义的窗口设置方法 self.init_ui() def init_ui(self): # 1. 基础属性(复习) self.setWindowTitle("QWidget进阶窗口") # 窗口标题 self.resize(400, 300) # 初始大小(宽×高) self.move(300, 200) # 初始位置(屏幕坐标:左×上) # 2. 进阶属性:设置窗口图标 # 注意:替换为你本地的图标路径(支持.ico/png/jpg格式) # 新手提示:可先找一张小图片(比如16×16/32×32像素),放在代码同目录下 self.setWindowIcon(QIcon("icon.png")) # 3. 进阶属性:固定窗口大小(禁止用户缩放) # 取消注释以下行,窗口将固定为400×300,无法拖动边缘缩放 # self.setFixedSize(400, 300) # 4. 进阶属性:窗口置顶(始终显示在其他窗口上方) self.setWindowFlags(Qt.WindowStaysOnTopHint) # 5. 进阶属性:设置窗口背景色(浅灰色) self.setStyleSheet("background-color: #f0f0f0;") # 6. 进阶:监听窗口关闭事件(比如关闭前弹出提示) def closeEvent(self, event): # 这里先简单打印提示,后续会讲弹窗提示 print("你点击了关闭按钮!") # 允许窗口关闭(如果想阻止关闭,可调用event.ignore()) event.accept() if __name__ == "__main__": # 创建应用实例 app = QApplication(sys.argv) # 创建自定义窗口实例 window = MyWindow() # 显示窗口 window.show() # 启动主循环 sys.exit(app.exec_())mjqxa0yu.png图片 代码关键解析 类的封装:我们把窗口逻辑封装到MyWindow类中(继承QWidget),这是PyQt5开发的标准写法,便于后续拓展功能; 窗口图标:setWindowIcon(QIcon("图标路径")),新手注意路径要正确(代码和图标同目录可直接写文件名); 固定大小:setFixedSize() 适合不需要缩放的工具窗口,取消注释即可生效; 窗口置顶:Qt.WindowStaysOnTopHint 是Qt的内置常量,实现窗口始终在最上层; 关闭事件:重写closeEvent()方法,可在窗口关闭前执行自定义逻辑(比如保存数据、弹出确认弹窗)。 运行效果 运行代码后,会弹出一个浅灰色、置顶显示、带自定义图标的窗口,关闭窗口时控制台会打印“你点击了关闭按钮!”。 三、布局管理器入门:解决控件排列混乱问题 新手最容易踩的坑:直接用setGeometry()手动设置控件位置,窗口缩放后控件会错位、重叠。PyQt5提供布局管理器自动管理控件位置,核心是: 无需手动设置控件坐标,布局会自动分配空间; 窗口缩放时,控件会按比例自适应。 我们先学最基础的两种线性布局: QVBoxLayout:垂直布局(控件从上到下排列); QHBoxLayout:水平布局(控件从左到右排列)。 完整代码:线性布局实操(带按钮+标签) import sys from PyQt5.QtWidgets import ( QApplication, QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout # 导入布局管理器 ) class LayoutWindow(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setWindowTitle("线性布局示例") self.resize(400, 300) # 1. 创建控件 label = QLabel("这是一个标签") btn1 = QPushButton("按钮1") btn2 = QPushButton("按钮2") # 2. 方式1:垂直布局(控件从上到下) # 创建垂直布局实例 layout = QVBoxLayout() # 向布局中添加控件(可添加多个) layout.addWidget(label) layout.addWidget(btn1) layout.addWidget(btn2) # 设置布局的间距(控件之间的距离,可选) layout.setSpacing(20) # 设置布局的边距(布局和窗口边缘的距离,可选) layout.setContentsMargins(50, 50, 50, 50) # 2. 方式2:水平布局(控件从左到右) # 取消注释以下代码,替换垂直布局 # layout = QHBoxLayout() # layout.addWidget(label) # layout.addWidget(btn1) # layout.addWidget(btn2) # 3. 将布局设置到窗口上(核心步骤,否则布局不生效) self.setLayout(layout) if __name__ == "__main__": app = QApplication(sys.argv) window = LayoutWindow() window.show() sys.exit(app.exec_())代码关键解析 布局使用三步法: 创建布局实例(QVBoxLayout()/QHBoxLayout()); 用addWidget()向布局中添加控件; 用setLayout()将布局绑定到窗口; 间距/边距设置: setSpacing():控件之间的间距(单位:像素); setContentsMargins(左, 上, 右, 下):布局和窗口边缘的距离; 自适应效果:运行后拖动窗口边缘缩放,控件会自动调整位置,不会错位。 对比:手动布局 vs 布局管理器 如果用手动布局(label.setGeometry(50,50,100,30)),窗口放大后标签和按钮仍停留在原地;而布局管理器会让控件均匀分布在窗口中,这是开发中必须掌握的核心技巧。 四、综合案例:带布局的多功能窗口(完整代码) 结合本节所有知识点,做一个带图标、置顶、垂直布局的完整窗口: import sys from PyQt5.QtWidgets import ( QApplication, QWidget, QLabel, QPushButton, QVBoxLayout ) from PyQt5.QtGui import QIcon from PyQt5.QtCore import Qt class ComprehensiveWindow(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): # 窗口基础属性 self.setWindowTitle("综合布局窗口") self.setWindowIcon(QIcon("icon.png")) # 替换为你的图标路径 self.setWindowFlags(Qt.WindowStaysOnTopHint) self.resize(400, 300) # 创建控件 title_label = QLabel("PyQt5布局演示") # 设置标签字体大小(简单美化) title_label.setStyleSheet("font-size: 18px; font-weight: bold;") btn_ok = QPushButton("确认") btn_cancel = QPushButton("取消") # 创建垂直布局 layout = QVBoxLayout() layout.addWidget(title_label) layout.addWidget(btn_ok) layout.addWidget(btn_cancel) layout.setSpacing(30) layout.setContentsMargins(80, 80, 80, 80) # 绑定布局 self.setLayout(layout) def closeEvent(self, event): print("窗口即将关闭!") event.accept() if __name__ == "__main__": app = QApplication(sys.argv) window = ComprehensiveWindow() window.show() sys.exit(app.exec_())运行效果:一个置顶的窗口,内部有大号标题标签和两个按钮,垂直排列且间距均匀,窗口缩放时控件自动适配。 mjqxbt56.png图片 五、常见问题排查 布局不生效:忘记调用setLayout(),或布局添加控件后未绑定到窗口; 图标不显示:图标路径错误(建议将图标放在代码同目录,直接写文件名); 窗口无法置顶:部分系统(如macOS)对窗口置顶有权限限制,属正常现象; 控件重叠:未使用布局,手动设置的控件坐标重复,优先用布局管理器解决。 总结 QWidget是PyQt5所有控件的基类,掌握setWindowIcon()/setFixedSize()等属性可自定义窗口样式; 布局管理器(QVBoxLayout/QHBoxLayout)是解决控件排列的核心,无需手动设置坐标,支持自适应; PyQt5开发建议封装成类,便于后续拓展功能(如监听事件、添加更多控件)。 下一篇我们会深入讲解PyQt5常用基础控件(标签、按钮、输入框)和信号与槽的核心逻辑,记得关注哦~如果有任何问题,评论区留言讨论! -
PyQt5入门指南:环境搭建与第一个桌面程序(超详细图文版) PyQt5入门指南:环境搭建与第一个桌面程序(超详细图文版) 哈喽,各位想入门GUI开发的小伙伴们~ 我是字节曜编辑寒烟似雪,今天开始,我们正式开启PyQt5从入门到精通的系列之旅!作为系列的第一篇,我会用最细致的步骤,带大家搞定PyQt5的环境搭建,并且写出第一个属于自己的桌面程序。全程无废话、无跳步,零基础也能轻松跟上,遇到问题还会附上排查方案,放心往下看~ 一、先搞懂:什么是PyQt5?为什么选它做GUI开发? 在动手之前,先简单了解下我们要学的工具: PyQt5是Qt框架的Python绑定版本,Qt是一款跨平台的C++图形用户界面开发框架,支持Windows、macOS、Linux等多个系统。简单说,PyQt5就是让我们能用Python这种简单易懂的语言,开发出专业、美观且能在多系统运行的桌面应用(比如办公软件、小游戏、数据可视化工具等)。 选择PyQt5的3个核心理由: 跨平台:写一次代码,多系统直接运行,不用重复开发; 功能强大:自带丰富的控件(按钮、表格、对话框等),满足大部分GUI开发需求; 学习成本低:结合Python的简洁语法,比直接学Qt(C++)门槛低很多,适合新手入门。 适用场景:桌面应用开发(如文本编辑器、数据管理工具、本地小工具等),不适合开发网页或手机APP哦~ 二、环境搭建:3大系统(Windows/macOS/Linux)详细步骤 环境搭建是入门的第一步,也是最容易踩坑的一步。这里分三个系统详细说明,你根据自己的电脑系统对应操作即可。核心准备:先安装Python,再安装PyQt5及配套工具包。 1. 前置准备:安装Python(所有系统通用) PyQt5需要依赖Python环境,建议安装Python 3.7-3.10版本(兼容性最好,太高版本可能存在部分依赖包不支持的问题)。 步骤: 访问Python官方下载地址:https://www.python.org/downloads/(无需科学上网,但是慢,可用阿里云镜像:https://mirrors.aliyun.com/python-release/,选择你的系统后选择版本即可); mjpq1alv.png图片 根据系统选择对应版本:Windows选“Windows Installer (64-bit)”,macOS选“macOS 64-bit universal2 installer”,Linux可通过系统自带软件商店安装; 安装注意事项: Windows系统:一定要勾选“Add Python 3.x to PATH”(自动配置环境变量,否则后续需手动配置,新手容易忘),然后点击“Install Now”默认安装即可; macOS/Linux系统:默认安装即可,安装完成后可通过终端输入命令验证。 验证Python是否安装成功: 打开终端(Windows按Win+R输入cmd,macOS按Command+空格输入terminal,Linux直接打开终端),输入以下命令:python --version # 或 python3 --version(macOS/Linux可能需要用python3)如果显示类似“Python 3.9.7”的版本号,说明安装成功;如果提示“不是内部或外部命令”,则是环境变量未配置好(Windows可重新运行安装包,勾选Add to PATH后修复安装)。 mjpq2x0t.png图片 mjpq4kpo.png图片 mjpq57lf.png图片 2. 安装PyQt5及工具包(所有系统通用) Python安装完成后,通过pip命令安装PyQt5核心包和配套工具包(pyqt5-tools包含Qt Designer可视化设计工具,后续做界面会用到,建议一起安装)。 步骤: 打开终端,输入以下安装命令(Windows/macOS/Linux通用): pip install pyqt5 等待安装完成:网络正常的情况下,几分钟即可安装完成。如果安装速度慢,可切换国内镜像源(推荐阿里云),命令如下: 验证是否安装成功: 在终端输入python(或python3),进入Python交互式环境,输入以下命令: import sys from PyQt5.QtWidgets import QWidget # 导入PyQt5的核心控件 print("导入成功")如果没有报错,显示“导入成功”,说明PyQt5安装成功;如果报错,参考下方“常见问题排查”部分。 mjpq6vqq.png图片 3. 环境搭建常见问题排查 新手在安装过程中容易遇到以下问题,提前整理好解决方案,遇到直接对应排查: 问题1:Windows系统安装pyqt5-tools失败,提示“error: Microsoft Visual C++ 14.0 or greater is required...” 解决方案:需要安装Microsoft Visual C++ Build Tools。访问微软官网下载:https://visualstudio.microsoft.com/visual-cpp-build-tools/,勾选“Desktop development with C++”,点击安装(无需安装完整的Visual Studio,仅安装工具集即可),安装完成后重新执行pip安装命令。 问题2:导入PyQt5时提示“ModuleNotFoundError: No module named 'PyQt5'” 解决方案:① 检查是否用错了Python版本(比如安装了Python3.9,但终端用的是python2),尝试用python3导入;② 检查pip是否对应正确的Python版本,Windows可输入“pip --version”查看,确保pip的路径是Python安装目录下的Scripts文件夹;③ 重新执行安装命令,加上--user参数:pip install pyqt5 pyqt5-tools --user。 问题3:macOS/Linux系统安装后,无法打开Qt Designer 解决方案:macOS/Linux系统的pyqt5-tools可能需要手动查找Designer路径,后续在讲解Qt Designer使用时会详细说明,此处先确保PyQt5能正常导入即可。 三、编写第一个PyQt5程序:Hello World窗口 环境搭建完成后,我们来写第一个程序——一个简单的“Hello World”窗口,感受一下PyQt5的魅力~ 这里用纯代码编写(后续会讲可视化设计工具),每一行代码都加详细注释,方便理解。 1. 步骤:新建文件并编写代码 新建一个文本文件,重命名为“first_pyqt5.py”(注意后缀名是.py,不是.txt,Windows系统要确保显示文件后缀名); 用记事本、VS Code、PyCharm等编辑器打开文件,粘贴以下代码(建议手动敲一遍,加深记忆): # 导入必要的模块 import sys # 导入PyQt5基础窗口相关控件 from PyQt5.QtWidgets import QApplication, QWidget if __name__ == "__main__": # 1. 创建应用程序实例(PyQt5程序必须有且仅有一个) app = QApplication(sys.argv) # 2. 创建基础窗口实例 window = QWidget() # 3. 设置窗口属性 window.setWindowTitle("我的第一个PyQt5程序") # 窗口标题 window.resize(400, 300) # 窗口大小:宽400px,高300px window.move(500, 200) # 窗口位置:距离屏幕左侧500px,顶部200px # 4. 显示窗口(默认隐藏,需手动调用show()) window.show() # 5. 启动应用主循环,确保程序正常退出 sys.exit(app.exec_())2. 运行程序 运行方式有2种,新手推荐第一种: 通过终端运行: 打开终端,切换到“first_pyqt5.py”文件所在的目录(比如文件放在桌面,Windows输入“cd Desktop”,macOS/Linux输入“cd ~/Desktop”); 输入运行命令:python first_pyqt5.py 如果没有报错,会弹出一个标题为“我的第一个PyQt5程序”的窗口,说明运行成功! 用编辑器运行:如果用VS Code或PyCharm,直接点击编辑器右上角的“运行”按钮即可(需要确保编辑器已选择正确的Python解释器)。 mjpqfq31.png图片 mjpqosc2.png图片 四、第一个程序运行原理解析 可能有小伙伴会疑惑,上面几行代码为什么能实现窗口?这里拆解核心逻辑,帮大家理解PyQt5程序的运行流程: QApplication(应用程序对象):每个PyQt5程序都必须有且只有一个应用程序对象,它负责管理程序的所有资源(如事件循环、窗口、控件等),sys.argv是为了让程序能接收命令行参数,简单程序也可以传空列表[]。 QWidget(窗口对象):这是PyQt5的基础窗口控件,所有的UI元素(按钮、标签等)都基于它。我们创建它的实例,就相当于创建了一个空白窗口。 窗口属性设置:setWindowTitle(标题)、resize(大小)、move(位置)都是QWidget的方法,用于自定义窗口样式,这些方法也可以省略,此时会显示默认大小和标题的窗口。 show()方法:PyQt5的窗口默认是隐藏状态,必须调用show()方法才能显示出来。 主循环(exec_()):这是程序的核心,启动后程序会进入“等待状态”,不断监听用户的操作(比如点击窗口、关闭窗口),并做出响应。sys.exit()确保程序关闭时能正确释放资源,避免内存泄漏。 简单总结流程:创建应用程序对象 → 创建窗口对象 → 设置窗口属性 → 显示窗口 → 启动主循环响应用户操作。 五、拓展:修改窗口,添加简单交互(可选) 如果觉得空白窗口太单调,可以简单修改代码,给窗口添加一个标签(显示“Hello World!”),代码如下(在原有代码基础上修改,新增部分标红): import sys # 新增导入QLabel(标签控件) from PyQt5.QtWidgets import QApplication, QWidget, QLabel if __name__ == "__main__": app = QApplication(sys.argv) window = QWidget() window.setWindowTitle("我的第一个PyQt5程序") window.resize(400, 300) window.move(500, 200) # 新增:创建标签控件,父对象是window(表示标签放在这个窗口里) label = QLabel("Hello World!", window) # 设置标签的位置和大小(距离窗口左侧150像素,顶部130像素,宽100像素,高40像素) label.setGeometry(150, 130, 100, 40) window.show() sys.exit(app.exec_())运行修改后的代码,窗口中会显示“Hello World!”文字,是不是更有成就感啦~ 这里的QLabel就是PyQt5的基础控件之一,后续我们会详细讲解各种控件的使用。 mjpqpf9u.png图片 六、总结 本篇我们完成了PyQt5入门的核心第一步: 了解了PyQt5的基本概念和优势; 完成了Windows/macOS/Linux三大系统的环境搭建(Python + PyQt5 + pyqt5-tools); 编写并运行了第一个PyQt5窗口程序,理解了程序的运行原理; 拓展添加了标签控件,实现简单的文字显示。 下一篇我们会深入学习QWidget基础窗口的更多属性设置,以及布局管理器的使用,解决控件排列混乱的问题。如果在环境搭建或程序运行过程中遇到问题,欢迎在评论区留言讨论~