PySide6 记事本应用开发教程

PySide6 记事本应用开发教程

基于 Python 3.12 的现代桌面应用开发实践

教程说明: 本教程采用渐进式开发模式,分步骤构建完整应用。每章都有对应的完整代码文件,建议按顺序学习。


第一章:项目搭建与环境配置

1.1 项目结构设计

复制代码
notepad_2026/
├── docs/           # 文档目录
├── src/            # 源代码目录
├── tests/          # 测试目录
└── requirements.txt # 依赖文件

1.2 环境要求

bash 复制代码
# requirements.txt
PySide6>=6.10.2

1.3 安装依赖

bash 复制代码
pip install -r requirements.txt

1.4 入口文件

python 复制代码
# src/main.py
"""
记事本应用入口文件
2026年最佳实践:
1. 明确类型注解
2. 使用清晰的模块导入
3. 结构化异常处理
"""

import sys
from PySide6.QtWidgets import QApplication


def main():
    """应用主入口函数"""
    app = QApplication(sys.argv)  # 创建Qt应用实例
    
    # 设置应用元数据
    app.setApplicationName("记事本2026")
    app.setOrganizationName("Python开发者")
    
    # 后续章节将在此处创建主窗口
    print("应用启动成功!")
    
    return app.exec()  # 进入事件循环


if __name__ == "__main__":
    sys.exit(main())

第二章:界面框架搭建

2.1 主窗口类设计

python 复制代码
# src/ui/window.py
"""
主窗口模块
负责窗口的基本布局和组件管理
"""

from PySide6.QtWidgets import QMainWindow, QTextEdit, QStatusBar
from PySide6.QtCore import Qt


class NotepadWindow(QMainWindow):
    """记事本主窗口类"""
    
    def __init__(self):
        super().__init__()
        
        # 初始化窗口属性
        self.current_file = None  # 当前打开的文件路径
        self.is_modified = False  # 文档修改状态
        
        self._setup_ui()  # 设置UI组件
        self._setup_window()  # 设置窗口属性
    
    def _setup_window(self):
        """配置窗口基本属性"""
        self.setWindowTitle("记事本 - 2026版")
        self.setGeometry(100, 100, 800, 600)  # 位置和大小
    
    def _setup_ui(self):
        """初始化所有UI组件"""
        # 创建中央文本编辑区
        self.text_edit = QTextEdit()
        self.text_edit.setFontPointSize(12)  # 设置默认字体大小
        self.setCentralWidget(self.text_edit)  # 设置为中心组件
        
        # 创建状态栏
        self.status_bar = QStatusBar()
        self.setStatusBar(self.status_bar)
        self.status_bar.showMessage("就绪 | 字符数: 0")
    
    def update_status(self, message: str):
        """更新状态栏信息"""
        char_count = len(self.text_edit.toPlainText())
        self.status_bar.showMessage(f"{message} | 字符数: {char_count}")

2.2 更新主文件

python 复制代码
# src/main.py
"""
第二章更新:整合主窗口
"""

import sys
from PySide6.QtWidgets import QApplication
from src.ui.window import NotepadWindow


def main():
    """应用主入口函数"""
    app = QApplication(sys.argv)
    app.setApplicationName("记事本2026")
    
    # 创建主窗口
    window = NotepadWindow()
    window.show()  # 显示窗口
    
    return app.exec()


if __name__ == "__main__":
    sys.exit(main())

第三章:菜单系统实现

3.1 完善菜单栏

python 复制代码
# src/ui/menu.py
"""
菜单系统模块
实现完整的菜单栏功能
"""

from PySide6.QtWidgets import QMenuBar, QMenu
from PySide6.QtGui import QAction, QKeySequence
from PySide6.QtCore import Qt


