Search code examples
pythonlayoutpyqtqsizepolicy

Pyqt: Enforcing sizeHint() dimensions on two-widget app with layout manager


I have two widgets residing one next to each other, WidgetA and WidgetB, on a QDialog, with a horizontal Layout Manager.
I am trying to enforce the following size/resize policies:
For WidgetA:

  • Horizontally: Width should be 900 with the ability to shrink (up to 100), and to expand (to whatever).
  • Vertically: Height should be 600 with the ability to expand (to whatever).

For WidgetB:

  • Horizontally: Width should be Fixed to 600.
  • Vertically: Height should be 600, with the ability to expand (to whatever) - same as WidgetA.

But no matter which size policy I choose for WidgetA, it still won't take up width of 900.
Here's a code sample:

class WidgetA(QTextEdit):

    def __init__(self, parent = None):
        super(WidgetA, self).__init__(parent)
        #self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.MinimumExpanding)   --> WidgetA width still smaller than 900
        #self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.MinimumExpanding)     --> WidgetA will be set to minimumSizeHint()
        #self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)       --> not good for me, since I want WidgetA to be able to shrink up to minimumSizeHint().width()
        #self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.MinimumExpanding)     --> not good for me for the same reason - I want WidgetA to be able to shrink up to minimumSizeHint().width()

    def minimumSizeHint(self):
        return QSize(100, 600)

    def sizeHint(self):
        return QSize(900, 600) 


class WidgetB(QTextEdit):

    def __init__(self, parent = None):
        super(WidgetB, self).__init__(parent)
        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)

    def sizeHint(self):
        return QSize(600, 600)

class MainForm(QDialog):

    def __init__(self, parent = None):
        super(MainForm, self).__init__(parent)

        label_a = QLabel('A')
        widget_a = WidgetA()
        label_a.setBuddy(widget_a)

        label_b = QLabel('B')
        widget_b = WidgetB()
        label_b.setBuddy(widget_b)

        hbox = QHBoxLayout()

        vbox = QVBoxLayout()
        vbox.addWidget(label_a)
        vbox.addWidget(widget_a)
        widget = QWidget()
        widget.setLayout(vbox)

        hbox.addWidget(widget)

        vbox = QVBoxLayout()
        vbox.addWidget(label_b)
        vbox.addWidget(widget_b)
        widget = QWidget()
        widget.setLayout(vbox)

        hbox.addWidget(widget)

        self.setLayout(hbox)



def run_app():
    app = QApplication(sys.argv)
    form = MainForm()
    form.show()
    app.exec_()

if __name__ == '__main__':
    run_app()

The closest I could get is when I set:

self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.MinimumExpanding)

On WidgetA.

enter image description here

It seems like WidgetB is taking a bite off of WidgetA's 900 width (eliminating WidgetB does the trick), but I shouldn't be limited by the window's width.
What's preventing from the window itself (the MainForm) to automatically expand horizontally in order to account for both WidgetA's 900 width and WidgetB's fixed 600 width?


Solution

  • According to the article on Layout Management, the rules applied when adding widgets are as follows:

    1. All the widgets will initially be allocated an amount of space in accordance with their QWidget::sizePolicy() and QWidget::sizeHint().
    2. If any of the widgets have stretch factors set, with a value greater than zero, then they are allocated space in proportion to their stretch factor (explained below).
    3. If any of the widgets have stretch factors set to zero they will only get more space if no other widgets want the space. Of these, space is allocated to widgets with an Expanding size policy first.
    4. Any widgets that are allocated less space than their minimum size (or minimum size hint if no minimum size is specified) are allocated this minimum size they require. (Widgets don't have to have a minimum size or minimum size hint in which case the stretch factor is their determining factor.)
    5. Any widgets that are allocated more space than their maximum size are allocated the maximum size space they require. (Widgets do not have to have a maximum size in which case the stretch factor is their determining factor.)

    So the most you can do is to define sizeHint and minimumSizeHint, set a stretch factor greater than all the others, and set a size-policy of Expanding. But even with all these things in place, it seems the layout manager is still not guaranteed to honour the size-hint when the minimum-size-hint is less.

    The problem is that the definition of Expanding is not quite strong enough:

    The sizeHint() is a sensible size, but the widget can be shrunk and still be useful. The widget can make use of extra space, so it should get as much space as possible (e.g. the horizontal direction of a horizontal slider).

    For whatever reason, the layout manager seems to prioritise shrinking over expanding if another widget has a stronger size-policy (e.g. Fixed). This looks like the wrong choice from the point of view of your example, but I suppose there may be other situations where it makes more sense. The behaviour seems to be exactly the same in both Qt4 and Qt5, so I don't know whether it should be considered a bug, or just a quirk of the implementation.

    The simplest work-around I could come up with to fix your example is to enforce the size-hint of widget_a like this:

    class MainForm(QDialog):
    
        def __init__(self, parent = None):
            ...
    
            self.setLayout(hbox)
    
            widget_a.setMinimumSize(widget_a.sizeHint())
            self.adjustSize()
            widget_a.setMinimumSize(widget_a.minimumSizeHint())
    

    (NB: only tested on Linux with the Openbox window manager).

    PS: see also this quite similar SO question: Resize window to fit content.