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 项目总结
通过本教程,我们完成了以下功能:
- ✅ 现代化PySide6界面框架
- ✅ 完整的菜单系统
- ✅ 文件读写功能(支持多种编码)
- ✅ 撤销/重做、复制/粘贴等编辑功能
- ✅ 字体设置功能
- ✅ 搜索和替换功能
- ✅ 关于对话框
- ✅ 完整的错误处理
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
学习要点总结
- 模块化设计: 将不同功能分离到独立模块
- 松耦合架构: 通过管理器类协调各个模块
- 错误处理: 对所有文件操作进行异常处理
- 用户体验: 状态栏、确认对话框等
- 代码规范: 类型注解、文档字符串、清晰注释
- 可维护性: 易于扩展新功能
- 现代化特性: 使用Python 3.12的新特性
本教程展示了如何使用PySide6和Python 3.12开发一个现代化的桌面应用,采用符合2026年最佳实践的代码风格和架构设计。