Qt/C++ 个人开源项目#串口助手(源码与发布链接)

news2024/12/22 18:58:52

一、项目概述

该串口助手工具基于Qt/C++开发,专为简化串口通信调试与开发而设计,适合新手快速上手。工具具有直观的用户界面和丰富的功能,旨在帮助用户与串口设备建立可靠通信,便于调试、数据传输和分析。

二、主要功能

波特率:支持常见波特率(如9600、115200等),并支持自定义。

数据位:提供从5到8位的选择。

校验位:支持无校验、奇校验、偶校验等。

停止位:提供1位或2位选择。

接收数据:支持实时显示从串口设备接收的数据,用户可以选择显示格式(如十六进制、ASCII等)。

发送数据:用户可以通过界面输入并发送数据,支持单次发送和定时发送,便于测试连续传输。

  1. 串口自动检测与连接串口助手具备自动检测功能,可以自动识别当前系统中的串口设备。用户通过界面选择设备并一键连接,简化了配置流程。

  2. 串口参数配置用户可以根据实际需求设置以下参数:

  3. 数据收发

三、软件安装与使用

1. 使用

该串口助手提供了编译好的发布包,用户可以自行下载可以直接启动应用。在应用界面中,选择要连接的串口设备,配置相关串口参数,点击“连接”即可开始数据通信。数据收发、等功能可以根据需求自由选择和配置。

四、下载链接

发布包下载链接:串口调试助手.zip

https://pan.baidu.com/s/12RQ_fxREV7zOH-MwqIUzlQ?pwd=cedu

五、源码分享

#ifndef SERIALWORKER_H
#define SERIALWORKER_H

#include <QThread>
#include <QSerialPort>
#include <QQueue>
#include <QMutex>
#include <QWaitCondition>
#include <QString>

#define tc(a) QString::fromLocal8Bit(a)

class SerialWorker : public QThread
{
    Q_OBJECT

public:
    SerialWorker();
    ~SerialWorker();

    void setupPort(const QString &portName, qint32 baudRate, QSerialPort::DataBits dataBits, QSerialPort::StopBits stopBits, QSerialPort::Parity parity);
    void addDataToSend(const QByteArray &data);

    inline void unpackData(const QByteArray& receivedData);




public slots:
    void startWorker(); // 用于替代start(),因为start()是QThread的成员函数
    void stop();

signals:
    void dataReceived(const QByteArray &data);
    void errorOccurred(const QString &error);

    // 新增信号
    void informations(int index, const QString &msg); // 用于发送一般信息
    void warnings(int index, const QString &msg);     // 用于发送警告信息
    void errors(int index, const QString &msg);       // 用于发送错误信息
    void connected(int index, bool success);          // 用于发送连接状态

protected:
    void run() override;

private:
    QSerialPort *serialPort;
    QQueue<QByteArray> sendQueue;
    QMutex mutex;
    QWaitCondition waitCondition;
    bool running;


    // 串口参数
    QString portName;
    qint32 baudRate;
    QSerialPort::DataBits dataBits;
    QSerialPort::StopBits stopBits;
    QSerialPort::Parity parity;
};

#endif // SERIALWORKER_H
#include "serialworker.h"
#include <QDebug>
#include <QEventLoop>
#include <QTimer>
SerialWorker::SerialWorker()
    : serialPort(nullptr), running(false)
{
}

SerialWorker::~SerialWorker()
{
    stop();
    if (serialPort && serialPort->isOpen()) {
        serialPort->close();
    }
    delete serialPort;
}

void SerialWorker::setupPort(const QString &portName, qint32 baudRate, QSerialPort::DataBits dataBits, QSerialPort::StopBits stopBits, QSerialPort::Parity parity)
{
    this->portName = portName;
    this->baudRate = baudRate;
    this->dataBits = dataBits;
    this->stopBits = stopBits;
    this->parity = parity;
}

void SerialWorker::addDataToSend(const QByteArray &data)
{
    QMutexLocker locker(&mutex);
    sendQueue.enqueue(data);
    waitCondition.wakeOne();
}

