I have two Canvas instances in my SplitPane. When I drag the divider bar, one Canvas grows. When I drag the divider back, the Canvas doesn't really shrink. It holds the same maximum dimension as it was ever given by the resize. So I only see a part of that Canvas, however much space the SplitPane can show. Similar behavior occurs for the other Canvas. I need them to shrink back to fit into their respective sections when the divider is dragged, not just have them clipped.
The Canvases are wrapped in a one-element GridPane subclass before being added to the SplitPane. This wrapper class resizes the Canvas whenever the wrapper resizes.
The problem is that the SplitPane is giving the wrong sizes: whatever the maximum size ever was for that Canvas. Why would the SplitPane think this is the right thing to do?
package experimental;
import javafx.application.Application;
import javafx.geometry.Orientation;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.SplitPane;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
public class SplitPaneProblem extends Application {
static public void main(String[] args) {launch(args);}
public SplitPaneProblem() { }
#Override
public void start(Stage primaryStage) throws Exception {
SplitPane splitPane = new SplitPane();
splitPane.setOrientation(Orientation.VERTICAL);
splitPane.getItems().add(new TestPane("0",new Canvas()));
splitPane.getItems().add(new TestPane("1",new Canvas()));
primaryStage.setScene(new Scene(splitPane));
primaryStage.setHeight(400);
primaryStage.setWidth(400);
primaryStage.show();
}
public class TestPane extends AnchorPane {
String name;
Canvas canvas;
public TestPane(String name,Canvas canvas) {
super();
this.canvas = canvas;
getChildren().add(canvas);
//1 canvas.setManaged(false);
this.name = name;
}
#Override
public void resize(double width,double height) {
System.out.println(name+" input "+width+' '+height);
super.resize(width, height);
System.out.println(name+" panel "+getWidth()+' '+getHeight());
//0 canvas.setWidth (width);
//0 canvas.setHeight(height);
System.out.println(name+" canvas "+canvas.getWidth()+' '+canvas.getHeight());
}
}
}
Canvas doesn't resize by itself. Run the code as shown, and drag the divider up and down. Read the console output. The size of the Canvas instances never changes from (0,0).
Uncomment the lines beginning with //0 and run it again. Now the Canvas instances are forcibly resized to the size of their containing panes. Drag the divider up and down. Note that the Canvas instances grow vertically, but they never shrink, even though the code tells them to. Imagine we were drawing an X across the corners of the Canvases. These X drawings would grow with the divider. But when you drag the divider the other direction, the image is clipped, but it never shrinks.
Uncomment the line beginning with //1 and run it again. Now it works.
I don't know about you, but I think this is bizarre.
Based on this post from Jasper Potts, you can create a resizable canvas, and use it in your split pane.
class ResizableCanvas extends Pane {
private final int id;
private final Canvas canvas = new Canvas();
public ResizableCanvas(int id) {
this.id=id;
getChildren().add(canvas);
}
#Override
protected void layoutChildren() {
final int top = (int)snappedTopInset();
final int right = (int)snappedRightInset();
final int bottom = (int)snappedBottomInset();
final int left = (int)snappedLeftInset();
final int w = (int)getWidth() - left - right;
final int h = (int)getHeight() - top - bottom;
canvas.setLayoutX(left);
canvas.setLayoutY(top);
if (w != canvas.getWidth() || h != canvas.getHeight()) {
canvas.setWidth(w);
canvas.setHeight(h);
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.clearRect(0, 0, w, h);
if(id==1){
gc.setFill(Color.BLUE);
gc.fillOval(5, 5, w-10, h-10);
} else {
gc.setFill(Color.RED);
gc.fillRect(10, 10, w-20, h-20);
}
}
}
}
#Override
public void start(Stage stage) throws Exception {
ResizableCanvas canvas1 = new ResizableCanvas(1);
ResizableCanvas canvas2 = new ResizableCanvas(2);
SplitPane split = new SplitPane();
split.getItems().addAll(canvas1,canvas2);
stage.setScene(new Scene(split, 600, 400));
stage.show();
}
Note that everytime the scene is resized or the split content divider is moved, both canvases will be redrawn, but always with the exact size of their respective panes. As he also suggests, you may consider snapshot your canvas contents, and use an image to improve performance.
Related
I am encountering an issue with SplitPane dividers if the content has multiline FlowPane. There is no issue if the FlowPane rendered in one row. If the FlowPane has more than one row then there is a shift in the content part.
The more the no of rows, the greater the shift is.
To demonstrate the issue, below is quick a demo. The demo contains three vertical splitPanes, where each SplitPane has FlowPane with different no. of rows. (1st splitPane - 1row, 2nd SplitPane - 2rows, 3rd SplitPane - 3rows)
When resizing the splitPane with 1 FlowPane row, there is no issue, everything works fine. Whereas if I resize the second splitPane, the content is shifting from its desired place leaving a void space in SplitPane. When resizing the third splitPane, the space is even much bigger.
I believe this should be some issue in SplitPane-FlowPane calculations (Or I might be wrong as well). But at this stage rather than trying to figure the root cause (which will be somewhere inside JavaFX source code), I am more desperate in fixing this with some work around.
I tried few ways by binding the heights, setting some Region constants, etc. But none worked. All the height calculations of FlowPane are indeed correct.
Do any of you have any suggestions on how I can fix this.
Note: The issue can be reproduced in all versions of JavaFX
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SplitPane;
import javafx.scene.control.ToolBar;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
#SuppressWarnings("javadoc")
public class SplitPaneDividerIssueDemo extends Application {
/**
* FlowPane for debugging purpose.
*/
class SimpleFlowPane extends FlowPane {
#Override
protected double computeMaxHeight(final double width) {
final double height = super.computeMaxHeight(width);
// Debugging the first FlowPane in each SplitPane
if (isFirst()) {
System.out.println("Computed max height for " + getId() + " :: " + height);
}
return height;
}
#Override
protected double computeMinHeight(final double width) {
final double height = super.computeMinHeight(width);
if (isFirst()) {
System.out.println("Computed min height for " + getId() + " :: " + height);
}
return height;
}
#Override
protected double computePrefHeight(final double width) {
final double height = super.computePrefHeight(width);
if (isFirst()) {
System.out.println("Computed pref height for " + getId() + " :: " + height);
}
return height;
}
private boolean isFirst() {
return getId().endsWith("-1");
}
}
private int splitId = 1;
private int flowId = 1;
public static void main(final String... a) {
Application.launch(a);
}
#Override
public void start(final Stage primaryStage) throws Exception {
final HBox root = new HBox(buildSplitPane(10), buildSplitPane(20), buildSplitPane(30));
root.setSpacing(10);
final Scene scene = new Scene(root, 1250, 700);
primaryStage.setScene(scene);
primaryStage.setTitle("SplitPane Divider Issue");
primaryStage.show();
}
private VBox buildContent(final int count) {
final Button button = new Button("Button");
final FlowPane flowPane = new SimpleFlowPane();
flowPane.setId("flow-" + splitId + "-" + flowId);
flowPane.setVgap(5);
flowPane.setHgap(5);
for (int i = 0; i < count; i++) {
flowPane.getChildren().add(new Button("" + i));
}
final ScrollPane scroll = new ScrollPane();
VBox.setVgrow(scroll, Priority.ALWAYS);
final ToolBar toolBar = new ToolBar();
toolBar.getItems().add(new Button("Test"));
final VBox pane = new VBox();
pane.setPadding(new Insets(5));
pane.setSpacing(5);
pane.setStyle("-fx-background-color:yellow;-fx-border-width:1px;-fx-border-color:red;");
pane.getChildren().addAll(button, flowPane, scroll, toolBar);
pane.parentProperty().addListener((obs,old,content)->{
if(content!=null){
content.layoutYProperty().addListener((obs1,old1,layoutY)->{
System.out.println("LayoutY of content having "+flowPane.getId()+" :: "+layoutY);
});
}
});
flowId++;
return pane;
}
private SplitPane buildSplitPane(final int count) {
final SplitPane splitPane = new SplitPane();
splitPane.setStyle("-fx-background-color:green;");
splitPane.setOrientation(Orientation.VERTICAL);
splitPane.setDividerPositions(.36, .70);
splitPane.getItems().addAll(buildContent(count), buildContent(count), buildContent(count));
HBox.setHgrow(splitPane, Priority.ALWAYS);
splitId++;
flowId = 1;
return splitPane;
}
}
The problem is within the minHeight of a FlowPane since it is oriented horizontally making that minHeight very dynamic. It appears to be designed where the minHeight is changed as it grows and shrinks in width. When you condense the parent vertically, the VBox calculates its minHeight as the "top/bottom insets plus the sum of each child's min height plus spacing between each child" according to the docs. Apparently there is some problem where a FlowPane's parent cannot account for its minHeight.
An HBox calculates its minHeight as the "top/bottom insets plus the largest of the children's min heights." So, if you wrap the FlowPane in an HBox, that HBox minHeight will be bound to the height of the FlowPane, and then place that HBox in the VBox where the FlowPane should be.
HBox flowPaneContainer = new HBox();
flowPaneContainer.getChildren().add(flowPane);
pane.getChildren().addAll(button, flowPaneContainer, scroll, toolBar);
EDIT: This is fine if your stage size is fixed. If your application is resizable, then more will have to be done because the flowPane minHeight will change, changing the HBox minHeight, and will then result in the same problem because there won't be enough room for everything inside every VBox.
With resizable apps, I normally handle this by wrapping each section of a SplitPane in a ScrollPane.
Creating new components in JavaFX is still a but muddy to me compared to "Everything is a JPanel" in Swing.
I'm trying to make a fixed size component. I hesitate to call it a control, it's a pane of activity, not a button.
But here's my problem.
The fixed size I want is smaller than the contents of the element.
The grid is, in truth, 200x200. I'm shifting it up and left 25x25, and I'm trying to make the fixed size of 150x150. You can see in my example I've tried assorted ways of forcing it to 150, but in my tests, the size never sticks. Also, to be clear, I would expect the lines to clip at the boundary of the component.
This is, roughly, what I'm shooting for in my contrived case (note this looks bigger than 150x150 because of the retina display on my Mac, which doubles everything):
I've put some in to a FlowPane, and they stack right up, but ignore the 150x150 dimensions.
FlowPane fp = new FlowPane(new TestPane(), new TestPane(), new TestPane());
var scene = new Scene(fp, 640, 480);
stage.setScene(scene);
I tried sticking one in a ScrollPane, and the scroll bars never appear, even after resizing the window.
TestPane pane = new TestPane();
ScrollPane sp = new ScrollPane(pane);
var scene = new Scene(sp, 640, 480);
stage.setScene(scene);
And I struggle to discern whether I should be extending Region or Control in these cases.
I am missing something fundamental.
package pkg;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.shape.Line;
import javafx.scene.transform.Translate;
public class TestPane extends Control {
public TestPane() {
setMinHeight(150);
setMaxHeight(150);
setMinWidth(150);
setMaxWidth(150);
setPrefHeight(150);
setPrefWidth(150);
populate();
}
#Override
protected double computePrefHeight(double width) {
return 150;
}
#Override
protected double computePrefWidth(double height) {
return 150;
}
#Override
protected double computeMaxHeight(double width) {
return 150;
}
#Override
protected double computeMaxWidth(double height) {
return 150;
}
#Override
protected double computeMinHeight(double width) {
return 150;
}
#Override
protected double computeMinWidth(double height) {
return 150;
}
#Override
public boolean isResizable() {
return false;
}
private void populate() {
Translate translate = new Translate();
translate.setX(-25);
translate.setY(-25);
getTransforms().clear();
getTransforms().addAll(translate);
ObservableList<Node> children = getChildren();
for (int i = 0; i < 4; i++) {
Line line = new Line(0, i * 50, 200, i * 50);
children.add(line);
line = new Line(i * 50, 0, i * 50, 200);
children.add(line);
}
}
}
Addenda, to clarify.
I want a fixed sized component. It's a rectangle. I want it X x Y big.
I want to draw things in my box. Lines, circles, text.
I want the things I draw to clip to the boundaries of the component.
I don't want to use Canvas.
More addenda.
What I'm looking for is not much different from what a ScrollPane does, save I don't want any scroll bars, and I don't want the size of the outlying pane to grow or shrink.
TLDR:
Subclass Region,
make isResizable() return true to respect pref, min, and max sizes,
explicitly set a clip to avoid painting outside the local bounds.
Most of the documentation for this is in the package documentation for javafx.scene.layout
First, note the distinction between resizable and non-resizable nodes. Resizable nodes (for which isResizable() returns true) are resized by their parent during layout, and the parent will make a best-effort to respect their preferred, minimum, and maximum sizes.
Non-resizable nodes are not resized by their parent. If isResizable() returns false, then resize() is a no-op and the preferred, minimum, and maximum sizes are effectively ignored. Their sizes are computed internally and reported to the parent via its visual bounds. Ultimately, all JavaFX nodes have a peer node in the underlying graphical system, and AFAIK the only way a non-resizable node can determine its size is by directly setting the size of the peer. (I'm happy to be corrected on this.)
So unless you want to get your hands really dirty with custom peer nodes (and I don't even know if the API has mechanisms for this), I think the preferred way to create a "fixed size node" is by creating a resizable node with preferred, minimum, and maximum sizes all set to the same value. This is likely by design: as noted in a comment to your question, fixed-size nodes in layout-driven UI toolkits are generally discouraged, other than very low-level components (Text, Shape, etc).
Transformations applied to resizable nodes are generally applied after layout (i.e. they don't affect the layout bounds). Therefore using a translation to manage the internal positioning of the child nodes is not a good approach; it will have effects on the layout of the custom node in the parent which you probably don't intend.
As you note, you are not really defining a control here; it has no behavior or skin. Thus subclassing Control is not really the rigth approach. The most appropriate hook in the API is to subclass Region. Override the layoutChildren() method to position the child nodes (for Shapes and Text nodes, set their coordinates, for resizable children call resizeRelocate(...)).
Finally, to prevent the node spilling out of its intended bounds (150x150 in your example), either ensure no child nodes are positioned outside those bounds, or explicitly set the clip.
Here's a refactoring of your example:
import javafx.scene.layout.Region;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
public class TestPane extends Region {
private Line[] verticalLines ;
private Line[] horizontalLines ;
private static final int WIDTH = 150 ;
private static final int HEIGHT = 150 ;
private static final int LINE_GAP = 50 ;
public TestPane() {
populate();
}
#Override
protected double computePrefHeight(double width) {
return HEIGHT;
}
#Override
protected double computePrefWidth(double height) {
return HEIGHT;
}
#Override
protected double computeMaxHeight(double width) {
return HEIGHT;
}
#Override
protected double computeMaxWidth(double height) {
return WIDTH;
}
#Override
protected double computeMinHeight(double width) {
return WIDTH;
}
#Override
protected double computeMinWidth(double height) {
return WIDTH;
}
#Override
public boolean isResizable() {
return true;
}
#Override
public void layoutChildren() {
double w = getWidth();
double h = getHeight() ;
double actualWidth = verticalLines.length * LINE_GAP ;
double actualHeight = horizontalLines.length * LINE_GAP ;
double hOffset = (actualWidth - w) / 2 ;
double vOffset = (actualHeight - h) / 2 ;
for (int i = 0 ; i < verticalLines.length ; i++) {
double x = i * LINE_GAP - hOffset;
verticalLines[i].setStartX(x);
verticalLines[i].setEndX(x);
verticalLines[i].setStartY(0);
verticalLines[i].setEndY(h);
}
for (int i = 0 ; i < horizontalLines.length ; i++) {
double y = i * LINE_GAP - vOffset;
horizontalLines[i].setStartY(y);
horizontalLines[i].setEndY(y);
horizontalLines[i].setStartX(0);
horizontalLines[i].setEndX(w);
}
setClip(new Rectangle(0, 0, w, h));
}
private void populate() {
verticalLines = new Line[4] ;
horizontalLines = new Line[4] ;
for (int i = 0 ; i <verticalLines.length ; i++) {
verticalLines[i] = new Line();
getChildren().add(verticalLines[i]);
}
for (int i = 0 ; i <horizontalLines.length ; i++) {
horizontalLines[i] = new Line();
getChildren().add(horizontalLines[i]);
}
}
}
A more sophisticated example might have, for example, LINE_GAP as a property. When that property changes you would call requestLayout() to mark the component as "dirty", so its layoutChildren() method would be called again on the next frame rendered.
Here's a quick test case:
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
public class App extends Application {
#Override
public void start(Stage stage) {
FlowPane root = new FlowPane();
root.setAlignment(Pos.TOP_LEFT);
root.setPadding(new Insets(10));
root.setHgap(5);
root.setVgap(5);
for (int i = 0; i < 6 ; i++) {
root.getChildren().add(new TestPane());
}
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
Which results in:
This plays nicely with the layout pane; resizing the window gives
In JavaFX I am trying to show an rotated ImageView in an Application Window.
Therefore I have put it into a stackPane to have it always centered and I have bound the widths/heights of the ImageView and the stackPane to the scene's width/height to view it just as large as possible.
This works fine as soon as the Image is not rotated.
As soon as I rotate the Image by 90° using stackPane.setRotate(90) (and exchange binding for width/height) then the stackPane is no longer bound to the upper left corner of the Application Window (or scene).
What can I do to place the rotated image correctly?
In the example code [any key] will toggle the rotation 90°/0° so the location problem of the rotated image becomes visible:
public class RotationTest extends Application {
boolean rotated = false;
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("Rotation test");
Group root = new Group();
Scene scene = new Scene(root, 1024,768);
//a stackPane is used to center the image
StackPane stackPane = new StackPane();
stackPane.setStyle("-fx-background-color: black;");
stackPane.prefHeightProperty().bind(scene.heightProperty());
stackPane.prefWidthProperty().bind(scene.widthProperty());
scene.setOnKeyPressed(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent event) {
//toggle rotate 90° / no rotation
rotated = !rotated;
stackPane.prefHeightProperty().unbind();
stackPane.prefWidthProperty().unbind();
if (rotated){
stackPane.setRotate(90);
//rotation: exchange width and height for binding to scene
stackPane.prefWidthProperty().bind(scene.heightProperty());
stackPane.prefHeightProperty().bind(scene.widthProperty());
}else{
stackPane.setRotate(0);
//no rotation: height is height and width is width
stackPane.prefHeightProperty().bind(scene.heightProperty());
stackPane.prefWidthProperty().bind(scene.widthProperty());
}
}
});
final ImageView imageView = new ImageView("file:D:/test.jpg");
imageView.setPreserveRatio(true);
imageView.fitWidthProperty().bind(stackPane.prefWidthProperty());
imageView.fitHeightProperty().bind(stackPane.prefHeightProperty());
stackPane.getChildren().add(imageView);
root.getChildren().add(stackPane);
primaryStage.setScene(scene);
primaryStage.show();
}
}
Results:
Without rotation the stackPane (black) fits the window perfectly and the image has the correct size even if the window is resized with the mouse.
After pressing [any key] the stackPane is rotated. The stackPane (black) seems to have the correct width/height and also the image seems to be correctly rotated. But the stackPane is no longer in the upper left corner??? It moves around when the window is resized with the mouse???
Why not simply leave the Group and the preferred sizes out of the equation?
The root is automatically resized to fit the scene and you can use it's width/height properties to bind the fitWidth and fitHeight properties:
private static void setRotated(boolean rotated, ImageView targetNode, Pane parent) {
double angle;
if (rotated) {
angle = 90;
targetNode.fitWidthProperty().bind(parent.heightProperty());
targetNode.fitHeightProperty().bind(parent.widthProperty());
} else {
angle = 0;
targetNode.fitWidthProperty().bind(parent.widthProperty());
targetNode.fitHeightProperty().bind(parent.heightProperty());
}
targetNode.setRotate(angle);
}
#Override
public void start(Stage primaryStage) {
Image image = new Image("file:D:/test.jpg");
ImageView imageView = new ImageView(image);
imageView.setPreserveRatio(true);
StackPane root = new StackPane(imageView);
root.setStyle("-fx-background-color: black;");
// initialize unrotated
setRotated(false, imageView, root);
Scene scene = new Scene(root, 1024, 768);
scene.setOnKeyPressed(evt -> {
// toggle between 0° and 90° rotation
setRotated(imageView.getRotate() == 0, imageView, root);
});
primaryStage.setScene(scene);
primaryStage.show();
}
Note that this may not result in correct layout, if placed in some other layout, since the size constraints may be calculated wrong.
You could implement your own region though to fix this:
public class CenteredImage extends Region {
private final BooleanProperty rotated = new SimpleBooleanProperty();
private final ImageView imageView = new ImageView();
public CenteredImage() {
// make sure layout gets invalidated when the image changes
InvalidationListener listener = o -> requestLayout();
imageProperty().addListener(listener);
rotated.addListener((o, oldValue, newValue) -> {
imageView.setRotate(newValue ? 90 : 0);
requestLayout();
});
getChildren().add(imageView);
imageView.setPreserveRatio(true);
}
public final BooleanProperty rotatedProperty() {
return rotated;
}
public final void setRotated(boolean value) {
this.rotated.set(value);
}
public boolean isRotated() {
return rotated.get();
}
public final void setImage(Image value) {
imageView.setImage(value);
}
public final Image getImage() {
return imageView.getImage();
}
public final ObjectProperty<Image> imageProperty() {
return imageView.imageProperty();
}
#Override
protected double computeMinWidth(double height) {
return 0;
}
#Override
protected double computeMinHeight(double width) {
return 0;
}
#Override
protected double computePrefWidth(double height) {
Image image = getImage();
Insets insets = getInsets();
double add = 0;
if (image != null && height > 0) {
height -= insets.getBottom() + insets.getTop();
add = isRotated()
? height / image.getWidth() * image.getHeight()
: height / image.getHeight() * image.getWidth();
}
return insets.getLeft() + insets.getRight() + add;
}
#Override
protected double computePrefHeight(double width) {
Image image = getImage();
Insets insets = getInsets();
double add = 0;
if (image != null && width > 0) {
width -= insets.getLeft() + insets.getRight();
add = isRotated()
? width / image.getHeight() * image.getWidth()
: width / image.getWidth() * image.getHeight();
}
return insets.getTop() + insets.getBottom() + add;
}
#Override
protected double computeMaxWidth(double height) {
return Double.MAX_VALUE;
}
#Override
protected double computeMaxHeight(double width) {
return Double.MAX_VALUE;
}
#Override
protected void layoutChildren() {
Insets insets = getInsets();
double left = insets.getLeft();
double top = insets.getTop();
double availableWidth = getWidth() - left - insets.getRight();
double availableHeight = getHeight() - top - insets.getBottom();
// set fit sizes
if (isRotated()) {
imageView.setFitWidth(availableHeight);
imageView.setFitHeight(availableWidth);
} else {
imageView.setFitWidth(availableWidth);
imageView.setFitHeight(availableHeight);
}
// place image
layoutInArea(imageView, left, top, availableWidth, availableHeight, 0, null, false,
false, HPos.CENTER, VPos.CENTER);
}
}
#Override
public void start(Stage primaryStage) {
Image image = new Image("file:D:/test.jpg");
ImageView imageView = new ImageView(image);
imageView.setPreserveRatio(true);
CenteredImage imageArea = new CenteredImage();
imageArea.setImage(image);
imageArea.setStyle("-fx-background-color: black;");
imageArea.setPrefWidth(300);
SplitPane splitPane = new SplitPane(new Region(), imageArea);
SplitPane.setResizableWithParent(imageArea, true);
Scene scene = new Scene(splitPane, 1024, 768);
scene.setOnKeyPressed(evt -> {
// toggle between 0° and 90° rotation
imageArea.setRotated(!imageArea.isRotated());
});
primaryStage.setScene(scene);
primaryStage.show();
}
I found a solution :-) Fabian's approach inspired me (thank you!!) And my old friend Pit helped me with debugging (also thank you!!)
It seems that the layout location algorithm of JavaFX has a problem when resize() is applied to rotated Panes (or even Nodes - I have not tried):
Following Fabian's idea I debugged into the layoutChildren() method of class Pane. I found that the relocation after setRotate() is correct and keeps the center of the child pane as expected. But as soon as resize() is called (which is done because of fitting the rotated child pane again into its father and additionally always when the window is resized by the user) the origin calculation goes wrong:
The picture above depicts a sequence of setRotate(90), resize() and relocate() in green and the same for setRotate(270) in blue. A little blue/green circle depicts the corresponding origin together with its coordinates in the 1024x786 example.
Analysis
It seems that for calculation the position of the Pane resize() does not use the height and width from BoundsInParent-Property (see JavaFX-Docu of Node) but from getWidth() and getHeight() which seem to reflect BoundsInLocal. As a consequence, for rotations of 90° or 270° height and width seem to be interchanged. Therefore the error in the calculation for the new origin is just the half of the difference between width and height (delta=(width-height)/2) when resize() tries to center the child pane again after the resizing.
Solution
A relocation(delta,-delta) needs to be applied after resizing for Panes with rotation=90 or 270 degrees.
The structure of my implementation follows Fabian's basic idea: I have build a layouter RotatablePaneLayouter:Region that just overwrites the layoutChildren() method. In its constructor it gets a Pane (in my example a StackPane) which can contain any number of children (in my example an ImageView) and that can be rotated.
LayoutChildren() then just executes resize() and relocate() for the child pane to fit it completely into the RotateablePaneLayouter respecting the orientation of the child pane.
The Layouter Helper (RotateablePaneLayouter:Region)
public class RotatablePaneLayouter extends Region {
private Pane child;
public RotatablePaneLayouter(Pane child) {
getChildren().add(child);
this.child = child;
// make sure layout gets invalidated when the child orientation changes
child.rotateProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
requestLayout();
}
});
}
#Override
protected void layoutChildren() {
// set fit sizes:
//resize child to fit into RotatablePane and correct movement caused by resizing if necessary
if ((child.getRotate() == 90)||(child.getRotate() == 270)) {
//vertical
child.resize( getHeight(), getWidth() ); //exchange width and height
// and relocate to correct movement caused by resizing
double delta = (getWidth() - getHeight()) / 2;
child.relocate(delta,-delta);
} else {
//horizontal
child.resize( getWidth(), getHeight() ); //keep width and height
//with 0° or 180° resize does no movement to be corrected
child.relocate(0,0);
}
}
}
To use it: Place the Pane to be rotated into the Layouter first instead of placing the Pane directly.
Here the code for the example's main program. You can use the space bar to rotate the child pane by 90, 180, 270 and again 0 degrees. You can also resize the window with the mouse. The layouter always manages to place the rotated pane correctly.
Expample for using the Layouter
public class RotationTest extends Application {
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage primaryStage) {
//image in a StackPane to be rotated
final ImageView imageView = new ImageView("file:D:/Test_org.jpg");
imageView.setPreserveRatio(true);
StackPane stackPane = new StackPane(imageView); //a stackPane is used to center the image
stackPane.setStyle("-fx-background-color: black;");
imageView.fitWidthProperty().bind(stackPane.widthProperty());
imageView.fitHeightProperty().bind(stackPane.heightProperty());
//container for layouting rotated Panes
RotatablePaneLayouter root = new RotatablePaneLayouter(stackPane);
root.setStyle("-fx-background-color: blue;");
Scene scene = new Scene(root, 1024,768);
scene.setOnKeyPressed(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent event) {
if (event.getCode() == KeyCode.SPACE) {
//rotate additionally 90°
stackPane.setRotate((stackPane.getRotate() + 90) % 360);
}
}
});
primaryStage.setTitle("Rotation test");
primaryStage.setScene(scene);
primaryStage.show();
}
}
For me this seems like a workaround of a javaFX bug in resize().
for example:
public class WebViewTest extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
final WebView view = new WebView();
final WebEngine webEngine = view.getEngine();
Scene scene = new Scene(view, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
Platform.runLater(() -> {
webEngine.getLoadWorker().progressProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
if (newValue.doubleValue() == 1D) {
String heightText = webEngine.executeScript(
"window.getComputedStyle(document.body, null).getPropertyValue('height')"
).toString();
double height = Double.valueOf(heightText.replace("px", ""));
String widthText = webEngine.executeScript(
"window.getComputedStyle(document.body, null).getPropertyValue('width')"
).toString();
double width = Double.valueOf(widthText.replace("px", ""));
System.out.println(width + "*" + height);
primaryStage.setWidth(width);
primaryStage.setHeight(height);
}
}
});
webEngine.load("http://www.baidu.com/");
});
}
public static void main(String[] args) {
launch(args);
}
}
I want to resize the primaryStage after loading. But finally, I get the size is 586*586, and the primaryStage shows like this:
enter image description here
Actually, I don't want the rolling style, so how can I remove the scroll bar? If I use primaryStage.setWidth() or primaryStage.setHeight() to set the size of primaryStage very big at the beginning, the scroll bar will not exist. But that not I need, I want to resize the size dynamically, because the url will change.
This is similar to the solution given by RKJ (relies on querying WebView for the document width and height).
This solution adds a couple of things:
Ability to completely remove WebView scroll bars at all times (you may or may not want this as it stops the user being able to scroll large documents or view complete documents if the user manually makes the window smaller).
A call to stage.sizeToScene() to size the stage precisely to the scene size.
The behavior of this solution is kind of weird due to some implementation details of WebView. WebView does not load the document unless it is displayed on the stage, so you can't know the document size until you try to display it. So you need to display the document, then resize the stage to fit the document, which results in a delay after the stage has been initially shown and when it resizes to exactly fit the document. This provides, for certain documents, a visible jump in the stage size which just looks weird. Also documents larger than the screen size (which are common on the web) cannot be displayed in full as the stage can only maximally resize to fill the available screen real estate and without any scroll bars you can't see part of the document. So in all, I don't think this solution is really useful.
no-overflow.css
body {
overflow-x: hidden;
overflow-y: hidden;
}
WebViewTest.java
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
public class WebViewTest extends Application {
#Override
public void start(Stage stage) throws Exception {
final WebView view = new WebView();
view.getEngine().setUserStyleSheetLocation(
getClass().getResource("no-overflow.css").toExternalForm()
);
final WebEngine webEngine = view.getEngine();
webEngine.getLoadWorker().runningProperty().addListener((observable, oldValue, newValue) -> {
System.out.println("Running: " + newValue);
if (!newValue) {
String heightText = webEngine.executeScript(
"document.height"
).toString();
double height = Double.valueOf(heightText.replace("px", ""));
String widthText = webEngine.executeScript(
"document.width"
).toString();
double width = Double.valueOf(widthText.replace("px", ""));
System.out.println(width + "*" + height);
view.setMinSize(width, height);
view.setPrefSize(width, height);
view.setMaxSize(width, height);
stage.sizeToScene();
System.out.println(view.getLayoutBounds());
}
});
webEngine.load("http://www.baidu.com");
Scene scene = new Scene(view);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
public class WebViewTest extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
final WebView view = new WebView();
final WebEngine webEngine = view.getEngine();
Scene scene = new Scene(view, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
Platform.runLater(() -> {
webEngine.getLoadWorker().progressProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
if (newValue.doubleValue() == 1D) {
String heightText = webEngine.executeScript("document.height").toString();
double height = Double.valueOf(heightText.replace("px", ""));
String widthText = webEngine.executeScript("document.width").toString();
double width = Double.valueOf(widthText.replace("px", ""));
System.out.println(width + "*" + height);
primaryStage.setWidth(width+50);
primaryStage.setHeight(height+50);
primaryStage.hide();
primaryStage.show();
}
}
});
webEngine.load("http://baidu.com/");
});
}
public static void main(String[] args) {
launch(args);
}
}
use document.height and document.width to get the actual dimension, there is slight difference between the pixel size and stage size measurement so, I added 50 pixel extra and hide the stage and show it again but it is more better if you use WebView inside StackPane Container.
rkjoshi
I am trying to wrap my head around Scroll- and Tilepanes atm, and I have come upon an issue I just cant solve without a dirty hack.
I have a horizontal TilePane that has 8 Tiles, and I set it to have 4 columns, resulting in 2 rows with 4 tiles.
That TilePane I put in an HBox, since if I put it in a StackPane it would stretch the size of the tilepane making my colum setting void. A bit weird that setting the prefColumns/Rows recalculates the size of the TilePane, rather than trying to set the actual amounts of columns/rows, feels more like a dirty hack.
Anyway, putting the HBox directly into the ScrollPane would not work either, since the Scrollbars would not appear even after the 2nd row of tiles would get cut off. Setting that HBox again in a Stackpane which I then put in a ScrollPane does the trick. Atleast until I resize the width of the window to be so small the tilepane has to align the tiles anew and a 3rd or more rows appear.
Here is the basic programm:
public class Main extends Application {
#Override
public void start(Stage stage) {
TilePane tilePane = new TilePane();
tilePane.setPadding(new Insets(5));
tilePane.setVgap(4);
tilePane.setHgap(4);
tilePane.setPrefColumns(4);
tilePane.setStyle("-fx-background-color: lightblue;");
HBox tiles[] = new HBox[8];
for (int i = 0; i < 8; i++) {
tiles[i] = new HBox(new Label("This is node #" + i));
tiles[i].setStyle("-fx-border-color: black;");
tiles[i].setPadding(new Insets(50));
tilePane.getChildren().add(tiles[i]);
}
HBox hbox = new HBox();
hbox.setAlignment(Pos.CENTER);
hbox.setStyle("-fx-background-color: blue;");
hbox.getChildren().add(tilePane);
StackPane stack = new StackPane();
stack.getChildren().add(hbox);
ScrollPane sp = new ScrollPane();
sp.setFitToHeight(true);
sp.setFitToWidth(true);
sp.setContent(stack);
stage.setScene(new Scene(sp, 800, 600));
stage.show();
}
public static void main(String[] args) {
launch();
}
}
I managed to achieve my wanted behaviour, but its more of a really dirty hack. I added a listener to the height and width of my HBox containing the TilePane and assumed that when the height changes its because the width got so small that a column was removed and a new row added. To be able to do that I put the HBox in a VBox so that it would not grow withe the height of the ScrollPane. For the width I simply calculated if there is space to display another colum (up to 4), to do it.
Here are the changes:
public class Main extends Application {
private boolean notFirstPassHeight;
private boolean notFirstPassWidth;
#Override
public void start(Stage stage) {
TilePane tilePane = new TilePane();
tilePane.setPadding(new Insets(5));
tilePane.setVgap(4);
tilePane.setHgap(4);
tilePane.setPrefColumns(4);
tilePane.setStyle("-fx-background-color: lightblue;");
// I took the value from ScenicView
tilePane.prefTileWidthProperty().set(182);
HBox tiles[] = new HBox[8];
for (int i = 0; i < 8; i++) {
tiles[i] = new HBox(new Label("This is node #" + i));
tiles[i].setStyle("-fx-border-color: black;");
tiles[i].setPadding(new Insets(50));
tilePane.getChildren().add(tiles[i]);
}
ScrollPane sp = new ScrollPane();
sp.setFitToHeight(true);
sp.setFitToWidth(true);
StackPane stack = new StackPane();
VBox vbox = new VBox();
vbox.setStyle("-fx-background-color: red");
HBox hbox = new HBox();
hbox.setAlignment(Pos.CENTER);
hbox.setStyle("-fx-background-color: blue;");
hbox.getChildren().add(tilePane);
notFirstPassHeight = false;
notFirstPassWidth = false;
hbox.heightProperty().addListener((observable, oldValue, newValue) -> {
if (oldValue.doubleValue() < newValue.doubleValue() && notFirstPassHeight) {
tilePane.setPrefColumns(tilePane.getPrefColumns() - 1);
stack.requestLayout();
}
notFirstPassHeight = true;
});
hbox.widthProperty().addListener((observable, oldValue, newValue) -> {
if (oldValue.doubleValue() < newValue.doubleValue() && notFirstPassWidth && tilePane.getPrefColumns() <= 3
&& (newValue.doubleValue() / (tilePane.getPrefColumns() + 1)) > tilePane.getPrefTileWidth()) {
tilePane.setPrefColumns(tilePane.getPrefColumns() + 1);
stack.requestLayout();
}
notFirstPassWidth = true;
});
vbox.getChildren().add(hbox);
stack.getChildren().add(vbox);
sp.setContent(stack);
stage.setScene(new Scene(sp, 800, 600));
stage.show();
}
public static void main(String[] args) {
launch();
}
}
However this approach requires me to
1.Know the Width of the Tiles in the Tilepane.
2.Consider Padding and Gap between tiles for my calculation to be accurate, which I dont do in my example.
And its just not a good approach at any rate if you ask me. Too complicated a process for such a basic thing. There has to be a way better and simple way to accomplish complete resizability and the wanted behaviour with TilePanes in a ScrollPane.
Setting the preferred number of columns and/or rows in the TilePane determines the calculation for the prefWidth and prefHeight values for that tile pane. If you want to force a maximum number of columns, you just need to make the maxWidth equal to the computed prefWidth: you can do this with
tilePane.setMaxWidth(Region.USE_PREF_SIZE);
This means that (as long as the tile pane is placed in something that manages layout), it will never be wider than the pref width, which is computed to allow the preferred number of columns. It may, of course, be smaller than that. (Note you could use the same trick with setMinWidth if you needed a minimum number of columns, rather than a maximum number of columns.)
The scroll pane's fitToHeight and fitToWidth properties will, when true, attempt to resize the height (respectively width) of the content to be equal to the height (width) of the scroll pane's viewport. These operations will take precedence over the preferred height (width) of the content, but will attempt to respect the minimum height (width).
Consequently, it's usually a mistake to call both setFitToWidth(true) and setFitToHeight(true), as this will almost always turn off scrolling completely (just forcing the content to be the same size as the scroll pane's viewport).
So here you want to make the max width of the tile pane respect the pref width, and fix the width of the tile pane to be the width of the scroll pane's viewport (so that when you shrink the width of the window, it shrinks the width of the viewport and creates more columns). This will add a vertical scrollbar if the number of rows grows large enough, and only add a horizontal scrollbar if the viewport shrinks horizontally below the minimum width of the tile pane (which is computed as the minimum of the preferred widths of all the nodes it contains).
I think the following version of your original code does essentially what you are looking for:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.TilePane;
import javafx.stage.Stage;
public class ScrollingTilePane extends Application {
#Override
public void start(Stage stage) {
TilePane tilePane = new TilePane();
tilePane.setPadding(new Insets(5));
tilePane.setVgap(4);
tilePane.setHgap(4);
tilePane.setPrefColumns(4);
tilePane.setStyle("-fx-background-color: lightblue;");
// dont grow more than the preferred number of columns:
tilePane.setMaxWidth(Region.USE_PREF_SIZE);
HBox tiles[] = new HBox[8];
for (int i = 0; i < 8; i++) {
tiles[i] = new HBox(new Label("This is node #" + i));
tiles[i].setStyle("-fx-border-color: black;");
tiles[i].setPadding(new Insets(50));
tilePane.getChildren().add(tiles[i]);
}
HBox hbox = new HBox();
hbox.setAlignment(Pos.CENTER);
hbox.setStyle("-fx-background-color: blue;");
hbox.getChildren().add(tilePane);
// StackPane stack = new StackPane();
// stack.getChildren().add(tilePane);
// stack.setStyle("-fx-background-color: blue;");
ScrollPane sp = new ScrollPane();
// sp.setFitToHeight(true);
sp.setFitToWidth(true);
sp.setContent(hbox);
stage.setScene(new Scene(sp, 800, 600));
stage.show();
}
public static void main(String[] args) {
launch();
}
}
Note that if you need to change the background color of the space outside the scroll pane's content, you can use the following in an external style sheet:
.scroll-pane .viewport {
-fx-background-color: red ;
}