Vladimir Prus


vladimirprus.com

Thursday, May 19, 2016

Qt Quick on Desktop

I worked with Qt quite a bit over the years, but it was only in 2015 where I had a chance to do substantial work with Qt Quick. I wanted to share some impressions, specifically for desktop applications.

Summary

Here is an advance summary of the key points, starting with advantages:

  • Qt Quick is now a mature way to build desktop application that either use standard-looking desktop controls, or have relatively simple custom UI.
  • Data binding is quite pleasant in all ways. However, it's only limited to binding UI properties to expressions over model properties. Dynamically changing UI structure is possible, but is rather convoluted.
  • Animation system is solid and support for GL effects is much more convenient than writing GL directly.
  • For a pleasant surprise, it has a state machine built in, thought with some quirks.

But not everything is perfect:

  • The set of standard controls and styles could be larger. If you wish to achieve Metro design or Material design on desktop, you might need to use third-party extensions or do it yourself.
  • Styling merchanisms (in Qt Quick Controls 1) are quite limited, having neither inheritance nor attributes, and Qt Quick Controls 2 have something else entirely.
  • There are two different layout mechanisms, each with its own quirks.
  • As of Qt 5.5, High DPI support involved doing the math yourself. This might have improved since.
  • Open GL is required, and especially on Windows, the set of possible GL configuration is large, the documentation is imperfect, and there are "interesting" differences in behaviour.

What is Qt Quick?

Let's clarify some terminology first:

  • QML is a language that defines a tree of objects, along with some property bindings and executable code. It uses custom language for the tree proper, and Javascript for expressions and functions
  • Qt Quick is a set of basic visual components, and an engine to render them
  • Qt Quick Controls is a set of standard UI controls and layouts.

These are pretty much always used together, so I'll use "Qt Quick" throughout regardless of what layer I really talk about.

QML and Qt Quick

In order to illustrate how Qt Quick UI is put together, I'll use a simple part of registration UI, shown below

As you start typing a phone number a decorative line under the input fields turns into progress bar, and as you submit the form, the progress bar becomes indefinite one, and when error is returned, the hint below the input fields becomes an error display.

The progress bar is a custom component that I won't discuss in detail, but once but once written, it can be easily used:

CustomInput {
    id: phoneNumber
}
CustomPercentageLine {
    percentage: model.validPhoneLength == -1 ? 1 : phoneNumber.text.length/model.validPhoneLength
    animated: model.working
}

Here, model is an instance of C++ class that we've injected into QML. The binding expression checks whether a phone has fixed size, and if so, computes progress bar percentage. And if model is busy validating phone number, and its working property is true, the animated property of the view is also updated, causing progress bar to show indefinite state. Similarly, this is how hint and error display is implemented

CustomLabel {
    text: model.error ? model.error : "We'll confirm your number by sending a one-time SMS"
}

Data binding is certainly good, and QML offers certain syntactic convenience, as we can use JavaScript expressions with no escaping - unlike XML-based templating engines. This gets particularly important for larger expressions, like the one below:

CustomLabel {
    text: {
        if (model.error) {  return model.error; }
        if (model.state === "resendingPin") {
            return "Enter the previous code or wait for a new code to be sent";
        } else {
            return "Enter the code";
        }
    }
    color: model.error ? "#ce4844" : "#999ba4"
}

The animated property of my custom progress bar is used to indicate indefinite progress, and is shown by three rectangles moving across the blue progress line. Making the rectangles move is easy with the animation framework, for example there's the animation definition for the first rectangle:

NumberAnimation {
    id: animation1
    target: rectangle1
    property: "x"
    duration: 2000
    easing.type: Easing.OutCubic
    loops: Animation.Infinite
    from: -6
    to: parent.width
}

Animation for the second and third rectangles is similar, but they should start with a delay, so we need to use a separate timer item:

Timer {
    id: timer2
    interval: 500
    onTriggered: animation2.start()
    repeat: false
}

As soon as the animated property of progress bar is set to true, we start animating the first rectangle, as well as the timers that will start animating two others:

animation1.start();
timer2.start();
timer3.start();

It would be more convenient if animation supported delayed start without auxilliary timers, but with exception of that, the mechanism is OK.

For managing larger UI changes, the state machine framework comes handy. For example, upon startup we show a spalsh screen and attempt to connect to backend server and check for authorization, showing the login screen only if necessary. If we can't connect quickly, we'll show a separate screen with progress bar. Here's the relevant code:

DSM.State {
       id: initial
       DSM.TimeoutTransition {
           targetState: waiting
           timeout: 10000
       }
       DSM.SignalTransition {
           signal: model.connectedChanged
           guard: model.connected && !model.isAuthenticated()
           targetState: needPhone
      }
}

