Introducing Custom Cursors to JavaFX Blog

Version 2

    {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 thejavafx.scene.Cursor class, andjavafx.scene.Node's andjavafx.scene.Scene's cursor variables, to which you assign Cursor instances (such asCursor.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, andjava.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.1CustomCursorsDemo project -- see this article's accompanying code archive) introduces aCustomCursor class for creating custom cursors. EachCustomCursor instance requires a url path to its image file, and a localizable name for use with Java Accessibility.

    CustomCursor creates the custom cursor in itsinit block, which is executed when instantiating this class. The init block first creates ajavafx.scene.image.Image instance to load the cursor's image file, and accesses this class's undocumentedplatformImage variable to obtain the equivalentjava.awt.Image instance.

    After obtaining the default Toolkit instance,init uses this instance to invokeToolkit's public Cursor createCustomCursor(Image cursor, Point hotSpot, String name) method with the recently obtained java.awt.Image instance, a Pointinstance 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 toNode's or Scene's cursorvariable (which is possible because CustomCursorsubclasses Cursor), you'll be rewarded with a thrown runtime exception.

    Instead, you must only assign CustomCursor.OPENHANDor CustomCursor.CLOSEDHAND (or your ownCustomCursor constants) toNode's/Scene's cursorvariable. These pre-initialized CustomCursor instances create java.awt.Cursor instances for the cursor shapes (shown in Figure 1) that are stored inres/openhand.gif andres/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 orCustomCursor.CLOSEDHAND to Node's orScene'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 thejava.awt.Cursor object that's stored inCustomCursor's cursorAWT variable.

    We first subclasscom.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'sconvertCursorFromFX(cursor: Cursor) function to returncursorAWT when cursor is aCustomCursor 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 thatjavafx-ui-swing.jar be added to the NetBeans project classpath. Accomplish this task by right-clickingCustomCursorsDemo in the projects window, and selecting Properties from the pop-up menu to access this project's Project Propertiesdialog. Then select the dialog's Librariescategory, and click its panel's Add JAR/Folderbutton to add this file.

    While Project Properties is still open, select the Run category. Enter-Djavafx.toolkit=customcursorsdemo.CursorToolkitin the resulting panel's JVM Arguments text field. This command-line option tells the JavaFX runtime to usecustomcursorsdemo.CursorToolkit in place ofSwingToolkit; the runtime defaults toSwingToolkit 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, theDraggableCircle 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 ofNumber variables, maxX andmaxY, 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 maxXand maxY to Scene's widthand height variables.

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

    Prior to a drag, onMousePressed assignsCustomCursor.CLOSEDHAND to its node'scursor variable. Similarly,onMouseReleased assignsCustomCursor.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'sjavafx.scene.Scene.MouseHandler class (injavafx-ui-common.jar) contains aprocess() function that invokesconvertCursorFromFX() in response to mouse events. If a mouse event hasn't occurred, process() andconvertCursorFromFX() aren't invoked, and the cursor shape isn't updated.

    To remedy this situation, I invoke Robot'smouseMove() method twice whenever I change the cursor. The first invocation ensures that process() andconvertCursorFromFX() 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 ajava.awt.AWTException if this class isn't supported by the underlying platform. For this reason, the use ofRobot 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 theprocess() 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 withoutRobot.

    Supporting Custom Cursors in JavaFX 1.1.1

    JavaFX 1.1.1 provides the same overall support for cursors (aCursor class, and a cursor variable in the Node and Scene classes), but this support is limited to the desktop profile. However, itsCursor 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.1CustomCursorsDemo1_1_1 project -- see this article'scode archive) is similar to its Listing 1 equivalent. The two differences areimpl_getAWTCursor(), an undocumentedCursor function invoked by the JavaFX runtime when it needs an AWT-based cursor, and bufferedImage, anImage variable that returns the underlyingjava.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 theRobot 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 thisupdateJSGPanel() 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 withcursor set to CustomCursor'scursorAWT variable value, and withisDefault set to false.

    Assuming that you've loaded CustomCursorsDemo1_1_1into NetBeans, you'll also need to add, via theProject 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 ofimpl_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'sicons 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 theicons variable in the process. A replacetrigger is attached to this variable, and used to store theicons sequence's underlyingjava.awt.Images (thanks tojavafx.scene.image.Image's bufferedImagevariable) in an arraylist. (This happens prior to thepostinit code being executed.)

    Moving on, the postinit code usesimpl_stageDelegate to obtain the underlyingcom.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.WindowStageDelegatesuperclass.

    WindowStageDelegate's package-private$window variable, of the typecom.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 invokesjava.awt.Window's setIconImages() method via getWindow()'s returned JFramereference (which happens to be a descendant ofWindow). The previously saved array list ofjava.awt.Images is passed tosetIconImages(), making these images available to the JavaFX desktop application's frame window.

    You'll notice that Listings 7 and 8 are part of thecustomcursorsdemo1_1_1 package -- their source files are stored in a separate xtra directory in this article'scode 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 ofjava.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

      
    http://today.java.net/im/a.gif