Interfaz Gtk

La integración de nuestro código Genie con el escritorio es bastante sencilla usando las librerías Gtk. Cuando digo integración con el escritotio quiero decir sacar a Genie de la consola para vestirlo con un traje elegante compuesto por ventanas, botones, etiquetas y esas cosas.

Por esa misma facilidad a veces se ha visto a Genie (y a Vala) como lenguajes específicos para la programación en Gnome, no obstante, como ya se ha comentado, ambos lenguajes son genéricos. Genie usa las librerías Glib (que por cierto, están en todas las distibuciones linux), así que para usar Genie no hay que instalar nada en esos sitemas. Pero la naturaleza genérica de Glib (disponible para distintos sistemas operativos) significa que se puede usar Genie para cualquier tipo de programación, lo que no quita su facilidad para la programación combinada con Gtk.

Para empezar a trabajar con Gtk hay que tener instaladas sus librerías (libgtk-3-0 y libgtk-3-dev) que existen en el 99% de las distribuciones linux (a veces con distintos nombres, como lib32-gtk) y también están disponibles para Windows y Mac. Puedes obtenerlas desde la sección de descargas de The GTK+ Project.

Por otra parte, siempre que trabajamos con esta interfaz gráfica hay que tener en cuenta que la compilación con valac requiere que se indique específicamente que requiere Gtk de esta forma:

valac --pkg gtk+-3.0 nombre_programa.gs

Para hacerte una idea de cómo se ven estas aplicaciones puedes pasarte por la sección de ejemplos de código (por ejemplo, Simple Text Viewer).

Un ejemplo básico con una ventana y un botón con un evento:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
 
init
	// inicializamos Gtk
	Gtk.init (ref args)
	// creamos el objeto test, que es una ventana
	var test = new TestVentana ()
	// mostramos todo
	test.show_all ()
	Gtk.main ();
 
class TestVentana: Window
	init
		// título de la ventana
		title = "Ejemplo Gtk"
		// tamaño de la ventana
		default_height = 250
		default_width = 250
		// posición de la ventana
		window_position = WindowPosition.CENTER
		// si la ventana se cierra el programa termina
		destroy.connect(Gtk.main_quit)
		// creamos un botón
		var boton = new Button.with_label ("Soy un botón. Click aquí")
		// evento del botón: llama a una función
		boton.clicked.connect(btn)		
		// añadimos el botón a la ventana
		add (boton)
	def btn(btn:Button)
		title = "Gracias por hacer click"
		btn.label = "Hola Genie + Gtk"

El elemento más básico es la ventana (Window), con sus propios métodos y propiedades en cuanto a tamaño, posición, título, color…

Las ventanas pueden contener un número ilimitado de controles o widgets, como son, entre otros, las etiquetas de texto (Label), las cajas de texto o de entrada (Entry), los botones (Button), barras de desplazamiento (ScrolledWindow), barras de menú (MenuBar), etc., cada uno de ellos con sus métodos, propiedades y eventos.

Y otro aspecto importante a tener en cuenta es el posicionamiento de esos widgets en la ventana. Para ello se utilizan las cajas horizontales y verticales (Hbox, Vbox), las tablas (Table) y las cuadrículas (Grid).

Ventanas

Una ventana (Window) es un objeto de nivel superior que puede contener otros widgets.

El sistema de ventanas tiene propiedades que permiten manipular sus funciones y aspecto (cambiar tamaño, posición, mover, cerrar…).

Normalmente las ventanas se crean en una nueva clase con una instancia creada desde la sección init del programa principal.

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
init
	// inicia la librería Gtk
	Gtk.init (ref args)
	// hace una instacia a la ventana
	var TestGtk = new Ventana()
	// decidimos que elementos son visibles
	TestGtk.show_all()
	Gtk.main() // control que sondea señales y eventos del bucle de GTK
 
class Ventana : Window
	init
		// titulo
		title = "Test Genie + GTK"
		// tamaño: ancho y alto
		default_width = 350
		default_height = 200;
		// posición
		window_position = WindowPosition.CENTER
		// si la ventana se cierra el programa termina
		destroy.connect(Gtk.main_quit)

Aunque también es perfectamente posible de esta manera:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
 
init	
	Gtk.init (ref args)
	var TestGtk = new Window()
 
	TestGtk.title = "Genie + GTK"
	TestGtk.window_position = WindowPosition.CENTER
	TestGtk.set_default_size (350, 70)
 
	TestGtk.destroy.connect (Gtk.main_quit)
 
	TestGtk.show_all()
	Gtk.main()

Como hemos visto, el tamaño de la ventana lo podemos establecer de dos maneras:

default_width = 350
default_height = 200
// y también:
set_default_size (200, 400)  // (ancho x alto)

Y lo podemos cambiar:

resize (600, 600)

Y también le podemos indicar que ocupe la pantalla completa. En el siguiente ejemplo se ha añadido un botón (que ocupa toda la pantalla) para poder salir y cerrar la ventana:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk 
init	
	Gtk.init (ref args)
	var test = new TestVentana ()
	test.show_all ()
	Gtk.main ();
 
class TestVentana: Window
	init		
		title = "Ejemplo Gtk"		
		window_position = WindowPosition.CENTER
		fullscreen()
		destroy.connect(Gtk.main_quit)
		var boton = new Button.with_label ("Click para Salir")		
		boton.clicked.connect(btn)
		add (boton)
	def btn(btn:Button)
		Gtk.main_quit()

También podemos fijar el borde interior de la ventana:

border_width = 20

La posición, además de CENTER también admite:

  • NONE
  • MOUSE, que hace que la ventana se coloque en la posición actual del ratón.
  • CENTER_ALWAYS, que mantiene la ventana centrada a medida que cambia de tamaño, etc. Esto no es muy recomendable porque no necesariamente funcionará bien con todos los gestores de ventanas.
  • CENTER_ON_PARENT. Centra la ventana respecto a su padre temporal (otra ventana).

Además de fijar su posición, también podemos mover la ventana. Vuelvo a incorporar un botón en el código de ejemplo para probar esta función (si arrastras la ventana hacia cualquier lugar, siempre volverá al mismo sitio al hacer clic en el botón).

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
 
init	
	Gtk.init (ref args)
	var test = new TestVentana ()
	test.show_all ()
	Gtk.main ();
 
class TestVentana: Window
	init		
		title = "Ejemplo Gtk"		
		window_position = WindowPosition.CENTER
		default_height = 250
		default_width = 250
		destroy.connect(Gtk.main_quit)
		var boton = new Button.with_label ("Click para Mover")		
		boton.clicked.connect(btn)
		add (boton)
	def btn(btn:Button)
		move(100, 100)

También podemos añadir un icono a la pestaña de la ventana:

icon = new Gdk.Pixbuf.from_file ("ico.png")

