Custom attached properties in QML - qt

I'm creating a custom QML component (a specialization of ListView that allows multiple selection). I'd like to provide attached properties to objects provided to my component. I see how to create attached properties using C++. However, I cannot find information on adding custom properties in pure QML. Is this possible using QML?

Is this possible using QML?
No.

There is an alternative, easy and clean way to this in QML - just use an adapter object that implements the desired properties. Then instead of attaching just nest into the adapter - use it as as a parent / container. You can also nest objects into the adapter, getting another C++ exclusive - grouped properties. A possible way to minimize the overhead of this is to use JS objects and properties, with a downside - no change notifications, which you can somewhat mitigate by emitting manually.
An example:
// Adapter.qml - interface with attached properties
Item {
id: adapter
property int customInt : Math.random() * 1000
property var group : {"a" : Math.random(), "b" : Math.random() }
default property Component delegate
width: childrenRect.width
height: childrenRect.height
Component.onCompleted: delegate.createObject(adapter)
}
// usage
ListView {
width: 100
height: 300
model: 5
delegate: Adapter {
Row {
spacing: 10
Text { text: index }
Text { text: customInt }
Text { text: group.a }
Text { text: group.a }
}
}
}
It is fairly painless and convenient compared to some other QML workarounds. You don't even have to do parent.parent.customInt - the properties are directly accessible as if they are attached, this works because of dynamic scoping. The default property allows to avoid setting the inner delegate as a property you just nest the delegate you want directly in the adapter.
In many cases those acrobatics are overkill, you can just wrap in place:
ListView {
width: 100
height: 300
model: 5
delegate: Item {
width: childrenRect.width
height: childrenRect.height
property string custom1: "another"
property string custom2: "set of"
property string custom3: "properties"
Row {
spacing: 10
Text { text: index }
Text { text: custom1 }
Text { text: custom2 }
Text { text: custom3 }
}
}
}
The only key part really is the binding for the size of the adapter object so that the view can properly layout the objects. I routinely use a Wrap element which essentially does the same but is implemented in C++, which is much more efficient than a QML binding.

Related

QML: Is there an opportunity to create ListModel using <for loop>, and then using loop index on calculating ListModel item coordinates?

