Qt:停靠小部件如何获得其初始大小?

Qt: How does a dock widget get its initial size?

当一个小部件停靠时,我希望它改变方向并且相对于停靠扩展的方式具有最小尺寸。

问题是,只要方向改变,停靠栏就会呈现出看似任意的宽度或高度。停靠时,我找不到将停靠小部件 resize/force 调整为特定大小的方法。我尝试了无数种覆盖 sizeHintminimumSizeHint、调用 adjustSize 和摆弄 sizePolicy.

的变体

如何确保初始停靠尺寸?


我的基本应用程序如下所示:

该应用程序显示主要和次要信息以及相应的控件集。包含主要内容和次要内容的选项卡小部件被设置为中央小部件。容纳各个仪表板中控件的 QStackedWidget 位于停靠栏中。当标签更改时,显示相应的仪表板。下面的 基本应用程序代码.

中给出了此代码

困难在于 改变仪表板的方向会扰乱扩展坞的大小。

调整dashboard的方向,我想到了两个比较合理的方案:

通过resizeEvent调整方向

在我看来,这似乎是更可取的选择。它允许用户最大的灵活性。如果他们不喜欢码头的方向,将其拖过特定限制将允许他们改变码头的方向。这里我检查它是否比高宽。

    class MyDock(QtWidgets.QDockWidget):

        def __init__(self):

            super(MyDock, self).__init__()

        def resizeEvent(self, event):
            size = event.size()
            is_wide = size.width() > size.height()

            container_object = self.widget().currentWidget()

            if is_wide:
                container_object.setDirection(QtWidgets.QBoxLayout.LeftToRight)
            else:
                container_object.setDirection(QtWidgets.QBoxLayout.TopToBottom)

下面 resize approach.

给出了完整的代码

dockLocationChange

改变方向

由于调整大小事件一直在发生,另一种方法可能是仅在停靠位置发生变化时才改变方向。为此,将功能连接到 dockLocationChanged 信号并根据扩展坞调整方向。

    class MyDock(QtWidgets.QDockWidget):

        def __init__(self):

            super(MyDock, self).__init__()

            self.dockLocationChanged.connect(self.dock_location_changed)

        def dock_location_changed(self, area):
            top    = QtCore.Qt.DockWidgetArea.TopDockWidgetArea
            bottom = QtCore.Qt.DockWidgetArea.BottomDockWidgetArea

            container_object = self.widget().currentWidget()

            if area in [top, bottom]:
                container_object.setDirection(QtWidgets.QBoxLayout.LeftToRight)
            else:
                container_object.setDirection(QtWidgets.QBoxLayout.TopToBottom)

基本应用代码

该程序由 5 个独立的 类.

对于

  1. MyWindow
  2. PrimaryDashboard
  3. SecondaryDashboard

分开的原因应该够清楚了吧

对于

  1. MyDock
  2. DockContainer

