Python:PyQt5 全栈开发教程,构建跨平台桌面应用

398 阅读24分钟

在当今的软件开发世界中,桌面应用程序依然占据着重要地位。虽然Web应用和移动应用发展迅猛,但桌面应用在性能、用户体验和功能丰富性方面仍有其独特优势。PyQt5作为Python生态系统中最强大的GUI开发框架之一,为开发者提供了构建专业级跨平台桌面应用的完整解决方案。

本教程将带您从零开始,系统性地掌握PyQt5开发技术,最终能够独立构建功能完整的桌面应用程序。

一、PyQt5 概览与环境搭建

1.1 PyQt5 简介

什么是PyQt5

PyQt5是一个功能强大的Python GUI工具包,它是Qt框架的Python绑定。Qt是由挪威Trolltech公司开发的跨平台C++图形用户界面应用程序开发框架,被广泛应用于开发GUI程序。

PyQt5的主要特点:

  • 跨平台:支持Windows、macOS、Linux等主流操作系统
  • 功能丰富:提供完整的GUI控件集合
  • 高性能:基于C++的Qt框架,性能优异
  • 专业外观:原生外观,用户体验佳
  • 开源免费:GPL和商业双重许可

PyQt5 vs 其他GUI框架对比

特性PyQt5TkinterwxPython
学习曲线中等简单中等
控件丰富度非常丰富基础丰富
外观美观度优秀一般良好
跨平台性优秀良好优秀
文档完整度优秀良好一般
社区活跃度中等

1.2 开发环境准备

Python环境要求

PyQt5支持Python 3.5及以上版本,推荐使用Python 3.7或更高版本:

# 检查Python版本
python --version
# 或
python3 --version

PyQt5安装方法

使用pip安装PyQt5是最简单的方法:

# 安装PyQt5
pip install PyQt5

# 安装PyQt5开发工具(包含Qt Designer)
pip install PyQt5-tools

# 验证安装
python -c "import PyQt5; print(PyQt5.Qt.PYQT_VERSION_STR)"

Qt Designer安装与配置

Qt Designer是PyQt5的可视化界面设计工具:

# 查找Qt Designer位置
python -c "from PyQt5 import Qt; print(Qt.QLibraryInfo.location(Qt.QLibraryInfo.BinariesPath))"

# Windows系统通常在:
# Python安装目录/Lib/site-packages/qt5_applications/Qt/bin/designer.exe

IDE推荐与配置

PyCharm配置:

  1. 打开File → Settings → Tools → External Tools
  2. 添加Qt Designer工具:
    • Name: Qt Designer
    • Program: designer.exe的完整路径
    • Working directory: ProjectFileDirProjectFileDir

VS Code配置: 安装Python扩展和Qt for Python扩展,配置tasks.json文件以便快速启动Qt Designer。

1.3 第一个PyQt5程序

让我们从一个简单的"Hello World"程序开始:

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QVBoxLayout
from PyQt5.QtCore import Qt

class HelloWorldWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
    
    def initUI(self):
        # 设置窗口属性
        self.setWindowTitle('我的第一个PyQt5程序')
        self.setGeometry(300, 300, 400, 200)
        
        # 创建标签
        label = QLabel('Hello, PyQt5!')
        label.setAlignment(Qt.AlignCenter)
        label.setStyleSheet("font-size: 18px; color: blue;")
        
        # 创建布局
        layout = QVBoxLayout()
        layout.addWidget(label)
        
        # 设置布局
        self.setLayout(layout)

def main():
    # 创建应用程序对象
    app = QApplication(sys.argv)
    
    # 创建窗口
    window = HelloWorldWindow()
    window.show()
    
    # 进入应用程序主循环
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()

程序结构分析

  1. 导入模块:导入必要的PyQt5模块
  2. 创建应用程序对象:QApplication管理整个应用程序
  3. 创建窗口类:继承自QWidget,定义窗口行为
  4. 初始化界面:设置窗口属性和控件
  5. 显示窗口:调用show()方法显示窗口
  6. 进入主循环:app.exec_()启动事件循环

二、PyQt5 核心概念与基础组件

2.1 核心架构理解

信号与槽机制

信号与槽是PyQt5最重要的特性之一,它提供了对象间通信的机制:

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

