Add invisible row in javafx gridpane - java

I would like to add an invisible row in a gridpane. Then I would like to show and hide it based on user selection. I've managed to get the general functionality working but the problem is when the row content are hidden there is an extra space. I am not sure if this is the actual row or the vgap? Anyway I would like to get rid of this extra space.
Here is my gridpane :
public class CustomGrid extends GridPane{
public CustomGrid() {
this.setHgap(20);
this.setVgap(20);
ColumnConstraints firstColConstraints = new ColumnConstraints();
firstColConstraints.setHalignment(HPos.RIGHT);
firstColConstraints.setPercentWidth(50.0);
firstColConstraints.setHgrow(Priority.ALWAYS);
ColumnConstraints secondColConstraints = new ColumnConstraints();
ColumnConstraints thirdColConstraints = new ColumnConstraints();
thirdColConstraints.setHgrow(Priority.ALWAYS);
thirdColConstraints.setHalignment(HPos.LEFT);
this.getColumnConstraints().addAll(firstColConstraints, secondColConstraints, thirdColConstraints);
VBox.setMargin(this, new Insets(10, 0, 50, 0));
}
I've added content then hidden it like so
Pane content = new Pane()
pane.setVisible(false);
pane.setManaged(false);
add(content, 0, 1);
I'm happy with it when the user makes it visible but when it is hidden there is an extra gap between the rows. Can anyone tell me a way to fix that.
Thank you.

As mentioned in the comment (link to comment):
Change the height to 0 when hidden.

I think the best way to handle this is to actually remove the row from the grid pane rather than trying to make the row "invisible". Inserting and removing rows at arbitrary locations in a GridPane is a little tricky as the GridPane API does not have native support of such functions. So I created a little utility based upon your custom GridPane implementation which demonstrates adding and removing rows within the GridPane. To do so, the custom implementation has to keep an internal notion of which nodes are located at which rows and to update the column constraints of each of the nodes and each of the rows whenever new rows are added or deleted. Hiding can be accomplished by removing a row from the grid pane and then, at a later stage, adding the same row back into the grid pane at the index from which it was earlier removed. I understand this isn't exactly what you were looking for, but it seemed the most reasonable solution to me.
/** Click on a Label to remove the row containing the label from the GridPane */
import javafx.application.Application;
import javafx.collections.*;
import javafx.geometry.*;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import java.util.*;
public class HiddenNodes extends Application {
private static final int N_COLS = 3;
private static final int INIT_N_ROWS = 10;
private int nextRowId = 0;
private Node[] lastRemovedRow = null;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
CustomGrid grid = new CustomGrid();
for (int i = 0; i < INIT_N_ROWS; i++) {
Label[] labels = createRow(grid);
grid.insertRow(i, labels);
}
Button prepend = new Button("Prepend");
prepend.setOnAction(event -> {
Label[] labels = createRow(grid);
grid.insertRow(0, labels);
});
Button append = new Button("Append");
append.setOnAction(event -> {
Label[] labels = createRow(grid);
grid.insertRow(grid.getRowsSize(), labels);
});
HBox controls = new HBox(10, prepend, append);
VBox layout = new VBox(10, controls, grid);
layout.setPadding(new Insets(10));
stage.setScene(new Scene(layout, 200, 600));
stage.show();
}
private Label[] createRow(CustomGrid grid) {
Label[] labels = new Label[N_COLS];
for (int j = 0; j < N_COLS; j++) {
final int fj = j;
labels[j] = new Label(nextRowId + ":" + j);
labels[j].setStyle("-fx-background-color: coral;");
labels[j].setOnMouseClicked((MouseEvent e) -> {
lastRemovedRow = grid.removeRowContaining(labels[fj]);
});
}
nextRowId++;
return labels;
}
class CustomGrid extends GridPane {
private ObservableList<Node[]> rows = FXCollections.observableArrayList();
CustomGrid() {
this.setHgap(20);
this.setVgap(20);
ColumnConstraints firstColConstraints = new ColumnConstraints();
firstColConstraints.setHalignment(HPos.RIGHT);
firstColConstraints.setPercentWidth(50.0);
firstColConstraints.setHgrow(Priority.ALWAYS);
ColumnConstraints secondColConstraints = new ColumnConstraints();
ColumnConstraints thirdColConstraints = new ColumnConstraints();
thirdColConstraints.setHgrow(Priority.ALWAYS);
thirdColConstraints.setHalignment(HPos.LEFT);
this.getColumnConstraints().addAll(firstColConstraints, secondColConstraints, thirdColConstraints);
VBox.setMargin(this, new Insets(10, 0, 50, 0));
}
void insertRow(int rowIdx, Node... nodes) {
for (int i = rows.size() - 1; i >= rowIdx; i--) {
for (int j = 0; j < rows.get(i).length; j++) {
setConstraints(rows.get(i)[j], j, i + 1);
}
}
addRow(rowIdx, nodes);
rows.add(rowIdx, nodes);
}
private Node[] removeRow(int rowIdx) {
for (int i = rows.size() - 1; i > rowIdx; i--) {
for (int j = 0; j < rows.get(i).length; j++) {
setConstraints(rows.get(i)[j], j, i - 1);
}
}
for (Node node: rows.get(rowIdx)) {
getChildren().remove(node);
}
return rows.remove(rowIdx);
}
Node[] removeRowContaining(Node node) {
Iterator<Node[]> rowIterator = rows.iterator();
int rowIdx = 0;
while (rowIterator.hasNext()) {
Node[] row = rowIterator.next();
for (Node searchNode : row) {
if (searchNode == node) {
return removeRow(rowIdx);
}
}
rowIdx++;
}
return null;
}
int getRowsSize() {
return rows.size();
}
}
}
The above program is just an outline, additional logic would be required to reshow the "hidden" node (which has been removed from the grid) at the appropriate row index (which is a reasonably tricky problem).
Note: You might consider using a TableView rather than a GridPane. By manipulating the item list backing the TableView it is quite trivial to add and remove nodes to the TableView. Of course, I understand a TableView is not the appropriate solution for all problems of this kind (as the TableView comes with a bunch of other functionality, like headers and sorting, which you may not wish to have).

