Introducing Custom Cursors to JavaFX
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, 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.
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.
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 (aCursor 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
onMousePressedupdateJSGPanel()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
onMouseReleasedupdateJSGPanel()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 thisupdateJSGPanel()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.
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 itsjavafx.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
- Sample code for this article
- "Hacking JavaFX 1.0 to use Java 1.6 Features"
- Login or register to post comments
- Printer-friendly version
- 6606 reads



