How to clone a node in the scene graph in JavaFX? - java

I have a HBox with prefHeight = 70 // no prefWidth or any width...
I also have a Pane with prefWidth = 50 // no prefHeight or any height...
I just want to add multiple instance of the pane to the HBox using some loop.
When I do add(pane) in the loop body it gives following error.
Exception in thread "JavaFX Application Thread" java.lang.IllegalArgumentException: Children: duplicate children added: parent = HBox[id=myHBox]
I need to find way to clone the pane(as it passes by value).
Can anybody help me please?
(taking snapshot are not work for me because prefHeight of the pane is not set/ computed using parent)

This error happens because you're trying to add the same instance of a Node to another Node. If you remove the comments from the example below you'll get that error as well. Loop, on the other hand, will work fine because in each iteration new Button instance is created.
#Override
public void start(Stage stage) {
FlowPane root = new FlowPane();
// Results in error
// Button b1 = new Button("Button");
// root.getChildren().addAll(b1,b1);
for (int i = 0; i < 4; i++) {
Button b = new Button("Button");
root.getChildren().add(b);
}
Scene scene = new Scene(root, 50, 100);
stage.setScene(scene);
stage.show();
}
Your pane is probably more complicated, but you have to use the same principle. Put the code responsible for creating your pane in a separate method, getPane() or such, and use it in a loop to obtain new instances.
JavaFX doesn't give you an out-of-the-box solution to make a deep copy of a Node. If your Node is crated statically you can:
Put the code responsible for creating it in a separate method and
use it throughtout your application every time you need to get a new
instance of your pane.
Define it in a FXML file and load it every time you need a new instance.
Things get significantly worse if your Node has properties or children that were created or modified dynamically by the user. In that case you have to inspect its elements and recreate them on your own.

Related

How do I make all the fields in my javafx form clickable using GridPane and HBox?

I want to make a form with three sections, two with fields and one with buttons.
public class Form extends Application{
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
StackPane root = new StackPane();
GridPane fp = new GridPane();
fp.setAlignment(Pos.TOP_CENTER);
fp.setHgap(6);
fp.setVgap(6);
fp.add(new Label("Name: "), 0, 0);
TextField name = new TextField();
name.setPrefWidth(450);
fp.add(name, 1, 0);
GridPane sp = new GridPane();
sp.setAlignment(Pos.CENTER);
sp.setHgap(6);
sp.setVgap(6);
sp.add(new Label("Another Name: "), 1, 0);
TextField anothername = new TextField();
anothername.setPrefWidth(120);
sp.add(anothername, 2, 0);
HBox hbox = new HBox();
hbox.setAlignment(Pos.BOTTOM_CENTER);
Button btn1 = new Button("Button 1");
hbox.getChildren().add(btn1);
Scene scene = new Scene(root, 500, 500);
root.getChildren().addAll(fp, sp, hbox);
stage.setScene(scene);
stage.show();
}
}
formatting and some text might be off but that is my general solution. I made a root stack pane to hold all the parts of my form. I then made two grid panes to hold text fields and an hbox to hold my buttons along the bottom.
example of how it looks
My problem is that only the name field can be clicked. If I try to click another name field it wont work. I can press tab to cycle through the fields and button but I want to be able to click on each field individually. Is there a better way to create one scene with multiple panes or hboxes? I am also open to only having one grid pane, but I thought having two would be easier for formatting since I want to separate different fields. Thank you!
The issue you're facing is caused by your using a StackPane as the root element of your scene.
A StackPane, as the name suggests, stacks its children one on top of the other. Any children placed on top will be the ones receiving events (such as clicking on the anothername field).
You have added 3 nodes as children of your StackPane:
GridPane #1 fp
GridPane #2 sp
HBox hbox
Since the HBox was added last, it is the only node that can receive click events.
Using your example, I've added borders to each of the 3 items above to illustrate how JavaFX is laying them out:
As you can see, each child of the StackPane get resized to fill the entire area (I used different widths for the borders so you can see them all).
You can try this yourself by adding the following code before you show your stage:
fp.setStyle("-fx-border-color: green; -fx-border-width: 15px");
sp.setStyle("-fx-border-color: blue; -fx-border-width: 10px");
hbox.setStyle("-fx-border-color: red; -fx-border-width: 5px");
To solve this, you will need to rethink your layout entirely; a StackPane is certainly not the correct layout pane to use in your case.
I highly recommend working through the examples in Oracle's Working With Layouts in JavaFX tutorial to get a better grasp on how to best layout your scene.

Accessing Arbitrary nodes from EventHandler target