Related

Navigating out of a TextField in a GridPane using the arrow keys in JavaFX

I am making a sodoku solver program in Java with the JavaFX library. The program incorporates an interactive sodoku board consisting of a series of TextFields in a GridPane. The board looks like this:
Right now, the cursor is in the top left most TextField. If the field had text in it, the user would be able to move the cursor through the text by using the arrow keys. However, I want the user to be able to use the arrow keys to navigate to a different TextField. The issue is, the field is in "typing mode" (I don't know the official terminology) so the arrow keys only move the cursor to a different point in the text, but otherwise it stays in the same field.
This is what I mean:
Pretend that line I drew is the cursor. Right now, if I click the left arrow key, the cursor will move to the left of the 1, but I want it to move the the TextField on the left, instead. If I click the down arrow key, nothing happens because there is no text below the 1 for the cursor to navigate to, but I want it to move to the TextField below, instead.
The code for the GridPane is this:
TextField[][] squares = new TextField[9][9];
GridPane grid = new GridPane();
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
squares[i][j] = new TextField();
squares[i][j].setPrefHeight(8);
squares[i][j].setPrefWidth(25);
grid.add(squares[i][j], j, i);
}
}
grid.setAlignment(Pos.CENTER);
The squares array is for me to have access to individual TextFields in the GridPane.
Any suggestions for how I can fix this?
To avoid the focused TextField from handling arrow keys at all you need to intercept the KeyEvent before it reaches said TextField. This can be accomplished by adding an event filter to the GridPane and consuming the event as appropriate. If you're not sure why this works you can check out the JavaFX: Handling Events tutorial.
Then you can use Node#requestFocus() to programmatically change the focused node.
I also recommend setting the prefColumnCount of each TextField rather than trying to set the preferred dimensions manually. That way the preferred dimensions are computed based on the font size.
Here's an example:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.text.Font;
import javafx.stage.Stage;
public class App extends Application {
private TextField[][] fields;
#Override
public void start(Stage primaryStage) {
GridPane grid = new GridPane();
grid.setHgap(3);
grid.setVgap(3);
grid.setPadding(new Insets(5));
grid.setAlignment(Pos.CENTER);
grid.addEventFilter(KeyEvent.KEY_PRESSED, this::handleArrowNavigation);
fields = new TextField[9][9];
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
fields[i][j] = createTextField();
grid.add(fields[i][j], j, i);
}
}
primaryStage.setScene(new Scene(grid));
primaryStage.show();
}
private void handleArrowNavigation(KeyEvent event) {
Node source = (Node) event.getSource(); // the GridPane
Node focused = source.getScene().getFocusOwner();
if (event.getCode().isArrowKey() && focused.getParent() == source) {
int row = GridPane.getRowIndex(focused);
int col = GridPane.getColumnIndex(focused);
// Switch expressions were standardized in Java 14
switch (event.getCode()) {
case LEFT -> fields[row][Math.max(0, col - 1)].requestFocus();
case RIGHT -> fields[row][Math.min(8, col + 1)].requestFocus();
case UP -> fields[Math.max(0, row - 1)][col].requestFocus();
case DOWN -> fields[Math.min(8, row + 1)][col].requestFocus();
}
event.consume();
}
}
private TextField createTextField() {
TextField field = new TextField();
// Rather than setting the pref sizes manually this will
// compute the pref sizes based on the font size.
field.setPrefColumnCount(1);
field.setFont(Font.font(20));
field.setTextFormatter(
new TextFormatter<>(
change -> {
// Only allow the text to be empty or a single digit between 1-9
if (change.getControlNewText().matches("[1-9]?")) {
// Without this the text goes "off screen" to the left. This also
// seems to have the added benefit of selecting the just-entered
// text, which makes replacing it a simple matter of typing another
// digit.
change.setCaretPosition(0);
return change;
}
return null;
}));
return field;
}
}
The above also adds a TextFormatter to each TextField to show a way to limit the text to digits between 1 and 9. Note the arrow navigation does not "wrap around" when it reaches the end of a row or column. You can of course modify the code to implement this, if desired.
You may want to consider creating a model for the game. That way the business logic is not tied directly to JavaFX UI objects. When you update the model it would notify the view (possibly via a "view model", depending on the architecture) and the view will update itself accordingly.
You need to set the event handler to move on arrow key press as you can see below look at the setTextHandler function there is no error handling I just wrote this up to give you an idea of what you should be doing it is called from the loop when you create the TextFields from there it checks for an arrow key press and from there it will .requestFocus() from the next TextField
public class Main extends Application {
private TextField[][] squares;
#Override
public void start(Stage primaryStage) throws Exception {
squares = new TextField[9][9];
GridPane grid = new GridPane();
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
squares[i][j] = new TextField();
squares[i][j].setPrefHeight(8);
squares[i][j].setPrefWidth(25);
setTextHandler(squares[i][j], i, j);
grid.add(squares[i][j], j, i);
}
}
grid.setAlignment(Pos.CENTER);
Scene scene = new Scene(grid);
primaryStage.setScene(scene);
primaryStage.show();
}
private void setTextHandler(TextField textField, int i, int j){
textField.setOnKeyPressed(keyEvent -> {
System.out.println(keyEvent.getCode());
if(keyEvent.getCode().isArrowKey()) {
if (keyEvent.getCode() == KeyCode.UP) {
squares[i-1][j].requestFocus();
} else if (keyEvent.getCode() == KeyCode.DOWN) {
squares[i+1][j].requestFocus();
} else if (keyEvent.getCode() == KeyCode.LEFT) {
squares[i][j-1].requestFocus();
} else if (keyEvent.getCode() == KeyCode.RIGHT) {
squares[i][j+1].requestFocus();
}
}
});
}
}

