一、 项目概述

记事本作为一款基础且常用的文本编辑工具,是用户处理日常文本的高频选择。本次 QT 记事本项目以系统自带记事本为原型进行逆向分析,在复刻核心功能的基础上进行功能拓展与体验优化,旨在帮助大家深入理解 QT 框架的使用,掌握桌面应用开发的基本流程和技巧。
项目不仅实现了记事本的核心功能 —— 新建、打开、保存文本文件等基础操作,同时新增多项实用功能:支持字符编码实时切换,解决不同编码格式 TXT 文件的打开与显示问题;通过快捷键或鼠标滚轮可快速调整字体大小,配合编辑行突出显示功能提升文本定位效率;同时采用 QSS 组件对 UI 界面进行美化,兼顾功能性与视觉体验。


二、我的开发环境与工具

在本次 QT 记事本项目开发过程中,所使用的开发环境与工具如下:

  • 操作系统:Windows 10
  • QT 版本:QT 5.8.0
  • 编译器:MinGW_32bit
  • 开发工具:QT Creator 4.2.1(Community)

三、界面设计与布局

1.总体界面概览

记事本界面
主要涉及的控件包括:QPushButton(打开、保存、关闭按钮),QTextEdit(文本区域显示),QLabel(显示当前行列,以及当前编码格式),QComboBox(切换文本字符编码)。

2.界面布局

界面布局

将界面可总体分为3部分,其中上方区域的按钮区、中间的文本显示区域和下方的状态显示区域。其中,上方区域由水平布局的三个按钮组成,同时增加了水平弹簧使得按钮紧贴左边;下方区域标签和下拉列表使用水平布局,左右弹簧用于调整界面布局,右边弹簧sizeType为Fixed,大小为20*20。

3.QSS组件进行界面美化

上方和下方灰色区域:使用QWidget类,设置合适的大小,并设定背景色,详细可见下图:
在这里插入图片描述
选择添加颜色下拉框选择背景色,并选择合适的颜色。(注意控件的包含关系:按钮在QWidget里面)
按钮:按钮使用了三个状态,分别为默认状态,悬停状态和点击状态,不同的状态对应三种不同图片。具体使用和参阅帮助文档Qt Style Sheets
在这里插入图片描述
下图为按钮的样式配置(以打开按钮为例):
在这里插入图片描述
其中不同按钮状态对应不同图片资源文件o1、o2、o3。
其中资源文件的添加如下图:在这里插入图片描述

四、关键技术点

1.打开文件

现象:首先弹出路径选择对话框,选择txt文件后保存路径,并以只读方式打开,然后逐行读取文件内容并加载到文本编辑框中。同时记录lastSavedContent ,该成员变量用于记录当前文本是否发生更改。

// 打开文件按钮点击事件处理函数
void Widget::on_btnOpen_clicked()
{
    // 打开文件选择对话框,参数分别为:父窗口、对话框标题、初始路径(../表示当前目录的上一级)、文件过滤器(仅显示txt文件)
    QString filename = QFileDialog::getOpenFileName(this,tr("Open File"),"../",tr("Text (*.txt)"));
    // 调试输出选中的文件路径,便于开发时查看文件是否正确选中
    qDebug()<<filename<<endl;

    // 设置QFile对象操作的目标文件路径
    file.setFileName(filename);
    // 尝试以读写模式(QIODevice::ReadWrite)和文本模式(QIODevice::Text)打开文件
    // 若打开失败,输出错误信息
    if(!file.open(QIODevice::ReadWrite | QIODevice::Text)){
        qDebug()<<"file open error!"<<endl;
    }
    // 更新窗口标题,显示当前打开的文件名(格式:文件名 - 我的记事本)
    this->setWindowTitle(filename + "-我的记事本");
    // 清空文本编辑区原有内容,准备加载新文件
    ui->textEdit->clear();
    
    // 创建文本流对象,关联已打开的文件,用于读取文件内容
    QTextStream in(&file);
    // 根据下拉框(comboBox)选中的编码格式设置文本流的编码(解决不同编码文件的乱码问题)
    in.setCodec(ui->comboBox->currentText().toStdString().c_str());
    // 循环读取文件内容,直到文件末尾
    while (!in.atEnd()) {
        // 读取一行文本内容
        QString context = in.readLine();
        // 将读取的内容追加到文本编辑区显示
        ui->textEdit->append(context);
    }
    // 记录当前文本内容作为"上次保存版本",用于后续判断文件是否被修改(如关闭前提示保存)
    lastSavedContent = ui->textEdit->toPlainText();
}

2.保存文件

