Initiation aux Interfaces Graphiques
, IIG est une matière d'ordre
méthodologique, permettant d'acquérir plusieurs savoir-faire :
- Modélisation d'un concept du monde
réel
Il faut identifier les informations qui décrivent le concept à modéliser, mais aussi les actions que l'on peut faire subir à ce concept :
- Structure de donnée (liste organisée des informations spécifiant le concept).
- Interface de cette structure : son mode d'emploi, la listes de fonctions permettant de la manipuler.
- Mise en œuvre d'une bibliothèque pour la réalisation d'un but précis.
- Conception de l'interface graphique d'une application :
- Défintion de l'interface (utilisation de la bibiliothèque graphique gtk)
- Programmation événementielle
Démarche de modélisation
Problématique
Le but est de modéliser un concept du monde réel pour le représenter sous format informatique. Cette modélisation peut être initalement motivée par un but d'utilisation précis. Il faut toujours penser : que ce but peut changer, évoluer, s'enrichir ; et donc, s'en rendre le plus indépendant possible.
Prenons l'exemple du concept
nombres complexes
. Un nombre complexe est la donné d'une partie réelle et d'une partie imaginaire. On peut pour le représenter utiliser la structure
struct s_complexe
suivante :
struct s_complexe
{
float reel;
float img;
};
typedef struct s_complexe complexe;
Pour manipuler les nombres complexes, on doit pouvoir (liste non exhaustive) : lire (consulter) la partie réelle / la partie imaginaire d'un nombre complexe, écrire (affecter) la partie réelle / la partie imaginaire d'un nombre complexe, déterminer le module / l'argument / le conjugué d'un nombre complexe, sommer, soustraire, multiplier, diviser deux nombres complexes entre eux, initialiser à 0 un nombre complexe ; soit, on doit disposer de la liste suivante de fonctions :
float complexe_get_reel(complexe z);
float complexe_get_img(complexe z);
complexe complexe_set_reel(complexe z, float reel);
complexe complexe_set_img(complexe z, float img);
complexe complexe_set_zero(complexe z);
float complexe_get_module(complexe z);
float complexe_get_argument(complexe z);
complexe complexe_get_conjugue(complexe z);
complexe complexe_get_somme(complexe z1, complexe z2);
complexe complexe_get_difference(complexe z1, complexe z2);
complexe complexe_get_produit(complexe z1, complexe z2);
complexe complexe_get_division(complexe z1, complexe z2);
On donne l'exemple de la définition de ces fonctions, pour quelques fonctions seulement :
float complexe_get_reel(complexe z)
{
return z.reel;
}
complexe complexe_set_img(complexe z, float img)
{
z.img = img;
return z;
}
complexe complexe_set_zero(complexe)
{
z.reel = 0;
z.img = 0;
return z;
}
float complexe_get_module(complexe z)
{
float module;
module = sqrt( pow(z.reel, 2) + pow(z.img, 2) );
return z;
}
complexe complexe_get_difference(complexe z1, complexe z2)
{
complexe z;
z.reel = z1.reel -z2.reel;
z.img = z1.img -z2.img;
return z;
}
Trois bonne raisons de définir l'interface d'une structure de donnée
- Pour permettre et faciliter la manipulation de la structure par des programmes tierces. Concernant les nombres complexes : on s'imagine que dès lors que l'on manipule des nombres (complexes ou autres), on peut vouloir les sommer ; il semble alors naturel de disposer d'une fonction
somme
.
- Pour contrôler l'accès au données et en maintenir la cohérence. Par exemple, si l'on manipule un type de donnée
personne
impléme nté par une structure struct s_personne
qui contient un champ date de naissance
et un champ age
, et si l'on définit l'accesseur en écriture personne_set_naissance
, alors cette fonction ne doit pas seulement mettre à jour le champ date de naissance
, mais aussi le champ age
de la variable du type personne
passée en paramètre (notons que ce n'est pas forcément une bonne idée que de manipuler un champ age
, mais passons). Le cas trésor
illustre bien cette problématique.
- Pour se rendre le plus possible indépendant de l'implémentation. Concernant les nombres complexes, la structure utiliséee et la définition des fonctions de manipulation peuvent changer, sans pour autant que cela impacte les programmes qui utilisent la bibliothèque
complexe
. Si l'on remplace la structure s_complexe par la structure s_complexe_2 suivante :
struct s_complexe_2
{
float module;
float argument;
};
typedef struct s_complexe_2 complexe;
Et considérons; alors les deux programmes suivants (où le fichier complexe.h
regroupe la spécifiation du type complexe
et la déclaration des fonctions de manipulation des nombres complexes) :
#include<stdlib.h>
#include<stdio.h>
#include"complexe.h"
int main()
{
complexe z;
z.reel=0;
z.img=0;
printf(Le zero complexe est %f + %f i, z.reel, z.img);
return EXIT_SUCCESS;
};
#include<stdlib.h>
#include<stdio.h>
#include"complexe.h"
int main()
{
complexe z;
z = complexe_set_zero(z);
printf(Le zero complexe est %f + %f i, complexe_get_reel(z), complexe_get_img(z));
return EXIT_SUCCESS;
};
Le premier programme ne compile pas après la modification apportée à la bibliothèque complexe
, tandis que le second programme, lui, n'a pas besoin d'être modifé.
Quelques règles pour concevoir l'interface d'une structure de donnée
- Règle numéro 0 : définir les règles de cohérence des informations. Par exemple : pour les personnes,
age == date - date de naissance
; pour une partie du jeu trésor
, si au moins 3 coups ont été joués et que le trésor n'a pas été trouvé, alors la partie est en état perdu
.
- Règle numéro 1 : en tenant compte de la règle numéro 0, pour toute information, écrire une méthode
get
et une méthode set
permettant d'accéder en lecture et en écriture à cette information. Pour le jeu trésor
par exemple, on se rend compte que l'état de la partie ne peut être modifié que lorsque l'on joue un coup (la partie à l'issue de ce coup peut passer en l'état gagné
ou perdu
), ou lorsque l'on réinitialise une partie ; alors il ne faut pas écrire de méthode tresor_set_etat
, car une telle méthode permettrait d'agir sur l'état de la partie, alors que ce n'est pas justifié par les autres informations du jeu.
- Règle numéro 2 : moduler la règle numéro 1, en fonction de l'utilisation que l'on imagine qu'il sera faite de la bibliothèque.
- Règle numéro 3 : moduler la règle numéro 2, en cherchant à rester le plus évolutif possible.
Modélisation du Jeu de trésor
Fichiers source
Ce que l'on doit modéliser
Un forrain propose l'animation suivante : 8 boîtes sont proposées à un joueur ; l'une de ces boîtes, et une seule, contient un trésor ; le joueur peut demander à ouvrir au plus 3 boîtes. Le joueur remporte le trésor s'il ouvre la boîte contenant le trésor.
Structures de donnée
Quelles sont les concepts et informations manipulés ?
- Concept de boîte. Une boîte peut être ouverte ou fermée ⇒ concept d'état d'une boîte. Une boîte peut être gagnante ou perdante ⇒ concept de statut d'une boîte.
- Concept de partie. La partie contient 8 boîtes qu'elle doit être capable d'identifier. Pour pouvoir jouer, une boîte gagnante doit avoir été désignée. Une partie peut être gagné (le joueur a trouvé la boîte gagnante en au plus 3 coups), perdue (le joueur a ouvert 3 boîtes, mais aucune de ces boîte ne contient le trésor) ou en cours (ou voire encore, pas lancée, si la boîte gagnante n'a pas été désignée).
Leur traduction en code C
enum e_etat_boite
{
BOITE_FERMEE;
BOITE_OUVERTE;
};
enum e_statut_boite
{
BOITE_PERDANTE;
BOITE_GAGNANTE;
};
struct s_boite
{
enum e_etat_boite etat;
enum e_statut_boite statut;
};
enum e_etat_partie
{
PARTIE_ENCOURS;
PARTIE_GAGNEE;
PARTIE_PERDUE;
};
#define NB_BOITE 8
#define NB_ESSAI 3
struct s_tresor
{
struct s_boite liste_boites[NB_BOITE];
int nb_essais_joues;
enum e_etat_partie etat;
};
typedef enum e_etat_boite etat_boite;
typedef enum e_statut_boite statut_boite;
typedef enum e_etat_partie etat_partie;
typedef struct s_boite boite;
typedef struct s_tresor tresor;
Remarque : on pourrait se passer des champs
nb_essais_joues
et
etat
de la structure
struct s_tresor
, car leur valeur peut être déduite des deux autres champs
liste_boites
et
id_boite_tresor
.
Interface
Si l'on devait tout écrire, la liste des fonctions de manipulation pour le concept de
trésor
serait la suivante (
règle 1) :
- Boite :
etat_boite boite_get_etat(boite une_boite);
statut_boite boite_get_statut(boite une_boite);
boite boite_set_etat(boite une_boite, etat_boite etat);
boite boite_set_statut(boite une_boite, statut_boite statut);
- Trésor :
int tresor_get_nb_coups_joues(tresor un_tresor);
etat_partie tresor_get_etat(tresor un_tresor);
etat_boite tresor_get_boite_etat(tresor un_tresor, int indice);
statut_boite tresor_get_boite_statut(tresor un_tresor, int indice);
tresor tresor_set_nb_coups_joues(tresor un_tresor, int nb_coups);
tresor tresor_set_etat(tresor un_tresor, etat_partie etat);
tresor tresor_set_boite_etat(tresor un_tresor, int indice, etat_boite etat);
tresor tresor_set_boite_statut(tresor un_tresor, int indice, statut_boite statut);
Règles de cohérence
Néanmoins, mieux vaut pour définir l'interface de la structure de donnée
tresor
définir d'abord les règles de cohérence (
règle 0). On s'appuie de plus sur les phases de jeu (utilisation qui sera faite de la structure
tresor
,
règle 2) :
- Pour pouvoir jouer, une partie doit d'abord être initialisée.
- Initialiser une partie, c'est :
- Déterminer une boîte gagnante.
- Fermer toutes les boîtes.
- Mettre à zéro le nombre de coups joués.
La partie est alors dans l'etat en cours
.
- Jouer un coup, c'est, pour une boîte désignée :
- Ouvrir la boîte.
- Incrémenter le nombre de coups joués.
Si la boîte ouverte contient le trésor, la partie passe en état gagnée
; sinon, si le nombre maximum de coups que l'on peut jouer est atteint, la partie passe en état perdue
.
De cette analyse, on déduit l'interface minimale suivante :
- Trésor (fonctions utilisées par exemple par un programme déroulant une partie du jeu
trésor
) :
int tresor_get_nb_coups_joues(tresor un_tresor);
etat_partie tresor_get_etat(tresor un_tresor);
etat_boite tresor_get_boite_etat(tresor un_tresor, int id);
statut_boite tresor_get_boite_statut(tresor un_tresor, int id);
tresor tresor_initialiser(tresor un_tresor);
tresor tresor_jouer_boite(tresor un_tresor, int id);
- Boite (fonctions utilisées par les fonctions de manipulation du type
tresor
, lorsque celles-ci accèdent aux éléments du tableau liste_boite) :
etat_boite boite_get_etat(boite une_boite);
statut_boite boite_get_statut(boite une_boite);
boite boite_set_etat(boite une_boite, etat_boite etat);
boite boite_set_statut(boite une_boite, statut_boite statut);
Exemple de mise en œuvre
Pour jouer au jeu de trésor en mode console, il ne reste plus qu'à écrire quelques fonctions d'entrée/sortie et un programme de jeu !
- Fonctions d'entrée/sortie :
void tresor_afficher(tresor un_tresor)
{
int ind_boite;
char car_affiche;
printf(Affichage de la partie ($ tresor, X boite vide, ? boite non ouverte) -- etat == %s, nombre coups joues == %d :\n, tresor_get_libelle_etat(un_tresor), tresor_get_nb_coups_joues(un_tresor));
for (ind_boite = 0 ; ind_boite < NB_BOITE ; ind_boite++)
{
if (tresor_get_boite_etat(un_tresor, ind_boite) == BOITE_OUVERTE)
car_affiche = ;
else
car_affiche = _;
printf( %c , car_affiche);
}
printf(\n);
for (ind_boite = 0 ; ind_boite < NB_BOITE ; ind_boite++)
{
if (tresor_get_boite_etat(un_tresor, ind_boite) == BOITE_OUVERTE || tresor_get_etat(un_tresor) != PARTIE_ENCOURS)
{
if (tresor_get_boite_statut(un_tresor, ind_boite) == BOITE_GAGNANTE)
car_affiche = $;
else
car_affiche = X;
}
else
car_affiche = ?;
printf(|%c| , car_affiche);
}
printf(\n);
for (ind_boite = 0 ; ind_boite < NB_BOITE ; ind_boite++)
printf( %d , ind_boite);
printf(\n);
}
const char* tresor_get_libelle_etat(tresor un_tresor)
{
if (tresor_get_etat(un_tresor) == PARTIE_GAGNEE)
return gagnee;
if (tresor_get_etat(un_tresor) == PARTIE_PERDUE)
return perdue;
if (tresor_get_etat(un_tresor) == PARTIE_ENCOURS)
return en cours;
return tresor donnees erronees : etat non reconnu;
}
void purger ()
{
char c;
while ((c = getchar()) != \n && c != EOF)
{}
}
tresor tresor_jouer_boite_par_saisie(tresor un_tresor)
{
int id = -1;
while (id < 0 || id > NB_BOITE -1)
{
printf(Saisir id boite a jouer dans l'intervalle %d...%d :\n, 0, NB_BOITE -1);
scanf(%d, &id);
if (id >= 0 && id <= NB_BOITE -1 && tresor_get_boite_etat(un_tresor, id) == BOITE_OUVERTE)
{
printf(Le boite %d a deja ete ouverte... \n, id);
id = -1;
}
}
un_tresor = tresor_jouer_boite(un_tresor, id);
return un_tresor;
}
- Programme :
int main()
{
char choix = O;
tresor un_tresor;
srand(time(NULL));
while (choix != S && choix != s)
{
un_tresor = tresor_initialiser(un_tresor);
while(tresor_get_etat(un_tresor) == PARTIE_ENCOURS)
{
un_tresor = tresor_jouer_boite_par_saisie(un_tresor);
purger();
tresor_afficher(un_tresor);
}
if (tresor_get_etat(un_tresor) == PARTIE_GAGNEE)
printf(Bravo, vous avez remporte le tresor !\n);
else
printf(Le tresor vous a malheureusement echappe.\n);
printf(... appuyer sur une touche pour continuer, s/S pour arreter\n);
choix = getchar();
}
return EXIT_SUCCESS;
}