JavaFX scale ScrollPane content centered - javafx

My goal is to have the ScrollPane with some content inside (vbox with some controls) scalable, so that when the scaled content reaches visible bounds, ScrollPane shows horizontal and vertical scroll bars. But there's a catch - in order to do so, I must wrap my content in a Group and by doing so, the scaling is not from the center but rather from the top left corner (0,0) of the ScrollPane, while without wrapping the content of the Group makes the scaling from the center but no scroll bars show. Here is the test application:
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Slider;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class ZoomScrollExample extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
Pane root = new Pane();
Scene scene = new Scene(root, 450, 450, Color.WHITE);
stage.setScene(scene);
VBox layout = new VBox();
layout.setMinHeight(400);
layout.setMinWidth(400);
Rectangle r = new Rectangle(200, 200);
r.setStroke(Color.RED);
r.setFill(Color.WHITE);
layout.getChildren().add(r);
layout.setAlignment(Pos.CENTER);
ScrollPane sp = new ScrollPane();
//comment this to get no scroll bars but scaling is centered
Group g = new Group(layout);
sp.setContent(g);
//uncomment this
//sp.setContent(layout);
sp.setPrefSize(450, 450);
Slider s = new Slider();
root.getChildren().add(sp);
root.getChildren().add(s);
s.setMin(100);
s.setMax(200);
// actual scaling
layout.scaleXProperty()
.bind(s.valueProperty().divide(100));
layout.scaleYProperty()
.bind(s.valueProperty().divide(100));
stage.show();
}
}
Is there any (easy) way to have both scroll bars and centered scaling?

Related

JavaFX ScrollPane and Scaling of the Content

