Qt QML Hot Tips #5
Mike Trahearn
Qt Champion??????, Qt QML C++ Specialist, Director, Codecept Software Pty Ltd; A Unique Thinker, Detailed Craftsman with Precise Foresight and a Personal Approach
Part 5 - I'm sorry, your access is restricted!
From Qt 5.15 and much more strictly in Qt 6, a series of improvements have been added to the way QML works which ensures the safety of the QML component portability and expected behaviour.
Objects must always qualify the object whose properties it is referencing (unless referencing its own properties) - and soon even within the same context.
QML files with such unqualified property references will not compile with the (new) QML Compiler.
In previous versions of Qt it was also possible to reference properties of the root QML component without qualification but this may have or will be removed in the future. Stick with the above rule so future code won't break.
Always qualify your property bindings with a QML id to avoid ANY confusion - see QML Hot Tips #4 for more info.
How well do you know your parent?
The use of parent.<property> will still be supported but discouraged unless you know the parent will NEVER change. Classic example: MyRootItem { anchors.fill: parent }... what parent?
These can be detected in the qmllint tool in Qt >=5.14 and will be depreciated in Qt 5.15 and completely removed in Qt6
What is unqualified access?
Unqualified access is wherever you are referencing a property outside your current component scope without clearly specifying which parent component it belongs to. (It is questionable that a QML object should be reaching outside its component scope anyway). Examples follow.
Existing resolutions for unqualified accesses (because Qt is being nice and tolerant for now) come with a performance penalty due to the QQmlEngine having to "figure out" which object the property reference is referring to. This is both slow and error prone as the following demonstrates:
Component portability
Your component will be woefully error prone if you use unqualified ids with it due to incomplete encapsulation. If a component is refactored to elsewhere in the application, such "figured out" qualifications may either result in a completely different object or non at all causing bugs or failures. I've seen an entire fully released customer project full of this particular infringement resulting in my complete annihilation of the original code and costly architecture restructure and code re-write.
Perspiration perspective
Using unqualified ids causes a maintenance burden: a component makes promises about components outside its scope which have to be manually kept for the component not to break - and well, they break. It's inevitable. When you do you get to know? At run-time. Who lets you know? The customer. Bad.
What are you looking at?
The type safety cannot be guaranteed (we can never be sure what type an unqualified property is going to end up resolving to - if at all.
Having no real way to tell anything about the type causes issues with tooling - limiting analysis with qmllint tool and others and you'll immediately be frustrated with the issues with the code model and type completion - which may simply give you the silent treatment.
Examples and Solutions
Avoid using Parent Property - inside a component
Avoid using bindings using the parent keyword even inside a QML component. This makes it possible to restructure the component easily without breaking bindings. If parent is used and the structure changes such that the parent (and its type) changes, then the binding may either break (syntax error) or unfortunately succeed to bind to a co-incidentally existing property and worse, with some potential type coercion (logical error). The QML code model has to work harder to figure out what type "parent" is for e.g. code completion and we should stop making assumptions on its grace in that matter.
New versions of components caused by incrementing an import version or moving your type into a different context may introduce new properties with the same name causing an unexpectedly different/wrong/undesired property binding as it now finds a matching property in the current scope before trying the root scope which would now change or break the implementation.
In some cases it isn't possible to know that the ID of an object isn't in fact reachable at run time within the scope of certain objects like models, inline Component declaration or Loaders.
领英推荐
import QtQuick
Item {
? id: master
? property int value
? Text {
? ? // text: parent.value
// The above kind of binding is found everywhere
// - but... reparenting this Text to another item
// would break this binding!
? ? text: master.value ?
// using qualified binding will ensure this Text
// will still work regardless of being reparented
? }
? Item {?
? ?id: anotherItem
property int: value
// "might work" because QML is being "nice" but be warned!
// but cannot guarantee the new parent has the property
// (nor should we assume or desire it to have)
? }
}
Avoid using Parent Property - outside a component
The following shows an example of referencing a parent outside of the component in much the same way but this time the dangers are clear. It might work given the context but it makes demands / promises on the external structure it cannot enforce. That assumed context cannot be seen here, and as such gives every reason to declare that this component is NOT portable.
import QtQuick
Item {
? property int value: parent.value // what type is this?
}
Avoid referencing objects outside the component
Here is a typical example where the developer exhibits unconscious laziness through familiarity with the overall application architecture e.g. main.qml most probably is a Window and whose id is set early on in development and seldom changes if ever.
The example will soon break if the id is changed or if the button happens to be reparented under a different object type having unfortunately the same id. Hammering home the issue of the effect of the QQmlEngine trying "its best" to not fail your otherwise ambiguous code.
import QtQuick
Button {
? text: qsTr("Enter Full Screen")
? onClicked: mainWindow.showFullScreen()
// assumes an object with id 'mainWindow' and
// that it is indeed a Window type with the given function
}
The solution is to fix the encapsulation using a required property (See also Qt QML Hot Tips #3 "Are Your Properties Required")
import QtQuick
Button {
? required property Window mainWindow
// the compiler will error if this is not set outside this component.
? text: qsTr("Enter Full Screen")
? onClicked: mainWindow.showFullScreen()
// completely unambiguous call on a known object
// (non-qualification within immediate component scope is allowed)
}
Avoid using Root Context Objects / Properties
These have been used typically to publish back end class instances from c++ to QML (without its really knowing about it) for global use from anywhere in the QML structure.
Don't do that anymore.
QQmlApplicationEngine engine
Backend backend;
engine.rootContext()->setContextProperty("backend", &backend);
engine.load("main.qml");
// the enginenow has access to "backend" but has no idea what it is really.
The following provides one possible way to avoid using root context properties without changing much else by using QQmlApplicationEngine::setInitialProperties.
Significantly, the Backend class must specify the QML_ELEMENT macro to make itself known to the QML code model.
QQmlApplicationEngine engine
Backend backend;
engine.setInitialProperties({ // available since Qt 5.14
? {
"backend", QVariant::fromValue(&backend)
}
// this is a QVariantMap (i.e. a JSON object)
});
engine.load("main.qml");
// now has access to "backend" but still has no idea what it is.
A Good Solution - use singletons
Use QML_SINGLETON macro or qmlRegisterSingletonType from C++ and allow QML to use it - it will be available in all QML contexts and the Code Model - this is fine because QML depends on C++. This API has been available for quite some time already but does require you to write some additional code providing the singleton factory provider function and its pointer. In this case, each instance of the QML engine within your application gets its own singleton instance.
Use qmlRegisterSingletonInstance to achieve the same as qmlRegisterSingletonType using an object declared on the stack e.g. in main() but without the need to write all the additional code. In this case there is always ONLY one instance of the singleton across all QML engines in the same application.
There is no need to pass a property around all over the QML stack - just import the QML Module registering the desired singleton and it is available anywhere. Normal ownership rules apply: created on first use. If QML creates it, the QQmlEngine will destroy it. If it is created in C++ ahead of QML loading any files, then care should be taken to QQmlEngine::setObjectOwnership(object, QQmlEngine::CppOwnership). Note this function is inherited from QJSEngine.
import QtQuick
import QtQuick.Controls
import MyBackend
// assume that MyBackend module contains the type registration
// QML_SINGLETON for the Backend class or is registered with one
// of the other methods
ApplicationWindow {
Component.onCompleted: Backend.init()
// The backend (capital "B") is available from anywhere
// in the QML structure, type safe and code completion aware.
}
That's it - you are now fully qualified!