JavaFX:how to resize the stage when using webview - javafx

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

Related

Javafx TilePane PrefColumns

I am having problems producing the correct number of columns in a TilePane row.
I'm trying just to have ten columns per row. I know I'm missing the obvious. So, I need another pair of eyes.
The setPrefColumns method does not appear to work the way I have it coded.
#SuppressWarnings("unused")
public class Main extends Application {
TilePane tp = new TilePane();
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
try {
primaryStage.setResizable(false);
tp.setPrefColumns(10);
setTP();
Scene scene = new Scene(tp,800,600);
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public void setTP() {
tp.setVisible(true);
int[] numbers = {1,2,3,4,5,6,7,8,9,10};
for(int row=0; row<11; row++) {
for (int i: numbers ) {
Text t = new Text(String.valueOf(i));
HBox hbox = new HBox();
hbox.getChildren().add(t);
hbox.setStyle("-fx-border-color: red;");
tp.getChildren().add(hbox);
}
}
}
}
Your example TilePane behaves as specified:
Note that prefColumns/prefRows is used only for calculating the preferred size and may not reflect the actual number of rows or columns, which may change as the tilepane is resized and the tiles are wrapped at its actual boundaries.
As root of the scene it is sized to fill the complete width (800 in your context):
Scene scene = new Scene(tp,800,600);
To make it wrap on its prefColumns, you need to add it to a layout that respects its content's prefWidth, f.i. a HBox:
Scene scene = new Scene(new HBox(tp),800,600);

Why does my JavaFX Preloader occasionally show up gray/black and other times it correctly loads?

I'm trying to get my JavaFX Preloader splash sccreen to show up before my application. I'm using Eclipse IDE and when I click "run", half the time the splash screen will display correctly and the other half of the time I will get a gray or black screen instead of where the image should be.
I'm not sure what the issue is to cause it to only display correctly sometimes.
SplashController:
public class SplashController extends Preloader {
private static final double WIDTH = 676;
private static final double HEIGHT = 227;
private Stage preloaderStage;
private Label progressText;
private Pane splashScreen;
public SplashController() {}
#Override
public void init() throws Exception {
ImageView splash =
new ImageView(new Image(Demo.class.getResource("pic.png").toString()));
progressText =
new Label("VERSION: " + getVersion() + " ~~~ Loading plugins, please wait...");
splashScreen = new VBox();
splashScreen.getChildren().addAll(splash, progressText);
progressText.setAlignment(Pos.CENTER);
}
#Override
public void start(Stage primaryStage) throws Exception {
this.preloaderStage = primaryStage;
Scene splashScene = new Scene(splashScreen);
this.preloaderStage.initStyle(StageStyle.UNDECORATED);
final Rectangle2D bounds = Screen.getPrimary().getBounds();
this.preloaderStage.setScene(splashScene);
this.preloaderStage.setX(bounds.getMinX() + bounds.getWidth() / 2 - WIDTH / 2);
this.preloaderStage.setY(bounds.getMinY() + bounds.getHeight() / 2 - HEIGHT / 2);
this.preloaderStage.show();
}
}
And then in my main class Demo I simply have:
public class Demo extends Application {
#Override
public void start(Stage stage) throws Exception {
FXMLLoader loader = new
FXMLLoader(Demo.class.getResource("FXMLDocument.fxml"));
GridPane root = loader.load();
--------other app code here---------
}
public static void main(String[] args) {
LauncherImpl.launchApplication(Demo.class, SplashController.class, args);
}
}
Likely, you are executing some long running process on the JavaFX application thread or a thread involved in the application startup, which prevents the smooth operation of the preloader.
I suggest you review an Oracle Preloader sample and compare to your application. Ensure that you are using concurrent features like Task correctly, similar to the linked example. Check the linked sample works in your environment.
Source code (just copied from the Oracle Preloader sample link)
Note how in the start method of the main LongAppInit application class, that a Task and thread is spawned to ensure that the long application initiation does not take place on the JavaFX application thread. Also see how the notifyPreloader() method of application is called at various times within the long application initialization to let the preloader know of the current state of the initialization process so that it can reflect the progress accurately in the UI in real time.
LongAppInitPreloader.java
public class LongAppInitPreloader extends Preloader {
ProgressBar bar;
Stage stage;
boolean noLoadingProgress = true;
private Scene createPreloaderScene() {
bar = new ProgressBar(0);
BorderPane p = new BorderPane();
p.setCenter(bar);
return new Scene(p, 300, 150);
}
public void start(Stage stage) throws Exception {
this.stage = stage;
stage.setScene(createPreloaderScene());
stage.show();
}
#Override
public void handleProgressNotification(ProgressNotification pn) {
//application loading progress is rescaled to be first 50%
//Even if there is nothing to load 0% and 100% events can be
// delivered
if (pn.getProgress() != 1.0 || !noLoadingProgress) {
bar.setProgress(pn.getProgress()/2);
if (pn.getProgress() > 0) {
noLoadingProgress = false;
}
}
}
#Override
public void handleStateChangeNotification(StateChangeNotification evt) {
//ignore, hide after application signals it is ready
}
#Override
public void handleApplicationNotification(PreloaderNotification pn) {
if (pn instanceof ProgressNotification) {
//expect application to send us progress notifications
//with progress ranging from 0 to 1.0
double v = ((ProgressNotification) pn).getProgress();
if (!noLoadingProgress) {
//if we were receiving loading progress notifications
//then progress is already at 50%.
//Rescale application progress to start from 50%
v = 0.5 + v/2;
}
bar.setProgress(v);
} else if (pn instanceof StateChangeNotification) {
//hide after get any state update from application
stage.hide();
}
}
}
LongAppInit.java
public class LongInitApp extends Application {
Stage stage;
BooleanProperty ready = new SimpleBooleanProperty(false);
private void longStart() {
//simulate long init in background
Task task = new Task<Void>() {
#Override
protected Void call() throws Exception {
int max = 10;
for (int i = 1; i <= max; i++) {
Thread.sleep(200);
// Send progress to preloader
notifyPreloader(new ProgressNotification(((double) i)/max));
}
// After init is ready, the app is ready to be shown
// Do this before hiding the preloader stage to prevent the
// app from exiting prematurely
ready.setValue(Boolean.TRUE);
notifyPreloader(new StateChangeNotification(
StateChangeNotification.Type.BEFORE_START));
return null;
}
};
new Thread(task).start();
}
#Override
public void start(final Stage stage) throws Exception {
// Initiate simulated long startup sequence
longStart();
stage.setScene(new Scene(new Label("Application started"),
400, 400));
// After the app is ready, show the stage
ready.addListener(new ChangeListener<Boolean>(){
public void changed(
ObservableValue<? extends Boolean> ov, Boolean t, Boolean t1) {
if (Boolean.TRUE.equals(t1)) {
Platform.runLater(new Runnable() {
public void run() {
stage.show();
}
});
}
}
});;
}
}

Javafx combobox not updating dropdown size upon change on realtime?

I am using Javafx v8.0.25-b18.
The problem I occur is that the size of the dynamic combox's dropdown list doesn't change, so if I had initially two items in the dropdown, then the dropdown size will be good for two items, but if I now populate the dynamic combox with three items then I get a small scrollbar inside!?, If I remove an item - I will have a blank space in the combox !?
I want to "reset" the dropdown size each time I put values into it, so it will be the right size each time it gets populated at runtime.
To clarify even more I am adding three images:
1. The first screenshot shows the initial dropdown size of 2
The second screenshot shows the same combox, where now at runtime I am adding 2 values, I EXPECT it to have now a dropdown with the size of 4, but instead the dropdown size stays 2 and only adds an unwanted scrollbar
Last screenshot is when I remove items and only one item remains in the combox, I EXPECT to see a dropdown of 1 item, but instead I unfortunately see a dropdown the size of 2 thus an empty space instead of the second item
I am adding the simple code to create this scenario, I want to thank #Gikkman that helped getting this far and the code is actually his!
public class Test extends Application {
private int index = 0;
#Override
public void start(Stage primaryStage) throws IOException {
VBox vbox = new VBox();
vbox.setSpacing(10);
vbox.setAlignment(Pos.CENTER);
final ComboBox<String> box = new ComboBox<>();
box.setPrefWidth(200);
box.setVisibleRowCount(10);
Button add = new Button("Add");
Button remove = new Button("Remove");
add.setOnAction( new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
box.getItems().add("Item " + index++);
box.getItems().add("Item " + index++);
}
});
remove.setOnAction( new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
if( index > 0 )
box.getItems().remove(--index);
}
});
vbox.getChildren().addAll(add, remove, box);
Scene scene = new Scene(vbox);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Try this:
box.hide(); //before you set new visibleRowCount value
box.setVisibleRowCount(rows); // set new visibleRowCount value
box.show(); //after you set new visibleRowCount value
It works for me with editable comboBox and I think it will work in your case.
I had same problem and I solved it with a quick trick.
Just try to show and immediately hide !
add.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
box.getItems().add("Item " + index++);
box.getItems().add("Item " + index++);
box.show();
box.hide();
}
});
Just like to offer my two cents here. You may add the following codes to your combobox which define a custom listview popup that has variable height according to the current number of items. You can tweak the maximum number of items to be displayed in the popup.
yourComboBox.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
#Override
public ListCell<String> call(ListView<String> param) {
ListCell cell = new ListCell<String>() {
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
int numItems = getListView().getItems().size();
int height = 175; // set the maximum height of the popup
if (numItems <= 5) height = numItems * 35; // set the height of the popup if number of items is equal to or less than 5
getListView().setPrefHeight(height);
if (!empty) {
setText(item.toString());
} else {
setText(null);
}
}
};
return cell;
}
});
You don't have to change the number of entries to be displayed. The implementation will handle that automatically.
Say you want to display at most 10 items. Then, you use comboBox.setVisibleRowCount( 10 ); If there are less than 10 items at any time, Javafx will only show as many rows as there are items.
Actually, changing the number of visible rows at runtime can sometimes cause errors, from my experience, so you are better of with just having a set number.
Hope that helps.
I have some problems understanding what the problem is. I made a short example bellow, can you try it and then say what it doesn't do that you want to do.
public class Test extends Application{
private int index = 0;
#Override
public void start(Stage primaryStage) throws IOException{
VBox vbox = new VBox();
vbox.setSpacing(10);
vbox.setAlignment(Pos.CENTER);
ComboBox<String> box = new ComboBox<>();
box.setPrefWidth(200);
box.setVisibleRowCount(10);
Button add = new Button("Add");
Button remove = new Button("Remove");
add.setOnAction( new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
box.getItems().add("Item " + index++);
}
});
remove.setOnAction( new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
if( index > 0 )
box.getItems().remove(--index);
}
});
vbox.getChildren().addAll(add, remove, box);
Scene scene = new Scene(vbox);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
You can use two JavaFx list. First one is previous com box list, another one is final combo box list. then you can change dynamically using yourCombo.getItems().setAll(Your List);
Here is my sample code:
import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class ComboBoxTest extends Application {
#Override
public void start(final Stage primaryStage) throws Exception {
primaryStage.centerOnScreen();
primaryStage.setHeight(200);
primaryStage.setWidth(300);
List<String> list1 = new ArrayList<>();
list1.add("one");
list1.add("two");
list1.add("three");
List<String> list2 = new ArrayList<>();
list2.add("one");
list2.add("two");
list2.add("three");
list2.add("four");
final ComboBox<String> combo = new ComboBox<String>();
combo.getItems().setAll(list1);
Button button = new Button("Change combo contents");
button.setOnAction(event -> {
if ( combo.getItems().size() == 3 ) {
combo.getItems().setAll(list2);
} else {
combo.getItems().setAll(list1);
}
combo.show();
});
VBox box = new VBox(20, combo, button );
box.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
primaryStage.setScene(new Scene( new StackPane(box) ));
primaryStage.show();
}
public static void main(String[] args) throws Exception {
launch(args);
}
}

