PyQt5 - 动态添加小部件到布局

Posted

技术标签:

【中文标题】PyQt5 - 动态添加小部件到布局【英文标题】:PyQt5 - Adding widget to layout dynamically 【发布时间】:2020-07-25 04:46:25 【问题描述】:

我正在尝试创建一个自定义小部件,用户可以在其中动态地将更多 [其他] 自定义小部件添加到窗口中。这是我的尝试:

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt


class LoadModelWidget(QtWidgets.QWidget):
    def __init__(self, *args, **kwargs):
        super(LoadModelWidget, self).__init__(*args, **kwargs)

        # ------------------------ GUI Components ------------------------ #
        self.setFixedHeight(100)
        self._container = QtWidgets.QGroupBox(self)
        self._container.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding))
        self._container.setFixedHeight(100)
        self._container.setFixedWidth(210)
        layout = QtWidgets.QGridLayout()

        self._model_to_select_label = QtWidgets.QLabel("Selected model")
        layout.addWidget(self._model_to_select_label, 0, 0)
        self._model_to_select_list = QtWidgets.QComboBox()
        layout.addWidget(self._model_to_select_list, 0, 1)
        _slice_method_label = QtWidgets.QLabel("Slice method")
        layout.addWidget(_slice_method_label, 1, 0)
        self._list_of_slicing_method = QtWidgets.QComboBox()
        layout.addWidget(self._list_of_slicing_method, 1, 1)

        self._bottom_radius_label = QtWidgets.QLabel("Bottom radius")
        layout.addWidget(self._bottom_radius_label, 2, 0)
        self._bottom_radius_edit = QtWidgets.QLineEdit()
        layout.addWidget(self._bottom_radius_edit, 2, 1)
        self._top_radius_label = QtWidgets.QLabel("Top radius")
        layout.addWidget(self._top_radius_label, 3, 0)
        self._top_radius_edit = QtWidgets.QLineEdit()
        layout.addWidget(self._top_radius_edit, 3, 1)
        self._height_label = QtWidgets.QLabel("Height")
        layout.addWidget(self._height_label, 4, 0)
        self._height_edit = QtWidgets.QLineEdit()
        layout.addWidget(self._height_edit, 4, 1)

        self._x_dir_label = QtWidgets.QLabel("X direction")
        layout.addWidget(self._x_dir_label, 5, 0)
        self._x_dir_edit = QtWidgets.QLineEdit()
        layout.addWidget(self._x_dir_edit, 5, 1)
        self._y_dir_label = QtWidgets.QLabel("Y direction")
        layout.addWidget(self._y_dir_label, 6, 0)
        self._y_dir_edit = QtWidgets.QLineEdit()
        layout.addWidget(self._y_dir_edit, 6, 1)
        self._z_dir_label = QtWidgets.QLabel("Z direction")
        layout.addWidget(self._z_dir_label, 7, 0)
        self._z_dir_edit = QtWidgets.QLineEdit()
        layout.addWidget(self._z_dir_edit, 7, 1)

        self._remove_btn = QtWidgets.QPushButton("Remove model")
        layout.addWidget(self._remove_btn, 8, 0)

        self._bottom_radius_label.setVisible(False)
        self._bottom_radius_edit.setVisible(False)
        self._top_radius_label.setVisible(False)
        self._top_radius_edit.setVisible(False)
        self._height_label.setVisible(False)
        self._height_edit.setVisible(False)
        self._x_dir_label.setVisible(False)
        self._x_dir_edit.setVisible(False)
        self._y_dir_label.setVisible(False)
        self._y_dir_edit.setVisible(False)
        self._z_dir_label.setVisible(False)
        self._z_dir_edit.setVisible(False)

        self._container.setLayout(layout)

        # -------------------- Other attributes -------------------- "
        self._filename = ""

        # -------------------- Assign slot and signal -------------------- "
        self._list_of_slicing_method.currentIndexChanged.connect(self.slice_method_changed)
        self._remove_btn.clicked.connect(self.remove_model)

    def set_model_dir(self, filename, filedir, suggested_slicing_method="Parallel"):
        # Assign only the name of the file, not the whole stl model
        self._filename = filename
        self._filedir = filedir

        self._list_of_slicing_method.addItem("Parallel")
        self._list_of_slicing_method.addItem("Revolution")
        self._list_of_slicing_method.addItem("Radial")
        self._list_of_slicing_method.addItem("ParallelCurve")

        self._list_of_slicing_method.setCurrentText(suggested_slicing_method)

    def get_model_full_dir(self):
        return self._filedir + "\\" + self._filename

    def remove_model(self):
        self._filename = ""
        self._filedir = ""
        self._list_of_slicing_method.clear()

    def slice_method_changed(self):
        is_parallel = self._list_of_slicing_method.currentText() == "Parallel"
        is_radial = self._list_of_slicing_method.currentText() == "Radial"

        self._bottom_radius_label.setVisible(is_radial)
        self._bottom_radius_edit.setVisible(is_radial)
        self._top_radius_label.setVisible(is_radial)
        self._top_radius_edit.setVisible(is_radial)
        self._height_label.setVisible(is_radial)
        self._height_edit.setVisible(is_radial)

        self._x_dir_label.setVisible(is_parallel)
        self._x_dir_edit.setVisible(is_parallel)
        self._y_dir_label.setVisible(is_parallel)
        self._y_dir_edit.setVisible(is_parallel)
        self._z_dir_label.setVisible(is_parallel)
        self._z_dir_edit.setVisible(is_parallel)

        padding = 50
        minimum_height = 100
        new_height = (self._bottom_radius_label.height() + padding)*is_radial + (self._top_radius_edit.height() + padding)*is_radial + (self._height_label.height() + padding)*is_radial + (self._x_dir_label.height() + padding)*is_parallel + (self._y_dir_label.height() + padding)*is_parallel + (self._z_dir_label.height() + padding)*is_parallel
        if (new_height > minimum_height):
            self._container.setFixedHeight(new_height)
            self.setFixedHeight(new_height)
        else:
            self._container.setFixedHeight(minimum_height)
            self.setFixedHeight(minimum_height)

