Qt - remove all widgets from layout?
Asked Answered
A

13

70

This doesn't seem easy. Basically, I add QPushButtons through a function to a layout, and when the function executes, I want to clear the layout first (removing all QPushButtons and whatever else is in there), because more buttons just get appended to the scrollview.

header

QVBoxLayout* _layout;

cpp

void MainWindow::removeButtonsThenAddMore(const QString &item) {

//remove buttons/widgets

QVBoxLayout* _layout = new QVBoxLayout(this);

QPushButton button = new QPushButton(item);
_layout->addWidget(button);

QPushButton button = new QPushButton("button");
_layout->addWidget(button);

QWidget* widget = new QWidget();
widget->setLayout(_layout);

QScrollArea* scroll = new QScrollArea();
scroll->setWidget(widget);
scroll->show();

}
Alphonsealphonsine answered 24/11, 2010 at 22:37 Comment(3)
Why not add everything that will be later deleted to a widget (with zero margins for its top-level layout or whatever), add this (temporary) widget to your layout, and when done, just delete it (which will remove it with all its layouts and widgets and spacers alltogether)?Biodegradable
This comment of @Biodegradable is clearly the best answer of all. Easy, modular, this is good OOD and will prevent from many bugs that other answers could introduce.Wellfixed
@Wellfixed I appreciate the praise -- feel free to drop me an e-mail (my address is in the profile) if you'd like to discuss OOD (incl. in Qt apps) over beers one day :)Biodegradable
Z
55

I had the same problem: I have a game app whose main window class inherits QMainWindow. Its constructor looks partly like this:

m_scene = new QGraphicsScene;
m_scene->setBackgroundBrush( Qt::black );
...
m_view = new QGraphicsView( m_scene );
...
setCentralWidget( m_view );

When I want to display a level of the game, I instantiate a QGridLayout, into which I add QLabels, and then set their pixmaps to certain pictures (pixmaps with transparent parts). The first level displays fine, but when switching to the second level, the pixmaps from the first level could still be seen behind the new ones (where the pixmap was transparent).

I tried several things to delete the old widgets. (a) I tried deleting the QGridLayout and instantiating a new one, but then learned that deleting a layout does not delete the widgets added to it. (b) I tried calling QLabel::clear() on the new pixmaps, but that of course had only an effect on the new ones, not the zombie ones. (c) I even tried deleting my m_view and m_scene, and reconstructing them every time I displayed a new level, but still no luck.

Then (d) I tried one of the solutions given above, namely

QLayoutItem *wItem;
while (wItem = widget->layout()->takeAt(0) != 0)
    delete wItem;

but that didn't work, either.

However, googling further, I found an answer that worked. What was missing from (d) was a call to delete item->widget(). The following now works for me:

// THIS IS THE SOLUTION!
// Delete all existing widgets, if any.
if ( m_view->layout() != NULL )
{
    QLayoutItem* item;
    while ( ( item = m_view->layout()->takeAt( 0 ) ) != NULL )
    {
        delete item->widget();
        delete item;
    }
    delete m_view->layout();
}

and then I instantiate a new QGridLayout as with the first level, add the new level's widgets to it, etc.

Qt is great in many ways, but I do think this problems shows that things could be a bit easier here.

Zabrine answered 7/5, 2011 at 23:36 Comment(2)
Actually, hold on, I might have spoken too soon. I saw the second game level displayed correctly, but there's still something funny going on. I'll get back to this once I've figured it out.Zabrine
This solution doesn't consider nested layouts correctly. See Darko Maksimovic's answer for the correct solution.Bravar
C
50

Layout management page in Qt's help states:

The layout will automatically reparent the widgets (using QWidget::setParent()) so that they are children of the widget on which the layout is installed.

My conclusion: Widgets need to be destroyed manually or by destroying the parent WIDGET, not layout

Widgets in a layout are children of the widget on which the layout is installed, not of the layout itself. Widgets can only have other widgets as parent, not layouts.

My conclusion: Same as above

To @Muelner for "contradiction" "The ownership of item is transferred to the layout, and it's the layout's responsibility to delete it." - this doesn't mean WIDGET, but ITEM that is reparented to the layout and will be deleted later by the layout. Widgets are still children of the widget the layout is installed on, and they need to be removed either manually or by deleting the whole parent widget.

If one really needs to remove all widgets and items from a layout, leaving it completely empty, he needs to make a recursive function like this:

// shallowly tested, seems to work, but apply the logic

void clearLayout(QLayout* layout, bool deleteWidgets = true)
{
    while (QLayoutItem* item = layout->takeAt(0))
    {
        if (deleteWidgets)
        {
            if (QWidget* widget = item->widget())
                widget->deleteLater();
        }
        if (QLayout* childLayout = item->layout())
            clearLayout(childLayout, deleteWidgets);
        delete item;
    }
}
Cervical answered 16/8, 2011 at 11:16 Comment(2)
this is dangerous if the widgets have signals connected to them because there may be pending events at the time clearLayout(...) is called.Polyhistor
In order to prevent this problem with pending events, widget->deleteLater() should maybe be called instead of just delete ??Besmirch
P
14

This code deletes all its children. So everything inside the layout 'disappears'.

qDeleteAll(yourWidget->findChildren<QWidget *>(QString(), Qt::FindDirectChildrenOnly));

This deletes all direct widgets of the widget yourWidget. Using Qt::FindDirectChildrenOnly is essential as it prevents the deletion of widgets that are children of widgets that are also in the list and probably already deleted by the loop inside qDeleteAll.

Here is the description of qDeleteAll:

void qDeleteAll(ForwardIterator begin, ForwardIterator end)

Deletes all the items in the range [begin, end] using the C++ delete > operator. The item type must be a pointer type (for example, QWidget *).

Note that qDeleteAll needs to be called with a container from that widget (not the layout). And note that qDeleteAll does NOT delete yourWidget - just its children.

Procryptic answered 2/3, 2016 at 15:18 Comment(4)
I have a grid layout for a group box, and calling qDeleteAll for the group box crashes the program. I tried also calling it with the layout object, but it does nothing. I'm using QT 5.8 for a desktop app.Electrum
This is rather convenient.Weeds
@MarcoSulla Adding a widget to a layout does not parent the widget to the layout but to the layout's parent (a widget). So calling children() on a layout correctly returns an empty list.Procryptic
he said "remove", not delete (destroy the objects) . To remove a widget from a layout you don't need to destroy the object.Uninstructed
K
10

Untried: Why not create a new layout, swap it with the old layout and delete the old layout? This should delete all items that were owned by the layout and leave the others.

Edit: After studying the comments to my answer, the documentation and the Qt sources I found a better solution:

If you still have Qt3 support enabled, you can use QLayout::deleteAllItems() which is basically the same as hint in the documentation for QLayout::takeAt:

The following code fragment shows a safe way to remove all items from a layout:

QLayoutItem *child;
while ((child = layout->takeAt(0)) != 0) {
  ...
  delete child;
}

Edit: After further research it looks like both version above are equivalent: only sublayouts and widget without parents are deleted. Widgets with parent are treated in a special way. It looks like TeL's solution should work, you only should be careful not to delete any top-level widgets. Another way would be to use the widget hierarchy to delete widgets: Create a special widget without parent and create all your removeable widgets as child of this special widget. After cleaning the layout delete this special widget.

Katharinekatharsis answered 25/11, 2010 at 9:16 Comment(6)
This will not delete widgets that already exist in layout, as widgets belong to a parent widget, not the layout.Antistrophe
If you downvote someone, you should at least read the correspondig documentation of QLayout::addItem, which contains the note: he ownership of item is transferred to the layout, and it's the layout's responsibility to delete it.Katharinekatharsis
@Katharinekatharsis OK, let's read the documentation. First, the "addItem" method you cite is not used in the example, and is "not recommended for use in applications". Second, the documentation for "~QVBoxLayout()" clearly states that "The layout's widgets aren't destroyed.". It would seem that user118657's criticism is correct.Molini
The documentation is contradictory: QLayout::addWidget contains: This function uses addItem(). So I went to the ultimate documentation - the source code. It looks like both of us are right: If you add a wdiget without a parent (like in the example of the OP) the layout takes ownership and the widget are destroyed, otherwise the widgets will be destroyed when their parents are destroyed.Katharinekatharsis
I just had a look at QLayout::deleteAllItems() (Qt3 support) and at the documentation for QLayout::takeAt. This gives another answer.Katharinekatharsis
Nice update. Your answer now has strengths from answers by Liz and TeL. I also am curious about TeL's point about adding "delete child->widget()". Is that important here?Molini
D
5

You also want to make sure that you remove spacers and things that are not QWidgets. If you are sure that the only things in your layout are QWidgets, the previous answer is fine. Otherwise you should do this:

QLayoutItem *wItem;
while (wItem = widget->layout()->takeAt(0) != 0)
      delete wItem;

It is important to know how to do this because if the layout you want to clear is part of a bigger layout, you don't want to destroy the layout. You want to ensure that your layout maintains it's place and relation to the rest of your window.

You should also be careful, you're are creating a load of objects each time you call this method, and they are not being cleaned up. Firstly, you probably should create the QWidget and QScrollArea somewhere else, and keep a member variable pointing to them for reference. Then your code could look something like this:

QLayout *_layout = WidgetMemberVariable->layout();

// If it is the first time and the layout has not been created
if (_layout == 0)
{
  _layout = new QVBoxLayout(this);
  WidgetMemberVariable->setLayout(_layout);
}

// Delete all the existing buttons in the layout
QLayoutItem *wItem;
while (wItem = widget->layout()->takeAt(0) != 0)
    delete wItem;

//  Add your new buttons here.
QPushButton button = new QPushButton(item);
_layout->addWidget(button);

QPushButton button = new QPushButton("button");
_layout->addWidget(button);
Drying answered 24/11, 2010 at 22:52 Comment(1)
this works well, but I had to add parentheses to following statement while ((wItem = ui->verticalLayoutForScenarios->takeAt(0)) != 0)Sassanid
D
2

You do not write about going the other way, but you could also just use a QStackedWidget and add two views to this, one for each arrangement of buttons that you need. Flipping between the two of them is a non issue then and a lot less risk than juggling various instances of dynamically created buttons

Doubleedged answered 6/12, 2010 at 18:16 Comment(0)
O
2

None of the existing answers worked in my application. A modification to Darko Maksimovic's appears to work, so far. Here it is:

void clearLayout(QLayout* layout, bool deleteWidgets = true)
{
    while (QLayoutItem* item = layout->takeAt(0))
    {
        QWidget* widget;
        if (  (deleteWidgets)
              && (widget = item->widget())  ) {
            delete widget;
        }
        if (QLayout* childLayout = item->layout()) {
            clearLayout(childLayout, deleteWidgets);
        }
        delete item;
    }
}

It was necessary, at least with my hierarchy of widgets and layouts, to recurse and to delete widgets explicity.

Oscillatory answered 28/8, 2013 at 19:10 Comment(0)
K
2

only works for my buttonlist, if the widgets themeselves are deleted, too. otherwise the old buttons are still visible:

QLayoutItem* child;
while ((child = pclLayout->takeAt(0)) != 0)
{
    if (child->widget() != NULL)
    {
        delete (child->widget());
    }
    delete child;
}
Kaitlin answered 29/1, 2014 at 14:42 Comment(0)
K
1

I had a similar case where I have a QVBoxLayout containing dynamically created QHBoxLayout objects containing a number of QWidget instances. For some reason I couldn't get rid of the widgets either by deleting neither the top level QVBoxLayout or the individual QHBoxLayouts. The only solution I got to work was by going through the hierarchy and removing and deleting everything specifically:

while(!vbox->isEmpty()) {
    QLayout *hb = vbox->takeAt(0)->layout();
    while(!hb->isEmpty()) {
        QWidget *w = hb->takeAt(0)->widget();
        delete w;
    }
    delete hb;
}
Keddah answered 15/6, 2011 at 9:32 Comment(0)
R
0

If you want to remove all widgets, you could do something like this:

foreach (QObject *object, _layout->children()) {
  QWidget *widget = qobject_cast<QWidget*>(object);
  if (widget) {
    delete widget;
  }
}
Rabkin answered 24/11, 2010 at 22:48 Comment(3)
Hmmm...this force closes the application, even if I wait for the layout to populate once. I must be doing something wrong.Alphonsealphonsine
Don't copy-paste the code. Look up what I used above in the Qt documentation and make it fit your needs. Use gdb or Qt Creator to find where it crashed by running it in debug mode.Rabkin
A QLayout cannot be a parent of a QWidget. So qLayout->childern() return 0 items.Badalona
P
0

If you don't do anything funny when adding widgets to layouts and layouts to other layouts they should all be reparented upon addition to their parent widget. All QObjects have a deleteLater() slot which will cause the QObject to be deleted as soon as control is returned to the event loop. Widgets deleted in this Manor also delete their children. Therefore you simply need to call deleteLater() on the highest item in the tree.

in hpp

QScrollArea * Scroll;

in cpp

void ClearAndLoad(){
    Scroll->widget()->deleteLater();
    auto newLayout = new QVBoxLayout;
    //add buttons to new layout
    auto widget = new QWidget;
    widget->setLayout(newLayout);
    Scroll->setWidget(widget);
}

also note that in your example the _layout is a local variable and not the same thing as the _layout in the header file (remove the QVBoxLayout* part). Also note that names beginning with _ are reserved for standard library implementers. I use trailing _ as in var_ to show a local variable, there are many tastes but preceding _ and __ are technically reserved.

Polyhistor answered 11/10, 2013 at 15:48 Comment(2)
Are you sure deleteLater() deletes after the queue is idle? (and how would idle be defined? idle wrt. to the object? unlikely...) The docs state: "The object will be deleted when control returns to the event loop." I've had a few problems with events sent to deleteLater()'d objects, though it might have been due to many things...That
I changed my wording to something less ambiguous. deleteLater() is implemented like other signal/slot arriving from other threads which are queued in order of occurrence. Therefore if other signals (events) are queued at the time deleteLater() is called they will be handled before it is deleted, this is by design.Polyhistor
A
0

I have a possible solution for this problem (see Qt - Clear all widgets from inside a QWidget's layout). Delete all widgets and layouts in two seperate steps.

Step 1: Delete all widgets

    QList< QWidget* > children;
    do
    {
       children = MYTOPWIDGET->findChildren< QWidget* >();
       if ( children.count() == 0 )
           break;
       delete children.at( 0 );
    }
    while ( true );

Step 2: Delete all layouts

    if ( MYTOPWIDGET->layout() )
    {
        QLayoutItem* p_item;
        while ( ( p_item = MYTOPWIDGET->layout()->takeAt( 0 ) ) != nullptr )
            delete p_item;
        delete MYTOPWIDGET->layout();
    }

After step 2 your MYTOPWIDGET should be clean.

Aguish answered 21/10, 2016 at 12:44 Comment(0)
O
0

There's some sort of bug in PyQt5 where if you employ the above methods, there remain undeleted items shown in the layout. So I just delete the layout and start over:

E.g.

   def run_selected_procedures(self):
      self.commandListLayout.deleteLater()
      self.commandListLayout = QVBoxLayout()
      widget = QWidget()
      widget.setLayout(self.commandListLayout)
      self.scrollArea.setWidget(widget)
      self.run_procedures.emit(self._procSelection)
Offcolor answered 4/10, 2022 at 1:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.