用Python PyQt6开发简易记事本软件:界面布局与事件处理

追风的人
追风的人
发布时间:2026-11-02 | 9人浏览过
原创

深入讲解如何使用Python与PyQt6开发桌面简易记事本软件,涵盖QMainWindow架构拆解、文本控件选型、菜单与工具栏布局及信号与槽事件处理实战。

在Python桌面应用开发中,使用PyQt6实现简易记事本是掌握图形界面编程的经典路径。构建这类文本编辑软件的核心在于两大支柱:一是依托QMainWindow所提供的标准化区域布局机制组织视窗控件;二是借助Qt的“信号与槽(Signals & Slots)”机制高效处理菜单触发、快捷键交互、文本输入监听及文件系统读写。本文将按规范的操作顺序,从基础骨架设计、核心控件选型对比,到完整的界面排布与事件连接,一步步实现一个具备完整编辑功能与状态感知的记事本程序。

一、QMainWindow 的界面布局骨架与组件层级

开发标准桌面软件时,避免手动通过绝对坐标定位控件。PyQt6的QMainWindow提供了一套预设的标准界面结构,天然契合文本编辑器等文档类应用。其布局空间划分为固定的五大区域:顶部的菜单栏(Menu Bar)、紧随其后的工具栏(Toolbars)、位于中央的中心部件(Central Widget)、两侧的停靠窗口(Dock Widgets)以及底部的状态栏(Status Bar)。

在记事本软件中,中心部件是整个界面的交互核心,占据窗口绝大部分可用空间,并随着主窗口缩放自适应拉伸。菜单栏和工具栏承载离散型触发操作,而状态栏负责被动呈现行号、列号与字数统计信息。清晰的层级树保证了应用各模块之间的职责隔离。

用Python PyQt6开发简易记事本软件:界面布局与事件处理
图1:QMainWindow 核心组件层级与区域布局规划示意图

二、核心控件选型对比:QPlainTextEdit 与 QTextEdit

在设计文本输入区域时,PyQt6提供了两种常用的多行文本控件:QPlainTextEdit与QTextEdit。理解两者的底层差异,对于设计轻量、高效的文本编辑器至关重要。

关键差异主要体现在底层排版机制与内存开销上:

  • 内存与段落渲染策略:QTextEdit基于完整的HTML/富文本排版引擎(QTextDocument)。即使只输入纯文本,它也会将所有段落封装为富文本格式对象,在处理大文件(如数万行日志或大型代码段落)时,内存消耗激增,且光标滚动时重绘负担重。而QPlainTextEdit专为纯文本设计,采用块缓存(Paragraph Block Cache)技术,仅计算可视区域的文本排版,具备近乎恒定的内存与CPU占用。
  • 功能特性与适用边界:如果软件需要支持加粗、斜体、内嵌图片或排版预览,QTextEdit是必选;但对于记事本、代码阅读器等只关心无格式文本内容的应用,QPlainTextEdit在启动速度、打字响应延迟及内存控制上明显占优。
用Python PyQt6开发简易记事本软件:界面布局与事件处理
图2:QPlainTextEdit 与 QTextEdit 的设计原理与性能特性对比

三、记事本软件开发实施步骤

实现该记事本应用可遵循四个清晰的开发步骤:从依赖安装与主窗口初始化开始,逐步推进到中心控件配置、操作指令动作构建,最终完成事件连接与读写闭环。

用Python PyQt6开发简易记事本软件:界面布局与事件处理
图3:从环境到事件闭环的记事本开发实操流程

1. 依赖准备与窗口定义

在开始编写代码前,需确保Python环境中已安装PyQt6模块。在终端中执行如下安装命令:

pip install PyQt6

主窗口类继承自QMainWindow。在构造函数中,通过self.setWindowTitle()和self.resize(900, 600)设定初始标题与窗口宽高。

2. 中心编辑区初始化

通过self.editor = QPlainTextEdit()实例化文本编辑控件,并通过self.setCentralWidget(self.editor)将其设定为主窗口的唯一中心部件。此时无需手动编写布局管理器,QMainWindow会自动维护该组件贴合菜单栏与状态栏之间的全部空白区域。

3. 使用 QAction 复用动作

在GUI设计中,菜单项和工具栏按钮通常执行完全相同的逻辑。例如点击“保存”菜单和点击工具栏上的“保存”图标,其底层方法完全一致。PyQt6通过QAction对象实现逻辑抽象。一个QAction可以同时包含显示文本、图标、快捷键提示,并一次性添加到QMenu和QToolBar中,实现行为与外观的高度统一。

四、事件处理与“信号与槽”通信模型

GUI程序是典型的事件驱动架构。用户在界面上的操作(敲击键盘、点击菜单项、拖曳滚动条、点击关闭按钮)均会被底层的视窗系统捕获,并转化为Qt内部事件。

用Python PyQt6开发简易记事本软件:界面布局与事件处理
图4:从信号触发到槽函数业务落地的完整事件流

