December 8, 2023 by Miłosz Kosobucki (Guest post from KDAB)
Beyond UI: Using Slint with C++
We invited Miłosz Kosobucki to share his team's work at KDAB on how to implement non-UI functionality when writing a C++ application with Slint.
Most applications these days want to communicate over the internet or, in the case of embedded devices, report status over MQTT, for example.
If you're using Slint from C++, you've probably wondered: what should I use to implement these features that are beyond the UI? This is more relevant when the needed functionality requires asynchronous or blocking operations. After all, your application is driven by the Slint event loop:
int main(int argc, char **argv)
{
auto ui = AppWindow::create();
/*...*/
ui->run();
return 0;
}
Waiting on the HTTP response in the main thread would lead to the application's UI freezing.
Some possible solutions are:
- Poll regularly (example with libcurl multi API):
auto curlMultiHandle = curl_multi_init();
//...
slint::Timer requestPollTimer(std::chrono::milliseconds(50), []{
int runningHandles;
CURLMCode mc = curl_multi_perform(curlMultiHandle, &runningHandles)
//...
});
ui->run();
- Move async operations to a separate thread and report results with
slint::invoke_from_event_loop()
Unfortunately, none of the above options are ideal:
- With polling, the application will have to wake up often to make sure that data transfers are handled in a timely manner. This is not good if you're targeting battery-powered devices.
- With a separate thread you will need to handle the complexity of cross-thread communication. Your event loop in the secondary thread will have to wait for readiness of ongoing i/o operations and somehow wake up on signals from the main thread to initiate new transfers.
Can we do it better?
What if we could integrate Slint event loop with asynchronous i/o facilities provided by the operating systems like epoll? If so, then UI events like mouse clicks and keystrokes could be handled together with network socket operations by a single event loop that wakes up only when necessary.
However, as the windowing library winit, that Slint uses, doesn't provide APIs for that, Slint's C++ API doesn't expose any functions to get notifications on your sockets.
But, there is a way!
KDAB's KDUtils library with its KDGui module can do precisely that. And more. It is a cross-platform abstraction for common functionalities needed by GUI applications.
Thanks to Slint's platform API, we can combine KDUtils' facilities for creating windows with Slint and drive the event loop from C++. IIf we also handle the socket/file descriptor activation with this solution, we're home as KDUtils gives us the necessary building blocks:
- Window creation and management
- Keyboard and mouse events
- Timers
- Event loop
- File descriptor notifiers
- Observer pattern and reactive programming primitives with KDBindings
GuiApplication
class that ties all of this together
Plus some other smaller goodies, all exposed in modern C++-17 based APIs. KDUtils supports Windows, Linux, macOS, and Android (note: not all features are available on all platforms).
We, at KDAB, have successfully used KDUtils in several customer and internal projects.
Slint + KDUtils = Mecaps
We started with a sample application that implements Slint's platform API using KDGui and utilizes well-known libraries for other functionality like networking. We called our result Mecaps:
- Modern
- Embedded
- C++
- APplication
- Starter
Although the name has Embedded in it, because of our current focus, Mecaps is relevant for deskop applications too. Here's how it looks right now:
This demo is fully driven by KDUtils event loop interoperating with Slint by implementing the necessary primitives from the platform API. It's available on KDABLabs GitHub page: https://github.com/KDABLabs/mecaps
For starters, we implemented the following features:
- Full integration of Slint Platform API
- A thin wrapper around libcurl using our
FileDescriptorNotifier
class for async notifications about network events: - HTTP Get
- FTP download/upload
- MQTT integration through libmosquitto, also done with async i/o
Our goal is to have a working application that solves common use cases of embedded applications, which we can use as a basis and knowledge base for future projects.
Note: Mecaps is still a prototype. It has many rough edges, bugs, and missing functionality. Please treat it as a proof-of-concept rather than production-grade software.
How does the code look like?
For example, the code to perform an HTTP request and show the response contents in Slint looks as follows:
ApplicationEngine::ApplicationEngine(const slint::ComponentHandle<AppWindow> &appWindow)
{
const auto &networkAccessManager = NetworkAccessManager::instance();
InitHttpDemo(appWindow->global<HttpSingleton>(), networkAccessManager);
//...
}
void ApplicationEngine::InitHttpDemo(const HttpSingleton &httpSingleton, const INetworkAccessManager &networkAccessManager)
{
httpSingleton.set_url("https://example.com");
auto startHttpQuery = [&]() {
const auto url = Url(httpSingleton.get_url().data());
auto *httpTransfer = new HttpTransferHandle(url, true);
httpTransfer->finished.connect([=, &httpSingleton](int result) {
const auto fetchedContent =
(result == CURLcode::CURLE_OK)
? slint::SharedString(httpTransfer->dataRead())
: slint::SharedString("Download failed");
httpSingleton.set_fetched_content(fetchedContent);
delete httpTransfer; // TODO: use deferred deletion once it's
// implemented in KDUtils
});
networkAccessManager.registerTransfer(*httpTransfer);
};
httpSingleton.on_request_http_query(startHttpQuery);
}
httpSingleton
is a Slint global singleton used for communicating with the http related functionality:
export global HttpSingleton {
in-out property<string> url;
in property<string> fetched-content;
callback request-http-query();
}
export component HttpPage inherits Page {
///...
LineEdit {
text <=> HttpSingleton.url;
}
Button {
text: @tr("GO!");
clicked => {
HttpSingleton.request-http-query();
}
}
}
When a button is clicked, the request-http-query
callback is executed which starts the query. We also connect the finished()
signal to the HttpTransferHandle
object to update a field in the UI. Everything happens asynchronously within the same thread.
What's next
We plan to keep improving Mecaps further. In terms of features, we're looking into areas like:
- 3D rendering inside Slint UI using KDGpu
- Using C++20 coroutines for even more pleasant async APIs
- WebSockets support
- Extending KDBindings and KDUtils to have cross-thread signal/slot connections for easier communication with libraries that need to live in separate threads
- Improving developer experience with documentation to KDUtils
We're definitely not going to write major features ourselves. Rather, we want Mecaps to be a catalogue of vetted high-quality open source libraries nicely integrated together. A catalogue that solves common problems of C++ developers building graphical embedded applications.
Interested?
If you're developing a Slint application with C++ and any of the above could be helpful to you, don't hesitate to check out the Mecaps repository at https://github.com/KDABLabs/mecaps or drop us an e-mail at [email protected]
Slint is a Rust-based toolkit for creating reactive and fluent user interfaces across a range of targets, from embedded devices with limited resources to powerful mobile devices and desktop machines. Supporting Android, Windows, Mac, Linux, and bare-metal systems, Slint features an easy-to-learn domain-specific language (DSL) that compiles into native code, optimizing for the target device's capabilities. It facilitates collaboration between designers and developers on shared projects and supports business logic development in Rust, C++, JavaScript, or Python.