javafx make a grid of buttons - java

I want to make a grid with a specific amount of buttons.
I know how many buttons there are need to be because I get the number of rows and columns.
I could do a loop, but I don't know how you can place buttons next to eachother and underneath.
Secondly, the buttons need a Text and an Id, text is no problem, but how do you give them an id?
And at last, and probably most difficult, it can occur that there are a lot of rows, so that a scrollbar should be available.
At the end it should look something like this:

#Override
public void start(Stage stage) {
GridPane grid = new GridPane();
grid.setPadding(new Insets(BUTTON_PADDING));
grid.setHgap(BUTTON_PADDING);
grid.setVgap(BUTTON_PADDING);
for (int r = 0; r < NUM_BUTTON_LINES; r++) {
for (int c = 0; c < BUTTONS_PER_LINE; c++) {
int number = NUM_BUTTON_LINES * r + c;
Button button = new Button(String.valueOf(number));
grid.add(button, c, r);
}
}
ScrollPane scrollPane = new ScrollPane(grid);
stage.setScene(new Scene(scrollPane));
stage.show();
}

The best solution would be:
itemNumber starts from 0 to N:
Grid.getChildren().get(itemNumber).setId("bt"+itemNumber);
Grid.getChildren().get(itemNumber).getId();

Related

Is specifying JavaFX CSS borders on GridPane contents supposed to collapse the sizing?

I was attempting to layout a JavaFX stage using a GridPane when I ran into the following problem. If I setup the grid with the appropriate constraints and add newly instantiated StackPanes to it, the default sizing of the scene, stage, and it's contents ensures that the contents are visible:
However, if I add a JavaFX CSS style specifying a border to the newly instantiated StackPane before adding it to the GridPane, then the default sizing of things seems to collapse complete:
My code is as follows:
public static void main(final String[] args) {
Platform.startup(() -> {});
Platform.runLater(() -> {
final GridPane gridPane = new GridPane();
final Scene scene = new Scene(gridPane);
final Stage stage = new Stage();
stage.setScene(scene);
final List<StackPane> panes = new ArrayList<>();
for (int i = 0; i < 4; i++) {
// Create a new pane with a random background color for
// illustration
final StackPane p = createNewPane();
panes.add(p);
// The addition / removal of the following line affects the
// layout.
p.setStyle("-fx-border-width:2px;-fx-border-color:red");
}
for (int r = 0; r < 2; r++) {
final RowConstraints rc = new RowConstraints();
rc.setPercentHeight(50);
gridPane.getRowConstraints().add(rc);
}
for (int c = 0; c < 2; c++) {
final ColumnConstraints cc = new ColumnConstraints();
cc.setPercentWidth(50);
gridPane.getColumnConstraints().add(cc);
}
for (int r = 0, i = 0; r < 2; r++) {
for (int c = 0; c < 2; c++) {
gridPane.add(panes.get(i++), c, r);
}
}
stage.show();
});
}
Curiously, if I move the stage.show() to right after I set the Scene, then everything works fine even with the CSS.
Can anyone help me understand, one, whether this is the expected behavior, and two, why the execution order of the stage.show() makes a difference?
Thanks!
What the issue is
Your example is a bit ambiguous. You don't set the preferred size of anything added to the Stage at any time. So, the JavaFX platform can really do whatever it wants in terms of sizing things. Setting a preferred percent size is not the same as setting a preferred absolute size. A percent size is relative, so the question becomes, relative to what? and the answer to that is unclear.
As to why this occurs:
// The addition / removal of the following line affects the
// layout.
p.setStyle("-fx-border-width:2px;-fx-border-color:red");
I couldn't say. My guess is that the use of CSS is triggering some additional layout logic which effects the resizing in the absence of any size hints.
How to fix it
Anyway, the solution is just to make things more clear and specify preferred sizing for at least something in the application, then the application will initially be sized to that preferred sizing.
Here is an example:
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class Starter {
public static void main(String[] args) {
Platform.startup(() -> {});
Platform.runLater(() -> {
final GridPane gridPane = new GridPane();
final Scene scene = new Scene(gridPane);
final Stage stage = new Stage();
stage.setScene(scene);
final List<StackPane> panes = new ArrayList<>();
for (int i = 0; i < 4; i++) {
// Create a new pane with a random background color for
// illustration
final StackPane p = createNewPane();
panes.add(p);
// The addition / removal of the following line affects the
// layout.
p.setStyle("-fx-border-width:2px;-fx-border-color:red");
}
for (int r = 0; r < 2; r++) {
final RowConstraints rc = new RowConstraints();
rc.setPercentHeight(50);
gridPane.getRowConstraints().add(rc);
}
for (int c = 0; c < 2; c++) {
final ColumnConstraints cc = new ColumnConstraints();
cc.setPercentWidth(50);
gridPane.getColumnConstraints().add(cc);
}
for (int r = 0, i = 0; r < 2; r++) {
for (int c = 0; c < 2; c++) {
gridPane.add(panes.get(i++), c, r);
}
}
stage.show();
});
}
private static final Random random = new Random(42);
private static StackPane createNewPane() {
StackPane pane = new StackPane();
pane.setBackground(
new Background(
new BackgroundFill(
randomColor(), null, null
)
)
);
pane.setPrefSize(150, 100);
return pane;
}
private static Color randomColor() {
return Color.rgb(
random.nextInt(256),
random.nextInt(256),
random.nextInt(256)
);
}
}
The key part of the solution is the call:
pane.setPrefSize(150, 100);
which sets the preferred size for the stack panes which have been placed in your layout.
Alternatively, rather than doing the bottom up preferred sizing by setting a preferred size on each of the StackPanes, you could also accomplish a similar thing from a top-down perspective by setting appropriate constraints on the GridPane instead, for example:
gridPane.setPrefSize(300, 200);
Note
I'd advise using a JavaFX Application class instead of Platform.startup() call unless there is a really good reason to use the latter (which there is in this case - interfacing with Swing, as you have noted in your comment).

