Accueil / Articles PiApplications. / La plate-forme Java / Java FX

Listes à partir de la classe TableView.

Avertissement : le lecteur est ici supposé avoir des notions en matière de modèle de conception (MVC notamment), de classes "patron" (classe de type MaClass<T>), d'interfaces fonctionnelles et de leur mise en oeuvre via des expressions lambda. Cet article est un peu long car l'objet de l'article manipule de nombreux concepts et tous ne sont pas si simples.

Les listes sont un type de contrôles très utilisés. Plusieurs contrôles permettent de les instancier mais le plus riche et le plus utile d'entre eux repose sur la classe TableView. En effet, ce contrôle permet d'afficher des objets complexes et comportant de nombreux attributs. Sa souplesse et le nombre de concepts sous-jacent à son emploi rendent cependant ce contrôle difficile à appréhender. Notez que ces concepts sont communs à la plupart des contrôles graphiques JavaFX. La compréhension de l'utilisation de cette classe permet en général d'appréhender facilement celle des autres classes de gestion de données.

Comme la plupart des classes destinées à implémenter un contrôle graphique, un objet de classe TableView repose sur une architecture logicielle nommée MVC (Modèle, Vue, Contrôleur). La description de cette architecture déborde du cadre de cet article. Elle a été décrite et généralisée par l'ouvrage Design Patterns d'Erith Gamma, Richard Helm, Ralph Johnson et John Vlissides, auteurs connus sous le nom de Gang of Four (GoF) [ISBN 0-201-63361-2]. Pour information une version française de cet ouvrage a également été publiée. Pour faire simple disons que ce modèle de conception découple ce qui relève des données, de leur présentation et des évènement qui agissent sur l'une ou l'autre.

Création et initialisation d'une table.

Avant de pouvoir utiliser un contrôle qui repose sur la classe TableView, il faut définir ses "colonnes". Généralement les colonnes affichent des objets de même classe mais nous verrons que cela n'est pas systématique. La colonne sert donc avant tout à structurer la forme "tabulaire" de la vue. Elle est définie au moyen de la classe TableColumn. Si par exemple nous cherchons à présenter une liste de fruits à partir de leur nom et de leur couleur on pourra coder :

TableView<Fruit> tvw = new TableView<>();
TableColumn tclName = new TableColumn("Privé");
TableColumn tclColor = new TableColumn("Numéro de téléphone");

Comme vous le constatez, la "Vue" de la table comporte une liste de colonnes qu'il est possible d'obtenir via la méthode getColumns. Cette liste permet de modifier dynamiquement la structure de la table si on le souhaite.

Les objets de classe TableColumn exposent des méthodes permettant de fixer de nombreux paramètres comme la largeur de colonne (setMinWidth, setMaxWidth, setPrefWidth ou setResizable), leur visibilité, leur capacité à autoriser al modification du contenu des cellules qu'elles contiennent, etc.

Le modèle associé à la classe TableView est, comme pour la plupart des autres contrôles, une liste de classe ObservableList<T>. En d'autre termes, si on modifie cette liste, on modifie automatiquement la présentation de la table. Voici un exemple de code qui instancie le modèle et l'associe à la table.

ObservableList<PhoneRow> ols = FXCollections.observableArrayList(
    new PhoneRow(true, "+33123456789",
    new PhoneRow(false, "0678452034",
    new PhoneRow(false, "123")));
tvw.setItems(ols)

Les étapes principales de création d'une colonne.

Une table peut être vue comme une liste de lignes constituées de cellules distribuées horizontalement ou comme une liste de colonnes également constituée de cellules mais distribuées verticalement. JavaFX a fait le choix de la vision "colonne". Un TableView et donc constitué de colonnes qui sont des objets de classe TableColumn. Ainsi, décrire un TableView revient à lui ajouter dans un ordre donné des objets de cette classe. Pour initier une TableColumn il faut deux étapes obligatoire et une troisième éventuelle :

  1. Indiquer à la colonne comment le contenu de chacune de ses cellules est accédé en lecture et éventuellement en écriture.
  2. Indiquer comment est présentée le donnée de chacune de ses cellules ;
  3. Affecter un éventuel "auditeur" capable de capturer la mise à jour de la cellule de façon à adapter cette mise à jour à l'environnement applicatif (mise à jour d'autre contrôles par exemple).

