Skip to main content

Introducing Custom Cursors to JavaFX

July 14, 2009

{cs.r.title}







JavaFX 1.2 offers many interesting features for building rich user interfaces; keyframe animation,
controls, layouts, and effects are examples. However, version 1.2 and its predecessors lack certain desirable UI features.
For example, they don't support custom cursors (user-defined mouse pointer shapes, such as a "grabbing hand" that
appears over an object being dragged).

This article addresses the custom cursors oversight. You first learn how to implement custom cursors in Version 1.2. Next,
you learn how to implement this feature in Version 1.1.1. As an aside, you implement JavaFX's stage icons feature in 1.1.1's
custom cursors demo application, to give this application an identical icon appearance to its 1.2 counterpart, which presents
custom icons on its titlebar and in other places.

Note: I've tested my custom cursors code with JavaFX 1.2, JavaFX 1.1.1, and
Java SE 6u12 on the Windows XP SP3 platform, the only platform available to me
for development and testing.

Supporting Custom Cursors in JavaFX 1.2

JavaFX 1.2's support for mouse cursors consists of the javafx.scene.Cursor class, and
javafx.scene.Node's and javafx.scene.Scene's cursor variables, to which you assign
Cursor instances (such as Cursor.CROSSHAIR or Cursor.HAND). Although
Cursor and cursor are available to all profiles, only the default cursor is displayed for the
mobile profile.

Unlike version 1.2's existing cursor support, my custom cursor support is restricted to the desktop profile. This limitation
is caused by the custom cursor implementation's dependence on the Abstract Window Toolkit's java.awt.Cursor,
java.awt.Image, java.awt.Point, and java.awt.Toolkit classes. These classes are
referenced in Listing 1's CustomCursor.fx source code.

Listing 1: CustomCursor.fx

/*
* CustomCursor.fx
*/

package customcursorsdemo;

import java.awt.Point;
import java.awt.Toolkit;

import javafx.scene.Cursor;

import javafx.scene.image.Image;

public def OPENHAND = CustomCursor
{
    url: "{__DIR__}res/openhand.gif"
    name: "open"
}

public def CLOSEDHAND = CustomCursor
{
    url: "{__DIR__}res/closedhand.gif"
    name: "close"
}

public class CustomCursor extends Cursor
{
    public-read var cursorAWT: java.awt.Cursor;

    var url: String;
    var name: String;

    init
    {
       def imageAWT = Image { url: url }.platformImage as java.awt.Image;
       def toolkit = Toolkit.getDefaultToolkit ();
       cursorAWT = toolkit.createCustomCursor (imageAWT, new Point (0, 0), name)
    }
}
CustomCursor.fx (part of a NetBeans IDE 6.5.1 CustomCursorsDemo project -- see this article's
accompanying code archive) introduces a CustomCursor class for creating custom cursors.
Each CustomCursor instance requires a url path to its image file, and a localizable
name for use with Java Accessibility.

CustomCursor creates the custom cursor in its init block, which is executed when instantiating this
class. The init block first creates a javafx.scene.image.Image instance to load the cursor's image
file, and accesses this class's undocumented platformImage variable to obtain the equivalent
java.awt.Image instance.

After obtaining the default Toolkit instance, init uses this instance to invoke
Toolkit's public Cursor createCustomCursor(Image cursor, Point hotSpot, String name) method with
the recently obtained java.awt.Image instance, a Point instance set to default hotspot (0, 0), and
name as arguments. The returned java.awt.Cursor is saved for future access.

Although you can instantiate CustomCursor from outside this source file, you shouldn't do so because you cannot
initialize name and url. Furthermore, if you assign an uninitialized CustomCursor
instance to Node's or Scene's cursor variable (which is possible because
CustomCursor subclasses Cursor), you'll be rewarded with a thrown runtime exception.

Instead, you must only assign CustomCursor.OPENHAND or CustomCursor.CLOSEDHAND (or your own
CustomCursor constants) to Node's/Scene's cursor variable. These
pre-initialized CustomCursor instances create java.awt.Cursor instances for the cursor shapes
(shown in Figure 1) that are stored in res/openhand.gif and res/closedhand.gif.

open hand cursor

Figure 1. The open hand cursor shape appears on the left; the closed hand cursor shape appears on the right
-- green is the transparent color. These 32-by-32-pixel shapes are scaled up to show the detail.

Assigning CustomCursor.OPENHAND or CustomCursor.CLOSEDHAND to Node's or
Scene's cursor variable doesn't result in the CustomCursor instance's cursor shape
being displayed when the mouse moves over that node or scene. Somehow, we have to tell the JavaFX runtime to access the
java.awt.Cursor object that's stored in CustomCursor's cursorAWT variable.

