Accessing members of ListView delegate which is loaded by Loader - qt

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.

Related

How can I give a Qt QML item a property that doesn't notify when it changes?

I (very) occasionally find myself needing to add a property to an item that isn't bindable in other expressions, that is to say that when its value changes, it doesn't notify anything that uses its value and thus doesn't cause expressions to be re-evaluated. The point of this is to have essentially what is a pure state variable that can't get itself involved in binding loops.
As a concrete example:
Item
{
property string state: ""
visible:
{
if(some_condition)
{
state = "foo";
return true;
}
if(state === some_other_external_state)
return true;
state = "";
return false;
}
}
In this case where some_condition becomes true the visible property sets state and a binding loop occurs since visible depends on the value of state. If the state property didn't bind and was, as its name implies, purely a state variable, this loop is avoided.
One hacky solution that I've used before is:
Item
{
property var state: [""]
visible:
{
if(some_condition)
{
state[0] = "foo";
return true;
}
if(state[0] === some_other_external_state)
return true;
state[0] = "";
return false;
}
}
This works because array properties don't notify when the contents of the array change, they only notify when the array itself changes. It's not pretty, but it works.
You cannot create non-bindable property in QML. But you can control what bindings will be created.
By design property bindings should not change values of other properties. This is the source of problem in code sample you provided. There is not enough details to provide full solution but consider this pseudo code:
Item {
// Item has state property already - you should not redefine it
// property string state: ""
visible: state !== "" // or whatever else is correct, but keep it simple
// Optionally:
onVisibleChanged: {
// Any extra operations that should be done
// when item is being shown or hidden
// But you should not change state variable here!
}
// Monitor all events that should change state and keep it up to date
// Usually Qt will already have a signal notifying about particular change
// or you can define signals you need and emit in proper places
Connections {
target: other_object
onSignalForSomeSpecificEvent: state = "bar"
}
// Some conditions you can define in declarative form
// Suitable for condition depending on other properties
property bool someCondition
onSomeConditionChaged: state = 'foo'
}
A lot depends on particular design but 2 rules of thumb:
bindings should not alter other properties
use signal handlers to avoid unwanted bindings

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.

QML - Capture all UI events of children objects

In my QML project, I need an object to capture all the UI events of its children objects. So, if any of its children register a click or something, the parent object needs to know about it. The issue here is that all of the children objects are pre-defined classes such as MyButton or MyComboBox. These classes all have defined MouseAreas and onClicked() functions that can't be overridden. Therefore, I need the parent object to capture all the events of its children WITHOUT modifying the MouseAreas of the children. Please let me know the best way to accomplish this.
You can crawl the object tree, connecting a function to every onClicked-signal.
For this we need three parts:
The signal that shall be connected
The function that does the crawling
A function that creates another function to call the signal with custom arguments.
I chose, that my signal shall have to arguments: sender and arguments. Sender is the object, that I clicked on, and arguments are the arguments of the clicked-signal of the clicked object. This is empty for QtQuick.Controls 2.x Buttons and contains one entry (mouse) for MouseAreas.
signal somethingWasClicked(var sender, var arguments)
As this signal has not the same signature as clicked for every clickable object, we can't connect it directly to this signal. We need a intermediary function that calls the signal for us, has the needed arguments in it's scope and has no arguments. We need to build this dynamically for each object we spy on.
function createSpyFunction(object) {
return function() { somethingWasClicked(object, arguments) }
}
And lastly for the crawl-function. We need to store the function we create somewhere. For this I utilize the fact that all QtObjects are JS-Objects in some sense, so I can use Object.defineProperty to dynamically add JS-properties to them. Here I can store our function, without the need of modyfing the sourcecode of the components them selves.
After creating this function, I connect it to the onClicked-handler.
function crawlChildren(obj) {
if (obj.onClicked) {
Object.defineProperty(obj, '__clickedFunction', { value: createSpyFunction(obj) })
obj.onClicked.connect(obj.__clickedFunction)
}
if (obj.children) {
var i = 0
for (; i < obj.children.length; i++) {
crawlChildren(obj.children[i])
}
}
}
This function I finally call in
Component.onCompleted: crawlObj(this.contentItem)
of the root ApplicationWindow of my programm.
As I only call it in the Component.onCompleted-handler, objects that will be added after this, won't be spied uppon. To change this, we will also need to spy on onChildrenChanged, and crawl the newly added objects as well. The process there is almost similar, so this is left as an exercise to the user, or might be subject to a new question.
You can try to overlay your items with a MouseArea. In the event handlers you can check the position and call the event handlers of the underlying items.
Item {
MyButton { id: mybutton
/* set the anchors */
}
MyMouseComboBox { id: myMouseComboBox
/* set the anchors */
}
MouseArea {
anchros.fill: parent
onClicked: {
// mouse.accepted = false
// Check whether clicked point is within mybutton
// on true, call mybutton.doSomething()
// or call mybotton.onPressed(mouse)
}
}

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.

How to check if qml component object (that is created dynamically) has been created?

I have a component that should be created dynamically.
Component {
id: myComponent
Rectangle {
id: letItBeRect
}
}
I want to create it dynamically (when some button clicked) in a function like this:
function loadComponent() {
myComponent.createObject(root); //root is some root component, doesn't matter
}
I have state for root component that depends on some letItBeRect property:
state: letItBeRect.visible ? "visible" : "hidden"
So the question is how do I check if letItBeRect has been created, so I could assign a proper value to "state" property of root component?
I get "ReferenceError: letItBeRect is not defined" so far, which is expected from this code snippet.
P.S. This is not the real code I have, because I don't want to put commercial code here. Thank you

Resources