Как сделать изменяемый размер пользователя 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_()