JavaFX not positioning the node in the parent node

I have a problem with centering my dialog boxes (that are basically AnchorPanes that have caption in them) in their parent node (that is also AnchorPane, but I beleive this is irrelevant).
Code goes as follows:
public void showDialog(final Dialog dialog) {
dialogStack.add(dialog);
dialogCanvas.toFront();
dialog.toFront();
dialogCanvas.setVisible(true);
dialog.setVisible(true);
getChildren().add(dialog);
dialog.widthProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
if(newValue.doubleValue() > 0) {
centerDialog(dialog);
}
}
});
}
public void centerDialog(final Dialog dialog) {
double width = getWidth();
double height = getHeight();
dialog.setLayoutX(width / 2 - dialog.getWidth() / 2);
dialog.setLayoutY(height / 2 - dialog.getHeight() / 2);
}
I use the widthProperty change listener because you can only center the node when it gets rendered and only then it gets the width/height set.
When I debug the code at point when I have set the dialog's layout x/y the values are calculated fine but the dialog node at the end gets positioned at top/left corner (X:0/Y:0).
Why???
Can I somehow trigger the redraw of a parent node so that the element gets positioned righty? What am I doing wrong?
Kind regards,
Stjepan
Your options:
Continue using AnchorPane as a parent but center the child by using anchor constraints as
AnchorPane.setTopAnchor(dialog, height / 2 - dialog.getHeight() / 2);
AnchorPane.setLeftAnchor(dialog, width / 2 - dialog.getWidth() / 2);
on every change of width and height values of parent pane. You need add event listener for both width and height properties if you want the child pane to stay centered on window resizing.
Use Pane instead of AnchorPane as parent and center the child pane by setLayoutX() / setLayoutY(), on every change of parent's width and height values (again using event listeners). The Pane, in opposite to AnchorPane, will not layout its children other than resizing them to their preferred sizes.
Use StackPane instead of AnchorPane as parent and no need to center the child pane since StackPane will do it for you automatically.
Use the ControlsFX Dialogs.
A demo code for the first option:
public class AnchorDemo extends Application {
private final AnchorPane anchorPaneParent = new AnchorPane();
private final AnchorPane anchorPaneChild = new AnchorPane(new Label("TEXT TEXT TEXT TEXT TEXT"));
#Override
public void start(Stage primaryStage) {
anchorPaneParent.setStyle("-fx-border-color:red");
anchorPaneChild.setStyle("-fx-border-color:green");
anchorPaneParent.getChildren().add(anchorPaneChild);
anchorPaneParent.layoutBoundsProperty().addListener(new ChangeListener<Bounds>() {
#Override
public void changed(ObservableValue<? extends Bounds> arg0, Bounds oldBound, Bounds newBound) {
System.out.println("oldBound = " + oldBound);
System.out.println("newBound = " + newBound);
System.out.println("");
if (oldBound.getHeight() != newBound.getHeight() || oldBound.getWidth() != newBound.getWidth()) {
updateChildAnchorConstraint();
}
}
});
Scene scene = new Scene(anchorPaneParent, 300, 250);
primaryStage.setScene(scene);
primaryStage.show();
updateChildAnchorConstraint();
}
private void updateChildAnchorConstraint() {
AnchorPane.setTopAnchor(anchorPaneChild, anchorPaneParent.getHeight() / 2 - anchorPaneChild.getHeight() / 2);
AnchorPane.setLeftAnchor(anchorPaneChild, anchorPaneParent.getWidth() / 2 - anchorPaneChild.getWidth() / 2);
}
public static void main(String[] args) {
launch(args);
}
}
The problem was that this was all done in the single thread and the information about layout was not yet known... But using the Platform.runLater(...) I was able to solve the problem.
At the end it is pretty elegant when you know how to solve it...
public void showDialog(final Dialog dialog) {
dialogStack.add(dialog);
dialogCanvas.toFront();
dialog.toFront();
dialogCanvas.setVisible(true);
dialog.setVisible(true);
getChildren().add(dialog);
dialog.widthProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
if(newValue.doubleValue() > 0) {
centerDialog(dialog);
}
}
});
}
public void centerDialog(final Dialog dialog) {
Platform.runLater(new Runnable() {
#Override
public void run() {
double width = getWidth();
double height = getHeight();
dialog.setLayoutX(width / 2 - dialog.getWidth() / 2);
dialog.setLayoutY(height / 2 - dialog.getHeight() / 2);
}
});
}

