Interface graphique
Javass – étape 9

1 Introduction

Le but de cette étape est d'écrire la plus grosse partie de la classe représentant l'interface graphique d'un joueur humain.

Tout comme l'étape précédente, celle-ci utilise des concepts qui n'auront pas été vus au cours lors de sa publication. Il est donc fortement conseillé de lire les sections 1 à 3 des notes de cours sur les interfaces graphiques avec JavaFX avant de continuer.

1.1 Interface graphique

L'interface graphique de Javass a été présentée à l'étape précédente. Pour mémoire, elles est composée de trois sections montrant, de haut en bas :

  1. les scores,
  2. le pli,
  3. la main du joueur.

Dans le cadre de cette étape, seules les deux premières sections sont à réaliser, et une fois terminée, l'interface graphique partielle ressemblera donc à ceci :

javass-partial-gui.png

Figure 1 : Interface graphique partielle

Cette interface est composée d'une unique fenêtre dont le titre est Javass suivi du nom du joueur auquel l'interface correspond, Jean dans l'exemple ci-dessus.

La fenêtre contient un empilement de trois panneaux qui sont, de l'arrière vers l'avant :

  1. le panneau principal,
  2. le panneau de victoire pour l'équipe 1,
  3. le panneau de victoire pour l'équipe 2.

Sur l'image ci-dessus, seul le premier est visible, les deux autres sont cachés pour n'être montrés qu'en fin de partie. Ainsi, lorsqu'une équipe dépasse 1000 points, le panneau de victoire qui lui correspond est rendu visible, cachant le panneau principal et indiquant la fin de la partie.

Les panneaux de victoire sont très simples et décrits plus bas, à la §1.3. Le panneau principal est bien plus complexe, et est donc décrit en détail ci-après.

1.2 Panneau principal

Comme nous l'avons déjà dit, le panneau principal est composée de trois sections qui sont, du haut vers le bas de l'écran :

  1. la section des scores,
  2. la section du pli,
  3. la section de la main.

1.2.1 Section des scores

