Somewhere you are adding some node(s) to the GridPane
without specifying a columnIndex
and rowIndex
. (This can also, under some circumstances, happen even if you don't explicitly add them yourself. E.g. you have gridLinesVisible
set to true
on the GridPane
.) It looks like at least one of those nodes is a Group
; the output you describe is the default implementation of toString()
coming from a Group
instance.
The way the GridPane
manages grid locations internally is to set specific values via each node's property map; the values in this map for the column index and row index are Integer
properties. The getColumnIndex()
and getRowIndex()
retrieve those Integer
properties and effectively call intValue()
on them, so if they are not set, you get a NullPointerException
.
So a quick fix may be to do something like
public Node getNodeByRowColumnIndex(final int row,final int column,GridPane gridPane) {
Node result = null;
ObservableList<Node> childrens = gridPane.getChildren();
for(Node node : childrens) {
if(node instanceof Circle && gridPane.getRowIndex(node) == row && gridPane.getColumnIndex(node) == column) {
result = node;
break;
}
}
return result;
}
This will work if all the nodes you add with column indexes and row indexes properly set (and that you are interested in) are Circle
s. Obviously you can add some other logic to work around other scenarios.
The reason you are running into difficulties here is that you are effectively trying to retrieve data (what is at a location) from a view component (the GridPane
). This is an anti-pattern: views should not be storing application data, they should merely passively observe it (at most). If you still have an extensive amount of work to do on the project, you should probably think about restructuring it so it is data-centric.
Here are some code snippets that might help you think about what I mean here:
public class Sheep {
// data for intrinsic state of sheep omitted..
private final Circle view ;
public Sheep() {
this.view = new Circle(...);
// configure view...
}
public Node getView() {
return view ;
}
// logic etc... manipulates intrinsic state of sheep....
}
Make sure you implement Point
so it can be used properly in a collection:
public class Point {
final int x, y ;
@Override
public boolean equals(Object other) {
if (other.getClass() != Point.class) {
return false ;
}
Point o = (Point)other ;
return o.x == x && o.y == y ;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}
game code:
ObservableMap<Point, Sheep> allSheep = FXCollections.observableHashMap();
GridPane gameView = new GridPane();
allSheep.addListener((Change<? extends Point, ? extends Sheep> c) -> {
if (c.wasAdded()) {
Point location = c.getKey();
Sheep addedSheep = c.getValue();
gameView.add(addedSheep.getView(), location.x, location.y);
} else if (c.wasRemoved()) {
Sheep removedSheep = c.getValue();
gameView.getChildren().remove(removedSheep.getView());
}
}
Now the view is effectively set up to take care of itself, and you just work with the data, just by manipulating the map of locations to sheep:
public void addSheep(int x, int y) {
allSheep.put(new Point(x,y), new Sheep());
}
public void removeSheep(int x, int y) {
allSheep.remove(new Point(x, y));
}
Note the map contains all the data pertinent to the game, so you can examine it whenever you need and make changes appropriately. You should be able to separate that logic from the "view" of the game, and just have controller/presenter type classes encapsulating the link between them (for example, the map listener defined above, which updates the grid when the entries in the map change).