Uploaded image for project: 'Qt'
  1. Qt
  2. QTBUG-79284

indexBelow enters infinite cycle when called during QTreeView expanded signal during expandAll

    XMLWordPrintable

Details

    • Suggestion
    • Resolution: Cannot Reproduce
    • Not Evaluated
    • None
    • 5.12.3
    • Widgets: Itemviews
    • None
    • Linux/X11

    Description

      In a large code base, I have a quite reproducible bug that happens when iterating over the items of a QTreeView model using QTreeView::indexBelow during an expandAll call.

      As I found out during debugging, indexBelow enters a cycle, returning the same two indices alternatingly ad infinitum:

       

      [...]
      expanded rows: 10828 index QModelIndex(1,0,0x555c059345b0,LabelModelProxy(0x555c05908b60)) indexBelow QModelIndex(1,0,0x555c059345b0,LabelModelProxy(0x555c05908b60))
      expanded rows: 10829 index QModelIndex(1,0,0x555c059345b0,LabelModelProxy(0x555c05908b60)) indexBelow QModelIndex(1,0,0x555c059345b0,LabelModelProxy(0x555c05908b60))
      expanded rows: 10830 index QModelIndex(1,0,0x555c059345b0,LabelModelProxy(0x555c05908b60)) indexBelow QModelIndex(1,0,0x555c059345b0,LabelModelProxy(0x555c05908b60))
      expanded rows: 10831 index QModelIndex(1,0,0x555c059345b0,LabelModelProxy(0x555c05908b60)) indexBelow QModelIndex(1,0,0x555c059345b0,LabelModelProxy(0x555c05908b60))
      expanded rows: 10832 index QModelIndex(1,0,0x555c059345b0,LabelModelProxy(0x555c05908b60)) indexBelow QModelIndex(1,0,0x555c059345b0,LabelModelProxy(0x555c05908b60))
      expanded rows: 10833 index QModelIndex(1,0,0x555c059345b0,LabelModelProxy(0x555c05908b60)) indexBelow QModelIndex(1,0,0x555c059345b0,LabelModelProxy(0x555c05908b60))
      expanded rows: 10834 index QModelIndex(1,0,0x555c059345b0,LabelModelProxy(0x555c05908b60)) indexBelow QModelIndex(1,0,0x555c059345b0,LabelModelProxy(0x555c05908b60))
      expanded rows: 10835 index QModelIndex(1,0,0x555c059345b0,LabelModelProxy(0x555c05908b60)) indexBelow QModelIndex(1,0,0x555c059345b0,LabelModelProxy(0x555c05908b60))
      [...]
      

       In my opinion this should never happen and is already a Qt bug. My first thought, that my model had a cycle somewhere but I'm fairly certain that it does not because the iteration using indexBelow and the displaying of the model using a QTreeView works in all other circumstances.

      I tried to create a minimal example but I failed to reproduce the bug, maybe because I did not use enough (>1000) elements? Or maybe some other condition also has to be met. Here is the example anyways, to illustrate what I'm doing:

      #include <QApplication>
      #include <QDebug>
      #include <QTreeWidget>
      class CustomTreeWidget : public QTreeWidget
      {
      public:
          explicit
          CustomTreeWidget( QWidget* parent = nullptr ) :
              QTreeWidget( parent )
          {
              auto item = new QTreeWidgetItem( { "Grand Grand Parent" } );
              auto item1 = new QTreeWidgetItem( { "Grand Parent" } );
              item->addChild( item1 );
              auto item2 = new QTreeWidgetItem( { "Parent" } );
              item1->addChild( item2 );
              auto child = new QTreeWidgetItem( { "Child 1" } );
              item2->addChild( child );
              child->addChild( new QTreeWidgetItem( { "Grand Child 1" } ) );
              child->addChild( new QTreeWidgetItem( { "Grand Child 2" } ) );
              child = new QTreeWidgetItem( { "Child 2" } );
              item2->addChild( child );
              child->addChild( new QTreeWidgetItem( { "Grand Child 1" } ) );
              child->addChild( new QTreeWidgetItem( { "Grand Child 2" } ) );
              child = new QTreeWidgetItem( { "Child 3" } );
              item2->addChild( child );
              child->addChild( new QTreeWidgetItem( { "Grand Child 1" } ) );
              child->addChild( new QTreeWidgetItem( { "Grand Child 2" } ) );
              insertTopLevelItem( 0, item );        connect( this, &QTreeView::expanded, this, &CustomTreeWidget::expandedSlot );        collapseAll();
              expandAll();
          }private:
          void
          expandedSlot() const
          {
              qDebug() << "an item was expanded!";
              size_t rows = 0;
              for ( auto index = model()->index( 0, 0 ); index.isValid(); index = indexBelow( index ) ) {
                  qDebug() << "expanded rows:" << ++rows << "index" << index << "indexBelow" << indexBelow( index );
              }
          }
      };
      int main(int argc, char *argv[])
      {
          QApplication app(argc, argv);    CustomTreeWidget tree;
          tree.show();    return app.exec();
      }
      

      I also looked in the Qt source code of Qt/5.12.3/Src/qtbase/src/widgets/itemviews/qtreeview.cpp and spotted a probable cause for the problem. QTreeView::indexBelow depends on the size and contents of viewItems:

      QModelIndex QTreeView::indexBelow(const QModelIndex &index) const
      {
          [...]
          if (++i >= d->viewItems.count())
              return QModelIndex();
          const QModelIndex firstColumnIndex = d->viewItems.at(i).index;
          return firstColumnIndex.sibling(firstColumnIndex.row(), index.column());
      }
      

      But in QTreeView::expandAll, viewItems is first cleared, then in QTreeViewPrivate::layout, viewItems is resized to the final size with garbage contents first before filling it with data! While the data is being written into the corresponding viewItems entries, the expanded signal is emitted. And even this is actually before item->expanded is set to true.

                  item = &viewItems[last];
                  item->index = current;
                  item->parentItem = i;
                  item->level = level;
                  item->height = 0;
                  item->spanning = q->isFirstColumnSpanned(current.row(), parent);
                  item->expanded = false;
                  item->total = 0;
                  item->hasMoreSiblings = false;
                  if ((recursiveExpanding && !(current.flags() & Qt::ItemNeverHasChildren)) || isIndexExpanded(current)) {
                      if (recursiveExpanding && storeExpanded(current) && !q->signalsBlocked())
                          emit q->expanded(current);
                      item->expanded = true;
                      layout(last, recursiveExpanding, afterIsUninitialized);
                      item = &viewItems[last];
                      children += item->total;
                      item->hasChildren = item->total > 0;
                      last = j - hidden + children;
                  } else {
                      item->hasChildren = hasVisibleChildren(current);
                  }

       Therefore, I believe that calling indexBelow with this hazardous half-filled viewItems member, leads to this bug in some cases.

      Attachments

        No reviews matched the request. Check your Options in the drop-down menu of this sections header.

        Activity

          People

            qt.team.quick.subscriptions Qt Quick and Widgets Team
            uiohbgq uiohbgq
            Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

            Dates

              Created:
              Updated:
              Resolved:

              Gerrit Reviews

                There are no open Gerrit changes