JavaFX correct scaling

I want to scale all nodes in a Pane on a scroll event.
What I have tried so far:
When I do scaleX or scaleY, border of pane
scales respectively (seen when set Pane style -fx-border-color: black;). So not every event would start if I'm not from the borders
of pane, so I need it all.
Next step I tried to scale each node and it turned out really bad,
something like this - (lines stretched through the points). Or if
scrolling in other side, it would be less
Another method I tried was to scale points of Node. It's better, but
I don't like it. It looks like
point.setScaleX(point.getScaleX()+scaleX) and for y and other nodes
appropriately.
I created a sample app to demonstrate one approach to performing scaling of a node in a viewport on a scroll event (e.g. scroll in and out by rolling the mouse wheel).
The key logic to the sample for scaling a group placed within a StackPane:
final double SCALE_DELTA = 1.1;
final StackPane zoomPane = new StackPane();
zoomPane.getChildren().add(group);
zoomPane.setOnScroll(new EventHandler<ScrollEvent>() {
#Override public void handle(ScrollEvent event) {
event.consume();
if (event.getDeltaY() == 0) {
return;
}
double scaleFactor =
(event.getDeltaY() > 0)
? SCALE_DELTA
: 1/SCALE_DELTA;
group.setScaleX(group.getScaleX() * scaleFactor);
group.setScaleY(group.getScaleY() * scaleFactor);
}
});
The scroll event handler is set on the enclosing StackPane which is a resizable pane so it expands to fill any empty space, keeping the zoomed content centered in the pane. If you move the mouse wheel anywhere inside the StackPane it will zoom in or out the enclosed group of nodes.
import javafx.application.Application;
import javafx.beans.value.*;
import javafx.event.*;
import javafx.geometry.Bounds;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.image.*;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
public class GraphicsScalingApp extends Application {
public static void main(String[] args) { launch(args); }
#Override public void start(final Stage stage) {
final Group group = new Group(
createStar(),
createCurve()
);
Parent zoomPane = createZoomPane(group);
VBox layout = new VBox();
layout.getChildren().setAll(
createMenuBar(stage, group),
zoomPane
);
VBox.setVgrow(zoomPane, Priority.ALWAYS);
Scene scene = new Scene(
layout
);
stage.setTitle("Zoomy");
stage.getIcons().setAll(new Image(APP_ICON));
stage.setScene(scene);
stage.show();
}
private Parent createZoomPane(final Group group) {
final double SCALE_DELTA = 1.1;
final StackPane zoomPane = new StackPane();
zoomPane.getChildren().add(group);
zoomPane.setOnScroll(new EventHandler<ScrollEvent>() {
#Override public void handle(ScrollEvent event) {
event.consume();
if (event.getDeltaY() == 0) {
return;
}
double scaleFactor =
(event.getDeltaY() > 0)
? SCALE_DELTA
: 1/SCALE_DELTA;
group.setScaleX(group.getScaleX() * scaleFactor);
group.setScaleY(group.getScaleY() * scaleFactor);
}
});
zoomPane.layoutBoundsProperty().addListener(new ChangeListener<Bounds>() {
#Override public void changed(ObservableValue<? extends Bounds> observable, Bounds oldBounds, Bounds bounds) {
zoomPane.setClip(new Rectangle(bounds.getMinX(), bounds.getMinY(), bounds.getWidth(), bounds.getHeight()));
}
});
return zoomPane;
}
private SVGPath createCurve() {
SVGPath ellipticalArc = new SVGPath();
ellipticalArc.setContent(
"M10,150 A15 15 180 0 1 70 140 A15 25 180 0 0 130 130 A15 55 180 0 1 190 120"
);
ellipticalArc.setStroke(Color.LIGHTGREEN);
ellipticalArc.setStrokeWidth(4);
ellipticalArc.setFill(null);
return ellipticalArc;
}
private SVGPath createStar() {
SVGPath star = new SVGPath();
star.setContent(
"M100,10 L100,10 40,180 190,60 10,60 160,180 z"
);
star.setStrokeLineJoin(StrokeLineJoin.ROUND);
star.setStroke(Color.BLUE);
star.setFill(Color.DARKBLUE);
star.setStrokeWidth(4);
return star;
}
private MenuBar createMenuBar(final Stage stage, final Group group) {
Menu fileMenu = new Menu("_File");
MenuItem exitMenuItem = new MenuItem("E_xit");
exitMenuItem.setGraphic(new ImageView(new Image(CLOSE_ICON)));
exitMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent event) {
stage.close();
}
});
fileMenu.getItems().setAll(
exitMenuItem
);
Menu zoomMenu = new Menu("_Zoom");
MenuItem zoomResetMenuItem = new MenuItem("Zoom _Reset");
zoomResetMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.ESCAPE));
zoomResetMenuItem.setGraphic(new ImageView(new Image(ZOOM_RESET_ICON)));
zoomResetMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent event) {
group.setScaleX(1);
group.setScaleY(1);
}
});
MenuItem zoomInMenuItem = new MenuItem("Zoom _In");
zoomInMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.I));
zoomInMenuItem.setGraphic(new ImageView(new Image(ZOOM_IN_ICON)));
zoomInMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent event) {
group.setScaleX(group.getScaleX() * 1.5);
group.setScaleY(group.getScaleY() * 1.5);
}
});
MenuItem zoomOutMenuItem = new MenuItem("Zoom _Out");
zoomOutMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.O));
zoomOutMenuItem.setGraphic(new ImageView(new Image(ZOOM_OUT_ICON)));
zoomOutMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent event) {
group.setScaleX(group.getScaleX() * 1/1.5);
group.setScaleY(group.getScaleY() * 1/1.5);
}
});
zoomMenu.getItems().setAll(
zoomResetMenuItem,
zoomInMenuItem,
zoomOutMenuItem
);
MenuBar menuBar = new MenuBar();
menuBar.getMenus().setAll(
fileMenu,
zoomMenu
);
return menuBar;
}
// icons source from: http://www.iconarchive.com/show/soft-scraps-icons-by-deleket.html
// icon license: CC Attribution-Noncommercial-No Derivate 3.0 =? http://creativecommons.org/licenses/by-nc-nd/3.0/
// icon Commercial usage: Allowed (Author Approval required -> Visit artist website for details).
public static final String APP_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/128/Zoom-icon.png";
public static final String ZOOM_RESET_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-icon.png";
public static final String ZOOM_OUT_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-Out-icon.png";
public static final String ZOOM_IN_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-In-icon.png";
public static final String CLOSE_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Button-Close-icon.png";
}
Update for a zoomed node in a ScrollPane
The above implementation works well as far as it goes, but it is useful to be able to place the zoomed node inside a scroll pane, so that when you zoom in making the zoomed node larger than your available viewport, you can still pan around the zoomed node within the scroll pane to view parts of the node.
I found achieving the behavior of zooming in a scroll pane difficult, so I asked for help on an Oracle JavaFX Forum thread.
Oracle JavaFX forum user James_D came up with the following solution which solves the zooming within a ScrollPane problem quite well.
His comments and code were as below:
A couple of minor changes first: I wrapped the StackPane in a Group so that the ScrollPane would be aware of the changes to the transforms, as per the ScrollPane Javadocs. And then I bound the minimum size of the StackPane to the viewport size (keeping the content centered when smaller than the viewport).
Initially I thought I should use a Scale transform to zoom around the displayed center (i.e. the point on the content that is at the center of the viewport). But I found I still needed to fix the scroll position afterwards to keep the same displayed center, so I abandoned that and reverted to using setScaleX() and setScaleY().
The trick is to fix the scroll position after scaling. I computed the scroll offset in local coordinates of the scroll content, and then computed the new scroll values needed after the scale. This was a little tricky. The basic observation is that
(hValue-hMin)/(hMax-hMin) = x / (contentWidth - viewportWidth), where x is the horizontal offset of the left edge of the viewport from the left edge of the content.
Then you have centerX = x + viewportWidth/2.
After scaling, the x coordinate of the old centerX is now centerX*scaleFactor. So we just have to set the new hValue to make that the new center. There's a bit of algebra to figure that out.
After that, panning by dragging was pretty easy :).
A corresponding feature request to add high level APIs to support zooming and scaling functionality in a ScrollPane is Add scaleContent functionality to ScrollPane. Vote for or comment on the feature request if you would like to see it implemented.
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.*;
import javafx.event.*;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.image.*;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
public class GraphicsScalingApp extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(final Stage stage) {
final Group group = new Group(createStar(), createCurve());
Parent zoomPane = createZoomPane(group);
VBox layout = new VBox();
layout.getChildren().setAll(createMenuBar(stage, group), zoomPane);
VBox.setVgrow(zoomPane, Priority.ALWAYS);
Scene scene = new Scene(layout);
stage.setTitle("Zoomy");
stage.getIcons().setAll(new Image(APP_ICON));
stage.setScene(scene);
stage.show();
}
private Parent createZoomPane(final Group group) {
final double SCALE_DELTA = 1.1;
final StackPane zoomPane = new StackPane();
zoomPane.getChildren().add(group);
final ScrollPane scroller = new ScrollPane();
final Group scrollContent = new Group(zoomPane);
scroller.setContent(scrollContent);
scroller.viewportBoundsProperty().addListener(new ChangeListener<Bounds>() {
#Override
public void changed(ObservableValue<? extends Bounds> observable,
Bounds oldValue, Bounds newValue) {
zoomPane.setMinSize(newValue.getWidth(), newValue.getHeight());
}
});
scroller.setPrefViewportWidth(256);
scroller.setPrefViewportHeight(256);
zoomPane.setOnScroll(new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
event.consume();
if (event.getDeltaY() == 0) {
return;
}
double scaleFactor = (event.getDeltaY() > 0) ? SCALE_DELTA
: 1 / SCALE_DELTA;
// amount of scrolling in each direction in scrollContent coordinate
// units
Point2D scrollOffset = figureScrollOffset(scrollContent, scroller);
group.setScaleX(group.getScaleX() * scaleFactor);
group.setScaleY(group.getScaleY() * scaleFactor);
// move viewport so that old center remains in the center after the
// scaling
repositionScroller(scrollContent, scroller, scaleFactor, scrollOffset);
}
});
// Panning via drag....
final ObjectProperty<Point2D> lastMouseCoordinates = new SimpleObjectProperty<Point2D>();
scrollContent.setOnMousePressed(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
lastMouseCoordinates.set(new Point2D(event.getX(), event.getY()));
}
});
scrollContent.setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
double deltaX = event.getX() - lastMouseCoordinates.get().getX();
double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
double deltaH = deltaX * (scroller.getHmax() - scroller.getHmin()) / extraWidth;
double desiredH = scroller.getHvalue() - deltaH;
scroller.setHvalue(Math.max(0, Math.min(scroller.getHmax(), desiredH)));
double deltaY = event.getY() - lastMouseCoordinates.get().getY();
double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
double deltaV = deltaY * (scroller.getHmax() - scroller.getHmin()) / extraHeight;
double desiredV = scroller.getVvalue() - deltaV;
scroller.setVvalue(Math.max(0, Math.min(scroller.getVmax(), desiredV)));
}
});
return scroller;
}
private Point2D figureScrollOffset(Node scrollContent, ScrollPane scroller) {
double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
double hScrollProportion = (scroller.getHvalue() - scroller.getHmin()) / (scroller.getHmax() - scroller.getHmin());
double scrollXOffset = hScrollProportion * Math.max(0, extraWidth);
double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
double vScrollProportion = (scroller.getVvalue() - scroller.getVmin()) / (scroller.getVmax() - scroller.getVmin());
double scrollYOffset = vScrollProportion * Math.max(0, extraHeight);
return new Point2D(scrollXOffset, scrollYOffset);
}
private void repositionScroller(Node scrollContent, ScrollPane scroller, double scaleFactor, Point2D scrollOffset) {
double scrollXOffset = scrollOffset.getX();
double scrollYOffset = scrollOffset.getY();
double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
if (extraWidth > 0) {
double halfWidth = scroller.getViewportBounds().getWidth() / 2 ;
double newScrollXOffset = (scaleFactor - 1) * halfWidth + scaleFactor * scrollXOffset;
scroller.setHvalue(scroller.getHmin() + newScrollXOffset * (scroller.getHmax() - scroller.getHmin()) / extraWidth);
} else {
scroller.setHvalue(scroller.getHmin());
}
double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
if (extraHeight > 0) {
double halfHeight = scroller.getViewportBounds().getHeight() / 2 ;
double newScrollYOffset = (scaleFactor - 1) * halfHeight + scaleFactor * scrollYOffset;
scroller.setVvalue(scroller.getVmin() + newScrollYOffset * (scroller.getVmax() - scroller.getVmin()) / extraHeight);
} else {
scroller.setHvalue(scroller.getHmin());
}
}
private SVGPath createCurve() {
SVGPath ellipticalArc = new SVGPath();
ellipticalArc.setContent("M10,150 A15 15 180 0 1 70 140 A15 25 180 0 0 130 130 A15 55 180 0 1 190 120");
ellipticalArc.setStroke(Color.LIGHTGREEN);
ellipticalArc.setStrokeWidth(4);
ellipticalArc.setFill(null);
return ellipticalArc;
}
private SVGPath createStar() {
SVGPath star = new SVGPath();
star.setContent("M100,10 L100,10 40,180 190,60 10,60 160,180 z");
star.setStrokeLineJoin(StrokeLineJoin.ROUND);
star.setStroke(Color.BLUE);
star.setFill(Color.DARKBLUE);
star.setStrokeWidth(4);
return star;
}
private MenuBar createMenuBar(final Stage stage, final Group group) {
Menu fileMenu = new Menu("_File");
MenuItem exitMenuItem = new MenuItem("E_xit");
exitMenuItem.setGraphic(new ImageView(new Image(CLOSE_ICON)));
exitMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
stage.close();
}
});
fileMenu.getItems().setAll(exitMenuItem);
Menu zoomMenu = new Menu("_Zoom");
MenuItem zoomResetMenuItem = new MenuItem("Zoom _Reset");
zoomResetMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.ESCAPE));
zoomResetMenuItem.setGraphic(new ImageView(new Image(ZOOM_RESET_ICON)));
zoomResetMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
group.setScaleX(1);
group.setScaleY(1);
}
});
MenuItem zoomInMenuItem = new MenuItem("Zoom _In");
zoomInMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.I));
zoomInMenuItem.setGraphic(new ImageView(new Image(ZOOM_IN_ICON)));
zoomInMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
group.setScaleX(group.getScaleX() * 1.5);
group.setScaleY(group.getScaleY() * 1.5);
}
});
MenuItem zoomOutMenuItem = new MenuItem("Zoom _Out");
zoomOutMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.O));
zoomOutMenuItem.setGraphic(new ImageView(new Image(ZOOM_OUT_ICON)));
zoomOutMenuItem.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
group.setScaleX(group.getScaleX() * 1 / 1.5);
group.setScaleY(group.getScaleY() * 1 / 1.5);
}
});
zoomMenu.getItems().setAll(zoomResetMenuItem, zoomInMenuItem,
zoomOutMenuItem);
MenuBar menuBar = new MenuBar();
menuBar.getMenus().setAll(fileMenu, zoomMenu);
return menuBar;
}
// icons source from:
// http://www.iconarchive.com/show/soft-scraps-icons-by-deleket.html
// icon license: CC Attribution-Noncommercial-No Derivate 3.0 =?
// http://creativecommons.org/licenses/by-nc-nd/3.0/
// icon Commercial usage: Allowed (Author Approval required -> Visit artist
// website for details).
public static final String APP_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/128/Zoom-icon.png";
public static final String ZOOM_RESET_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-icon.png";
public static final String ZOOM_OUT_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-Out-icon.png";
public static final String ZOOM_IN_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-In-icon.png";
public static final String CLOSE_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Button-Close-icon.png";
}
The answer from jewelsea has one issue, if the size of original content in the zoomPane is already larger than View Port. Then the following code will not work.
zoomPane.setMinSize(newValue.getWidth(), newValue.getHeight());
The result is when we zoom out, the content is not centered any more.
To resolve this issue, you need to create another StackPane in between the zoomPane and ScrollPane.
// Create a zoom pane for zoom in/out
final StackPane zoomPane = new StackPane();
zoomPane.getChildren().add(group);
final Group zoomContent = new Group(zoomPane);
// Create a pane for holding the content, when the content is smaller than the view port,
// it will stay the view port size, make sure the content is centered
final StackPane canvasPane = new StackPane();
canvasPane.getChildren().add(zoomContent);
final Group scrollContent = new Group(canvasPane);
// Scroll pane for scrolling
scroller = new ScrollPane();
scroller.setContent(scrollContent);
And in the viewportBoundsProperty listener, Change zoomPane to canvasPane
// Set the minimum canvas size
canvasPane.setMinSize(newValue.getWidth(), newValue.getHeight());
JavaFx is too complicated for zoom in/out. To achieve the same effect, WPF is much easier.

Resources