#python #pyqt5 #qlayout
Вопрос:
Я создаю складной виджет. Он содержит таблицу и встроен в другой виджет под некоторыми групповыми полями. Все поставлено на скамью подсудимых. Таблица, содержащаяся в складном виджете, вертикально заполняет док-станцию при развертывании складного виджета; групповые поля остаются фиксированными. Однако размер док-станции изменяется в соответствии с высотой групповых блоков и кнопки складного виджета только в том случае, если размер док-станции не был изменен первым.
Обратите внимание, что после изменения размера док-станции док-станция остается того же размера, что и свернутая таблица:
Как я могу изменить размер док-станции, как при первой загрузке, до минимальной высоты групповых блоков и кнопки переключения? Или, может быть, лучший вопрос: как виджет dock определяет свой минимальный размер и как я могу посоветовать ему быть минимального размера (если не с помощью минимального расширения)?
import sys
from PyQt5 import QtCore, QtWidgets, QtWidgets
class CollapsibleWidget(QtWidgets.QWidget):
def __init__(self, title="", parent=None):
super().__init__(parent)
self.toggle_button = QtWidgets.QToolButton(text=title, checkable=True, checked=True)
self.toggle_button.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
self.toggle_button.setArrowType(QtCore.Qt.RightArrow)
self.toggle_button.setStyleSheet("QToolButton { border: none; }")
self.toggle_button.pressed.connect(self.on_pressed)
self.content_layout = QtWidgets.QVBoxLayout()
self.content_widget = QtWidgets.QWidget()
self.content_widget.setLayout(self.content_layout)
self.content_widget.hide()
lay = QtWidgets.QVBoxLayout(self)
lay.setContentsMargins(0, 0, 0, 0)
lay.addWidget(self.toggle_button, alignment=QtCore.Qt.AlignTop)
lay.addWidget(self.content_widget)
def on_pressed(self):
checked = self.toggle_button.isChecked()
self.toggle_button.setArrowType(QtCore.Qt.DownArrow if checked else QtCore.Qt.RightArrow)
self.content_widget.setVisible(checked)
class ControlWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
# Checkboxes
self.checkbox1 = QtWidgets.QCheckBox("Checkbox1")
self.checkbox2 = QtWidgets.QCheckBox("Checkbox2")
# Buttons
self.button1 = QtWidgets.QPushButton('Button1')
self.button2 = QtWidgets.QPushButton('Button2')
self.button3 = QtWidgets.QPushButton('Button3')
# Checkbox group
self.gb_checkbox = QtWidgets.QGroupBox("Checkboxes")
self.layout_gb_checkbox = QtWidgets.QHBoxLayout()
self.layout_gb_checkbox.addWidget(self.checkbox1)
self.layout_gb_checkbox.addWidget(self.checkbox2)
self.gb_checkbox.setLayout(self.layout_gb_checkbox)
self.gb_checkbox.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
# Button group
self.gb_button = QtWidgets.QGroupBox("Buttons")
self.layout_gb_button = QtWidgets.QHBoxLayout()
self.layout_gb_button.addWidget(self.button1)
self.layout_gb_button.addWidget(self.button2)
self.layout_gb_button.addWidget(self.button3)
self.gb_button.setLayout(self.layout_gb_button)
self.gb_button.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
# groups layout
self.groups_layout = QtWidgets.QHBoxLayout()
self.groups_layout.addWidget(self.gb_checkbox)
self.groups_layout.addWidget(self.gb_button)
# table
self.table = QtWidgets.QTableWidget()
for i in range(20):
self.table.insertRow(i)
# Collapsible widget
self.collapsible_widget = CollapsibleWidget("Table")
self.collapsible_widget.content_layout.addWidget(self.table)
layout = QtWidgets.QVBoxLayout()
layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
layout.addLayout(self.groups_layout)
layout.addWidget(self.collapsible_widget)
self.setLayout(layout)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
controls = ControlWidget()
# Dock
dock_layout = QtWidgets.QVBoxLayout()
dock_layout.setContentsMargins(4, 0, 4, 0)
dock_layout.addWidget(controls)
dock = QtWidgets.QDockWidget("Control Panel")
dock_contents = QtWidgets.QWidget()
dock_contents.setLayout(dock_layout)
dock.setWidget(dock_contents)
# central widget
central_widget = QtWidgets.QWidget()
central_widget.setStyleSheet('background-color: gray')
# main window
main_window = QtWidgets.QMainWindow()
main_window.resize(640, 480)
main_window.addDockWidget(QtCore.Qt.TopDockWidgetArea, dock)
main_window.setCentralWidget(central_widget)
main_window.show()
sys.exit(app.exec_())
Я попытался установить очень низкий размер док-станции и установить политику размера на док-станции на минимальное расширение или расширение. Я ожидал, что док-станция затем попытается изменить размер до минимума, но затем изменит размер до минимума своего содержимого. Заметных изменений в поведении не произошло.
Я попытался получить доступ к док-станции в вызове on_pressed() и заставить ее изменить размер(). Опять же, никаких заметных изменений в поведении.
Ответ №1:
К сожалению, расположение окна Qmain (и расположение областей док-станции) почти недоступно, по крайней мере, из python. Основная проблема заключается в том, что виджеты док-станции добавляются во внутреннюю систему макетов, которая также отслеживает изменение размера вручную, и нет никакого способа (по крайней мере, насколько я знаю) «сбросить» эти размеры.
Однако существуют некоторые возможные обходные пути.
Одна из идей заключается в том, что складной виджет выдает сигнал всякий раз, когда он сворачивается, и этот сигнал подключен к специализированной функции главного окна.
В этом случае я автоматически подключу сигнал всякий раз, когда виджет док-станции будет установлен в качестве родительского элемента главного окна (но есть и другие способы сделать это). Тогда весь фокус в том, чтобы проверить, плавает док или нет, а затем, соответственно:
- измените его размер в соответствии с подсказкой минимального (вертикального) размера
- принудительно измените вертикальный размер дока
class CollapsibleWidget(QtWidgets.QWidget):
collapsed = QtCore.pyqtSignal()
# ...
def on_pressed(self):
checked = self.toggle_button.isChecked()
self.toggle_button.setArrowType(
QtCore.Qt.DownArrow if checked else QtCore.Qt.RightArrow)
self.content_widget.setVisible(checked)
if not checked:
self.collapsed.emit()
class MainWindow(QtWidgets.QMainWindow):
def childEvent(self, event):
if event.added() and isinstance(event.child(), QtWidgets.QDockWidget):
for resizable in event.child().findChildren(CollapsibleWidget):
resizable.collapsed.connect(self.collapsibleResized)
def collapsibleResized(self):
widget = self.sender()
dock = widget.parent()
while not isinstance(dock, QtWidgets.QDockWidget):
dock = dock.parent()
if dock.isFloating():
def delayedResize():
dock.resize(dock.width(), dock.minimumSizeHint().height())
else:
def delayedResize():
self.resizeDocks(
[dock],
[dock.widget().minimumSizeHint().height()],
QtCore.Qt.Vertical
)
QtWidgets.QApplication.processEvents()
QtCore.QTimer.singleShot(0, delayedResize)
Могут возникнуть некоторые проблемы при добавлении нескольких док-станций (или сведении их в таблицы), и я не уверен в восстановлении состояния док-станции, поэтому вам, вероятно, следует провести некоторое глубокое тестирование.
Комментарии:
1. Еще раз спасибо вам! Есть ли какая-то особая причина, по которой вы подключаете таблицу через
childEvent
? Я бы ожидал, что это будет сделано с помощью чего-то вроде » self.controls.collapsible_widget.collapsed.connect(self.on_collapse)` в MainWidget. Это то, что вы имели в виду, говоря «но есть и другие способы сделать это»? Наконец, прав ли я в том, что одиночный снимок используется для учета обработки событий?2. @LoremIpsum 1. Да: я не знаю, что вы собираетесь реализовать это, используя childEvent гарантирует, что любой новый QDockWidget, который имеет разборный виджет будет автоматически и правильно подключены к соответствующим сигналом; обратите внимание, что для согласованности, вы также должны отключить его в случае, если вы планируете удалить (Delete или реорганизация) виджетов. 2. Да, изменение размера часто приводит к событиям, требующим большего количества циклов цикла событий, поэтому вы не можете выполнить его мгновенно: вы должны дождаться, пока макет правильно завершит «свою работу».
Ответ №2:
Я удовлетворен следующим, основываясь на ответе @musicamante. Код немного улучшен по сравнению с вопросом:
- Заменены политики фиксированного размера для групповых ящиков с помощью растяжки на складном виджете; это делает групповые ящики одинакового размера
- Удален интервал(0); это позволяет групповым блокам не сталкиваться друг с другом
- Обновлено on_pressed() до toggle_expanded(); позволяет программно использовать
В противном случае, благодаря тестированию, следующее работает довольно хорошо. Существуют сигналы для «свернутых» и «расширенных» изменений состояния. Они подключены непосредственно в главном окне. При сворачивании размер док-станции изменяется в соответствии с минимальной высотой (для @musicamante). Текущая высота дока сохраняется перед сворачиванием. При расширении восстанавливается последняя известная высота дока. Изменение размера блокируется при сворачивании, чтобы предотвратить любые странные промежуточные состояния (когда док-станция выходит за пределы таблицы, как показано в исходном вопросе).
При тестировании с сохранением состояния и геометрии сохраняется только расположение виджета dock и сама геометрия. Любое состояние в доке, например, развернут ли складной виджет, должно обрабатываться отдельно после инициализации. Используйте функцию toggle_expanded().
Я пробовал различные перестановки событий обработки, длительности задержек и немедленного вызова функции delayed_resize (). Все они время от времени вызывали мерцание, при котором таблица расширялась примерно до 6 строк, но сразу же изменялась в размерах. Мерцание нерегулярно, возникает только во время расширения и, насколько я могу судить, является незначительной деталью. Иногда, при переключении между плавающим и закрепленным, высота немного уменьшалась. По-видимому, это связано с тем, что высота док-станции включает строку заголовка при плавании. Строка заголовка отсутствует при закреплении.
Для удобства есть дополнительный виджет док-станции. Все размеры, как и ожидалось.
import sys
from PyQt5 import QtCore, QtWidgets, QtWidgets
class CollapsibleWidget(QtWidgets.QWidget):
collapsed = QtCore.pyqtSignal()
expanded = QtCore.pyqtSignal()
def __init__(self, title="", parent=None):
super().__init__(parent)
self.setObjectName('CollapsibleWidget')
self.toggle_button = QtWidgets.QToolButton(text=title, checkable=True, checked=False)
self.toggle_button.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
self.toggle_button.setArrowType(QtCore.Qt.RightArrow)
self.toggle_button.setStyleSheet("QToolButton { border: none; }")
self.toggle_button.clicked.connect(self.toggle_expanded)
self.content_layout = QtWidgets.QVBoxLayout()
self.content_layout.setContentsMargins(0, 0, 0, 0)
self.content_widget = QtWidgets.QWidget()
self.content_widget.setLayout(self.content_layout)
self.content_widget.hide()
lay = QtWidgets.QVBoxLayout(self)
lay.setContentsMargins(0, 0, 0, 0)
lay.addWidget(self.toggle_button, alignment=QtCore.Qt.AlignTop)
lay.addWidget(self.content_widget)
def toggle_expanded(self, expanded=True):
self.toggle_button.setArrowType(QtCore.Qt.DownArrow if expanded else QtCore.Qt.RightArrow)
self.content_widget.setVisible(expanded)
if not expanded: # regardless of whether the state actually changed
self.collapsed.emit()
else:
self.expanded.emit()
class ControlWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("ControlWidget")
# Checkboxes
self.checkbox1 = QtWidgets.QCheckBox("Checkbox1")
self.checkbox2 = QtWidgets.QCheckBox("Checkbox2")
# Buttons
self.button1 = QtWidgets.QPushButton('Button1')
self.button2 = QtWidgets.QPushButton('Button2')
self.button3 = QtWidgets.QPushButton('Button3')
# Checkbox group
self.gb_checkbox = QtWidgets.QGroupBox("Checkboxes")
self.layout_gb_checkbox = QtWidgets.QHBoxLayout()
self.layout_gb_checkbox.addWidget(self.checkbox1)
self.layout_gb_checkbox.addWidget(self.checkbox2)
self.gb_checkbox.setLayout(self.layout_gb_checkbox)
# Button group
self.gb_button = QtWidgets.QGroupBox("Buttons")
self.layout_gb_button = QtWidgets.QHBoxLayout()
self.layout_gb_button.addWidget(self.button1)
self.layout_gb_button.addWidget(self.button2)
self.layout_gb_button.addWidget(self.button3)
self.gb_button.setLayout(self.layout_gb_button)
# groups layout
self.groups_layout = QtWidgets.QHBoxLayout()
self.groups_layout.addWidget(self.gb_checkbox)
self.groups_layout.addWidget(self.gb_button)
# table
self.table = QtWidgets.QTableWidget()
for i in range(20):
self.table.insertRow(i)
# Collapsible widget
self.collapsible_widget = CollapsibleWidget("Table")
self.collapsible_widget.content_layout.addWidget(self.table)
layout = QtWidgets.QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.addLayout(self.groups_layout)
layout.addWidget(self.collapsible_widget, stretch=1)
self.setLayout(layout)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Controls
self.controls = ControlWidget()
self.controls.collapsible_widget.collapsed.connect(self.on_collapse)
self.controls.collapsible_widget.expanded.connect(self.on_expand)
# Dock
self.dock_layout = QtWidgets.QVBoxLayout()
self.dock_layout.setContentsMargins(4, 0, 4, 0)
self.dock_layout.addWidget(self.controls)
self.dock_contents = QtWidgets.QWidget()
self.dock_contents.setLayout(self.dock_layout)
# Don't allow resizing unless expanded; initial states is collapsed
self.dock_contents.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
self.dock = QtWidgets.QDockWidget("Control Panel")
self.dock.setWidget(self.dock_contents)
self._dock_last_expanded_height = self.dock.minimumSizeHint().height()
# Extra Dock
self.extra_dock_layout = QtWidgets.QVBoxLayout()
self.extra_dock_layout.addWidget(QtWidgets.QLabel('Extra Dock'))
self.extra_dock_layout.addWidget(QtWidgets.QLabel('Extra Dock'))
self.extra_dock_layout.addWidget(QtWidgets.QLabel('Extra Dock'))
self.extra_dock_layout.addWidget(QtWidgets.QLabel('Extra Dock'))
self.extra_dock_layout.addWidget(QtWidgets.QLabel('Extra Dock'))
self.extra_dock_layout.addWidget(QtWidgets.QLabel('Extra Dock'))
self.extra_dock_layout.addWidget(QtWidgets.QLabel('Extra Dock'))
self.extra_dock_contents = QtWidgets.QWidget()
self.extra_dock_contents.setLayout(self.extra_dock_layout)
self.extra_dock = QtWidgets.QDockWidget("Extra dock")
self.extra_dock.setWidget(self.extra_dock_contents)
# Central widget
self.central_widget = QtWidgets.QWidget()
self.central_widget.setStyleSheet('background-color: gray')
self.resize(640, 480)
self.addDockWidget(QtCore.Qt.TopDockWidgetArea, self.dock)
self.addDockWidget(QtCore.Qt.TopDockWidgetArea, self.extra_dock)
self.setCentralWidget(self.central_widget)
def on_collapse(self):
self._dock_last_expanded_height = self.dock.height()
if self.dock.isFloating():
def delayed_resize():
self.dock.resize(self.dock.width(), self.dock.minimumSizeHint().height())
else:
def delayed_resize():
self.resizeDocks(
[self.dock],
[self.dock.widget().minimumSizeHint().height()],
QtCore.Qt.Vertical
)
# Don't allow resizing unless expanded
self.dock_contents.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
# QtWidgets.QApplication.processEvents()
# QtCore.QTimer.singleShot(0, delayed_resize)
delayed_resize()
def on_expand(self):
if self.dock.isFloating():
def delayed_resize():
self.dock.resize(self.dock.width(), self._dock_last_expanded_height)
else:
def delayed_resize():
self.resizeDocks(
[self.dock],
[self._dock_last_expanded_height],
QtCore.Qt.Vertical
)
# Allow resizing when expanded
self.dock_contents.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
# QtWidgets.QApplication.processEvents()
# QtCore.QTimer.singleShot(0, delayed_resize)
delayed_resize()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
sys.exit(app.exec_())
Комментарии:
1. Мерцание может быть вызвано срабатыванием нескольких таймеров. Я предлагаю вам добавить несколько простых инструкций печати в
delayed_resize
функции, чтобы проверить, так ли это. Если это так, то вместо этого вы можете создать постоянный QTimer, затем вызвать егоdisconnect()
в блоке try/except и, наконец, снова подключиться к соответствующей функции и перезапустить таймер: таким образом, он будет вызываться только в «последний раз», когда это действительно требуется.2. АФАИК,
delayed_resize
его вызывают только один раз. Спасибо вам за совет.3. Я немного проверил ваш код, я вижу только некоторые (случайные) мерцания при сворачивании. Это может зависеть от разных аспектов, включая платформу (я работаю в Linux). Я бы посоветовал вам сохранить QTimer в
delayed_resize
любом случае, так как изменение размера часто требует определенного количества циклов событий. Попробуйте немного увеличить интервал (5-10 мс). Кроме того, проблема изменения размера плавающего состояния действительно зависит от строки заголовка (вот почему я использовалminimumSizeHint
док-станцию, а не ее виджет). Вы можете сохранить правильный размер, выполнив правильную проверку подключения кtopLevelChanged
сигналу.