How to link from ComboBox to another ComboBox - qt

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

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

QML property binding for repeater item

How do property bindings work for repeater items? I am trying to use a property binding on an element generated by a repeater, but the binding doesn't work:
Rectangle
{
id: root
property int currentLocation: 0
Button {
text: "Change location"
onClicked: root.currentLocation = (root.currentLocation + 1) % 2
}
Repeater {
id: locationlRepeater
model: locationModel
Item {
visible: root.currentLocation == model.location // Why doesn't this update on button click?
Text {
id: locationText
text: "Location: " + model.location
}
}
}
ListModel {
id: locationModel
ListElement {
location: 0
// etc.
}
ListElement {
location: 1
// etc.
}
}
Updated: using Qt 5.6.2

Qml Get the text of listElement using the mouseAera

I want to get the name of listElement (append dynamicaly with python) when I click for reuse this name with python function. How can I get this name ?
In this example I can only get the index 0 element...
QML part :
Rectangle {
id: listRowDb
anchors.top: toolbar.bottom
width: head_row.width
height: units.gu(100)
ListModel {
id: listDb
ListElement {
name: ""
}
}
Component {
id: listDbDelegate
Row {
spacing: 100
Text { text: 'Nom de la DB : ' + name}
}
}
ListView {
id: listView1
anchors.fill: parent
model: listDb
delegate: listDbDelegate
#Don't work. When I click I get the index 0 name.
MouseArea {
anchors.fill: parent
onClicked: {python.call('example.speak2', [listDb.get(listView1.currentIndex).name], function(returnValue) {console.log(returnValue)});}
}
}
}
Python {
id: python
Component.onCompleted: {
addImportPath(Qt.resolvedUrl('../src/'));
importModule('example', function() {
console.log('module imported');
#Just a litlle test
python.call('example.speak', ['Hello World!'], function(returnValue) {
console.log('example.speak returned ' + returnValue);
})
#Python list all DB and append in listModel
python.call('example.listDb', [], function(returnValue) {
for(var i=0; i<returnValue.length; i++) {
listDb.append({"name":returnValue[i]});
}
});
});
}
The python part work (only print element name)
You have to set the MouseArea on the delegate:
Component {
id: listDbDelegate
Row {
spacing: 100
Text {
text: 'Nom de la DB : ' + name
MouseArea{
anchors.fill: parent
onClicked: console.log(name, index)
}
}
}
}
ListView {
id: listView1
anchors.fill: parent
model: listDb
delegate: listDbDelegate
}

Change property of delegate on signal

I have a TreeView, and on the signal onSortIndicatorChanged I would like to set the checked property of a CheckBox inside the delegate of a TableViewColumn in my TreeView. However, I don't know how to do this.
Component {
id: mycomp
Item {
id: myitm
CheckBox {
id: mycbx
checked: true
}
}
}
TreeView {
TableViewColumn {
delegate: myDelegate
}
onSortIndicatorChanged{
// set the checked property of the delegates to true
}
}
You could try to add a property checked to your tree view and bind the check boxes to this property instead. Then, in the even handler, just set the value of the TreeView's property:
Component {
id: mycomp
Item {
id: myitm
CheckBox {
id: mycbx
checked: view.checked
}
}
}
TreeView {
id: view
property bool checked: false
TableViewColumn {
delegate: mycomp
}
onSortIndicatorChanged {
view.checked = true
}
}
Update:
In case you want to set a per-item checked state, you can use a similar approach where you store a function as a property of the view. In the delegate, you could call this stored function to get an item specific property value:
Component {
id: mycomp
Item {
id: myitm
CheckBox {
id: mycbx
checked: view.checkFunction ? view.checkFunction(modelData) : true
}
}
}
TreeView {
id: view
property var checkFunction: null
TableViewColumn {
delegate: mycomp
}
onSortIndicatorChanged {
view.checkFunction = function(modelData) {
// Calculate the checked state based on the modelData
return modelData.foo == "bar";
}
}
}

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

Resources