Order and manner of property binding evaluations in QML - c++

From what I understand, when properties are used in binding expressions, their NOTIFY signals are connected to signal the reevaluation of the expression every time a property value is changed.
So consider this trivial example:
Rectangle {
width: 200
height: width - 100
Text {
text: "value is " + (parent.width + parent.height)
}
}
In it the height property is bound to the width property, and the text property is bound to both.
In this example, it is not clear what is the order of operation. I mean if width changes, it will emit to reevaluate both of the expressions which reference it. But width will also change height which itself will trigger evaluation of text as well.
Is the text property therefore evaluated twice? Once when width changes, and once again when width changes height? Or maybe QML has some mechanism of optimizing that behavior away, like for example the notification signals do not trigger actual reevaluation but only mark the expressions "dirty" and they are reevaluated on the next event loop iteration, when all signaling has been propagated? And even so, how would the engine know to update height before updating text to avoid the double reevaluation of the latter? Or maybe the connection is direct rather than queued, and some more complex mechanism is used to figure out the order of reevaluation? Or maybe there is nothing of the sort and text might actually reevaluate twice if it just so happens to be the arbitrary order?
I really need to understand how this works, because I have a project where I modify multiple properties of multiple QML objects on the C++ side in a imperative manner which are bound on the QML side and I get very inconsistent and erratic behavior, so I definitely need to take into consideration the way those work.

Yes. it looks that you are right and text will be update twice. The simple example:
Rectangle {
id: testRect
width: 200
height: width - 100
Text {
text: "value is " + (parent.width + parent.height)
onTextChanged: {
console.log("text was changed (" + parent.width + "," + parent.height + ")");
}
}
MouseArea {
anchors.fill: parent
onClicked: {
testRect.width = 300
}
}
}
output:
qml: text was changed (200,100)
qml: text was changed (300,100)
qml: text was changed (300,200)
I guess it's simple behavior and updates relevant property when changes some properties this item based on.
Qt docs advices to avoid such situation - However, if a binding is overly complex - such as involving multiple lines, or imperative loops - it could indicate that the binding is being used for more than describing property relationships. Complex bindings can reduce code performance, readability, and maintainability. It may be a good idea to redesign components that have complex bindings, or at least factor the binding out into a separate function.

Related

How to prevent QListView from calling sizeHint of each item?

