I am currently experimenting with JavaFX's scene graph. I came across a strange problem that i can't really explain. Have a look at the following code:
final BorderPane bp1 = new BorderPane();
final Button button = new Button("CLICK ME");
Scene sc1 = new Scene(bp1,100,100);
bp1.setCenter(button);
stage1.setScene(sc1);
stage1.show();
Stage stage2 = new Stage();
final BorderPane bp2 = new BorderPane();
Scene sc2 = new Scene(bp2,100,100);
stage2.setScene(sc2);
stage2.setX(250);
stage2.show();
bp2.setCenter(button);
bp1.setCenter(button);
What i am trying to do here is to add a node (Button) to a Borderpane which lies within a scene on stage1. At the end of the code i try to add the node to a different scene(pane) on stage2. This actually works. But the last line does not. The button remains on stage2. The strange thing is that if i replace the borderpane with a gridpane, the code works as intended. Why does the borderpane behave different from the gridpane in this situation?
Another question which came to my mind in this case: I assume that there is exactly 1 scene graph per scene (or stage). Is that correct?
I guess you found a bug in the BorderPane implementation. The BorderPane has a bit of peculiar way how it manages its children. You can find the BorderPane implementation here. I suggest you file a bug report at the javafx jira
My testcase:
final BorderPane bp1 = new BorderPane();
final BorderPane bp2 = new BorderPane();
System.out.println("bp1 "+bp1);
System.out.println("bp2 "+bp2);
final Button button = new Button("CLICK ME");
button.parentProperty().addListener(new ChangeListener<Parent>() {
#Override
public void changed(ObservableValue<? extends Parent> observableValue, Parent parent, Parent parent2) {
System.out.println("changed");
System.out.println(button.getParent());
}
});
bp1.centerProperty().addListener(new InvalidationListener() {
#Override
public void invalidated(Observable observable) {
System.out.println("bp1 center invalidated ");
}
});
bp2.centerProperty().addListener(new InvalidationListener() {
#Override
public void invalidated(Observable observable) {
System.out.println("bp2 center invalidated ");
}
});
// bp1.centerProperty().addListener(new ChangeListener<Node>() {
// #Override
// public void changed(ObservableValue<? extends Node> observableValue, Node node, Node node2) {
// System.out.println("bp1 center changed "+node+" -> "+node2);
// }
// });
// bp2.centerProperty().addListener(new ChangeListener<Node>() {
// #Override
// public void changed(ObservableValue<? extends Node> observableValue, Node node, Node node2) {
// System.out.println("bp2 center changed "+node+" -> "+node2);
// }
// });
Scene sc1 = new Scene(bp1,100,100);
bp1.setCenter(button);
primaryStage.setScene(sc1);
primaryStage.show();
Stage stage2 = new Stage();
Scene sc2 = new Scene(bp2,100,100);
stage2.setScene(sc2);
stage2.setX(250);
stage2.show();
bp2.setCenter(button);
bp1.setCenter(button);
output:
bp1 BorderPane#48a80c67
bp2 BorderPane#10c66375
changed
BorderPane#48a80c67[styleClass=root]
bp1 center invalidated
changed
null
changed
BorderPane#10c66375[styleClass=root]
bp2 center invalidated
Explanation:
I found that bp1's center property is not invalidated and thus not updated when the button is added to the second scene. Adding a node to another scene should normally cause it to be removed from the previous scene. However, since BorderPane uses a peculiar way to hold its values (object properties for center, left and so on), after the button is added to the second scene, the button is not removed from the center property of the BorderPane. Setting the button a second time on the same BorderPane doesn't add the button to its children because it thinks that it is already set as the center node.
To your second question:
yes, only one scene graph per stage/scene.
Related
Please go through the image below.
In the above image you can find that horizontal scrolling is not started.
Now visit the After Scroll image of the same contents...
Now in the second image you can see that horizontal scrolling is done ...
JFXPanel contents are scroll horizontally... Which was perfect...
Now the third image will describe the problem....
It is liitle bit stretched to see as it is maximized...
You can see that the JFXPanel contents have changed their original position...
Moreover the contents must start with X_DisplaceMent = 0.0 [X-Cordinate], which was done automatically in the first two images...
All the contents are nodes like [Rectangle,Line etc.. ], after that all are placed in Group node...
And this Group node is set in the ScrollPane through
js.setContent(Group node);
Each component is placed with given x,y cordiante value .. then how did this happen while doing the maximized ?
Please help me to find the root cause ...
Thanks in advance...
Here are some facts that cause the problem.
- Start Position of Scene : 0.0
- Start Position of Group in Scene : 49.5
- Width of the root : 364.5
- Start Position of Scene : 0.0
- Start Position of Group in Scene : 63.5
- Width of the root : 364.5
- Start Position of Scene : 0.0
- Start Position of Group in Scene : 83.5
- Width of the root : 364.5
Whenever we drag the window horizontally Group is moving in the scene... That should not happen... how to avoid this ...
Ok... Here is the MCVE.....
There is a Frame. which contain SplitPane having vertical split.
The SplitPane will show the contents of two JFxPanels.
Both fxpanels are having rectangle on same x cordinate but Y cordinate is different.
And both the fxPanels are horizontal scroll sync. Not bi-directional. When you scroll lower panel horizonatally, the upper panel will get scrolled due to horizontal sync.
Here is the code for fxPanel 1...
public class FxPanel1 extends JFXPanel
{
private ScrollPane scroll ;
public ScrollPane getJs() {
return scroll;
}
public void setJs(ScrollPane js) {
this.scroll = js;
}
private boolean initFX(JFXPanel fxPanel) {
Scene scene = createScene();
fxPanel.setScene(scene);
return true;
//craneAssignmentChartView.setFxPanel(fxPanel);
}
private Scene createScene() {
Group root = new Group();
Rectangle rect = new Rectangle(10.0, 20.0, 800, 40);
rect.setFill(javafx.scene.paint.Color.TRANSPARENT);
rect.setStroke(javafx.scene.paint.Color.RED);
AnchorPane anchor = new AnchorPane();
anchor.getChildren().add(rect);
GridPane grid = new GridPane();
grid.setHgap(0);
grid.setVgap(0);
grid.setPadding(new Insets(0, 0, 0, 0));
grid.add(anchor, 1, 0);
root.getChildren().add(grid);
ScrollPane scroll = new ScrollPane();
scroll.setHbarPolicy(ScrollBarPolicy.ALWAYS);
scroll.setVbarPolicy(ScrollBarPolicy.AS_NEEDED);
scroll.setContent(root);
setJs(scroll);
return new Scene(scroll, javafx.scene.paint.Color.WHITE);
}
private void createUI(final JFXPanel fxPanel)
{
Platform.runLater(new Runnable() {
#Override
public void run()
{
initFX(fxPanel);
}
});
}
public FxPanel1( JFXPanel fxPanel)
{
createUI(fxPanel);
}
}
Now the code for second fxPanel looks like ...
public class FxPanel2 extends JFXPanel
{
private ScrollPane scroll ;
public ScrollPane getJs() {
return scroll;
}
public void setJs(ScrollPane js) {
this.scroll = js;
}
private boolean initFX(JFXPanel fxPanel) {
Scene scene = createScene();
fxPanel.setScene(scene);
return true;
//craneAssignmentChartView.setFxPanel(fxPanel);
}
private Scene createScene() {
Group root = new Group();
Rectangle rect = new Rectangle(10.0, 180.0, 800, 40);
rect.setFill(javafx.scene.paint.Color.TRANSPARENT);
rect.setStroke(javafx.scene.paint.Color.RED);
AnchorPane anchor = new AnchorPane();
anchor.getChildren().add(rect);
GridPane grid = new GridPane();
grid.setHgap(0);
grid.setVgap(0);
grid.setPadding(new Insets(0, 0, 0, 0));
grid.add(anchor, 1, 0);
root.getChildren().add(grid);
ScrollPane scroll = new ScrollPane();
scroll.setHbarPolicy(ScrollBarPolicy.ALWAYS);
scroll.setVbarPolicy(ScrollBarPolicy.NEVER);
scroll.setContent(root);
setJs(scroll);
return new Scene(scroll, javafx.scene.paint.Color.WHITE);
}
private void createUI(final JFXPanel fxPanel)
{
Platform.runLater(new Runnable() {
#Override
public void run()
{
initFX(fxPanel);
}
});
}
public FxPanel2( JFXPanel fxPanel)
{
createUI(fxPanel);
}
}
The main class looks like ....
public class DemoToCheckUIAlignment extends JFrame
{
public static void main(String[] args)
{
final DemoToCheckUIAlignment demo = new DemoToCheckUIAlignment();
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
JFXPanel panel1 = new JFXPanel();
FxPanel1 fxObj1 = new FxPanel1(panel1);
JFXPanel panel2 = new JFXPanel();
FxPanel2 fxObj2 = new FxPanel2(panel2);
DemoToCheckUIAlignment frame = new DemoToCheckUIAlignment();
frame.setSize(800, 500);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JSplitPane chartSplitPane = new JSplitPane();
chartSplitPane.setOrientation(JSplitPane.VERTICAL_SPLIT);
chartSplitPane.setDividerLocation(200);
chartSplitPane.setDividerSize(2);
chartSplitPane.setTopComponent(panel1);
chartSplitPane.setBottomComponent(panel2);
demo.provideScrollSyncBetweenFXPanels(fxObj1.getJs(), fxObj2.getJs());
frame.getContentPane().add(chartSplitPane);
//frame.getContentPane().add(panel2);
frame.setVisible(true);
}
});
}
public static void provideScrollSyncBetweenFXPanels(final ScrollPane upperSP, final ScrollPane lowerSP)
{
lowerSP.hvalueProperty().addListener(new ChangeListener<Number>()
{
public void changed(ObservableValue<? extends Number> ov,
Number old_val, Number new_val)
{
upperSP.hvalueProperty().set(new_val.doubleValue());
}
});
}
}
Now to check the problem follow the simple steps...
Ofcorse run the program...
Scroll the bottom Panel ....that is FxPanel2...
And maximized the window .... The x - position for the inner contents is changed now...
which does not happen with Swing....
Here are the screen shots where the problem reproduce for the attached MCVE....Please go through the images....
You may use setFitToWidth of the ScrollPane Object to match a particular dimension. For more details you may refer to the link http://docs.oracle.com/javafx/2/ui_controls/scrollpane.htm at down the page, Resizing Components in the Scroll Pane, you may find more on the solution
This question already has answers here:
Get the height of a node in JavaFX (generate a layout pass)
(2 answers)
Closed 8 years ago.
I am developing an application for which it is necessary to layout nodes besides each other (or on top of each other etc.). However, this layout is only an initial placement and the user is able to move these nodes arbitrarily. How is this done in the correct way in JavaFX? I will explain my problem with a simplified example:
Assume I have 2 rectangles and want to place rect2 to the right of rect1.
// create first rectangle at position x= 5, y=5
rect1 = rectangle(5,5);
// create second rectangle to the right of rect1
rect2 = rectangle(5+rect1.width(), 5);
In this scenario JavaFX will not yet have determined the width of rect1 and it will be zero. Intuitively, I would perform a call that lets JavaFX draw rect1 and thus determine its width and afterwards add rect2. See the following example:
// create first rectangle at position x= 5, y=5
rect1 = rectangle(5,5);
// let JavaFX draw rect1 (width will be calculated and set)
draw();
// create second rectangle to the right of rect1
rect2 = rectangle(5+rect1.width(), 5);
Unfortunately I haven't found a method that does what I want. My current workaround makes use of Platform.runLater() but this does not work properly all the time. If my understanding of bindings is correct, bindings are also not suitable for this problem. I only want to initially layout the nodes, so I would have to remove the binding after the initial layout (or else rect2 would move if rect1 is moved).
Thanks in advance for any help.
EDIT: Here is a minimal working example. The width of the button is 0. I tried calling root.layout() to force a layout pass etc. but it does not seem to work.
public class Test extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
Button btn = new Button();
StackPane root = new StackPane();
Scene scene = new Scene(root, 300, 250);
primaryStage.setScene(scene);
primaryStage.show();
btn.setText("Say 'Hello World'");
root.getChildren().add(btn);
// prints out 0
System.out.println(btn.getWidth());
}
}
Given the example from above, if I set the scene of the stage to null and then reset it, the button width will be set correctly when calling System.out.println(). It seems that this forces a layout pass on the whole stage? However, this just seems to be another workaround, in particular I have performance concerns.
#Override
public void start(Stage primaryStage) {
Button btn = new Button();
StackPane root = new StackPane();
Scene scene = new Scene(root, 300, 250);
primaryStage.setScene(scene);
primaryStage.show();
btn.setText("Say 'Hello World'");
root.getChildren().add(btn);
// reset scene
primaryStage.setScene(null);
primaryStage.setScene(scene);
// prints out 107.0
System.out.println(btn.getWidth());
}
The following program fails to resize the line chart horizontally when embedded in a Pane (or borderpane of anchorpane for the matter)
If the line chart is directly parented to the VBox instead, then everything works as expected.
I found I needed to bind the chart size to the parent pane, which I assume must be done automatically by VBox and HBox.
After trying different combination of enclosing in HBox/VBox, setting growing and alignment policies, I am quite confused about how layouts work.
I observe that there are differences in how ui components behave wrt resizing.
Any clarification (or digest insight on javadoc unclear documentation) is appreciated.
Best regards.
Source edited and clarified
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.chart.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
public class App extends Application {
#Override
public void start(Stage stage) {
NumberAxis xAxis = new NumberAxis();
NumberAxis yAxis = new NumberAxis();
xAxis.setLabel("X");
yAxis.setLabel("Y");
final LineChart<Number, Number> lineChart = new LineChart<>(xAxis, yAxis);
lineChart.setTitle("x = f(y)");
XYChart.Series data = new XYChart.Series();
data.setName("Serie 1");
for (int i = 0; i < 10; i++) {
data.getData().add(new XYChart.Data(i, i * i));
}
lineChart.getData().add(data);
VBox vb = new VBox();
vb.setFillWidth(true);
HBox hb = new HBox();
hb.getChildren().add(lineChart);
hb.setFillHeight(true);
vb.getChildren().add(hb);
HBox.setHgrow(lineChart, Priority.ALWAYS);
VBox.setVgrow(hb, Priority.ALWAYS);
Scene scene = new Scene(vb);
stage.setScene(scene);
stage.centerOnScreen();
stage.setResizable(true);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
For the record, the problem of the provided example is solved after modifying this typo:
HBox.setHgrow(hb, Priority.ALWAYS);
to:
HBox.setHgrow(lineChart, Priority.ALWAYS);
This fixes resizing horizontally.
When embedded directly in VBox, the chart's size is recomputed on resize, as it is anchored to the VBox, which boundaries change.
When embedded in a HBox, we have to provide a hint for the HBox to grow horizontally, and vertically.
Vertically it's done with:
VBox.setVgrow(hb, Priority.ALWAYS);
Horizontally it's done by requesting its content to occupy all available space, which the fix above is about.
I've been trying to work with the scaling transform in JavaFX, but haven't quite been able to wrap my head around it. Basically, I have a Pane containing a complex graph and would like to be able to rescale it. The scaling part itself works fine, however, the enclosing scroll pane will not adapt to the graph.
For simplicity's sake, i'll post a short example in which my graph is replaced by a label:
public class TestApp extends Application {
#Override public void start(final Stage stage) throws Exception {
final Label label = new Label("Hello World");
label.getTransforms().setAll(new Scale(0.5, 0.5));
label.setStyle("-fx-background-color:blue");
label.setFont(new Font(200));
final ScrollPane scrollPane = new ScrollPane();
scrollPane.setContent(label);
stage.setScene(new Scene(scrollPane));
stage.setWidth(200);
stage.setHeight(100);
stage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
}
The label will scale correctly, but the enclosing scroll pane's bars will still accomodate a component of the original size.
I've tried so far:
Playing around with the labels min and pref size
wrapping the label inside a Group (no scrollbars will appear whatsoever)
scaling the enclosing Group rather than the label
What am I missing? What can I do to make the ScrollPane adapt to the content view?
Thanks for your help.
According to the ScrollPane document you might try to wrap a Pane in a Group so the ScrollPane is scroll by visual bound not the actual layout bound.
ScrollPane layout calculations are based on the layoutBounds rather than the
boundsInParent (visual bounds) of the scroll node. If an application wants the
scrolling to be based on the visual bounds of the node (for scaled content etc.),
they need to wrap the scroll node in a Group.
I implemented scaling in a ScrollPane for Graphs and other nodes in
this example of scrollpane viewports, transforms and layout bounds in JavaFX.
The code was implemented when I was first learning JavaFX, so certainly the code could be cleaner and perhaps there are simpler ways to accomplish this (e.g. using a Group as the container for the scaled node as suggested in the ScrollPane documentation).
One key to getting the solution I wanted (ScrollBars only appearing when you are zoomed in and the node is larger than the visible viewport), was this code:
// create a container for the viewable node.
final StackPane nodeContainer = new StackPane();
nodeContainer.getChildren().add(node);
// place the container in the scrollpane and adjust the pane's viewports as required.
final ScrollPane scrollPane = new ScrollPane();
scrollPane.setContent(nodeContainer);
scrollPane.viewportBoundsProperty().addListener(
new ChangeListener<Bounds>() {
#Override public void changed(ObservableValue<? extends Bounds> observableValue, Bounds oldBounds, Bounds newBounds) {
nodeContainer.setPrefSize(
Math.max(node.getBoundsInParent().getMaxX(), newBounds.getWidth()),
Math.max(node.getBoundsInParent().getMaxY(), newBounds.getHeight())
);
}
});
...
// adjust the view layout based on the node scalefactor.
final ToggleButton scale = new ToggleButton("Scale");
scale.setOnAction(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent actionEvent) {
if (scale.isSelected()) {
node.setScaleX(3); node.setScaleY(3);
} else {
node.setScaleX(1); node.setScaleY(1);
}
// runlater as we want to size the container after a layout pass has been performed on the scaled node.
Platform.runLater(new Runnable() {
#Override public void run() {
nodeContainer.setPrefSize(
Math.max(nodeContainer.getBoundsInParent().getMaxX(), scrollPane.getViewportBounds().getWidth()),
Math.max(nodeContainer.getBoundsInParent().getMaxY(), scrollPane.getViewportBounds().getHeight())
);
}
});
}
});
I have noticed an issue with setFocusTraversable() on MenuBar control. If I call setFocusTraversable(false) on menuBar object the focus traverses (I can see the menubar getting highlighted/selected) to menu when I press Tab from TextField but the event (changed()) does not get fired. If I call setFocusTraversable(true) on menuBar object and press Tab when in TextField the focus does not visually traverses to MenuBar(TextField loses focus) but the event gets fired and additionally the focus can not be set on TextField using Tab or Shift + Tab. I am not sure if this is a bug or problem with my understanding.
Here is the code.
public class MenuTest extends Application
implements ChangeListener
{
MenuBar menuBar = new MenuBar();
TextField tf1 = new TextField("One");
public static void main(String[] args)
{
Application.launch(args);
}
#Override
public void start(Stage primaryStage)
{
Group content = new Group();
BorderPane paneLayout = new BorderPane();
final Menu menu1 = new Menu("File");
menuBar.getMenus().addAll(menu1);
Menu exit = new Menu("Exit");
menu1.getItems().add(exit);
content.getChildren().add(tf1);
paneLayout.setTop(menuBar);
paneLayout.setCenter(content);
Scene scene = new Scene(paneLayout, 300, 250);
primaryStage.setScene(scene);
primaryStage.show();
menuBar.setFocusTraversable(false);
menuBar.focusedProperty().addListener(this);
tf1.focusedProperty().addListener(this);
tf1.requestFocus();
}
public void changed(ObservableValue ov, Object t, Object t1)
{
System.out.println("focus gained - " + ov.toString());
}
}
Please help.
Thanks,
KK
PS: MenuBar API explicitly says that "MenuBar sets focusTraversable to false." but behaves differently.
Unfortunately you've met a bug: http://javafx-jira.kenai.com/browse/RT-20595