A simple Rust GUI with QML

2017-02-17

You may have heard of Rust by now. The new programming language that "pursuis the trifecta: safety, concurrency, and speed". You have to admit, even if you don't know what trifecta means, it sounds exciting.

I've been toying with Rust for a while and have given a presentation at QtCon comparing C++ and Rust. I've been meaning to turn that presentation into a blog post. This is not that blog post.

Here I show how you can use QML and Rust together to create graphical applications with elegant code. The example we're building is a very simple file browser. People that are familiar with Rust can ogle and admire the QML snippets. If you're a Qt and QML veteran, I'm sure you can read the Rust snippets here quite well. And if you're new to both QML and Rust, you can learn twice as much.

The example here is kept simple and poor in features intentionally. At the end, I'll give suggestions for simple improvements that you can make as an exercise. The code is available as a nice tarball and in a git repo.

Command-line Hello, world!

First we set up the project. We will need to have QML and Rust installed. If you do not have those yet, just continue reading this post and you'll be all the more motivated to go ahead and install them.

Once those two are installed, we can create a new project with Rust's package manager and build tool cargo.

[~/src]$ # Create a new project called sousa (it's a kind of dolphin ;-)
[~/src]$ cargo new --bin sousa
     Created binary (application) `sousa` project

[~/src]$ cd sousa

