news 2026/9/1 20:00:59

用C++和Qt构建实时预览的Markdown编辑器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用C++和Qt构建实时预览的Markdown编辑器

用 C++ 写一个带实时预览的 Markdown 编辑器,听起来像是一个不小的工作量,其实拆开看只有三件事:左侧的文本编辑区、右侧的预览区,以及把 Markdown 内容即时转换为 HTML 的解析层。这篇文章用 Qt 和 cmark 库,从零搭出一个最小可运行版本,并逐步把语法高亮、本地图片加载、常见环境问题排查等内容补上。如果你已经能写完一个简单的 Qt Widgets 窗口程序,但对 Markdown 解析和实时刷新还没有完整思路,这篇文章会给你一条清晰的落地路径。

实时预览不是指用户点一下按钮才渲染,而是编辑区内容发生变化后,预览区自动更新。要做到这一点,依赖一个稳定的信号循环和解析库。读完这篇内容后,你可以得到一个带实时预览的桌面 Markdown 编辑器骨架;如果要继续扩展成笔记工具、技术写作工具或内部文档客户端,也可以在此基础上直接加文件管理、主题切换、导入导出等功能。

1. 实时预览编辑器要拆成哪几个模块

1.1 输入、解析、渲染三层

Markdown 编辑器的核心并不神秘。用户在一个文本控件里输入 Markdown 原文,程序把原文交给解析器,解析器生成 HTML,再把 HTML 交给预览控件渲染。整个流程就是“输入 -> 解析 -> 渲染”三个环节。

  • 输入层:QPlainTextEdit,负责接收键盘输入、支持基本的文本选择、撤销重做。
  • 解析层:cmark,把 Markdown 文本转换成 HTML 字符串。
  • 渲染层:QTextBrowser,把 HTML 字符串展示为富文本,外部链接可以交给系统浏览器处理。

把这三个环节拆开之后,实时预览就只是一个信号连接问题:当编辑区的文本发生变化时,重新执行一次“解析 -> 渲染”即可。

这段链路里,最容易被低估的是解析层。很多人会先写一个 replace 函数,把#替换成<h1>,把**替换成<strong>。这种方式处理简单的单行语法没问题,一旦遇到嵌套列表、代码块、转义字符和链接,就会漏洞百出。所以直接使用成熟的 Markdown 解析库,是更值得投入的选择。

1.2 为什么选 Qt + cmark,不自己写解析器

Markdown 语法看起来简单,真正解析起来非常容易出错。列表嵌套、段落换行、行内代码、转义字符、引用块、任务列表、表格,每一类都有边界情况。自己写一个支持常用语法的解析器,至少要几百行,而且处理不完整会造成预览和源码不一致。直接使用开源解析库是更稳妥的方案。

cmark 是 CommonMark 官方参考实现,用 C 语言编写,提供了非常简洁的 C API。它没有额外依赖,编译出来只有一个静态库或动态库,很适合嵌入到 C++ 工程里。Qt 则负责 UI、事件循环、富文本显示和跨平台编译。这两个库搭配,可以比较轻松地完成一个可用的桌面编辑器。

在选型时还要考虑渲染层。最简单的是QTextBrowser,它基于QTextDocument,支持 HTML 子集,不需要额外安装浏览器内核。若需要完整支持 CSS、JavaScript、代码高亮,可以使用QWebEngineView,但代价是运行时依赖 WebEngine。对于大多数笔记场景,QTextBrowser已经足够。

注意:预览控件的选型会影响发布包体积和启动速度。QTextBrowser 轻量,QWebEngineView 渲染能力强,两者不是替代关系,而是不同场景的取舍。

1.3 最小闭环目标和学习环境差异

这篇文章的最小目标很明确:打开程序后,左侧输入 Markdown,右侧实时刷新 HTML 渲染结果。先把这条链路跑通,再谈高亮、文件保存、主题切换等功能。

在学习环境里,建议直接使用 Qt Creator 或 CMake 命令行工程,不要一开始就加入复杂的插件系统。生产环境则需要考虑打包、日志、配置外置、崩溃收集等问题。如果你是在公司项目里落地,还需要把 Markdown 解析结果做缓存、控制在每次按键时是否全量解析,以及兼容不同电脑上的运行时版本。

2. 环境准备与依赖配置

2.1 编译工具链和 Qt 版本选择

文章示例使用 Qt Widgets 模块,不依赖 QML。推荐 Qt 5.15 或 Qt 6.5 以上,二者 API 在本文用到的范围内基本一致。如果你在 Windows 上使用 MSVC,需要同时安装 Visual C++ Redistributable;在 Linux 上使用 GCC,则需要安装 base-devel 或 build-essential。很多 C++ 项目启动时报缺少VCRUNTIME140.dll,本质上就是 Visual C++ Redistributable 版本没有装全。

