清除PyQt布局中的所有小部件

64

有没有一种方法可以清除(删除)布局中的所有小部件?

self.plot_layout = QtGui.QGridLayout()
self.plot_layout.setGeometry(QtCore.QRect(200,200,200,200))
self.root_layout.addLayout(self.plot_layout)
self.plot_layout.addWidget(MyWidget())

现在我想用一个新窗口小部件替换plot_layout中的窗口小部件,有没有一种简单的方法可以清除plot_layout中的所有窗口小部件?我没有看到任何方法可以这样做。

14个回答

134

经过大量的研究(这个花费了相当长的时间,所以我在这里添加以作为将来的参考),我找到了一种真正清除和删除布局中小部件的方法:

for i in reversed(range(layout.count())): 
    layout.itemAt(i).widget().setParent(None)

关于 QWidget 的文档说明如下:

当它的父类被删除时,新的窗口部件也会被删除。

重要提示:在移除元素时需要反向循环,因为从开头移除会导致元素位置发生变化,进而影响布局顺序。

测试并确认布局为空:

for i in range(layout.count()): print i
似乎还有另一种方法来做这件事。不要使用setParent函数,而是像这样使用deleteLater()函数:

deleteLater()

for i in reversed(range(layout.count())): 
    layout.itemAt(i).widget().deleteLater()

文档说明了 QObject.deleteLater (self)

安排此对象进行删除。

然而,如果运行上述指定的测试代码,则会打印一些值。这表明布局仍然存在项目,而不是使用setParent代码。


使用 for i in reversed(range(layout.count()-1)): 是正确的,可以避免越界错误。 - Max
3
应该使用takeAt()而不是itemAt()takeAt()函数还可以将没有父级的空白/拉伸项从QBoxLayout中删除:widget = layout.takeAt(i).widget(); if widget is not None: widget.setParent(None) - Stefan Scherfke
1
你确定 setParent(None) 的方法可行吗?我认为这并不会删除 Widget。"当其父对象被删除时,新的控件也会被删除" 对我来说听起来像是在调用父对象的 deleteLater() 函数时进行递归调用。 - Skandix
1
我写了一个 简单的测试,证明 setParent(None) 不会删除小部件。即使这个命令以清除布局的方式回答了问题,也不应该被推荐。这会导致内存泄漏! - Skandix
layout.item(i).widget().setParent(None) 不会删除小部件,而是将其父级设置为 None。这意味着小部件仍在内存中,但未附加到任何父级。因此会导致内存泄漏。您应该使用 deleteLater() 函数。 - Yakup Beyoglu
这个回答和其他回答对我帮助很大,但我注意到一个小问题。将小部件的父级设置为None会导致小部件变成一个可能会在显示器上短暂出现的窗口。您可以先隐藏它以防止这种情况发生,或者使用deleteLater。请参阅https://www.pythonguis.com/faq/pyqt-widgets-appearing-as-separate-windows/。 - undefined

41

这可能有点晚了,但只是想为将来参考添加这个:

def clearLayout(layout):
  while layout.count():
    child = layout.takeAt(0)
    if child.widget():
      child.widget().deleteLater()

参考自Qt文档http://doc.qt.io/qt-5/qlayout.html#takeAt。请记住,当您在while或for循环中从布局中移除子项时,实际上是修改了布局中每个子项的索引号。这就是为什么使用for i in range()循环会遇到问题的原因。


1
我猜你的意思是child.widget().deleteLater()... 非常感谢你提供这个解决方案 - 看起来相当优雅。如果deleteLater()出现任何问题,我可能会尝试像其他人建议的那样使用close()或setParent(None)。 - flutefreak7

29

如果您不需要将新的小部件放入您的布局中,则来自PALEN的答案效果很好。

for i in reversed(range(layout.count())): 
    layout.itemAt(i).widget().setParent(None)

如果您多次清空和填充布局,或者使用许多小部件进行填充,最终会出现“分段错误(核心已转储)”。看起来布局保留了一个小部件列表,并且该列表的大小受到限制。

如果您以这种方式删除小部件:

for i in reversed(range(layout.count())): 
    widgetToRemove = layout.itemAt(i).widget()
    # remove it from the layout list
    layout.removeWidget(widgetToRemove)
    # remove it from the gui
    widgetToRemove.setParent(None)

你不会遇到那个问题。


1
非常感谢,这让我疯狂了,因为我清除并添加了几次项目后,一直出现“进程以退出代码139结束”的错误。 - Ramon Blanquer
2
setParent(None)不会删除小部件。请查看我的评论和对palens答案的回复。这也是Segmentation fault的原因。请改用deleteLater() - Skandix
widget.setAttribute(QtCore.Qt.WA_DeleteOnClose) widget.close() gridLayout.removeWidget(widget)这对我完美地起作用了。 - HOuadhour