I need to create many similar elements, that differ only by coordinates.
I learned how to create this one by one:
Repeater {
model: ListModel {
ListElement { p:100; q:100; w:50; h:50; c:"red"; o:0 }
ListElement { p:200; q:100; w:50; h:50; c:"red"; o:0 }
ListElement { p:300; q:100; w:50; h:50; c:"red"; o:0 }
ListElement { p:400; q:100; w:50; h:50; c:"red"; o:0 }
ListElement { p:500; q:100; w:50; h:50; c:"red"; o:0 }
}
delegate: Rectangle {
x: p
y: q
width: w
height: h
color: c
rotation: o
}
}
I have now come to the conclusion that it would be convenient to do this with a simple for loop, using the loop index when calculating the coordinates:
Repeater {
model: ListModel {
for ( var i = 1; i <= 5; i++ )
{
ListElement { p:100*i; q:100; w:50; h:50; c:"red"; o:0 }
}
}
delegate: Rectangle {
x: p
y: q
width: w
height: h
color: c
rotation: o
}
}
Unfortunately, there is a problem with loop scope, and of course I don't even expect it will work in that shape...
I am trying to show only my idea in the second code.
Is there an opportunity to create ListElements like this or am I confusing two different ways of item creating?
As mentioned above, before you start writing a program, you need to learn the basics. You can't mix QML and Javascript in this way. It also seems to me that ListElement is completely out of place here.
Here's how it can be rewritten:
Repeater {
model: 5
delegate: Rectangle {
x: 100 * index
y: 100
width: 50
height: 50
color: "red"
rotation: 0
}
}
If you need more control over the data, you can change to this:
Repeater {
model: [1, 2, 3, 4, 5]
delegate: Rectangle {
x: 100 * modelData
...
}
}
For your example, I highly recommend #folibis answer as that demonstrates the usage of index and modelData which both are most appropriate for your scenario.
Generally speaking, you can mix QML/Javascript, you just need more practice in knowing the correct syntax/context where you can use QML and where you can use Javascript. Also, there are legimite scenarios where you want to populate a ListModel programmatically.
In the following demo, a ListModel is populated programmatically in JavaScript imperatively. In fact, it is even executed after all the QML components are initialized, so, the Repeater actually initially sees an empty ListModel. Whilst executing the populate function, each append signals changes to the ListModel which your Repeater will acknowledge through incremental updates.
import QtQuick
import QtQuick.Controls
Page {
Repeater {
model: listModel
delegate: Rectangle {
x: p
y: q
width: w
height: h
color: c
rotation: o
}
}
ListModel {
id: listModel
function populate() {
for ( let i = 1; i <= 5; i++ )
{
listModel.append( { p:100*i, q:100, w:50, h:50, c:"red", o:0 } );
}
}
}
Component.onCompleted: listModel.populate()
}
You can Try it Online!
Some time ago, when I was playing around with QML with the kids, we mocked up a "Click Invaders" game. The game has the following features:
ListModel will contain the position of the invaders
Repeater is used to render the invaders
Timer object is used to guarantee to make 10 invaders slowly appear on the screen
When the user clicks, the invader will "die" and be removed - this will trigger more invaders to spawn
Check it out. It has some concepts such as using SVG to render the graphic. It dynamically creates and destroys objects in different positions and orientations:
https://github.com/stephenquan/qmlonline6/wiki/Example-Click-Invaders
A very important section of the documentation to start with when coming to Qt Quick without JavaScript knowledge:
https://doc.qt.io/qt-6/qtqml-javascript-expressions.html
It's a must-see here if you didn't expect that you'll need to learn something besides C++ along with Qt.
The JavaScript environment provided by QML is stricter than that in a web browser. For example, in QML you cannot add to, or modify, members of the JavaScript global object. In regular JavaScript, it is possible to do this accidentally by using a variable without declaring it. In QML this will throw an exception, so all local variables must be explicitly declared. See JavaScript Environment Restrictions for a complete description of the restrictions on JavaScript code executed from QML.
Various parts of QML documents can contain JavaScript code:
The body of property bindings. These JavaScript expressions describe relationships between QML object properties. When dependencies of a property change, the property is automatically updated too, according to the specified relationship.
The body of Signal handlers. These JavaScript statements are automatically evaluated whenever a QML object emits the associated signal.
The definition of custom methods. JavaScript functions that are defined within the body of a QML object become methods of that object.
Standalone JavaScript resource (.js) files. These files are actually separate from QML documents, but they can be imported into QML documents. Functions and variables that are defined within the imported files can be used in property bindings, signal handlers, and custom methods.
Should carefully distinguish between JavaScript and QML elements in Qt Quick. If QML is a language for creating a GUI, its elements are the types described in doc.qt.io. On the other hand, loops, variable definitions, arrays, etc. are JavaScript elements. It's not so obvious at first. By the way, this is a very good lesson if someone did not program in JS, only in C ++. The closeness of the syntax of the two languages ​​may be surprising.
The documentation section above explains these dependencies

In Qt Quick, how to ensure a ListView's delegate's width to equal the view's width?

