Replace a node at (row,col) in a JavaFX GridPane - java

I am making a grid-style game/simulation based on bugs "sensing" and eating food. I am using a gridPane (called worldGrid) of labels to show the grid of bugs and food. This is obviously going to be constantly updated when a bug moves cells towards food etc.
I currently have a function updateGrid(int col, int row, String cellContent) which I want to replace the label at [row,col] with a label that has the new text in cellContent.
I have the follow which works
worldGrid.add(new Label(cellContent), row,col);
however im worried that that is just adding a label on top of the current label and obviously over 100 iterations of the simulation thats not ideal.
I have tried this before adding the label:
worldGrid.getChildren().remove(row,col);
However I then get an IllegalArgumentException when trying to do the add line.
Any ideas on how to do this? Or even better, any ideas on how best to show a constantly changing grid that will eventually use sprites instead of text?

The col/row provided by grid.add(node, col, row) (ATTENTION first comes col!) is only a layout constraint. This does not provide any means to access columns or rows like slots in a 2-dimensional array. So to replace a node, you have to know its object itself, e.g. remember them in a separate array.
Then you are able to call getChildren().remove(object)... e.g.:
GridPane grid = new GridPane();
Label first = new Label("first");
Label second = new Label("second");
grid.add(first, 1, 1);
grid.add(second, 2, 2);
second.setOnMouseClicked(e -> {
grid.getChildren().remove(second);
grid.add(new Label("last"), 2, 2);
});
box.getChildren().addAll(grid);

I agree with Jens-Peter but I would add that you can use GridPane's getColumnIndex and getRowIndex to obtain a particular node's location.
For example ( this is in a component which extends GridPane ):
// set the appropriate label
for (Node node : getChildren()) {
if (node instanceof Label
&& getColumnIndex(node) == column
&& getRowIndex(node) == row) {
((Label)node).setTooltip(new Tooltip(tooltip));
}
}
in other words, go through all the nodes in the GridPane and check for a match of the conditions you want, in this case row and column.

You called getChildren().remove() method directly will cause the gridpane to go out of sync with the constraints. When you add, it also setup the constraint for the node. Add clearConstraints() method.

Related

Is there any other way to implement setOnMouseClicked on JavaFX

I had to create a matrix in javaFX. I created it without any problem with GridPane. The next thing is to create like "buttons" on the right side of the matrix, these buttons will move +1 element of the matrix to the right. Like this:
110 <-
101 <- //ie: I clicked this button
100 <-
The result:
110 <-
110 <-
100 <-
The way I handle this bit-shifting-moving was with circular linked list. I don't have any problem with that I think you can ommit that part. I use this method:
private void moveRowRight(int index){
//recives a index row and moves +1 the elements of that row in the matrix.
}
cells is the matrix
The problem is that, first the matrix can be modified by the user input i.e. 5x5 6x6 7x7, so the number of buttons will also change. I tried using a BorderPane(center: gridpane(matrix), right: VBox()) and this is the part of the code how I added some HBox inside the Vbox (right part of the border pane) and using the setOnMouseClicked.
private void loadButtonsRight(){
for(int i = 0; i < cells[0].length ; i++){
HBox newBox = new HBox();
newBox.getChildren().add(new Text("MOVE"));
newBox.prefHeight(50);
newBox.prefWidth(50);
newBox.setOnMouseClicked(e -> {
moveRowRight(i);
});
VBRightButtons.getChildren().add(newBox); //where I add the HBox to the VBox (right part of the Border Pane)
}
}
}
But then there's this problem.
Local variables referenced from lambda expression must be final or effectively final
It seems that I cannot implement lambda with a value that will change. Is there any way to help me to put "buttons" that depends of the matrix size and that uses the method I've created?
The message tells you all you need to know to fix the problem:
Local variables referenced from lambda expression must be final or effectively final
Assign your changing variable to a final constant and use the constant value in the lambda instead of the variable:
final int idx = i;
newBox.setOnMouseClicked(e ->
moveRowRight(idx);
);
If you wish to understand this more, see the baeldung tutorial
https://www.baeldung.com/java-lambda-effectively-final-local-variables

Java - textField for loop not completing

I've been trying to make a 9x9 grid of textFields, where each textField is allocated to an element in a 2d array. For example the top left textField is field[0][0], the one right of that is field[1][0], and the bottom right textField is field[8][8].
So far I have
TextField[][] fields = new TextField[9][9]; {
for (Y=0;Y<9;Y++) {
XPosition=0;
for (X=0;X<9;X++) {
fields[X][Y] = new TextField(1);
fields[X][Y].setColumns(1);
fields[X][Y].setBounds(XPosition, YPosition ,32, 32);
frame.getContentPane().add(fields[X][Y]);
XPosition=XPosition+32;
}
YPosition = YPosition+32;
}
}
For some reason when I run the program, only the first 5 textFields in the top row get created. I have a feeling all the other textFields are placed under the visible ones. The frame is definitely big enough, and I can't figure out any issues with the code.
Use a grid layout : https://docs.oracle.com/javase/tutorial/uiswing/layout/grid.html
Absolute positioning is not reliable.
Adding
frame.getContentPane().setLayout(null);
has fixed the error

