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 drag Shape nodes across a Pane. Over and above a simple walk-through, I’ll talk about best practice in customising node functionality.

  • Creating flexible functionality that can be built upon
  • Making the functionality reusable to keep future code concise.
  • Encapsulating functionality to keep our Controller class manageable.
Draggable node with property binding allowing the functionality to be selectively disabled

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:

  • Record the initial location of the node using the Node’s LayoutX and LayoutY properties.
  • Measure the total mouse movement as the node is dragged, applying this movement to the node.
  • Commit the changes to the Node when the mouse is released.

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.

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:

  • Code complexity: defining all of this in your Controller is very messy. As your program becomes more complex, and you define more functionality, your Controller is going to explode with code.
  • User expectations: users expect to be able to cancel a move command by pressing the right mouse button. If you try that on the code above, notice that it breaks the application because it resets the anchor positions. We need to fix that. Adding this functionality in the Controller only makes the complexity worse.
  • Unstable convenience methods: defining this functionality based on convenience methods like setOnMousePressed() is messy. Later in your project, you might decide that you’d like to add selection functionality, or contextual colour highlighting. If you have to add that by modifying your convenience methods, tracking the state of every process simultaneously becomes a problem.

b. The Correct Strategy

The correct strategy involves three main steps to making the code more readable and reusable:

  • Encapsulate the drag functionality in a Class with responsibility for node-dragging
  • Correctly track the progress of a click-drag-release cycle to allow right-click cancellation
  • Define this functionality with Event Filters to maximise flexibility of resulting code.

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:

  • If the primary mouse button gets pressed, start a drag event
  • If the secondary mouse button gets pressed, cancel the click-drag-release cycle and reset the node to its original position

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.

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.