I have this piece of code here that has a borderpane as a parent and 2 other panes, one is a minesweeper game and the other is an empty pane with a black background, the minesweeper game is set to be in the center of the borderpane and the empty pane is set to be in the bottom. Right now when I run the code, it will only show the minesweeper game and not the empty pane (can be seen in the image). How do I make it show the empty pane at the bottom of the borderpane?
(Layout as of right now)
package project;
import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class MinesweeperApp extends Application {
private static final int TILE_SIZE = 40;
private static final int W = 800;
private static final int H = 800;
private static final int X_TILES = W / TILE_SIZE;
private static final int Y_TILES = H / TILE_SIZE;
private Tile[][] grid = new Tile[X_TILES][Y_TILES];
// private ArrayList[][] grid = new ArrayList[X_TILES][Y_TILES];
private Scene scene;
private Parent createContent() {
BorderPane root = new BorderPane();
Pane middlepane = new Pane();
Pane lowerPane = new Pane();
lowerPane.setStyle("-fx-background-color: black;");
root.setTop(lowerPane);
root.setBottom(middlepane);
middlepane.setPrefSize(W, H);
for (int y = 0; y < Y_TILES; y++) {
for (int x = 0; x < X_TILES; x++) {
Tile tile = new Tile(x, y, Math.random() < 0.2);
grid[x][y] = tile;
middlepane.getChildren().add(tile);
}
}
for (int y = 0; y < Y_TILES; y++) {
for (int x = 0; x < X_TILES; x++) {
Tile tile = grid[x][y];
if (tile.hasBomb)
continue;
long bombs = getNeighbors(tile).stream().filter(t -> t.hasBomb).count();
if (bombs > 0)
tile.text.setText(String.valueOf(bombs));
}
}
return root;
}
private List<Tile> getNeighbors(Tile tile) {
List<Tile> neighbors = new ArrayList<>();
// ttt
// tXt
// ttt
int[] points = new int[] {
-1, -1,
-1, 0,
-1, 1,
0, -1,
0, 1,
1, -1,
1, 0,
1, 1
};
for (int i = 0; i < points.length; i++) {
int dx = points[i];
int dy = points[++i];
int newX = tile.x + dx;
int newY = tile.y + dy;
if (newX >= 0 && newX < X_TILES
&& newY >= 0 && newY < Y_TILES) {
neighbors.add(grid[newX][newY]);
}
}
return neighbors;
}
private class Tile extends StackPane {
private int x, y;
private boolean hasBomb;
private boolean isOpen = false;
private Rectangle border = new Rectangle(TILE_SIZE - 2, TILE_SIZE - 2);
private Text text = new Text();
public Tile(int x, int y, boolean hasBomb) {
this.x = x;
this.y = y;
this.hasBomb = hasBomb;
border.setStroke(null);
border.setFill(Color.NAVY);
text.setFont(Font.font(18));
text.setText(hasBomb ? "X" : "");
text.setVisible(false);
getChildren().addAll(border, text);
setTranslateX(x * TILE_SIZE);
setTranslateY(y * TILE_SIZE);
setOnMouseClicked(e -> open());
}
public void open() {
System.out.println("clicked");
System.out.println("x: " + this.x + " " + "y: " + this.y);
if (isOpen){
return;
}
if (hasBomb) {
System.out.println("Game Over");
scene.setRoot(createContent());
return;
}
isOpen = true;
text.setVisible(true);
border.setFill(Color.RED);
if (text.getText().isEmpty()) {
getNeighbors(this).forEach(Tile::open);
}
}
}
#Override
public void start(Stage stage) throws Exception {
scene = new Scene(createContent());
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Absent children, an empty Pane has zero width and height. Try adding non-empty content to the top and bottom to see the effect:
root.setTop(new Label("top"));
root.setCenter(middlepane);
root.setBottom(new Label("bottom"));
As an aside, your original root.setBottom(middlepane) may be misleading.
Setting Constraints
Set size constraints on a pane to “see” a Pane which has no content.
This will make the pane take up space in the layout.
To make the app behave as you desire, configure the pane's:
min width or height, OR
pref width or height, OR
any combination of those.
For example, to set a min height for the top pane in a border pane:
BorderPane borderPane = new BorderPane();
Pane top = new Pane();
top.setMinHeight(15);
borderPane.setTop(top);
Hiding Content
Or you could put some content in the pane and make it invisible until it is needed, while still reserving the space required to display it.
For example:
Label header = new Label("header");
header.setVisible(false);
Pane top = new Pane(header);
borderPane.setTop(top);
This works because things in the scene graph which are not visible still take up layout space, even though they are not displayed, unless you also call setManaged(false).
For the specific case of a label, you wouldn’t need the visibility setting, you could just set the label to an empty string and it would still take up room, even though nothing would be shown.
For myself, I try to avoid sizing hints where I can, so I would use an empty label. Or, set the visibility, for a more complex pane. That way I let the layout engine calculate the appropriate amount of space to reserve.
Spacing Content
This doesn't apply in your case with border pane, but for other layouts like HBox and VBox you might want to push a node all the way right or to the bottom of the layout. You can do that by adding an empty pane before the last node and setting a layout constraint on the pane like this:
HBox.setHgrow(spacer, Priority.ALWAYS);
This technique is explained more in the answer to:
How to align a button right in JavaFX?
Related
I'm trying to recreate the "snake" game using GridPane in JavaFX. My code seems to run properly except for this specific error, where anytime I use my keys to traverse the green node (titled head) toward the yellow node (titled food), the rows or columns of the grid seem to shorten by one unit, causing the grid to collapse in some manner. Is there a way to stop the GridPane from resizing? Below is my code:
package snake;
import java.util.ArrayList;
import java.util.Random;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.TilePane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Modality;
import javafx.stage.Stage;
public class snakemain extends Application {
Random random = new Random();
int posX = random.nextInt(21), posY = random.nextInt(21), foodposX = random.nextInt(24), foodposY = random.nextInt(24);
public static void main(String... args) {
launch(args);
}
public void start(Stage stage) {
//Creates Scene and stage setting + gridpane
stage.setTitle("Snake by Yeldor");
GridPane gridpane = new GridPane();
Random random = new Random();
Scene scene = new Scene(gridpane, 500, 500);
scene.setFill(Color.WHITE);
gridpane.setGridLinesVisible(true);
gridpane.setHgap(20);
gridpane.setVgap(20);
//creates head of snake, Arraylist of snakes body parts, and food consumable for snake
ArrayList<Rectangle> snakeBody = new ArrayList<Rectangle>();
Rectangle head = new Rectangle(20,20,Color.GREEN.brighter().brighter());
//invisible block to manage grid
Rectangle food = new Rectangle(20,20, Color.YELLOW.brighter().brighter());
//adds rectangles to grippane
gridpane.add(head, posX, posY);
gridpane.add(food, foodposX, foodposY);
//makes food non mangaged by the gridpane
scene.setOnKeyPressed(e ->
{
String s = e.getCode().toString();
try {
switch(s) {
case "W": gridpane.getChildren().remove(head);
gridpane.add(head, posX, --posY);
break;
case "A": gridpane.getChildren().remove(head);
gridpane.add(head, --posX, posY);
break;
case "S": gridpane.getChildren().remove(head);
gridpane.add(head, posX, ++posY);
break;
case "D": gridpane.getChildren().remove(head);
gridpane.add(head, ++posX, posY);
break;
}
if (posX == 24 || posY == 24 || posX == -1 || posY == -1) {
missionFailed();
stage.close();
}
}
catch(IllegalArgumentException error) {
missionFailed();
stage.close();
}
});
stage.setScene(scene);
stage.show();
}
void missionFailed() {
Stage failedPopup = new Stage();
failedPopup.setTitle("You Died!");
failedPopup.initModality(Modality.APPLICATION_MODAL);
Button OK = new Button("OK");
Group group = new Group();
Scene miniScene = new Scene(group, 150, 100);
group.getChildren().add(OK);
failedPopup.setScene(miniScene);
failedPopup.show();
OK.setOnMouseClicked(q -> {
failedPopup.close();
});
}
}
Using hgap and vgap does not modify the column size, it modifies the space between columns. If you e.g. place the Rectangles next to each other horizontally (same row index, column index differing by 1), the distance between both nodes is vgap; there are exactly 2 columns that are filled and have a width != 0. Now move the Rectangles to the same cell and only a single column with a width != 0 leading to unexpected results.
Instead you should restrict the size of the rows/columns:
// gridpane.setHgap(20);
// gridpane.setVgap(20);
int rows = 500 / 20;
int columns = 500 / 20;
RowConstraints rConstraints = new RowConstraints(20);
ColumnConstraints cConstraints = new ColumnConstraints(20);
for (int i = 0; i < columns; i++) {
gridpane.getColumnConstraints().add(cConstraints);
}
for (int i = 0; i < rows; i++) {
gridpane.getRowConstraints().add(rConstraints);
}
Another Solution that worked for me was to fill up the whole Area with transparent Rectangulars and just set the color of the Rectangulars used at the moment.
I have a few rectangles that I assign rotation one by one ((javafx.scene.shape.Rectangle) shape).rotateProperty().bind(rotate);
For example, 45 degrees
My rectangle rotates around its center. Please tell me how to make the enemy a few right-angles around their common center.
To get something like this
this.rotate.addListener((obs, old, fresh) -> {
for (VObject vObject : children ) {
vObject.rotate.set(this.rotate.get());
}
});
This is how I add rotation. How can I specify the angle
Update: I used the advice below and now I set the rotation individually for each rectangle. (The selection is still a bit wrong)
this.rotate.addListener((obs, old, fresh) -> {
Rotate groupRotate = new Rotate(rotate.get(),
this.x.getValue().doubleValue() + this.width.getValue().doubleValue() / 2 ,
this.y.getValue().doubleValue() + this.height.getValue().doubleValue() / 2);
for (VObject vObject : children ) {
vObject.getShape().getTransforms().clear();
vObject.getShape().getTransforms().add(groupRotate);
}
});
But now the axis also rotates depending on the rotation.
Can I set the rotation to the rectangles without turning the coordinate axis?
If you put all rectangles into a common Group, you can rotate them at once:
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class RotateAllApplication extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
BorderPane root = new BorderPane();
// the common group
Group group = new Group();
group.getChildren().addAll(new Rectangle(10, 10, 80, 40), //
new Rectangle(110, 10, 80, 40), //
new Rectangle(10, 110, 80, 40), //
new Rectangle(110, 110, 80, 40));
// rotate the group instead of each rectangle
group.setRotate(45.0);
root.setCenter(group);
primaryStage.setScene(new Scene(root, 600, 400));
primaryStage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
}
Update: If you don't want to create a parent Group object, you can apply the same rotation transformation to each child instead. While Node#setRotate(double) always rotates around the center of the node, adding a transformation to Node#getTransforms() is more general and not restricted to simple rotations.
The following statement will apply a rotation around the point (100.0/100.0) of the parent coordinate system to all children in the list:
childrenList.forEach(child -> child.getTransforms().add(Transform.rotate(45.0, 100.0, 100.0)));
Use a Rotate transform and specify the appropriate pivot point:
#Override
public void start(Stage primaryStage) throws IOException {
Pane pane = new Pane();
pane.setPrefSize(600, 600);
Rectangle[] rects = new Rectangle[4];
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
Rectangle rect = new Rectangle(100 + i * 200, 100 + j * 100, 150, 50);
rects[i * 2 + j] = rect;
pane.getChildren().add(rect);
}
}
Slider slider = new Slider(0, 360, 0);
double minX = Double.POSITIVE_INFINITY;
double minY = Double.POSITIVE_INFINITY;
double maxX = Double.NEGATIVE_INFINITY;
double maxY = Double.NEGATIVE_INFINITY;
Rotate rotate = new Rotate();
// find pivot point
for (Rectangle rect : rects) {
double val = rect.getX();
if (minX > val) {
minX = val;
}
val += rect.getWidth();
if (maxX < val) {
maxX = val;
}
val = rect.getY();
if (minY > val) {
minY = val;
}
val += rect.getHeight();
if (maxY < val) {
maxY = val;
}
rect.getTransforms().add(rotate);
}
rotate.setPivotX(0.5 * (maxX + minX));
rotate.setPivotY(0.5 * (maxY + minY));
rotate.angleProperty().bind(slider.valueProperty());
Scene scene = new Scene(new VBox(10, pane, slider));
primaryStage.setScene(scene);
primaryStage.sizeToScene();
primaryStage.show();
}
If you're planing to apply multiple transformations, you may need to adjust the code for finding the pivot point to use transforms for calculating the bounds...
After a player moves a stone (Candycrush-like game), in the logic i gather information about if the player move result in a Structure, which then needs to be exploded. Ofcourse once a Structure gets exploded by removing the structure elements and dropping the stones above, new Structures can appear that also needs to be exploded sequentially.
For that i have an AnimationData class that has a List of ExplosionData, which has the size of the found structures, caused by the initial player move.
My code works fine for just ONE explosion but messes up if there are multiple explosions. The problem is that the loop doesn't wait until the explosion animation is done before it continues the iteration.
Clarification : method updateGui, loop inside switchAnimation.setOnFinished
Visually :
Clips of a single Explosion and multiple Explosion that i recorded
public void updateGui(AnimationData aData) {
final int rowHeight = (int) (boardGPane.getHeight() / boardGPane.getRowConstraints().size());
Coords switchSourceCoords = aData.getSwitchSourceCoords();
Coords switchTargetCoords = aData.getSwitchTargetCoords();
// Apply player move
ParallelTransition switchAnimation = switchStones(switchSourceCoords, switchTargetCoords);
switchAnimation.play();
// Revert switch, if the move was invalid
if (aData.geteData().isEmpty()) {
switchAnimation.setOnFinished(event -> {
ParallelTransition switchBackAnimation = switchStones(switchSourceCoords, switchTargetCoords);
switchBackAnimation.play();
});
} else {
switchAnimation.setOnFinished(event -> {
// Animate explosions for every found Structure
for (ExplosionData eData : aData.geteData()) {
SequentialTransition explosionAnimation = new SequentialTransition();
// Coordinates of where the bonusStone appears
Coords bonusSource = eData.getBonusSourceCoords();
// Coordinates of where the bonusStone need to be repositioned
Coords bonusTarget = eData.getBonusTargetCoords();
// Remove all Structure elements and make Stones above drop to their target
// positions. Also translate them back to the same position for the animation
removeStructureAndReplaceIvs(eData, bonusTarget, bonusSource, rowHeight);
// This shall only proceed if the animation involves handeling a bonusStone
if (bonusSource != null && bonusTarget != null) {
int rowsToMove = bonusTarget.getRow() - bonusSource.getRow();
ImageView bonusIv = (ImageView) JavaFXGUI.getNodeFromGridPane(boardGPane, bonusTarget.getCol(), bonusTarget.getRow());
// BonusStone shall fade in at the source Position
explosionAnimation = bonusStoneFadeIn(explosionAnimation, rowsToMove, bonusIv, rowHeight);
// Translate to targetPosition, if sourcePosition is not equal to targetPosition
explosionAnimation = bonusStoneMoveToTargetCoords(explosionAnimation, rowsToMove, bonusIv, rowHeight);
}
// Make the Stone ImageViews translate from their origin position to their new target positions
explosionAnimation = dropAndFillUpEmptySpace(explosionAnimation, eData, bonusTarget, bonusSource, rowHeight);
explosionAnimation.play();
}
});
}
}
private void removeStructureAndReplaceIvs(ExplosionData eData,
Coords bonusTargetCoords,
Coords bonusSourceCoords,
final int rowHeight) {
// Removing the Structure and all stones above by deleting the ImageViews col by col
for (DropInfo info : eData.getExplosionInfo()) {
// Coordinates of the Structure element that is going to be removed in this col
int col = info.getCoords().getCol();
int row = info.getCoords().getRow();
// If a bonusStone will apear, the heightOffset gets reduced by one
int offset = getAppropiateOffset(bonusTargetCoords, info, col);
// Remove the Structure and all ImageViews above
removeImageViewsFromCells(col, row, row + 1);
List<String> stoneToken = info.getFallingStoneToken();
for (int r = row, i = 0; r >= 0; --r, ++i) {
// Fill up removed Cells with new ImageViews values
ImageView newIv = new ImageView(new Image(preImagePath + stoneToken.get(i) + ".png"));
// Place each iv to their target Coords
addImageViewToPane(newIv, col, r);
// Translate all non-bonusStones to the position they were placed before
if (ignoreBonusTargetCoordinates(bonusTargetCoords, bonusSourceCoords, r, col)) {
newIv.setTranslateY(-rowHeight * offset);
}
}
}
}
// If the removed Structure results to generate a bonusStone, make it fade in at source position
private SequentialTransition bonusStoneFadeIn(SequentialTransition explosionAnimation,
int sourceToTargetDiff,
ImageView bonusIv,
final int rowHeight) {
FadeTransition bonusFadeIn = new FadeTransition(Duration.seconds(1), bonusIv);
bonusFadeIn.setFromValue(0f);
bonusFadeIn.setToValue(1f);
// If the target Position is not the same, place it to target and translate to source position
if (sourceToTargetDiff > 0) {
bonusIv.setTranslateY(-rowHeight * sourceToTargetDiff);
}
explosionAnimation.getChildren().add(bonusFadeIn);
return explosionAnimation;
}
// If the bonusStone must be moved from source Coordinates to target Coordinates
private SequentialTransition bonusStoneMoveToTargetCoords(SequentialTransition explosionAnimation,
int sourceToTargetDiff,
ImageView bonusIv,
final int rowHeight) {
// Difference in row from bonusSourceCoordinates to bonusTargetCoordinates
if (sourceToTargetDiff > 0) {
TranslateTransition moveToTargetCoords = new TranslateTransition(Duration.seconds(1), bonusIv);
moveToTargetCoords.fromYProperty().set(-rowHeight * sourceToTargetDiff);
moveToTargetCoords.toYProperty().set(0);
explosionAnimation.getChildren().add(moveToTargetCoords);
}
return explosionAnimation;
}
private SequentialTransition dropAndFillUpEmptySpace(SequentialTransition explosionAnimation,
ExplosionData eData,
Coords bonusTargetCoords,
Coords bonusSourceCoords,
final int rowHeight) {
ParallelTransition animateDrop = new ParallelTransition();
for (int i = 0; i < eData.getExplosionInfo().size(); i++) {
// List of all stoneToken to create respective ImageViews for each col
List<DropInfo> allDropInfo = eData.getExplosionInfo();
int col = allDropInfo.get(i).getCoords().getCol();
int row = allDropInfo.get(i).getCoords().getRow();
// If a bonusStone will apear, the heightOffset gets reduced by one
int offset = getAppropiateOffset(bonusTargetCoords, allDropInfo.get(i), col);
for (int r = row; r >= 0; --r) {
// Drop all Stones above the removed Structure to fill up the empty space
// Ignore possible bonusStones since they are being animated seperately
if (ignoreBonusTargetCoordinates(bonusTargetCoords, bonusSourceCoords, r, col)) {
ImageView iv = (ImageView) JavaFXGUI.getNodeFromGridPane(boardGPane, col, r);
TranslateTransition tt = new TranslateTransition(Duration.millis(1500), iv);
tt.fromYProperty().set(-rowHeight * offset);
tt.toYProperty().set(0);
animateDrop.getChildren().add(tt);
}
}
}
explosionAnimation.getChildren().add(animateDrop);
return explosionAnimation;
}
private int getAppropiateOffset(Coords bonusTargetCoords, DropInfo dropInfo, int col) {
int bonusOffset = (bonusTargetCoords != null && col == bonusTargetCoords.getCol()) ? 1 : 0;
return dropInfo.getHeightOffset() - bonusOffset;
}
private boolean ignoreBonusTargetCoordinates(Coords bonusTargetCoords,
Coords bonusSourceCoords,
int row,
int col) {
return bonusSourceCoords == null
|| bonusTargetCoords != null && col != bonusTargetCoords.getCol()
|| bonusTargetCoords != null && row != bonusTargetCoords.getRow();
}
A SequentialTransition can be made up of other SequentialTransitions. For your code you could create a "master" SequentialTransition and build it up with each SequentialTransition you create per iteration of the for loop. Then you would play the master transition.
switchAnimation.setOnFinished(event -> {
SequentialTransition masterAnimation = new SequentialTransition();
for (ExplosionData eData : aData.geteData()) {
SequentialTransition explosionAnimation = new SequentialTransition();
// ... configure the explosionAnimation ...
masterAnimation.getChildren().add(explosionAnimation); // add to masterAnimation
}
masterAnimation.play(); // play all the explosionAnimations in squential order
});
The reason your code doesn't wait for the animation to finish before moving on to the next iteration of the loop is because Animation.play() is an "asynchronous call".
When you call play() the animation gets scheduled in the background with some internal clock/timer and the method returns immediately.
Plays Animation from current position in the direction indicated by
rate. If the Animation is running, it has no effect.
When rate > 0 (forward play), if an Animation is already positioned at
the end, the first cycle will not be played, it is considered to have
already finished. This also applies to a backward (rate < 0) cycle if
an Animation is positioned at the beginning. However, if the Animation
has cycleCount > 1, following cycle(s) will be played as usual.
When the Animation reaches the end, the Animation is stopped and the
play head remains at the end.
To play an Animation backwards from the end:
animation.setRate(negative rate);
animation.jumpTo(overall duration of animation);
animation.play();
Note:
play() is an asynchronous call, the Animation may not start immediately.
(empasis mine)
Here's a workable example. It takes a Rectangle and translates it to each corner of the scene. Each "translate-to-corner" is a separate animation which also involves rotating the Rectangle and changes its color. Then all "translate-to-corner" animations are put into one SequentialTransition. The Button at the top is disabled when clicked and re-enabled when the master SequentialTransition finishes.
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.ParallelTransition;
import javafx.animation.RotateTransition;
import javafx.animation.SequentialTransition;
import javafx.animation.Timeline;
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Separator;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Main extends Application {
private Button playBtn;
private StackPane groupParent;
private Rectangle rectangle;
#Override
public void start(Stage primaryStage) {
playBtn = new Button("Play Animation");
playBtn.setOnAction(ae -> {
ae.consume();
playBtn.setDisable(true);
playAnimation();
});
HBox btnBox = new HBox(playBtn);
btnBox.setAlignment(Pos.CENTER);
btnBox.setPadding(new Insets(8));
rectangle = new Rectangle(150, 100, Color.BLUE);
groupParent = new StackPane(new Group(rectangle));
groupParent.getChildren().get(0).setManaged(false);
VBox root = new VBox(btnBox, new Separator(), groupParent);
root.setMaxSize(600, 400);
root.setAlignment(Pos.CENTER);
VBox.setVgrow(groupParent, Priority.ALWAYS);
Scene scene = new Scene(root, 600, 400);
primaryStage.setScene(scene);
primaryStage.setTitle("Animation");
primaryStage.setResizable(false);
primaryStage.show();
}
private void playAnimation() {
double maxX = groupParent.getWidth() - rectangle.getWidth();
double maxY = groupParent.getHeight() - rectangle.getHeight();
ParallelTransition pt1 = createAnimation(-25, maxY - 25, 90, Color.FIREBRICK);
ParallelTransition pt2 = createAnimation(maxX, maxY, 180, Color.BLUE);
ParallelTransition pt3 = createAnimation(maxX + 25, 25, 270, Color.FIREBRICK);
ParallelTransition pt4 = createAnimation(0, 0, 360, Color.BLUE);
SequentialTransition st = new SequentialTransition(rectangle, pt1, pt2, pt3, pt4);
st.setOnFinished(ae -> {
ae.consume();
rectangle.setTranslateX(0);
rectangle.setTranslateY(0);
rectangle.setRotate(0);
playBtn.setDisable(false);
});
st.play();
}
private ParallelTransition createAnimation(double x, double y, double r, Color c) {
TranslateTransition tt = new TranslateTransition(Duration.seconds(1.0));
tt.setToX(x);
tt.setToY(y);
RotateTransition rt = new RotateTransition(Duration.seconds(1));
rt.setToAngle(r);
Timeline tl = new Timeline(new KeyFrame(Duration.seconds(1), new KeyValue(rectangle.fillProperty(), c)));
return new ParallelTransition(tt, rt, tl);
}
}
I made a 2D grid of ASCII characters before in which i was able to move an ascii character around. Now i want to take it to the next level by making a "visualised" version of it in a javafx window. I have tried making 2 image objects, one with a black square inside of it, and one with a white one and then putting those 2 objects multiple times inside a 2D grid like this:
Image[][] Grid = {
{B,W,B,B,B,B,B,B,B,B,B},
{B,B,B,B,W,W,W,B,B,B,B},
{B,B,B,B,B,B,B,B,B,B,B}
};
The problem is that the only way i know how to display them, is by making an imageview object for each index and if i were to say, want a 25X25 grid, that would mean i would have to make 625 imageview objects which would obviously be ridiculous.
I also tried simply putting the grid indexes one by one into the pane like this:
HBox gameLayout = new HBox(Grid[1][1], Grid[1][2], Grid[1][3]);
but that gives me a "invocationTargetException".
My goal is to be able to make snake by specifically targeting and manipulating grid elements. I want the square-color/imageView/rectangle/whatever to change when i change the value of a "B"array element to "W" (white) but the things i'v tried are either very inefficient or just don't work.
It's not at all clear what your objection is to creating multiple ImageViews. Since they can refer to the same Image instance, this should be fairly efficient (the image data doesn't need to be replicated).
This seems to work just fine:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class TiledBoard extends Application {
private final int tileSize = 30 ;
#Override
public void start(Stage primaryStage) {
Image b = createImage(Color.BLACK);
Image w = createImage(Color.WHITE);
Image[][] grid = {
{b,w,b,b,b,b,b,b,b,b,b},
{b,b,b,b,w,w,w,b,b,b,b},
{b,b,b,b,b,b,b,b,b,b,b}
};
GridPane gridPane = new GridPane();
// for visualizing the different squares:
gridPane.setHgap(2);
gridPane.setVgap(2);
gridPane.setStyle("-fx-background-color: grey;");
for (int y = 0 ; y < grid.length ; y++) {
for (int x = 0 ; x < grid[y].length ; x++) {
ImageView imageView = new ImageView(grid[y][x]);
imageView.setFitWidth(tileSize);
imageView.setFitHeight(tileSize);
gridPane.add(imageView, x, y);
}
}
Scene scene = new Scene(gridPane);
primaryStage.setScene(scene);
primaryStage.show();
}
private Image createImage(Color color) {
WritableImage image = new WritableImage(1, 1);
image.getPixelWriter().setColor(0, 0, color);
return image ;
}
public static void main(String[] args) {
launch(args);
}
}
You could do this with some kind of Shape (e.g. Rectangle), if you prefer:
#Override
public void start(Stage primaryStage) {
Color b = Color.BLACK;
Color w = Color.WHITE;
Color[][] grid = {
{b,w,b,b,b,b,b,b,b,b,b},
{b,b,b,b,w,w,w,b,b,b,b},
{b,b,b,b,b,b,b,b,b,b,b}
};
GridPane gridPane = new GridPane();
// for visualizing the different squares:
gridPane.setHgap(2);
gridPane.setVgap(2);
gridPane.setStyle("-fx-background-color: grey;");
for (int y = 0 ; y < grid.length ; y++) {
for (int x = 0 ; x < grid[y].length ; x++) {
Rectangle rect = new Rectangle(tileSize, tileSize, grid[y][x]);
gridPane.add(rect, x, y);
}
}
Scene scene = new Scene(gridPane);
primaryStage.setScene(scene);
primaryStage.show();
}
or with a Region:
#Override
public void start(Stage primaryStage) {
Color b = Color.BLACK;
Color w = Color.WHITE;
Color[][] grid = {
{b,w,b,b,b,b,b,b,b,b,b},
{b,b,b,b,w,w,w,b,b,b,b},
{b,b,b,b,b,b,b,b,b,b,b}
};
GridPane gridPane = new GridPane();
// for visualizing the different squares:
gridPane.setHgap(2);
gridPane.setVgap(2);
gridPane.setStyle("-fx-background-color: grey;");
for (int y = 0 ; y < grid.length ; y++) {
for (int x = 0 ; x < grid[y].length ; x++) {
Region rect = new Region();
rect.setMinSize(tileSize, tileSize);
rect.setPrefSize(tileSize, tileSize);
rect.setMaxSize(tileSize, tileSize);
rect.setBackground(new Background(new BackgroundFill(grid[y][x], CornerRadii.EMPTY, Insets.EMPTY)));
gridPane.add(rect, x, y);
}
}
Scene scene = new Scene(gridPane);
primaryStage.setScene(scene);
primaryStage.show();
}
Which of these is better really just depends on what else you want to do with them.
I'm trying to code a zoom-able image in a JScrollPane.
When the image is fully zoomed out it should be centered horizontally and vertically. When both scroll bars have appeared the zooming should always happen relative to the mouse coordinate, i.e. the same point of the image should be under the mouse before and after the zoom event.
I have almost achieves my goal. Unfortunately the "scrollPane.getViewport().setViewPosition()" method sometimes fails to update the view position correctly. Calling the method twice (hack!) overcomes the issue in most cases, but the view still flickers.
I have no explanation as to why this is happening. However I'm confident that it's not a math problem.
Below is a MWE. To see what my problem is in particular you can do the following:
Zoom in until you have some scroll bars (200% zoom or so)
Scroll into the bottom right corner by clicking the scroll bars
Place the mouse in the corner and zoom in twice. The second time you'll see how the scroll position jumps towards the center.
I would really appreciate if someone could tell me where the problem lies. Thank you!
package com.vitco;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;
import java.util.Random;
/**
* Zoom-able scroll panel test case
*/
public class ZoomScrollPanel {
// the size of our image
private final static int IMAGE_SIZE = 600;
// create an image to display
private BufferedImage getImage() {
BufferedImage image = new BufferedImage(IMAGE_SIZE, IMAGE_SIZE, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
// draw the small pixel first
Random rand = new Random();
for (int x = 0; x < IMAGE_SIZE; x += 10) {
for (int y = 0; y < IMAGE_SIZE; y += 10) {
g.setColor(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255)));
g.fillRect(x, y, 10, 10);
}
}
// draw the larger transparent pixel second
for (int x = 0; x < IMAGE_SIZE; x += 100) {
for (int y = 0; y < IMAGE_SIZE; y += 100) {
g.setColor(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255), 180));
g.fillRect(x, y, 100, 100);
}
}
return image;
}
// the image panel that resizes according to zoom level
private class ImagePanel extends JPanel {
private final BufferedImage image = getImage();
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g.create();
g2.scale(scale, scale);
g2.drawImage(image, 0, 0, null);
g2.dispose();
}
#Override
public Dimension getPreferredSize() {
return new Dimension((int)Math.round(IMAGE_SIZE * scale), (int)Math.round(IMAGE_SIZE * scale));
}
}
// the current zoom level (100 means the image is shown in original size)
private double zoom = 100;
// the current scale (scale = zoom/100)
private double scale = 1;
// the last seen scale
private double lastScale = 1;
public void alignViewPort(Point mousePosition) {
// if the scale didn't change there is nothing we should do
if (scale != lastScale) {
// compute the factor by that the image zoom has changed
double scaleChange = scale / lastScale;
// compute the scaled mouse position
Point scaledMousePosition = new Point(
(int)Math.round(mousePosition.x * scaleChange),
(int)Math.round(mousePosition.y * scaleChange)
);
// retrieve the current viewport position
Point viewportPosition = scrollPane.getViewport().getViewPosition();
// compute the new viewport position
Point newViewportPosition = new Point(
viewportPosition.x + scaledMousePosition.x - mousePosition.x,
viewportPosition.y + scaledMousePosition.y - mousePosition.y
);
// update the viewport position
// IMPORTANT: This call doesn't always update the viewport position. If the call is made twice
// it works correctly. However the screen still "flickers".
scrollPane.getViewport().setViewPosition(newViewportPosition);
// debug
if (!newViewportPosition.equals(scrollPane.getViewport().getViewPosition())) {
System.out.println("Error: " + newViewportPosition + " != " + scrollPane.getViewport().getViewPosition());
}
// remember the last scale
lastScale = scale;
}
}
// reference to the scroll pane container
private final JScrollPane scrollPane;
// constructor
public ZoomScrollPanel() {
// initialize the frame
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setSize(600, 600);
// initialize the components
final ImagePanel imagePanel = new ImagePanel();
final JPanel centerPanel = new JPanel();
centerPanel.setLayout(new GridBagLayout());
centerPanel.add(imagePanel);
scrollPane = new JScrollPane(centerPanel);
scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
frame.add(scrollPane);
// add mouse wheel listener
imagePanel.addMouseWheelListener(new MouseAdapter() {
#Override
public void mouseWheelMoved(MouseWheelEvent e) {
super.mouseWheelMoved(e);
// check the rotation of the mousewheel
int rotation = e.getWheelRotation();
boolean zoomed = false;
if (rotation > 0) {
// only zoom out until no scrollbars are visible
if (scrollPane.getHeight() < imagePanel.getPreferredSize().getHeight() ||
scrollPane.getWidth() < imagePanel.getPreferredSize().getWidth()) {
zoom = zoom / 1.3;
zoomed = true;
}
} else {
// zoom in until maximum zoom size is reached
double newCurrentZoom = zoom * 1.3;
if (newCurrentZoom < 1000) { // 1000 ~ 10 times zoom
zoom = newCurrentZoom;
zoomed = true;
}
}
// check if a zoom happened
if (zoomed) {
// compute the scale
scale = (float) (zoom / 100f);
// align our viewport
alignViewPort(e.getPoint());
// invalidate and repaint to update components
imagePanel.revalidate();
scrollPane.repaint();
}
}
});
// display our frame
frame.setVisible(true);
}
// the main method
public static void main(String[] args) {
new ZoomScrollPanel();
}
}
Note: I have also looked at the question here JScrollPane setViewPosition After "Zoom" but unfortunately the problem and solution are slightly different and do not apply.
Edit
I have solved the issue by using a hack, however I'm still no closer to understanding as to what the underlying problem is. What is happening is that when the setViewPosition is called some internal state changes trigger additional calls to setViewPosition. These additional calls only happen occasionally. When I'm blocking them everything works perfectly.
To fix the problem I simply introduced a new boolean variable "blocked = false;" and replaced the lines
scrollPane = new JScrollPane(centerPanel);
and
scrollPane.getViewport().setViewPosition(newViewportPosition);
with
scrollPane = new JScrollPane();
scrollPane.setViewport(new JViewport() {
private boolean inCall = false;
#Override
public void setViewPosition(Point pos) {
if (!inCall || !blocked) {
inCall = true;
super.setViewPosition(pos);
inCall = false;
}
}
});
scrollPane.getViewport().add(centerPanel);
and
blocked = true;
scrollPane.getViewport().setViewPosition(newViewportPosition);
blocked = false;
I would still really appreciate if someone could make sense of this!
Why does this hack work? Is there a cleaner way to achieve the same functionality?
Here is the completed, fully functional Code. I still don't understand why the hack is necessary, but at least it now works as expected:
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;
import java.util.Random;
/**
* Zoom-able scroll panel
*/
public class ZoomScrollPanel {
// the size of our image
private final static int IMAGE_SIZE = 600;
// create an image to display
private BufferedImage getImage() {
BufferedImage image = new BufferedImage(IMAGE_SIZE, IMAGE_SIZE, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
// draw the small pixel first
Random rand = new Random();
for (int x = 0; x < IMAGE_SIZE; x += 10) {
for (int y = 0; y < IMAGE_SIZE; y += 10) {
g.setColor(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255)));
g.fillRect(x, y, 10, 10);
}
}
// draw the larger transparent pixel second
for (int x = 0; x < IMAGE_SIZE; x += 100) {
for (int y = 0; y < IMAGE_SIZE; y += 100) {
g.setColor(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255), 180));
g.fillRect(x, y, 100, 100);
}
}
return image;
}
// the image panel that resizes according to zoom level
private class ImagePanel extends JPanel {
private final BufferedImage image = getImage();
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g.create();
g2.scale(scale, scale);
g2.drawImage(image, 0, 0, null);
g2.dispose();
}
#Override
public Dimension getPreferredSize() {
return new Dimension((int)Math.round(IMAGE_SIZE * scale), (int)Math.round(IMAGE_SIZE * scale));
}
}
// the current zoom level (100 means the image is shown in original size)
private double zoom = 100;
// the current scale (scale = zoom/100)
private double scale = 1;
// the last seen scale
private double lastScale = 1;
// true if currently executing setViewPosition
private boolean blocked = false;
public void alignViewPort(Point mousePosition) {
// if the scale didn't change there is nothing we should do
if (scale != lastScale) {
// compute the factor by that the image zoom has changed
double scaleChange = scale / lastScale;
// compute the scaled mouse position
Point scaledMousePosition = new Point(
(int)Math.round(mousePosition.x * scaleChange),
(int)Math.round(mousePosition.y * scaleChange)
);
// retrieve the current viewport position
Point viewportPosition = scrollPane.getViewport().getViewPosition();
// compute the new viewport position
Point newViewportPosition = new Point(
viewportPosition.x + scaledMousePosition.x - mousePosition.x,
viewportPosition.y + scaledMousePosition.y - mousePosition.y
);
// update the viewport position
blocked = true;
scrollPane.getViewport().setViewPosition(newViewportPosition);
blocked = false;
// remember the last scale
lastScale = scale;
}
}
// reference to the scroll pane container
private final JScrollPane scrollPane;
// constructor
public ZoomScrollPanel() {
// initialize the frame
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setSize(600, 600);
// initialize the components
final ImagePanel imagePanel = new ImagePanel();
final JPanel centerPanel = new JPanel();
centerPanel.setLayout(new GridBagLayout());
centerPanel.add(imagePanel);
scrollPane = new JScrollPane();
scrollPane.setViewport(new JViewport() {
private boolean inCall = false;
#Override
public void setViewPosition(Point pos) {
if (!inCall || !blocked) {
inCall = true;
super.setViewPosition(pos);
inCall = false;
}
}
});
scrollPane.getViewport().add(centerPanel);
scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
frame.add(scrollPane);
// add mouse wheel listener
imagePanel.addMouseWheelListener(new MouseAdapter() {
#Override
public void mouseWheelMoved(MouseWheelEvent e) {
super.mouseWheelMoved(e);
// check the rotation of the mousewheel
int rotation = e.getWheelRotation();
boolean zoomed = false;
if (rotation > 0) {
// only zoom out until no scrollbars are visible
if (scrollPane.getHeight() < imagePanel.getPreferredSize().getHeight() ||
scrollPane.getWidth() < imagePanel.getPreferredSize().getWidth()) {
zoom = zoom / 1.3;
zoomed = true;
}
} else {
// zoom in until maximum zoom size is reached
double newCurrentZoom = zoom * 1.3;
if (newCurrentZoom < 1000) { // 1000 ~ 10 times zoom
zoom = newCurrentZoom;
zoomed = true;
}
}
// check if a zoom happened
if (zoomed) {
// compute the scale
scale = (float) (zoom / 100f);
// align our viewport
alignViewPort(e.getPoint());
// invalidate and repaint to update components
imagePanel.revalidate();
scrollPane.repaint();
}
}
});
// display our frame
frame.setVisible(true);
}
// the main method
public static void main(String[] args) {
new ZoomScrollPanel();
}
}
Some time ago I was facing the same issue. I had some scalable/zoomable content (SWT widgets) stored in Viewport in JScrollPane and some features implemented to enable panning and zooming the content. I didn't look into your code if it's basically the same, but the issue that I was observing was completely the same. When zooming outside from the right/bottom side, sometimes, the view position jumped a little bit into the center (from my point-of-view that definitely points to a scale factor). Using doubled "setViewPosition" somehow enhanced the behavior but still not usable.
After some investigation, I've found out that the issue on my side was between the moment when I changed the scale factor of the content inside the scroll panel and the moment when view position was set in scroll panel. The thing is that scroll panel doesn't know about the content size updates until layout is done. So basically, it's updating the position based on old content size, extent size and view position.
So, at my side, this helped a lot.
// updating scroll panel content scale goes here
viewport.doLayout();
// setting view position in viewport goes here
Checking method BasicScrollPaneUI#syncScrollPaneWithViewport() was very useful on my side.
very useful example, excellent zoom at mouse pointer, here is the same code slightly modified to include mouse panning:
original code added taken from --> Scroll JScrollPane by dragging mouse (Java swing)
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;
import java.util.Random;
/**
* Zoom-able scroll panel
*/
// https://stackoverflow.com/questions/22649636/zoomable-jscrollpane-setviewposition-fails-to-update
public class ZoomPanScrollPanel {
// the size of our image
private final static int IMAGE_SIZE = 1600;
// create an image to display
private BufferedImage getImage() {
BufferedImage image = new BufferedImage(IMAGE_SIZE, IMAGE_SIZE, BufferedImage.TYPE_INT_ARGB);
Graphics g = image.getGraphics();
// draw the small pixel first
Random rand = new Random();
for (int x = 0; x < IMAGE_SIZE; x += 10) {
for (int y = 0; y < IMAGE_SIZE; y += 10) {
g.setColor(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255), rand.nextInt(255)));
g.fillRect(x, y, 10, 10);
}
}
// draw the larger transparent pixel second
for (int x = 0; x < IMAGE_SIZE; x += 100) {
for (int y = 0; y < IMAGE_SIZE; y += 100) {
g.setColor(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255), 180));
g.fillRect(x, y, 100, 100);
}
}
return image;
}
// the image panel that resizes according to zoom level
private class ImagePanel extends JPanel {
private final BufferedImage image = getImage();
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g.create();
g2.scale(scale, scale);
g2.drawImage(image, 0, 0, null);
g2.dispose();
}
#Override
public Dimension getPreferredSize() {
return new Dimension((int)Math.round(IMAGE_SIZE * scale), (int)Math.round(IMAGE_SIZE * scale));
}
}
// the current zoom level (100 means the image is shown in original size)
private double zoom = 100;
// the current scale (scale = zoom/100)
private double scale = 1;
// the last seen scale
private double lastScale = 1;
// true if currently executing setViewPosition
private boolean blocked = false;
public void alignViewPort(Point mousePosition) {
// if the scale didn't change there is nothing we should do
if (scale != lastScale) {
// compute the factor by that the image zoom has changed
double scaleChange = scale / lastScale;
// compute the scaled mouse position
Point scaledMousePosition = new Point(
(int)Math.round(mousePosition.x * scaleChange),
(int)Math.round(mousePosition.y * scaleChange)
);
// retrieve the current viewport position
Point viewportPosition = scrollPane.getViewport().getViewPosition();
// compute the new viewport position
Point newViewportPosition = new Point(
viewportPosition.x + scaledMousePosition.x - mousePosition.x,
viewportPosition.y + scaledMousePosition.y - mousePosition.y
);
// update the viewport position
blocked = true;
scrollPane.getViewport().setViewPosition(newViewportPosition);
blocked = false;
// remember the last scale
lastScale = scale;
}
}
// reference to the scroll pane container
private final JScrollPane scrollPane;
// constructor
public ZoomPanScrollPanel() {
// initialize the frame
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setSize(600, 600);
// initialize the components
final ImagePanel imagePanel = new ImagePanel();
final JPanel centerPanel = new JPanel();
centerPanel.setLayout(new GridBagLayout());
centerPanel.add(imagePanel);
scrollPane = new JScrollPane();
scrollPane.setViewport(new JViewport() {
private boolean inCall = false;
#Override
public void setViewPosition(Point pos) {
if (!inCall || !blocked) {
inCall = true;
super.setViewPosition(pos);
inCall = false;
}
}
});
scrollPane.getViewport().add(centerPanel);
scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
frame.add(scrollPane);
// add mouse wheel listener
imagePanel.addMouseWheelListener(new MouseAdapter() {
#Override
public void mouseWheelMoved(MouseWheelEvent e) {
super.mouseWheelMoved(e);
// check the rotation of the mousewheel
int rotation = e.getWheelRotation();
boolean zoomed = false;
if (rotation > 0) {
// only zoom out until no scrollbars are visible
if (scrollPane.getHeight() < imagePanel.getPreferredSize().getHeight() ||
scrollPane.getWidth() < imagePanel.getPreferredSize().getWidth()) {
zoom = zoom / 1.3;
zoomed = true;
}
} else {
// zoom in until maximum zoom size is reached
double newCurrentZoom = zoom * 1.3;
if (newCurrentZoom < 1000) { // 1000 ~ 10 times zoom
zoom = newCurrentZoom;
zoomed = true;
}
}
// check if a zoom happened
if (zoomed) {
// compute the scale
scale = (float) (zoom / 100f);
// align our viewport
alignViewPort(e.getPoint());
// invalidate and repaint to update components
imagePanel.revalidate();
scrollPane.repaint();
}
}
});
//mouse panning
//original code: https://stackoverflow.com/questions/31171502/scroll-jscrollpane-by-dragging-mouse-java-swing
MouseAdapter ma = new MouseAdapter() {
private Point origin;
#Override
public void mousePressed(MouseEvent e) {
origin = new Point(e.getPoint());
}
#Override
public void mouseReleased(MouseEvent e) {
}
#Override
public void mouseDragged(MouseEvent e) {
if (origin != null) {
JViewport viewPort = (JViewport) SwingUtilities.getAncestorOfClass(JViewport.class, imagePanel);
if (viewPort != null) {
int deltaX = origin.x - e.getX();
int deltaY = origin.y - e.getY();
System.out.println("X pan = "+ deltaX);
System.out.println("Y pan = "+ deltaY);
Rectangle view = viewPort.getViewRect();
view.x += deltaX;
view.y += deltaY;
imagePanel.scrollRectToVisible(view);
}
}
}
};
imagePanel.addMouseListener(ma);
imagePanel.addMouseMotionListener(ma);
imagePanel.setAutoscrolls(true);
// display our frame
frame.setVisible(true);
}
// the main method
public static void main(String[] args) {
new ZoomPanScrollPanel();
}
}