Aunque ese código funciona, al compilar nos avisa: “warning: unhandled error `GLib.Error'”. Para solucionarlo:

try
	icon = new Gdk.Pixbuf.from_file ("genie48.ico")
except e : GLib.Error
	stderr.printf ("Error: %s\n", e.message)

Puedes probar con imágenes de 48×48 en formato png o ico.

Aunque es un método obsoleto, podemos alterar la transparencia de la ventana (añadir después de show_all()).

TestGtk.show_all()
TestGtk.set_opacity (0.5)   // OBSOLETO para ventanas

No obstante, set_opacity() es un método perfectamente aplicable a los widgets que veremos a continuación. Y para conseguir la transparencia en las ventanas podemos utilizar el siguiente código:

set_app_paintable(true)	
set_visual(Gdk.Screen.get_default().get_rgba_visual())

Este código consigue una transparencia completa de la ventana. Para graduar con facilidad su nivel de transparencia / opacidad podemos recurrir a Cairo.

Aplicación Gtk+

Al parecer, ahora la manera recomendada para crear aplicaciones Gtk+ consiste en utilizar la clase Gtk.Application. Al iniciarlo, Gtk.Application llama directamente a Gtk.init y Gtk.main por lo que no es necesario llamarlos manualmente en el código.

Además de gestionar la inicialización de GTK+ (entre otras cosas, como acciones, menús, iconos y más), Gtk.Application administra también una lista de ventanas cuyo ciclo de vida está vinculado automáticamente al ciclo de vida de la aplicación, y por tanto tampoco es necesario controlar que el programa termine cuando la ventana principal se cierra con destroy.connect(Gtk.main_quit). O al contrario, con GLib.Application.hold() podemos indicar que la aplicación permanezca activa a pesar de haber cerrado la ventana principal.

Aunque Gtk.Application funciona bien con Gtk.Window, se recomienda utilizarlo junto con Gtk.ApplicationWindow. Gtk.ApplicationWindow es una subclase de ventana que ofrece algunas funcionalidades adicionales para una mejor integración con las funciones de Gtk.Application. En particular, puede gestionar tanto el menú de aplicaciones como la barra de menús y permite agregar acciones específicas de la ventana.

Un ejemplo elemental:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses Gtk
 
class MyApplication : Gtk.Application	
 
	def override activate ()		
		var window = new Gtk.ApplicationWindow (this)
		window.title = "Test Gtk.Application con Genie"
		window.set_default_size (400, 300)
		window.set_border_width (24)
 
		etiqueta: Gtk.Label = new Gtk.Label ("Soy una etiqueta en una aplicación GTK+")
		window.add (etiqueta)		
		window.show_all()
init	
	var app = new MyApplication( )  // crea la aplicación
	app.run (args)			// ejecuta la aplicación

También podemos inicializar Gtk.Application así (optimizando el código):

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses Gtk
 
class MyApplication : Gtk.Application	
 
	def override activate ()		
		var window = new Gtk.ApplicationWindow (this)
		window.title = "Test Gtk.Application con Genie"
		window.set_default_size (400, 300)
		window.set_border_width (24)
 
		etiqueta: Gtk.Label = new Gtk.Label ("Soy una etiqueta en una aplicación GTK+")
		window.add (etiqueta)		
		window.show_all()
init
	new MyApplication ().run ()  // crea e inicia la aplicación

Aunque una manera más completa es:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses Gtk
 
init
	// inicia la aplicación con parámetros id y flags
	new MyApplication( "testing.my.application",
		ApplicationFlags.FLAGS_NONE
		).run( args )
 
class MyApplication:Gtk.Application
	// chequea el id de aplicación
	construct( application_id:string, flags:ApplicationFlags )
		if !id_is_valid( application_id )
			error( "application id %s is not valid", application_id )
		this.application_id = application_id
		this.flags = flags
 
	def override activate ()
		var window = new Gtk.ApplicationWindow( this )
		window.title = "Test Gtk.Application con Genie"
		window.set_default_size (400, 300)		
 
		etiqueta: Gtk.Label = new Gtk.Label ("Soy una etiqueta en una aplicación GTK+")
		window.add (etiqueta)		
		window.show_all()

Una breve explicación del código:

La estructura para crear una nueva instancia de aplicación es: new NombreAplicacion (application_id: string, flags: ApplicationFlags).

El parámetro application_id sirve para que nuestra aplicación sea identificada por el sistema, como una aplicación única (la aplicación se registra en el bus de sesión mediante el ID de la aplicación). Si no se da el ID de la aplicación, algunas características (especialmente la unicidad de la aplicación) se desactivarán. Un ID de aplicación nulo sólo se permite con GTK+ versión 3.6 o posterior. El ID de la aplicación es un string que debe estar formado por al menos dos nombres separados por un punto.

En linux podemos comprobar fácilmente qué es esto de la unicidad de la aplicación, especialmente con el programa DFeet (en los repositorios oficiales como d-feet): Abre dos consolas y ejecuta en cada una de ellas cualquier código Genie + Gtk sin Gtk.Application. Abre el Gestor de Tareas (en Sistema) de tu distribución y comprobarás que hay dos instancias de ese código. Si abres d-feet, pasa lo mismo, aparece dos veces como dos procesos distintos. Cerramos las dos ventanas Gtk y ahora ejecutamos en cada consola el código anterior con Gtk.Application y con application_id. Ahora en el Gestor de Tareas el nombre del archivo (no el nombre de la aplicación) aparece solo una vez y, más importante, en d-feet, en la pestaña Session Bus, también aparece solo una vez pero ahora por su nombre identificador (testing.my.application) y al hacer clic sobre él reconoce que tiene dos ventanas abiertas pero de la misma aplicación. Estamos constatando la unicidad de aplicación.

Los flags son utilizados para definir el comportamiento de una aplicación. El valor por defecto es FLAGS_NONE. Otros valores posibles son IS_SERVICE (se ejecuta como un servicio), IS_LAUNCHER, HANDLES_OPEN (control para abrir archivos), HANDLES_COMMAND_LINE (gestiona los argumentos de la línea de comandos), SEND_ENVIRONMENT, NON_UNIQUE y CAN_OVERRIDE_APP_ID.

La función activate() es la señal (o función virtual) predeterminada cuando se ejecuta Gtk.Application, pero se pueden emitir señales alternativas en función de los flags ajustados.

Antes de la función activate(), se comprueba que el id de la aplicación es válido (si no lo fuera, se permite la compilación pero en la ejecución salta un error que detiene la aplicación).

En principio, lo que se explica a continuación sobre los widgets en referencia a Gtk.Window es perfectamente aplicable a Gtk.Application y Gtk.ApplicationWindow, como se ve en algunos ejemplos.

Widgets

En las ventanas podemos colocar diversos controles (widgets) como botones, cajas de entrada de datos, etiquetas, barras de desplazamiento, etc.

Para colocar cualquier widget en una ventana primero tenemos que crearlo, luego podemos asignarle propiedades y/o eventos y finalmente tenemos que añadirlo a la ventana (con add (widget)) y mostrarlo (si no tenemos show_all).

Etiquetas

Las etiquetas (Label) muestran un texto en la ventana. Habitualmente se utilizan para etiquetar otros widgets, como un botón, un ítem de un menú o un ComboBox.

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
init
 
	Gtk.init (ref args)
	var TestGtk = new Ventana()
	TestGtk.show_all()	
	Gtk.main()
 
class Ventana : Window		
	init		
		title = "Test Genie + GTK: Widgets"
		default_width = 350		
		default_height = 200		
		window_position = WindowPosition.CENTER	
		destroy.connect(Gtk.main_quit)
 
		var etiqueta = new Label("Mi primera etiqueta en Genie + GTK")
		add (etiqueta)

Una etiqueta puede contener varios párrafos aunque si son muchos puede verse afectado el rendimiento.

Podemos cambiar el formato del texto en una etiqueta (color, fuente, etc.):

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
init
 
	Gtk.init (ref args)
	var TestGtk = new Ventana()
	TestGtk.show_all()	
	Gtk.main()
 
class Ventana : Window		
	init		
		title = "Test Genie + GTK: Widgets"
		default_width = 350		
		default_height = 200		
		window_position = WindowPosition.CENTER	
		destroy.connect(Gtk.main_quit)
 
		var etiqueta = new Label("")		
		etiqueta.set_markup ("<b>Hola en negrita</b>\n<small>Texo pequeño</small>")		
		add (etiqueta)

O mejor así:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
init
 
	Gtk.init (ref args)
	var TestGtk = new Ventana()
	TestGtk.show_all()	
	Gtk.main()
 
class Ventana : Window		
	init		
		title = "Test Genie + GTK: Widgets"
		default_width = 350		
		default_height = 200		
		window_position = WindowPosition.CENTER	
		destroy.connect(Gtk.main_quit)
 
		var etiqueta = new Label("<b>Hola en negrita</b>\n<small>Texo pequeño</small>")		
		etiqueta.set_use_markup (true)		
		add (etiqueta)

Otro ejemplo:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
init
 
	Gtk.init (ref args)
	var TestGtk = new Ventana()
	TestGtk.show_all()	
	Gtk.main()
 
class Ventana : Window		
	init		
		title = "Test Genie + GTK: Widgets"
		default_width = 350		
		default_height = 200		
		window_position = WindowPosition.CENTER	
		destroy.connect(Gtk.main_quit)
 
		var etiqueta = new Label("""
		<span foreground='blue' size='x-large'>Texto AZUL</span>
		<b>Hola en negrita</b>
		<small>Texo pequeño</small>
		<i>cursiva</i>""")		
		etiqueta.set_use_markup (true)				
		add (etiqueta)

Para ver los formatos admitidos visita Text Attribute Markup. Otros ejemplos (font_family, size, background, links):

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
init
 
	Gtk.init (ref args)
	var TestGtk = new Ventana()
	TestGtk.show_all()	
	Gtk.main()
 
class Ventana : Window		
	init		
		title = "Test Genie + GTK: Widgets"
		default_width = 350		
		default_height = 200		
		window_position = WindowPosition.CENTER	
		destroy.connect(Gtk.main_quit)		
 
		var etiqueta = new Label ("""
		<span size='20800' font_family='times'>Wiki Genie</span>
		<span background='#00FF00'>Aprende Genie en</span>		
		<a href='http://genie.webierta.skn1.com'>Genie Doc</a>
		""")		
		etiqueta.set_use_markup (true)
		//etiqueta.set_alignment(0,0)   // funciona pero OBSOLETO
		add (etiqueta)

Cajas de texto: Entry

Las cajas de entrada o cajas de texto (Entry) son un widget de entrada de texto de una sola línea.

Vemos cómo crearlas y algunas posibilidades que permiten:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
init
 
	Gtk.init (ref args)
	var TestGtk = new Ventana()
	TestGtk.show_all()	
	Gtk.main()
 
class Ventana : Window		
	init		
		title = "Test Genie + GTK: Widgets"
		default_width = 350		
		default_height = 70		
		window_position = WindowPosition.CENTER
		border_width = 20
		destroy.connect(Gtk.main_quit)
 
		// creamos la caja de entrada
		var entrada = new Entry()
		// y la añadimos a la ventana
		add (entrada)	
 
		// establece el contenido
		entrada.set_text("Hola")		
 
		// y lo modifica
		var texto = "Hola Genie"
		entrada.set_text(texto)
 
		// true o false para establecer si es modificable o no
		entrada.set_editable(true)
 
		// podemos recuperar el texto
		TextoCaja:string
		TextoCaja = entrada.get_text()
		print "En la caja de texto pone: %s", TextoCaja

Podemos asociar acciones a la caja de texto. Un ejemplo muy simple, que cierra la ventana y la aplicación al editar el texto:

// salir al cambiar
entrada.changed.connect(Gtk.main_quit)

Compara en el siguiente código las distintas acciones asociadas:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
init
 
	Gtk.init (ref args)
	var TestGtk = new Ventana()
	TestGtk.show_all()	
	Gtk.main()
 
class Ventana : Window
 
	init		
		title = "Test Genie + GTK: Widgets"
		default_width = 350		
		default_height = 70		
		window_position = WindowPosition.CENTER
		border_width = 20
		destroy.connect(Gtk.main_quit)
 
		var entrada = new Entry()
		add (entrada)	
 
		entrada.set_text("Hola Genie")		
		entrada.set_editable(true)	
 
		// acciones
		entrada.changed.connect(cambiado)		
		entrada.activate.connect(entrar)
 
	def cambiado ()
		print("Has cambiado el texto") 	
 
	def entrar ()
		print("ENTER")

También podemos incorporar una imagen:

// añadimos una imagen (SECONDARY: al final; PRIMARY: al principio)		
icon = new Gdk.Pixbuf.from_file ("limpiar.png")		
entrada.set_icon_from_pixbuf(Gtk.EntryIconPosition.SECONDARY, icon)

Y finalmente asignamos una acción a esa imagen (observa que ahora definimos entry en el bloque principal de la ventana para que sea accesible desde la nueva función):

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
init 
	Gtk.init (ref args)
	var TestGtk = new Ventana()
	TestGtk.show_all()	
	Gtk.main()
 
class Ventana : Window
 
	entrada: Entry = new Entry()
 
	init		
		title = "Test Genie + GTK: Widgets"
		default_width = 350		
		default_height = 70		
		window_position = WindowPosition.CENTER
		border_width = 20
		destroy.connect(Gtk.main_quit)
 
		//var entrada = new Entry()
		add (entrada)	
 
		entrada.set_text("Escribe y borra...")		
		entrada.set_editable(true)	
 
		icon = new Gdk.Pixbuf.from_file ("limpiar.png")		
		entrada.set_icon_from_pixbuf(Gtk.EntryIconPosition.SECONDARY, icon)
 
		entrada.icon_press.connect(limpiar)
 
	def limpiar()				
		entrada.set_text("")

Botones

Los botones son los widgets sobre los que se puede hacer clic para realizar alguna acción o proceso. Suele ser habitual (y conveniente) definir un atajo de teclado para realizar esa misma acción, lo que se indica usando un carácter subrayado en la etiqueta del botón (esto se conoce como 'mnemonic').

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
init
 
	Gtk.init (ref args)
	var TestGtk = new Ventana()
	TestGtk.show_all()	
	Gtk.main()
 
class Ventana : Window
 
	init		
		title = "Test Genie + GTK: Widgets"
		default_width = 350		
		default_height = 70		
		window_position = WindowPosition.CENTER
		border_width = 20
		destroy.connect(Gtk.main_quit)
 
		var boton = new Button.with_mnemonic("Alguna _Acción")
		add (boton)

Al ejecutar ese código vemos un botón, y si mantenemos presionada la tecla «Alt» nos muestra el atajo de teclado. Entonces «Alt» + 'a' hace la misma función que pulsar el botón (de momento nada). También así:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
init
 
	Gtk.init (ref args)
	var TestGtk = new Ventana()
	TestGtk.show_all()	
	Gtk.main()
 
class Ventana : Window
 
	init		
		title = "Test Genie + GTK: Widgets"
		default_width = 350		
		default_height = 200		
		window_position = WindowPosition.CENTER
		border_width = 40
		destroy.connect(Gtk.main_quit)
 
		var boton = new Button()		
		boton.set_label("_Hola")
		boton.set_use_underline(true)		
		add (boton)

Ahora le asignamos una nueva función:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
init
 
	Gtk.init (ref args)
	var TestGtk = new Ventana()
	TestGtk.show_all()	
	Gtk.main()
 
class Ventana : Window
 
	boton: Button = new Button.with_mnemonic("Cuenta _Clics")
	clics: int = 0
 
	init		
		title = "Test Genie + GTK: Widgets"
		default_width = 350
		default_height = 70
		window_position = WindowPosition.CENTER
		border_width = 30
		destroy.connect(Gtk.main_quit)
 
		add(boton)
 
		boton.clicked.connect(contar)
 
	def contar()
		boton.label = "Llevas %d _Clics".printf (++clics)

Un ejemplo parecido usando Gtk.Application y el sistema de notificaciones emergentes en el entorno de escritorio:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
 
uses Gtk
 
init
	new MyApplication( "test.notification",
		ApplicationFlags.FLAGS_NONE
		).run( args )
 
class MyApplication:Gtk.Application
 
	boton: Button
	clics: int = 0
 
	construct( application_id:string, flags:ApplicationFlags )
		if !id_is_valid( application_id )
			error( "application id %s is not valid", application_id )
		this.application_id = application_id
		this.flags = flags
 
	def override activate ()
 
		var window = new Gtk.ApplicationWindow( this )
		window.set_default_size (200, 200)
		window.window_position = WindowPosition.CENTER
		window.set_border_width(40)		
 
		boton = new Button.with_label("Cuenta Clics")
		window.add(boton)
		boton.clicked.connect(contar)
 
		window.show_all ()
 
	def contar()
		boton.label = "Llevas %d Clics".printf (++clics)
		var notification = new Notification ("Contando clics")
		notification.set_body(boton.label)
		this.send_notification("test.notification", notification)

Dependiendo de la función asignada, podemos asociar un aspecto al botón para indicar que es una acción sugerida, de cierre o de información:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses Gtk
 
init
	new MyApplication( "test.application",
		ApplicationFlags.FLAGS_NONE
		).run( args )
 
class MyApplication:Gtk.Application
 
	construct( application_id:string, flags:ApplicationFlags )
		if !id_is_valid( application_id )
			error( "application id %s is not valid", application_id )
		this.application_id = application_id
		this.flags = flags
 
	def override activate ()		
 
		var window = new Gtk.ApplicationWindow( this )
		window.title = "Genie + GTK+"	
		window.window_position = WindowPosition.CENTER
		window.set_border_width(10)		
 
		var grid = new Gtk.Grid()		
 
		var boton1 = new Gtk.Button ()
		boton1.label = "SUGGESTED"
		boton1.margin = 10
		boton1.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION)		
 
		var boton2 = new Gtk.Button ()		
		boton2.label = "DESTRUCTIVE"
		boton2.margin = 10		
		boton2.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION)
 
		var boton3 = new Gtk.Button ()
		boton3.label = "INFO"
		boton3.margin = 10
		boton3.get_style_context ().add_class (Gtk.STYLE_CLASS_INFO)
 
		var boton4 = new Gtk.Button ()		
		boton4.label = "DEFAULT"
		boton4.margin = 10		
		boton4.get_style_context ().add_class (Gtk.STYLE_CLASS_DEFAULT)		
 
		grid.attach(boton1, 0, 0, 2, 1)
		grid.attach_next_to (boton2, boton1, Gtk.PositionType.RIGHT, 2, 1)
 
		grid.attach(boton3, 0, 1, 2, 1)
		grid.attach_next_to (boton4, boton3, Gtk.PositionType.RIGHT, 2, 1)
 
		window.add(grid)
		window.show_all()

Gtk ofrece iconos estándar para los botones asociados a funciones habituales (abrir un archivo, cerrar una ventana, etc. (ver todos):

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
init
 
	Gtk.init (ref args)
	var TestGtk = new Ventana()
	TestGtk.show_all()	
	Gtk.main()
 
class Ventana : Window
 
	init		
		title = "GTK"
		//default_width = 100		
		//default_height = 100		
		window_position = WindowPosition.CENTER	
		destroy.connect(Gtk.main_quit)
 
		var layout = new Gtk.Grid ()
		layout.column_spacing = 4
		layout.row_spacing = 4
		add(layout)
 
		// OBSOLETO
		var boton_open = new ToolButton.from_stock (STOCK_OPEN)	
		var boton_ok = new ToolButton.from_stock (STOCK_OK)
		var boton_cancel = new ToolButton.from_stock (STOCK_CANCEL)
		var boton_quit = new ToolButton.from_stock (STOCK_QUIT)
		var boton_no = new ToolButton.from_stock (STOCK_NO)
		var boton_paste = new ToolButton.from_stock (STOCK_PASTE)
		var boton_prefe = new ToolButton.from_stock (STOCK_PREFERENCES)
		var boton_print = new ToolButton.from_stock (STOCK_PRINT)
		var boton_refresh = new ToolButton.from_stock (STOCK_REFRESH)		
		var boton_remove = new ToolButton.from_stock (STOCK_REMOVE)
		var boton_save = new ToolButton.from_stock (STOCK_SAVE)
		var boton_stop = new ToolButton.from_stock (STOCK_STOP)		
 
		layout.attach (boton_open, 0, 1, 1, 1)
		layout.attach (boton_ok, 1, 1, 1, 1)
		layout.attach (boton_cancel, 2, 1, 1, 1)		
		layout.attach (boton_quit, 0, 2, 1, 1)
		layout.attach (boton_no, 1, 2, 1, 1)
		layout.attach (boton_paste, 2, 2, 1, 1)		
		layout.attach (boton_prefe, 0, 3, 1, 1)
		layout.attach (boton_print, 1, 3, 1, 1)
		layout.attach (boton_refresh, 2, 3, 1, 1)		
		layout.attach (boton_remove, 0, 4, 1, 1)
		layout.attach (boton_save, 1, 4, 1, 1)
		layout.attach (boton_stop, 2, 4, 1, 1)

Este código produce este resultado:

ACTUALIZACIÓN: En las últimas versiones (desde la 3.10) este método resulta OBSOLETO y por tanto no aconsejable. La manera actual (y correcta) de hacerlo es así:

var boton_open = new Button.from_icon_name("document-open", IconSize.SMALL_TOOLBAR)

IconSize admite distintos valores que corresponden a tamaños apropiados para distintos elementos:

  • MENU: 16px
  • SMALL_TOOLBAR: 16px
  • LARGE_TOOLBAR: 24px
  • BUTTON: 16px
  • DND: 32px
  • DIALOG: 48px

Aunque también podemos incorporar cualquier imagen a un botón:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
init
 
	Gtk.init (ref args)
	var TestGtk = new Ventana()
	TestGtk.show_all()	
	Gtk.main()
 
class Ventana : Window
 
	init		
		title = "Test Genie + GTK: Widgets"
		default_width = 350		
		default_height = 200		
		window_position = WindowPosition.CENTER
		border_width = 40
		destroy.connect(Gtk.main_quit)
 
		var boton = new Button()		
		icon: Gtk.Widget = new Gtk.Image.from_file ("ico.png")
		boton.set_image (icon)			
 
		add (boton)

Por último, mencionar que existe una clase especial de botones, como por ejemplo ColorButton o FontButton, que nos permiten seleccionar colores o tipografías:

var boton = new ColorButton ()
boton.set_use_alpha (true)  // OBSOLETO		
boton.set_title("Selecciona tu color favorito")		
add (boton)
var boton = new FontButton ()

TreeView

El widget TreeView (vista de árbol) permite mostrar cualquier número de filas de datos. Asociado a un objeto de almacén de datos sirve para implementar vistas de árbol o listas a modo de hoja de cálculo (datos en una cuadrícula). Estos almacenes de datos también se conocen como modelos de datos.

TreeView no es un widget habitual, es realmente potente aunque algo más complicado, por lo que de momento solo puedo dejar unos breves apuntes sobre el tema (pendiente de ampliar).

Creamos un nuevo TreeView de la misma manera que otros widgets:

var MiTreeView = new TreeView()

Pero TreeView no funciona como un widget independiente, sino que necesita una serie de widgets: TreeStore o ListStore, TreeViewColumn y CellRenderers.

Los datos que muestra se almacenan en TreeStore (o en ListStore donde los datos no están en un formato de árbol). Para configurar un ListStore para el TreeView:

// Configura un nuevo "Modelo" de datos en un ListStore con 3 columnas que contienen distintos tipos de datos
var  MiListStore = new ListStore(3, typeof (string), typeof (string), typeof(double)) 
MiTreeView.set_store = MiListStore // asocia los datos del ListStore al TreeView

Un TreeView se compone de un número de TreeViewColumns. Cada columna tiene un encabezado (el título de la columna) sobre el que se puede hacer clic para ordenar la lista y para cambiar el tamaño del ancho de la columna. En cada columna hay un CellRenderer (que están empaquetados en TreeViewColumns) cuyas propiedades determinan cómo se muestran los datos de toda la columna (no sólo una sola celda, sino todas las celdas de esa columna). Hay diferentes procesadores de celdas para diferentes tipos de datos.

Cuando se crea un TreeViewColumn, se requieren el título y el CellRenderer de la columna. Por lo tanto, la secuencia en la que se construye el TreeView es la siguiente:

  1. Se inicia el CellRenderer para cada columna y
  2. entonces se crea el TreeViewColumn,
  3. los TreeViewColumns se agregan al TreeView.
var Col1CR = new CellRendererText()
var Columna1 = new TreeViewColumn.with_attributes("Columna Primera", Col1CR)
MiTreeView.append_columns(Columna1) // añade la columna 1 al TreeView 

El objeto CellRenderer es el que tiene los eventos (señales) que se pueden “conectar” con una función para realizar una acción después de una entrada de usuario para cambiar un campo en el TreeView.

Como decíamos, TreeStore o ListStore son los widgets que almacenan los datos para mostrarlos en un TreeView. TreeStore es una matriz multidimensional mientras que ListStore es una matriz de una sola dimensión. Los datos pueden ser introducidos, cambiados y eliminados mediante una serie de métodos.

Para hacer referencia a las filas de datos en un TreeStore utilizamos TreePath. La dirección de una fila de datos viene dada por una serie de enteros separados por dos puntos, por ejemplo 4:2:3 hace referencia a la 5ª fila (se empieza contando desde 0), a la tercera fila en el siguiente nivel del árbol y a la 4ª fila del último nivel. Para un ListStore, en cambio, sólo se requiere un solo entero, ya que no tiene estructura de árbol.

Sin embargo, TreeIter es la forma más común de hacer referencia a las filas. La posición actual en TreeStore viene dada por TreeIter. Para agregar datos a un TreeStore primero se necesita agregar un registro en blanco y recuperar el TreeIter dónde actualizar los valores. El método append de TreeStore (o de ListStore) devuelve un parámetro “out” que es un TreeIter para indicar al programa dónde se ha agregado la nueva fila.

Un ejemplo sencillo de TreeView con ListStore (puedes ver otro ejemplo de TreeView con ListStore en la sección de Pango):

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
init
 
	Gtk.init (ref args)
	var TestGtk = new Ventana()
	TestGtk.show_all()
	Gtk.main()
 
class Ventana : Window
 
	init		
		title = "Genie + TreeView"
		default_width = 250
		default_height = 100
		window_position = WindowPosition.CENTER
		destroy.connect(Gtk.main_quit)
 
		var tabla = new TreeView ()
		add (tabla)
 
		var modelo = new Gtk.ListStore (4, typeof (string), typeof (string), typeof (string), typeof (string))
		tabla.set_model (modelo)
 
		tabla.insert_column_with_attributes (-1, "Cuenta", new CellRendererText (), "text", 0)
		tabla.insert_column_with_attributes (-1, "Tipo", new CellRendererText (), "text", 1)
 
		var celda = new CellRendererText ()
		celda.set ("foreground_set", true)
		tabla.insert_column_with_attributes (-1, "Balance", celda, "text", 2, "foreground", 3)
 
		iter: TreeIter
		modelo.append (out iter)
		modelo.set (iter, 0, "BBVA", 1, "Cuenta", 2, "2408,10", 3, "blue")
 
		modelo.append (out iter)
		modelo.set (iter, 0, "Visa", 1, "Tarjeta", 2, "102,10", 3, "red")
 
		modelo.append (out iter)
		modelo.set (iter, 0, "Mastercard", 1, "Tarjeta", 2, "10,20", 3, "red")

Otro ejemplo de TreeView, ahora con TreeStore:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
init 
	Gtk.init (ref args)
	var TestGtk = new Ventana()
	TestGtk.show_all()
	Gtk.main()
 
class Ventana : Window 
	init		
		title = "Genie + TreeView"
		default_width = 250		
		default_height = 100		
		window_position = WindowPosition.CENTER
		destroy.connect(Gtk.main_quit)
 
		var vista = new TreeView ()		
		add (vista)	  
 
		var tienda = new TreeStore (2, typeof (string), typeof (string))
		vista.set_model (tienda)
 
		vista.insert_column_with_attributes (-1, "Producto", new CellRendererText (), "text", 0, null)
		vista.insert_column_with_attributes (-1, "Precio", new CellRendererText (), "text", 1, null)
 
		root: TreeIter
		categoria: TreeIter
		producto:TreeIter
 
		tienda.append (out root, null)
		tienda.set (root, 0, "Todos los productos", -1)
 
		tienda.append (out categoria, root)
		tienda.set (categoria, 0, "Libros", -1)
 
		tienda.append (out producto, categoria)
		tienda.set (producto, 0, "Moby Dick", 1, "10.36€", -1)
		tienda.append (out producto, categoria)
		tienda.set (producto, 0, "Don Quijote", 1, "4.99€", -1)
		tienda.append (out producto, categoria)
		tienda.set (producto, 0, "Ulysses", 1, "26.09€", -1)
		tienda.append (out producto, categoria)
		tienda.set (producto, 0, "Aprender Genie", 1, "38.99€", -1)
 
		tienda.append (out categoria, root)
		tienda.set (categoria, 0, "Películas", -1)
 
		tienda.append (out producto, categoria)
		tienda.set (producto, 0, "Amores Perros", 1, "7.99€", -1)
		tienda.append (out producto, categoria)
		tienda.set (producto, 0, "La princesa prometida", 1, "14.99€", -1)
		tienda.append (out producto, categoria)
		tienda.set (producto, 0, "Vértigo", 1, "20.49€", -1)
 
		vista.expand_all ()

Este código genera una ventana como ésta:

Por otro lado, al TreeView podemos agregarle barras de desplazamiento. Para ello se requiere un widget “contenedor” llamado ScrolledWindow, donde se coloca el TreeView. ScrolledWindow se agrega a la ventana principal usando un widget de empaquetado. De forma predeterminada, el widget TreeView se redimensiona en función del número de filas de datos en el ListStore o TreeStore que se van a mostrar en el widget.

Una captura de otro ejemplo del widget TreeView (puedes ver el código en el apartado de ejemplos):

Otros Widgets

Gtk dispone de muchos más widgets (ver en Valadoc), por lo que aquí solo dejaré una pequeña muestra de código con algunos ejemplos.

RadioButton

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses Gtk
init	
	Gtk.init (ref args)	
	var test = new TestVentana ()	
	test.show_all ()	
	Gtk.main ()
 
class TestVentana: Window			
 
	init		
		title = "Ejemplo Gtk"		
		default_height = 300
		default_width = 300
		border_width = 50		
		window_position = WindowPosition.CENTER		
		destroy.connect(Gtk.main_quit)
 
		var op1 = new Gtk.RadioButton (null)
		op1.set_label ("Opción 1")
		var op2 = new Gtk.RadioButton.with_label (op1.get_group(),"Opción 2")
		var op3 = new Gtk.RadioButton.with_label_from_widget (op1, "Opción 3")
 
		var grid = new Gtk.Grid ()
		grid.attach (op1, 0, 0, 1, 1)
		grid.attach (op2, 0, 1, 1, 1)
		grid.attach (op3, 0, 2, 1, 1)		
		add (grid)
 
		op1.toggled.connect (button_toggled_cb)
		op2.toggled.connect (button_toggled_cb)
		op3.toggled.connect (button_toggled_cb)
 
	def button_toggled_cb (button: Gtk.ToggleButton)
		var state = "unknown"
		if (button.get_active ())
			state = "activada"
		else
			state = "desactivada"		
		print (button.get_label() + " ha sido " + state + ".")

Spinner

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses Gtk
init	
	Gtk.init (ref args)	
	var test = new TestVentana ()	
	test.show_all ()	
	Gtk.main ()
 
class TestVentana: Window
 
	spinner: Gtk.Spinner		
 
	init		
		title = "Ejemplo Gtk"		
		default_height = 300
		default_width = 300
		border_width = 50		
		window_position = WindowPosition.CENTER		
		destroy.connect(Gtk.main_quit)
 
		spinner = new Gtk.Spinner ()		
		spinner.active = true		
		add (spinner)
 
		// key_press_event += tecla // OBSOLETO
		key_press_event.connect(tecla)	
 
	def tecla(key : Gdk.EventKey):bool		
		if spinner.active == true
			spinner.active = false						
		else
			spinner.active = true			
		return true
// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses Gtk
init	
	Gtk.init (ref args)	
	var test = new TestVentana ()	
	test.show_all ()	
	Gtk.main ()
 
class TestVentana: Window			
 
	boton: Button = new Button.with_mnemonic("""
	_Clic aquí
	para abrir
	un diálogo""")	
	_dialogo: Gtk.Dialog
 
	init		
		title = "Ejemplo Gtk"		
		default_height = 300
		default_width = 450
		border_width = 60		
		window_position = WindowPosition.CENTER		
		destroy.connect(Gtk.main_quit)		
 
		add (boton)	
		boton.clicked.connect(dia)			
 
	def dia()	
		_dialogo = new Gtk.Dialog.with_buttons ("Gtk + Dialogo", this,
			Gtk.DialogFlags.MODAL,
			("_OK"),  // OBSOLETO: Gtk.Stock.OK, 
			Gtk.ResponseType.OK, ("_Cancel"),												  
			Gtk.ResponseType.NO, null)
 
		var content_area = _dialogo.get_content_area ()
		var label = new Gtk.Label ("Esto es un diálogo con una etiqueta y dos botones")
		content_area.add (label)
		_dialogo.border_width = 20
		_dialogo.response.connect(respuesta)
		_dialogo.show_all ()
 
	def respuesta(_dialogo:Gtk.Dialog, response_id:int)
		print ("El valor es %d\n", response_id)  // Para ver el valor int de ResponseType
		_dialogo.destroy ()

Diálogo de mensajes

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses Gtk
init	
	Gtk.init (ref args)	
	var test = new TestVentana ()	
	test.show_all ()	
	Gtk.main ()
 
class TestVentana: Window			
 
	boton: Button = new Button.with_mnemonic("""
	_Clic aquí para abrir
	un mensaje de diálogo""")	
	_msg: Gtk.MessageDialog
 
	init		
		title = "Ejemplo Gtk"
		set_default_size (350, 100)		
		border_width = 60		
		window_position = WindowPosition.CENTER		
		destroy.connect(Gtk.main_quit)		
 
		add (boton)	
		boton.clicked.connect(dia)
 
	def resp(_msg:Gtk.Dialog, response_id:int)
		case (response_id)
			when Gtk.ResponseType.OK
				stdout.puts ("Ok\n")
				break
			when Gtk.ResponseType.CANCEL
				stdout.puts ("Cancela\n")
				break
			when Gtk.ResponseType.DELETE_EVENT
				stdout.puts ("Elimina\n")
				break					
		_msg.destroy()			
 
	def dia()
		_msg = new Gtk.MessageDialog (this,
			Gtk.DialogFlags.MODAL,
			Gtk.MessageType.WARNING,
			Gtk.ButtonsType.OK_CANCEL,
			"¡Cuidadín, cuidadín!")
		_msg.border_width = 10
		_msg.response.connect(resp)	
		_msg.show_all ()

Otra forma, quizá más sencilla:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses Gtk
init
	Gtk.init (ref args)
	var Hola=new MessageDialog (null,
		Gtk.DialogFlags.MODAL,
		Gtk.MessageType.INFO,
		Gtk.ButtonsType.OK,
		"Hola, ¡soy un diálogo!")
	Hola.format_secondary_text ("Esto es un diálogo de ejemplo.")
	Hola.run ()

Imágenes

Gtk.Image

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses Gtk
init	
	Gtk.init (ref args)
	var TestGtk = new Ventana()
	TestGtk.show_all()	
	Gtk.main()
 
class Ventana : Window
	init		
		title = "Test Genie + GTK"
		window_position = WindowPosition.CENTER
		destroy.connect(Gtk.main_quit)
 
		var image = new Gtk.Image ()
		image.set_from_file ("genielogo2.png")
		add (image)

Scrolled window

Se trata de una clase de contenedor con barras de desplazamiento. Puedes ver un ejemplo en la sección de Pango.

Posicionamiento

Ahora mostramos cómo distribuir nuestros widgets en ventanas o diálogos.

Cuando diseñamos la interfaz de usuario de una aplicación, decidimos qué widgets usaremos y cómo organizaremos esos widgets. Para organizarlos usamos widgets especializados no visibles llamados contenedores de diseño, como son las cajas (Box), las tablas (Table), la posición fija (Fixed) y la alineación (Alignment).

Alignmentn ha quedado obsoleto en las últimas versiones (desde la 3.14) y en su lugar se debe utilizar un sistema alternativo de posicionamiento, y para establecer el alineamiento y el margen de un widget se recomienda usar las propiedades de alineación y margen del widget (set_halign, set_valign y set_margin).

Alignment controla la alineación y el tamaño de un widget secundario. Tiene cuatro configuraciones: xscale, yscale, xalign y yalign. Sería algo así:

// posicionamiento
var alinear = new Alignment (0.50f, 0.25f, 1.0f, 0.5f)
alinear.right_padding = 20
alinear.left_padding = 10
add (alinear)
 
// contenido
var boton = new Button.with_label ("Boton alineado")
alinear.add (boton)

Coordenadas fijas

Fixed construye un contenedor que permite posicionar widgets en base a unas coordenadas. Este contenedor permite colocar widgets en posiciones fijas y con tamaños fijos, expresados en píxeles, dentro de una ventana en relación con su esquina superior izquierda. La posición de los widgets se puede cambiar dinámicamente.

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
 
init
	Gtk.init (ref args)	
	var TestGtk = new Ventana()	
	TestGtk.show_all()	
	Gtk.main()
 
class Ventana : Window
 
	init		
		title = "Genie"
		default_width = 350		
		default_height = 200		
		window_position = WindowPosition.CENTER
		destroy.connect(Gtk.main_quit)	
 
		// Posicionamiento
		var fijo = new Gtk.Fixed ()
		add (fijo)
 
		// Contenido
		var boton1 = new Gtk.Button.with_label ("Botón 1")
		fijo.put (boton1, 0, 0)
 
		var boton2 = new Gtk.Button.with_label ("Botón 2")
		fijo.put (boton2, 50, 50)

El contenedor Fixed coloca los widgets secundarios en posiciones fijas y con tamaños fijos, por lo que no realiza ninguna administración automática de diseño (por ejemplo, ante un cambio de fuente), por lo que en la mayoría de las aplicaciones no resulta recomendable.

No obstante, puntualmente este sistema puede ser útil, como por ejemplo en algunos casos para posicionar gráficos o imágenes o en aplicaciones muy elementales en las que se prefiere la simplicidad de Fixed, aunque no siempre compensa (por ejemplo, el posicionamiento fijo hace que sea algo molesto agregar o quitar elementos ya que hay que reposicionar todos los demás elementos, lo que origina problemas de mantenimiento a largo plazo para la aplicación).

Cajas

Los widgets son “embalados” o “empaquetados” en cajas horizontales y verticales.

Podemos configurar el espaciado y optar por apilar las cajas desde la izquierda o desde la derecha en el caso de cajas horizontales, o desde la parte superior o inferior en el caso de cajas verticales. Utilizando este sistema de cajas, los widgets se pueden reorganizar y adaptar a cualquier cambio de tamaño de ventana.

Para ello, creamos un objeto de caja horizontal con dos parámetros. El primero (conocido como homogéneo) puede ser true o false. Si es true, los widgets se distribuyen uniformemente. El segundo parámetro se refiere a la cantidad de espaciado entre Widgets.

VBox es un contenedor de caja vertical que organiza sus widgets (secundarios o hijos) en una sola columna, mientras que HBox es un contenedor de caja horizontal que organiza sus widgets en una sola fila.

var Contenedor_A = new Hbox(false, 0)

Una vez creada una caja o contenedor, podemos empaquetar los widgets en las cajas usando:

Contenedor_A.pack_start (Widget, false, true, 0)

De nuevo los parámetros utilizados determinan el espaciado / alineación de los elementos dentro de la caja. Además, es posible colocar cajas ya embaladas dentro de otras cajas de la misma forma que cualquier otro widget.

No obstante, desde la versión 3.2 VBox y HBox han quedado obsoletos y en su lugar se puede utilizar simplemente Box indicando la orientación:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
 
init
	Gtk.init (ref args)	
	var TestGtk = new Ventana()	
	TestGtk.show_all()	
	Gtk.main()
 
class Ventana : Window
 
	init		
		title = "Genie"
		default_width = 350		
		default_height = 70		
		window_position = WindowPosition.CENTER
		destroy.connect(Gtk.main_quit)	
 
		// contenedor: caja	
		var caja = new Box (Orientation.HORIZONTAL, 0)  // en horizontal
		// var caja = new Box (Orientation.VERTICAL, 0) // distribución vertical
 
		// contenido: etiquetas
		caja.pack_start (new Label ("1"), true, false, 0)
		caja.pack_start (new Label ("2"), true, false, 0)
		caja.pack_start (new Label ("3"), true, false, 0)
		add (caja)

El parámetro númerico (entero) de Box se refiere al espaciado, mientras que los parámetros (boleanos los dos primeros y númerico el último) de pack_start son, por este orden y después del widget correspondiente:

  • Expand. True si se le da espacio adicional al nuevo widget. El espacio adicional se dividirá uniformemente entre todos los widgets que utilicen esta opción.
  • Fill. True si el espacio dado por la opción de expansión está realmente asignado al widget. Este parámetro no tiene ningún efecto si la expansión se establece en false.
  • Padding. Espacio adicional en píxeles para poner entre este widget y sus vecinos, más allá de la cantidad global especificada por la propiedad de espaciado. Si el widget está en uno de los extremos, entonces los píxeles de relleno también se ponen entre él y el borde de referencia.

Tablas

Las tablas proporcionan un método alternativo de posicionamiento, esto es, de poner widgets en la ventana. Proporcionan una rejilla donde se pueden colocar widgets cubriendo cualquier rango de celdas. Por ejemplo, para hacer una tabla con 4 filas y 6 columnas:

var Mi_Tabla = new Table (4,6,true)

El tercer parámetro es el parámetro homogéneo (todas las celdas son del mismo tamaño si es true). Si es false, las celdas tendrán la misma altura que el widget más alto de la fila y la columna será tan ancha como el widget más ancho de la columna.

Siguiendo con el ejemplo de la tabla anterior, el widget de un botón se coloca en la tabla usando:

Mi_Tabla.attach_defaults(Un_Button,2,3,4,6)

Esto posiciona el botón en la tabla de modo que se coloca, respecto a las columnas, entre los límites de la 2ª y la 3ª, y respecto a las filas, entre los límites de la 4ª y la 6ª.

Las columnas se cuentan desde la izquierda y las filas se desde la parte superior. Por lo tanto, la celda superior se indica con 0,1,0,1. Usando esta sintaxis es posible que los widgets ocupen cualquier área especificada de la tabla.

Rejilla: Grid

Posiblemente el sistema de posicionamiento más recomendado, Grid (cuadrícula o rejilla) es un contenedor que organiza los widgets en filas y columnas.

Es muy similar a Table y a Box, pero utiliza el margen de los widgets y las propiedades de expansión en lugar de las propiedades de los widgets contenidos, soportando completamente la gestión de geometría alto x ancho.

Los contenidos (widgets) se agregan usando attach y pueden abarcar varias filas o columnas. También es posible agregar un widget al lado de otro existente, usando attach_next_to. El comportamiento de Grid cuando varios widgets ocupan la misma celda de la cuadrícula no está definido.

Grid también se puede usar como una caja (Box) simplemente usando add, que colocará a los widgets uno junto al otro en la dirección determinada por la propiedad de orientación.

Se posiciona con números enteros que indican la posición (izquierda y arriba) y el tamaño (ancho y alto): attach(widget, left, top, width, height). Por ejemplo:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
 
init
	Gtk.init (ref args)	
	var TestGtk = new Ventana()	
	TestGtk.show_all()	
	Gtk.main()
 
class Ventana : Window
 
	init		
		title = "Genie + Grid"				
		window_position = WindowPosition.CENTER
		destroy.connect(Gtk.main_quit)			
 
		// Grid
		var grid = new Gtk.Grid()
		add (grid)
 
		// 01 __ __
		// __ __ __
		// __ __ __
		var boton1 = new Button.with_label ("1")
		grid.attach(boton1, 0, 0, 1, 1)
 
		// 01__ __ __
		// 02 __ __
		// 02 __ __
		var boton2 = new Button.with_label ("2")
		grid.attach(boton2, 0, 1, 1, 2)
 
		// 01 __ __
		// 02 03 __
		// 02 __ __
		var boton3 = new Button.with_label ("3")
		grid.attach(boton3, 1, 1, 1, 1)
 
		// 01 __ __
		// 02 03 __
		// 02 __ 04
		var boton4 = new Button.with_label ("4")
		grid.attach(boton4, 2, 2, 1, 1)
 
		// 01 05 05
		// 02 03 __
		// 02 __ 04
		var boton5 = new Button.with_label ("5")
		grid.attach_next_to (boton5, boton1, Gtk.PositionType.RIGHT, 2, 1)

Paneles

Gtk.Paned presenta dos paneles dispuestos horizontalmente o verticalmente.

En cada uno de los paneles podemos añadir widgets secundarios separados por un divisor ajustable por el usuario.

Gtk.Paned

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses Gtk
init	
	Gtk.init (ref args)	
	var TestGtk = new Ventana()	
	TestGtk.show_all()	
	Gtk.main()
 
class Ventana : Window
	init		
		title = "Test Genie + GTK"			
		window_position = WindowPosition.CENTER		
		destroy.connect(Gtk.main_quit)
 
		var paned = new Gtk.Paned (Gtk.Orientation.HORIZONTAL)
 
		var image1 = new Gtk.Image ()
		image1.set_from_file ("genielogo.png")
		var image2 = new Gtk.Image ()
		image2.set_from_file ("pythonlogo.png")
 
		paned.add1 (image1)
		paned.add2 (image2)
 
		add (paned)

Un widget contenedor parecido es Gtk.Stack, que solo muestra uno de sus hijos a la vez. Puedes ver un ejemplo en Gtk.Stack y Gtk.show_about_dialog.

Un menú implementa una serie desplegable de componentes estructurados en forma de lista por las que el usuario puede navegar y activar para realizar funciones de la aplicación.

Habitualmente un menú desplegable se sitía en la barra de una ventana, pero también puede estar presente en widgets complejos como ComboBox o Notebook, o puede mostrarse como un elemento emergente en una aplicación (por ejemplo, al presionar el botón secundario del ratón).

Lo creamos con MenuBar:

var BarraMenu = new MenuBar()

Un menú continene una lista de objetos. En una barra de menú, cada palabra es un elemento del menú que inicia o lanza un submenú con cualquier número de objetos propios que se muestran debajo de él cuando se hace clic en su etiqueta. Esto es, los MenuItem(s) aparecen en la barra de menú, y cada uno de ellos invoca su propio submenú.

Más claro en un ejemplo:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses 
	Gtk
 
init
	Gtk.init (ref args)	
	var TestGtk = new Ventana()	
	TestGtk.show_all()	
	Gtk.main()
 
class Ventana : Window
 
	init		
		title = "Genie + Menú"				
		window_position = WindowPosition.CENTER
		set_default_size (350, 270)
		destroy.connect(Gtk.main_quit)
 
		// Grid
		var grid = new Grid()
		add (grid)		
 
		// Barra de Menú
		var barra = new Gtk.MenuBar ()
		grid.attach(barra, 0, 0, 10, 1)
 
		// Elementos principales del Menú: Archivo y Ayuda
		var item_file = new Gtk.MenuItem.with_label ("Archivo")
		barra.add (item_file)		
		var item_help = new Gtk.MenuItem.with_label ("Ayuda")
		barra.add (item_help)
 
		// Submenú de Archivo: Abrir y Salir
		var filemenu = new Gtk.Menu ()		
		item_file.set_submenu (filemenu)				
		var item_open = new Gtk.MenuItem.with_label ("Abrir")
		filemenu.add (item_open)
		var item_exit = new Gtk.MenuItem.with_label ("Salir")
		filemenu.add (item_exit)
 
		// Submenú de Abrir
		var submenu = new Gtk.Menu ()
		item_open.set_submenu (submenu)
		var item_new = new Gtk.MenuItem.with_label ("Nuevo")
		submenu.add (item_new)
		var item_rec = new Gtk.MenuItem.with_label ("Reciente")
		submenu.add (item_rec)

Aquí también podemos usar los mnemonic que ya vimos:

var item_file = new Gtk.MenuItem.with_label ("_Archivo")
item_file.set_use_underline(true)

Podemos incluir una linea de separación entre dos ítems. Por ejemplo, en el código anterior lo podemos insertar entre los ítems 'Abrir' y 'Salir':

var sep = new SeparatorMenuItem()
filemenu.append(sep)

También podemos añadir imágenes. Siguiendo con el ejemplo anterior, añadimos una imagen para el menú 'Abrir':

var item_open = new Gtk.ImageMenuItem.with_label ("Abrir")  // OBSOLETO
var open = new Image.from_icon_name("document-open", IconSize.MENU)
item_open.always_show_image = true
item_open.set_image (open)
filemenu.add (item_open)

Comprobamos que el código funciona, pero se trata de un método obsoleto desde la version 3.10, por lo que mejor lo hacemos así:

// Creamos un contenedor con imagen y etiqueta
var caja = new Box (Orientation.HORIZONTAL, 4)
var open = new Image.from_icon_name("document-open", IconSize.MENU)
var eti = new Label ("Abrir")				
caja.pack_start(open, false, false, 0)
caja.pack_start(eti, false, false, 0)
 
var item_open = new Gtk.MenuItem()		
item_open.add(caja)  // agrega el contenedor al menú Abrir
filemenu.add(item_open)

Así conseguimos un menú como éste:

Ahora solo falta asociar una función a cada ítem, por ejemplo:

	item_exit.activate.connect(salida)
 
def salida()
	stdout.printf("Adiós\n")
	Gtk.main_quit()

Finalmente, incorporando todo lo mencionado y además utilizando Gtk.Tooltip, el código de nuestro menú con imágenes nos queda así:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses Gtk
 
init
	Gtk.init (ref args)	
	var TestGtk = new Ventana()	
	TestGtk.show_all()	
	Gtk.main()
 
class Ventana : Window
 
	init		
		title = "Genie + Menú"				
		window_position = WindowPosition.CENTER
		set_default_size (350, 270)
		destroy.connect(Gtk.main_quit)
 
		// Grid
		var grid = new Grid()
		add (grid)		
 
		// Barra de Menú
		var barra = new Gtk.MenuBar ()
		grid.attach(barra, 0, 0, 10, 1)
 
		// Elementos principales del Menú: Archivo y Ayuda
		var caja_archivo = new Box (Orientation.HORIZONTAL, 4)
		var archivo = new Image.from_icon_name("go-home", IconSize.MENU)
		var eti_archivo = new Label ("_Archivo")
		eti_archivo.set_use_underline(true)
		caja_archivo.pack_start(archivo, false, false, 0)
		caja_archivo.pack_start(eti_archivo, false, false, 0)
		var item_file = new Gtk.MenuItem()				
		item_file.set_tooltip_text ("Crear un nuevo archivo")	
		item_file.add(caja_archivo)
		barra.add (item_file)		
 
		var caja_help = new Box (Orientation.HORIZONTAL, 4)
		var help = new Image.from_icon_name("dialog-information", IconSize.MENU)
		var eti_help = new Label ("_Help")
		eti_help.set_use_underline(true)
		caja_help.pack_start(help, false, false, 0)
		caja_help.pack_start(eti_help, false, false, 0)
		var item_help = new Gtk.MenuItem()
		item_help.set_tooltip_markup("Buscar <b>ayuda</b>")
		item_help.add(caja_help)
		barra.add (item_help)		
 
		// Submenú de Archivo: Abrir y Salir
		var filemenu = new Gtk.Menu ()		
		item_file.set_submenu (filemenu)
 
		var caja = new Box (Orientation.HORIZONTAL, 4)		
		var open = new Image.from_icon_name("document-open", IconSize.MENU)
		var eti = new Label ("Abrir")				
		caja.pack_start(open, false, false, 0)
		caja.pack_start(eti, false, false, 0)		 
		var item_open = new Gtk.MenuItem()		
		item_open.add(caja)  // agrega el contenedor al menú Abrir
		filemenu.add(item_open)	
 
		var sep = new SeparatorMenuItem()
		filemenu.append(sep)
 
		var caja2 = new Box (Orientation.HORIZONTAL, 4)
		var exit = new Image.from_icon_name("exit", IconSize.MENU)
		var eti2 = new Label ("Salir")
		caja2.pack_start(exit, false, false, 0)
		caja2.pack_start(eti2, false, false, 0)
		var item_exit = new Gtk.MenuItem()
		item_exit.add(caja2)		
		filemenu.add (item_exit)
		item_exit.activate.connect(salida)
 
		// Submenú de Abrir
		var submenu = new Gtk.Menu ()
		item_open.set_submenu (submenu)
 
		var caja3 = new Box (Orientation.HORIZONTAL, 4)		
		var nuevo = new Image.from_icon_name("document-new", IconSize.MENU)
		var eti3 = new Label ("Nuevo")
		caja3.pack_start(nuevo, false, false, 0)
		caja3.pack_start(eti3, false, false, 0)		
		var item_new = new Gtk.MenuItem()
		item_new.add(caja3)
		submenu.add (item_new)		
 
		var caja4 = new Box (Orientation.HORIZONTAL, 4)		
		var buscar = new Image.from_icon_name("edit-find", IconSize.MENU)
		var eti4 = new Label ("Buscar")
		caja4.pack_start(buscar, false, false, 0)
		caja4.pack_start(eti4, false, false, 0)		
		var item_bus = new Gtk.MenuItem()
		item_bus.add(caja4)
		submenu.add (item_bus)
 
	def salida()
		stdout.printf("Adiós\n")
		Gtk.main_quit()

Una manera de mejorar el aspecto de la ventana pricipal de nuestra aplicación es sustituir la tradicional barra de menús por un headBar, algo similar a un Box horizontal encabezando la ventana.

Gtk.HeaderBar permite contener otros widgets al principio o al final, mostrar un título y un subtítulo centrados, además de los controles típicos de minimizar, maximizar y cerrar.

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
 
uses Gtk
 
init
	new MyApplication( "test.application",
		ApplicationFlags.FLAGS_NONE
		).run( args )
 
class MyApplication:Gtk.Application
 
	window: Gtk.ApplicationWindow
	icon: Gdk.Pixbuf
 
	construct( application_id:string, flags:ApplicationFlags )
		if !id_is_valid( application_id )
			error( "application id %s is not valid", application_id )
		this.application_id = application_id
		this.flags = flags
 
	def override activate ()
 
		var window = new Gtk.ApplicationWindow( this )
		window.set_default_size (600, 400)
		window.window_position = WindowPosition.CENTER
		window.set_border_width(10)		
 
		headerbar: Gtk.HeaderBar = new Gtk.HeaderBar()
		headerbar.show_close_button = true		
		headerbar.title = "GENIE DOC"
		headerbar.set_subtitle ("Wiki de programación con Genie")
		window.set_titlebar(headerbar)			
 
		inicio: Gtk.Button = new Gtk.Button.with_label ("Abrir")								
		headerbar.pack_start(inicio)
 
		button: Gtk.Button = new Gtk.Button.with_label ("About")								
		headerbar.pack_end(button)
 
		var boton_set = new Button.from_icon_name("document-properties", IconSize.LARGE_TOOLBAR)
		headerbar.pack_end(boton_set)
 
		var image1 = new Gtk.Image ()
		image1.set_from_file ("genielogo2.png")		
 
		vbox: Gtk.Box = new Gtk.Box(Gtk.Orientation.VERTICAL, 0)
		vbox.pack_start(image1, false, false, 0)		
 
		window.add(vbox)		
		window.show_all ()

Diseño CSS

Hemos visto algunas maneras de cambiar el aspecto de los widgets, como por ejemplo con los atributos markup, pero podemos aplicarles directamente propiedades propias de las hojas de estilo CSS para cambiar su aspecto.

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses Gtk
 
init
	new MyApplication( "test.application",
		ApplicationFlags.FLAGS_NONE
		).run( args )
 
class MyApplication:Gtk.Application
 
	construct( application_id:string, flags:ApplicationFlags )
		if !id_is_valid( application_id )
			error( "application id %s is not valid", application_id )
		this.application_id = application_id
		this.flags = flags
 
	def override activate ()
 
		var window = new Gtk.ApplicationWindow( this )      
		window.window_position = WindowPosition.CENTER
		window.set_border_width(10)		
		window.get_style_context().add_class("mi_ventana")
 
		var grid = new Gtk.Grid()       
 
		var boton1 = new Gtk.Button ()      
		boton1.label = "Boton 1"
		boton1.margin = 10
		boton1.get_style_context().add_class("boton1") 
 
		var boton2 = new Gtk.Button ()      
		boton2.label = "Boton 2"
		boton2.margin = 10      
		boton2.get_style_context().add_class("boton2")
 
		var etiqueta = new Gtk.Label ("Soy una etiqueta con diseño")		
		boton2.margin = 10      
		etiqueta.get_style_context().add_class("etiqueta")		
 
		var css = """
		.mi_ventana {				
		background: linear-gradient(to right, rgba(180,255,0,0.5), rgba(180,255,0,1));
		}		
		.boton1 {
		background-color: white;
		color: black;
		font-size: 14px;
		border: 2px solid #4CAF50;
		border-radius: 50%;
		box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2), 0 6px 20px 0 rgba(0,0,0,0.19);
		}
		.boton2 {
		background: red;
		color: #FFFF00;
		padding: 32px 16px;
		font-size: 14px;
		border-radius: 12px;
		box-shadow: 0 9px #999;}
		.etiqueta {
		color: #0101DF;
		font-size: 24px;
		padding: 10px;
		font-weight: bold;
		font-family: "Times New Roman", Times, serif;
		text-shadow: 3px 2px red;}
		"""
 
		var provider = new Gtk.CssProvider()
 
		try
			provider.load_from_data(css, css.length)
		except e: GLib.Error
			stderr.printf ("Hoja de estilo no cargada: %s\n", e.message)		
 
		Gtk.StyleContext.add_provider_for_screen(
			Gdk.Screen.get_default(),
			provider,
			Gtk.STYLE_PROVIDER_PRIORITY_USER)	
 
		grid.attach(boton1, 0, 0, 2, 1)
		grid.attach_next_to (boton2, boton1, Gtk.PositionType.RIGHT, 2, 1)		
		grid.attach(etiqueta, 0, 3, 4, 1)		
 
		window.add(grid)
		window.show_all()   

También podemos separar las características de diseño en un archivo independiente, para ser leído por nuestro código. El siguiente ejemplo, produce el mismo resultado que el ejemplo anterior:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses Gtk
 
init
	new MyApplication( "test.application",
		ApplicationFlags.FLAGS_NONE
		).run( args )
 
class MyApplication:Gtk.Application
 
	construct( application_id:string, flags:ApplicationFlags )
		if !id_is_valid( application_id )
			error( "application id %s is not valid", application_id )
		this.application_id = application_id
		this.flags = flags
 
	def override activate ()
 
		var window = new Gtk.ApplicationWindow( this )      
		window.window_position = WindowPosition.CENTER
		window.set_border_width(10)		
		window.get_style_context().add_class("mi_ventana")
 
		var grid = new Gtk.Grid()       
 
		var boton1 = new Gtk.Button ()      
		boton1.label = "Boton 1"
		boton1.margin = 10
		boton1.get_style_context().add_class("boton1") 
 
		var boton2 = new Gtk.Button ()      
		boton2.label = "Boton 2"
		boton2.margin = 10      
		boton2.get_style_context().add_class("boton2")
 
		var etiqueta = new Gtk.Label ("Soy una etiqueta con diseño")		
		boton2.margin = 10      
		etiqueta.get_style_context().add_class("etiqueta")		
 
		path: string = "custom.css"
		var provider = new Gtk.CssProvider()
 
		try
			provider.load_from_path(path)
		except e: GLib.Error
			stderr.printf ("Hoja de estilo no cargada: %s\n", e.message)		
 
		Gtk.StyleContext.add_provider_for_screen(
			Gdk.Screen.get_default(),
			provider,
			Gtk.STYLE_PROVIDER_PRIORITY_USER)	
 
		grid.attach(boton1, 0, 0, 2, 1)
		grid.attach_next_to (boton2, boton1, Gtk.PositionType.RIGHT, 2, 1)		
		grid.attach(etiqueta, 0, 3, 4, 1)		
 
		window.add(grid)
		window.show_all() 
Descarga: custom.css
.mi_ventana {				
background: linear-gradient(to right, rgba(180,255,0,0.5), rgba(180,255,0,1));
}		
.boton1 {
background-color: white;
color: black;
font-size: 14px;
border: 2px solid #4CAF50;
border-radius: 50%;
box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2), 0 6px 20px 0 rgba(0,0,0,0.19);
}
.boton2 {
background: red;
color: #FFFF00;
padding: 32px 16px;
font-size: 14px;
border-radius: 12px;
box-shadow: 0 9px #999;}
.etiqueta {
color: #0101DF;
font-size: 24px;
padding: 10px;
font-weight: bold;
font-family: "Times New Roman", Times, serif;
text-shadow: 3px 2px red;}

Glade

Hasta ahora hemos visto la manera convencional de utilizar Gtk, pero también es posible hacerlo arrastrando y soltando ventanas y widgets en una interfaz gráfica más agradable gracias a Glade, un programa de diseño de interfaz de usuario para aplicaciones Gtk+.

Glade es una herramienta multiplataforma (software libre publicado bajo licencia GNU GPL) que permite el desarrollo rápido y fácil de interfaces de usuario GTK+ en numerosos lenguajes de programación, incluido, por supuesto, Genie.

Glade produce un archivo XML que puede ser leído por la clase Gtk.Builder para construir la interfaz gráfica Gtk. Sin embargo, para algunos esta solución va contra el concepto de lenguaje compilado puesto que el archivo XML tiene que ser cargado, analizado e interpretado en tiempo de ejecución del código.

Sin embargo, a este inconveniente podemos contraponer algunas ventajas: Proporciona un entorno gráfico que permite al programador agregar, quitar y modificar rápidamente widgets, lo que se hace de una manera muy simple, pero potente, facilitando a los desarrolladores novatos diseñar una interfaz Gtk en poco tiempo. Además Glade crea una separación entre el diseño visual de la aplicación y su comportamiento, lo que facilita su revisión y mantenimiento. Otra ventaja: al cargarse el archivo XML en tiempo de ejecución, se pueden hacer ciertos ajustes en la interfaz sin tener que volver a compilar el código. Pros y contras, pero supongo que cada uno debe valorar hasta que punto Glade conviene a su aplicación o no.

Para instalarlo en linux: $ sudo apt-get install glade

A la hora de compilarlo, le especificamos a valac: --pkg gmodule-2.0

Vamos a describir algunas de sus posibilidades a lo largo de un ejemplo (hay otro ejemplo disponible en los ejemplos de código).

En primer lugar, utilizaremos Glade para construir una interfaz de usuario simple con una ventana y dos botones que se utilizará después en nuestro programa. Será la estructura básica o el esqueleto del código posterior. Se puede ver cada paso en el siguiente video:

Finalmente, en la misma carpeta donde se encuentra nuestro código, guardamos el archivo generado por Glade, que debe ser más o menos así:

Descarga: sample.ui
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.0 -->
<interface>
  <requires lib="gtk+" version="3.20"/>
  <object class="GtkWindow" id="window">
    <property name="can_focus">False</property>
    <property name="border_width">10</property>
    <property name="title" translatable="yes">Test Genie + Glade</property>
    <property name="window_position">center</property>
    <property name="default_width">300</property>
    <property name="default_height">70</property>
    <signal name="destroy" handler="gtk_main_quit" swapped="no"/>
    <child>
      <object class="GtkBox" id="box1">
        <property name="visible">True</property>
        <property name="can_focus">False</property>
        <property name="spacing">4</property>
        <property name="homogeneous">True</property>
        <child>
          <object class="GtkButton" id="button1">
            <property name="label" translatable="yes">Click, por favor</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <signal name="clicked" handler="on_button1_clicked" swapped="no"/>
          </object>
          <packing>
            <property name="expand">True</property>
            <property name="fill">True</property>
            <property name="position">0</property>
          </packing>
        </child>
        <child>
          <object class="GtkButton" id="button2">
            <property name="label" translatable="yes">Y a mi también</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <signal name="clicked" handler="on_button2_clicked" swapped="no"/>
          </object>
          <packing>
            <property name="expand">True</property>
            <property name="fill">True</property>
            <property name="position">1</property>
          </packing>
        </child>
      </object>
    </child>
  </object>
</interface>

Después ya podemos a escribir el código, teniendo en cuenta el nombre de los objetos creados en Glade (para ello, viene bien tener abierto a la vez el archivo XML, por defecto con extensión ui).

El elemento clave en nuestro código será Gtk.Builder, que será el encargado de invocar al archivo de Glade y hacer referencia a todos los objetos construidos.

Con el método get_object(“nombre_widget”) de Gtk.Builder accedemos a la ventana creada para mostrar todos los objetos que contiene. Y la función connect_signals() de Gtk.Builder permite conectar eventos a las señales de algunos objetos.

Finalmente, el código queda así (date cuenta lo breve que es gracias a Glade):

// compila con valac --pkg gtk+-3.0 --pkg gmodule-2.0 nombre_archivo.gs
uses Gtk
 
def on_button1_clicked (source:Button)
	source.label = "¡Gracias!"
 
def on_button2_clicked (source:Button)
	source.label = "Adiós"
 
init
	Gtk.init (ref args)		
 
	var builder = new Builder ()
	try
		builder.add_from_file ("sample.ui")
	except ex : Error
		print "Error: %s", ex.message		
		Process.exit(-1)
 
	builder.connect_signals (null)	
 
	var ventana = builder.get_object ("window") as Window	
 
	ventana.show_all ()	
	Gtk.main ()

Ejemplos

A continuación se exponen algunos ejemplos de código, más o menos sencillos, que utilizan Gtk+. Se pueden ver más ejemplos de cómo utilizar Gtk+ con Genie en la sección Ejemplos de código.

Spaces to Tab

Spaces To Tab es una sencilla aplicación que sustituye todos los bloques de 4 espacios por tabuladores en cualquier archivo de texto, lo que puede resultar muy útil cuando se trabaja con lenguajes de programación identados como Genie.

Nota: He compilado y ejecutado el código sin errores en Debian Testing con Gnome, pero sin embargo en Xubuntu 16.10 la compilación ha devuelto algunas advertencias y la aplicación no funciona correctamente. Todavía no he resuelto este problema, por lo que es posible que no funcione correctamente en algunos entornos. Si lo pruebas en tu sistema, se agradece información sobre su funcionamiento para recopilar más datos (gracias!).

Descarga: spacestotab.gs
// compila con valac --pkg gtk+-3.0 spacestotab.gs
uses Gtk
 
init
	Intl.setlocale( LocaleCategory.ALL, "" )
	new MiApp( "spaces.to.tab",
		ApplicationFlags.HANDLES_OPEN).run(args)
 
class MiApp:Gtk.Application
 
	construct( application_id:string, flags:ApplicationFlags )
		if !id_is_valid( application_id )
			error( "application id %s is not valid", application_id )
		this.application_id = application_id
		this.flags = flags
 
	def override activate ()
 
		dialogo_open: Gtk.FileChooserDialog
		filename: string
		texto_fuente:string
		confirma: Gtk.MessageDialog
 
		// VENTANA DE INICIO
		var ventana = new Gtk.ApplicationWindow (this)
		ventana.window_position = WindowPosition.CENTER
		ventana.border_width = 10
		ventana.set_default_size (400, 200)
		ventana.destroy.connect(Gtk.main_quit)
 
		var header = new Gtk.HeaderBar()
		header.show_close_button = true
		header.title = "SPACES TO TAB"
		header.hexpand = true
 
		ventana.set_titlebar(header)
		var grid = new Gtk.Grid()
		ventana.add (grid)
 
		var dialogo_inicio = new Gtk.Dialog.with_buttons ("SPACES TO TAB", ventana,
			Gtk.DialogFlags.DESTROY_WITH_PARENT,
			"gtk-help", Gtk.ResponseType.HELP,
			"gtk-quit", Gtk.ResponseType.CLOSE,
			"gtk-open", Gtk.ResponseType.APPLY)
		var content_area = dialogo_inicio.get_content_area ()
		content_area.set_spacing (10)
		content_area.set_border_width (10)
 
		var box_inicio= new Gtk.Box (Gtk.Orientation.VERTICAL, 10)
		content_area.add (box_inicio)
 
		var titulo_inicio = new Gtk.Label(
			"<span foreground='blue' size='xx-large'>Spaces To Tab</span>")
		titulo_inicio.set_use_markup (true)
		box_inicio.pack_start(titulo_inicio)
 
		var label1_inicio = new Gtk.Label("Versión 0.0.1")
		label1_inicio.set_use_markup (true)
		box_inicio.pack_start(label1_inicio)
 
		var label2_inicio = new Gtk.Label(
			"<small>Free Software - GNU General Public License</small>")
		label2_inicio.set_use_markup (true)
		box_inicio.pack_start(label2_inicio)
 
		var label3_inicio = new Gtk.Label("<small>Copyleft - All Wrongs Reserved</small>")
		label3_inicio.set_use_markup (true)
		box_inicio.pack_start(label3_inicio)
 
		var label4_inicio = new Gtk.Label("2017 - Jesús Cuerda")
		box_inicio.pack_start(label4_inicio)
 
		var label5_inicio = new Gtk.Label(
			"<a href='http://genie.webierta.skn1.com'>Genie Doc</a>")
		label5_inicio.set_use_markup (true)
		label5_inicio.set_margin_bottom (20)
		box_inicio.pack_start(label5_inicio)
 
		dialogo_inicio.show_all()
 
		// BUCLE DE DIALOGO DE INICIO
		welcome: bool = true
		do
			inicio: int = dialogo_inicio.run()
 
			if inicio == Gtk.ResponseType.APPLY   // BOTON ABRIR
 
				// DIALOGO PARA SELECCIONAR ARCHIVO
				dialogo_open = new FileChooserDialog ("Selecciona un archivo",
					dialogo_inicio, Gtk.FileChooserAction.OPEN,
					"_Cancelar",Gtk.ResponseType.CANCEL,
					"_Abrir", Gtk.ResponseType.ACCEPT)
				dialogo_open.select_multiple = false
				dialogo_open.set_modal(true)
				var filtro_txt = new Gtk.FileFilter ()
				dialogo_open.set_filter (filtro_txt)
				filtro_txt.add_mime_type ("text/plain")
				res: int = dialogo_open.run()
 
				if res == Gtk.ResponseType.ACCEPT  // SELECCIONA UN ARCHIVO
 
					// ABRE EL ARCHIVO
					filename = dialogo_open.get_filename()
					len:ulong
					try
						FileUtils.get_contents(filename, out texto_fuente, out len)
					except e: Error
						msg_error: string = "Error: " + e.message
						var noti = new MessageDialog(ventana,
							Gtk.DialogFlags.MODAL,
							Gtk.MessageType.ERROR,
							Gtk.ButtonsType.CLOSE,
							msg_error)
						var res_noti = noti.run()
						if res_noti == Gtk.ResponseType.CLOSE
							noti.destroy()
 
					// HAY BLOQUES DE 4 ESPACIOS
					if texto_fuente.contains ("    ")
 
						// PIDE CONFIRMACIÓN
						msg_confirma: string = ("¿Modificar el archivo\n"
							+ filename + "\n"
							+ "para sustituir todos los bloques de 4 espacios\n"
							+ "por tabuladores?")
						confirma = new MessageDialog(dialogo_inicio,
							Gtk.DialogFlags.MODAL,
							Gtk.MessageType.QUESTION,
							Gtk.ButtonsType.OK_CANCEL,
							msg_confirma)
						var res_confirma = confirma.run()
						// SI CONFIRMA
						if res_confirma == Gtk.ResponseType.OK
							//dialogo_open.set_do_overwrite_confirmation(true)
							texto_nuevo: string = texto_fuente.replace("    ","\t")
							try
								FileUtils.set_contents(filename, texto_nuevo)
							except e: Error
								msg_error: string = "Error: " + e.message
								var noti = new MessageDialog(ventana,
									Gtk.DialogFlags.MODAL,
									Gtk.MessageType.ERROR,
									Gtk.ButtonsType.CLOSE,
									msg_error)
								var res_noti = noti.run()
								if res_noti == Gtk.ResponseType.CLOSE
									noti.destroy()
							confirma.destroy()
							// MENSAJE DE TRABAJO REALIZADO
							msg_hecho: string = ("Cambios realizados")
							var noti_hecho = new MessageDialog(dialogo_open,
								Gtk.DialogFlags.MODAL,
								Gtk.MessageType.INFO,
								Gtk.ButtonsType.CLOSE,
								msg_hecho)
							var res_hecho = noti_hecho.run()
							if res_hecho == Gtk.ResponseType.CLOSE
								noti_hecho.destroy()
							dialogo_open.destroy()
							welcome = true
						// NO CONFIRMA
						else
							confirma.destroy()
							dialogo_open.destroy()
							welcome = true
 
					else  // NO HAY BLOQUES DE 4 ESPACIOS
						msg_nada: string = ("Archivo sin bloques de 4 espacios.\n\n"
							+"¡Nada que hacer!")
						var noti_nada = new Gtk.MessageDialog(dialogo_open,
							Gtk.DialogFlags.MODAL,
							Gtk.MessageType.INFO,
							Gtk.ButtonsType.CLOSE,
							msg_nada)
						var res_nada = noti_nada.run()
						if res_nada == Gtk.ResponseType.CLOSE
							noti_nada.destroy()
						dialogo_open.destroy()
						welcome = true
 
				else  // NO SELECCIONA NINGUN ARCHIVO
					dialogo_open.destroy()
					welcome = true
 
				grid.show_all ()
				//welcome = false
 
			else if inicio == Gtk.ResponseType.HELP   // BOTON AYUDA
				msg_help: string = ("Spaces To Tab es una sencilla aplicación\n"
				+ "que sustituye todos los bloques de 4 espacios\n"
				+ "por tabuladores en cualquier archivo de texto.\n\n"
				+ "Esta aplicación se ha desarrollado para Genie Doc,\n"
				+ "la Wiki de programación con Genie, un fantástico\n"
				+ "lenguaje de programación identado.\n")
				var help = new Gtk.MessageDialog(dialogo_inicio,
					Gtk.DialogFlags.MODAL,
					Gtk.MessageType.INFO,
					Gtk.ButtonsType.CLOSE,
					msg_help)
				var res_help = help.run()
				if res_help == Gtk.ResponseType.CLOSE
					help.destroy()
				grid.show_all ()
 
			else  // BOTON SALIR
				dialogo_inicio.destroy()
				welcome = false
				Process.exit (0)
 
		while welcome == true
 
		dialogo_inicio.destroy()
		ventana.show_all()
 

Captura de Escritorio

El siguiente código utiliza Gdk.get_default_root_window() para acceder al escritorio y conecta un botón para hacer una captura de pantalla:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses Gtk
 
init	
	Gtk.init (ref args)	
	var test = new TestVentana ()	
	test.show_all ()
	Gtk.main ()
 
class TestVentana: Window
	init		
		title = "Escritorio"		
		border_width = 10
		window_position = WindowPosition.CENTER		
		destroy.connect(Gtk.main_quit)
 
		var grid = new Gtk.Grid()
		add (grid)
 
		var boton = new Button.with_label ("Capturar")		
		boton.clicked.connect(btn)
		boton.border_width = 10	
		grid.attach(boton, 0, 0, 2, 1)
 
		var boton_salir = new Button.with_label ("Salir")
		boton_salir.clicked.connect(btn_salir)
		boton_salir.border_width = 10	
		grid.attach(boton_salir, 2, 0, 2, 1)			
 
	def btn(btn:Button)
		escritorio: Gdk.Window = Gdk.get_default_root_window()
		ancho: int = escritorio.get_width()
		alto: int = escritorio.get_height()	
		screenshot: Gdk.Pixbuf = Gdk.pixbuf_get_from_window(escritorio, 0, 0, ancho, alto)
		try
			screenshot.save("screenshot.png","png")
		except e: GLib.Error
			stderr.printf ("Error: %s\n", e.message)
 
	def btn_salir(btn:Button)
		Gtk.main_quit()

El mismo código adaptado a Gtk.Application:

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
uses Gtk
 
init
	new MyApplication( "captura.escritorio",
		ApplicationFlags.FLAGS_NONE
		).run( args )
 
class MyApplication:Gtk.Application
 
	construct( application_id:string, flags:ApplicationFlags )
		if !id_is_valid( application_id )
			error( "application id %s is not valid", application_id )
		this.application_id = application_id
		this.flags = flags
 
	def override activate ()
		var window = new Gtk.ApplicationWindow( this )
		window.title = "Escritorio"				
		window.border_width = 10		
		window.window_position = WindowPosition.CENTER		
 
		var grid = new Gtk.Grid()
		window.add (grid)
 
		var boton = new Button.with_label ("Capturar")		
		boton.clicked.connect(btn)
		boton.border_width = 10	
		grid.attach(boton, 0, 0, 2, 1)
 
		var boton_salir = new Button.with_label ("Salir")
		boton_salir.clicked.connect(btn_salir)
		boton_salir.border_width = 10	
		grid.attach(boton_salir, 2, 0, 2, 1)
 
		window.show_all()		
 
	def btn(btn:Button)
		escritorio: Gdk.Window = Gdk.get_default_root_window()
		ancho: int = escritorio.get_width()
		alto: int = escritorio.get_height()	
		screenshot: Gdk.Pixbuf = Gdk.pixbuf_get_from_window(escritorio, 0, 0, ancho, alto)
		try
			screenshot.save("screenshot.png","png")
		except e: GLib.Error
			stderr.printf ("Error: %s\n", e.message)
 
	def btn_salir(btn:Button)		
		this.quit()

Gtk.Stack y Gtk.show_about_dialog

Ejemplo de Gtk.Stack, un widget contenedor que solo muestra uno de sus hijos a la vez, y Gtk.show_about_dialog, una función que muestra un diálogo con información sobre la aplicación. En este caso se ha utilizado Gtk.Application.

// compila con valac --pkg gtk+-3.0 nombre_archivo.gs
 
uses Gtk
 
init
	new MyApplication( "test.application",
		ApplicationFlags.FLAGS_NONE
		).run( args )
 
class MyApplication:Gtk.Application
 
	window: Gtk.ApplicationWindow
	icon: Gdk.Pixbuf
 
	construct( application_id:string, flags:ApplicationFlags )
		if !id_is_valid( application_id )
			error( "application id %s is not valid", application_id )
		this.application_id = application_id
		this.flags = flags
 
	def override activate ()
 
		var window = new Gtk.ApplicationWindow( this )
		window.set_default_size (800, 600)
		window.window_position = WindowPosition.CENTER
		window.set_border_width(10)		
 
		headerbar: Gtk.HeaderBar = new Gtk.HeaderBar()
		headerbar.show_close_button = true		
		headerbar.title = "GENIE DOC"
		window.set_titlebar(headerbar)			
 
		button: Gtk.Button = new Gtk.Button.with_label ("About")				
		button.clicked.connect(acercade)		
		headerbar.pack_end(button)
 
		stack: Gtk.Stack = new Gtk.Stack()
		stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT)
 
		var image1 = new Gtk.Image ()
		image1.set_from_file ("genielogo2.png")
		stack.add_titled(image1, "label1", "GENIE")
 
		var image2 = new Gtk.Image ()
		image2.set_from_file ("pythonlogo.png")		
		stack.add_titled(image2, "label2", "PYTHON")		
 
		stack_switcher: Gtk.StackSwitcher = new Gtk.StackSwitcher()
		stack_switcher.halign = Gtk.Align.CENTER
		stack_switcher.set_stack(stack)
 
		vbox: Gtk.Box = new Gtk.Box(Gtk.Orientation.VERTICAL, 0)
		vbox.pack_start(stack_switcher, false, false, 0)
		vbox.pack_start(stack, false, false, 10)
 
		window.add(vbox)		
		window.show_all ()
 
	def acercade(button:Button)
 
		try		
			icon = new Gdk.Pixbuf.from_file ("genie64.png")
		except e : GLib.Error
			stderr.printf ("Error: %s\n", e.message)		
		authors: array of string = { "JESUS CUERDA", null }		
		license: string = "CC Attribution 4.0 International"
		Gtk.show_about_dialog (window,
			"program-name", ("Wiki Genie Doc"),
			"logo", icon,
			"copyright", ("CC BY 4.0 2017 Wiki GENIE DOC"),
			"license", license,
			"authors", authors,
			"website", "http://genie.webierta.skn1.com",
			"website-label", ("Wiki Genie Doc"),
			null)
🔝