PyQt5 的使用

PyQt5 是 Python 里基于 Qt 框架的 GUI 开发工具,能做桌面应用,跨平台(Windows/macOS/Linux 都能用)。你可能想知道:怎么开始用?有哪些核心组件?怎么写界面逻辑?别急,咱们一步步拆解,多写代码示例,你跟着敲一遍就清楚了。

一、环境搭建:先让 PyQt5 跑起来

首先得安装 PyQt5。打开终端(命令提示符),输入:

pip install pyqt5

如果想设计界面更方便,可以装个 Qt Designer(官方推荐的可视化工具):

pip install pyqt5-tools

安装后,在 Python 安装目录的 Scripts 文件夹里,能找到 designer.exe(Windows 系统),双击就能用。

二、第一个 PyQt5 程序:Hello World 窗口

先写个最基础的窗口程序,看看 PyQt5 的基本结构。代码如下:

import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel

if __name__ == "__main__":
    # 创建应用程序对象
    app = QApplication(sys.argv)
    
    # 创建主窗口
    window = QMainWindow()
    window.setWindowTitle("PyQt5 Hello World")  # 设置窗口标题
    window.setGeometry(100, 100, 300, 200)  # 设置窗口位置和大小(x, y, 宽, 高)
    
    # 创建标签组件
    label = QLabel("Hello, PyQt5!", window)
    label.setGeometry(50, 50, 200, 30)  # 设置标签位置和大小
    
    # 显示窗口
    window.show()
    
    # 进入应用程序主循环
    sys.exit(app.exec_())

代码解析:

  • QApplication:管理应用程序的资源,每个程序必须有且仅有一个实例。
  • QMainWindow:主窗口类,包含标题栏、菜单栏、工具栏等标准组件。
  • QLabel:标签组件,用于显示文本或图片。
  • setGeometry():前两个参数是组件的坐标(相对于父容器),后两个是宽高。
  • app.exec_():启动事件循环,让窗口保持显示状态,直到用户关闭窗口。

运行后会看到一个带 "Hello, PyQt5!" 文本的窗口,这就是最基础的 PyQt5 程序。

三、组件布局:让界面更整齐

刚才的例子用 setGeometry() 手动设置组件位置,适合简单界面,但复杂界面需要布局管理器(Layout)自动排列组件。PyQt5 有四种常用布局:

  1. 水平布局(QHBoxLayout):组件水平排列
  2. 垂直布局(QVBoxLayout):组件垂直排列
  3. 网格布局(QGridLayout):组件按行、列网格排列
  4. 表单布局(QFormLayout):标签和输入框左右排列(类似表单)
示例:水平布局 + 按钮点击事件
import sys
from PyQt5.QtWidgets import (
    QApplication, QWidget, QHBoxLayout, QPushButton, QLabel
)

class MyWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
    
    def initUI(self):
        # 创建水平布局
        layout = QHBoxLayout()
        
        # 创建按钮和标签
        self.btn = QPushButton("点击我", self)
        self.label = QLabel("未点击", self)
        
        # 按钮点击时连接到槽函数
        self.btn.clicked.connect(self.on_click)
        
        # 将组件添加到布局中
        layout.addWidget(self.btn)
        layout.addWidget(self.label)
        
        # 设置窗口的布局
        self.setLayout(layout)
        
        # 设置窗口属性
        self.setWindowTitle("布局示例")
        self.setGeometry(100, 100, 300, 100)
    
    def on_click(self):
        # 按钮点击后的逻辑:修改标签文本
        self.label.setText("已点击!")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MyWindow()
    window.show()
    sys.exit(app.exec_())

关键点:

  • QWidget:基础容器组件,可作为独立窗口或其他组件的父容器。
  • layout.addWidget():将组件添加到布局中,会自动排列。
  • clicked.connect():连接信号(clicked)和槽函数(on_click),点击按钮时触发函数。

四、常用组件详解

PyQt5 有几十种组件,这里挑最常用的讲,每个组件配一个代码示例。

1. 按钮(QPushButton)

除了点击事件,还可以设置图标、快捷键等。

from PyQt5.QtGui import QIcon, QKeySequence
from PyQt5.QtCore import Qt

# 创建带图标的按钮
btn = QPushButton(QIcon("icon.png"), "按钮", self)
btn.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_S))  # 设置快捷键 Ctrl+S
2. 文本输入框(QLineEdit)

单行输入框,支持输入验证、掩码等。

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLineEdit, QLabel, QVBoxLayout