分离是为了便于覆盖 sizeHintsetDirection 或其他方法。

    import qtpy
    from qtpy import QtWidgets, QtGui, QtCore
    import sys


    class PrimaryDashboard(QtWidgets.QWidget):

        def __init__(self):

            super(PrimaryDashboard, self).__init__()

            self.init_widgets()
            self.init_layout()

        def init_widgets(self):
            self.label = QtWidgets.QLabel('Primary dashboard')
            self.ok = QtWidgets.QPushButton('OK')
            self.cancel = QtWidgets.QPushButton('Cancel')

        def init_layout(self):
            self.layout = QtWidgets.QHBoxLayout()
            self.layout.addWidget(self.label)
            self.layout.addWidget(self.ok)
            self.layout.addWidget(self.cancel)
            self.setLayout(self.layout)

        def setDirection(self, direction):
            self.layout.setDirection(direction)


    class SecondaryDashboard(QtWidgets.QWidget):

        def __init__(self):

            super(SecondaryDashboard, self).__init__()

            self.init_widgets()
            self.init_layout()

        def init_widgets(self):
            self.label = QtWidgets.QLabel('Secondary dashboard')

            self.descr1 = QtWidgets.QLabel('Thing 1')
            self.check1 = QtWidgets.QCheckBox()

            self.descr2 = QtWidgets.QLabel('Thing 2')
            self.check2 = QtWidgets.QCheckBox()

        def init_layout(self):
            self.layout = QtWidgets.QVBoxLayout()

            self.grid = QtWidgets.QGridLayout()
            self.grid.addWidget(self.descr1, 0, 0)
            self.grid.addWidget(self.check1, 0, 1)
            self.grid.addWidget(self.descr2, 1, 0)
            self.grid.addWidget(self.check2, 1, 1)

            self.layout.addWidget(self.label)
            self.layout.addLayout(self.grid)
            self.setLayout(self.layout)

        def setDirection(self, direction):
            self.layout.setDirection(direction)


    class DockContainer(QtWidgets.QStackedWidget):

        def __init__(self):

            super(DockContainer, self).__init__()


    class MyDock(QtWidgets.QDockWidget):

        def __init__(self):

            super(MyDock, self).__init__()


    class MyWindow(QtWidgets.QMainWindow):

        def __init__(self, parent=None):
            super(MyWindow, self).__init__(parent=parent)

            self.resize(600, 400)

            self.init_widgets()
            self.init_layout()

        def init_widgets(self):

            self.tab_widget = QtWidgets.QTabWidget()
            self.tab1 = QtWidgets.QLabel('Primary content')
            self.tab2 = QtWidgets.QLabel('Secondary content')
            self.tab_widget.addTab(self.tab1, 'Primary')
            self.tab_widget.addTab(self.tab2, 'Secondary')
            self.tab_widget.currentChanged.connect(self.tab_selected)

            self.primary_dashboard = PrimaryDashboard()
            self.secondary_dashboard = SecondaryDashboard()

            self.dashboard = DockContainer()
            self.dashboard.addWidget(self.primary_dashboard)
            self.dashboard.addWidget(self.secondary_dashboard)
            self.dashboard.setCurrentWidget(self.primary_dashboard)

            self.dock = MyDock()
            self.dock.setWidget(self.dashboard)
            self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.dock)

        def init_layout(self):
            self.main_layout = QtWidgets.QVBoxLayout()
            self.main_layout.addWidget(self.tab_widget)

            self.main_widget = QtWidgets.QWidget()
            self.main_widget.setLayout(self.main_layout)
            self.setCentralWidget(self.main_widget)

        def tab_selected(self):
            tab_index = self.tab_widget.currentIndex()
            if self.tab_widget.tabText(tab_index) == 'Secondary':
                self.dashboard.setCurrentWidget(self.secondary_dashboard)
                self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.dock)
            else:  # Primary
                self.dashboard.setCurrentWidget(self.primary_dashboard)
                self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.dock)


    if __name__ == '__main__':
        app = QtWidgets.QApplication(sys.argv)

        window = MyWindow()
        window.show()
        sys.exit(app.exec_())

调整大小方法