We first subclass com.sun.javafx.tk.swing.SwingToolkit (see Listing 2), which is stored (on my Windows XP
platform) in C:\Program Files\NetBeans 6.5.1\javafx2\javafx-sdk\lib\desktop\javafx-ui-swing.jar. The subclass
overrides SwingToolkit's convertCursorFromFX(cursor: Cursor) function to return
cursorAWT when cursor is a CustomCursor instance.

Listing 2: CursorToolkit.fx

/*
* CursorToolkit.fx
*/

package customcursorsdemo;

import javafx.scene.Cursor;

import com.sun.javafx.tk.swing.SwingToolkit;

public class CursorToolkit extends SwingToolkit
{
    public override function convertCursorFromFX (cursor: Cursor)
    {
        if (cursor instanceof CustomCursor)
            return (cursor as CustomCursor).cursorAWT;

        return super.convertCursorFromFX (cursor) // use predefined AWT cursor
    }
}

The SwingToolkit dependency requires that javafx-ui-swing.jar be added to the NetBeans project
classpath. Accomplish this task by right-clicking CustomCursorsDemo in the projects window, and selecting
Properties from the pop-up menu to access this project's Project
Properties
dialog. Then select the dialog's Libraries category, and click its
panel's Add JAR/Folder button to add this file.

While Project Properties is still open, select the Run category.
Enter -Djavafx.toolkit=customcursorsdemo.CursorToolkit in the resulting panel's
JVM Arguments text field. This command-line option tells the JavaFX runtime to use
customcursorsdemo.CursorToolkit in place of SwingToolkit; the runtime defaults to
SwingToolkit if this property isn't set.

At this point, the JavaFX runtime is capable of changing the mouse cursor shape. To demonstrate this new feature, I've
prepared a demo that drags a circle around the scene. Before we examine this demo's main source code, let's examine Listing
3's source code, the DraggableCircle class, which adds drag-and-drop support to the
javafx.scene.shape.Circle class.

Listing 3: DraggableCircle.fx

/*
* DraggableCircle.fx
*/

package customcursorsdemo;

import java.awt.Robot;

import javafx.scene.input.MouseEvent;

import javafx.scene.shape.Circle;

public class DraggableCircle extends Circle
{
    public var maxX: Number;
    public var maxY: Number;

    var startX = 0.0;
    var startY = 0.0;

    def robot = new Robot ();

    override def onMousePressed = function (me: MouseEvent): Void
    {
        startX = me.sceneX-translateX;
        startY = me.sceneY-translateY;
        cursor = CustomCursor.CLOSEDHAND;
        robot.mouseMove (me.screenX+1, me.screenY);
        robot.mouseMove (me.screenX, me.screenY)
    }

    override def onMouseDragged = function (me: MouseEvent): Void
    {
        var tx = me.sceneX-startX;
        if (tx < 0)
            tx = 0;
        if (tx > maxX-boundsInLocal.width)
            tx = maxX-boundsInLocal.width;
        translateX = tx;

        var ty = me.sceneY-startY;
        if (ty < 0)
            ty = 0;
        if (ty > maxY-boundsInLocal.height)
            ty = maxY-boundsInLocal.height;
        translateY = ty
    }

    override def onMouseReleased = function (me: MouseEvent): Void
    {
        cursor = CustomCursor.OPENHAND;
        robot.mouseMove (me.screenX+1, me.screenY);
        robot.mouseMove (me.screenX, me.screenY)
    }
}
DraggableCircle exposes a pair of Number variables, maxX and maxY, that
specify the maximum boundaries of the area in which the circle can be dragged -- this area's minimum boundaries are fixed at
(0, 0). You'll often bind maxX and maxY to Scene's width and
height variables.

The actual drag-and-drop logic is confined to the functions assigned to the variables onMousePressed and
onMouseDragged. The onMousePressed function records the starting position of a drag-and-drop
operation, whereas the onMouseDragged function updates the node's translateX and
translateY variables, keeping the node's location within the bounding box governed by (0, 0) and
(maxX, maxY).

Prior to a drag, onMousePressed assigns CustomCursor.CLOSEDHAND to its node's cursor
variable. Similarly, onMouseReleased assigns CustomCursor.OPENHAND to this variable after a drop.
However, without help from the java.awt.Robot class, the cursor doesn't immediately change to a closed hand when
a mouse button is pressed, or revert to an open hand when the button is released.

The JavaFX runtime's javafx.scene.Scene.MouseHandler class (in javafx-ui-common.jar) contains a
process() function that invokes convertCursorFromFX() in response to mouse events. If a mouse event
hasn't occurred, process() and convertCursorFromFX() aren't invoked, and the cursor shape isn't
updated.

