Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

Impossible to get true text hight with QFontMetrics/QFontMetricsF



  • I have spent almost a full working day on a thing that should be simple and straightforward: aligning the text vertically centered inside a widget.
    The problem is that QFontMetrics/QFontMetricsF return height values that are 1) not correct, and 2) don't depend on the actual text.

    Some examples:
    QFontMetricsF{ font() }.boundingRect("a").height() gives 13.26
    QFontMetricsF{ font() }.boundingRect(")").height() gives 13.26

    QFontMetricsF{ font() }.size(0, "a"); gives 13.26
    QFontMetricsF{ font() }.size(0, ")"); gives 13.26

    But the height of a brace ) in my font is much larger than that of a.

    This is a bug, but before I report it, I wanted to consult with you. Perhaps, there is another obscure way to get the actual height of a symbol in pixels as rendered on the screen by the widget?

    Here's a more complete repro:

    #include <QApplication>
    #include <QDebug>
    #include <QFontMetricsF>
    
    int main(int argc, char *argv[])
    {
      QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
      QApplication a(argc, argv);
    
      QFontMetricsF fm(a.font());
      qInfo() << a.font();
      qInfo() << fm.boundingRect(")").height();
      qInfo() << fm.boundingRect("a").height();
      qInfo() << fm.size(0, ")").height();
      qInfo() << fm.size(0, "a").height();
    
      return 0;
    }
    

    Output:

    QFont(MS Shell Dlg 2,8.14286,-1,5,50,0,0,0,0,0)
    13.2656
    13.2656
    13.2656
    13.2656


  • Moderators



  • @Chris-Kawa said in Impossible to get true text hight with QFontMetrics/QFontMetricsF:

    See QFontMetricsF::tightBoundingRect().

    tightBoundingRect also does not yield the correct value, but it does depend on the string contents so it's a step in the right direction.

    Meanwhile, I traced the actual problem why I can't align the text vertically properly: p.drawText(tightTextRect, 0, myText) renders the text with vertical offset towards the bottom. This code:

    auto tightTextRect = QFontMetricsF{font()}.boundingRect(myText);
    tightTextRect.moveCenter(QRectF(rect()).center());
    p.fillRect(tightTextRect, Qt::darkBlue);
    p.drawText(tightTextRect, 0, myText);
    

    Results in this:

    alt text


  • Moderators

    Works for me. Which Qt version and OS are you using? Do you have resolution scaling enabled in the OS?

    Btw. if all you want to do is center text you don't need to calculate precise rectangle for that manually. Just pass rect() and appropriate alignment flags to drawText, e.g.

    p.drawText(rect(), Qt::AlignCenter, myText);
    


  • @Chris-Kawa said in Impossible to get true text hight with QFontMetrics/QFontMetricsF:

    Just pass rect() and appropriate alignment flags to drawText

    That doesn't work either. As I said, there is an offset to the right and to the bottom. Compensating for the offset seems to solve the problem. So the problem is not with the height of the font, although I have doubts in that as well (after measuring the pixel sizes on the screen), but with the unexpected offset at which the text is drawn.

    Tested only on Windows so far, but on different PCs. Qt 5.14.1 and 5.15, no change between them. I do have a 175% and a 100% monitor in this system, AA_EnableHighDpiScaling enabled, but I have also tried on a pure 100% PC (single monitor, no scaling). I have a repro and I think I should report a bug.
    Here's the repro:

    #include <QApplication>
    #include <QFontMetricsF>
    #include <QMainWindow>
    #include <QPainter>
    
    struct TestWidget : QWidget
    {
    protected:
    	void paintEvent(QPaintEvent*) override
    	{
    		const QString text = "Hello!";
    		QPainter p(this);
    		p.fillRect(rect(), Qt::darkBlue);
    
    		auto textRect = QFontMetricsF(font()).tightBoundingRect(text);
    		textRect.moveTopLeft(QPointF(0.0, 0.0));
    		//textRect.setTopLeft(QPointF(textRect.left() - 2.0, textRect.top() - 6.0));
    
    		p.fillRect(textRect, Qt::green);
    		p.drawText(textRect, 0, text);
    	}
    };
    
    int main(int argc, char *argv[])
    {
    	QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    	QApplication a(argc, argv);
    
    	QMainWindow mw;
    	auto* widget = new TestWidget;
    
    	auto f = widget->font();
    	f.setPixelSize(30);
    	widget->setFont(f);
    
    	mw.setCentralWidget(widget);
    	mw.show();
    
    	return a.exec();
    }
    

    Here's its output:

    alt text



  • Same problem on macOS 10.15, even the amount of the offset is about the same.

    To clarify: p.drawText(rect(), Qt::AlignCenter, myText); also exhibits the same offset, so when total height of the widget is only a few pixels larger than the text height it becomes obvious that the text is not centered.


  • Moderators

    Well you still need to center text in that calculated rectangle:

    p.drawText(textRect, Qt::AlignCenter, text);
    

    With that change I get
    text alignment 1

    And here's a slightly modified version:

    void paintEvent(QPaintEvent*) override
    {
        const QString text = "Hello!";
        QPainter p(this);
        
        const QRectF textRect = QFontMetricsF(font()).boundingRect(rect(), Qt::AlignCenter, text);
        
        p.fillRect(rect(), Qt::darkBlue);
        p.fillRect(textRect, Qt::green);
        p.drawText(rect(), Qt::AlignCenter, text);
    }
    

    This gives me

    text alignment 2

    And if I change textRect to

    QRectF textRect = QFontMetricsF(font()).tightBoundingRect(text);
    textRect.moveCenter(QRectF(rect()).center());
    

    I get this

    text alignment 3

    So everything seems as it should.
    I don't know. Seems to be something specific to your setup.



  • @Violet-Giraffe
    Normally, when you call drawText, it will render the text including any spacing that should go with it. The tightBoundingRect gives you the minimum space necessary to render the characters itself, but the drawText will still try to put the default spacing on top. You have to calculate the necessary offsets, and you get this information from the QFontMetricsF class: ascent and descent.

    Take a look at this stackoverflow question and the highest rated answer. It should help.



  • @Chris-Kawa said in Impossible to get true text hight with QFontMetrics/QFontMetricsF:

    Well you still need to center text in that calculated rectangle

    Why? Since when is it a requirement such that if you don't center the text it's rendered at a wrong position?
    In other words, in which reality does the output of my original example make any sense?

    I agree that the second two of your results are interesting, but they require AlignCenter for the same reason. Thanks for the tip, though, I didn't realize such combination of manual alignment (by specifying the pre-aligned rect) + automatic alignment (AlignCenter) improves the situation.

    Note that I say "improves", not "fixes". Even on your last screenshot the offset from the top is 11px and from the bottom it's 10px! But I can live with that. Probably due to integer rounding and high DPI re-calculations.



  • @Asperamanca said in Impossible to get true text hight with QFontMetrics/QFontMetricsF:

    Take a look at this stackoverflow question and the highest rated answer. It should help.

    Thanks! This is a great description, but it matches what I already understood intuitively thus far.


  • Moderators

    @Violet-Giraffe said:

    In other words, in which reality does the output of my original example make any sense?

    In this one :) As @Asperamanca said there's more to text layout than a simple rectangle. I'm guessing offset is the ascent you also see in my second picture. Query it from the font metrics to see if that matches.

    Even on your last screenshot the offset from the top is 11px and from the bottom it's 10px!

    How would you draw sharp half a pixel?
    If you really want to you can enable antialiasing on the painter with

    p.setRenderHint(QPainter::RenderHint::Antialiasing);
    

    and this will give you blurry 10.5 pixel i.e. sharp 10 and alphablended 0.5.



  • @Chris-Kawa, you're of course right about half a pixel, I realized 1px difference is just rounding just as I submitted the comment, and I should have deleted that part.

    But how about this code? As you can see, there's still a bug, and I don't mean the string :)

    struct TestWidget : QWidget
    {
    protected:
    	void paintEvent(QPaintEvent*) override
    	{
    		const QString text = "bug()";
    		QPainter p(this);
    		p.fillRect(rect(), Qt::darkBlue);
    
    		auto textRect = QFontMetricsF(font()).tightBoundingRect(text);
    		textRect.moveTo(0.0, 0.0);
    		p.fillRect(textRect, Qt::green);
    
    		p.drawText(textRect, Qt::AlignCenter, text);
    	}
    };
    
    int main(int argc, char *argv[])
    {
    	QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    	QApplication a(argc, argv);
    
    	QMainWindow mw;
    	auto* widget = new TestWidget;
    
    	auto f = widget->font();
    	f.setPixelSize(30);
    	widget->setFont(f);
    
    	mw.setCentralWidget(widget);
    	mw.show();
    
    	return a.exec();
    }
    

    e1f0daae-7ff9-4b10-9f58-826a77ecdfa4-image.png

    Also, could you please explain the meaning of negative top left coordinates that boundingRect() / tightRect() often have? That's why moveTo(0.0, 0.0) is needed.

    P. S. I have established that the following magic manipulation solves the problem, but these numbers depend on the font and its size. Any idea how to calculate them, what font properties should be factored in to get this 7?

    textRect.setTopLeft(QPointF(textRect.left(), textRect.top() - 7.0));

    This is what it looks like:

    569f3b56-38d8-4089-9206-06ff0b7eae4b-image.png



  • I think the magic offset is -fm.descent() - fm.underlinePos(). Note that it's NOT translation, it is expansion of the original rect towards the top.



  • @Violet-Giraffe
    Try putting your font into italic and see whether it does not fit, horizontally?
    I'm thinking there might need to be a modification to textRect.left() too? leftBearing()? rightBearing()?
    tightBoundingRect:

    Note that the bounding rectangle may extend to the left of (0, 0), e.g. for italicized fonts



  • @JonB, you are correct, thanks for this tip!



  • @Violet-Giraffe
    And that's where your "magic" - fm.underlinePos() comes from: you have removed the underline position from your area, for right or for wrong. If the text were to be underlined, you would currently exclude it, I'm thinking. :)



  • @JonB, I don't think so, because as I said, I did not just translate the rect, and certainly didn't shrink it. I expanded it towards the top (possibly into negative coordinates).


Log in to reply