Création d’un widget

Les widgets nous permettent de visualiser, de modifier et d’interagir avec les données publiées via différentes sources de données. Les plug-ins CameraServer, NetworkTables et Base fournissent les widgets pour contrôler les types de données de base (y compris les types de données spécifiques à FRC). Cependant, les widgets personnalisés nous permettent de contrôler nos types de données personnalisés que nous avons créés dans les sections précédentes ou les objets Java.

L’interface de base Widget hérite des interfaces Component et Sourced . Component est le bloc de construction le plus élémentaire des composants à afficher dans Shuffleboard. Sourced est une interface pour les choses qui peuvent gérer et interagir avec des sources de données pour afficher ou modifier des données. Les widgets qui ne prennent pas en charge les liaisons de données mais qui ont simplement des nœuds-enfants n’utiliseraient pas l’interface « Sourced » mais simplement l’interface Component . Les deux sont des éléments de base pour créer des widgets et nous permettent de modifier et d’afficher des données.

Un bon widget permet à l’utilisateur de personnaliser le widget en fonction de ses besoins. Un exemple pourrait être de permettre à l’utilisateur de contrôler la plage d’un curseur numérique, c’est-à-dire son maximum et son minimum ou l’orientation du curseur lui-même. La vue du widget ou son apparence est définie à l’aide de FXML. FXML est un langage basé sur XML qui est utile pour définir la disposition statique du widget (volets, étiquettes et contrôles).

Plus d’informations sur FXML peuvent être trouvées ici.

Définition du FXML d’un widget

Dans cet exemple, nous allons créer deux curseurs pour nous aider à contrôler les coordonnées X et Y de notre type de données Point2D que nous avons créé dans les sections précédentes. Il est utile de placer le fichier FXML dans le même package que la classe Java.

Afin de créer une fenêtre vide et initiale pour notre widget, nous devons créer un volet, ou Pane. Un volet est un nœud-parent qui contient d’autres nœuds-enfants, dans ce cas, 2 curseurs. Il existe de nombreux types de volets différents, comme indiqué:

  • Volet de pile -Stack-

    • Les panneaux de pile permettent de superposer des éléments. En outre, StackPanes par défaut des nœuds enfants centraux.

  • Volet de grille

    • Les volets de grille sont extrêmement utiles pour définir des éléments- enfants à l’aide d’un système de coordonnées en créant une grille flexible de lignes et de colonnes sur le volet.

  • Volet de flux

    • Les volets de flux enveloppent tous les nœuds enfants-dans un ensemble de limites. Les nœuds-enfants peuvent circuler verticalement (enveloppés à la limite de hauteur du volet) ou horizontalement (enveloppés à la limite de largeur du volet).

  • Volet d’ancrage

    • Les volets d’ancrage permettent de placer des éléments-enfants en haut, en bas, à gauche, à droite ou au centre du volet.

Les volets de disposition sont extrêmement utiles pour placer des nœuds- enfants sur une ligne horizontale à l’aide d’un HBox ou d’une verticale à l’aide d’une colonne VBox.

La syntaxe de base pour définir un volet à l’aide de FXML serait la suivante:

<?import javafx.scene.layout.*?>
<StackPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="/path/to/widget/class" fx:id="root">
   ...
</StackPane>

L’attribut fx:controller contient le nom de la classe de widget. Une instance de cette classe est créée lors du chargement du fichier FXML. Pour que cela fonctionne, la classe de contrôleur doit avoir un constructeur sans argument.

Création d’une classe de widgets

Maintenant que nous avons un volet, nous pouvons maintenant ajouter des éléments-enfants à ce volet. Dans cet exemple, nous pouvons ajouter deux objets curseur. N’oubliez pas d’ajouter un fx:id à chaque élément afin qu’ils puissent être référencés dans notre classe Java que nous ferons plus tard. Nous utiliserons une VBox pour positionner notre curseur l’un sur l’autre.

<?import javafx.scene.layout.*?>
<StackPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="/path/to/widget/class" fx:id="root">

   <VBox>
      <Slider fx:id = "xSlider"/>
      <Slider fx:id = "ySlider"/>
   </VBox>

</StackPane>

Maintenant que nous avons fini de créer notre fichier FXML, nous pouvons maintenant créer une classe de widget. La classe de widget doit inclure une annotation @Description qui indique les types de données pris en charge du widget et le nom du widget. Si une annotation @Description n’est pas présente, la classe du plugin doit implémenter la méthode get() pour retourner ses widgets.

Elle doit également inclure une annotation @ParametrizedController qui pointe vers le fichier FXML contenant la mise en page du widget. Si la classe qui ne prend en charge qu’une seule source de données, elle doit étendre la classe SimpleAnnotatedWidget. Si la classe prend en charge plusieurs sources de données, elle doit étendre la classe ComplexAnnotatedWidget. Pour plus d’informations, voir Types de widgets.

import edu.wpi.first.shuffleboard.api.widget.Description;
import edu.wpi.first.shuffleboard.api.widget.ParametrizedController;
import edu.wpi.first.shuffleboard.api.widget.SimpleAnnotatedWidget;

