1
votes

Requirements:

  • QScrollArea containing several widgets.
  • Each widget should be individually resizable by the user (in either horizontal, or vertical, but not both directions).
  • User resizing of a widget should not change the size of other widgets. It should increase/decrease the area available in the QScrollArea.

Using a QSplitter doesn't help, because the QSplitter remains of fixed width, and resizing any of its splits causes other splits to shrink. [1] [2] [3]

Surely it can be done by creating a custom widget, adding a visual bar for indicating the draggable area, and listening to a drag event to resize the widget via code. Is there a simpler solution?

1

1 Answers

0
votes

I had the same problem. Came up with a nasty hack:

  • put a QSplitter inside the QScrollArea
  • store the old sizes of all QSplitter child widgets
  • when a QSplitterHandle moves (i.e. on SIGNAL splitterMoved() )
    • Calculate how much the changed child widget has grown/shrunk
    • Change the min size of the whole QSplitter by that amount
    • Update my stored size for the changed child widget only
    • Set the sizes of the QSplitter child widgets to my stored sizes.

It works for me (for now). But it's kludgy, and there are some yucky magic numbers in it to make it work. So if anyone comes up with a better solution, that would be great! Anyway - in case anyone finds it useful, Code (in Python3 & PySide2)

    import sys
    
    from PySide2.QtCore import Qt
    from PySide2.QtWidgets import QWidget, QScrollArea, QSplitter
    from PySide2.QtWidgets import QApplication, QMainWindow, QLabel, QFrame
      
        
    class ScrollSplitter(QScrollArea):
        def __init__(self, orientation, parent=None):
            super().__init__(parent)
    
            # Orientation = Qt.Horizontal or Qt.Vertical
            self.orientation = orientation
            
            # Keep track of all the sizes of all the QSplitter's child widgets BEFORE the latest resizing,
            # so that we can reinstate all of them (except the widget that we wanted to resize)
            self.old_sizes = []
            self._splitter = QSplitter(orientation, self)
            
            # TODO - remove magic number. This is required to avoid zero size on first viewing.
            if orientation == Qt.Horizontal :
                self._splitter.setMinimumWidth(500)
            else :
                self._splitter.setMinimumHeight(500)
            
            # In a default QSplitter, the bottom widget doesn't have a drag handle below it.
            # So create an empty widget which will always sit at the bottom of the splitter,
            # so that all of the user widgets have a handle below them
            #
            # I tried playing with the max width/height of this bottom widget - but the results were crummy. So gave up.
            bottom_widget = QWidget(self)
            self._splitter.addWidget(bottom_widget)
            
            # Use the QSplitter.splitterMoved(pos, index) signal, emitted every time the splitter's handle is moved.
            # When this signal is emitted, the splitter has already resized all child widgets to keep its total size constant.
            self._splitter.splitterMoved.connect(self.resize_splitter)
    
            # Configure the scroll area.
            if orientation == Qt.Horizontal :
                self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
                self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
            else :
                self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
                self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
            
            self.setWidgetResizable(True)
            self.setWidget(self._splitter)
                    
            
        # Called every time a splitter handle is moved
        #   We basically undo the QSplitter resizing of all the other children,
        #   and resize the QSplitter (using setMinimumHeight() or setMinimumWidth() ) instead.
        def resize_splitter(self, pos, index):
            # NOTE: index refs the child widget AFTER the moved splitter handle.
            #       pos is position relative to the top of the splitter, not top of the widget.
            
            # TODO - find a better way to initialise the old_sizes list.
            # Ideally whenever we add/remove a widget.
            if not self.old_sizes :
                self.old_sizes = self._splitter.sizes()
                
            # The 'index' arg references the QWidget below the moved splitter handle.
            # We want to change the QWidget above the moved splitter handle, so...
            index_above = index - 1
            
            # Careful with the current sizes - QSplitter has already mucked about with the sizes of all other child widgets
            current_sizes = self._splitter.sizes()
            
            # The only change in size we are interested in is the size of the widget above the splitter
            size_change = current_sizes[index_above] - self.old_sizes[index_above]
    
            # We want to keep the old sizes of all other widgets, and just resize the QWidget above the splitter.
            # Update our old_list to hold the sizes we want for all child widgets
            self.old_sizes[index_above] = current_sizes[index_above]
            
            # Increase/decrease the(minimum) size of the QSplitter object to accommodate the total new, desired size of all of its child widgets (without resizing most of them)
            if self.orientation == Qt.Horizontal :
                self._splitter.setMinimumWidth(max(self._splitter.minimumWidth() + size_change, 0))
            else :
                self._splitter.setMinimumHeight(max(self._splitter.minimumHeight() + size_change, 0)) 
    
            # and set the sizes of all the child widgets back to their old sizes, now that the QSplitter has grown/shrunk to accommodate them without resizing them
            self._splitter.setSizes(self.old_sizes)
            #print(self.old_sizes)
    
    
        # Add a widget at the bottom of the user widgets
        def addWidget(self, widget):
            self._splitter.insertWidget(self._splitter.count()-1, widget)
            
        # Insert a widget at 'index' in the splitter.
        # If the widget is already in the splitter, it will be moved.
        # If the index is invalid, widget will be appended to the bottom of the (user) widgets
        def insertWidget(self, index, widget):
            if index >= 0 and index < (self._splitter.count() - 1) :
                self._splitter.insertWidget(index, widget)
            
            self.addWidget(widget)
            
        # Replace a the user widget at 'index' with this widget. Returns the replaced widget
        def replaceWidget(self, index, widget):
            if index >= 0 and index < (self._splitter.count() - 1) :
                return self._splitter.replaceWidget(index, widget)
            
        # Return the number of (user) widgets
        def count(self):
            return self._splitter.count() - 1
            
        # Return the index of a user widget, or -1 if not found.
        def indexOf(self, widget):
            return self._splitter.indexOf(widget)
        
        # Return the (user) widget as a given index, or None if index out of range.
        def widget(self, index):
            if index >= 0 and index < (self._splitter.count() - 1) :
                return self._splitter.widget(index)
            return None
    
        # Save the splitter's state into a ByteArray.
        def saveState(self):
            return self._splitter.saveState()
        
        # Restore the splitter's state from a ByteArray
        def restoreState(self, s):
            return self._splitter.restoreState(s)
    
    
    
    class MainWindow(QMainWindow):
        def __init__(self):
            super().__init__()
            
            self.setWindowTitle("ScrollSplitter Test")
            self.resize(640, 400)
            
            self.splitter = ScrollSplitter(Qt.Vertical, self)
            self.setCentralWidget(self.splitter)    
            
            for color in ["Widget 0", "Widget 1", "Widget 2", "Some other Widget"]:
                widget = QLabel(color)
                widget.setFrameStyle(QFrame.Panel | QFrame.Raised)
                self.splitter.addWidget(widget)
                   
    app = QApplication(sys.argv)
    
    window = MainWindow()
    window.show()
    
    app.exec_()