Pour exposer les différents concepts, nous prendrons un cas simple mais représentatif : l'affichage d'une liste de numéros de téléphone. Chaque numéro est une chaîne de caractères et l'on précise pour chacun d'eux s'il est privé ou public. Voici une classe qui peut représenter un numéro de téléphone :

public class PhoneRow
{
  private boolean _fPrivate;
  private boolean _sPhoneNumber;

  public PhoneRow()
  {
  }

  public PhoneRow(boolean fPrivate, String sPhoneNumber)
  {
    _sPrivate = fPrivate;
    _sPhoneNumber = sPhoneNumber;
  }

  public boolean getPrivate()
  {
    return _fPrivate;
  }

  public void setPrivate(boolean fValue)
  {
    _fPrivate = fValue;
  }

  public String getPhoneNumber()
  {
    return _sPhoneNumber;
  }

  public void setPhoneNumber(String sValue)
  {
    _sPhoneNumber = sValue;
  }
}

Indication de l'accès à la donnée de la cellule.

Par "donnée" nous entendons ici un objet de même classe pour toutes les cellules d'une même colonne. On pourrez imaginer que l'on puisse directement passer à notre colonne la classe de l'objet à présenter en lui indiquant l'attribut concerné par la colonne : un booléen pour la première colonne et une chaîne de caractères pour la seconde. Toutefois, cette approche limiterait les cellules à la gestion de classes simples et imposerait de créer un objet par cellule qu'il faudrait alors transmettre à la colonne y compris lors de l'ajout de nouvelles lignes ce qui n'est clairement pas commode. JavaFX a cherché à rendre le mécanisme le plus compact et le plus générique possible.

La méthode de la classe TableColumn qui fixe la classe qui permet de peupler la colonne est setCellValueFactory. Cette méthode est en quelque sorte le lien entre le "modèle" et la "vue". Comme son nom l'indique, cette classe est une fabrique de classe. Chaque fois qu'une cellule doit être initialisée, la TableColumn invoque cette fabrique pour instancier son contenu. La méthode setCellValueFactory n'attend qu'un seul argument de type Callback<TableColumn.CellDataFeatures<S,T>,ObservableValue<T>>. Comme on le voit, cet argument est une classe qui implémente l'interface Callback. Une Callback est une classe qui comporte une méthode "réflexe" (call) qui sera automatiquement invoquée par l'infrastructure sous-jacente. Il s'agit d'une interface fonctionnelle. Ainsi, lorsqu'une TableColumn a besoin de créer une cellule, elle invoque la méthode callde cette interface. La Callback est une classe générique de patrons P et R(dans cet ordre). La patron P est la classe de l'argument transmis à la méthode call tandis que le patron R est la classe de l'objet retourné par cette méthode.

Si nous reprenons le prototype de la méthode setCellValueFactory de la classe TableColumn, nous voyons que l'objet transmis à la méthode call (patron P) est une instance de la classe générique TableColumn.CellDataFeatures<S,T>. Le patron S est la classe qui correspond au contenu d'une ligne du TableView et le patron T est la classe présentée par chaque cellule de la colonne. Ainsi la classe TableColumn.CellDataFeatures<S,T> se comporte comme un descripteur de la classe à initialiser au sein de la cellule.

La résultat produit par la méthode call (patron R) de la Callback est un objet de la classe présentée par la colonne.

Si l'on reprend notre exemple, S correspond à notre classe PhoneNumber et T à Boolean ou String suivant la colonne. Toutefois, pour construire un objet de la classe TableColumn.CellDataFeatures<PhoneNumber,Boolean> par exemple, il faut fournir à chaque ligne une instance de la classe PhoneNumber. Là encore ce n'est pas commode. L'idéal serait que la TableColumn puisse aller chercher elle-même cette instance dans le modèle attaché au TableView. C'est exactement ce que sait faire la méthode getProperty d'une "fabrique de propriétés".

Une fabrique de propriétés est un objet de classe PropertyValueFactory<S,T>. Cette classe implémente l'interface Callback<TableColumn.CellDataFeatures<S,T>,ObservableValue<T>>. On peut alors écrire le code qui suit :

TableColumn<PhoneRow,Boolean> tcl = new TableColumn<PhoneRow,Boolean>("Privé");
tcl.setCellValueFactory(new Callback<CellDataFeatures<PhoneRow,Boolean>, ObservableValue<Boolean>>()
{
  public ObservableValue<Boolean> call(CellDataFeatures<PhoneRow, Boolean> cdf)
  {
    return new SimpleBooleanProperty(cdf.getValue().getPrivate());
  }
});