现象:如果在已打开的文件上点击保存会首先检查当前文本编辑框内容和上次保存的内容是否一致,如果一致则说明未更改,函数直接返回。否则说明发生更改 (区别于打开文件的更改和未打开文件的更改,若打开文件,则首先会记录lastSavedContent 为文本中的内容,若未打开文件,则lastSavedContent 为空) ,则进行清空原文件内容,将文本编辑框中的内容一次性写入文件。

void Widget::on_btnSave_clicked()
{
    // 获取文本编辑框(ui->textEdit)中当前的文本内容
    QString currentContent = ui->textEdit->toPlainText();

    // 1. 检查当前内容与上次保存的内容是否一致
    // 若内容未发生变化,则无需执行保存操作,直接返回
    if (currentContent == lastSavedContent) {
        qDebug() << "内容未修改,无需保存" << endl;
        return;  // 内容未变,终止函数执行
    }

    // 2. 处理文件路径(适用于首次保存或更换保存路径的场景)
    // 判断文件是否处于打开状态(若未打开,说明需要重新指定保存路径)
    if(!file.isOpen())
    {
        // 弹出文件保存对话框,让用户选择保存路径和文件名
        QString filename = QFileDialog::getSaveFileName(
                    this,               // 父窗口指针
                    tr("Save File"),    // 对话框标题
                    "../",              // 默认打开的路径(当前路径的上一级)
                    tr("Text (*.txt)")  // 文件筛选器(仅显示txt格式文件)
                    );

        // 如果用户取消选择保存路径(文件名为空),则终止保存操作
        if(filename.isEmpty()) {
            return;
        }

        // 设置文件对象的保存路径
        file.setFileName(filename);
        // 尝试以"只写"和"文本模式"打开文件
        if(!file.open(QIODevice::WriteOnly | QIODevice::Text))
        {
            // 若打开失败,输出错误信息并终止操作
            qDebug() << "文件打开失败:" << file.errorString() << endl;
            return;
        }
        // 更新窗口标题,显示当前保存的文件名(格式:"文件名 - 我的记事本")
        this->setWindowTitle(filename + "-我的记事本");
    }

    // 3. 写入内容到文件(仅在内容有变化时执行)
    file.resize(0);               // 清空文件原有内容(避免新旧内容混合)
    file.seek(0);                 // 将文件指针移动到开头(确保从起始位置写入)
    
    QTextStream out(&file);       // 创建文本流对象,用于写入文件
    // 设置文本编码格式为组合框(ui->comboBox)中用户选择的编码
    out.setCodec(ui->comboBox->currentText().toStdString().c_str());
    out << currentContent;        // 将当前文本内容写入文件

    // 4. 更新"上次保存的内容"(关键步骤:将当前内容记录为新的基准值,用于下次对比)
    lastSavedContent = currentContent;
    qDebug() << "修改的内容已保存!" << endl;  // 输出保存成功的调试信息
}

3.关闭文件

现象:首先检查文件是否更改,如果未更改则直接关闭当前文件,否则弹出对话框,供用户选择。

