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

在当今的软件开发世界中,桌面应用程序依然占据着重要地位。虽然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框架对比

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

1.2 开发环境准备

Python环境要求

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

bash 复制代码
# 检查Python版本
python --version
# 或
python3 --version

PyQt5安装方法

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

bash 复制代码
# 安装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的可视化界面设计工具:

bash 复制代码
# 查找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: <math xmlns="http://www.w3.org/1998/Math/MathML"> P r o j e c t F i l e D i r ProjectFileDir </math>ProjectFileDir

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

1.3 第一个PyQt5程序

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

python 复制代码
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最重要的特性之一,它提供了对象间通信的机制:

python 复制代码
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()

自定义信号

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

python 复制代码
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是最常用的顶级窗口类,提供了菜单栏、工具栏、状态栏等:

python 复制代码
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 常用控件详解

按钮类控件

python 复制代码
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)

输入类控件

python 复制代码
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水平布局

python 复制代码
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垂直布局

python 复制代码
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网格布局

python 复制代码
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)

嵌套布局技巧

python 复制代码
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

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

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

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

.ui文件转换与使用

方法一:使用pyuic5工具转换

bash 复制代码
# 将.ui文件转换为.py文件
pyuic5 -o main_window.py main_window.ui

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

python 复制代码
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应用程序:

python 复制代码
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;
        }
        """

主题切换实现

python 复制代码
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提供了多种信号槽连接方式:

python 复制代码
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}")

自定义信号高级用法

python 复制代码
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 事件处理机制

鼠标事件处理

python 复制代码
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])

键盘事件处理

python 复制代码
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 用户交互实现

菜单栏与工具栏

python 复制代码
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表格操作

python 复制代码
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树形结构

python 复制代码
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 文件操作

python 复制代码
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 配置文件管理

python 复制代码
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 多线程编程

python 复制代码
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()
相关推荐
Felix_M.1 小时前
CLAM复现问题记录
python
猫头虎1 小时前
用 Python 写你的第一个爬虫:小白也能轻松搞定数据抓取(超详细包含最新所有Python爬虫库的教程)
爬虫·python·opencv·scrapy·beautifulsoup·numpy·scipy
三年呀1 小时前
**超融合架构中的发散创新:探索现代编程语言的挑战与机遇**一、引言随着数字化时代的快速发展,超融合架构已成为IT领域的一种重要趋势
python·架构
Q_Q19632884751 小时前
python基于Hadoop的超市数据分析系统
开发语言·hadoop·spring boot·python·django·flask·node.js
MediaTea2 小时前
Python 第三方库:Requests(HTTP 客户端)
开发语言·网络·python·网络协议·http
华科云商xiao徐2 小时前
分布式爬虫双核引擎:Java大脑+Python触手的完美协同
java·爬虫·python
计算机毕业设计木哥3 小时前
计算机毕设大数据选题推荐 基于spark+Hadoop+python的贵州茅台股票数据分析系统【源码+文档+调试】
大数据·hadoop·python·计算机网络·spark·课程设计
Re_draw_debubu3 小时前
torchvision中数据集的使用与DataLoader 小土堆pytorch记录
pytorch·python·小土堆
猫先生OVO3 小时前
shellgpt
python