0

Update:
This is an MRE based on my original experiment, putting a QSplitter as the direct child widget of another QSplitter. I want to achieve a situation with the upper/lower height proportions being 66%-33% and the left/right width proportion of the upper QSplitter being 66%-33%.

import sys
from PyQt5 import QtWidgets, QtCore, QtGui

MAIN_WINDOW_HEIGHT = 900
TOP_FRAME_PERCENTAGE = 66 # can try with other percentages: 50, 75, 90...

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setMinimumSize(1000, MAIN_WINDOW_HEIGHT)
        self.setStyleSheet('background-color: magenta');
        main_splitter = QtWidgets.QSplitter(self)
        main_splitter.setOrientation(QtCore.Qt.Vertical)
        self.setCentralWidget(main_splitter)

        # try swapping and using a TopSplitter instead of a QFrame        
        # top_frame = QtWidgets.QFrame()
        top_frame = TopSplitter()
        
        self.setStyleSheet('background-color: red');
        main_splitter.addWidget(top_frame)
        
        self.bottom_panel = QtWidgets.QFrame()
        main_splitter.addWidget(self.bottom_panel)
        self.bottom_panel.setStyleSheet("background-color: green");
        print(f'main_splitter.indexOf(self.bottom_panel) {main_splitter.indexOf(self.bottom_panel)}')
        
        def bp_size_hint(*args):
            # NB is never called
            print('YYY') 
            return QtCore.QSize(200, (100 - TOP_FRAME_PERCENTAGE) * int(MAIN_WINDOW_HEIGHT/100))
        self.bottom_panel.sizeHint = bp_size_hint
        
        main_splitter.setStretchFactor(0, TOP_FRAME_PERCENTAGE)
        main_splitter.setStretchFactor(1, 100 - TOP_FRAME_PERCENTAGE)
        print(f'A self.bottom_panel.height() {self.bottom_panel.height()}')
        
    def show(self):
        print(f'B self.bottom_panel.height() {self.bottom_panel.height()}')
        super().show()
        print(f'C self.bottom_panel.height() {self.bottom_panel.height()}')
        # 305 pixels with a QFrame, but 263 pixels with a TopSplitter
        
class TopSplitter(QtWidgets.QSplitter):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setStyleSheet('background-color: cyan');
    
        top_left_panel = QtWidgets.QFrame()
        top_left_panel.setStyleSheet('background-color: yellow');
        self.addWidget(top_left_panel)
        top_right_panel = QtWidgets.QFrame()
        top_right_panel.setStyleSheet('background-color: black');
        self.addWidget(top_right_panel)
        
        # these work exactly as you'd expect, to achieve 66%-33%
        self.setStretchFactor(0, 20)
        self.setStretchFactor(1, 10)
        
    def sizeHint(self):
        # NB called frequently
        print('XXX')
        return QtCore.QSize(200, TOP_FRAME_PERCENTAGE * int(MAIN_WINDOW_HEIGHT/100))

app = QtWidgets.QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec_()