It illustrates two features. First, it's possible to transition to a different state simply after a timeout. Second, it's possible to transition to a different state on a signal, if a particular condition is met. However the second example also shows one of the largest inconveniences: you can't easily transition when a particular expression becomes true. I'd much rather not bother thinking what signals are emitted, and write this:

DSM.SignalTransition {
    guard: model.connected && !model.isAuthenticated()
    targetState: needPhone
}

After all, tracking changes to expression already works for properties. There is a workaround to get this effect with auxilliary items, but it ought to be a standard feature. Still, having state machines as standard feature much simplifies UI logic.

Controls Styling

I'll start the critical part of my post with controls and styling. The below screenshots shows the controls gallery example with the two styles available to Windows desktop application in Qt 5.5.

You might wonder what is the difference between Desktop and Base styles. The Desktop style, which is the default, actually uses Qt Widgets style engine to render everything, so it looks exactly the same as the standard Qt Widgets. It's good if you want to take advantage of Qt Quick without changing the look, but creating custom styles in C++ certainly does not look very attractive.

One can easily switch to the Base style, implemented entirely in QML, but it it slightly less polished, and has quite of lot of hardcoded styling, such as:

Rectangle {
    radius: TextSingleton.implicitHeight * 0.16
    border.color: control.activeFocus ? "#47b" : "#999"
}

The literal 0.16 is repated in 8 places over several files in the base style definitions, while #47b is found at 14 locations - not what I'd call solid engineering, compared to say most CSS frameworks or Android style system, where you can make consistent changes with a few attribute declarations. There are various third-party solution that offer modern styles for Qt, including Ubuntu Components and Papyros, but these are not actually styles -- you need to use their own buttons and inputs and other components, you can't just restyle your existing code.

The set of controls is quite standard too. For example, there's not even implementation of filtered list view.

Layouts

Qt Quick offers two approaches to layout. Anchor-based layout allows you to position an item relatively to another item - for example you can fill parent and add margin, or you can put an item to the right of another item. It's easy, but not very adaptive.

There's is also dynamic layout, where desired size of items and available geometry is used to place items on screen. That's what I used, and it mostly works, except for annoying inability to set margins - as the margins feature in anchor layout is actually specific to anchor layout. I ended up writing a custom component that specifically adds margin around content.

Not invented here

Both styling and layout issues above makes me thing that Qt Quick is being too original. It's a declarative UI language, so looking at CSS and trying to be more conceptually similar would have helped. Sure, CSS is not perfect, but a lot of people know how to use it to style everything from text to buttons to toolbars, and even create random geometric shapes. Common behaviours like borders, margins, and shadows are trivial to accomblish. Android also has declarative UI with what I find a better style system and layout mechanisms. XAML is also a popular solution. It would be great if Qt Quick was at least conceptually similar to one of these.

Also, given that QML is heavily based on JavaScript, one would imagine JavaScript modules would work. Maybe not the fancy async module systems, but just standard CommonJS modules. They don't work, with no progress since 2011, as you can see in the issue.

Overall, it would be great if Qt Quick were more aligned with other technologies. I have no idea whether it was possible given actual development timelines.

Quality of implementation

There are a couple of issues that were important, but not fundamental.

As of Qt 5.5, we found that High DPI support is not quite good. Every time you specify a position or size, it's in pixels, so for good look on a High DPI system, one needs to have an utility function to scale pixels according to physical resolution. This might be fixed in Qt 5.6, but I haven't had a chance to try.

Qt Quick uses Open GL for all rendering. Of course, it creates lots of possiblities, but also configuration problems. Say, on Windows you might end up with 5 different GL implementations, not counting different video card vendors. We ended up with quite some crashes from users that had 'GL' in backtrace, and could not be reproduced. We also had fun getting things to work under Virtual Box, at one point even reverting particular Windows update for things to start working. This should eventually improve, but beware for now.

Wrapping Up

I found Qt Quick to be a pleasant framework for developing a desktop app with a relatively simple interface. There were some limitations and issues, but they probably will eventually be fixed.

The biggest concern is that the set of controls and styles is quite basic, and that the styling mechanism is not quite flexible. Furthermore, the new direction is is Qt Quick Controls 2, and it is both focused on mobile devices and uses a different styling approach, so unlikely to bring improvements on desktop. If I were to use Qt Quick on a larger project, I would probably use a third-party controls library.

Qt remains the best framework for desktop applications using C++. Whether it's the best desktop framework overall, given that Xamarin recently was open-sourced, remains to be seen.