Adrianistán

Crear ventanas y botones en Rust con GTK

29/09/2017
Cuando salió el Macintosh allá por 1984, fue novedosa su interfaz gráfica. En efecto, fue la primera interfaz gráfica popular y ha servido de inspiración para muchos otros sistemas de ventanas.

Hoy vamos a introducir una manera de crear interfaces gráficas de usuario (GUI) con Rust. Para ello usaremos GTK. GTK funciona sobre GNU/Linux, macOS y Windows (aunque es un poco más complicado de lo que debiera).

Para ello usaremos la fantástica crate gtk del proyecto Gtk-rs.

Instalando gtk


Añade al fichero Cargo.toml las siguiente líneas:


[dependencies]
gtk = "0.2.0"


Y ejecuta cargo build.

Creando una ventana


Lo primero que vamos a hacer es crear una ventana.

Importamos gtk e iniciamos GTK.


extern crate gtk;

use gtk::prelude::*;

fn main() {
if gtk::init().is_err() {
println!("Failed to initialize GTK.");
return;
}


Ahora podemos crear una ventana. GtkWindow es un struct con métodos asociados, por lo que podemos ajustar ciertos parámetros. En este código parece que estamos usando orientación a objetos, pero recuerdo que Rust técnicamente no tiene clases ni herencia.


let window = gtk::Window::new(gtk::WindowType::Toplevel);

window.set_title("Adrianistan - GTK - Rust");
window.set_border_width(10);
window.set_position(gtk::WindowPosition::Center);
window.set_default_size(350, 70);


Ahora vamos a añadir un evento para que cuando se pulse la X en nuestra ventana, se cierre el programa. Para ello tenemos que entender como funciona GTK. Cuando programamos en GTK lo que hacemos es configurar la aplicación y posteriormente ceder la ejecución a una función gtk_main que procesa todos los eventos según haya sido configurado. Para configurar los eventos usaremos callbacks, que en Rust se pueden implementar con closures. Las funciones que en GTK nos permiten conectar eventos siempre tienen el prefijo connect.


window.connect_delete_event(|_, _| {
gtk::main_quit();
Inhibit(false)
});


Ahora vamos a mostrar la ventana. Por defecto en GTK todos los widgets (todo lo que se muestra en pantalla) está oculto. Para mostrar todos los widgets que se han añadido a la ventana se suele usar show_all, que va a haciendo show de forma recursiva. Por último, le damos el control de la aplicación a gtk::main.


window.show_all();
gtk::main();
}


Una vez hecho esto, si compilamos con cargo run ya deberíamos ver una preciosa ventana GTK.

Y por supuesto, si pulsamos la X, la aplicación se cierra.

Layouts y botones en GTK


Vamos ahora a añadir dos cosas: un botón y un label que nos de un número del dado. Para poner varios elementos en una aplicación GTK es recomendable usar un layout. Voy a usar el layout Box, que permite agrupar los widgets de forma vertical u horizontal. Para los números aleatorios voy a usar la crate rand. El código final sería así:


extern crate gtk;
extern crate rand;

use gtk::prelude::*;
use rand::distributions::{IndependentSample, Range};

fn pick(a: i32, b: i32) -> i32 {
let between = Range::new(a, b);
let mut rng = rand::thread_rng();
between.ind_sample(&mut rng)
}

fn main() {
if gtk::init().is_err() {
println!("Failed to initialize GTK.");
return;
}

let window = gtk::Window::new(gtk::WindowType::Toplevel);

window.set_title("Adrianistán - GTK - Rust");
window.set_border_width(10);
window.set_position(gtk::WindowPosition::Center);
window.set_default_size(350, 70);

window.connect_delete_event(|_, _| {
gtk::main_quit();
Inhibit(false)
});

let vbox = gtk::Box::new(gtk::Orientation::Vertical,10);

let button = gtk::Button::new_with_label("Tirar el dado");

let label = gtk::Label::new("No has tirado el dado todavía");

let l = label.clone();
button.connect_clicked(move |_| {
let dado = pick(1,7);
let text: String = format!("Dado: {}",dado);
l.set_text(text.as_str());
});

vbox.add(&button);
vbox.add(&label);
window.add(&vbox);

window.show_all();
gtk::main();
}