class LineEditDemo(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
    
    def initUI(self):
        layout = QVBoxLayout()
        
        # 创建输入框
        self.edit = QLineEdit()
        self.edit.setPlaceholderText("请输入内容")  # 提示文本
        self.edit.textChanged.connect(self.on_text_change)  # 文本变化时触发
        
        # 创建标签显示输入内容
        self.label = QLabel("内容:")
        
        layout.addWidget(self.edit)
        layout.addWidget(self.label)
        self.setLayout(layout)
        
        self.setWindowTitle("文本输入框示例")
        self.setGeometry(100, 100, 300, 150)
    
    def on_text_change(self, text):
        self.label.setText(f"内容:{text}")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = LineEditDemo()
    window.show()
    sys.exit(app.exec_())
3. 文本编辑框(QTextEdit)

多行文本输入,支持富文本(HTML)。

from PyQt5.QtWidgets import QTextEdit

text_edit = QTextEdit()
text_edit.setHtml("<h1>加粗文本</h1><p>普通文本</p>")  # 设置富文本
print(text_edit.toPlainText())  # 获取纯文本内容
4. 下拉框(QComboBox)
combo = QComboBox()
combo.addItems(["选项1", "选项2", "选项3"])
combo.currentIndexChanged.connect(lambda idx: print(f"选中索引:{idx}"))
5. 复选框(QCheckBox)
check_box = QCheckBox("记住密码")
check_box.toggled.connect(lambda is_checked: print(f"是否勾选:{is_checked}"))
6. 单选按钮(QRadioButton)
layout = QHBoxLayout()
radio1 = QRadioButton("男")
radio2 = QRadioButton("女")
radio1.setChecked(True)  # 默认选中
layout.addWidget(radio1)
layout.addWidget(radio2)
7. 列表视图(QListView)
from PyQt5.QtCore import QStringListModel

model = QStringListModel()
model.setStringList(["苹果", "香蕉", "橙子"])

list_view = QListView()
list_view.setModel(model)

五、对话框(Dialog):与用户交互的关键

PyQt5 提供了多种预定义对话框,比如消息框、文件选择框、颜色选择框等。

1. 消息框(QMessageBox)
from PyQt5.QtWidgets import QMessageBox

# 信息提示框
QMessageBox.information(self, "提示", "操作成功!")

# 警告框
QMessageBox.warning(self, "警告", "文件未保存!")

# 确认框(返回用户点击的按钮)
reply = QMessageBox.question(
    self, "确认", "是否退出程序?",
    QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
if reply == QMessageBox.Yes:
    sys.exit()
2. 文件选择框(QFileDialog)
from PyQt5.QtWidgets import QFileDialog

# 选择单个文件(返回文件路径)
file_path, _ = QFileDialog.getOpenFileName(
    self, "选择文件", "", "文本文件 (*.txt);;所有文件 (*.*)"
)
if file_path:
    with open(file_path, "r") as f:
        content = f.read()

# 选择文件夹(返回文件夹路径)
folder_path = QFileDialog.getExistingDirectory(self, "选择文件夹")
3. 颜色选择框(QColorDialog)
from PyQt5.QtGui import QColor

color = QColorDialog.getColor()
if color.isValid():
    print(f"选择的颜色:RGB({color.red()}, {color.green()}, {color.blue()})")

六、主窗口结构:QMainWindow 的高级用法

QMainWindow 是应用程序的主窗口,包含以下重要区域:

  • 菜单栏(Menu Bar):位于顶部,包含多个菜单(如文件、编辑)。
  • 工具栏(Tool Bar):位于菜单栏下方,包含常用功能按钮。
  • 中心部件(Central Widget):主窗口中间的区域,用于放置主要内容。
  • 状态栏(Status Bar):位于底部,显示状态信息。
示例:完整主窗口结构
import sys
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QAction, QMenu, QTextEdit, QMessageBox
)

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()
    
    def initUI(self):
        # 设置中心部件(文本编辑框)
        self.text_edit = QTextEdit()
        self.setCentralWidget(self.text_edit)
        
        # 创建菜单栏
        menu_bar = self.menuBar()
        
        # 文件菜单
        file_menu = menu_bar.addMenu("文件(&F)")  # &F 表示快捷键 Alt+F
        
        # 新建动作
        new_action = QAction("新建(&N)", self)
        new_action.setShortcut("Ctrl+N")
        new_action.triggered.connect(self.new_file)
        
        # 打开动作
        open_action = QAction("打开(&O)", self)
        open_action.setShortcut("Ctrl+O")
        open_action.triggered.connect(self.open_file)
        
        # 退出动作
        exit_action = QAction("退出(&X)", self)
        exit_action.setShortcut("Ctrl+Q")
        exit_action.triggered.connect(self.close)
        
        # 将动作添加到文件菜单
        file_menu.addAction(new_action)
        file_menu.addAction(open_action)
        file_menu.addSeparator()  # 添加分隔线
        file_menu.addAction(exit_action)
        
        # 编辑菜单(带子菜单)
        edit_menu = menu_bar.addMenu("编辑(&E)")
        format_submenu = edit_menu.addMenu("格式")
        bold_action = QAction("加粗", self)
        italic_action = QAction("斜体", self)
        format_submenu.addAction(bold_action)
        format_submenu.addAction(italic_action)
        
        # 创建工具栏
        tool_bar = self.addToolBar("常用工具")
        tool_bar.addAction(new_action)
        tool_bar.addAction(open_action)
        
        # 创建状态栏
        self.status_bar = self.statusBar()
        self.status_bar.showMessage("就绪", 3000)  # 显示消息3秒
        
        # 窗口设置
        self.setWindowTitle("主窗口示例")
        self.setGeometry(100, 100, 800, 600)
    
    def new_file(self):
        self.text_edit.clear()
        self.status_bar.showMessage("新建文件", 2000)
    
    def open_file(self):
        file_path, _ = QFileDialog.getOpenFileName(self, "打开文件", "", "文本文件 (*.txt)")
        if file_path:
            try:
                with open(file_path, "r") as f:
                    self.text_edit.setText(f.read())
                self.status_bar.showMessage(f"打开文件:{file_path}", 2000)
            except Exception as e:
                QMessageBox.warning(self, "错误", f"打开文件失败:{str(e)}")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

关键组件说明:

  • QAction:菜单栏、工具栏中的动作项,可关联快捷键和函数。
  • setCentralWidget():设置主窗口的中心部件(只能有一个)。
  • statusBar():获取状态栏,用于显示临时消息。

七、信号与槽:组件交互的核心机制

PyQt5 的核心是 信号与槽(Signals and Slots)

  • 信号(Signal):组件状态变化时发出的通知(如按钮点击、输入框文本变化)。
  • 槽(Slot):接收信号并执行的函数(可以是普通函数或 lambda 表达式)。
信号与槽的三种连接方式
  1. 直接连接(最常用)
    btn.clicked.connect(self.on_click)  # 连接到类中的方法
    
  2. 连接到 lambda 表达式
    btn.clicked.connect(lambda: print("按钮被点击"))
    
  3. 断开连接(可选)
    btn.clicked.disconnect(self.on_click)  # 断开信号与槽的连接
    
自定义信号:跨组件通信

如果需要在自定义的类之间传递数据,可以定义自己的信号。

from PyQt5.QtCore import pyqtSignal, QObject

class MySignal(QObject):
    # 定义一个带参数的信号(字符串类型)
    my_signal = pyqtSignal(str)

# 使用信号
signal_obj = MySignal()
signal_obj.my_signal.connect(lambda msg: print(f"收到信号:{msg}"))
signal_obj.my_signal.emit("你好,自定义信号!")  # 发送信号,输出:收到信号:你好,自定义信号!

八、Qt Designer 的使用:可视化设计界面

手动写代码布局复杂界面效率低,推荐用 Qt Designer 可视化设计,再将设计文件转换为 Python 代码。

步骤 1:用 Qt Designer 设计界面
  1. 打开 designer.exe,选择模板(如 Main Window)。
  2. 从左侧组件栏拖放组件到窗口中,调整布局(右键点击组件选择布局)。
  3. 保存为 .ui 文件(例如 main.ui)。
步骤 2:将 .ui 文件转换为 Python 代码

用命令行工具 pyuic5 转换:

pyuic5 -o main_window.py main.ui

转换后的代码会生成一个继承自 QMainWindow 的类,包含所有组件的定义。

步骤 3:在 Python 中加载界面并添加逻辑
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
from main_window import Ui_MainWindow  # 导入转换后的 UI 类

class MyApp(QMainWindow, Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.setupUi(self)  # 初始化 UI
        self.btn_ok.clicked.connect(self.on_ok_click)  # 连接按钮点击事件
    
    def on_ok_click(self):
        text = self.line_edit.text()
        self.label_result.setText(f"你输入了:{text}")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MyApp()
    window.show()
    sys.exit(app.exec_())

优点: 界面设计和逻辑代码分离,修改界面无需改代码,适合团队协作。

九、多线程处理:避免界面卡顿

PyQt5 中,主线程(UI 线程)负责渲染界面和处理用户交互,而耗时操作(如网络请求、文件读写、大数据计算等)若直接在主线程执行,会导致界面卡顿甚至无响应。此时需使用多线程将耗时任务放到子线程中执行,确保 UI 流畅。

PyQt5 多线程核心要点
  1. 避免在子线程中直接操作 UI 组件
    子线程不能直接修改 UI 组件(可能引发线程安全问题),需通过信号与槽机制将结果传递给主线程,由主线程更新界面。
  2. 使用 QThread 或 QThreadPool
    • QThread:创建独立线程,适合单个长时间任务。
    • QThreadPool:线程池,适合管理多个短时间任务,避免频繁创建 / 销毁线程的开销。
示例 1:使用 QThread 实现耗时任务

以下代码演示一个模拟耗时任务(循环计数),通过信号传递进度和结果到主线程更新界面。

import sys
from PyQt5.QtCore import QThread, pyqtSignal
from PyQt5.QtWidgets import (
    QApplication, QWidget, QPushButton, QLabel, QVBoxLayout, QProgressBar
)

# ----------------------
# 自定义子线程类(继承 QThread)
# ----------------------
class WorkerThread(QThread):
    # 定义信号:用于传递进度(int)和完成通知(无参数)
    progress_signal = pyqtSignal(int)  # 进度信号
    finish_signal = pyqtSignal()       # 完成信号

    def __init__(self, total_steps=10):
        super().__init__()
        self.total_steps = total_steps  # 总任务步数

    def run(self):
        """子线程的核心逻辑(耗时任务)"""
        for step in range(self.total_steps + 1):
            # 模拟耗时操作(这里用 sleep 代替)
            self.sleep(1)  # 暂停 1 秒(需导入 from PyQt5.QtCore import QThread)
            
            # 发送进度信号(step 从 0 到 total_steps)
            self.progress_signal.emit(step)
        
        # 任务完成后发送完成信号
        self.finish_signal.emit()

# ----------------------
# 主窗口类
# ----------------------
class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
        self.init_thread()  # 初始化子线程

    def initUI(self):
        """设置界面布局"""
        layout = QVBoxLayout()
        
        # 启动按钮
        self.start_btn = QPushButton("开始任务", self)
        self.start_btn.clicked.connect(self.start_task)  # 点击时启动任务
        
        # 进度条
        self.progress_bar = QProgressBar()
        self.progress_bar.setRange(0, 100)  # 进度范围 0~100
        
        # 结果标签
        self.result_label = QLabel("等待任务开始...")
        
        layout.addWidget(self.start_btn)
        layout.addWidget(self.progress_bar)
        layout.addWidget(self.result_label)
        self.setLayout(layout)
        
        self.setWindowTitle("多线程示例")
        self.setGeometry(100, 100, 300, 150)

    def init_thread(self):
        """初始化子线程并连接信号"""
        self.worker = WorkerThread(total_steps=10)  # 创建子线程实例
        
        # 连接子线程的信号到主线程的槽函数
        self.worker.progress_signal.connect(self.update_progress)  # 进度更新
        self.worker.finish_signal.connect(self.task_finished)       # 任务完成
        
        # 线程结束后自动销毁(避免内存泄漏)
        self.worker.finished.connect(self.worker.deleteLater)

    def start_task(self):
        """启动子线程任务"""
        # 禁用按钮防止重复启动
        self.start_btn.setEnabled(False)
        self.result_label.setText("任务进行中...")
        
        # 启动子线程(会自动调用 worker.run())
        self.worker.start()

    def update_progress(self, step):
        """更新进度条(由 progress_signal 触发)"""
        progress = int(step / 10 * 100)  # 将 step(0~10)转为百分比(0~100)
        self.progress_bar.setValue(progress)

    def task_finished(self):
        """任务完成后的处理(由 finish_signal 触发)"""
        self.result_label.setText("任务完成!")
        self.start_btn.setEnabled(True)  # 重新启用按钮

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

代码解析:

  1. 子线程类 WorkerThread

    • 继承自 QThread,重写 run() 方法作为线程入口。
    • progress_signal 和 finish_signal 用于向主线程传递进度和完成状态。
    • self.sleep(1) 模拟耗时操作(实际开发中替换为真实任务,如文件读写)。
  2. 主线程与子线程通信

    • 通过 connect() 将子线程的信号连接到主线程的槽函数(update_progress 和 task_finished)。
    • 子线程通过 emit() 发送信号,主线程接收到信号后安全地更新 UI。
  3. 线程管理

    • worker.finished.connect(worker.deleteLater):线程结束后自动释放资源,避免内存泄漏。
    • 按钮在任务启动时禁用,完成后重新启用,防止重复启动。
示例 2:使用 QThreadPool 和 QRunnable 管理多任务

QThreadPool 适用于管理多个短时间任务(如同时加载多个图片、处理多个数据文件),通过 QRunnable 封装任务逻辑。

import sys
from PyQt5.QtCore import QRunnable, pyqtSignal, pyqtSlot, QThreadPool
from PyQt5.QtWidgets import (
    QApplication, QWidget, QPushButton, QLabel, QVBoxLayout, QListWidget
)

# ----------------------
# 自定义任务类(继承 QRunnable)
# ----------------------
class Task(QRunnable):
    def __init__(self, task_id):
        super().__init__()
        self.task_id = task_id  # 任务编号
        self.result = None       # 任务结果

    @pyqtSlot()  # 必须使用 pyqtSlot 装饰器作为入口
    def run(self):
        """任务逻辑:模拟耗时计算(这里计算任务编号的平方)"""
        import time
        time.sleep(2)  # 模拟耗时 2 秒
        self.result = f"任务 {self.task_id} 结果:{self.task_id ** 2}"
        
        # 发送信号(通过自定义信号或直接调用主线程方法)
        MainWindow.task_completed_signal.emit(self.result)  # 直接调用主线程的信号

# ----------------------
# 主窗口类
# ----------------------
class MainWindow(QWidget):
    # 定义类级别的信号(用于任务完成通知)
    task_completed_signal = pyqtSignal(str)
    
    def __init__(self):
        super().__init__()
        self.initUI()
        self.init_thread_pool()
    
    def initUI(self):
        layout = QVBoxLayout()
        
        # 启动多任务按钮
        self.start_tasks_btn = QPushButton("启动 3 个任务", self)
        self.start_tasks_btn.clicked.connect(self.start_tasks)
        
        # 任务结果列表
        self.result_list = QListWidget()
        
        layout.addWidget(self.start_tasks_btn)
        layout.addWidget(self.result_list)
        self.setLayout(layout)
        
        self.setWindowTitle("线程池示例")
        self.setGeometry(100, 100, 300, 200)
        
        # 连接任务完成信号到槽函数
        self.task_completed_signal.connect(self.add_result_to_list)

    def init_thread_pool(self):
        """初始化线程池(全局单例,无需手动创建实例)"""
        self.thread_pool = QThreadPool.globalInstance()
        print(f"线程池最大线程数:{self.thread_pool.maxThreadCount()}")  # 默认根据 CPU 核心数自动设置

    def start_tasks(self):
        """启动多个任务并添加到线程池"""
        self.start_tasks_btn.setEnabled(False)
        for task_id in range(1, 4):
            task = Task(task_id=task_id)
            self.thread_pool.start(task)  # 将任务添加到线程池执行

    def add_result_to_list(self, result):
        """将任务结果添加到列表(由信号触发)"""
        self.result_list.addItem(result)
        if self.result_list.count() == 3:  # 所有任务完成后恢复按钮
            self.start_tasks_btn.setEnabled(True)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

代码解析:

  1. 任务类 Task

    • 继承自 QRunnable,重写 run() 方法(需用 @pyqtSlot() 装饰)。
    • 任务逻辑中通过 time.sleep(2) 模拟耗时操作,计算任务编号的平方作为结果。
    • 通过主线程的 task_completed_signal 发送结果,避免子线程直接操作 UI。
  2. 线程池 QThreadPool

    • 使用全局实例 QThreadPool.globalInstance(),无需手动管理线程生命周期。
    • thread_pool.start(task) 将任务添加到线程池,线程池自动分配空闲线程执行任务。
  3. 批量任务管理

    • 启动 3 个任务,每个任务完成后通过信号更新结果列表。
    • 所有任务完成后恢复按钮状态,避免重复启动。
多线程注意事项
  1. 避免跨线程操作 UI
    永远不要在子线程中直接调用 UI 组件的方法(如 self.label.setText()),必须通过信号传递到主线程处理。

  2. 信号线程安全
    PyQt5 的信号与槽机制是线程安全的,信号会自动排队到接收者所在线程执行。

  3. 内存管理

    • 子线程结束后,建议通过 finished.connect(deleteLater) 自动释放资源。
    • 避免在子线程中持有对主线程对象的强引用,防止循环引用导致内存泄漏。
  4. 耗时任务拆分
    若任务特别长(如超过 10 秒),建议拆分为多个子步骤,通过信号定期汇报进度,避免界面长时间无响应。

十、实战案例:文件批量处理器

结合前面的知识点,实现一个简单的文件批量处理器,功能包括:

  • 选择文件夹,显示其中所有文件。
  • 勾选文件后,点击 “处理” 按钮,在子线程中批量重命名或修改文件内容。
  • 显示处理进度和结果。
import sys
import os
from PyQt5.QtCore import QThread, pyqtSignal, QDir
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
    QListWidget, QListWidgetItem, QPushButton, QProgressBar,
    QCheckBox, QMessageBox, QFileDialog
)

# ----------------------
# 文件处理子线程
# ----------------------
class FileProcessorThread(QThread):
    progress_signal = pyqtSignal(int, str)  # 进度(百分比)和状态消息
    finish_signal = pyqtSignal(list)       # 处理结果列表

    def __init__(self, selected_files, operation):
        super().__init__()
        self.selected_files = selected_files  # 选中的文件列表
        self.operation = operation            # 操作类型("rename" 或 "modify")

    def run(self):
        total = len(self.selected_files)
        results = []
        
        for idx, file_path in enumerate(self.selected_files):
            # 模拟处理耗时(实际可替换为真实文件操作)
            self.sleep(1)  # 暂停 1 秒
            
            # 计算进度百分比
            progress = int((idx + 1) / total * 100)
            status = f"处理中:{os.path.basename(file_path)}"
            self.progress_signal.emit(progress, status)  # 发送进度和状态
            
            # 模拟操作(这里仅记录结果,不实际修改文件)
            if self.operation == "rename":
                result = f"重命名:{file_path} -> {file_path}_processed.txt"
            else:
                result = f"修改内容:{file_path}"
            
            results.append(result)
        
        self.finish_signal.emit(results)  # 发送处理结果

# ----------------------
# 主窗口类
# ----------------------
class FileProcessorWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()
        self.selected_files = []  # 保存选中的文件路径

    def initUI(self):
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        
        layout = QVBoxLayout(central_widget)
        
        # 文件列表和勾选框
        self.file_list = QListWidget()
        self.file_list.setSelectionMode(QListWidget.ExtendedSelection)  # 支持多选
        layout.addWidget(self.file_list)
        
        # 操作按钮栏
        btn_layout = QHBoxLayout()
        self.select_folder_btn = QPushButton("选择文件夹")
        self.select_folder_btn.clicked.connect(self.select_folder)
        
        self.process_btn = QPushButton("开始处理(重命名)")
        self.process_btn.clicked.connect(lambda: self.start_process("rename"))
        
        self.modify_btn = QPushButton("开始处理(修改内容)")
        self.modify_btn.clicked.connect(lambda: self.start_process("modify"))
        
        btn_layout.addWidget(self.select_folder_btn)
        btn_layout.addWidget(self.process_btn)
        btn_layout.addWidget(self.modify_btn)
        layout.addLayout(btn_layout)
        
        # 进度条和结果显示
        self.progress_bar = QProgressBar()
        self.progress_bar.setRange(0, 100)
        self.status_label = QLabel("等待选择文件...")
        
        layout.addWidget(self.progress_bar)
        layout.addWidget(self.status_label)
        
        self.setWindowTitle("文件批量处理器")
        self.setGeometry(100, 100, 600, 400)

    def select_folder(self):
        """选择文件夹并加载文件列表"""
        folder_path = QFileDialog.getExistingDirectory(self, "选择文件夹")
        if not folder_path:
            return
        
        # 清空现有列表
        self.file_list.clear()
        self.selected_files = []
        
        # 遍历文件夹中的文件(仅显示文件,不显示子文件夹)
        dir = QDir(folder_path)
        files = dir.entryInfoList(["*"], QDir.Files)  # 筛选文件
        for file_info in files:
            item = QListWidgetItem(file_info.fileName())
            item.setData(0, file_info.filePath())  # 存储文件路径到 item 的数据中
            self.file_list.addItem(item)
        
        self.status_label.setText(f"加载了 {len(files)} 个文件")

    def start_process(self, operation):
        """启动文件处理任务"""
        # 获取选中的文件项
        selected_items = self.file_list.selectedItems()
        if not selected_items:
            QMessageBox.warning(self, "警告", "请先勾选要处理的文件!")
            return
        
        # 提取文件路径
        self.selected_files = [item.data(0) for item in selected_items]
        self.process_btn.setEnabled(False)
        self.modify_btn.setEnabled(False)
        self.status_label.setText("处理开始...")
        self.progress_bar.setValue(0)
        
        # 创建并启动子线程
        self.worker = FileProcessorThread(self.selected_files, operation)
        self.worker.progress_signal.connect(self.update_process_status)
        self.worker.finish_signal.connect(self.process_completed)
        self.worker.start()

    def update_process_status(self, progress, status):
        """更新处理进度和状态"""
        self.progress_bar.setValue(progress)
        self.status_label.setText(status)

    def process_completed(self, results):
        """处理完成后的回调"""
        self.process_btn.setEnabled(True)
        self.modify_btn.setEnabled(True)
        self.progress_bar.setValue(100)
        self.status_label.setText(f"处理完成!共处理 {len(results)} 个文件")
        
        # 显示结果(这里用消息框展示,实际可优化为列表显示)
        result_text = "\n".join(results)
        QMessageBox.information(self, "处理结果", result_text)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = FileProcessorWindow()
    window.show()
    sys.exit(app.exec_())

功能说明:

  1. 选择文件夹
    点击 “选择文件夹” 按钮,加载该文件夹下的所有文件到列表,每个文件项附带文件路径数据。

  2. 勾选文件
    按住 Ctrl 或 Shift 键可多选文件,选中的文件将被批量处理。

  3. 处理任务

  1. 点击 “重命名” 或 “修改内容” 按钮,创建子线程处理选中的文件。
  2. 子线程通过 progress_signal 实时汇报进度,主线程更新进度条和状态标签。
  3. 处理完成后,通过 finish_signal 返回结果列表,用消息框展示详细结果。
  1. 线程安全

    • 所有 UI 更新都通过信号与槽机制在主线程完成,确保线程安全。
    • 处理过程中禁用操作按钮,防止重复提交任务。

十一、数据库操作:集成 SQLite

PyQt5 内置对 SQLite 的支持,适合开发中小型桌面应用。以下示例展示如何创建数据库、执行 CRUD 操作,并在界面中展示数据。

import sys
from PyQt5.QtSql import QSqlDatabase, QSqlTableModel, QSqlQuery
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QTableView, QPushButton, QVBoxLayout, 
    QWidget, QMessageBox, QLineEdit, QHBoxLayout, QLabel
)

class DatabaseApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()
        self.initDatabase()
        
    def initUI(self):
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        layout = QVBoxLayout(central_widget)
        
        # 顶部输入栏(添加新记录)
        input_layout = QHBoxLayout()
        input_layout.addWidget(QLabel("姓名:"))
        self.name_input = QLineEdit()
        input_layout.addWidget(self.name_input)
        
        input_layout.addWidget(QLabel("年龄:"))
        self.age_input = QLineEdit()
        input_layout.addWidget(self.age_input)
        
        self.add_btn = QPushButton("添加")
        self.add_btn.clicked.connect(self.addRecord)
        input_layout.addWidget(self.add_btn)
        
        layout.addLayout(input_layout)
        
        # 表格视图(展示数据)
        self.table_view = QTableView()
        layout.addWidget(self.table_view)
        
        # 底部按钮栏
        btn_layout = QHBoxLayout()
        self.refresh_btn = QPushButton("刷新")
        self.refresh_btn.clicked.connect(self.refreshTable)
        btn_layout.addWidget(self.refresh_btn)
        
        self.delete_btn = QPushButton("删除选中")
        self.delete_btn.clicked.connect(self.deleteRecord)
        btn_layout.addWidget(self.delete_btn)
        
        layout.addLayout(btn_layout)
        
        self.setWindowTitle("SQLite 数据库示例")
        self.setGeometry(100, 100, 600, 400)
    
    def initDatabase(self):
        """初始化数据库连接和表结构"""
        # 创建数据库连接
        self.db = QSqlDatabase.addDatabase("QSQLITE")
        self.db.setDatabaseName("example.db")
        
        if not self.db.open():
            QMessageBox.critical(None, "数据库错误", f"无法连接数据库: {self.db.lastError().text()}")
            sys.exit(1)
        
        # 创建表(如果不存在)
        query = QSqlQuery()
        query.exec_(
            """
            CREATE TABLE IF NOT EXISTS users (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL,
                age INTEGER
            )
            """
        )
        
        # 初始化表格模型
        self.model = QSqlTableModel()
        self.model.setTable("users")
        self.model.select()
        
        # 设置表格视图
        self.table_view.setModel(self.model)
        self.table_view.setEditTriggers(QTableView.NoEditTriggers)  # 禁止直接编辑
    
    def addRecord(self):
        """添加新记录到数据库"""
        name = self.name_input.text().strip()
        age_text = self.age_input.text().strip()
        
        if not name:
            QMessageBox.warning(self, "警告", "姓名不能为空!")
            return
        
        try:
            age = int(age_text) if age_text else None
        except ValueError:
            QMessageBox.warning(self, "警告", "年龄必须是数字!")
            return
        
        # 插入数据
        query = QSqlQuery()
        query.prepare("INSERT INTO users (name, age) VALUES (:name, :age)")
        query.bindValue(":name", name)
        query.bindValue(":age", age)
        
        if query.exec_():
            QMessageBox.information(self, "成功", "记录添加成功!")
            self.refreshTable()  # 刷新表格
            self.name_input.clear()
            self.age_input.clear()
        else:
            QMessageBox.critical(self, "错误", f"添加记录失败: {query.lastError().text()}")
    
    def refreshTable(self):
        """刷新表格数据"""
        self.model.select()
    
    def deleteRecord(self):
        """删除选中的记录"""
        selected_indexes = self.table_view.selectedIndexes()
        if not selected_indexes:
            QMessageBox.warning(self, "警告", "请先选择要删除的记录!")
            return
        
        # 获取选中行的 ID(假设 ID 在第一列)
        row = selected_indexes[0].row()
        id_index = self.model.index(row, 0)  # 第一列是 ID
        record_id = self.model.data(id_index)
        
        # 确认删除
        reply = QMessageBox.question(
            self, "确认删除", 
            f"确定要删除 ID 为 {record_id} 的记录吗?",
            QMessageBox.Yes | QMessageBox.No
        )
        
        if reply == QMessageBox.Yes:
            # 删除记录
            query = QSqlQuery()
            query.prepare("DELETE FROM users WHERE id = :id")
            query.bindValue(":id", record_id)
            
            if query.exec_():
                QMessageBox.information(self, "成功", "记录删除成功!")
                self.refreshTable()
            else:
                QMessageBox.critical(self, "错误", f"删除记录失败: {query.lastError().text()}")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = DatabaseApp()
    window.show()
    sys.exit(app.exec_())

