将停靠栏调整为可见内容的高度

Resize dock to height of visible contents

我正在创建一个可折叠的小部件。它包含一个 table 并嵌入在某些组框下方的另一个小部件中。一切都被放入码头。当可折叠小部件展开时,可折叠小部件中包含的 table 垂直填充停靠栏;组框保持固定。但是,停靠栏会根据组框和可折叠小部件按钮的高度调整大小 仅当停靠栏未首先调整大小时

请注意调整停靠栏大小后,停靠栏如何保持与折叠的 table 相同的大小:

我怎样才能像第一次加载时那样调整扩展坞的大小,使其达到组框和切换按钮的最小高度?或者也许是一个更好的问题,停靠小部件如何确定其最小尺寸以及我如何建议它为最小尺寸(如果不是通过 MinimumExpanding)?

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_())

我尝试将扩展坞的 sizeHint 设置得非常低,并将扩展坞上的 sizePolicy 设置为 MinimumExpanding 或 Expanding。我希望扩展坞然后尝试将大小调整到最小,但随后将其大小调整到其内容的最小值。行为没有明显变化。

我尝试在 on_pressed() 调用中访问停靠栏并强制其调整大小 ()。同样,行为没有明显变化。

不幸的是,QMainWindow 的布局(以及停靠区域的布局)几乎无法访问,至少从 python 开始是这样。主要问题是停靠小部件被添加到内部布局系统中,该系统还保留手动调整大小的痕迹,并且没有办法(至少,据我所知)“重置”这些大小。

虽然存在一些可能的解决方法。

一个想法是,可折叠小部件在折叠时发出一个信号,并且该信号连接到主 window.

的一个专门函数

在这种情况下,只要将停靠小部件设置为主 window 的父级,我就会自动连接信号(但还有其他方法可以这样做)。然后技巧就是检查dock是否漂浮,然后,分别:

  • 根据其最小(垂直)大小提示调整大小
  • 强制扩展坞的垂直尺寸
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)

添加(或标签化)多个停靠栏时可能会出现一些问题,我不确定是否恢复停靠栏状态,因此您可能应该进行一些深入测试。

根据@musicamante 的回复,我对以下内容感到满意。代码比问题略有改进:

  • 将组框上的固定大小策略替换为可折叠小部件上的拉伸;这使得组框大小相同
  • 删除了间距(0);这使得组框不会 运行 变成另一个
  • 已将 on_pressed() 更新为 toggle_expanded();允许编程使用

否则,通过测试,下面的效果很好。存在“折叠”和“展开”状态变化的信号。这些在主窗口中直接连接。折叠时,停靠栏会根据最小高度调整大小(根据@musicamante)。当前停靠高度在折叠前保存。扩展时,将恢复最后已知的停靠高度。折叠时锁定调整大小以防止出现任何奇怪的中间状态(停靠栏超出 table,如原始问题所示)。

通过保存状态和几何图形进行测试,仅保留停靠小部件位置和几何图形本身。 dock 中的任何状态,例如可折叠小部件是否展开,都必须在 init 之后单独处理。使用 toggle_expanded()。

我尝试了处理事件、延迟持续时间和立即调用 delayed_resize() 的各种排列组合。所有产生的偶尔闪烁其中 table 扩展到类似 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_())