To remedy this situation, I invoke Robot's mouseMove() method twice whenever I change the cursor.
The first invocation ensures that process() and convertCursorFromFX() are invoked, and that the
cursor shape is changed. The second invocation prevents the mouse cursor from slowly creeping toward the right side of the
screen, by reverting it to its original location.


Bad Robot!
Robot's constructor throws a java.awt.AWTException if this class isn't supported by the
underlying platform. For this reason, the use of Robot with JavaFX isn't a good solution if you're making your
JavaFX application available to any platform. As an alternative, it might be possible to directly invoke the
process() function without causing runtime problems, but I haven't tested this possibility.

Listing 4 presents the demo's main source file.

Listing 4: Main.fx

/*
* Main.fx
*/

package customcursorsdemo;

import javafx.scene.Scene;

import javafx.scene.effect.Lighting;

import javafx.scene.effect.light.DistantLight;

import javafx.scene.image.Image;

import javafx.scene.paint.Color;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;

import javafx.stage.Stage;

Stage
{
    title: "Custom Cursors Demo"
    width: 350
    height: 350

    icons:
    [
        Image { url: "{__DIR__}res/icon16x16.png" }
        Image { url: "{__DIR__}res/icon32x32.png" }
    ]

    var sceneRef: Scene
    scene: sceneRef = Scene
    {
        fill: LinearGradient
        {
            startX: 0.0
            startY: 0.0
            endX: 0.0
            endY: 1.0
            stops:
            [
                Stop
                {
                    offset: 0.0
                    color: Color.BLUE
                }
                Stop
                {
                    offset: 1.0
                    color: Color.ALICEBLUE
                }
            ]
        }

        content: DraggableCircle
        {
            centerX: 30.0
            centerY: 30.0
            radius: 30.0
            maxX: bind sceneRef.width
            maxY: bind sceneRef.height
            fill: Color.RED
            effect: Lighting
            {
                light: DistantLight
                {
                    azimuth: -135
                }
                surfaceScale: 5
            }
            cursor: CustomCursor.OPENHAND
        }
    }
}

This code creates a scene consisting of a red draggable circle on a bluish gradient background -- see Figure 2. The circle's
lighting effect gives it a more realistic, three-dimensional appearance. Also, the circle's cursor variable is
initialized to CustomCursor.OPENHAND so that an open hand shape is displayed when the mouse first enters the
circle's boundaries.

drag circle, Windows XP task switcher

Figure 2. Preparing to drag the circle node. The Windows XP task switcher is also shown to reveal the larger
stage icon.

While playing with this demo, you might notice that clicking the mouse over the background and dragging the arrow cursor
over the circle causes this node to reposition itself somewhere nearby, with the cursor immediately reverting to the open hand
shape. In case you're wondering, Robot isn't responsible for the repositioning anomaly; the anomaly occurs even without Robot.

Supporting Custom Cursors in JavaFX 1.1.1

JavaFX 1.1.1 provides the same overall support for cursors (a Cursor class, and a cursor variable
in the Node and Scene classes), but this support is limited to the desktop profile. However, its
Cursor class makes it much easier to implement desktop-oriented custom cursors, which Listing 5 accomplishes.

Listing 5: CustomCursor.fx

/*
* CustomCursor.fx
*/

package customcursorsdemo1_1_1;

import java.awt.Point;
import java.awt.Toolkit;

import javafx.scene.Cursor;

import javafx.scene.image.Image;

public def OPENHAND = CustomCursor
{
    url: "{__DIR__}res/openhand.gif"
    name: "open"
}

public def CLOSEDHAND = CustomCursor
{
    url: "{__DIR__}res/closedhand.gif"
    name: "closed"
}

public class CustomCursor extends Cursor
{
    public-read var cursorAWT: java.awt.Cursor;

    var url: String;
    var name: String;

    public override function impl_getAWTCursor (): java.awt.Cursor
    {
        if (cursorAWT == null)
        {
            def imageAWT = Image { url: url }.bufferedImage;
            def toolkit = Toolkit.getDefaultToolkit ();
            cursorAWT = toolkit.createCustomCursor (imageAWT, new Point (0, 0),
                                                    name)
        }
        cursorAWT
    }
}
CustomCursor.fx (part of a NetBeans IDE 6.5.1 CustomCursorsDemo1_1_1 project -- see this article's
code archive) is similar to its Listing 1 equivalent. The two differences are
impl_getAWTCursor(), an undocumented Cursor function invoked by the JavaFX runtime when it needs an
AWT-based cursor, and bufferedImage, an Image variable that returns the underlying
java.awt.Image.

