I have a ScrollPane as below:
ScrollPane scroller = new ScrollPane();
scroller.getStyleClass().add("scroller");
scroller.setPrefWidth(width);
scroller.setFocusTraversable(Boolean.FALSE);
scroller.setPannable(Boolean.TRUE);
scroller.setFitToWidth(Boolean.TRUE);
scroller.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroller.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
this.setCenter(scroller);
scroller.contentProperty().addListener((observableValue, last, now) ->
{
ScrollBar scrollBar = (ScrollBar) scroller.lookup(".scroll-bar:vertical");
if (scrollBar != null)
{
if (scrollBar.isVisible())
{
log.info("Scrollbar visible, setting lower card width..");
}
else
{
log.info("Scrollbar not visible, setting default card width..");
}
}
});
As you can see I've attached a listener to the content property to know when the content is set. I am trying to see if the scrollbar is visible when the content is updated. Even though I can see the scroll bar on the UI, it always goes to else part - "Scrollbar not visible".
Not sure if there is any other way to do this? Checked a lot on StackOverflow and Oracle docs - nothing solid found to suggest otherwise.
-- Adding context to the problem to better understand:
Just trying to explain what the problem is not sure if I should put it as a reply comment or edit the question, please advise and will change it:
So I have this view that brings up records from Firebase that need to be loaded on the TilePane that is hosted in ScrollPane which goes into the Center of the BorderPane.
The time by which I get the response from the Firebase is unpredictable as its async. So the UI gets loaded up with the empty TilePane and then the async call goes to fetch data. When the data is available, I need to prepare Cards (which is HBox) but the number of columns is fixed. So have to adjust the width of the cards to keep the gap (16px) and padding (16px) consistent on the TilePane at the same time maintain 5 columns. The width of each card needs to be recalculated based on the fact that whether or not there is a scrollbar on the display. Because if the scrollbar is displayed it takes some space and the TilePane will down it to 4 columns leaving a lot of empty space. Happy to explain further if this is not clear.
I strongly suggest to follow the suggestions given in the comments. It is all about choosing the correct layout.
The purpose of me answering this question is, in future, if someone comes across this question for dealing with scroll bar visibility, they will atleast know a way to get that (in JavaFX 8).
One way to check for the scrollbar visiblity is to register the appropriate scrollbar on layoutChildren and add a listener to its visilble property. Something like...
ScrollPane scrollPane = new ScrollPane() {
ScrollBar vertical;
#Override
protected void layoutChildren() {
super.layoutChildren();
if (vertical == null) {
vertical = (ScrollBar) lookup(".scroll-bar:vertical");
vertical.visibleProperty().addListener((obs, old, val) -> updateContent(val));
updateContent(vertical.isVisible());
}
}
};
The updateContent(visible) method is stuff you want to do when the visibility gets updated.
A complete working demo is as below.
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class ScrollPaneScrollBarVisibility_Demo extends Application {
#Override
public void start(Stage stage) throws Exception {
BorderPane borderPane = new BorderPane();
Scene sc = new Scene(borderPane, 300, 300);
stage.setScene(sc);
stage.setTitle("ScrollBar visibility");
stage.show();
ScrollPane scrollPane = new ScrollPane() {
ScrollBar vertical;
#Override
protected void layoutChildren() {
super.layoutChildren();
if (vertical == null) {
vertical = (ScrollBar) lookup(".scroll-bar:vertical");
vertical.visibleProperty().addListener((obs, old, val) -> updateContent(val));
updateContent(vertical.isVisible());
}
}
};
scrollPane.setContent(getContent());
borderPane.setCenter(scrollPane);
}
private void updateContent(boolean scrollBarVisible) {
System.out.println("Vertical scroll bar visible :: " + scrollBarVisible);
}
private VBox getContent() {
VBox labels = new VBox();
labels.setSpacing(5);
for (int i = 0; i < 10; i++) {
labels.getChildren().add(new Label("X " + i));
}
Button add = new Button("Add");
add.setOnAction(e -> labels.getChildren().add(new Label("Text")));
Button remove = new Button("Remove");
remove.setOnAction(e -> {
if (!labels.getChildren().isEmpty()) {
labels.getChildren().remove(labels.getChildren().size() - 1);
}
});
HBox buttons = new HBox(add, remove);
buttons.setSpacing(15);
VBox content = new VBox(buttons, labels);
content.setPadding(new Insets(15));
content.setSpacing(15);
return content;
}
public static void main(String[] args) {
Application.launch(args);
}
}
As #James_D said, used GridPane and it worked without any listeners:
GridPane cards = new GridPane();
cards.setVgap(16);
cards.setHgap(16);
cards.setAlignment(Pos.CENTER);
cards.setPadding(new Insets(16));
ColumnConstraints constraints = new ColumnConstraints();
constraints.setPercentWidth(20);
constraints.setHgrow(Priority.ALWAYS);
constraints.setFillWidth(Boolean.TRUE);
cards.getColumnConstraints().addAll(constraints, constraints, constraints, constraints, constraints);
I have 5 columns, so 5 times constraints. Worked just fine.
Related
I have a BorderPane inside a Tabpane inside a ScrollPane. The ScrollPane.ScrollBarPolicy.AS_NEEDED does work if i remove the TabPane and put the BorderPane as Content of the ScrollPane. How do i get this to work with the TabPane?
Somehow the BorderPane is able to tell the ScrollPane when to display Scrollbars and the TabPane unable to do so. I looked through the avaible Methods for the Tabpane but couldn't find any for this resizing.
Working Example:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.layout.*;
import javafx.stage.Stage;
public class FXApplication extends Application {
private BorderPane border;
private GridPane inner;
private TabPane tabPane;
#Override
public void start(Stage primaryStage) {
tabPane = new TabPane();
Tab tab = new Tab("test");
tabPane.getTabs().add(tab);
border = new BorderPane();
border.setCenter(innerGrid());
tab.setContent(border);
ScrollPane scp = new ScrollPane();
scp.setFitToHeight(true);
scp.setFitToWidth(true);
scp.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
scp.setHbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
// scp.setContent(border); // this works
scp.setContent(tabPane); // this doesnt
Scene s = new Scene(scp);
primaryStage.setScene(s);
primaryStage.show();
}
private GridPane innerGrid() {
inner = new GridPane();
for(int i=0; i<11 ;i++) {
ColumnConstraints columnConstraints = new ColumnConstraints();
columnConstraints.setHgrow(Priority.SOMETIMES);
inner.getColumnConstraints().add(columnConstraints);
RowConstraints rowConstraints = new RowConstraints();
rowConstraints.setVgrow(Priority.SOMETIMES);
inner.getRowConstraints().add(rowConstraints);
}
for(int i=0; i<100 ;i++) {
inner.add(new Button("Button " + i), i/10, i%10);
}
return inner;
}
public static void main(String[] args) {
FXApplication.launch(args);
}
}
Astonishingly, the exact behavior of AS_NEEDED is unspecified. All we have is the ScrollPaneSkin to look at. The decision whether or not to show the (f.i.) horizontal bar happens in its private method determineHorizontalSBVisible()
private boolean determineHorizontalSBVisible() {
final ScrollPane sp = getSkinnable();
if (Properties.IS_TOUCH_SUPPORTED) {
return (tempVisibility && (nodeWidth > contentWidth));
}
else {
// RT-17395: ScrollBarPolicy might be null. If so, treat it as "AS_NEEDED", which is the default
ScrollBarPolicy hbarPolicy = sp.getHbarPolicy();
return (ScrollBarPolicy.NEVER == hbarPolicy) ? false :
((ScrollBarPolicy.ALWAYS == hbarPolicy) ? true :
((sp.isFitToWidth() && scrollNode != null ? scrollNode.isResizable() : false) ?
(nodeWidth > contentWidth && scrollNode.minWidth(-1) > contentWidth) : (nodeWidth > contentWidth)));
}
}
Here nodeWidth is the actual width of the content node - has been calculated previously, respecting the node's min/max widths - and contentWidth is the width available for laying out the content.
Unreadable code (for me ;) In the case of resizable content and fitting into scrollPane's content area boils down to returning true if both content's actual and min width are greater than the available width.
The minWidth makes the difference in your context: BorderPane has a min > 0, TabPane has a min == 0, so the method above always returns false.
The other way round: to allow the hbar being visible with the TabPane it needs a min, f.i. by relating it to its pref:
tabPane.setMinWidth(Region.USE_PREF_SIZE);
Thank you ahead of time for your time taken.
Currently, I am in the process of creating a JavaFX GUI for a simple-enough client/server application.
On the right side of a SplitPane is a GridPane, where-by every time a message is sent or received, that Message is displayed within the new ROW in the GridPane, and the message is basically an ImageView(image) followed by a TextArea with a String in it displaying the message sent/received.
My issue is that I cannot figure out after over a week how to size the TextArea appropriately for the block of text within it.
Before you mark this question as a duplicate, I have tried every implementation I could find.
Firstly, the ScrollBar listening solution does not work on runtime, this only appears to work WHILE a user is typing, so I have scratched that as a potential solution for my particular issue.
The solution I'm currently using (which isn't working) is using a Text object and getting the layout bounds/height of THAT for the TextArea.
I am fine with my TextAreas (acting as message bubbles) all being the same width, as of now I am specifying the minWidth to be 300.0, the problem again is with the HEIGHT.
My code is as follows:
HBox messageBox = new HBox(10);
messageBox.setPadding(new Insets(0, 0, 0, 25));
TextArea textArea = new TextArea();
textArea.setText(message);
textArea.setFont(new Font(20));
textArea.setWrapText(true);
final Text helper = new Text();
helper.setText(message);
helper.setFont(textArea.getFont());
helper.setWrappingWidth(300.0);
double width = helper.getLayoutBounds().getWidth();
double height = helper.getLayoutBounds().getHeight();
textArea.setMinWidth(width);
textArea.setPrefHeight(height);
messageBox.getChildren().addAll(imageView, textArea);
messagePane.add(messageBox, 0, rowCount);
rowCount++;
Please note that I have also tried placing my helper Text object into a throw-away Pane, which renders almost identical results.
Lastly, I have tried adding padding to the setPrefHeight() of the TextArea, I have tried MinHeight/MaxHeight combinations.
This picture illustrates my FIRST problem, the 3rd message has far too much space below the end of the block of text, while preceding message look fine, (IMO). The second picture BELOW demonstrated my 2nd problem, larger blocks of text seem to gradually decrease the width of the TextAreas or perhaps the HBox's above them. Before these subsequent HBox's were, added, the highlighted TextArea had enough space, for instance.
Is there any solution that will work for my needs?
I would be very grateful, thank you for your time!
Keith
This is not a trivial task (unless you find a workaround), I am afraid you will have to somehow to compute the actual width and height and apply it to the TextArea. The way I am thinking is to either find your magic numbers by trial and error approach or better take the message text add it to a Label and then compute its dimensions (width, height) and then use those in order to set the TextArea. Here is a small example :
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.control.TextArea;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;
public class MessagerTest extends Application {
private VBox displayPane = new VBox(5);
private TextArea messageArea = new TextArea();
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
BorderPane mainPane = new BorderPane();
ScrollPane scrollPane = new ScrollPane(displayPane);
displayPane.setPadding(new Insets(10));
displayPane.prefWidthProperty().bind(scrollPane.widthProperty());
scrollPane.prefWidthProperty().bind(mainPane.widthProperty());
scrollPane.setHbarPolicy(ScrollBarPolicy.NEVER);
mainPane.setCenter(scrollPane);
mainPane.setBottom(messageArea);
mainPane.setPadding(new Insets(10));
messageArea.setPrefHeight(120);
messageArea.setFont(Font.font(16));
messageArea.setWrapText(true);
messageArea.setPromptText("Type a message here...");
messageArea.setOnKeyPressed(e -> {
if (e.getCode() == KeyCode.ENTER && !e.isShiftDown()) {
sendMessage(messageArea.getText());
e.consume();
} else if (e.getCode() == KeyCode.ENTER && e.isShiftDown()) {
messageArea.appendText(System.lineSeparator());
}
});
mainPane.getStylesheets().add(this.getClass().getResource("messanger.css").toExternalForm());
stage.setScene(new Scene(mainPane, 450, 600));
stage.show();
}
private void sendMessage(String message) {
TextArea txtArea = new TextArea(message);
txtArea.setWrapText(true);
txtArea.setId("Message");
txtArea.setEditable(true);
resizeTextArea(txtArea);
displayPane.getChildren().add(txtArea);
messageArea.clear();
}
private void resizeTextArea(TextArea txtArea) {
String text = txtArea.getText();
double maxWidth = displayPane.getWidth() - 40;
HBox h = new HBox();
Label l = new Label(text);
l.setFont(Font.font(15));
h.getChildren().add(l);
Scene s = new Scene(h);
l.impl_processCSS(true);
l.applyCss();
double width = l.prefWidth(-1) + 20;
double height = l.prefHeight(-1) + 20;
if (width > maxWidth) {
txtArea.setMaxWidth(maxWidth);
txtArea.setMinWidth(maxWidth);
} else {
txtArea.setMaxWidth(width);
txtArea.setMinWidth(width);
}
txtArea.setMinHeight(height);
txtArea.setMaxHeight(height);
}
}
In case you want the CSS file too :
#Message {
-fx-background-color : transparent;
-fx-font-size : 15px;
-fx-text-fill: black;
-fx-display-caret:false;
}
#Message .content:pressed {
-fx-background-color: #E5E4E4;
}
#Message .content {
-fx-background-color: #F1F0F0;
}
.scroll-pane > .viewport {
-fx-background-color: white;
}
The problem with the above is that when you write everything is one line and let the TextArea wrap the text this cause the actual label height to be bigger so you will have to adjust the values a bit in that case.
To be honest I am not sure if this is the only approach you can take or if its the optimal solution. I believe its worth to lose the mouse selection of the text and use a Label instead of doing the above with the TextArea.
I want to align i.e Position CENTER an OK button of a DialogPane. I have tried the below code but its not working.
Dialog dialog = new Dialog();
DialogPane dialogPane = dialog.getDialogPane();
dialogPane.setStyle("-fx-background-color: #fff;");
// Set the button types.
ButtonType okButtonType = new ButtonType("Ok", ButtonBar.ButtonData.OK_DONE);
ButtonType cancelButtonType = new ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialog.getDialogPane().getButtonTypes().addAll(okButtonType, cancelButtonType);
dialogPane.lookupButton(cancelButtonType).setVisible(false);
// Testing
Button okButton = (Button) dialog.getDialogPane().lookupButton(okButtonType);
okButton.setAlignment(Pos.CENTER);
// End Testing
dialog.showAndWait();
Centering buttons in the ButtonBar of a Dialog is actually surprisingly difficult to achieve in a non-hacky way.
Below is the best solution I could come up with. It relies upon a dynamic CSS lookup of the HBox for the button container, to which it then adds a spacer region on the right to push the buttons to the left (the default ButtonSkin implementation already places an implicit spacer of the left which pushes the buttons to the right, which I determined using ScenicView). The combination of the left and right spacers end up aligning the buttons in the center. The solution also overrides the ButtonBar creation to stop the ButtonSkin internally reordering and performing additional layout of buttons, as, when it does that, you can't really reliably customize the layout yourself.
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import java.util.Optional;
public class CenteredDialogButtons extends Application {
#Override
public void start(Stage stage) {
Button show = new Button("Show Dialog");
Dialog<ButtonType> dialog = new Dialog<>();
DialogPane dialogPane = new DialogPane() {
#Override
protected Node createButtonBar() {
ButtonBar buttonBar = (ButtonBar) super.createButtonBar();
buttonBar.setButtonOrder(ButtonBar.BUTTON_ORDER_NONE);
return buttonBar;
}
};
dialog.setDialogPane(dialogPane);
dialogPane.getButtonTypes().addAll(ButtonType.OK);
dialogPane.setContentText("Centered Button");
Region spacer = new Region();
ButtonBar.setButtonData(spacer, ButtonBar.ButtonData.BIG_GAP);
HBox.setHgrow(spacer, Priority.ALWAYS);
dialogPane.applyCss();
HBox hbox = (HBox) dialogPane.lookup(".container");
hbox.getChildren().add(spacer);
show.setOnAction(e -> {
Optional<ButtonType> result = dialog.showAndWait();
if (result.isPresent() && result.get() == ButtonType.OK) {
System.out.println("OK");
}
});
StackPane layout = new StackPane(
show
);
layout.setPadding(new Insets(50));
Scene scene = new Scene(layout);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The reason I don't quite like this solution is that the dynamic CSS lookups kind of violate API encapsulation, as the CSS structure of the JavaFX scene graphs for controls such as button bars is not really part of their public API. However, I don't think it is really possible to get centered buttons in a ButtonBar using the existing public APIs for JavaFX 8 and a default ButtonBar skin.
An alternate approach would be to create a custom skin for the ButtonBar associated with the dialog, but that approach is quite difficult and I wouldn't recommend it for this task.
Basically, the takeaway from all this is, just leave the default button layout and order for dialogs whenever you can, rather than trying to customize the dialog button layout. If you do want to have completely customized layout to the level of things like button placement, then you may be better off just creating your own custom dialog class by subclassing Stage rather than basing your custom dialog implementation on the in-built dialog class.
Related, but slightly different information is in:
Enter Key Event Is Not Working On Dialog In Javafx?
I tried to center OK button in Alert and I am not sure if this is bug or feature (Java8) but it was possible to center single button by setting new one:
alert.getButtonTypes().set(0, new ButtonType("OK", ButtonBar.ButtonData.LEFT));
As long as there is only one button with ButtonData.LEFT, it is centered in the middle of button panel. Obviously this solution does not work for panel with multiple buttons, but it might help to position single OK button.
Add this method to your code and call it when you need to align the buttons in a Dialog or Alert:
private void centerButtons(DialogPane dialogPane) {
Region spacer = new Region();
ButtonBar.setButtonData(spacer, ButtonBar.ButtonData.BIG_GAP);
HBox.setHgrow(spacer, Priority.ALWAYS);
dialogPane.applyCss();
HBox hboxDialogPane = (HBox) dialogPane.lookup(".container");
hboxDialogPane.getChildren().add(spacer);
}
Call it in this way: centerButtons(dialog.getDialogPane);
It's a kind of hack, but you could just do something like this:
okButton.translateXProperty().bind(okButton.prefWidthProperty().divide(-2));
The DialogPane is horizontal centered, so subtracting the okButton's half width will do the trick.
But I think this is a really dirty solution ;-)
Based on #ManuelSeiche's answer, here is how to compute exact distance to the center:
#FXML private Dialog<ButtonType> dialog;
#FXML private ButtonType btClose;
#FXML
private void initialize()
{
dialog.setOnShown(event ->
{
Platform.runLater(() ->
{
Button btnClose = (Button) dialog.getDialogPane().lookupButton(btClose);
HBox hBox = (HBox) btnClose.getParent();
double translateAmount = hBox.getWidth() / 2.0 - btnClose.getWidth() / 2.0 - hBox.getPadding().getLeft();
btnClose.translateXProperty().set(-translateAmount);
});
});
}
With Textflow.setMaxWidth(double) I can achieve text wrapping.
But how can I adjust the width of TextFlow afterwards so that it is based on the actual wrapping position?
In other words, how to let the TextFlow bounds snap to all of its children Text bounds to get rid of the empty space on the right:
**Edit** I have made some progress on this issue. My class derived from TextFlow now contains:
double maxChildWidth = 0;
for (Node child : getManagedChildren()) {
double childWidth = child.getLayoutBounds().getWidth();
maxChildWidth = Math.max(maxChildWidth, childWidth);
}
double insetWidth = getInsets().getLeft() + getInsets().getRight();
double adjustedWidth = maxChildWidth + insetWidth;
setMaxWidth(adjustedWidth);
Unfortunately, this approach does not seem to be accurate yet, since it results in a second text flow change in some cases.
The solution you posted in your question seemed to work OK for me on testing.
In the image below the scene has been resized to trigger wrapping of the TextFlow (shown in red). On the right, the TextFlow does not exactly wrap at the last visible character, because the last character in the line is a space, so the wrapping occurs after the space. Taking into account the TextFlow bounds are flush to all of the text as required.
If you unclamp the max width, you get the default behavior of the text box which is to have the width grow (as you can see below).
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.text.*;
import javafx.stage.Stage;
public class TextFlowWrapper extends Application {
#Override
public void start(Stage stage) {
TextFlow textFlow = new TextFlow(
new Text(
"Box with long description that should be wrapped"
)
) {
#Override
protected void layoutChildren() {
super.layoutChildren();
double maxChildWidth = 0;
for (Node child : getManagedChildren()) {
double childWidth = child.getLayoutBounds().getWidth();
maxChildWidth = Math.max(maxChildWidth, childWidth);
}
double insetWidth = getInsets().getLeft() + getInsets().getRight();
double adjustedWidth = maxChildWidth + insetWidth;
setMaxWidth(adjustedWidth);
}
};
textFlow.setStyle("-fx-background-color: red");
textFlow.setMaxWidth(Control.USE_PREF_SIZE);
textFlow.setMaxHeight(Control.USE_PREF_SIZE);
Button unclamp = new Button("Unclamp max width");
unclamp.setOnAction(e -> textFlow.setMaxWidth(Double.MAX_VALUE));
StackPane wrappedText = new StackPane(textFlow);
VBox vbox = new VBox(
unclamp,
wrappedText
);
VBox.setVgrow(wrappedText, Priority.ALWAYS);
Scene scene = new Scene(
vbox
);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The solution is a little bit of a hack, you might want to ask one of the JavaFX developers for some input on the openjfx developer list. Maybe some additional api like a boolean pack property which would conditionally update the internal textflow layout algorithm to automatically pack the textflow to a minimal size would be a way to do this better. Part of the difficulty is that it is a hard question to write in such a way that the issue is easily understandable. It is also hard to say if this is some kind of corner case or if it is something that more people will encounter (hence perhaps justifying the complexity of an additional API).
i was searching in google for hours and i still cant find the right answer, so i have a last chance to come here and ask.
i'm making school year JAVA FX project. I'm using NetBeans.
I have a point that i can see on the application i have. The problem is: I would like to have a big map (background) and I need to be able to move with my view. For example move by 50 to the right (x).
I have Application where I use Stage, Scene, StackPane.
I heard something about Dimensions in Java, but i can't use it in javafx application. Is there something similar, what can I use in my Application?
Thank you very much.
What I think you are asking for is a Scene with a map (represented as an Image) in the background and controls layered on top of the map to allow interaction with the map at certain positions. Your question is a little unclear, so I'm not exactly sure if that is what you are asking.
If so, here is some sample code to implement that.
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.event.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
/** Constructs a scene with a pannable Map background. */
public class PannableView extends Application {
private Image backgroundImage;
#Override public void init() {
backgroundImage = new Image("https://www.narniaweb.com/wp-content/uploads/2009/08/NarniaMap.jpg");
}
#Override public void start(Stage stage) {
stage.setTitle("Drag the mouse to pan the map");
// construct the scene contents over a stacked background.
StackPane layout = new StackPane();
layout.getChildren().setAll(
new ImageView(backgroundImage),
createKillButton()
);
// wrap the scene contents in a pannable scroll pane.
ScrollPane scroll = createScrollPane(layout);
// show the scene.
Scene scene = new Scene(scroll);
stage.setScene(scene);
stage.show();
// bind the preferred size of the scroll area to the size of the scene.
scroll.prefWidthProperty().bind(scene.widthProperty());
scroll.prefHeightProperty().bind(scene.widthProperty());
// center the scroll contents.
scroll.setHvalue(scroll.getHmin() + (scroll.getHmax() - scroll.getHmin()) / 2);
scroll.setVvalue(scroll.getVmin() + (scroll.getVmax() - scroll.getVmin()) / 2);
}
/** #return a control to place on the scene. */
private Button createKillButton() {
final Button killButton = new Button("Kill the evil witch");
killButton.setStyle("-fx-base: firebrick;");
killButton.setTranslateX(65);
killButton.setTranslateY(-130);
killButton.setOnAction(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent t) {
killButton.setStyle("-fx-base: forestgreen;");
killButton.setText("Ding-Dong! The Witch is Dead");
}
});
return killButton;
}
/** #return a ScrollPane which scrolls the layout. */
private ScrollPane createScrollPane(Pane layout) {
ScrollPane scroll = new ScrollPane();
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setPannable(true);
scroll.setPrefSize(800, 600);
scroll.setContent(layout);
return scroll;
}
public static void main(String[] args) { launch(args); }
}
For the example use the mouse (or probably touch commands or trackpad scroll gestures - though I haven't a touch screen or trackpad to test it) to drag the map around. Click on the button to "Kill the evil witch".
The solution works by:
Creating an ImageView to hold the background map.
Constructing the scene contents in a StackPane over the stacked background ImageView.
Wrapping the scene in a ScrollPane bound to the scene's size.
Setting properties on the ScrollPane to make it pannable.