拉伸布局时无法使 spacer 占据小部件之间的空白 space

Can't make a spacer occupy the blank space between widgets when stretching a layout

我最近开始使用 PyQt5,现在正在尝试为应用程序编写一个界面。

我想要一个带有不同工具的侧边栏,本质上是 window 底部的一个 I/O 面板。

此 I/O 面板应仅包含 2 QPushButtons - Ok and Cancel.

当我更改应用程序的大小时 window,右按钮 (Ok) 应该留在右下角,而左按钮 (Cancel) - 在左下角 -手角,就在侧边栏旁边(但不在下方)。为此,我在按钮之间添加了一个水平分隔符。

但是,这两个按钮始终位于 window 的右下角。我玩过 QSizePolicy 参数,但它似乎没有做任何事情。我能够得到我想要的东西的唯一方法是将构造函数中的水平间隔宽度设置为非零值,但我对这个解决方案不满意。我应该在下面的代码中更改什么以获得我想要的?

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *

class Sidebar(QWidget):
    def __init__(self, parent=None):
        super().__init__()
        layout = QVBoxLayout()
        layout.addWidget(QPushButton(maximumWidth = 200, minimumHeight = 50))
        layout.addWidget(QPushButton(maximumWidth = 200, minimumHeight = 50))
        layout.addWidget(QPushButton(maximumWidth = 200, minimumHeight = 50))
        layout.addWidget(QPushButton(maximumWidth = 200, minimumHeight = 50))
        layout.addWidget(QPushButton(maximumWidth = 200, minimumHeight = 50))
        layout.addWidget(QPushButton(maximumWidth = 200, minimumHeight = 50))
        layout.addItem(QSpacerItem(200, 0, QSizePolicy.Maximum, QSizePolicy.Expanding))
        
        self.setLayout(layout)

        self.setMaximumWidth(200)
        sizePolicy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Expanding)
        self.setSizePolicy(sizePolicy)
        layout.setSpacing(0)
        layout.setContentsMargins(0, 0, 0, 0)

class IOPanel(QWidget):
    def __init__(self, parent=None):
        super().__init__()
        layout = QHBoxLayout()
        layout.addWidget(QPushButton("Cancel", maximumWidth = 100, maximumHeight = 30), 0, Qt.AlignLeft)
        layout.addItem(QSpacerItem(0, 30, QSizePolicy.Expanding, QSizePolicy.Maximum))
        layout.addWidget(QPushButton("Ok", maximumWidth = 100, minimumHeight = 30), 0, Qt.AlignRight)
        
        self.setLayout(layout)

        sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum)
        self.setSizePolicy(sizePolicy)

class Canvas(QWidget):
    def __init__(self, parent=None):
        super().__init__()
        layout = QVBoxLayout()
        panel = IOPanel()
        layout.addWidget(QWidget(), 0, Qt.AlignTop)
        layout.addWidget(panel, 0, Qt.AlignBottom)

        self.setLayout(layout)

        sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.setSizePolicy(sizePolicy)

class Interface(QWidget):
    def __init__(self, parent=None):
        super().__init__()
        layout = QHBoxLayout()
        layout.addWidget(Sidebar(), 0, Qt.AlignLeft)
        layout.addWidget(Canvas(), 0, Qt.AlignRight)
        layout.setContentsMargins(0, 0, 0, 0)

        self.setLayout(layout)
        sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.setSizePolicy(sizePolicy)

class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__()
        interface = Interface()
        self.setCentralWidget(interface)
    
def main():
    import sys
    app = QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

您可以在界面小部件中将布局设置为 QGridLayout。您可以调整行数和列数以适合您的 setup/goal.

....
class Canvas(QWidget):
    def __init__(self, parent=None):
        super().__init__()
        layout = QVBoxLayout()
        #panel = IOPanel() # Remove IOPanel from Canvas widget add it to Main Interface
        layout.addWidget(QWidget(), 0, Qt.AlignTop)
        #layout.addWidget(panel, 0, Qt.AlignBottom)

        self.setLayout(layout)

        sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.setSizePolicy(sizePolicy)

class Interface(QWidget):
    def __init__(self, parent=None):
        super().__init__()
        layout = QGridLayout()
        layout.addWidget(Sidebar(), 0, 0, 4, 1)
        layout.addWidget(Canvas(), 0, 1, 3, 3)
        layout.addWidget(IOPanel(), 3, 1, 3, 3)
        layout.setContentsMargins(0, 0, 0, 0)

        self.setLayout(layout)
        sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.setSizePolicy(sizePolicy)

问题不在于布局的设置方式,而是由于 addWidget()(强调我的)的对齐参数的使用:

The alignment is specified by alignment. The default alignment is 0, which means that the widget fills the entire cell.

这意味着指定对齐方式 不会 使小部件填充整个单元格,而只会填充其 首选 大小(什么是 return由 sizeHint() 编辑)。此外,由于您已经将 spacer 添加到侧边栏,并且按钮将位于 canvas 垂直布局的底部,因此无论如何都不需要指定对齐方式。

因此,解决方案很简单:从 addWidget() 中删除所有不需要的参数(包括已经默认为 0 的拉伸):

class Interface(QWidget):
    def __init__(self, parent=None):
        # ...
        layout.addWidget(Sidebar())
        layout.addWidget(Canvas())

请注意,您可能也想对 Canvas 中的所有 addWidget() 执行相同的操作。

其他注意事项:

  • QPushButton 已经尝试展开,只要它所添加的布局允许它;因为您已经为面板设置了最大宽度,所以也为按钮设置它是没有意义的;
  • 盒装布局已经为 spacers(addStretch()addSpacing())提供了方便的功能,它们在内部自行创建 QSpacerItems:您可以在IOPanel;
  • 的按钮
  • 除非你需要在大小策略中设置高级方面,否则创建新对象没有用:你可以直接使用self.setSizePolicy(<horizontal policy>, <vertical policy>)代替;
  • 多级小部件及其布局会增加对象之间的边距量(这就是为什么 IO 按钮周围有很多 space 的原因)除非为 all 涉及布局;除非您选择网格布局,否则请考虑使用较少的容器小部件并使用 嵌套 布局 addLayout();另一种选择是为所有 PM_Layout*Margin 指标覆盖 QStyle 的 pixelMetric 和 return 0,但除非明确设置,否则默认 all 布局边距为 0;
  • 为面板设置的大部分与尺寸相关的代码都有些样板;考虑使用更简单的概念并避免不必要的调用:删除 spacer 项目并添加拉伸,使用样式表 min-size 属性并正确实施 sizeHint();如您所见,生成的代码更加清晰易读:
class Sidebar(QWidget):
    def __init__(self, parent=None):
        super().__init__()

        layout = QVBoxLayout(self)
        layout.setSpacing(0)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(QPushButton())
        layout.addWidget(QPushButton())
        layout.addWidget(QPushButton())
        layout.addWidget(QPushButton())
        layout.addWidget(QPushButton())
        layout.addWidget(QPushButton())
        layout.addStretch()

        self.setStyleSheet('''
            Sidebar > QPushButton { min-height: 50; }
        ''')
        self.setMaximumWidth(200)

    def sizeHint(self):
        return QSize(self.maximumWidth(), super().sizeHint().height())