Playing around with QHighlighter

published at 09.10.2024 16:52 by Jens Weller
Save to Instapaper Pocket

Thinking about conference t-shirt design for Meeting C++ 2024, I started to play around with the text highlighting abilities in Qt. I've started with the QHighlighter example.

The example is a great introduction, and pretty much what I need for prototyping. I wonder if there is a better way then using regular expressions, but for now I've stuck to the highlighter class from the example. My use case does not require high performance, but using a boyer moore horspool searcher might work very well for keywords.

But I couldn't resist to extend the highlighter class to cover more keywords:

keywordFormat.setForeground(Qt::darkBlue);
keywordFormat.setFontWeight(QFont::Bold);
const QString keywordPatterns[] = {
    QStringLiteral("\\bauto\\b"),QStringLiteral("\\bbool\\b"),
    QStringLiteral("\\bchar\\b"), QStringLiteral("\\bclass\\b"), QStringLiteral("\\bconst\\b"),QStringLiteral("\\bconstexpr\\b"),QStringLiteral("\\bconcept\\b"),QStringLiteral("\\bco_await\\b"),QStringLiteral("\\bco_return\\b"),QStringLiteral("\\bconsteval\\b"),
    QStringLiteral("\\bdelete\\b"), QStringLiteral("\\bdouble\\b"),QStringLiteral("\\bdo\\b"),
    QStringLiteral("\\benum\\b"),
    QStringLiteral("\\bexplicit\\b"),QStringLiteral("\\belse\\b"),
    QStringLiteral("\\bfriend\\b"),QStringLiteral("\\bfloat\\b"),QStringLiteral("\\bfor\\b"),
    QStringLiteral("\\binline\\b"), QStringLiteral("\\bint\\b"),QStringLiteral("\\bif\\b"),
    QStringLiteral("\\blong\\b"),
    QStringLiteral("\\bnamespace\\b"), QStringLiteral("\\bnew\\b"), QStringLiteral("\\bnullptr\\b"),
    QStringLiteral("\\boperator\\b"),QStringLiteral("\\boverride\\b"),
    QStringLiteral("\\bprivate\\b"), QStringLiteral("\\bprotected\\b"), QStringLiteral("\\bpublic\\b"),
    QStringLiteral("\\bshort\\b"), QStringLiteral("\\bsignals\\b"), QStringLiteral("\\bsigned\\b"),
    QStringLiteral("\\bslots\\b"), QStringLiteral("\\bstatic\\b"), QStringLiteral("\\bstruct\\b"),QStringLiteral("\\bshort\\b"),
    QStringLiteral("\\btemplate\\b"), QStringLiteral("\\bthis\\b"),QStringLiteral("\\btypedef\\b"), QStringLiteral("\\btypename\\b"),
    QStringLiteral("\\bunion\\b"),QStringLiteral("\\busing\\b"), QStringLiteral("\\bunsigned\\b"), QStringLiteral("\\bvirtual\\b"),
    QStringLiteral("\\bvoid\\b"), QStringLiteral("\\bvolatile\\b"),QStringLiteral("\\bwhile\\b")
};
for (const QString &pattern : keywordPatterns) {
    rule.pattern = QRegularExpression(pattern);
    rule.format = keywordFormat;
    highlightingRules.append(rule);
}

classFormat.setFontWeight(QFont::Bold);
classFormat.setForeground(Qt::darkMagenta);
rule.pattern = QRegularExpression(QStringLiteral("\\bQ[A-Za-z]+\\b"));
rule.format = classFormat;
highlightingRules.append(rule);

preprocessorFormat.setFontWeight(QFont::Bold);
preprocessorFormat.setForeground(Qt::darkBlue);
rule.pattern = QRegularExpression(QStringLiteral("\\#[A-Za-z]+"));
rule.format = preprocessorFormat;
highlightingRules.append(rule);

I assume the keyword list is still incomplete. I've added the preprocessor regex so that #include & co works. Which shows that for some part a rule based parser/regex is needed for highlighting.

I'm trying to use this in a different context, seeing the highlighter work in a QTextEdit is nice, but I'm not sure if I should print that on a t-shirt. So what if one could actually use this in a QPainter? One needs to know a little bit about Qts APIs to connect this. After all even if QTextEdit had a method allowing you to pass in a QPainter as its context, it would draw the QTextEdit likely. But the QTextDocument class has a drawContent method with QPainter as its argument, and actually this is what is passed from the QTextEdit to the highlighter:

QFont font;
font.setFamily("Courier");
font.setFixedPitch(true);
font.setPointSize(10);

//editor = new QTextEdit;
editor = ui->editor;
editor->setFont(font);

highlighter = new Highlighter(editor->document());

editor->setPlainText("#include \n\n//some tests\nclass Pair{int x,y;};\n bool operator==(const Pair& p1,const Pair p2){return p1.x == p2.x && p1.y == p2.y;}\nconstexpr std::string s{\"test bool const\"};");
ui->paintpanel->setDoc(editor->document());

So if we had a different Widget, all that is needed is to pass in the pointer to the text document used by the QTextEdit. Then one can paint the text with a QPainter in a Paint event outside of the QTextEdit:

void PaintPanel::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);
    QBrush b(Qt::lightGray);
    painter.setBrush(b);
    painter.setPen(Qt::darkGreen);
    painter.drawRect(50,10,400,200);
    if(doc)
    {
        doc->setTextWidth(300);
        doc->drawContents(&painter);
    }
}

PaintPanel is a QWidget derived class demonstrating this. I was happy to see it working at first, but then started wondering on how to actually use this. As the background color in both widgets is white, I was not sure if drawContent would also paint a background color. For that reason I paint a rectangle first, to test if it does. Which it doesn't. For a prototype this is far enough. Its usability is limited, as drawContent allows to pass in a rectangle, but that only makes it paint the text that maps with that rectangle. You can't set a point/offset where the painting should start. Likely you'd have to do that either with the QPainter instance, or paint into a QPixmap/QImage which then later gets painted by another QPainter on a different surface.

But being able to simply add needed keywords and set their properties in the text is interesting.

In the end this is what the prototype looks like:

blog/highlighterprototype.png

For the moment I'm happy with this.

 

Join the Meeting C++ patreon community!
This and other posts on Meeting C++ are enabled by my supporters on patreon!