How can I fill a combobox with the selection from another combobox? JavaFX

I've started creating a GUI for a March Madness bracket generator by displaying all 64 teams for round1 as Labels and now I'm trying to create a ComboBox dropdown menu for each match.
I've created a ComboBox for 2 matches and now I want to create a new ComboBox that pulls its options from the other two ComboBox's before it. So in the example diagram below, the new ComboBox should have the options Duke and VCU for the user to choose from.
(2 combo boxes) (new combo box)
Duke------
Duke ---
ND St. ---
X
VCU -----
VCU ---
UCF -----
How can I do so?
public class ControlPanel extends Application
{
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("March Madness 2019 Generator");
BorderPane componentLayout = new BorderPane();
componentLayout.setPadding(new Insets(20,0,20,20));
final FlowPane choicePane = new FlowPane();
choicePane.setHgap(100);
Label choiceLbl = new Label("Match1");
ArrayList<Team> round1 = new ArrayList<Team>();
round1.add(new Team("Duke", 0.670, 1)); //0
round1.add(new Team("North Dakota St", 0.495, 16));
round1.add(new Team("VCU", 0.609, 8));
round1.add(new Team("UCF", 0.606, 9));
//The choicebox is populated from an observableArrayList
ChoiceBox r2Match1 = new ChoiceBox(FXCollections.observableArrayList( match(round1, 0, 1) ));
//Add the label and choicebox to the flowpane
choicePane.getChildren().add(choiceLbl);
choicePane.getChildren().add(r2Match1);
//put the flowpane in the top area of the BorderPane
componentLayout.setTop(choicePane);
//Add the BorderPane to the Scene
Scene appScene = new Scene(componentLayout,500,500);
//Add the Scene to the Stage
primaryStage.setScene(appScene);
primaryStage.show();
}
private ArrayList<Team> match(ArrayList<Team> roundPullFrom, int team1, int team2) {
ArrayList<Team> temp = new ArrayList<Team>();
temp.add(roundPullFrom.get(team1));
temp.add(roundPullFrom.get(team2));
return temp;
}
}
Combine ComboBoxes pairwise using the approach posted in my previous answer until you're left with a single ComboBox.
The following code layouts the nodes in something that resembles a tree structure too, but you could easily decouple the layout by keeping every round in a data structure instead of overwriting the values of a single array. (Since you'll want to access the data, you should store the combos in a proper data structure anyways.)
private static ComboBox<String> createCombo(double x, double y, double width) {
ComboBox<String> comboBox = new ComboBox<>();
comboBox.setLayoutX(x);
comboBox.setLayoutY(y);
comboBox.setMaxWidth(Region.USE_PREF_SIZE);
comboBox.setMinWidth(Region.USE_PREF_SIZE);
comboBox.setPrefWidth(width);
return comboBox;
}
private static Label createLabel(String text, double maxWidth) {
Label label = new Label(text);
label.setMaxWidth(maxWidth);
return label;
}
#Override
public void start(Stage primaryStage) {
String[] teams = new String[64];
for (int i = 0; i < teams.length; i++) {
teams[i] = Integer.toString(i);
}
final double offsetY = 30;
final double offsetX = 100;
final double width = 90;
Pane root = new Pane();
// array storing the comboboxes
// combos for previous round are at the lowest indices
ComboBox<String>[] combos = new ComboBox[teams.length / 2];
// create initial team labels & comboboxes
for (int i = 0, offsetTeams = 0; i < combos.length; i++, offsetTeams += 2) {
Label label = createLabel(teams[offsetTeams], width);
double y = offsetTeams * offsetY;
label.setLayoutY(y);
root.getChildren().add(label);
label = createLabel(teams[offsetTeams+1], width);
label.setLayoutY(y+offsetY);
ComboBox<String> comboBox = createCombo(offsetX, y + offsetY / 2, width);
comboBox.getItems().addAll(teams[offsetTeams], teams[offsetTeams+1]);
combos[i] = comboBox;
root.getChildren().addAll(label, comboBox);
}
double x = 2 * offsetX;
int count = combos.length / 2; // combos still left for the next round
for (; count > 0; count /= 2, x += offsetX) { // for each round
// create comboboxes combining the combos from previous round pairwise
for (int i = 0, ci = 0; i < count; i++, ci+=2) {
// get combos pairwise
ComboBox<String> c1 = combos[ci];
ComboBox<String> c2 = combos[ci+1];
ComboBox<String> combo = createCombo(x, (c1.getLayoutY() + c2.getLayoutY()) / 2, width) ;
// combine data from previous round
ChangeListener<String> listener = (o, oldValue, newValue) -> {
final List<String> items = combo.getItems();
int index = items.indexOf(oldValue);
if (index >= 0) {
if (newValue == null) {
items.remove(index);
} else {
items.set(index, newValue);
}
} else if (newValue != null) {
items.add(newValue);
}
};
c1.valueProperty().addListener(listener);
c2.valueProperty().addListener(listener);
root.getChildren().add(combo);
combos[i] = combo;
}
}
primaryStage.setScene(new Scene(new ScrollPane(root), 600, 400));
primaryStage.show();
}
The structure of your problem is a tree. So you might want to have your solution support that structure. Either you use a Binary Tree data structure to resemble the tournament or you create such a structure by e.g. having classes like:
class Team {
String name;
}
class Match {
Team teamA;
Team teamB;
String where;
Date when;
public Team selectWinner() {
...
}
}
class Tournament {
List<Team> teams;
List<Match> getMatches(int round,List<Team> teams) {
List<Match> matches=new ArrayList<Match>)();
if (round==1) {
for (teamIndex=1;teamIndex<=teams.size();teamIndex+=2) {
Match match=new Match(teams[teamIndex-1],teams(teamIndex)];
matches.add(match);
}
} else {
List<Team> winners=new ArrayList<Team>();
for (Match match:getMatches(round-1)) {
winners.add(match.selectWinner());
}
return getMatches(1,winners);
}
}
}
From this structure you can then derive the necessary gui components to make the selection dynamic and let the GUI components take their values from the Tournament, Match and Team classes.