class MenuManager:
    """菜单管理器"""
    
    def __init__(self, window):
        self.window = window
        self.actions = {}  # 存储所有动作引用
        self._create_menus()
    
    def _create_menus(self):
        """创建所有菜单"""
        menubar = self.window.menuBar()
        
        # 文件菜单
        file_menu = self._create_file_menu(menubar)
        
        # 编辑菜单
        edit_menu = self._create_edit_menu(menubar)
        
        # 格式菜单
        format_menu = self._create_format_menu(menubar)
        
        # 帮助菜单
        help_menu = self._create_help_menu(menubar)
    
    def _create_file_menu(self, menubar) -> QMenu:
        """创建文件菜单"""
        file_menu = menubar.addMenu("文件(&F)")
        
        # 新建
        new_action = QAction("新建(&N)", self.window)
        new_action.setShortcut(QKeySequence.New)
        new_action.setStatusTip("创建新文档")
        self.actions['new'] = new_action
        file_menu.addAction(new_action)
        
        # 打开
        open_action = QAction("打开(&O)...", self.window)
        open_action.setShortcut(QKeySequence.Open)
        open_action.setStatusTip("打开现有文件")
        self.actions['open'] = open_action
        file_menu.addAction(open_action)
        
        file_menu.addSeparator()  # 分隔线
        
        # 保存
        save_action = QAction("保存(&S)", self.window)
        save_action.setShortcut(QKeySequence.Save)
        save_action.setStatusTip("保存当前文档")
        self.actions['save'] = save_action
        file_menu.addAction(save_action)
        
        # 另存为
        save_as_action = QAction("另存为(&A)...", self.window)
        save_as_action.setShortcut(QKeySequence("Ctrl+Shift+S"))
        save_as_action.setStatusTip("另存当前文档")
        self.actions['save_as'] = save_as_action
        file_menu.addAction(save_as_action)
        
        file_menu.addSeparator()
        
        # 退出
        exit_action = QAction("退出(&X)", self.window)
        exit_action.setShortcut(QKeySequence.Quit)
        exit_action.setStatusTip("退出程序")
        self.actions['exit'] = exit_action
        file_menu.addAction(exit_action)
        
        return file_menu
    
    def _create_edit_menu(self, menubar) -> QMenu:
        """创建编辑菜单"""
        edit_menu = menubar.addMenu("编辑(&E)")
        
        # 撤销/重做
        undo_action = QAction("撤销(&U)", self.window)
        undo_action.setShortcut(QKeySequence.Undo)
        undo_action.setEnabled(False)  # 初始禁用
        self.actions['undo'] = undo_action
        edit_menu.addAction(undo_action)
        
        redo_action = QAction("重做(&R)", self.window)
        redo_action.setShortcut(QKeySequence.Redo)
        redo_action.setEnabled(False)
        self.actions['redo'] = redo_action
        edit_menu.addAction(redo_action)
        
        edit_menu.addSeparator()
        
        # 剪切/复制/粘贴
        cut_action = QAction("剪切(&T)", self.window)
        cut_action.setShortcut(QKeySequence.Cut)
        self.actions['cut'] = cut_action
        edit_menu.addAction(cut_action)
        
        copy_action = QAction("复制(&C)", self.window)
        copy_action.setShortcut(QKeySequence.Copy)
        self.actions['copy'] = copy_action
        edit_menu.addAction(copy_action)
        
        paste_action = QAction("粘贴(&P)", self.window)
        paste_action.setShortcut(QKeySequence.Paste)
        self.actions['paste'] = paste_action
        edit_menu.addAction(paste_action)
        
        edit_menu.addSeparator()
        
        # 全选
        select_all_action = QAction("全选(&A)", self.window)
        select_all_action.setShortcut(QKeySequence.SelectAll)
        self.actions['select_all'] = select_all_action
        edit_menu.addAction(select_all_action)
        
        return edit_menu
    
    def _create_format_menu(self, menubar) -> QMenu:
        """创建格式菜单(第三章占位)"""
        format_menu = menubar.addMenu("格式(&O)")
        
        # 字体菜单(将在第四章实现)
        font_action = QAction("字体(&F)...", self.window)
        font_action.setEnabled(False)  # 暂未实现
        self.actions['font'] = font_action
        format_menu.addAction(font_action)
        
        return format_menu
    
    def _create_help_menu(self, menubar) -> QMenu:
        """创建帮助菜单"""
        help_menu = menubar.addMenu("帮助(&H)")
        
        # 关于
        about_action = QAction("关于(&A)...", self.window)
        about_action.setStatusTip("显示程序信息")
        self.actions['about'] = about_action
        help_menu.addAction(about_action)
        
        return help_menu

3.2 更新主窗口

python 复制代码
# src/ui/window.py
"""
第三章更新:集成菜单系统
"""

from PySide6.QtWidgets import QMainWindow, QTextEdit, QStatusBar
from PySide6.QtCore import Qt
from .menu import MenuManager


class NotepadWindow(QMainWindow):
    """记事本主窗口类"""
    
    def __init__(self):
        super().__init__()
        
        # 初始化属性
        self.current_file = None
        self.is_modified = False
        
        # 初始化UI组件
        self._setup_window()
        self._setup_ui()
        
        # 创建菜单管理器
        self.menu_manager = MenuManager(self)
        
        # 连接信号槽
        self._connect_signals()
    
    def _setup_window(self):
        """配置窗口属性"""
        self.setWindowTitle("记事本 - 2026版")
        self.setGeometry(100, 100, 800, 600)
    
    def _setup_ui(self):
        """设置UI组件"""
        self.text_edit = QTextEdit()
        self.text_edit.setFontPointSize(12)
        self.text_edit.textChanged.connect(self._on_text_changed)
        self.setCentralWidget(self.text_edit)
        
        self.status_bar = QStatusBar()
        self.setStatusBar(self.status_bar)
        self.update_status("就绪")
    
    def _connect_signals(self):
        """连接信号和槽函数"""
        # 文件菜单信号
        self.menu_manager.actions['new'].triggered.connect(self._on_new)
        self.menu_manager.actions['open'].triggered.connect(self._on_open)
        self.menu_manager.actions['save'].triggered.connect(self._on_save)
        self.menu_manager.actions['save_as'].triggered.connect(self._on_save_as)
        self.menu_manager.actions['exit'].triggered.connect(self.close)
        
        # 编辑菜单信号
        self.menu_manager.actions['undo'].triggered.connect(self.text_edit.undo)
        self.menu_manager.actions['redo'].triggered.connect(self.text_edit.redo)
        self.menu_manager.actions['cut'].triggered.connect(self.text_edit.cut)
        self.menu_manager.actions['copy'].triggered.connect(self.text_edit.copy)
        self.menu_manager.actions['paste'].triggered.connect(self.text_edit.paste)
        self.menu_manager.actions['select_all'].triggered.connect(
            self.text_edit.selectAll
        )
        
        # 帮助菜单信号
        self.menu_manager.actions['about'].triggered.connect(self._on_about)
    
    def _on_text_changed(self):
        """文本变化处理"""
        self.is_modified = True
        self.update_status("已修改")
    
    def _on_new(self):
        """新建文件"""
        print("新建文件功能(第四章实现)")
    
    def _on_open(self):
        """打开文件"""
        print("打开文件功能(第四章实现)")
    
    def _on_save(self):
        """保存文件"""
        print("保存文件功能(第四章实现)")
    
    def _on_save_as(self):
        """另存为"""
        print("另存为功能(第四章实现)")
    
    def _on_about(self):
        """关于对话框"""
        print("关于功能(第五章实现)")
    
    def update_status(self, message: str):
        """更新状态栏"""
        char_count = len(self.text_edit.toPlainText())
        self.status_bar.showMessage(f"{message} | 字符数: {char_count}")

