Search code examples
pythonpyqtqgridlayoutqsplitter

How to create a grid of splitters


What I'm trying to do is add splitter to a QGridLayout in order to resize the layout with the mouse. So for instance with this :

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

class SurfViewer(QMainWindow):
    def __init__(self, parent=None):
        super(SurfViewer, self).__init__()
        self.parent = parent
        self.setFixedWidth(300)
        self.setFixedHeight(100)

        self.wid = QWidget()
        self.setCentralWidget(self.wid)

        self.grid = QGridLayout()

        l_a = QLabel('A')
        l_b = QLabel('B')
        l_c = QLabel('C')
        l_d = QLabel('D')
        l_e = QLabel('E')
        l_f = QLabel('F')
        l_g = QLabel('G')
        l_h = QLabel('H')
        l_i = QLabel('I')
        self.grid.addWidget(l_a, 0, 0)
        self.grid.addWidget(l_b, 0, 1)
        self.grid.addWidget(l_c, 0, 2)
        self.grid.addWidget(l_d, 1, 0)
        self.grid.addWidget(l_e, 1, 1)
        self.grid.addWidget(l_f, 1, 2)
        self.grid.addWidget(l_g, 2, 0)
        self.grid.addWidget(l_h, 2, 1)
        self.grid.addWidget(l_i, 2, 2)
        self.wid.setLayout(self.grid)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = SurfViewer(app)
    ex.setWindowTitle('window')
    ex.show()
    sys.exit(app.exec_( ))

I get this:

enter image description here

What I would like is instead of the colored line, have the possibility to click and drag vertically (for green lines) and horizontally (for red lines) the grid borders.

I tried something with QSplitter directly, but I end up with:

enter image description here

The Horizontal splits are okay, but the vertical ones are not aligned any more:

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

class SurfViewer(QMainWindow):
    def __init__(self, parent=None):
        super(SurfViewer, self).__init__()
        self.parent = parent
        self.setFixedWidth(300)
        self.setFixedHeight(100)

        self.wid = QWidget()
        self.setCentralWidget(self.wid)

        # self.grid = QGridLayout()
        self.globallayout = QVBoxLayout()
        self.split_V = QSplitter(Qt.Vertical)

        l_a = QLabel('A')
        l_b = QLabel('B')
        l_c = QLabel('C')
        l_d = QLabel('D')
        l_e = QLabel('E')
        l_f = QLabel('F')
        l_g = QLabel('G')
        l_h = QLabel('H')
        l_i = QLabel('I')
        split_H = QSplitter(Qt.Horizontal)
        split_H.addWidget(l_a)
        split_H.addWidget(l_b)
        split_H.addWidget(l_c)
        self.split_V.addWidget(split_H)

        split_H = QSplitter(Qt.Horizontal)
        split_H.addWidget(l_d)
        split_H.addWidget(l_e)
        split_H.addWidget(l_f)
        self.split_V.addWidget(split_H)

        split_H = QSplitter(Qt.Horizontal)
        split_H.addWidget(l_g)
        split_H.addWidget(l_h)
        split_H.addWidget(l_i)
        self.split_V.addWidget(split_H)

        self.globallayout.addWidget(self.split_V)

        self.wid.setLayout(self.globallayout)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = SurfViewer(app)
    ex.setWindowTitle('window')
    ex.show()
    sys.exit(app.exec_( ))

Update

I think I almost found a solution where a function is used so that whenever the vertical splits are changed, it re-aligns them:

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

class SurfViewer(QMainWindow):
    def __init__(self, parent=None):
        super(SurfViewer, self).__init__()
        self.parent = parent
        self.setFixedWidth(300)
        self.setFixedHeight(100)

        self.wid = QWidget()
        self.setCentralWidget(self.wid)

        # self.grid = QGridLayout()
        self.globallayout = QVBoxLayout()
        self.split_V = QSplitter(Qt.Vertical)

        l_a = QLabel('A')
        l_b = QLabel('B')
        l_c = QLabel('C')
        l_d = QLabel('D')
        l_e = QLabel('E')
        l_f = QLabel('F')
        l_g = QLabel('G')
        l_h = QLabel('H')
        l_i = QLabel('I')
        self.split_H1 = QSplitter(Qt.Horizontal)
        self.split_H1.addWidget(l_a)
        self.split_H1.addWidget(l_b)
        self.split_H1.addWidget(l_c)
        self.split_V.addWidget(self.split_H1)

        self.split_H2 = QSplitter(Qt.Horizontal)
        self.split_H2.addWidget(l_d)
        self.split_H2.addWidget(l_e)
        self.split_H2.addWidget(l_f)
        self.split_V.addWidget(self.split_H2)

        self.split_H3 = QSplitter(Qt.Horizontal)
        self.split_H3.addWidget(l_g)
        self.split_H3.addWidget(l_h)
        self.split_H3.addWidget(l_i)
        self.split_V.addWidget(self.split_H3)

        self.globallayout.addWidget(self.split_V)

        self.wid.setLayout(self.globallayout)

        self.split_H1.splitterMoved.connect(self.moveSplitter)
        self.split_H2.splitterMoved.connect(self.moveSplitter)
        self.split_H3.splitterMoved.connect(self.moveSplitter)

        # self.split_H1.splitterMoved
        # self.moveSplitter(0,self.split_H1.at )

    def moveSplitter( self, index, pos ):
        # splt = self._spltA if self.sender() == self._spltB else self._spltB
        self.split_H1.blockSignals(True)
        self.split_H2.blockSignals(True)
        self.split_H3.blockSignals(True)
        self.split_H1.moveSplitter(index, pos)
        self.split_H2.moveSplitter(index, pos)
        self.split_H3.moveSplitter(index, pos)
        self.split_H1.blockSignals(False)
        self.split_H2.blockSignals(False)
        self.split_H3.blockSignals(False)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = SurfViewer(app)
    ex.setWindowTitle('window')
    ex.show()
    sys.exit(app.exec_( ))

However, I still have an issue at the begining - the alignment is not correct :

enter image description here

I don't know How call the function moveSplitter in the __init__


Solution

  • It seems that directly calling moveSplitter (which is a protected method) may be problematic. Using Qt-5.10.1 with PyQt-5.10.1 on Linux, I found that it can often result in a core dump when called during __init__. There is probably a good reason why Qt provides setSizes as a public method for changing the position of the splitters, so it may be wise to prefer it over moveSplitter.

    With that in mind, I arrived at the following implementation:

    class SurfViewer(QMainWindow):
        def __init__(self, parent=None):
            ...
            self.split_H1.splitterMoved.connect(self.moveSplitter)
            self.split_H2.splitterMoved.connect(self.moveSplitter)
            self.split_H3.splitterMoved.connect(self.moveSplitter)
    
            QTimer.singleShot(0, lambda: self.split_H1.splitterMoved.emit(0, 0))
    
        def moveSplitter(self, index, pos):
            sizes = self.sender().sizes()
            for index in range(self.split_V.count()):
                self.split_V.widget(index).setSizes(sizes)
    

    The single-shot timer is needed because on some platforms the geometry of the window may not be fully initialized before it is shown on screen. And note that setSizes does not trigger splitterMoved, so there is no need to block signals when using it.