Here's my QML view:
// Imports ommitted
Item {
id: paymentMethods
required property PaymentMethodsModel model
ColumnLayout {
anchors.fill: parent;
Text {
text: "Payment methods";
}
ListView {
Layout.fillHeight: true
Layout.fillWidth: true
model: paymentMethods.model
delegate: PaymentMethods.Item { }
}
ToolBar { }
}
}
The problem is, it looks like this:
I think it's because the delegate doesn't specify width, because if I do this:
delegate: PaymentMethods.Item {
width: parent.width
onPmSaved: {
ListView.view.model.rename(index, newName)
}
}
It looks much better:
The problem is, when I do edits that reorder the items, I get this error:
qrc:/PaymentMethods.qml:32: TypeError: Cannot read property 'width' of null
Is there a good way to set a QML ListView's delegate's width to full parent's width?
From the ListView documentation:
Delegates are instantiated as needed and may be destroyed at any time. As such, state should never be stored in a delegate. Delegates are usually parented to ListView's contentItem, but typically depending on whether it's visible in the view or not, the parent can change, and sometimes be null. Because of that, binding to the parent's properties from within the delegate is not recommended. If you want the delegate to fill out the width of the ListView, consider using one of the following approaches instead:
ListView {
id: listView
// ...
delegate: Item {
// Incorrect.
width: parent.width
// Correct.
width: listView.width
width: ListView.view.width
// ...
}
}
In the transition of the reordering, the item does not have a parent, so the error indicates it, a possible solution is to set the width of the item depending on whether it has a parent or not.
width: parent ? parent.width : 40 // default value

Add elements to a ListView inside another ListView

I need to insert elements in a ListView inside another ListView (via JS code inside my QML file) but when I try to access the inner ListView I get the error :
TypeError: Cannot call method 'insert' of undefined
Here is an example code to show my problem :
Item{
id:list
width: parent.width-210
height: parent.height
x:105
Component{
id:listDelegate
Item {
id:elem
height: 100
width: parent.width
Item{
id:titre_liste
height: 50
width: parent.width
Text{
anchors.left: parent.left
color:"white"
text:titre_txt
font.pixelSize: 25
font.bold: false
}
}
Item{
id:listInList
width: parent.width-100
height: parent.height
Component{
id:listInListDelegate
Item{
id:element_liste
height: parent.height
width: parent.width/5
Text{
anchors.left: parent.left
color:"white"
text:element_txt
font.pixelSize: 25
font.bold: true
}
}
}
ListView {
id: viewin
anchors.fill: parent
model: ListModel{
id:listModel_in
}
delegate: listInListDelegate
}
}
}
}
ListView {
id: viewglobal
anchors.fill: parent
model: ListModel{
id:listModel
}
delegate: listDelegate
}
}
And here is my JS code, at the end of the QML file :
function addItem(){
var i;
var numListe = -1;
var liste = "titre"
var item = "item"
for(i = 0;i<listModel.count;i++)
{
if(listModel.get(i).titre_txt === liste)
{
numListe = i;
}
}
if(numListe === -1)//if the list doesn't exist
{
listModel.append({titre_txt:liste});
numListe = listModel.count-1;
}
listModel.get(numListe).listModel_in.insert(0,{element_txt:item});
}
The error come from the last line of the JS code, when I try to insert a new element in the inner list. I verified that the value of "numListe" is 0 so it is not just a problem of wrong index.
How can I add elements to the inner list ?
There is a lot of stuff wrong with that code.
For starters - it is a mess, which is a very bad idea for someone who is obviously new at this stuff. Keep it clean - that's always a good idea regardless of your level of expertise.
listModel_in is an id and as such cannot be accessed outside of the delegate component.
That object however happens to be bound to the view's model property, so as long as the model doesn't change, you can access listModel_in via the model property. However, the view itself doesn't look like it is the delegate root object, so you have to interface it, for example by using an alias.
However, the inner model doesn't exist in the outer model, it only exists in the outer model's delegate item.
So you cannot possibly get it from listModel. You can get it from the viewglobal view, however ListView doesn't provide access by index. So you will have to set currentIndex for every index and use currentItem.
So it will look like this:
viewglobal.currentItem.modelAlias.insert(0,{element_txt:item});
But it should go without saying, you are putting data in the GUI layer, which is conceptually wrong. But it gets worse than conceptually wrong - you might not be aware of this, but ListView only creates items that it needs to show, meaning that it creates and destroys delegates as necessary. Meaning if your item falls out of view, it will be destroyed, and when it comes back into view, a new one will be created, and all the data you had in the model of the old delegate item will be lost. The view should never store data, just show it.
The inner model should be inside the outer model. However, last time I checked, QMLs ListModel didn't support model nesting, neither using declarative nor imperative syntax. If you want to nest models, I have provided a generic object model QML type you can use.

