0

I am trying to write a JavaFX Sudoku program where the user can fill a cell by:

  1. Clicking one of nine buttons (corresponding to 1-9)
  2. Clicking one of the cells

The way the code works is that when a button is clicked, the int corresponding to that button is assigned to selectedNum. And when the Cell is clicked, the Cell assigns its text to selectedNum. However, this is where the issue occurs--in implementing the setOnAction function inside the Cell constructor. Cell cannot access selectedNum. How can I pass selectedNum when it's a variable that is outside the scope of its class?

Cell Class:

import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;

/**
 * The Cell class inherits StackPane, representing one of the cells in Sudoku.
 * @author rkuni
 *
 */
public class Cell extends StackPane{
    
    private Text text; //displays the number inside the cell
    private Rectangle rect; //visual element for the cell
    
    /**
     * Default Constructor for Cell
     */
    public Cell() {
        text = new Text("");
        text.setFont(Font.font(30));
        rect = new Rectangle(50, 50);
        rect.setStyle("-fx-fill: white; -fx-stroke: black; -fx-stroke-width: 1;");
        this.getChildren().add(rect);
        this.getChildren().add(text);
        
        this.setOnMouseClicked(new EventHandler<MouseEvent>() { 
            @Override
            public void handle(MouseEvent t) {
                text.setText("" + 1); //when clicked, I want to set text to the number that is selected.
            }
        });
    }
}

Main Class

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.stage.Stage;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.text.Font;

public class Main extends Application {
    
    private int selectedNum = 0;
    