void SerialWorker::unpackData(const QByteArray &receivedData)
{
      emit dataReceived(receivedData);
}



void SerialWorker::startWorker()
{
    if(!isRunning())
        start();

}

void SerialWorker::stop()
{
    QMutexLocker locker(&mutex);
    running = false;
    waitCondition.wakeAll();
}

void SerialWorker::run()
{

    serialPort = new QSerialPort();
    // 设置串口参数
    serialPort->setPortName(portName);
    serialPort->setBaudRate(baudRate);
    serialPort->setDataBits(dataBits);
    serialPort->setStopBits(stopBits);
    serialPort->setParity(parity);

    int index = 0;  // 可以根据需要设置适当的索引值

    if (!serialPort->open(QIODevice::ReadWrite)) {
        emit errors(index, serialPort->errorString());
        emit connected(index, false);
        delete serialPort;
        serialPort = nullptr;
        running=false;
        return;
    }



    running=true;
    emit connected(index, true);  // 成功连接
    emit informations(index, tc("串口已打开"));

    QEventLoop eventLoop;
    QTimer timer;
    timer.setInterval(10); // 设置定时器的间隔,控制事件循环的频率
    connect(&timer, &QTimer::timeout, &eventLoop, &QEventLoop::quit);
    timer.start();

    while (running)
    {
        QByteArray data;
        {
            QMutexLocker locker(&mutex);
            if (!sendQueue.isEmpty()) {
                data = sendQueue.dequeue();
            }
        }

        if (!data.isEmpty())
        {
            serialPort->write(data);

            if (!serialPort->waitForBytesWritten()) {
                emit errors(index, serialPort->errorString());
            } else {
                emit informations(index, tc("数据已发送[%1]").arg(QString::fromUtf8(data)));
            }
        }

        if (serialPort->waitForReadyRead(10)) { // 使用一个小的超时时间
            QByteArray receivedData = serialPort->readAll();
             emit dataReceived(receivedData);

        }

        // 处理事件循环,避免阻塞信号槽机制
        eventLoop.exec();
    }

    serialPort->close();
    emit informations(index, tc("串口已关闭"));
    emit connected(index, false);  // 断开连接

    delete serialPort;
    serialPort = nullptr;
}
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include "serialworker.h"
namespace Ui {
class MainWindow;
}
enum RunTimeStatus
{
    Error,            ///< 错误信息
    Warning,          ///< 警告信息
    Information,     ///< 常规信息
};
class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

     void writeRunTimeMsgs(const QString&msg,const int level);


     void createSendLinEdit();

     // 创建配置文件的函数,如果文件不存在则创建
     void createConfigFile(const QString &fileName, const QStringList &values);

     // 读取配置文件的函数
     QStringList readConfigFile(const QString &fileName);




private slots:
    void on_openButton_clicked();
    void on_closeButton_clicked();
    void onDataReceived(const QByteArray &data);



    void onRespondNormalMessage(const int index,const QString&msg);
    void onRespondWarningMessage(const int index,const QString&msg);
    void onRespondErrorMessage(const int index,const QString&msg);
    void onRespondSerialConnectState(const int index,const bool state);


    void on_clearRecvButton_clicked();

private:
    Ui::MainWindow *ui;


    SerialWorker *serialwork;
};

