Rant : QML Horrors
I decided to make a Stock Photo Selector with QT Widgets.
Then, after quickly understanding how the QT Widgets system is old
and a pain to deal with, when dealing with web resources, I decided
to go the QML route.
The thing with QML is that you can quickly put an interface in place, add some code and then… When you try to architecture the whole program, things get complicated.
The idea
So, the whole concept of the Stock Photo Selector is to test Stock Photos on websites quickly and efficiently. There’s two objectives :
- Be able to test the Stock Photos on PC and mobiles
- Be able to save the right Stock Photo in the right folder of the
website when done.
The first part can be done with simple Javascript, the second part however, not so much.
So, I went like “How about I create a search engine app with QT. It will search the photo. Once I double-click a photo, it sends it to the browser through a WebSocket, which then put it on the right place. Then when I got the right image, I just click ‘Save on server’ in the app and boom, done !“.
Sure, there’s still parts that are not clearly decided yet.
But I don’t care, because I want to test if the whole WebSocket
communication is possible, in the first place.
Dealing with QML
QT Creator is almost useless with QML. Seriously.
The only thing that “kind of matter” is the Designer.
The IDE can really kick in when adding C++ helpers. But that’s it.
For the rest, the IDE is USELESS. It provides almost no useful
information, and can sometimes even provide idiotic completion or
add things that are not understood by the QML parser.
I won’t even talk about the Designer crashing here and there, when the interface gets “too complicated”, and how the categorization of the wigets (?) make no sense most of the time.
In terms of “Debugging”, we’re far from In-Browser Javascript or Electron (kind of the same thing).
First, QML is an UI descriptive language. You can describe your UI in a human-readable manner. Something like this :
Window {
id: window
visible: true
width: 640
height: 480
title: qsTr("Hello World")
MyyToolbar {
id: toolbar
x: 0
anchors.bottom: parent.bottom
searchButton.onClicked: {
var http = new XMLHttpRequest()
var url = "https://pixabay.com/api/?image_type=photo&q=" + query.text + "&key=" + config.pixabay_key;
http.open("GET", url, true);
http.onreadystatechange = function() {
if (http.readyState === 4) {
if (http.status === 200) {
results.addPixabayResults(JSON.parse(http.responseText))
console.debug(http.responseText);
}
else {
console.debug("XC")
}
}
}
http.send();
}
}
}
However, trying to debug UI problems quickly become a pain in the ass, if you’re not accustomed to the weird QT Creator UI.
By default, the debugger is UNABLE to provide you a good view of the layout tree. If you click on “Pause” during the execution, you’ll see this :
>-app
|-argc
>-argv
>-engine
>-url
When I saw this I was like “??? So… ? app
maybe ? Let’s see…”
v-app
| >-[d]
| >-[parent]
| v-[children]
| | >-[0]
| | >-[1]
| | >-[2]
| | >-[3]
| | >-[4]
| | >-[5]
| | >-[6]
| | >-[7]
...
Then I was like “Whaaat ?”
I then discovered on the debugging view, you have your main code on the center, the locals on the right, and the stack trace pane below. In the toolbar of the stack trace pane, you have :
- a menu Debugger ▼ which basically helps you swap this pane place with another pane
- a menu GDB for “NameOfYourProject” ▼ which dictate which debugger you want to choose.
Turns out that, here, you want to click on
GDB for “NameOfYourProject” ▼, then select
QML for “NameOfYourProject” in the dropdown menu.
THEN, you’ll be able to see something that looks like this :
v-QQmlApplicationEngine
>-Properties
>-QTranslator
>-QQmlFileSelector
>-QQuickIcon
>-QQuickFontValueType
>-QQuickPalette
>-window
If you expand the window
list, you’ll see all the children of your
view, with all its properties and such.
Which brings my first question :
WHY IS QML NOT SELECTED BY DEFAULT, DURING QML APPLICATION DEBUGGING !?
I hate the whole “Where’s Wally” UI game. Having to specifically
click ONE element of a pane which seems to only dispay stack traces
(with thread selection, step by step debugging, assembler debugging,
…), in order to see my UI elements is madness.
I kind of understand the mental process that went behind it “Well, to
select which locals you want to display, you’ll have to select the
right thread in the stack trace pane, so we’ll put the menu there so
that they can switch between C++ locals and QML/Javascript locals”
but… ugh… it’s still unintuitive.
I’d highly prefer a tool that’s specifically oriented towards UI
debugging.
If you programmed with Android, you know what I’m talking about.
I’m talking about a panel where you can see all the currently
instantiated UI elements.
Following the previous example, such a panel would look like this :
Window
⌞ MyyToolbar
⌞ TextField
⌞ ComboBox
⌞ Button
Then when you click on an element, it selects it in the program and you can view its properties. Like its position, width, height, z-index, …
That kind of thing helps TREMENDOUSLY in trying to determine why something is not displayed correctly.
But well, with QT Creator, you don’t have this.
But wait, there’s more !
Dimensions hell
So, in my app, I was able to display the thumbnails of the Pixabay’s queries results in a Grid, and I was able to trigger the download of a decent resolution version of a photo when clicking on it…
“Wow, you were able to display images in a Grid and add an OnClickListener ! Must have been really hard ! /s”
Well, YES ! IT WAS !
Due to some to mystic positioning and size rules on the Image
widget,
I lost an entire day figuring out how to add an “Click” listener on a
freaking Image widget !
By default, in my case, the Image
widget would start with no width
and height, because it changes based on the width and height of the
thumbnail downloaded.
So, when adding a Mouse Area as a child, the MouseArea
starts with a
width and height of 0.
And even with anchors.fill
, changing the width and height properties
of Image will NOT change the properties of the MouseArea
child.
Because… REASONS !
Trying to put a height/width on the Image
on start also didn’t work.
However ! When doing something like this :
Rectangle {
Image { anchors.fill: parent }
MouseArea { anchors.fill: parent }
}
It works !
Turns out that updating the Rectangle
dimensions, when displaying the
thumbnail in Image
, makes the MouseArea
redimension itself
correctly and the whole things start working.
YAY ! I just lost an entire day !
Broken WebSocket and useless Javascript debugger
So, the pictures are displayed correctly (but are still going over the
toolbar for reasons I don’t get !… ugh… z-index maybe ?), I can
click on them :
GOOD !
Now, what’s remaining is downloading them and sending them through WebSocket.
The setup of the WebSocket server was impressively easy ! Almost no fuss at all. It just worked as intended.
So, I found a way to save the WebSocket object provided during the
client connection. Then, when clicking on a thumbnail, I triggered
an HTTP Request with a good old XMLHttpRequest
object, and when
receiving the response, I did :
ws.sendBinaryMessage(http.response)
On the browser side, I first tried to rush and do something like this :
if (message.constructor === Blob) {
var reader = new FileReader();
reader.readAsDataURL(message);
reader.onloadend = function() {
var base64data = reader.result.substring("data:application/octet-stream;base64,".length);
console.log(base64data);
const mimed_data = "url('data:image/jpg;base64," + base64data + "')";
document.querySelector("#wss_image").style.backgroundImage = mimed_data;
}
}
And… it didn’t work…
Hmm… I tried to copy the base64 to a file and convert it back, to see how it went.
Apparté
Now, let me just say how most of today apps can’t deal with large block of text. KDE apps being on the worst offender side.
In Firefox, the UI was almost to a crawl when trying to select the large block of Base64 text representing the picture.
Seriously, I tried to save it in Kate : Nope. I tried to save it in KWrite : Nope. Then, I launched notepad through Wine. YES, seriously !
IT WORKED !
But it was sluggish as hell…
Then I brought emacs and did the operation in 3 seconds.
What’s the spec of the machine doing this already ? Hmm ?
Oh, it’s just an 8 core / 16 threads AMD Ryzen 7 2700X with 32GB of RAM
and NVMe disks. So it’s normal if it can’t copy 2MB of text…
KDE apps are so well optimised.
Back on track
I saved the base64 block in a text file, decoded it with a simple Ruby script :
require "base64"
File.write("decoded.jpg", Base64.decode64(File.read("encoded_image.txt")))
Then downloaded the actual picture on Firefox, saved it and compared the size… ALMOST TWICE THE SIZE !?… WHAT ?
Ok…
Can I see the headers ?
ef bf bd ef bf bd ef bf bd ef bf bd 00 10 4a 46 49 46
4a 46 49 46
is JFIF. But what’s that ef bf bd
thing ?
Let’s compare it with a valid JPG header.
ff d8 ff e0 00 10 4a 46 49 46
… ??? Okay ??? Where’s the ff d8 ff
sequence in the
converted version ? Did it convert it ?
If I do a websearch for ef bf bd ef
, maybe I’ll get a
clue about what’s going on…
… It’s an UTF-8 to Latin-1 decoding issue … !!?
YOU’RE FUCKING KIDDING ME ?
I TELL THE FUCKING SOFTWARE TO SEND EVERYTHING IN BINARY, AND IT DOES AN UTF-8 TO LATIN-1 CONVERSION ON BINARY DATA !?
WHAAT ?
Wait, maybe that’s Firefox ? Maybe I didn’t setup the whole
<meta charset="utf8">
correctly and it starts to convert data.
Let’s bring Wireshark, listen on lo
, redo the whole operation;
NO, OKAY, IT’S QML !
QML, WHAT THE FUCK ! WHEN I SEND A BINARY MESSAGE, IT’S A BINARY MESSAGE ! DON’T TOUCH IT ! JUST SEND THE DATA ‘AS-IS’ !
For fuck sake, this reminds me the whole bullshit that reading
files is on Windows. If you don’t open files in “binary” mode on
Windows, lonely \x0d
and \x0a
are automatically converted into
\x0d\x0a
sequences, for archaic Mac OS and UNIX compatability
reasons, botching up binary files to no end.
Let’s bring up the debugger
Back to the QML code, I put a breakpoint just below
if (http.readyState === 4 && http.status === 200) {
in the
onreadystatechange
callback of the ‘XMLHttpRequest’ used for the
request.
Basically, it looks like this :
http.onreadystatechange = function() {
if (http.readyState === 4 && http.status === 200) {
ws.sendBinaryMessage(http.response);
console.debug(typeof(http.response))
console.debug(http.response.length);
console.debug(http.responseText.length);
console.debug("Got : " + picture.data["largeImageURL"]);
}
}
You’d be surprised to know that the size provided by
http.response.length
and http.responseText.length
are identical
and they also also seem wrong.
They’re down a few kilo-bytes compared to the original image…
I don’t know why.
But it’s way different from the size of the element sent via
sendBinaryMessage
which inflates the size by almost 2 times.
So I put a breakpoint, run the code with the debugger, the debugger stops and in the locals, all I can see is this :
|-this
v-http
| ⌞onreadystatechange
|-onClicked
v-mouse
⌞ ...
So this
is empty. http
has one field onreadystatechange
and…
that’s it. onClicked
is empty and mouse
has fields corresponding
the pointer properties when clicking the area.
The locals pane is USELESS to me, here.
Sure I can test a few things in the QML console, but that doesn’t
help me that much.
Ok, how about Step into
, maybe I can see the code of that
sendBinaryMessage
thing… it just jumped onto the next
console.debug
line…
How stupid this thing can be ?
Switching back to GDB for … shows nothing…
Ugh… so I have zero idea about :
- Why did it do a conversion ?
- How to prevent it ?
I LOVE to code with QML ?
The idea now is to add some C++ helpers, hoping that the C++ part is
not AS broken AS the Javascript part.
Worse part, I’ll have to resort to an external WebSocket C/C++ library,
use it in C++ helpers and hope that it does the trick.