    @Override
    public void start(Stage primaryStage) {
        try {
            
            Group root = new Group();
            Cell cell1 = new Cell();
            cell1.setLayoutX(100);
            cell1.setLayoutY(100);
            root.getChildren().add(cell1);
            
            Button numSelection[] = new Button[9];
            for (int i = 0; i < 9; i++) {
                final int j = i; 
                numSelection[i] = new Button("" + (i + 1));
                numSelection[i].setLayoutY((i / 3) * 55 + 200);
                numSelection[i].setLayoutX((i % 3) * 55 + 630);
                numSelection[i].setMinHeight(50);
                numSelection[i].setMinWidth(50);
                numSelection[i].setFont(Font.font(20));
                root.getChildren().add(numSelection[i]);

                numSelection[i].setOnAction(new EventHandler<ActionEvent>() {
                    @Override
                    public void handle(ActionEvent arg0) {
                        selectedNum = j + 1;
                    }
                });
            }
            
            Scene scene = new Scene(root,900,500);
            primaryStage.setScene(scene);
            primaryStage.show();
            
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

Potential Solutions

A potential solution is to not have a Cell class at all, and the code will work. The problem is no longer there because all the variables are within the same scope. However, I'd like to keep the Cell class because it makes for more organized code. Here it is anyway:

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.stage.Stage;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;


public class Main extends Application {
    
    private int selectedNum; //will take the value of the last pressed button (1-9)
    
    @Override
    public void start(Stage primaryStage) {
        try {
            Group root = new Group();
            
            //setting up a stackpane that will contain a rect and text, making for a "cell"
            StackPane stack1 = new StackPane();
            Text text1 = new Text("");
            Rectangle rect1 = new Rectangle(50,50);
            rect1.setStyle("-fx-fill: white; -fx-stroke: black; -fx-stroke-width: 1;");
            stack1.getChildren().add(text1);
            stack1.getChildren().add(rect1);
            stack1.setLayoutX(100);
            stack1.setLayoutY(100);
            stack1.setOnMouseClicked(new EventHandler<MouseEvent>() { 
                @Override
                public void handle(MouseEvent t) {
                    text1.setText("" + selectedNum); //when clicked, set text to the number that is selected.
                }
            });
            
            //setting up the buttons that will designate which number will go into the cell.
            Button numButtons[] = new Button[9];
            for (int i = 0; i < 9; i++) {
                final int j = i;
                numButtons[i] = new Button("" + (i + 1));
                numButtons[i].setLayoutY((i / 3) * 55 + 200);
                numButtons[i].setLayoutX((i % 3) * 55 + 630);
                numButtons[i].setMinHeight(50);
                numButtons[i].setMinWidth(50);
                numButtons[i].setFont(Font.font(20));
                root.getChildren().add(numButtons[i]);

                numButtons[i].setOnAction(new EventHandler<ActionEvent>() {
                    @Override
                    public void handle(ActionEvent arg0) {
                        selectedNum = j + 1;
                    }
                });
            }
            
            text1.toFront();
            root.getChildren().add(stack1);
            Scene scene = new Scene(root,900,500);
            primaryStage.setScene(scene);
            primaryStage.show();
            
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

I've also thought about overriding the function to add a parameter but haven't gotten that to work successfully.

Roy K.
  • 25
  • 5
  • There are multiple ways to do this. You can either publish an event in `Cell` and catch it in `Main`. Or you could give Cell a new function property, that is called in your `mouseclicked` eventhandler. You could set this inside the constructor of your `Main` class – Amir Schnell Dec 16 '20 at 15:53
  • Or create an `IntegerProperty` in your main class to hold the selected value, and pass it to your `Cell`. – James_D Dec 16 '20 at 16:26

1 Answers1

3

The (more-or-less) standard approach for things like this is to use some kind of variant of the MVC design pattern. The key thing here is to have a separate class ("model") that holds the data; you can then pass a single instance of that class to any actor that needs access to it.

public class Model {

    private int currentlySelectedValue ;

    // probably other properties here...

    public void setCurrentlySelectedValue(int value) {
        currentlySelectedValue = value ;
    }

    public int getCurrentlySelectedValue() {
        return currentlySelectedValue ;
    }
}

Then you can do:

public class Cell extends StackPane{

    private final Model model ;
    
    private Text text; //displays the number inside the cell
    private Rectangle rect; //visual element for the cell
    
    /**
     * Default Constructor for Cell
     */
    public Cell(Model model) {
        this.model = model ;
        text = new Text("");
        text.setFont(Font.font(30));
        rect = new Rectangle(50, 50);
        rect.setStyle("-fx-fill: white; -fx-stroke: black; -fx-stroke-width: 1;");
        this.getChildren().add(rect);
        this.getChildren().add(text);
        
        this.setOnMouseClicked(new EventHandler<MouseEvent>() { 
            @Override
            public void handle(MouseEvent t) {
                text.setText("" + model.getCurrentlySelectedValue()); 
            }
        });
    }
}

and

public class Main extends Application {
    
    private int selectedNum = 0;
    
    @Override
    public void start(Stage primaryStage) {

        Model model = new Model();

        try {
            
            Group root = new Group();
            Cell cell1 = new Cell(model);
            cell1.setLayoutX(100);
            cell1.setLayoutY(100);
            root.getChildren().add(cell1);
            
            Button numSelection[] = new Button[9];
            for (int i = 0; i < 9; i++) {
                final int j = i + 1; 
                numSelection[i] = new Button("" + j);
                numSelection[i].setLayoutY((i / 3) * 55 + 200);
                numSelection[i].setLayoutX((i % 3) * 55 + 630);
                numSelection[i].setMinHeight(50);
                numSelection[i].setMinWidth(50);
                numSelection[i].setFont(Font.font(20));
                root.getChildren().add(numSelection[i]);

                numSelection[i].setOnAction(new EventHandler<ActionEvent>() {
                    @Override
                    public void handle(ActionEvent arg0) {
                        model.setCurrentlySelectedValue(j);
                    }
                });
            }
            
            Scene scene = new Scene(root,900,500);
            primaryStage.setScene(scene);
            primaryStage.show();
            
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

Note that there are some "shortcut" approaches if all you need is one single value; e.g. you could use an IntegerProperty instead of a custom model class. The structure and concept is really the same though.

James_D
  • 201,275
  • 16
  • 291
  • 322
  • So this is a showcase of references in Java? Each cell has a `Model` object that is a reference to the `model` inside `Main`. And as the `selectedNum` in `model` changes, each cell has access to it because they have a reference to it. Wow, this blew my mind. – Roy K. Dec 16 '20 at 19:00
  • Is the MVC design pattern common with JavaFX? – Roy K. Dec 16 '20 at 19:02
  • @RoyK. Yes, I guess. This is just how Java works... all objects are accessed via references. – James_D Dec 16 '20 at 19:04
  • @RoyK. And yes, MVC is common in any UI development, regardless of toolkit. It's a design pattern that goes back to the 1970s; there are a lot of different variations these days (MVP, MVVM) and it's also been adapted for use in web applications (though that adaptation makes it look pretty different). See https://stackoverflow.com/questions/32342864/applying-mvc-with-javafx – James_D Dec 16 '20 at 19:06