CustomCursor is all that's required to make custom cursor shapes available to JavaFX scenes and nodes. To prove
this, I've prepared a draggable circle demo that's similar to the demo presented earlier. Because the main source file is
identical to Listing 4 (except for the package statement), I'm only showing the demo's
DraggableCircle source code -- see Listing 6.

Listing 6: DraggableCircle.fx

/*
* DraggableCircle.fx
*/

package customcursorsdemo1_1_1;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import javafx.scene.input.MouseEvent;

import javafx.scene.shape.Circle;

public class DraggableCircle extends Circle
{
    public var maxX: Number;
    public var maxY: Number;

    var startX = 0.0;
    var startY = 0.0;

    override def onMousePressed = function (me: MouseEvent): Void
    {
        startX = me.sceneX-translateX;
        startY = me.sceneY-translateY;
        cursor = CustomCursor.CLOSEDHAND;
        updateJSGPanel (CustomCursor.CLOSEDHAND)
    }

    override def onMouseDragged = function (me: MouseEvent): Void
    {
        var tx = me.sceneX-startX;
        if (tx < 0)
            tx = 0;
        if (tx > maxX-boundsInLocal.width)
            tx = maxX-boundsInLocal.width;
        translateX = tx;

        var ty = me.sceneY-startY;
        if (ty < 0)
            ty = 0;
        if (ty > maxY-boundsInLocal.height)
            ty = maxY-boundsInLocal.height;
        translateY = ty
    }

    override def onMouseReleased = function (me: MouseEvent): Void
    {
        cursor = CustomCursor.OPENHAND;
        updateJSGPanel (CustomCursor.OPENHAND)
    }

    function updateJSGPanel (cc: CustomCursor): Void
    {
        def panel = impl_getSGNode ().getPanel ().getClass ().getSuperclass ();
        def methods: Method [] = panel.getDeclaredMethods ();
        for (i in [0..<sizeof methods])
             if ("setCursor" == methods [i].getName ())
             {
                 def x = methods [i].getGenericParameterTypes ();
                 if (sizeof x == 2)
                     try
                     {
                         // Invoke JSGPanel's private void setCursor(Cursor
                         // cursor, boolean isDefault) method, passing false to
                         // isDefault so that the custom cursor is not selected
                         // as the JSGPanel's default cursor.

                         def o: Object = impl_getSGNode ().getPanel ();
                         methods [i].setAccessible (true);
                         methods [i].invoke (o, cc.cursorAWT, false)
                     }
                     catch (ex: java.lang.IllegalAccessException)
                     {
                         println (ex)
                     }
                     catch (ex: InvocationTargetException)
                     {
                         println (ex)
                     }
             }
    }
}

Although similar, Listings 6 and 3 present two key differences: the absence of the Robot class and the presence
of the function updateJSGPanel(). I use this function to correct two visual anomalies -- I could have used the
Robot class to correct the visual anomaly associated with the onMouseReleased function, but it
wouldn't correct the onMousePressed function's anomaly:

  • The onMousePressed updateJSGPanel() function call is needed to correct an anomaly where the default
    arrow pointer cursor appears while you're dragging the circle node. You can view this anomaly for yourself by commenting out
    this updateJSGPanel() function call, dragging the default arrow pointer cursor onto the circle node, releasing
    the mouse button, pressing the mouse button (without moving the mouse), and dragging the circle node.
  • The onMouseReleased updateJSGPanel() function call is needed to correct an anomaly where the
    default arrow pointer cursor sometimes appears over the circle node at the end of a drag. You can view this anomaly for
    yourself by commenting out this updateJSGPanel() function call and quickly dragging the circle node over a large
    distance. Once the arrow appears, the slightest mouse movement over the node will revert the mouse cursor to the open hand
    shape.

The updateJSGPanel() function uses Java reflection to invoke the

private void setCursor(Cursor cursor, 
boolean isDefault)
method in the JSGPanel layer of the circle node's Java implementation. This method is
invoked with cursor set to CustomCursor's cursorAWT variable value, and with
isDefault set to false.

Assuming that you've loaded CustomCursorsDemo1_1_1 into NetBeans, you'll also need to add, via the
Project Properties dialog, Scenario.jar (in the C:\Program Files\NetBeans
6.5.1\javafx2\javafx-sdk\lib\desktop
directory, on my platform) to the project's classpath before you can compile and
run the code. You need to add this JAR because of impl_getSGNode() (see Listing 6).

Figure 3 reveals the circle node being dragged around the scene -- note the closed hand cursor over this node.

