Измените размер док — станции до высоты видимого содержимого

#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 сигналу.