So basically i am making a game in which the circle will move in the four directions. When the "S" or "RIGHT" key is pressed once, the circle will move till it reaches the maxX of the pane. And when i press the "A" key or "LEFT", the circle will move till it reaches minX of pane. The "UP" or "W" and "DOWN" or "Z" keys also follow similar logic. If it get to maxY of pane it stops till another key is pressed and vice versa.
Here is the code for the controller class
#FXML
private Pane board;
private BooleanProperty wPressed = new SimpleBooleanProperty();
private BooleanProperty aPressed = new SimpleBooleanProperty();
private BooleanProperty zPressed = new SimpleBooleanProperty();
private BooleanProperty sPressed = new SimpleBooleanProperty();
private BooleanBinding keyPressed = wPressed.or(aPressed).or(zPressed).or(sPressed);
private Circle circle = new Circle(20,40,20);
private Rectangle[][] grid;
private int size = 960;
private int spots = 16;
private int squareSize = size / spots;
#FXML
void handleB(ActionEvent event) {
}
AnimationTimer timer = new AnimationTimer() {
double deltaX = 2;
double deltaY = 2;
#Override
public void handle(long timestamp) {
double rectX = circle.getLayoutX();
Bounds bounds = board.getBoundsInLocal();
double maxX = bounds.getMaxX();
double maxY = bounds.getMaxY();
double minX = bounds.getMinX();
double minY = bounds.getMinY();
if (circle.getLayoutX() >= ( maxX- circle.getRadius())) {
deltaX = 0;
//System.out.println(rect.getLayoutX());
}
if (circle.getLayoutY() >= (maxY - circle.getRadius())) {
deltaY = 0;
}
if (circle.getLayoutX() <= ( minX + circle.getRadius())) {
deltaX = 0;
//System.out.println(rect.getLayoutX());
}
if (circle.getLayoutY() <= ( minY + circle.getRadius())) {
deltaY = 0;
}
if(wPressed.get()) {
circle.setLayoutY(circle.getLayoutY() - deltaY);
}
if(zPressed.get()){
circle.setLayoutY(circle.getLayoutY() + deltaY);
}
if(aPressed.get()){
circle.setLayoutX(circle.getLayoutX() - deltaX);
}
if(sPressed.get()){
circle.setLayoutX(circle.getLayoutX() + deltaX);
}
}
};
#Override
public void initialize(URL url, ResourceBundle resourceBundle) {
grid = new Rectangle[spots][spots];
for (int i = 0; i < size; i += squareSize) {
for (int j = 0; j < size; j += squareSize) {
Rectangle r = new Rectangle(i, j, squareSize, squareSize);
grid[i / squareSize][j / squareSize] = r;
// Filling each grid with the cell image
r.setFill(Color.GRAY);
r.setStroke(Color.BLACK);
board.getChildren().add(r);
}
}
board.getChildren().addAll(circle);
movementSetup();
keyPressed.addListener(((observableValue, aBoolean, t1) -> {
if(!aBoolean){
timer.start();
} else {
timer.stop();
}
}));
}
private void movementSetup() {
board.setOnKeyPressed(e -> {
if(e.getCode() == KeyCode.UP || e.getCode() == KeyCode.W ) {
zPressed.set(false);
sPressed.set(false);
aPressed.set(false);
wPressed.set(true);
}
if(e.getCode() == KeyCode.A || e.getCode() == KeyCode.LEFT) {
zPressed.set(false);
sPressed.set(false);
wPressed.set(false);
aPressed.set(true);
}
if(e.getCode() == KeyCode.Z || e.getCode() == KeyCode.DOWN) {
wPressed.set(false);
aPressed.set(false);
sPressed.set(false);
zPressed.set(true);
}
if(e.getCode() == KeyCode.S || e.getCode() == KeyCode.RIGHT) {
wPressed.set(false);
aPressed.set(false);
zPressed.set(false);
sPressed.set(true);
}
});
}
}
** I did the pane in scenebuilder which i inserted it into an anchorpane as well**
The fxml file is below (ui.fxml)
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.Pane?>
<AnchorPane maxHeight="1000.0" maxWidth="1200.0" minHeight="0.0" minWidth="0.0" prefHeight="1000.0" prefWidth="1200.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="fx.game.Controller">
<children>
<Pane fx:id="board" maxHeight="960.0" maxWidth="960.0" minHeight="0.0" minWidth="0.0" prefHeight="960.0" prefWidth="960.0">
<children>
<Button layoutX="367.0" layoutY="934.0" mnemonicParsing="false" onAction="#handleB" text="Button" />
</children>
</Pane>
</children>
</AnchorPane>
Lastly the main class
package fx.game;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
public class HelloApplication extends Application {
#Override
public void start(Stage stage) throws IOException {
Parent root = FXMLLoader.load(getClass().getResource("ui.fxml"));
stage.setTitle("Hello World");
stage.setScene(new Scene(root));
stage.show();
}
public static void main(String[] args) {
launch();
}
}
Am really bad at game development
I might be doing it completely wrong
Anyone who can give me a better solution will be appreciated
From my limited understanding and testing, board.setOnKeyPressed isn't responding to key events, I used the Scene instead. You should also be taking into account onKeyReleased.
I don't know why you're starting and stoping the timer, just start it and on each, evaluate the state and make the changes that are required.
Your movement logic seems to be off as well. Get the circles current location, apply any movement to it as required, then determine if the new position is outside the acceptable bounds, adjust the position as needed and then update the circles position.
I also found that the Bounds the board would update based on the position of it's children, which made it hard to do bounds checking on, instead, I calculate the expected size based on the squareSize and the number of spots
For example...
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Bounds;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class App extends Application {
private BooleanProperty wPressed = new SimpleBooleanProperty();
private BooleanProperty aPressed = new SimpleBooleanProperty();
private BooleanProperty zPressed = new SimpleBooleanProperty();
private BooleanProperty sPressed = new SimpleBooleanProperty();
private BooleanBinding keyPressed = wPressed.or(aPressed).or(zPressed).or(sPressed);
private Circle circle = new Circle(20, 20, 20);
private Rectangle[][] grid;
private int size = 960;
private int spots = 16;
private int squareSize = size / spots;
AnimationTimer timer = new AnimationTimer() {
double deltaX = 2;
double deltaY = 2;
#Override
public void handle(long timestamp) {
double rectX = circle.getLayoutX();
Bounds bounds = board.getBoundsInLocal();
double width = squareSize * spots;
double height = squareSize * spots;
double diameter = circle.getRadius() * 2;
double x = circle.getLayoutX();
double y = circle.getLayoutY();
if (wPressed.get()) {
y -= deltaY;
}
if (zPressed.get()) {
y += deltaY;
}
if (aPressed.get()) {
x -= deltaX;
}
if (sPressed.get()) {
x += deltaX;
}
if (x >= (width - diameter)) {
x = width - diameter;
}
if (x < 0) {
x = 0;
}
if (y > (height - diameter)) {
y = height - diameter;
}
if (y < 0) {
y = 0;
}
circle.setLayoutX(x);
circle.setLayoutY(y);
}
};
private Pane board = new AnchorPane();
#Override
public void start(Stage stage) {
stage.setTitle("Dice Game");
setup();
Scene scene = new Scene(board);
movementSetup(scene);
stage.setScene(scene);
stage.sizeToScene();
stage.show();
}
private void movementSetup(Scene scene) {
scene.setOnKeyPressed(e -> {
zPressed.set(false);
sPressed.set(false);
aPressed.set(false);
wPressed.set(false);
if (e.getCode() == KeyCode.UP || e.getCode() == KeyCode.W) {
wPressed.set(true);
} else if (e.getCode() == KeyCode.A || e.getCode() == KeyCode.LEFT) {
aPressed.set(true);
}else if (e.getCode() == KeyCode.Z || e.getCode() == KeyCode.DOWN) {
zPressed.set(true);
}else if (e.getCode() == KeyCode.S || e.getCode() == KeyCode.RIGHT) {
sPressed.set(true);
}
});
}
protected void setup() {
grid = new Rectangle[spots][spots];
for (int i = 0; i < size; i += squareSize) {
for (int j = 0; j < size; j += squareSize) {
Rectangle r = new Rectangle(i, j, squareSize, squareSize);
grid[i / squareSize][j / squareSize] = r;
// Filling each grid with the cell image
r.setFill(Color.GRAY);
r.setStroke(Color.BLACK);
board.getChildren().add(r);
}
}
board.getChildren().addAll(circle);
timer.start();
}
public static void main(String[] args) {
launch();
}
}
nb: I'm a complete JavaFX noob, so there's probably better ways to approach this issue and maybe having a look at things like Introduction to JavaFX for Game Development (or other "javafx game development blogs" might provide better ideas
Related
So I have my custom mouse adapter. I want to call a method if:
The x and y coordinates are in a Rectangle with for example the coordinates x = 40 y = 40 and the width = 10 length = 10. How can I check if the x and y coordinates (of the mouse) are in the rectangle?
I tried this:
#Override
public void mouseClicked(MouseEvent e) {
if(isXinBounds(e.getX()) || isYinBounds(e.getY())) {
//This happens if the mouse click is in the rectangle
}
}
//Check if the x coordinate of the mouse click is in the rectangle
private boolean isXinBounds(int x) {
if(x <= ob.getBounds().getX() + ob.getBounds().getWidth() / 2 && x >= ob.getBounds().getX() - ob.getBounds().getWidth() / 2) return true;
return false;
}
//Check if the y coordinate of the mouse click is in the rectangle
private boolean isYinBounds(int y) {
if(y <= ob.getBounds().getY() + ob.getBounds().getHeight() / 2 && y >= ob.getBounds().getY() - ob.getBounds().getHeight() / 2) return true;
return false;
}
But that didn't work
The x and y coordinates are in a Rectangle
The Rectangle class already supports a contains(…) method.
You can just use:
if (yourRectangle.contains(e.getPoint())
// do something
You're making a really weird formula to check if your click is in bounds of your rectangle.
If we set the rectangle coords:
x = 10
y = 10
width = 50
height = 50
And create a new Rectangle with those values:
Rectangle rect = new Rectangle(x, y, widht, height)
We can then do this for both X & Y
private boolean xIsInBounds(int x) {
if (x >= rect.x && x <= rect.width + x) {
return true;
}
return false;
}
Because we check for the X & Y values as they are going to be always the same, but we have to add the X & Y values to the width and height respectively due to it being moved due to the X & Y values not being on 0,0.
The other issue in your code is that you're asking:
if (xIsInBounds(...) || yIsInBounds(...)) { ... }
When it should be an && instead of ||.
Here's the code that produces the following output in the form of a Minimal Reproducible Example
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class ClickOnBounds extends MouseAdapter {
private JFrame frame;
private JPanel pane;
private static final int X_COORD = 10;
private static final int Y_COORD = 10;
private static final int RECT_WIDTH = 50;
private static final int RECT_HEIGHT = 50;
private Rectangle rect = new Rectangle(X_COORD, Y_COORD, RECT_WIDTH, RECT_HEIGHT);
#SuppressWarnings("serial")
private void createAndShowGUI() {
frame = new JFrame(this.getClass().getSimpleName());
pane = new JPanel() {
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.draw(rect);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(70, 70);
}
};
pane.addMouseListener(this);
frame.add(pane);
frame.pack();
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
#Override
public void mouseClicked(MouseEvent e) {
super.mouseClicked(e);
if (xIsInBound(e.getX()) && yIsInBound(e.getY())) {
System.out.println("Yes! " + e.getX() + " " + e.getY());
} else {
System.out.println("No! " + e.getX() + " " + e.getY());
}
}
private boolean xIsInBound(int x) {
if (x >= rect.x && x <= rect.width + X_COORD) {
return true;
}
return false;
}
private boolean yIsInBound(int y) {
if (y >= rect.y && y <= rect.height + Y_COORD) {
return true;
}
return false;
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new ClickOnBounds()::createAndShowGUI);
}
}
And, as you can see in the image above, it works perfectly.
I am trying to build a Rubiks Cube from scratch as my first real app using JavaFx. In the Moves class Im selecting Boxes from ObservableList based on position. The problem is that the Box.getTranslate dosent update so when I try to move the front and the left faces successively the Boxes selected by both are always moved resulting in.. chaos. How could I re-write this so that the Move methods correctly select the boxes to move? Here is the code, work in progress.
Main
package ro.adrianpush;
import javafx.application.Application;
import javafx.collections.ObservableList;
import javafx.scene.*;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Box;
import javafx.stage.Stage;
public class Main extends Application {
private static final int WIDTH = 800;
private static final int HEIGHT = 600;
final Group root = new Group();
#Override
public void start(Stage primaryStage) throws Exception{
Camera camera = new PerspectiveCamera();
camera.translateXProperty().setValue(-200);
camera.translateYProperty().setValue(0);
camera.translateZProperty().set(-500);
AnchorPane pane = new AnchorPane();
Rubik rubik = new Rubik();
ObservableList<Box> boxArrayList = rubik.getBoxArrayList();
for (Box box: boxArrayList
) {
pane.getChildren().addAll(box);
}
primaryStage.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
switch (event.getCode()){
case E:
Moves.rotateFront(boxArrayList, "clockwise");
break;
case Q:
Moves.rotateFront(boxArrayList, "counterclockwise");
break;
case A:
Moves.rotateLeft(boxArrayList, "clockwise");
break;
case D:
Moves.rotateLeft(boxArrayList, "counterclockwise");
break;
}
});
root.getChildren().add(pane);
Scene scene = new Scene(root, WIDTH, HEIGHT, true);
scene.setCamera(camera);
scene.setFill(Color.ROYALBLUE);
primaryStage.setTitle("The game nobody wants to play");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Rubik
package ro.adrianpush;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.shape.Box;
public class Rubik {
private ObservableList<Box> boxArrayList = FXCollections.observableArrayList();
public Rubik(){
for(int i = 1; i < 4; i+=1){
for( int j = 1; j < 4; j++){
for(int k = 1; k < 4; k++){
Box box = new Box(100,100,100);
box.setTranslateX(i*100);
box.setTranslateY(j*100);
box.setTranslateZ(k*100);
boxArrayList.add(box);
}
}
}
}
public ObservableList<Box> getBoxArrayList() {
return boxArrayList;
}
}
Moves
package ro.adrianpush;
import javafx.collections.ObservableList;
import javafx.scene.shape.Box;
import javafx.scene.transform.Rotate;
public class Moves {
public static void rotateFront(ObservableList<Box> boxArrayList, String direction) {
for (Box box: boxArrayList
) {
if(box.getTranslateZ() == 100){
Rotate rotate = new Rotate();
rotate.setAxis(Rotate.Z_AXIS);
if(direction == "clockwise"){
rotate.setAngle(5);
} else if (direction == "counterclockwise"){
rotate.setAngle(-5);
}
if(box.getTranslateX() == 100){
rotate.setPivotX(100);
} if (box.getTranslateX() == 300){
rotate.setPivotX(-100);
} if(box.getTranslateY() == 100){
rotate.setPivotY(100);
} if(box.getTranslateY() == 300){
rotate.setPivotY(-100);
}
box.getTransforms().add(rotate);
}
}
}
public static void rotateBack(ObservableList<Box> boxArrayList, String direction) {
for (Box box: boxArrayList
) {
if(box.getTranslateZ() == 300){
Rotate rotate = new Rotate();
if(direction == "clockwise"){
rotate.setAngle(5);
} else if (direction == "counterclockwise"){
rotate.setAngle(-5);
}
if(box.getTranslateX() == 100){
rotate.setPivotX(100);
} if (box.getTranslateX() == 300){
rotate.setPivotX(-100);
} if(box.getTranslateY() == 100){
rotate.setPivotY(100);
} if(box.getTranslateY() == 300){
rotate.setPivotY(-100);
}
box.getTransforms().add(rotate);
}
}
}
public static void rotateLeft(ObservableList<Box> boxArrayList, String direction) {
for (Box box: boxArrayList
) {
if(box.getTranslateX() == 100){
Rotate rotate = new Rotate();
rotate.setAxis(Rotate.X_AXIS);
if(direction == "clockwise"){
rotate.setAngle(5);
} else if (direction == "counterclockwise"){
rotate.setAngle(-5);
}
if(box.getTranslateY() == 100){
rotate.setPivotY(100);
} if (box.getTranslateY() == 300){
rotate.setPivotY(-100);
} if(box.getTranslateZ() == 100){
rotate.setPivotZ(100);
} if(box.getTranslateZ() == 300){
rotate.setPivotZ(-100);
}
box.getTransforms().add(rotate);
}
}
}
}
I am trying to make a cryptex program using JavaFX, but sometimes the rendering gets corrupted.
What it should be showing:
However, sometimes the rotation snags it show this:
Why is this happening? What can I do to fix this? I've found lots of other posts saying that JavaFX should handle all of the rendering for you, but forcing a redraw seems like the only solution, and I couldn't figure out how to do that either.
Here is my code:
import java.awt.Image;
import java.util.Timer;
import java.util.TimerTask;
import javafx.application.Application;
import javafx.scene.effect.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeType;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.*;
import javafx.stage.Stage;
public class Cryptex extends Application{
private Spool[] spools;
private double radius = 100, perspectiveScale = 0.1;
Stage stage;
Scene scene;
public static void main(String[] args) {
launch();
}
#Override
public void start(Stage primaryStage){
stage = primaryStage;
stage.setTitle("Criptex");
Group root = new Group();
scene = new Scene(root, 550, 400);
stage.setScene(scene);
stage.show();
spools = new Spool[5];
spools[0] = new Spool("jdkwndityc", -150);
spools[1] = new Spool("lqhmnxfgso", -75);
spools[2] = new Spool("usjnzuvbid", 0);
spools[3] = new Spool("ihgkewobde", 75);
spools[4] = new Spool("cdelsprkar", 150);
for (Spool sp : spools){
sp.angle = Math.random()*Math.PI*2;
root.getChildren().add(sp);
}
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
#Override
public void run() {
for (Spool s : spools){
if (s.angle != 0){
if (Math.abs(s.distToAngle(0)) < Math.toRadians(1)){
System.out.println("snap");
s.snapToAngle(0);
for (Spool.Cell c : s.cells){
c.draw(s.angle);
}
}
else {
s.rotate(s.directionToAngle(0));
System.out.println((s.x+150)/75+1 + ": "+Math.round((s.angle/Math.PI)*180));
for (Spool.Cell c : s.cells){
c.draw(s.angle);
}
}
}
}
//System.out.println("--");
}
}, 0, 10);
}
public class Spool extends Group{
double angle;
char[] chars;
Image[] letters;
int x;
Cell[] cells;
public Spool(String charList, int x){
chars = charList.toCharArray();
letters = new Image[chars.length];
this.x = x;
angle = 0;
cells = new Cell[chars.length];
for (int i = 0; i < chars.length; i++){
cells[i] = new Cell(i);
this.getChildren().add(cells[i]);
}
}
public void rotate(double distance){
angle += Math.toRadians(distance);
if (angle >= Math.PI*2){
angle -= Math.PI*2;
}
if (angle < 0){
angle += Math.PI*2;
}
}
//TODO: check if this is way off
public double distToAngle(double angle){
rotate(0);
if ((this.angle > angle && this.angle - angle > Math.PI) ||
(this.angle < angle && angle - this.angle > Math.PI)){
return Math.abs(this.angle - angle);
}
else {
return -Math.abs(this.angle - angle);
}
}
public double closestAngle(){
int closest = 0;
for (int i = 0; i < chars.length; i++){
if (Math.abs(distToAngle(i*((Math.PI*2)/chars.length)))
< Math.abs(distToAngle(closest*((Math.PI*2)/chars.length)))){
closest = i;
}
}
return closest;
}
public boolean snapToAngle(double angle){
if (Math.abs(this.distToAngle(angle)) > Math.toRadians(1)){
if (this.distToAngle(angle) > 0){
this.rotate(-1);
}
else {
this.rotate(1);
}
return false;
}
else if (this.angle != angle){
this.angle = angle;
}
return true;
}
public double indexToAngle(int index){
return ((Math.PI*2)/chars.length)*index;
}
public double perspectiveWidth(double d){
return Math.sqrt(Math.pow(radius, 2) - Math.pow(d, 2)) * perspectiveScale;
}
public double toAngle(int index){
return ((Math.PI*2)/chars.length)*(index) + angle + ((Math.PI*2)/(chars.length*2));
}
public int directionToAngle(double angle){
return (int) Math.signum(this.distToAngle(angle));
}
public class Cell extends Group {
private int index;
PerspectiveTransform pt = new PerspectiveTransform();
public Cell(int index){//, double angle, Stage stage){
this.index = index;
Text text = new Text();
//System.out.println("char: " + String.valueOf(chars[c]));
text.setText(String.valueOf(chars[index]).toUpperCase());
text.setFont(Font.font("Monospaced", FontWeight.BOLD, 36));
text.setFill(Color.BLACK);
text.setX(9);
text.setY(32);
Rectangle rect = new Rectangle(40, 40);
rect.setFill(Color.BEIGE);
rect.setStrokeType(StrokeType.OUTSIDE);
rect.setStrokeWidth(3);
rect.setStroke(Color.BLACK);
this.draw(angle);
this.setCache(true);
this.getChildren().addAll(rect, text);
}
public void draw(double angle){
double cx = stage.getWidth()/2 + x;
double cy = (stage.getHeight()-100)/2;
if (cy + radius*Math.sin(toAngle(index)+angle) <= cy + radius*Math.sin(toAngle(index+1)+angle)){
this.setVisible(true);
pt.setUlx(cx - 20 - perspectiveWidth(radius*Math.sin(toAngle(index)+angle)));
pt.setUrx(cx + 20 + perspectiveWidth(radius*Math.sin(toAngle(index)+angle)));
pt.setLrx(cx + 20 + perspectiveWidth(radius*Math.sin(toAngle(index+1)+angle)));
pt.setLlx(cx - 20 - perspectiveWidth(radius*Math.sin(toAngle(index+1)+angle)));
pt.setUly(cy + radius*Math.sin(toAngle(index)+angle));
pt.setUry(cy + radius*Math.sin(toAngle(index)+angle));
pt.setLry(cy + radius*Math.sin(toAngle(index+1)+angle));
pt.setLly(cy + radius*Math.sin(toAngle(index+1)+angle));
this.setEffect(pt);
}
else {
this.setVisible(false);
}
}
}
}
}
Your issue is the same as the one seen here, using a java.util.Timer directly inside JavaFX is not thread safe.
This is because timer uses it's own background thread, but if you want to do any updates to the GUI, you need to be using the JavaFX GUI thread (a special thread JavaFX creates and handles). Trying to touch the GUI from another thread creates problems like the ones you are seeing.
You can pass changes to JavaFX components to the special JavaFX thread by wrapping the changes in a Platform.runLater block.
The code looks like this:
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
#Override
public void run() {
Platform.runLater(() -> { //Lambda for Runnable
for (Spool s : spools){
if (s.angle != 0){
if (Math.abs(s.distToAngle(0)) < Math.toRadians(1)){
System.out.println("snap");
s.snapToAngle(0);
for (Spool.Cell c : s.cells){
c.draw(s.angle);
}
}
else {
s.rotate(s.directionToAngle(0));
System.out.println((s.x+150)/75+1 + ": "
+ Math.round((s.angle/Math.PI)*180));
for (Spool.Cell c : s.cells){
c.draw(s.angle);
}
}
}
}
});
}
}, 0, 10);
Pure JavaFX Style:
You can also use a JavaFX Timeline and not have to worry about the thread issues:
Timeline timer = new Timeline(new KeyFrame(
Duration.millis(10),
event -> {//Same for loop as above}
));
timer.setCycleCount(Timeline.INDEFINITE);
timer.play();
I am trying to make a puzzle game but the problem is that I can't do the EventHandler setOnMouseClicked.Also I want to know how to make possible that to re-arrange the puzzle so it shows random images in every-coordinate. This is how far I got.
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
public class eliss extends Application{
#Override
public void start(Stage primaryStage) {
ImageView view[][] = new ImageView[5][5];
Image imag = new Image("http://images.cdn.autocar.co.uk/sites/autocar.co.uk/files/styles/gallery_slide/public/ferrari-laferrari-zfye-059_1.jpg?itok=hfLNxUD9",600,600,false,true);
GridPane pane = new GridPane();
pane.setAlignment(Pos.CENTER);
pane.setVgap(2);
pane.setHgap(2);
PuzzleImage(view, imag, pane);
Scene scene = new Scene(pane,1100,1100);
primaryStage.setTitle("Elis");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] x)
{
launch(x);
}
public void PuzzleImage( ImageView view[][],Image imag,GridPane pane)
{
for(int i=0;i<5;i++)
{
for(int j=0;j<5;j++)
{
if(j==4 && i==4) view[i][j]=null;
else{
view[i][j]=new ImageView(imag);
Rectangle2D rect = new Rectangle2D(120*i,120*j,120,120);
view[i][j].setViewport(rect);
pane.add(view[i][j], i, j);
}
}
}
}
}
What you are asking is too much for a question on StackOverflow. However, I got a little bit of spare time, so I quickly drafted up some code to demonstrate what you need:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javafx.animation.PathTransition;
import javafx.application.Application;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.stage.Stage;
import javafx.util.Duration;
public class PuzzleGame extends Application {
private Image image = new Image("http://images.cdn.autocar.co.uk/sites/autocar.co.uk/files/styles/gallery_slide/public/ferrari-laferrari-zfye-059_1.jpg?itok=hfLNxUD9",600,600,false,true);
private static double SCENE_WIDTH = 1024;
private static double SCENE_HEIGHT = 768;
public static int TILE_ROW_COUNT = 5;
public static int TILE_COLUMN_COUNT = 5;
public static double TILE_SIZE = 120;
public static double offsetX = (SCENE_WIDTH - TILE_ROW_COUNT * TILE_SIZE) / 2;
public static double offsetY = (SCENE_HEIGHT - TILE_COLUMN_COUNT * TILE_SIZE) / 2;
List<Cell> cells = new ArrayList<>();
#Override
public void start(Stage primaryStage) {
// create grid
for (int x = 0; x < TILE_ROW_COUNT; x++) {
for (int y = 0; y < TILE_COLUMN_COUNT; y++) {
// create tile
ImageView tile = new ImageView(image);
Rectangle2D rect = new Rectangle2D(TILE_SIZE * x, TILE_SIZE * y, TILE_SIZE, TILE_SIZE);
tile.setViewport(rect);
// consider empty cell, let it remain empty
if (x == (TILE_ROW_COUNT - 1) && y == (TILE_COLUMN_COUNT - 1)) {
tile = null;
}
cells.add(new Cell(x, y, tile));
}
}
// shuffle cells
shuffle();
// create playfield
Pane pane = new Pane();
// put tiles on playfield, assign event handler
for (int i = 0; i < cells.size(); i++) {
Cell cell = cells.get(i);
Node imageView = cell.getImageView();
// consider empty cell
if (imageView == null)
continue;
// click-handler: swap tiles, check if puzzle is solved
imageView.addEventFilter(MouseEvent.MOUSE_CLICKED, mouseEvent -> {
moveCell((Node) mouseEvent.getSource());
});
// position images on scene
imageView.relocate(cell.getLayoutX(), cell.getLayoutY());
pane.getChildren().add(cell.getImageView());
}
Scene scene = new Scene(pane, SCENE_WIDTH, SCENE_HEIGHT);
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* Swap images of cells randomly
*/
public void shuffle() {
Random rnd = new Random();
for (int i = 0; i < 1000; i++) {
int a = rnd.nextInt(cells.size());
int b = rnd.nextInt(cells.size());
if (a == b)
continue;
// skip bottom right cell swap, we want the empty cell to remain there
if( cells.get(a).isEmpty() || cells.get(b).isEmpty())
continue;
swap( cells.get(a), cells.get(b));
}
}
public void swap( Cell cellA, Cell cellB) {
ImageView tmp = cellA.getImageView();
cellA.setImageView(cellB.getImageView());
cellB.setImageView(tmp);
}
public boolean checkSolved() {
boolean allSolved = true;
for (Cell cell : cells) {
if (!cell.isSolved()) {
allSolved = false;
break;
}
}
System.out.println("Solved: " + allSolved);
return allSolved;
}
public void moveCell(Node node) {
// get current cell using the selected node (imageview)
Cell currentCell = null;
for (Cell tmpCell : cells) {
if (tmpCell.getImageView() == node) {
currentCell = tmpCell;
break;
}
}
if (currentCell == null)
return;
// get empty cell
Cell emptyCell = null;
for (Cell tmpCell : cells) {
if (tmpCell.isEmpty()) {
emptyCell = tmpCell;
break;
}
}
if (emptyCell == null)
return;
// check if cells are swappable: neighbor distance either x or y must be 1 for a valid move
int steps = Math.abs(currentCell.x - emptyCell.x) + Math.abs(currentCell.y - emptyCell.y);
if (steps != 1)
return;
System.out.println("Transition: " + currentCell + " -> " + emptyCell);
// cells are swappable => create path transition
Path path = new Path();
path.getElements().add(new MoveToAbs(currentCell.getImageView(), currentCell.getLayoutX(), currentCell.getLayoutY()));
path.getElements().add(new LineToAbs(currentCell.getImageView(), emptyCell.getLayoutX(), emptyCell.getLayoutY()));
PathTransition pathTransition = new PathTransition();
pathTransition.setDuration(Duration.millis(100));
pathTransition.setNode(currentCell.getImageView());
pathTransition.setPath(path);
pathTransition.setOrientation(PathTransition.OrientationType.NONE);
pathTransition.setCycleCount(1);
pathTransition.setAutoReverse(false);
final Cell cellA = currentCell;
final Cell cellB = emptyCell;
pathTransition.setOnFinished(actionEvent -> {
swap( cellA, cellB);
checkSolved();
});
pathTransition.play();
}
private static class Cell {
int x;
int y;
ImageView initialImageView;
ImageView currentImageView;
public Cell(int x, int y, ImageView initialImageView) {
super();
this.x = x;
this.y = y;
this.initialImageView = initialImageView;
this.currentImageView = initialImageView;
}
public double getLayoutX() {
return x * TILE_SIZE + offsetX;
}
public double getLayoutY() {
return y * TILE_SIZE + offsetY;
}
public ImageView getImageView() {
return currentImageView;
}
public void setImageView(ImageView imageView) {
this.currentImageView = imageView;
}
public boolean isEmpty() {
return currentImageView == null;
}
public boolean isSolved() {
return this.initialImageView == currentImageView;
}
public String toString() {
return "[" + x + "," + y + "]";
}
}
// absolute (layoutX/Y) transitions using the pathtransition for MoveTo
public static class MoveToAbs extends MoveTo {
public MoveToAbs(Node node) {
super(node.getLayoutBounds().getWidth() / 2, node.getLayoutBounds().getHeight() / 2);
}
public MoveToAbs(Node node, double x, double y) {
super(x - node.getLayoutX() + node.getLayoutBounds().getWidth() / 2, y - node.getLayoutY() + node.getLayoutBounds().getHeight() / 2);
}
}
// absolute (layoutX/Y) transitions using the pathtransition for LineTo
public static class LineToAbs extends LineTo {
public LineToAbs(Node node, double x, double y) {
super(x - node.getLayoutX() + node.getLayoutBounds().getWidth() / 2, y - node.getLayoutY() + node.getLayoutBounds().getHeight() / 2);
}
}
public static void main(String[] args) {
launch(args);
}
}
You can use Collections.shuffle to generate a random permutation of a List This can be used to place the tiles in a random order:
private static final int COLUMN_COUNT = 5;
private static final int ROW_COUNT = 5;
private static void fillGridPane(GridPane pane, ImageView[][] view, Image imag) {
List<ImageView> images = new ArrayList<>(24);
for (int i = 0; i < COLUMN_COUNT; i++) {
for (int j = 0, end = i == (COLUMN_COUNT - 1) ? ROW_COUNT - 1 : ROW_COUNT; j < end; j++) {
ImageView iv = new ImageView(imag);
images.add(iv);
view[i][j] = iv;
Rectangle2D rect = new Rectangle2D(120 * i, 120 * j, 120, 120);
iv.setViewport(rect);
}
}
Collections.shuffle(images);
Iterator<ImageView> iter = images.iterator();
for (int i = 0; i < COLUMN_COUNT; i++) {
for (int j = 0, end = i == (COLUMN_COUNT - 1) ? ROW_COUNT - 1 : ROW_COUNT; j < end; j++) {
pane.add(iter.next(), i, j);
}
}
}
The onMouseClicked event handler can be attached to the GridPane and MouseEvent.getTarget can be used to get the node that was clicked:
pane.setOnMouseClicked(event -> {
if (move(event)) {
if (checkWin()) {
new Alert(Alert.AlertType.INFORMATION, "You Win!").show();
}
}
});
private int emptyTileX = 4;
private int emptyTileY = 4;
private boolean move(MouseEvent event) {
Object target = event.getTarget();
if (target instanceof ImageView) {
ImageView iv = (ImageView) target;
int row = GridPane.getRowIndex(iv);
int column = GridPane.getColumnIndex(iv);
int dx = Math.abs(column - emptyTileX);
int dy = Math.abs(row - emptyTileY);
if ((dx == 0 && dy == 1) || (dx == 1 && dy == 0)) {
// swap image and empty tile, if they are next to each other
GridPane.setConstraints(iv, emptyTileX, emptyTileY);
emptyTileX = column;
emptyTileY = row;
return true;
}
}
return false;
}
To check the winning condition, you can simply check the positions of the ImageViews in the GridPane
private boolean checkWin() {
for (int i = 0; i < COLUMN_COUNT; i++) {
for (int j = 0, end = i == (COLUMN_COUNT - 1) ? ROW_COUNT - 1 : ROW_COUNT; j < end; j++) {
ImageView iv = view[i][j];
if (GridPane.getColumnIndex(iv) != i || GridPane.getRowIndex(iv) != j) {
return false;
}
}
}
return true;
}
I am trying to place a few components inside a ScrollPane. These components should have the ability to be moved across this pane by mouse (click and drag). The ScrollPane itself is pannable and zoomable.
Now if I pick one of them and drag it to a new position the mouse is faster than the component if I have zoomed out. When zoomed in, the component gets moved faster than the mouse movement.
If not zoomed it works until I reach a certain position where the ScrollPane automatically pans.
It must have to do something with the determined coordinates of the nodes. Does anyone have an idea what I have to add to make it work correctly?
My controller class:
public class MainWindowController implements Initializable {
private final double SCALE_DELTA = 1.1;
private final StackPane zoomPane = new StackPane();
private Group group = new Group();
#FXML
private ScrollPane scrollPane;
#Override
public void initialize(URL url, ResourceBundle rb) {
Node node1 = new Node("Test");
Node node2 = new Node("Test2", 100, 200);
group.getChildren().addAll(node1, node2);
zoomPane.getChildren().add(group);
Group scrollContent = new Group(zoomPane);
scrollPane.setContent(scrollContent);
scrollPane.viewportBoundsProperty().addListener((ObservableValue<? extends Bounds> observable,
Bounds oldValue, Bounds newValue) -> {
zoomPane.setMinSize(newValue.getWidth(), newValue.getHeight());
});
zoomPane.setOnScroll(
(ScrollEvent event) -> {
event.consume();
if (event.getDeltaY() == 0) {
return;
}
double scaleFactor = (event.getDeltaY() > 0) ? SCALE_DELTA : 1 / SCALE_DELTA;
Point2D scrollOffset = figureScrollOffset(scrollContent, scrollPane);
group.setScaleX(group.getScaleX() * scaleFactor);
group.setScaleY(group.getScaleY() * scaleFactor);
repositionScroller(scrollContent, scrollPane, scaleFactor, scrollOffset);
}
);
group.getChildren()
.add(new Node("Test3", 500, 500));
}
private Point2D figureScrollOffset(javafx.scene.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(javafx.scene.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());
}
}
}
The node class:
public class Node extends Parent {
private NodeStatus status = NodeStatus.OK;
private final Image okImage = new Image(getClass().getResourceAsStream("/images/MasterOK.png"));
private ImageView image = new ImageView(okImage);
private final Text label = new Text();
private final Font font = Font.font("Courier", 20);
double orgSceneX, orgSceneY;
double layoutX, layoutY;
public Node(String labelText) {
this(labelText, 0, 0);
}
public Node(String labelText, double x, double y) {
label.setText(labelText);
label.setFont(font);
label.setLayoutX(okImage.getWidth() + 10);
label.setLayoutY(okImage.getHeight() / 2 + 10);
getChildren().add(image);
getChildren().add(label);
setLayoutX(x);
setLayoutY(y);
setCursor(Cursor.MOVE);
setOnMousePressed(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
orgSceneX = t.getSceneX();
orgSceneY = t.getSceneY();
layoutX = getLayoutX();
layoutY = getLayoutY();
}
});
setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
setLayoutX(layoutX + t.getSceneX() - orgSceneX);
setLayoutY(layoutY + t.getSceneY() - orgSceneY);
}
});
}
public NodeStatus getStatus() {
return status;
}
public void setStatus(NodeStatus status) {
this.status = status;
}
}
class Delta {
double x, y;
}
and the fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import nodes.*?>
<AnchorPane id="AnchorPane" prefHeight="600.0" prefWidth="800.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="cqsmonitor.MainWindowController">
<children>
<Pane layoutX="666.0" layoutY="14.0" prefHeight="572.0" prefWidth="114.0" AnchorPane.bottomAnchor="14.0" AnchorPane.rightAnchor="14.0" AnchorPane.topAnchor="14.0">
<children>
<TextField layoutY="30.0" prefHeight="25.0" prefWidth="114.0" />
<Label layoutY="12.0" text="Search:" />
<ChoiceBox layoutY="90.0" prefHeight="25.0" prefWidth="114.0" />
<Label layoutY="73.0" text="View:" />
</children>
</Pane>
<ScrollPane fx:id="scrollPane" layoutX="14.0" layoutY="14.0" pannable="true" prefHeight="571.0" prefWidth="644.0" AnchorPane.bottomAnchor="15.0" AnchorPane.leftAnchor="14.0" AnchorPane.rightAnchor="142.0" AnchorPane.topAnchor="14.0">
</ScrollPane>
</children>
</AnchorPane>
Since nobody answered yet, here's some code. I don't want to dig into yours and re-invent the wheel.
You can move nodes by dragging with the left mouse button, scale the pane with the mouse wheel, pan the pane with the right mouse button. No ScrollPane needed. However, if you want ScrollBars, you can always add them if you prefer them.
The code:
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
/**
* The canvas which holds all of the nodes of the application.
*/
class PannableCanvas extends Pane {
DoubleProperty myScale = new SimpleDoubleProperty(1.0);
public PannableCanvas() {
setPrefSize(600, 600);
setStyle("-fx-background-color: lightgrey; -fx-border-color: blue;");
// add scale transform
scaleXProperty().bind(myScale);
scaleYProperty().bind(myScale);
// logging
addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
System.out.println(
"canvas event: " + ( ((event.getSceneX() - getBoundsInParent().getMinX()) / getScale()) + ", scale: " + getScale())
);
System.out.println( "canvas bounds: " + getBoundsInParent());
});
}
/**
* Add a grid to the canvas, send it to back
*/
public void addGrid() {
double w = getBoundsInLocal().getWidth();
double h = getBoundsInLocal().getHeight();
// add grid
Canvas grid = new Canvas(w, h);
// don't catch mouse events
grid.setMouseTransparent(true);
GraphicsContext gc = grid.getGraphicsContext2D();
gc.setStroke(Color.GRAY);
gc.setLineWidth(1);
// draw grid lines
double offset = 50;
for( double i=offset; i < w; i+=offset) {
// vertical
gc.strokeLine( i, 0, i, h);
// horizontal
gc.strokeLine( 0, i, w, i);
}
getChildren().add( grid);
grid.toBack();
}
public double getScale() {
return myScale.get();
}
/**
* Set x/y scale
* #param myScale
*/
public void setScale( double scale) {
myScale.set(scale);
}
/**
* Set x/y pivot points
* #param x
* #param y
*/
public void setPivot( double x, double y) {
setTranslateX(getTranslateX()-x);
setTranslateY(getTranslateY()-y);
}
}
/**
* Mouse drag context used for scene and nodes.
*/
class DragContext {
double mouseAnchorX;
double mouseAnchorY;
double translateAnchorX;
double translateAnchorY;
}
/**
* Listeners for making the nodes draggable via left mouse button. Considers if parent is zoomed.
*/
class NodeGestures {
private DragContext nodeDragContext = new DragContext();
PannableCanvas canvas;
public NodeGestures( PannableCanvas canvas) {
this.canvas = canvas;
}
public EventHandler<MouseEvent> getOnMousePressedEventHandler() {
return onMousePressedEventHandler;
}
public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
return onMouseDraggedEventHandler;
}
private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// left mouse button => dragging
if( !event.isPrimaryButtonDown())
return;
nodeDragContext.mouseAnchorX = event.getSceneX();
nodeDragContext.mouseAnchorY = event.getSceneY();
Node node = (Node) event.getSource();
nodeDragContext.translateAnchorX = node.getTranslateX();
nodeDragContext.translateAnchorY = node.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// left mouse button => dragging
if( !event.isPrimaryButtonDown())
return;
double scale = canvas.getScale();
Node node = (Node) event.getSource();
node.setTranslateX(nodeDragContext.translateAnchorX + (( event.getSceneX() - nodeDragContext.mouseAnchorX) / scale));
node.setTranslateY(nodeDragContext.translateAnchorY + (( event.getSceneY() - nodeDragContext.mouseAnchorY) / scale));
event.consume();
}
};
}
/**
* Listeners for making the scene's canvas draggable and zoomable
*/
class SceneGestures {
private static final double MAX_SCALE = 10.0d;
private static final double MIN_SCALE = .1d;
private DragContext sceneDragContext = new DragContext();
PannableCanvas canvas;
public SceneGestures( PannableCanvas canvas) {
this.canvas = canvas;
}
public EventHandler<MouseEvent> getOnMousePressedEventHandler() {
return onMousePressedEventHandler;
}
public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
return onMouseDraggedEventHandler;
}
public EventHandler<ScrollEvent> getOnScrollEventHandler() {
return onScrollEventHandler;
}
private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// right mouse button => panning
if( !event.isSecondaryButtonDown())
return;
sceneDragContext.mouseAnchorX = event.getSceneX();
sceneDragContext.mouseAnchorY = event.getSceneY();
sceneDragContext.translateAnchorX = canvas.getTranslateX();
sceneDragContext.translateAnchorY = canvas.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
// right mouse button => panning
if( !event.isSecondaryButtonDown())
return;
canvas.setTranslateX(sceneDragContext.translateAnchorX + event.getSceneX() - sceneDragContext.mouseAnchorX);
canvas.setTranslateY(sceneDragContext.translateAnchorY + event.getSceneY() - sceneDragContext.mouseAnchorY);
event.consume();
}
};
/**
* Mouse wheel handler: zoom to pivot point
*/
private EventHandler<ScrollEvent> onScrollEventHandler = new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
double delta = 1.2;
double scale = canvas.getScale(); // currently we only use Y, same value is used for X
double oldScale = scale;
if (event.getDeltaY() < 0)
scale /= delta;
else
scale *= delta;
scale = clamp( scale, MIN_SCALE, MAX_SCALE);
double f = (scale / oldScale)-1;
double dx = (event.getSceneX() - (canvas.getBoundsInParent().getWidth()/2 + canvas.getBoundsInParent().getMinX()));
double dy = (event.getSceneY() - (canvas.getBoundsInParent().getHeight()/2 + canvas.getBoundsInParent().getMinY()));
canvas.setScale( scale);
// note: pivot value must be untransformed, i. e. without scaling
canvas.setPivot(f*dx, f*dy);
event.consume();
}
};
public static double clamp( double value, double min, double max) {
if( Double.compare(value, min) < 0)
return min;
if( Double.compare(value, max) > 0)
return max;
return value;
}
}
/**
* An application with a zoomable and pannable canvas.
*/
public class ZoomAndScrollApplication extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Group group = new Group();
// create canvas
PannableCanvas canvas = new PannableCanvas();
// we don't want the canvas on the top/left in this example => just
// translate it a bit
canvas.setTranslateX(100);
canvas.setTranslateY(100);
// create sample nodes which can be dragged
NodeGestures nodeGestures = new NodeGestures( canvas);
Label label1 = new Label("Draggable node 1");
label1.setTranslateX(10);
label1.setTranslateY(10);
label1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
label1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Label label2 = new Label("Draggable node 2");
label2.setTranslateX(100);
label2.setTranslateY(100);
label2.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
label2.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Label label3 = new Label("Draggable node 3");
label3.setTranslateX(200);
label3.setTranslateY(200);
label3.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
label3.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Circle circle1 = new Circle( 300, 300, 50);
circle1.setStroke(Color.ORANGE);
circle1.setFill(Color.ORANGE.deriveColor(1, 1, 1, 0.5));
circle1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
circle1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
Rectangle rect1 = new Rectangle(100,100);
rect1.setTranslateX(450);
rect1.setTranslateY(450);
rect1.setStroke(Color.BLUE);
rect1.setFill(Color.BLUE.deriveColor(1, 1, 1, 0.5));
rect1.addEventFilter( MouseEvent.MOUSE_PRESSED, nodeGestures.getOnMousePressedEventHandler());
rect1.addEventFilter( MouseEvent.MOUSE_DRAGGED, nodeGestures.getOnMouseDraggedEventHandler());
canvas.getChildren().addAll(label1, label2, label3, circle1, rect1);
group.getChildren().add(canvas);
// create scene which can be dragged and zoomed
Scene scene = new Scene(group, 1024, 768);
SceneGestures sceneGestures = new SceneGestures(canvas);
scene.addEventFilter( MouseEvent.MOUSE_PRESSED, sceneGestures.getOnMousePressedEventHandler());
scene.addEventFilter( MouseEvent.MOUSE_DRAGGED, sceneGestures.getOnMouseDraggedEventHandler());
scene.addEventFilter( ScrollEvent.ANY, sceneGestures.getOnScrollEventHandler());
stage.setScene(scene);
stage.show();
canvas.addGrid();
}
}
Let me know if this doesn't help you at all, then I'll delete the post. I'm not interested in the bounty.
I'll just post an answer to summarize the solution to my problem:
The problem why my nodes where lagging behind was because the had no clue about the scaling that might have occured in the pane one level above. So as a final solution I mixed my code with Roland's answer. if anyone has further optimizations in mind, please post them.
Controller class:
public class MainWindowController implements Initializable {
private final Group group = new Group();
private static final double MAX_SCALE = 10.0d;
private static final double MIN_SCALE = .1d;
#FXML
private ScrollPane scrollPane;
#Override
public void initialize(URL url, ResourceBundle rb) {
ZoomableCanvas canvas = new ZoomableCanvas();
MasterNode node1 = new MasterNode("Test");
MasterNode node2 = new MasterNode("Test", 100, 200);
canvas.getChildren().add(node1);
canvas.getChildren().add(node2);
group.getChildren().add(canvas);
scrollPane.setContent(group);
scrollPane.addEventHandler(ScrollEvent.ANY, new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
double delta = 1.2;
double scale = canvas.getScale(); // currently we only use Y, same value is used for X
double oldScale = scale;
if (event.getDeltaY() < 0) {
scale /= delta;
} else {
scale *= delta;
}
scale = clamp(scale, MIN_SCALE, MAX_SCALE);
double f = (scale / oldScale) - 1;
double dx = (event.getSceneX() - (canvas.getBoundsInParent().getWidth() / 2 + canvas.getBoundsInParent().getMinX()));
double dy = (event.getSceneY() - (canvas.getBoundsInParent().getHeight() / 2 + canvas.getBoundsInParent().getMinY()));
canvas.setScale(scale);
// note: pivot value must be untransformed, i. e. without scaling
canvas.setPivot(f * dx, f * dy);
event.consume();
}
});
}
private double clamp(double value, double min, double max) {
if (Double.compare(value, min) < 0) {
return min;
}
if (Double.compare(value, max) > 0) {
return max;
}
return value;
}
}
The canvas:
public class ZoomableCanvas extends Pane {
DoubleProperty scale = new SimpleDoubleProperty(1.0);
public ZoomableCanvas() {
scaleXProperty().bind(scale);
scaleYProperty().bind(scale);
getChildren().addListener((Change<? extends javafx.scene.Node> c) -> {
while (c.next()) {
if (c.wasAdded()) {
for (Node child : c.getAddedSubList()) {
((MasterNode) child).scaleProperty.bind(scale);
}
}
if (c.wasRemoved()) {
for (Node child : c.getRemoved()) {
((MasterNode) child).scaleProperty.unbind();
}
}
}
});
}
public double getScale() {
return scale.get();
}
public void setScale(double scale) {
this.scale.set(scale);
}
public void setPivot(double x, double y) {
setTranslateX(getTranslateX() - x);
setTranslateY(getTranslateY() - y);
}
}
Node:
public class MasterNode extends Parent {
private NodeStatus status = NodeStatus.OK;
private final Image okImage = new Image(getClass().getResourceAsStream("/images/MasterOK.png"));
private ImageView image = new ImageView(okImage);
private final Text label = new Text();
private final Font font = Font.font("Courier", 20);
private DragContext nodeDragContext = new DragContext();
public DoubleProperty scaleProperty = new SimpleDoubleProperty(1.0);
double orgSceneX, orgSceneY;
double layoutX, layoutY;
public MasterNode(String labelText) {
this(labelText, 0, 0);
}
public MasterNode(String labelText, double x, double y) {
scaleXProperty().bind(scaleProperty);
scaleYProperty().bind(scaleProperty);
label.setText(labelText);
label.setFont(font);
label.setLayoutX(okImage.getWidth() + 10);
label.setLayoutY(okImage.getHeight() / 2 + 10);
getChildren().add(image);
getChildren().add(label);
setLayoutX(x);
setLayoutY(y);
setCursor(Cursor.MOVE);
setOnMousePressed(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
// left mouse button => dragging
if (!event.isPrimaryButtonDown()) {
return;
}
nodeDragContext.setMouseAnchorX(event.getSceneX());
nodeDragContext.setMouseAnchorY(event.getSceneY());
Node node = (Node) event.getSource();
nodeDragContext.setTranslateAnchorX(node.getTranslateX());
nodeDragContext.setTranslateAnchorY(node.getTranslateY());
}
});
setOnMouseDragged(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
// left mouse button => dragging
if (!event.isPrimaryButtonDown()) {
return;
}
Node node = (Node) event.getSource();
node.setTranslateX(nodeDragContext.getTranslateAnchorX() + ((event.getSceneX() - nodeDragContext.getMouseAnchorX()) / getScale()));
node.setTranslateY(nodeDragContext.getTranslateAnchorY() + ((event.getSceneY() - nodeDragContext.getMouseAnchorY()) / getScale()));
event.consume();
}
});
}
public double getScale() {
return scaleProperty.get();
}
public void setScale(double scale) {
scaleProperty.set(scale);
}
public NodeStatus getStatus() {
return status;
}
public void setStatus(NodeStatus status) {
this.status = status;
}
}