Previous MRE for this question:
(This was the second thing I tried: making both children of the main splitter QFrames, and then putting a left-right QSplitter under the top one... but again, it doesn't get the height proportions right...)
Here is an MRE:

import sys
from PyQt5 import QtWidgets, QtCore, QtGui

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setMinimumSize(1000, 800)
        self.main_splitter = QtWidgets.QSplitter(self)
        self.main_splitter.setOrientation(QtCore.Qt.Vertical)
        self.setCentralWidget(self.main_splitter) 
        
        TopFrame(self.main_splitter)
        
        self.bottom_panel = QtWidgets.QFrame(self.main_splitter)
        self.bottom_panel.setStyleSheet("background-color: green");
        print(f'self.main_splitter.indexOf(self.bottom_panel) {self.main_splitter.indexOf(self.bottom_panel)}')
        self.main_splitter.setStretchFactor(0, 20)
        self.main_splitter.setStretchFactor(1, 10)
        
class TopFrame(QtWidgets.QFrame):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # A
        self.lr_splitter = QtWidgets.QSplitter(self)
        self.lr_splitter.setOrientation(QtCore.Qt.Horizontal)
        # B
        self.setLayout(QtWidgets.QHBoxLayout())
        self.x_panel = QtWidgets.QFrame(self.lr_splitter)
        self.x_panel.setStyleSheet("background-color: red");
        self.y_panel = QtWidgets.QFrame(self.lr_splitter)
        self.y_panel.setStyleSheet("background-color: cyan");
        
        
        self.lr_splitter.setStretchFactor(0, 20)
        self.lr_splitter.setStretchFactor(1, 10)


app = QtWidgets.QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec_()

What I'm trying to do is make the children of the left-right splitter extend across the full width and full height of the upper frame. The stretch factor should preserve the ratios of the widths of the two components as the window is made wider or less wide.

It is possible to make a QSplitter the direct child of another QSplitter... but my experiments with this seem to show that the setStretchFactor doesn't then work. For that reason I have chosen to make the upper child of the main splitter a QFrame.

But there's another problem: if you comment out everything under the line "# A" in __init__ you will see that the stretch factor is as expected: the top frame is twice the height of the lower one. But if you uncomment and display again you'll see that that ratio has been changed mysteriously. Experimentation shows that the culprit line is the line under line "# B", setLayout....

So ultimately what I'm looking for is a solution where the top QSplitter children occupy the full real estate of the TopFrame and where the ratios set by setStretchFactor are what one might expect, in both QSplitters.

Edit

As per discussion, I then tried this for the TopFrame class:

class TopFrame(QtWidgets.QSplitter):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setOrientation(QtCore.Qt.Horizontal)
        
        h_layout = QtWidgets.QHBoxLayout()
        self.setLayout(h_layout)
        # this line causes two errors: 1) can't add layout to splitter; 2) can't add parent widget to its child layout
        # h_layout.addWidget(self)
        
        self.x_panel = QtWidgets.QFrame()
        self.addWidget(self.x_panel)
        self.x_panel.setStyleSheet("background-color: red");
        self.y_panel = QtWidgets.QFrame()
        self.addWidget(self.y_panel)
        self.y_panel.setStyleSheet("background-color: cyan");
        self.setStretchFactor(0, 20)
        self.setStretchFactor(1, 10)
mike rodent
  • 14,126
  • 11
  • 103
  • 157
  • You're creating a layout for `TopFrame`, but you never add the `lr_splitter` to it. Also, note that, while supported, adding widgets to a QSplitter using it as their parent is discouraged (see the note on the [documentation](https://doc.qt.io/qt-5/qsplitter.html#childEvent)) and you should always use `addWidget()` or `insertWidget()` instead. – musicamante Aug 13 '22 at 16:11
  • Thanks... that solves the problem of the expansion of the `TopFrame`'s `QSplitter`'s children. But it doesn't seem to solve the question of the changed ratio between the heights of `main_splitter`'s children. – mike rodent Aug 13 '22 at 16:22
  • Interestingly, stretching (when you pull the top edge of the window up or down) does seem to work (though I can't judge by eye whether it is really obeying the ratio of stretch factors)... in any event the initial ratio (between top and bottom) when the window first appears is different. Trying to understand what factors are influencing this... – mike rodent Aug 13 '22 at 16:57
  • As the [`setStretchFactor()`](https://doc.qt.io/qt-5/qsplitter.html#setStretchFactor) documentation explains, it doesn't set the *effective* stretch factor, but a ratio based on the initial size (hint) of the widgets. You will probably need to override `sizeHint()` for the top and/or bottom widgets. Also note that, unless you plan to add more widgets *other than those inside the splitter* on top, the `TopFrame` is actually not really necessary: QSplitter is already a widget, and it also inherits from QFrame, so you could just subclass it from QSplitter. – musicamante Aug 13 '22 at 16:57
  • Thanks. As mentioned, that's what I tried initially but, firstly, `setStretchFactor` did not then seem to work on `main_splitter` for this top element. But in addition, I can't see how I can use a `QLayout`. Will add a new version of `TopFrame` based on this idea. – mike rodent Aug 13 '22 at 17:05
  • Where did you mention that you tried to override the `sizeHint()`? Besides, just use `addWidget()`: QSplitter manages the layout of its widgets on its own, so trying to set a layout for it doesn't make any sense and it obviously doesn't work (this is also clearly written in the documentation, so, please carefully read it in *its entirety*). – musicamante Aug 13 '22 at 18:27
  • I've really read more than you're assuming. No, I didn't try using `sizeHint` but I did try with a `QSplitter` directly acting as the upper component of the main splitter. Adding another MRE to illustrate the difficulty in that case: using a `QFrame` it is easy to achieve a 66%/33% split between upper/lower. But with a `QSplitter` as the upper nothing works to achieve that accuracy. – mike rodent Aug 14 '22 at 10:12
  • `sizeHint()` is called as soon as a widget is added to a layout manager (including the private one used by QSplitter), and PyQt uses function reference caching, meaning that if you overwrite a function *after* the default one is called, the overwritten one will never be called internally. So you either overwrite `self.bottom_panel.sizeHint` *before* `addWidget()`, or you properly use a subclass that overrides it (which is the preferred choice). – musicamante Aug 14 '22 at 19:16
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/247266/discussion-between-mike-rodent-and-musicamante). – mike rodent Aug 14 '22 at 19:24

1 Answers1

0

As often before, a hint from musicamante led the way:

import sys
from PyQt5 import QtWidgets, QtCore, QtGui

MAIN_WINDOW_HEIGHT = 900
TOP_FRAME_PERCENTAGE = 66 # can try with other percentages: 50, 75, 90...

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setMinimumSize(1000, 300)
        self.resize(1000, MAIN_WINDOW_HEIGHT)
        self.setStyleSheet('background-color: magenta');
        main_splitter = QtWidgets.QSplitter(self)
        main_splitter.setOrientation(QtCore.Qt.Vertical)
        self.setCentralWidget(main_splitter)
        top_frame = TopSplitter()
        self.setStyleSheet('background-color: red');
        main_splitter.addWidget(top_frame)
        self.bottom_panel = QtWidgets.QFrame()
        main_splitter.addWidget(self.bottom_panel)
        self.bottom_panel.setStyleSheet("background-color: green");
        # to achieve 66%-33% heights ratio
        main_splitter.setStretchFactor(0, TOP_FRAME_PERCENTAGE)
        main_splitter.setStretchFactor(1, 100 - TOP_FRAME_PERCENTAGE)
         
    def show(self):
        super().show()
        print(f'C self.bottom_panel.height() {self.bottom_panel.height()}')
         
class TopSplitter(QtWidgets.QSplitter):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setStyleSheet('background-color: cyan');
        self.top_left_panel = QtWidgets.QFrame()
        self.top_left_panel.setStyleSheet('background-color: yellow');
        self.addWidget(self.top_left_panel)
        top_right_panel = QtWidgets.QFrame()
        top_right_panel.setStyleSheet('background-color: black');
        self.addWidget(top_right_panel)
        # to achieve 66%-33% widths ratio
        self.setStretchFactor(0, 20)
        self.setStretchFactor(1, 10)
         
    def sizeHint(self):
        print(f'super().sizeHint().height() {super().sizeHint().height()}')
        return self.top_left_panel.sizeHint()

app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()

... the idea in the above being, after observing with puzzlement why a QFrame results in the desired heights ratio, but a QSplitter does something unexpected, to "spoof" the QSplitter so that its sizeHint() returns whatever a typical QFrame does.

This is far from ideal, and could run into trouble if you wanted to mess around with the geometry of the TopSplitter's top_left_panel. But at least it gives a controllable outcome.

It results in the height of the lower QFrame coming out at 304 pixels (with 900 main window height).

mike rodent
  • 14,126
  • 11
  • 103
  • 157