6.11 Construire une bibliothèque graphique grâce à FLTK - naver/lispe GitHub Wiki

Bibliothèque Graphique

English Version

Rajouter une bibliothèque graphique est un élément important dans la création d'un nouveau langage. C'est là que le talent artistique naturel de tout informaticien qui se respecte peut enfin trouver sa consécration. Hélas, quand on a choisi C++ comme langage de programmation, on se retrouve rapidement confronté à un problème de taille.

C++ n'offre aucune bibliothèque graphique native...

J'entends certains dire déjà, "c'est normal, c'est un langage bas niveau". Sauf que Java ou Python offrent leurs propres bibliothèques graphiques et qu'imaginer que C++ puisse aussi disposer de sa propre solution n'a rien d'extravagant.

Certains ont décidé de remédier à ce problème et il existe aujourd'hui nombre de bibliothèques graphiques en C++ que l'on peut intégrer dans son code.

Notre choix s'est porté sur FLTK.

FLTK

FLTK propose l'une des API les plus simples et le plus riches, à notre avis, pour intégrer un moteur graphique au sein de votre code.

Une fenêtre se crée en trois lignes de code:

  //On crée notre fenêtre
  Fl_Window win(500, 500, "Bonjour au Monde!");
  //On l'affiche
  win.show();
  
  //On lance la boucle des événements
  return Fl::run();

Rajouter des objets graphiques au sein de cette fenêtre est tout aussi simple:

/**********************************************************************************************************************************/
static void rappel(Fl_Widget *w, void *uData) {
  printf("Le bouton a été pressé\n");
}

/**********************************************************************************************************************************/
int main() {
  //On crée une fenêtre
  Fl_Window win(500, 70, "Simple bouton");
  //On crée un bouton
  Fl_Button bouton(20, 20, 100, 30, "Test bouton");
  //que l'on associe à une fonction de rappel
  bouton.callback(rappel, (void *)0);
  win.show();
  return Fl::run();
}

En fait, l'objet Fl_Window fonctionne comme une parenthèse que l'on ouvre, dans laquelle tous les objets créés sont automatiquement enregistrés.

Surcharge

Il est aussi possible de surcharger les méthodes pour y dessiner ses propres objets.

class MaFenetre : public Fl_Window {
public:

  MaFenetre(int x, int y, const char* txt) : Fl_Window(x,y,txt) {}

  //On surcharge la méthode draw de façon à tracer un cercle rouge
  void draw() {
    fl_color(FL_RED);
    fl_circle(100,100,20);
  }
};