I have a QListView with a lot of items that are of various heights. I implement a custom delegate for painting items and set layout mode to Batched.
However, when the model is assigned, the list view requests sizeHint for every item in the model upfront, ignoring the Batched setting and thus ruining the performance because to calculate the size, the delegate has to layout a lot of text (which is not fast).
Probably it does this to calculate the scrollbar position, but I reckoned that when the number of items is large, the scrollbar position can be based on item indices only, not taking into consideration item heights. However it seems that this is not how QListView works.
I also tried to use canFetchMore/fetchMore in the model, but this leads to bad user experience - the scrollbar position is no longer accurate, and the list jumps around when more items are loaded, it was not smooth at all.
So, the question is:
Is there a way to prevent QListView from calling sizeHint for invisible items?
If the only way is to use canFetchMore/fetchMore, how to get smooth scrolling and stable and accurate scrollbar?
Thanks a lot!
UPD: Here is the minimal example that reproduces this behavior:
https://github.com/ajenter/qt_hugelistview
Note the huge startup delay and the debug messages showing that sizeHint of all 5000 items is requested upfront.
Well it seems that I've found a solution, so I'll share it here for sake of anyone who has the same problem and googles this thread.
First of all, I've found that this is actually a bug in Qt registered back in 2011 and still open:
https://bugreports.qt.io/browse/QTBUG-16592
I've added my vote to it (and you should, too!).
Then decided to try out using QTableView instead of QListView - and, surpise, I managed to make it work, or so it seems.
Unlike QListView, QTableView only resizes rows upon explicit request, by calling resizeRowToContents(rowNum). So the trick is to call it in a just-in-time fashion for rows that become visible in the viewport.
Here's what I did:
Inherit from QTableView (let's call it MyTableView)
Replace QListView with MyTableView and initialize it like this in the constructor. This assigns custom item delegate, hides table headers and applies "by row" selection mode:
MyTableView::MyTableView(QWidget* parent) : QTableView(parent)
{
setSelectionBehavior(QAbstractItemView::SelectRows);
horizontalHeader()->setStretchLastSection(true);
horizontalHeader()->hide();
verticalHeader()->hide();
setItemDelegateForColumn(0, new CustomDelegate(&table)); // for custom-drawn items
}
In MyTableView, add a QItemSelection private field and a public function that calculates real heights of rows, but only those that are currently visible:
QItemSelection _itemsWithKnownHeight; // private member of MyTableView
void MyTableView::updateVisibleRowHeights()
{
const QRect viewportRect = table.viewport()->rect();
QModelIndex topRowIndex = table.indexAt(QPoint(viewportRect.x() + 5, viewportRect.y() + 5));
QModelIndex bottomRowIndex = table.indexAt(QPoint(viewportRect.x() + 5, viewportRect.y() + viewportRect.height() - 5));
qDebug() << "top row: " << topRowIndex.row() << ", bottom row: " << bottomRowIndex.row();
for (auto i = topRowIndex.row() ; i < bottomRowIndex.row() + 1; ++i)
{
auto index = model()->index(i, 0);
if (!_itemsWithKnownHeights.contains(index))
{
resizeRowToContents(i);
_itemsWithKnownHeights.select(index, index);
qDebug() << "Marked row #" << i << " as resized";
}
}
}
Note: if item heights depend on control's width, you need to override resizeEvent(), clear _itemsWithKnownHeights, and call updateVisibleRowsHeight() again.
Call updateVisibleRowHeights() after assigning a model to MyTableView instance, so that initial view is correct:
table.setModel(&myModel);
table.updateVisibleRowHeights();
In fact it should be done in some MyTableView's method that reacts to model changes, but I'll leave it as an exercise.
Now all that's left is to have something call updateRowHeights whenever table's vertical scroll position changes. So we need to add the following to MyTableView's constructor:
connect(verticalScrollBar(), &QScrollBar::valueChanged, [this](int) {
updateRowHeights();
});
Done - it works really fast even with model of 100,000 items! And startup is instantenious!
A basic proof-of-concept example of this technique (using pure QTableView instead of subclass) can be found here:
https://github.com/ajenter/qt_hugelistview/blob/tableview-experiment/src/main.cpp
Warning: this technique is not battle proven yet and may contain some yet unknown issues. Use at own risk!

How to lower the memory usage of my (unorthodox) Qt5/QML-based software?

CONTEXT:
I am developing a research prototype for a novel interaction concept and computational desktop environment, I currently call Sketchable Interaction (SI).
Currently, SI works only on Debian-based linuxes.
In a nutshell, SI allows users to draw interactive regions on their desktop which carry effects.
Once two or more regions overlap, regions which's effects are compatible to each other apply their effects to each other as well.
In this way, graphical representations and data of files, etc. can be set, modified or deleted.
Here are some screenshots to give a visual example:
Showing Desktop Environment:
Drawing region (blue one) for Opening Folders/Files:
Finished drawing of blue region:
Opended Desktop-Folder by overlapping it with the blue region and drew a Preview File region:
Moved image file (png) with the cat out of the folder:
Overlapped image file with the cat with the green region to show a preview of the image:
TECHNICAL STATUS QUO OF SI
SI is written in C++ with the current Qt5 and QML versions.
SI-Plugins which represent the effects you saw in the screenshots, are written in python3.7+, with the use of Boost.Python and do not use PyQt5.
SI opens a MainWindow and every region drawing (everything you see in the screenshots is a region, including the mouse cursor) is a QWidget which is a borderless child of that MainWindow.
In order to do any styling e.g. display textures, like the folder icon, SI uses QML files, represented as QQuickWidgets which is a borderless child of that QWidget (I am aware of the stacking order problem, but we can ignore that for this question!)
I am able to change QML styling from within SI-Python-Plugins at runtime.
This internally uses QMetaObject to pass a QMap<qstr, QVariant> to a function in the container component.
QMetaObject::invokeMethod(reinterpret_cast<QObject *>(d_view->rootObject()), "updateData", QGenericReturnArgument(), Q_ARG(QVariant, region->data()));
I tested this with signals/slots as well, yet was unable to get it working as I intended, the above method does work as intended.
Apparently, this is due to initializing exactly one QQmlEngine, instead of one per QQuickWidget.
This single QQmlEngine has CppOwnership.
engine = new QQmlEngine(this);
engine->setObjectOwnership(engine, QQmlEngine::CppOwnership);
THE PROBLEM
For testing purposes and performance benchmarking I intend to spawn thousands of regions:
The following screenshots shows 1009 regions (1000 in the center).
This one is with all QML related code deactivated
This yields, according to the tool htop, roughly 200 MB memory consumption.
This one is with all QML related code activated
This yields roughly 4900 MB memory consumption.
The texture used in the yellow regions in the example with QML is a 64x64 px 32-bit RGBA image.
This memory difference really strikes me as odd.
The memory required for all images equals 1000 (number of regions) * 64 * 64 (number of pixels) * 4 (number of bytes if 4 channels with 8 bit) = 16,384,000 bytes which are ~16.5 MB. Of course there should be some further overhead per image, yet not 4.8 GB of overhead.
I found out via other questions here or other sources that QML apparently needs a lot memory (some call it a memory hog).
E.g.:
QML memory usage on big grid
Yet, this high difference could stem from my unorthodox usage of Qt5 and QML.
QUESTION/S
Is there a way to lower this memory consumption, given the current state of the SI software?
Are their alternative approaches I did not come up with?
Is their a flag in Qt5/QML docs I missed which trivializes the problem?
Sorry for the lengthy post and thanks in advance for your help.
Edit: Typos, Addition of potential critical or suspected code as requested.
Suspected Code:
This is the constructor of a QWidget which contains a QQmlQuickWidget and represents a region
RegionRepresentation::RegionRepresentation(QWidget *parent, QQmlEngine* engine, const std::shared_ptr<Region>& region):
d_color(QColor(region->color().r, region->color().g, region->color().b, region->color().a)),
d_qml_path(region->qml_path()),
d_view(new QQuickWidget(engine, this)),
d_type(region->type()),
d_uuid(region->uuid()),
d_name(region->name())
{
if(!d_qml_path.empty())
d_view->setSource(QUrl::fromLocalFile(QString(d_qml_path.c_str())));
d_view->setGeometry(0, 0, region->aabb()[3].x - region->aabb()[0].x, region->aabb()[1].y - region->aabb()[0].y);
d_view->setParent(this);
d_view->setAttribute(Qt::WA_AlwaysStackOnTop);
d_view->setAttribute(Qt::WA_NoSystemBackground);
d_view->setClearColor(Qt::transparent);
setParent(parent);
setGeometry(region->aabb()[0].x, region->aabb()[0].y, region->aabb()[3].x - region->aabb()[0].x, region->aabb()[1].y - region->aabb()[0].y);
if(region->effect()->has_data_changed())
QMetaObject::invokeMethod(reinterpret_cast<QObject *>(d_view->rootObject()), "updateData", QGenericReturnArgument(), Q_ARG(QVariant, region->data()));
d_fill.moveTo(region->contour()[0].x - region->aabb()[0].x, region->contour()[0].y - region->aabb()[0].y);
std::for_each(region->contour().begin() + 1, region->contour().end(), [&](auto& point)
{
d_fill.lineTo(point.x - region->aabb()[0].x, point.y - region->aabb()[0].y);
});
show();
}
I can acess and set data in the QQmlQuickWidget from a plugin (python) in that way:
self.set_QML_data(<key for QMap as str>, <value for key as QVariant>, <datatype constant>)
Every region has such a QMap and when it is updated in any way, this is called by RegionRepresentation:
if(region->effect()->has_data_changed())
QMetaObject::invokeMethod(reinterpret_cast<QObject *>(d_view->rootObject()), "updateData", QGenericReturnArgument(), Q_ARG(QVariant, region->data()));
Populating the QMap is done in this way:
QVariant qv;
switch (type)
{
case SI_DATA_TYPE_INT:
d_data[QString(key.c_str())] = QVariant( bp::extract<int>(value))
d_data_changed = true;
break;
case SI_DATA_TYPE_FLOAT:
d_data[QString(key.c_str())] = QVariant(bp::extract<float>(value));
d_data_changed = true;
break;
case SI_DATA_TYPE_STRING:
d_data[QString(key.c_str())] = QVariant(QString(bp::extract<char*>(value)));
d_data_changed = true;
break;
case SI_DATA_TYPE_BOOL:
d_data[QString(key.c_str())] = QVariant(bp::extract<bool>(value));
d_data_changed = true;
break;
case SI_DATA_TYPE_IMAGE_AS_BYTES:
int img_width = bp::extract<int>(kwargs["width"]);
int img_height = bp::extract<int>(kwargs["height"]);
QImage img(img_width, img_height, QImage::Format::Format_RGBA8888);
if(!value.is_none())
{
const bp::list& bytes = bp::list(value);
int len = bp::len(bytes);
uint8_t buf[len];
for(int i = 0; i < len; ++i)
buf[i] = (uint8_t) bp::extract<int>(value[i]);
img.fromData(buf, len, "PNG");
d_data[QString(key.c_str())] = QVariant(img);
}
else
{
d_data[QString(key.c_str())] = QVariant(QImage());
}
d_data_changed = true;
break;
}
In QML this QMap is used that way:
// data is QMap<QString, QVariant>
function updateData(data)
{
// assume that data has key "width" assigned from python as shown in above code snippet
qmlcomponent.width = data.width;
}
Here is the typical layout of QML files which are used for styling regions/effects:
Item
{
function updateData(data)
{
texture.width = data.icon_width;
texture.height = data.icon_height;
texture.source = data.img_path;
}
id: container
visible: true
Item {
id: iconcontainer
visible: true
Image {
id: texture
anchors.left: parent.left
anchors.top: parent.top
visible: true
}
}
}
One of the central ideas is, that users of the system can create custom styling for regions and effect and address that styling dynamically at runtime via the associated plugins.
While this is not an answer to your question, i think it might be a valuable info for you, and since i do not have enough reputation points to comment, i'm posting it as an answer.
The memory issue you are seeing looks like a bug and not Qt/QML related. Below is a simple example of how to display a bunch of images in QML, and what to expect regarding memory consumption.
Code below displaying 1040 images with QML consumes under 30 MB of memory (with 64x64 px 32-bit RGBA source image, but it doesn't change much when using larger images). The displayed images are scaled down to 20x20 px, but even if you had enough of screen real estate to show them as 64x64 px and in worst case scenario if the memory consumption would increase linearly, it should be around 10x increase and nowhere near 4.8 GB. I hope this helps, and this is the code i've used:
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Layouts 1.3
Window {
visible: true
width: 1200
height: 1000
color: "#00000000"
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
Repeater {
model: 26
RowLayout {
Repeater {
model: 40
Image {
Layout.preferredWidth: 20
Layout.preferredHeight: 20
source: "qrc:/tile.png"
}
}
}
}
}
}
And the memory consumption:
First of all, I would suggest to not ask StackOverflow, but ask a profiler what is using our memory. Try heaptrack for instance.
However, I can tell you that the way you are using QQuickWidget is not as it is designed to use. It seems likely that this is where your memory is being used. I would suggest you change your design to use a single QQuickWidget or even use a QGraphicsArea instead of instantiating a new QQuickWidget per item.
Then on the use of QMetaObject::invokeMethod: please don't. It's an anti-pattern to try to poke into your QML. Instead, expose whatever you want to get into QML as a property or a QAbstractItemModel, and bind to that from your QML.

QML: modifying a property of a Child object that is defined in a different QML file (not main.qml)

Basically, I have something like:
Main.qml:
ApplicationWindow{
width: 500
height: 500
Page{
id: page0
DataPage{
id: datapage0
}
}
}
DataPage.qml:
Page{
id: displayPage
DataDisplay{
id: dataShow
}
}
DataDisplay.qml:
Label{
text: "data: "
}
TextArea{
id: dataArea
text: ""
}
I've removed the stuff I think isn't relevant (such as anchors, height, width, etc.). Now, in main.qml, I have a signal coming from the c++ backend:
Connections{
target: modb
onPostData: {
page0.datapage0.dataShow.dataArea.text = string;
}
And I get the following error:
TypeError: Cannot read property 'dataArea' of undefined
So, I wanted to ask: how do I connect that signal to the child object that is defined in DataDisplay.qml? I'm able to get info into main.qml using signals, but seem to be unable to dereference child objects
Edit:
main.cpp:
QQmlContext* ctx0 = engine.rootContext();
ctx0->setContextProperty("ark", &ark);
QQmlContext* ctx1 = engine.rootContext();
ctx1->setContextProperty("comm", ark.comm);
QQmlContext* ctx2 = engine.rootContext();
ctx2->setContextProperty("modb", ark.modb);
is how I set the Context (of 3 classes, as you can see). I'm able to get signals from any of the three into main.qml, as well as call slots in any of the three in main.qml; I haven't yet tried to call slots from the c++ classes in the other qml files, but I assume it would work because I can access the parent's properties from the child
1 - you have 3 pointers pointing to the same object. One is enough. Really!
2 - as long as ark is properly implemented, you can access ark.comm and ark.modb from QML, no need to expose them individually.
3 - you don't seem to understand the scope of ids. Take a look at this exhaustive answer. dataShow is simply not visible from wherever you made the connection.
4 - context properties are not very efficient, that's more of a "quick and dirty" approach to expose C++ to qml. For optimal performance consider a more efficient approach.
All in all, you exhibit the typical symptoms of "getting ahead of yourself". Take the time to learn before you practice.
As you indeed assume you can use the modb variable in the other qml's as well, since it is added to the rootContext. I would advise this option.
Another option you could try is just using dataArea.text = string since the id's go all over the place (it's javascript after all), you should use strong id's in this case.
Another option is to define property alias's to pass the string through over the objects (See Qt docs). Or use property string, but that's even more work.

Accessing Qt / QML objects with C++

I'm working on a C++ Qt project that will eventually communicate with the serial port. One part of this is accessing QML objects in the C++ portion. I have code that can set properties of the QML, but accessing those features that are methods now has me stumped. View the following code:
object = view.rootObject();
rect = object->findChild<QObject *>("box");
rect->setProperty("color", "red"); // Verifies the object tree is accessible
viewer = object->findChild<QObject *>("viewer"); // Access the viewer text box
viewer->append("dummy text"); // OOPS! This doesn't compile!!!
Now, the type as a method setProperty(..), but how do you access methods of an object. "viewer" is a TextArea and I want to first do a selectAll(), then a cut() to clear the box.
The question here is how is this coded? Thanks all.
Of course it would not compile, QObject doesn't have an append() method.
If it is a C++ function, you will have to qobject_cast to the appropriate type that has it. This however is not always readily available for many of the stock QML types that are implemented in C++, and as C++ types they are not part of the public API and not generally intended for direct use by an end user.
If it is a JS function, you will have to use QMetaObject::invokeMethod. That will also work for C++ functions for which meta data has been generated. Which is also how setProperty() works, whereas setColor() would not work with a QObject* much like append() doesn't.
Last but not least, there is absolutely no good reason for you to be doing those kinds of things from C++. Using QML objects from C++ is poor design and an anti-pattern. You will only develop bad habits trying to do that. Such interactions must be limited to a clearly defined interface using signals, slots and properties. Generally speaking, it is OK for QML to reach into C++, because that only happens through an exposed interface, but the opposite way, even if possible, should not utilized.
Think of it like this - a car uses the engine, the engine doesn't use the car. And the engine control is interfaced through the starter key and the gas pedal, it is not used directly. The C++ stuff should be reserved to the application engine - the high performance or efficiency core logic, or the back-end, whereas the QML part is for the GUI/front-end.
The author's QML part may expose alias property to operate with desired text field content:
import QtQuick 2.0
import QtQuick.Controls 1.2
Item {
property alias viewerText: viewer.text // added
width: 350
height: 450
TextArea {
id: viewer
x: 8
y: 8
width: 223
height: 415
text: "text"
font.pixelSize: 12
objectName: "viewer"
}
Button {
id: open
x: 251
y: 8
text: "Open"
}
}
And then the author's C++ part can easily do:
auto* object = view.rootObject();
viewer = object->findChild<QObject *>("viewer");
viewer->setProperty("viewerText", "dummy text"); // using the new property added
Using the posted answer here using the invoke method, here's the solution that works:
// C++ Code to call function reset()
QMetaObject::invokeMethod(object, "reset");
// QML code to select all text the delete it
function reset() {
viewer.selectAll()
viewer.cut()
}

QML two-way C++ binding doesn't receive changes anymore

I'm facing a big problem that it's taking a lot of time to be fixed because I don't know the cause and how to fix it. The problem is really simple: I have an example QML component defined as:
Rectangle {
id: rect
property bool test: myclass.testproperty
Rectangle {
width: 50
height: 50
visible: parent.test
}
}
and I connected a MouseArea onClicked signal to do this:
test = !test
so I switch the value of the boolean variable. To push the value from C++ to QML and from QML to C++ Q_PROPERTY with READ, WRITE and NOTIFY signals, I used this
Binding {
target: myclass
property: "testproperty"
value: rect.test
}
everything works fine until I click on the mouseArea and so I push the changes via the binding. After that, every time I try to set a new property value from C++ I don't see any change in QML, like if the binding is destroyed. But if I try to click on the MouseArea I still call the setTestProperty method of the C++ class. Basically, it goes out of sync the C++ -> QML way. Why? I can't find the problem, the signal is emitted because QSignalSpy gives me 1 as count of emitted times after using
emit this->testPropertyChanged(newvalue)
EDIT
here an example: basically here we're using a QString property with the same exact signals. The only thing that changes is that instead of using a Rectangle QML element and binding to a own property, I'm using a TextInput element
TextInput {
id: mytext
text: myclass.testproperty
}
Binding {
target: myclass
property: "testproperty"
value: mytext.text
}
There is no problem here. It is a standard QML bindings behaviour. When you change some QML Widget's property in JavaScript code, then all declarative bindings to it will be terminated. It is your choice to use declarative binding or update values manually in JS code of event handlers.
EDIT
Rectangle {
id: rect
// Establishing initial value for 'test' property via QML binding
property bool test: myclass.testproperty
// Manual update of 'test' property when binding will be broken
Connections {
target: myclass
onTestpropertyChanged: {
rect.test = xxx
}
}
}