Dynamically sizing the contents of a GridPane to the properties of their parent

I am creating a "threes" clone for my assignment and one of the properties was to have a dynamically sizable GridPane depending on the number of columns and rows, how can I achieve this?
I've looked at adding ColumnConstraints as well as RowConstraints, given them a percentile width of the total GridPane size divided by the number of columns:
// width = total / number of columns
// height = total / number of rows
In Scene Builder the GridPane has by default a 4x4 size; these elements seem to resize just fine but when say a 4x5 size is loaded it seems to only want to show 4x4 (styled) and the other remaining tiles look "weird".
Here's what it looks like: https://imgur.com/gallery/Qv1oWZb
// width = total / number of columns
// height = total / number of rows
gridPane.setGridLinesVisible(false);
gridPane.setAlignment(Pos.TOP_LEFT);
gridPane.getChildren().clear();
RowConstraints rowConstraint = new RowConstraints();
rowConstraint.setPercentHeight(300 / game.getBoardSizeY());
ColumnConstraints colConstraint = new ColumnConstraints();
colConstraint.setPercentWidth(350 / game.getBoardSizeX());
for(int x= 0; x < game.getBoardSizeX(); x++) {
gridPane.getColumnConstraints().add(colConstraint);
for(int y = 0; y < game.getBoardSizeY(); y++) {
gridPane.getRowConstraints().add(rowConstraint);
Tile tile = game.getBoard().getPos(x, y);
Pane pane = new Pane();
String lblText = "";
String itemClass = "";
if(tile.getValue() != 0) {
lblText = String.valueOf(tile.getValue());
}
int tileVal = tile.getValue();
if(tileVal == 1) {
itemClass = "blueTile";
}else if (tileVal == 2) {
itemClass = "redTile";
}else if (tileVal >= 3 ) {
itemClass = "whiteTile";
}else {
itemClass = "defaultTile";
}
Label lblVal = new Label(lblText);
pane.getStyleClass().addAll("tile", itemClass);
lblVal.layoutXProperty().bind(pane.widthProperty().subtract(lblVal.widthProperty()).divide(2));
pane.getChildren().add(lblVal);
gridPane.add(pane, x, y);
}
}
I expect it to fill my entire GridPane accordingly but instead it acts up and shows me the result shown in the images.
Edit:
I've gotten it to work however when I have differentiating rows (like 4x5 for example) it doesn't quite work out the sizing yet.
gridPane.setGridLinesVisible(false);
gridPane.setAlignment(Pos.TOP_LEFT);
gridPane.getChildren().clear();
gridPane.getChildren().removeAll();
gridPane.getColumnConstraints().clear();
gridPane.getRowConstraints().clear();
RowConstraints rowConstraint = new RowConstraints();
rowConstraint.setPercentHeight(350 / game.getBoardSizeY());
ColumnConstraints colConstraint = new ColumnConstraints();
colConstraint.setPercentWidth(300 / game.getBoardSizeX());
for(int x= 0; x < game.getBoardSizeX(); x++) {
gridPane.getColumnConstraints().add(colConstraint);
gridPane.getRowConstraints().add(rowConstraint);
for(int y = 0; y < game.getBoardSizeY(); y++) {
Tile tile = game.getBoard().getPos(x, y);
Pane pane = new Pane();
String lblText = "";
String itemClass = "";
if(tile.getValue() != 0) {
lblText = String.valueOf(tile.getValue());
}
int tileVal = tile.getValue();
if(tileVal == 1) {
itemClass = "blueTile";
}else if (tileVal == 2) {
itemClass = "redTile";
}else if (tileVal >= 3 ) {
itemClass = "whiteTile";
}else {
itemClass = "defaultTile";
}
Label lblVal = new Label(lblText);
pane.getStyleClass().addAll("tile", itemClass);
lblVal.layoutXProperty().bind(pane.widthProperty().subtract(lblVal.widthProperty()).divide(2));
pane.getChildren().add(lblVal);
gridPane.add(pane, x, y);
}
}
If I understood correctly, you want the rows and columns to resize so that all columns have the same width and all rows have the same height. If that's correct, this sounds wrong:
I've looked at adding columnConstraints as well as rowConstraints, given them a percentile width of the total gridpane size devided by the number of columns
// width = total / number of columns
// height = total / number of rows
The percentile width/height is a relative measurement. You don't wan't to calculate that based on the current width/height. For instance, if you had 4 rows you'd have each row be 25% of the height.
That said, I don't believe you need to mess around with the width or height settings on the constraints. Just set the RowConstraints.vgrow and ColumnConstraints.hgrow properties to Priority.ALWAYS. Here's an example:
import javafx.application.Application;
import javafx.geometry.HPos;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.RowConstraints;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
GridPane root = new GridPane();
root.setAlignment(Pos.CENTER);
root.setGridLinesVisible(true);
for (int col = 0; col < 5; col++) {
root.getColumnConstraints()
.add(new ColumnConstraints(-1, -1, -1, Priority.ALWAYS, HPos.CENTER, false));
for (int row = 0; row < 3; row++) {
if (col == 0) {
root.getRowConstraints()
.add(new RowConstraints(-1, -1, -1, Priority.ALWAYS, VPos.CENTER, false));
}
root.add(new Label(String.format("(%d,%d)", row, col)), col, row);
}
}
primaryStage.setScene(new Scene(root, 500, 300));
primaryStage.show();
}
}