La section des scores est constituée de deux lignes de texte donnant les scores de chacune des deux équipes, en commençant par l'équipe 1. Chaque ligne contient, de gauche à droite :

  • le nom des deux membres de l'équipe, séparés par « et »,
  • le nombre de points obtenus dans le tour courant,
  • le nombre de points obtenus avec le dernier pli remporté dans le tour courant (entre parenthèses, précédé d'un +),
  • le nombre de points obtenus lors des tours précédents.

Par exemple, si l'équipe 1 est composée de Marc et Marie, qu'elle possède 4 points dans le tour actuel, que le dernier pli remporté lui a rapporté justement ces 4 points, et qu'elle n'a encore remporté aucun point dans les tours précédent, sa ligne de score sera :

Marc et Marie : 4 (+4) / Total : 0

Afin que les points des deux équipes soient alignés, ce qui facilite leur comparaison, les différentes parties de ces lignes de texte sont placées dans une grille. Les cellules de cette grille ne sont normalement pas visibles dans l'interface mais sont montrées dans l'image ci-dessous afin de rendre la structure claire :

score-grid.png

Figure 2 : Grille des scores

Comme on le voit sur cette image, le contenu des colonnes 1, 2 et 5 est aligné à droite, celui des autres colonnes à gauche.

1.2.2 Section du pli

La section du pli montre, en son centre, une image représentant l'atout. Elle est entourée des noms des joueurs, et des éventuelles cartes qu'ils ont posé durant le pli. La carte qui est actuellement la plus forte du pli est mise en évidence par un halo rouge.

Les joueurs sont placés dans la section du pli de manière à ce que le joueur auquel correspond l'interface graphique soit toujours placé au bas de l'écran. Comme d'habitude au Jass, les joueurs sont organisés dans le sens contraire des aiguilles d'une montre.

Par exemple, si le joueur auquel correspond l'interface porte l'identité PLAYER_3, les quatre joueurs sont organisés de la manière suivante à l'écran :

  PLAYER_1  
PLAYER_2 atout PLAYER_4
  PLAYER_3  

Le nom de chaque joueur est toujours affiché juste au-dessus de sa carte, sauf pour le joueur auquel l'interface correspond, dont le nom apparaît juste au-dessous de sa carte.

1.3 Panneaux de la victoire

Les deux panneaux de la victoire sont extrêmement simples et constitués d'une unique ligne de texte centrée, donnant le nom des membres de l'équipe victorieuse, leurs points et les points de l'équipe adverse.

L'image ci-dessous montre à quoi ressemble l'interface du programme lorsque l'équipe composée des joueurs Jean et Claude remporte la partie avec 1024 points contre 880, et que leur panneau de la victoire est rendu visible, recouvrant ainsi le panneau principal.

javass-victory.png

Figure 3 : L'austère panneau de la victoire

2 Mise en œuvre Java

2.1 Images

La réalisation de cette étape nécessitant des images représentant les cartes et les couleurs d'atout, nous mettons à votre disposition une archive Zip les contenant. Après l'avoir importée dans votre projet, vous devriez voir apparaître un nouveau dossier nommé images qu'il vous faut ajouter au build path de votre projet en suivant la procédure décrite à la fin de notre guide sur l'importation de fichiers dans Eclipse.

L'archive contient un total 76 fichiers, qui sont :

  • les images des cartes d'une taille de 160×240 pixels (36 fichiers),
  • les images des cartes d'une taille de 240×360 pixels (36 fichiers),
  • les images des couleurs d'atout d'une taille de 202×202 pixels (4 fichiers).

Le nom des fichiers contenant les cartes est construit en concaténant :

  • le texte card_,
  • l'entier représentant la couleur (0 pour SPADE, 1 pour HEART, etc.),
  • un caractère souligné (_),
  • l'entier représentant le rang (0 pour SIX, 1 pour SEVEN, etc.),
  • un caractère souligné (_),
  • la largeur en pixels de l'image (160 ou 240),
  • le texte .png, extension signifiant qu'il s'agit d'images au format PNG.

Par exemple, l'image de la dame de cœur en 240×360 pixels se trouve dans le fichier nommé card_1_6_240.png.

Les noms des fichiers contenant les couleurs d'atout est construit en concaténant le texte trump_, l'entier représentant la couleur, et le texte .png.

Les images ont été obtenues de Wikimedia Commons et ont été dessinées par Dmitry Fomin, qui les a placées dans le domaine public.

Une fois importées dans votre projet, ces images sont très simples à charger en mémoire grâce à la classe Image de JavaFX. Par exemple, l'image de la dame de cœur en 240×360 pixels peut être chargée ainsi :

new Image("/card_1_6_240.png")

Attention à ne pas oublier la barre oblique (/) au début du nom!

2.2 Classe GraphicalPlayer

La classe GraphicalPlayer, du paquetage ch.epfl.javass.gui, représente l'interface graphique d'un joueur humain. Attention, contrairement à ce que son nom pourrait suggérer, cette classe n'implémente pas l'interface Player.

L'interface publique de cette classe est très simple et constituée uniquement d'un constructeur et d'une méthode créant l'unique fenêtre de l'interface.

Le constructeur prend en arguments :

  • l'identité du joueur auquel l'interface correspond,
  • une table associative associant les noms des joueurs à leur identité,
  • les beans des scores et du pli.

Au moyen de ces différents objets, le constructeur se charge de construire la quasi-totalité de l'interface graphique, à l'exception de la fenêtre elle-même, c-à-d à l'exception de l'instance de Stage représentant la fenêtre. La création de celle-ci est faite dans une méthode séparée, nommée p.ex. createStage, qui la retourne.

La raison pour laquelle la création de la fenêtre doit être faite dans une méthode séparée est liée aux différents fils d'exécution (threads) utilisés par JavaFX. Les détails seront expliqués à l'étape suivante — et le sont en partie dans la §5 des notes de cours sur JavaFX —, mais l'idée est que les instances de Stage, contrairement aux instances d'autres nœuds graphiques, doivent impérativement être créées sur le fil JavaFX (JavaFX Application Thread). Il est donc bien de séparer le constructeur, exécutable sur n'importe quel fil, de la méthode createStage, qui doit impérativement être exécutée sur le fil JavaFX.

2.2.1 Conseils de programmation

Il est très fortement conseillé de définir des méthodes privées dans la classe GraphicalPlayer afin de séparer le code créant les différentes parties de l'interface.

Au minimum, nous vous conseillons de définir une méthode créant chacun des panneaux principaux, à savoir :

  • le panneau des scores (méthode createScorePane),
  • le panneau du pli (méthode createTrickPane),
  • les deux panneaux de victoire (méthode createVictoryPanes).

Ces méthodes reçoivent en argument les éléments dont elles ont besoin, p.ex. le bean contenant les propriétés qui les concernent, et retournent le(s) panneau(x) en question. Le constructeur de GraphicalPlayer les appelle et construit le panneau principal en les combinant comme il se doit.

En particulier, les panneaux des scores et du pli peuvent être placés respectivement dans la zone du haut et la zone centrale d'un panneau BorderPane, qui est le panneau principal. Les panneaux de victoire peuvent être superposés à ce dernier au moyen d'un panneau StackPane.

  1. Panneau des scores

    La section des scores peut être placée dans un panneau de type GridPane comportant 2 lignes de 5 colonnes chacune, comme illustré à la figure 2, que nous nommerons le panneau des scores.

    Les cellules de cette grille contiennent des instances de Text dont le contenu est, dans plusieurs cas, obtenu au moyen de liaisons aux propriétés correspondantes du bean des scores. Notez à ce sujet que la méthode convert de la classe Bindings permet de convertir en texte une propriété de type quelconque, ce qui est fort utile ici.

    Vous remarquerez que le bean des scores n'offre pas de propriété donnant le nombre de points obtenus lors du dernier pli gagné par une équipe. Il est toutefois simple de définir une nouvelle propriété locale dont la valeur s'obtient en attachant un observateur à la propriété turnPoints du bean des scores. En effet, l'observateur reçoit en argument l'ancienne et la nouvelle valeur de la propriété, et peut donc aisément calculer la différence.

    Afin qu'il ait l'aspect attendu, le panneau des scores doit de plus être configuré pour utiliser la bonne police de caractères (Optima 16 points), la bonne couleur de fond, etc. Cela est faisable au moyen de différentes méthodes héritées de Pane ou Node, mais une solution plus simple existe : la méthode setStyle, qui prend en argument une chaîne de caractères décrivant le style du composant au moyen d'un sous-ensemble du langage CSS utilisé sur le Web.

    Nous vous suggérons donc d'utiliser cette méthode, et nous vous donnons systématiquement ci-dessous les styles à appliquer aux différents nœuds graphiques. Ainsi, celui de la grille des scores est :

    -fx-font: 16 Optima;
    -fx-background-color: lightgray;
    -fx-padding: 5px;
    -fx-alignment: center;
    

    ce qui signifie que vous devez appeler la méthode setStyle sur cette grille, en lui passant en argument une chaîne contenant ce style, sur une seule ligne. En d'autres termes, vous devez écrire quelque chose comme (la seconde ligne a été tronquée pour des raisons de présentation) :

    GridPane scorePane = /* … */;
    scorePane.setStyle("-fx-font: 16 Optima; -fx-backgr…");
    
  2. Panneau du pli

    La section du pli peut être placée dans un panneau de type GridPane de 3 lignes de 3 colonnes chacune, que nous appellerons le panneau du pli :

    • la colonne de gauche contient le couple carte/nom pour le joueur situé à gauche de l'écran, qui occupe les trois lignes de la colonne (!),
    • la colonne centrale contient le couple carte/nom pour le joueur situé en haut de l'écran, l'image de l'atout, et le couple carte/nom pour le joueur auquel l'interface correspond,
    • la colonne de droite est similaire à la colonne de gauche.

    Pour faire en sorte que les couples carte/nom des colonnes extérieures occupent la totalité des lignes de la grille, vous pouvez utiliser une variante de la méthode add.

    Le style à attribuer au panneau du pli est :

    -fx-background-color: whitesmoke;
    -fx-padding: 5px;
    -fx-border-width: 3px 0px;
    -fx-border-style: solid;
    -fx-border-color: gray;
    -fx-alignment: center;
    

    L'affichage des images se fait très simplement au moyen de la classe ImageView de JavaFX. Il faut toutefois noter qu'il est indispensable de forcer les dimensions de l'image affichée au moyen des méthodes setFitWidth et setFitHeight à 101×101 pour l'image de l'atout et 120×180 pour les images des cartes. La raison en est que les images que nous vous fournissons sont deux fois plus grandes que nécessaire, afin qu'elles aient bel aspect également sur les écrans à très haute résolution (dits HiDPI ou Retina).

    Notez que si vous vous arrangez pour avoir à disposition une table associative (observable, mais constante) associant les images des cartes aux cartes elles-mêmes, le choix de l'image à afficher peut se faire au moyen d'une simple liaison de la propriété imageProperty du nœud ImageView à la propriété trick du bean du pli, moyennant une utilisation judicieuse de la variante 1 et de la variante 2 de la méthode valueAt de Bindings.

    Le halo entourant l'image d'une carte s'obtient en empilant sur elle, au moyen d'un panneau StackPane, un rectangle aux coins arrondis, flouté pour donner l'effet de halo. Le rectangle, une instance de Rectangle, a les mêmes dimensions que la carte (120×180) et le style suivant :

    -fx-arc-width: 20;
    -fx-arc-height: 20;
    -fx-fill: transparent;
    -fx-stroke: lightpink;
    -fx-stroke-width: 5;
    -fx-opacity: 0.5;
    

    Pour le flouter, il faut encore lui attacher, au moyen de setEffect, l'effet GaussianBlur avec un rayon de 4. Bien entendu, ce halo ne doit être visible que si la carte est la plus forte du pli, ce qui se fait facilement au moyen d'une liaison de sa propriété visibleProperty à la propriété winningPlayer du bean du pli et de la méthode isEqualTo de cette dernière.

    Les couples carte/nom s'obtiennent en combinant dans un nœud de type VBox une instance de Text contenant le nom et une instance de ImageView contenant l'image de la carte.

    Le nœud VBox a le style suivant :

    -fx-padding: 5px;
    -fx-alignment: center;
    

    tandis que le nœud Text a le style suivant :

    -fx-font: 14 Optima;
    

    Finalement, pour que la vue image montrant l'atout soit correctement centrée horizontalement, il faut appeler la méthode statique (!) setHalignment de GridPane en lui passant en premier argument la vue image, et en second argument la constante HPos.CENTER.

  3. Panneaux de la victoire

    Les panneaux de la victoire sont de simples instances de BorderPane dont seule la zone centrale est occupée, et elle l'est par une instance de Text donnant le texte montré plus haut. Chacun de ces panneaux a le style suivant :

    -fx-font: 16 Optima;
    -fx-background-color: white;
    

    L'instance de Text se construit très facilement et élégamment au moyen de la méthode format de Bindings à laquelle il est possible de passer des valeurs observables (p.ex. des propriétés du bean des scores) afin d'obtenir une chaîne qui se met à jour automatiquement lorsque ces valeurs changent.

    Chacun des panneaux de la victoire ne doit être visible que si l'équipe à laquelle il correspond a gagné, ce qui peut se faire en liant leur propriété visibleProperty au test d'égalité entre l'équipe en question et la propriété winningTeamProperty du bean des scores.

2.3 Tests

Pour tester votre interface, il est conseillé d'écrire un petit programme principal affichant l'interface à l'écran et vérifiant qu'elle se met bien à jour lorsque les propriétés des beans changent.

Le programme ci-dessous est un embryon d'un tel programme principal. Il crée l'interface graphique puis simule une partie dans laquelle les différents joueurs jouent toutes les cartes d'un tas de cartes de Jass, dans l'ordre. Pour que le jeu n'aille pas trop vite, une nouvelle carte est jouée chaque seconde, au moyen d'un minuteur d'animation (classe AnimationTimer).

public final class GuiTest extends Application {
  public static void main(String[] args) { launch(args); }

  @Override
  public void start(Stage primaryStage) throws Exception {
    Map<PlayerId, String> ns = new EnumMap<>(PlayerId.class);
    PlayerId.ALL.forEach(p -> ns.put(p, p.name()));
    ScoreBean sB = new ScoreBean();
    TrickBean tB = new TrickBean();
    GraphicalPlayer g =
      new GraphicalPlayer(PlayerId.PLAYER_2, ns, sB, tB);
    g.createStage().show();

    new AnimationTimer() {
      long now0 = 0;
      TurnState s = TurnState.initial(Color.SPADE,
				      Score.INITIAL,
				      PlayerId.PLAYER_3);
      CardSet d = CardSet.ALL_CARDS;

      @Override
      public void handle(long now) {
	if (now - now0 < 1_000_000_000L || s.isTerminal())
	  return;
	now0 = now;

	s = s.withNewCardPlayed(d.get(0));
	d = d.remove(d.get(0));
	tB.setTrump(s.trick().trump());
	tB.setTrick(s.trick());

	if (s.trick().isFull()) {
	  s = s.withTrickCollected();
	  for (TeamId t: TeamId.ALL)
	    sB.setTurnPoints(t, s.score().turnPoints(t));
	}
      }
    }.start();
  }
}

La vidéo ci-dessous montre l'évolution de l'interface graphique lors de l'exécution de ce programme de test.

3 Résumé

Pour cette étape, vous devez :

  • écrire la classe GraphicalPlayer (ou équivalent) en fonction des indications données plus haut,
  • tester votre code,
  • documenter la totalité des entités publiques que vous avez définies.

Aucun rendu n'est à faire pour cette étape avant le rendu final. N'oubliez pas de faire régulièrement des copies de sauvegarde de votre travail en suivant nos indications à ce sujet.