INFO505 Programmation C
Loading...
Searching...
No Matches
TP3 - Mini-tetris graphique avec GTK

1 - Objectifs

L'objectif de ce TP est de rendre "graphique" votre Tetris "texte" mis au point lors du TP précédent (TP2 - Mini-projet Tetris simplifié).

On va donc se servir d'une interface graphique classique, appelée GTK (http://www.gtk.org). Cette interface, écrite en C, mais avec une philosophie objet, a été écrite pour réaliser le projet GIMP, qui est une très bonne alternative libre au logiciel de retouche d'image Photoshop. GTK est aussi à la base de l'environnement graphique GNOME.

On se servira aussi de Cairo (http://www.cairographics.org/) qui est une bibliothèque très polyvalente pour du dessin vectoriel dans n'importe quel tampon (image, fichier svg, buffer OpenGL, et fenêtre). C'est depuis GTK+ 2.8 la façon officielle de faire du dessin dans les fenêtres.

Note importante. Ce TP vous guide pas à pas pour que vous ayez les meilleures chances possibles de réussir un tetris graphique simplifié. Si vous trouvez ce canevas trop rigide, libre à vous de faire votre propre tetris graphique en suivant votre propre cheminement.

Pointeurs utiles:

2 - Compilation séparée

On va réutiliser votre travail de la dernière fois. Pour bien faire les choses, on va faire des modules distincts. Le premier module sera la partie logique du Tetris, c'est-à-dire en gros votre dernier TP. Ce travail sera placé dans le module tetris, constitué du fichier entête tetris.h et du fichier source tetris.c. Le deuxième module sera la partie graphique du tetris et contiendra le programme principal. On créera donc un fichier gtktetris.c.

Note
Si vous êtes allé jusqu'au bout du TP2, vous pouvez utiliser normalement indifféremment la version "tableau" ou la version "liste" du TETRIS.

2.1 - Module tetris

Vous récupérez votre TP de la dernière fois (version tableau ou liste, peu importe, du moment qu'elle fonctionne). Vous allez écrire un fichier tetris.h ne contenant que les prototypes des fonctions que vous avez écrite la dernière fois ainsi que les types de données.

// tetris.h
#ifndef _TETRIS_H_
#define _TETRIS_H_
/* Constantes. */
#define HAUTEUR 15
#define LARGEUR 10
#define NB_PIECES 3
/* ... */
/* Types. */
typedef char Grille[ HAUTEUR ][ LARGEUR ];
typedef struct SPiece {
/* ... */
/* Fonctions. */
extern void genererPieces( Piece tabPiece[ NB_PIECES ] );
extern void affichePieces( Piece p );
extern char lireCase( Grille G, int ligne, int colonne );
/* ... */
#endif /* ifndef _TETRIS_H_ */

Vous placerez ensuite les fonctions (prototypes et corps) dans un fichier tetris.c. Il faudra aussi enlever la fonction main car un programme C n'a qu'un seul main. Une façon élégante de le faire est de tout simplement renommer votre fonction main, par exemple en main_console. Enfin, il faudra inclure le fichier entête tetris.h au début de votre fichier tetris.c, juste après les inclusions d'entêtes systèmes.

// tetris.c
#include <stdlib.h>
#include <stdio.h>
#include "tetris.h"
void genererPieces( Piece tabPiece[ NB_PIECES ] )
{
....
}

2.2 - Création d'un makefile

La commande make est un outil classique pour développer et compiler des programmes, notamment sur les systèmes UNIX. Cette commande regarde dans le répertoire où elle est lancé la présence d'un fichier Makefile ou makefile, et suit les directives écrites dedans. Créer donc le fichier Makefile avec:

CC=gcc
CFLAGS=-g -Wall -pedantic -std=c99

all: tetris.o

tetris.o: tetris.h tetris.c
	$(CC) -c $(CFLAGS) tetris.c

Celui-ci compile simplement le fichier tetris.c en son fichier objet tetris.o. On voit que l'on peut définir des variables pour ensuite les utiliser.

On rajoute maintenant de quoi compiler notre futur programme gtktetris. Recopier ce mini-fichier source gtktetris-1.c.

// gtktetris.c
#include <stdlib.h>
#include <math.h>
#include <gtk/gtk.h>
#include "tetris.h"
int main( int argc,
char *argv[] )
{
Grille g;
Piece tabPieces[ NB_PIECES ];
genererPieces( tabPieces );
initialiseGrille( g );
afficheGrille( g );
return 0;
}

Pour compiler tetris.c et gtktetris.c dans le même programme, on utilise maintenant le makefile suivant. On note qu'il nous permettra de compiler notre programme avec l'interface graphique GTK (ici version 3), même si pour l'instant on ne s'en sert pas.

CC=gcc
LD=gcc
CFLAGS=-g -Wall -pedantic -std=c99
GTKCFLAGS:=-g $(shell pkg-config --cflags gtk+-3.0)
GTKLIBS:=$(shell pkg-config --libs gtk+-3.0)

all: gtktetris

gtktetris: gtktetris.o tetris.o
	$(LD) gtktetris.o tetris.o -o gtktetris $(GTKLIBS)

gtktetris.o: gtktetris.c
	$(CC) -c $(CFLAGS) $(GTKCFLAGS) gtktetris.c

tetris.o: tetris.h tetris.c
	$(CC) -c $(CFLAGS) tetris.c

clean:
	rm -f gtktetris gtktetris.o tetris.o

Vérifier maintenant que la compilation fonctionne. Tapez maintenant make dans votre terminal. Il devrait afficher quelque chose ressemblant à ci-dessous. Vous pouvez vérifier que le programme gtktetris a bien été compilé. Si vous l'exécutez, cela affiche la grille sur le terminal, comme d'habitude.

$ make
gcc -c -g -Wall -pedantic -std=c99 -g -I/opt/local/include/gtk-3.0 -I/opt/local/lib/gtk-3.0/include -I/opt/local/include/atk-1.0 -I/opt/local/include/cairo -I/opt/local/include/pango-1.0 -I/opt/local/include -I/opt/local/include/glib-3.0 -I/opt/local/lib/glib-3.0/include -I/opt/local/include/pixman-1 -I/opt/local/include/freetype2 -I/opt/local/include/libpng12 gtktetris.c
gcc -c -g -Wall -pedantic -std=c99 tetris.c
gcc -L/opt/local/lib -lgtk-x11-3.0 -lgdk-x11-3.0 -latk-1.0 -lgdk_pixbuf-3.0 -lpangocairo-1.0 -lgio-3.0 -lXinerama -lXi -lXrandr -lXcursor -lXcomposite -lXdamage -lpangoft2-1.0 -lXext -lXfixes -lcairo -lpixman-1 -lpng12 -lXrender -lX11 -lXau -lXdmcp -lpango-1.0 -lm -lfontconfig -lexpat -lfreetype -lz -lgobject-3.0 -lgmodule-3.0 -lglib-3.0 -lintl -liconv gtktetris.o tetris.o -o gtktetris

La compilation est plus complexe car GTK utilise beaucoup de bibliothèques différentes (X, mais ausii cairo, pango, gdk, Xi, Xext, ...). Mais tout ceci se fait tout seul.

Note
Le petit bout de code donné pour gtktetris.c est correct pour un type Grille qui est un tableau bidimensionnel. Si vous avez choisi une structure pour le type Grille, il faut passer la grille par adresse.

3 - L'interface GTK

Pour créer une interface graphique, GTK propose un système classique à base de composants. On dispose du type générique GtkWidget pour les désigner. La fenêtre (GtkWindow) est le composant dans lequel on place les autres. Certains composants peuvent contenir d'autres composants, on les appelle des conteneurs (GtkWindow est un conteneur ne contenant pas plus d'un composant, le GtkBox est un conteneur permettant de stocker autant d'autres composants que l'on souhaite). On dispose de plus de beaucoup de composants classiques pour les IHM : boutons, menus, checkbox, roulettes, zones de dessin, onglets, etc.

Ensuite, on associe à ces composants des réactions à certains événements. On pourra ainsi spécifier que la fonction quitter sera appelée lorsqu'on clique sur un bouton.

Une fois ces composants créés puis assemblés dans le bon ordre, on peut lancer l'application en donnant la main à GTK (appel de gtk_main). Ce sera GTK qui appelera automatiquement nos réactions selon les événements induits par le comportement de l'utilisateur. On parle de programmation événementielle.

3.1 - Première fenêtre

Voilà un programme minimaliste pour créer une fenêtre GTK.

// gtktetris.c
#include <stdlib.h>
#include <math.h>
#include <gtk/gtk.h>
#include "tetris.h"
int main( int argc,
char *argv[] )
{
GtkWidget *window;
/* Passe les arguments à GTK, pour qu'il extrait ceux qui le concernent. */
gtk_init (&argc, &argv);
/* Crée une fenêtre. */
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
/* La rend visible. */
gtk_widget_show (window);
/* Rentre dans la boucle d'événements. */
/* Tapez Ctrl-C pour sortir du programme ! */
gtk_main ();
return 0;
}

On note qu'il faut un pointeur (de type GtkWidget*) pour pointer vers les composants créés. Chaque composant est créé avec une fonction de la forme gtk_xxxx_new( ... ), où xxxx désigne le type de composant voulu. Enfin, on doit appeler la fonction gtk_widget_show sur chaque composant créé pour le rendre visible. De la même façon, vous pouvez rendre invisible tout composant (et les composants qu'il contient) en appelant la fonction gtk_widget_hide.

Une fois tout en place, on appelle la boucle d'attente d'événements de GTK avec gtk_main. Compilez et exécutez ce code. Notez qu'il faut interrompre le programme car rien n'est prévu pour quitter la boucle.

3.2 - Premier bouton et réaction

On va créer un bouton (cliquable) avec gtk_button_new_with_label. Rajouter les lignes suivantes dans votre programme avant l'appel au gtk_main.

GtkWidget* button_quit;
// Crée le bouton.
button_quit = gtk_button_new_with_label ( "Quit" );
// Connecte la réaction gtk_main_quit à l'événement "clic" sur ce bouton.
g_signal_connect( button_quit, "clicked",
G_CALLBACK( gtk_main_quit ),
NULL);
// Rajoute le bouton dans le conteneur window.
gtk_container_add( GTK_CONTAINER( window ), button_quit );
// Rend visible le bouton.
gtk_widget_show( button_quit );
// ... la suite

Vous avez maintenant une fenêtre composée d'un seul bouton. Si vous cliquez dessus, le programme se termine.

Note
Important ! Vous voyez qu'on a passé en paramètre un nom de fonction (via une macro G_CALLBACK). En fait, le programme passe l'adresse de la fonction comme argument. De son côté, la fonction g_signal_connect récupère cette adresse et la place dans un pointeur pour ensuite appeler cette fonction en réaction à l'événement "clic sur le bouton". On parle alors de pointeur de fonction, plutôt que de pointeur tout court, car le pointeur ne pointe pas sur des données mais sur l'adresse du code assembleur de la fonction.

3.3 - Boîte pour mettre plusieurs composants

Comme une fenêtre ne contient qu'un composant au maximum, on place un autre composant (la boîte) dans la fenêtre et c'est dans cette boîte qu'on mettra plusieurs composants.

Les boîtes permettent de ranger des composants, mais ne rajoutent pas de décor autour des composants. Elles servent à mettre en page votre IHM. Il y a les boîtes horizontales (créées avec gtk_box_new(GTK_ORIENTATION_HORIZONTAL,...) pour placer les composants horizontalement et les boîtes verticales (créées avec gtk_box_new(GTK_ORIENTATION_VERTICAL,...)) pour placer les composants en vertical.

// 10: bords autour des composants.
GtkWidget* hbox1 = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
// Crée un label
GtkWidget* label = gtk_label_new( "Grille TETRIS" );
gtk_container_add ( GTK_CONTAINER (hbox1), label );

Ajouter maintenant aussi le bouton button_quit dans la boîte hbox1, ajouter la boîte dans la fenêtre window. N'oubliez pas de rendre visible tous les composants !

Note
On peut rendre visible chaque composant avec gtk_widget_show. On peut rendre visible tous les composants contenus dans un conteneur avec gtk_widget_show_all.
On peut voir comment GTK normalise les casts à la C via des macros, par exemple GTK_CONTAINER( hbox1 ) transforme un pointeur GtkWidget* en un pointeur GtkContainer*: c'est valide car c'est bien ainsi que le composant a été créé. Cela permet à GTK de simuler l'héritage en C, et de vérifier à l'exécution que le transtypage est valide.

3.4 - Mise en page de l'IHM

On se propose de faire la mise en page suivante pour notre IHM. Il vous faut créer autant de hbox et vbox nécessaires pour structurer cette mise en page. On note qu'il y aura des boutons (New, Quit et les 3 flèches), des labels (Score et Delay) et des labels qu'on va modifier (la valeur du score et le temps restant). Il y aura enfin une zone de dessin (GtkDrawingArea).

On va aussi créer des boutons flèches. On pourrait utiliser des petits icones, mais le plus simple est de faire des labels "<", "v", ">". Pour faire un bouton flèche gauche, on écrit les lignes suivantes.

GtkWidget* left = gtk_button_new_with_label( "<" );

Pour créer une zone de dessin de taille donnée, on fait ainsi.

GtkWidget* drawing_area = gtk_drawing_area_new ();
// largeur=150 pixels, hauteur = 100 pixels.
gtk_widget_set_size_request (drawing_area, 150, 100);

Vous définirez une constante TAILLE_CARRE qui vaut 16 et qui donne la taille de chaque carré de vos pièces de tetris. Il faudra donc créer une zone de dessin de largeur TAILLE_CARRE*(LARGEUR+4) et de hauteur TAILLE_CARRE*(HAUTEUR+7), car il faut prévoir la place pour le décor autour de la grille et pour la pièce qui arrive en haut.

Pour faire les choses un peu plus proprement, vous créerez une fonction creerIHM() pour créer tous ces composants et les mettre en page. Pour simplifier, les variables pointant vers les composants "utiles" pourront être globales au module (fenêtre, zone de dessin, label pour la valeur du score, label pour la valeur du delay).

Note
Important ! Ne passez pas 1h à mettre en page votre IHM. Faites un premier jet, même moche, et passez à la suite.

3.5 - Ajouter une réaction

On peut écrire ses propres fonctions réactions pour les événements. Par exemple, si on veut que le bouton "gauche" appelle la fonction Gauche lorsqu'il est cliqué, on connecte le composant à cette fonction grâce à g_signal_connect. On note qu'on peut passer un pointeur en paramètre de plus.

// Réaction à un clic sur `button_left`
// @param widget le widget qui a déclenché l'appel de cette réaction (ici `button_left`)
// @param data le pointeur donné lors du `g_signal_connect` (ici `global_val0`)
gboolean Gauche( GtkWidget *widget, gpointer data )
{
// Recupère la valeur passée en paramètre.
int* ptr_val0 = (int*) data;
int val0 = *ptr_val0;
printf( "Gauche, val=%d\n", val0 ); // affichera 17
return TRUE; // Tout s'est bien passé
}
// ...
// variable globale
int global_val0 = 17;
// ...
// Connecte au bouton `button_left` la réaction `Gauche`.
// Quand on clique sur ce bouton, le pointeur vers `global_val0` est donnée en second
// paramètre à la fonction `Gauche`.
g_signal_connect( button_left, "clicked", G_CALLBACK( Gauche ), &global_val0 );

Ecrivez des réactions similaires pour les boutons flèches gauche, bas, droite et "New". On les réécrira plus tard pour nos besoins. Il est important de remarquer que dans l'exemple ci-dessus, la valeur affichée est celle de global_val0 lorsque le bouton sera cliqué, il est donc primordial que cette variable existe encore à ce moment là. On note que ce mécanisme de réaction est basé sur la notion de pointeur de fonction.

Note
Comme la variable global_val0 doit exister lorsque Gauche est appelée, ce ne peut être une variable définie dans le fonction creerIHM. &global_val0 doit désigner soit l'adresse d'une variable globale, soit l'adresse d'une variable définie dans la fonction appelante, i.e. la fonction main.

3.6 - Accès à toutes les données

Nous allons avoir besoin d'accéder à la partie logique du jeu (grille tetris ainsi que pièces, score, autres) à partir de l'IHM. Or, dans les réactions que l'on va associer aux événements, une seule donnée (un pointeur) est envoyée en même temps. On va donc se créer un type qui rassemble toutes nos données en un seul endroit et on passera l'adresse d'une variable de ce type à toutes nos réactions.

typedef struct {
Grille g;
Piece tab[ NB_PIECES ];
int piece; // la piece "en cours" que l'on cherche à placer.
int col; // la colonne actuelle pour la pièce que l'on cherche à caser.
int score; // le score
int delay; // le delay
// ... peut-être d'autres choses.
} Jeu;
// déclaration
void creerIHM( Jeu* ptrJeu );
// ...
int main( ... )
{
Jeu jeu;
initialiseGrille( jeu.g );
// initialiser les pièces et autres ...
// ...
creerIHM( &jeu );
// ...
}
// définition
void creerIHM( Jeu* ptrJeu )
{
...
}

Il faudra donc rajouter un paramètre à creerIHM. Son prototype deviendra void creerIHM( Jeu* ptrJeu ).

Warning
Important. Dans la suite, vous aurez à mettre des pointeurs vers des GtkWidget dans la structure Jeu. Et ptrJeu sera la donnée que vous passerez aux différentes réactions/callbacks.

4 - Affichage du Tetris dans la zone de dessin.

On va se servir de Cairo (http://www.cairographics.org/) pour dessiner notre grille tetris ainsi que les pièces.

4.1 - Realize et draw events

Lorsqu'un composant est placé dans une fenêtre affichée à l'écran, il reçoit des signaux pour lui dire quand se réafficher. Cela arrive bien sûr la première fois que l'IHM est affichée, mais aussi par la suite si jamais la fenêtre a subi une occlusion partielle et doit donc être redessinée. Le composant GtkDrawingArea est par défaut rempli de gris, c'est tout ! On va donc se connecter aux signaux "realize" (appelé une seule fois lors du premier affichage) et "draw" (appelé régulièrement par le serveur de fenêtre) pour tracer notre tetris. On part donc du squelette suivant.

// Pas indispensable, appelé au début de la création du widget.
// Force ici un réaffichage.
gboolean
realize_evt_reaction( GtkWidget *widget, gpointer data )
{ // force un événement "draw" juste derrière.
// La même ligne est utile pour les fonctions `Gauche`, `Droite`, pour forcer
// le réaffichage de la fenêtre (la pièce a bougé).
gtk_widget_queue_draw( widget );
return TRUE;
}
// c'est la réaction principale qui va redessiner tout.
gboolean
on_draw( GtkWidget *widget, GdkEventExpose *event, gpointer data )
{
// Ces premières lignes permettent de récupérer la zone d'affichage
// et d'initialiser un objet Cairographics `cr` qui permettra de dessiner
// sur cette zone.
GdkWindow* window = gtk_widget_get_window(widget);
cairo_region_t* cairoRegion = cairo_region_create();
GdkDrawingContext* drawingContext
= gdk_window_begin_draw_frame( window, cairoRegion );
cairo_t* cr = gdk_drawing_context_get_cairo_context( drawingContext );
// (A) maintenant je peux dessiner
cairo_set_source_rgb (cr, 1, 1, 1); // choisit le blanc.
cairo_paint( cr ); // remplit tout dans la couleur choisie.
cairo_set_source_rgb (cr, 0, 1, 0); // choisit le vert
cairo_rectangle (cr, 50, 50, 100, 100 ); // x, y, largeur, hauteur
cairo_fill_preserve( cr ); // remplit la forme actuelle (un rectangle)
// => "_preserve" garde la forme (le rectangle) pour la suite
cairo_set_line_width(cr, 3);
cairo_set_source_rgb (cr, 0, 0.5, 0); // choisit le vert sombre
cairo_stroke( cr ); // trace la forme actuelle (le même rectangle)
// => pas de "_preserve" donc la forme (le rectangle) est oublié.
// (B) On a fini, on peut détruire la structure.
gdk_window_end_draw_frame(window,drawingContext);
// On vide la mémoire utilisée.
cairo_region_destroy(cairoRegion);
return TRUE;
}
// Vous connectez ces réactions dans votre fonction creerIHM.
void creerIHM( Jeu* ptrJeu) {
...
// ... votre zone de dessin s'appelle ici "drawing_area"
g_signal_connect( G_OBJECT(drawing_area), "realize",
G_CALLBACK(realize_evt_reaction), ptrJeu );
g_signal_connect( G_OBJECT (drawing_area), "draw",
G_CALLBACK (on_draw), ptrJeu );
...
}

Maintenant, vous devriez voir à l'écran ceci lors de l'exécution.

4.2 - Dessin de la grille

Vous allez maintenant dessiner la grille en faisant un décor qui remplace les tubes '|' et en affichant un carré de couleur pour les pièces. Si cela vous amuse, vous pouvez les faire de différentes couleurs suivant vos choix pour les pièces (genre rouge pour '#', vert pour '@', etc). Les carrés seront donc de la taille TAILLE_CARRE * TAILLE_CARRE pixels. Pour plus de commandes sous cairo, allez voir http://cairographics.org/tutorial .

Vous allez modifier ce qui est entre (A) et (B) au-dessus.
Pour découper votre code, faites les fonctions

// Dessine le carré de type c à la ligne et colonne spécifiée dans le
// contexte graphique cr.
void dessineCarre( cairo_t* cr, int ligne, int colonne, char c );
// Dessine toute la grille g dans le contexte graphique cr.
void dessineGrille( cairo_t* cr, Grille g );
// ....

On note qu'on récupère le pointeur vers le jeu dans la fonction on_draw en convertissant le pointeur data ainsi:

Jeu* pJeu = (Jeu*) data;
// La grille est alors pJeu->g

Voilà un petit snapshot du jeu maintenant.

Note
On choisit la couleur de tracé via cairo_set_source_rgb(cr, r, g, b), où r,g,b sont les intensités (entre 0.0 et 1.0) des couleurs rouge, vert et bleu.

4.3 - Affichage de la pièce en attente

On voit que, dans la structure Jeu, on mémorise la pièce courante ainsi que sa colonne courante. On va faire une fonction qui met à jour une structure Jeu en choisissant aléatoirement une nouvelle pièce courante et la place arbitrairement dans la colonne (LARGEUR-piece.largeur)/2. Appelons-là nouvellePiece. Elle sera appelée en début de partie et à chaque fois que le joueur vient de poser une pièce.

Ecrivez ensuite la fonction dessinePiece( cairo_t* cr, Jeu* pJeu ) qui l'affiche, et qui sera appelée depuis on_draw (comme tous les dessins dans la zone graphique).

4.4 - Déplacement de la pièce

On peut maintenant écrire proprement les réactions Gauche et Droite. Elles doivent recevoir la donnée jeu aussi. Ensuite, leur seul rôle est de changer la colonne courante de la pièce, en la bloquant sur les bords. Il faut ensuite redessiner la zone_de_dessin. On procède alors de la même manière que dans realize_evt_reaction, c'est-à-dire avec la commande

gtk_widget_queue_draw( pJeu->drawing_area ); //< drawing_area doit être stocké dans Jeu.

Chaque clic sur les flèches gauche ou droite doit maintenant réafficher votre pièce déplacée dans la bonne direction.

4.5 - Placement de la pièce

Il ne vous reste plus qu'à écrire la réaction au clic sur la flèche vers le bas, qui ne fait qu'appeler vos fonctions écrites dans le TP précédent (hauteurMax et ecrirePiece).

N'oubliez pas de rappeler ensuite nouvellePiece puis d'invalider la zone de dessin pour qu'elle soit réaffichée. Il faudrait aussi détecter les lignes complètes et les enlever.

Vous avez maintenant le même jeu de tetris qu'en mode texte !

5 - Un peu d'action

En général, le tetris donne un temps limité pour chaque pièce, sinon la pièce est placée pour vous (en fait elle descend d'un cran de plus en plus rapidement). On va faire quelque chose de similaire. On va juste laisser un temps à l'utilisateur au bout duquel, si l'utilisateur n'a pas appuyer sur la flèche bas, le jeu fait comme si vous avez appuyé sur la flèche vers le bas.

5.1 - Réaction pour un time out

Il existe une façon très simple en GTK pour appeler une fonction spécifiée au bout d'un certain délai. Par exemple la fonction tic ci-dessous sera appelée toutes les 20ms, et recevra à chaque fois le pointeur vers le jeu.

gint tic( gpointer data )
{
Jeu* pJeu = (Jeu*) data;
printf( "tic\n" );
g_timeout_add (20, tic, (gpointer) pJeu ); // réenclenche le timer.
return 0;
}
// ...
// enclenche le timer pour se déclencher dans 20ms.
// (dans creerIHM)
g_timeout_add (20, tic, (gpointer) ptrJeu );

Pour simplifier, on va supposer que le label pour le délai a été placé dans une variable globale du genre

GtkWidget* valeur_delai;

En utilisant gtk_label_set_text( GTK_LABEL( valeur_delai ), str ), vous remplacez le texte stocké dans ce label par le contenu de la chaine de caractère str. On va donc stocker le délai courant dans la structure Jeu ainsi que le délai maximum redonné à chaque fois. Votre fonction tic doit décrémenter ce compteur délai et l'afficher. Lorsqu'il arrive à zéro, la réaction Bas doit être appelée et le compteur réinitialisé au délai maximum. Eventuellement, vous pouvez vous amuser à diminuer progressivement le délai maximum, avec le même timer ou un autre timer.

Note
Pour convertir un entier (le délai) en chaîne de caractère, on rappelle qu'il suffit d'utiliser sprintf( buffer, "%d", delai), où buffer est un tableau de caractère suffisament grand.

5.2 - Touches pour déplacer la pièce

Le tetris sera beaucoup plus sympa à jouer si au lieu de cliquer sur les boutons, vous utilisez des touches pour déplacer les pièces. On peut mettre des événements "touches pressées" en Gtk/Gdk.

Il suffit de récupérer les symboles désignant les touches possibles en ajoutant #include <gdk/gdkkeysyms.h>, de connecter une réaction à l'événement "key_press_event", et dans la réaction de gérer les touches et d'appeler les bonnes fonctions.

...
g_signal_connect (G_OBJECT (window), "key_press_event",
G_CALLBACK (on_key_press), jeu );
...
gboolean
on_key_press (GtkWidget *widget, GdkEventKey *event, gpointer user_data)
{
switch (event->keyval)
{
case GDK_KEY_Left:
Gauche( widget, user_data );
break;
...
}
}

5.3 - Jeu complet

Il ne reste plus (!) qu'à fignoler le jeu.

  • On peut mettre un booléen qui indique si une partie est en cours ou non. Cela permet de lancer un nouveau Jeu lorsqu'on clique sur New et de bloquer la partie lorsque le placement de la pièce dépasse le haut de la grille.
  • On peut changer le score à chaque pièce placée, ainsi qu'à chaque fois qu'on fait des lignes.
  • on peut s'occuper de la rotation des pièces. Il suffit de prévoir déjà les pièces qui sont rotations l'une de l'autre, de rajouter des boutons "rotation g/d"/ touches "rotation g/d" et de faire un tableau qui dit quel est l'indice de la pièce tournée.
  • on peut mettre des couleurs sympas...

6 - Remise du tp

  • Ce TP peut être fait par binôme.
  • A la fin de votre séance TP, vous m'enverrez votre TP via TPLab. Ce mail devra contenir une archive nommée TP3-[votre ou vos nom(s)] contenant tous les fichiers sources, entêtes, makefile. Vous placerez un README précisant l'état d'avancement (ce qui marche, ce qui marche à moitié, et ce qui ne marche pas).
  • Vous m'enverrez la version finale de votre TP une semaine après au plus tard (donc le mercredi 15 novembre minuit pour le groupe 1) via TPLab. Ce mail devra contenir une archive nommée TP3-[votre ou vos nom(s)] contenant tous les fichiers sources, entêtes, makefile. Vous complèterez le README en précisant l'état d'avancement (ce qui marche, ce qui marche à moitié, et ce qui ne marche pas) relativement à la version initiale.
  • Bien entendu, il faut que vos programmes compilent sous Linux lorsque j'écris make dans le terminal.