JavaFX: Update ImageView on GridPane click
Asked Answered
F

2

2

I'm working on a Minesweeper game using JavaFX:

(On game start)

My GridPane is made of ImageViews, and I'm trying to find a way to figure out the column and row indexes of the image that was clicked on the GridPane. I have this code currently:

gameGrid.setOnMouseClicked(event -> //gameGrid is my GridPane
{
    Node source = (Node)event.getSource();
    int columnIndex = GridPane.getColumnIndex(source);
    System.out.println(columnIndex);
    int rowIndex = GridPane.getRowIndex(source);
    if(event.getButton()== MouseButton.PRIMARY)
    game.newRevealCell(columnIndex,rowIndex);
    else if(event.getButton()==MouseButton.SECONDARY)

    game.getGrid()[columnIndex][rowIndex].setFlagged(true);
    //game.getGrid is a 2D array containing the game cells

});

However, the methods getColumnIndex and getRowIndex are returning null. What am I doing wrong?

Fleam answered 3/10, 2021 at 22:37 Comment(3)
#53459034 Check this!Borsch
A complicated solution using custom events. There are simpler solutions than the linked example. It was designed to demonstrate creating and handling custom events, which isn’t necessary to solve this problem, but it does provide one potential solution.Caustic
It would probably be easier to add a unique event handler to each "cell" (the "cell" is the node you place in the grid pane). That way you "just know" which cell was interacted with because only interacting with that specific cell will invoke that specific event handler.Salmanazar
G
3

Starting from this example, the variation below fills a GridPane with N x N instances of Button, adding each to a List<Button>. The method getGridButton() shows how to obtain a button reference efficiently based on its grid coordinates, and the action listener shows that the clicked and found buttons are identical.

How does each grid button know its own location on the grid? Each one gets its own instance of an anonymous class—the button's event handler—that has access to the row and column parameters passed to createGridButton(). These parameters are effectively final; in contrast, the original example using an earlier version of Java had to make the parameters explicitly final.

While this approach obtains the row and column of any Node in the grid, a Button or ToggleButton may be more convenient in this context. In particular, you can use setGraphic​() with ContentDisplay.GRAPHIC_ONLY to get the desired appearance.

As an aside, the GridPane methods getColumnIndex and getRowIndex return null as they refer to the child's index constraints, only if set, as illustrated here.

row major order

import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

/** @see https://mcmap.net/q/541837/-javafx-update-imageview-on-gridpane-click */
public class GridButtonTest extends Application {
    
    private static final int N = 5;
    private final List<Button> list = new ArrayList<>();
    
    private Button getGridButton(int r, int c) {
        int index = r * N + c;
        return list.get(index);
    }
    
    private Button createGridButton(int row, int col) {
        Button button = new Button("r" + row + ",c" + col);
        button.setOnAction((ActionEvent event) -> {
            System.out.println(event.getSource() == getGridButton(row, col));
        });
        return button;
    }
    
    @Override
    public void start(Stage stage) {
        stage.setTitle("GridButtonTest");
        GridPane root = new GridPane();
        for (int i = 0; i < N * N; i++) {
            int row = i / N;
            int col = i % N;
            Button gb = createGridButton(row, col);
            list.add(gb);
            root.add(gb, col, row);
            GridPane.setMargin(gb, new Insets(N));
        }
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.show();
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

For models using a two dimensional array, also consider a List<List<Button>>, illustrated below:

import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

/** @see https://mcmap.net/q/541837/-javafx-update-imageview-on-gridpane-click */
public class GridButtonTest extends Application {

    private static final int N = 5;
    private final List<List<Button>> list = new ArrayList<>();

    private Button getGridButton(int r, int c) {
        return list.get(r).get(c);
    }

    private Button createGridButton(int row, int col) {
        Button button = new Button("r" + row + ",c" + col);
        button.setOnAction((ActionEvent event) -> {
            System.out.println(event.getSource() == getGridButton(row, col));
        });
        return button;
    }

    @Override
    public void start(Stage stage) {
        stage.setTitle("GridButtonTest");
        GridPane root = new GridPane();
        for (int row = 0; row < N; row++) {
            list.add(new ArrayList<>());
            for (int col = 0; col < N; col++) {
                Button gb = createGridButton(row, col);
                list.get(row).add(gb);
                root.add(gb, col, row);
                GridPane.setMargin(gb, new Insets(N));
            }
        }
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}
Gyn answered 3/10, 2021 at 23:56 Comment(1)
A related example is seen here.Gyn
A
3

+1 for all the comments and the answer provided by @trashgod.

But trying to address your actual issue, I think using event.getSource is the root cause for your issue. The source returned from the event will be always the GridPane.

event.getTarget() method will give you the target node you clicked. So I think changing it to getTarget() will fix your issue.

Below is the working demo of what I mean. This works even if I dont set the index contraint explicitly.

Update: I was mistaken that explicit constraint settings is done only through GridPane.add method. But using add/addRow/addColumn methods sets the index constraints as well.

Note: This will work if the target node you clicked is a direct child of GridPane (in your case). But if your target node is not a direct child of GridPane, like if the StackPane has a Label and if you clicked on that Label, a null exception will be thrown.

import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class GridPaneIndexingDemo extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        StackPane root = new StackPane();
        Scene scene = new Scene(root, 450, 450);
        stage.setScene(scene);
        stage.setTitle("GridPane");
        stage.show();

        GridPane grid = new GridPane();
        grid.setGridLinesVisible(true);
        grid.addRow(0, getPane(1), getPane(2), getPane(3));
        grid.addRow(1, getPane(4), getPane(5), getPane(6));
        grid.addRow(2, getPane(7), getPane(8), getPane(9));
        grid.setOnMouseClicked(event -> {
            Node source = (Node) event.getTarget();
            int columnIndex = GridPane.getColumnIndex(source);
            int rowIndex = GridPane.getRowIndex(source);
            System.out.println("Row : " + rowIndex + ", Col : " + columnIndex);
        });
        root.getChildren().add(grid);
    }

    private StackPane getPane(int i) {
        String[] colors = {"grey", "yellow", "blue", "pink", "brown", "white", "silver", "orange", "lightblue", "grey"};
        StackPane pane = new StackPane();
        pane.setPrefSize(150,150);
        pane.setStyle("-fx-background-color:" + colors[i] + ";-fx-border-width:2px;");
        return pane;
    }

    public static void main(String[] args) {
        Application.launch(args);
    }
}
Alleneallentown answered 4/10, 2021 at 7:26 Comment(2)
If I understand correctly, addRow() sets the constraints implicitly; cited hereGyn
@trashgod, Thanks for the correction :). I quickly checked by adding the node using getChildren().add() and it is not setting the constraint.Alleneallentown

© 2022 - 2024 — McMap. All rights reserved.