ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

QCodeEditor 集成实战:给 Qt 应用打造带语法高亮的代码编辑器

QCodeEditor 集成实战:给 Qt 应用打造带语法高亮的代码编辑器 简介这是一份面向Qt开发者的代码编辑器小部件组件包基于C11与Qt5构建提供编辑/查看代码的核心能力。组件内置自动括号匹配、自动缩进、空格替换制表符并内置GLSL、C、XML、JSON、Lua、Python等多种语言的语法高亮与代码补全规则同时支持Qt Creator风格主题切换与框架选择可直接嵌入需要代码编辑界面的工具、IDE插件或教学演示项目。整个工程以CMake库形式组织可作为子模块集成到现有工程中。压缩包共73个文件大小108KB。其中18个hpp和18个cpp构成核心源码框架7个xml描述高亮规则2个qrc管理资源引用另含json、lua、glsl、py等语言相关文件以及示例工程和MIT许可证目录划分清晰便于按需裁剪或二次开发。目前已有817人学习下载对想理解语法高亮实现原理的Qt进阶者而言是一份轻量且完整的参考实现。1. QCodeEditor 是什么给 Qt 工具加编辑器为什么不用 QPlainTextEdit 硬写很多 Qt 开发者第一次遇到 QCodeEditor 的场景很一致手头工具需要一个能看又能改代码的窗口先拖一个 QPlainTextEdit 进去结果行号没有、高亮没有、缩进靠运气写完自己都嫌弃。QCodeEditor 是一个开源的 Qt 代码编辑器小部件它的定位是给 Qt 桌面应用嵌入一个“够用且不折腾”的编辑区不需要像 QScintilla 那样引入一大套第三方库也不需要自己从 QAbstractScrollArea 开始堆轮子它直接基于 QPlainTextEdit 加了一层行号区域、语法高亮框架和搜索组件做到开箱即用。这个方案适合谁如果你在做的工具是日志查看器、脚本配置面板、SQL 客户端、内嵌 Python 调试器或者任何“需要一段带高亮的文本编辑区”的桌面程序QCodeEditor 能帮你把编辑器部分半天内落地。它的短板也明显不是完整的 IDE 内核代码补全、诊断、断点这些都不提供需要自己接。这篇文章会从编译集成、最小窗口、高亮规则定制到避坑逐个拆开给出能直接复现的做法和参数。文中所有命令和代码都以 Qt 5.15 / 6.x CMake 为基准MSVC 和 MinGW 的差异会在踩坑章节单独说明。2. 编译集成把 QCodeEditor 源码接进 Qt 工程的两条可靠路径2.1 路径一CMake add_subdirectory 源码级集成QCodeEditor 的源码结构不复杂核心就是 QCodeEditor 类、QHighlightRule、QHighlighter、QLineNumberArea、QSearchWidget 这几个文件。最常见的接入方式是直接把源码目录放进你的工程用 CMake 的 add_subdirectory 把它编译成库再让目标链接它。这种方式的好处是调试时可以直接跟进 QCodeEditor 内部坏处是你的工程里会多出一份第三方源码更新版本时需要注意。# CMakeLists.txt 片段 cmake_minimum_required(VERSION 3.16) project(MyEditorApp VERSION 0.1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 如果 Qt 不在默认路径手动指定 # set(CMAKE_PREFIX_PATH D:/Qt/5.15.2/msvc2019_64) find_package(Qt5 COMPONENTS Widgets REQUIRED) # 引入 QCodeEditor 源码目录假设第三方库放在 3rdparty 下 add_subdirectory(3rdparty/QCodeEditor) add_executable(MyEditorApp main.cpp MainWindow.cpp MainWindow.h ) target_link_libraries(MyEditorApp PRIVATE Qt5::Widgets QCodeEditor )这里的关键是 add_subdirectory 之后QCodeEditor 库会自动把 Widgets 依赖、自身头文件搜索路径一并传给 MyEditorApp你不需要再单独加 include 目录。如果编译报错提示找不到 QCodeEditor 头文件先检查这个库的 CMakeLists 里是否设置了 target_include_directories有的旧版本 fork 用的是 BUILD_INTERFACE 而不是 INSTALL_INTERFACE源码级接入时没问题但如果你想把它 install 出去复用就得改一下导出配置。另外一个常见做法是直接把 QCodeEditor 的 .cpp/.h 全部加到你的工程里不走 add_subdirectory。如果你的工程本身就是 qmake 的 .pro 文件这种直放方式反而最省事把源码文件追加进 HEADERS 和 SOURCES再链接 Qt5::Widgets 就够了。源码直放的坑是 QCodeEditor 内部用到了 QPainterPath 和 QTextObjectInterface这些都是 Widgets 模块的内容如果你只 link 了 Qt5::Core链接期会报一堆无法解析的外部符号。2.2 路径二CMake 命令行构建与编译器套件匹配无论你用 Qt Creator 还是 VS Code本质都在调 CMake。构建 QCodeEditor 本身不需要额外配置但嵌入你的工程后常见翻车点是 Qt 套件和编译器版本对不上。我用 Qt 5.15.2 配 MSVC2019 构建过也用过 Qt 6.2 配 MinGW 11两条线都稳定但有一点必须强调MSVC 套件只能用 MSVC 编译器MinGW 套件只能用 MinGW 编译器混搭时 Qt 的元对象编译和二进制兼容会直接崩给你看。# 命令行构建示例Windows MSVC2019 Qt 5.15.2 mkdir build cd build cmake .. -G Visual Studio 16 2019 -A x64 ^ -DCMAKE_PREFIX_PATHD:/Qt/5.15.2/msvc2019_64 cmake --build . --config Release参数说明-G指定生成器这里用 VS2019-A x64指定目标架构如果你是 32 位插件进程改成 Win32关键参数是CMAKE_PREFIX_PATH它告诉 CMake 去哪找 Qt 的 CMake 配置文件Qt5Config.cmake 或 Qt6Config.cmake。这个路径一旦写错find_package(Qt5)就会失败报错信息是“Could not find a package configuration file”此时要去确认你的 Qt 安装目录里是否有lib/cmake/Qt5这个子目录。在 Windows 上装 Qt 时如果你嫌 Qt 官方下载器慢可以用清华镜像源这个做法在社区里很普遍下载器里设置镜像地址后下载速度会快很多。另外注意 Qt 5.15.2 是最后一个支持 Win7 的版本如果你的目标机器还在 Win7 上跑就不要用 Qt 6反过来Qt 6 对 C17 是硬性要求你的代码里不要再出现 C14 的写法。2.3 集成后的最小验证头文件能过、符号能链接集成完成后先别急着写界面做一个最小编译验证。新建一个 main.cpp只保留一个能创建 QCodeEditor 对象的main函数确认这个库能编译、链接、运行。#include QApplication #include QCodeEditor int main(int argc, char *argv[]) { QApplication app(argc, argv); QCodeEditor editor; editor.setPlainText(int x 42;); editor.resize(640, 480); editor.show(); return app.exec(); }这段代码能跑起来说明你的工程和 QCodeEditor 之间的目录、链接、Qt 模块三样东西都通顺。注意这里用setPlainText而不是setText因为 QCodeEditor 继承自 QPlainTextEditsetText不存在老写 QTextEdit 的人在这个地方会浪费十秒钟。运行后你会看到一个带行号、默认有高亮规则的编辑器窗口默认高亮规则覆盖 C 关键字这算是一个内置的冒烟验证。如果这一步失败了不要急着改代码先看 CMake 缓存把build/CMakeCache.txt里的CMAKE_PREFIX_PATH和CMAKE_CXX_COMPILER两个变量打出来核对百分之八十的集成问题都出在这两个变量上而不是 QCodeEditor 自己。3. 最小可跑窗口五步搭出带打开、保存、行号的编辑器核心3.1 主窗口布局QCodeEditor 放进 QMainWindow 的正确姿势QCodeEditor 是普通 QWidget 子类放进 QMainWindow 的setCentralWidget就行不用包 ScrollArea也不用手动管理布局。行号区是它内部自动绘制的不需要你自己再画。这里要把 tab 键行为和缩进语义设置好不然代码编辑体验很差。// MainWindow.h #pragma once #include QMainWindow #include QCodeEditor class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent nullptr); private slots: void openFile(); void saveFile(); private: QCodeEditor *m_editor; };// MainWindow.cpp #include MainWindow.h #include QMenuBar #include QToolBar #include QFileDialog #include QFile #include QTextStream #include QMessageBox #include QStandardPaths MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { m_editor new QCodeEditor(this); setCentralWidget(m_editor); // 打开、保存动作 QAction *actOpen new QAction(tr(打开), this); QAction *actSave new QAction(tr(保存), this); connect(actOpen, QAction::triggered, this, MainWindow::openFile); connect(actSave, QAction::triggered, this, MainWindow::saveFile); QToolBar *toolbar addToolBar(tr(文件)); toolbar-addAction(actOpen); toolbar-addAction(actSave); }这段代码里我故意把菜单和工具栏精简了核心是你需要理解 QCodeEditor 作为 centralWidget 时它的尺寸策略是优先填满窗口而且它内部的行号区宽度会根据数字位数自动调整不需要你做额外计算。addToolBar返回的 QToolBar 可以直接往里插 QAction这是 Qt 里最省代码的工具栏写法。3.2 文件读写编码处理和换行符的两个小坑文件读写看起来是 trivial 的但 Qt 的QTextStream默认编码是 UTF-8在 Windows 上如果你读到一个 GBK 编码的旧脚本会乱码。做法是读取时先检查 BOM再看内容是否合法 UTF-8最后回退到本地编码。QCodeEditor 本质是 QPlainTextEdittoPlainText()拿到的永远是 QString内部无编码概念所以编码转换必须在文件边界做掉。void MainWindow::openFile() { const QString path QFileDialog::getOpenFileName(this, tr(打开文件)); if (path.isEmpty()) return; QFile file(path); if (!file.open(QIODevice::ReadOnly)) { QMessageBox::warning(this, tr(错误), tr(无法打开文件: %1).arg(file.errorString())); return; } QByteArray raw file.readAll(); file.close(); // 编码判定优先 UTF-8 BOM其次合法 UTF-8最后本地编码 QString content; if (raw.startsWith(\xEF\xBB\xBF)) { content QString::fromUtf8(raw.mid(3)); } else { QTextCodec *codec QTextCodec::codecForName(UTF-8); if (codec codec-canEncode(QString::fromUtf8(raw))) { content QString::fromUtf8(raw); } else { content QString::fromLocal8Bit(raw); } } m_editor-setPlainText(content); // 这个属性让编辑器记住当前文件路径保存时不需要再次弹对话框 m_editor-setProperty(filePath, path); setWindowTitle(path); }注意几个参数QString::fromUtf8(raw.mid(3))去掉 BOM 再转避免行首出现不可见字符QTextCodec::canEncode不是完美的 UTF-8 检测手段但对付绝大多数场景够用了。如果你在处理日志文件时发现中文仍然乱码那么十有八九文件是 GB18030 编码此时直接把codecForName(UTF-8)换成QTextCodec::codecForName(GB18030)即可但这种硬编码不推荐更好的做法是把编码选择做成记忆式下拉框。3.3 核心交互设置tab 宽度、自动缩进和行尾模式QCodeEditor 本身提供了几个值得一调的公共接口。setTabReplaceSize控制 tab 替换成几个空格setAutoIndentation开关自动缩进setLineNumbersVisible控制行号区显隐。这三个参数基本决定了一个编辑器“像不像编辑器”。// 推荐参数 m_editor-setTabReplaceSize(4); // tab 替换为 4 个空格 m_editor-setAutoIndentation(true); // 回车后自动对齐上一行缩进 m_editor-setLineNumbersVisible(true); // 显示行号 // 括号高亮是 QCodeEditor 的特色之一建议打开 // 它内部用 QTextCursor 的 anchor 和 position 来做括号配对开销很小 QFont font(Cascadia Code, 11); font.setStyleHint(QFont::Monospace); m_editor-setFont(font);setTabReplaceSize(4)之后编辑器里敲 Tab 会输入 4 个空格而不是\t这是为了配合语法高亮规则里的^\s正则匹配空格缩进在后续自定义高亮时更可控。setAutoIndentation(true)的实现原理是捕获 Key_Return 和 Key_Enter取当前行行首空白串拼到新行前面它不做智能缩进分析所以不要指望它像 IDE 那样自动补花括号层级。字体选择上Cascadia Code 或 JetBrains Mono 都好关键是setStyleHint(QFont::Monospace)否则 Windows 下会退回到无法对齐的字体行号区间距也会乱。4. 语法高亮定制QHighlightRule 规则表与 setFormat 背后的匹配逻辑4.1 QSyntaxHighlighter 的 block 回调机制为什么规则要返回 indexQCodeEditor 的高亮底层是 Qt 的 QSyntaxHighlighter。这个类的工作方式不是“对整个文档跑一遍正则”而是按行block逐个回调highlightBlock(const QString text)。QCodeEditor 在自己内部把一组 QHighlightRule 遍历执行每条规则包含一个 QRegularExpression 和一个 QTextCharFormat匹配到就调用setFormat(0, match.capturedLength(), format)。理解这个机制的第一个关键点highlightBlock只在文档变化时被调用而且只调用受影响的行及其后续行块。因此如果你的高亮规则性能差大文件编辑时会感受到输入卡顿因为每次按键可能触发多行重排。QCodeEditor 默认规则集对 C 而言性能是可接受的但如果你自己要加规则务必避免使用过于宽泛的贪婪匹配。第二个关键点是捕获组索引index。QHighlightRule 里有个index字段默认是 0意思是整个匹配都上色。如果你写正则\b(int|float|double)\bindex0表示匹配到的完整 token 上色。如果你有嵌套需求比如函数名后跟括号你可能想只给函数名上色那么 index 设为 1正则里把函数名放进第一个捕获组。{ pattern: \\b(class|struct|enum)\\b, format: { color: #569CD6 }, index: 0 }4.2 自定义高亮类仿 VS Code 深色主题的规则表下面给出一个完整可用的自定义高亮实现以 JSON 加载规则的形式组织方便你像配置主题一样调整颜色。QCodeEditor 本身支持loadSyntaxDefinition从 JSON 加载语法定义这是它相比其他小部件最省事的地方。// 自定义语法定义 my_syntax.json { rules: [ { pattern: \\b(if|else|for|while|return|break|continue)\\b, format: { color: #C586C0, fontStyle: bold }, index: 0 }, { pattern: (\\b[A-Za-z_][A-Za-z0-9_]*)\\s*\\(, format: { color: #DCDCAA }, index: 1 }, { pattern: //[^\\n]*, format: { color: #6A9955, fontStyle: italic }, index: 0 }, { pattern: \[^\\\n]*\, format: { color: #CE9178 }, index: 0 } ] }// 加载语法定义 QFile syntaxFile(:/syntax/my_syntax.json); if (syntaxFile.open(QIODevice::ReadOnly)) { m_editor-loadSyntaxDefinition(syntaxFile.readAll()); syntaxFile.close(); } else { m_editor-loadSyntaxDefinition(:/syntax/cpp.json); // 回退到内置 C }第二段代码里第一行是追加高亮断点的用法QHighlighter内部类QHighlightRule如果index设置为 0整条匹配都会应用格式。index: 1就只看第一个捕获组。有一种常见误用想高亮函数名但index没改结果整段func(...)全部变色这就是 index 没理解到位。一个额外的建议如果你需要高亮 HTML 标签内的属性名建议写\\b[A-Za-z_-]\\s*配合 index 捕获属性名而不是把整条namevalue都涂上一种颜色后者的视觉噪声非常大。语法规则表的顺序也有讲究先放关键字再放函数名再放字符串和注释。因为 QSyntaxHighlighter 的 setFormat 是后写覆盖前写字符串里的保留字如果不希望被关键字规则染色需要把字符串规则放在关键字规则前面。4.3 为什么不推荐直接修改内置 cpp.jsonQCodeEditor 内置的 cpp.json 覆盖了 C 关键字、预处理器、数字、注释等常见 token。看起来直接改这个文件最快但实际上这个文件路径在源码资源里你改了之后一旦升级库就会被覆盖。正确的做法是把自定义 JSON 作为 qrc 资源放进你的工程在代码里显式加载优先级高于内置。同时内置 cpp.json 的着色规则偏静态缺少对 C11/14 新关键字的区分比如nullptr和constexpr在部分旧版库里没有单独规则这也是为什么自定义规则表几乎是必做事项。还有一处隐藏细节是 QCodeEditor 的注释折叠。它支持代码折叠折叠标记写在注释里默认是// [fold]和// [/fold]这样的约定。如果你在公司内部做代码生成器可以用这个折叠约定把自动生成段折叠起来只露出手写区交互体验会好很多。这个功能对应的底层是 Qt 的 QTextBlockUserData 和setFoldBlockStart标记QCodeEditor 把它们封装成了简单的字符串约定。5. 避坑排查高亮不生效、折叠失效、MSVC 构建报错的 6 条血泪记录5.1 高亮全部失效但代码能编译能链接现象是编辑器光秃秃一片像普通 QPlainTextEdit既没有关键字变色也没有行号高亮。原因非常隐蔽QCodeEditor 的高亮器QHighlighter是构造时关联编辑器的但如果你在加载语法定义之前就setPlainText了大量代码QSyntaxHighlighter 的重高亮事件没有触发文本一直停留在“未高亮”状态。解决方法是重新触发一次高亮在加载语法定义完成后调用m_editor-rehighlight()。更稳的方式是调整顺序先构造编辑器再加载语法定义最后设置文本内容。我一般习惯用后者因为rehighlight()在大文件上是全量重算代价不小。如果你已经踩了这个坑需要补救用下面的代码// 重新触发高亮 editor-setSyntaxDefinition(editor-syntaxDefinition()); // 或者直接强制重画 editor-viewport()-update();5.2 自定义 JSON 加载后没有效果日志也没有报错这个坑的根源是 JSON 的格式问题。QCodeEditor 的loadSyntaxDefinition对 JSON 的解析依赖 QJsonDocument但它对color字段的合法性没有做严格校验。如果你写color: #569CD6没问题但不小心写成color: 569CD6少了#QColor 构造函数会失败这个 rule 被静默丢弃。排查方法比较土但有效把 JSON 单独复制到一个 Qt 工程里用QJsonDocument::fromJson解析一遍再检查每条 rule 里的QColor(colorString).isValid()。我遇到过三次这种问题都是复制粘贴时丢了#。另一个常见错误是fontStyle字段写了bold italic而 QCodeEditor 解析时只认单个值两个样式只生效一个参考源码里的写法应该用bold或italic分开写。5.3 MSVC 构建报:-1: error: dependent ..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\qtwidgets\...头文件找不到这个报错在热搜词里出现过是 Windows 上 Qt 工程最经典的路径依赖错误。现象是 CMake 配置成功但编译到 QCodeEditor 的某个 .cpp 文件时编译器报错说找不到 Qt 的qtwidgets相关头文件。原因有且只有一个你的工程里某个 CMakeLists 用绝对路径引用了 Qt 头文件或者编译器的 Include 目录被手动改过导致 cl.exe 用相对路径回退时爆炸。解决方法是不要手动在 Qt Creator 的构建设置里添加INCLUDEPATH让 CMake 的target_link_libraries自动传递。检查build目录下的CMakeCache.txt里QT_HEADERS_DIR是否存在以及CMAKE_PREFIX_PATH是否正确指向 Qt 安装根目录。网上有人用INCLUDE环境变量配 Qt 头文件那个方案容易造成多版本 Qt 冲突不推荐。另外如果你用 VS2022 打开 CMake 工程首次构建会触发 Qt 的 Automatic MOC如果你的 VS 插件版本较旧也会报类似的误报错误升级 Qt VS Tools 即可解决。5.4 QSearchWidget 在关闭窗口时崩溃QCodeEditor 内置了查找替换组件 QSearchWidget但它的父对象指针处理有个小坑。如果你在代码里强制searchWidget-deleteLater()或者让 QCodeEditor 先于 SearchWidget 析构程序退出时会 double-free。做法是不要手动管理搜索框生命周期让 QCodeEditor 自己销毁。如果你确实需要提前移除搜索框要先调用editor-removeSearchWidget()再 delete顺序不能反。这个坑在 Qt 5.15 和 Qt 6.2 上都出现过源码里用的是setParent但没有显式deleteLater堆对象析构顺序是未定义的。5.5 中文注释高亮变成乱码方块这个问题绝大多数出现在 Windows MSVC 组合下。原因是源码文件和 JSON 资源文件的编码不是 UTF-8MSVC 在编译字符串字面量时按本地代码页GBK解释导致中文字符串写进源码后变成了乱码进而显示为方块。解决措施是让所有源文件和资源文件统一为 UTF-8。我的习惯是在每个 .cpp 文件头部加#pragma execution_character_set(utf-8)这是只影响 MSVC 的前置指令不影响 GCC/Clang。注意 JSON 资源文件本身也必须是 UTF-8且在 Qt 的 .qrc 文件里不要标注错误的 codepage。另外 Qt 6 默认 UTF-8 源码这个问题会少很多。5.6 大文件打开卡顿输入延迟明显现象是打开 5MB 以上的文件时编辑器长时间无响应打开后输入字符有明显延迟。QPlainTextEdit 本身对大文件有块缓存机制但 QSyntaxHighlighter 默认是逐行全量解析5MB 的代码文件意味着几千行都要跑一遍正则成本很高。QCodeEditor 没有内置分片处理机制常见做法是关闭高亮后加载等用户完成编辑再手动重新高亮或者只对前 N 行应用高亮日志查看器场景。具体到参数setLineNumbersVisible(false)能省掉行号区绘制开销setAutoIndentation(false)能省掉缩进计算。对于日志查看这类用途很多人其实不需要语法高亮直接用 QPlainTextEdit 反而更轻松这是一个选型问题需要你对场景诚实。6. 从能跑到能用主题切换、搜索跳转与一个验证脚本的技巧QCodeEditor 的默认样式是浅色底深色字直接拿给深色 UI 的工具用会很突兀。好消息是它支持setStyleSheet连同行号区和搜索框一起换肤。这个特性经常被忽略事实上 QCodeEditor 的样式表接口比 QPlainTextEdit 更完整它可以选中QLineNumberArea的背景色、QSearchWidget的按钮样式一次性把整个编辑区变成统一风格。建议在启动时根据用户配置选择样式表然后把样式表字符串独立成一个qss资源文件方便以后调整。对于做嵌入式 HMI 工具链的开发者深色主题几乎是刚需一行setStyleSheet(codeEditorQss)能避免整个界面亮度刺眼的问题。搜索跳转方面QCodeEditor 自带 QSearchWidget但少数源码版本默认不弹出搜索框。做法是做一个快捷键触发调用editor-showSearchWidget()然后用setSearchText预置搜索文本。QSearchWidget 支持findNext和findPrevious底层用的是 QTextDocument 的find函数不是正则搜索所以搜索速度对大文件来说不错。搜索框默认是浮动在右上角的独立小窗口不会挤压编辑区。如果你需要向上翻页逐条高亮当前搜索结果QSearchWidget 只高亮当前匹配项不改写全文。如果你需要全文高亮所有匹配项得自己用QTextEdit::ExtraSelection遍历文本这是 QCodeEditor 没有封装的部分不过对大多数查找场景来说单个高亮的体验也够用了。最后说一个验证脚本的小技巧。集成完 QCodeEditor 后为了保证每次改动不破坏既有功能我会写一个基于 QTest 的冒烟测试构造一个带 C 代码的 QString设置到编辑器触发高亮然后用QTextCursor::select(QTextCursor::WordUnderCursor)遍历所有单词检查关键 token 的QTextCharFormat::foreground().color()是否与预期一致。这个脚本不用 UI用QApplication而不是QCoreApplication因为它需要字体和布局系统。跑一次大概消耗一两秒但能确保你在后续升级库或修改高亮规则时不会把大块的关键字颜色改坏。这个验证方式成本很低值得作为持续集成的一部分保留。我自己的习惯是每次做完一个基于 QCodeEditor 的工具都会把自定义的 JSON 语法文件和样式表模板整理到同一个资源目录里维护。这个方案的弹性在于语法规则是可配置的样式是可切换的编辑器本体是稳定的在遇到下一个需要编辑器的场景时只需要复制目录而不是重写代码。这几年我在多个内部工具上用过它踩过的坑就是上面那六条写出来希望帮到你。本文还有配套的精品资源点击获取
返回列表