Problems with calling a Method (Minesweeper)

I am facing right now a problem where I think that my main is executing a method over and over again, instead of one time. Its better if I explain it according to an example. I already were able to program a Minesweeper game. But i wrote it all in one class MAIN. This time I am trying to do it again but using methods and classes, for the sake of practice and better overview.
As you can see, in my Class Calculations, I am trying to create an Array of Labels. In my Main I am trying to add all the Labels from the Array inside the GridPane. Since it is a minesweeper game, i have to add also random bombs, which will be "X" in my example. I did this little test if it works lbs[10].setText("x"), just to see if it works. It doesnt. It will set the text of ALL labels to X once this method is called! I also want to set an onMouseClicked Event in this class. I would appreciate any help and thank you for your time to read this. I surrounded the codes with Hashtag -> ######
//Main
package application;
import...
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
try {
GridPane grid = new GridPane();
Scene scene = new Scene(grid, (20 * 20), (20 * 20));
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
for(int i = 0; i < 20; i++) {
ColumnConstraints column = new ColumnConstraints(20);
grid.getColumnConstraints().add(column);
}
for(int i = 0; i < 20; i++) {
RowConstraints row = new RowConstraints(20);
grid.getRowConstraints().add(row);
}
//#########################################################
Calculations c = new Calculations();
int count = 0;
for (int x = 0; x < c.test().length/20; x++)
{
for (int y = 0; y < c.test().length/20; y++)
{
grid.add(c.test()[count], x, y);
count++;
}
}
//#########################################################
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
And here my class "Calculations"
package application;
import...
public class Calculations {
public Label[] test() {
Label label = new Label();
Label lbs[] = new Label[20*20];
int a = 0;
for (int i = 0 ; i < 400; i++) {
lbs[i] = label;
}
lbs[10].setText("x"); //##### <- doesnt work the way it should be
return lbs;
}
}
This is because all the elements in the array lbs point to the same Label label.
So, when you set the text of any one to "x", it changes the text of label, which is, actually, every label.
Change this line, in the loop:
lbs[i] = label;
to:
lbs[i] = new Label();

Blank fields in Libgdx's table

I have another problem with tables in Libgdx. I want to create a game board using it and give to user a possibility to add / remove elements from that board ( table ). So first of all I would like to know how to initialize that table so that it already has fields but invisible fields ( so that it's possible to get position of that field and put there new element ). So those are my 2 first questions :
How to create those invisible fields ( all fields have fixed static size ) ?
How to get position and change element ( actor ) in that field?
Hope someone can help me.
You can create those by adding a cell to a table and set the size of the cell.
You can then add a invisible button to that cell and if the button is clicked you can remove it and add a new element.
Here some example code:
// uiskin from libgdx tests
skin = new Skin(Gdx.files.internal("uiskin.json"));
table = new Table();
// invisible button style
final ButtonStyle bStyle = new ButtonStyle();
int colNum = 10, rowNum = 10;
for (int row = 0; row < rowNum; row++)
{
for (int col = 0; col < colNum; col++)
{
final Button l = new Button(bStyle);
l.addListener(new ClickListener()
{
#Override
public void clicked(InputEvent event, float x, float y)
{
Cell<Button> cell = table.getCell(l);
cell.clearActor();
cell.setActor(new Label("test", skin));
}
});
table.add(l).size(100, 100);
}
table.row();
}
stage = new Stage();
table.setFillParent(true);
stage.addActor(table);
Gdx.input.setInputProcessor(stage);
The skin files can be downloaded here: https://github.com/libgdx/libgdx/tree/master/tests/gdx-tests-android/assets/data
You need the uiskin.json, uiskin.atlas and uiskin.png file.

How would I add an action listener to buttons on a loop?

I want it so every time a button is pressed in my 4x4 grid, that it increments moves by 1 . This is creating a 4x4 layout of buttons. Each time any of those buttons are pressed, I want moves to increment. Basically I'm creating the memory game, where you flip cards over to match each other. I just have to keep count of the total amount of moves a player does to solve the puzzle.
private int moves = 0;
private GridPane makeGridPane(){
ConcentrationModel c = new ConcentrationModel();
GridPane grid = new GridPane();
ColumnConstraints col1 = new ColumnConstraints();
col1.setPercentWidth( 50 );
grid.getColumnConstraints().addAll(col1, col1, col1, col1);
RowConstraints row1 = new RowConstraints();
row1.setPercentHeight( 50 );
grid.getRowConstraints().addAll(row1, row1, row1, row1);
for(int row = 0; row < 4; row ++){
for(int col = 0; col < 4; col++){
Button btn = new Button();
ImageView image = new ImageView(c.getCards().get(0).getImage());
image.setFitWidth(WIDTH/4);
image.setFitHeight(HEIGHT/4);
btn.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
btn.setGraphic(image);
grid.add(btn, col, row);
}
}
return grid;
}
You can create a single event handler and reuse it for all the buttons. Since you probably want the buttons to do other things too, I would recommend adding this as an event handler, instead of using the convenience method setOnAction(...):
EventHandler<ActionEvent> incrementMovesHandler = e -> moves++ ;
for(int row = 0; row < 4; row ++){
for(int col = 0; col < 4; col++){
Button btn = new Button();
btn.addEventHandler(ActionEvent.ACTION, incrementMovesHandler);
// ...
}
}
This is actually pretty simple. All you have to do is add this bit of code just before your grid.add(...):
btn.setOnAction(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent event) {
moves++;
}
});
Or, equivalently, the Java 8 version:
btn.setOnAction((ActionEvent e) -> {moves++;});
(I can't currently test this, but it should work. If not, lemme know and I'll try to fix it.)
If you're concerned about memory, Christian points out below that this creates a new instance of EventHandler for every button. While this may not be too terrible, it's probably a bad habit to get into. The best way to handle (no pun intended) this is by making an object before your for loop:
EventHandler<ActionEvent> eh = new EventHandler<>() {
#Override public void handle(ActionEvent event) {
moves++;
}
};
Then, for each of your buttons, instead of the top-most code, you'd simply write:
btn.setOnAction(eh);
That way, a single EventHandler is being created and used to handle all the events. You'll want to use this one if you need to create more than just a few buttons, both because it's faster (doesn't need to allocate memory for each object) and more memory-efficient (...it, uh, doesn't need to allocate the memory for each object). In this case, I think it's pretty trivial, but it's good to know nonetheless.
You need to implement the ActionListener interface in your class.
Then in the actionPerformed function, just increment the moves variable.
You just have to call btn.addActionListener(this); for each of the button that
you created.
public class Sample extends JFrame implements ActionListener {
public Sample ()
{
}
private GridPane makeGridPane()
{
ConcentrationModel c = new ConcentrationModel();
GridPane grid = new GridPane();
ColumnConstraints col1 = new ColumnConstraints();
col1.setPercentWidth( 50 );
grid.getColumnConstraints().addAll(col1, col1, col1, col1);
RowConstraints row1 = new RowConstraints();
row1.setPercentHeight( 50 );
grid.getRowConstraints().addAll(row1, row1, row1, row1);
for(int row = 0; row < 4; row ++){
for(int col = 0; col < 4; col++){
Button btn = new Button();
ImageView image = new ImageView(c.getCards().get(0).getImage());
image.setFitWidth(WIDTH/4);
image.setFitHeight(HEIGHT/4);
btn.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
btn.setGraphic(image);
btn.addActionListener(this);
grid.add(btn, col, row);
}
}
return grid;
}
public void actionPerformed(ActionEvent e)
{
moves++;
}
}