如果你是第一次创建工程,建议用 Qt Creator 自带的 “Qt Widgets Application” 模板,再手动加入 cmark 依赖。本文的代码结构如下:

MarkdownEditor/ ├── CMakeLists.txt ├── src/ │ ├── main.cpp │ ├── MainWindow.h │ ├── MainWindow.cpp │ ├── MarkdownHighlighter.h │ └── MarkdownHighlighter.cpp

这个结构足够支撑一个带预览、高亮和文件处理的小型桌面程序。如果你后续要加入测试或插件,可以在src旁边继续增加testsplugins目录。

2.2 安装 cmark 的三种方式

cmark 的安装方式取决于操作系统和包管理器。

在 Ubuntu / Debian 上:

sudo apt update sudo apt install libcmark-dev

在 Windows 上使用 vcpkg:

vcpkg install cmark

如果你不想使用系统包管理器,也可以直接源码编译:

git clone https://github.com/commonmark/cmark.git cd cmark mkdir build && cd build cmake .. cmake --build . --config Release sudo cmake --install .

源码编译方式是通用兜底方案。需要注意的是,不同发行版提供的 cmark 版本可能不同,CMake config 文件的 target 名称也可能不同。落地上机前先确认安装版本和 CMake 能找到的 target 名称。

2.3 CMake 工程里同时连接 Qt 和 cmark

在 CMakeLists.txt 中,先查找 Qt,再查找 cmark。下面是一个兼容 Qt5 / Qt6 的示例:

cmake_minimum_required(VERSION 3.16) project(MarkdownEditor LANGUAGES CXX C) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) find_package(Qt6 COMPONENTS Widgets QUIET) if(NOT Qt6_FOUND) find_package(Qt5 5.15 REQUIRED COMPONENTS Widgets) endif() find_package(cmark CONFIG QUIET) if(NOT cmark_FOUND) find_package(PkgConfig REQUIRED) pkg_check_modules(CMARK IMPORTED_TARGET REQUIRED libcmark) endif() if(TARGET cmark::cmark) set(CMARK_TARGET cmark::cmark) elseif(TARGET PkgConfig::CMARK) set(CMARK_TARGET PkgConfig::CMARK) else() set(CMARK_TARGET cmark) endif() add_executable(markdown_editor src/main.cpp src/MainWindow.cpp src/MainWindow.h src/MarkdownHighlighter.cpp src/MarkdownHighlighter.h ) target_link_libraries(markdown_editor PRIVATE Qt${QT_VERSION_MAJOR}::Widgets ${CMARK_TARGET} )

关键点有两处。一是CMAKE_AUTOMOC必须开启,因为MainWindowMarkdownHighlighter都是 QObject 子类,需要处理信号槽。二是LANGUAGES CXX C要同时包含 C,因为 cmark 本身是 C 项目,虽然最终通过库连接,但某些构建方式会需要 C 语言编译规则。

如果find_package找不到 cmark,通常是因为没有安装开发包或没有设置CMAKE_PREFIX_PATH。不要直接手写target_link_libraries指向一个假设的路径,优先使用 CMake 的 package 机制,这样换机器后更容易复现。

3. 实现一个最小可运行版本

3.1 创建主窗口与左右分栏布局

主窗口使用QSplitter把编辑区和预览区左右排布。QSplitter不仅提供分隔条,还允许用户拖动调整左右宽度,这对 Markdown 编辑器是基本体验要求。

#include <QMainWindow> #include <QSplitter> #include <QPlainTextEdit> #include <QTextBrowser> class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); private slots: void renderMarkdown(); private: QPlainTextEdit *editor_; QTextBrowser *preview_; };

构造函数的实现如下:

#include "MainWindow.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), editor_(new QPlainTextEdit(this)), preview_(new QTextBrowser(this)) { auto *splitter = new QSplitter(Qt::Horizontal, this); splitter->addWidget(editor_); splitter->addWidget(preview_); splitter->setStretchFactor(0, 1); splitter->setStretchFactor(1, 1); setCentralWidget(splitter); setWindowTitle("C++ Markdown Live Preview"); resize(1000, 700); connect(editor_, &QPlainTextEdit::textChanged, this, &MainWindow::renderMarkdown); }

renderMarkdown()尚未实现之前,程序可以编译运行,但右侧不会更新。先把界面搭出来是为了验证布局是否正常,再继续加入解析逻辑。

