QCodeEditor - Qt 小部件






4.81/5 (20投票s)
支持语法高亮、代码补全和行号显示的插件式代码编辑器
引言
QCodeEditor
是一个支持自动补全、语法高亮和行号显示的编辑器。它面向所有希望支持多种语言的人,这些语言范围涵盖编程语言、标记语言甚至自定义脚本语言。目前,它可以完美处理上述所有功能,但仍有许多内容有待实现。QCodeEditor
基于 Qt 的 QPlainTextEdit
,它已经提供了添加语法高亮和自动补全的接口。
Using the Code
使用编辑器本身非常简单。可以通过将 QPlainTextEdit
添加到窗体并将其提升为 kgl::QCodeEditor
(请注意,必须将 'include/KGL/Widgets' 添加到 INCLUDEPATH
变量中,并且需要选中 'global include
' 复选框) 或通过编程方式将其添加到窗体来实现。
using namespace kgl;
// ## MainWindow::MainWindow
QCodeEditor *editor = new QCodeEditor;
setCentralWidget(editor); // or: ui->someLayout->addWidget(editor);
可以使用 QCodeEditorDesign
类来更改编辑器的视觉外观。在下面的示例中,我们假设代码编辑器被多个小部件包围,因此为其添加了一个边框。我们还额外修改了外观,使其具有“暗黑”风格。
using namespace kgl;
// ## MainWindow::MainWindow
QCodeEditorDesign design;
design.setLineColumnVisible(false);
design.setEditorBackColor(0xff333333);
design.setEditorTextColor(0xffdddddd);
design.setEditorBorderColor(0xff999999);
design.setEditorBorder(QMargins(1,1,1,1)); // l t r b
editor->setDesign(design);
点击 这里 查看上述代码的实际效果。点击 这里 查看视觉属性参考。
但是,如何为代码添加语法高亮规则,就像上面图片所示的那样?有两种方法:以编程方式添加,或者从 XML 文件中提取。
以编程方式
using namespace kgl;
// ## MainWindow::MainWindow
QList<QSyntaxRule> rules;
QSyntaxRule rule;
rule.setForeColor(QColor(Qt::blue));
rule.setRegex("\\bFoo\\b");
rule.setId("Foo");
rules.push_back(rule);
editor->setRules(rules);
XML 文件
using namespace kgl;
// ## MainWindow::MainWindow
QCodeEditorDesign design;
// modify design ...
QList<QSyntaxRule> rules =
QSyntaxRules::loadFromFile(":/rule_cpp.xml", design);
editor->setRules(rules);
// Note: ':/rule_cpp.xml' refers to the path of a QRC resource
有关如何创建这些 XML 规则的指南将在下一章提供。但首先,我们的编辑器需要一些自动补全关键字。
// ## MainWindow::MainWindow
QStringList keywords = { "printf", "scanf" };
editor->setKeywords(keywords);
如果希望添加图标以指示关键字是函数/成员/宏等,则需要创建一个自定义的 QStandardItemModel
并将其传递给 'QCodeEditor::setKeywordModel(model)
'。
创建 XML 规则文件
XML 规则文件包含一个顶层的 <rules>
元素,该元素由多个 <rule>
子元素组成。每个子元素必须包含一个正则表达式或一个关键字列表,所有其他属性都是可选的。
<rules>
<rule>
<regex>\bFoo\b</regex>
<keywords>foo bar key</keywords>
</rule>
</rules>
有关所有可用属性的参考,请访问 rules_template.xml 的 GitHub 页面。QCodeEditor
甚至支持由多行组成的规则。虽然它们对于实现多行注释很有用,但它们也可以用于其他目的。
ID 的用处
如 rules_template.xml 中所示,规则甚至可以定义自定义 ID。在本节中,我将说明如何使用 ID 以及为什么它们如此有用。你可能已经注意到,静态添加关键字并不是一个好方法,尤其是当一种语言允许包含其他文件或定义变量时。
'onMatch' 信号
QCodeEditorHighlighter
一旦通过正则表达式找到一个 string
并对其进行高亮显示,就会发出 'onMatch
' 信号。这使我们能够检索到相关的 string
。
// ## MainWindow.h
using namespace kgl;
class MainWindow : public QMainWindow {
Q_OBJECT
public:
...
private slots:
void addMacro(const QSyntaxRule &rule, QString seq, QTextBlock block);
private:
QMap<QTextBlock, QSyntaxRule> m_RuleMap;
QMap<QTextBlock, QString> m_MacroMap;
QCodeEditor *m_Editor;
};
// ## MainWindow::MainWindow
QSyntaxRule defineRule;
defineRule.setRegex("(#define\\s+)\\K(\\D\\w*)(?=\s+\S+)");
defineRule.setId("define");
editor->setRules({ defineRule });
connect(m_Editor->highlighter(), SIGNAL(onMatch(QSyntaxRule,QString,QTextBlock)),
this, SLOT(addMacro(QSyntaxRule,QString,QTextBlock)));
// ## MainWindow::addMacro
if (rule.id() == "define") {
foreach (const QTextBlock &b, m_RuleMap.keys()) {
if (b.userData() != NULL && block.userData() != NULL) {
auto *d1 = static_cast<QCodeEditorBlockData *>(block.userData());
auto *d2 = static_cast<QCodeEditorBlockData *>(b.userData());
if (d1->id == d2->id) {
return;
}
}
}
// Not existing yet; add it
QString def = seq.split(' ').at(0);
m_RuleMap.insert(block, rule);
m_MacroMap.insert(block, def);
m_Editor->addKeyword(def);
}
这样,就可以为自定义类、变量和定义提供自动补全,或者包含其他文件并从它们导入符号。
'onRemove' 信号
移除曾经添加的宏可能会有些棘手,因为 QTextBlock 的设计几乎没有给我们追踪它的可能性。QCodeEditor
Highlighter
提供了 'onRemove
' 信号,当高亮器检测到之前匹配的规则不再匹配时,就会发出此信号。
// ## MainWindow.h
private slots:
void addMacro(const QSyntaxRule &rule, QString seq, QTextBlock block);
void removeMacro(QCodeEditorBlockData *data); // Add this to the slots
// ## MainWindow::MainWindow
// Add another connection
connect(m_Editor->highlighter(), SIGNAL(onRemove(QCodeEditorBlockData*)),
this, SLOT(removeMacro(QCodeEditorBlockData*)));
// ## MainWindow::removeMacro
foreach (const QTextBlock &b, m_RuleMap.keys()) {
if (b.userData()) {
auto *d = static_cast<QCodeEditorBlockData *>(b.userData());
if (d->id == data->id) {
// Data is the same; block must be the one from before!
m_Editor->removeKeyword(m_MacroMap.value(b));
m_RuleMap.remove(b);
m_MacroMap.remove(b);
}
}
}
实现这样一个相对简单的信号是一项艰巨的任务。有关更多信息,请查看“要点”章节。
编译说明
要编译 QCodeEditor,你需要定义 'KGL_BUILD
' 以将符号导出到动态库。如果你想构建静态库,只需定义 'KGL_STATIC
'。另外,请确保使用 Qt 5 或更高版本。
关注点
渲染行号是一个主要的障碍。虽然将行号列添加为子小部件很容易,但确定滚动时所有可见的行号却并非如此。经过长时间阅读 Qt 文档,我发现我可以通过迭代跳转到下一行,并在当前行不再可见时停止迭代。
另一个主要障碍是实现当按下制表符键时多行缩进。我通过使用非常棒的 'QTextCursor::movePosition
' 方法解决了这个问题,它使得实现此功能(以及反向缩进)变得很容易。
QTextBlock
尽管 QTextBlock 具有出色的功能和可能性,但它仍然有一个弱点:我们几乎不可能在文本小部件中跟踪 QTextBlock。为了实现一个允许移除关键字的信号,我首先尝试复制相关的 QTextBlock,然后使用重载的 '==' 运算符检查相等性。这不起作用,因为行号可能会发生变化并导致相等性检查失败。唯一追踪 QTextBlock 的方法是使用 'setUserData
' 函数为其分配一个 QTextBlockUserData。为了实现这一点,我继承了 QTextBlockUserData 并将其中的 uuid
和 regex string
存储起来。uuid(+一些 do-while 循环)确保该块在整个应用程序中保持唯一。通过这些措施,'onRemove
' 信号最终变得可靠且无 bug。
更新 10 月 26 日
我犯的一个严重错误导致所有使用 Microsoft Visual C++ 编译器的人都遇到了大量的编译器错误和警告。我没有指出必须指定 KGL_BUILD
来导出库的符号,这是我的错误。另一个错误导致所有 Windows 用户(无论使用 MSVC 还是 GCC 编译)都定义了 __declspec(dllimport)
。GCC 忽略了这一点,只是默默地不创建动态符号,但 MSVC 却对此进行了处理。请参阅 编译说明 部分,以正确编译 QCodeEditor。对于由此带来的任何不便,我深表歉意!
更新 10 月 29 日
在上一版本中,移除曾经添加的关键字非常麻烦且复杂。通过新引入的 onRemove
信号,现在应该很容易了。有关完整的演示,请下载本文开头的 QCodeEditor_-_Example.zip
。它能够添加和删除通过 C/C++ 的 #define
定义的宏。
待办事项
QCodeEditorDesign
的 XML 文件解析QCodeEditor::addKeyword
&QCodeEditor::removeKeyword
&QCodeEditor::keywordExists
onMatch
信号伴随当前行号发出- 实时代码验证器的接口
- 其他相关小部件(搜索和替换、快捷键映射等)
非常欢迎提出建议。
历史
- 2016 年 10 月 16 日:
QCodeEditor
首发 - 2016 年 10 月 26 日:修复各种编译错误
- 2016 年 10 月 29 日:编辑器设计的 XML 文件,动态关键字添加/删除 & 增强的
onMatch
/onRemove
信号