void Widget::on_btnClose_clicked()
{
    // 检查当前文本编辑框内容与上次保存的内容是否不一致(即存在未保存的修改)
    if(ui->textEdit->toPlainText() != lastSavedContent)
    {
        // 弹出警告对话框,提示用户有未保存的修改,并提供三个选项:保存、放弃、取消
        int ret = QMessageBox::warning(this,tr("提醒"),tr("当前文件已经修改,\n""是否要保存修改?")\
                                       ,QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
        
        // 根据用户选择的按钮执行对应操作
        switch(ret)
        {
        case QMessageBox::Save:
            // 用户选择"保存":调用保存按钮的点击事件处理函数,执行保存操作
            on_btnSave_clicked();
            qDebug()<<"用户选择了保存"<<endl;  // 调试信息:记录用户选择
            break;
            
        case QMessageBox::Discard:
            // 用户选择"放弃":不保存修改,直接清空文本编辑框内容
            ui->textEdit->clear();
            // 如果文件处于打开状态,关闭文件并重置窗口标题为默认值"我的记事本"
            if(file.isOpen())
            {
                file.close();
                this->setWindowTitle("我的记事本");
            }
            qDebug()<<"用户选择了放弃保存"<<endl;  // 调试信息:记录用户选择
            break;
            
        case QMessageBox::Cancel:
            // 用户选择"取消":不执行任何操作(保持当前状态,不关闭也不修改内容)
            qDebug()<<"用户选择了取消关闭"<<endl;  // 调试信息:记录用户选择
            break;
            
        default:
            // 其他未定义情况:不做处理
            break;
        }
    }
    else
    {
        // 如果内容未修改(与上次保存一致):直接清空文本编辑框
        ui->textEdit->clear();
        // 如果文件处于打开状态,关闭文件并重置窗口标题为默认值
        if(file.isOpen())
        {
            file.close();
            this->setWindowTitle("我的记事本");
        }
    }
}

4.文本编码格式实时切换

现象:当切换文件编码时,首先清空当前文本编辑框,并以新选择的编码格式重新加载文本内容到TextEdit。

void Widget::on_comboBox_currentIndexChanged(int index)
{
    // 清空文本编辑框内容,准备显示重新编码后的文件内容
    ui->textEdit->clear();
    
    // 检查文件是否处于打开状态(只有文件已打开时才需要重新读取)
    if(file.isOpen())
    {
        // 创建文本流对象,关联到当前打开的文件,用于读取文件内容
        QTextStream in(&file);
        
        // 设置文本流的编码格式为组合框(comboBox)当前选中的编码(如UTF-8、GBK等)
        in.setCodec(ui->comboBox->currentText().toStdString().c_str());
        
        // 将文件指针移动到文件开头,确保从文件起始位置开始读取
        file.seek(0);
        
        // 循环读取文件的每一行内容,直到文件末尾
        while (!in.atEnd()) {
            // 读取一行文本
            QString context = in.readLine();
            // 将读取的一行文本追加到文本编辑框中显示
            ui->textEdit->append(context);
        }
        
        // 输出调试信息,提示编码方式已更改并重新加载内容
        qDebug()<<"编码方式已更改,内容已重新加载!"<<endl;
    }
}

5.当前行列显示及编辑行高亮显示

使用文本编辑框的信号void cursorPositionChanged(),即光标位置改变来在槽函数中获取当前光标位置对象QTextCursor,然后进行字符串组包并显示。
基于获取的光标位置设置所在行的额外显示格式,并进行配置。

void Widget::on_textEdit_cursorPositionChanged()
{
    // 获取文本编辑框(ui->textEdit)当前的光标对象
    QTextCursor cursor = ui->textEdit->textCursor();
    
    // 计算当前光标所在的行号:blockNumber()返回从0开始的块索引,+1转为实际显示的行号(从1开始)
    QString blocknumber = QString::number(cursor.blockNumber() + 1);
    // 计算当前光标所在的列号:columnNumber()返回从0开始的列索引,+1转为实际显示的列号(从1开始)
    QString colnumber = QString::number(cursor.columnNumber() + 1);
    
    // 拼接行号和列号为显示文本(格式:"第X行第Y列")
    const QString textmsg = "第" + blocknumber + "行第" + colnumber + "列";
    // 在标签(ui->label)上显示光标位置信息
    ui->label->setText(textmsg);


    // 以下部分实现光标所在行的高亮显示功能
    // 创建额外选择区域的列表(用于存储需要特殊显示的文本区域)
    QList<QTextEdit::ExtraSelection> extraSelections;
    // 创建一个额外选择对象,用于配置高亮区域的属性
    QTextEdit::ExtraSelection ext;
    
    // 将当前光标关联到额外选择对象(指定要高亮的行)
    ext.cursor = cursor;
    // 创建浅灰色画刷,用于设置高亮背景色
    QBrush qBrush(Qt::lightGray);
    // 为额外选择对象设置背景色
    ext.format.setBackground(qBrush);
    // 设置为全宽选择(即高亮整行,而非仅光标所在位置的部分文本)
    ext.format.setProperty(QTextFormat::FullWidthSelection, true);
    
    // 将配置好的额外选择对象添加到列表中
    extraSelections.append(ext);
    // 应用额外选择设置到文本编辑框,使光标所在行显示浅灰色背景高亮
    ui->textEdit->setExtraSelections(extraSelections);
}

6.Ctrl+鼠标滚轮控制字体大小

Ctrl和鼠标滚轮为两个不同事件,要在事件的基础上实现功能,因此使用到了子类MyTextEdit继承父类的QTextEdit,并重写父类虚函数完成自定义功能,即文本字体的放大和缩小。对此,先介绍一下QT中的事件分发器:
QEvent是所有事件的父类,其中event()函数为事件分发器,它不处理事件,而是将事件派发到各个具体的事件处理函数(均为虚函数,可重写),如鼠标点击事件,键盘按下事件等等,如果想要自定义事件,可以使用QT的继承与多态思想,继承父类,并重写父类的虚函数。
在这里插入图片描述
因此,我们新建一个类,并继承于QTextEdit,重新其中的事件,以下为新建类mytextedit.h:

#ifndef MYTEXTEDIT_H
#define MYTEXTEDIT_H

#include <QTextEdit>
#include <QWidget>
#include "widget.h"



class MyTextEdit : public QTextEdit//子类继承父类
{
public:
    MyTextEdit(QWidget *parent);
private:
    int ctrlKeyPressed = 0;//标志位:用于记录Ctrl按键是否按下
protected:
    virtual void wheelEvent(QWheelEvent *event);//鼠标滚轮事件
    virtual void keyPressEvent(QKeyEvent *event);//鼠标按下事件
    virtual void keyReleaseEvent(QKeyEvent *event);//鼠标释放事件

};

#endif // MYTEXTEDIT_H

mytextedit.c

#include "mytextedit.h"

#include <QTextEdit>
#include <QDebug>
#include <QCloseEvent>

MyTextEdit::MyTextEdit(QWidget *parent):QTextEdit(parent)
{

}

// 自定义文本编辑框类MyTextEdit的鼠标滚轮事件处理函数
// 重写此函数以实现按住Ctrl键时通过滚轮缩放字体大小的功能
void MyTextEdit::wheelEvent(QWheelEvent *event)
{
    // 输出滚轮滚动的角度变化值(y值为正表示向上滚动,为负表示向下滚动)
    qDebug()<<event->angleDelta().y();
    
    // 判断Ctrl键是否被按下(ctrlKeyPressed为1表示按下)
    if(ctrlKeyPressed == 1)
    {
        // 当滚轮向上滚动时(角度变化为正),放大字体
        if(event->angleDelta().y()>0)
        {
            // 获取当前文本框使用的字体
            QFont font = this->font();
            // 获取当前字体的大小(以点为单位)
            int pointsize = font.pointSize();
            
            // 如果字体大小获取失败(返回-1,可能字体大小单位不是点),则不执行缩放
            if(pointsize == -1) return;
            
            // 计算新的字体大小(当前大小+1)
            int newpointsize = pointsize + 1;
            // 设置新的字体大小
            font.setPointSize(newpointsize);
            // 应用新字体到文本框
            this->setFont(font);
        }
        // 当滚轮向下滚动时(角度变化为负),缩小字体
        else if(event->angleDelta().y()<0)
        {
            // 获取当前文本框使用的字体
            QFont font = this->font();
            // 获取当前字体的大小(以点为单位)
            int pointsize = font.pointSize();
            
            // 如果字体大小获取失败,不执行缩放
            if(pointsize == -1) return;
            
            // 计算新的字体大小(当前大小-1)
            int newpointsize = pointsize - 1;
            // 设置新的字体大小
            font.setPointSize(newpointsize);
            // 应用新字体到文本框
            this->setFont(font);
        }
        
        // 接受此事件,不再传递给父类处理(避免触发默认的滚轮行为,如滚动文本)
        event->accept();
    }
    else
    {
        // 如果Ctrl键未按下,调用父类QTextEdit的滚轮事件处理函数,保持默认滚动行为
        QTextEdit::wheelEvent(event);
    }
}

// 重写按键按下事件处理函数,用于监测Ctrl键的按下状态
void MyTextEdit::keyPressEvent(QKeyEvent *event)
{
    // 判断按下的键是否为Ctrl键
    if(event->key() == Qt::Key_Control)
    {
        // 设置Ctrl键按下状态标志为1(表示已按下)
        ctrlKeyPressed = 1;
        // 输出调试信息,提示Ctrl键已按下
        qDebug()<<"ctrl pressed!";
    }
    
    // 调用父类的按键按下事件处理函数,确保其他按键功能(如输入文本)正常工作
    QTextEdit::keyPressEvent(event);
}

// 重写按键释放事件处理函数,用于监测Ctrl键的释放状态
void MyTextEdit::keyReleaseEvent(QKeyEvent *event)
{
    // 判断释放的键是否为Ctrl键
    if(event->key() == Qt::Key_Control)
    {
        // 设置Ctrl键按下状态标志为0(表示已释放)
        ctrlKeyPressed = 0;
        // 输出调试信息,提示Ctrl键已释放
        qDebug()<<"ctrl released!";
    }
    
    // 调用父类的按键释放事件处理函数,确保其他按键功能正常
    QTextEdit::keyReleaseEvent(event);
}

五、可执行文件与源代码获取

1.可执行文件

在这里插入图片描述

该可执行文件已在不同电脑测试,直接双击.exe文件即可执行。

2.C++源码

在这里插入图片描述

3.百度网盘链接

通过网盘分享的文件:我的记事本
链接: https://pan.baidu.com/s/1J5cWR3nFmYH3NUYU2BQ8FA 提取码: vwrn

总结

本次QT记事本项目以系统记事本为原型,复刻基础功能并拓展优化,助力理解QT框架及桌面开发。开发环境为Windows 10、QT 5.8.0等。界面分上(按钮区)、中(文本区)、下(状态区)三部分,用QSS美化,按钮设三种状态对应不同图片。实现了打开、保存、关闭文件功能,支持编码实时切换,能显示行列位置并高亮编辑行,还可通过Ctrl+鼠标滚轮调字体大小,兼顾功能与体验。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