Loading QML Conditionally
My multiFEED application was originally built at a time when most users were still running the BlackBerry 10.0 operating system. Even as I write this, there are still quite a few sluggish carriers around the world that haven’t issued an update to 10.1, leaving their customers locked out from using some of the more interesting apps that have become available since the BlackBerry 10 launch at the end of January 2013. multiFEED allows extensive user configuration, and some of those settings required somewhat awkward combinations of checkboxes, dropdowns, and sliders. I decided that some of them would work much better as “custom pickers“. This style of control looks much like the wheels of a mechanical slot machine, and has been used in a few BlackBerry 10 native controls, such as DateTimePicker
, since the advent of BlackBerry 10. I was halfway through happily converting some of my settings to use custom pickers when I realized they were introduced with BlackBerry 10.1 and were not available on 10.0.
Now, it is important to me not to alienate or isolate my existing users that are still stuck on 10.0 by changing the minimum required OS version to 10.1.0, so I knew I needed a solution that would give 10.1 users access to the new custom pickers but continue using the old style sliders and dropdowns if the user was running an older OS version. Since I am fluent with C++ my initial idea was to move the control definitions out of QML and into C++ classes. The plan was that I would check the OS version, then build the controls on the fly to use only the control types that were available on that platform. It seemed like a workable solution, but I discovered very quickly that it didn’t work. The instant your C++ code includes a reference to the Picker
class, the application won’t start up, even if that class is never instantiated. What I needed was a way to get the app to start up successfully, and only then compile the source code to decide which types of control to instantiate. The answer? Conditional QML loading.
If you have developed your application entirely with QML you may not realize that all the QML you write is only turned into machine code at runtime. All BlackBerry Cascades apps start up some C++ code, which instantiates a class to manage the application, and which in-turn reads main.qml
and all other QML source files that it refers to. If your QML code contains errors, it is the C++ code in your application class that reports the errors and shuts the application down when you try to start it.
In a QML-only application this is the only place where the QML code is read and converted to machine instructions, but you can load QML from C++ code anywhere in your application you like.
The Basic Code
Let’s build a basic C++ class that can decide which QML to read at runtime. Here is the header file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#ifndef MYPICKER_HPP_ #define MYPICKER_HPP_ #include <QtCore> #include <bb/cascades/CustomControl> #include <bb/cascades/Container> using namespace bb::cascades; class MyPicker: public CustomControl { Q_OBJECT private: Container* root; public: MyPicker(); virtual ~MyPicker(); }; #endif /* MYPICKER_HPP_ */ |
Our new class inherits from CustomControl
so it can be placed on a page just like any standard QML control type. We need the Q_OBJECT
macro declaration so we can make our smart control available to the rest of the QML in the application. Note that we have declared a Container*
variable called root
. The type of this class is important and must match the outermost control type in the QML we will be loading. If the QML was going to be a Sheet definition, then this declaration would be a Sheet*
instead. Now for the basic cpp file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#include "MyPicker.hpp" #include <bb/platform/PlatformInfo> #include <QStringList> #include <bb/cascades/QmlDocument> using namespace bb::platform; MyPicker::MyPicker() { QmlDocument* qmlPicker; PlatformInfo info; QStringList tokens = info.osVersion().split( "." ); int majorVer = tokens[0].toInt(); int minorVer = tokens[1].toInt(); if ( ( majorVer == 10 ) && ( minorVer == 0 ) ) { // ---This OS version doesn't support custom pickers qmlPicker = QmlDocument::create( "asset:///MyPickerOld.qml" ).parent( this ); } else { // ---This OS version supports custom pickers qmlPicker = QmlDocument::create( "asset:///MyPickerNew.qml" ).parent( this ); } if ( qmlPicker->hasErrors() ) { qDebug() << "Error loading MyPicker QML"; } else { this->root = qmlPicker->createRootObject(); setRoot( this->root ); } } MyPicker::~MyPicker() { } |
Lines 10-14 are used to retrieve the OS version and extract the major and minor version portions. Lines 16-22 use this information to load the correct QML source file for that OS version. If all goes well then lines 27 and 28 get a reference to the outermost Container in the loaded QML and set it as the root for our smart control. MyPickerOld.qml
and MyPickerNew.qml
are standard QML source files, and will look something like this:
1 2 3 |
Container { // ---Your OS version appropriate control layout and functionality here } |
The only other thing we need to do now is make our smart control available to the other application QML files. In main.cpp
at the beginning of the main()
function, declare the following:
1 |
qmlRegisterType<MyPicker>( "CustomPickers", 1, 0, "MyPicker" ); |
… and then in any QML file that you wish to use your smart picker control in add this at the top of the file:
1 |
import CustomPickers 1.0 |
Getting Stuff In and Out
If your MyPickerOld.qml
and MyPickerNew.qml
are completely self-contained and you don’t need to get any values in or out that aren’t standard CustomControl
properties or respond to any of their custom signals outside of their own internal signal handlers, then you are done. Just design the two versions of your QML and the appropriate one will be loaded depending on the OS version. In most cases though you will need access to custom properties as well as handle custom signals.
Let’s start by adding a custom property to both of our QML source files:
1 2 3 4 5 |
Container { property int myIntProperty // ---Your OS version appropriate control layout and functionality here } |
You might think that since what the user sees on their screen is one of the Containers you have designed that you can just access this property directly like any other control. Unfortunately this won’t work because the control on the screen is actually not a Container
, it is a CustomControl
. The Container the user is seeing is actually held in the private root
variable on MyPicker
. To access the new property we need to duplicate it on the CustomControl
and add read and write handlers for it to the hpp file:
1 2 3 4 5 6 7 |
class MyPicker: public bb::cascades::CustomControl { Q_OBJECT Q_PROPERTY(int myIntProperty READ getMyIntProperty WRITE setMyIntProperty) private: int getMyIntProperty(); void setMyIntProperty( int newValue ); } |
…and the cpp file:
1 2 3 4 5 6 7 |
int MyPicker::getMyIntProperty() { return this->root->property("myIntProperty").toInt(); } void MyPicker::setMyIntProperty( int newValue ) { this->root->setProperty("myIntProperty", newValue); } |
All the handler functions do is pass the requests on down to the Container
property. Now you can reference myIntProperty
on the parent page and you will be get or set the value on the actual QML control. Setting the QML version of the property this way will trigger the onMyIntPropertyChanged
signal on the Container, so your custom QML control will know and can respond if myIntProperty
is changed from the parent page.
Handling Signals
Unfortunately we are not quite done yet, since the signal sent when myIntProperty
on the QML control is changed won’t propagate up to the CustomControl
for use on the parent page. Once again our CustomControl
must act as intermediary. To do this we must declare a matching signal on MyPicker
:
1 2 3 4 5 6 7 8 9 |
class MyPicker: public bb::cascades::CustomControl { Q_OBJECT Q_PROPERTY(int myIntProperty READ getMyIntProperty WRITE setMyIntProperty) private: int getMyIntProperty(); void setMyIntProperty( int newValue ); signals: void myIntPropertyChanged(); } |
…and then connect the QML signal to it:
1 2 3 |
this->root = qmlPicker->createRootObject(); QObject::connect( this->root, SIGNAL(myIntPropertyChanged()), this, SIGNAL(myIntPropertyChanged()) ); setRoot( this->root ); |
Now you can handle changes to myIntProperty
on the parent page like usual:
1 2 3 4 |
MyPicker { onMyIntPropertyChange: { } } |
Improving Performance
If you frequently access myIntProperty
from the parent page the overhead of passing the request down to the QML each time might slow things down a bit too much. If so, consider keeping a local copy of the value on MyPicker and return that value instead. To do this, create a new private int
variable to hold the current value and a new slot on MyPicker
to handle the change signal from the QML:
1 2 3 4 5 6 7 8 9 10 11 12 |
class MyPicker: public bb::cascades::CustomControl { Q_OBJECT Q_PROPERTY(int myIntProperty READ getMyIntProperty WRITE setMyIntProperty) private: int currentIntPropertyValue; int getMyIntProperty(); void setMyIntProperty( int newValue ); signals: void myIntPropertyChanged(); public slots: void handleMyIntPropertyChanged(); } |
…then connect the QML change signal to it instead of directly to the MyPicker
signal:
1 2 3 |
this->root = qmlPicker->createRootObject(); QObject::connect( this->root, SIGNAL(myIntPropertyChanged()), this, SLOT(handleMyIntPropertyChanged()) ); setRoot( this->root ); |
…and finally create the slot function and change the get handler:
1 2 3 4 5 6 7 8 9 10 11 |
void MyPicker::handleMyIntPropertyChanged() { // ---Save the value locally this->currentIntPropertyValue = this->root->property( "myIntProperty" ).toInt(); // ---Pass the signal on to the parent page emit myIntPropertyChanged(); } int MyPicker::getMyIntProperty() { return this->currentIntPropertyValue; } |
Putting It All Together
Now lets combine everything. First create two OS version specific QML files:
1 2 3 4 5 6 7 8 |
Container { property int myIntProperty onMyIntPropertyChange: { // ---Property may have been changed below or by parent page } // ---Your OS version appropriate control layout and functionality here } |
Next we need a header file to define the MyPicker
class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#ifndef MYPICKER_HPP_ #define MYPICKER_HPP_ #include <QtCore> #include <bb/cascades/CustomControl> #include <bb/cascades/Container> using namespace bb::cascades; class MyPicker: public CustomControl { Q_OBJECT Q_PROPERTY(int myIntProperty READ getMyIntProperty WRITE setMyIntProperty) private: Container* root; int currentIntPropertyValue; int getMyIntProperty(); void setMyIntProperty( int newValue ); public: MyPicker(); virtual ~MyPicker(); signals: void myIntPropertyChanged(); public slots: void handleMyIntPropertyChanged(); }; #endif /* MYPICKER_HPP_ */ |
And finally the body of the MyPicker
class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
#include "MyPicker.hpp" #include <bb/platform/PlatformInfo> #include <QStringList> #include <bb/cascades/QmlDocument> using namespace bb::platform; MyPicker::MyPicker() { QmlDocument* qmlPicker; PlatformInfo info; QStringList tokens = info.osVersion().split( "." ); int majorVer = tokens[0].toInt(); int minorVer = tokens[1].toInt(); if ( ( majorVer == 10 ) && ( minorVer == 0 ) ) { // ---This OS version doesn't support custom pickers qmlPicker = QmlDocument::create( "asset:///MyPickerOld.qml" ).parent( this ); } else { // ---This OS version supports custom pickers qmlPicker = QmlDocument::create( "asset:///MyPickerNew.qml" ).parent( this ); } if ( qmlPicker->hasErrors() ) { qDebug() << "Error loading MyPicker QML"; } else { this->root = qmlPicker->createRootObject(); QObject::connect( this->root, SIGNAL(myIntPropertyChanged()), this, SLOT(handleMyIntPropertyChanged()) ); setRoot( this->root ); } } void MyPicker::handleMyIntPropertyChanged() { // ---Save the value locally this->currentIntPropertyValue = this->root->property( "myIntProperty" ).toInt(); // ---Pass the signal on to the parent page emit myIntPropertyChanged(); } int MyPicker::getMyIntProperty() { return this->currentIntPropertyValue; } void MyPicker::setMyIntProperty( int newValue ) { this->root->setProperty("myIntProperty", newValue); } MyPicker::~MyPicker() { } |
Testing
While you are developing the two versions of the custom control QML file you can use the latest version of the simulator to write them both, but I strongly suggest you test everything before release with device simulators for the OS versions your QML is designed to handle, or an actual device running those versions to ensure that you don’t inadvertently include any code that won’t run on the older operating system.
Conclusion
BlackBerry continues to improve their flagship mobile OS by adding new features and controls, but if you want to use these improvements without forfeiting the ability of your application to run on older versions of BlackBerry 10 you need a way to load QML on-the-fly depending on the OS version the app finds itself running on. Even if your whole app was developed entirely in QML and you know only the bare minimum C++ to make your app run, you should be able to copy-paste the code here and tweak it for your own needs.
WebView and HTML anchor tags ›