I am a beginner who just recently started learning JavaFX, and I seem to be making the same reoccurring mistake within my programs. For example, in the following code I am trying to draw the X-Axis and Y-Axis and have it binded to half the width and height of the pane. When executed, the axes are very small and located at the topleft corner, but as you resize the window of the application, the axes slowly increase in size until the window not being resized anymore.
public class Debug extends Application {
#Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
GraphFunc test = new GraphFunc();
pane.getChildren().add(test);
Scene scene = new Scene(pane, 500, 500);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
class GraphFunc extends Pane {
private double xAxisSpan, yAxisSpan;
public GraphFunc() {
xAxisSpan = 100;
yAxisSpan = 100;
drawAxes();
}
private void drawAxes() {
Line xAxis = new Line(0, 50, 100, 50);
Line yAxis = new Line(50, 0, 50, 100);
xAxis.setStartX(0);
xAxis.startYProperty().bind(heightProperty().divide(2));
xAxis.endXProperty().bind(widthProperty());
xAxis.endYProperty().bind(heightProperty().divide(2));
yAxis.startXProperty().bind(widthProperty().divide(2));
yAxis.setStartY(0);
yAxis.endXProperty().bind(widthProperty().divide(2));
yAxis.endYProperty().bind(heightProperty());
getChildren().addAll(xAxis, yAxis);
}
}
I am confused because when pane is change to a StackPane, this does not happen. Also if I moved the code in drawAxes() to start() and added the lines to pane it would also not do this. Please explain, I cannot seem to understand what is happening after researching and playing around with it.
This is happening because of Pane. Pane is meant to be used when absolute positioning of children is required.
This is from Oracle documentation:
This class may be used directly in cases where absolute positioning of children is required since it does not perform layout beyond resizing resizable children to their preferred sizes. It is the application's responsibility to position the children since the pane leaves the positions alone during layout.
Pane pane = new Pane();
GraphFunc test = new GraphFunc();
pane.getChildren().add(test);
Scene scene = new Scene(pane, 500, 500);
If you remove the width and height from the scene and instead set the width and height directly on to test like this:
public void start(Stage primaryStage) {
GridPane pane = new GridPane();
GraphFunc test = new GraphFunc();
test.setPrefSize(500, 500);
pane.getChildren().add(test);
Scene scene = new Scene(pane);
primaryStage.setScene(scene);
primaryStage.show();
}
You will get this:
drawn image
However you will not be able to span.
From Oracle documentation:
Note: if an application needs children to be kept aligned within a
parent (centered, positioned at top-left, etc), it should use a
StackPane instead.
Also from Oracle documentation:
Pane does not clip its content by default, so it is possible that
childrens' bounds may extend outside its own bounds, either if
children are positioned at negative coordinates or the pane is resized
smaller than its preferred size.
Related
Maybe I'm misunderstanding something basic, but I'm experimenting with JavaFX and am baffled why scaling a Canvas (using .setScaleX/Y) with value of 2 doesn't result in canvas with two times bigger width/height.
The relevant code is this: (I'm not using any .fxml at this point)
public class Main extends Application {
#Override
public void start(Stage primaryStage){
AnchorPane pane = new AnchorPane();
Canvas canvas = new Canvas();
canvas.setWidth(100);
canvas.setHeight(100);
canvas.getGraphicsContext2D().setFill(Color.BLUE);
canvas.getGraphicsContext2D().fillRect(0,0,100,100);
canvas.setScaleX(2);
canvas.setScaleY(2);
pane.getChildren().add(canvas);
primaryStage.setScene(new Scene(pane, 200, 200));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
This results in the canvas taking about 2/3 (in terms of width/heighth, not area) of the window. Using scale of 3 makes it fill the whole window (bar one pixel gap at the edge) and I don't understand why. I looked at the documentation but it seems like scaling factor of 2 should result in two times the dimensions.
EDIT:
So per suggestion below, translating the canvas by half it's width/height fixes the issue. But so does centering it in a StackPane, so I don't understand why the AnchorPane won't anchor it by it's top/left edge if it really is 200x200 after scaling, it's as if it was anchoring it by it's 100x100 edges. But if the Canvas is 100x100 then how is it drawing outside of those bounds if simply centered in StackPane?
EDIT2:
With some experimenting I think I confirmed that the AnchorPane is positioning the Canvas "as if" it was still 100x100. So by setting the top and left anchors to 50 I can get the same results as translating by 50 or as if centering it in StackPane.
But that means that effectively the Canvas is "drawing" outside of it's "edges". Surely this can't be right?
The information added in your edits in the original post give most of the answer to the question.
First note that scaleX and scaleY scale a node around its center. From the documentation: "Defines the factor by which coordinates are scaled about the center of the object along the X axis of this Node."
Second, note that transformations applied to a node are not taken into account in the layout calculations for that node. From the layout documentation (See the section "Visual Bounds vs Layout Bounds"):
So for example ... if a ScaleTransition is used to pulse the size of a
button, that pulse animation will not disturb layout around that
button. If an application wishes to have the effect, clip, or
transform factored into the layout of a node, it should wrap that node
in a Group.
The last sentence in that quote from the documentation refers to the fact that the layout bounds of a Group includes any transformations applied to the group's children (but not to the group itself).
To visualize what is happening, start with a 100x100 canvas. The layout ignores the scaling, so you can visualize this as anchoring the canvas to the top left corner of the anchor pane before applying the scaling. Then the canvas is scaled about its center, pushing all four corners 50 pixels away from the center in both dimensions. Consequently the bounds of the canvas in the anchor pane's coordinate system are from (-50, -50) to (150, 150). You can verify this by adding
System.out.println(canvas.getBoundsInParent());
after the stage is shown.
There are two solutions. One is to wrap the canvas in a Group. The layout bounds of the Group will account for the scaling on the canvas:
public class Main extends Application {
#Override
public void start(Stage primaryStage){
AnchorPane pane = new AnchorPane();
Canvas canvas = new Canvas();
canvas.setWidth(100);
canvas.setHeight(100);
canvas.getGraphicsContext2D().setFill(Color.BLUE);
canvas.getGraphicsContext2D().fillRect(0,0,100,100);
canvas.setScaleX(2);
canvas.setScaleY(2);
Group group = new Group(canvas);
AnchorPane.setTopAnchor(group, 0.0);
AnchorPane.setLeftAnchor(group, 0.0);
pane.getChildren().add(group);
primaryStage.setScene(new Scene(pane, 200, 200));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The other solution is to apply a transformation which scales the canvas about its top-left corner:
public class Main extends Application {
#Override
public void start(Stage primaryStage){
AnchorPane pane = new AnchorPane();
Canvas canvas = new Canvas();
canvas.setWidth(100);
canvas.setHeight(100);
canvas.getGraphicsContext2D().setFill(Color.BLUE);
canvas.getGraphicsContext2D().fillRect(0,0,100,100);
Scale scale = new Scale(2,2);
scale.setPivotX(0);
scale.setPivotY(0);
canvas.getTransforms().add(scale);
pane.getChildren().add(canvas);
primaryStage.setScene(new Scene(pane, 200, 200));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
You've already added canvas to the pane, try to apply .setScaleX/Y before pane.getChildren().add(canvas).
Consider a custom component which inherits from VBox. The component's height and width may change depending on some selection. In my Scene I usually have enough vertical space to fit the custom component but it may extend too far horizontally.
I tried placing the custom component inside a ScrollPane and make it grow vertically by binding the ViewPort's minHeight to the custom component's (i.e. the VBox') height. Thus, preventing vertical scrollbars from appearing.
Problem:
In JavaFX 8 the ScrollPane only resized to the correct height after another user input (e.g. a click inside the ScrollPane or resizing the window).
The issue is reproducible with the following code:
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
final VBox root = new VBox(5);
final Scene scene = new Scene(root, 400, 500);
final ListView<String> listView = new ListView<>();
final Rectangle rectangle = new Rectangle(600, 300, Color.RED);
final VBox customComponent = new VBox(rectangle);
final ScrollPane scrollPane = new ScrollPane(customComponent);
scrollPane.minViewportHeightProperty().bind(customComponent.heightProperty());
listView.getItems().addAll("Smaller", "Taller");
listView.getSelectionModel().selectedItemProperty().addListener((obs, old, item) -> {
if (Objects.equals(item, "Smaller")) {
rectangle.setHeight(200);
} else if (Objects.equals(item, "Taller")) {
rectangle.setHeight(400);
}
});
root.getChildren().addAll(listView, scrollPane);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Just click on either Smaller or Taller inside the ListView.
The issue does not appear when adding the rectangle directly to the ScrollPane without the VBox. However, that's not a solution to my problem since I can't easily change the custom component I'm working with.
Switching to OpenJFX 11 the ScrollPane correctly grows vertically, but doesn't shrink without another user input. So just upgrading my JavaFX version does not seem to fix the problem.
Is there a way to force the ScrollPane to immediately resize itself or simply another way to fit the ScrollPane to the custom component's height and only scroll horizontally?
I've been working in JavaFX for the first time to try to make an app which I can use to demonstrate a simple animation with button controls. To do this I've used a BoarderPane for the primary stage, with both the left,right, and bottom using Gridpanes.
However, for the center I need to be able to draw a sphere with a line within it which I can rotate for different views while simultaneously being able to animated, or at the very least snap-move, the line within.
I've tried using a Pane for the center which doesn't work. I've tried making it it's own scene and sub scene which doesn't work. And I can't use a canvas as that is only for 2D animation.
Is there a way I can animate the line or rotate the camera while maintaining the BoarderPane layout I've created?
I've tried looking at the following before to understand what I could do but most just seem incompatible with the BoarderPane:
JavaFX Rotating Camera Around a Pivot
JavaFX Canvas rotate image with fixed center (and without bouncing)
Whenever you want to mix 2D and 3D (and camera) you have to use a SubScene container for the 3D content:
SubScene provides separation of different parts of a scene, each of which can be rendered with a different camera, depth buffer, or scene anti-aliasing. A SubScene is embedded into the main scene or another sub-scene.
If you have a BorderPane container, you can perfectly add the subScene to its center.
For a similar use case, you can check the Qubit3D class from here, which is mainly a group that holds a sub scene with an Sphere and a cylinder (both from the FXyz 3D library).
You can add this group easily to your 2D scene:
private final Rotate rotate = new Rotate(0, 0, 0, 0, javafx.geometry.Point3D.ZERO.add(1, 1, 1));
#Override
public void start(Stage primaryStage) throws Exception {
final Timeline timeline = new Timeline(
new KeyFrame(Duration.seconds(10),
new KeyValue(rotate.angleProperty(), 360)));
final Qubit3D qubit = new Qubit3D();
final BorderPane root = new BorderPane(qubit);
final Button buttonAnimate = new Button("Animate");
buttonAnimate.setOnAction(e -> {
rotate.setAngle(0);
timeline.playFromStart();
});
root.setLeft(new StackPane(buttonAnimate));
final Button buttonStop = new Button("Stop");
buttonStop.setOnAction(e -> timeline.stop());
root.setRight(new StackPane(buttonStop));
Scene scene = new Scene(root, 600, 400, true, SceneAntialiasing.BALANCED);
scene.setFill(Color.BISQUE);
primaryStage.setScene(scene);
primaryStage.setTitle("Qubit3D Sample");
primaryStage.show();
qubit.rotateRod(rotate);
}
The only modification I've added to Qubit3D is:
public void rotateRod(Rotate rotate) {
rodSphere.getTransforms().setAll(rotate);
}
If you run it:
Note that you can interact with the sphere (via mouse dragged events), while you can also start/stop a full rotation of sphere and rod.
I am working on an application that enables the user to draw graphs, i.e. edges and nodes. As nodes I am currently using plain JavaFX label elements. When drawing the edges, I need to consider the bounds of the label, however, the width and the height of the labels seem to be initialized only after "drawing" it. E.g., when starting the application the label bounds have a width/height of 0, but if a label is repositioned by the user, the width/height is correct.
Is there a possibility to force JavaFX to draw the current elements? The code is rather complex, but the following gives an idea of what I want to do:
stackpane = new StackPane();
text = new Label("Test");
text.setStyle("-fx-border-color:black; -fx-padding:3px;");
stackpane.getChildren().addAll(text);
...
// is it possible to force JavaFX to draw the text here?
...
// some calculations with the bounds of the label
Node node = getLabel();
Bounds bounds = node.getBoundsInParent();
double height = bounds.getHeight();
double width = bounds.getWidth();
I also tried to wrap the text in a rectangle and then manually set the width/height of the rectangle. This works, but the nodes have labels of different length and thus manually setting it is not always suitable.
You may try to bind your logic to node.boundsInParentProperty() changes
public class SmartBorder extends Application {
#Override
public void start(Stage primaryStage) {
final Label txt = new Label("Example");
txt.relocate(100, 100);
Pane root = new Pane();
final Rectangle border = new Rectangle();
border.setFill(Color.TRANSPARENT);
border.setStroke(Color.RED);
// here we autoupdate border
txt.boundsInParentProperty().addListener(new ChangeListener<Bounds>() {
#Override
public void changed(ObservableValue<? extends Bounds> ov, Bounds old, Bounds b) {
border.setX(b.getMinX() - 1);
border.setY(b.getMinY() - 1);
border.setWidth(b.getWidth()+2);
border.setHeight(b.getHeight()+2);
}
});
root.getChildren().addAll(txt, border);
// click to see automatic border reaction
root.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
txt.relocate(Math.random()*200, Math.random()*200);
txt.setText(Math.random() + "");
}
});
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
}
Here is my summary code
public class KlArgon extends Application {
BorderPane border ;
Scene scene ;
Stage stage;
#Override
public void start(Stage stage) {
:
border = new BorderPane();
:
HBox infoBox = addInfoHBox();
border.setTop(infoBox);
:
VBox menuBox = addMenuVBox();
border.setLeft(menuBox);
:
border.setCenter(addAnchorPane(addGridPane()));
// setRight and setBottom is not used
:
scene = new Scene (border);
stage.setScene(scene);
stage.show();
}
private Node addAnchorPane(GridPane grid) {
AnchorPane anchorpane = new AnchorPane();
anchorpane.getChildren().add(grid);
AnchorPane.setTopAnchor(grid, 10.0);
return anchorpane;
}
private GridPane addGridPane() {
GridPane grid = new GridPane();
grid.setHgap(10);
grid.setVgap(10);
grid.setPadding(new Insets(0, 10, 0, 10));
grid.add(addWhiteboard(), 1, 0);
return grid;
}
private Node addWhiteboard() {
Canvas canvas = new Canvas (wboardWd, wdboardHt);
GraphicsContext gc = canvas.getGraphicsContext2D();
drawShapes(gc);
drawfromClipboard(gc);
return canvas;
}
}
I refer to the Center pane as the "Whiteboard". Among other things, I have two buttons in menuBox - btnCopyFromClipboard and btnClearWhiteboard.
When user presses btnCopyFromClipboard - the user should be able to draw an rectangle in the "Whiteboard" only (i.e. Center pane only) and then the clipboard image will be copied (scaled) into that rectangle.
So I made border,scene, stage as global and I am trying to get this to work - not only it is buggy/ugly- to me it looks like a hack. Is there a cleaner way to do this i.e. manage Center Pane when button in left pane is pressed?
Basically I want the Center Pane to be the Canvas and the GraphicsContext operations are performed whe the Buttons in Left Pane is pressed.
What I have working is pressing the btnCopyFromClipboard lets me draw the rectangle anywhere/everywhere (instead of limiting it to the Center Pane / the whiteboard). I want to restrict the rectangle to be drawn inside the Center Pane / the whiteboard only.
Some inputs/pointers from someone who has been through this will be very much appreciated.
http://docs.oracle.com/javafx/2/layout/builtin_layouts.htm was helpful to get me started.
I was in a fix as well and here is a question which I asked in Oracle Forums, to which, James gave me a vivid reply. Please go through this, it has your answer
https://community.oracle.com/thread/2598756