Wednesday, December 22, 2021

QML Quirks - A laundry list of bizzare happenings, bugs, and dodgy incomplete crap

Over the past few years, I've built a fair few UI's using QML (Qt's DSL for writing UI code) - proper ones, including one for a mission-critical / safety-of-life application, and another powering the tool to be used across a large group of non CS types. In other words, things that had to work, and not just be interactive "nice to have" toys (aka research prototypes).

Memorably, I was once asked during an interview whether I would recommend using QML and/or how it compares to using the more battle-tested QWidgets. At the time, I'd only really used it for a bunch of research prototypes (i.e. implementing HCI experiments to be exact), where it presented a great environment for implementing the kinds of dynamic non-traditional interfaces I needed. For that it was great and saved a lot of time. But, admittedly, it did also throw up a bunch of glitches (e.g. randomly sampling garbage from the wrong texture buffers / other applications even, particles not showing when chaining several scenes together but being fine when used in isolation, etc.). At the time, I could only attribute some of these to me perhaps trying to combine a few too many highly experimental techniques where perhaps the framework hadn't been tested so great.

However, knowing what I do now, I would strongly recommend that unless you were building something non-mission critical, and where the thing is loaded with animations / dynamic effects, that you really shouldn't be using it. Sure, you may be able to knock out a prototype quite quickly - but at some point - often at ill-timed moments, you will randomly stumble across one or more intractable bugs / quirks from left field that have you scrambling to rewrite / refactor the whole lot.

 

Disclaimer: Just to be clear - I generally do still like the idea of the QML language, and I think it does many things right. However, there are also many ways in which the "declarative paradigm" is really awkward to work with (*ahem* creating dialogs / temporary items / sequential-flow-based-types / etc.) 

More disconcertingly though, implementation-wise, it is seriously lacking in quality / completeness / stability, etc. in enough ways that mean that I cannot in good conscience recommend any new greenfield projects to start adopting it now.

(Plus, the fact that the embedded scripting / logic programming language it uses is Javascript... blegh!)

1) ShaderEffect gl_fragCoord is in *window space*

The implication of this is that if you are trying to render multiple independent viewports (e.g. multiple ShaderToy-style shaders), in a window, you cannot use the gl_fragCoord value, as you'll only end up seeing part of the scene in each case.

The solution here is to define your own "varying highp vec2 viewportCoord" to provide this info. Example ShaderEffect snippet:

ShaderEffect {

    vertexShader: "
        uniform highp mat4 qt_Matrix;
        attribute highp vec4 qt_Vertex;
        attribute highp vec2 qt_MultiTexCoord0;
        
        // Fragment's coordinates in viewport space
        varying highp vec2 viewportCoord;
        
        // Size of the viewport
        uniform vec2 iResolution;
        
        void main()
        {
            // Note: Need to invert y-axis of vertex coordinates to match ShaderToy scene behaviour
            viewportCoord = vec2(qt_Vertex.x, iResolution.y - qt_Vertex.y);
            
            gl_Position = qt_Matrix * qt_Vertex;
        }"
 

    fragmentShader: "
        uniform lowp float qt_Opacity;
        varying highp vec2 qt_MultiTexCoord0;
        
        // Fragment's coordinates in viewport space
        varying highp vec2 viewportCoord;

        uniform vec2 iResolution;
        #line 1

        void mainImage( out vec4 fragColor, in vec2 fragCoord )
        {
            // Placeholder Gradient for Debugging - The shader here gets replaced with your code
            //vec2 uv = vec2(fragCoord.x / iResolution.x, fragCoord.y / iResolution.y);   // <-- Enable to see how the fragCoord is in window space

            vec2 uv = vec2(viewportCoord.x / iResolution.x, viewportCoord.y / iResolution.y);
            fragColor = vec4(uv.x, uv.y, 0.0, 1.0);
        }

    "

 }

 

2a) There is no "DoubleSpinbox" control

If you need a number input control, QML only provides one for integers (SpinBox), with the official docs recommending an ugly "text-validation" approach (see "floating point number" example here) - internally, the SpinBox stores the numbers as "x 100" or even "x 1000" integer values that then need to be converted to/from strings.


2b) These spinboxes get "stuck" when trying to increment the values past 0.28 and 0.56

I'm unsure whether it's a consequence of the "bidirectional property bindings" I'm using (I should post an article / code for those sometime), or whether it's some evil float precision muckery when using the above hack to get a "DoubleSpinbox" control, but on the project I'm currently working on, the DoubleSpinboxes suffer from a weird bug where you cannot increment them (i.e. using the .increase() method, as bound to the +/- buttons OR the scrollwheel) past 0.28 or 0.56 without manually typing in a value one step up (i.e.  "0.29" or "0.57").

Another workaround for this is to use the "Ctrl-Scrollwheel power scroll" hack I added, where, if the ctrl-key is pressed when a scrollwheel event happens, the stepSize is temporarily set to 10 times the normal amount before calling `.increase()` or `.decrease()`, and then immediately reset afterwards.


3) Text edit controls have selection / arrow keys / copy/paste / etc. disabled by default, with an ugly default selection colour to boot

Making the text edit controls usable / nice looking to integrate into a UI with functionality people commonly expect requires a LOT of effort with the default control set. So, you'll eventually end up needing to subclass all the widgets and define your own base-ones that have all these things defined anyway.

 

On a related note, if you ever need to try to customise the styling of the TextInput cursor, see https://stackoverflow.com/questions/68249784/how-to-make-textinput-cursordelegate-wider-taller/68249806#68249806


4) Speaking of customisation / styling, the "QtQuick 2 controls" are often much more annoying / troublesome to customise

A key example is how you cannot easily customise the scrollbars (e.g. thickness + colour contrast in particular) of ScrollViews / Flickables, without effectively having to overwrite those elements and implement them from scratch. But then, you're left with the problem of the scrollbars overlapping in the corner in an ugly way.


5) File Browsers - The API here is perpetually incomplete, and may never be feature complete. In essence, apart from one or two trivial use cases, it's unusable. The problem is that while it supports some of the basic settings you need to set, more often than not, the critical ones you need end up not being available. (I had a list of these at some point, but I've misplaced the exact ones now)

The only solution ultimately ends up being to have to define a method / slot on your "backend" controller code (i.e. Python (PyQt / PySide) or C++), that you then use that from QML  - for example. 

filename = cmdApp.getOpenFileName(windowTitle, startPath, defaultFileName, fileFiltersList)

There are then still 2 quirks to be aware of with this approach:

    * i)  Depending on how you wire up the events, you could end up with the backend event handler "blocking" the QML UI until it completes (e.g. the "File" menu will stay open until after your handler has exited)

    * ii) The logic used to determine what directory to use when the supplied starting path is invalid / blank follows some rules that are not always easy to understand.


6) Distribution issues on Linux

With Ubuntu dropping support for their homegrown QML based dock/launcher (Unity?) recently, using QML on Ubuntu / Linux has suddenly become a whole lot more difficult if you're a small team that has to support a technically naive userbase (i.e. most userbases) with minimal resources.

For instance, on Ubuntu 20.04, users need to run the following addition commands to install packages via apt, instead of being able to run "pip" and have everything work (for a PyQt5 + QML app):

```

$ sudo apt-get install python3-pyqt5  
$ sudo apt-get install pyqt5-dev-tools
$ sudo apt-get install qttools5-dev-tools

``` 

With Ubuntu 21.04 however, it seems that in addition to these, you need to install each of the various QML modules individually (!)  There apparently isn't a single package you can install that will do all of them at once for you. The best workaround I've found for now is to get folks to install Ultimaker's "Cura" slicer, which at the time of writing still uses QML for its UI, with its packages installing the long list of needed deps.

```

$ sudo apt install cura

```


7) Scrollpanes, Clipping Issues, and Software Rendering

If deploying on Linux, and you need screen recording to work (particularly if that screen recording needs to happen in for apps running in VM's), be warned that GPU rendering will not be an option (due to various X11 context shenanigans). So, your UI's will have to be rendered using the "software renderer" pipeline (QSGRendererInterface::Software) - this alternative rendering pipeline uses QPainter to render the UI instead of the usual GPU-based approach, but since hardly anyone uses it, it will be buggy + slow.

It's not really terrible, but at the same time, you'll run into bugs like the fact that the renderer will render entire objects (beyond their bounds if need be) because the element clipping code is buggy and it bypasses the clipping flag/step entirely when some common combination of options is encountered. (I have the details somewhere in my notes, but I'd have to dig them up)

 

8) PyQt only allows you to register 60 (!!!) QML types with registerQmlType()

I recently learned that PyQt5 only supports registering up to 60 custom QML types using registerQmlType()

 

Trying to do it, you'll be faced with the following exception:

"TypeError: a maximum of 60 types may be registered with QML"

 

* It doesn't seem to matter what namespace you're registering those types to. It only lets you register 60 in the whole application / process! Not 60 per namespace, or 60 per module / file, but 60 for your whole PyQt application! 

AFAIK, there are no secret settings to increase this value. And why specifically this value is a mystery too - if it was 100 or 256, I could understand that, but 60 (!) of all numbers?!

 

* There doesn't seem to be any other way to register types so that QML won't complain about trying to access stuff it doesn't know about

 

* There is basically no documentation about this anywhere on the internet. Attempts to find solutions to this problem only turned up one garbled digest of a long-defunct mailing list (and an aggregated copy of that).

Unfortunately, if you're ever in this situation, you may have only 1 option: To rewrite your UI to not require QML!  That's one of the first major tranches of what I'll be up to in the New Year!  :(

 

9) You cannot directly pass a QImage to a QML "Image" element and tell it to draw it

Instead, getting image data into QML "Image" elements requires one of several rather roundabout hacks:

1) Re-loading the file data from disk via a filename

2) Writing a "QmlImageProvider" then string-encoding the image data into the "Image.source" parameter so that the image provider will be delegated to handle it

3) Writing a custom QQuickPaintedItem that does the drawing


10) You cannot define your own "ListModel" like model objects for defining a custom data model for automatically populating a UI

This one probably requires a bit more explanation that's best left for a separate post. 

But, the gist of it is that if you try to auto-generate a UI layout from a list of property id's (i.e. targetObject pointer + string-based identifier), and you try to express this using a custom ListModel-like data structure (like the following example), you'll end up running into a whole bunch of weird memory errors / crashes-on-exit / delayed-crashes + general slowness, etc. - and that's if you succeed in getting past the compiler syntax-erroring your attempts first.

The closest you can currently get to this is to do away with the list model, and to directly create the "placeholder widgets" that live as part of the UI Layout (i.e. a "FormLayout", which is really just a dressed up "GridLayout"). These placeholder widgets then use "Loader's" to dynamically show the appropriate delegate for the property whose metadata you are referring to using the pointer + identifier. The main downside of this approach is that you still need to manually specify the labels beside these widgets manually (instead of having them auto-generated from the same list model),


11)  "Data Type Conversion Between QML and C++" - https://doc.qt.io/qt-5/qtqml-cppintegration-data.html

Aaarrrggh! 

 

'Nuff said.

No comments:

Post a Comment