Sign up for the KDAB Newsletter
Stay on top of the latest news, publications, events and more.
Go to Sign-up
You've reached the third and final entry of the Instantiating arbitrary Qt Quick components with JSON series. Part 1 and Part 2 can be found at their respective links.
The first part focuses on the software design pattern used to dynamically instantiate components. The second one shows how to layout these dynamic components by incorporating QML' s positioning and layout APIs.
On the last entry I wrote about:
Now there's:
Unfortunately for us, this is were the QML Engine becomes a limiting factor. Items in QML, instantiate QObjects. Every QObject in Qt contains an ID. We can set that ID from QML the moment we instantiate our component, like so:
Item {
id: myID
}
Nevertheless, id is not a normal QML property. In fact it is no property at all. As a result, we're unable to set an id after instantiation. Since neither Repeater or Instantiator allows us to set individual IDs during object creation, we have no means for naming our instantiated components, and therefore we cannot refer to them from other components. This limits our ability to anchor components to siblings that come from the repeater. We can only anchor to the parent and to siblings defined traditionally from the QML document.
With this in mind, let's see how far can we take the use of anchors in our components factory.
But first, a quick reminder... Qt provides us with two syntaxes for using anchors, an object syntax and a shorthand for its individual properties:
anchors.left: parent.right
anchors: {
left: parent.right
}
To keep things simple, I make use of the longer syntax in our model:
// This model lays out buttons vertically
property var factoryModel: [
{
"component": "RowLayout"
"anchors": {
"fill": "parent"
}
"children": [
{
"component": "Button"
"text": "Button 1"
},
{
"component": "Button"
"text": "Button 2"
}
]
}
]
Let's start by implementing fill: parent. If we were to assign the string from our model, which says "parent", to our instantiator, the assignment would fail because we cannot assign a string where an object id is expected.
// This would fail...
instantiator.anchors.fill = modelData.anchors.fill;
// because it evaluates to this:
instantiator.anchors.fill = "parent";
Instead, we must parse the value from our model into valid JavaScript, and for that we use Javascript's eval function. Calling eval will return whichever value the JS expression inside the string evaluates to. Here "parent" will point to the parent property relative to the context where eval is run. For the purposes of anchoring, this is fine.
Our code now looks like:
if (typeof(modelData.anchors.fill) === "string")
instantiator.anchors.fill = eval("instantiator."
+ modelData.anchors.fill);
There's a couple of issues:
"parent;this.onPressed=function(){/*arbitrary_code_goes_here*/};" // Note that I don't know if this example really works
This takes care of the parent assignment, then overrides the contents of onPressed with a function that runs arbitrary code for the user to unsuspectingly execute. This is why every respectable linter and static analyzer for JavaScript will warn you not to use eval or any of its equivalent methods; in order to prevent remote code execution.
error: The use of eval() or the use of the arguments object in signal handlers is not supported when compiling qml files ahead of time. That is because it's ambiguous if any signal parameter is called "arguments". Similarly the string passed to eval might use "arguments". Unfortunately we cannot distinguish between it being a parameter or the JavaScript arguments object at this point.
My preferred way to sanitize inputs is using Regular Expressions. We want to guarantee the strings from our models evaluate to a valid anchor name.
Valid anchor names include:
Here's a RegEx I wrote that covers all anchors:
const anchorsRegex =
/[a-z_][a-zA-Z_0-9]*(\.(top|right|bottom|left|baseline|(vertical|horizontal)Center))?/;
We can use it to sanitize anchor inputs from the model, like so:
anchorsRegex.test(modelData.anchors.fill))
The key is to remove all characters that could be used to execute malicious content. If one of such characters must be present, then we make sure its uses are restricted, as we've seen here.
Here's what my final code for attaching anchors looks like:
Component {
id: loaderComp
Loader {
id: instantiator
// ...
onItemChanged: {
// Anchor properties
if (typeof(modelData.anchors) === "object") {
// Regex for validating id and id.validAnchorline
const anchorsRegex = /[a-z_][a-zA-Z_0-9]*(\.(top|right|bottom|left|baseline|(vertical|horizontal)Center))?/;
// Before attempting to assign,
// check that properties are present in object
// and that model attributes aren't undefined.
if ((typeof(modelData.anchors.fill) === "string")
&& anchorsRegex.test(modelData.anchors.fill)) {
instantiator.anchors.fill = eval("instantiator."
+ modelData.anchors.fill);
}
else if ((typeof(modelData.anchors.centerIn) === "string")
&& anchorsRegex.test(modelData.anchors.centerIn)) {
instantiator.anchors.centerIn = eval("instantiator."
+ modelData.anchors.centerIn);
}
else {
if ((typeof(modelData.anchors.left) === "string")
&& anchorsRegex.test(modelData.anchors.left)) {
instantiator.anchors.left = eval("instantiator."
+ modelData.anchors.left);
item.anchors.left = item.parent.anchors.left;
}
if ((typeof(modelData.anchors.right) === "string")
&& anchorsRegex.test(modelData.anchors.right)) {
instantiator.anchors.right = eval("instantiator."
+ modelData.anchors.right);
item.anchors.right = item.parent.anchors.right;
}
if ((typeof(modelData.anchors.baseline) === "string")
&& anchorsRegex.test(modelData.anchors.baseline)) {
instantiator.anchors.baseline = eval("instantiator."
+ modelData.anchors.baseline);
item.anchors.top = item.parent.anchors.top;
}
else {
if ((typeof(modelData.anchors.top) === "string")
&& anchorsRegex.test(modelData.anchors.top)) {
instantiator.anchors.top = eval("instantiator."
+ modelData.anchors.top);
item.anchors.top = item.parent.anchors.top;
}
if ((typeof(modelData.anchors.bottom) === "string")
&& anchorsRegex.test(modelData.anchors.bottom)) {
instantiator.anchors.bottom = eval("instantiator."
+ modelData.anchors.bottom);
item.anchors.bottom = item.parent.anchors.bottom;
}
}
}
// ...
Like with Layouts, we attach our anchors to the instantiator, and not just the item. The item must attach itself to the instantiator where applicable.
Then take care of the rest of the anchor's properties like so:
// ...
// Anchor number properties
if (typeof(modelData.anchors.margins) !== "undefined")
instantiator.anchors.margins = Number(modelData.anchors.margins);
if (typeof(modelData.anchors.leftMargin) !== "undefined")
instantiator.anchors.leftMargin = Number(modelData.anchors.leftMargin);
if (typeof(modelData.anchors.rightMargin) !== "undefined")
instantiator.anchors.rightMargin = Number(modelData.anchors.rightMargin);
if (typeof(modelData.anchors.topMargin) !== "undefined")
instantiator.anchors.topMargin = Number(modelData.anchors.topMargin);
if (typeof(modelData.anchors.bottomMargin) !== "undefined")
instantiator.anchors.bottomMargin = Number(modelData.anchors.bottomMargin);
if (typeof(modelData.anchors.horizontalCenterOffset) !== "undefined")
instantiator.anchors.horizontalCenterOffset = Number(modelData.anchors.horizontalCenterOffset);
if (typeof(modelData.anchors.verticalCenterOffset) !== "undefined")
instantiator.anchors.verticalCenterOffset = Number(modelData.anchors.verticalCenterOffset);
if (typeof(modelData.anchors.baselineOffset) !== "undefined")
instantiator.anchors.baselineOffset = Number(modelData.anchors.baselineOffset);
}
}
}
}
As you can see, we nullify the dangers of eval by sanitizing our inputs properly. This doesn't fix our inability to anchor to sibling components or our inability to pre-compile the QML, but we can use this tool to refer to other items in the QML tree. The tree itself may come from a document loaded from a remote location, using a Loader.
Running any remote document in our QML code would also open the doors to arbitrary code execution attacks. We may not be able to sanitize an entire QML document like we can sanitize for individual properties, therefore you should only allow QML and JS file downloads from a trusted source, and always use an encrypted connection to prevent a targeted attack. Self-signed certificates are better than no certificate. Without encryption, a malicious actor could intercept the traffic and alter our code while it's on its way.
This has been the last entry in this series. Thanks to Jan Marker for his time reviewing my code. I hope you've learned something from it. I personally enjoyed looking back at Part I the most, because the technique shown there has more real world applications.
About KDAB
The KDAB Group is a globally recognized provider for software consulting, development and training, specializing in embedded devices and complex cross-platform desktop applications. In addition to being leading experts in Qt, C++ and 3D technologies for over two decades, KDAB provides deep expertise across the stack, including Linux, Rust and modern UI frameworks. With 100+ employees from 20 countries and offices in Sweden, Germany, USA, France and UK, we serve clients around the world.
Stay on top of the latest news, publications, events and more.
Go to Sign-up
Upgrade your applications from Qt 5 to Qt 6 with KDAB’s migration services. Get a free migration assessment and join a hands-on workshop to prepare your team for a successful transition!
Learn more
4 Comments
20 - Jun - 2024
Grecko
Hello,
Interesting project, is the final code available somewhere? I'd like to play with it.
You wrote that valid anchor names includes idName. but also mention that "we cannot refer to [our instantiated components] from other components". Does your eval logic works with anything other than parent as the id then? I feel like this could be done without eval and adding for loops would simplify the code.
27 - Jun - 2024
Javier Cordero
Hi Grecko,
I would rather not host the full code because I worry that would legitimize it. The techniques I focused on during the conclusion of each blog post are good things on their own, but people should steer away from using tree models to instantiate arbitrary Components from QML, as that would produce code that's difficult to maintain and develop on top of. It also goes in a direction contrary to The Qt Company's efforts in making QML compilers.
Having said that, I made sure one could re-create a working project only by copy-pasting code from each of the blog entries. So, you should be able to get a working project by reading carefully and copy-pasting the right snippets.
The eval logic will work with any id that can be accessed from directly inside of the instantiator component, because "instantiator." is a prefix in all of the calls to eval. The prefix is only there because anchors in QML have a limitation: they will only work with components that are adjacent to them or with the parent, but we cannot anchor to the parent's parent, the root element, or to the child of a sibling. Meaning you could forgo the prefix in other uses.
Now, the reason I said “we cannot refer to [our instantiated components] from other components” is unrelated to that. The components from the model are being instantiated via an instantiator. Both instantiators and Repeaters will not allow us to set ids to their components. Defining a property and calling it id won't do because ids are not properties. Since we can't and therefore don't give the components an id, "we cannot refer to [our instantiated components] from other components".
You're absolutely right about using for loops to simplify or shorten the code. Nevertheless, eval would still be needed for the anchors because ids are different from properties.
1 - Jul - 2024
Grecko
So here we can only anchors to the parent and not the siblings since we can't set an id to the siblings. In pseudo JS it could be done like this:
No need for an eval here because we always anchors to parent. In your code you do :
instantiator.anchors.centerIn = eval("instantiator." + modelData.anchors.centerIn)
butmodelData.anchors.centerIn
would always be "parent" unless I'm mistaken.1 - Jul - 2024
Javier Cordero
Nice strategy. Given the value would always be parent, we could get rid of eval that way. I stand corrected.