第四章:文件操作模块

4.1 文件管理器

python 复制代码
# src/core/file_manager.py
"""
文件管理器模块
处理所有文件IO操作,符合2026年最佳实践:
1. 使用pathlib处理路径
2. 完整的错误处理
3. 支持多种编码
"""

from pathlib import Path
import json
from typing import Optional, Dict, Any


class FileManager:
    """文件管理器类"""
    
    def __init__(self):
        # 默认编码配置
        self.encoding = "utf-8"
        self.backup_enabled = True
        self.auto_save_interval = 300  # 自动保存间隔(秒)
    
    def read_text_file(self, file_path: str) -> str:
        """
        读取文本文件
        
        Args:
            file_path: 文件路径
            
        Returns:
            str: 文件内容
            
        Raises:
            FileNotFoundError: 文件不存在
            PermissionError: 无读取权限
            UnicodeDecodeError: 编码错误
        """
        path = Path(file_path)
        
        # 验证文件
        self._validate_file(path)
        
        # 尝试多种编码读取
        encodings = [self.encoding, "gbk", "utf-16", "latin-1"]
        
        for encoding in encodings:
            try:
                return path.read_text(encoding=encoding)
            except UnicodeDecodeError:
                continue
        
        # 所有编码尝试失败
        raise UnicodeDecodeError(
            f"无法解码文件,尝试的编码: {', '.join(encodings)}"
        )
    
    def write_text_file(self, file_path: str, content: str) -> None:
        """
        写入文本文件
        
        Args:
            file_path: 文件路径
            content: 要写入的内容
        """
        path = Path(file_path)
        
        # 创建备份(如果启用)
        if self.backup_enabled and path.exists():
            self._create_backup(path)
        
        # 确保目录存在
        path.parent.mkdir(parents=True, exist_ok=True)
        
        # 写入文件
        path.write_text(content, encoding=self.encoding)
    
    def get_file_info(self, file_path: str) -> Dict[str, Any]:
        """
        获取文件详细信息
        
        Args:
            file_path: 文件路径
            
        Returns:
            Dict: 文件信息字典
        """
        path = Path(file_path)
        
        if not path.exists():
            return {"error": "文件不存在"}
        
        stat = path.stat()
        
        return {
            "path": str(path.absolute()),
            "name": path.name,
            "size": self._format_size(stat.st_size),
            "created": stat.st_ctime,
            "modified": stat.st_mtime,
            "is_file": path.is_file(),
            "extension": path.suffix.lower(),
            "encoding": self._detect_encoding(path)
        }
    
    def _validate_file(self, path: Path) -> None:
        """验证文件是否可读"""
        if not path.exists():
            raise FileNotFoundError(f"文件不存在: {path}")
        
        if not path.is_file():
            raise IsADirectoryError(f"路径是目录: {path}")
        
        if not path.is_symlink() and not os.access(path, os.R_OK):
            raise PermissionError(f"无读取权限: {path}")
    
    def _create_backup(self, path: Path) -> None:
        """创建备份文件"""
        backup_path = path.with_suffix(f"{path.suffix}.bak")
        
        try:
            # 复制文件
            import shutil
            shutil.copy2(path, backup_path)
        except Exception as e:
            print(f"备份创建失败: {e}")
    
    def _format_size(self, size_bytes: int) -> str:
        """格式化文件大小"""
        for unit in ['B', 'KB', 'MB', 'GB']:
            if size_bytes < 1024.0:
                return f"{size_bytes:.1f} {unit}"
            size_bytes /= 1024.0
        return f"{size_bytes:.1f} TB"
    
    def _detect_encoding(self, path: Path) -> str:
        """检测文件编码"""
        import chardet
        
        try:
            with open(path, 'rb') as f:
                raw_data = f.read(4096)  # 读取前4KB
            
            result = chardet.detect(raw_data)
            return result.get('encoding', 'unknown')
        except:
            return 'unknown'

4.2 对话框管理器

python 复制代码
# src/ui/dialogs.py
"""
对话框管理器
管理所有文件对话框和消息对话框
"""

from PySide6.QtWidgets import (
    QFileDialog, QMessageBox, QInputDialog
)
from PySide6.QtCore import Qt