JavaFX cover and uncover Button text in a Button field

Okay, so when initializing the playboard for Minesweeper, my code iterates through all the buttons created in the pane and sets the text to either an X for bomb or a number (indicating how many bombs are neighbors). If it's neither it does nothing. But now I wonder how to hide that text when initializing the game so that it can be uncovered and later recovered by clicking the mouse?
Here's the iteration logic:
//iterate through rows and columns to fill board with random bombs
for (int y = 0; y < model.Y_FIELDS; y++) {
for (int x = 0; x < model.X_FIELDS; x++) {
Field field = new Field(x, y, Math.random() < 0.2, model);
model.array[x][y] = field;
root.getChildren().add(field);
}
}
for (int y = 0; y < model.Y_FIELDS; y++) {
for (int x = 0; x < model.X_FIELDS; x++) {
Field field = model.array[x][y];
if (field.isBomb())
continue;
long number = field.getSurrounding().stream().filter(f -> f.isBomb()).count();
if (number > 0)
field.board.setText(String.valueOf(number));
}
}
I would like them to be blank at first. Where do I put setText("")? In a left mouse click event I want to uncover them. That would look something like if(leftmouseclick) then set.Visible or something like that...
You can use for example the PseudoClass API to change the CSS pseudo state of your Buttons between "revealed" and "unrevealed".
You need to define a CSS pseudo class like:
.button:unrevealed { -fx-text-fill: transparent; }
which will represent the button when it was not pressed yet and makes the text of the Button invisible.
And you have to define the JavaFX PseudoClass like:
PseudoClass unrevealedPseudo = PseudoClass.getPseudoClass("unrevealed");
Then to use it:
Button button = new Button("X");
button.pseudoClassStateChanged(unrevealedPseudo, true);
button.setOnAction(e -> button.pseudoClassStateChanged(unrevealedPseudo, false));
In the snippet the Button is set to be "unrevealed" when it is created, then on press leaves that state, therefore the -fx-text-fill property will be changed back to the default one.
If you apply the same creation logic for all of your buttons, it does not matter what the initial text of them, as it is hidden until it is not revealed (by button press or by programatically changing it).
Note1: You can use the same API to define more pseudo-classes which will be handy if you for example want to set a "flag" on the button on right click, as you can simply use these CSS classes to define how the buttons should look like in the different states.
Note2: If you have a backend, that stores the state of each field (revealed, flagged, unrevealed) for example using a property, in the frontend while creating a separate Button for each element of the domain model, you can simply check for the update of the state property of the element in the model, and you can simply put the Button into the correct pseudo class. It is much more elegant then changing it on e.g. button click.
An example with the approach in Note2:
Model:
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
public class MineSweeperField {
public enum State {
UNREVEALED, REVEALED, FLAGGED
};
private ObjectProperty<State> state = new SimpleObjectProperty<State>(State.UNREVEALED);
public ObjectProperty<State> stateProperty() {
return state;
}
public State getState() {
return stateProperty().get();
}
public void setState(State state) {
stateProperty().set(state);
}
}
Button:
import application.MineSweeperField.State;
import javafx.css.PseudoClass;
import javafx.scene.control.Button;
public class MineSweepButton extends Button {
static PseudoClass unrevealedPseudo = PseudoClass.getPseudoClass("unrevealed");
static PseudoClass revealedPseudo = PseudoClass.getPseudoClass("revealed");
static PseudoClass flaggedPseudo = PseudoClass.getPseudoClass("flagged");
public MineSweepButton(MineSweeperField field) {
this.getStyleClass().add("minesweep-button");
this.pseudoClassStateChanged(unrevealedPseudo, true);
field.stateProperty().addListener((obs, oldVal, newVal) -> changePseudoClass(newVal));
changePseudoClass(field.getState());
}
private void changePseudoClass(State state) {
this.pseudoClassStateChanged(unrevealedPseudo, false);
this.pseudoClassStateChanged(revealedPseudo, false);
this.pseudoClassStateChanged(flaggedPseudo, false);
switch (state) {
case FLAGGED:
this.pseudoClassStateChanged(flaggedPseudo, true);
break;
case REVEALED:
this.pseudoClassStateChanged(revealedPseudo, true);
break;
case UNREVEALED:
this.pseudoClassStateChanged(unrevealedPseudo, true);
break;
}
}
}
CSS:
.minesweep-button:unrevealed { -fx-text-fill: transparent; }
.minesweep-button:revealed { -fx-text-fill: black; }
.minesweep-button:flagged { -fx-text-fill: orange; }
Usage:
BorderPane root = new BorderPane();
MineSweeperField field = new MineSweeperField();
MineSweepButton msButton = new MineSweepButton(field);
msButton.setText("5");
Button reveal = new Button("Reveal");
Button unreveal = new Button("Unreveal");
Button flag = new Button("Flag");
root.setTop(new VBox(msButton, new HBox(reveal, unreveal, flag)));
reveal.setOnAction(e -> field.setState(State.REVEALED));
unreveal.setOnAction(e -> field.setState(State.UNREVEALED));
flag.setOnAction(e -> field.setState(State.FLAGGED));
And the output:
Just don't set the text before the button is clicked. If you store the Buttons in a GridPane, the row and column indices are stored in the Buttons anyways. The mines could simply be stored in a boolean[][] array and looked up based on the indices. BTW: I recommend using ToggleButtons, since they already provide a selected and a unselected state, which could be used to represent nodes already uncovered.
private static boolean checkMine(boolean[][] mines, int row, int column) {
return row >= 0 && column >= 0 && row < mines.length && column < mines[row].length && mines[row][column];
}
#Override
public void start(Stage primaryStage) {
GridPane field = new GridPane();
boolean[][] mines = new boolean[][]{
new boolean[]{false, false, false},
new boolean[]{false, true, false},
new boolean[]{false, false, false}
};
EventHandler<ActionEvent> handler = event -> {
ToggleButton source = (ToggleButton) event.getSource();
// find column/row indices in GridPane
Integer row = GridPane.getRowIndex(source);
Integer column = GridPane.getColumnIndex(source);
int r = row == null ? 0 : row;
int c = column == null ? 0 : column;
boolean mine = mines[r][c];
if (mine) {
source.setText("X");
System.out.println("you loose");
// TODO: Represent lost state in GUI
} else {
int mineCount = 0;
// count surrounding mines
for (int i = -1; i < 2; i++) {
for (int j = -1; j < 2; j++) {
if (checkMine(mines, r + i, c + j)) {
mineCount++;
}
}
}
if (mineCount > 0) {
source.setText(Integer.toString(mineCount));
}
}
source.setDisable(true);
// keep activated look
source.setOpacity(1);
};
for (int i = 0; i < mines.length; i++) {
boolean[] row = mines[i];
for (int j = 0; j < row.length; j++) {
ToggleButton toggleButton = new ToggleButton();
toggleButton.setPrefSize(30, 30);
toggleButton.setOnAction(handler);
field.add(toggleButton, j, i);
}
}
Scene scene = new Scene(field);
primaryStage.setScene(scene);
primaryStage.show();
}

