I'm going crazy. I have a ListView inside a ScrollView, hooked up to a model that inherits QAbstractListModel. When objects are added to the model, the ListView shows them using a delegate. So far, so good.
But I really want the view to stay scrolled to the bottom (like a chat window), and I'm having a very difficult time making that happen. Here is the relevant QML code:
Rectangle {
ScrollView {
[anchor stuff]
ListView {
id: messageList
model: textMessageFiltered
delegate: messageDelegate
}
}
TextField {
id: messageEditor
[anchor stuff]
onAccepted: {
controller.sendTextMessage(text)
text = ""
/* This works. */
//messageList.positionViewAtEnd();
}
}
Component {
id: messageDelegate
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
color: "white"
height: nameText.height + 4
Text {
id: nameText
wrapMode: Text.Wrap
text: "<b>" + authorName + " (" + authorId + ")</b> " + message
[anchor stuff]
}
ListView.onAdd: {
console.log("This prints just fine!")
messageList.positionViewAtEnd()
}
}
}
}
The really strange thing, is that messageList.positionViewAtEnd()
(at the end of the file) actually jumps it to the beginning. Without the call, the view stays where it is, even as new entries appear in the list. And indeed, if you look at the Qt documentation for the ListView.positionViewAtEnd(), it says:
Positions the view at the beginning or end, taking into account ...
Is that a silly error in the documentation, or what? I've tried everything I can think of to make this work, particularly the positionViewAtIndex()
method and using highlighters to force the scroll to happen. But nothing works. Note the /* This works. */
comment in the source code above. When that is enabled, it works totally fine! (except of course, it jumps to the ListView.count()-2 index, instead of the end of the list)
Does anyone have any idea what might be wrong here? Any examples I could try to prove that there's a terrible, terrible bug in QML?
I'm using Qt 5.3.1 with QtQuick 2.0 (or 2.1 or 2.2 fail too). I've tried many, many other configurations and code as well, so please ask if you need more info. I've completely exhausted my google-fu.
Thanks!
Edit 1
While the accepted answer does solve the above problem, it involves adding the Component.onCompleted
to the delegate. This seems to cause problems when you scroll the list, because (I believe) the delegates are added to the view when you scroll up, causing the onCompleted trigger to be called even if the model item isn't new. This is highly undesirable. In fact, the application is freezing when I try to scroll up and then add new elements to the list.
It seems like I need a model.onAdd() signal instead of using the existence of a delegate instance to trigger the scroll. Any ideas?
Edit 2
And how does this NOT work?
ListView {
id: messageList
model: textMessageFiltered
delegate: messageDelegate
onCountChanged: {
console.log("This prints properly.")
messageList.positionViewAtEnd()
}
}
The text "This prints properly" prints, so why doesn't it position? In fact, it appears to reset the position to the top. So I tried positionViewAtBeginning()
, but that did the same thing.
I'm totally stumped. It feels like a bug.
You need to set the currentIndex
as well.
testme.qml
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Window 2.0
ApplicationWindow {
title: qsTr("Hello World")
width: 300
height: 240
ScrollView {
anchors.fill: parent
ListView {
anchors.fill: parent
id: messageList
model: messageModel
delegate: Text { text: mytextrole }
highlight: Rectangle { color: "red" }
highlightMoveDuration: 0
onCountChanged: {
var newIndex = count - 1 // last index
positionViewAtEnd()
currentIndex = newIndex
}
}
}
ListModel {
id: messageModel
ListElement { mytextrole: "Dog"; }
ListElement { mytextrole: "Cat"; }
}
Timer {
property int counter: 0
running: true
interval: 500
repeat: true
onTriggered: {
messageModel.append({"mytextrole": "Line" + (counter++)})
}
}
}
There is still some jumping to the first element and jumping back down for a fraction of a second.
There is a note in documentation:
Note: methods should only be called after the Component has completed. To position the view at startup, this method should be called by Component.onCompleted.
Change your ListView.onAdd:
to
Component.onCompleted: {
console.log("This prints just fine!")
messageList.positionViewAtEnd()
}
And it works well.
In your case, the ListView emits add
signal before the new delegate is created and completed. The ListView is still working on something behind the scene, so positionViewAtEnd
cannot work as expected. And /* This works. */
because it is called after the new delegate is completed. However, don't assume this always works. Simply follow the note, call positionViewAtEnd
in Component.onCompleted
, in documentation.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With