I'm working on a small JavaFX application. In this application I have the following components :
BorderPane -> as the root element
HBox top, bottom -> top and bottom regions
VBox left, right -> left and right regions
FlowPane center -> central region
When the central region is clicked i need to access a node in the top region containing some text. In order to access it i climb the graph upwards from the event's target like this :
public EventHandler<MouseEvent> fieldClicked = (MouseEvent e) -> {
FlowPane target = (FlowPane)e.getTarget();
BorderPane root = (BorderPane)target.getParent();
HBox top = (HBox)root.getChildren().get(0);
HBox top_left = (HBox)top.getChildren().get(0);
Text total = (Text)top_left.getChildren().get(0);
ObservableList<Node> dices = target.getChildren();
/* Do some stuff with retrieved nodes */
};
Is there a better and less verbose way to access an arbitrary node in the scene graph beside iteratively calling Node.getParent()
If you do not store the field in some other way, no. You may attach some id to find a node via CSS selector (lookup) but in that case you're better of doing this a different way:
Store the nodes you need to access in fields (or effectively final local variables, if you register event handlers in the same scope where you create the nodes).
...
private BorderPane root;
private HBox top;
private Text total;
private FlowPane target;
public EventHandler<MouseEvent> fieldClicked = (MouseEvent e) -> {
ObservableList<Node> dices = target.getChildren();
/* Do some stuff with fields */
};
private void initializeNodes() {
...
total = new Text();
top = new HBox(total);
root.setTop(top);
target = new FlowPane();
root.setCenter(target);
...
}
It's better to decouple the modification of certain values from the layout of the scene as much as possible anyways, since this makes it easier for you to rearrange the scene without having to worry about event handlers navigating the scene correctly via up-/downward navigation though the scene. Furthermore you'll get into trouble, if you're using your approach in cases where you use a "parent" other than a Pane or Group, e.g. ScrollPane since the skin of ScrollPane inserts the content node into the scene as it's descendant, but not as it's child and it doesn't do this until the first layout pass.
BTW: Note that it's Event.getSource that yields the node the event handler was triggered for, not Event.getTarget.
To get a specific Node, you can use the lookup() method of the javafx.scene.Scene class.
For Example, you can set an ID on the node containing some text and then find it with scene.lookup("#theID");
public EventHandler<MouseEvent> fieldClicked = (MouseEvent e) -> {
FlowPane target = (FlowPane)e.getTarget();
Text total = (Text) target.getScene().lookup("#myTextID");
/* Do some stuff with retrieved nodes */
};
The ID you can set by:
Text text = new Text("My Text element somewhere");
text.setId("myTextID");
I'm new to JavaFX so I don't know if this is the best way either. But I hope, this is what you are looking for.
By the way, if you want to get to the root node, you can instead use:
public EventHandler<MouseEvent> fieldClicked = (MouseEvent e) -> {
FlowPane target = (FlowPane)e.getTarget();
BorderPane root = (BorderPane) target.getScene().getRoot();
};
Its maybe helpful when you have more Elements in your FlowPane, then you also don't have to call that often Node.getParent().
Hope it helps!

JavaFX TabPane tabs don't update position

