Qt5 Syntax Highlighting in QML
Qt Quick's TextEdit
item exposes a textDocument
property, of type QQuickTextDocument
. This is explicitly exposed so you can use QSyntaxHighlighter
directly with the document.
QtQuick textEdit documentation for Qt 5.3
I have two answers:
- a pure QML answer
- a C++ answer involving QSyntaxHighlighter
For a pure QML answer, we can use a TextArea one can set TextArea::textFormat to textFormat: TextEdit.RichText
for formatting. We can use TextArea::getText() to get the plain text and set TextArea::text with the rich text. Here's a mock example that:
- color uppercase identifiers (e.g. Button) as purple
- color lowercase identifiers (e.g. x y z) as red
- color numbers (e.g. 123 456) as blue
- but leave symbols (e.g. = + ;) as black
Here's the snippet:
TextArea {
id: output
property bool processing: false
text: "<p>x = 123;</p><p>y = 456;</p><p>z = x + y;</p>"
textFormat: TextEdit.RichText
selectByMouse: true
onTextChanged: {
if (!processing) {
processing = true;
let p = cursorPosition;
let markUp = getText(0, length).replace(
/([A-Z][A-Za-z]*|[a-z][A-Za-z]*|[0-9]+|[ \t\n]|['][^']*[']|[^A-Za-z0-9\t\n ])/g,
function (f) {
console.log("f: ", JSON.stringify(f));
if (f.match(/^[A-Z][A-Za-z]*$/))
return "<span style='color:#800080'>" + f + "</span>";
if (f.match(/^[a-z][A-Za-z]*$/))
return "<span style='color:#800000'>" + f + "</span>";
else if (f.match(/^[0-9]+$/))
return "<span style='color:#0000ff'>" + f + "</span>";
else if (f.match(/^[ ]/))
return " "
else if (f.match(/^[\t\n]/))
return f;
else if (f.match(/^[']/))
return "<span style='color:#008000'>" + f + "</span>";
else
return f;
}
);
text = markUp;
cursorPosition = p;
processing = false;
}
}
}
To use Qt's QSyntaxHighlighter, you need the following:
- In QML, use TextEdit QML type in your application
- In C++, define a QSyntaxHighlighter and connect TextEdit QML type to it via the TextEdit::textDocument property
- In C++, implement QSyntaxHighlighter::highlightBlock( const QString& text ) in your derived class, calling QSyntaxHighlighter::setFormat() as often as needed to tokenize the text found.
To make things easier, I create a sample app https://github.com/stephenquan/QtSyntaxHighlighterApp which wraps QSyntaxHighlighter and QTextFormat as SyntaxHighlighter
and TextFormat
QML Types. That way, one can handle onHighlightBlock signal and put the business logic of the syntax highlighter in Javascript instead of C++:
TextEdit {
id: textEdit
selectByMouse: true
text: [
"import QtQuick 2.12",
"",
"Item {",
" Rectangle {",
" width: 50",
" height: 50",
" color: '#800000'",
" }",
"}",
].join("\n") + "\n"
font.pointSize: 12
}
SyntaxHighlighter {
id: syntaxHighlighter
textDocument: textEdit.textDocument
onHighlightBlock: {
let rx = /\/\/.*|[A-Za-z.]+(\s*:)?|\d+(.\d*)?|'[^']*?'|"[^"]*?"/g;
let m;
while ( ( m = rx.exec(text) ) !== null ) {
if (m[0].match(/^\/\/.*/)) {
setFormat(m.index, m[0].length, commentFormat);
continue;
}
if (m[0].match(/^[a-z][A-Za-z.]*\s*:/)) {
setFormat(m.index, m[0].match(/^[a-z][A-Za-z.]*/)[0].length, propertyFormat);
continue;
}
if (m[0].match(/^[a-z]/)) {
let keywords = [ 'import', 'function', 'bool', 'var',
'int', 'string', 'let', 'const', 'property',
'if', 'continue', 'for', 'break', 'while',
];
if (keywords.includes(m[0])) {
setFormat(m.index, m[0].length, keywordFormat);
continue;
}
continue;
}
if (m[0].match(/^[A-Z]/)) {
setFormat(m.index, m[0].length, componentFormat);
continue;
}
if (m[0].match(/^\d/)) {
setFormat(m.index, m[0].length, numberFormat);
continue;
}
if (m[0].match(/^'/)) {
setFormat(m.index, m[0].length, stringFormat);
continue;
}
if (m[0].match(/^"/)) {
setFormat(m.index, m[0].length, stringFormat);
continue;
}
}
}
}
TextCharFormat { id: keywordFormat; foreground: "#808000" }
TextCharFormat { id: componentFormat; foreground: "#aa00aa"; font.pointSize: 12; font.bold: true; font.italic: true }
TextCharFormat { id: numberFormat; foreground: "#0055af" }
TextCharFormat { id: propertyFormat; foreground: "#800000" }
TextCharFormat { id: stringFormat; foreground: "green" }
TextCharFormat { id: commentFormat; foreground: "green" }