I would like to show a photo as an ImageView in a ScrollPane with an ZoomIn and ZoomOut Function. But if I reduce by means of scale the imageview, an undesirable empty edge is created in the ScrollPane. How can you make sure that the ScrollPane is always the size of the scaled ImageView?
See the following example. For simplicity, I replaced the ImageView with a rectangle.
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class ScrollPaneDemo extends Application {
double scale;
Pane contPane = new Pane();
#Override
public void start(Stage primaryStage) {
BorderPane pane = new BorderPane();
ScrollPane sp = new ScrollPane();
sp.setContent(contPane);
sp.setVvalue(0.5);
sp.setHvalue(0.5);
Rectangle rec = new Rectangle(2820, 1240,Color.RED);
scale = 0.2;
contPane.setScaleX(scale);
contPane.setScaleY(scale);
contPane.getChildren().add(rec);
Button but1 = new Button("+");
but1.setOnAction((ActionEvent event) -> {
scale*=2;
contPane.setScaleX(scale);
contPane.setScaleY(scale);
});
Button but2 = new Button("-");
but2.setOnAction((ActionEvent event) -> {
scale/=2;
contPane.setScaleX(scale);
contPane.setScaleY(scale);
});
HBox buttons = new HBox(but1, but2);
pane.setTop(buttons);
pane.setCenter(sp);
Scene scene = new Scene(pane, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
contPane scaled by using transform don't change its layoutBounds automatically. If you want not to make empty space in contPane, you'd better wrap the node in Group.
See this post. Layout using the transformed bounds
sp.setContent(new Group(contPane));
In addition, if you don't want to make empty space in ScrollPane, limit minimum scale to rate which width or height of the content fits viewport's one.
Button but1 = new Button("+");
but1.setOnAction((ActionEvent event) -> {
updateScale(scale * 2.0d);
});
Button but2 = new Button("-");
but2.setOnAction((ActionEvent event) -> {
updateScale(scale / 2.0d);
});
HBox buttons = new HBox(but1, but2);
pane.setTop(buttons);
pane.setCenter(sp);
Scene scene = new Scene(pane, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
updateScale(0.2d);
private void updateScale(double newScale) {
scale = Math.max(newScale, Math.max(sp.getViewportBounds().getWidth() / rec.getWidth(), sp.getViewportBounds().getHeight() / rec.getHeight()));
contPane.setScaleX(scale);
contPane.setScaleY(scale);
}
Consider a case of the image is smaller than ScrollPane's viewport. Because for showing no empty space, this code will stretch contents when it doesn't have enough size.
In a case of huge images, TravisF's comment helps you.

Java FX, weird behavior from Label

I have managed to fix the below issue by using the Text class instead of Label, however I would still like to understand the behavior.
I am trying to create a pane with an image and some text below that image.
I want both the text and the image to be centered inside of the pane.
I also want the text to stick to the bottom of the pane while the image to stick to the top of the pane.
Below is the rough code that achieves what I want. The problem is, If I run the code, the image comes out a little off center. The label also doesn't appear.
If I resize the stage with my mouse, the layout seems to fix itself. The image becomes fully centered and the text appears. Please explain, is this some kind of a bug?
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import static javafx.beans.binding.Bindings.subtract;
public class TestApp extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
AnchorPane pane = new AnchorPane();
BorderPane borderPane = new BorderPane();
ImageView imageView = new ImageView();
Label label = new Label("some text");
AnchorPane.setBottomAnchor(borderPane, 0.0);
AnchorPane.setTopAnchor(borderPane, 0.0);
AnchorPane.setLeftAnchor(borderPane, 0.0);
AnchorPane.setRightAnchor(borderPane, 0.0);
imageView.setPreserveRatio(true);
pane.getChildren().add(borderPane);
borderPane.setTop(imageView);
borderPane.setBottom(label);
BorderPane.setAlignment(imageView, Pos.CENTER);
BorderPane.setAlignment(label, Pos.CENTER);
imageView.fitHeightProperty().bind(subtract(pane.heightProperty(), label.heightProperty()));
imageView.setImage(new Image("file:C:\\pathToFile.jpg"));
primaryStage.setScene(new Scene(pane, 200, 150));
primaryStage.show();
}
}

Disable Background stage javafx

I'm french, sorry for mistake.
I have an primary stage, and foreground an small second stage. I want to color in gray all the primary stage when the second stage is visible.
It's good, i cannot click in the primary stage with the line :
stage.initModality(Modality.WINDOW_MODAL);
But I want to put a color gray in the primary stage.
I try to disable all componant in the primary stage (every componant is gray and disable) but imageViews are not gray, it's a problem.
Help please.
Thanks.
You can add all to a Stackpane and make a Region as a veil (visible=true/false).
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class VeilDemo extends Application {
#Override
public void start(Stage primaryStage) {
// that is the veil
Region veil = new Region();
veil.setStyle("-fx-background-color: rgba(0, 0, 0, 0.3)");
veil.setVisible(false);
Button btn = new Button();
btn.setText("Open Dialog");
btn.setOnAction((ActionEvent event) -> {
Alert a = new Alert(Alert.AlertType.INFORMATION);
//veil is only visible when alert window is showing
veil.visibleProperty().bind(a.showingProperty());
a.setContentText("The main window should be decorated with a veil.");
a.setX(primaryStage.getX() + 200); // This is only for showing main window
a.showAndWait();
});
Image img = new Image("https://www.gnu.org/graphics/gnu-head-sm.png");
ImageView iv = new ImageView(img);
// this should be the normal root of window
BorderPane bp = new BorderPane(iv);
bp.setBottom(btn);
StackPane root = new StackPane();
root.getChildren().addAll(bp, veil);
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
The main window will look like this:
and if the button was clicked, the info window opens and the veil is visibile on the main stage.

keep elements X position fixed after scrolling javafx

How can I keep an element always visible even after scrolling in a scrollPane, that means the node should be immovable after a horizontal scrolling.its position should be fixed. I have tried this but it doesn't work for my case , the elements still moving with scrolling, I'm adding a scrollPane which contains all the elements to an AnchorPane.
Just use a stack pane and add the fixed elements after the ScrollPane.
Depending on what scrolling you do not want to allow just comment out the "scrollProperty" listener that I added - if you want the element completely fixed - remove them both:
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
StackPane root = new StackPane();
root.setAlignment(Pos.TOP_LEFT);
ScrollPane scrollPane = new ScrollPane();
Pane pane = new Pane();
pane.setMinHeight(1000);
pane.setMinWidth(1000);
scrollPane.setContent(pane);
root.getChildren().add(scrollPane);
Label fixed = new Label("Fixed");
root.getChildren().add(fixed);
// Allow vertical scrolling of fixed element:
scrollPane.hvalueProperty().addListener( (observable, oldValue, newValue) -> {
double xTranslate = newValue.doubleValue() * (scrollPane.getViewportBounds().getWidth() - fixed.getWidth());
fixed.translateXProperty().setValue(-xTranslate);
});
// Allow horizontal scrolling of fixed element:
scrollPane.vvalueProperty().addListener( (observable, oldValue, newValue) -> {
double yTranslate = newValue.doubleValue() * (scrollPane.getViewportBounds().getHeight() - fixed.getWidth());
fixed.translateYProperty().setValue(-yTranslate);
});
Scene scene = new Scene(root, 500, 500);
primaryStage.setScene(scene);
primaryStage.show();
}
}

JavaFX ScrollBar value confusion

I have a simple JavaFX application with a Canvas and a ScrollBar.
The Canvas acts as a virtualized canvas whose contents can be scrolled with the ScrollBar.
The width of the Canvas is 500 pixels but the logical width of the Canvas is 1000 pixels.
I've set the ScrollBar's max to 1000 and the ScrollBar's visible amount to 500.
The problem is that when the ScrollBar is scrolled all the way to the right the value of the
ScrollBar is 1000, not 500.
Logically when a ScrollBar is scrolled all the way to the right there should be some way to determine the actual scrolled offset and that does not seem possible.
Please suggest how the amount of scroll required can be obtained. Thank you
Here is the example scrolled all the way to the left.
The scrollbar looks good. Its visible width is 50% of the window size.
Here is the example scrolled halfway to the right.
Here the problem is clear. The Canvas has been scrolled 500 pixels to the right, but if the Canvas was correctly scrolled halfway it would be scrolled only 250 pixels.
Here is the example scrolled all the way to the right.
import javafx.application.Application;
import javafx.beans.value.*;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.ScrollBar;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.stage.Stage;
public class Sc extends Application {
public ScrollBar scrollBar;
double scrolled;
Canvas canvas;
#Override
public void start(Stage stage) {
VBox vbox = new VBox();
Scene scene = new Scene(vbox);
stage.setScene(scene);
canvas = new Canvas(500, 100);
scrollBar = new ScrollBar();
scrollBar.setMin(0);
scrollBar.setMax(1000);
scrollBar.setVisibleAmount(500);
vbox.getChildren().addAll(canvas, scrollBar);
draw();
scrollBar.valueProperty().addListener(new ChangeListener<Number>() {
public void changed(ObservableValue<? extends Number> ov,
Number old_val, Number new_val) {
scrolled = new_val.doubleValue();
draw();
}
});
stage.show();
}
private void draw()
{
GraphicsContext graphics = canvas.getGraphicsContext2D();
graphics.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
Stop[] stops = new Stop[] { new Stop(0, Color.BLACK), new Stop(1, Color.RED)};
LinearGradient lg = new LinearGradient(0, 0, 1, 0, true, CycleMethod.NO_CYCLE, stops);
graphics.setFill(lg);
graphics.fillRect(0-scrolled, 30, 1000, 40);
}
public static void main(String[] args) {
launch(args);
}
}
Think of it this way:
min, max, and value are the logical values of the scrollbar. value ranges between min and max, and is equal to min if and only if the scrollbar is scrolled all the way to the left (or top, for a vertical scrollbar). It is equal to max if and only if the scrollbar is scrolled as far as possible to the right (or bottom).
The visibleAmount property is really just a visual property that determines the size of the "thumb". The relationship is something like
thumbSize / trackSize = visibleAmount / (max - min)
The visibleAmount will affect the relationship between actual pixels and "logical units"; however it will not change the fact that value can vary along the full range of [min, max].
So the only change you need to make to your code is to set the maximum value of the scrollbar to the maximum it can be scrolled by (not the size of the image you are drawing), which is 1000 - canvas.getWidth():
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.ScrollBar;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.stage.Stage;
public class ScrollBarExample extends Application {
private final static double TOTAL_WIDTH = 1000 ;
#Override
public void start(Stage stage) {
VBox vbox = new VBox();
Scene scene = new Scene(vbox, 500, 130);
stage.setScene(scene);
Canvas canvas = new Canvas(500, 100);
ScrollBar scrollBar = new ScrollBar();
scrollBar.setMin(0);
scrollBar.setMax(TOTAL_WIDTH - canvas.getWidth());
scrollBar.setVisibleAmount(scrollBar.getMax() * canvas.getWidth() / TOTAL_WIDTH);
vbox.getChildren().addAll(canvas, scrollBar);
draw(canvas, scrollBar.getValue());
scrollBar.valueProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> ov,
Number old_val, Number new_val) {
draw(canvas, scrollBar.getValue());
}
});
stage.show();
}
private void draw(Canvas canvas, double scrollAmount)
{
GraphicsContext graphics = canvas.getGraphicsContext2D();
graphics.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
Stop[] stops = new Stop[] { new Stop(0, Color.BLACK), new Stop(1, Color.RED)};
LinearGradient lg = new LinearGradient(0, 0, 1, 0, true, CycleMethod.NO_CYCLE, stops);
graphics.setFill(lg);
graphics.fillRect(0-scrollAmount, 30, TOTAL_WIDTH, 40);
}
public static void main(String[] args) {
launch(args);
}
}

Resources