Monday, August 25, 2014

Confused About Sizes

In this ongoing comedy of errors, I am still fumbling around trying to understand why a QPushButton takes up so much more vertical space than a QLineEdit, as described in the preceding post. This has brought me up against the Qt rules for layouts, which I have always found most confusing.

Go to the QWidget doc page and search for the string "QSize", that is, every method that either takes or returns a QSize or QSizePolicy. Start with the description passage on "Size Hints and Size Policies". Read the property descriptions for Size and SizeHint and SizePolicy, and follow some links. When you've read about all of those, try to explain in simple terms, what determines the vertical size of a default QPushButton and in particular, when you lay out pushbuttons vertically why do they take up more space than line edits do, as shown here:

On the left, three QPushButtons in a VBox. On the right, three QLineEdits in a VBox. Why are the pushbuttons spaced more widely than the line-edits? I sure can't say.

I put in code to display the QSizeHint from each after the layout was complete.

pb1 True 32 74
le1 True 21 142

Both return valid QSizeHints ("True" from isValid()), and vertical size hints that differ by 11 pixels, 21 versus 32. Well, alrighty then. I made a custom QPushButton that returned a size hint with a vertical of 21, same as the line-edit does, and the width of 74 as a stock QPushButton did in this layout. Here's what I got.

Two things of immediate note. One, the height looks right! The pushbutton lines up nicely with the line-edit. But, the look has drastically changed; it's no longer a Mac OS button shape! The rounded corners are gone and the shading is reversed. This is clearer if you click it to see the full resolution "retina" size dialog.

In an effort to better understand the size controls, I modified this custom class to report every size-related method call:

class SHPB(QPushButton):
    def __init__(self,text='',parent=None):
        super().__init__(text,parent)
        self.setText('Shpb')
    def sizeHint(self):
        qs = super().sizeHint()
        print('sizehint',qs.width(),qs.height())
        #return qs
        return QSize(74,21)
    def sizePolicy(self):
        sp = super().sizePolicy()
        print('sizepolicy',int(sp.horizontalPolicy()),int(sp.verticalPolicy()))
        return sp
    def baseSize(self):
        print('baseSize')
        return super().baseSize()
    def maximumSize(self):
        print('maximumSize')
        return super().maximumSize()
    def minimumSize(self):
        print('minimumSize')
        return super().minimumSize()
    def minimumSizeHint(self):
        qs = super().minimumSizeHint()
        print('minimumsizehint',qs.width(),qs.height())
        return qs
    def size(self):
        print('size')
        return super().size()

So which of these methods gets called? Just two: the only print statements come from sizeHint() (two calls) and minimumSizeHint() (one call). No other size-related method appears to be called. I strongly suspected that Qt is accessing the properties for base, minimum, and maximum sizes and the size policy directly, without going through a method call, or at least not going through a method that PyQt handled. So, wait a minute, suppose I set a property? I added this line to the __init__() code:

        self.setSizePolicy(self.sizePolicy())

Aha! On the next run, the sizePolicy method was called. It is returning (1,0), i.e. Minimum and Fixed. So apparently PyQt doesn't insert a Python hook in a C++ method, even when I provide an overriding method, unless there's some reason? Yup. I added these lines,

        self.setMinimumSize(self.minimumSize())
        self.setMaximumSize(self.maximumSize())

and then my minimumSize() and maximumSize() overrides were called when they were not called before.

Well, that's interesting, but it doesn't address the issue. Apparently I can, by returning a modified size hint, get a pushbutton to line up with a line-edit. (Later: I can also do self.setMaximumHeight(21) for the same result.) But this comes with two major drawbacks:

  • I have to hard-code the height. 21 pixels looks right on Mac OS 10.9, but it might not be the best height for every platform. I'd much rather have Qt pick specific heights.
  • I lose the native button shape, and get a Qt non-native button shape. Which means I would need to do it for every button, or else have inconsistent button shapes.

I think I would rather have uniform, native button shapes and Qt-assigned heights that are based on the platform, the active font, etc. The price of that is a "puffy" layout of the stack of Replace controls. I can live with that, I guess.

No comments: