17

I'm creating an application in Qt that allows users to drag around various "modules" in a QGraphicsView. Whenever one of these modules is selected, it emits a signal which is then picked up by a "ConfigurationWidget" which is a side-panel which should display all of the relevant parameters of the selected module:

class ConfigurationWidget : public QWidget
{
  Q_OBJECT

  public:
    ConfigurationWidget(QWidget *parent) : QWidget(parent) {}  

  public slots:
    void moduleSelected(Module* m)
    {
      if(layout())
      { 
        while (itsLayout->count()>0) 
        { 
          delete itsLayout->takeAt(0); 
        }
      }
      delete layout();

      itsLayout = new QFormLayout(this);
      itsLayout->addRow(QString(tr("Type:")),     new QLabel(m->name()));
      itsLayout->addRow(QString(tr("Instance:")), new QLabel(m->instanceID()));
      // ... Display a whole bunch of other fields that depends on the module
    }
};

The problem is that the ConfigurationWidget never actually gets cleared when a module is selected. The new fields are just drawn over the old ones. I've tried various combinations of hide() and show(), invalidate(), update(), etc. to no avail.

What's the correct way to make a widget that can change its fields like this on the fly?

rcv
  • 6,078
  • 9
  • 43
  • 63

8 Answers8

38

The code loop I've used before is as follows:

void clearLayout(QLayout *layout) {
    if (layout == NULL)
        return;
    QLayoutItem *item;
    while((item = layout->takeAt(0))) {
        if (item->layout()) {
            clearLayout(item->layout());
            delete item->layout();
        }
        if (item->widget()) {
           delete item->widget();
        }
        delete item;
    }
}

Hopefully that will be helpful to you!

Wes Hardaker
  • 21,735
  • 2
  • 38
  • 69
  • 2
    I'd add `if(QSpacerItem *SpacerItem = Item->spacerItem()){ delete SpacerItem; }` to that as well :) – sam-w Oct 20 '11 at 13:49
  • Further investigation also suggests that `delete item` is unnecessary, as Qt handles that for us (use `Qlayout::count()` to verify) – sam-w Oct 20 '11 at 13:57
  • 4
    You don't need the `if (item->widget())`. Deleting `NULL` is a nop. – Timmmm Dec 09 '12 at 15:11
  • 2
    @sjwarner that causes an error: `item->spacerItem()` is essentially `return dynamic_cast(item)`. I.e. you'd be double freeing that block of memory. Note: I know this only empirically. – chacham15 Apr 24 '14 at 07:30
  • 1
    This is the only valid code from all the answers. However, there is a more efficient way which does not require recursion. First you delete all the widget's direct children, then you delete layout items without recursion. – galinette Apr 04 '17 at 10:26
  • 3
    ASAN gives me a heap-use-after-free error at `if (item->widget())`. – bparker Aug 20 '18 at 17:13
  • @galinette "First you delete all the widget's direct children"-- But we're not passed a widget, we're passed a layout. " then you delete layout items without recursion " -- But if a simple delete isn't sufficient for the parent layout, then why would it be sufficient for the children layouts? – Patrick Parker Feb 05 '20 at 18:56
  • Its flawed in that layout passed in could be **nullptr** and **takeAt** will fail if the layout has not children. – SPlatten Jun 10 '21 at 07:35
  • 1
    @SPlatten - added a NULL check. – Wes Hardaker Jun 10 '21 at 15:40
  • The example above is giving me seg fault when trying to clear the grid layout – TheBestPlayer May 04 '23 at 11:46
17

If you transfer the layout to another widget allocated on the stack, the widgets in that layout become the children of the new widget. When the temporary object goes out of scope it destroys automatically the layout and all the widgets in it.

void moduleSelected(Module* m)
{
    if(layout())
        QWidget().setLayout(layout());

    itsLayout = new QFormLayout(this);
    itsLayout->addRow(QString(tr("Type:")),     new QLabel(m->name()));
    itsLayout->addRow(QString(tr("Instance:")), new QLabel(m->instanceID()));
    // ... Display a whole bunch of other fields that depends on the module
}
alexisdm
  • 29,448
  • 6
  • 64
  • 99
  • 6
    This is insanely ugly and insanely clever simultaneously. ;) Thanks for sharing this hack. I wish there was `QLayout::clear()`... – leemes Jun 15 '13 at 19:07
6

It looks like the best way to do this is to use a QStackedLayout, as hinted at by armonge:

void ConfigurationWidget::moduleSelectedSlot(Module* m)
{
  QStackedLayout *stackedLayout = qobject_cast<QStackedLayout*>(layout());

  QWidget *layoutWidget = new QWidget(this);
  QFormLayout *formLayout = new QFormLayout(layoutWidget);
  formLayout->addRow(QString(tr("Type:")),     new QLabel(m->name()));
  formLayout->addRow(QString(tr("Instance:")), new QLabel(m->instanceID()));
  // ... Display a whole bunch of other fields that depends on the module

  delete stackedLayout->currentWidget();
  stackedLayout->addWidget(layoutWidget);
  stackedLayout->setCurrentWidget(layoutWidget);
}
rcv
  • 6,078
  • 9
  • 43
  • 63
  • 4
    When using QObject derived class, you should use qobject_cast instead of dynamic_cast: http://doc.qt.nokia.com/latest/qobject.html#qobject_cast – Patrice Bernassola Feb 01 '11 at 07:38
2

I recently run into the same problem with a QFormLayout. What worked for me (and is also in Qt's documentation: http://qt-project.org/doc/qt-5/layout.html) was the takeAt(int) method.

void clearLayout(QLayout *layout)
{
     QLayoutItem *item;
     while ((item = layout->takeAt(0)))
         delete item;
}
Kádi
  • 2,796
  • 1
  • 16
  • 22
  • 1
    Be careful : this will not delete the widgets, only remove them from the layout, as layout items do not own widgets. So the program may end up eating all the memory if you recreate the ui a lot of times with that code. This is not a real memory leak, because the child widgets will be deleted with the parent widget, but this may have the same effect. – galinette Apr 04 '17 at 10:24
1

Using while((item = layout->takeAt(0)))will cause warning message "Invalid index take at 0". I use count() instead

void clearLayout(QLayout *layout) 
{
    if (layout) {
        while(layout->count() > 0){
            QLayoutItem *item = layout->takeAt(0);
            QWidget* widget = item->widget();
            if(widget)
                delete widget;
            delete item;
        }
    }
}
Dorian
  • 377
  • 5
  • 18
  • 2
    A valid layout item may return a null pointer with item->widget(), for instance with child layouts or spacers. This code will crash in these cases. – galinette Apr 04 '17 at 10:22
  • @galinette It depends, but yeah, in general case it is not wise to call delete on nulltpr. http://stackoverflow.com/questions/6731331/is-it-still-safe-to-delete-nullptr-in-c0x – Dorian Apr 04 '17 at 15:01
  • 2
    Instead of using 'delete widget', I would suggest using widget->deleteLater(). In most cases, it would be hard to notice the difference, but there may be one. As documentation about QObject suggests: 'The object will be deleted when control returns to the event loop'. This is the main point. It is possible that widget sends some signal, and after sending the signal some other calls are made in that same widget instance. If you would delete widget in a 'raw way', then you would go out of scope, and in this case you can not handle anything after the signal is sent. – Vaidotas Strazdas Apr 24 '17 at 14:30
1

Reparent the items to NULL when you want them to disappear from a window. I had similar problems recently and reparenting solved them.

nyu
  • 11
  • 1
  • Welcome to Stack Overflow. This question already has a handful of answers as well as one that has been accepted. Please explain how your answer is different or better than those solutions already provided. – avojak Jul 14 '17 at 13:56
  • The "reparenting to NULL" makes perfect sense to me. This is a way of making the objects invisible but not deleting them. They are still available for use and can be added back into the layer later. [You can use this to make your own version of the QStackedWidget class. The QStackedWidget class works well in simple cases, but has oddities and peculiarities of its own in which make it difficult to adjust its size.] – David Casper Feb 03 '19 at 13:54
1

I have found none of the other answers so far work. They either crash or don't clear the layout completely. So here is what I have found to work, I hope others find this useful.

void eraseLayout(QLayout * layout)
{
    while(layout->count() > 0)
    {
        QLayoutItem *item = layout->takeAt(0);

        QWidget* widget = item->widget();
        if(widget)
        {
            delete widget;
        }
        else
        {
            QLayout * layout = item->layout();
            if (layout)
            {
                eraseLayout(layout);
            }
            else
            {
                QSpacerItem * si = item->spacerItem();
                if (si)
                {
                    delete si;
                }
            }
        }
        delete item;
    }
}
David Casper
  • 394
  • 2
  • 9
0

Thanks to Vaidotas Strazdas's commentary, I found my solution to reset a layout properly. Using the deleteLater() method works very good, otherwise the delete keyword causes massive display errors in the refreshed layout.

void resetLayout(QLayout* apLayout)
{
    QLayoutItem *vpItem;
    while ((vpItem = apLayout->takeAt(0)) != 0)  {
        if (vpItem->layout()) {
            resetLayout(vpItem->layout());
            vpItem->layout()->deleteLater();
        }
        if (vpItem->widget()) {
            vpItem->widget()->deleteLater();
        }
        delete vpItem;
    }
}
Theo
  • 13
  • 5