Как сделать изменяемый размер пользователя QWidget внутри QScrollArea?

Требования:

  • QScrollArea, содержащий несколько виджетов.
  • Каждый виджет должен быть индивидуально изменен пользователем (в горизонтальном или вертикальном направлении, но не в обоих направлениях).
  • Изменение размера виджета пользователем не должно изменять размер других виджетов. Это должно увеличить / уменьшить область, доступную в QScrollArea.

Использование QSplitter не помогает, потому что QSplitter остается фиксированной ширины, а изменение размера любого из его разбиений приводит к сокращению других разбиений. [1] [2] [3]

Конечно, это можно сделать, создав пользовательский виджет, добавив визуальную панель для обозначения перетаскиваемой области и прослушав событие перетаскивания, чтобы изменить размер виджета с помощью кода. Есть ли более простое решение?

1 ответ

У меня такая же проблема. Придумал неприятный хак:

  • поместите QSplitter внутри QScrollArea
  • сохранить старые размеры всех дочерних виджетов QSplitter
  • когда QSplitterHandle перемещается (т.е. на SIGNAL splitterMoved() )
    • Рассчитайте, насколько измененный дочерний виджет увеличился/уменьшился
    • Измените минимальный размер всего QSplitter на эту сумму
    • Обновить мой сохраненный размер только для измененного дочернего виджета
    • Установите размеры дочерних виджетов QSplitter в соответствии с моими сохраненными размерами.

У меня работает (пока). Но это глупо, и в нем есть несколько гадких магических чисел, чтобы заставить его работать. Так что, если кто-то придумает лучшее решение, это было бы здорово! В любом случае - если кто-то найдет это полезным, код (в 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_()
Другие вопросы по тегам