QFontMetrics leave extra space between lines

Question:

I’m trying to draw multiple paragraphs of text with PySide6’s QPainter and QFontMetrics. I want to draw them with the same spacing as they would have if I drew them all in a single block of text, but the line spacing isn’t quite right.

In the following example, the font metrics say that the font’s line spacing is 17. When I measure a single line of text, the bounding rectangle is indeed 17 pixels high. However, when I measure two lines of text, the bounding rectangle is 35 pixels high, not 34. Where does the extra pixel come from, and can I see it on some property of the font or the font metrics?

from PySide6.QtGui import QFont, QFontMetrics
from PySide6.QtWidgets import QApplication

app = QApplication()
font = QFont()
metrics = QFontMetrics(font)
print(metrics.lineSpacing())  # 17
print(metrics.boundingRect(0, 0, 100, 100, 0, 'A').height())  # 17
print(metrics.boundingRect(0, 0, 100, 100, 0, 'AnB').height())  # 35 != 17 * 2
print(metrics.leading())  # 0
print(metrics.ascent())  # 14
print(metrics.descent())  # 3

By the way, it isn’t always one extra pixel. If I make the font bigger, the extra space increases.

Update

I thought I had figured this out with musicamante’s suggestion of switching from QFontMetrics to QFontMetricsF, but there’s still a difference.

from PySide6.QtCore import QRectF
from PySide6.QtGui import QFont, QFontMetricsF
from PySide6.QtWidgets import QApplication

app = QApplication()
font = QFont()
metrics = QFontMetricsF(font)
print(metrics.height())  # 16.8125
print(metrics.boundingRect(QRectF(0, 0, 100, 100),
                           0,
                           'A').getCoords())  # (0.0, 0.0, 9.9375, 16.8125)
print(metrics.boundingRect(QRectF(0, 0, 100, 100),
                           0,
                           'AnB').getCoords())  # (0.0, 0.0, 9.9375, 34.8125)
# Note the height of that rect doesn't match the next calculation.
print(metrics.height() + metrics.lineSpacing())  # 34.046875

# I can't see any combination of these numbers that makes 34.8125
print(metrics.lineSpacing())  # 17.234375
print(metrics.leading())  # 0.421875
print(metrics.ascent())  # 13.984375
print(metrics.descent())  # 2.828125
Asked By: Don Kirkby

||

Answers:

First of all, the height of a bounding rect of a font metrics doesn’t depend on the characters used, but on the font specifications.

Two lines of text don’t have the double of the height() of the bounding rect of a single line: instead, you have to consider the lineSpacing().

In practice, the height of a bounding rect is normally the sum of:

  • the height() multiplied the number of lines;
  • the leading() multiplied by the number of spaces between the lines (aka: number of lines – 1);

Or, similarly, the sum of:

  • the ascent();
  • the lineSpacing() multiplied by the number of spaces between lines;
  • the descent();

Note that, obviously, the number of lines depends on the input text and the given options, for instance, if word wrapping was enabled and any of the source lines didn’t fit the given source rectangle.

Also consider that most fonts are vectorial, meaning that their coordinates and metrics are proportional and in floating point values. QFontMetrics, instead, works with integer values for simplicity and optimization reasons, so you might get inconsistent results caused by rounding in cases for which the point size doesn’t give rounded values: non integer numbers are generally "floored" (like int() in python).
In your case, the leading is probably more than 0 (but still less than 1), so you don’t get a proper sum of the aforementioned heights.

Specifically, QFontMetrics.boundingRect() returns a QRect resulting by the QRectF.toAlignedRect() of the computed formatted text, which is always "the smallest possible integer rectangle that completely contains this rectangle".

If you need to get precise coordinates, you need to use QFontMetricsF, which is the floating point counterpart of the default basic QFontMetrics.

That said, if you plan on drawing formatted text with QPainter, then consider using QTextDocument or, at least, QTextLayout, which is consistent with the standard Qt text drawing and is generally faster, more reliable and "simpler" (well, once you get to know it). While it might seem a bit too complex than required, it’s actually what Qt does when calling boundingRect(), so if you need custom painting, the QTextLayout option is actually better, especially if you can combine it with some smart caching (see QPicture) to avoid the common python bottleneck.

Answered By: musicamante
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.