Première simplification.

Nous avons vu que la Callback est une interface fonctionnelle ce qui nous permet d'utiliser "l'élégance" d'une expression lambda pour grandement simplifier ce code :

tcl.setCellValueFactory(cdf ->
{
  return new SimpleBooleanProperty(cdf.getValue().getPrivate());
});

Seconde simplification.

Une fabrique de propriétés peut être vue comme un "canal" de communication bi-directionnel entre les données du modèle et la vue de la liste tabulaire.

On peut donc simplifier l'étape en faisant des attributs des classes du modèle des "propriétés". Voici ce que devient notre classe PhoneRow :

public class PhoneRow implements IPrivate
{
  private final SimpleBooleanProperty _sbpPrivate;

  private final SimpleStringProperty _sspPhoneNumber;

  public PhoneRow()
  {
    _sbpPrivate = new SimpleBooleanProperty();
    _sspPhoneNumber = new SimpleStringProperty();
  }

  public PhoneRow(boolean fPrivate, String sPhoneNumber)
  {
    _sbpPrivate = new SimpleBooleanProperty(fPrivate);
    _sspPhoneNumber = new SimpleStringProperty(sPhoneNumber);
  }

  public boolean getPrivate()
  {
    return _sbpPrivate.get();
  }

  public void setPrivate(boolean fPrivate)
  {
    _sbpPrivate.set(fPrivate);
  }

  public String getPhoneNumber()
  {
    return _sspPhoneNumber.get();
  }

  public void setPhoneNumber(String sNumber)
  {
    _sspPhoneNumber.set(sNumber);
  }
}

Vous noterez que les accesseurs des attributs sont les traditionnelles méthodes get et setdes Beans Java. Cela n'est pas un hasard. Cette forme d'écriture permet à la fabrique de propriété de classe PropertyValueFactory<S,T>d'accéder directement aux attributs d'un objet de type "propriété" par leur nom. Par convention, ce nom est celui des accesseurs de l'attribut libéré du préfixe get ou set. La première lettre est en minuscule mais le reste du nom doit respecter la casse de l'accesseur. Dans notre exemple, le nom permettant à la fabrique de propriétés pour accéder à la colonne "Privé" est privatetandis que celui qui permet l'accès à la colonne "Numéro de téléphone" et phoneNumber. Dans ces conditions, notre code d'accès à la colonne "Privé" ce simplifie en :

tcl.setCellValueFactory(new PropertyValueFactory<>("private"));

Indication de la présentation de la donnée de la cellule.

Maintenant que l'on sait créer un canal entre les données du modèle et les cellules, il faut étudier comment présenter ces données (lecture) et éventuellement comment les modifier (écriture). La méthode qui permet de fixer la présentation, c'est-à-dire le contrôle d'affichage de la donnée dans une cellule d'une colonne est la méthode setCellFactory. Cette méthode prend un seul argument qui est de nouveau une Callback : Callback<TableColumn<S,T>,TableCell<S,T>>. L'argument de la méthode call est ici une objet de classe TableColumn<S,T>, c'est à dire la colonne elle-même. Il ne présente donc pas de difficulté majeure à ce niveau de l'exposé. Le second argument est un objet drivé de la classe TableCell<S,T>. Cet objet est en fait le contrôle générique de présentation de la donnée. Les classes patron ne posent pas non plus de difficulté de compréhension : S est la classe de la donnée présentée par une ligne de TableView (par exemple PhoneRow) et T la classe de la donnée présentée par la cellule est extraite du modèle par la fabrique de propriétés. Toutefois, le rôle de présentation de cette classe est assez limité.

Pour faciliter le fonctionnement, elle dispose de plusieurs classes dérivées qui sont adaptées à des classes de contenu données ou à certaines formes de présentation et d'action :

Lorsque vous pouvez employer l'une de ces classes, il est très simple de fixer le contrôle de présentation/modification via la méthode statique forTableColumn de chacune d'elle. A titre d'exemple voici le code pour associer un case à cocher à notre donnée "Privé" du numéro de téléphone.

tcl.setCellFactory(CheckBoxTableCell.forTableColumn(tcl));