关键功能说明:

  1. 数据库连接

    • 使用 QSqlDatabase.addDatabase("QSQLITE") 创建 SQLite 数据库连接。
    • QSqlQuery 用于执行 SQL 语句,包括创建表、插入数据、删除数据等。
  2. 数据展示

    • QSqlTableModel 作为表格数据模型,自动映射数据库表结构。
    • QTableView 显示表格数据,设置为不可编辑模式(防止用户直接修改)。
  3. 增删操作

    • 添加记录时,使用预处理语句(prepare() 和 bindValue())防止 SQL 注入。
    • 删除记录前,通过 selectedIndexes() 获取用户选择的行,并确认操作。

十二、打包发布:将应用转换为可执行文件

完成开发后,需将 PyQt5 应用打包为独立可执行文件,方便分发。推荐使用 PyInstaller 或 Nuitka

1. 使用 PyInstaller 打包
# 安装 PyInstaller
pip install pyinstaller

# 基本打包命令
pyinstaller --onefile --windowed your_script.py

# 参数说明:
# --onefile:打包为单个可执行文件
# --windowed:不显示命令行窗口(仅适用于 GUI 应用)
# --name:指定输出文件名(可选)
# --icon:指定应用图标(可选,需提供 .ico 文件)
2. 解决打包常见问题
  • 缺少依赖:PyInstaller 有时无法自动检测所有依赖,需手动添加:

    pyinstaller --onefile --windowed --hidden-import=module_name your_script.py
    
  • 中文显示问题
    在代码中添加字体设置:

    font = QFont("SimHei")  # 黑体
    app.setFont(font)
    
  • 大文件打包优化
    排除不必要的模块,减少包体积:

    pyinstaller --onefile --windowed --exclude-module=tkinter your_script.py
    

