找到
14
篇与
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文本与表格控件: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),进一步拓展界面的功能边界。如果在布局实操中遇到问题,或者有复杂界面的排版需求,欢迎在评论区留言讨论~ -
阶段一实战项目:仿照记事本开发简易文本编辑器(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基础窗口的更多属性设置,以及布局管理器的使用,解决控件排列混乱的问题。如果在环境搭建或程序运行过程中遇到问题,欢迎在评论区留言讨论~