Aquí pasan varias cosas interesantes. La primera es que usamos add para ir añadiendo de forma jerárquica los widgets. Debajo de Window está Box y debajo de Box tenemos Button y Label.

Por otra parte, vemos que la función asociada al evento del click de los botones es connect_clicked. Bien, en este ejemplo he introducido algo importante. Estoy modificando un widget desde un evento relacionado a otro widget. ¿Esto como se lleva con las reglas de propiedad/ownership de Rust? Bastante mal. Rust no puede saber si cuando se ejecuta el evento tenemos acceso al widget en cuestión que vamos a modificar. Afortunadamente, la API de gtk-rs ha sido diseñada con esto en cuenta y simplemente podemos hacer una llamada a clone para obtener otra referencia al objeto, que podemos pasar al closure (con move). Este clonado no lo es tal, sino que hace uso de Rc. Simplemente se nos presenta de forma transparente.

El ejemplo final: dibujando con Cairo


En este último ejemplo voy a añadir un widget donde se podrá ver la cara del dado que ha salido. Para ello uso un GtkDrawingArea, que permite usar la API de Cairo.


extern crate gtk;
extern crate rand;

use gtk::prelude::*;
use rand::distributions::{IndependentSample, Range};
use std::rc::Rc;
use std::cell::Cell;

fn pick(a: i32, b: i32) -> i32 {
let between = Range::new(a, b);
let mut rng = rand::thread_rng();
between.ind_sample(&mut rng)
}

fn main() {
if gtk::init().is_err() {
println!("Failed to initialize GTK.");
return;
}

let window = gtk::Window::new(gtk::WindowType::Toplevel);

window.set_title("Adrianistán - GTK - Rust");
window.set_border_width(10);
window.set_position(gtk::WindowPosition::Center);
window.set_default_size(350, 70);

window.connect_delete_event(|_, _| {
gtk::main_quit();
Inhibit(false)
});

let vbox = gtk::Box::new(gtk::Orientation::Vertical,10);

let button = gtk::Button::new_with_label("Tirar el dado");

let label = gtk::Label::new("No has tirado el dado todavía");

let r = Rc::new(Cell::new(0));

let random = r.clone();
let drawingarea = gtk::DrawingArea::new();
drawingarea.set_size_request(300,300);
drawingarea.connect_draw(move |widget,cr|{
let width: f64 = widget.get_allocated_width() as f64;
let height: f64 = widget.get_allocated_height() as f64;
cr.rectangle(0.0,0.0,width,height);
cr.set_source_rgb(1.0,1.0,1.0);
cr.fill();

cr.set_source_rgb(0.,0.,0.);
let random = random.get();
if random == 1 || random == 3 || random == 5{
cr.arc(width/2.0,height/2.,height/10.,0.0,2.0*std::f64::consts::PI);
}
cr.fill();
if random == 2 || random == 3 || random == 4 || random == 5 || random == 6 {
cr.arc(width/4.,height/4.,height/10.,0.0,2.0*std::f64::consts::PI);
cr.arc(3.*width/4.,3.*height/4.,height/10.,0.0,2.0*std::f64::consts::PI);
}
cr.fill();

if random == 4 || random == 5 || random == 6 {
cr.arc(3.*width/4.,height/4.,height/10.,0.0,2.0*std::f64::consts::PI);
cr.arc(width/4.,3.*height/4.,height/10.,0.0,2.0*std::f64::consts::PI);
}
cr.fill();

if random == 6 {
cr.arc(width/2.,height/4.,height/10.,0.0,2.0*std::f64::consts::PI);
cr.arc(width/2.,3.*height/4.,height/10.,0.0,2.0*std::f64::consts::PI);
}
cr.fill();
Inhibit(false)
});

let l = label.clone();
let dado = r.clone();
let da = drawingarea.clone();
button.connect_clicked(move |_| {
dado.set(pick(1,7));
let text: String = format!("Dado: {}",dado.get());
l.set_text(text.as_str());
da.queue_draw();
});

vbox.add(&button);
vbox.add(&label);
vbox.add(&drawingarea);
window.add(&vbox);

window.show_all();
gtk::main();
}


Con esto tenemos lo básico para empezar a diseñar GUIs con GTK y Rust.

 
Tags: gtk programacion gui linux rust tutorial ventana boton