Search code examples
pythonpython-3.xpyqtpyqt5qsplitter

QSplitter() doesn't treat a QScrollArea() and QFrame() equally


1. Problem explained

I'm experimenting with Qt's QSplitter() widget. I've built a very simple sample project in PyQt5 showing a QSplitter() encapsulating a QScrollArea() on the left and a QFrame() on the right:

enter image description here

I've given both the QScrollArea() and QFrame() equal stretch factors, but the QSplitter() doesn't treat them equally. The QScrollArea() always gets most space. I have no idea why.

 

Minimal, Reproducible Example

Simply copy-paste the code below in a .py script and run it. I've got Python 3.7 with PyQt5 running on a Windows 10 machine.

from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys

class Scroller(QScrollArea):
    '''
    The Scroller(), will be first widget in the Splitter().

    '''
    def __init__(self):
        super().__init__()
        self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding))
        self.setStyleSheet("""
            QScrollArea {
                background-color:#fce94f;
                border-color:#c4a000;
                padding: 0px 0px 0px 0px;
                margin: 0px 0px 0px 0px;
            }
        """)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)

        # 1. FRAME
        self.__frm = QFrame()
        self.__frm.setStyleSheet("QFrame { background: #ff25292d; border: none; }")
        self.__frm.setMinimumHeight(100)

        # 2. LAYOUT
        self.__lyt = QVBoxLayout()
        self.__frm.setLayout(self.__lyt)

        # 3. SELF
        self.setWidget(self.__frm)
        self.setWidgetResizable(True)
        return

class Frame(QFrame):
    '''
    The Frame(), will be second widget in the Splitter()

    '''
    def __init__(self):
        super().__init__()
        self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding))
        # 1. FRAME
        self.setStyleSheet("""
            QFrame {
                background-color:#fcaf3e;
                border-color:#ce5c00;
                padding: 0px 0px 0px 0px;
                margin: 0px 0px 0px 0px;
            }
        """)
        self.__lyt = QVBoxLayout()
        self.__lyt.setAlignment(Qt.AlignTop)
        self.__lyt.setSpacing(0)
        self.__lyt.setContentsMargins(10, 10, 10, 10)
        self.setLayout(self.__lyt)
        return

class Splitter(QSplitter):
    '''
    The Splitter().

    '''
    def __init__(self, widg1, widg2):
        super().__init__()
        self.setOrientation(Qt.Horizontal)
        self.addWidget(widg1)
        self.addWidget(widg2)
        self.setStretchFactor(0, 5)
        self.setStretchFactor(1, 5)
        return

    def createHandle(self):
        return QSplitterHandle(self.orientation(), self)

class CustomMainWindow(QMainWindow):
    '''
    CustomMainWindow(), a QMainWindow() to start the whole setup.

    '''
    def __init__(self):
        super().__init__()
        self.setGeometry(100, 100, 600, 300)
        self.setWindowTitle("QSPLITTER TEST")

        # 1. OUTER FRAME
        self.__frm = QFrame()
        self.__frm.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.__frm.setStyleSheet("""
            QFrame {
                background-color: #eeeeec;
                border-color: #2e3436;
            }
        """)
        self.__lyt = QVBoxLayout()
        self.__frm.setLayout(self.__lyt)
        self.setCentralWidget(self.__frm)
        self.show()

        # 2. WIDGETS TO BE PUT IN SPLITTER
        self.__widg1 = Scroller()
        self.__widg2 = Frame()

        # 3. SPLITTER
        self.__splitter = Splitter(self.__widg1, self.__widg2)
        self.__lyt.addWidget(self.__splitter)
        return

if __name__== '__main__':
    app = QApplication(sys.argv)
    QApplication.setStyle(QStyleFactory.create('Plastique'))
    myGUI = CustomMainWindow()
    sys.exit(app.exec_())

Solution

  • QSplitter not only takes the stretch factor as a reference, it also takes into account the sizeHint(). If the following is added:

    # ...
    # 3. SPLITTER
    self.__splitter = Splitter(self.__widg1, self.__widg2)
    self.__lyt.addWidget(self.__splitter)
    print(self.__widg1.sizeHint(), self.__widg2.sizeHint())
    return
    

    You get the following:

    PyQt5.QtCore.QSize(38, 22) PyQt5.QtCore.QSize(20, 20)
    

    Where we see that the QScrollArea has a greater width in the sizeHint() than the QFrame, and that explains why the observed behavior.

    The solution is to establish the same width of sizeHint(), that is, not depend on what it contains.

    class Scroller(QScrollArea):
        # ...
    
        def sizeHint(self):
            s = super().sizeHint()
            s.setWidth(20) # same width
            return s
    
    class Frame(QFrame):
        # ...
    
        def sizeHint(self):
            s = super().sizeHint()
            s.setWidth(20) # same width
            return s
    # ...
    

    Output:

    enter image description here