JavaFX - Making GridPane take up all available space in BorderPane

I'm using the following code to create a simple grid of buttons for a calculator:
BorderPane rootNode = new BorderPane();
GridPane gridPane = new GridPane();
rootNode.setCenter(gridPane);
int counter = 1;
for(int row = 3; row > 0; row--)
for(int col = 0; col < 3; col++) {
Button button = new Button("" + counter++);
button.setOnAction(this);
gridPane.add(button, col, row);
}
gridPane.add(new Button("R"), 0, 4);
gridPane.add(new Button("0"), 1, 4);
(Wanted to post an image of how it looks here, but I don't have enough reputation points to be allowed to do so)
The GridPane ends up being tiny and crammed up in the upper left corner, but I want it to take up all available space in the BorderPane's center region (that would be the entire window in this case). I've tried messing around with a bunch of things like various setSize methods, but havn't had much luck.
Your GridPane is already taking all the space available to it i.e. the entire Borderpane. Its the GridCells which are not using the space available to the GridPane.
You can make use of the Hgrow and Vgrow options available in the GridPane to make the cells take up the entire space available.
Here is an example which uses setHgrow to use the entire width available to the GridPane. You may add the same for the Height.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage stage) {
BorderPane rootNode = new BorderPane();
rootNode.setMinSize(300, 300);
rootNode.setStyle("-fx-background-color: RED;");
GridPane gridPane = new GridPane();
gridPane.setStyle("-fx-background-color: BLUE;");
rootNode.setCenter(gridPane);
int counter = 1;
for (int row = 3; row > 0; row--)
for (int col = 0; col < 3; col++) {
Button button = new Button("" + counter++);
gridPane.add(button, col, row);
GridPane.setHgrow(button, Priority.ALWAYS);
}
Button r = new Button("R");
gridPane.add(r, 0, 4);
GridPane.setHgrow(r, Priority.ALWAYS);
Button r0 = new Button("0");
gridPane.add(r0, 1, 4);
GridPane.setHgrow(r0, Priority.ALWAYS);
Scene scene = new Scene(rootNode);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Sorry, I missunderstood the question.
The Buttons are just too small, so not taking all the space they have in the borderpane? The width of a button depends on the size of the text it has. You just write a number in it so they are pretty small then. Use button.setPrefWidth(Integer.MAX_VALUE) and they use all the width they can get!

Categories

Resources