class DialogManager:
    """对话框管理器"""
    
    def __init__(self, parent):
        self.parent = parent
    
    def open_file_dialog(self, current_dir: str = "") -> tuple:
        """
        打开文件对话框
        
        Returns:
            tuple: (文件路径, 选择的过滤器)
        """
        file_path, selected_filter = QFileDialog.getOpenFileName(
            self.parent,
            "打开文件",
            current_dir,
            "文本文件 (*.txt);;所有文件 (*.*)"
        )
        return file_path, selected_filter
    
    def save_file_dialog(self, current_dir: str = "") -> tuple:
        """
        保存文件对话框
        
        Returns:
            tuple: (文件路径, 选择的过滤器)
        """
        file_path, selected_filter = QFileDialog.getSaveFileName(
            self.parent,
            "保存文件",
            current_dir,
            "文本文件 (*.txt);;所有文件 (*.*)"
        )
        return file_path, selected_filter
    
    def confirm_save_dialog(self) -> int:
        """
        保存确认对话框
        
        Returns:
            int: 用户选择 (QMessageBox.Save, QMessageBox.Discard, QMessageBox.Cancel)
        """
        return QMessageBox.question(
            self.parent,
            "保存更改",
            "文档已修改,是否保存更改?",
            QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel,
            QMessageBox.Save
        )
    
    def error_dialog(self, title: str, message: str):
        """错误对话框"""
        QMessageBox.critical(self.parent, title, message)
    
    def info_dialog(self, title: str, message: str):
        """信息对话框"""
        QMessageBox.information(self.parent, title, message)

4.3 更新主窗口

python 复制代码
# src/ui/window.py
"""
第四章更新:集成文件操作功能
"""

from PySide6.QtWidgets import QMainWindow, QTextEdit, QStatusBar
from PySide6.QtCore import Qt
from .menu import MenuManager
from .dialogs import DialogManager
from src.core.file_manager import FileManager


class NotepadWindow(QMainWindow):
    """记事本主窗口类"""
    
    def __init__(self):
        super().__init__()
        
        # 初始化属性
        self.current_file = None
        self.is_modified = False
        
        # 初始化管理器
        self.file_manager = FileManager()
        self.dialog_manager = DialogManager(self)
        
        # 初始化UI
        self._setup_window()
        self._setup_ui()
        
        # 创建菜单
        self.menu_manager = MenuManager(self)
        
        # 连接信号
        self._connect_signals()
        
        # 设置窗口修改状态
        self._update_window_title()
    
    def _setup_window(self):
        """配置窗口"""
        self.setWindowTitle("记事本 - 2026版")
        self.setGeometry(100, 100, 800, 600)
    
    def _setup_ui(self):
        """设置UI组件"""
        self.text_edit = QTextEdit()
        self.text_edit.setFontPointSize(12)
        self.text_edit.textChanged.connect(self._on_text_changed)
        self.setCentralWidget(self.text_edit)
        
        self.status_bar = QStatusBar()
        self.setStatusBar(self.status_bar)
        self.update_status("就绪")
    
    def _connect_signals(self):
        """连接信号"""
        actions = self.menu_manager.actions
        
        # 文件菜单
        actions['new'].triggered.connect(self.new_file)
        actions['open'].triggered.connect(self.open_file)
        actions['save'].triggered.connect(self.save_file)
        actions['save_as'].triggered.connect(self.save_file_as)
        actions['exit'].triggered.connect(self.close)
        
        # 编辑菜单
        actions['undo'].triggered.connect(self.text_edit.undo)
        actions['redo'].triggered.connect(self.text_edit.redo)
        actions['cut'].triggered.connect(self.text_edit.cut)
        actions['copy'].triggered.connect(self.text_edit.copy)
        actions['paste'].triggered.connect(self.text_edit.paste)
        actions['select_all'].triggered.connect(self.text_edit.selectAll)
        
        # 帮助菜单
        actions['about'].triggered.connect(self.show_about)
    
    def _on_text_changed(self):
        """文本变化处理"""
        self.is_modified = True
        self._update_window_title()
        self.update_status("已修改")
    
    def _update_window_title(self):
        """更新窗口标题"""
        if self.current_file:
            base_name = Path(self.current_file).name
            title = f"{base_name} - 记事本"
        else:
            title = "未命名 - 记事本"
        
        if self.is_modified:
            title = f"*{title}"
        
        self.setWindowTitle(title)
    
    def new_file(self):
        """新建文件"""
        if not self._check_save_changes():
            return
        
        self.text_edit.clear()
        self.current_file = None
        self.is_modified = False
        self._update_window_title()
        self.update_status("新建文件")
    
    def open_file(self):
        """打开文件"""
        if not self._check_save_changes():
            return
        
        file_path, _ = self.dialog_manager.open_file_dialog()
        
        if file_path:  # 用户选择了文件
            try:
                content = self.file_manager.read_text_file(file_path)
                self.text_edit.setPlainText(content)
                self.current_file = file_path
                self.is_modified = False
                self._update_window_title()
                self.update_status(f"已打开: {Path(file_path).name}")
            except Exception as e:
                self.dialog_manager.error_dialog("打开失败", str(e))
    
    def save_file(self):
        """保存文件"""
        if self.current_file is None:
            self.save_file_as()  # 未保存过,执行另存为
        else:
            self._save_to_file(self.current_file)
    
    def save_file_as(self):
        """另存为"""
        file_path, _ = self.dialog_manager.save_file_dialog()
        
        if file_path:  # 用户选择了路径
            # 确保文件扩展名
            if not file_path.endswith('.txt'):
                file_path += '.txt'
            
            self._save_to_file(file_path)
    
    def _save_to_file(self, file_path: str):
        """保存到指定文件"""
        try:
            content = self.text_edit.toPlainText()
            self.file_manager.write_text_file(file_path, content)
            self.current_file = file_path
            self.is_modified = False
            self._update_window_title()
            self.update_status(f"已保存: {Path(file_path).name}")
        except Exception as e:
            self.dialog_manager.error_dialog("保存失败", str(e))
    
    def _check_save_changes(self) -> bool:
        """检查是否需要保存更改"""
        if not self.is_modified:
            return True
        
        result = self.dialog_manager.confirm_save_dialog()
        
        if result == QMessageBox.Save:
            self.save_file()
            return True
        elif result == QMessageBox.Discard:
            return True
        else:  # Cancel
            return False
    
    def closeEvent(self, event):
        """关闭事件处理"""
        if self._check_save_changes():
            event.accept()
        else:
            event.ignore()
    
    def show_about(self):
        """显示关于对话框(第五章实现)"""
        pass
    
    def update_status(self, message: str):
        """更新状态栏"""
        char_count = len(self.text_edit.toPlainText())
        self.status_bar.showMessage(f"{message} | 字符数: {char_count}")

