创建一个 小部件

小部件使我们能够查看,更改和与通过不同数据源发布的数据进行交互。 CameraServer,NetworkTables 和 Base 插件提供了用于控制基本数据类型(包括特定于 FRC 的数据类型)的小部件。但是,自定义小部件允许我们控制在上一节或 Java 对象中创建的自定义数据类型。

基本的 “Widget” 接口继承自 “Component” 和 “Sourced” 接口。“组件” 是在 Shuffleboard 中显示的最基本的组件构建块。Sourced 是用于处理和显示数据源或修改数据源的接口。不支持数据绑定但仅具有子节点的小部件将不使用 Sourced 接口,而仅使用 Component 接口。两者都是制作小部件的基本构建块,并允许我们修改和显示数据。

一个好的小部件可以让用户自定义窗口小部件以适应他们的需求。一个示例可能是允许用户控制数字滑块的范围,即其最大和最小值或滑块本身的方向。小部件的视图或其外观是使用 FXML 定义的。FXML 是一种基于 XML 的语言,可用于定义小部件(面板,标签和控件)的静态布局。

有关 FXML 的更多信息,请参见此处 https://openjfx.io/javadoc/11/javafx.fxml/javafx/fxml/doc-files/introduction_to_fxml.html

定义小组件的 FXML

在此示例中,我们将创建两个滑块,以帮助我们控制在上一节中创建的 Point2D 数据类型的 X 和 Y 坐标。将 FXML 文件与 Java Class 放在同一包中会很有帮助。

为了给我们的小部件创建一个空的空白窗口,我们需要创建一个 “Pane”。Pane 窗格是一个父节点,其中包含其他子节点,在本例中为2个滑块。有许多不同类型的窗格,如下所示:

  • 堆栈窗格

    • 堆栈窗格允许叠加的元素。另外,默认情况下,堆栈窗格默认为中心子节点。

  • 网格窗格

    • ”网格窗格“通过在窗格上创建行和列的灵活网格来使用坐标系,在定义子元素非常有用。

  • 流窗格

    • 流窗格将所有子节点都包装在边界集中。子节点可以垂直流动(包裹在窗格的高度边界处)或水平流动(包裹在窗格的宽度边界处)。

  • 锚定窗格

    • 锚定窗格允许把子元素放在上方、下方、左侧、右侧和窗格的中心

布局窗格在用 HBox(https://openjfx.io/javadoc/11/javafx.graphics/javafx/scene/layout/HBox.html)把子元素放在一行,或用 VBox(https://openjfx.io/javadoc/11/javafx.graphics/javafx/scene/layout/VBox.html)把子元素放在一列时,会非常有用。

使用 FXML 定义窗格的基本语法如下:

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

fx:controller 属性会包含小组件的类的名称。加载 FXML 文件时,将创建此类的实例。 为此,“控制器”类必须没有实际参数的构造函数。

创建一个小组件类

现在我们有了一个“窗格”,我们可以将子元素添加到该窗格中。在此示例中,我们可以添加两个滑块对象。记住要在每个元素上添加一个“ fx:id”,以便可以在我们稍后将要创建的Java类中引用。 我们将使用 “VBox” 将滑块放·置在彼此的顶部。

<?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>

现在我们已经完成了 FXML 文件的创建,现在可以创建一个小部件类了。小部件类应包含一个@Description 注释,该注释说明小部件支持的数据类型和小部件名称。如果不存在@Description 注解,则插件类必须实现 get()方法以返回其小部件。

它还必须包含一个 @ParametrizedController 注释,该注释指向包含小部件布局的 FXML 文件。 如果该类仅支持一个数据源,则必须扩展 SimpleAnnotatedWidget 类。如果该类支持多个数据源,则它必须扩展 ComplexAnnotatedWidget 类。 有关更多信息,请参见:widget-types。

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> {

}

如果您没有使用自定义数据类型,则可以引用任何 Java 数据类型(即 Double.class),或者如果小部件不需要数据绑定,则可以传递 NoneType.class。

现在我们已经创建了类,我们可以使用 @FXML 批注为在 FXML 文件中声明的小部件创建字段。 对于我们的两个滑块,一个示例是:

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;
}

为了在我们的自定义窗口小部件上显示我们的窗格,我们需要重写getView()方法并返回我们的 Stack Pane。

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;
   }

}

绑定元素和添加侦听器

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.

在这种情况下,示例将通过分别更改 xSlider 和 ySlider 的值来更改 2D 点的 X 和 Y 坐标。

一个好的做法是在带有@FXML批注的“ initialize()”方法中设置绑定,如果该方法不是“ public”,则必须从 FXML 调用该方法。

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.

当滑块或数据源已更改时,使用侦听器是另一种更改值的方法。例如,滑块的侦听器为:

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

在这种情况下,setData()方法会将小部件的数据源中的值设置为 newValue。

探索自定义组件

加载插件时不会自动发现窗口小部件;定义插件必须导出它才能使用。采用这种方法可以在同一个 JAR 中定义多个插件。

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

设置默认小组件数据类型

为了将小部件设置为自定义数据类型的默认值,您可以覆盖插件类中的 getDefaultComponents(),该类存储所有默认小部件的 Map,如下所示:

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