Issue
I have the following extended JavaFX object in my code:
package shipsinspace.view.board;
import javafx.scene.shape.Rectangle;
import shipsinspace.common.Coordinates;
public class Tile extends Rectangle {
private Coordinates coordinates;
public Tile(double width, double height, Coordinates coordinates) {
super(width, height);
this.coordinates = coordinates;
}
public Coordinates getCoordinates() {
return coordinates;
}
}
It uses this customer Java class I wrote to keep track of Tile location:
package shipsinspace.common;
import java.util.Objects;
public class Coordinates {
private int xCoordinate;
private int yCoordinate;
public Coordinates(int xCoordinate, int yCoordinate) {
this(xCoordinate, yCoordinate, 10, false);
}
public Coordinates(int xCoordinate, int yCoordinate, int max) {
this(xCoordinate, yCoordinate, max, false);
}
public Coordinates(int xCoordinate, int yCoordinate, int max, boolean allowedZero) {
if (allowedZero) {
if ((xCoordinate >= 0 && yCoordinate >= 0) && (xCoordinate <= max && yCoordinate <= max)) {
this.xCoordinate = xCoordinate;
this.yCoordinate = yCoordinate;
} else {
throw new IllegalArgumentException(String.format("Either X or Y has set to value <= 0, or > %d", max));
}
} else {
if ((xCoordinate > 0 && yCoordinate > 0) && (xCoordinate <= max && yCoordinate <= max)) {
this.xCoordinate = xCoordinate;
this.yCoordinate = yCoordinate;
} else {
throw new IllegalArgumentException(String.format("Either X or Y has set to value <= 0, or > %d", max));
}
}
}
public int getX() {
return xCoordinate;
}
public int getY() {
return yCoordinate;
}
public Coordinates returnNeighbour(int axis, int direction) {
if (axis == 0) {
try {
return new Coordinates(this.getX() + direction, this.getY());
} catch (IllegalArgumentException e) {
return new Coordinates(this.getX(), this.getY());
}
} else {
try {
return new Coordinates(this.getX(), this.getY() + direction);
} catch (IllegalArgumentException e) {
return new Coordinates(this.getX(), this.getY());
}
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Coordinates that = (Coordinates) o;
return xCoordinate == that.xCoordinate &&
yCoordinate == that.yCoordinate;
}
@Override
public int hashCode() {
return Objects.hash(xCoordinate, yCoordinate);
}
@Override
public String toString() {
return String.format("Coordinates (%d, %d)", xCoordinate, yCoordinate);
}
}
Now, I would like to build a scene (via JavaFX Scene Builder), which uses a GridPane, with a TILE object in each of its cells. I have decided to first build a scene in Scene Builder, using JavaFX Rectangle objects instead of Tiles, and then manually edited the .fxml file and changed Rectangle to Tile obejcts in there. The problem is, that Intellij now tells me Tile objects in FXML file are not possible to instantiate:
...
<center>
<GridPane BorderPane.alignment="CENTER">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<Tile arcHeight="5.0" arcWidth="5.0" fill="DODGERBLUE" height="52.0" stroke="BLACK" strokeType="INSIDE" width="53.0" GridPane.halignment="CENTER" GridPane.valignment="CENTER">
<GridPane.margin>
<Insets bottom="1.0" left="1.0" right="1.0" top="1.0" />
</GridPane.margin>
</Tile>
...
I can tell, that this is because my Tile object requires a Coordinates object to be passed into its constructor, other than height and width, but I cannot figure out how to paste them into the FXML code. Any help would be greatly appreciated.
Solution
To allow the FXMLLoader
to instantiate classes without no-arg constructors, you need to annotate the constructor parameters. (The reason for this is that in Java parameter names are not guaranteed to be retained at runtime, so a mechanism is needed to reflectively match unordered values to parameters based on names at runtime.) See What is the purpose of @NamedArg annotation in javaFX 8? for more information.
So your Tile
and Coordinates
classes now look like:
import javafx.beans.NamedArg;
import javafx.scene.shape.Rectangle;
public class Tile extends Rectangle {
private Coordinates coordinates;
public Tile(
@NamedArg("width") double width,
@NamedArg("height") double height,
@NamedArg("coordinates") Coordinates coordinates) {
super(width, height);
this.coordinates = coordinates;
}
public Coordinates getCoordinates() {
return coordinates;
}
}
import java.util.Objects;
import javafx.beans.NamedArg;
public class Coordinates {
private int xCoordinate;
private int yCoordinate;
public Coordinates(
@NamedArg("xCoordinate") int xCoordinate,
@NamedArg("yCoordinate") int yCoordinate) {
this(xCoordinate, yCoordinate, 10, false);
}
public Coordinates(
@NamedArg("xCoordinate") int xCoordinate,
@NamedArg("yCoordinate") int yCoordinate,
@NamedArg("max") int max) {
this(xCoordinate, yCoordinate, max, false);
}
public Coordinates(
@NamedArg("xCoordinate") int xCoordinate,
@NamedArg("yCoordinate") int yCoordinate,
@NamedArg("max") int max,
@NamedArg("allowedZero") boolean allowedZero) {
if (allowedZero) {
if ((xCoordinate >= 0 && yCoordinate >= 0) && (xCoordinate <= max && yCoordinate <= max)) {
this.xCoordinate = xCoordinate;
this.yCoordinate = yCoordinate;
} else {
throw new IllegalArgumentException(String.format("Either X or Y has set to value <= 0, or > %d", max));
}
} else {
if ((xCoordinate > 0 && yCoordinate > 0) && (xCoordinate <= max && yCoordinate <= max)) {
this.xCoordinate = xCoordinate;
this.yCoordinate = yCoordinate;
} else {
throw new IllegalArgumentException(String.format("Either X or Y has set to value <= 0, or > %d", max));
}
}
}
// remaining code unaltered...
}
To use this in FXML, you can do:
<Tile width="100.0" height="100.0">
<coordinates>
<Coordinates xCoordinate="1" yCoordinate="1"/>
</coordinates>
</Tile>
or
<fx:define>
<Coordinates fx:id="tileCoordinates" xCoordinate="1" yCoordinate="1" />
</fx:define>
<Tile width="100.0" height="100.0" coordinates="$tileCoordinates" />
The choice between these is mostly just a choice of style; however note that the latter options gives you the opportunity for multiple Tile
s to share the same Coordinates
instance (probably not applicable in this particular use case, but in general it can be useful).
Answered By - James_D