I noticed that when adding and deleting tabs from a TabPane, it fails to match the position of the order of tabs in the underlying list. This only happens when at least one tab is hidden entirely due to the width of the parent. Here's some code that replicates the issue:
public class TabPaneTester extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
Scene scene = sizeScene();
primaryStage.setMinHeight(200);
primaryStage.setWidth(475);
primaryStage.setScene(scene);
primaryStage.show();
}
private Scene sizeScene(){
TabPane tabPane = new TabPane();
tabPane.setTabMinWidth(200);
tabPane.getTabs().addAll(newTabs(3));
Scene scene = new Scene(tabPane);
scene.setOnKeyPressed(e -> tabPane.getTabs().add(1, tabPane.getTabs().remove(0)));
return scene;
}
private static Tab[] newTabs(int numTabs){
Tab[] tabs = new Tab[numTabs];
for(int i = 0; i < numTabs; i++) {
Label label = new Label("Tab Number " + (i + 1));
Tab tab = new Tab();
tab.setGraphic(label);
tabs[i] = tab;
}
return tabs;
}
public static void main(String[] args) {
launch();
}
}
When you press a key, it removes the first tab (at index 0) and puts it back at index 1, effectively swapping the first two tabs. However, when run the tabs don't actually visually swap (even though the tab switcher menu does switch their position).
If you change the width of the screen to include even a pixel of the third tab that was hidden (replace 475 with 500), it works as intended. Any clues as to how to fix this?
This is indeed a bug and I couldn't find it reported in the public JIRA it is now reported at https://bugs.openjdk.java.net/browse/JDK-8193495.
All my analysis is based on the code in TabPaneSkin if you want to have a look yourself.
Summary
The problem arises when you remove and then add the tab "too quickly". When a tab is removed, asynchronous calls are made during the removal process. If you make another change such as adding a tab before the async calls finish (or at least "finish enough"), then the change procedure sees the pane at an invalid state.
Details
Removing a tab calls removeTabs, which is outlined below:
Various internal removal methods are called.
Then it checks if closing should be animated.
If yes (GROW),
an animation queues a call to a requestLayout method, which itself is invoked asynchronously,
and the animations starts (asynchronously) and the method returns.
If not (NONE),
requestLayout is called immediately and the method returns.
The time during which the pane is at an invalid state is the time from when the call returns until requestLayout returns (on another thread). This duration is equivalent to the duration of requestLayout plus the duration of the animation (if there is one), which is ANIMATION_SPEED = 150[ms]. Invoking addTabs during this time can cause undesired effects because the data needed to properly add the tab is not ready yet.
Workaround
Add an artificial pause between the calls:
ObservableList<Tab> tabs = tabPane.getTabs();
PauseTransition p = new PauseTransition(Duration.millis(150 + 20));
scene.setOnKeyPressed(e -> {
Tab remove = tabs.remove(0);
p.setOnFinished(e2 -> tabs.add(1, remove));
p.play();
});
This is enough time for the asynchronous calls to return (don't call the KeyPressed handler too quickly in succession because you will remove the tabs faster than they can be added). You can turn off the removal animation with
tabPane.setStyle("-fx-close-tab-animation: NONE;");
which allows you to decrease the pause duration. On my machine 15 was safe (here you can also call the KeyPressed handler quickly in succession because of the short delay).
Possible fix
Some synchronization on tabHeaderArea.

How to prevent FlowPane from wrapping (vertically) when end of Scene is reached

public void start(Stage stage) throws Exception {
FlowPane flowPane = new FlowPane();
flowPane.setOrientation(Orientation.VERTICAL);
for(int i = 0; i < 101;i++) {
Label aLabel = new Label("Label number: " + i);
flowPane.getChildren().add(aLabel);
}
Scene applicationScene = new Scene(flowPane);
stage.setHeight(400.0);
stage.setWidth(400.0);
stage.setScene(applicationScene);
stage.show();
}
I am trying to write the code so that all of the labels end up in the same column even though the labels will not be within the window (I plan on adding a scrollPane to make the labels still viewable issue). However, I have no clue as to why, since the labels automatically begin filling up the next column when the first one is filled (example here). How should I go about this?
I have no clue as to why since the labels automatically begin filling
up the next column when the first one is filled
That's the functionality of a FlowPane. From the documentation:
FlowPane lays out its children in a flow that wraps at the flowpane's
boundary. ... A vertical flowpane lays out nodes in columns, wrapping at the flowpane's height.
You should use a VBox instead:
public void start(Stage stage) throws Exception {
VBox vbox = new VBox();
for(int i = 0; i < 101;i++) {
Label aLabel = new Label("Label number: " + i);
vbox.getChildren().add(aLabel);
}
Scene applicationScene = new Scene(vbox);
stage.setHeight(400.0);
stage.setWidth(400.0);
stage.setScene(applicationScene);
stage.show();
}
If you have a lot of data to display, you might also consider using a ListView. The ListView has a more complex API (it manages selection, and can be editable if you choose), and provides its own scrollbars when needed, but it can be more efficient for large amounts of data (basically it only creates UI controls for the visible data, and reuses them as the user scrolls).

Set tooltip on TableColumn(JavaFX) without side effect

There is a way to set tooltip like that
if (tooltipText != null) {
Label lb = new Label(column.getText());
lb.setTooltip(new Tooltip(tooltipText));
column.setText(null);
column.setGraphic(lb);
}
Unfortunately then will be exists ugly side-effect.
We set null to text of column but menuItems invoke column.getText(). If we don't do this, there will be double name in header.How to solve it? Suppose by means of css..
This answer doesn't erase side-effect
How to add a tooltip to a TableView header cell in JavaFX 8
Note: The following code relies on internals of the TableView skin. These could be subject to change, since they reside in the com.sun packages.
CSS lookup can be used to get access to the TableColumnHeader nodes after the first layout pass on the TableView. Those can be used to retrieve the Label that is used to display the TableColumn's text property.
#Override
public void start(Stage primaryStage) {
TableView tv = createTableView();
Scene scene = new Scene(tv);
// generate layout pass
tv.applyCss();
tv.layout();
// assign tooltips to headers
tv.lookupAll(".column-header").stream().forEach(n -> {
TableColumnHeader header = (TableColumnHeader) n;
Tooltip tooltip = new Tooltip("Tooltip: " + header.getTableColumn().getText());
((Control) header.lookup(".label")).setTooltip(tooltip);
});
primaryStage.setScene(scene);
primaryStage.show();
}

Categories

Resources