int main(int argc, char *argv[]) {
   MaFenetre wnd(100,100,"Test);
   wnd.show();
   Fl::run();
}

API

Comme on le voit, l'API est extrêmement simple et surtout elle est universelle. FLTK est disponible aussi bien sur Windows, Mac OS que Linux. D'ailleurs, sur les plate-formes Linux, la bibliothèque est souvent pré-installée.

L'API est plutôt riche en objets de base. On y trouve toutes sortes de boutons, de sliders, de moyens d'édition... On peut afficher facilement du texte, des images au sein des fenêtres, dessiner des courbes, choisir les couleurs.

Tout dérive de la classe de base: Fl_Widget...

Il suffit de se référer à la documentation pour découvrir à quel point cette bibliothèque est riche...

Interfaçage

FLTK est non seulement très simple à utiliser, mais elle se révèle tout aussi simple à interfacer avec LispE.

LispE fournit déjà un mécanisme pour créer vos propres bibliothèques dynamiques.

Nous avons créé trois sortes de classes:

Lispe_gui

Il s'agit de la classe qui permet de créer les objets graphiques (fenêtre, boutons, sliders etc.) et d'appeler les différentes méthodes graphiques pour dessiner, placer des objets graphiques ou écrire du texte.

Fltk_widget

Il s'agit de la classe mère de tous les objets graphiques manipulés dans la bibliothèque.

Fltk_window

Cette classe est une dérivation de Fltk_widget; C'est elle qui gère l'affichage des fenêtres.

Fltk_widget, Fltk_input, Fltk_output_, Fltk_button, Fltk_slider

Toutes ces classes sont des dérivations de Fltk_widget. Ces objets gèrent les boutons et les zones d'entrée/sortie. Notons que nous n'avons pas encore intégré l'ensemble des objets graphiques offert par FLTK. Il suffit de dériver une nouvelle classe de Fltk_widget pour enrichir la bibliothèque.

Doublewindow

Il s'agit d'une dérivation de Fl_Double_Window, une classe propre à FLTK dont on va surcharger la méthode draw de façon a pouvoir y exécuter des fonctions LispE.

Fonctions de rappel (callback)

En fait, rien n'est plus facile que de passer une fonction de rappel dans LispE. Il suffit de fournir l'atome qui correspond au nom de la fonction.

Fonctionnement

Lorsque l'on charge la bibliothèque GUI, on associe un nom de fonction avec un objet de type Lispe_gui qui reçoit comme paramètre un identifiant unique dont la liste est dans: typedef enum {...} fltk_action

La liste complète de toutes les fonctions exportées se trouve ici

Voici un exemple d'une telle fonction:

lisp->extension("deflib fltk_create (x y w h label (function) (object))", new Lispe_gui(lisp, fltk_gui, fltk_widget, fltk_create));

Nous créons ici une fonction: fltk_create qui prend la liste suivante d'arguments: (x y w h label (function) (object)), que l'on associe à un objet Lispe_gui à qui l'on fournit l'identifiant fltk_create, défini dans fltk_action.

Les autres arguments: fltk_gui et fltk_widget sont des identifiants qui définissent respectivement le type (au sens LispE) d'un objet Lisp_gui et le type d'un objet Fltk_widget.

Cette classe comprend sa propre dérivation de: eval.

Element* Lispe_gui::eval(LispE* lisp) {
    try {
        switch (action) {
            case fltk_create: ...
            case fltk_run: ...
            case fltk_resize: ...
            case fltk_close: ...
            case fltk_end: ...

Ainsi, chaque fonction est associée avec une instance unique d'un objet de type Lispe_gui dont l'évaluation permettra l'exécution des instructions qui nous intéressent.

Certaines de ces instructions comme fltk_button ou fltk_slider renvoient un objet LispE de type Fltk_button ou Fltk_slider, lesquels encapsulent chacun un pointeur vers un objet FLTK. Ainsi, les fonctions qui prennent un argument de ce type pourront communiquer avec la bibliothèque graphique FLTK.

Enfin, nous avons ajouté une fonction de rappel : close_callback qui permet d'intercepter la fermeture d'une fenêtre, de façon à pouvoir nettoyer la mémoire de tous les objets graphiques créés jusque là.

Utilisation

L'utilisation de cette bibliothèque dans LispE est aussi simple que la version C++ présentée ci-dessus.

Voici comment l'on crée une fenêtre en quelques lignes de LispE:

; D'abord on charge notre bibliothèque graphique
(use 'gui)

; On crée notre fenêtre dont on stocke le résultat dans 'wnd'
(setq wnd (fltk_create_resizable 100 100 "Test"))

; La fenêtre peut être redimensionné entre une taille minimale de 100x100 à 500x500
(fltk_resize wnd 100 100 500 500)

; on finalise et on affiche notre fenêtre
(fltk_end wnd)

;Puis on lance notre boucle des événements
(fltk_run wnd)

Avec un Bouton

La création d'un bouton est tout aussi simple.

On crée une fenêtre, puis on lui associe un bouton. On intègre dans le constructeur du bouton le nom d'une fonction et le tour est joué.

Remarquons que la fonction de rappel: pushed nécessite deux paramètres:

  • le premier paramètre correspond à l'objet graphique qui a été activé
  • le deuxième paramètre est l'objet (optionnel) fourni au constructeur du bouton...
(use 'gui)

; notre fonction de rappel
; b correspond donc au bouton
; o correspond à la chaine: "mes données" fourni comme argument au constructeur plus bas

(defun pushed(b o)
   (printerrln "Pressé" (fltk_label b) o)
)

; On crée une fenêtre de dimension fixe
(setq wnd (fltk_create 100 100 1000 1000 "Test"))

; Notre bouton est associé à la fonction 'pushed'
(setq button (fltk_button wnd 50 50 100 100 "ok" 'pushed "mes données"))

; On finalise
(fltk_end wnd)

; On lance la boucle des événements
(fltk_run wnd)

Dessiner un cercle

On peut évidemment dessiner au sein de la fenêtre, pour cela il suffit d'associer une fonction de rappel lors de la définition de notre fenêtre.

On peut aussi lui associer une minuterie qui permet de rappeler cette fonction à temps fixe.

Dans ce cas, la fonction de rappel doit nécessairement renvoyer une valeur numérique (qui peut être différente de la valeur initiale) pour que la minuterie soit de nouveau réinitialisée.

Autrement dit, cette minuterie force le système à redessiner la fenêtre tous les tantièmes de seconde, permettant ainsi des animations simples à l'écran.


(use 'gui)

; Ces deux macros permettent d'incrémenter localement 
; dans une liste des valeurs numériques
(defmacro ++(x i inc) (at x i (+ (at x i) inc)))
(defmacro --(x i inc) (at x i (- (at x i) inc)))

; On crée d'abord notre fenêtre de dessin...
; Important on lui fournit un nom de fonction: appel et un objet ici des coordonnées...
(setq wnd (fltk_create 100 100 1000 1000 "Cercle" 'appel '(30.0 40.0 20.0)))

(setq direction true)

; o correspond à l'objet fournit à fltk_create
(defun appel(w o)
 ; On dessine nos deux cercles
   (fltk_circle w (at o 0) (at o 1) (at o 2) FL_BLUE)
   (fltk_circle w (at o 1) (at o 0) (at o 2) FL_RED)
   (check direction
      (++ o 0 1)
      (++ o 1 1)
      (++ o 2 1)
      (if (> (at o 2) 500)
         (setg direction nil)
      )
      ; on retourne une valeur qui correspond à la minuterie
      (return 0.01)
   )
   (-- o 0 1)
   (-- o 1 1)
   (-- o 2 1)
   (if (< (at o 2) 10)
      (setg direction true)
   )

      ; on retourne une valeur qui correspond à la minuterie
   (return 0.01)
)

; on finalise la création de la fenêtre tout en fournissant une minuterie de 0,01s
(fltk_end wnd 0.01)

; On lance notre boucle d'événements
(fltk_run wnd)


La fonction plot

Cette fonction est peut-être la plus puissante du lot. Elle prend une liste de coordonnées, où chaque coordonnées est une liste de deux éléments: (x,y), et elle calcule un repère pour que chaque point de cette liste puisse être dessinée à l'écran. Elle renvoie une liste comprenant l'ensemble des informations correspondant à ce repère:

XminWindow: Largeur minimale de la fenêtre
YminWindow: Hauteur minimale de la fenêtre
XmaxWindow: Largeur maximale de la fenêtre
YmaxWindow: Hauteur maximale de la fenêtre
XminValue: Valeur minimale dans en X dans la liste de points
YminValue: Valeur minimale dans en Y dans la liste de points
XmaxValue: Valeur maximale dans en X dans la liste de points
YmaxValue: Valeur maximale dans en Y dans la liste de points
incX: le pas de dessin en X
incY: le pas de dessin en Y
épaisseur: l'épaisseur choisie pour dessiner

La fonction elle-même a la forme suivante:

(deflib fltk_plot (fenêtre points épaisseur (repère))

Si l'épaisseur vaut 0, les points sont alors reliés par des lignes entre eux. Sinon, épaisseur correspond au diamètre du point représenté à l'écran.

Cette fonction renvoie un repère qui peut être alors fourni à des appels successifs de plot de façon à pouvoir dessiner toutes les courbes dans le même plan.

Voici un exemple pour dessiner une rosace:

; Des macros pour incrémenter une valeur dans une liste à une position donnée
(defmacro ++(x i inc) (at x i (+ (at x i) inc)))
(defmacro --(x i inc) (at x i (- (at x i) inc)))

(use 'gui)

; dimension maximale de la courbe
(setq maxx 100)
(setq maxy 900)
(setq direction true)

(setq incb 0)

; fonction de rappel pour le slider
(defun slide(s o)
   (setg incb (fltk_value s))
)

; On crée une fenêtre
(setq wnd (fltk_create 100 100 1000 1000 "Polar" 'appel '(0.0 0.0)))

; On y rajoute un slider
(setq incb_slider (fltk_slider wnd 30 900 200 20 "incb" FL_HOR_SLIDER true 'slide))

; dont les limites de valeur sont entre -100 et 100
(fltk_boundaries incb_slider -100 100)

; La fenêtre sera redessinée toutes les 0,01s
(fltk_end wnd 0.01)

; valeurs initiales comprises en 0 et 2π par pas de 0.005
(setq valeurs (range 0 (* 2 _pi) 0.005))

; On calcule notre position via une simple équation 
(defun equation(o a)
   (+  (cos (* o (at a 0))) (at a 1))
)

; fonction de rappel pour dessiner
(defun appel(w o)
   ; on range la valeur incb dans o
   ; à la position 1: o[1] = incb
   (at o 1 incb)

   ; selon la direction, on incrémente
   ; ou on décréments o[0]
   (if direction
      (block
         (++ o 0 2)
         (if (> (at o 0) maxx)
            (setg direction nil)
         )
      )
      (block
         (-- o 0 2)
         (if (< (at o 0) 3)
            (setg direction true)
         )
      )
   )

   (setq coords ())
   ; on crée nos coordonnées polaires via l'équation ci-dessus
   (loop x valeurs
      (setq v (equation x o))
      (push coords
         (list
            (* (cos x) v)
            (* (sin x) v)
         )
      )
   )

   (fltk_drawcolor w FL_RED)
   ; On dessine alors notre courbe en reliant chaque point 
   ; par une ligne
   (fltk_plot w coords 0)
   ; la fonction est appelée toutes les 0,01s
   (return 0.01)
)

(fltk_run wnd)

Conclusion

La liste des classes et objets graphiques disponible dans FLTK est loin d'avoir été complètement intégrée, mais nous y travaillons. Vous pouvez d'ailleurs trouver une description complète ici

Addendum

Les binaires que nous fournissons pour Windows et Mac OS contiennent déjà une version compilée de cette bibliothèques.

Si vous avez besoin de compiler la bibliothèque graphique de LispE sur une machine Linux différente, vous pouvez vous référer à la section suivante: compilation gui