GTK, nociones de programación básicas

GTK es una biblioteca de componentes usada para hacer aplicaciones gráficas, es decir, aplicaciones de ordenador con ventanas, botones, etiquetas y esas cosas. La gente joven tal vez no sepa esto, pero antes las aplicaciones de ordenador (como los reproductores de música, las aplicaciones de chat o los organizadores de imágenes) no se programaban en HTML, sino que se hacían mediante programas que había que instalar en el ordenador (como cuando instalas Instagram en el móvil).

GTK es una de las bibliotecas predominantes en el mundo del software libre, ya que proyectos como el entorno de escritorio GNOME o el entorno de escritorio Xfce lo utilizan como base para muchas de las aplicaciones y herramientas que se instalan con el entorno de escritorio. Sin embargo, GTK es multiplataforma y se pueden compilar aplicaciones para Microsoft Windows y macOS que también utilicen esta biblioteca de componentes gráficos.

Cómo se programa GTK

GTK es una biblioteca que tiene bindings para muchos lenguajes de programación. El lenguaje tradicional es C, pero en realidad muchos lenguajes de programación tienen alguna forma de interactuar mediante funciones con los servicios de GTK. A saber: Python (PyGObject, que ha sustituido al viejo PyGTK), JavaScript (GJS), C++ (gtkmm), Vala o Rust (gtk-rs).

GTK es orientado a objetos, incluso en los lenguajes de programación en los que no hay orientación a objetos como característica del lenguaje. En este tipo de lenguajes de programación, la orientación a objetos suele ser primitiva y como la implementaría alguien de forma ingenua. Por ejemplo, en el lenguaje de programación C, lo normal es que los métodos de una clase se implementen como una función que tiene como nombre [clase]_[metodo] y que como primer parámetro acepte la instancia. Así que algo que en un lenguaje de programación más orientado a objetos como Java podría ser:

Animal ani = new Animal("Bobby");
ani.alimentar("Pienso");

En C tendría una forma como la siguiente:

Animal* ani = animal_new("Bobby");
animal_alimentar(ani, "Pienso");

Esto forma parte de GObject, que es una biblioteca que hay por debajo de GTK y que de hecho sirve para más cosas, pero eso es material para otro día. Lo importante es que en GTK los componentes (botones, ventanas, etc) usan una jerarquía de objetos con opción a herencia (así que una GtkWindow es un tipo de GtkWidget, y un GtkLinkButton es un tipo de GtkButton), y con opción a implementar interfaces (así que un GtkEntry, que es el nombre que recibe el campo de texto, es un tipo de GtkEditable, como lo son otras clases que implementan esta interfaz).

En el sistema de objetos de GTK, también tenemos otras cosas, como:

  • Métodos: como el método present() de la clase Gtk.Window, que es el que usaríamos para mostrar una ventana que previamente no era visible.
  • Propiedades: podemos entenderla como los atributos de las clases. Por ejemplo, la propiedad label permite obtener o cambiar el texto de una etiqueta o de un botón.
  • Constructores: que permiten fabricar instancias. Una clase puede tener varios, que podrían crear de forma especializada un mismo objeto.

En GTK, las clases también tienen señales. Los puristas de GTK dirán que no es lo mismo que un evento, pero podemos entenderlo como un concepto muy similar. Si fabricas una función y la agregas como callback de una señal mediante una conexión, cuando esa clase dispare una señal, se llamará a tu método. Si has programado JavaScript, te sonará mucho a su sistema de eventos. Por ejemplo, un botón dispara una señal llamada clicked cada vez que se hace clic sobre él, así que si agregamos un callback a la señal clicked de un botón, cada vez que ese botón se pulse se llamará a nuestra función.

Un ejemplo en Python

Voy a proponer un ejemplo en Python, porque será más fácil de contar. Si alguien se atreve, puede aprender a programar GTK en lenguajes como C, Vala o Rust.

Para importar GTK en Python hoy día lo normal es utilizar la siguiente construcción. Incluso si es tu primera vez viendo esta biblioteca, te pido que leas despacio cada una de las siguientes líneas y verás que es muy intuitivo lo que hace, que no es más que asegurarse de que tienes una versión de GTK mayor o igual a la esperada y traerse el espacio de nombres del repositorio:

import gi
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk

Vamos a crear una aplicación GTK y ejecutarla. En GTK, una aplicación es una clase que nos proporciona cierta integración con el lenguaje de programación (por ejemplo, nos permite entregarle el control del programa a GTK con el fin de que lleve el event loop por nosotros), o con el entorno de escritorio (por ejemplo, establecer el nombre de la aplicación que se va a mostrar en notificaciones o lanzadores de aplicaciones).

app = Gtk.Application()
app.run()

Sin embargo, si ejecutamos este programa Python con estas cinco líneas, no vamos a ver nada. Esto es porque la aplicación no hace absolutamente nada. Tendremos que crear algún tipo de ventana o algo.

Cuando una aplicación GTK se inicia, dispara una señal llamada activate, así que vamos a crear un callback para esta señal y conectarlo. El callback activate nos entrega como parámetro una referencia a la aplicación. El código nuevo está marcado con negrita:

def on_activate(app):
  print('Hola desde mi aplicación')
app = Gtk.Application()
app.connect('activate', on_activate)
app.run()

Si ahora intentas ejecutar este programa Python, sigues sin ver nada, pero al menos aparece nuestro mensaje por la consola del sistema. Es un avance.

Vamos a crear ahora una ventana, a darle un tamaño y título de ventana, y a presentarla. Modificando el evento, puedo crear una ApplicationWindow. Es una ventana conectable a una aplicación para indicar que pertenece a esta.

def on_activate(app):
    win = Gtk.ApplicationWindow()
    win.set_application(app)
    win.set_title('Mi programa')
    win.set_default_size(400, 400)
    win.present()
  • Primero creamos la ApplicationWindow en la variable win.
  • Luego llamo a la función set_application para indicar a qué aplicación pertenece la ventana.
  • Después llamo a set_title para establecer el título de la barra de título.
  • No me olvido de llamar a set_default_size para establecer el tamaño por defecto de la ventana.
  • Y finalmente llamo al método present para hacer visible la ventana.

¿El resultado? Una ventana, como puedes imaginar.

Una ventana de ordenador en blanco con el t́itulo "Mi programa" en la barra de título

Esta ventana no tiene nada en su interior todavía, pero eso lo podemos arreglar. En GTK, muchos componentes pueden tener componentes en su interior. Una ventana puede poseer el elemento de interfaz que decora. Un botón contiene normalmente la etiqueta de texto con el texto del botón, o el icono.

Las ventanas pueden tener un hijo (y solo un hijo). Si te estás preguntando cómo es posible entonces hacer ventanas que tengan barra de herramientas, una barra lateral, una parte principal… es porque hay componentes que nos permiten hacer trampa, como el Box, que es un widget que permite agrupar más de un hijo. Así que lo normal podría ser ponerle a una ventana un hijo que sea un Box, y ya a ese Box empezar a meterle barra de herramientas, área principal, barra de estado…

Pero para cerrar este tutorial vamos a ponerle a la ventana un simple botón que muestre el valor de un contador. Agrega este código en la función on_activate justo antes de llamar al método present de la ventana:

value = 0
button = Gtk.Button()
button.set_label(f"{value}")
win.set_child(button)
win.present()

De nuevo, creo que en este caso las líneas de código son bastante auto-explicativas de por sí. Declaramos una variable llamada value, luego instanciamos un nuevo Gtk.Button, le ponemos el valor como label, y establecemos el botón como hijo de la ventana.

La ventana pero ahora muestra un número en su interior.

Para que este botón sea útil, vamos a agregarle un callback que escuche al evento clicked e incremente y actualice su valor al hacer clic. Agrega el siguiente código también antes del present.

def increment():
  nonlocal value
  value += 1
  button.set_label(f"{value}")
button.connect('clicked', lambda x: increment())
win.present()

En este caso, la función increment es la que hace todo esto. Utilizo nonlocal porque esto es una función dentro de otra función (recuerda, la función increment está dentro de la función on_activate y quiero poder tocar su valor).

La llamada connect hace que la función se llame cuando se pulsa el botón. No pongo directamente la función sino que uso una lambda, porque la señal clicked nos entrega un parámetro y yo lo quiero descartar silenciosamente. Con esto ahora tendremos un botón que cuando se pulsa, cambia su valor.

Una ventana muy parecida a la del ejemplo anterior muestra un número en su interior, pero este número es mayor

Ya os contaré más cosas sobre GTK a medida que las vaya queriendo dejar escritas por aquí.