此代码与 基本应用程序代码 相同,但 resizeEvent 在停靠栏小部件中被覆盖。

    import qtpy
    from qtpy import QtWidgets, QtGui, QtCore
    import sys


    class PrimaryDashboard(QtWidgets.QWidget):

        def __init__(self):

            super(PrimaryDashboard, self).__init__()

            self.init_widgets()
            self.init_layout()

        def init_widgets(self):
            self.label = QtWidgets.QLabel('Primary dashboard')
            self.ok = QtWidgets.QPushButton('OK')
            self.cancel = QtWidgets.QPushButton('Cancel')

        def init_layout(self):
            self.layout = QtWidgets.QHBoxLayout()
            self.layout.addWidget(self.label)
            self.layout.addWidget(self.ok)
            self.layout.addWidget(self.cancel)
            self.setLayout(self.layout)

        def setDirection(self, direction):
            self.layout.setDirection(direction)


    class SecondaryDashboard(QtWidgets.QWidget):

        def __init__(self):

            super(SecondaryDashboard, self).__init__()

            self.init_widgets()
            self.init_layout()

        def init_widgets(self):
            self.label = QtWidgets.QLabel('Secondary dashboard')

            self.descr1 = QtWidgets.QLabel('Thing 1')
            self.check1 = QtWidgets.QCheckBox()

            self.descr2 = QtWidgets.QLabel('Thing 2')
            self.check2 = QtWidgets.QCheckBox()

        def init_layout(self):
            self.layout = QtWidgets.QVBoxLayout()

            self.grid = QtWidgets.QGridLayout()
            self.grid.addWidget(self.descr1, 0, 0)
            self.grid.addWidget(self.check1, 0, 1)
            self.grid.addWidget(self.descr2, 1, 0)
            self.grid.addWidget(self.check2, 1, 1)

            self.layout.addWidget(self.label)
            self.layout.addLayout(self.grid)
            self.setLayout(self.layout)

        def setDirection(self, direction):
            self.layout.setDirection(direction)


    class DockContainer(QtWidgets.QStackedWidget):

        def __init__(self):

            super(DockContainer, self).__init__()


    class MyDock(QtWidgets.QDockWidget):

        def __init__(self):

            super(MyDock, self).__init__()

        def resizeEvent(self, event):
            size = event.size()
            is_wide = size.width() > size.height()

            container_object = self.widget().currentWidget()

            if is_wide:
                container_object.setDirection(QtWidgets.QBoxLayout.LeftToRight)
            else:
                container_object.setDirection(QtWidgets.QBoxLayout.TopToBottom)


    class MyWindow(QtWidgets.QMainWindow):

        def __init__(self, parent=None):
            super(MyWindow, self).__init__(parent=parent)

            self.resize(600, 400)

            self.init_widgets()
            self.init_layout()

        def init_widgets(self):

            self.tab_widget = QtWidgets.QTabWidget()
            self.tab1 = QtWidgets.QLabel('Primary content')
            self.tab2 = QtWidgets.QLabel('Secondary content')
            self.tab_widget.addTab(self.tab1, 'Primary')
            self.tab_widget.addTab(self.tab2, 'Secondary')
            self.tab_widget.currentChanged.connect(self.tab_selected)

            self.primary_dashboard = PrimaryDashboard()
            self.secondary_dashboard = SecondaryDashboard()

            self.dashboard = DockContainer()
            self.dashboard.addWidget(self.primary_dashboard)
            self.dashboard.addWidget(self.secondary_dashboard)
            self.dashboard.setCurrentWidget(self.primary_dashboard)

            self.dock = MyDock()
            self.dock.setWidget(self.dashboard)
            self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.dock)

        def init_layout(self):
            self.main_layout = QtWidgets.QVBoxLayout()
            self.main_layout.addWidget(self.tab_widget)

            self.main_widget = QtWidgets.QWidget()
            self.main_widget.setLayout(self.main_layout)
            self.setCentralWidget(self.main_widget)

        def tab_selected(self):
            tab_index = self.tab_widget.currentIndex()
            if self.tab_widget.tabText(tab_index) == 'Secondary':
                self.dashboard.setCurrentWidget(self.secondary_dashboard)
                self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.dock)
            else:  # Primary
                self.dashboard.setCurrentWidget(self.primary_dashboard)
                self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.dock)


    if __name__ == '__main__':
        app = QtWidgets.QApplication(sys.argv)

        window = MyWindow()
        window.show()
        sys.exit(app.exec_())

dockLocationChanged接近

