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.
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.
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
afterextern 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!");
.set_property("message", &message);
engine
// Load some QML
.load_data("
engine import QtQuick 2.0
import QtQuick.Controls 1.0
ApplicationWindow {
visible: true
Text {
anchors.fill: parent
text: message
}
}
");
.exec();
engine}
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.
Like above, the application can be run with
cargo run
.
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 {
.fill: parent
anchorstext: 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!");
.set_property("message", &text);
engine
// Load some QML
#[cfg(debug_assertions)]
.load_file("src/sousa.qml");
engine#[cfg(not(debug_assertions))]
.load_data(include_str!("sousa.qml"));
engine.exec();
engine}
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.
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> {
: String,
file_name: bool,
is_dir}
}
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 {
.fill: parent
anchorsmodel: dirModel
delegate: Text {
text: file_name
.italic: is_dir
font
}
} }
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();
¤t_dir, &mut dir_model).expect("Could not read directory.");
list_dir(.set_and_store_property("dirModel", &dir_model); engine
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());
.clear();
modelfor entry in entry_iter {
if let Ok(metadata) = entry.metadata() {
if let Ok(file_name) = entry.file_name().into_string() {
.append_item(FileItem {
model: file_name,
file_name: metadata.is_dir(),
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.
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 {
: PathBuf,
current_dir: QDirModel,
model}
Q_OBJECT!{
pub DirLister as QDirLister {
:
signals:
slotsfn 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 {
: dir_model,
model: current_dir.into(),
current_dir};
let q_dir_lister = QDirLister::new(dir_lister);
.set_and_store_property("dirLister", q_dir_lister.get_qobj()); engine
And this is how we use it from QML:
import QtQuick 2.0
import QtQuick.Controls 1.0
ApplicationWindow {visible: true
ListView {
.fill: parent
anchorsmodel: dirModel
delegate: Text {
text: file_name
.italic: is_dir
font
MouseArea {
.fill: parent
anchorscursorShape: is_dir ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
if (is_dir) {
.change_directory(file_name);
dirLister
}
}
}
}
} }
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
.
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.
I promised some assignments. Here they are.
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.
Show the file size in the file listing.
Do not make directories clickable if the user has no permission to open them.
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
By Erich Gubler at Fri, 17 Feb 2017 22:04:11 +0100
A simple Rust GUI with QML
I've commented on that issue here:
https://github.com/White-Oak/qml-rust/issues/31
Success!
By Jos van den Oever at Fri, 17 Feb 2017 23:38:04 +0100
A simple Rust GUI with QML
Great write up!
By Mauricio Uribe (mxuribe) at Thu, 23 Feb 2017 19:19:41 +0100
Advanced Rust GUI with QML?
Thanks for the nice intro to this crate. I played around with the code and finally wanted to try to create a somehow more advanced project.
The idea is to provide a small GUI for an scientific image manipulation software written in Rust. I have coded functions to open and read the image into a buffer, which now I'm trying to display in QML. But here I'm stuck, since I don't know how to get the buffer content to be displayed in a QML Image {}. According to Qt docs I have to provide a QQuickImageProvider http://doc.qt.io/qt-5/qquickimageprovider.html.
Does somebody know if it is possible to achieve this directly in Rust using the qml crate somehow or do I need to add some C++ FFI code?
By Chris at Sun, 26 Feb 2017 16:50:10 +0100