第五章:高级功能实现

5.1 关于对话框

python 复制代码
# src/ui/about_dialog.py
"""
关于对话框
显示应用信息
"""

from PySide6.QtWidgets import (
    QDialog, QVBoxLayout, QLabel, QPushButton, QHBoxLayout
)
from PySide6.QtCore import Qt


class AboutDialog(QDialog):
    """关于对话框"""
    
    def __init__(self, parent=None):
        super().__init__(parent)
        
        self.setWindowTitle("关于记事本")
        self.setFixedSize(400, 300)
        
        self._setup_ui()
    
    def _setup_ui(self):
        """设置UI布局"""
        layout = QVBoxLayout()
        
        # 应用标题
        title_label = QLabel("<h2>记事本 2026版</h2>")
        title_label.setAlignment(Qt.AlignCenter)
        layout.addWidget(title_label)
        
        # 版本信息
        version_label = QLabel("版本: 1.0.0")
        version_label.setAlignment(Qt.AlignCenter)
        layout.addWidget(version_label)
        
        # 描述信息
        description = QLabel("""
        <p>基于 Python 3.12 + PySide6 开发的现代记事本应用</p>
        <p>采用模块化设计,代码符合2026年最佳实践</p>
        <hr>
        <p><b>主要特性:</b></p>
        <ul>
            <li>支持多种文本编码</li>
            <li>自动保存备份</li>
            <li>完整的编辑功能</li>
            <li>现代化界面</li>
        </ul>
        <p><i>© 2026 Python开发者社区</i></p>
        """)
        description.setWordWrap(True)
        layout.addWidget(description)
        
        # 按钮区域
        button_layout = QHBoxLayout()
        
        ok_button = QPushButton("确定")
        ok_button.clicked.connect(self.accept)
        button_layout.addStretch()
        button_layout.addWidget(ok_button)
        
        layout.addLayout(button_layout)
        
        self.setLayout(layout)

5.2 搜索功能

python 复制代码
# src/core/search_manager.py
"""
搜索管理器
实现文本搜索和替换功能
"""

from PySide6.QtGui import QTextCursor
from typing import List, Tuple


class SearchManager:
    """搜索管理器"""
    
    def __init__(self, text_edit):
        self.text_edit = text_edit
        self.search_results = []
        self.current_index = -1
    
    def search(self, keyword: str, case_sensitive: bool = False) -> List[Tuple[int, int]]:
        """
        搜索文本
        
        Args:
            keyword: 搜索关键词
            case_sensitive: 是否区分大小写
            
        Returns:
            List[Tuple]: 匹配位置列表
        """
        if not keyword:
            return []
        
        text = self.text_edit.toPlainText()
        self.search_results = []
        
        if not case_sensitive:
            text = text.lower()
            keyword = keyword.lower()
        
        start = 0
        while True:
            pos = text.find(keyword, start)
            if pos == -1:
                break
            
            self.search_results.append((pos, pos + len(keyword)))
            start = pos + len(keyword)
        
        self.current_index = -1
        return self.search_results.copy()
    
    def find_next(self) -> bool:
        """查找下一个匹配项"""
        if not self.search_results:
            return False
        
        self.current_index = (self.current_index + 1) % len(self.search_results)
        self._highlight_current()
        return True
    
    def find_previous(self) -> bool:
        """查找上一个匹配项"""
        if not self.search_results:
            return False
        
        self.current_index = (self.current_index - 1) % len(self.search_results)
        self._highlight_current()
        return True
    
    def _highlight_current(self):
        """高亮当前匹配项"""
        if 0 <= self.current_index < len(self.search_results):
            start, end = self.search_results[self.current_index]
            
            cursor = self.text_edit.textCursor()
            cursor.setPosition(start)
            cursor.setPosition(end, QTextCursor.KeepAnchor)
            
            self.text_edit.setTextCursor(cursor)
            self.text_edit.ensureCursorVisible()
    
    def replace(self, old_text: str, new_text: str, replace_all: bool = False) -> int:
        """
        替换文本
        
        Args:
            old_text: 要替换的文本
            new_text: 新文本
            replace_all: 是否全部替换
            
        Returns:
            int: 替换的数量
        """
        if not old_text:
            return 0
        
        cursor = self.text_edit.textCursor()
        text = self.text_edit.toPlainText()
        
        if replace_all:
            # 全部替换
            new_text_full = text.replace(old_text, new_text)
            self.text_edit.setPlainText(new_text_full)
            
            # 计算替换次数
            count = text.count(old_text)
            return count
        else:
            # 替换当前选中的文本
            if cursor.hasSelection() and cursor.selectedText() == old_text:
                cursor.insertText(new_text)
                return 1
        
        return 0

