Dragging shapes is all about Events. Moving shapes is about understanding when information cascades start in JavaFX and how to intercept them effectively.
Dragging shapes can be accomplished by attaching Listeners to a target node’s MouseEvents. These intercept JavaFX’s click-drag-release cycle to store the initial position of the node, and the total mouse movement. This allows the real-time position of the node to be calculated as it is dragged.
What we’ll achieve in this tutorial
Over the course of this tutorial, I’ll cover how to Shape nodes across a Pane (if you’re looking for a guide on drag-and-drop applications, you’ll find it here!). Over and above a simple walk-through, I’ll talk about best practice in customising node functionality.

We’ll focus on how to organise your code to make that dragging capability flexible, concise, and reusable.
Adding functionality to a program isn’t just about grabbing code snippets and cramming them in your controller. It’s about creating reusable functionality.
Dragging Shapes on a Pane
The simplest use case for dragging shapes in JavaFX is Shape
nodes that have been generated and need to be dragged on a Pane
node.
a. The Simplest Solution
The simplest solution is to intercept JavaFX’s click-drag-release cycle in three simple steps, defining the logic up-top in the initialize()
method of the controller. The three steps are:
We can insert all of this code into the Controller of our View (assuming we’ve defined draggableCircle
as a node in our FXML file. Click the drop-down for the code.
Even in this simple example there are a few best practices you need to bear in mind:
package com.edencoding; import javafx.fxml.Initializable; import javafx.scene.shape.Circle; import java.net.URL; import java.util.ResourceBundle; public class Controller implements Initializable { public Circle draggableCircle; private double anchorX; private double anchorY; private double mouseOffsetFromNodeZeroX; private double mouseOffsetFromNodeZeroY; @Override public void initialize(URL location, ResourceBundle resources) { draggableCircle.setOnMousePressed(event -> { anchorX = event.getSceneX(); anchorY = event.getSceneY(); }); draggableCircle.setOnMouseDragged(event -> { draggableCircle.setTranslateX(event.getSceneX() - anchorX); draggableCircle.setTranslateY(event.getSceneY() - anchorY); }); draggableCircle.setOnMouseReleased(event -> { //commit changes to LayoutX and LayoutY draggableCircle.setLayoutX(event.getSceneX() - mouseOffsetFromNodeZeroX); draggableCircle.setLayoutY(event.getSceneY() - mouseOffsetFromNodeZeroY); //clear changes from TranslateX and TranslateY draggableCircle.setTranslateX(0); draggableCircle.setTranslateY(0); }); } }
Here, we used the convenience methods setOnMouseClicked()
, setOnMouseDragged()
and setOnMouseReleased()
to define the EventHandler
that triggers on each of the specified events.
If you’re just looking for the core functionality, this might do quite nicely, but there are three main problems that come from this approach:
b. The Correct Strategy
The correct strategy involves three main steps to making the code more readable and reusable:
We’ll do each one in turn to make concise, reusable code.
i. Encapsulating drag responsibility in a DragController class
To stop our Controller being overly-cluttered, let’s define a new class DragController. This takes our Node as a parameter in the constructor.
public DragController(Node target){ this(target, false); } public DragController(Node target, boolean isDraggable) { this.target = target; createHandlers(); setDraggable(isDraggable); }
It will also fire two methods – createHandlers()
and setDraggable()
. We’ve created two constructors. One that creates an object, but doesn’t make the node draggable by default.
The method createHandlers() repeats the original code for storing the initial mouse location, updating the location of the node during drag movements, and committing this to the node Layout properties on mouse release.
The immediate flexibility we gain from encapsulating this code is that we can define the setDraggable()
method. This is a public method that will either attach or detach the EventHandler objects to the node based on whether we currently want it to be draggable.
We maintain strong references to the EventHandler objects to allow us to add and remove the functionality whenever we need.
Because this functionality will work on any node, we’ll attach EventHandler
objects as Event Filters. That way, the functionality cannot be overridden by children.
private EventHandler<MouseEvent> setAnchor; private EventHandler<MouseEvent> updatePositionOnDrag; private EventHandler<MouseEvent> commitPositionOnRelease; public void setDraggable(boolean draggable) { if (draggable) { target.addEventFilter(MouseEvent.MOUSE_PRESSED, setAnchor); target.addEventFilter(MouseEvent.MOUSE_DRAGGED, updatePositionOnDrag); target.addEventFilter(MouseEvent.MOUSE_RELEASED, commitPositionOnRelease); } else { target.removeEventFilter(MouseEvent.MOUSE_PRESSED, setAnchor); target.removeEventFilter(MouseEvent.MOUSE_DRAGGED, updatePositionOnDrag); target.removeEventFilter(MouseEvent.MOUSE_RELEASED, commitPositionOnRelease); } }
One common extension of this functionality with Event Filters is to create draggable groups of nodes. Oracle made a good example of this for dragging panels of controls.
ii. Satisfying user expectations for drag events
Now that we’ve encapsulated the code, we can add functionality without cluttering the Controller class. In this case, we’ll filter MouseKey.PRESSED events by whether the secondary mouse button is down. That way, when the secondary mouse button is pressed, it cancel’s the whole drag event.
setAnchor = event -> { if (event.isPrimaryButtonDown()) { cycleStatus = ACTIVE; anchorX = event.getSceneX(); anchorY = event.getSceneY(); mouseOffsetFromNodeZeroX = event.getX(); mouseOffsetFromNodeZeroY = event.getY(); } if (event.isSecondaryButtonDown()) { cycleStatus = INACTIVE; target.setTranslateX(0); target.setTranslateY(0); }
Strictly, this doesn’t measure whether the secondary mouse button was the last mouse button to be pressed, but the effect is just what we want:
To add the remaining functionality, we simply need to check that the drag event is still active whenever we go to implement a drag or release action.
updatePositionOnDrag = event -> { if (cycleStatus != INACTIVE) { target.setTranslateX(event.getSceneX() - anchorX); target.setTranslateY(event.getSceneY() - anchorY); } }; commitPositionOnRelease = event -> { if (cycleStatus != INACTIVE) { //commit changes to LayoutX and LayoutY target.setLayoutX(event.getSceneX() - mouseOffsetFromNodeZeroX); target.setLayoutY(event.getSceneY() - mouseOffsetFromNodeZeroY); //clear changes from TranslateX and TranslateY target.setTranslateX(0); target.setTranslateY(0); } };
iii. Add functionality to expose drag-ability as a property
An optional – but useful – final touch is to expose whether the node is draggable as a BooleanProperty, which allows us to bind that functionality however we want. For example, to turn this functionality off at the literal press of a button.
private BooleanProperty isDraggable; public void createDraggableProperty() { isDraggable = new SimpleBooleanProperty(); isDraggable.addListener((observable, oldValue, newValue) -> { if (newValue) { target.addEventFilter(MouseEvent.MOUSE_PRESSED, setAnchor); target.addEventFilter(MouseEvent.MOUSE_DRAGGED, updatePositionOnDrag); target.addEventFilter(MouseEvent.MOUSE_RELEASED, commitPositionOnRelease); } else { target.removeEventFilter(MouseEvent.MOUSE_PRESSED, setAnchor); target.removeEventFilter(MouseEvent.MOUSE_DRAGGED, updatePositionOnDrag); target.removeEventFilter(MouseEvent.MOUSE_RELEASED, commitPositionOnRelease); } }); } public boolean isIsDraggable() { return isDraggable.get(); } public BooleanProperty isDraggableProperty() { return isDraggable; }
iv. Using the result
Setting up our draggable shape in the Controller has plummeted from 32 to one line of code. To take advantage of our new binding capability, we’ll include a CheckBox in our scene, which we’ll bind to the ability to drag the node. That brings our total set up to a glorious 2 lines of code.
@Override public void initialize(URL location, ResourceBundle resources) { DragController dragController = new DragController(circle, true); dragController.isDraggableProperty().bind(isDraggableBox.selectedProperty()); }
The result is a draggable node that gives us the flexibility to add additional functionality as we choose. To demonstrate this, we’ll use the convenience methods setOnMouseClicked()
and setOnMouseReleased()
to change the circle colour as it’s highlighted.
circle.setOnMousePressed(event -> { circle.setFill(Color.RED); }); circle.setOnMouseReleased(event -> { circle.setFill(Color.DODGERBLUE); });
The result is a draggable Shape (in this case a circle) that turns red when it’s clicked:

As always, the full code for the DragController is in the drop-down do you don’t have to cobble it together from the code snippets we’ve walked through.
package com.edencoding.nodeFunctionality; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.event.EventHandler; import javafx.scene.Node; import javafx.scene.input.MouseEvent; public class DragController { private final Node target; private double anchorX; private double anchorY; private double mouseOffsetFromNodeZeroX; private double mouseOffsetFromNodeZeroY; private EventHandler<MouseEvent> setAnchor; private EventHandler<MouseEvent> updatePositionOnDrag; private EventHandler<MouseEvent> commitPositionOnRelease; private final int ACTIVE = 1; private final int INACTIVE = 0; private int cycleStatus = INACTIVE; private BooleanProperty isDraggable; public DragController(Node target) { this(target, false); } public DragController(Node target, boolean isDraggable) { this.target = target; createHandlers(); createDraggableProperty(); this.isDraggable.set(isDraggable); } private void createHandlers() { setAnchor = event -> { if (event.isPrimaryButtonDown()) { cycleStatus = ACTIVE; anchorX = event.getSceneX(); anchorY = event.getSceneY(); mouseOffsetFromNodeZeroX = event.getX(); mouseOffsetFromNodeZeroY = event.getY(); } if (event.isSecondaryButtonDown()) { cycleStatus = INACTIVE; target.setTranslateX(0); target.setTranslateY(0); } }; updatePositionOnDrag = event -> { if (cycleStatus != INACTIVE) { target.setTranslateX(event.getSceneX() - anchorX); target.setTranslateY(event.getSceneY() - anchorY); } }; commitPositionOnRelease = event -> { if (cycleStatus != INACTIVE) { //commit changes to LayoutX and LayoutY target.setLayoutX(event.getSceneX() - mouseOffsetFromNodeZeroX); target.setLayoutY(event.getSceneY() - mouseOffsetFromNodeZeroY); //clear changes from TranslateX and TranslateY target.setTranslateX(0); target.setTranslateY(0); } }; } public void createDraggableProperty() { isDraggable = new SimpleBooleanProperty(); isDraggable.addListener((observable, oldValue, newValue) -> { if (newValue) { target.addEventFilter(MouseEvent.MOUSE_PRESSED, setAnchor); target.addEventFilter(MouseEvent.MOUSE_DRAGGED, updatePositionOnDrag); target.addEventFilter(MouseEvent.MOUSE_RELEASED, commitPositionOnRelease); } else { target.removeEventFilter(MouseEvent.MOUSE_PRESSED, setAnchor); target.removeEventFilter(MouseEvent.MOUSE_DRAGGED, updatePositionOnDrag); target.removeEventFilter(MouseEvent.MOUSE_RELEASED, commitPositionOnRelease); } }); } public boolean isIsDraggable() { return isDraggable.get(); } public BooleanProperty isDraggableProperty() { return isDraggable; } }
Conclusions
Dragging shapes in JavaFX is a simple case of intercepting the MouseEvents corresponding to click, drag, and release mouse actions.
By encapsulating the dragging functionality ensures the Controller class remains concise and readable. It also frees us to re-use that functionality and extend it when we need to.
Finally, by using Event Filters, rather than using convenience methods, we can layer functionality on nodes to create custom behaviour in a sustainable and reusable way.