pythoncssqtfontspyside2

PySide / QT Align text in vertical center for QPushButton


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:

enter image description here enter image description here enter image description here

Note that I am running this code in Maya but it should be the same problem in an QT environment I think.


Solution

  • 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.

    Brief sum of vertical font elements

    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Ö_+:

    Vertical aspects of a font

    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:

    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.

    Vertical alignment is wrong (or not?)

    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:

    Button using underscore character as text

    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?

    Possible implementation

    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)
    

    Further notes

    The above implementation is not perfect. Most importantly:

    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.