class LoadModelColumn(QtWidgets.QWidget):
    def __init__(self, *args, **kwargs):
        super(LoadModelColumn, self).__init__(*args, **kwargs)

        # ------------------------ GUI Components ------------------------ #
        self._layout = QtWidgets.QGridLayout()
        self._add_model_btn = QtWidgets.QPushButton("Add model(s)")
        self._layout.addWidget(self._add_model_btn, 0, 0)
        self._clear_all_btn = QtWidgets.QPushButton("Clear all")
        self._layout.addWidget(self._clear_all_btn, 0, 1)

        self._all_load_model_widgets = []

        self.verticalSpacer = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.MinimumExpanding)
        self._layout.addItem(self.verticalSpacer)

        self._load_model_group = QtWidgets.QGroupBox()
        self._load_model_group.setLayout(self._layout)

        self._scroll_area = QtWidgets.QScrollArea(self)
        self._scroll_area.setWidgetResizable(True)
        self._scroll_area.setFixedHeight(600)
        self._scroll_area.setFixedWidth(250)
        self._scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        self._scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self._scroll_area.setWidget(self._load_model_group)

        # -------------------- Assign slot and signal -------------------- "
        self._clear_all_btn.clicked.connect(self._clear_all_models)
        self._add_model_btn.clicked.connect(self._add_models)

    def _clear_all_models(self):
        for widget in self._all_load_model_widgets:
            self._layout.removeWidget(widget)
            widget.deleteLater()

        self._all_load_model_widgets = []

    def _add_models(self):
        widget = LoadModelWidget()
        widget.set_model_dir("hi", "hello")
        self._all_load_model_widgets.append(widget)
        self._layout.addWidget(widget, len(self._all_load_model_widgets), 0, 1, 0)

