使用qt编写简单的markdown编辑器

33 阅读3分钟

使用qt编写简单的markdown编辑器

  • 最近想用qt模仿anki来编写一套适合自己的复习软件,首先需要解决的是markdown,anki不支持markdown,而我的笔记都是以markdown形式保存的,所以复习软件必须要支持markdown。
  • qt6.3之后就内置了markdown解析器,所以先研究下qt内置的markdown解析器能不能满足我的需求
  • 先上效果图
    • image-20260124215926356.png
    • 从以上图片来看,解析的效果还可以,暂且使用qt内置的markdown解析器,如果以后满足不了需求的话再考虑第三方库。

布局与初始实现

  • 从效果图来看,布局十分简单,就是一个水平均分的布局,左边的原始文本编辑器(QPlainTextEdit 控件),右边是markdown的渲染效果(QTextEdit 控件)

  • 左边原始文本编辑器文本变化的时候,右边markdown的渲染会同步更新,使用qt内置的markdown解析器十分简单,如下代码所示

    void ReviewApp::on_plainTextEdit_textChanged()
    {
        QString text = ui.plainTextEdit->toPlainText();
        
        ui.markdownTextEdit->setMarkdown(text);
    }
    
  • 设置字体大小、字体、Tab的宽度

    // 假设你的 QPlainTextEdit 名为 plainTextEdit
    QFont font;
    font.setFamily("Consolas");      // 推荐等宽字体,如 "Consolas"、"Courier New"、"Fira Mono"
    font.setPointSize(14);           // 设置字号
    ui.plainTextEdit->setFont(font);
    ui.markdownTextEdit->setFont(font);
    
    // 设置 Tab 宽度(以空格数为单位,常见为4或2)
    const int tabStop = 4; // 4个空格
    QFontMetrics metrics(font);
    ui.plainTextEdit->setTabStopDistance(tabStop * metrics.horizontalAdvance(' '));
    

实现粘贴图片的功能

  • 左侧的文本编辑器需要支持粘贴图片功能,原始的控件无法支持这个功能,我们需要定义一个类继承 QPlainTextEdit 控件并重写 insertFromMimeData 方法
class MarkdownPlainTextEdit : public QPlainTextEdit
{
    Q_OBJECT
public:
    explicit MarkdownPlainTextEdit(QWidget* parent = nullptr) : QPlainTextEdit(parent) {}

protected:
    void insertFromMimeData(const QMimeData* source) override
    {
        if (source->hasImage()) {
            QImage image = qvariant_cast<QImage>(source->imageData());
            QString fileName = QDir::temp().filePath(
                QString("pasted_%1.png").arg(QDateTime::currentMSecsSinceEpoch()));
            image.save(fileName, "PNG");
            // 插入 Markdown 图片语法
            this->insertPlainText(QString("![Images](%1)\n").arg(fileName));
        }
        else {
            QPlainTextEdit::insertFromMimeData(source);
        }
    }
};

改变tab的默认行为

  • 如果在编辑器中选中一段文本,并按下Tab键,选中的文本将被替换为 Tab,这个在 markdown 中十分的 ugly,所以我们改变这一默认行为

  • 还是在 MarkdownPlainTextEdit 重写 keyPressEvent 方法

    void keyPressEvent(QKeyEvent* event) override
    {
        if (event->key() == Qt::Key_Tab) {
            QTextCursor cursor = textCursor();
            if (cursor.hasSelection()) {
                int start = cursor.selectionStart();
                int end = cursor.selectionEnd();
                cursor.setPosition(start);
                int firstBlock = cursor.blockNumber();
    
                cursor.setPosition(end, QTextCursor::KeepAnchor);
                int lastBlock = cursor.blockNumber();
    
                // 记录原始光标
                int selStart = cursor.selectionStart();
                int selEnd = cursor.selectionEnd();
    
                cursor.beginEditBlock();
                for (int i = firstBlock; i <= lastBlock; ++i) {
                    QTextBlock block = document()->findBlockByNumber(i);
                    QTextCursor blockCursor(block);
                    blockCursor.insertText("\t"); // 或用空格替代
                }
                cursor.endEditBlock();
    
                // 重新选中原区域(向右偏移)
                QTextCursor newCursor = textCursor();
                newCursor.setPosition(selStart);
                newCursor.setPosition(selEnd + (lastBlock - firstBlock + 1), QTextCursor::KeepAnchor);
                setTextCursor(newCursor);
                return;
            }
        }
        QPlainTextEdit::keyPressEvent(event);
    }
    

简单实现代码高亮

  • qt内置的解析器无法高亮显示代码,我们需要简单的实现一下
#pragma once
#include <QSyntaxHighlighter>
#include <QTextCharFormat>
#include <QRegularExpression>

class CodeHighlighter : public QSyntaxHighlighter
{
    Q_OBJECT
public:
    explicit CodeHighlighter(QTextDocument* parent)
        : QSyntaxHighlighter(parent)
    {
        // 关键字格式
        keywordFormat.setForeground(Qt::blue);
        keywordFormat.setFontWeight(QFont::Bold);
        QStringList keywordPatternsList = {
            "\\bclass\\b", "\\bconst\\b", "\\bvoid\\b", "\\bint\\b", "\\bfloat\\b",
            "\\bdouble\\b", "\\bQString\\b", "\\bpublic\\b", "\\bprivate\\b", "\\bprotected\\b",
            "\\bif\\b", "\\belse\\b", "\\bfor\\b", "\\bwhile\\b", "\\breturn\\b"
        };
        for (const QString& pattern : keywordPatternsList) {
            keywordPatterns.append(QRegularExpression(pattern));
        }

        // 注释格式
        commentFormat.setForeground(Qt::darkGreen);
        commentFormat.setFontItalic(true);

        // 字符串格式
        stringFormat.setForeground(Qt::darkRed);
    }

protected:
    void highlightBlock(const QString& text) override
    {
        // 高亮关键字
        for (const QRegularExpression& pattern : keywordPatterns) {
            QRegularExpressionMatchIterator i = pattern.globalMatch(text);
            while (i.hasNext()) {
                QRegularExpressionMatch match = i.next();
                setFormat(match.capturedStart(), match.capturedLength(), keywordFormat);
            }
        }

        // 高亮字符串
        QRegularExpression stringPattern("\".*?\"");
        QRegularExpressionMatchIterator stringIt = stringPattern.globalMatch(text);
        while (stringIt.hasNext()) {
            QRegularExpressionMatch match = stringIt.next();
            setFormat(match.capturedStart(), match.capturedLength(), stringFormat);
        }

        // 高亮注释
        QRegularExpression commentPattern("//[^\n]*");
        QRegularExpressionMatchIterator commentIt = commentPattern.globalMatch(text);
        while (commentIt.hasNext()) {
            QRegularExpressionMatch match = commentIt.next();
            setFormat(match.capturedStart(), match.capturedLength(), commentFormat);
        }
    }

private:
    QTextCharFormat keywordFormat;
    QTextCharFormat commentFormat;
    QTextCharFormat stringFormat;
    QList<QRegularExpression> keywordPatterns;
};
  • 在app的构造函数中只需要调用以下代码即可
new CodeHighlighter(ui.markdownTextEdit->document());