此代码与 基本应用程序代码 相同,但 dockLocationChanged 信号连接到调整基于当前停靠位置的方向。

    import qtpy
    from qtpy import QtWidgets, QtGui, QtCore
    import sys


    class PrimaryDashboard(QtWidgets.QWidget):

        def __init__(self):

            super(PrimaryDashboard, self).__init__()

            self.init_widgets()
            self.init_layout()

        def init_widgets(self):
            self.label = QtWidgets.QLabel('Primary dashboard')
            self.ok = QtWidgets.QPushButton('OK')
            self.cancel = QtWidgets.QPushButton('Cancel')

        def init_layout(self):
            self.layout = QtWidgets.QHBoxLayout()
            self.layout.addWidget(self.label)
            self.layout.addWidget(self.ok)
            self.layout.addWidget(self.cancel)
            self.setLayout(self.layout)

        def setDirection(self, direction):
            self.layout.setDirection(direction)


    class SecondaryDashboard(QtWidgets.QWidget):

        def __init__(self):

            super(SecondaryDashboard, self).__init__()

            self.init_widgets()
            self.init_layout()

        def init_widgets(self):
            self.label = QtWidgets.QLabel('Secondary dashboard')

            self.descr1 = QtWidgets.QLabel('Thing 1')
            self.check1 = QtWidgets.QCheckBox()

            self.descr2 = QtWidgets.QLabel('Thing 2')
            self.check2 = QtWidgets.QCheckBox()

        def init_layout(self):
            self.layout = QtWidgets.QVBoxLayout()

            self.grid = QtWidgets.QGridLayout()
            self.grid.addWidget(self.descr1, 0, 0)
            self.grid.addWidget(self.check1, 0, 1)
            self.grid.addWidget(self.descr2, 1, 0)
            self.grid.addWidget(self.check2, 1, 1)

            self.layout.addWidget(self.label)
            self.layout.addLayout(self.grid)
            self.setLayout(self.layout)

        def setDirection(self, direction):
            self.layout.setDirection(direction)


    class DockContainer(QtWidgets.QStackedWidget):

        def __init__(self):

            super(DockContainer, self).__init__()


    class MyDock(QtWidgets.QDockWidget):

        def __init__(self):

            super(MyDock, self).__init__()

            self.dockLocationChanged.connect(self.dock_location_changed)

        def dock_location_changed(self, area):
            top    = QtCore.Qt.DockWidgetArea.TopDockWidgetArea
            bottom = QtCore.Qt.DockWidgetArea.BottomDockWidgetArea
            # left   = QtCore.Qt.DockWidgetArea.LeftDockWidgetArea
            # right  = QtCore.Qt.DockWidgetArea.RightDockWidgetArea

            container_object = self.widget().currentWidget()

            if area in [top, bottom]:
                container_object.setDirection(QtWidgets.QBoxLayout.LeftToRight)
            else:
                container_object.setDirection(QtWidgets.QBoxLayout.TopToBottom)


    class MyWindow(QtWidgets.QMainWindow):

        def __init__(self, parent=None):
            super(MyWindow, self).__init__(parent=parent)

            self.resize(600, 400)

            self.init_widgets()
            self.init_layout()

        def init_widgets(self):

            self.tab_widget = QtWidgets.QTabWidget()
            self.tab1 = QtWidgets.QLabel('Primary content')
            self.tab2 = QtWidgets.QLabel('Secondary content')
            self.tab_widget.addTab(self.tab1, 'Primary')
            self.tab_widget.addTab(self.tab2, 'Secondary')
            self.tab_widget.currentChanged.connect(self.tab_selected)

            self.primary_dashboard = PrimaryDashboard()
            self.secondary_dashboard = SecondaryDashboard()

            self.dashboard = DockContainer()
            self.dashboard.addWidget(self.primary_dashboard)
            self.dashboard.addWidget(self.secondary_dashboard)
            self.dashboard.setCurrentWidget(self.primary_dashboard)

            self.dock = MyDock()
            self.dock.setWidget(self.dashboard)
            self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.dock)

        def init_layout(self):
            self.main_layout = QtWidgets.QVBoxLayout()
            self.main_layout.addWidget(self.tab_widget)

            self.main_widget = QtWidgets.QWidget()
            self.main_widget.setLayout(self.main_layout)
            self.setCentralWidget(self.main_widget)

        def tab_selected(self):
            tab_index = self.tab_widget.currentIndex()
            if self.tab_widget.tabText(tab_index) == 'Secondary':
                self.dashboard.setCurrentWidget(self.secondary_dashboard)
                self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.dock)
            else:  # Primary
                self.dashboard.setCurrentWidget(self.primary_dashboard)
                self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.dock)


    if __name__ == '__main__':
        app = QtWidgets.QApplication(sys.argv)

        window = MyWindow()
        window.show()
        sys.exit(app.exec_())

