Search |
||||
Introducing Custom Cursors to JavaFX
Tue, 2009-07-14
|
||||
| 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.
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:
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.
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.
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.
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.)
|
|