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

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

Related

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

ListModel content not set properly

I have the following code. It basically builds a dialog with a TableView in it, in which I can set values manually in the cells:
import QtQuick 2.2
import QtQuick.Window 2.1
import QtQuick.Controls 1.2
import QtQuick.Dialogs 1.2
Window {
visible: true
width: 538
height: 360
MouseArea {
anchors.fill: parent
onClicked: {
dial.open()
}
}
Dialog {
id:dial
width: 300
height: 500
title: "Set Path Parameters"
standardButtons: StandardButton.Ok | StandardButton.Cancel
signal updSig(var content)
ListModel {
id: streetsModel
property int count: 100
Component.onCompleted: {
for (var i=1 ; i<= count ; ++i)
streetsModel.append({"street_alias":"str_"+i , "start_xstart_y": "", "end_xend_y": "", "width": ""})
}
//todo: erste Spalte auch einbeziehen
function getColumnContent(role) {
var cont = ""
for (var i=0 ; i< count ; ++i) {
var cellContent;
cellContent = streetsModel.get(i).role
//streetsModel.get(styleData.row).role
if(!(cellContent === "") && !(cellContent === undefined))
cont += cellContent === "" ? "" : (cellContent + "\n")
}
cont = cont.slice(0,-1)
return cont
}
}
Item {
anchors.fill: parent
Component {
id: editableDelegate
Item {
TextInput {
id: textinput
width: parent.width
anchors.margins: 4
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: styleData.value
color: styleData.textColor
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: textinput.forceActiveFocus()
}
onEditingFinished: {
var role = styleData.role
streetsModel.set(styleData.row, {role: textinput.text})
console.log(streetsModel.get(styleData.row).role)
console.log(styleData.row, role)
}
}
}
}
}
TableView {
id: streetsTab
model: streetsModel
anchors.margins: 12
anchors.fill:parent
TableViewColumn {
id: strt_al_cl
role: "street_alias"
title: "Street Alias"
width: 120
}
TableViewColumn {
id: start_xy_cl
role: "start_xstart_y"
title: "StartX,StartY"
width: 120
}
TableViewColumn {
id: end_xy_cl
role: "end_xend_y"
title: "EndX,EndY"
width: 120
}
TableViewColumn {
id: width_cl
role: "width"
title: "Width"
width: 120
}
itemDelegate: {
return editableDelegate;
}
}
onAccepted: {
var content = [streetsModel.getColumnContent(start_xy_cl.role), streetsModel.getColumnContent(end_xy_cl.role), streetsModel.getColumnContent(width_cl.role)];
updSig(content)
this.close()
}
onRejected: this.close()
}
}
Now, if I set the values 1 in cellnumber (0,0), 2 in cellnumber (0,1) and 3 in cellnumber (0,2) (and click on an arbitrary other cell after that to insert the content), I get as content in my onAccepted-method [3,3,3]. This means the values for the first two roles start_xstart_y and end_xend_y get overwritten by the third role value for the role width. Why is this? If the code is run, on the console, the correct values in the cells are printed (see the console.log(...) parts), which stem from the model directly. So I don't understand why they are finally overwritten

QML - Retrieving index of delegate in DelegateModel

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

QML: setProperty has no effect

When I try to change a property value of an item contained into a ListModel the following code has no effect:
Main.qml
import QtQuick 2.0
Item {
anchors.fill: parent
ListModel { id: modelCrayon }
Component.onCompleted: {
for (var i = 0; i < 10; i++)
modelCrayon.append( { _tag: i, _source: "resources/crayon-green.png", _selected: false } )
}
Column {
x: -170
spacing: 0
Repeater {
model: modelCrayon
delegate: Crayon {
tag: _tag
source: _source
selected: _selected
onCrayonSelected: {
for (var i = 0; i < modelCrayon.count; i++) {
if (i == tag) continue;
modelCrayon.setProperty(i, "_selected", false);
}
}
}
}
}
}
Crayon.qml
import QtQuick 2.0
Image {
property bool selected
property int tag
signal crayonSelected()
id: crayon
smooth: true
fillMode: Image.PreserveAspectFit
onSelectedChanged: console.debug(tag, selected)
MouseArea {
anchors.fill: parent
onClicked: {
selected = !selected
if (selected) crayonSelected()
}
}
states: State {
name: "selected"; when: selected == true
PropertyChanges { target: crayon; x: 30 }
}
transitions: Transition {
from: ""; to: "selected"
PropertyAnimation { property: "x"; duration: 500; easing.type: Easing.InOutQuad }
}
}
Nothing is shown on console, so the "selected" var is never changed.
I'm sure there's something obvious I'm missing.
By the way, is there a smarter way to use a ListModel as a OptionBox? I mean I want only ONE item at time must have the selected property == true. Or, in other words, keep tracks of the selected index.
This is a working code to achieve what I asked. But it doens't answer why the property was not set.
ListView {
id: list
anchors.verticalCenter: parent.verticalCenter
height: parent.height
x: -150
spacing: 0
orientation: ListView.Vertical
focus: true
model: modelCrayon
delegate: Crayon {
id: delegate
source: _source
selected: ListView.isCurrentItem
MouseArea {
anchors.fill: parent
onClicked: list.currentIndex = index
}
}
}
I have tested your sample code (the Column version), and it works well with Qt 5.4 / Windows 7 64bit.
What is your running environment?

Move items between ListModels

Assume a QML application with two list models, each holding, say 3, items. Graphically the two lists are displayed in two containers. I'd like to implement a drag-and-drop functionality to reorder the items inside their list and to swap two items between different lists.
Reordering in one list poses no problem due to the move method of the ListModel. For swapping items between the two list models, however, I think I have to use the remove and insert or set operations of ListModel. First experiments using set didn't work.
What is the canonical way to achieve this that also works with transitions? (E.g. just swapping the items' roles is no solution, I will have to swap the items themselves.)
You can just use ListModel.get() to fetch an element from one view and ListModel.append() or ListModel.insert() to put it in another one (not exactly with same type)
The simple example:
import QtQuick 2.4
import QtQuick.Window 2.2
import QtQuick.Layouts 1.1
import QtQuick.Controls 1.4
Window {
width: 400
height: 400
visible: true
Component.onCompleted: {
fillModel(model1);
fillModel(model2);
}
Component {
id: delegate
Rectangle {
width: parent.width
property int itemIndex: index
property var view: ListView.view
color: (itemIndex === view.currentIndex) ? "orange" : "white"
height: 20
Text { text: name; color: moved ? "red" : "black"; anchors.centerIn: parent }
MouseArea {
anchors.fill: parent
onClicked: {
view.currentIndex = itemIndex;
}
}
}
}
RowLayout {
anchors.fill: parent
ListView {
id: list1
Layout.fillHeight: true
Layout.fillWidth: true
model: ListModel { id: model1 }
delegate: delegate
}
ColumnLayout {
Layout.alignment: Qt.AlignCenter
width: 50
Button { text: ">>>"; onClicked: moveItem(list1,list2) }
Button { text: "<<<"; onClicked: moveItem(list2,list1) }
}
ListView {
id: list2
Layout.fillHeight: true
Layout.fillWidth: true
model: ListModel { id: model2 }
delegate: delegate
add: Transition {
id: list2Transition
enabled: false
property int fromX
property int fromY
ParallelAnimation {
NumberAnimation { properties: "x"; from: list2Transition.fromX; duration: 300; }
NumberAnimation { properties: "y"; from: list2Transition.fromY; duration: 300;easing.type: Easing.OutCirc; }
PropertyAnimation {property: "color"; from: "red"; to: "white"; duration: 500 }
}
}
}
}
function fillModel(model) {
for(var i = 0;i < 15;i ++)
model.append({name: "item" + i, moved: false});
}
function moveItem(listfrom,listto) {
var item = listfrom.model.get(listfrom.currentIndex);
var newPos1 = listfrom.parent.mapFromItem(listfrom,0,listfrom.currentIndex * 20);
var newPos2 = listto.parent.mapFromItem(listto,0,listto.currentIndex * 20);
list2Transition.fromX = newPos1.x - newPos2.x;
list2Transition.fromY = newPos1.y;
list2Transition.enabled = true;
item.moved = true;
listto.model.insert(listto.currentIndex, item);
listfrom.model.remove(listfrom.currentIndex, 1);
}
}
You can also do the same action by using drag & drop functionality.

Resources