#endif // MAINWINDOW_H
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QSerialPortInfo>
#include <QDateTime>
#include <QDebug>
#include <QSettings>
#include <QFile>
#include <QTextCodec>
#include "timesendwidget.h"
# define tc(a) QString::fromLocal8Bit(a)
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow), serialwork(new SerialWorker())
{
    ui->setupUi(this);

    // 示例值列表
    QStringList values;
    values << tc("极客晨风") << tc("1327666247@qq.com")  << tc("微信公众号:码上Qt")<<tc("欢迎关注") ;

    createConfigFile("config.ini",values);

    //绘制发送界面
    createSendLinEdit();

    // 填充串口选项
    // 遍历当前系统可用的所有串口,并将它们的名字添加到端口名的QComboBox中
    foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) {
        ui->portNameComboBox->addItem(info.portName());
    }

    // 填充波特率选项
    // 将常用的波特率值添加到波特率的QComboBox中,数据以“字符串-枚举值”形式存储
    ui->baudRateComboBox->addItem("9600", QSerialPort::Baud9600);        // 波特率9600
    ui->baudRateComboBox->addItem("19200", QSerialPort::Baud19200);      // 波特率19200
    ui->baudRateComboBox->addItem("38400", QSerialPort::Baud38400);      // 波特率38400
    ui->baudRateComboBox->addItem("115200", QSerialPort::Baud115200);    // 波特率115200
    ui->baudRateComboBox->setCurrentIndex(0);  // 设置默认选项为第一个(9600)

    // 填充数据位选项
    // 将串口通信中常用的数据位数添加到数据位的QComboBox中,数据以“字符串-枚举值”形式存储
    ui->dataBitsComboBox->addItem("5", QSerialPort::Data5);   // 5个数据位
    ui->dataBitsComboBox->addItem("6", QSerialPort::Data6);   // 6个数据位
    ui->dataBitsComboBox->addItem("7", QSerialPort::Data7);   // 7个数据位
    ui->dataBitsComboBox->addItem("8", QSerialPort::Data8);   // 8个数据位(通常最常用)
    ui->dataBitsComboBox->setCurrentIndex(3);  // 设置默认选项为8个数据位(索引3)

    // 填充停止位选项
    // 将串口通信中常用的停止位添加到停止位的QComboBox中,数据以“字符串-枚举值”形式存储
    ui->stopBitsComboBox->addItem("1", QSerialPort::OneStop);            // 1个停止位
    ui->stopBitsComboBox->addItem("1.5", QSerialPort::OneAndHalfStop);   // 1.5个停止位(较少使用)
    ui->stopBitsComboBox->addItem("2", QSerialPort::TwoStop);            // 2个停止位
    ui->stopBitsComboBox->setCurrentIndex(0);  // 设置默认选项为1个停止位

    // 填充奇偶校验选项
    // 将串口通信中常用的奇偶校验方式添加到奇偶校验的QComboBox中,数据以“字符串-枚举值”形式存储
    ui->parityComboBox->addItem("None", QSerialPort::NoParity);          // 无校验(最常用)
    ui->parityComboBox->addItem("Even", QSerialPort::EvenParity);        // 偶校验
    ui->parityComboBox->addItem("Odd", QSerialPort::OddParity);          // 奇校验
    ui->parityComboBox->addItem("Mark", QSerialPort::MarkParity);        // 标志位校验(较少使用)
    ui->parityComboBox->addItem("Space", QSerialPort::SpaceParity);      // 空格位校验(较少使用)
    ui->parityComboBox->setCurrentIndex(0);  // 设置默认选项为无校验


    connect(serialwork, &SerialWorker::dataReceived, this, &MainWindow::onDataReceived);
    // 连接信号与槽
    connect(serialwork, &SerialWorker::informations, this, &MainWindow::onRespondNormalMessage);
    connect(serialwork, &SerialWorker::warnings, this, &MainWindow::onRespondWarningMessage);
    connect(serialwork, &SerialWorker::errors, this, &MainWindow::onRespondErrorMessage);
    connect(serialwork, &SerialWorker::connected, this, &MainWindow::onRespondSerialConnectState);

}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::writeRunTimeMsgs(const QString &msg, const int level )
{
    QString Level;
    switch (level) {
    case Error:      Level=tc("[ ***错误信息*** ]");break;
    case Warning:    Level=tc("[ ***警报信息*** ]");break;
    case Information:Level=tc("[ ***正常信息*** ]");break;
    }
    ui->statusBar->showMessage(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss ") +Level+msg );
}

void MainWindow::createSendLinEdit()
{


    for(auto val:readConfigFile("config.ini"))
    {
        TimeSendWidget *sendWidget=new TimeSendWidget;
        sendWidget->setLinText(val);
        connect(sendWidget,&TimeSendWidget::sendLineData,[=](const QByteArray &data )
        {

            if(ui->openButton->isEnabled())
            {
                writeRunTimeMsgs(tc("串口未打开,无法发送"),Error);
                return ;
            }


            if(data.isEmpty())
            {
                writeRunTimeMsgs(tc("信息为空,拒绝发送"),Warning);
                return ;
            }

            //主界面显示
            if(ui->isShowSendButton->isChecked())
            {
                QString msg;
                if(ui->isShowTimeButton->isChecked())
                    msg.append(QDateTime::currentDateTime().toString("hh:mm:ss.(zzz)  "));
                msg.append(tc("发送  "));
                msg.append(ui->isShowHexButton->isChecked()? data.toHex(' ').toUpper():QString::fromLocal8Bit(data));
                ui->receiveTextEdit->append(msg);

            }

            //更新显示信息
            ui->sendByte->setValue(ui->sendByte->value()+data.size());
            ui->sendFram->setValue(ui->sendFram->value()+1);
            serialwork->addDataToSend(data);
        });
        ui->sendGroup->layout()->addWidget(sendWidget);
    }


}

void MainWindow::createConfigFile(const QString &fileName, const QStringList &values)
{
    // 检查文件是否存在
    QFile file(fileName);
    if (file.exists())
        return;

    QSettings settings(fileName, QSettings::IniFormat);
    settings.setIniCodec(QTextCodec::codecForName("UTF-8"));

    // 根据传入的值动态创建组和键值对
    int groupIndex = 1;
    for (const QString &value : values)
    {
        QString groupName = QString("%1").arg(groupIndex);
        settings.beginGroup(groupName);
        settings.setValue(QString("%1").arg(groupIndex), value);
        settings.endGroup();
        groupIndex++;
    }

}

QStringList MainWindow::readConfigFile(const QString &fileName)
{
    QStringList iniinfors;

    QSettings settings(fileName, QSettings::IniFormat);
    settings.setIniCodec(QTextCodec::codecForName("UTF-8"));
    // 获取所有组并排序
    QStringList groups = settings.childGroups();
    groups.sort();

    for (const QString &group : groups)
    {
        settings.beginGroup(group);
        QStringList keys = settings.childKeys();
        for (const QString &key : keys)
            iniinfors<< settings.value(key).toString();
        settings.endGroup();
    }
    return  iniinfors;
}
void MainWindow::on_openButton_clicked()
{
    QString portName = ui->portNameComboBox->currentText();
    qint32 baudRate = ui->baudRateComboBox->currentData().toInt();
    QSerialPort::DataBits dataBits = static_cast<QSerialPort::DataBits>(ui->dataBitsComboBox->currentData().toInt());
    QSerialPort::StopBits stopBits = static_cast<QSerialPort::StopBits>(ui->stopBitsComboBox->currentData().toInt());
    QSerialPort::Parity parity = static_cast<QSerialPort::Parity>(ui->parityComboBox->currentData().toInt());
    serialwork->setupPort(portName, baudRate, dataBits, stopBits, parity);
    serialwork->startWorker();
}

void MainWindow::on_closeButton_clicked()
{
    serialwork->stop();

}



void MainWindow::onDataReceived(const QByteArray &data)
{



    if(ui->isShowRecvButton->isChecked())
    {
        QString msg;
        if(ui->isShowTimeButton->isChecked())
            msg.append(QDateTime::currentDateTime().toString("hh:mm:ss.(zzz)  "));
        msg.append(tc("接收  "));
        msg.append(ui->isShowHexButton->isChecked()? data.toHex(' ').toUpper():QString::fromLocal8Bit(data));
        ui->receiveTextEdit->append(msg);
    }


    //更新显示信息
    ui->recvByte->setValue(ui->recvByte->value()+data.size());
    ui->recvFram->setValue(ui->recvFram->value()+1);


}

void MainWindow::onRespondNormalMessage(const int , const QString &msg)
{
    writeRunTimeMsgs(msg,Information);
}

void MainWindow::onRespondWarningMessage(const int , const QString &msg)
{
    writeRunTimeMsgs(msg,Warning);
}

void MainWindow::onRespondErrorMessage(const int , const QString &msg)
{
    writeRunTimeMsgs(msg,Error);
}

void MainWindow::onRespondSerialConnectState(const int , const bool state)
{
    ui->openButton->setEnabled(!state);
    ui->closeButton->setEnabled(state);

}

void MainWindow::on_clearRecvButton_clicked()
{
    ui->recvByte->setValue(0);
    ui->recvFram->setValue(0);
    ui->sendByte->setValue(0);
    ui->sendFram->setValue(0);
    ui->receiveTextEdit->clear();
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2113612.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Qt QLineEdit 输入内容后字数在右侧动态展示

前言 QLineEdit 设置可输入最大长度可以使用 lineEdit->setMaxLength(10); 怎么实时的把当前输入字数显示出来呢&#xff0c;像饿了么的 input 组件那样 <el-inputtype"text"placeholder"请输入内容"v-model"text"maxlength"10&q…

基于orangePi的智能家居系统

目录 一.接线图 1.orangePi接线 2.继电器接线 二.语音模块的配置 1.pin脚的配置 2.命令词自定义信息 三.测试 1.通过gpio指令测试烟雾检测器是否正确连接 2.编写脚本测试其他模组接线是否正常 四.人脸识别方案 1.首先开通人脸搜索识别服务 2. 点击产品控制台,向人…

【重学 MySQL】十五、过滤数据

【重学 MySQL】十五、过滤数据 基本用法使用AND、OR和NOT使用IN操作符使用BETWEEN操作符使用LIKE操作符使用IS NULL和IS NOT NULL 在MySQL中&#xff0c;过滤数据主要通过WHERE子句来实现。WHERE子句允许你指定条件来过滤从表中检索出来的行。只有当行满足WHERE子句中的条件时&…

个人随想-一个有意思的鼠标截屏RAG

multi-model rag现在的技术已经比较成熟了&#xff0c;我们也落地了很多公司的rag和agent项目。今天先不说项目落地&#xff0c;今天给大家分享一个有意思的需求。 广州的一家公司&#xff0c;当我们rag搭建完成后&#xff0c;他们的一个产品经理提了一个需求。他们说&#xf…

C#转java工具

使用 激活 点击关闭即可 参考文献 https://www.cnblogs.com/liyhbk/p/17358520.html

【视频教程】手把手AppWizard轻松制作一个emWin滑动主界面控制框架,任意跳转控制(2024-09-06)

现在的新版AppWizard已经比较好用&#xff0c;用户可以轻松的创建各种项目常规界面。 比如早期创建一个支持滑动的主界面框架&#xff0c;并且可以跳转各种子界面&#xff0c;仅仅界面布局和各种图片格式转换都要花不少时间&#xff0c;而现在使用AppWizard&#xff0c;可以说…

关键点检测(7)——yolov8-head的搭建

前两节我学习了yolov8的backbone和head操作。这一节就到了head部分。  我们知道yolov8在流行的yolov5的架构上进行了扩展。在多个方面提供了改进。尤其是head部分&#xff0c;变化最大。yolov8模型与其前身的主要区别在于使用了无锚点检测&#xff08;即从原先的耦合头变成了解…

无人机电调接线

接线方式&#xff1a; 电调的作用是将飞控板的PWM控制信号转变为电流信号 因为电机的电流是很大的&#xff0c;通常每个点击正常工作时都平均有3A左右的电流&#xff0c;如果没有电调的存在&#xff0c;飞控无法承受这么大的电流。 电调的选择&#xff1a;电调上标的电流值是…

240907-Gradio渲染装饰器Render-Decorator

A. 最终效果 B. 示例代码 import gradio as gr import gradio as grwith gr.Blocks() as demo:input_text gr.Textbox()gr.render(inputsinput_text)def show_split(text):if len(text) 0:gr.Markdown("## No Input Provided")else:# for letter in text:for lett…

精通Java微服务

第1章 微服务是在面向服务架构SOA的基础上进一步发展而来的&#xff0c;它比SOA做得更加彻底&#xff0c;其单体服务被更加彻底地划分&#xff0c;最大限度地实现了服务的单一职责。 1.1.2互联网 即计算机网络&#xff0c;连接了世界上数以万计的计算机设备&#xff08;可联…

ROADM(可重构光分插复用器)-介绍

1. 引用 https://zhuanlan.zhihu.com/p/163369296 https://zhuanlan.zhihu.com/p/521352954 https://zhuanlan.zhihu.com/p/91103069 https://zhuanlan.zhihu.com/p/50610236 术语&#xff1a; 英文缩写描述灰光模块彩光模块CWDM&#xff1a;Coarse Wave-Length Division …

1233333333333

&#x1f4e2;博客主页&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01; &#x1f4e2;本文由 JohnKi 原创&#xff0c;首发于 CSDN&#x1f649; &#x1f4e2;未来很长&#…

【JVM】JVM栈帧中的动态链接 与 Java的面向对象特性--多态

栈帧 每一次方法调用都会有一个对应的栈帧被压入栈&#xff08;虚拟机栈&#xff09;中&#xff0c;每一个方法调用结束后&#xff0c;都会有一个栈帧被弹出。 每个栈帧中包括&#xff1a;局部变量表、操作数栈、动态链接、方法返回地址。 JavaGuide&#xff1a;Java内存区域…

STM32CubeMX和HAL库

目录 STM32CubeMX和HAL库介绍 STM32Cube主要包括两部分 安装MCU固件包 软件功能与基本使用 STM32CubeMX和HAL库介绍 STM32CubeMX软件是ST有限公司为STM32系列微控制器快速建立工程&#xff0c;并快速初始化使用到的外设、GPIO等而设计的&#xff0c;大大缩短了开发时间。 同…

【面试八股总结】MySQL日志:undo log、redo log、binlog

MySQL中存在三种日志&#xff1a; undo log&#xff08;回滚日志&#xff09;&#xff1a;是 Innodb 存储引擎层生成的日志&#xff0c;实现了事务中的原子性&#xff0c;主要用于事务回滚和 MVCC。redo log&#xff08;重做日志&#xff09;&#xff1a;是 Innodb 存储引擎层…

服务网关工作原理,如何获取用户真实IP?

文章目录 一、什么是网关二、网关工作原理 (★)三、SpringCloud Gateway3.1 Gateway 简介3.2 Gateway 环境搭建3.3 自定义路由规则 (★)3.4 局部过滤器3.5 全局过滤器&#xff08;案例&#xff1a;获取用户真实IP地址&#xff09; (★) 补充1&#xff1a;不同类型的客户端如何设…

MapSet之相关概念

系列文章&#xff1a; 1. 先导片--Map&Set之二叉搜索树 2. Map&Set之相关概念 目录 1.搜索 1.1 概念和场景 1.2 模型 2.Map的使用 2.1 关于Map的说明 2.2 关于Map.Entry的说明 2.3 Map的常用方法说明 3.Set的说明 3.1关于Set说明 3.2 常见方法说明 1.搜…

CTFHub技能树-Git泄漏-Index

目录 一、Git索引&#xff08;Index&#xff09;的基本概念 二、解题过程 主旨&#xff1a;使用git泄漏恢复源代码 方法一&#xff1a;使用GitHack手动恢复 方法二&#xff1a;直接使用Git_Extract获取网站源代码拿去flag 当前大量开发人员使用git进行版本控制&#xff0c…

图神经网络(2)预备知识

1. 图的基本概念 对于接触过数据结构和算法的读者来说&#xff0c;图并不是一个陌生的概念。一个图由一些顶点也称为节点和连接这些顶点的边组成。给定一个图G(V,E), 其 中V{V1,V2,…,Vn} 是一个具有 n 个顶点的集合。 1.1邻接矩阵 我们用邻接矩阵A∈Rnn表示顶点之间的连接关…

初识Linux · 有关gdb

目录 前言&#xff1a; 1 预备知识 2 gdb的使用 前言&#xff1a; 当我们Linux学到了这里的时候&#xff0c;我们大概会有一种感觉是&#xff0c;从VS2022转战Linux&#xff0c;写代码对我们来说是一种重新构建读写代码的一个过程&#xff0c;从文本编辑器&#xff0c;到文…