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();
}
Related
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();
}
}
});
}
}
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.
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).
I created two-dimensional array of Button actor, then I added new ClickListener() {
touchDragged } as the following code:
buttons = new Button[3][3];
for (int row = 0; row < buttons.length; row++) {
for (int col = 0; col < buttons[0].length; col++) {
buttons[row][col] = new Button(drawable);
buttons[row][col].addListener(new ClickListener() {
#Override
public void touchDragged(InputEvent event, float x, float y, int pointer) {
for (int row = 0; row < buttons.length; row++) {
for (int col = 0; col < buttons[0].length; col++) {
if (buttons[row][col].isOver()) {
buttons[row][col].setColor(Color.GREEN);
}
}
}
}
}
}
the code inside touchDragged method if the buttons isOver the buttons colored GREEN (it works fine) as shown in image
Now, How can I remove Color.GREEN i.e. (Color.WHITE) from buttons in the same calling touchDragged method, I mean undo GREEN to WHITE if the isOver() still true??
this image clear my question :
like Alphabetty Game from king company, if you know it :).
Sorry, For bad English
You could use an if statement to check which colour the square is. If it is white, colour it green and vice versa.
I have something similar and for some reason you can't directly compare colours in an if statement, however changing them to an rgb int value solves this. You can choose from the various rgb options such as rgba8888 or argb8888 etc, choose one which will suit your needs. The simplest one would just be rgb888. It is a static method in the colour class, pass it a colour it will return an int.
if(Color.rgb888(button[row][col].getColor()) == Color.rgb888(Color.Green()))
{
button[row][col].setColor(Color.White());
}
1- Create List :
private List<Integer> list;
// In create OR show Method
list = new ArrayList<Integer>();
2- Create this line code (as ID for buttons[row][col]):
buttons[row][col].setName("" + id++);
3- Write this code inside your touchDragged .. after your loop:
if (buttons[row][col].isOver()) {
try {
if (list.contains(Integer.parseInt(buttons[row][col].getName()))) {
if(Integer.parseInt(buttons[row][col].getName()) == list.get(list.size() - 2)) {
stage.getRoot().findActor("" + list.get(list.size() - 1)).setColor(Color.WHITE);
list.remove(list.size() - 1);
}
} else {
buttons[row][col].setColor(Color.GREEN);
list.add(Integer.parseInt(buttons[row][col].getName()));
}
} catch (Exception e) {
System.out.println(e.getClass());
}
}
4- See the answer in this video
I am looking for a way to add plus and minus buttons to table cells in LibGDX.
I'm trying to do something like this image:
I noticed that the uiskin files I'm using contain plus and minus buttons (right next to the checkbox) which will do just fine (uiskin.png).
Any tips on how I can add those to my table and make it so they increase/decrease the integer value in that table cell?
I've added a rudimentary sample code with an indication of what I want to do:
#Override
public void create() {
Stage stage = new Stage();
Skin skin = new Skin(Gdx.files.internal("skins/uiskin.json"));
Table table = new Table(skin);
table.setPosition(Gdx.graphics.getWidth() / 2, Gdx.graphics.getHeight() / 2);
for(int i = 0; i < 10; i++){
table.add(Integer.toString(i)); //Somehow add plus/minus buttons left
//and right that increase/decrease the integer value
table.row();
}
stage.addActor(table);
Gdx.input.setInputProcessor(stage);
}
#Override
public void render() {
Gdx.gl.glClearColor(0, 0, 0, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
stage.act(Gdx.graphics.getDeltaTime());
stage.draw();
}
This is just some example code, but if it works here I will certainly be able to figure it out.
Thanks
First, you need an array to store your number values. It's not good practice to use your View (table cells) to store your Model (number values), and besides, Label cells store a String, not an integer, so it would be inconvenient.
private int[] tableData = new int[10];
Now you need a button that can be associated with a row in your data. Here's one way Create a subclass of ImageButton that can take a row number and mode (subtract or add).
public class IncrementButton extends ImageButton {
public final int rowNumber;
public final boolean decrement;
public IncrementButton (Skin skin, String styleName, int rowNumber, boolean decrement){
super(skin, styleName);
this.rowNumber = rowNumber;
this.decrement = decrement;
}
}
The plus and minus images in the uiskin atlas you're using are called "tree-minus" and "tree-plus". You need an ImageButton style for each of these that uses one of these as the imageUp property. (imageUp is used as a default if you don't define images for other states such as imageDown.) You can add these to uiskin.json:
com.badlogic.gdx.scenes.scene2d.ui.ImageButton$ImageButtonStyle: {
plus: { down: default-round-down, up: default-round, imageUp: tree-plus },
minus: { down: default-round-down, up: default-round, imageUp: tree-minus }
},
Now you can create a ChangeListener for these buttons that will modify the numbers in the data and update the table accordingly. And then set it all up:
Stage stage = new Stage();
Skin skin = new Skin(Gdx.files.internal("skins/uiskin.json"));
final Table table = new Table(skin);
final Label[] labels = new Label[tableData.length]; //keep references to the labels for updating them.
final ChangeListener incrementListner = new ChangeListener() {
#Override
public void changed(ChangeEvent event, Actor actor) {
IncrementButton incrementButton = (IncrementButton)actor;
int row = incrementButton.rowNumber;
tableData[row] += incrementButton.decrement ? -1 : 1;
labels[row].setText(Integer.toString(tableData[row]));
}
};
table.setPosition(Gdx.graphics.getWidth() / 2, Gdx.graphics.getHeight() / 2);
for(int i = 0; i < tableData.length; i++){
IncrementButton decrementButton = new IncrementButton(skin, "minus", i, true);
decrementButton.addListener(incrementListner);
IncrementButton incrementButton = new IncrementButton(skin, "plus", i, false);
incrementButton.addListener(incrementListner);
table.add(decrementButton);
labels[i] = table.add(Integer.toString(i)).getActor();//Add number label and keep reference to it in labels array for the change listener to look up
table.add(incrementButton);
table.row();
}
stage.addActor(table);
Gdx.input.setInputProcessor(stage);
If you want to set min and max values for the data, then you need some arrays for the plus buttons and minus buttons (similar to the labels array) so the change listener can look up and disable/enable the buttons accordingly when a limit is hit.