Subnavigation

Designing code for QML UIs

QML is starting to approach its initial release. Those who have been using QML for a while can rejoice, for this means that we're no longer changing everything and the stability of having an actual release can now be seen on the horizon (although maybe I'm just bitter because I had to change Samegame again today, because the QML behavior underneath has changed in what should be the last destabilizing change of this release). To celebrate, I'm going to do some blog posts on QML; just some various brain dumps on how to get the most out of QML. I think this is useful, largely because I think that QML is so amazingly powerful when you do get the most out of it. For now, getting the most out of QML is sadly limited to just awesome, fluid UIs. It's still worth it. Since the docs explain how to get started and animating your UIs with QML, I'm going to start from the other side here. Presumably a lot of labs readers are C++ developers, and are wondering how they can get the most out of QML even though they write C++. In this case you can get a lot out of QML from using it as the UI layer on top of your C++ application logic. So here are the 'recommended best practices' to think about, so that you can make your next application QML-compatible from the start. Since this is basically just separation of the UI layer and the application logic layer, these practices are probably a good idea anyways. (Note that this is NOT the model-view-controller pattern, even though there is some overlap.)

Separating the UI from the application logic, QML style, starts at design time. Think about what it is that your application really does. Naturally you'll want to have a fluid, aethetically pleasing yet eminently usable user interface for it, but that part comes later. Your application may interface with a custom database, or it may play a game, or it may perform complex calculations over and over. For simple things, like just accessing web content or adding two numbers, you can make do with what comes with QML, such as the integrate QtScript or XmlListModel. For everything else, there's Qt C++. If your application is, for example, interfacing with a custom database then you'll likely be implementing that interface in C++ still. Because the layers are separate you should implement, on its own, the C++ parts that drive the application logic.

When writing the C++ layer, you want to end up with a QObject derived class which exposes all the relevant application data through properties, signals and slots. For a database example, you would have properties that get set to provide the connection details, then you would have signals when the connection state progresses, and then you would have some way of getting the data. Depending on the data, this could be slots that return a value, properties that include whole models for viewing in the UI, or a mixture with slots that might update the data in the models exposed through properties. This is similar to existing Qt APIs for data objects. If you look at QAction, you will see that it provides a variety of properties for setting details about the action as well as a signal when the action is triggered. Looking at QAction as an example, it also has slots for manipulating properties. While this is not necessary for QML, it is perfectly acceptable for use from C++. Another example would be the folderlistmodel plugin in src/imports/folderlistmodel, A final real-world example would be the system info API in Qt Mobility which you could expose to QML and then just pick up and use due to the property based interface.

For the most part it really is as simple as just using properties, signals and slots. The only QML specific advice I'd give is to think more declaratively. The techincal requirements for QML integration are easy to meet, but for something that's really easy to use with QML it helps to consider the declarative approach when designing the interface. This is not incompatible with a good C++ interface. The main difference is with the use of properties. From C++, properties are little more than a convenience for the documentation. But from QML, properties are the primary method of interaction. It will be more useful from QML to have a boolean property isOn than to have turnOn and turnOff slots in C++. It will also be a lot easier than having a toggleOnOffState() slot and a getOnOffState() slot. Because properties are the main method of interaction for QML, it's also better to have properties rather than just slots when passing data in or out of the C++ class - even when the communication is strictly one way. This is because the declarative approach thinks in terms of properties (The state is on. I want the state off.) rather than imperative steps (The state is on when getState()==true. If getState()==true, setState(false).). Other minor details from thinking declaratively are to always remember to have a notify signal for properties, and to allow the properties to be set in any order instead of having strict initialization order requirements. It's a subtle distinction though, and in the end though you'll just have a C++ class with a black box implementation and an interface comprised of properties, signals and slots. Such a class can then be used by a variety of things to construct a full application by adding a UI. From this class, you can add a C++ QWidget UI, a QML UI or something else (perhaps a pyside UI) on top to create a full application.

As this is a best practices approach, it includes a test plan. You'll want to test this with automated unit tests before even writing your first UI. Because you want a C++ part completely separate from the UI, it needs to be tested separately to any of your UI front ends. This can seem like a lot of overhead, but the actual data part is usually quite small compared to the UI. If it isn't then it's probably something really hairy and you need the extra testing anyways ;) .

The advantage of having a completely separate UI and data layer is that you can alter or add new UIs at a moments notice. Imagine 'themes' that can change the entire look and feel, not just a few colors and pixmaps. Even better, imagine the ease in which you can port your application to a completely different platform. I think Qt's cross-platform widgets do a fine job on some of the desktop platforms, although that is debatable. Less debatable is how well they do when porting your app from a desktop platform to a 240x320 keypad phone and a 800x480 touchscreen phone. With such radical changes in the form factor and basic I/O devices, there is no alternative to a complete UI rewrite if you want to have an excellent UI. But if you have the UI and data layer completely separated, you can easily rewrite the entire UI in QML, and get the same functionality behind it with a simple recompile. So while 'write once, deploy everywhere' is an impossible dream with the UI across phones (and similar devices) you don't have to give up hope on the application logic, which was the harder one to write anyways.

A second advantage is that the separate UI and data layer allows them to be developed individually. While a developer is writing and testing the data logic, a designer can prototype and write the UI with dummy data. Then the unification of the two to make a real application becomes really simple. Or, for free software projects, if the developer has a great idea but wrote a lousy interface for it, then a designer has a much lower barrier towards contributing to make it more usable. He could just write a UI in QML and, at least in cases where the C++ logic was deployed via the QML plugin system, actually create and use it without needing to compile or even read the 'intimidating' C++ code.

It might be Qt specific, but my advice for future designs is to expose the application logic in a QObject derived class, using properties, signals and slots for the interface. You can then expose it through a QML or C++ plugin, use it with a QML or C++ UI, or even run unit tests on it directly. This design helps with modularity where it counts, in the data layer, and allows UI swapping at will (or even at runtime, if you're so inclined).


Blog Topics:

Comments