Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt
  • Unsolved
Collapse
Brand Logo
  1. Home
  2. Qt Development
  3. QML and Qt Quick
  4. ListView for chat message history: positionViewAtIndex doesn't work reliably (+repro)
Qt 6.12 LTS is out! See what's new in the release blog

ListView for chat message history: positionViewAtIndex doesn't work reliably (+repro)

Scheduled Pinned Locked Moved Unsolved QML and Qt Quick
3 Posts 3 Posters 298 Views
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • M Offline
    M Offline
    magackame
    wrote last edited by
    #1

    Hey folks. Building a messaging app in QML and want to use ListView for the message history.

    What I need from the widget:

    1. 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)
    2. Ability to jump to a particular item in the list
    3. 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 positionViewAtIndex doesn't work reliably.

    My question to you:

    1. Is ListView just not suitable for this use? Is there a better alternative or will I have to make my custom C++ impl?
    2. Is this a genuine bug in LIstView or is positionViewAtIndex inherently 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 px
    

    Would really appreciate any help.

    1 Reply Last reply
    0
    • M Offline
      M Offline
      michalgrabarczyk
      wrote last edited by
      #2

      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.

      Author of QAccelPlot – GPU-accelerated plotting for Qt Quick / QML

      1 Reply Last reply
      1
      • J Offline
        J Offline
        Jacob
        wrote last edited by
        #3

        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.

        1 Reply Last reply
        0

        • Login

        • Login or register to search.
        • First post
          Last post
        0
        • Categories
        • Recent
        • Tags
        • Popular
        • Users
        • Groups
        • Search
        • Get Qt
        • Unsolved