Details
-
Bug
-
Resolution: Cannot Reproduce
-
P1: Critical
-
None
-
5.15.2, 6.0.1, 6.2.0 Alpha
-
None
-
Windows 10 x64, MSVC ABI
Reproduced on Qt 5.15.2, binary and from source, as well as Qt 6 dev branch built from source
Description
Steps to reproduce
Create a Qt Widgets application which does the following:
- Create a QListWidget, or QListView with any model (including QStringListModel).
- Call QListView::setWrapping(true).
- Call QListView::scrollTo(QListView::model()->index(0, 0)).
- Both functions must happen during the QMainWindow subclass constructor call, before the event loop resumes and the widget shows up on-screen.
I've attached a test case reproducer to this bug.
Results
On Windows (not Linux), this causes the QListView/Widget to layout incorrectly and take up more vertical space than available (creating a vertical scrollbar). Focusing any element in the first column, then pressing the keyboard down-arrow key until the focus wraps to the next column, results in an internal crash in QListModeViewBase::verticalScrollToValue(), at the line containin scrollValueMap.at(verticalScrollBar()->value()). Stack trace (taken from Qt 6) is available here.
If you call setWrapping(true) a second time after scrollTo(), the bug goes away.
Analysis
- Create a QListWidget, or QListView with any model (including QStringListModel).
- Call QListView::setWrapping(true). Looking at the Qt 5 source, this is what happens:
- d->modeProperties |= uint(QListViewPrivate::Wrap);
- d->setWrapping(enable); (merely assigns a bool)
- d->doDelayedItemsLayout(); (starts a QTimer with 0 delay)
- if (!delayedPendingLayout) (branch skipped, function returns)
- Call QListView::scrollTo(QListView::model()->index(0, 0)). There's a second optional argument defaulting to EnsureVisible.
- ... it doesn't matter.
- const QRect rect = visualRect(index);
- QListView::visualRect():
- return d->mapToViewport(rectForIndex(index));
- QListView::rectForIndex():
- return d_func()->rectForIndex(index);
- QListViewPrivate::rectForIndex():
- ...
- executePostedLayout();
- QAbstractItemViewPrivate::executePostedLayout():
- interruptDelayedItemsLayout();
- QAbstractItemViewPrivate::interruptDelayedItemsLayout():
- delayedLayout.stop();
- delayedPendingLayout = false;
- const_cast<QAbstractItemView*>(q_func())->doItemsLayout();
- QListView::doItemsLayout():
- ???
- ... it doesn't matter.
- (Optional) To workaround the bug, call QListView::setWrapping(true) again.
- d->doDelayedItemsLayout();
- if (!delayedPendingLayout) (branch taken)
- delayedPendingLayout = true;
- delayedLayout.start(delay, q_func());
- if (!delayedPendingLayout) (branch taken)
- d->doDelayedItemsLayout();
- Once the widget becomes visible... QAbstractItemView::event()
- switch (event->type()) case QEvent::Show:
- d->executePostedLayout();
- (see stack trace above)
- switch (event->type()) case QEvent::Show:
It seems as if QListView::scrollTo() runs the layout algorithm before the widget gets shown, but with the wrong widget dimensions or something. Then it fails to recompute something (not sure) once the widget gets shown. This results in too many items per column, and crashes when switching columns.
I'm not sure about the best solution; QListView::visualRect() isn't even a well-defined concept for a list view that isn't shown. But a crash is probably not a good behavior; maybe Qt could output a warning about computing the visualRect or layout, or calling scrollTo(), on a hidden widget. Or maybe scrollTo() can be queued and applied, or reapplied, once the widget is shown.
Versions
This happens with both the WindowsVista and Fusion styles, on multiple versions of Qt (5.15.2 and dev branch "6.2.0").