Dragging the circle node

Figure 3. Dragging the circle node

While playing with this demo, you might notice that clicking a mouse button while over the background and dragging the arrow
cursor onto the circle causes the arrow shape to appear over this node. Clicking a mouse button changes the cursor to the
closed hand shape; moving the mouse changes the cursor to the open hand shape. As with the version 1.2 demo anomaly, this
anomaly doesn't cause any problems.

Supporting Stage Icons for the JavaFX 1.1.1 Version of the Custom Cursors Demo

Figure 3 reveals a deficiency in JavaFX 1.1.1. Unlike version 1.2, 1.1.1 doesn't support its javafx.stage.Stage
class's icons variable (for desktop UIs). Fortunately, it's not too difficult to provide this support. However,
you'll need to work with undocumented capabilities and swap out JavaFX's minimal Java runtime with a runtime that takes Java
SE 6 into account. For starters, take a look at Listing 7.

Listing 7: IconsStage.fx

/*
* IconsStage.fx
*/

package customcursorsdemo1_1_1;

import java.util.ArrayList;

import javafx.stage.Stage;

public class IconsStage extends Stage
{
    var listImages: ArrayList;
    override var icons on replace
    {
        listImages = new ArrayList ();
        for (i in [0..<sizeof icons])
             listImages.add (icons [i].bufferedImage)
    }
    postinit
    {
        def sdsc = impl_stageDelegate.getClass ().getSuperclass ();
        def fields = sdsc.getDeclaredFields ();
        for (i in [0..<sizeof fields])
             if ("$window" == fields [i].getName ())
             {
                  def w = fields [i].get (impl_stageDelegate);
                  new Win ().getWindow (w).setIconImages (listImages);
                  break
             }
    }
}

Our first task is to subclass Stage, overriding the icons variable in the process. A replace
trigger is attached to this variable, and used to store the icons sequence's underlying
java.awt.Images (thanks to javafx.scene.image.Image's bufferedImage variable) in an
arraylist. (This happens prior to the postinit code being executed.)

Moving on, the postinit code uses impl_stageDelegate to obtain the underlying
com.sun.javafx.stage.FrameStageDelegate object. getClass() and
getSuperclass() are then invoked on this object to obtain the Class object for its
com.sun.javafx.stage.WindowStageDelegate superclass.

WindowStageDelegate's package-private $window variable, of the type
com.sun.javafx.runtime.location.ObjectVariable, stores the needed javax.swing.JFrame reference.
Because JavaFX doesn't let us access an ObjectVariable's value, this task is shunted to the
getWindow() method of Listing 8's Win Java class.

Listing 8: Win.java

// Win.java

package customcursorsdemo1_1_1;

import java.awt.Window;

class Win
{
   public Window getWindow (Object o)
   {
       com.sun.javafx.runtime.location.ObjectVariable ov;
       ov = (com.sun.javafx.runtime.location.ObjectVariable) o;
       return (Window) ov.get ();
   }
}

Upon return from this method, postinit invokes java.awt.Window's setIconImages()
method via getWindow()'s returned JFrame reference (which happens to be a descendant of
Window). The previously saved array list of java.awt.Images is passed to
setIconImages(), making these images available to the JavaFX desktop application's frame window.

You'll notice that Listings 7 and 8 are part of the customcursorsdemo1_1_1 package -- their source files are
stored in a separate xtra directory in this article's code archive. Before adding these
files to the CustomCursorsDemo1_1_1 project, you must replace the NetBeans-bundled rt15.jar file
with its Java SE 6 equivalent, to access the 1.6 version of java.awt.Window and its setIconImages()
method.

The Java SE 6 equivalent, rt.jar, is located in the JDK's %JAVA_HOME%/jre/lib directory. After
backing up rt15.jar (in case you want to revert to it at a later time), copy rt.jar to
rt15.jar. Then restart NetBeans so that this change takes effect. (For more information, check out Stephen
Chin's "Hacking JavaFX 1.0 to use Java 1.6 Features" blog post.)

Conclusion

Although it's not good to use undocumented capabilities because dependent code can break in future JavaFX versions (even code
that relies on documented APIs can break), I believe that a custom cursors facility for JavaFX 1.2 outweighs the risk of
future code breakage. Also, I plan to implement custom cursors in the next JavaFX version, unless Sun or someone else
(perhaps you) beats me to it.

Resources

  • JavaFX's official website

  • width="1" height="1" border="0" alt=" " />
    Jeff Friesen is a freelance software developer and educator specializing in Java technology. Check out his site at javajeff.mb.ca.
    Related Topics >> GUI   |   Programming   |   Featured Article   |