3.2 用 cmark 把 Markdown 转成 HTML

这是整篇文章最核心的一段代码。先写一个工具函数,输入QString,输出QString

#include <cmark.h> QString markdownToHtml(const QString &markdown) { QByteArray utf8 = markdown.toUtf8(); char *html = cmark_markdown_to_html( utf8.constData(), utf8.size(), CMARK_OPT_DEFAULT ); QString result = QString::fromUtf8(html); free(html); return result; }

cmark_markdown_to_html接受const char*和字节长度,所以必须先把 QString 转成 UTF-8 字节数组。返回的char*是 cmark 内部通过 malloc 分配的内存,用完后必须调用free(),否则每次预览都会泄漏一块内存。

CMARK_OPT_DEFAULT表示使用 CommonMark 默认行为。常用选项如下:

选项作用使用建议
CMARK_OPT_DEFAULT默认解析行为,不输出源码位置,保留 HTML 块中的潜在危险标签基础场景使用
CMARK_OPT_SOURCEPOS在输出 HTML 中加入 sourcepos 属性调试解析问题
CMARK_OPT_HARDBREAKS将普通换行渲染为<br>需要保留换行时使用
CMARK_OPT_UNSAFE允许渲染原始 HTML 和危险链接仅信任输入时使用
CMARK_OPT_SMART将直引号、省略号等转为弯引号文档排版美化场景

默认情况下,<script>等原始 HTML 不会被渲染成可执行内容,这是安全设计,不是 bug。若编辑器用于公开内容,不要随便开启CMARK_OPT_UNSAFE

3.3 用 textChanged 信号驱动实时预览

QPlainTextEdit::textChanged在每次文本内容变化时触发。在这里连接renderMarkdown,就能实现“敲一个字,预览区刷新”的效果。

void MainWindow::renderMarkdown() { QString html = markdownToHtml(editor_->toPlainText()); preview_->setHtml(html); }

这段代码简单直接,但每次按键都会对全文重新解析。对于几 KB 的文本没有问题,一旦打开几百 KB 的文档,连续打字会造成明显卡顿。更稳妥的做法是加一个去抖定时器,把连续触发合并为一次解析。

auto *deb
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/31 17:54:32

遗传算法优化随机森林的Matlab回归预测实现

简介&#xff1a;本资源是一套面向机器学习初学者与科研人员的Matlab回归建模实战方案&#xff0c;聚焦多输入单输出&#xff08;MISO&#xff09;场景下的高精度预测需求&#xff0c;通过遗传算法&#xff08;GA&#xff09;自动优化随机森林&#xff08;RF&#xff09;超参数…

作者头像 李华
网站建设 2026/8/31 17:54:24

Transformer会被取代吗?解析自注意力与Mamba等新型架构

过去两年里&#xff0c;大模型领域的迭代速度快到让人应接不暇&#xff0c;但有一个事实很容易被忽略&#xff1a;真正处于地基位置的模型架构&#xff0c;其实一直没有变。2017 年论文《Attention Is All You Need》提出的 Transformer&#xff0c;至今仍是 GPT、BERT、LLaMA、…

作者头像 李华
网站建设 2026/8/31 17:54:22

Transformer会被取代吗?从技术本质与工程视角看架构演进

如果你关注 AI 圈&#xff0c;每隔一段时间总能看到类似“XX 架构要取代 Transformer”的标题。从最初的稀疏注意力&#xff0c;到线性注意力&#xff0c;再到 Mamba、RWKV、混合专家架构&#xff0c;每一轮讨论都让人忍不住问&#xff1a;Transformer 真的会被推翻吗&#xff…

作者头像 李华
网站建设 2026/8/31 17:53:45

从README缺失的仓库看模块化工具:技术选型与落地实践指南

看到lightningpixel / modly这个仓库名时&#xff0c;我第一反应不是“这是个什么工具”&#xff0c;而是“作者到底想用这个词表达什么”。modly很像modular&#xff08;模块化&#xff09;的变体&#xff0c;也像mod加后缀-ly&#xff0c;暗示“以模块的方式做事”。lightnin…

作者头像 李华
网站建设 2026/8/31 17:52:57

基于Qt/C++的三维牙齿模型自动化预处理:分割、编号与缺失识别

简介&#xff1a;本资源是一套面向高校计算机、生物医学工程及相关专业本科生的毕业设计与课程设计实践项目&#xff0c;聚焦三维牙科扫描数据的自动化预处理问题&#xff0c;为口腔临床辅助诊断提供可复现的技术方案。资源包共25个文件&#xff0c;含5个典型上下颌STL牙齿模型…

作者头像 李华