How can I vertically align the text in a large font QPushButton? For example this Python code creates a button where the text is not vertically aligned. I have tried everything I can think of to solve it but I can't get it working.
from PySide2 import QtCore, QtGui, QtWidgets
button = QtWidgets.QPushButton("+") # "a" or "A"
button.setStyleSheet("font-size: 100px")
layout = QtWidgets.QVBoxLayout()
layout.addWidget(button)
window = QtWidgets.QWidget()
window.setLayout(layout)
window.show()'
Here is what the code above creates:
Note that I am running this code in Maya but it should be the same problem in an QT environment I think.
For example this Python code creates a button where the text is not vertically aligned.
In reality, the text is vertically aligned: you are not considering how text is normally rendered.
When dealing with text drawing, the font metrics (see typeface anatomy) should always be considered. Vertically speaking, a typeface always has a fundamental height, and its drawing is always based on the base line.
Consider the following image, which renders the text xgdÖ_+
:
The "baseline" is the vertical reference position from which every character is being drawn; you can consider it like the lines of a notebook: it's where you normally place the bottom part of the circle of "o", or the dot of a question mark, and the bottom of letters such as "g" are normally drawn underneath that line.
Qt always uses the QFontMetrics of a given font, and it uses font metrics functions in order to properly display text; those functions always have some reference to the base line above.
From the base line, then, we can get the following relative distances:
descent()
: which is the distance to the lowest point of the characters (the descender);xHeight()
: the distance to the height of a lower case character that has no ascender, which normally is the height of the letter x
(see x-height);capHeight()
: the height of a standard upper case letter (see cap height);ascent()
: the maximum upper extent of a font above the x-height, normally (but not always) allowing further space for letters such "d", diacritic marks for upper case letters, or fancy decorations;Finally, the whole font height()
is the sum of the ascent and descent. From there, you can get the "center" (often coinciding but not to be confused with the median or mean line), which is where a Qt.AlignVCenter
aligned text would normally have as its virtual middle line when showing vertically centered text in Qt.
Simply put (as more complex text layouts may complicate things), when Qt draws some text, it uses the font metrics height()
as a reference, and then aligns the text on the computed distances considering the overall height and/or the ascent and descent of the metrics. Once the proper base line has been found, the text is finally drawn based on it.
When Qt aligns some text, it always considers the metrics of the font, which can be misleading. Consider the case above, and imagine that you wanted to use the underscore character (_
) for your button, which clearly is placed way below the base line.
The result would be something like this:
This is clearly not "vertically aligned". It seems wrong, but is it?
As you can see from the image above, the "+" symbol is not vertically aligned to the center in the font I'm using. But, even if it was, would it be valid?
For instance, consider a button with "x" as its text, but using that letter as its mnemonic shortcut, which is normally underlined. Even assuming that you can center the x, would it be properly aligned when underlined?
Some styles even show the underlined mnemonic only upon user interaction (usually by pressing Alt): should the text be vertically translated in that case?
Now, it's clear that considering the overall vertical alignment including the mnemonic is not a valid choice.
But there is still a possibility, using QStyle functions and some ingenuity.
QPushButton draws its contents using a relatively simple paintEvent()
override:
void QPushButton::paintEvent(QPaintEvent *)
{
QStylePainter p(this);
QStyleOptionButton option;
initStyleOption(
&option);
p.drawControl(QStyle::CE_PushButton,
option);
}
This can be ported in Python with the following:
class CustomButton(QPushButton):
def paintEvent(self, event):
p = QStylePainter(self)
option = QStyleOptionButton()
self.initStyleOption(option)
p.drawControl(QStyle.CE_PushButton, option)
Since p.drawControl(QStyle.CE_PushButton, option)
will use the option.text
, we can draw a no-text button with the following:
class CustomButton(QPushButton):
def paintEvent(self, event):
p = QStylePainter(self)
option = QStyleOptionButton()
option.text = ''
self.initStyleOption(option)
p.drawControl(QStyle.CE_PushButton, option)
Now comes the problem: how to draw the text.
While we could simply use QPainter functions such as drawText()
, this won't be appropriate, as we should consider style aspects that use custom drawing: for instance, disabled buttons, or style sheets.
A more appropriate approach should consider the offset between the center of the font metrics and the visual center of the character(s) we want to draw.
QPainterPath allows us to add some text to a vector path and get its visual center based on its contents. So, we can use addText()
and subtract the difference of its center from the center of the font metrics height()
.
Here is the final result:
class CenterTextButton(QPushButton):
def paintEvent(self, event):
if not self.text().strip():
super().paintEvent(event)
return
qp = QStylePainter(self)
# draw the button without text
opt = QStyleOptionButton()
self.initStyleOption(opt)
text = opt.text
opt.text = ''
qp.drawControl(QStyle.CE_PushButton, opt)
fm = self.fontMetrics()
# ignore mnemonics temporarily
tempText = text.replace('&', '')
if '&&' in text:
# consider *escaped* & characters
tempText += '&'
p = QPainterPath()
p.addText(0, 0, self.font(), tempText)
# the relative center of the font metrics height
fontCenter = fm.ascent() - fm.height() / 2
# the relative center of the actual text
textCenter = p.boundingRect().center().y()
# here comes the magic...
qp.translate(0, -(fontCenter + textCenter))
# restore the original text and draw it as a real button text
opt.text = text
qp.drawControl(QStyle.CE_PushButtonLabel, opt)
The above implementation is not perfect. Most importantly:
opt.state &= ~QStyle.State_HasFocus
before qp.drawControl(QStyle.CE_PushButton, opt)
, but may have effects in case stylesheets are being used;Finally, for simple symbols like in this case, using an icon is almost always the better choice.
Luckily, Qt also has SVG file support for icons, and it also provides a QIconEngine API that allows further customization.