Drawing circular text in Qt

published at 16.02.2018 20:17 by Jens Weller

For a few weeks now, I use Friday afternoon to do some coding. As often managing Meeting C++ has become a non-code activity, it feels good to have a spot in the week where I focus on "what could I code today?". Today I focused on drawing circular text in Qt, which mostly consisted of writing prototype code, there is still lots to tweak if you'd wanted to use this in production.

I'll only need this, to create a svg version of the r/cpp_review logo with Qt. Lets dive in.

So when you google this, you'll find this stackoverflow answer, which actually shows how to draw text on a bezier curve. Cool! So I adopted that solution for drawing text around a circle, and refactored the code into a function, with this signature:

void drawCircularText(QPainter& painter,const QPen& pen,const QFont& font,const QString& text,int x, int y, int diameter,qreal percentfactor = 0.5,qreal start_in_percent=0)

Short view on the arguments:

painter - the painter knowing on which context to draw.

pen - the Pen which you want to draw with

font - since its text, lets also have that

text - what ends up circular

x,y - center of the circle

diameter - how big would you like to have your circle?

qreal - its a Qt typedef for double/float. percentfactor - this determines a factor by which each letter is separated on the circle, the original just spreads it on 360°, what is not what everyone needs start_in_percent - as the original code uses percent, this is where to start on the circle between 0-1.



Not much has changed from the original code, as mentioned the path now uses addEllipse(x,y,diameter,diameter) to have a circular layout for the text. The only important change is these two tweaks:

qreal percentIncrease = (qreal) 1.0/text.size()*percentfactor; for ( int i = 0; i < text.size(); i++ ) { start_in_percent += percentIncrease; if(start_in_percent > 1.0) { qDebug() << "start_in_percent is over 1.0:" << start_in_percent; start_in_percent -= 1.0; } QPointF point = path.pointAtPercent(start_in_percent); qreal angle = path.angleAtPercent(start_in_percent); // Clockwise is negative painter.save(); // Move the virtual origin to the point on the curve painter.translate(point); // Rotate to match the angle of the curve // Clockwise is positive so we negate the angle from above painter.rotate(-angle); // Draw a line width above the origin to move the text above the line // and let Qt do the transformations painter.drawText(QPoint(0, -pen.width()),QString(text[i])); painter.restore(); }

Multiplying with percentfactor makes it possible to fine tune the spreading of the letter accross the circle. As one keeps adding to start_in_percent, I check if it goes over 1.0, to adjust the value. This prevents that nothing is drawn, when the text is too long for the circle. After this the actual drawing happens.

This works very well, but what if you want to have your text counter clock wise?

Counter clock wise - the hard way

So, the internet knows really nothing about this, at least in relation to Qt. Which also motivated me to blog about this, as future generations might just find this article over google...

It turns out there are two solutions, a hard one, and an easy one. The easy one is a bit tricky, as it feels so natural, once you've seen it, but without knowing every detail of the Qt APIs, its easily overlooked. Which happened to me, so behold my first, not very well working solution:

Reverse the text string (std::reverse) and then just draw with drawText! Oh, well, that doesn't work, as the letters are still in the wrong direction. But they are already in the right position. All I'd need is to flip them. Why not draw them on a QImage, and then just flip that image, so that the letter magically ends up correct? I'm not sure if the painter API would offer something similar without drawing first on an image, but lets first check this solution out, before we go further.

First, it seemed not to work, as the painted images just were really trashy. Can one use more then one painter in parallel in the same thread? But then I noticed an interesting pattern, the first image was almost correct, except that the not drawn part of the QImage contained trash seemingly. More interesting, it seemed as that at runtime, the same QImage would be used to draw the whole text on. The further images all just had more piled letters on top of each other, until one could only see a blob of lines and curves. So it seems that the optimizer - at least I blame the optimizer for this - was like, hey thats a really expensive operation to always allocate a new image in this loop, lets just simply always reuse that one! So I refactored that code into a function:

QImage drawTextOnPixmap(const QString& text,QFont font,int size) { QImage pixmap(size,size,QImage::Format_ARGB32); QPainter pmp(&pixmap); pmp.setRenderHint(QPainter::Antialiasing); pmp.setFont(font); pmp.setBackground(QBrush(Qt::white)); pmp.fillRect(pixmap.rect(),QBrush(Qt::black)); pmp.setPen(Qt::white); pmp.drawText(pixmap.rect(),Qt::AlignCenter,text); return pixmap;//.scaled(size,size,Qt::KeepAspectRatio,Qt::SmoothTransformation); }

That was super easy! Just, NRVO is now doing, what previously seemed the optimizer to do: we're always drawing on the same image! Which isn't so bad, as that way some allocations are saved. A fillRect each time makes sure the whole image gets redrawn. While this works, and draws text counter clock wise, its a hack, and has big problem:

Do you see the antialiasing? While the painter happily draws the text in a good quality, the image is getting rotated into place and the result of the actual text shown is not very pretty. Also, I'm not sure what std::reverse would do to unicode text...

Counter clock wise text - the easy way

While the first solution "worked", it was clear that its not a good solution, and not even bringing in the needed result. There needs to be a solution, which works with only using the Qt API. And there is, and its so easy. QPainter offers lots of drawXYZ member functions, so it would not be so surprising if drawCircularText existed. But there is no such thing, no drawRoundedText, or any other interface in QPainter offering this. Thats why the above solution for circular text is the actual working one I'm aware of so far. And its quite easy to tweak it to produce circular text in anti-clockwise direction:

path = path.toReversed();

With this, the QPainterPath now flows in the counter-clock wise direction, and magically the text is anti-clock wise! One little difference there is though: while the clock wise text is on the out side of the circle, the anti-clock wise text is on the inside. This time its a little better:

One problem, which can be fixed easily, persists: the spacing of the letters currently does not address the width of the letter in the font. QFontMetrics::width makes this quite easy.

Join the Meeting C++ patreon community!

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