构建一个功能丰富的记事本应用

在现代软件开发中,记事本应用是一个经典的入门项目,它不仅能够帮助开发者熟悉 GUI 编程,还能在实践中掌握事件处理、文件操作等核心概念。本文将详细介绍如何使用 PyQt5 构建一个功能丰富的记事本应用,包括新建、打开、保存文件,以及字体大小调整等高级功能。

一、项目概述

我们的记事本应用将包含以下核心功能:

  1. 文件操作:新建、打开、保存、另存为、退出。
  2. 文本编辑:支持基本的文本编辑功能,包括字体大小调整。
  3. 状态显示:在状态栏显示当前文件状态,如是否已保存。
  4. 语言模式:支持多种语言模式,如 Python、Java、HTML 等,以便于代码高亮。

二、环境搭建

在开始编码之前,确保你的 Python 环境中已安装 PyQt5。如果未安装,可以通过以下命令进行安装:

pip install PyQt5

三、核心组件实现

1. 主窗口 Notebook

主窗口类 Notebook 负责初始化整个应用的 UI 组件,包括菜单栏、工具栏、编辑区和状态栏。

class Notebook(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.setWindowTitle("简易记事本")
        self.resize(800, 600)
        self.languageModeList = {
            "xb": "qt记事本",
            "txt": "txt",
            "py": "python",
            "java": "Java",
            "js": "Javascript",
            "html": "HTML",
            "css": "CSS",
            "cpp": "C++",
            "c": "C",
            "hpp": "C++",
            "h": "C++",
        }
        self.languageMode = "qt记事本"
        self.currentFilePath = "未命名1.xb"
        self.hasSaved = False
        self.languageLabel = QLabel(text=f"当前语言: {self.languageMode}", parent=self)
        self.addController()
        self.addShortCut()
2. 编辑区 CustomTextEdit

编辑区是记事本应用的核心,我们通过继承 QTextEdit 来创建 CustomTextEdit 类,增加了字体大小调整的功能。

class CustomTextEdit(QTextEdit):
    def __init__(self):
        super().__init__()
        self.font_size = 14
        self.max_font_size = 30
        self.min_font_size = 8
        self.canCtrl = True
        self.setFontPointSize(self.font_size)

    def wheelEvent(self, event: QWheelEvent):
        modifiers = event.modifiers()
        if modifiers == Qt.ControlModifier and self.canCtrl:
            delta = event.angleDelta().y()
            new_size = max(self.min_font_size, min(self.max_font_size, self.currentFont().pointSize() + delta // 120))
            str = self.toPlainText()
            self.clear()
            self.setFontPointSize(new_size)
            self.setPlainText(str)
        else:
            super().wheelEvent(event)

    def setFontPointSize(self, size):
        font = self.currentFont()
        font.setPointSize(size)
        self.setCurrentFont(font)
        self.update()
3. 菜单和工具栏

菜单和工具栏为用户提供了便捷的文件操作入口。我们创建了一个工具栏和一个文件菜单,用于文件操作。

def addController(self):
    toolBar = QToolBar(self)
    self.file_menu = QMenu("文件(F)", self)
    newfileAction = QAction("新建(N)", self)
    openfileAction = QAction("打开(O)", self)
    savefileAction = QAction("保存(S)", self)
    saveAsfileAction = QAction("另存为(A)", self)
    exitAction = QAction("退出(X)", self)
    self.file_menu.addActions([newfileAction, openfileAction, savefileAction, saveAsfileAction])
    self.file_menu.addSeparator()
    self.file_menu.addAction(exitAction)
    file_menu_button = QPushButton("文件(F)", self)
    file_menu_button.setMenu(self.file_menu)
    toolBar.addWidget(file_menu_button)
    self.addToolBar(toolBar)
    self.w = CustomTextEdit()
    self.w.setPlaceholderText("请输入内容...")
    self.w.document().contentsChanged.connect(self.checkModified)
    self.setCentralWidget(self.w)
    self.createStatusBar()
    self.connectActions(newfileAction, openfileAction, savefileAction, saveAsfileAction, exitAction)
4. 状态栏

状态栏显示当前文件的路径和状态,如是否已保存。

def createStatusBar(self):
    statusBar = QStatusBar(self)
    statusBar.addWidget(QPushButton(text="状态栏", parent=statusBar))
    statusBar.addWidget(self.languageLabel)
    self.file_name_label = QLabel(text=self.currentFilePath if self.hasSaved else self.currentFilePath + " *", parent=statusBar)
    statusBar.addWidget(self.file_name_label)
    statusBar.setStyleSheet("QPushButton,QLabel{border: none;background-color: rgb(0,0,0);color: white;font-size: 14px;font-weight: bold;padding: 5px;} QStatusBar{border: 1px solid #C0C0C0;background-color: rgb(0,0,0)}")
    self.setStatusBar(statusBar)
5. 文件操作

文件操作是记事本应用的基础功能,包括新建文件、打开文件、保存文件和另存为。

def saveFile(self):
    try:
        with open(self.currentFilePath, "w", encoding="utf-8") as file:
            if self.languageMode == "qt记事本":
                file.write(self.w.toHtml())
            else:
                file.write(self.w.toPlainText())
        self.hasSaved = True
        self.file_name_label.setText(self.currentFilePath)
        QMessageBox.information(self, "提示", "文件已保存")
    except Exception as e:
        QMessageBox.critical(self, "错误", f"无法保存文件: {e}")

def saveAsFile(self):
    options = QFileDialog.Options()
    file_name, _ = QFileDialog.getSaveFileName(self, "保存文件", "", "qt记事本文件(*.xb);;所有文件 (*)", options=options)
    if file_name:
        self.currentFilePath = file_name
        self.saveFile()

def newFile(self):
    self.w.clear()
    self.hasSaved = False
    self.file_name_label.setText(self.currentFilePath + " *")
    self.w.canCtrl = False
    self.setLanguageMode("qt记事本")
    self.w.setPlaceholderText("请输入内容...")

def openFile(self):
    options = QFileDialog.Options()
    file_name, _ = QFileDialog.getOpenFileName(self, "打开文件", "", "所有文件 (*);;qt记事本文件 (*.xb);;文本文件 (*.txt);;python文件 (*.py);;java文件 (*.java);;javascript文件 (*.js);;html文件 (*.html);;css文件 (*.css);;C++文件 (*.cpp *.hpp *.h)", options=options)
    if file_name:
        self.currentFilePath = file_name
        suffix = file_name[file_name.rfind(".") + 1 :]
        self.setLanguageMode(self.languageModeList.get(suffix, "qt记事本"))
        try:
            with open(file_name, "r", encoding="utf-8") as file:
                if suffix == "xb":
                    self.w.setHtml(file.read())
                else:
                    self.w.canCtrl = True
                    self.w.setPlainText(file.read())
        except Exception as e:
            QMessageBox.critical(self, "错误", f"无法打开文件: {e}")

四、扩展功能

为了使记事本应用更加强大,我们可以考虑添加以下扩展功能:

  1. 文本格式化:提供文本加粗、斜体、下划线等格式化选项。
  2. 查找和替换:实现文本查找和替换功能,提高编辑效率。
  3. 撤销和重做:支持撤销和重做操作,方便用户编辑文本。
  4. 代码高亮:根据不同的语言模式,实现代码高亮显示。

五、结论

通过本文的介绍,我们成功构建了一个功能丰富的记事本应用。这个应用不仅包括了基本的文件操作,还支持字体大小调整和多种语言模式。你可以在此基础上继续扩展,增加更多实用功能,使其成为一个更加完善的文本编辑工具。

希望这篇文章能帮助你更好地理解 PyQt5 的应用开发,为你的项目开发提供参考和启发。如果你有任何问题或想要进一步扩展这个项目,欢迎在评论区留言讨论。

六、完整代码

customTextEdit.py

import sys
from tkinter import font
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *


class CustomTextEdit(QTextEdit):
    def __init__(self):
        super().__init__()
        self.font_size = 14  # 初始字体大小
        self.max_font_size = 30  # 最大字体大小
        self.min_font_size = 8  # 最小字体大小
        self.canCtrl = True  # 是否可以修改字体大小
        self.setFontPointSize(self.font_size)  # 应用初始字体大小

    def wheelEvent(self, event: QWheelEvent):
        print("wheelEvent", event.modifiers(), event.angleDelta().y())
        # 获取当前的修饰符
        modifiers = event.modifiers()

        if modifiers == Qt.ControlModifier and self.canCtrl:
            print("ControlModifier")
            # 仅按下 Control 键,修改显示大小
            delta = event.angleDelta().y()
            new_size = max(
                self.min_font_size,
                min(self.max_font_size, self.currentFont().pointSize() + delta // 120),
            )
            str = self.toPlainText()
            self.clear()
            self.setFontPointSize(new_size)  # 设置新字体大小
            self.setPlainText(str)
        # 检查是否按下 Ctrl 和 Shift 键
        if (modifiers & (Qt.ControlModifier | Qt.ShiftModifier)) == (
            Qt.ControlModifier | Qt.ShiftModifier
        ):
            # 同时按下了 Control 和 Shift 修饰符,修改输入文字大小
            delta = event.angleDelta().y()
            if delta > 0:
                self.font_size += 1  # 增加字体大小
            elif delta < 0:
                self.font_size -= 1  # 减小字体大小
                if self.font_size < 1:  # 限制最小字体大小
                    self.font_size = 1
            self.setFontPointSize(self.font_size)  # 更新字体大小
            self.update()  # 刷新控件的显示
        else:
            super().wheelEvent(event)  # 调用基类的事件处理

    def setFontPointSize(self, size):
        font = self.currentFont()
        font.setPointSize(size)  # 设置字体大小
        self.setCurrentFont(font)  # 应用字体
        self.update()  # 刷新控件的显示


if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = CustomTextEdit()
    w.show()
    sys.exit(app.exec_())

Notebook.py

from PyQt5.QtWidgets import *
import CustomTextEdit


class Notebook(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.setWindowTitle("简易记事本")
        self.resize(800, 600)

        # 语言模式列表
        self.languageModeList = {
            "xb": "qt记事本",
            "txt": "txt",
            "py": "python",
            "java": "Java",
            "js": "Javascript",
            "html": "HTML",
            "css": "CSS",
            "cpp": "C++",
            "c": "C",
            "hpp": "C++",
            "h": "C++",
        }

        self.languageMode = "qt记事本"  # 默认语言模式
        self.currentFilePath = "未命名1.xb"  # 默认文件路径
        self.hasSaved = False  # 是否已保存标志

        self.languageLabel = QLabel(text=f"当前语言: {self.languageMode}", parent=self)
        self.languageLabel.setStyleSheet(
            "QLabel{color: white;font-size: 14px;font-weight: bold;padding: 5px;}"
        )

        self.addController()  # 添加组件
        self.addShortCut()  # 添加快捷键

    def addController(self):
        # 创建工具栏
        toolBar = QToolBar(self)
        toolBar.setMovable(True)

        # 创建文件菜单
        self.file_menu = QMenu("文件(F)", self)
        self.file_menu.setStyleSheet(
            "QMenu{background-color: rgb(255,255,255);color: black;font-size: 14px;font-weight: bold;padding: 5px;}"
        )

        # 创建菜单项
        newfileAction = QAction("新建(N)", self)
        openfileAction = QAction("打开(O)", self)
        savefileAction = QAction("保存(S)", self)
        saveAsfileAction = QAction("另存为(A)", self)
        exitAction = QAction("退出(X)", self)

        # 添加动作到菜单
        self.file_menu.addActions(
            [newfileAction, openfileAction, savefileAction, saveAsfileAction]
        )
        self.file_menu.addSeparator()
        self.file_menu.addAction(exitAction)

        # 创建菜单按钮并添加到工具栏
        file_menu_button = QPushButton("文件(F)", self)
        file_menu_button.setStyleSheet(
            "QPushButton{border: none;color: black;font-size: 14px;font-weight: bold;padding: 5px;}"
        )
        file_menu_button.setMenu(self.file_menu)
        toolBar.addWidget(file_menu_button)
        self.addToolBar(toolBar)

        # 创建中心编辑区
        self.w = CustomTextEdit.CustomTextEdit()
        self.w.setPlaceholderText("请输入内容...")
        self.w.document().contentsChanged.connect(self.checkModified)
        self.setCentralWidget(self.w)

        # 创建状态栏
        self.createStatusBar()

        # 信号与槽的连接
        self.connectActions(
            newfileAction, openfileAction, savefileAction, saveAsfileAction, exitAction
        )

    def createStatusBar(self):
        statusBar = QStatusBar(self)
        statusBar.addWidget(QPushButton(text="状态栏", parent=statusBar))
        statusBar.addWidget(self.languageLabel)

        self.file_name_label = QLabel(
            text=self.currentFilePath if self.hasSaved else self.currentFilePath + " *",
            parent=statusBar,
        )
        statusBar.addWidget(self.file_name_label)

        statusBar.setStyleSheet(
            "QPushButton,QLabel{border: none;background-color: rgb(0,0,0);color: white;font-size: 14px;font-weight: bold;padding: 5px;} QStatusBar{border: 1px solid #C0C0C0;background-color: rgb(0,0,0)}"
        )
        self.setStatusBar(statusBar)

    def connectActions(
        self,
        newfileAction,
        openfileAction,
        savefileAction,
        saveAsfileAction,
        exitAction,
    ):
        openfileAction.triggered.connect(self.openFile)
        savefileAction.triggered.connect(self.saveFile)
        saveAsfileAction.triggered.connect(self.saveAsFile)
        newfileAction.triggered.connect(self.newFile)
        exitAction.triggered.connect(self.close)

    def addShortCut(self):
        self.file_menu.actions()[0].setShortcut("Ctrl+N")
        self.file_menu.actions()[1].setShortcut("Ctrl+O")
        self.file_menu.actions()[2].setShortcut("Ctrl+S")
        self.file_menu.actions()[3].setShortcut("Ctrl+A")
        self.file_menu.actions()[4].setShortcut("Ctrl+X")

    def checkModified(self):
        self.hasSaved = not self.w.document().isModified()
        self.file_name_label.setText(
            self.currentFilePath + (" *" if not self.hasSaved else "")
        )

    def setLanguageMode(self, languageMode):
        self.languageMode = languageMode
        self.languageLabel.setText(f"当前语言: {self.languageMode}")

    def saveFile(self):
        try:
            with open(self.currentFilePath, "w", encoding="utf-8") as file:
                if self.languageMode == "qt记事本":
                    file.write(self.w.toHtml())
                else:
                    file.write(self.w.toPlainText())
            self.hasSaved = True
            self.file_name_label.setText(self.currentFilePath)
            QMessageBox.information(self, "提示", "文件已保存")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"无法保存文件: {e}")

    def saveAsFile(self):
        options = QFileDialog.Options()
        file_name, _ = QFileDialog.getSaveFileName(
            self, "保存文件", "", "qt记事本文件(*.xb);;所有文件 (*)", options=options
        )
        if file_name:
            self.currentFilePath = file_name
            self.saveFile()  # 直接调用保存文件的方法

    def newFile(self):
        self.w.clear()
        self.hasSaved = False
        self.file_name_label.setText(self.currentFilePath + " *")
        self.w.canCtrl = False
        self.setLanguageMode("qt记事本")
        self.w.setPlaceholderText("请输入内容...")

    def openFile(self):
        options = QFileDialog.Options()
        file_name, _ = QFileDialog.getOpenFileName(
            self,
            "打开文件",
            "",
            "所有文件 (*);;qt记事本文件 (*.xb);;文本文件 (*.txt);;python文件 (*.py);;java文件 (*.java);;javascript文件 (*.js);;html文件 (*.html);;css文件 (*.css);;C++文件 (*.cpp *.hpp *.h)",
            options=options,
        )
        if file_name:
            self.currentFilePath = file_name
            suffix = file_name[file_name.rfind(".") + 1 :]
            self.setLanguageMode(self.languageModeList.get(suffix, "qt记事本"))
            try:
                with open(file_name, "r", encoding="utf-8") as file:
                    if suffix == "xb":
                        self.w.setHtml(file.read())
                    else:
                        self.w.canCtrl = True
                        self.w.setPlainText(file.read())
            except Exception as e:
                QMessageBox.critical(self, "错误", f"无法打开文件: {e}")

    def closeEvent(self, event):
        if not self.hasSaved:
            reply = QMessageBox.question(
                self,
                "提示",
                "是否保存文件?",
                QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel,
                QMessageBox.Cancel,
            )
            if reply == QMessageBox.Yes:
                self.saveFile()
            elif reply == QMessageBox.Cancel:
                event.ignore()
            else:
                event.accept()
        else:
            event.accept()