将一个应用程序视为一组 Matryoshka dolls. An inner doll's size dictates that of the subsequent outer ones. Clearly, an inner doll cannot be bigger than the one that contains it! QWidgets 相似建模。

By default, composite widgets which do not provide a size hint will be sized according to the space requirements of their child widgets.

QWidget.sizeHint() 的文档接着说,

The default implementation of QWidget.sizeHint() returns an invalid size if there is no layout for this widget, and returns the layout’s preferred size otherwise.

总而言之,小部件的大小根据布局由内而外调整。

如果您要为 基本应用程序代码 中的每个对象实现 resizeEvent1,您将请参阅以下尺码序列,

  1. PrimaryDashboard resizeEvent
  2. DockContainer resizeEvent
  3. MyDock resizeEvent

这是我们期望的嵌套。首先查询 PrimaryDashboard 的大小,然后是 DockContainer,然后是 MyDock。从技术上讲,它一直是小部件。但是,PrimaryDashboard 包含的按钮和标签在大多数情况下应该小于 MainWindow 的 width/height。序列中第一个显着影响码头尺寸的娃娃是 PrimaryDashboard.

使用 event.size() 检查 resizeEvent,我们可以看到水平仪表板的合理最小高度是 120 像素,而垂直方向具有合理的最小宽度146。然后可以将 sizeHint() 设置为 return minimumSizeHint() 并使每个仪表板的最小值 return (146, 120)2。实际上,这告诉应用程序更喜欢 (146, 120) 的最小大小,同时仍允许在一般情况下调整大小。

def sizeHint(self):
    return self.minimumSizeHint()

def minimumSizeHint(self):
    return QtCore.QSize(146, 120)

诚然,使用固定尺寸可能很危险,因为根据定义,绝对值是无情的且不灵活的。但是,内容可能具有自然的最小大小 3。我们可以在整个应用程序上简单地 setMinimumSize() 以不允许调整大小小于我们的 minimumSizeHint().

要更改停靠小部件内容的方向,我们可以使用 dockLocationChanged 信号。我们还可以使代码比问题中的代码更整洁。我们可以在 MyWindow 内的实例级别连接它,而不是在停靠小部件内连接信号。事实上,根本不需要定义MyDock。一个普通的 QDockWidget 就足够了。

具有最小初始大小的停靠点

import qtpy
from qtpy import QtWidgets, QtGui, QtCore
import sys


class PrimaryDashboard(QtWidgets.QWidget):

    def __init__(self):

        super(PrimaryDashboard, self).__init__()

        self.init_widgets()
        self.init_layout()

    def init_widgets(self):
        self.label = QtWidgets.QLabel('Primary dashboard')
        self.ok = QtWidgets.QPushButton('OK')
        self.cancel = QtWidgets.QPushButton('Cancel')

    def init_layout(self):
        self.layout = QtWidgets.QHBoxLayout()
        self.layout.addWidget(self.label)
        self.layout.addWidget(self.ok)
        self.layout.addWidget(self.cancel)
        self.setLayout(self.layout)

    def setDirection(self, direction):
        self.layout.setDirection(direction)

    def sizeHint(self):
        return self.minimumSizeHint()

    def minimumSizeHint(self):
        return QtCore.QSize(146, 120)


class SecondaryDashboard(QtWidgets.QWidget):

    def __init__(self):

        super(SecondaryDashboard, self).__init__()

        self.init_widgets()
        self.init_layout()

    def init_widgets(self):
        self.label = QtWidgets.QLabel('Secondary dashboard')

        self.descr1 = QtWidgets.QLabel('Thing 1')
        self.check1 = QtWidgets.QCheckBox()

        self.descr2 = QtWidgets.QLabel('Thing 2')
        self.check2 = QtWidgets.QCheckBox()

    def init_layout(self):
        self.layout = QtWidgets.QVBoxLayout()

        self.grid = QtWidgets.QGridLayout()
        self.grid.addWidget(self.descr1, 0, 0)
        self.grid.addWidget(self.check1, 0, 1)
        self.grid.addWidget(self.descr2, 1, 0)
        self.grid.addWidget(self.check2, 1, 1)

        self.layout.addWidget(self.label)
        self.layout.addLayout(self.grid)
        self.setLayout(self.layout)

    def setDirection(self, direction):
        self.layout.setDirection(direction)

    def sizeHint(self):
        return self.minimumSizeHint()

    def minimumSizeHint(self):
        return QtCore.QSize(146, 120)


