QML - Retrieving index of delegate in DelegateModel - qt

I have trouble retrieving the index of a delegate that is instantiated inside a DelegateModel for a ListView.
The minimal example as following:
LastProcedures.qml
ListModel {
ListElement {
procedure: "Liver Resection"
surgeon: "Prof. Dr. Joyride"
recent: true
}
...
}
main.qml
ListView {
id: list_lastProcedures
model: displayDelegateModel
}
DelegateModel {
id: displayDelegateModel
delegate: lastProceduresDelegate
model: LastProcedures {}
groups: [
DelegateModelGroup {
includeByDefault: false
name: "recent"
}
]
filterOnGroup: "recent"
Component.onCompleted: {
var rowCount = model.count;
items.remove(0,rowCount);
for( var i = 0;i < rowCount;i++ ) {
var entry = model.get(i);
// Only the recent three
if((entry.recent == true) && (items.count < 3)) {
items.insert(entry, "recent");
}
}
}
}
Component {
id: lastProceduresDelegate
Text{
text: model.index
}
}
The text index prints always -1. Without a DelegateModel it prints the index in the ListView. How can I access the correct index of the delegate in the Listview?

you can use "lastProceduresDelegate.DelegateModel.itemsIndex" instead of "model.index"
just like this:
Component {
id: lastProceduresDelegate
Text{
text: lastProceduresDelegate.DelegateModel.itemsIndex
}

I ended up with not removing all entries and adding them back to groups, but instead just remove unwanted entries. This ways the index stays valid.
If someone could explain this behavior further, that would be nice.
DelegateModel {
id: displayDelegateModel
delegate: lastProceduresDelegate
model: LastProcedures {}
groups: [
DelegateModelGroup {
includeByDefault: true
name: "recenttrue"
}
]
filterOnGroup: "recenttrue"
Component.onCompleted: {
for( var i = 0;i < items.count;i++ ) {
var entry = items.get(i).model;
// Only the recent
if((entry.recent != true)) {
items.removeGroups(i, 1, "recenttrue");
}
}
}
}

The DelegateModel has some hidden magic regarding groups (it's not very visible but it's here ). For each group you create, the DelegateModel attached property will receive two new properties: <group>Index and in<Group>.
In your case this means you will get the following properties: recentIndex and inRecent (or in your own answer: recenttrueIndex and inRecenttrue).
I think with what you want to do you should go the recenttrue route and draft the Component as follows:
Component {
id: lastProceduresDelegate
Text {
text: lastProceduresDelegate.DelegateModel.recenttrueIndex
}
}

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 create different component base on ListModel in QML

There are 9 parameters that I need to use TextField1 to input value.
So I use
ListModel lstPara {
ListElement{
text:"A";value:"123"
}...(9 Elements)
}
Grid{
id: grid
anchors.fill: parent
columns: 3
spacing: 5
Repeater {
id: rpPara
model: lstPara
delegate: TextField1 {
}
}
}
But now there is a parameter that i need to use in another QML type to set the value, all others are used in TextField1.
I tried to define ListModel like this
ListModel lstPara{
ListElement {
text: "A";
type: 1";
value: "123"
}
ListElement {
text: "B";
type: 2";
value: "321"
}
...(9 Elements)
}
Grid{
id: grid
anchors.fill: parent
columns: 3
spacing: 5
Repeater {
id: rpPara
model: lstPara
(some code : like this)
delegate: {
return type === 1 ?
TextField1 {
}
:
another QML type {
}
}
}
}
The code above can not run.
And I don`t want to write 8 TextField1 and 1 another QML type.
So, is there a way to use ListModel?
You can't directly use an Item declaration in a conditional expression like that, but you can do it with a Component. Here's an example of how to do it using a Loader as your delegate, and choosing which Component to load based on the model:
ListModel {
id: lstPara
ListElement {
text: "A"
type: 1
value: "123"
}
ListElement {
text: "B"
type: 2
value: "321"
}
}
Grid {
id:grid
anchors.fill: parent
columns: 3
spacing: 5
Repeater {
id: rpPara
model: lstPara
delegate: Loader {
sourceComponent: type === 1 ? someText : otherText
onLoaded: {
item.text = text
item.value = value
}
}
}
Component {
id: someText
Text {
property string value
color: "blue"
}
}
Component {
id: otherText
Text {
property string value
color: "red"
}
}
}

ReferenceError: "Item" is not defined

I want to check if an item of component is vissible or not but I don't know how to do this.
Here is my code but error is "ReferenceError: tagFilterPlaylist is not defined" how to solve this?
Repeater {
id: tagRepeater
model: main.playListBrowseModel
Component {
id: componentTagFilterPlaylist
BasicUI.Tag {
id: tagFilterPlaylist
tag: "playlist"
selected: true
}
}
Loader {
id: filterLoader
sourceComponent:
if (item_type === "playlist"){
console.debug("check"+tagFilterPlaylist)
if (!tagFilterPlaylist) {
tagFilterPlaylist.visible = true;
return componentTagFilterPlaylist
}
}
}
}
You can't refer to an id within a Component because you first need to have an instance of that Component. What you should do is simply keep a boolean property outside the Repeater that keeps track of whether or not you're displaying your one and only tagFilterPlaylist.
property bool playlistVisible: false
Component {
id: componentTagFilterPlaylist
...
}
Repeater {
id: tagRepeater
model: main.playListBrowseModel
Loader {
id: filterLoader
sourceComponent:
if (item_type === "playlist"){
if (!playlistVisible) {
playlistVisible = true;
return componentTagFilterPlaylist
}
}
}
}

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;
}
}
}
}
}

How to link from ComboBox to another ComboBox

I try to make a converter app depending on two ComboBoxes using Controls 2.
For example:
// First combobox is input unit:
ComboBox {
id: res_combo1
width: res_comborect1.width
currentIndex: 0
model: ListModel {
id: model1
ListItem { text: qsTr("meter") }
ListItem { text: qsTr("mile") }
ListItem { text: qsTr("km") }
}
}
}
// Second combobox is output unit:
ComboBox {
id: res_combo2
width: res_comborect1.width
currentIndex: 0
model: ListModel {
id: model2
ListItem { text: qsTr("meter") }
ListItem { text: qsTr("mile") }
ListItem { text: qsTr("km") }
}
}
}
When current index is selected from first combobox, second combobox should remove the current index. When another index is selected from first, second should restore previous index and it should remove current index dynamically. For example if meter is selected, second combobox should be {"mile", "km"}
I only know a very long way that can be performed with combination of a few index, but my combobox data includes 20 item so I cannot apply this way. Long way:
function visible1(){
if(res_combo1.currentIndex==0){
return true
}
else{
return false
}
}
function visible2(){
if(res_combo1.currentIndex==1){
return true
}
else{
return false
}
}
function visible3(){
if(res_combo1.currentIndex==2){
return true
}
else{
return false
}
}
// if meter is selected from first combobox, second combobox:
RowLayout{
visible: parent.visible1()
ComboBox {
id: outcombo1
currentIndex: 0
model: ListModel {
id: model_o1
ListElement { text: qsTr("mile") }
ListElement { text: qsTr("km") }
}
}
}
// if mile is selected from first combobox:
RowLayout{
visible: parent.visible2()
ComboBox {
id: outcombo1
currentIndex: 0
model: ListModel {
id: model_o2
ListElement { text: qsTr("meter") }
ListElement { text: qsTr("km") }
}
}
}
// if km is selected from first combobox:
RowLayout{
visible: parent.visible3()
ComboBox {
id: outcombo1
currentIndex: 0
model: ListModel {
id: model_o3
ListElement { text: qsTr("meter") }
ListElement { text: qsTr("mile") }
}
}
}
I think we cannot change ListItem dynamically, so JavaScript with JSON list model is required by finding the current index via:
function find(model, criteria) {
for(var i = 0; i < model.count; ++i) if (criteria(model.get(i))) return i
return null
}
But I couldn't do this in QML. Is there any solution? Thanks
You cannot change a ListItem, but you CAN change the ListModel itself.
ListModel {
id: fromModel
ListElement {text: "m" }
ListElement {text: "mile" }
ListElement {text: "km" }
}
ListModel {
id: toModel
function updateModel()
{
// Update model Here
}
}
RowLayout
{
anchors.centerIn: parent
ComboBox
{
id: fromCombobox
model: fromModel
onCurrentIndexChanged: toModel.updateModel()
}
ComboBox
{
id: toComboBox
model: toModel
}
}
This way, every time the first combo changes, the second combo's model will be updated.
Before you update the model 2, check if the previously selected item in combo 2 will still be available after the update, in order to restore it once the model2 is rebuilt.
function updateModel()
{
// Save old selection
var selectedFrom = fromModel.get(fromCombobox.currentIndex).text
if(toComboBox.currentText != selectedFrom)
var valueToRestore = toComboBox.currentText
else
valueToRestore = ""
// Update model
clear()
for(var i=0; i<fromModel.count; i++)
{
if(i == fromCombobox.currentIndex)
continue
append(fromModel.get(i))
}
//Restore selection
for(var i=0; i<toModel.count; i++)
{
// If no value to restore, select first available
if(valueToRestore == "")
{
if(toModel.get(i).text != selectedFrom)
{
toComboBox.currentIndex = i
break
}
}
// Else, restore previously selected item
else
{
if(toModel.get(i).text == valueToRestore)
{
toComboBox.currentIndex = i
break
}
}
}
}

Resources