Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / QT

QCodeEditor - Widget for Qt

4.81/5 (21 votes)
29 Oct 2016LGPL35 min read 48.1K   2.7K  
Extensible code editor with syntax highlighting, code completion and line numbering

Image 1

Introduction

QCodeEditor is a code editor supporting auto completion, syntax highlighting and line numbering. It is directed to all people who want to support a wide set of languages, ranging from programming languages to markup languages and even custom scripting languages. In its current state, it can perfectly handle all the features mentioned above, but there are still many things to be implemented. QCodeEditor is based on Qt's QPlainTextEdit that already contains an interface for adding syntax highlighting and auto completion.

Using the Code

Using the editor itself is quite simple. It can either be done by adding a QPlainTextEdit to the form and promoting it to kgl::QCodeEditor (note that 'include/KGL/Widgets' has to be added to the INCLUDEPATH variable and that the 'global include' checkbox has to be checked) or by adding it programmatically to the form:

C++
using namespace kgl;

// ## MainWindow::MainWindow
QCodeEditor *editor = new QCodeEditor;
setCentralWidget(editor); // or: ui->someLayout->addWidget(editor);

There are many possibilities to change the visual appearance of the editor by using the QCodeEditorDesign class. In the following example, we assume that the code editor is surrounded by multiple widgets and therefore add a border to it. We additionally modify the appearance to have a 'dark' style:

C++
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);

Click here to see the above code in action. Click here for a visual property reference.

But how to actually add some syntax highlighting rules, as shown in the picture above? There are two ways to do it: To add them programmatically or to extract them out of an XML file.

Programmatically:

C++
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 file:

C++
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

A guide of how to create these XML rules will be provided in the next chapter. But first, our editor requires some auto completion keywords:

C++
// ## MainWindow::MainWindow
QStringList keywords = { "printf", "scanf" };
editor->setKeywords(keywords);

If one wishes to add icons in order to imply that a keyword is a function/member/macro/... a custom QStandardItemModel needs to be created and passed to 'QCodeEditor::setKeywordModel(model)'.

Creating the XML Rules File

The XML rules file contains a topmost <rules> element that consists of multiple <rule> child elements. Each of the child element has to contain either a regular expression or a list of keywords, all other properties are optional:

XML
<rules>
    <rule>
        <regex>\bFoo\b</regex>
        <keywords>foo bar key</keywords>
    </rule>
</rules>

For a reference of all the available properties, go to the github page of the rules_template.xml. QCodeEditor even supports rules that consist of more than just one line. While they are useful for implementing multi-line comments, they can serve other purposes, too.

The Usefulness of IDs

As can be seen in the rules_template.xml, rules can even define a custom ID. In this section, I will illustrate how to use IDs and why they are so useful. One might have noticed that adding keywords statically is not a nice practice, especially if you have a language that allows one to include other files or define variables.

The 'onMatch' Signal

QCodeEditorHighlighter emits a signal called 'onMatch' as soon as a string - found via regex - was highlighted. This allows us to retrieve the string in question:

C++
// ## 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);
}

This way, one can provide auto-completion for custom classes, variables and definitions or include other files and import symbols from them.

The 'onRemove' Signal

Removing a once added macro can be a bit tricky because the design of the QTextBlock gives us little to no possibility to track it. QCodeEditorHighlighter provides the 'onRemove' signal, that is emitted once the highlighter detects that a previously matched rule does not match anymore:

C++
// ## 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);
        }
    }
}

Realising such a signal that is relatively simple to use was a hard task. Check out the 'Points of Interest' chapter for more information.

Compile instructions

In order to compile QCodeEditor, you need to define 'KGL_BUILD' to export symbols to a dynamic library. If you want to build a static library instead, simply define 'KGL_STATIC'. Also make sure you use a Qt version that is 5 or higher.

Points of Interest

One of the big obstacles was rendering the line numbers correctly. While adding the line column as child widget was quite easy, determining all the visible line numbers on scrolling was not. After reading the Qt documentation for a good amount of time, I figured I could just jump to the next line in an iteration and stop the iteration as soon as the current line is not visible anymore.

Another big obstacle was implementing indentation of multiple selected lines when the tabulator key is pressed. I solved it by using the truly amazing 'QTextCursor::movePosition' method which made implementing this feature (and back-indentation) an easy job.

QTextBlock

Despite of QTextBlock's amazing features and possibilities, it still has one weakness: It is for us practically impossible to track a QTextBlock within a text widget. In order to implement a signal that lets one remove keywords, I first tried to copy the QTextBlock in question and later on check for equality with the overloaded '==' operator. That did not work because the line numbers can change and cause the equation to fail. The only possibility to track down the QTextBlock was assigning it a QTextBlockUserData using the 'setUserData' function. To achieve that, I subclassed QTextBlockUserData and stored a uuid and the regex string in it. The uuid (+ some do-while loop) ensures that the block really stays unique across the entire application. With these measurements, the 'onRemove' signal finally was reliable and bug-free.

Update 26th of October

A horrendous carelessness of mine caused everyone using the Microsoft Visual C++ compiler to have tons of compiler errors and warnings. It was my mistake to not point out that KGL_BUILD must be specified in order to export the library's symbols. Another mistake caused all Windows users (regardless of compiling with MSVC or GCC) to define __declspec(dllimport). GCC ignored this and just quietly didn't create dynamic symbols but MSVC on the other hand cared about that. Take a look at the Compile Instructions section to now properly compile QCodeEditor. I am sorry for any inconveniences!

Update 29th of October

In the last version, removing once added keywords was hacky and complicated. With the newly introduced onRemove signal, it should now be easy. For a complete demo, download the QCodeEditor_-_Example.zip at the beginning of this article. It is capable of adding and removing macroes defined via C/C++'s #define.

TODO

  • XML file parsing for QCodeEditorDesign
  • QCodeEditor::addKeyword & QCodeEditor::removeKeyword & QCodeEditor::keywordExists
  • onMatch signal emitted along with current line number
  • Interface for a real-time code validator
  • Other related widgets (search&replace, hotkey mapping, ...)

Suggestions are highly appreciated.

History

  • October 16th, 2016: First release of QCodeEditor
  • October 26th, 2016: Fix various compilation bugs
  • October 29th, 2016: XML file for editor design, dynamic keyword addition/deletion & enhanced onMatch/onRemove signals

 

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)