pass data from one window to another (inside same QML file)

i got two Windows inside the same .qml file.
Window1 has a textinput1 and a button, and when I press the button, i'd like to send the string value from that textinput to the Window2 textInput2.
I'm new to Qt and QML, been reading a lot on signals, Loaders, properties and can't materialize this kind of transfer. Can anyone do a simple 10-line example of such transfer please?
Window {
id:window
TextInput {
id:text1
text: "This value is needed in the second Window!"
}
Button {
onClicked: window2.open()
}
Window {
id.window2
function open(){
visible = true
}
Text {
text: text1.text
}
}
}
If I do this it gives me ReferenceError: text1 is not defined, how can I reference the text1 from the first Window?
I would prefer to use signals in such case:
Window {
id: window1
title: "window 1"
visible: true
width: 600
height: 600
signal someSignal()
Button {
anchors.centerIn: parent
text: "Open window"
onClicked: window1.someSignal()
}
Window {
id: window2
title: "window 2"
width: 400
height: 400
// you can use this instead of Connections
//Component.onCompleted: {
// window1.someSignal.connect(show);
//}
}
Connections {
target: window1
onSomeSignal: {
window2.show();
}
}
}
I think this is more ... how do you say? ... more imperative -)
i got two Windows inside the same .qml file.
If you did then your code will work. Since it doesn't work, I will assume each window is defined in its own qml file, and you only instantiate the two windows in the same qml file.
If I do this it gives me ReferenceError: text1 is not defined, how can
I reference the text1 from the first Window?
You will have to be able to reference the window first, and it should provide an interface to reference the text.
Keep in mind that ideally ids should only be used to reference stuff in the same source. On rare occasions you could go further, and reference ids down the object tree, but only parents, and none of their out-of-line children, it will however work for in-line children that are given ids in that same source. Meaning that if window2 is created inside window then you will be able to reference window from inside window2. But if both windows are siblings in another object, the id won't resolve.
Obj1
Obj2
Obj4
Obj3
In this example object tree, Obj1 will resolve from any of the objects. However, Obj3 will not be able to resolve Obj2 if the id is given inside Obj2, but will resolve if the id for Obj2 is given inside Obj1. But there is no way to resolve Obj4 from Obj3. because the id doesn't act like a property, you can't do someId.someOtherId, that's only possible for properties. You cannot do somePropertyObject.someId neither. You can only begin with either an id or a property, and continue only with sub-properties.
When the id is not applicable, can expose objects or properties either as properties or property aliases. The first is useful when you want to expose a whole object, the second when you want to expose a particular property of an object:
Item {
property Item innerItem: inner // gives you access to the "entire" inner object
property alias innerWidth: inner.width // gives you access to a property of inner
Item {
id: inner
}
}
You can also have aliases to aliases.

asynchronous (kind of) animation in qml

let's say i have the following QML Components:
Foo.qml
import Qt 4.7
Rectangle {
Repeater {
model: myModel
delegate: Bar {
barProp: elemProp
}
}
}
Bar.qml
import Qt 4.7
Rectangle {
property string barProp: ""
Text {
text: barProp
NumberAnimation on x {
from: 0; to: 100
duration: 1000
loops: Animation.Infinite
}
}
}
I maintain myModel from C++, it has the following Q_PROPERTY declaration:
Q_PROPERTY (QDeclarativeListProperty <Bar> myModel READ myModel
NOTIFY myModelChanged)
Now, my problem is that every time I add a new element to the underlying QList, the animation specified in Bar resets, so in practice, the elements always completely overlap. What I want is that the element animations are not synchronous, and each can continue seamlessly regardless of the rest. Is this possible to do?
Cheers
You should use a QAbstractItemModel (QStandardItemModel may be easiest) rather than a QList. QAbstractItemModel notifies the view when new items are inserted/removed/moved and the view reacts appropriately by modifying its content. In contrast, the view knows nothing about the changes made to a QList; only that something has changed. This means that the list has no choice but to destroy and recreate all delegates.

Resources