DEV Community

Ayush
Ayush

Posted on • Updated on

Writing a QML Application in Rust (Part 1)

Introduction

I have been a KDE user for a long time and have been getting into contributing to KDE. I also love Rust Programming Language. With the release of GTK4, the Rust bindings for GTK have become official and are pretty great to use. However, Qt bindings for Rust are still relatively limited or not practical. So I decided to improve the situation as much as I could instead of just complaining since I have a lot of time.
I also have a few applications I would like to create, so this works out perfectly. I am not sure how long this blog series will be, but it might be a long one (unless I give up).

Application

The application can be found here. It is supposed to be a tracker for things like Visual Novels, Anime and Manga. Maybe it will start including other regular trackers at some point, but that is not the goal. Also, the application should look native in KDE.

Goals

  1. Assess and improve the state of writing QML applications in Rust.
  2. Create general purpose rust bindings for the KDE frameworks as needed.
  3. Learn more about QT and KDE development.
  4. Package as Flatpak

Initial Setup

Setup QMAKE in Fedora

Qtmetaobject.rs use Qmake for searching for qt install and other things. qmake is available as qmake-qt5 in Fedora, so we need to set the environment variable. I just put it in my .zshrc as:

export QMAKE=/usr/bin/qmake-qt5
Enter fullscreen mode Exit fullscreen mode

System Dependencies

Since I already have most qt5 dependencies in my system, I am unsure what needs to be installed on a new system. I will update this section if I start fresh again.

Rust Setup

Create Project

This step is as simple as running:

cargo new weebtk
Enter fullscreen mode Exit fullscreen mode

Adding Dependencies

Add the following in the Cargo.toml file:

[dependencies]
qmetaobject = "0.2"
cstr = "0.2"
Enter fullscreen mode Exit fullscreen mode

Add Starter Code

This is taken directly from qtmetaobject-rs Readme:

use cstr::cstr;
use qmetaobject::prelude::*;

// The `QObject` custom derive macro allows to expose a class to Qt and QML
#[derive(QObject, Default)]
struct Greeter {
    // Specify the base class with the qt_base_class macro
    base: qt_base_class!(trait QObject),
    // Declare `name` as a property usable from Qt
    name: qt_property!(QString; NOTIFY name_changed),
    // Declare a signal
    name_changed: qt_signal!(),
    // And even a slot
    compute_greetings: qt_method!(fn compute_greetings(&self, verb: String) -> QString {
        format!("{} {}", verb, self.name.to_string()).into()
    })
}

fn main() {
    // Register the `Greeter` struct to QML
    qml_register_type::<Greeter>(cstr!("Greeter"), 1, 0, cstr!("Greeter"));
    // Create a QML engine from rust
    let mut engine = QmlEngine::new();
    // (Here the QML code is inline, but one can also load from a file)
    engine.load_data(r#"
        import QtQuick 2.6
        import QtQuick.Window 2.0
        // Import our Rust classes
        import Greeter 1.0

        Window {
            visible: true
            // Instantiate the rust struct
            Greeter {
                id: greeter;
                // Set a property
                name: "World"
            }
            Text {
                anchors.centerIn: parent
                // Call a method
                text: greeter.compute_greetings("hello")
            }
        }
    "#.into());
    engine.exec();
}
Enter fullscreen mode Exit fullscreen mode

We can run the project now but I would like to add one more step to this post.

Extract Qml to ui/main.qml

Qml files can be rendered separately and have imperative programming capabilities. So it stands to reason that almost every code will separate the qml and rust code. While it is possible to directly load file from path, I decided to add instructions on how to use the Qt resource system.

  1. First create the file ui/main.qml at the project root (same level as src) with the qml contents we were loading earlier:
import QtQuick 2.6
import QtQuick.Window 2.0
// Import our Rust classes
import Greeter 1.0

Window {
    visible: true
    // Instantiate the rust struct
    Greeter {
        id: greeter;
        // Set a property
        name: "World"
    }
    Text {
        anchors.centerIn: parent
        // Call a method
        text: greeter.compute_greetings("hello")
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Modify main.rs:
use cstr::cstr;
use qmetaobject::prelude::*;
use qmetaobject::QUrl;

// The `QObject` custom derive macro allows to expose a class to Qt and QML
#[derive(QObject, Default)]
struct Greeter {
    // Specify the base class with the qt_base_class macro
    base: qt_base_class!(trait QObject),
    // Declare `name` as a property usable from Qt
    name: qt_property!(QString; NOTIFY name_changed),
    // Declare a signal
    name_changed: qt_signal!(),
    // And even a slot
    compute_greetings: qt_method!(fn compute_greetings(&self, verb: String) -> QString {
        format!("{} {}", verb, self.name.to_string()).into()
    })
}

qrc!(root_qml,
    "" {
        "ui/main.qml" as "main.qml",
    }
);

fn main() {
    // Register the `Greeter` struct to QML
    qml_register_type::<Greeter>(cstr!("Greeter"), 1, 0, cstr!("Greeter"));
    // Create a QML engine from rust
    let mut engine = QmlEngine::new();

    // Load Resource
    root_qml();

    engine.load_url(QUrl::from(QString::from("qrc:///main.qml")));
    engine.exec();
}
Enter fullscreen mode Exit fullscreen mode

Run the Project

Finally, we can run the project using:

cargo run
Enter fullscreen mode Exit fullscreen mode

If everything goes well, you should see a sample application with "hello World".

Conclusion

This should be enough to get everything setup for actually starting with creating the application. In the next section, I will probably take a look at setting up the logging side of things since I find that logging is extremely important but can be forgotten since it is mostly already setup.

Top comments (0)