QML prevent ListView delegate to update all the time - qt

I'm relatively new to QML/QtQuick and still learning. I have a little performane issue with a very small private project. I just tryed to implement a filter function to my ListView, because >15.000 objects are a lot to search manually. I just want to update the ListView when I finished the editing of my search field or pressing "return". But instead it's refreshing every time I insert or delete a character from this textfield which needs sometimes a few seconds.
Anyone have an idea how to prevent the list to be refreshed permanently or reducing theese performance issues?
Thanks a lot
import QtQuick 2.12
import QtQuick.Controls 2.5
import QtQuick.XmlListModel 2.12
import Anime_initialiser 1.0
import "."
Page {
TextField{
id: searchField
width: parent.width
z: 1
/*onEditingFinished: {
XL.animeListModel.reload()
}*/
}
ListView {
z: 0
ScrollBar.vertical: ScrollBar { active: true }
id: listView
width: parent.width
height: parent.height
model: XL.animeListModel
y: searchField.height
Anime_initialiser {
id: initialiser
onShowAnimeDetails: {
xmlDataString = xmlString
swipeView.currentIndex = swipeView.currentIndex+1
}
}
delegate: ItemDelegate {
visible: {
if (searchField.length > 0)
return (main_title.toLowerCase().match(searchField.text.toLowerCase()) || de_title.toLowerCase().match(searchField.text.toLowerCase())) ? true : false
else
return true
}
height: visible ? Button.height : 0
width: parent ? parent.width : 0
Button {
anchors.fill: parent
onClicked: {
anime_id = aid
initialiser.buttonClicked(anime_id)
}
Text {
width: parent.width
height: parent.height
font.pointSize: 100
minimumPointSize: 12
fontSizeMode: Text.Fit
text: aid + ": " + main_title + (de_title ? "\nDE: " + de_title : "")
}
}
}
}
}

Rather than toggling the visible flag of all of your delegates, you should use a QSortFilterProxyModel. The idea is that the proxy model would use your XL.animeListModel as a source model, and then you can give the proxy a regular expression telling it which ones to filter out. Depending on how you want it to filter, you could just call setFilterRole() to tell it which property to compare against your regex, or you could do a custom filter by overriding the filterAcceptsRow() function.
EDIT:
If you don't want to use a proxy, you can still prevent the constant updates by not binding on the visible property directly to the search field. You were on the right track with your onEditingFinished code. You could create a separate text string that just holds the completed search text.
property string searchText: ""
Then update that string when you are done typing your search text.
onEditingFinished: {
searchText = searchField.text.toLowerCase();
}
And finally, bind your visible property to this new string.
visible: {
if (searchText.length > 0)
return (main_title.toLowerCase().match(searchText) || de_title.toLowerCase().match(searchText)) ? true : false
else
return true
}

Related

QML Video element randomly disappears on Label text change

I'm working on an app which is supposed to display a camera stream from a webcam. It's a single-page app which only shows a Video QML element showing the stream (which currently is a simple .avi file) and a Label element indicating the current connection state from an MQTT connection.
Here's the code:
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.1
import QtMultimedia 5.12
import MqttClient 1.0
Window {
width: 360
height: 640
visible: true
title: qsTr("Doorbell")
MqttClient {
id: client
// TODO
property string _host: "localhost"
property string _port: "1883"
property string _topic: "my/topic"
hostname: _host
port: _port
Component.onCompleted: {
connectToHost()
}
onConnected: {
subscribe(_topic)
}
onMessageReceived: {
video.play()
}
Component.onDestruction: {
disconnectFromHost()
}
}
GridLayout {
anchors.fill: parent
anchors.margins: 10
columns: 2
Video {
id: video
objectName: "vplayer"
width: parent.width
height: 300
Layout.columnSpan: 2
autoPlay: false
source: "file:///path/to/my/test.avi"
onErrorChanged: {
console.log("error: " + video.errorString)
}
MouseArea {
anchors.fill: parent
onClicked: {
video.muted = !video.muted
}
}
focus: true
Image {
id: muteIndicator
source: "mute_white.png"
width: 64
height: width
visible: video.muted
anchors.centerIn: parent
}
}
Label {
function stateToString(value) {
if (value === 0)
return "Disconnected"
else if (value === 1)
return "Connecting"
else if (value === 2)
return "Connected"
else
return "Unknown"
}
Layout.columnSpan: 2
Layout.fillWidth: true
text: stateToString(client.state) + "(" + client.state + ")"
}
}
}
Here's a screenshot:
Here's the clue:
I tried removing the Label and replacing it with a simple rectangle indicator (red or green) to show if the connection is currently active.
However, when replacing the text content of the Label element, the Video element completely disappears.
What I've tried:
removing the stateToString(client.state) + "(" + client.state + ")" part and replacing it with text: "Connected(2)"
replacing stateToString(... with an empty string (text: "")
replacing the content of stateToString(...) with return "Connected(2)"
and a lot of different more seemingly completely useless things
Example code:
// ...
Label {
function stateToString(value) {
if (value === 0)
return "Disconnected"
else if (value === 1)
return "Connecting"
else if (value === 2)
return "Connected"
else
return "Unknown"
}
Layout.columnSpan: 2
Layout.fillWidth: true
text: "Connected(2)"
// enabled: client.state === MqttClient.Connected
}
// ...
Unless I set text to the exact value stateToString(client.state) + "(" + client.state + ")" or at least stateToString(client.state), the Video element will always disappear in the QML view:
I have no idea of what might be the reason for this.
Any help is greatly appreciated, thanks.
Try adding:
Layout.fillWidth: true
after:
objectName: "vplayer"
I'm 99% sure the root of the problem is in the way you have used the layouts. In another word, you have not placed your QML elements in a robust way. since QML has so much flexibility in positioning elements in UI, it matters in QML to put your elements in a well-defined, robust way.

QML: Separators between list delegates

Has anyone found a good way to add separators between QML list delegates?
There is a very similar question here already but my problem is a bit more complex: How to set custom separator between items of ListView
Most of the time I use something similar as in an answer there:
ListView {
delegate: ItemDelegate {
...
Separator {
anchors { left: parent.left; bottom: parent.bottom; right: parent.right }
visible: index < ListView.View.count
}
}
}
However, depending on the design and backend data, I don't always have a ListView/Repeater at hand and need to add manual items in a ColumnLayout instead or a mix of some items from a repeater and some manual ones:
ColumnLayout {
ItemDelegate {
...
}
Separator {
Layout.fillWidth: true
}
ItemDelegate {
...
}
}
Now, both of those work but it's extremely annoying to always remember and type that separator. After lots of trying I still haven't been able to figure out a component that would take care of it.
The closest I've come to a custom Layout component like this (e.g ItemGroup.qml):
Item {
default property alias content: layout.data
ColumnLayout {
id: layout
}
Repeater {
model: layout.children
delegate: Separator {
parent: layout.children[index]
anchors { left: parent.left; bottom: parent.bottom; right: parent.right }
visible: index < layout.children.length
}
}
}
Now this works fine for manually adding items to such a group, but again it will not work in many corner cases. For instance putting a Repeater into such an ItemGroup will create a separator for the Repeater too (given it inherits Item and thus is included in children) which results in a visual glitch with one seemingly floating separator too much...
Anyone came up with a more clever solution for this?
I'd try this approach:
Make a custom component based on ColumnLayout.
Use default property ... syntax to capture children added to it into a separate list property.
Create a binding for the children property of ColumnLayout that interleaves each real child in your default property list with one of your Separators (using a Component to declare it and createObject() to create each one).
Here's a working example:
Separator.qml
import QtQuick 2.0
import QtQuick.Layouts 1.12
Rectangle {
Layout.fillWidth: true
height: 1
color: "red"
}
SeparatedColumnLayout.qml
import QtQuick 2.15
import QtQuick.Layouts 1.12
ColumnLayout {
id: layout
default property list<Item> actualChildren
property Component separatorComponent: Qt.createComponent("Separator.qml")
children: {
var result = [];
for(var i = 0;i < actualChildren.length;++i) {
result.push(actualChildren[i]);
if (i < actualChildren.length - 1) {
result.push(separatorComponent.createObject(layout));
}
}
return result;
}
}
main.qml:
import QtQuick 2.11
import QtQuick.Window 2.11
import QtQuick.Controls 2.4
import QtQuick.Layouts 1.12
ApplicationWindow {
id: root
visible: true
width: 640
height: 480
title: qsTr("Hello World")
SeparatedColumnLayout {
anchors.fill: parent
Text {
Layout.alignment: Qt.AlignHCenter
text: "1"
}
Text {
Layout.alignment: Qt.AlignHCenter
text: "2"
}
Text {
Layout.alignment: Qt.AlignHCenter
text: "3"
}
}
}
The result:

How i can save into JSON file a QML list model? [duplicate]

I am able to save settings for list items which is statically created using Component.onComponent method. But Settings for statically created list items take affect after reopening app. I would like to save settings for dynamically created list model. I am unable to save Settings for a dynamically created list item. The code below does that a list item is on and off while clicking Show/Hide action. When I reopen the app, created list item disappears. How to save list item using Setting?
import QtQuick 2.9
import Fluid.Controls 1.0
import Qt.labs.settings 1.0
import QtQuick.Controls 1.4
ApplicationWindow {
id:root
visible: true
width: 640
height: 480
property variant addlist
property int countt2: 0
Settings{
id:mysetting4
property alias ekranCosinus: root.countt2
}
function listonoff(){
if(countt2%2==1){
return true
}
else if(countt2%2==0){
return false
}
}
Connections {
target: addlist
onTriggered: listonoff()
}
addlist: favourite2
/* main.qml */
menuBar: MenuBar {
Menu {
title: "&Edit"
MenuItem { action: favourite2 }
}
}
Action {
id:favourite2
text: qsTr("Show/Hide")
onTriggered: {
countt2++
console.log(countt2)
if(listonoff()===true){
return list_model.insert(list_model.index,{ title: "First item."} )
}
else if(listonoff()===false){
return list_model.remove(list_model.index)
}
}
}
ListView {
id:contactlist
width: parent.width
height: parent.height
focus: true
interactive: true
clip: true
model: ListModel {
id:list_model
}
delegate: ListItem {
text: model.title
height:60
}
}
MouseArea {
id: mouse
anchors.fill: parent
}
}
Quite curious that you expect that saving a single integer value will somehow be able to store the content of an arbitrary data model... It doesn't work even for the static model data, it is only "restored" because it is static - it is part of the code, you are not really saving and restoring anything.
If you want to store all that data, you will have to serialize it when your app quits, and deserialize it when the app starts.
You could still use Settings, but to store a string value, that will represent the serialized data.
The easiest way to do it is to transfer the model items back and forth with a JS array, this way the JS JSON object functionality can be used to easily serialize and deserialize the data:
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Window 2.3
import Qt.labs.settings 1.0
ApplicationWindow {
id: main
width: 640
height: 480
visible: true
property string datastore: ""
Component.onCompleted: {
if (datastore) {
dataModel.clear()
var datamodel = JSON.parse(datastore)
for (var i = 0; i < datamodel.length; ++i) dataModel.append(datamodel[i])
}
}
onClosing: {
var datamodel = []
for (var i = 0; i < dataModel.count; ++i) datamodel.push(dataModel.get(i))
datastore = JSON.stringify(datamodel)
}
Settings {
property alias datastore: main.datastore
}
ListView {
id: view
anchors.fill: parent
model: ListModel {
id: dataModel
ListElement { name: "test1"; value: 1 }
}
delegate: Text {
text: name + " " + value
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
if (mouse.button === Qt.LeftButton) {
var num = Math.round(Math.random() * 10)
dataModel.append({ "name": "test" + num, "value": num })
} else if (dataModel.count) {
dataModel.remove(0, 1)
}
}
}
}
The application begins with a single data model value, more data items can be added or removed by pressing the left and right mouse button respectively.
As long as the application is closed properly, the data model will be copied into an array, which will be serialized to a string, which will be stored by the Settings element. So upon relaunching the app, if the data string is present, the model is cleared to remove the initial value so it is not duplicated, the data string is deserialized back into an array, which is iterated to restore the content of the data model. Easy peasy.
Of course, you could also use the LocalStorage API as well, or even write a simple file reader and writer by exposing a C++ object to QML. All this approach needs is to be able to store and retrieve a single string.

How to recreate Android's Pull to Refresh icon in QML?

This is the pull to refresh icon used to refresh views in Android.
I've been trying to bring that to qml but it is not so easy.
There are so many transitions that it quickly becomes very complex.
How difficult this should be to recreated in QML?
Is using canvas the better solution?
As i have first seen, the swipe brings down the arrow in a different pace of the swipe, while the arrow rotates. If this arrow comes from a canvas how can it relate to outside events, that is the swipe?
I used something like this:
//
// Slot called when the flick has started
//
onFlickStarted: {
refreshFlik = atYBeginning
}
//
// Slot called when the flick has finished
//
onFlickEnded: {
if ( atYBeginning && refreshFlik )
{
refresh()
}
}
It seems to work as expected and it is easy to implement
The problem is that Flickable and the derived ListView don't really provide any over-drag or over-shoot information in the cases where the visual behavior is disabled.
If dragging the visual over the beginning is not a problem for you, you can simply use the negated value of contentY which goes into the negative if the view is dragged before its beginning.
The only solution I can think of to not have any visual over-dragging but still get the over-drag information in order to drive your refresher is to set the view interactive property to false, and put another mouse area on top of that, and redirect drags and flicks manually to the now non-interactive view.
That last part might sound complex, but it isn't that complex, and I happen to know for a fact that it works well, because I have already used this approach and the source code is already here on SO.
So once you have access to the mouse area that controls the view, you can track how much you are in the negative, and use that information to drive the logic and animation of the refresher.
The notable difference between the implementation in the linked answer and what you need is that the linked answer has the mouse area in each delegate, due to the requirements of the specific problem I wanted to solve. You don't need that, you only need one single mouse area that covers the view.
I did like this recently.
Basically I use the position of a ScrollBar and if it goes negative I show a spinner and refresh. So I don't need to mess with the flick stuff.
import QtQuick.Controls 6.0
import QtQuick 6.0
ListView {
ScrollBar.vertical: ScrollBar {
id: scrollbar
}
property bool negativescroll: scrollbar.position < 0
onNegativescrollChanged: {
if (spinner.visible) {
refresh()
}
spinner.visible = !spinner.visible
}
BusyIndicator {
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
visible: false
running: visible
id: spinner
}
width: 180; height: 200
model: model
delegate: Text {
text: name + ": " + number
}
ListModel {
id: model
ListElement {
name: "Bill Smith"
number: "555 3264"
}
ListElement {
name: "John Brown"
number: "555 8426"
}
ListElement {
name: "Sam Wise"
number: "555 0473"
}
}
}
I came to a simpler solution based on dtech's experience involving multiple Flickable elements, which basically consists on filling the Flickable with a MouseArea, setting its boundsBehavior property to Flickable.StopAtBounds, and from there, if it's at the top, do things based on mouseY values.
The better approximation i could get is in the following code. A possible drawback is that diagonal swiping also counts as a refresh intention. It could be improved with GestureArea, but i'm too lazy to get my hands on this at the moment.
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Window 2.2
ApplicationWindow {
property real mm: Screen.pixelDensity
property real margins: 2 * mm
id: mainWindow
visible: true
width: 60 * mm
height: 120 * mm
title: qsTr("Hello World")
ListModel {
id: myModel
Component.onCompleted: {
for(var i = 0; i <= 100; ++i) {
myModel.append({num: i})
}
}
}
ListView {
id: view
boundsBehavior: Flickable.StopAtBounds
interactive: true
anchors.fill: parent
model: myModel
spacing: 4
delegate: Rectangle {
width: parent.width
height: 25 * mm
border.color: 'red'
Text {
id: name
text: num
anchors.centerIn: parent
}
}
Rectangle {
signal follow
id: swatch
width: 15 * mm
height: width
radius: width / 2
color: 'lightgray'
anchors.horizontalCenter: parent.horizontalCenter
y: - height
}
MouseArea {
property int mouseYSart
property int biggerMouseY
anchors.fill: view
onPressed: {
mouseYSart = mouseY
biggerMouseY = 0
}
onMouseYChanged: {
if(view.contentY == 0) {
var currentMouseY = mouseY
if(currentMouseY > biggerMouseY) {
biggerMouseY = currentMouseY
swatch.y += 1
}
if(currentMouseY < biggerMouseY) {
biggerMouseY = currentMouseY
swatch.y -= 1
}
}
}
onReleased: swatch.y = - swatch.height
}
}
}

How can I switch the focus for the pop-up window?

I encounter a problem which is that the pop-up window cannot get the focus when it is shown. I tried to use the activefocus function in main window, but it doesn't work. It is supposed that if I press the enter key, the pop-window will be closed. How can I get the focus for the pop-up window? Thanks.
...
GridView {
id:grid_main
anchors.fill: parent
focus: true
currentIndex: 0
model: FileModel{
id: myModel
folder: "c:\\folder"
nameFilters: ["*.mp4","*.jpg"]
}
highlight: Rectangle { width: 80; height: 80; color: "lightsteelblue" }
delegate: Item {
width: 100; height: 100
Text {
anchors { top: myIcon.bottom; horizontalCenter: parent.horizontalCenter }
text: fileName
}
MouseArea {
anchors.fill: parent
onClicked: {
parent.GridView.view.currentIndex = index
}
}
}
Keys.onPressed: { //pop up window
if (event.key == 16777220) {//enter
subWindow.show();
subWindow.forceActiveFocus();
event.accepted = true;
grid_main.focus = false;
}
}
}
Window {
id: subWindow
Keys.onPressed: {
if (event.key == 16777220) {//press enter
subWindow.close();
}
}
}
...
Let's start with some basics:
Keys.onPressed: { //pop up window
if (event.key == 16777220) {//enter
subWindow.show()
...
event.accepted = true
}
}
Not to mention how error-prone it is, just for the sake of readability, please don't hard-code enum values like 16777220. Qt provides Qt.Key_Return and Qt.Key_Enter (typically located on the keypad) and more conveniently, Keys.returnPressed and Keys.enterPressed signal handlers. These convenience handlers even automatically set event.accepted = true, so you can replace the signal handler with a lot simpler version:
Keys.onReturnPressed: {
subWindow.show()
...
}
Now, the next thing is to find the correct methods to call. First of all, the QML Window type does not have such method as forceActiveFocus(). If you pay some attention to the application output, you should see:
TypeError: Property 'forceActiveFocus' of object QQuickWindowQmlImpl(0x1a6253d9c50) is not a function
The documentation contains a list of available methods: Window QML type. You might want to try a combination of show() and requestActivate().
Keys.onReturnPressed: {
subWindow.show()
subWindow.requestActivate()
}
Then, you want to handle keys in the sub-window. Currently, you're trying to attach QML Keys to the Window. Again, if you pay attention to the application output, you should see:
Could not attach Keys property to: QQuickWindowQmlImpl(0x1ddb75d7fe0) is not an Item
Maybe it's just the simplified test-case, but you need to get these things right when you give a testcase, to avoid people focusing on wrong errors. Anyway, what you want to do is to create an item, request focus, and handle keys on it:
Window {
id: subWindow
Item {
focus: true
Keys.onReturnPressed: subWindow.close()
}
}
Finally, to put the pieces together, a working minimal testcase would look something like:
import QtQuick 2.9
import QtQuick.Window 2.2
Window {
id: window
width: 300
height: 300
visible: true
GridView {
focus: true
anchors.fill: parent
// ...
Keys.onReturnPressed: {
subWindow.show()
subWindow.requestActivate()
}
}
Window {
id: subWindow
Item {
focus: true
anchors.fill: parent
Keys.onReturnPressed: subWindow.close()
}
}
}
PS. Key events rely on focus being in where you expect it to be. This may not always be true, if the user tab-navigates focus elsewhere, for example. Consider using the Shortcut QML type for a more reliable way to close the popup.

Resources