5.3 字体设置功能

python 复制代码
# src/ui/font_dialog.py
"""
字体设置对话框
"""

from PySide6.QtWidgets import (
    QDialog, QVBoxLayout, QHBoxLayout, QLabel, 
    QFontComboBox, QSpinBox, QCheckBox, QPushButton
)
from PySide6.QtGui import QFont
from PySide6.QtCore import Qt


class FontDialog(QDialog):
    """字体设置对话框"""
    
    def __init__(self, current_font, parent=None):
        super().__init__(parent)
        
        self.current_font = current_font
        self.selected_font = None
        
        self.setWindowTitle("字体设置")
        self.setFixedSize(400, 300)
        
        self._setup_ui()
        self._load_current_font()
    
    def _setup_ui(self):
        """设置UI布局"""
        layout = QVBoxLayout()
        
        # 字体选择
        font_layout = QHBoxLayout()
        font_label = QLabel("字体:")
        self.font_combo = QFontComboBox()
        self.font_combo.currentFontChanged.connect(self._on_font_changed)
        font_layout.addWidget(font_label)
        font_layout.addWidget(self.font_combo)
        layout.addLayout(font_layout)
        
        # 字号选择
        size_layout = QHBoxLayout()
        size_label = QLabel("字号:")
        self.size_spin = QSpinBox()
        self.size_spin.setRange(8, 72)
        self.size_spin.valueChanged.connect(self._on_size_changed)
        size_layout.addWidget(size_label)
        size_layout.addWidget(self.size_spin)
        layout.addLayout(size_layout)
        
        # 样式选项
        style_layout = QVBoxLayout()
        
        self.bold_check = QCheckBox("粗体")
        self.bold_check.toggled.connect(self._on_bold_changed)
        style_layout.addWidget(self.bold_check)
        
        self.italic_check = QCheckBox("斜体")
        self.italic_check.toggled.connect(self._on_italic_changed)
        style_layout.addWidget(self.italic_check)
        
        self.underline_check = QCheckBox("下划线")
        self.underline_check.toggled.connect(self._on_underline_changed)
        style_layout.addWidget(self.underline_check)
        
        layout.addLayout(style_layout)
        
        # 预览区域
        preview_layout = QVBoxLayout()
        preview_label = QLabel("预览:")
        self.preview_label = QLabel("AaBbYyZz 012345 示例文本")
        self.preview_label.setFrameStyle(1)
        self.preview_label.setMinimumHeight(60)
        preview_layout.addWidget(preview_label)
        preview_layout.addWidget(self.preview_label)
        layout.addLayout(preview_layout)
        
        # 按钮区域
        button_layout = QHBoxLayout()
        
        ok_button = QPushButton("确定")
        ok_button.clicked.connect(self.accept)
        
        cancel_button = QPushButton("取消")
        cancel_button.clicked.connect(self.reject)
        
        button_layout.addStretch()
        button_layout.addWidget(ok_button)
        button_layout.addWidget(cancel_button)
        
        layout.addLayout(button_layout)
        
        self.setLayout(layout)
    
    def _load_current_font(self):
        """加载当前字体设置"""
        if self.current_font:
            self.font_combo.setCurrentFont(self.current_font)
            self.size_spin.setValue(self.current_font.pointSize())
            self.bold_check.setChecked(self.current_font.bold())
            self.italic_check.setChecked(self.current_font.italic())
            self.underline_check.setChecked(self.current_font.underline())
            self._update_preview()
    
    def _on_font_changed(self, font):
        """字体变化处理"""
        self._update_font()
    
    def _on_size_changed(self, size):
        """字号变化处理"""
        self._update_font()
    
    def _on_bold_changed(self, checked):
        """粗体变化处理"""
        self._update_font()
    
    def _on_italic_changed(self, checked):
        """斜体变化处理"""
        self._update_font()
    
    def _on_underline_changed(self, checked):
        """下划线变化处理"""
        self._update_font()
    
    def _update_font(self):
        """更新字体"""
        font = self.font_combo.currentFont()
        font.setPointSize(self.size_spin.value())
        font.setBold(self.bold_check.isChecked())
        font.setItalic(self.italic_check.isChecked())
        font.setUnderline(self.underline_check.isChecked())
        
        self.selected_font = font
        self._update_preview()
    
    def _update_preview(self):
        """更新预览"""
        if self.selected_font:
            self.preview_label.setFont(self.selected_font)
    
    def get_font(self):
        """获取选择的字体"""
        return self.selected_font