JavaFX clock number transitions

I am attempting to create a clock in JavaFX, everything worked out well, except for the numbers which represent the time (from 1 to 12).
I have this piece of code:
Group numbers = new Group();
for(int i = 0; i < 12; i++){
//create a label.
Label label = new Label(String.valueOf(i));
//center it
label.setTranslateX(100);
label.setTranslateY(100);
label.getTransforms().add(new Rotate(i * (360 / 12)));
//rotate it.
numbers.getChildren().add(label);
}
This doesn't work, the numbers are just rotated in the center, but i want them to move outside (to the edge of the outer circle, like a normal clock.
Can someone help me?
Thank you very much.
If you combine Translate with Rotate transforms:
Group numbers = new Group();
for(int i = 0; i < 12; i++){
//create a label.
Label label = new Label(String.valueOf(i==0?12:i));
label.getTransforms().add(new Rotate(i * (360d / 12d)));
label.getTransforms().add(new Translate(100d,100d));
numbers.getChildren().add(label);
}
You will have a 'rotated' clock:
But as you can see, you are rotating your labels, not their position.
One way to approach this is finding the position of each label by using a small Circle, rotating it to its final position and then moving the label:
Group numbers = new Group();
for(int i = 0; i < 12; i++){
//create a label.
Label label = new Label(String.valueOf(i==0?12:i));
Circle c=new Circle(1);
c.getTransforms().add(new Rotate(i * (360d / 12d)));
c.getTransforms().add(new Translate(0,-100));
label.setTranslateX(c.localToParent(0,0).getX());
label.setTranslateY(c.localToParent(0,0).getY());
numbers.getChildren().addAll(c,label);
}
Note I've adjusted the translation to start right at the 12 hour position.
You will notice the labels are located down and right from their circle, so you should move them accordingly.
EDIT
To avoid the need of fixing the labels position, you can use a StackPane to wrap each pair of circles and labels:
for(int i = 0; i < 12; i++){
Label label = new Label(String.valueOf(i==0?12:i));
Circle c=new Circle(1);
c.getTransforms().add(new Rotate((i) * (360d / 12d)));
c.getTransforms().add(new Translate(0,-100d));
label.setTranslateX(c.localToParent(0,0).getX());
label.setTranslateY(c.localToParent(0,0).getY());
StackPane sp = new StackPane(c,label);
numbers.getChildren().add(sp);
}
Finally, have a look at this question, maybe it's easier just using a circular pane.

Categories