Créer une interface graphique GTK en Haskell
On associe souvent Haskell au calcul pur, aux types et à la ligne de commande. Pourtant, rien n’empêche d’écrire une interface graphique en Haskell. La bibliothèque GTK, qui motorise le bureau GNOME et d’innombrables applications Linux, dispose de bindings Haskell mûrs et confortables. Dans ce tutoriel, nous allons construire une petite fenêtre avec un bouton et brancher une action dessus, ce qui suffit à comprendre le modèle événementiel de GTK.
gtk2hs ou haskell-gi ?
Historiquement, les bindings GTK pour Haskell portaient le nom de gtk2hs. C’est le projet qui a longtemps servi de référence, exposant l’API de GTK 2 sous forme de modules Haskell. Il fonctionne toujours et beaucoup de tutoriels s’y réfèrent.
Le projet a ensuite évolué vers haskell-gi, qui génère automatiquement les bindings à partir des fichiers d’introspection GObject Introspection. C’est aujourd’hui l’approche recommandée pour cibler GTK 3 ou GTK 4 : le paquet s’appelle gi-gtk. Les deux partagent la même philosophie ; nous montrerons ici les deux styles pour que vous reconnaissiez le code que vous croiserez.
Installer les dépendances
GTK est une bibliothèque C : il faut donc que ses en-têtes de développement soient présentes sur le système avant de compiler le binding.
# Debian / Ubuntu
sudo apt install libgtk-3-dev
# Fedora
sudo dnf install gtk3-devel
Ensuite, on ajoute le paquet Haskell au projet. Avec Stack, on déclare la dépendance dans le fichier package.yaml :
dependencies:
- base
- gi-gtk
- haskell-gi-base
Avec Cabal, on ajoute simplement gi-gtk à la section build-depends du fichier .cabal. La compilation télécharge et bâtit le binding ; la première fois peut être longue, c’est normal.
Une première fenêtre
Voici le squelette minimal d’une application gi-gtk. On initialise GTK, on crée une fenêtre, on demande son affichage et on lance la boucle d’événements.
{-# LANGUAGE OverloadedStrings #-}
module Main where
import qualified GI.Gtk as Gtk
main :: IO ()
main = do
_ <- Gtk.init Nothing
fenetre <- Gtk.windowNew Gtk.WindowTypeToplevel
Gtk.setWindowTitle fenetre "Bonjour Coder Studio"
Gtk.setWindowDefaultWidth fenetre 320
Gtk.setWindowDefaultHeight fenetre 200
Gtk.widgetShowAll fenetre
Gtk.main
Deux points méritent l’attention. L’extension OverloadedStrings permet d’écrire les titres directement entre guillemets, car GTK attend des chaînes Text et non des String. Et surtout, Gtk.main est une boucle bloquante : c’est elle qui attend les clics, les frappes et les redessins. Tant qu’on ne la quitte pas, la fenêtre reste vivante.
Fermer proprement l’application
En l’état, cliquer sur la croix ferme la fenêtre mais ne rend pas la main au programme. Il faut connecter le signal destroy de la fenêtre à l’arrêt de la boucle GTK.
import Data.GI.Base (on)
_ <- on fenetre #destroy Gtk.mainQuit
La fonction on est le pont central du modèle événementiel : elle relie un signal émis par un widget à une action Haskell. Ici, quand la fenêtre émet destroy, on appelle mainQuit, ce qui fait retourner Gtk.main et termine main.
Ajouter un bouton et un callback
Passons à l’essentiel : réagir à une interaction. On crée un bouton, on le place dans la fenêtre, puis on branche son signal clicked sur une fonction.
bouton <- Gtk.buttonNewWithLabel "Cliquez-moi"
Gtk.containerAdd fenetre bouton
_ <- on bouton #clicked $ do
putStrLn "Le bouton a été cliqué !"
Gtk.setButtonLabel bouton "Merci !"
Gtk.widgetShowAll fenetre
À chaque clic, le callback s’exécute : il affiche un message dans la console et change le texte du bouton. C’est exactement le principe de GTK — on décrit quoi faire quand tel événement survient, et la boucle principale se charge d’appeler nos fonctions au bon moment. Notez que containerAdd n’accepte qu’un seul enfant direct ; pour disposer plusieurs widgets, on utilise un conteneur comme Gtk.boxNew.
Le même code en style gtk2hs
Si vous tombez sur du code utilisant l’ancien binding gtk2hs, la structure est très proche, avec une syntaxe légèrement différente pour connecter les signaux :
import Graphics.UI.Gtk
main :: IO ()
main = do
initGUI
fenetre <- windowNew
bouton <- buttonNewWithLabel "Cliquez-moi"
containerAdd fenetre bouton
on bouton buttonActivated $ putStrLn "Cliqué !"
on fenetre objectDestroy mainQuit
widgetShowAll fenetre
mainGUI
On retrouve les mêmes idées : initGUI pour démarrer, la connexion des signaux avec on, et mainGUI comme boucle bloquante. Seuls les noms changent.
Compiler et lancer
Avec Stack, il suffit d’un stack run ; avec Cabal, d’un cabal run. Une petite fenêtre apparaît, et chaque clic déclenche votre callback. Vous tenez là toutes les briques fondamentales d’une GUI GTK : fenêtre, widget, signal et boucle d’événements.
Aller plus loin
À partir de ce socle, on ajoute des champs de saisie (Gtk.entryNew), des dispositions en boîtes ou en grilles, et l’on peut même charger l’interface depuis un fichier décrit avec l’éditeur Glade. La force de Haskell reste entière : la logique métier demeure pure et typée, seule la couche d’interaction touche à IO.
À lire ensuite
- Si Haskell est nouveau pour vous, commencez par débuter avec Haskell sans prise de tête.
- Comparez les approches GUI multiplateformes avec l’introduction à wxWidgets.
- Prenez du recul sur le paradigme avec notre guide de la programmation fonctionnelle.