5.4 最终主窗口

python 复制代码
# src/ui/window.py
"""
第五章完成版:完整记事本应用
"""

from PySide6.QtWidgets import QMainWindow, QTextEdit, QStatusBar
from PySide6.QtGui import QFont
from pathlib import Path
from .menu import MenuManager
from .dialogs import DialogManager
from .about_dialog import AboutDialog
from .font_dialog import FontDialog
from src.core.file_manager import FileManager
from src.core.search_manager import SearchManager


class NotepadWindow(QMainWindow):
    """记事本主窗口类"""
    
    def __init__(self):
        super().__init__()
        
        # 初始化属性
        self.current_file = None
        self.is_modified = False
        
        # 初始化管理器
        self.file_manager = FileManager()
        self.dialog_manager = DialogManager(self)
        self.search_manager = None
        
        # 初始化UI
        self._setup_window()
        self._setup_ui()
        
        # 创建菜单
        self.menu_manager = MenuManager(self)
        
        # 初始化搜索管理器
        self.search_manager = SearchManager(self.text_edit)
        
        # 连接信号
        self._connect_signals()
        
        # 更新窗口标题
        self._update_window_title()
        
        # 启用编辑菜单项
        self._update_edit_actions()
    
    def _setup_window(self):
        """配置窗口"""
        self.setWindowTitle("记事本 - 2026版")
        self.setGeometry(100, 100, 800, 600)
    
    def _setup_ui(self):
        """设置UI组件"""
        self.text_edit = QTextEdit()
        self.text_edit.setFontPointSize(12)
        self.text_edit.textChanged.connect(self._on_text_changed)
        self.text_edit.undoAvailable.connect(self._on_undo_available)
        self.text_edit.redoAvailable.connect(self._on_redo_available)
        self.setCentralWidget(self.text_edit)
        
        self.status_bar = QStatusBar()
        self.setStatusBar(self.status_bar)
        self.update_status("就绪")
    
    def _connect_signals(self):
        """连接所有信号"""
        actions = self.menu_manager.actions
        
        # 文件菜单
        actions['new'].triggered.connect(self.new_file)
        actions['open'].triggered.connect(self.open_file)
        actions['save'].triggered.connect(self.save_file)
        actions['save_as'].triggered.connect(self.save_file_as)
        actions['exit'].triggered.connect(self.close)
        
        # 编辑菜单
        actions['undo'].triggered.connect(self.text_edit.undo)
        actions['redo'].triggered.connect(self.text_edit.redo)
        actions['cut'].triggered.connect(self.text_edit.cut)
        actions['copy'].triggered.connect(self.text_edit.copy)
        actions['paste'].triggered.connect(self.text_edit.paste)
        actions['select_all'].triggered.connect(self.text_edit.selectAll)
        
        # 格式菜单
        actions['font'].triggered.connect(self.change_font)
        actions['font'].setEnabled(True)  # 启用字体菜单
        
        # 帮助菜单
        actions['about'].triggered.connect(self.show_about)
    
    def _on_text_changed(self):
        """文本变化处理"""
        self.is_modified = True
        self._update_window_title()
        self.update_status("已修改")
    
    def _on_undo_available(self, available: bool):
        """撤销可用性变化"""
        self.menu_manager.actions['undo'].setEnabled(available)
    
    def _on_redo_available(self, available: bool):
        """重做可用性变化"""
        self.menu_manager.actions['redo'].setEnabled(available)
    
    def _update_window_title(self):
        """更新窗口标题"""
        if self.current_file:
            base_name = Path(self.current_file).name
            title = f"{base_name} - 记事本"
        else:
            title = "未命名 - 记事本"
        
        if self.is_modified:
            title = f"*{title}"
        
        self.setWindowTitle(title)
    
    def _update_edit_actions(self):
        """更新编辑菜单项状态"""
        has_text = bool(self.text_edit.toPlainText())
        has_selection = self.text_edit.textCursor().hasSelection()
        
        # 剪切/复制仅在文本选中时可用
        self.menu_manager.actions['cut'].setEnabled(has_selection)
        self.menu_manager.actions['copy'].setEnabled(has_selection)
        
        # 粘贴始终可用
        self.menu_manager.actions['paste'].setEnabled(True)
        
        # 全选仅在文本存在时可用
        self.menu_manager.actions['select_all'].setEnabled(has_text)
    
    def new_file(self):
        """新建文件"""
        if not self._check_save_changes():
            return
        
        self.text_edit.clear()
        self.current_file = None
        self.is_modified = False
        self._update_window_title()
        self.update_status("新建文件")
    
    def open_file(self):
        """打开文件"""
        if not self._check_save_changes():
            return
        
        file_path, _ = self.dialog_manager.open_file_dialog()
        
        if file_path:
            try:
                content = self.file_manager.read_text_file(file_path)
                self.text_edit.setPlainText(content)
                self.current_file = file_path
                self.is_modified = False
                self._update_window_title()
                self.update_status(f"已打开: {Path(file_path).name}")
            except Exception as e:
                self.dialog_manager.error_dialog("打开失败", str(e))
    
    def save_file(self):
        """保存文件"""
        if self.current_file is None:
            self.save_file_as()
        else:
            self._save_to_file(self.current_file)
    
    def save_file_as(self):
        """另存为"""
        file_path, _ = self.dialog_manager.save_file_dialog()
        
        if file_path:
            if not file_path.endswith('.txt'):
                file_path += '.txt'
            
            self._save_to_file(file_path)
    
    def _save_to_file(self, file_path: str):
        """保存到指定文件"""
        try:
            content = self.text_edit.toPlainText()
            self.file_manager.write_text_file(file_path, content)
            self.current_file = file_path
            self.is_modified = False
            self._update_window_title()
            self.update_status(f"已保存: {Path(file_path).name}")
        except Exception as e:
            self.dialog_manager.error_dialog("保存失败", str(e))
    
    def _check_save_changes(self) -> bool:
        """检查是否需要保存更改"""
        if not self.is_modified:
            return True
        
        result = self.dialog_manager.confirm_save_dialog()
        
        if result == QMessageBox.Save:
            self.save_file()
            return True
        elif result == QMessageBox.Discard:
            return True
        else:
            return False
    
    def change_font(self):
        """更改字体"""
        current_font = self.text_edit.currentFont()
        
        dialog = FontDialog(current_font, self)
        
        if dialog.exec():
            new_font = dialog.get_font()
            if new_font:
                self.text_edit.setCurrentFont(new_font)
                self.update_status("字体已更改")
    
    def show_about(self):
        """显示关于对话框"""
        dialog = AboutDialog(self)
        dialog.exec()
    
    def closeEvent(self, event):
        """关闭事件处理"""
        if self._check_save_changes():
            event.accept()
        else:
            event.ignore()
    
    def update_status(self, message: str):
        """更新状态栏"""
        char_count = len(self.text_edit.toPlainText())
        self.status_bar.showMessage(f"{message} | 字符数: {char_count}")