class SignalSlotDemo(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
    
    def initUI(self):
        self.setWindowTitle('信号与槽示例')
        self.setGeometry(300, 300, 300, 150)
        
        # 创建控件
        self.label = QLabel('点击次数: 0')
        self.button = QPushButton('点击我')
        self.counter = 0
        
        # 连接信号与槽
        self.button.clicked.connect(self.on_button_clicked)
        
        # 布局
        layout = QVBoxLayout()
        layout.addWidget(self.label)
        layout.addWidget(self.button)
        self.setLayout(layout)
    
    def on_button_clicked(self):
        """按钮点击事件处理函数(槽函数)"""
        self.counter += 1
        self.label.setText(f'点击次数: {self.counter}')

def main():
    app = QApplication(sys.argv)
    window = SignalSlotDemo()
    window.show()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()

自定义信号

您也可以创建自定义信号:

from PyQt5.QtCore import pyqtSignal, QObject

class CustomSignalDemo(QWidget):
    # 定义自定义信号
    custom_signal = pyqtSignal(str)
    
    def __init__(self):
        super().__init__()
        self.initUI()
        
        # 连接自定义信号
        self.custom_signal.connect(self.handle_custom_signal)
    
    def initUI(self):
        self.setWindowTitle('自定义信号示例')
        
        self.button = QPushButton('发送自定义信号')
        self.button.clicked.connect(self.emit_custom_signal)
        
        self.label = QLabel('等待信号...')
        
        layout = QVBoxLayout()
        layout.addWidget(self.button)
        layout.addWidget(self.label)
        self.setLayout(layout)
    
    def emit_custom_signal(self):
        """发送自定义信号"""
        self.custom_signal.emit("自定义信号被触发!")
    
    def handle_custom_signal(self, message):
        """处理自定义信号"""
        self.label.setText(message)

2.2 基础窗口组件

QMainWindow主窗口

QMainWindow是最常用的顶级窗口类,提供了菜单栏、工具栏、状态栏等:

from PyQt5.QtWidgets import (QMainWindow, QMenuBar, QStatusBar, 
                            QToolBar, QAction, QTextEdit)
from PyQt5.QtCore import Qt

class MainWindowDemo(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()
    
    def initUI(self):
        self.setWindowTitle('主窗口示例')
        self.setGeometry(100, 100, 800, 600)
        
        # 创建中央控件
        central_widget = QTextEdit()
        central_widget.setPlainText('这是主窗口的中央区域')
        self.setCentralWidget(central_widget)
        
        # 创建菜单栏
        self.create_menu_bar()
        
        # 创建工具栏
        self.create_tool_bar()
        
        # 创建状态栏
        self.create_status_bar()
    
    def create_menu_bar(self):
        """创建菜单栏"""
        menubar = self.menuBar()
        
        # 文件菜单
        file_menu = menubar.addMenu('文件')
        
        new_action = QAction('新建', self)
        new_action.setShortcut('Ctrl+N')
        new_action.triggered.connect(self.new_file)
        file_menu.addAction(new_action)
        
        open_action = QAction('打开', self)
        open_action.setShortcut('Ctrl+O')
        open_action.triggered.connect(self.open_file)
        file_menu.addAction(open_action)
        
        file_menu.addSeparator()
        
        exit_action = QAction('退出', self)
        exit_action.setShortcut('Ctrl+Q')
        exit_action.triggered.connect(self.close)
        file_menu.addAction(exit_action)
    
    def create_tool_bar(self):
        """创建工具栏"""
        toolbar = self.addToolBar('主工具栏')
        
        new_action = QAction('新建', self)
        new_action.triggered.connect(self.new_file)
        toolbar.addAction(new_action)
        
        open_action = QAction('打开', self)
        open_action.triggered.connect(self.open_file)
        toolbar.addAction(open_action)
    
    def create_status_bar(self):
        """创建状态栏"""
        status_bar = self.statusBar()
        status_bar.showMessage('就绪')
    
    def new_file(self):
        self.statusBar().showMessage('新建文件', 2000)
        self.centralWidget().clear()
    
    def open_file(self):
        self.statusBar().showMessage('打开文件', 2000)

2.3 常用控件详解

按钮类控件

from PyQt5.QtWidgets import (QWidget, QPushButton, QRadioButton, 
                            QCheckBox, QVBoxLayout, QHBoxLayout, 
                            QButtonGroup, QLabel)

class ButtonDemo(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
    
    def initUI(self):
        self.setWindowTitle('按钮控件示例')
        self.setGeometry(300, 300, 400, 300)
        
        main_layout = QVBoxLayout()
        
        # QPushButton 普通按钮
        self.push_button = QPushButton('普通按钮')
        self.push_button.clicked.connect(self.on_push_button_clicked)
        main_layout.addWidget(self.push_button)
        
        # QRadioButton 单选按钮
        radio_layout = QHBoxLayout()
        radio_layout.addWidget(QLabel('选择性别:'))
        
        self.radio_male = QRadioButton('男')
        self.radio_female = QRadioButton('女')
        self.radio_male.setChecked(True)  # 默认选中
        
        # 创建按钮组确保单选
        self.gender_group = QButtonGroup()
        self.gender_group.addButton(self.radio_male)
        self.gender_group.addButton(self.radio_female)
        
        radio_layout.addWidget(self.radio_male)
        radio_layout.addWidget(self.radio_female)
        main_layout.addLayout(radio_layout)
        
        # QCheckBox 复选框
        main_layout.addWidget(QLabel('兴趣爱好:'))
        
        self.check_reading = QCheckBox('阅读')
        self.check_music = QCheckBox('音乐')
        self.check_sports = QCheckBox('运动')
        
        main_layout.addWidget(self.check_reading)
        main_layout.addWidget(self.check_music)
        main_layout.addWidget(self.check_sports)
        
        # 结果显示
        self.result_label = QLabel('结果显示区域')
        main_layout.addWidget(self.result_label)
        
        # 提交按钮
        submit_button = QPushButton('提交')
        submit_button.clicked.connect(self.on_submit)
        main_layout.addWidget(submit_button)
        
        self.setLayout(main_layout)
    
    def on_push_button_clicked(self):
        self.result_label.setText('普通按钮被点击了!')
    
    def on_submit(self):
        # 获取单选按钮选择
        gender = '男' if self.radio_male.isChecked() else '女'
        
        # 获取复选框选择
        hobbies = []
        if self.check_reading.isChecked():
            hobbies.append('阅读')
        if self.check_music.isChecked():
            hobbies.append('音乐')
        if self.check_sports.isChecked():
            hobbies.append('运动')
        
        result = f'性别: {gender}, 兴趣: {", ".join(hobbies) if hobbies else "无"}'
        self.result_label.setText(result)

输入类控件

from PyQt5.QtWidgets import (QWidget, QLineEdit, QTextEdit, QSpinBox, 
                            QDoubleSpinBox, QVBoxLayout, QHBoxLayout, 
                            QLabel, QPushButton, QSlider)
from PyQt5.QtCore import Qt

class InputDemo(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
    
    def initUI(self):
        self.setWindowTitle('输入控件示例')
        self.setGeometry(300, 300, 500, 400)
        
        layout = QVBoxLayout()
        
        # QLineEdit 单行文本输入
        layout.addWidget(QLabel('姓名:'))
        self.name_edit = QLineEdit()
        self.name_edit.setPlaceholderText('请输入您的姓名')
        self.name_edit.textChanged.connect(self.on_name_changed)
        layout.addWidget(self.name_edit)
        
        # QTextEdit 多行文本输入
        layout.addWidget(QLabel('个人简介:'))
        self.intro_edit = QTextEdit()
        self.intro_edit.setPlaceholderText('请输入个人简介...')
        self.intro_edit.setMaximumHeight(100)
        layout.addWidget(self.intro_edit)
        
        # QSpinBox 整数输入
        age_layout = QHBoxLayout()
        age_layout.addWidget(QLabel('年龄:'))
        self.age_spin = QSpinBox()
        self.age_spin.setRange(0, 120)
        self.age_spin.setValue(25)
        self.age_spin.valueChanged.connect(self.on_age_changed)
        age_layout.addWidget(self.age_spin)
        layout.addLayout(age_layout)
        
        # QDoubleSpinBox 浮点数输入
        salary_layout = QHBoxLayout()
        salary_layout.addWidget(QLabel('薪资(K):'))
        self.salary_spin = QDoubleSpinBox()
        self.salary_spin.setRange(0.0, 999.9)
        self.salary_spin.setDecimals(1)
        self.salary_spin.setValue(10.0)
        salary_layout.addWidget(self.salary_spin)
        layout.addLayout(salary_layout)
        
        # QSlider 滑块
        slider_layout = QVBoxLayout()
        slider_layout.addWidget(QLabel('满意度评分:'))
        self.satisfaction_slider = QSlider(Qt.Horizontal)
        self.satisfaction_slider.setRange(0, 10)
        self.satisfaction_slider.setValue(5)
        self.satisfaction_slider.valueChanged.connect(self.on_slider_changed)
        
        self.slider_label = QLabel('5')
        self.slider_label.setAlignment(Qt.AlignCenter)
        
        slider_layout.addWidget(self.satisfaction_slider)
        slider_layout.addWidget(self.slider_label)
        layout.addLayout(slider_layout)
        
        # 显示结果
        self.result_label = QLabel('输入信息将在这里显示')
        layout.addWidget(self.result_label)
        
        # 提交按钮
        submit_button = QPushButton('获取所有输入')
        submit_button.clicked.connect(self.get_all_inputs)
        layout.addWidget(submit_button)
        
        self.setLayout(layout)
    
    def on_name_changed(self, text):
        if len(text) > 10:
            self.name_edit.setText(text[:10])
    
    def on_age_changed(self, value):
        if value >= 60:
            self.result_label.setText('注意:已达到退休年龄')
        else:
            self.result_label.setText('')
    
    def on_slider_changed(self, value):
        self.slider_label.setText(str(value))
    
    def get_all_inputs(self):
        name = self.name_edit.text()
        intro = self.intro_edit.toPlainText()
        age = self.age_spin.value()
        salary = self.salary_spin.value()
        satisfaction = self.satisfaction_slider.value()
        
        result = f"""
输入信息汇总:
姓名: {name}
年龄: {age}
薪资: {salary}K
满意度: {satisfaction}/10
简介: {intro[:50]}{'...' if len(intro) > 50 else ''}
        """
        self.result_label.setText(result)

三、布局管理与界面设计

3.1 布局管理器

布局管理器是PyQt5中控制控件位置和大小的重要工具。

QHBoxLayout水平布局

from PyQt5.QtWidgets import (QWidget, QHBoxLayout, QPushButton, 
                            QVBoxLayout, QLabel)

class HBoxLayoutDemo(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
    
    def initUI(self):
        self.setWindowTitle('水平布局示例')
        self.setGeometry(300, 300, 400, 100)
        
        # 创建水平布局
        hbox = QHBoxLayout()
        
        # 添加按钮
        btn1 = QPushButton('按钮1')
        btn2 = QPushButton('按钮2')
        btn3 = QPushButton('按钮3')
        
        hbox.addWidget(btn1)
        hbox.addWidget(btn2)
        hbox.addWidget(btn3)
        
        # 设置拉伸因子
        hbox.setStretchFactor(btn1, 1)  # 按钮1占1份
        hbox.setStretchFactor(btn2, 2)  # 按钮2占2份
        hbox.setStretchFactor(btn3, 1)  # 按钮3占1份
        
        self.setLayout(hbox)

QVBoxLayout垂直布局

class VBoxLayoutDemo(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
    
    def initUI(self):
        self.setWindowTitle('垂直布局示例')
        self.setGeometry(300, 300, 200, 300)
        
        # 创建垂直布局
        vbox = QVBoxLayout()
        
        # 添加控件
        label = QLabel('垂直布局示例')
        btn1 = QPushButton('第一个按钮')
        btn2 = QPushButton('第二个按钮')
        btn3 = QPushButton('第三个按钮')
        
        vbox.addWidget(label)
        vbox.addWidget(btn1)
        vbox.addWidget(btn2)
        vbox.addWidget(btn3)
        
        # 添加弹性空间
        vbox.addStretch()
        
        self.setLayout(vbox)

QGridLayout网格布局

from PyQt5.QtWidgets import QGridLayout

class GridLayoutDemo(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
    
    def initUI(self):
        self.setWindowTitle('网格布局示例 - 计算器')
        self.setGeometry(300, 300, 300, 400)
        
        # 创建网格布局
        grid = QGridLayout()
        
        # 显示屏
        display = QLineEdit()
        display.setReadOnly(True)
        display.setStyleSheet("font-size: 18px; padding: 10px;")
        grid.addWidget(display, 0, 0, 1, 4)  # 跨4列
        
        # 按钮数据
        buttons = [
            ('C', 1, 0), ('±', 1, 1), ('%', 1, 2), ('÷', 1, 3),
            ('7', 2, 0), ('8', 2, 1), ('9', 2, 2), ('×', 2, 3),
            ('4', 3, 0), ('5', 3, 1), ('6', 3, 2), ('-', 3, 3),
            ('1', 4, 0), ('2', 4, 1), ('3', 4, 2), ('+', 4, 3),
            ('0', 5, 0, 1, 2), ('.', 5, 2), ('=', 5, 3)
        ]
        
        # 创建按钮
        for btn_data in buttons:
            text = btn_data[0]
            row = btn_data[1]
            col = btn_data[2]
            row_span = btn_data[3] if len(btn_data) > 3 else 1
            col_span = btn_data[4] if len(btn_data) > 4 else 1
            
            button = QPushButton(text)
            button.setMinimumHeight(50)
            button.setStyleSheet("font-size: 16px;")
            
            grid.addWidget(button, row, col, row_span, col_span)
        
        self.setLayout(grid)

嵌套布局技巧

class NestedLayoutDemo(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
    
    def initUI(self):
        self.setWindowTitle('嵌套布局示例')
        self.setGeometry(300, 300, 500, 400)
        
        # 主垂直布局
        main_layout = QVBoxLayout()
        
        # 标题
        title = QLabel('用户注册表单')
        title.setStyleSheet("font-size: 18px; font-weight: bold; padding: 10px;")
        title.setAlignment(Qt.AlignCenter)
        main_layout.addWidget(title)
        
        # 表单区域(网格布局)
        form_layout = QGridLayout()
        
        # 基本信息
        form_layout.addWidget(QLabel('姓名:'), 0, 0)
        form_layout.addWidget(QLineEdit(), 0, 1)
        
        form_layout.addWidget(QLabel('邮箱:'), 1, 0)
        form_layout.addWidget(QLineEdit(), 1, 1)
        
        form_layout.addWidget(QLabel('电话:'), 2, 0)
        form_layout.addWidget(QLineEdit(), 2, 1)
        
        # 性别选择(水平布局)
        gender_layout = QHBoxLayout()
        gender_layout.addWidget(QRadioButton('男'))
        gender_layout.addWidget(QRadioButton('女'))
        gender_layout.addStretch()
        
        form_layout.addWidget(QLabel('性别:'), 3, 0)
        form_layout.addLayout(gender_layout, 3, 1)
        
        main_layout.addLayout(form_layout)
        
        # 按钮区域(水平布局)
        button_layout = QHBoxLayout()
        button_layout.addStretch()
        button_layout.addWidget(QPushButton('取消'))
        button_layout.addWidget(QPushButton('注册'))
        
        main_layout.addLayout(button_layout)
        
        self.setLayout(main_layout)

3.2 Qt Designer可视化设计

Qt Designer是PyQt5提供的可视化界面设计工具,可以大大提高开发效率。

Qt Designer基本使用

  1. 启动Qt Designer

    # Windows
    designer.exe
    
    # 或通过Python启动
    python -m PyQt5.uic.pyuic
    
  2. 创建新窗体

    • File → New → Widget/MainWindow/Dialog
    • 选择合适的窗体类型
  3. 设计界面

    • 从左侧控件面板拖拽控件到窗体
    • 使用右侧属性面板设置控件属性
    • 使用布局管理器组织控件

.ui文件转换与使用

方法一:使用pyuic5工具转换

# 将.ui文件转换为.py文件
pyuic5 -o main_window.py main_window.ui

方法二:动态加载.ui文件

import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.uic import loadUi

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        # 动态加载.ui文件
        loadUi('main_window.ui', self)
        
        # 连接信号与槽
        self.pushButton.clicked.connect(self.on_button_clicked)
    
    def on_button_clicked(self):
        self.label.setText('按钮被点击了!')

def main():
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

3.3 样式表与美化

QSS样式表语法

QSS(Qt Style Sheets)类似于CSS,用于美化PyQt5应用程序:

class StyledWidgetDemo(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
        self.setStyleSheet(self.get_style())
    
    def initUI(self):
        self.setWindowTitle('样式表示例')
        self.setGeometry(300, 300, 400, 300)
        
        layout = QVBoxLayout()
        
        # 标题标签
        title = QLabel('现代化界面设计')
        title.setObjectName('title')
        layout.addWidget(title)
        
        # 主要按钮
        primary_btn = QPushButton('主要按钮')
        primary_btn.setObjectName('primary-btn')
        layout.addWidget(primary_btn)
        
        # 次要按钮
        secondary_btn = QPushButton('次要按钮')
        secondary_btn.setObjectName('secondary-btn')
        layout.addWidget(secondary_btn)
        
        # 危险按钮
        danger_btn = QPushButton('危险按钮')
        danger_btn.setObjectName('danger-btn')
        layout.addWidget(danger_btn)
        
        # 输入框
        input_field = QLineEdit()
        input_field.setPlaceholderText('请输入内容...')
        input_field.setObjectName('input-field')
        layout.addWidget(input_field)
        
        self.setLayout(layout)
    
    def get_style(self):
        return """
        QWidget {
            background-color: #f0f0f0;
            font-family: 'Microsoft YaHei', Arial, sans-serif;
        }
        
        #title {
            font-size: 24px;
            font-weight: bold;
            color: #333;
            padding: 20px;
            text-align: center;
        }
        
        QPushButton {
            padding: 10px 20px;
            border: none;
            border-radius: 6px;
            font-size: 14px;
            font-weight: bold;
            cursor: pointer;
            margin: 5px;
        }
        
        #primary-btn {
            background-color: #007bff;
            color: white;
        }
        
        #primary-btn:hover {
            background-color: #0056b3;
        }
        
        #primary-btn:pressed {
            background-color: #004085;
        }
        
        #secondary-btn {
            background-color: #6c757d;
            color: white;
        }
        
        #secondary-btn:hover {
            background-color: #545b62;
        }
        
        #danger-btn {
            background-color: #dc3545;
            color: white;
        }
        
        #danger-btn:hover {
            background-color: #c82333;
        }
        
        #input-field {
            padding: 12px;
            border: 2px solid #ddd;
            border-radius: 6px;
            font-size: 14px;
            margin: 5px;
        }
        
        #input-field:focus {
            border-color: #007bff;
            outline: none;
        }
        """

主题切换实现

from PyQt5.QtWidgets import QComboBox

class ThemeDemo(QWidget):
    def __init__(self):
        super().__init__()
        self.themes = {
            '默认主题': self.get_default_theme(),
            '深色主题': self.get_dark_theme(),
            '绿色主题': self.get_green_theme()
        }
        self.initUI()
    
    def initUI(self):
        self.setWindowTitle('主题切换示例')
        self.setGeometry(300, 300, 400, 300)
        
        layout = QVBoxLayout()
        
        # 主题选择器
        theme_layout = QHBoxLayout()
        theme_layout.addWidget(QLabel('选择主题:'))
        
        self.theme_combo = QComboBox()
        self.theme_combo.addItems(self.themes.keys())
        self.theme_combo.currentTextChanged.connect(self.change_theme)
        theme_layout.addWidget(self.theme_combo)
        
        layout.addLayout(theme_layout)
        
        # 示例控件
        layout.addWidget(QLabel('这是一个标签'))
        layout.addWidget(QPushButton('这是一个按钮'))
        layout.addWidget(QLineEdit('这是一个输入框'))
        
        self.setLayout(layout)
        
        # 应用默认主题
        self.change_theme('默认主题')
    
    def change_theme(self, theme_name):
        if theme_name in self.themes:
            self.setStyleSheet(self.themes[theme_name])
    
    def get_default_theme(self):
        return """
        QWidget {
            background-color: white;
            color: black;
        }
        QPushButton {
            background-color: #e7e7e7;
            border: 1px solid #ccc;
            padding: 8px;
            border-radius: 4px;
        }
        QPushButton:hover {
            background-color: #d4edda;
        }
        """
    
    def get_dark_theme(self):
        return """
        QWidget {
            background-color: #2b2b2b;
            color: white;
        }
        QPushButton {
            background-color: #404040;
            border: 1px solid #555;
            padding: 8px;
            border-radius: 4px;
            color: white;
        }
        QPushButton:hover {
            background-color: #505050;
        }
        QLineEdit {
            background-color: #404040;
            border: 1px solid #555;
            padding: 8px;
            border-radius: 4px;
            color: white;
        }
        """
    
    def get_green_theme(self):
        return """
        QWidget {
            background-color: #f0f8f0;
            color: #2d5a2d;
        }
        QPushButton {
            background-color: #28a745;
            border: none;
            padding: 8px;
            border-radius: 4px;
            color: white;
            font-weight: bold;
        }
        QPushButton:hover {
            background-color: #218838;
        }
        QLineEdit {
            border: 2px solid #28a745;
            padding: 8px;
            border-radius: 4px;
            background-color: white;
        }
        """

四、事件处理与交互逻辑

4.1 信号与槽深入

连接方式详解

PyQt5提供了多种信号槽连接方式:

from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtWidgets import QWidget, QPushButton, QVBoxLayout, QLabel

class AdvancedSignalSlotDemo(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
        self.setup_connections()
    
    def initUI(self):
        self.setWindowTitle('高级信号槽示例')
        layout = QVBoxLayout()
        
        self.button1 = QPushButton('按钮1')
        self.button2 = QPushButton('按钮2')
        self.label = QLabel('状态显示')
        
        layout.addWidget(self.button1)
        layout.addWidget(self.button2)
        layout.addWidget(self.label)
        
        self.setLayout(layout)
    
    def setup_connections(self):
        # 方式1: 连接到成员函数
        self.button1.clicked.connect(self.on_button1_clicked)
        
        # 方式2: 连接到lambda函数
        self.button2.clicked.connect(lambda: self.label.setText('按钮2被点击'))
        
        # 方式3: 带参数的连接
        self.button1.clicked.connect(lambda: self.show_message('来自按钮1'))
        
        # 方式4: 一个信号连接多个槽
        self.button1.clicked.connect(self.update_status)
        
        # 方式5: 信号链接(信号连接信号)
        # self.button2.clicked.connect(self.button1.clicked)
    
    def on_button1_clicked(self):
        self.label.setText('按钮1被点击了')
    
    def show_message(self, message):
        print(f"消息: {message}")
    
    def update_status(self):
        import datetime
        now = datetime.datetime.now().strftime("%H:%M:%S")
        self.label.setText(f"最后点击时间: {now}")

自定义信号高级用法

from PyQt5.QtCore import QObject, pyqtSignal, QTimer
import random

class DataProcessor(QObject):
    """数据处理器,演示自定义信号"""
    
    # 定义各种类型的信号
    progress_updated = pyqtSignal(int)  # 进度更新信号
    data_processed = pyqtSignal(str, dict)  # 数据处理完成信号
    error_occurred = pyqtSignal(str)  # 错误信号
    
    def __init__(self):
        super().__init__()
        self.timer = QTimer()
        self.timer.timeout.connect(self.process_data)
        self.progress = 0
    
    def start_processing(self):
        """开始数据处理"""
        self.progress = 0
        self.timer.start(100)  # 每100ms更新一次
    
    def process_data(self):
        """模拟数据处理"""
        self.progress += random.randint(1, 10)
        self.progress_updated.emit(self.progress)
        
        if self.progress >= 100:
            self.timer.stop()
            # 发送处理完成信号
            result_data = {
                'records_processed': 1000,
                'time_taken': '5.2s',
                'success_rate': '98.5%'
            }
            self.data_processed.emit("处理完成", result_data)
        
        # 随机模拟错误
        if random.random() < 0.05:  # 5%概率出错
            self.error_occurred.emit("随机处理错误")

class CustomSignalDemo(QWidget):
    def __init__(self):
        super().__init__()
        self.processor = DataProcessor()
        self.initUI()
        self.setup_connections()
    
    def initUI(self):
        self.setWindowTitle('自定义信号高级示例')
        layout = QVBoxLayout()
        
        self.start_button = QPushButton('开始处理数据')
        self.progress_label = QLabel('进度: 0%')
        self.status_label = QLabel('状态: 待机')
        self.result_label = QLabel('结果: 无')
        
        layout.addWidget(self.start_button)
        layout.addWidget(self.progress_label)
        layout.addWidget(self.status_label)
        layout.addWidget(self.result_label)
        
        self.setLayout(layout)
    
    def setup_connections(self):
        # 连接按钮信号
        self.start_button.clicked.connect(self.start_processing)
        
        # 连接自定义信号
        self.processor.progress_updated.connect(self.update_progress)
        self.processor.data_processed.connect(self.on_data_processed)
        self.processor.error_occurred.connect(self.on_error)
    
    def start_processing(self):
        self.start_button.setEnabled(False)
        self.status_label.setText('状态: 处理中...')
        self.processor.start_processing()
    
    def update_progress(self, progress):
        self.progress_label.setText(f'进度: {progress}%')
    
    def on_data_processed(self, message, data):
        self.status_label.setText(f'状态: {message}')
        result_text = f"结果: 处理了{data['records_processed']}条记录," \
                     f"耗时{data['time_taken']},成功率{data['success_rate']}"
        self.result_label.setText(result_text)
        self.start_button.setEnabled(True)
    
    def on_error(self, error_message):
        self.status_label.setText(f'错误: {error_message}')

4.2 事件处理机制

鼠标事件处理

from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter, QPen
from PyQt5.QtWidgets import QWidget

class MouseEventDemo(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
        self.drawing = False
        self.brush_size = 3
        self.brush_color = Qt.black
        self.last_point = None
        self.points = []
    
    def initUI(self):
        self.setWindowTitle('鼠标事件示例 - 简单画板')
        self.setGeometry(300, 300, 600, 400)
        self.setMouseTracking(True)  # 启用鼠标跟踪
    
    def mousePressEvent(self, event):
        """鼠标按下事件"""
        if event.button() == Qt.LeftButton:
            self.drawing = True
            self.last_point = event.pos()
            self.points = [event.pos()]
    
    def mouseMoveEvent(self, event):
        """鼠标移动事件"""
        if event.buttons() & Qt.LeftButton and self.drawing:
            self.points.append(event.pos())
            self.update()  # 触发重绘
    
    def mouseReleaseEvent(self, event):
        """鼠标释放事件"""
        if event.button() == Qt.LeftButton:
            self.drawing = False
    
    def mouseDoubleClickEvent(self, event):
        """鼠标双击事件"""
        if event.button() == Qt.LeftButton:
            self.points.clear()
            self.update()
    
    def wheelEvent(self, event):
        """鼠标滚轮事件"""
        # 改变画笔大小
        delta = event.angleDelta().y()
        if delta > 0:
            self.brush_size = min(20, self.brush_size + 1)
        else:
            self.brush_size = max(1, self.brush_size - 1)
        
        self.setWindowTitle(f'画板 - 画笔大小: {self.brush_size}')
    
    def paintEvent(self, event):
        """绘制事件"""
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)
        
        pen = QPen(self.brush_color, self.brush_size, 
                  Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
        painter.setPen(pen)
        
        # 绘制线条
        for i in range(1, len(self.points)):
            painter.drawLine(self.points[i-1], self.points[i])

键盘事件处理

from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QTextEdit, QVBoxLayout, QLabel

class KeyboardEventDemo(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
        self.key_combinations = []
    
    def initUI(self):
        self.setWindowTitle('键盘事件示例')
        self.setGeometry(300, 300, 500, 400)
        
        layout = QVBoxLayout()
        
        self.info_label = QLabel('按下任意键或组合键试试...')
        layout.addWidget(self.info_label)
        
        self.text_edit = QTextEdit()
        self.text_edit.setPlaceholderText('在这里输入文本,支持快捷键操作')
        layout.addWidget(self.text_edit)
        
        self.status_label = QLabel('状态: 就绪')
        layout.addWidget(self.status_label)
        
        self.setLayout(layout)
        
        # 设置焦点策略
        self.setFocusPolicy(Qt.StrongFocus)
    
    def keyPressEvent(self, event):
        """键盘按下事件"""
        key = event.key()
        modifiers = event.modifiers()
        
        # 检测修饰键
        ctrl_pressed = modifiers & Qt.ControlModifier
        shift_pressed = modifiers & Qt.ShiftModifier
        alt_pressed = modifiers & Qt.AltModifier
        
        # 构建按键信息
        key_info = []
        if ctrl_pressed:
            key_info.append('Ctrl')
        if shift_pressed:
            key_info.append('Shift')
        if alt_pressed:
            key_info.append('Alt')
        
        # 获取按键名称
        key_name = self.get_key_name(key)
        if key_name:
            key_info.append(key_name)
        
        key_combination = '+'.join(key_info)
        self.info_label.setText(f'按键: {key_combination}')
        
        # 处理特殊快捷键
        if ctrl_pressed:
            if key == Qt.Key_S:
                self.save_text()
                event.accept()
                return
            elif key == Qt.Key_O:
                self.open_text()
                event.accept()
                return
            elif key == Qt.Key_N:
                self.new_text()
                event.accept()
                return
        
        # 处理功能键
        if key == Qt.Key_F1:
            self.show_help()
            event.accept()
            return
        elif key == Qt.Key_Escape:
            self.text_edit.clear()
            event.accept()
            return
        
        # 传递事件给父类处理
        super().keyPressEvent(event)
    
    def get_key_name(self, key):
        """获取按键名称"""
        key_map = {
            Qt.Key_A: 'A', Qt.Key_B: 'B', Qt.Key_C: 'C', Qt.Key_D: 'D',
            Qt.Key_E: 'E', Qt.Key_F: 'F', Qt.Key_G: 'G', Qt.Key_H: 'H',
            Qt.Key_I: 'I', Qt.Key_J: 'J', Qt.Key_K: 'K', Qt.Key_L: 'L',
            Qt.Key_M: 'M', Qt.Key_N: 'N', Qt.Key_O: 'O', Qt.Key_P: 'P',
            Qt.Key_Q: 'Q', Qt.Key_R: 'R', Qt.Key_S: 'S', Qt.Key_T: 'T',
            Qt.Key_U: 'U', Qt.Key_V: 'V', Qt.Key_W: 'W', Qt.Key_X: 'X',
            Qt.Key_Y: 'Y', Qt.Key_Z: 'Z',
            Qt.Key_Return: 'Enter', Qt.Key_Space: 'Space',
            Qt.Key_Backspace: 'Backspace', Qt.Key_Delete: 'Delete',
            Qt.Key_F1: 'F1', Qt.Key_F2: 'F2', Qt.Key_F3: 'F3',
            Qt.Key_Escape: 'Esc', Qt.Key_Tab: 'Tab'
        }
        return key_map.get(key, f'Key_{key}')
    
    def save_text(self):
        self.status_label.setText('状态: 保存文本 (Ctrl+S)')
    
    def open_text(self):
        self.status_label.setText('状态: 打开文本 (Ctrl+O)')
    
    def new_text(self):
        self.text_edit.clear()
        self.status_label.setText('状态: 新建文本 (Ctrl+N)')
    
    def show_help(self):
        help_text = """
快捷键帮助:
Ctrl+S - 保存
Ctrl+O - 打开
Ctrl+N - 新建
F1 - 显示帮助
Esc - 清空文本
        """
        self.text_edit.setPlainText(help_text)
        self.status_label.setText('状态: 显示帮助 (F1)')

4.3 用户交互实现

菜单栏与工具栏

from PyQt5.QtWidgets import (QMainWindow, QMenuBar, QToolBar, QAction, 
                            QTextEdit, QFileDialog, QMessageBox, QStatusBar)
from PyQt5.QtGui import QIcon, QKeySequence
from PyQt5.QtCore import Qt

class MenuToolbarDemo(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()
        self.create_actions()
        self.create_menus()
        self.create_toolbars()
        self.create_statusbar()
    
    def initUI(self):
        self.setWindowTitle('菜单栏与工具栏示例')
        self.setGeometry(100, 100, 800, 600)
        
        # 创建中央控件
        self.text_edit = QTextEdit()
        self.setCentralWidget(self.text_edit)
        
        # 文件状态
        self.current_file = None
        self.is_modified = False
        
        # 监听文本变化
        self.text_edit.textChanged.connect(self.text_changed)
    
    def create_actions(self):
        """创建动作"""
        # 文件操作
        self.new_action = QAction('新建(&N)', self)
        self.new_action.setShortcut(QKeySequence.New)
        self.new_action.setStatusTip('创建新文档')
        self.new_action.triggered.connect(self.new_file)
        
        self.open_action = QAction('打开(&O)', self)
        self.open_action.setShortcut(QKeySequence.Open)
        self.open_action.setStatusTip('打开文档')
        self.open_action.triggered.connect(self.open_file)
        
        self.save_action = QAction('保存(&S)', self)
        self.save_action.setShortcut(QKeySequence.Save)
        self.save_action.setStatusTip('保存文档')
        self.save_action.triggered.connect(self.save_file)
        
        self.save_as_action = QAction('另存为(&A)', self)
        self.save_as_action.setShortcut(QKeySequence.SaveAs)
        self.save_as_action.setStatusTip('另存为文档')
        self.save_as_action.triggered.connect(self.save_as_file)
        
        self.exit_action = QAction('退出(&X)', self)
        self.exit_action.setShortcut(QKeySequence.Quit)
        self.exit_action.setStatusTip('退出应用程序')
        self.exit_action.triggered.connect(self.close)
        
        # 编辑操作
        self.undo_action = QAction('撤销(&U)', self)
        self.undo_action.setShortcut(QKeySequence.Undo)
        self.undo_action.setStatusTip('撤销上一步操作')
        self.undo_action.triggered.connect(self.text_edit.undo)
        
        self.redo_action = QAction('重做(&R)', self)
        self.redo_action.setShortcut(QKeySequence.Redo)
        self.redo_action.setStatusTip('重做操作')
        self.redo_action.triggered.connect(self.text_edit.redo)
        
        self.cut_action = QAction('剪切(&T)', self)
        self.cut_action.setShortcut(QKeySequence.Cut)
        self.cut_action.setStatusTip('剪切选中文本')
        self.cut_action.triggered.connect(self.text_edit.cut)
        
        self.copy_action = QAction('复制(&C)', self)
        self.copy_action.setShortcut(QKeySequence.Copy)
        self.copy_action.setStatusTip('复制选中文本')
        self.copy_action.triggered.connect(self.text_edit.copy)
        
        self.paste_action = QAction('粘贴(&P)', self)
        self.paste_action.setShortcut(QKeySequence.Paste)
        self.paste_action.setStatusTip('粘贴文本')
        self.paste_action.triggered.connect(self.text_edit.paste)
        
        # 帮助操作
        self.about_action = QAction('关于(&A)', self)
        self.about_action.setStatusTip('关于此应用程序')
        self.about_action.triggered.connect(self.about)
    
    def create_menus(self):
        """创建菜单栏"""
        menubar = self.menuBar()
        
        # 文件菜单
        file_menu = menubar.addMenu('文件(&F)')
        file_menu.addAction(self.new_action)
        file_menu.addAction(self.open_action)
        file_menu.addSeparator()
        file_menu.addAction(self.save_action)
        file_menu.addAction(self.save_as_action)
        file_menu.addSeparator()
        file_menu.addAction(self.exit_action)
        
        # 编辑菜单
        edit_menu = menubar.addMenu('编辑(&E)')
        edit_menu.addAction(self.undo_action)
        edit_menu.addAction(self.redo_action)
        edit_menu.addSeparator()
        edit_menu.addAction(self.cut_action)
        edit_menu.addAction(self.copy_action)
        edit_menu.addAction(self.paste_action)
        
        # 帮助菜单
        help_menu = menubar.addMenu('帮助(&H)')
        help_menu.addAction(self.about_action)
    
    def create_toolbars(self):
        """创建工具栏"""
        # 主工具栏
        main_toolbar = self.addToolBar('主工具栏')
        main_toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        
        main_toolbar.addAction(self.new_action)
        main_toolbar.addAction(self.open_action)
        main_toolbar.addAction(self.save_action)
        main_toolbar.addSeparator()
        main_toolbar.addAction(self.cut_action)
        main_toolbar.addAction(self.copy_action)
        main_toolbar.addAction(self.paste_action)
        
        # 编辑工具栏
        edit_toolbar = self.addToolBar('编辑工具栏')
        edit_toolbar.addAction(self.undo_action)
        edit_toolbar.addAction(self.redo_action)
    
    def create_statusbar(self):
        """创建状态栏"""
        self.status_bar = self.statusBar()
        self.status_bar.showMessage('就绪')
        
        # 添加永久组件
        self.char_count_label = QLabel('字符数: 0')
        self.status_bar.addPermanentWidget(self.char_count_label)
    
    def new_file(self):
        """新建文件"""
        if self.check_save():
            self.text_edit.clear()
            self.current_file = None
            self.is_modified = False
            self.update_title()
            self.status_bar.showMessage('新建文档', 2000)
    
    def open_file(self):
        """打开文件"""
        if self.check_save():
            file_path, _ = QFileDialog.getOpenFileName(
                self, '打开文件', '', 'Text Files (*.txt);;All Files (*)')
            
            if file_path:
                try:
                    with open(file_path, 'r', encoding='utf-8') as f:
                        content = f.read()
                    
                    self.text_edit.setPlainText(content)
                    self.current_file = file_path
                    self.is_modified = False
                    self.update_title()
                    self.status_bar.showMessage(f'打开文件: {file_path}', 2000)
                except Exception as e:
                    QMessageBox.warning(self, '错误', f'无法打开文件:\n{str(e)}')
    
    def save_file(self):
        """保存文件"""
        if self.current_file:
            self.save_to_file(self.current_file)
        else:
            self.save_as_file()
    
    def save_as_file(self):
        """另存为文件"""
        file_path, _ = QFileDialog.getSaveFileName(
            self, '保存文件', '', 'Text Files (*.txt);;All Files (*)')
        
        if file_path:
            self.save_to_file(file_path)
    
    def save_to_file(self, file_path):
        """保存到指定文件"""
        try:
            with open(file_path, 'w', encoding='utf-8') as f:
                f.write(self.text_edit.toPlainText())
            
            self.current_file = file_path
            self.is_modified = False
            self.update_title()
            self.status_bar.showMessage(f'保存文件: {file_path}', 2000)
        except Exception as e:
            QMessageBox.warning(self, '错误', f'无法保存文件:\n{str(e)}')
    
    def check_save(self):
        """检查是否需要保存"""
        if self.is_modified:
            reply = QMessageBox.question(
                self, '保存确认', 
                '文档已修改,是否保存?',
                QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
            
            if reply == QMessageBox.Yes:
                self.save_file()
                return not self.is_modified  # 如果保存失败,返回False
            elif reply == QMessageBox.Cancel:
                return False
        
        return True
    
    def text_changed(self):
        """文本变化处理"""
        self.is_modified = True
        self.update_title()
        
        # 更新字符计数
        char_count = len(self.text_edit.toPlainText())
        self.char_count_label.setText(f'字符数: {char_count}')
    
    def update_title(self):
        """更新窗口标题"""
        title = '文本编辑器'
        if self.current_file:
            title += f' - {self.current_file}'
        if self.is_modified:
            title += ' *'
        self.setWindowTitle(title)
    
    def about(self):
        """关于对话框"""
        QMessageBox.about(self, '关于', 
                         '这是一个使用PyQt5开发的简单文本编辑器\n'
                         '演示了菜单栏、工具栏和状态栏的使用')
    
    def closeEvent(self, event):
        """窗口关闭事件"""
        if self.check_save():
            event.accept()
        else:
            event.ignore()

五、高级控件与复杂界面

5.1 表格与树形控件

QTableWidget表格操作

from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, 
                            QTableWidget, QTableWidgetItem, QPushButton,
                            QHeaderView, QAbstractItemView, QMessageBox,
                            QInputDialog, QLineEdit)
from PyQt5.QtCore import Qt
import random

class TableDemo(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
        self.setup_table()
        self.load_sample_data()
    
    def initUI(self):
        self.setWindowTitle('表格控件示例')
        self.setGeometry(200, 200, 800, 600)
        
        layout = QVBoxLayout()
        
        # 按钮区域
        button_layout = QHBoxLayout()
        
        self.add_btn = QPushButton('添加行')
        self.add_btn.clicked.connect(self.add_row)
        button_layout.addWidget(self.add_btn)
        
        self.delete_btn = QPushButton('删除行')
        self.delete_btn.clicked.connect(self.delete_row)
        button_layout.addWidget(self.delete_btn)
        
        self.edit_btn = QPushButton('编辑')
        self.edit_btn.clicked.connect(self.edit_item)
        button_layout.addWidget(self.edit_btn)
        
        self.search_btn = QPushButton('搜索')
        self.search_btn.clicked.connect(self.search_items)
        button_layout.addWidget(self.search_btn)
        
        button_layout.addStretch()
        layout.addLayout(button_layout)
        
        # 表格
        self.table = QTableWidget()
        layout.addWidget(self.table)
        
        self.setLayout(layout)
    
    def setup_table(self):
        """设置表格"""
        # 设置列数和列标题
        columns = ['ID', '姓名', '年龄', '部门', '薪资', '入职日期']
        self.table.setColumnCount(len(columns))
        self.table.setHorizontalHeaderLabels(columns)
        
        # 设置表格属性
        self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.table.setAlternatingRowColors(True)
        
        # 设置列宽
        header = self.table.horizontalHeader()
        header.setSectionResizeMode(0, QHeaderView.ResizeToContents)  # ID列
        header.setSectionResizeMode(1, QHeaderView.Stretch)  # 姓名列
        header.setSectionResizeMode(2, QHeaderView.ResizeToContents)  # 年龄列
        header.setSectionResizeMode(3, QHeaderView.Stretch)  # 部门列
        header.setSectionResizeMode(4, QHeaderView.ResizeToContents)  # 薪资列
        header.setSectionResizeMode(5, QHeaderView.ResizeToContents)  # 入职日期列
        
        # 连接信号
        self.table.cellDoubleClicked.connect(self.on_cell_double_clicked)
        self.table.itemSelectionChanged.connect(self.on_selection_changed)
    
    def load_sample_data(self):
        """加载示例数据"""
        sample_data = [
            [1, '张三', 28, '技术部', 15000, '2020-01-15'],
            [2, '李四', 32, '销售部', 12000, '2019-05-20'],
            [3, '王五', 26, '技术部', 13000, '2021-03-10'],
            [4, '赵六', 35, '人事部', 11000, '2018-08-05'],
            [5, '钱七', 29, '财务部', 14000, '2020-11-12'],
            [6, '孙八', 31, '技术部', 16000, '2019-02-28'],
            [7, '周九', 27, '销售部', 11500, '2021-06-15'],
            [8, '吴十', 33, '技术部', 17000, '2018-12-01']
        ]
        
        self.table.setRowCount(len(sample_data))
        
        for row, data in enumerate(sample_data):
            for col, value in enumerate(data):
                item = QTableWidgetItem(str(value))
                
                # 设置ID列不可编辑
                if col == 0:
                    item.setFlags(item.flags() & ~Qt.ItemIsEditable)
                
                # 设置数值列对齐方式
                if col in [0, 2, 4]:  # ID、年龄、薪资列
                    item.setTextAlignment(Qt.AlignCenter)
                
                self.table.setItem(row, col, item)
    
    def add_row(self):
        """添加行"""
        dialog_data = self.get_employee_data()
        if dialog_data:
            row_count = self.table.rowCount()
            self.table.insertRow(row_count)
            
            # 生成新ID
            new_id = self.get_next_id()
            
            # 设置数据
            data = [new_id] + dialog_data
            for col, value in enumerate(data):
                item = QTableWidgetItem(str(value))
                
                if col == 0:  # ID列不可编辑
                    item.setFlags(item.flags() & ~Qt.ItemIsEditable)
                
                if col in [0, 2, 4]:  # 数值列居中
                    item.setTextAlignment(Qt.AlignCenter)
                
                self.table.setItem(row_count, col, item)
    
    def delete_row(self):
        """删除选中行"""
        current_row = self.table.currentRow()
        if current_row >= 0:
            reply = QMessageBox.question(
                self, '确认删除', 
                f'确定要删除第{current_row + 1}行数据吗?',
                QMessageBox.Yes | QMessageBox.No)
            
            if reply == QMessageBox.Yes:
                self.table.removeRow(current_row)
    
    def edit_item(self):
        """编辑选中项"""
        current_row = self.table.currentRow()
        if current_row >= 0:
            # 获取当前行数据
            current_data = []
            for col in range(1, self.table.columnCount()):  # 跳过ID列
                item = self.table.item(current_row, col)
                current_data.append(item.text() if item else '')
            
            # 显示编辑对话框
            dialog_data = self.get_employee_data(current_data)
            if dialog_data:
                # 更新数据
                for col, value in enumerate(dialog_data, 1):
                    item = QTableWidgetItem(str(value))
                    if col in [2, 4]:  # 年龄、薪资列居中
                        item.setTextAlignment(Qt.AlignCenter)
                    self.table.setItem(current_row, col, item)
    
    def search_items(self):
        """搜索功能"""
        search_text, ok = QInputDialog.getText(self, '搜索', '请输入搜索关键词:')
        
        if ok and search_text:
            # 清除之前的选择
            self.table.clearSelection()
            
            # 搜索匹配项
            found_items = self.table.findItems(search_text, Qt.MatchContains)
            
            if found_items:
                # 选中找到的项
                for item in found_items:
                    item.setSelected(True)
                
                # 滚动到第一个匹配项
                self.table.scrollToItem(found_items[0])
                
                QMessageBox.information(self, '搜索结果', 
                                      f'找到 {len(found_items)} 个匹配项')
            else:
                QMessageBox.information(self, '搜索结果', '未找到匹配项')
    
    def get_employee_data(self, current_data=None):
        """获取员工数据对话框"""
        from PyQt5.QtWidgets import QDialog, QFormLayout, QDialogButtonBox
        
        dialog = QDialog(self)
        dialog.setWindowTitle('员工信息')
        dialog.setModal(True)
        
        layout = QFormLayout()
        
        # 创建输入控件
        name_edit = QLineEdit()
        age_edit = QLineEdit()
        dept_edit = QLineEdit()
        salary_edit = QLineEdit()
        date_edit = QLineEdit()
        
        # 如果有当前数据,填充到输入框
        if current_data:
            name_edit.setText(current_data[0])
            age_edit.setText(current_data[1])
            dept_edit.setText(current_data[2])
            salary_edit.setText(current_data[3])
            date_edit.setText(current_data[4])
        
        # 添加到布局
        layout.addRow('姓名:', name_edit)
        layout.addRow('年龄:', age_edit)
        layout.addRow('部门:', dept_edit)
        layout.addRow('薪资:', salary_edit)
        layout.addRow('入职日期:', date_edit)
        
        # 按钮
        buttons = QDialogButtonBox(
            QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        buttons.accepted.connect(dialog.accept)
        buttons.rejected.connect(dialog.reject)
        layout.addRow(buttons)
        
        dialog.setLayout(layout)
        
        if dialog.exec_() == QDialog.Accepted:
            return [
                name_edit.text(),
                age_edit.text(),
                dept_edit.text(),
                salary_edit.text(),
                date_edit.text()
            ]
        return None
    
    def get_next_id(self):
        """获取下一个ID"""
        max_id = 0
        for row in range(self.table.rowCount()):
            item = self.table.item(row, 0)
            if item:
                try:
                    id_value = int(item.text())
                    max_id = max(max_id, id_value)
                except ValueError:
                    pass
        return max_id + 1
    
    def on_cell_double_clicked(self, row, column):
        """单元格双击事件"""
        if column != 0:  # 非ID列才能编辑
            self.edit_item()
    
    def on_selection_changed(self):
        """选择变化事件"""
        has_selection = len(self.table.selectedItems()) > 0
        self.delete_btn.setEnabled(has_selection)
        self.edit_btn.setEnabled(has_selection)

QTreeWidget树形结构

from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, 
                            QTreeWidget, QTreeWidgetItem, QPushButton,
                            QInputDialog, QMessageBox, QMenu, QAction)
from PyQt5.QtCore import Qt

class TreeDemo(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
        self.setup_tree()
        self.load_sample_data()
    
    def initUI(self):
        self.setWindowTitle('树形控件示例 - 组织结构')
        self.setGeometry(200, 200, 600, 500)
        
        layout = QVBoxLayout()
        
        # 按钮区域
        button_layout = QHBoxLayout()
        
        self.add_root_btn = QPushButton('添加根节点')
        self.add_root_btn.clicked.connect(self.add_root_node)
        button_layout.addWidget(self.add_root_btn)
        
        self.add_child_btn = QPushButton('添加子节点')
        self.add_child_btn.clicked.connect(self.add_child_node)
        button_layout.addWidget(self.add_child_btn)
        
        self.delete_btn = QPushButton('删除节点')
        self.delete_btn.clicked.connect(self.delete_node)
        button_layout.addWidget(self.delete_btn)
        
        self.expand_all_btn = QPushButton('展开所有')
        self.expand_all_btn.clicked.connect(self.tree.expandAll)
        button_layout.addWidget(self.expand_all_btn)
        
        self.collapse_all_btn = QPushButton('收起所有')
        self.collapse_all_btn.clicked.connect(self.tree.collapseAll)
        button_layout.addWidget(self.collapse_all_btn)
        
        button_layout.addStretch()
        layout.addLayout(button_layout)
        
        # 树形控件
        self.tree = QTreeWidget()
        layout.addWidget(self.tree)
        
        self.setLayout(layout)
    
    def setup_tree(self):
        """设置树形控件"""
        # 设置列标题
        self.tree.setHeaderLabels(['部门/员工', '职位', '人数/工号'])
        
        # 设置列宽
        self.tree.setColumnWidth(0, 200)
        self.tree.setColumnWidth(1, 150)
        self.tree.setColumnWidth(2, 100)
        
        # 连接信号
        self.tree.itemClicked.connect(self.on_item_clicked)
        self.tree.itemDoubleClicked.connect(self.on_item_double_clicked)
        self.tree.itemSelectionChanged.connect(self.on_selection_changed)
        
        # 设置右键菜单
        self.tree.setContextMenuPolicy(Qt.CustomContextMenu)
        self.tree.customContextMenuRequested.connect(self.show_context_menu)
    
    def load_sample_data(self):
        """加载示例数据"""
        # 创建公司根节点
        company = QTreeWidgetItem(self.tree)
        company.setText(0, 'ABC科技公司')
        company.setText(1, '公司')
        company.setText(2, '156人')
        company.setExpanded(True)
        
        # 技术部
        tech_dept = QTreeWidgetItem(company)
        tech_dept.setText(0, '技术部')
        tech_dept.setText(1, '部门')
        tech_dept.setText(2, '45人')
        tech_dept.setExpanded(True)
        
        # 技术部员工
        tech_employees = [
            ('张三', '技术总监', 'T001'),
            ('李四', '高级工程师', 'T002'),
            ('王五', '前端工程师', 'T003'),
            ('赵六', '后端工程师', 'T004'),
            ('钱七', '测试工程师', 'T005')
        ]
        
        for name, position, emp_id in tech_employees:
            employee = QTreeWidgetItem(tech_dept)
            employee.setText(0, name)
            employee.setText(1, position)
            employee.setText(2, emp_id)
        
        # 销售部
        sales_dept = QTreeWidgetItem(company)
        sales_dept.setText(0, '销售部')
        sales_dept.setText(1, '部门')
        sales_dept.setText(2, '32人')
        
        sales_employees = [
            ('孙八', '销售经理', 'S001'),
            ('周九', '销售代表', 'S002'),
            ('吴十', '客户经理', 'S003')
        ]
        
        for name, position, emp_id in sales_employees:
            employee = QTreeWidgetItem(sales_dept)
            employee.setText(0, name)
            employee.setText(1, position)
            employee.setText(2, emp_id)
        
        # 人事部
        hr_dept = QTreeWidgetItem(company)
        hr_dept.setText(0, '人事部')
        hr_dept.setText(1, '部门')
        hr_dept.setText(2, '8人')
        
        hr_employees = [
            ('郑十一', '人事经理', 'H001'),
            ('王十二', '招聘专员', 'H002')
        ]
        
        for name, position, emp_id in hr_employees:
            employee = QTreeWidgetItem(hr_dept)
            employee.setText(0, name)
            employee.setText(1, position)
            employee.setText(2, emp_id)
    
    def add_root_node(self):
        """添加根节点"""
        name, ok = QInputDialog.getText(self, '添加根节点', '请输入节点名称:')
        if ok and name:
            root_item = QTreeWidgetItem(self.tree)
            root_item.setText(0, name)
            root_item.setText(1, '根节点')
            root_item.setText(2, '0')
            self.tree.setCurrentItem(root_item)
    
    def add_child_node(self):
        """添加子节点"""
        current_item = self.tree.currentItem()
        if current_item is None:
            QMessageBox.warning(self, '警告', '请先选择一个父节点')
            return
        
        name, ok = QInputDialog.getText(self, '添加子节点', '请输入节点名称:')
        if ok and name:
            child_item = QTreeWidgetItem(current_item)
            child_item.setText(0, name)
            child_item.setText(1, '子节点')
            child_item.setText(2, '1')
            
            # 展开父节点
            current_item.setExpanded(True)
            self.tree.setCurrentItem(child_item)
    
    def delete_node(self):
        """删除节点"""
        current_item = self.tree.currentItem()
        if current_item is None:
            QMessageBox.warning(self, '警告', '请先选择要删除的节点')
            return
        
        reply = QMessageBox.question(
            self, '确认删除', 
            f'确定要删除节点 "{current_item.text(0)}" 及其所有子节点吗?',
            QMessageBox.Yes | QMessageBox.No)
        
        if reply == QMessageBox.Yes:
            parent = current_item.parent()
            if parent:
                parent.removeChild(current_item)
            else:
                index = self.tree.indexOfTopLevelItem(current_item)
                self.tree.takeTopLevelItem(index)
    
    def show_context_menu(self, position):
        """显示右键菜单"""
        item = self.tree.itemAt(position)
        if item is None:
            return
        
        menu = QMenu()
        
        add_child_action = QAction('添加子节点', self)
        add_child_action.triggered.connect(self.add_child_node)
        menu.addAction(add_child_action)
        
        if item.parent() is not None:  # 不是根节点
            delete_action = QAction('删除节点', self)
            delete_action.triggered.connect(self.delete_node)
            menu.addAction(delete_action)
        
        menu.addSeparator()
        
        expand_action = QAction('展开节点', self)
        expand_action.triggered.connect(lambda: item.setExpanded(True))
        menu.addAction(expand_action)
        
        collapse_action = QAction('收起节点', self)
        collapse_action.triggered.connect(lambda: item.setExpanded(False))
        menu.addAction(collapse_action)
        
        menu.exec_(self.tree.mapToGlobal(position))
    
    def on_item_clicked(self, item, column):
        """项目点击事件"""
        print(f"点击了: {item.text(0)}")
    
    def on_item_double_clicked(self, item, column):
        """项目双击事件"""
        if column == 0:  # 只允许编辑第一列
            # 启用编辑模式
            self.tree.editItem(item, column)
    
    def on_selection_changed(self):
        """选择变化事件"""
        current_item = self.tree.currentItem()
        has_selection = current_item is not None
        
        self.add_child_btn.setEnabled(has_selection)
        self.delete_btn.setEnabled(has_selection and current_item.parent() is not None)

六、文件处理与数据管理

6.1 文件操作

import os
import json
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
                            QTextEdit, QFileDialog, QMessageBox, QLabel,
                            QListWidget, QSplitter)
from PyQt5.QtCore import Qt, QSettings

class FileManagerDemo(QWidget):
    def __init__(self):
        super().__init__()
        self.current_file = None
        self.recent_files = []
        self.settings = QSettings('FileManager', 'Demo')
        self.initUI()
        self.load_recent_files()
    
    def initUI(self):
        self.setWindowTitle('文件管理器')
        self.setGeometry(200, 200, 800, 600)
        
        # 创建分割器
        splitter = QSplitter(Qt.Horizontal)
        
        # 左侧:最近文件列表
        left_widget = QWidget()
        left_layout = QVBoxLayout()
        
        left_layout.addWidget(QLabel('最近文件:'))
        self.recent_list = QListWidget()
        self.recent_list.itemDoubleClicked.connect(self.open_recent_file)
        left_layout.addWidget(self.recent_list)
        
        clear_recent_btn = QPushButton('清空最近文件')
        clear_recent_btn.clicked.connect(self.clear_recent_files)
        left_layout.addWidget(clear_recent_btn)
        
        left_widget.setLayout(left_layout)
        left_widget.setMaximumWidth(250)
        
        # 右侧:主编辑区域
        right_widget = QWidget()
        right_layout = QVBoxLayout()
        
        # 工具栏
        toolbar_layout = QHBoxLayout()
        
        new_btn = QPushButton('新建')
        new_btn.clicked.connect(self.new_file)
        toolbar_layout.addWidget(new_btn)
        
        open_btn = QPushButton('打开')
        open_btn.clicked.connect(self.open_file)
        toolbar_layout.addWidget(open_btn)
        
        save_btn = QPushButton('保存')
        save_btn.clicked.connect(self.save_file)
        toolbar_layout.addWidget(save_btn)
        
        save_as_btn = QPushButton('另存为')
        save_as_btn.clicked.connect(self.save_as_file)
        toolbar_layout.addWidget(save_as_btn)
        
        toolbar_layout.addStretch()
        
        # 文件信息
        self.file_info_label = QLabel('无文件打开')
        toolbar_layout.addWidget(self.file_info_label)
        
        right_layout.addLayout(toolbar_layout)
        
        # 文本编辑器
        self.text_edit = QTextEdit()
        self.text_edit.textChanged.connect(self.on_text_changed)
        right_layout.addWidget(self.text_edit)
        
        right_widget.setLayout(right_layout)
        
        # 添加到分割器
        splitter.addWidget(left_widget)
        splitter.addWidget(right_widget)
        splitter.setSizes([200, 600])
        
        # 主布局
        main_layout = QVBoxLayout()
        main_layout.addWidget(splitter)
        self.setLayout(main_layout)
    
    def new_file(self):
        """新建文件"""
        if self.check_save_changes():
            self.text_edit.clear()
            self.current_file = None
            self.update_file_info()
    
    def open_file(self):
        """打开文件"""
        if self.check_save_changes():
            file_path, _ = QFileDialog.getOpenFileName(
                self, '打开文件', '', 
                'Text Files (*.txt);;Python Files (*.py);;All Files (*)')
            
            if file_path:
                self.load_file(file_path)
    
    def load_file(self, file_path):
        """加载文件"""
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                content = f.read()
            
            self.text_edit.setPlainText(content)
            self.current_file = file_path
            self.add_to_recent_files(file_path)
            self.update_file_info()
            
        except Exception as e:
            QMessageBox.warning(self, '错误', f'无法打开文件:\n{str(e)}')
    
    def save_file(self):
        """保存文件"""
        if self.current_file:
            self.save_to_file(self.current_file)
        else:
            self.save_as_file()
    
    def save_as_file(self):
        """另存为文件"""
        file_path, _ = QFileDialog.getSaveFileName(
            self, '保存文件', '', 
            'Text Files (*.txt);;Python Files (*.py);;All Files (*)')
        
        if file_path:
            self.save_to_file(file_path)
    
    def save_to_file(self, file_path):
        """保存到指定文件"""
        try:
            with open(file_path, 'w', encoding='utf-8') as f:
                f.write(self.text_edit.toPlainText())
            
            self.current_file = file_path
            self.add_to_recent_files(file_path)
            self.update_file_info()
            
            QMessageBox.information(self, '成功', '文件保存成功!')
            
        except Exception as e:
            QMessageBox.warning(self, '错误', f'无法保存文件:\n{str(e)}')
    
    def add_to_recent_files(self, file_path):
        """添加到最近文件列表"""
        if file_path in self.recent_files:
            self.recent_files.remove(file_path)
        
        self.recent_files.insert(0, file_path)
        
        # 最多保存10个最近文件
        if len(self.recent_files) > 10:
            self.recent_files = self.recent_files[:10]
        
        self.update_recent_list()
        self.save_recent_files()
    
    def update_recent_list(self):
        """更新最近文件列表显示"""
        self.recent_list.clear()
        for file_path in self.recent_files:
            if os.path.exists(file_path):
                self.recent_list.addItem(os.path.basename(file_path))
    
    def open_recent_file(self, item):
        """打开最近文件"""
        index = self.recent_list.row(item)
        if 0 <= index < len(self.recent_files):
            file_path = self.recent_files[index]
            if os.path.exists(file_path):
                if self.check_save_changes():
                    self.load_file(file_path)
            else:
                QMessageBox.warning(self, '错误', '文件不存在')
                self.recent_files.remove(file_path)
                self.update_recent_list()
                self.save_recent_files()
    
    def clear_recent_files(self):
        """清空最近文件"""
        self.recent_files.clear()
        self.update_recent_list()
        self.save_recent_files()
    
    def load_recent_files(self):
        """从设置中加载最近文件"""
        self.recent_files = self.settings.value('recent_files', [])
        if not isinstance(self.recent_files, list):
            self.recent_files = []
        self.update_recent_list()
    
    def save_recent_files(self):
        """保存最近文件到设置"""
        self.settings.setValue('recent_files', self.recent_files)
    
    def update_file_info(self):
        """更新文件信息显示"""
        if self.current_file:
            file_name = os.path.basename(self.current_file)
            file_size = os.path.getsize(self.current_file)
            self.file_info_label.setText(f'{file_name} ({file_size} bytes)')
        else:
            self.file_info_label.setText('无文件打开')
    
    def on_text_changed(self):
        """文本变化事件"""
        # 可以在这里添加自动保存等功能
        pass
    
    def check_save_changes(self):
        """检查是否需要保存更改"""
        # 简化版本,实际应用中应该检查文本是否已修改
        if self.text_edit.toPlainText().strip():
            reply = QMessageBox.question(
                self, '保存确认', 
                '当前文档可能有未保存的更改,是否继续?',
                QMessageBox.Yes | QMessageBox.No)
            return reply == QMessageBox.Yes
        return True
    
    def closeEvent(self, event):
        """窗口关闭事件"""
        if self.check_save_changes():
            self.save_recent_files()
            event.accept()
        else:
            event.ignore()

6.2 配置文件管理

import json
import configparser
from PyQt5.QtCore import QSettings
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QFormLayout,
                            QLineEdit, QSpinBox, QCheckBox, QComboBox,
                            QPushButton, QTabWidget, QGroupBox, QMessageBox)

class ConfigManagerDemo(QWidget):
    def __init__(self):
        super().__init__()
        self.config_methods = {
            'QSettings': self.load_qsettings,
            'JSON': self.load_json_config,
            'INI': self.load_ini_config
        }
        self.initUI()
        self.load_config()
    
    def initUI(self):
        self.setWindowTitle('配置管理示例')
        self.setGeometry(300, 300, 500, 600)
        
        layout = QVBoxLayout()
        
        # 配置方式选择
        method_layout = QHBoxLayout()
        method_layout.addWidget(QLabel('配置方式:'))
        
        self.method_combo = QComboBox()
        self.method_combo.addItems(list(self.config_methods.keys()))
        self.method_combo.currentTextChanged.connect(self.on_method_changed)
        method_layout.addWidget(self.method_combo)
        
        method_layout.addStretch()
        layout.addLayout(method_layout)
        
        # 创建选项卡
        self.tab_widget = QTabWidget()
        
        # 通用设置选项卡
        self.create_general_tab()
        
        # 界面设置选项卡
        self.create_ui_tab()
        
        # 高级设置选项卡
        self.create_advanced_tab()
        
        layout.addWidget(self.tab_widget)
        
        # 按钮区域
        button_layout = QHBoxLayout()
        
        load_btn = QPushButton('加载配置')
        load_btn.clicked.connect(self.load_config)
        button_layout.addWidget(load_btn)
        
        save_btn = QPushButton('保存配置')
        save_btn.clicked.connect(self.save_config)
        button_layout.addWidget(save_btn)
        
        reset_btn = QPushButton('重置默认')
        reset_btn.clicked.connect(self.reset_to_defaults)
        button_layout.addWidget(reset_btn)
        
        button_layout.addStretch()
        layout.addLayout(button_layout)
        
        self.setLayout(layout)
    
    def create_general_tab(self):
        """创建通用设置选项卡"""
        general_widget = QWidget()
        layout = QVBoxLayout()
        
        # 基本信息组
        basic_group = QGroupBox('基本信息')
        basic_layout = QFormLayout()
        
        self.username_edit = QLineEdit()
        basic_layout.addRow('用户名:', self.username_edit)
        
        self.email_edit = QLineEdit()
        basic_layout.addRow('邮箱:', self.email_edit)
        
        self.auto_save_check = QCheckBox('自动保存')
        basic_layout.addRow('', self.auto_save_check)
        
        basic_group.setLayout(basic_layout)
        layout.addWidget(basic_group)
        
        # 文件设置组
        file_group = QGroupBox('文件设置')
        file_layout = QFormLayout()
        
        self.max_recent_spin = QSpinBox()
        self.max_recent_spin.setRange(1, 20)
        file_layout.addRow('最近文件数量:', self.max_recent_spin)
        
        self.default_encoding_combo = QComboBox()
        self.default_encoding_combo.addItems(['UTF-8', 'GBK', 'ASCII'])
        file_layout.addRow('默认编码:', self.default_encoding_combo)
        
        file_group.setLayout(file_layout)
        layout.addWidget(file_group)
        
        layout.addStretch()
        general_widget.setLayout(layout)
        self.tab_widget.addTab(general_widget, '通用')
    
    def create_ui_tab(self):
        """创建界面设置选项卡"""
        ui_widget = QWidget()
        layout = QVBoxLayout()
        
        # 外观设置组
        appearance_group = QGroupBox('外观设置')
        appearance_layout = QFormLayout()
        
        self.theme_combo = QComboBox()
        self.theme_combo.addItems(['默认', '深色', '浅色'])
        appearance_layout.addRow('主题:', self.theme_combo)
        
        self.font_size_spin = QSpinBox()
        self.font_size_spin.setRange(8, 24)
        appearance_layout.addRow('字体大小:', self.font_size_spin)
        
        self.show_toolbar_check = QCheckBox('显示工具栏')
        appearance_layout.addRow('', self.show_toolbar_check)
        
        self.show_statusbar_check = QCheckBox('显示状态栏')
        appearance_layout.addRow('', self.show_statusbar_check)
        
        appearance_group.setLayout(appearance_layout)
        layout.addWidget(appearance_group)
        
        # 窗口设置组
        window_group = QGroupBox('窗口设置')
        window_layout = QFormLayout()
        
        self.window_width_spin = QSpinBox()
        self.window_width_spin.setRange(400, 2000)
        window_layout.addRow('窗口宽度:', self.window_width_spin)
        
        self.window_height_spin = QSpinBox()
        self.window_height_spin.setRange(300, 1500)
        window_layout.addRow('窗口高度:', self.window_height_spin)
        
        self.remember_size_check = QCheckBox('记住窗口大小')
        window_layout.addRow('', self.remember_size_check)
        
        window_group.setLayout(window_layout)
        layout.addWidget(window_group)
        
        layout.addStretch()
        ui_widget.setLayout(layout)
        self.tab_widget.addTab(ui_widget, '界面')
    
    def create_advanced_tab(self):
        """创建高级设置选项卡"""
        advanced_widget = QWidget()
        layout = QVBoxLayout()
        
        # 性能设置组
        performance_group = QGroupBox('性能设置')
        performance_layout = QFormLayout()
        
        self.cache_size_spin = QSpinBox()
        self.cache_size_spin.setRange(10, 1000)
        self.cache_size_spin.setSuffix(' MB')
        performance_layout.addRow('缓存大小:', self.cache_size_spin)
        
        self.thread_count_spin = QSpinBox()
        self.thread_count_spin.setRange(1, 16)
        performance_layout.addRow('线程数量:', self.thread_count_spin)
        
        self.enable_logging_check = QCheckBox('启用日志')
        performance_layout.addRow('', self.enable_logging_check)
        
        performance_group.setLayout(performance_layout)
        layout.addWidget(performance_group)
        
        # 网络设置组
        network_group = QGroupBox('网络设置')
        network_layout = QFormLayout()
        
        self.proxy_host_edit = QLineEdit()
        network_layout.addRow('代理主机:', self.proxy_host_edit)
        
        self.proxy_port_spin = QSpinBox()
        self.proxy_port_spin.setRange(1, 65535)
        network_layout.addRow('代理端口:', self.proxy_port_spin)
        
        self.timeout_spin = QSpinBox()
        self.timeout_spin.setRange(1, 300)
        self.timeout_spin.setSuffix(' 秒')
        network_layout.addRow('超时时间:', self.timeout_spin)
        
        network_group.setLayout(network_layout)
        layout.addWidget(network_group)
        
        layout.addStretch()
        advanced_widget.setLayout(layout)
        self.tab_widget.addTab(advanced_widget, '高级')
    
    def get_default_config(self):
        """获取默认配置"""
        return {
            # 通用设置
            'username': 'User',
            'email': 'user@example.com',
            'auto_save': True,
            'max_recent_files': 10,
            'default_encoding': 'UTF-8',
            
            # 界面设置
            'theme': '默认',
            'font_size': 12,
            'show_toolbar': True,
            'show_statusbar': True,
            'window_width': 800,
            'window_height': 600,
            'remember_window_size': True,
            
            # 高级设置
            'cache_size': 100,
            'thread_count': 4,
            'enable_logging': False,
            'proxy_host': '',
            'proxy_port': 8080,
            'timeout': 30
        }
    
    def load_config(self):
        """加载配置"""
        method = self.method_combo.currentText()
        if method in self.config_methods:
            config = self.config_methods[method]()
            self.apply_config(config)
    
    def load_qsettings(self):
        """使用QSettings加载配置"""
        settings = QSettings('ConfigDemo', 'Application')
        config = {}
        
        defaults = self.get_default_config()
        for key, default_value in defaults.items():
            config[key] = settings.value(key, default_value)
            
            # QSettings的类型转换
            if isinstance(default_value, bool):
                config[key] = str(config[key]).lower() == 'true'
            elif isinstance(default_value, int):
                config[key] = int(config[key])
        
        return config
    
    def load_json_config(self):
        """从JSON文件加载配置"""
        try:
            with open('config.json', 'r', encoding='utf-8') as f:
                config = json.load(f)
            return {**self.get_default_config(), **config}
        except FileNotFoundError:
            return self.get_default_config()
        except Exception as e:
            QMessageBox.warning(self, '错误', f'加载JSON配置失败:\n{str(e)}')
            return self.get_default_config()
    
    def load_ini_config(self):
        """从INI文件加载配置"""
        config = configparser.ConfigParser()
        defaults = self.get_default_config()
        
        try:
            config.read('config.ini', encoding='utf-8')
            
            result = {}
            for key, default_value in defaults.items():
                section = 'General'
                if key in ['theme', 'font_size', 'show_toolbar', 'show_statusbar', 
                          'window_width', 'window_height', 'remember_window_size']:
                    section = 'UI'
                elif key in ['cache_size', 'thread_count', 'enable_logging', 
                           'proxy_host', 'proxy_port', 'timeout']:
                    section = 'Advanced'
                
                if config.has_option(section, key):
                    value = config.get(section, key)
                    if isinstance(default_value, bool):
                        result[key] = value.lower() == 'true'
                    elif isinstance(default_value, int):
                        result[key] = int(value)
                    else:
                        result[key] = value
                else:
                    result[key] = default_value
            
            return result
        
        except Exception as e:
            QMessageBox.warning(self, '错误', f'加载INI配置失败:\n{str(e)}')
            return defaults
    
    def apply_config(self, config):
        """应用配置到界面"""
        # 通用设置
        self.username_edit.setText(config.get('username', ''))
        self.email_edit.setText(config.get('email', ''))
        self.auto_save_check.setChecked(config.get('auto_save', True))
        self.max_recent_spin.setValue(config.get('max_recent_files', 10))
        
        encoding = config.get('default_encoding', 'UTF-8')
        index = self.default_encoding_combo.findText(encoding)
        if index >= 0:
            self.default_encoding_combo.setCurrentIndex(index)
        
        # 界面设置
        theme = config.get('theme', '默认')
        index = self.theme_combo.findText(theme)
        if index >= 0:
            self.theme_combo.setCurrentIndex(index)
        
        self.font_size_spin.setValue(config.get('font_size', 12))
        self.show_toolbar_check.setChecked(config.get('show_toolbar', True))
        self.show_statusbar_check.setChecked(config.get('show_statusbar', True))
        self.window_width_spin.setValue(config.get('window_width', 800))
        self.window_height_spin.setValue(config.get('window_height', 600))
        self.remember_size_check.setChecked(config.get('remember_window_size', True))
        
        # 高级设置
        self.cache_size_spin.setValue(config.get('cache_size', 100))
        self.thread_count_spin.setValue(config.get('thread_count', 4))
        self.enable_logging_check.setChecked(config.get('enable_logging', False))
        self.proxy_host_edit.setText(config.get('proxy_host', ''))
        self.proxy_port_spin.setValue(config.get('proxy_port', 8080))
        self.timeout_spin.setValue(config.get('timeout', 30))
    
    def get_current_config(self):
        """获取当前界面配置"""
        return {
            # 通用设置
            'username': self.username_edit.text(),
            'email': self.email_edit.text(),
            'auto_save': self.auto_save_check.isChecked(),
            'max_recent_files': self.max_recent_spin.value(),
            'default_encoding': self.default_encoding_combo.currentText(),
            
            # 界面设置
            'theme': self.theme_combo.currentText(),
            'font_size': self.font_size_spin.value(),
            'show_toolbar': self.show_toolbar_check.isChecked(),
            'show_statusbar': self.show_statusbar_check.isChecked(),
            'window_width': self.window_width_spin.value(),
            'window_height': self.window_height_spin.value(),
            'remember_window_size': self.remember_size_check.isChecked(),
            
            # 高级设置
            'cache_size': self.cache_size_spin.value(),
            'thread_count': self.thread_count_spin.value(),
            'enable_logging': self.enable_logging_check.isChecked(),
            'proxy_host': self.proxy_host_edit.text(),
            'proxy_port': self.proxy_port_spin.value(),
            'timeout': self.timeout_spin.value()
        }
    
    def save_config(self):
        """保存配置"""
        method = self.method_combo.currentText()
        config = self.get_current_config()
        
        try:
            if method == 'QSettings':
                self.save_qsettings(config)
            elif method == 'JSON':
                self.save_json_config(config)
            elif method == 'INI':
                self.save_ini_config(config)
            
            QMessageBox.information(self, '成功', '配置保存成功!')
        
        except Exception as e:
            QMessageBox.warning(self, '错误', f'保存配置失败:\n{str(e)}')
    
    def save_qsettings(self, config):
        """使用QSettings保存配置"""
        settings = QSettings('ConfigDemo', 'Application')
        for key, value in config.items():
            settings.setValue(key, value)
        settings.sync()
    
    def save_json_config(self, config):
        """保存到JSON文件"""
        with open('config.json', 'w', encoding='utf-8') as f:
            json.dump(config, f, indent=2, ensure_ascii=False)
    
    def save_ini_config(self, config):
        """保存到INI文件"""
        ini_config = configparser.ConfigParser()
        
        # 分组保存
        ini_config.add_section('General')
        ini_config.add_section('UI')
        ini_config.add_section('Advanced')
        
        # 通用设置
        general_keys = ['username', 'email', 'auto_save', 'max_recent_files', 'default_encoding']
        for key in general_keys:
            ini_config.set('General', key, str(config[key]))
        
        # 界面设置
        ui_keys = ['theme', 'font_size', 'show_toolbar', 'show_statusbar', 
                  'window_width', 'window_height', 'remember_window_size']
        for key in ui_keys:
            ini_config.set('UI', key, str(config[key]))
        
        # 高级设置
        advanced_keys = ['cache_size', 'thread_count', 'enable_logging', 
                        'proxy_host', 'proxy_port', 'timeout']
        for key in advanced_keys:
            ini_config.set('Advanced', key, str(config[key]))
        
        with open('config.ini', 'w', encoding='utf-8') as f:
            ini_config.write(f)
    
    def reset_to_defaults(self):
        """重置为默认配置"""
        reply = QMessageBox.question(
            self, '确认重置', 
            '确定要重置为默认配置吗?',
            QMessageBox.Yes | QMessageBox.No)
        
        if reply == QMessageBox.Yes:
            defaults = self.get_default_config()
            self.apply_config(defaults)
    
    def on_method_changed(self, method):
        """配置方式改变"""
        self.load_config()

七、多线程与性能优化

7.1 多线程编程

import time
import requests
from PyQt5.QtCore import QThread, pyqtSignal, QMutex, QWaitCondition
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
                            QProgressBar, QTextEdit, QLabel, QSpinBox,
                            QListWidget, QGroupBox)

class WorkerThread(QThread):
    """工作线程示例"""
    
    # 定义信号
    progress_updated = pyqtSignal(int)
    status_updated = pyqtSignal(str)
    result_ready = pyqtSignal(str)
    error_occurred = pyqtSignal(str)
    
    def __init__(self, task_type='download', urls=None):
        super().__init__()
        self.task_type = task_type
        self.urls = urls or []
        self.is_running = False
        self.is_paused = False
        self.mutex = QMutex()
        self.condition = QWaitCondition()
    
    def run(self):
        """线程主函数"""
        self.is_running = True
        
        try:
            if self.task_type == 'download':
                self.download_files()
            elif self.task_type == 'process':
                self.process_data()
            elif self.task_type == 'calculate':
                self.heavy_calculation()
        
        except Exception as e:
            self.error_occurred.emit(str(e))
        
        finally:
            self.is_running = False
    
    def download_files(self):
        """下载文件示例"""
        total_files = len(self.urls)
        
        for i, url in enumerate(self.urls):
            if not self.is_running:
                break
            
            # 检查暂停状态
            self.check_pause()
            
            self.status_updated.emit(f"正在下载: {url}")
            
            try:
                # 模拟下载
                response = requests.get(url, timeout=10)
                if response.status_code == 200:
                    self.result_ready.emit(f"下载成功: {url}")
                else:
                    self.result_ready.emit(f"下载失败: {url} (状态码: {response.status_code})")
            
            except Exception as e:
                self.result_ready.emit(f"下载错误: {url} ({str(e)})")
            
            # 更新进度
            progress = int((i + 1) / total_files * 100)
            self.progress_updated.emit(progress)
            
            # 模拟下载时间
            self.msleep(1000)
    
    def process_data(self):
        """数据处理示例"""
        total_steps = 100
        
        for i in range(total_steps):
            if not self.is_running:
                break
            
            self.check_pause()
            
            self.status_updated.emit(f"处理步骤 {i + 1}/{total_steps}")
            
            # 模拟耗时操作
            self.msleep(50)
            
            # 更新进度
            progress = int((i + 1) / total_steps * 100)
            self.progress_updated.emit(progress)
        
        if self.is_running:
            self.result_ready.emit("数据处理完成")
    
    def heavy_calculation(self):
        """重计算示例"""
        n = 1000000
        total = 0
        
        for i in range(n):
            if not self.is_running:
                break
            
            if i % 10000 == 0:
                self.check_pause()
                progress = int(i / n * 100)
                self.progress_updated.emit(progress)
                self.status_updated.emit(f"计算进度: {i}/{n}")
            
            # 模拟重计算
            total += i * i
        
        if self.is_running:
            self.result_ready.emit(f"计算完成,结果: {total}")
    
    def check_pause(self):
        """检查暂停状态"""
        self.mutex.lock()
        try:
            while self.is_paused and self.is_running:
                self.condition.wait(self.mutex)
        finally:
            self.mutex.unlock()
    
    def pause(self):
        """暂停线程"""
        self.is_paused = True
    
    def resume(self):
        """恢复线程"""
        self.mutex.lock()
        try:
            self.is_paused = False
            self.condition.wakeAll()
        finally:
            self.mutex.unlock()
    
    def stop(self):
        """停止线程"""
        self.is_running = False
        self.resume()  # 确保线程能够退出

class DownloadManagerThread(QThread):
    """下载管理器线程"""
    
    download_started = pyqtSignal(str)
    download_finished = pyqtSignal(str, bool)
    progress_updated = pyqtSignal(str, int)
    
    def __init__(self):
        super().__init__()
        self.download_queue = []
        self.current_downloads = {}
        self.max_concurrent = 3
        self.mutex = QMutex()
    
    def add_download(self, url):
        """添加下载任务"""
        self.mutex.lock()
        try:
            self.download_queue.append(url)
        finally:
            self.mutex.unlock()
    
    def run(self):
        """运行下载管理器"""
        while True:
            self.mutex.lock()
            try:
                # 检查是否有新任务且当前下载数未达到上限
                if (self.download_queue and 
                    len(self.current_downloads) < self.max_concurrent):
                    
                    url = self.download_queue.pop(0)
                    self.start_download(url)
                
                # 检查已完成的下载
                completed = []
                for url, thread in self.current_downloads.items():
                    if not thread.isRunning():
                        completed.append(url)
                
                for url in completed:
                    thread = self.current_downloads.pop(url)
                    self.download_finished.emit(url, True)
            
            finally:
                self.mutex.unlock()
            
            self.msleep(100)  # 避免过度消耗CPU
    
    def start_download(self, url):
        """开始单个下载"""
        thread = SingleDownloadThread(url)
        thread.progress_updated.connect(
            lambda progress: self.progress_updated.emit(url, progress))
        
        self.current_downloads[url] = thread
        self.download_started.emit(url)
        thread.start()

class SingleDownloadThread(QThread):
    """单个下载线程"""
    
    progress_updated = pyqtSignal(int)
    
    def __init__(self, url):
        super().__init__()
        self.url = url
    
    def run(self):
        """执行下载"""
        try:
            # 模拟下载过程
            for i in range(101):
                self.progress_updated.emit(i)
                self.msleep(50)
        except Exception:
            pass

class ThreadDemo(QWidget):
    def __init__(self):
        super().__init__()
        self.worker_thread = None
        self.download_manager = None
        self.initUI()
    
    def initUI(self):
        self.setWindowTitle('多线程示例')
        self.setGeometry(200, 200, 700, 600)
        
        layout = QVBoxLayout()
        
        # 基本线程操作组
        basic_group = QGroupBox('基本线程操作')
        basic_layout = QVBoxLayout()
        
        # 任务类型选择
        task_layout = QHBoxLayout()
        task_layout.addWidget(QLabel('任务类型:'))
        
        self.download_btn = QPushButton('文件下载')
        self.download_btn.clicked.connect(lambda: self.start_task('download'))
        task_layout.addWidget(self.download_btn)
        
        self.process_btn = QPushButton('数据处理')
        self.process_btn.clicked.connect(lambda: self.start_task('process'))
        task_layout.addWidget(self.process_btn)
        
        self.calc_btn = QPushButton('重计算')
        self.calc_btn.clicked.connect(lambda: self.start_task('calculate'))
        task_layout.addWidget(self.calc_btn)
        
        task_layout.addStretch()
        basic_layout.addLayout(task_layout)
        
        # 控制按钮
        control_layout = QHBoxLayout()
        
        self.pause_btn = QPushButton('暂停')
        self.pause_btn.clicked.connect(self.pause_task)
        self.pause_btn.setEnabled(False)
        control_layout.addWidget(self.pause_btn)
        
        self.resume_btn = QPushButton('继续')
        self.resume_btn.clicked.connect(self.resume_task)
        self.resume_btn.setEnabled(False)
        control_layout.addWidget(self.resume_btn)
        
        self.stop_btn = QPushButton('停止')
        self.stop_btn.clicked.connect(self.stop_task)
        self.stop_btn.setEnabled(False)
        control_layout.addWidget(self.stop_btn)
        
        control_layout.addStretch()
        basic_layout.addLayout(control_layout)
        
        # 进度显示
        self.progress_bar = QProgressBar()
        basic_layout.addWidget(self.progress_bar)
        
        self.status_label = QLabel('就绪')
        basic_layout.addWidget(self.status_label)
        
        basic_group.setLayout(basic_layout)
        layout.addWidget(basic_group)
        
        # 下载管理器组
        download_group = QGroupBox('下载管理器')
        download_layout = QVBoxLayout()
        
        # 添加下载
        add_layout = QHBoxLayout()
        add_layout.addWidget(QLabel('并发数
     
        self.concurrent_spin = QSpinBox()
        self.concurrent_spin.setRange(1, 10)
        self.concurrent_spin.setValue(3)
        add_layout.addWidget(self.concurrent_spin)
        
        self.add_download_btn = QPushButton('添加下载任务')
        self.add_download_btn.clicked.connect(self.add_download_task)
        add_layout.addWidget(self.add_download_btn)
        
        self.start_manager_btn = QPushButton('启动管理器')
        self.start_manager_btn.clicked.connect(self.start_download_manager)
        add_layout.addWidget(self.start_manager_btn)
        
        add_layout.addStretch()
        download_layout.addLayout(add_layout)
        
        # 下载列表
        self.download_list = QListWidget()
        download_layout.addWidget(self.download_list)
        
        download_group.setLayout(download_layout)
        layout.addWidget(download_group)
        
        # 结果显示
        result_group = QGroupBox('执行结果')
        result_layout = QVBoxLayout()
        
        self.result_text = QTextEdit()
        self.result_text.setMaximumHeight(150)
        result_layout.addWidget(self.result_text)
        
        result_group.setLayout(result_layout)
        layout.addWidget(result_group)
        
        self.setLayout(layout)
    
    def start_task(self, task_type):
        """启动任务"""
        if self.worker_thread and self.worker_thread.isRunning():
            self.result_text.append("任务正在运行中...")
            return
        
        # 准备测试URL
        test_urls = [
            'https://httpbin.org/delay/1',
            'https://httpbin.org/delay/2',
            'https://httpbin.org/delay/1',
            'https://httpbin.org/status/200',
            'https://httpbin.org/json'
        ]
        
        self.worker_thread = WorkerThread(task_type, test_urls)
        
        # 连接信号
        self.worker_thread.progress_updated.connect(self.update_progress)
        self.worker_thread.status_updated.connect(self.update_status)
        self.worker_thread.result_ready.connect(self.show_result)
        self.worker_thread.error_occurred.connect(self.show_error)
        self.worker_thread.finished.connect(self.task_finished)
        
        # 更新UI状态
        self.set_task_running(True)
        
        # 启动线程
        self.worker_thread.start()
        self.result_text.append(f"开始执行{task_type}任务...")
    
    def pause_task(self):
        """暂停任务"""
        if self.worker_thread:
            self.worker_thread.pause()
            self.pause_btn.setEnabled(False)
            self.resume_btn.setEnabled(True)
            self.status_label.setText("任务已暂停")
    
    def resume_task(self):
        """恢复任务"""
        if self.worker_thread:
            self.worker_thread.resume()
            self.pause_btn.setEnabled(True)
            self.resume_btn.setEnabled(False)
            self.status_label.setText("任务已恢复")
    
    def stop_task(self):
        """停止任务"""
        if self.worker_thread:
            self.worker_thread.stop()
            self.worker_thread.wait()  # 等待线程结束
            self.task_finished()
            self.status_label.setText("任务已停止")
    
    def task_finished(self):
        """任务完成"""
        self.set_task_running(False)
        self.progress_bar.setValue(0)
        self.status_label.setText("任务完成")
    
    def set_task_running(self, running):
        """设置任务运行状态"""
        self.download_btn.setEnabled(not running)
        self.process_btn.setEnabled(not running)
        self.calc_btn.setEnabled(not running)
        self.pause_btn.setEnabled(running)
        self.stop_btn.setEnabled(running)
        self.resume_btn.setEnabled(False)
    
    def update_progress(self, value):
        """更新进度"""
        self.progress_bar.setValue(value)
    
    def update_status(self, status):
        """更新状态"""
        self.status_label.setText(status)
    
    def show_result(self, result):
        """显示结果"""
        self.result_text.append(result)
    
    def show_error(self, error):
        """显示错误"""
        self.result_text.append(f"错误: {error}")
    
    def start_download_manager(self):
        """启动下载管理器"""
        if self.download_manager and self.download_manager.isRunning():
            return
        
        self.download_manager = DownloadManagerThread()
        self.download_manager.max_concurrent = self.concurrent_spin.value()
        
        # 连接信号
        self.download_manager.download_started.connect(self.on_download_started)
        self.download_manager.download_finished.connect(self.on_download_finished)
        self.download_manager.progress_updated.connect(self.on_download_progress)
        
        self.download_manager.start()
        self.start_manager_btn.setText('管理器运行中')
        self.start_manager_btn.setEnabled(False)
    
    def add_download_task(self):
        """添加下载任务"""
        test_urls = [
            'https://httpbin.org/delay/2',
            'https://httpbin.org/delay/3',
            'https://httpbin.org/delay/1',
            'https://httpbin.org/json',
            'https://httpbin.org/status/200'
        ]
        
        if not self.download_manager:
            self.start_download_manager()
        
        for url in test_urls[:2]:  # 添加前两个URL
            self.download_manager.add_download(url)
            self.download_list.addItem(f"队列中: {url}")
    
    def on_download_started(self, url):
        """下载开始"""
        for i in range(self.download_list.count()):
            item = self.download_list.item(i)
            if url in item.text():
                item.setText(f"下载中: {url} (0%)")
                break
    
    def on_download_finished(self, url, success):
        """下载完成"""
        for i in range(self.download_list.count()):
            item = self.download_list.item(i)
            if url in item.text():
                status = "完成" if success else "失败"
                item.setText(f"{status}: {url}")
                break
    
    def on_download_progress(self, url, progress):
        """下载进度更新"""
        for i in range(self.download_list.count()):
            item = self.download_list.item(i)
            if url in item.text() and "下载中" in item.text():
                item.setText(f"下载中: {url} ({progress}%)")
                break
    
    def closeEvent(self, event):
        """窗口关闭事件"""
        # 停止所有线程
        if self.worker_thread and self.worker_thread.isRunning():
            self.worker_thread.stop()
            self.worker_thread.wait()
        
        if self.download_manager and self.download_manager.isRunning():
            self.download_manager.terminate()
            self.download_manager.wait()
        
        event.accept()