十三、性能优化与调试技巧

1. 调试技巧
  • 打印调试信息
    使用 print() 输出关键变量值,或使用 logging 模块记录详细日志。

  • 断点调试
    在 PyCharm 等 IDE 中设置断点,逐行调试代码。

  • 异常捕获
    使用 try-except 块捕获并处理异常,避免程序崩溃:

    try:
        # 可能出错的代码
        result = 1 / 0
    except Exception as e:
        QMessageBox.critical(self, "错误", f"操作失败: {str(e)}")
    
2. 性能优化
  • 避免频繁刷新 UI
    大量数据更新时,使用 setUpdatesEnabled(False) 临时禁用 UI 更新,完成后再启用。

  • 使用线程处理耗时操作
    如前所述,将耗时任务放到子线程,避免阻塞主线程。

  • 缓存机制
    对于频繁访问的数据,使用缓存减少重复计算:

    from functools import lru_cache
    
    @lru_cache(maxsize=32)  # 缓存最近 32 次结果
    def calculate_something(arg):
        # 复杂计算逻辑
        return result
    

十四、高级主题:自定义组件与样式

1. 自定义组件(继承现有组件)

创建带图标的按钮组件:

from PyQt5.QtWidgets import QPushButton
from PyQt5.QtGui import QIcon

class IconButton(QPushButton):
    def __init__(self, icon_path, text="", parent=None):
        super().__init__(text, parent)
        self.setIcon(QIcon(icon_path))
        self.setIconSize(24, 24)  # 设置图标大小
        self.setStyleSheet("padding: 5px;")  # 设置内边距
2. 自定义样式(QSS)

使用 Qt Style Sheets 自定义组件外观:

# 设置全局样式
app.setStyleSheet("""
    QMainWindow {
        background-color: #f5f5f5;
    }
    
    QPushButton {
        background-color: #4CAF50;
        color: white;
        border-radius: 5px;
        padding: 8px 16px;
    }
    
    QPushButton:hover {
        background-color: #45a049;
    }
    
    QLabel {
        font-size: 14px;
    }
""")

十五、学习资源推荐

  1. 官方文档

  2. 书籍推荐

    • 《Python Qt GUI 快速编程》(Mark Summerfield)
    • 《PyQt5 快速开发与实战》(董伟明)
  3. 在线教程

  4. 社区与论坛

总结

PyQt5 是一个功能强大的 GUI 框架,适合开发各种类型的桌面应用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

亿只小灿灿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值