ListView with custom CellFactory trims invisible nodes

My layout issue
I have a little issue with ListView and I'm not sure if it's because of some knowledge I missing or if my approach is flawed. Have to admit I'm not yet clear with how JavaFX handle the layout in the many possible cases.
The above screenshot shows the result I get twice with the exact same code, except that on the second one an invisible shape I use for coherent layout is made visible for debug.
The various classes involved by the CellFactory extend Group, I tried with some other Parent without much success so far.
How to reproduce
Rather than sharing my StarShape, StarRow and some other misc classes (I'd be happy to if requested) I wrote a sample reproducing the issue. The class extends Application and overrides the start(...) method as such:
#Override
public void start(Stage primaryStage) throws Exception {
final StackPane root = new StackPane();
final Scene scene = new Scene(root, 400, 600);
final ListView<Boolean> listView = new ListView<>();
listView.setCellFactory(this::cellFactory);
for (int i = 0; i < 5 ; i++) {
listView.getItems().add(true);
listView.getItems().add(false);
}
root.getChildren().add(listView);
primaryStage.setScene(scene);
primaryStage.setTitle("ListView trims the invisible");
primaryStage.show();
}
where this::cellFactory is
private ListCell<Boolean> cellFactory(ListView<Boolean> listView) {
return new ListCell<Boolean>() {
#Override
protected void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
final Rectangle tabShape = new Rectangle();
tabShape.setHeight(20);
tabShape.setWidth(40);
tabShape.setVisible(item);
final Label label = new Label(item.toString());
label.setLayoutX(40);
final Group cellRoot = new Group();
cellRoot.getChildren().add(tabShape);
cellRoot.getChildren().add(label);
setGraphic(cellRoot);
}
}
};
}
The above will display a ListView<Boolean> with black shapes in front of true items (because of the tabShape.setVisible(item); bit). The false items are looking like regular Label objects as if the invisible shape in their Group wasn't there (but it is).
Closing comments
Debugging this, it turns out groups with the invisible shapes are given negative layoutX property values. Thus Label controls aren't aligned as I'd like them to be. It doesn't happen when I call setLayoutX and setLayoutY outside of a ListView (the invisible shapes do force offsets), but it's probably not the only place where it would happen.
What's happening and how to avoid it? Alternatively, as I'm guessing I'm approaching this wrong, what'd be the right way? In other words, what is the question I should be asking instead of this?
Taking from #dlatikay's comment, instead of setting the placeholder items to invisible, you can render them transparent by setting their opacity to 0.0.
Applied to the MCVE from your question, this would be done by replacing:
tabShape.setVisible(item);
with:
tabShape.setOpacity(item ? 1.0 : 0.0);
In terms of user experience, you could take this one step further. Instead of setting the "inactive" stars to fully transparent, you could set them to be near-transparent, as in this mockup (with opacity set to 0.1):
The benefits that I see are:
It indicates not only the rating of an item in the list, but also the maximum rating.
It avoids awkward empty spaces for list items with zero stars.
I'm guessing I'm approaching this wrong
No, you're not. As with all layouts, there's often multiple ways to approach the same problem. Your approach is actually correct, and you're very close to a working solution.
You can achieve what you're after with a mere 1 line change. That is, changing the Group to an HBox.
An HBox ensures that elements are ordered horizontally, one after another. They also allow invisible elements to still take up space.
I also commented out one line: label.setLayoutX(40). I did this because HBox will not respect this setting, and actually you don't need it to. It will automatically shift the elements horizontally by as much is required.
#Override
protected void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
}
else {
final Rectangle tabShape = new Rectangle();
tabShape.setHeight(20);
tabShape.setWidth(40);
tabShape.setVisible(item);
final Label label = new Label(item.toString());
//label.setLayoutX(40);
final HBox cellRoot = new HBox();
cellRoot.getChildren().add(tabShape);
cellRoot.getChildren().add(label);
setGraphic(cellRoot);
}
}
When I make those changes, your layout will render like so:
Important: Your example and your screenshots are slightly different. You may want to use a VBox for your star example (V for 'vertical', H for 'horizontal').

Javafx: GridPane: Prevent column to grow width when text is too long

