how to implement nested listmodels in qml - qt

Is it possible to implement 3 ListModels (one inside the other) and if yes how could I do it?
The 3 ListModels are for hour day and month accordingly.In other words I want one model for hour inside the model for day inside the model for month and I am going to use them in nested ListView s (3 of them ) to display the hour the day and the month in a calendar. I have made a try below :
ListModel{
id:monthlistModel
ListElement {
monthName:0
daylistModel:[
ListElement {
day:0
hourlistModel: [
ListElement{ hour:0;notes:"" }
]
}
]
}
ListElement {
monthName:1
daylistModel:[
ListElement {
day:1
hourlistModel: [
ListElement{ hour:1;notes:"" }
]
}
]
}
but I could not finish it .
Moreover I have some typeerror issues when I am running my code.The hourlistModel insists to be undefined for my nested listview and I dont no why.
Anyway back to my question , how can I go on with the above listmodel to display 24 hours , 31 days and 12 months ?

I suggest doing this imperatively with javascript rather than declaratively in QML, as it can be more dynamic and brief. One downside is that this is not well documented in my experience.
If you append an array to a ListModel, all of the array elements are converted into ListElements. Further than this, if an array is appended, and that array has nested arrays inside of it, the nested arrays are automatically converted to nested ListModels inside.
Here is a full example:
import QtQuick 2.15
import QtQuick.Window 2.0
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.12
Window {
visible: true
width: 1000
height: 600
ListModel {
id: monthsModel
Component.onCompleted: {
let months = [
{
name: "January",
days: createDays(31) // returns an array, nested arrays become nested ListModels when appended
},
{
name: "February",
days: createDays(28)
},
// add more months etc.
]
append(months) // appending a whole array makes each index into a ListElement at the top level
}
function createDays(dayCount) {
let days = []
for (let i = 0; i < dayCount; i++) {
days.push({
day: i + 1,
hours: createHours()
}
)
}
return days
}
function createHours() {
let hours = []
for (let i = 0; i < 24; i++) {
hours.push({
hour: i,
notes: ""
}
)
}
return hours
}
}
// Visual example code starts here ///////////////
ColumnLayout {
id: monthsColumn
Repeater {
model: monthsModel
delegate: Rectangle {
id: month
color: "pink"
implicitWidth: daysRow.implicitWidth + 10
implicitHeight: daysRow.implicitHeight + 10
RowLayout {
id: daysRow
anchors {
centerIn: parent
}
Text {
text: model.name
}
Repeater {
model: days // refers to the "days" entry in monthsModel.get(<monthIndex>)
delegate: Rectangle {
id: day
color: "orange"
implicitWidth: hoursColumn.implicitWidth + 10
implicitHeight: hoursColumn.implicitHeight + 10
ColumnLayout {
id: hoursColumn
anchors {
centerIn: parent
}
Text {
text: model.day
}
Repeater {
model: hours // refers to the "hours" entry in monthsModel.get(<monthIndex>).get(<dayIndex>)
delegate: Rectangle {
id: hour
color: "yellow"
implicitHeight: 5
implicitWidth: 5
// do something here with model.notes for each hour
}
}
}
}
}
}
}
}
}
}
The output of this shows months in pink, days in orange, and hours in yellow:

Related

QML need to create component with bool property all list elements were within limits

I was thinking I need a component similar to ListModel, but I need to extend it to expose a readonly bool property such as "all list elements were within minimum and maximum limit" so I can do logic outside the component the determine certain things. How should I go about doing this extending a boolean property based on model's contents?
I guess naive way is to just add the qml property and do javascript loop on QML side to check all model contents but that might not be so good performance
Have you considered DelegateModel? It allows you to create "views" on your ListModel so you can control what you want to be displayed via the filterOnGroup property.
It is rather difficult to comprehend, but, in the following example, I have a ListModel containing 5 cities. When you start changing the RangeSlider the 5 cities will be filtered based on the minimum/maximum population selected. This works by updating the boolean function filter on the DelegateModel to reflect the cities that are now visible.
property var filter: model => model.pop >= rangeSlider.first.value
&& model.pop <= rangeSlider.second.value
Here's the full code snippet:
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQml.Models 2.15
Page {
anchors.fill: parent
ColumnLayout {
anchors.fill: parent
Label { text: qsTr("States") }
ListView {
Layout.fillWidth: true
Layout.fillHeight: true
model: DelegateModel {
id: filterDelegateModel
property int updateIndex: 0
property var filter: model => model.pop >= rangeSlider.first.value
&& model.pop <= rangeSlider.second.value
onFilterChanged: Qt.callLater(update)
model: us_states
groups: [
DelegateModelGroup {
id: allItems
name: "all"
includeByDefault: true
onCountChanged: {
if (filterDelegateModel.updateIndex > allItems.count) filterDelegateModel.updateIndex = allItems.count;
if (filterDelegateModel.updateIndex < allItems.count) Qt.callLater(update, filterDelegateModel.updateIndex);
}
},
DelegateModelGroup {
id: visibleItems
name: "visible"
}]
filterOnGroup: "visible"
delegate: Frame {
id: frame
width: ListView.view.width - 20
background: Rectangle {
color: (frame.DelegateModel.visibleIndex & 1) ? "#f0f0f0" : "#e0e0e0"
border.color: "#c0c0c0"
}
RowLayout {
width: parent.width
Text {
text: (frame.DelegateModel.visibleIndex + 1)
color: "#808080"
}
Text {
Layout.fillWidth: true
text: model.state
}
Text {
text: qsTr("pop: %1 M").arg((pop / 1000000).toFixed(2))
}
}
}
function update(startIndex) {
startIndex = startIndex ?? 0;
if (startIndex < 0) startIndex = 0;
if (startIndex >= allItems.count) {
updateIndex = allItems.count;
return;
}
updateIndex = startIndex;
if (updateIndex === 0) {
allItems.setGroups(0, allItems.count, ["all"]);
}
for (let ts = Date.now(); updateIndex < allItems.count && Date.now() < ts + 50; updateIndex++) {
let visible = !filter || filter(allItems.get(filterDelegateModel.updateIndex).model);
if (!visible) continue;
allItems.setGroups(updateIndex, 1, ["all", "visible"]);
}
if (updateIndex < allItems.count) Qt.callLater(update, updateIndex);
}
Component.onCompleted: Qt.callLater(update)
}
}
Label { text: "Population Range" }
RangeSlider {
id: rangeSlider
Layout.fillWidth: true
from: 0
to: 100000000
first.value: 1
first.onMoved: Qt.callLater(filterDelegateModel.update)
second.value: 100000000
second.onMoved: Qt.callLater(filterDelegateModel.update)
stepSize: 1000000
}
Label { text: qsTr("Minimum %1 M").arg((rangeSlider.first.value / 1000000).toFixed(2)) }
Label { text: qsTr("Maximum %1 M").arg((rangeSlider.second.value / 1000000).toFixed(2)) }
}
ListModel {
id: us_states
ListElement { state:"California"; pop: 39350000 }
ListElement { state:"Texas"; pop: 28640000 }
ListElement { state:"New York"; pop: 8380000 }
ListElement { state:"Nevada"; pop: 3030000 }
ListElement { state:"Las Vegas"; pop: 644000 }
}
}
You can Try it Online!
I have refactored the above into a FilterDelegateModel reusable component. Feel free to check it out:
https://github.com/stephenquan/qt5-qml-toolkit
https://github.com/stephenquan/qt5-qml-toolkit/wiki/FilterDelegateModel

How to bind to a property of a repeater-generated item outside of the repeater?

I would like to be able to bind to a property of an item generated by Repeater to do something with it, e.g. to show its coordinates. For that purpose I am using itemAt() like this:
ListModel {
id: modelNodes
ListElement { name: "Banana"; x: 100; y: 200 }
ListElement { name: "Orange"; x: 150; y: 100 }
}
Repeater {
id: foo
model: modelNodes
Rectangle {
x: model.x; y: model.y
width: textBox.implicitWidth + 20
height: textBox.implicitHeight + 20
color: "red"
Drag.active: true
Text {
id: textBox
anchors.centerIn: parent
color: "white"
text: model.name + ": " + foo.itemAt(index).x
}
MouseArea {
anchors.fill: parent
drag.target: parent
}
}
}
Text {
id: moo
Binding {
target: moo
property: "text"
value: foo.itemAt(0).x + " -> " + foo.itemAt(1).x
}
}
Inside the delegate this works fine, but when I attempt to use it outside of the Repeater (i.e. to bind moo's text to it), I get the following error:
TypeError: Cannot read property 'x' of null
How to fix this?
The reason the Binding object doesn't work outside of the Repeater is because the Repeater has not constructed its items yet when the binding is being evaluated. To fix this, you can move the binding into the Component.onCompleted handler. Then just use the Qt.binding() function to do binding from javascript (docs).
Text {
Component.onCompleted: {
text = Qt.binding(function() { return foo.itemAt(0).x + ", " + foo.itemAt(1).x })
}
}
You don't.
(or more precisely, you shouldn't)
Delegates shouldn't store state or data, just display it or be able to interact with it.
In your case what you are after is the data stored in the model.
Your solution should be to modify your model in your delegates and get the data from your model if you want.
I've created a small example of what I mean:
import QtQuick 2.15
import QtQuick.Window 2.12
import QtQuick.Controls 2.12
Window {
visible: true
width: 800
height: 640
ListModel {
id: modelNodes
ListElement { name: "Banana"; x: 50; y: 50 }
ListElement { name: "Orange"; x: 50; y: 100 }
}
Row {
anchors.centerIn: parent
spacing: 1
Repeater {
model: 2 // display 2 copy of the delegates for demonstration purposes
Rectangle {
color: "transparent"
width: 300
height: 300
border.width: 1
Repeater {
id: foo
model: modelNodes
Rectangle {
x: model.x; y: model.y
width: textBox.implicitWidth + 20
height: textBox.implicitHeight + 20
color: "red"
DragHandler {
dragThreshold: 0
}
onXChanged: model.x = x // modify model data when dragging
onYChanged: model.y = y
Text {
id: textBox
anchors.centerIn: parent
color: "white"
text: model.name + ": " + foo.itemAt(index).x
}
}
}
}
}
}
Instantiator {
model: modelNodes
delegate: Binding { // the hacky solution to the initial problem.
target: myText
property: model.name.toLowerCase() + "Point"
value: Qt.point(model.x, model.y)
}
}
Text {
id: myText
property point bananaPoint
property point orangePoint
anchors.right: parent.right
text: JSON.stringify(bananaPoint)
}
ListView {
anchors.fill: parent
model: modelNodes
delegate: Text {
text: `${model.name} - (${model.x} - ${model.y})`
}
}
}
I've used a hacky solution to your initial problem with an Instantiator of Bindings, I don't really understand the usecase so that might not be the ideal solution. Here it creates a binding for every element of your model but that's weird. If you only want data from your first row, you may want to do when: index === 0 in the Binding. I've created a third party library to get a cleaner code : https://github.com/okcerg/qmlmodelhelper
This will result in the following code for your outside Text (and allowing you to get rid of the weird Instantiator + Binding part):
Text {
readonly property var firstRowData: modelNodes.ModelHelper.map(0)
text: firstRowData.x + ", " + firstRowData.y
}
Note that my point about not storing data in delegates (or accessing them from outside) still stands for whatever solution you chose.

QML SwipeView or Tumbler gets reloaded upon content change

I'm trying to set up a crawler that holds items with signals that may change over time. Unfortunately, I realized, that upon any signal update, the whole SwipeView is reset, which causes the index to jump back to its initial state.
The following example you can run with e.g. qmlscene to see the behavior. Whenever the trigger is fired, then the currentIndex is reset and the view jumps back to 1/first.
Is there any way how I can remain at the index I was at? I tried a little with setting the index back manually, however, then I ended up in a flickering behavior.
The same happens if I put content into a Tumbler.
import QtQuick 2.12
import QtQuick.Controls 2.12
SwipeView {
id: id_swipeView
width: 100
height: 100
property int value: 0
property var pages: [{"a": "1", "b": "first"}, {"a": "2", "b": "second"}, {"a": id_swipeView.value, "b": "continuous"}]
Repeater {
model: id_swipeView.pages
Item{
Column{
Text{
text: modelData["a"]
}
Text{
text: modelData["b"]
}
}
}
}
Timer{
repeat: true; running: true; interval: 1000;
onTriggered: {id_swipeView.value += 1}
}
}
Instead of using a list of objects as a model, it is better to use a ListModel that allows you to handle the values in a simple way, unlike the list that is created if an element is modified.
import QtQuick 2.12
import QtQuick.Controls 2.12
SwipeView {
id: id_swipeView
width: 100
height: 100
ListModel {
id: list_model
ListElement {
a: 1
b: "first"
}
ListElement {
a: 2
b: "second"
}
ListElement {
a: 0
b: "continuous"
}
}
Repeater {
model: list_model
Item {
Column {
Text {
text: model.a
}
Text {
text: model.b
}
}
}
}
Timer {
repeat: true
running: true
interval: 1000
onTriggered: {
var value = list_model.get(2).a;
list_model.setProperty(2, "a", value + 1);
}
}
}

QML repeater item highlight handling

I have implemented the following section
{
id: idLeftArrow
.
.
.
.
}
Row
{
id: idIpEditModeItem
anchors.left: idLeftArrow.right
visible: true
Repeater
{
id: idIpHighlightRepeater
model: 12
Text
{
id: idDigits
text: "0"
font.pointSize: 10
color: "yellow"
}
}
}
Image
{
id: idIpHiglight_Image
width: editModeIPWidth
height: editModeIPHeight
x: idIpHighlightRepeater.itemAt(ipCurrSelectedDigitIndex).x
y: idIpHighlightRepeater.itemAt(ipCurrSelectedDigitIndex).y
visible: false
source: "focus.png"
}
Here I am getting output like this
But I want output like this(there will be a gap between each character)
Also I have a idIpHiglight_Image which is using to highlight each digit. On launch I need output like this
But in my case the highlight is not getting set to the proper location. I am getting output something like this
Could anyone please help me to set the output exactly like this:
Also, on each left and right key press, I need to move the cursor properly to next/previous digit.
I wrote code like
onIpCurrSelectedDigitIndexChanged:
{
if( idIpHighlightRepeater.count == ipCurrSelectedDigitIndex)
{
ipCurrSelectedDigitIndex = 0
}
else if( 0 > ipCurrSelectedDigitIndex)
{
ipCurrSelectedDigitIndex = idIpHighlightRepeater.count - 1
}
}
After executing the code, I am getting error like
[W] (qrc:/common/qml/controls/CustomItem.qml:120) qrc:/common/qml/controls/EditListItem.qml:120: TypeError: Type error
[W] (qrc:/common/qml/controls/CustomItem.qml:119) qrc:/common/qml/controls/EditListItem.qml:119: TypeError: Type error
This the lines were i am getting the above error
I would do 2 different Components for the number and for the delimeter, something like this:
import QtQuick 2.12
import QtQuick.Window 2.12
Window {
id: main
visible: true
width: 600
height: 400
Component {
id: number
Text
{
text: "0"
font.pointSize: 16
color: "yellow"
padding: 5
Rectangle {
anchors.fill: parent
color: "transparent"
border { width: 3; color: "orange" }
visible: itemIndex == itemSelected
}
}
}
Component {
id: delimeter
Text
{
text: "."
font.pointSize: 16
color: "yellow"
}
}
Rectangle
{
id: rect
property int selected: -1;
color: "black"
anchors.centerIn: parent
width: layout.width
height: layout.height
Row {
id: layout
Repeater
{
id: repeater
model: 15
delegate: Loader {
id: loader
property int itemSelected: rect.selected;
property int itemIndex: index;
sourceComponent: ((index + 1) % 4 === 0) ? delimeter : number
}
}
}
}
Timer {
interval: 1000
repeat: true
running: true
onTriggered: {
if(rect.selected >= 15)
rect.selected = 0;
else
rect.selected ++;
}
}
}
the result:

QML GridView hide one cell

It's possible to hide certain cell in GridView? I set delegate, but I still got empty place for this GridView element. It's possible to do this?
visible: false
width: 0
height: 0
As was said in the comment, you can indeed use a QSortFilterProxy model, but here is another solution. You could implement a pure-QML FilterProxyModel, using DelegateModel and DelegateModelGroup
import QtQuick 2.10
import QtQml.Models 2.3
DelegateModel {
property var filterAccepts: function(item) {
return true
}
onFilterAcceptsChanged: refilter()
function refilter() {
if(hidden.count>0)
hidden.setGroups(0, hidden.count, "default")
if(items.count>0)
items.setGroups(0, items.count, "default")
}
function filter() {
while (unsortedItems.count > 0) {
var item = unsortedItems.get(0)
if(filterAccepts(item.model))
item.groups = "items"
else
item.groups = "hidden"
}
}
items.includeByDefault: false
groups: [
DelegateModelGroup {
id: default
name: "default"
includeByDefault: true
onChanged: filter()
},
DelegateModelGroup {
id: hidden
name: "hidden"
}
]
}
Explanation: Every time an item is added to the model, it is added in the "default" group, which triggers the onChanged handler that will call filter().
Filter() will look for items in the default group, and move them either in the items group (which will make them visible) or to the hidden group, depending on the result of the filterAccepts function.
When filterAccept changes, the SortProxyModel will move every item to the default group to trigger a global refiltering.
You can then use your proxy model like this:
FilterProxyModel
{
id: filterProxyModel
model: <YourBaseModel>
delegate: <YourDelegate>
filterAccepts: function(item) {
// Eg: Only "small" items will be displayed
return item.size == "small"
}
}
GridView
{
anchors.fill: parent
model: filterProxyModel
cellHeight: 100
cellWidth: 100
}
Another simplified solution with QML only, based on hiding items.
import QtQuick 2.7
import QtQuick.Window 2.2
import QtQuick.Layouts 1.2
Window {
id: window
title: "test"
visible: true
width: 400
height: 400
GridLayout {
id: layout
anchors.fill: parent
columns: 4
Repeater {
id: container
model: 20
Rectangle {
id: item
property int itemIndex: index
Layout.fillWidth: true
height: 60
color: Qt.rgba(Math.random(),Math.random(),Math.random(),1)
Text {
anchors.centerIn: parent
text:item.itemIndex
}
MouseArea {
anchors.fill: parent
onClicked: {
item.visible = false;
layout.doIt(item.itemIndex);
}
}
}
}
function doIt(index)
{
var item = container.itemAt(index);
if(item)
item.visible = false;
for(var i = index - 1;i >= 0;i --)
{
var prev_item = container.itemAt(i);
if(prev_item.visible) {
prev_item.Layout.columnSpan ++;
break;
}
}
}
}
}

Resources