ListView for chat message history: positionViewAtIndex doesn't work reliably (+repro)
-
Hey folks. Building a messaging app in QML and want to use ListView for the message history.
What I need from the widget:
- Ability to preserve scroll position for given data, so that when user changes channels I can stash away the scroll position of current channel, change the underlying data to the messages of new channel and restore the previously stashed scroll of the new channel (basically preserve scroll as data without the need to keep a bunch of widgets in memory, like 5 instances of ListView with their models)
- Ability to jump to a particular item in the list
- Support for items of variable height
I have been wrestling with ListView for god knows how long, I fix something up, all seems to work and then I come across a particular message contents and position combination where scroll jumping/restoration breaks again. Basically
positionViewAtIndexdoesn't work reliably.My question to you:
- Is ListView just not suitable for this use? Is there a better alternative or will I have to make my custom C++ impl?
- Is this a genuine bug in LIstView or is
positionViewAtIndexinherently not guaranteed to work reliably?
Run the reproducer with
qml Repro.qml. Scroll down/up so that the jump target index is out of view and then click jump. Any index and position combination works correctly, except for index 1. If you scroll far away enough from index 1 the jump always misses.repro:
// Run: qml Repro.qml // Reproduced with Qt 6.8.2. // // 1. Start the application and leave the index at 1. // 2. Click JUMP once: the requested rectangle is not aligned correctly. // 3. Click JUMP again: it aligns correctly, without changing any heights. // Restart the application to repeat from the initial state. // // With BottomToTop and ListView.End, the requested rectangle should align // with the TOP of the viewport (unless constrained by a real list boundary). // Index 1 has enough content on either side to permit that alignment. import QtQuick import QtQuick.Window import QtQuick.Controls Window { id: root visible: true width: 800 height: 640 title: "repro" color: "#202020" property int jumpCount: 0 property string result: "Click JUMP once, then again. Expected target offset: 0 px." ListView { id: list anchors { top: parent.top left: parent.left right: parent.right bottom: controls.top } clip: true spacing: 0 verticalLayoutDirection: ListView.BottomToTop model: [ { height: 3425, color: "#8e44ad" }, { height: 82, color: "#2980b9" }, { height: 454, color: "#16806a" }, { height: 176, color: "#b45f06" }, { height: 64, color: "#a93226" }, { height: 22, color: "#566573" }, { height: 218, color: "#784212" }, { height: 2000, color: "#333344" } ] delegate: Rectangle { id: rectangle required property int index required property var modelData width: list.width height: modelData.height color: modelData.color border.color: "white" border.width: 1 Text { color: "white" font.pixelSize: 14 text: "Index " + rectangle.index + " | height " + rectangle.height + " px" } } } Rectangle { id: controls anchors { left: parent.left right: parent.right bottom: parent.bottom } height: 81 color: "#252525" Row { x: 8 y: 6 spacing: 8 SpinBox { id: indexInput from: 0 to: 7 value: 1 editable: true } Button { id: jumpButton text: "JUMP" onClicked: { console.log("Geometry at JUMP:", "window", root.width + "x" + root.height, "viewport", list.width + "x" + list.height, "(logical px), DPR", root.Screen.devicePixelRatio); const index = indexInput.value; list.positionViewAtIndex(index, ListView.End); const item = list.itemAtIndex(index); ++root.jumpCount; if (!item) { root.result = "Target delegate was not realized."; } else { const offset = item.y - list.contentY; root.result = "Call " + root.jumpCount + " | index " + index + " | target.y " + item.y + " | contentY " + list.contentY + " | offset " + offset + " px"; } console.log(root.result); } } } Text { x: 8 y: 55 width: parent.width - 16 color: "white" font.pixelSize: 13 elide: Text.ElideRight text: root.result } } }My logs (first is jump from below, second is jump from above, the offset should be
0, but it's not):qml: Geometry at JUMP: window 1276x1390 viewport 1276x1309 (logical px), DPR 1.5 qml: Call 1 | index 1 | target.y -3507 | contentY -4303 | offset 796 px qml: Geometry at JUMP: window 1276x1390 viewport 1276x1309 (logical px), DPR 1.5 qml: Call 2 | index 1 | target.y 7477 | contentY 6681 | offset 796 pxWould really appreciate any help.
-
Hey! I retested your repro on Qt 6.7.3 and 6.11.2 on Windows and got exactly your numbers on both.
Using ListView like this is fine, I use it in a similar way myself. My delegates all have the same height though, so I never hit this. With equal heights, every jump lands correctly.
ListView doesn't create delegates that are far off screen, so it estimates their height from the ones it has. When delegate heights vary, that estimate can be way off (your index 0 is 3425 px, but it gets guessed at a few hundred), and the position calculation for the jump ends up wrong.
Try calling positionViewAtIndex twice as a workaround:
list.positionViewAtIndex(index, ListView.End); list.positionViewAtIndex(index, ListView.End);The first call creates the delegates around the target, so on the second call ListView knows their real heights and positions correctly.
-
This is an interesting issue with Qt Quick ListView, especially when working with variable-height delegates. The estimated delegate sizes can sometimes cause unexpected scroll positioning when jumping to items that are outside the visible area.
Calling
positionViewAtIndex()twice may help as a workaround, as the first call allows ListView to create nearby delegates and update its size estimates.