在记事本中,主要涉及以下四类核心事件的监听与处理:

  1. 动作触发信号 (action.triggered):用户点击菜单项或按下 Ctrl+S、Ctrl+O 等快捷键时,对应的 QAction 会发射 triggered 信号,直接连接到具体的文件操作方法。
  2. 文本修改监听 (textChanged):用于跟踪文档的脏状态(Dirty State)。一旦正文内容发生修改,在窗口标题栏添加星号(*)标识,并在用户关闭应用前执行安全校验。
  3. 光标位置变化 (cursorPositionChanged):用户输入或通过方向键、鼠标移动光标时,QPlainTextEdit 会发出此信号。槽函数通过 textCursor() 获取当前的物理行号与列号,并实时格式化输出到状态栏。
  4. 窗口关闭事件拦截 (closeEvent):当用户点击窗口右上角的关闭按钮时,Qt 会派发 QCloseEvent。重写主窗口的 closeEvent(event) 方法可以实现优雅退出:若有未保存修改,弹出确认对话框提示用户保存,根据选择决定 event.accept() 接受关闭或 event.ignore() 阻止窗口关闭。

五、完整代码实现与逐模块解析

以下是具备完整功能的简易记事本完整代码实现。代码包含异常处理、UTF-8强制字符编码读写及未保存拦截机制:

import sys
from PyQt6.QtWidgets import (
    QApplication, QMainWindow, QPlainTextEdit,
    QFileDialog, QMessageBox, QLabel
)
from PyQt6.QtGui import QAction, QKeySequence, QFont
from PyQt6.QtCore import Qt

class NotepadWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.current_file_path = None
        self.is_modified = False
        
        self.init_ui()

    def init_ui(self):
        self.setWindowTitle("简易记事本 - 未命名")
        self.resize(900, 600)

        # 1. 中心部件初始化:使用 QPlainTextEdit
        self.editor = QPlainTextEdit()
        font = QFont("Consolas", 12)
        font.setStyleHint(QFont.StyleHint.Monospace)
        self.editor.setFont(font)
        self.setCentralWidget(self.editor)

        # 2. 状态栏初始化
        self.status_bar = self.statusBar()
        self.status_label = QLabel("第 1 行,第 1 列 | 字符数: 0")
        self.status_bar.addPermanentWidget(self.status_label)
        self.status_bar.showMessage("就绪", 3000)

        # 3. 构建动作与菜单栏
        self.create_actions()
        self.create_menus()
        self.create_toolbars()

        # 4. 信号与槽连接
        self.editor.textChanged.connect(self.on_text_changed)
        self.editor.cursorPositionChanged.connect(self.update_cursor_info)

    def create_actions(self):
        # 文件操作
        self.act_new = QAction("新建", self)
        self.act_new.setShortcut(QKeySequence.StandardKey.New)
        self.act_new.triggered.connect(self.new_file)

        self.act_open = QAction("打开...", self)
        self.act_open.setShortcut(QKeySequence.StandardKey.Open)
        self.act_open.triggered.connect(self.open_file)

        self.act_save = QAction("保存", self)
        self.act_save.setShortcut(QKeySequence.StandardKey.Save)
        self.act_save.triggered.connect(self.save_file)

        self.act_save_as = QAction("另存为...", self)
        self.act_save_as.setShortcut(QKeySequence.StandardKey.SaveAs)
        self.act_save_as.triggered.connect(self.save_file_as)

        self.act_exit = QAction("退出", self)
        self.act_exit.setShortcut(QKeySequence.StandardKey.Quit)
        self.act_exit.triggered.connect(self.close)

        # 编辑操作 (复用 QPlainTextEdit 内建槽)
        self.act_undo = QAction("撤销", self)
        self.act_undo.setShortcut(QKeySequence.StandardKey.Undo)
        self.act_undo.triggered.connect(self.editor.undo)

        self.act_redo = QAction("重做", self)
        self.act_redo.setShortcut(QKeySequence.StandardKey.Redo)
        self.act_redo.triggered.connect(self.editor.redo)

        self.act_cut = QAction("剪切", self)
        self.act_cut.setShortcut(QKeySequence.StandardKey.Cut)
        self.act_cut.triggered.connect(self.editor.cut)

        self.act_copy = QAction("复制", self)
        self.act_copy.setShortcut(QKeySequence.StandardKey.Copy)
        self.act_copy.triggered.connect(self.editor.copy)

        self.act_paste = QAction("粘贴", self)
        self.act_paste.setShortcut(QKeySequence.StandardKey.Paste)
        self.act_paste.triggered.connect(self.editor.paste)

    def create_menus(self):
        menu_bar = self.menuBar()

        # 文件菜单
        file_menu = menu_bar.addMenu("文件(&F)")
        file_menu.addAction(self.act_new)
        file_menu.addAction(self.act_open)
        file_menu.addAction(self.act_save)
        file_menu.addAction(self.act_save_as)
        file_menu.addSeparator()
        file_menu.addAction(self.act_exit)

        # 编辑菜单
        edit_menu = menu_bar.addMenu("编辑(&E)")
        edit_menu.addAction(self.act_undo)
        edit_menu.addAction(self.act_redo)
        edit_menu.addSeparator()
        edit_menu.addAction(self.act_cut)
        edit_menu.addAction(self.act_copy)
        edit_menu.addAction(self.act_paste)

    def create_toolbars(self):
        toolbar = self.addToolBar("常用工具")
        toolbar.setMovable(False)
        toolbar.addAction(self.act_new)
        toolbar.addAction(self.act_open)
        toolbar.addAction(self.act_save)
        toolbar.addSeparator()
        toolbar.addAction(self.act_undo)
        toolbar.addAction(self.act_redo)

    # ========== 事件处理槽函数 ==========
    def on_text_changed(self):
        if not self.is_modified:
            self.is_modified = True
            title = self.windowTitle()
            if not title.endswith("*"):
                self.setWindowTitle(title + "*")

    def update_cursor_info(self):
        cursor = self.editor.textCursor()
        line = cursor.blockNumber() + 1
        col = cursor.columnNumber() + 1
        char_count = len(self.editor.toPlainText())
        self.status_label.setText(f"第 {line} 行,第 {col} 列 | 字符数: {char_count}")

    def prompt_save_changes(self):
        if not self.is_modified:
            return True
        reply = QMessageBox.question(
            self, "保存确认",
            "当前文档内容已被修改,是否保存更改?",
            QMessageBox.StandardButton.Save |
            QMessageBox.StandardButton.Discard |
            QMessageBox.StandardButton.Cancel
        )
        if reply == QMessageBox.StandardButton.Save:
            return self.save_file()
        elif reply == QMessageBox.StandardButton.Discard:
            return True
        else:
            return False

    def new_file(self):
        if not self.prompt_save_changes():
            return
        self.editor.clear()
        self.current_file_path = None
        self.is_modified = False
        self.setWindowTitle("简易记事本 - 未命名")
        self.status_bar.showMessage("已新建文档", 2000)

    def open_file(self):
        if not self.prompt_save_changes():
            return
        file_path, _ = QFileDialog.getOpenFileName(
            self, "打开文本文件", "", "文本文档 (*.txt);;所有文件 (*.*)"
        )
        if file_path:
            try:
                with open(file_path, "r", encoding="utf-8") as f:
                    content = f.read()
                self.editor.setPlainText(content)
                self.current_file_path = file_path
                self.is_modified = False
                self.setWindowTitle(f"简易记事本 - {file_path}")
                self.status_bar.showMessage("文件加载成功", 2000)
            except Exception as e:
                QMessageBox.critical(self, "读取错误", f"无法打开文件:\n{str(e)}")

    def save_file(self):
        if not self.current_file_path:
            return self.save_file_as()
        try:
            with open(self.current_file_path, "w", encoding="utf-8") as f:
                f.write(self.editor.toPlainText())
            self.is_modified = False
            self.setWindowTitle(f"简易记事本 - {self.current_file_path}")
            self.status_bar.showMessage("文件已保存", 2000)
            return True
        except Exception as e:
            QMessageBox.critical(self, "保存错误", f"保存文件失败:\n{str(e)}")
            return False

    def save_file_as(self):
        file_path, _ = QFileDialog.getSaveFileName(
            self, "另存为文件", "", "文本文档 (*.txt);;所有文件 (*.*)"
        )
        if file_path:
            self.current_file_path = file_path
            return self.save_file()
        return False

    def closeEvent(self, event):
        if self.prompt_save_changes():
            event.accept()
        else:
            event.ignore()

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

六、操作验证与关键注意事项

开发与排错要点:
  1. 文本编码一致性:在文件读写时,必须明确声明 encoding="utf-8"。若未指定编码,在 Windows 环境下可能会默认使用系统当前编码(如 GBK),导致含有中文或跨平台共享文件时出现乱码。
  2. 标准快捷键的跨平台兼容:在创建 QAction 时,优先采用 QKeySequence.StandardKey.Save 等系统语义快捷键枚举,而非硬编码 Ctrl+S。这能自动在 macOS 上映射为 Cmd+S,保持原生系统的操作习惯。
  3. 退出拦截顺序:退出逻辑不仅包含菜单栏的“退出”项,还涵盖快捷键退出与直接点击窗口右上角的关闭按钮。通过将确认提示放在 closeEvent 中统一接管,确保所有退出入口都能得到数据完整性保护。

完成上述代码后,通过命令 python notepad.py(假设保存文件名为 notepad.py)即可运行。读者可以验证窗口拉伸时编辑区域是否自适应调整、状态栏是否实时跟踪光标位移、未保存时退出是否正常唤起拦截确认框。这套基于 QMainWindow 与信号槽机制的架构,具备高度的可扩展性,后续可平滑增加“查找与替换”、“行号侧边栏”或“字体大小缩放”等增强功能。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表