[~/src/sousa]$ # Add a dependency for the QML bindings version 0.0.9
[~/src/sousa]$ echo 'qml = "0.0.9"' >> Cargo.toml
[~/src/sousa]$ # Build, this will download and compile dependencies and the project.
[~/src/sousa]$ cargo build
    Updating registry `https://github.com/rust-lang/crates.io-index`
   Compiling libc v0.2.20
   Compiling qml v0.0.9
   Compiling lazy_static v0.2.2
   Compiling sousa v0.1.0 (file:///home/oever/src/sousa)
    Finished debug [unoptimized + debuginfo] target(s) in 25.39 secs

[~/src/sousa]$ # Run the program.
[~/src/sousa]$ cargo run
    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/sousa`
Hello, world!

The same without output:

cargo new --bin sousa
cd sousa
echo 'qml = "0.0.9"' >> Cargo.toml
cargo build
cargo run

The mix of Rust and QML lives! Of course the program is not using any QML yet. Let's fix that.

Hello, world! with QML

Now that we have a starting point we can start adding some QML. Let's change src/main.rs from a command-line Hello, world to a graphical Hello, world! application.

main.rs before

fn main() {
    println!("Hello, world!");
}

Some explanation for the people reading Rust code for the first time: things that look like functions but have a name that ends with ! are macros. Forget everything you know about C/C++ macros. Macros in Rust are elegant and powerful. We will see this below when we mock moc.

main.rs after

extern crate qml;

use qml::*;

fn main() {
    // Create a new QML Engine.
    let mut engine = QmlEngine::new();

    // Bind a message to the QML enviroment.
    let message = QVariant::from("Hello, world!");
    engine.set_property("message", &message);

    // Load some QML
    engine.load_data("
        import QtQuick 2.0
        import QtQuick.Controls 1.0

        ApplicationWindow {
            visible: true
            Text {
                anchors.fill: parent
                text: message
            }
        }
    ");
    engine.exec();
}

Modules in Rust are called "crates". This example uses QML bindings that currently have version number 0.0.9. So the API may change.

In the example above, the QML is placed literally in the code. Literal strings in Rust can span multiple lines.

Usually you do not need to specify the type of a variable, you can just type let (for immutable objects) or let mut for mutable ones. Like in C++, & is used to pass an object by reference. You have to use the & in the function definition, but also when calling the function (unless your variable is a reference already).

The QML code has an ApplicationWindow with a Text. The message, Hello, world! is passed to the QML environment as a QVariant. This is the first time in our program that information goes between Rust and QML.

Hello, world!

Hello, world!

Like above, the application can be run with cargo run.

Splitting the code

Let's make this code a bit more maintainable. The QML is moved to a separate file src/sousa.qml which we load from Rust.

import QtQuick 2.0
import QtQuick.Controls 1.0

ApplicationWindow {
    visible: true
    Text {
       anchors.fill: parent
       text: message
    }
}

You can see the adapted Rust code below. In debug mode, the file is read from the file system. In release mode, the file is embedded into the executable to make deployment easier.

extern crate qml;

use qml::*;

fn main() {
    // Create a new QML Engine.
    let mut engine = QmlEngine::new();

    // Bind a message to the QML enviroment.
    let text = QVariant::from("Hello, world!");
    engine.set_property("message", &text);

    // Load some QML
#[cfg(debug_assertions)]
    engine.load_file("src/sousa.qml");
#[cfg(not(debug_assertions))]
    engine.load_data(include_str!("sousa.qml"));
    engine.exec();
}

The statements #[cfg(debug_assertions)] and #[cfg(not(debug_assertions))] are conditional compilation for the next expression. So when you run cargo run, the QML file will be read from disk and with cargo run --release, the QML will be inside the executable. In debug mode it is convenient to avoid recompilation for changes to the QML code.

Listing the contents of a folder

Now that we've created an application that combines Rust and QML let's go a step further and list the contents of a directory instead of a simple message.

QML has a ListView that can display the contents of a ListModel. The ListModel can be filled by the Rust code. First we create a simple Rust structure that contains information about files.

Q_LISTMODEL_ITEM!{
    pub QDirModel<FileItem> {
        file_name: String,
        is_dir: bool,
    }
}

Q_LISTMODEL_ITEM! ends on a !, so it's a macro. Rust macros use pattern matching on the content of a macro. The matched values are used to generate code. The macro system is not unlike C++ templates, but with a more flexible sytax and simpler rules.

On the QML side, we'd like to show the file names. Directory names should be shown in italic.

ApplicationWindow {
    visible: true

    ListView {
        anchors.fill: parent
        model: dirModel
        delegate: Text {
            text: file_name
            font.italic: is_dir
        }
    }
}

The ListView shows data from a ListModel that we'll define later.

The delegate in the ListView is a kind of template. When an entry in the list is visible in the UI, the delegate is the UI component that shows that entry. The delegate that is shown here is very simple. It is just a Text that shows the file name.

Next, we need to connect the information on the file_system to the model. That is done in two steps.

Instead of binding a Hello, world! message to the QML environment, we create an instance of our QDirModel and bind it to the QML environment.

    // Create a model with files.
    let dir_str = ".";
    let current_dir = fs::canonicalize(dir_str)
        .expect(&format!("Could not canonicalize {}.", dir_str));
    let mut dir_model = QDirModel::new();
    list_dir(&current_dir, &mut dir_model).expect("Could not read directory.");
    engine.set_and_store_property("dirModel", &dir_model);

The model is initialized with the current directory. That directory is canonicalized. That means it is made absolute and symbolic links are resolved. This function may fail and Rust forces us to deal with that. If there is an error in fs::canonicalize(dir_str), the returned result is an error instead of a value. The function expect() takes the error and an additional message, prints it and stops the current thread or program in a controlled way. Rust is a safe programming language because of features like this where potential problems are prevented at compile-time.

The last missing piece is the function list_dir that reads entries in a directory and places them in the QDirModel.

fn list_dir(dir: &Path, model: &mut QDirModel) -> io::Result<()> {
    // get iterator over readable entries
    let entry_iter = fs::read_dir(dir)?.filter_map(|e| e.ok());

    model.clear();
    for entry in entry_iter {
        if let Ok(metadata) = entry.metadata() {
            if let Ok(file_name) = entry.file_name().into_string() {
                model.append_item(FileItem {
                    file_name: file_name,
                    is_dir: metadata.is_dir(),
                });
            }
        }
    }
    Ok(())
}

There is a lot happening in the first line of this function. An iterator is taken over the contents of a directory. If the reading of the directory fails, the function stops and returns an Err. This is coded by the ? in fs::read_dir(dir)?. When reading each entry, another error may occur. If that happens the iterator returns an Err. We choose here to skip over the erroneous reads; we filter them out with filter_map(|e| e.ok()).

Next, the entries are added to the model in a for loop. Again we see code that deals with possible errors. Reading the metadata for a file may give an error. We choose to skip entries with such errors. Only the entries for which Ok is returned are handled.

The UI should display the file name. Rust uses UTF-8 internally and the file name can be be nearly any sequence of bytes. If the entry is not a valid UTF-8 string, we ignore that entry here. Another option would be to keep the byte array (Vec<u8>) and use a lossy representation of the file name in the user interface that leaves out the parts that cannot be represented in UTF-8.

In other programming languages, it'd be easier to handle these cases sloppily. In Rust we have to be explicit. This explicit code is safer and more understandable for the next programmer reading it.

And here is the result of cargo run. A directory listing with two files and two folders.

a listing of files

a listing of files

A simple file browser

Listing only one fixed directory is no fun. We want to navigate to other directories by clicking on them. We'd like to have an object that can receive the name of a folder that it should enter and update the directory listing.

To achieve that we need a staple from the Qt stable: QObject. A QObject can send signals and receive signals. Signals are received in slots. When programming in C++, a special step is needed during compilation: the program moc generates code from the C++ headers.

Thanks to macroergonomics, Rust has more powerful macros and can skip this extra step. The syntax to define a QObject is simple in Rust and C++. This is our QDirLister:

pub struct DirLister {
    current_dir: PathBuf,
    model: QDirModel,
}
Q_OBJECT!{
    pub DirLister as QDirLister {
        signals:
        slots:
            fn change_directory(dir_name: String);
        properties:
    }
}

The macro Q_OBJECT takes the struct DirLister and wraps it in another struct QDirLister that has signals, slots and properties.

Our simple QDirLister defines only one slot, change_directory, that will receive signals from the QML code when a directory name is clicked. Here is the implementation:

impl QDirLister {
    fn change_directory(&mut self, dir_name: String) -> Option<&QVariant> {
        let new_dir = if dir_name == ".." {
            // go to parent if there is a parent
            self.current_dir.parent().unwrap_or(&self.current_dir).to_owned()
        } else {
            self.current_dir.join(dir_name)
        }; 
        if let Err(err) = list_dir(&new_dir, &mut self.model) {
            println!("Error listing {}: {}",
                     self.current_dir.to_string_lossy(),
                     err));
            return None;
        }
        // listing the directory succeeded so update the current_dir
        self.current_dir = new_dir;
        None
    }
}

If the directory is .., we move up one directory with parent(). Again we have to explicitly handle the case that there is no parent directory. We choose to stay on the same directory in that case.

If the directory is not .., we join() the directory name to the current_dir. We update the model with a new directory listing and print an error and stay on the current directory if that fails.

QDirLister has to be hooked up to the QML code. We add this snippet to the fn main() that we defined earlier.

    // Create a DirLister and pass it to QML
    let dir_lister = DirLister {
        model: dir_model,
        current_dir: current_dir.into(),
    };
    let q_dir_lister = QDirLister::new(dir_lister);
    engine.set_and_store_property("dirLister", q_dir_lister.get_qobj());

And this is how we use it from QML:

import QtQuick 2.0
import QtQuick.Controls 1.0

ApplicationWindow {
    visible: true

    ListView {
        anchors.fill: parent
        model: dirModel
        delegate: Text {
            text: file_name
            font.italic: is_dir

            MouseArea {
                anchors.fill: parent
                cursorShape: is_dir ? Qt.PointingHandCursor : Qt.ArrowCursor
                onClicked: {
                    if (is_dir) {
                        dirLister.change_directory(file_name);
                    }
                }
            }
        }
    }
}

To receive mouse input in QML, there needs to be a MouseArea. When it is clicked (onClicked), it calls a bit of Javascript that sends the file_name to the dirLister via the slot change_directory.

our file browser

our file browser

Conclusion

Hooking up QML and Rust is elegant. We've created a simple file browser with one QML file, sousa.qml, one Rust file, main.rs and one package/build file Cargo.toml.

There are many nice QML user interfaces out there that can be repurposed on top of Rust code. QML can be visually edited with QtCreator. QML can be used for mobile and desktop applications. It's very nice that this wonderful method of creating user interfaces can be used with Rust.

To the C++ programmers: I hope you enjoyed the Rust code and find some inspiration from it. Because Rust is a new language it can introduce innovative features that cannot be easily added to C++. Rust and C++ can be mixed in one codebase as is done in Firefox.

Rust has many more wonderful features than can be covered in this blog. You can read more in the Rust book.

Assignments

I promised some assignments. Here they are.

  1. Show an error dialog when a directory cannot be shown. (Hint: the code is already in the git repo and shows a QML feature that we did not use yet: signals.

  2. Show the file size in the file listing.

  3. Do not make directories clickable if the user has no permission to open them.

  4. Open simple files like pictures and text files when clicked by showing them in a separate pane.

Comments

Post a comment

Can't get this to build!

Can't get this to build on Windows 10 right now after installing Qt 5.8. Submitted an issue here:

https://gitlab.com/vandenoever/sousa/issues/1

Reply to this comment

A simple Rust GUI with QML

I've commented on that issue here:

https://github.com/White-Oak/qml-rust/issues/31

Success!

Reply to this comment