Bien que les classes héritées de TableCell<S,T> permettent de traiter la plupart des cas, vous pouvez être amené à rencontrer de nombreux cas particuliers que ces classes ne résolvent pas. Imaginons par exemple que nous voulions afficher une icône comportant une petite clef lorsqu'un numéro de téléphone est "privé". Nous devons ajouter une troisième colonne dont la présentation sera fonction de l'état de l'indicateur "privé" du numéro de téléphone. Nous n'avons toutefois aucune classe pré-définie pour assurer sa présentation (ou non de notre icône).

Nous devons concevoir alors nous-même une classe qui hérite de TableCell<S,T> et qui réponde à notre besoin de présentation et éventuellement de communication avec l'utilisateur. Ici, nous créons la classe BooleanImageTableCell<S,Boolean>. Le caractère générique de cette classe nous permettra d'associer une image à toute valeur booléenne de la classe patron S. Voici le code source d'une telle classe. Il faut convenir que sans une bonne connaissance des classes génériques, ce code n'est pas très intuitif.

public class BooleanImageTableCell<S> extends TableCell<S, Boolean>
{
  private final ImageView _ivw;

  private final Image _img;

  public BooleanImageTableCell(final Callback<Integer, ObservableValue<Boolean>> cbk,
                               final StringConverter<Boolean> scn,
                               Image img, double dWidth, double dHeight)
  {
    _img = img;
    _ivw = new ImageView(img);
    _ivw.setFitWidth(dWidth);
    _ivw.setFitHeight(dHeight);
    setGraphic(null);
  }

  public BooleanImageTableCell(Image img, double dWidth, double dHeight)
  {
    this(null, null, img, dWidth, dHeight);
  }

  @Override
  public void updateItem(Boolean blnData, boolean fEmpty)
  {
    if (fEmpty)
    {
      setGraphic(null);
      return;
    }
    if (blnData)
    {
      _ivw.setImage(_img);
      setGraphic(_ivw);
    }
    else
      setGraphic(null);
  }

  public static <S> Callback<TableColumn<S,Boolean>, TableCell<S,Boolean>>
      forTableColumn(TableColumn<S,Boolean> tcl, Image img, double dWidth, double dHeight)
  {
    return forTableColumn(null, null, img, dWidth, dHeight);
  }

  public static <S> Callback<TableColumn<S,Boolean>, TableCell<S,Boolean>>
      forTableColumn(final Callback<Integer, ObservableValue<Boolean>> cbk,
                     final StringConverter<Boolean> scn,
                     Image img, double dWidth, double dHeight)
  {
    return new Callback<TableColumn<S,Boolean>, TableCell<S,Boolean>>()
    {
      @Override
      public TableCell<S,Boolean> call(TableColumn<S,Boolean> tcl)
      {
        return new BooleanImageTableCell<>(cbk, scn, img, dWidth, dHeight);
      }
    };
  }

Nous aurions pu simplifier l'écriture de la dernière méthode via une expression lambda car l'interface Callback est une interface fonctionnelle. La méthode principale est ici la méthode updateItem car c'est elle qui décide de la présentation en fonction de la donnée remontée par la propriété correspondante à la colonne "Privé". Si le numéro est "public", rien n'est affiché (setGraphic(null)) alors que si le numéro est "privé" l'icône l'est. Comme dans le cas précédent, l'association de la fabrique de la classe de présentation est très simple :

tcl.setCellFactory(BooleanImageTableCell.forTableColumn(tcl, piANDFX.getImage(EnumImages.Private), 48, 48));

Nous sommes ici dans un cas simple ou la donnée est seulement lue. Si la donnée devait être modifiée par le contrôle de présentation, nous devrions encore surcharger au moins l'une des méthodes startEdit, cancelEdit et commitEdit en fonction du besoin auquel répondre. Pour comprendre ces méthodes, consultez la documentation de la classe TableCell<S,T>.

Ecoute de la mise à jour des cellules.

Il est assez fréquent que la mise à jour d'une donnée dans la liste tabulaire entraîne une modification de l'interface graphique. Dans ce cas, il nous faut un mécanisme qui détecte ce changement et nous permette d'y réagir en conséquence. L'exposé ci-dessus fournit une possibilité via la surcharge de la méthode commitEdit. Cela impose toutefois de dériver la classe TableCell<S,T> ce qui est un travail laborieux.

Cas général.

Un des moyens les plus simples pour y parvenir est la mise en place d'un gestionnaire d'évènements via la propriété onEditCommit. On peut fixer le contenu de cette propriété via la méthode setOnEditCommit de la classe TableColumn.

Notez que de la même manière, il est possible de réagir à un abandon (setOnEditCancel) de la saisie ou à son démarrage (setOnEditCancel).

Cas particulier de la classe CheckBoxTableCell.

Peut être certains d'entre vous l'ont ils expérimenté : ces captures d'évènement ne fonctionnent pas avec la classe CheckBoxTableCell. En fait la documentation de cette nous classe nous en averti : Note that the CheckBoxTableCell renders the CheckBox 'live', meaning that the CheckBox is always interactive and can be directly toggled by the user. This means that it is not necessary that the cell enter its editing state (usually by the user double-clicking on the cell). A side-effect of this is that the usual editing callbacks (such as on edit commit) will not be called. If you want to be notified of changes, it is recommended to directly observe the boolean properties that are manipulated by the CheckBox.

En d'autre terme, le fonctionnement de la classe CheckBox sous-jacente est autonome et ne nécessite pas de passage en mode édition. Cela revient à dire que les méthodes startEdit, cancelEdit et commitEdit héritées de TableCell ne sont jamais invoquées. En conséquence, les mise en place des gestionnaires d'évènement vus ci-dessus ne sert à rien puisque ces évènements ne sont jamais déclenchés.

Du coup, et même si la documentation décrit le contraire, la mise à jour de la propriété liée à la cellule n'est pas non plus effectuée même après l'emploi explicite de la méthode setSelectedStateCallback qui est censée l'assurer. La difficulté semble résider dans le code de la méthodeupdateItem qui, elle, est invoquée. Dans ce code, la liaison bidirectionnelle devrait être assurée dès lors que la propriété liée hérite de la classe BooleanProperty. Dans les faits ce n'est pas le cas.

Pour contourner cette difficulté, le plus simple est de dériver sa propre classe. Voici un exemple qui peut vous inspirer :

public class CheckTableCell extends TableCell<IPrivate, Boolean>
{
  private final CheckBox _chk;

