65.9K
CodeProject 正在变化。 阅读更多。
Home

QCodeEditor - Qt 小部件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (20投票s)

2016 年 10 月 16 日

LGPL3

5分钟阅读

viewsIcon

49173

downloadIcon

2799

支持语法高亮、代码补全和行号显示的插件式代码编辑器

引言

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 的设计几乎没有给我们追踪它的可能性。QCodeEditorHighlighter 提供了 '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 并将其中的 uuidregex 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 信号

 

© . All rights reserved.