第六章:项目总结与扩展

6.1 项目总结

通过本教程,我们完成了以下功能:

  1. ✅ 现代化PySide6界面框架
  2. ✅ 完整的菜单系统
  3. ✅ 文件读写功能(支持多种编码)
  4. ✅ 撤销/重做、复制/粘贴等编辑功能
  5. ✅ 字体设置功能
  6. ✅ 搜索和替换功能
  7. ✅ 关于对话框
  8. ✅ 完整的错误处理

6.2 扩展建议

python 复制代码
# 可能的扩展功能示例
"""
扩展功能建议:

1. 标签页功能
   - 支持多文档同时编辑
   - 标签页切换
   - 独立保存

2. 语法高亮
   - 支持不同编程语言
   - 自定义主题
   - 代码折叠

3. 插件系统
   - 可扩展架构
   - 插件管理界面
   - 热重载支持

4. 云端同步
   - 多设备同步
   - 版本历史
   - 冲突解决

5. 高级搜索
   - 正则表达式
   - 文件夹搜索
   - 批量替换
"""

6.3 最终项目结构

复制代码
notepad_2026/
├── docs/                    # 文档
├── src/
│   ├── __init__.py
│   ├── main.py             # 主入口
│   ├── core/              # 核心模块
│   │   ├── __init__.py
│   │   ├── file_manager.py
│   │   └── search_manager.py
│   └── ui/                 # 界面模块
│       ├── __init__.py
│       ├── window.py
│       ├── menu.py
│       ├── dialogs.py
│       ├── about_dialog.py
│       └── font_dialog.py
├── tests/                  # 测试
├── requirements.txt        # 依赖
├── README.md              # 说明文档
└── setup.py              # 安装脚本

6.4 运行说明

bash 复制代码
# 安装依赖
pip install -r requirements.txt

# 运行应用
python src/main.py

学习要点总结

  1. 模块化设计: 将不同功能分离到独立模块
  2. 松耦合架构: 通过管理器类协调各个模块
  3. 错误处理: 对所有文件操作进行异常处理
  4. 用户体验: 状态栏、确认对话框等
  5. 代码规范: 类型注解、文档字符串、清晰注释
  6. 可维护性: 易于扩展新功能
  7. 现代化特性: 使用Python 3.12的新特性

本教程展示了如何使用PySide6和Python 3.12开发一个现代化的桌面应用,采用符合2026年最佳实践的代码风格和架构设计。

相关推荐
2501_945424802 小时前
实战:用Python开发一个简单的区块链
jvm·数据库·python
bestadc2 小时前
智能体构建的三种经典套路:从零开始理解ReAct、Plan-and-Solve和Reflection
python
骇客野人2 小时前
用python实现一个查询当天天气的MCP服务器
服务器·开发语言·python
天空属于哈夫克32 小时前
拒绝被动响应:企业微信主动调用接口高阶方案
开发语言·python
belldeep2 小时前
python:spaCy 工业级 NLP 库
python·自然语言处理·nlp·spacy
Flittly2 小时前
【从零手写 ClaudeCode:learn-claude-code 项目实战笔记】(11)Autonomous Agents (自治智能体)
笔记·python·ai·ai编程
2301_776508722 小时前
使用PyQt5创建现代化的桌面应用程序
jvm·数据库·python
dmlcq2 小时前
一文读懂 PageQueryUtil:分页查询的优雅打开方式
开发语言·windows·python
今儿敲了吗2 小时前
python基础学习笔记第八章——异常
笔记·python·学习