  public CheckTableCell(final Callback<Integer, ObservableValue<Boolean>> cbk)
  {
    getStyleClass().add("check-box-table-cell");
    _chk = new CheckBox();
    // Association d'un gestionnaire dès que la case est cochée ou décochée
    _chk.setOnAction(aev ->
    {
      int iRow = getIndex();
      IPrivate prv = getTableView().getItems().get(iRow);
      prv.setPrivate(_chk.isSelected());
    });
    setGraphic(_chk);
  }

  public CheckTableCell()
  {
    this(null);
  }

  @Override
  public void updateItem(Boolean blnData, boolean fEmpty)
  {
    if (fEmpty)
    {
      setText(null);
      setGraphic(null);
      return;
    }
    _chk.setSelected(blnData);
    setGraphic(_chk);
  }

  public static Callback<TableColumn<IPrivate,Boolean>, TableCell<IPrivate,Boolean>>
    forTableColumn(TableColumn<IPrivate,Boolean> tcl)
  {
    return forTableCell(null);
  }

  private static Callback<TableColumn<IPrivate,Boolean>, TableCell<IPrivate,Boolean>>
    forTableCell(final Callback<Integer, ObservableValue<Boolean>> cbk)
  {
    return new Callback<TableColumn<IPrivate,Boolean>, TableCell<IPrivate,Boolean>>()
    {
      @Override
      public TableCell<IPrivate,Boolean> call(TableColumn<IPrivate,Boolean> tcl)
      {
        return new CheckTableCell(cbk);
      }
    };
  }
}

Tout le "secret" réside dans l'association d'un gestionnaire d'évènement (voir le constructeur) à la case à cocher dès que la sélection de celle-ci est modifiée. Ce gestionnaire force alors la mise à jour de la propriété associée à al donnée booléenne transmise par un objet issu d'une classe qui implémente l'interface IPrivate. Cette interface ne contient que 2 méthodes : getPrivate et setPrivate.

Pour réagir à cette mise à jour afin, par exemple, d'adapter l'interface graphique, nous vous recommandons de ne pas le faire dans ce gestionnaire car cela entraînerait un couplage fort entre l'architecture MVC de votre liste tabulaire et votre application réduisant d'autant ses capacités d'évolution. Associez plutôt à la propriété booléenne liée au sein de la classe qui implémente l'interface IPrivate un gestionnaire d'évènement invoqué lors de chaque mise à jour. Voici par exemple comment modifier les contructeurs d ela classe PhoneRow :

public PhoneRow()
  {
    this(null);
  }