目前,当添加一个小部件时,由于某种原因,它会尝试占据所有滚动区域。但很明显,我真的不希望这样,因为我希望它只分配到必要的空间。

以下是用户按下“添加模型”按钮时它正在执行的操作的示例:

但这是我想要的:

【问题讨论】:

【参考方案1】:

您正在将小部件添加到***窗口小部件,但您没有为它们设置布局。

您的问题的简单答案是正确设置您正在创建的小部件的布局。在LoadModelWidget__init__底部添加以下内容:

        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(self._container)

这在底部是__init__LoadModelColumn

        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(self._scroll_area)

然后删除所有不必要的setFixedHeight,这没有任何意义。

我强烈建议您更好地研究layout manager work,因为所有需要调整其内容的小部件也需要您为它们设置布局。 然后我还建议您更好地阐明类的对象结构。例如,如果 LoadModelWidget 始终包含一个组框,则无需从 QWidget 子类化 LoadModelWidget:只需子类化 QGroupBox。

作为附注(但仍然非常重要),请始终尽量减少示例(阅读有关创建 minimal, reproducible examples 的更多信息(您甚至可能最终自己找到解决方案) ; 另外,这也很重要,始终努力使这些示例立即可重现:从您的代码中,不清楚清楚哪个类被用作父级),你的例子很长;我们必须能够复制和粘贴您的代码并尽可能轻松地运行它:这意味着我们想要帮助您,但您必须帮助我们帮助你。

【讨论】:

苛刻,但非常真实。感谢 QGroupBox 的提示,真的让我大开眼界。我是前端开发的新手,所以我还在学习。但是,当我按照您的建议将布局添加到 init 的底部时,我仍然无法得到我想要的。这几乎就像我缺少一个垫片(我认为我已经包含在 self._layout.addItem(self.verticalSpacer) 中)。有什么想法吗? 在那张纸条上,我想我确实为我的两个班级添加了一个布局(尽管我可能没有正确添加它)。使用 LoadModelWidget,我将布局添加到 QGroupBox (_containter)。使用 LoadModelColumn,我还将布局添加到 QGroupBox (_load_model_group),然后将其包含在滚动区域内。如果我理解正确,滚动区域只是使 QGroupBox 可滚动,因此不需要布局。 如果你需要一个“间隔”来确保之前的小部件只使用最小的所需大小,你可以使用addStretch(),只是要注意始终使用insertWidget(index, widget)和索引等于layout.count() - 1。关于第二条评论:如前所述,添加一个带有单个子小部件且没有为父级设置布局管理器的父 QWidget 只会使事情变得不必要地复杂;只需继承 QGroupBox (正确设置布局)并添加其实例:布局将自动确保内容正确调整大小。 澄清一下:你没有没有为这些类设置布局:你已经为孩子设置了布局。这意味着父小部件将对子内容一无所知;是的,您已经为这些父母设置了固定高度,但在大多数情况下效果不佳(实际上就像您的一样:大多数小部件大小 hints 实际上只在它们“重新实际显示,而不是在__init__没有)。布局管理器对于这些目的也很有用:它们确保在任何情况下都正确调整小部件的大小,即使在它们的初始化之后它们的孩子。 感谢您解决了我的一些困惑!目前我正在使用网格布局,我认为我不能为此使用 addStretch 。有没有类似的?

以上是关于PyQt5 - 动态添加小部件到布局的主要内容,如果未能解决你的问题,请参考以下文章

通过 pyqt5 动态添加和删除小部件

Pyqt5 addStretch 在小部件之间?

将自定义组件小部件动态添加到 Android 中的布局中

尽管更新了小部件,但 Pyqt5 更新的小部件未添加到布局中

如何将小部件动态添加到网格?

QT:如何在布局中动态添加和删除小部件?