class DockContainer(QtWidgets.QStackedWidget):

    def __init__(self):

        super(DockContainer, self).__init__()

    def dock_location_changed(self, area):
        top    = QtCore.Qt.DockWidgetArea.TopDockWidgetArea
        bottom = QtCore.Qt.DockWidgetArea.BottomDockWidgetArea

        container_object = self.currentWidget()

        if area in [top, bottom]:
            container_object.setDirection(QtWidgets.QBoxLayout.LeftToRight)
        else:
            container_object.setDirection(QtWidgets.QBoxLayout.TopToBottom)


class MyWindow(QtWidgets.QMainWindow):

    def __init__(self, parent=None):
        super(MyWindow, self).__init__(parent=parent)

        # Force minimumSize to ensure a sensible dashboard size
        self.setMinimumSize(QtCore.QSize(600, 400))

        self.init_widgets()
        self.init_layout()

    def init_widgets(self):

        self.tab_widget = QtWidgets.QTabWidget()
        self.tab1 = QtWidgets.QLabel('Primary content')
        self.tab2 = QtWidgets.QLabel('Secondary content')
        self.tab_widget.addTab(self.tab1, 'Primary')
        self.tab_widget.addTab(self.tab2, 'Secondary')
        self.tab_widget.currentChanged.connect(self.tab_selected)

        self.primary_dashboard = PrimaryDashboard()
        self.secondary_dashboard = SecondaryDashboard()

        self.dashboard = DockContainer()
        self.dashboard.addWidget(self.primary_dashboard)
        self.dashboard.addWidget(self.secondary_dashboard)
        self.dashboard.setCurrentWidget(self.primary_dashboard)

        self.dock = QtWidgets.QDockWidget()
        self.dock.setWidget(self.dashboard)
        self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.dock)

        # Connect signal at the main application level
        self.dock.dockLocationChanged.connect(self.dashboard.dock_location_changed)

    def init_layout(self):
        self.main_layout = QtWidgets.QVBoxLayout()
        self.main_layout.addWidget(self.tab_widget)

        self.main_widget = QtWidgets.QWidget()
        self.main_widget.setLayout(self.main_layout)
        self.setCentralWidget(self.main_widget)

    def tab_selected(self):
        tab_index = self.tab_widget.currentIndex()
        if self.tab_widget.tabText(tab_index) == 'Secondary':
            self.dashboard.setCurrentWidget(self.secondary_dashboard)
            self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.dock)
        else:  # Primary
            self.dashboard.setCurrentWidget(self.primary_dashboard)
            self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.dock)


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)

    window = MyWindow()
    window.show()
    sys.exit(app.exec_())


1. 如何实现这样一个 resizeEvent 以查看谁在调整大小:

    def resizeEvent(self, event):
        print('resizeEvent for ', self, flush=True)

2. 一个很自然的问题是,"Why not simply set the sizeHint() to return the minimum size instead of calling minimumSizeHint()? The best response I have is, "这样不行。" 只设置 sizeHint() 不会像您预期的那样将扩展坞的大小调整到最小。

sizeHint()minimumSizeHint()方法是虚函数。我的猜测是 Qt 有其他我们不知道的功能,它们独立地引用这些方法,要求我们以这种方式定义事物。

3. 如果内容是地图,例如,用户不太可能希望地图是 10px x 10px。此外,可以合理假设用户不会使用低于 600 x 400 的屏幕分辨率,除非他们使用移动设备。但是,如果您使用 PySide 或 PyQt5 进行移动开发,您应该问自己一些重要的问题,例如 "Why?".