  public PhoneRow(ChangeListener<Boolean> cls)
  {
    _fNewly = true;
    _sbpPrivate = new SimpleBooleanProperty();
    if (cls != null)
      _sbpPrivate.addListener(cls);
    _sspPhoneNumber = new SimpleStringProperty();
  }

  public PhoneRow(boolean fNewly, boolean fPrivate, String sPhoneNumber, ChangeListener<Boolean> cls)
  {
    _fNewly = fNewly;
    _sbpPrivate = new SimpleBooleanProperty(fPrivate);
    if (cls != null)
      _sbpPrivate.addListener(cls);
    _sspPhoneNumber = new SimpleStringProperty(sPhoneNumber);
  }

Si vous décidez ensuite d'associer un gestionnaire pour traiter le changement d'état de la propriété booléenne, voici un exemple de la façon de procéder :

PhoneRow prw = new PhoneRow((ovl, blnOldValue, blnNewValue) ->
{
  boolean fOld = blnOldValue;
  boolean fNew = blnNewValue;
  if (fOld != fNew)
    lightRedLED();
});

Dans cet exemple, le changement d'état de la case à cocher entraîne la mise à jour de la propriété booléenne de l'objet de classe PhoneRow lié à la ligne. Cette mise à jour entraîne l'invocation du gestionnaire d'évènement lié au changement d'état qui allume alors une diode rouge quelque part sur l'interface graphique (méthode lightRedLED).

Tri des données.

Les en-têtes de colonne permettent le tri des lignes. Ce tri peut être ascendant (1er clic), descendant (second clic) ou annulé (3ème clic). L'ordre de tri peut néanmoins être fixé par code via la méthode TableColumn.setSortType. La méthode TableColumn.setSortable permet, quant à elle d'activer ou d'inhiber cette capacité de tri sur la colonne.

Un algorithme de tri par défaut permet de classer les lignes par rapport à une colonne lorsque les objets stockés par la colonne sont de des types primitifs. Lorsqu'il s'agit d'objets, ils sont classés selon un classement alphabétique sur chaînes de caractères crées par la méthode toString. Ce tri par défaut utilise donc un comparateur par défaut qui peut ne pas convenir aux objets stockés par les cellule d'une même colonne ou dont le classement nécessite la prise en compte des autres objets de la même ligne. A cette fin, il est possible de substituer au comparateur par défaut votre propre comparateur via la méthode setComparator.

Accès aux lignes ou aux cellules sélectionnés.

Une fois la liste affichée, il est souvent utile de pouvoir accéder à l'objet sélectionné ou à ses coordonnées. Il faut donc pouvoir passer de la "Vue" à des articles particuliers du "Modèle" (les articles sélectionnés dans la vue). Pour cela, la classe TableView dispose d'un second modèle qui s'intercale entre la vue et le modèle : le "modèle de sélection".

Ce modèle contient une projection des données du modèle qui sont sélectionnées. Il établit une relation entre ces données particulières et leurs coordonnées dans la vue. On accède à ce modèle via la méthode getSelectionModel. Un fois le modèle de sélection obtenu, on peut obtenir les objets sélectionnés ou leurs indices. Il est même possible d'accéder au contenu d'une cellule sélectionnée.

Il est également souvent indispensable de pouvoir réagir à la sélection d'une ligne d'une liste tabulaire pour adapter l'interface graphique en conséquence. Par exemple, si vous adoptez une interface graphique de type "maître-détail", le "détail" devra être mis à jour si la sélection "maître" est modifiée. Si on observe attentivement les évènements qu'il est possible de capturer depuis un TableView, on constate qu'aucun ne concerne la sélection.

Pour détecter le changement de sélection, il faut ajouter un gestionnaire qui implémente l'interface ChangeListener<S> à la propriété qui gère la sélection. Nous donnons un exemple de code ci-dessous :

_tvwCntMaster.getSelectionModel().selectedItemProperty().addListener((ovl, crwOld, crwNew) ->
{
  // Code du gestionnaire
  ...
}

Dans cet extrait _tvwCntMaster est une référence sur un objet de classe TableView. le paramètre ovl est de classe Observable<S>Sest la classe des objets contenu par le modèle du TableView. crwOld et crwNew sont des objets de la même classe S et représentent respectivement l'ancien et le nouvel objet sélectionné. Notez que crwOld peut être un objet nul si le TableView n'avait pas sélection antérieure.

(c) PiApplications 2016