How to highlight multiline text on a QTextEdit?
-
I'm trying to highlight multiline comments on a
QTextEdit
, I searched around for how this is done on Qt Designer (in theQTextEdit
when you are editing astylesheet
multiline comments are highlighted)I found a class named
HtmlHighlighter
atmsvc2019_64\include\QtGui\qsyntaxhighlighter.h
(not sure if this is the correct class used by Designer)when I set it on the first
QTextEdit
of theQMainWindow
nothing is highlighted, I'm missing something?Based on the
HtmlHighlighter
class, I have written theMyHtmlHighlighter
class, the problem is it only highlights comments that are on the same line:How i could get multiline comments highlighted in a
QTextEdit
?#include "htmlhighlighter.h" int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); QList<QTextEdit*> textEditList = w.findChildren<QTextEdit*>(); HtmlHighlighter* highlighter = new HtmlHighlighter(textEditList[0]); MyHtmlHighlighter* highlighter_2 = new MyHtmlHighlighter(textEditList[1]); return a.exec(); }
mainwindow
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow()) { ui->setupUi(this); QString text = QString(R"(/*abc*/ /* abc */ )"); QTextEdit* textEdit = new QTextEdit(this); QTextEdit* textEdit_2 = new QTextEdit(this); QVBoxLayout* layout = new QVBoxLayout(ui->centralWidget); layout->addWidget(textEdit); layout->addWidget(textEdit_2); textEdit->setText(text); textEdit_2->setText(text); }
htmlhighlighter
class MyHtmlHighlighter : public QSyntaxHighlighter { public: MyHtmlHighlighter(QWidget* parent) : QSyntaxHighlighter(parent) { commentFormat.setForeground(QColor("#29ff3d")); commentFormat.setFontItalic(true); highlightingRules.append({QRegularExpression("/\\*(.|\\n)*?\\*/"), commentFormat}); }; protected: void highlightBlock(const QString& text) override { for (const HighlightingRule& rule : highlightingRules) { QRegularExpressionMatchIterator matchIterator = rule.pattern.globalMatch(text); while (matchIterator.hasNext()) { QRegularExpressionMatch match = matchIterator.next(); setFormat(match.capturedStart(), match.capturedLength(), rule.format); } } } private: struct HighlightingRule { QRegularExpression pattern; QTextCharFormat format; }; QList<HighlightingRule> highlightingRules; QTextCharFormat commentFormat; }; // extracted from: msvc2019_64\include\QtGui\qsyntaxhighlighter.h using namespace Qt::StringLiterals; class HtmlHighlighter : public QSyntaxHighlighter { Q_OBJECT public: enum Construct { Entity, Tag, Comment, Attribute, Value, LastConstruct = Value }; HtmlHighlighter(QTextEdit* textEdit) : QSyntaxHighlighter(textEdit->document()) { QTextCharFormat entityFormat; entityFormat.setForeground(Qt::red); setFormatFor(Entity, entityFormat); QTextCharFormat tagFormat; tagFormat.setForeground(Qt::darkMagenta); tagFormat.setFontWeight(QFont::Bold); setFormatFor(Tag, tagFormat); QTextCharFormat commentFormat; commentFormat.setForeground(Qt::gray); commentFormat.setFontItalic(true); setFormatFor(Comment, commentFormat); QTextCharFormat attributeFormat; attributeFormat.setForeground(Qt::black); attributeFormat.setFontWeight(QFont::Bold); setFormatFor(Attribute, attributeFormat); QTextCharFormat valueFormat; valueFormat.setForeground(Qt::blue); setFormatFor(Value, valueFormat); } void setFormatFor(Construct construct, const QTextCharFormat& format) { m_formats[construct] = format; rehighlight(); } QTextCharFormat formatFor(Construct construct) const { return m_formats[construct]; } protected: enum State { NormalState = -1, InComment, InTag }; void highlightBlock(const QString& text) override { static const QChar tab = u'\t'; static const QChar space = u' '; int state = previousBlockState(); qsizetype len = text.size(); qsizetype start = 0; qsizetype pos = 0; while (pos < len) { switch (state) { case NormalState: default: while (pos < len) { QChar ch = text.at(pos); if (ch == u'<') { if (QStringView{text}.sliced(pos).startsWith("<!--"_L1)) { state = InComment; } else { state = InTag; start = pos; while (pos < len && text.at(pos) != space && text.at(pos) != u'>' && text.at(pos) != tab && !QStringView{text}.sliced(pos).startsWith("/>"_L1)) { ++pos; } if (QStringView{text}.sliced(pos).startsWith("/>"_L1)) ++pos; setFormat(start, pos - start, m_formats[Tag]); break; } break; } if (ch == u'&') { start = pos; while (pos < len && text.at(pos++) != u';') ; setFormat(start, pos - start, m_formats[Entity]); } else { // No tag, comment or entity started, continue... ++pos; } } break; case InComment: start = pos; for (; pos < len; ++pos) { if (QStringView{text}.sliced(pos).startsWith("-->"_L1)) { pos += 3; state = NormalState; break; } } setFormat(start, pos - start, m_formats[Comment]); break; case InTag: QChar quote = QChar::Null; while (pos < len) { QChar ch = text.at(pos); if (quote.isNull()) { start = pos; if (ch == '\''_L1 || ch == u'"') { quote = ch; } else if (ch == u'>') { ++pos; setFormat(start, pos - start, m_formats[Tag]); state = NormalState; break; } else if (QStringView{text}.sliced(pos).startsWith("/>"_L1)) { pos += 2; setFormat(start, pos - start, m_formats[Tag]); state = NormalState; break; } else if (ch != space && text.at(pos) != tab) { // Tag not ending, not a quote and no whitespace, so // we must be dealing with an attribute. ++pos; while (pos < len && text.at(pos) != space && text.at(pos) != tab && text.at(pos) != u'=') ++pos; setFormat(start, pos - start, m_formats[Attribute]); start = pos; } } else if (ch == quote) { quote = QChar::Null; // Anything quoted is a value setFormat(start, pos - start, m_formats[Value]); } ++pos; } break; } } setCurrentBlockState(state); } private: QTextCharFormat m_formats[LastConstruct + 1]; };
-
@SGaist do you mean in the QRegularExpression? i tried it, same result.
-edit-
I noticed that the text is received "splitted" in the thehighlightBlock
function, when its/* abc */
text
is/*
thenabc
then*/
, this is why the regex is working only when its all on the same line.I don't understand why the
HtmlHighlighter
class copied from the mentioned file isn't working. -
I realized, you are over complicating things, take a look at the QSyntaxHighlighter documentation. It provides an example for doing exactly what you want.
-
For the sake of availability and saving someone's time, I'll share my solution again here.
There's a very good and detailed Syntax Highlighter Example in the Qt Documentation, with a section that explains how to deal with multiline comments explained in Highlighter class implementation section.
Here's my own extraction of it:
the multiline comment needs special care due to the design of the QSyntaxHighlighter class.
After a QSyntaxHighlighter object is created, its highlightBlock() function will be called automatically whenever it is necessary by the rich text engine, highlighting the given text block. The problem appears when a comment spans several text blocks. We will take a closer look at how this problem can be solved when reviewing the implementation of the Highlighter::highlightBlock() function. At this point we only specify the multiline comment's color.
setCurrentBlockState(0);
To deal with constructs that can span several text blocks (like the C++ multiline comment), it is necessary to know the end state of the previous text block (e.g. "in comment"). Inside your highlightBlock() implementation you can query the end state of the previous text block using the QSyntaxHighlighter::previousBlockState() function. After parsing the block you can save the last state using QSyntaxHighlighter::setCurrentBlockState().
The previousBlockState() function return an int value. If no state is set, the returned value is -1. You can designate any other value to identify any given state using the setCurrentBlockState() function. Once the state is set, the QTextBlock keeps that value until it is set again or until the corresponding paragraph of text is deleted.
In this example we have chosen to use 0 to represent the "not in comment" state, and 1 for the "in comment" state. When the stored syntax highlighting rules are applied we initialize the current block state to 0.
int startIndex = 0; if (previousBlockState() != 1) startIndex = text.indexOf(commentStartExpression);
If the previous block state was "in comment" (previousBlockState() == 1), we start the search for an end expression at the beginning of the text block. If the previousBlockState() returns 0, we start the search at the location of the first occurrence of a start expression.
while (startIndex >= 0) { QRegularExpressionMatch match = commentEndExpression.match(text, startIndex); int endIndex = match.capturedStart(); int commentLength = 0; if (endIndex == -1) { setCurrentBlockState(1); commentLength = text.length() - startIndex; } else { commentLength = endIndex - startIndex + match.capturedLength(); } setFormat(startIndex, commentLength, multiLineCommentFormat); startIndex = text.indexOf(commentStartExpression, startIndex + commentLength); }
When an end expression is found, we calculate the length of the comment and apply the multiline comment format. Then we search for the next occurrence of the start expression and repeat the process. If no end expression can be found in the current text block we set the current block state to 1, i.e. "in comment".
TL;DR: QSyntaxHighlighter only formats one text block at a time, a multiline comment is formed of several ones, hence why you are only able to format single line comments.
Based on that, I changed
MyHtmlHighlighter
as follows:class MyHtmlHighlighter : public QSyntaxHighlighter { public: MyHtmlHighlighter(QWidget* parent) : QSyntaxHighlighter(parent) { commentFormat.setForeground(QColor("#29ff3d")); commentFormat.setFontItalic(true); commentStartExpression = QRegularExpression(QStringLiteral("/\\*")); commentEndExpression = QRegularExpression(QStringLiteral("\\*/")); } protected: void highlightBlock(const QString& text) override { //useless for multiline comment formatting for (const HighlightingRule& rule : highlightingRules) { QRegularExpressionMatchIterator matchIterator = rule.pattern.globalMatch(text); while (matchIterator.hasNext()) { QRegularExpressionMatch match = matchIterator.next(); setFormat(match.capturedStart(), match.capturedLength(), rule.format); } } //this is where multiline comments processing starts setCurrentBlockState(0); int startIndex = 0; if (previousBlockState() != 1) startIndex = text.indexOf(commentStartExpression); while (startIndex >= 0) { QRegularExpressionMatch match = commentEndExpression.match(text, startIndex); int endIndex = match.capturedStart(); int commentLength = 0; if (endIndex == -1) { setCurrentBlockState(1); commentLength = text.length() - startIndex; } else { commentLength = endIndex - startIndex + match.capturedLength(); } setFormat(startIndex, commentLength, commentFormat); startIndex = text.indexOf(commentStartExpression, startIndex + commentLength); } } private: struct HighlightingRule { QRegularExpression pattern; QTextCharFormat format; }; QList<HighlightingRule> highlightingRules; QTextCharFormat commentFormat; //added QRegularExpression commentStartExpression; QRegularExpression commentEndExpression; };
Here's a test: