"Bind" two QML CheckBoxes together, ensuring their states are always identical - qt

I'd like to create two checkboxes on different pages of a GUI such that they are semantically the "same" checkbox -- same label, same effect. (Having them on both pages is just for the user's convenience.)
This requires "binding" two CheckBox QML elements together such that the state of one is always reflected by the other, and vice-versa.
This is equivalent to what's being asked here, except I'm using QML/JS instead of JS/JQuery.
I thought that a naive implementation of binding the checked state of each checkbox to some global persistent property would work:
// Global shared state object
pragma Singleton
MySharedState {
my_feature_on: false
}
Then, on two separate pages, the exact same CheckBox instantiation:
// Checkbox implementation (on both pages
CheckBox {
checked: MySharedState.my_feature_on
onClicked: MySharedState.my_feature_on = checked
}
However, this doesn't work, because when a checkbox is clicked, it breaks the initial checked binding. This is the intended behavior, not a bug.
So how can I ensure that two checkboxes always share the same "checked" state?
EDIT: According to a comment bellow, the above implementation will work without modification in Qt Quick Controls 2, which was released with Qt 5.7, so this question only applies to prior versions of Qt (including 5.6, which is a "long-term support" release).

When a checkbox is clicked its' checked property is changed and the original checked: MySharedState.my_feature_on binding is removed.
You need to create a property binding from Javascript to restore the original binding as explained by J-P Nurmi in the bug report you linked.
For that you have to use Qt.binding().
CheckBox {
checked: MySharedState.my_feature_on
onClicked: { // the checked binding is removed since checked has been changed externally to the binding
MySharedState.my_feature_on = checked
checked = Qt.binding(function() {return MySharedState.my_feature_on}); //we restore the checked binding
}
}

Using a two-way binding with the Binding type works:
import QtQuick 2.5
import QtQuick.Controls 1.0
ApplicationWindow {
objectName: "window"
width: 600
height: 200
visible: true
Row {
CheckBox {
id: checkBox1
text: "Check Box 1"
}
CheckBox {
id: checkBox2
text: "Check Box 2"
}
}
Binding {
target: checkBox2
property: "checked"
value: checkBox1.checked
}
Binding {
target: checkBox1
property: "checked"
value: checkBox2.checked
}
}
Although I'm not sure why it doesn't complain about a binding loop.

Related

how to add dynamically created object into a Container

I am trying to create ListModels dynamically, and insert these ListModels into a Container.
I get a variable number of ListModels, each with variable content, from a backend system. Based on the user's interaction with the GUI, one of those ListModels needs to be loaded into a known ListView. The client wants to avoid Qt/C++, so I am looking to solve this via QML.
How do I get a dynamically created object (in my case, a ListModel) into a Container? This code does not work:
property string strID;
property string strName;
Container {
id: functionListContainer;
}
// create ListModel
var newObject = Qt.createQmlObject("import QtQml.Models 2.14; ListModel { }", mainWindow);
...
// dynamically append elements into the ListModel
newObject.append({ deviceID: strID, deviceName: strName })
...
// add ListModel to Container
functionListContainer.addItem(newObject);
createQmlObject and append seem to work as intended. I get this error when running addItem code above:
"Could not convert argument 0 at"
...
"Passing incompatible arguments to C++ functions from JavaScript is dangerous and deprecated."
"This will throw a JavaScript TypeError in future releases of Qt!"
Any idea regarding how to get this to work? I know that addItem is expecting an Item, not an Object, but I do not know how to get this to work. I have tried replacing var with property Item newItem and property QtObject newObject [combined with addItem(newObject.item)], and all give (seemingly identical) errors. Is it a simple issue of casting the object into an Item? If so, what is the syntax that needs to be used?
Lastly, assuming I do have a Container with N ListModels, is it possible to refer to the container directly in the ListView? i.e.:
property int idx;
Container {
id: functionListContainer;
}
// add ListModels to container...
// use ListModel inside container
ListView {
...
model: functionListContainer.itemAt(idx);
...
}
I know that ListView is expecting a ListModel or something equivalent. But I am not sure how to connect the ListView with a Container containing ListModels, or if it is even possible.
If I were to summarize my problem, I am trying to have a ListView display different ListModels based on context, and I am trying to do this within a pure-QML framework as much as possible. Both of my questions above related to this. It would be helpful even only to clarify that this is not an option and that it is necessary to use another method like clearing and populating the ListModel (suggestions welcome).
You are trying to add a QObject (since ListModel is inherited from QAbstractListModel, which is in turn inherited from QObject) as a visual item using the addItem function. However, in QML a QObject (or QtObject) is regarded as an "storage element".
What you want to do it add the contentData of the Container:
Container {
id: functionListContainer
}
// create ListModel
var newObject = Qt.createQmlObject("import QtQml.Models 2.14; ListModel { }", mainWindow);
...
// dynamically append elements into the ListModel
newObject.append({ deviceID: strID, deviceName: strName })
...
// add ListModel to Container
functionListContainer.contentData.push(newObject)
The contentData property is the place where all children reside in the Container; QObject and QQuickItem (note that QQuickItem is inherited from QObject).
About referencing the lists, this also becomes easy when using the contentData property:
// create ListModel
var newObject = Qt.createQmlObject("import QtQml.Models 2.14; ListModel { objectName: \"the_list\" }", mainWindow);
...
// add ListModel to Container
functionListContainer.contentData.push(newObject)
console.log(functionListContainer.contentData[0])
will yield:
qml: QQmlListModel(0x55bcb7119720, "the_list")
Note that this is almost the same as using a Javascript array, the documentation of QQmlListProperty (which is what contentData is) states it is transparent.

How to detect if any of QML editable items in the root Item were edited?

I have an item of some type that consists of several QML editing controls:
Column {
id: inputItem
SpinBox {
}
TextInput {
}
ComboBox {
}
Button {
id: enableMeButton // this needs to be enabled if
text: "Apply" // anything was changed above
enabled: false
}
}
This item [Column as example] can be potentially inserted to some list view as "polymorphic" editing item so that we don't know beforehand what edit fields to handle (say we would want to move Apply outside). Or we want to use a common type with Apply to develop different editing delegates (which is the story here).
How can we detect that any of the data handled in this form was changed? Is there a generic way to do so or there is some trick to accomplish it?

Accessing members of ListView delegate which is loaded by Loader

ListView {
id: listView
model: someModel {}
delegate: Loader {
id: delegateLoader
source: {
var where;
if(model.type === 1) {
where = "RadioQuestion.qml";
}
else
where = "TextQuestion.qml";
if(delegateLoader.status == Loader.Ready) {
delegateLoader.item.question= model.question;
}
return Qt.resolvedUrl(where);
}
}
I show some questions to user by using ListView. But I cannot access members of delegate loaded by Loader.
RadioQuestion.qml has Radio Buttons and Text is just text field. I just want to get all answers after pressing a submit button but I could not figure out how I can traverse among delegates.
Maybe my approach is wrong about building this structure. Therefore I am open for better solutions.
Your question is already exposed through the model, so your delegates should directly bind to it, so assuming that question is a property of the model:
// from inside the delegate
question: delegateRootId.ListView.view.model.question
or assuming that question is a list element role:
// from inside the delegate
question: model.question
And if you were careful enough not to name the property inside the delegate question thus shadowing the model role, you could simply:
// from inside the delegate
questionSource: question
Not to mention that if your model "scheme" is known and it is a given that you will have a question role and that role will be displayed in the delegate, you don't even need any additional property in the delegate to begin with, you can bind the actual item directly to the question, for example:
// from inside the delegate
Text { text: question }
And that's all that's really needed.
Alternatively you can use a Binding or a Connections element or a simple signal handler to delay the operation until the loader has actually completed the loading. For example:
delegate: Loader {
source: Qt.resolvedUrl(model.type === 1 ? "RadioQuestion.qml" : "TextQuestion.qml")
onStatusChanged: if (status === Loader.Ready) if (item) item.question= model.question
}
Button {
text: "Submit answers"
onClicked: {
for(var child in listView.contentItem.children) {
var c = listView.contentItem.children[child].item;
if(typeof c !== "undefined")
console.log(c.answer)
}
}
}
You can get properties of loader delegate like this. We use "undefined" check, because one object in children of contentItem is undefined. That was second object in the list, I do not know why.

QML: How to implement signal handler of a child

I'm editing a component in Qt Creator. It suggested me to split the component in UI and not UI parts. My components exposes 2 custom properties.
ComponentViewForm {
property string step: '0'
property string setStep: '0'
}
A TextInput inside the UI-Part is bound to step
It should set the property setStep in onAccepted handler.
First one is easy. The binding can be edited in UI-Editor directly
But how do I implement the signal-handler of the child?
I've implemented it directly in the UI.
TextInput {
id: step
text: parent.step
onAccepted:
{
parent.setStep = text
}
}
It works, but Qt-Creator rejects to open it in UI-mode any more.
You can export the TextInput from your ComponentViewForm. There's a small Export button in the Navigator tab in Qt Quick UI forms editor. Assume that the id of TextInput is stepInput, ComponentViewForm.ui.qml should have an alias property property alias stepInput: stepInput in source code after you click the Export button.
You can implement property binding and signal handlers in ComponentView.qml like this:
ComponentViewForm {
property string step: '0'
property string setStep: '0'
stepInput.text: step
stepInput.onAccepted:
{
setStep = stepInput.text;
}
}

How to know when the selected item has been changed in a QML QListView?

I'm using QtQuick 2.0 and and a QML ListView to display some items, and I need to know when the user chooses a different item. Emitting a signal when the user clicks a mouse area in the delegate works, i.e.
MouseArea{
onClicked: {
controller.itemChanged(model.item);
someList.currentIndex = index;
}
}
but only if the user uses the mouse to choose the item, but it doesn't work if the user uses the arrow keys.
I've been looking through the docs to find what signal is emitted when the currentIndex is changed, but I can't seem to find any. I'm looking for something similar to QListWidget::itemSelectionChanged() but it seems QML ListView doesn't have that.
You just need onCurrentItemChanged:{} in your ListView.
I ended up having to re-implement keyboard behaviour and exposing the model data from the delegate so I could fire the signal when a key is pressed.
ListView {
id: myList
focus: true
orientation: "Horizontal" //This is a horizontal list
signal itemChanged(var item)
interactive: false //Disable interactive so we can re-implement key behaviour
Keys.onPressed: {
if (event.key == Qt.Key_Left){
myList.decrementCurrentIndex(); //Change the current list selection
itemChanged(myList.currentItem.selectedItem.data); //Fire signal notifying that the selectedItem has changed
}
else if (event.key == Qt.Key_Right){
myList.incrementCurrentIndex(); //Change the current list selection
itemChanged(myList.currentItem.selectedItem.data); //Fire signal notifying that the selectedItem has changed
}
}
delegate: Component {
Rectangle {
id: myItem
property variant selectedItem: model //expose the model object
MouseArea {
anchors.fill: parent
onClicked: {
myList.currentIndex = index; //Change the current selected item to the clicked item
itemChanged(model.data); //Fire signal notifying that the selectedItem has changed
}
}
}
}
}
With this solution you have to manually change the item in QML whenever the user clicks an item or presses a key. I'm not sure this'd be an optimal solution with GridView but it works fine with ListView.
See this question. There are two approaches you can take
Connect to another component's event
Handle the event within that component
The signal handler is named on<SignalName> with the first letter of the signal in uppercase.

Resources