Home > Mobile >  How to center QToolButton icon?
How to center QToolButton icon?

Time:01-27

when I connect a Qt QToolButton to a QAction that has an icon assigned to it, the icon shows in the QToolButton, but the icon is off-center and misaligned, see the image below:

Icons are clearly shifted a few pixels to the left

I am setting up the buttons like this (Python code):

self.actionExpand_All.setIcon(QIcon("icons_16:arrow-out.png"))
self.actionCollapse_All.setIcon(QIcon("icons_16:arrow-in.png"))

self.toolButton_expandAll.setDefaultAction(self.actionExpand_All)
self.toolButton_collapseAll.setDefaultAction(self.actionCollapse_All)

The icons come from the Fugue set and are 16x16 pngs. The toolButtonStyle is set to 'ToolButtonIconOnly'. The QActions and the QToolButtons are defined via Qt Designer in a .ui file which I convert to Python via pyuic command. I am using PyQt 6.4.

I googled but could not find any solution, only mention of this problem from 2017 on StackExchange had some suggestions but none worked. I also tried centering the icon myself via QToolButton stylesheet fields such as 'margin' and 'padding' but to no avail. I would be happy with making the QToolButton a bit bigger to center the icon but the QToolButton size seems to 'automatically' fit the icon and is not controlled from Qt Designer.

Thanks

CodePudding user response:

For some reason, Qt developers chose to return sizes that may result in odd numbers for the size hint of QToolButton.

To understand that, we need to remember that Qt uses QStyle for many aspects, including indirect size management: many widgets query the current style() of the widget in order to compute correct size requirements for their contents, by calling styleFromContents().

Almost all styles use a default QCommonStyle as basis for many aspects, and here we have the first issue.

According to the sources, QCommonStyle does that when sizeFromContents() is called along with CT_ToolButton:

QSize QCommonStyle::sizeFromContents(ContentsType ct, const QStyleOption *opt,
                                     const QSize &csz, const QWidget *widget) const
{
    Q_D(const QCommonStyle);
    QSize sz(csz);
    switch (ct) {

    # ...

    case CT_ToolButton:
        sz = QSize(sz.width()   6, sz.height()   5);

    # ...

    }
    return sz;
}

As you can see, we already have a problem: assuming that the given csz is based on the icon size alone (which usually has even values, usually 16x16), we will get a final hint with an odd value for the height (eg. 22x21).

This happens even in styles that don't rely on QCommonStyle, which is the case of the "Windows" style:

    case CT_ToolButton:
        if (qstyleoption_cast<const QStyleOptionToolButton *>(opt))
            return sz  = QSize(7, 6);

Which seems partially consistent with your case, with the button border occupying 21 pixels: I suppose that the "missing" 2 pixels (16 7 = 23) are used as a margin, or for the "outset" border shown when the button is hovered.

Now, there are various possible solutions, depending on your needs.

Subclass QToolButton

If you explicitly need to use QToolButton, you can use a subclass that will "correct" the size hint:

class ToolButton(QToolButton):
    def sizeHint(self):
        hint = super().sizeHint()
        if hint.width() & 1:
            hint.setWidth(hint.width()   1)
        if hint.height() & 1:
            hint.setHeight(hint.height()   1)
        return hint

This is the simplest solution, but it only works for QToolButtons created in python: it will not work for buttons of QToolBar.

You could even make it a default behavior without subclassing, with a little hack that uses some "monkey patching":

def toolButtonSizeHint(btn):
    hint = btn.__sizeHint(btn)
    if hint.width() & 1:
        hint.setWidth(hint.width()   1)
    if hint.height() & 1:
        hint.setHeight(hint.height()   1)
    return hint
QToolButton.__sizeHint = QToolButton.sizeHint
QToolButton.sizeHint = toolButtonSizeHint

Note that the above code must be put as soon as possible in your script (possibly, in the main script, right after importing Qt), and should be used with extreme care, as "monkey patching" using class methods may result in unexpected behavior, and may be difficult to debug.

Use a proxy style

QProxyStyle works as an "intermediary" within the current style and a custom implementation, which potentially overrides the default "base style". You can create a QProxyStyle subclass and override sizeFromContents():

class ToolButtonStyle(QProxyStyle):
    def sizeFromContents(self, ct, opt, csz, w):
        size = super().sizeFromContents(ct, opt, csz, w)
        if ct == QStyle.CT_ToolButton:
            w = size.width()
            if w & 1:
                size.setWidth(w   1)
            h = size.height()
            if h & 1:
                size.setHeight(h   1)
        return size

# ...
if __name__ == "__main__":
    app = QApplication(sys.argv)
    app.setStyle(ToolButtonStyle())

This has the benefit of working for any QToolButton, including those internally created for QToolBar. But it's not perfect:

  • if you set a stylesheet (for the toolbar, a parent, or the whole application), that alters aspects of the look of QToolButtons which may affect the size (most importantly, borders), the override will be ignored: the private QStyleSheetStyle never calls QProxyStyle overridden functions whenever the style sheet may affect them;
  • only an application wide style (QApplication.setSheet()) is propagated to all widgets, but setting a QStyle to a widget will not propagate the style to its children: toolbar.setStyle() will not change the style (and resulting size) of its buttons;

Subclass QToolBar and set minimum sizes

Another possibility is to subclass QToolBar and explicitly set a minimum size (based on their hints) for all QToolButton created for its actions. In order to do that, we need a small hack: access to functions (and overwriting virtuals) on objects created outside Python is not allowed, meaning that we cannot just try to "monkey patch" things like sizeHint() at runtime; the only solution is to react to LayoutRequest events and always set an explicit minimum size whenever the hint of the QToolButton linked to the action does not use even numbers for its values.

class ToolBar(QToolBar):
    def event(self, event):
        if event.type() == event.LayoutRequest:
            for action in self.actions():
                if not isinstance(action, QWidgetAction):
                    btn = self.widgetForAction(action)
                    hint = btn.sizeHint()
                    w = hint.width()
                    h = hint.height()
                    if w & 1 or h & 1:
                        if w & 1:
                            w  = 1
                        if h & 1:
                            h  = 1
                        btn.setMinimumSize(w, h)
                    else:
                        btn.setMinimumSize(0, 0)
        return super().event(event)

This will work even if style sheets have effect on tool bars and QToolButtons. It will obviously have no effect for non-toolbar buttons, but you can still use the first solution above. In the rare case you explicitly set a minimum size on a tool button (not added using addWidget(), that size would be potentially reset.

Final notes

  • solutions 1 and 3 only work when explicitly creating objects (QToolButton or QToolBar) from python, not when using ui or pyuic generated files; for that, you'll need to use promoted widgets;
  • considering the common implementation of sizeFromContents() you can probably subtract the "extra odd pixel" instead of adding it, but there's no guarantee that it will always work: some styles might completely ignore borders and margins, which would potentially result in unexpected behavior and display;
  • not all styles do the above: for instance, it seems like the "Oxygen" style always uses even values for QToolButton sizes;
  • any of the above considers the case of tool buttons that may display text, in case no icon is set, or if the button style (of the button or the tool bar) also requires text to be shown and a text actually exists; you can check that by using the relative functions of QToolButton in case of subclassing, by querying the given QStyleOption (opt, above);
  • you may consider filing a report on the Qt bug manager about this;

Note that while the linked post could make this as a duplicate, I don't know C enough to provide an answer to that. To anybody reading this, feel free to make that one as a duplicate of this, or post a related answer with appropriate code in that language.

  • Related