/*
 * If the FXML file and Java file are in the same package, that is the Java file is in src/main/java and the
 * FXML file is under src/main/resources or your code equivalent package, the relative path will work
 * However, if they are in different packages, an absolute path will be required.
*/

@Description(name = "MyPoint2D", dataTypes = MyPoint2D.class)
@ParametrizedController("Point2DWidget.fxml")
public final class Point2DWidget extends SimpleAnnotatedWidget<MyPoint2D> {

}

Si vous n’utilisez pas de type de données personnalisées, vous pouvez référencer n’importe quel type de données Java (par exemple, Double.class), ou si le widget n’a pas besoin de liaison de données, vous pouvez passer NoneType.class.

Maintenant que nous avons créé notre classe, nous pouvons créer des champs pour les widgets que nous avons déclarés dans notre fichier FXML en utilisant l’annotation @FXML. Pour nos deux curseurs, un exemple serait:

import edu.wpi.first.shuffleboard.api.widget.Description;
import edu.wpi.first.shuffleboard.api.widget.ParametrizedController;
import edu.wpi.first.shuffleboard.api.widget.SimpleAnnotatedWidget;
import javafx.fxml.FXML;

@Description(name = "MyPoint2D", dataTypes = MyPoint2D.class)
@ParametrizedController("Point2DWidget.fxml")
public final class Point2DWidget extends SimpleAnnotatedWidget<MyPoint2D> {

   @FXML
   private Pane root;

   @FXML
   private Slider xSlider;

   @FXML
   private Slider ySlider;
}

Afin d’afficher notre volet sur notre widget personnalisé, nous devons remplacer la méthode getView() et renvoyer notre StackPane.

import edu.wpi.first.shuffleboard.api.widget.Description;
import edu.wpi.first.shuffleboard.api.widget.ParametrizedController;
import edu.wpi.first.shuffleboard.api.widget.SimpleAnnotatedWidget;
import javafx.fxml.FXML;

@Description(name = "MyPoint2D", dataTypes = MyPoint2D.class)
@ParametrizedController("Point2DWidget.fxml")
public final class Point2DWidget extends SimpleAnnotatedWidget<MyPoint2D> {

   @FXML
   private StackPane root;

   @FXML
   private Slider xSlider;

   @FXML
   private Slider ySlider;

   @Override
   public Pane getView() {
      return root;
   }

}

Lier des éléments et ajouter des écouteurs

Binding is a mechanism that allows JavaFX widgets to express direct relationships with the data source. For example, changing a widget will change its related NetworkTableEntry and vice versa.

Un exemple, dans ce cas, serait de changer les coordonnées X et Y de notre point 2D en changeant les valeurs de xSlider et ySlider respectivement.

Une bonne pratique consiste à définir des liaisons dans la méthode initialize() étiquetée avec l’annotation @FXML qui est nécessaire pour appeler la méthode depuis FXML si la méthode n’est pas déclarée public.

import edu.wpi.first.shuffleboard.api.widget.Description;
import edu.wpi.first.shuffleboard.api.widget.ParametrizedController;
import edu.wpi.first.shuffleboard.api.widget.SimpleAnnotatedWidget;
import javafx.fxml.FXML;

@Description(name = "MyPoint2D", dataTypes = MyPoint2D.class)
@ParametrizedController("Point2DWidget.fxml")
public final class Point2DWidget extends SimpleAnnotatedWidget<MyPoint2D> {

   @FXML
   private StackPane root;

   @FXML
   private Slider xSlider;

   @FXML
   private Slider ySlider;

   @FXML
   private void initialize() {
      xSlider.valueProperty().bind(dataOrDefault.map(MyPoint2D::getX));
      ySlider.valueProperty().bind(dataOrDefault.map(MyPoint2D::getY));
   }

   @Override
   public Pane getView() {
      return root;
   }

 }

The above initialize method binds the slider’s value property to the MyPoint2D data class” corresponding X and Y value. Meaning, changing the slider will change the coordinate and vice versa. The dataOrDefault.map() method will get the data source’s value, or, if no source is present, will return the default value.

L’utilisation d’un écouteur est une autre façon de modifier les valeurs lorsque le curseur ou la source de données a changé. Par exemple, un écouteur pour notre curseur serait:

xSlider.valueProperty().addListener((observable, oldValue, newValue) -> setData(getData().withX(newValue));

Dans ce cas, la méthode setData() définit la valeur de la source de données du widget sur la newValue.

Explorer les composants personnalisés

Les widgets ne sont pas automatiquement trouvés lors du chargement des plugins; le plugin de définition doit l’exporter explicitement pour qu’il soit utilisable. Cette approche est adoptée pour permettre à plusieurs plugins d’être définis dans le même JAR.

@Override
public List<ComponentType> getComponents() {
  return List.of(WidgetType.forAnnotatedWidget(Point2DWidget.class));
}

Définir le widget par défaut pour le type de données

Afin de définir votre widget par défaut pour votre type de données personnalisé, vous pouvez remplacer le getDefaultComponents() dans votre classe de plugin qui stocke une carte pour tous les widgets par défaut comme indiqué ci-dessous:

@Override
public Map<DataType, ComponentType> getDefaultComponents() {
   return Map.of(Point2DType.Instance, WidgetType.forAnnotatedWidget(Point2DWidget.class));
}