Problems with accessing GridPane Node

2020-04-14 12:26发布

问题:

i hope you can help me with a little project I want to do. I'm trying to create a video game using JavaFX for the GUI and I'm having some troubles. Here is the code I made for practice and I don't know why this don't work.

public class Main extends Application {
public GridPane map;
private int MAXH = 40;
private int MAXV = 40;
Random r = new Random();

@Override
public void start(Stage primaryStage) throws Exception{

    map = new GridPane();
    map.setGridLinesVisible(true);

    map.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
        @Override
        public void handle(MouseEvent mouseEvent) {
            //Get X position
            int posX = (int) mouseEvent.getX() / 40;
            //Get Y position
            int posY = (int) mouseEvent.getY() / 40;

            System.out.println("(" + posX + " " + posY + ")");

            Canvas c = getCanvasNode(map, posX, posY);
        }
    });

    for (int i = 0; i < MAXH; i++){
        for(int j = 0; j < MAXV; j++){
            //For each cell create a Canvas node
            Canvas c = new Canvas(40, 40);
            GraphicsContext gc = c.getGraphicsContext2D();
            int k = r.nextInt();


            //Randomly paint the Canvas
            if (k % 3 == 0) gc.setFill(Color.BLUE);
            else if (k % 2 == 0) gc.setFill(Color.RED);
            else if (k % 7 == 0)gc.setFill(Color.GREEN);

            //Filling the Canvas
            gc.fillRect(0, 0, 40, 40);
            //Adding to our Grid Pane
            map.add(c, i, j);
        }
    }

    primaryStage.setHeight(700);
    primaryStage.setWidth(1200);
    primaryStage.setScene(new Scene(map));
    primaryStage.show();


}

private Canvas getCanvasNode(GridPane gridPane, int col, int row) {
    for (Node node : gridPane.getChildren()) {
        if (GridPane.getColumnIndex(node) == col && GridPane.getRowIndex(node) == row) {
            return (Canvas) node;
        }
    }
    return null;
}

public static void main(String[] args) {
    launch(args);
}}

The problem becomes when I use the function "getCanvasNode" which produces a null pointer exception and i don't know why.

What i want to do is get the node in which i click-on through the map Event Handler can someone help me?

PD: I dont know if this is the most efficient/simple way to do this, if someone could give me some tips it would be awesome. I'm new in Java/JavaFX.

回答1:

When you do

for (Node node : gridPane.getChildren()) { ... }

you iterate through all the child nodes of the grid pane, whether those are nodes you have added yourself, or nodes that are part of the internal implementation of the grid pane.

What is actually happening in your case is that, because you have called

map.setGridLinesVisible(true);

the grid pane has added a child node to represent the grid lines. This node doesn't have properties set for the columnIndex or rowIndex. So when the loop encounters that node, GridPane.getColumnIndex(node) throws a NullPointerException. (The implementation of getColumnIndex looks in the node's property map for an Integer object with a specific key, and then implicitly calls intValue() on the result. So if no value is set, it attempts to call intValue() on a null reference.)

So you can fix this by doing something along the lines of

Integer columnIndex = GridPane.getColumnIndex(node);

and then treating the special case:

if (columnIndex != null && columnIndex.intValue() == col)  

etc.

However, there is a far better approach to what you are trying to achieve. Instead of registering a listener with the pane, and then digging in to find out which child was clicked, just register listeners with the child nodes instead:

for (int i = 0; i < MAXH; i++){
    for(int j = 0; j < MAXV; j++){
        //For each cell create a Canvas node
        Canvas c = new Canvas(40, 40);
        GraphicsContext gc = c.getGraphicsContext2D();
        int k = r.nextInt();


        //Randomly paint the Canvas
        if (k % 3 == 0) gc.setFill(Color.BLUE);
        else if (k % 2 == 0) gc.setFill(Color.RED);
        else if (k % 7 == 0)gc.setFill(Color.GREEN);

        //Filling the Canvas
        gc.fillRect(0, 0, 40, 40);
        //Adding to our Grid Pane
        map.add(c, i, j);

        String message = "Click on cell ["+i+", "+j+"]";
        c.setOnMouseClicked(e -> {
            System.out.println(message);
            // do anything else you need with the canvas....
        });
    }
}


回答2:

The GridPane.getColumnIndex() returns Integer Java Object, which wraps and serves as boxing for primitive int Java data type. Primitive data type never can be null, however Object (so the Integer) can be assigned to null. If we read the java api documentation of GridPane.getColumnIndex() it says,

Returns:
the column index for the child or null if no column index was set

it can return null. The statement

if (GridPane.getColumnIndex(node) == col) {}

is implicitly broken down to

Integer colIndex = GridPane.getColumnIndex(node);
int colIndexVal = colIndex.intValue();
if (colIndexVal == col) {}

and if colIndex is null, then colIndex.intValue() throws the NullPointerException. Which is happening in your code.

The solution is to take into account the null values:

if (GridPane.getColumnIndex(node) != null 
     && GridPane.getColumnIndex(node) == col
     && GridPane.getRowIndex(node) != row
     && GridPane.getRowIndex(node) == row) {
            return (Canvas) node;
        }

The other approach can be changing the argument parameter type of col and row to Integer

private Canvas getCanvasNode( GridPane gridPane, Integer col, Integer row )

and use equals() since the now we are comparing two Integer objects. This will take care of null values:

if ( col.equals( GridPane.getColumnIndex( node ) ) 
     && row.equals( GridPane.getRowIndex( node ) ) ) {
    return ( Canvas ) node;
}

For more info please read on

  • Data types in Java. Boxing and Unboxing
  • Comparing Java primitive types and Objects

About the program itself, I think you are using too many Canvas objects there. You may use JavaFX Rectangle or Label controls instead.

Accessing the child according to col & row values can be simplified as

Node node = gridPane.getChildren().get( col * MAXV + row + 1 );
return (node instanceof Canvas) ? ( Canvas ) node : null;