20

这是我清理布局的方法:

def clearLayout(layout):
    if layout is not None:
        while layout.count():
            child = layout.takeAt(0)
            if child.widget() is not None:
                child.widget().deleteLater()
            elif child.layout() is not None:
                clearLayout(child.layout())

显然从https://dev59.com/qWDVa4cB1Zd3GeqPfqsa#9383780中获取? - bugmenot123

14
你可以使用 `widget` 的 `close()` 方法:
for i in range(layout.count()): layout.itemAt(i).widget().close()

我猜这不会删除它们,只是关闭它们,你可以用接受的答案测试一下:for i in range(layout.count()): print i / print(i) - pippo1980
PyQt6 QWidget::close()方法的文档指出,如果设置了Qt::WA_DeleteOnClose标志,该方法将删除小部件。 - Michal

7

我使用:

    while layout.count() > 0: 
        layout.itemAt(0).setParent(None)

1
对于我的PyQT版本,必须在中间添加一个.widget()调用:layout.itemAt(0).widget().setParent(None) - Ghost8472

4

大部分现有答案都没有考虑嵌套布局,因此我编写了一个递归函数,给定一个布局,它将递归删除其中的所有内容以及其中的所有布局。这是代码:

def clearLayout(layout):
    print("-- -- input layout: "+str(layout))
    for i in reversed(range(layout.count())):
        layoutItem = layout.itemAt(i)
        if layoutItem.widget() is not None:
            widgetToRemove = layoutItem.widget()
            print("found widget: " + str(widgetToRemove))
            widgetToRemove.setParent(None)
            layout.removeWidget(widgetToRemove)
        elif layoutItem.spacerItem() is not None:
            print("found spacer: " + str(layoutItem.spacerItem()))
        else:
            layoutToRemove = layout.itemAt(i)
            print("-- found Layout: "+str(layoutToRemove))
            clearLayout(layoutToRemove)

我可能没有考虑到所有的UI类型,不确定。希望这有所帮助!


支持递归版本。我也喜欢你添加了许多其他答案忽略的 None 检查。我之前也遇到过在 setParents 之前调用 removeWidget 的问题,就像其他人建议的那样。 - Cerno
话虽如此,我认为这段代码可以稍微整理一下。如果我正确地阅读了其他评论,那么使用takeAt()而不是itemAt()将隐式处理间隔符,因此您可能可以摆脱elif分支。另一件事是,您调用widget()和itemAt()的频率比您需要的要高。调用它们一次并使用变量将使代码更易读。 - Cerno
1
还有一件事:我认为你的变量名有点不对。当你调用itemAt()时,结果变量应该被称为item而不是layout,因为该函数返回一个QLayoutItem而不是QLayout。这也使得你的函数实际上期望什么样的输入有点混淆。你的输入变量被称为layout,但它实际上期望一个QLayoutItem,对吧?命名在这里很重要,以使代码更易于理解。 - Cerno

3

我的解决方案是重写QWidget的setLayout方法。下面的代码将布局更新为新布局,其中可能包含已显示的项目,也可能不包含。您只需创建一个新的布局对象,添加您想要的任何内容,然后调用setLayout。当然,您也可以调用clearLayout以删除所有内容。

def setLayout(self, layout):
    self.clearLayout()
    QWidget.setLayout(self, layout)

def clearLayout(self):
    if self.layout() is not None:
        old_layout = self.layout()
        for i in reversed(range(old_layout.count())):
            old_layout.itemAt(i).widget().setParent(None)
        import sip
        sip.delete(old_layout)

2

我之前遇到了一些问题,之前提到的解决方案中仍有残留的小部件导致问题。我猜测删除操作被安排但未完成。此外,我还不得不将小部件的父级设置为“None”。 这是我的解决方案:

def clearLayout(layout):
    while layout.count():
        child = layout.takeAt(0)
        childWidget = child.widget()
        if childWidget:
            childWidget.setParent(None)
            childWidget.deleteLater()

2

文档中提到:

要从布局中移除一个小部件,请调用 removeWidget()。在小部件上调用 QWidget.hide() 也会有效地将小部件从布局中移除,直到调用 QWidget.show()

removeWidget 继承自 QLayout,这就是为什么它没有列在 QGridLayout 方法中的原因。


但是 removeWidget() 需要一个小部件作为参数 removeWidget (self, QWidget w)。我没有这个小部件的引用。 - Falmarri
1
@Falmarri:使用grid.itemAt(idx).widget()按索引获取。 - user395760

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接