Invoking a C++ function from QML, asynchronously
In the Imaginario code I had written a C++ method
to find all files present in a directory tree (for some reason this code is
always encoding file paths as QUrl
, but feel free to ignore that):
The Utils
mentioned in this snipped is a QObject
-derived class which is
registered to QML as
This allows me to call the C++ findFiles()
method from QML, like this:
So far, so good. However, I couldn't help noticing that when the selected
folder contains a large number of files, the whole UI freezes until the
findUtils()
method has returned. So, how can I invoke my C++ method without
blocking the UI?
QML offers a WorkerScript
element which seems to do exactly what we need, but unfortunately the
possibility of invoking C++ code is only there since Qt 5.12 (before that
version, worker script could not use import
statements), and in any case the
requirement to store the script into a separate Javascript file makes the code
less readable.
A better option, in my opinion, is to write a C++ method that performs the
lengthy task in a thread and invokes a Javascript callback once the job
execution is completed. This sounds pretty complex at first, but fortunately
Qt's nice APIs make this an almost trivial task. Without modifying our previous
findFiles()
method, we write a second one which will execute it in a thread,
using QtConcurrent::run():
You might be surprised, but this is all what is needed in the C++ side. The changes to the QML side are equally trivial:
|
import Imaginario 1.0
|
|
|
|
...
|
|
onClicked: {
|
|
Utils.findFiles(folder, true, function(files) {
|
|
importer.addFiles(files)
|
|
})
|
|
}
|
That's it! Line 6 in this last snippet will be invoked once the findFiles()
method has completed its execution, and you'll be glad to see that the UI will
be responsive throughout the duration of the operation.
QtConcurrent alternatives
QtConcurrent::run()
is very simple to use, but it's not without its
shortcomings, the biggest of which is that it doesn't support cancelling. This
is something I can live with in this particular case, but, if you can't, don't
despair: there are other options. The second simplest one is probably the
static QThread::create()
method, which behaves similarly
to QtConcurrent::run()
but returns a QThread
object which can be requested
to terminate, for example with
QThread::requestInterruption().
This method exists only since Qt 5.10, and that's why I didn't list it as my
first choice; but if you are using such a recent version of Qt, then it's
probably the best option, because one doesn't even need to subclass
QThread
(and there's no need to depend on QtConcurrent). Of course,
subclassing QThread is also an option, but this involves a little more typing.
Beware of threading issues
The most tempting solution (and I confess, my first approach to the problem was exactly this) is to invoke the Javascript callback directly from the thread:
However, this will work 95% of the times only, because QJSValue::call()
is
not thread-safe, and therefor your application will be victim of random
crashes. So, we need to write a couple of lines more and invoke the callback
from the main thread, like I'm doing before.
Commentos
There's also webmention support.