I have some legacy code to fix and I'm struggling on the following:
I have 2 nested gridpanes. Inside the inner grid, text has to be added.
The column widths of both inner and outer grids are calculated relative to the screen size using following function:
private GridPane createGridPane( int []colsPercent, int []rowsPercent ) {
GridPane gridPane = new GridPane()
// setup columns
ColumnConstraints []colConst = new ColumnConstraints[colsPercent.length];
for( int i = 0 ; i < colsPercent.length ; i++ ) {
colConst[i] = new ColumnConstraints();
colConst[i].setPercentWidth(colsPercent[i]);
colConst[i].setFillWidth(true);
}
gridPane.getColumnConstraints().addAll(colConst);
// setup rows
RowConstraints []rowConst = new RowConstraints[rowsPercent.length];
for( int i = 0 ; i < rowsPercent.length ; i++ ) {
rowConst[i] = new RowConstraints();
rowConst[i].setPercentHeight(rowsPercent[i]);
rowConst[i].setFillHeight(true);
}
gridPane.getRowConstraints().addAll(rowConst);
return gridPane;
}
The grids and their contents are added by following code:
gridPane = createGridPane(colConstFeedbackScreenPercent,
rowConstFeedbackScreenPercent);
gridPane.setGridLinesVisible(true);
GridPane.setHgrow(gridPane, Priority.NEVER);
GridPane innerGridPane = createGridPane(colConstFeedbackInnerPercent,
rowConstFeedbackInnerPercent);
GridPane.setHgrow(innerGridPane, Priority.NEVER);
innerGridPane.setGridLinesVisible(true);
gridPane.add(innerGridPane, 1, 1);
If I add text to the inner gridPane, like so:
VBox vBox = new VBox();
vBox.setSpacing(25);
{
Text text = new Text(IRAPfx.getData().getOptionString("Localised text 6"));
text.setFont(Font.font("Arial",FontPosture.ITALIC,FONT_SIZE_INSTRUCTIONS));
text.setFill(Color.WHITE);
GridPane.setHgrow(text, Priority.NEVER);
vBox.getChildren().add(text);
}
...
GridPane.setHgrow(vBox, Priority.NEVER);
innerGridPane.add(vBox, 1, 1 + resultsCell );
The inner grid, and together with this inner grid the outer grid, will grow when the text string is too large to fit in its cell, resulting in the whole grid (inner and outer) being "stretched" to fit this long contents, and running over the right edge of the screen.
What I would like to archieve is that the long string runs over the outer grid, or even out of the screen if it is really long, but not stretching the whole grid.
As you can see in the code, I tried setting Hgrow to NEVER to no avail. Any suggestions how to fix it (I can't find examples or tips on the www..) would be very welcome.
Thanks in advance,
Joris
I found a partial solution to it:
You can clip or wrap the text to the available width of the cell by using 'label' in stead of 'text'. See below:
For wrapping:
{
Label label = new Label(IRAPfx.getData().getOptionString("Localised text 6"));
label.setFont(Font.font("Arial",FontPosture.ITALIC,FONT_SIZE_INSTRUCTIONS));
label.setWrapText(true);
label.setTextFill(Color.WHITE);
vBox.getChildren().add(label);
}
For clipping:
{
Label label = new Label(IRAPfx.getData().getOptionString("Localised text 6"));
label.setFont(Font.font("Arial",FontPosture.ITALIC,FONT
label.setTextOverrun(OverrunStyle.CLIP);
label.setTextFill(Color.WHITE);
vBox.getChildren().add(label);
}
This more or less solves my problem, however I still did not manage to let the text run over into the next columns (the ones to the right of the cell where the text/label is put).
Any help on that still welcome!
Have you tried the setWrappingWidth method on Text object, you can define a predefined with of this way, 200 in this case:
text.setWrappingWidth(200);
It isn't really clear to me why you have the Text elements inside a VBox before putting them into the GridPane. But you can do something like this:
vbox.maxWidthProperty().bind(innerGridPane.widthProperty().multiply(innerGridPane.getColumnConstraints().get(1).percentWidthProperty());
text.wrappingWidthProperty().bind(vbox.maxWidthProperty());
Unfortunately, you can't just get a GridPane column and get it's width property to bind it, so you have to recalculate it using the ColumnConstraints properties.
Any case where you need to have items dynamically change behaviours base on window sizes and so on, you'll need to bind the relevant properties together.

Get a component from JList by click location

How can I fetch a component from a JList, with the click location?
I have my own list cell renderer where I insert some panels and labels.
Now i want to get e.g. the label where the user clicked at.
I tried the method list.getComponentAt(evt.getPoint()); but it returns only the entire JList.
I've not tested this, but the basics would be...
Use JList#locationToIndex(Point) to get the index of the element at
the given point.
Get the "element" at the specified index (using
JList#getModel#getElementAt(int)).
Get the ListCellRenderer using JList#getCellRenderer.
Render the element and get it's Component representation
Set the renderer's bounds to the required cell bounds
Convert the original Point to the Components context
Use getComponentAt on the renderer...
Possibly, something like...
int index = list.locationToIndex(p);
Object value = list.getModel().getElementAt(int);
Component comp = listCellRenderer.getListCellRendererComponent(list, value, index, true, true);
comp.setBounds(list.getCellBounds(index, index));
Point contextPoint = SwingUtilities.convertPoint(list, p, comp);
Component child = comp.getComponentAt(contextPoint);
MadProgrammer's works fine as long as the user doesn't click outside a cell. If he does that, the index returned by locationToIndex() will be the last index's cell, so the converted point will be "under" the rendered component
To check if the user really clicked a cell you have to do:
int index = list.locationToIndex(p);
if (index > -1 && list.getCellBounds(index, index).contains(p)){
// rest of MadProgrammer solution
...
}

Categories

Resources