JavaFX's CustomNode class lets you create reusable node-based components for your user interfaces. Because these components aren't skinnable, you can't change a component's appearance and/or behavior without rewriting the component. Fortunately, Sun has addressed this limitation by providing the javafx.scene.control package with its two core classes:
Control class, which subclasses CustomNode, provides the component's model.
Skin class provides the component's look and feel.
Because JavaFX 1.1's documentation on Control and Skin is somewhat obtuse, I created this blog post to share some insight into using these classes to create components beyond javafx.scene.control's TextBox class. I'll specifically reveal a button component in terms of its Button model and ButtonSkin look and feel classes, and demonstrate using this component with the textbox.
Control is the base class for user-interface components that support skins. You extend this class to define the component's model, supplying appropriate model-oriented attributes and an init block to initialize the component's default skin. For example, Listing 1 presents a Button class that subclasses Control to specify the model for a simple push button.
Listing 1: The Button model class
public class Button extends Control
{
public var text: String;
public var action: function (): Void;
override protected var focused on replace
{
println ("focused = {focused}")
}
override protected var hover on replace
{
println ("hover = {hover}")
}
override protected var pressed on replace
{
println ("pressed = {pressed}")
}
init
{
skin = ButtonSkin {}
}
}
This model requires two attributes: text to display, and a function to run whenever the button is activated (by a mouse click, or by pressing a special key while the button is focused). Additionally, the init block creates the button's skin as an instance of the ButtonSkin class, assigning this skin to Button's skin attribute, which this class inherits from Control.
You'll notice that I've overridden Node's focused, hover, and pressed attributes. Whenever the values of the skin's corresponding focused, hover, and pressed attributes change, the values of these attributes also change (thanks to behind-the-scenes binding). The replace triggers let you observe these changes.
Skin is the base class for defining a component's look and feel. Along with focused, hover, and pressed, Skin provides a control attribute for referencing the component's model's attributes, and a scene attribute for specifying the look and feel's scene graph. Listing 2's ButtonSkin class subclasses Skin to supply this look and feel.
Listing 2: The ButtonSkin look and feel class
class ButtonSkin extends Skin
{
def buttonControl = bind control as Button;
override protected def focused = bind scene.focused;
override protected def hover = bind scene.hover;
override protected def pressed = bind scene.pressed on replace
{
if (pressed)
scene.requestFocus ()
}
init
{
scene = Group
{
var rectRef: Rectangle
var textRef: Text
content:
[
rectRef = Rectangle
{
arcHeight: 12
arcWidth: 12
height: bind buttonControl.height
width: bind buttonControl.width
stroke: bind if (focused) Color.STEELBLUE else Color.GRAY
strokeWidth: bind if (focused) 2.0 else 1.5
opacity: 0.7
fill: LinearGradient
{
startX: 0.0
startY: 0.0
endX: 0.0
endY: 1.0
stops:
[
Stop
{
offset: 0.0
color: Color.web ("#dceaff")
}
Stop
{
offset: 0.49
color: Color.web ("#6885b2")
}
Stop
{
offset: 0.5
color: Color.web ("#2c599c")
}
Stop
{
offset: 1.0
color: Color.web ("#bed3f4")
}
]
}
}
textRef = Text
{
var w: Number
var h: Number
var _focused = false
def x = bind rectRef.boundsInLocal on replace
{
if (focused)
_focused = true
else if (not _focused)
{
w = x.width;
h = x.height;
}
}
translateX: bind (w-textRef.boundsInLocal.width)/2
translateY: bind h/2
x: bind if (pressed) 1 else 0
y: bind if (pressed) 1 else 0
content: bind buttonControl.text
font: Font
{
name: "Arial BOLD"
size: 12
}
fill: Color.WHITE
}
]
onMouseClicked: function (e: MouseEvent): Void
{
buttonControl.action ()
}
onKeyPressed: function (e: KeyEvent): Void
{
if (e.code != KeyCode.VK_SPACE)
return;
def pause = PauseTransition
{
duration: 0.15s
action: function (): Void
{
scene.pressed = false
}
}
scene.pressed = true;
pause.play ();
buttonControl.action ()
}
}
}
}
ButtonSkin binds Skin's control attribute to buttonControl so that it can access the model's action and text attributes, not to mention inherited attributes such as height and width. (I probably should have specified buttonModel or model instead of buttonControl, to more accurately indicate this attribute's purpose.)
ButtonSkin next overrides focused, hover, and pressed (which are implicitly bound to Button's focused, hover, and pressed attributes), binding them to scene's versions of these attributes -- when scene's attributes change, binding causes ButtonSkin's same-named attributes to change, which implicitly causes Button's same-named attributes to change.
When ButtonSkin's pressed attribute changes its value (in response to the user pressing the mouse button while the mouse is over the component's scene graph, or releasing this button, whether or not the mouse is still over the scene graph), the replace trigger executes. If pressed is true, the trigger tells the JavaFX runtime to give focus to the scene graph (via requestFocus()).
When the scene graph receives focus (via requestFocus() or the Tab key), ButtonSkin's focused attribute is set to true. This attribute is used (via binding) to enlarge the button's outline and change its color (to indicate focus), and to solve a small problem (discussed later). (The scene graph receives focus when its root node is assigned a key handler, such as the function assigned to onKeyPressed.)
ButtonSkin's init block creates the scene graph, assigning its root node to the scene attribute. This node is of type Group because the scene graph consists of a Text node overlaying a Rectangle node. To ensure that the button receives focus, Group's onKeyPressed attribute is assigned a key handler. (Assigning a key handler to Rectangle's or Text's onKeyPressed attribute wouldn't work.)
A mouse handler is similarly assigned to Group's onMouseClicked attribute. Unlike the key handler, you could place the mouse handler in the Rectangle node and achieve the same effect. Alternatively, you could place this handler in the Text node. In this case, however, the handler would only activate (in response to a button click) whenever the mouse was exactly over the text -- not over the rectangle.
The Rectangle node's look is governed by its arcWidth and arcHeight attributes (which should probably be a percentage of this node's size rather than being specific values), its width and height attributes (which are bound to the model), its stroke and strokeWidth attributes (whose values depend on the scene graph's focused state), and its opacity and fill attributes (which specify its background).
The number of stops and their values for the LinearGradient assigned to the fill attribute originated in Amer Sohail's informative Creating Custom Button in JavaFX blog post -- I like the glassy appearance achieved by this gradient.
Moving on, we discover the Text node. Its object literal begins in a strange way, by specifying w, h, _focused, and x variables. Variable x is bound to the rectangle's boundsInLocal attribute, and provides a replace trigger to keep track of this attribute's width and height attribute values (in variables w and h) until the scene graph is first focused.
The w and h values are used with translateX and translateY to center the text horizontally over the rectangle background, and to give the text a nice vertical alignment according to its baseline. I bind to w and h instead of boundsInLocal.width and boundsInLocal.height because these dimensions change when the scene graph is focused/unfocused, which causes the text to move around disconcertingly.
In addition to binding x and y to expressions that use pressed to allow the text to shift slightly when the mouse is pressed, the Text literal also binds the content attribute to the model's text attribute. However, it supplies a specific font and a specific text color. (I elected to hardcode these values because they can be changed via stylesheet settings, but only in the desktop profile.)
Finally, Group's onMouseClicked and onKeyPressed handler functions invoke the model's action function. However, onKeyPressed's function has more work to do than onMouseClicked's function. Specifically, it detects an appropriate key to serve as the trigger for invoking the action function -- I've chosen the spacebar key. It also simulates a button click with the help of scene.pressed and a pause transition.
I've written a script that employs the button and textbox components in the user interface of a utility that converts between degrees and radians. Enter a numeric degrees/radians value into the textbox, and click a button to convert this value to radians or degrees. The result appears in the textbox, whose contents are erased by clicking a third button. Figure 1 shows this user interface from the desktop perspective.
Figure 1: The textbox displays the result of converting 180 degrees to radians via the Deg2Rad button, which currently has the focus. (Click to enlarge.)
Listing 3 excerpts the script's Stage literal, which is located in the Main.fx file of a NetBeans project named DRC. This literal specifies the user interface shown in Figure 1.
Listing 3: The DRC script's stage
Stage
{
var model = Model {}
title: "DRC"
width: 390
height: 190
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.YELLOW },
Stop { offset: 1.0 color: Color.CYAN }
]
}
var input: TextBox
content: VBox
{
translateY: 20
spacing: 20
var titleRef: Text
content:
[
titleRef = Text
{
content: "Degrees/Radians Converter"
font: Font
{
name: "Arial BOLD",
size: 18
}
textOrigin: TextOrigin.TOP
translateX: bind (sceneRef.width-
titleRef.boundsInLocal.width)/2
}
input = TextBox
{
value: bind model.input with inverse
translateX: bind (sceneRef.width-
input.boundsInLocal.width)/2
}
HBox
{
def BTNWIDTH = 80
def SPACING = 10
spacing: SPACING
translateX: bind (sceneRef.width-3*(BTNWIDTH+SPACING))/2
content:
[
Button
{
text: "Deg2Rad"
width: BTNWIDTH
height: 28
action: function (): Void
{
model.deg2rad ()
}
}
Button
{
text: "Rad2Deg"
width: BTNWIDTH
height: 28
action: function (): Void
{
model.rad2deg ()
}
}
Button
{
text: "Clear"
width: BTNWIDTH
height: 28
action: function (): Void
{
input.value = ""
}
}
]
}
]
}
}
}
You might be wondering why I assign, to HBox's translateX attribute, the bind (sceneRef.width-3*(BTNWIDTH+SPACING))/2 expression rather than the equivalent bind (sceneRef.width-panel.boundsInLocal.width)/2 expression (where panel references the HBox literal), to horizontally center the three buttons.
If I assign bind (sceneRef.width-panel.boundsInLocal.width)/2 to translateX and, in the ButtonSkin class, increase the font size to a certain value such as 14 or 18, I've found that the leftmost button's text shifts horizontally as focus moves from one component to another. I'm not sure why this anomaly occurs, but I can overcome it via (sceneRef.width-3*(BTNWIDTH+SPACING))/2.
Listing 4 presents the script's Model class, which the stage instantiates and the button handlers use to perform the conversions. (The conversion code is unecessarily complicated, but interesting.)
Listing 4: The DRC script's Model class
class Model
{
def base = "http://www.webservicex.net/ConvertAngle.asmx/ChangeAngleUnit?";
var input: String;
function deg2rad (): Void
{
def request = HttpRequest
{
location: "{base}AngleValue={input}&fromAngleUnit=degrees&"
"toAngleUnit=radians"
onException: function (ex: Exception): Void
{
input = ex.getMessage ()
}
onInput: function (is: InputStream)
{
def parser = PullParser
{
documentType: PullParser.XML
input: is
onEvent: function (event: Event)
{
if (event.type == PullParser.END_ELEMENT)
if (event.qname.name == "double")
input = event.text
}
}
parser.parse ();
parser.input.close ()
}
}
request.enqueue ()
}
function rad2deg (): Void
{
def request = HttpRequest
{
location: "{base}AngleValue={input}&fromAngleUnit=radians&"
"toAngleUnit=degrees"
onException: function (ex: Exception): Void
{
input = ex.getMessage ()
}
onInput: function (is: InputStream)
{
def parser = PullParser
{
documentType: PullParser.XML
input: is
onEvent: function (event: Event)
{
if (event.type == PullParser.END_ELEMENT)
if (event.qname.name == "double")
input = event.text
}
}
parser.parse ();
parser.input.close ()
}
}
request.enqueue ()
}
}
The Model class specifies an input attribute that stores the value to be converted, and also stores the result of the conversion. Look closely at Listing 3 and you'll discover the bind model.input with inverse expression, which is assigned to the TextBox's value attribute. This binding allows the model to easily access the input value, and store the result in the textbox.
Model also specifies functions deg2rad() and rad2deg() for performing the degrees-to-radians and radians-to-degrees conversions. Although I could have easily hardcoded these calculations in these functions, I found a Web service that performs this task, and wanted to play with the HttpRequest and PullParser classes.
Whenever deg2rad() or rad2deg() is called, the function creates an HttpRequest object that specifies the location of the Web service, the angle value to pass to this service, and the conversion direction (degrees to radians or radians to degrees). The function then enqueues the request to run in the background, which keeps the user interface responsive.
If a non-numeric value (a letter or the empty string, for example) or otherwise illegal value (such as a mixture of digits and letters) is assigned to AngleValue and the Web service is invoked, the Web service returns an error value that results in the handler function assigned to onException being invoked. For simplicity, I assign this value to the model's input attribute, allowing it to appear in the textbox.
The onInput handler function is invoked whenever the Web service successfully returns the converted value, via a small XML document. This function creates a PullParser to parse out the converted value from the document's double tag, and stores this value in the model's input attribute, which subsequently appears in the user interface's textbox.
That's pretty much it for how the DRC script works. Because this script and the button component access only classes belonging to the common profile (unfortunately, I can't take advantage of the desktop-profile-oriented javafx.scene.effect classes), the script will run on the mobile emulator. Check out Figure 2 for the proof.
Figure 2: The user interface must be rotated 90 degrees to see it in its entirety. In a future blog post, I'll discuss creating user interfaces that automatically adapt to different screen dimensions. (Click to enlarge.)
While running the script on the mobile emulator, an interesting thing happens whenever you specify an invalid value (leave the textbox empty, for example), and then click a conversion button. Instead of the onException handler function being invoked, allowing the Web service's error message to appear in the textbox, the following NullPointerException is thrown:
java.lang.NullPointerException: 0
- com.sun.javafx.data.pull.ukit.xml.ParserStAX.panic(), bci=25
- com.sun.javafx.data.pull.ukit.xml.Parser.step(), bci=1048
- com.sun.javafx.data.pull.ukit.xml.ParserStAX.next(), bci=204
- javafx.data.pull.PullParser.next$impl(), bci=40
- javafx.data.pull.PullParser.next(), bci=1
- javafx.data.pull.PullParser.parse$impl(), bci=24
- javafx.data.pull.PullParser.parse(), bci=1
- drc.Main$Model$2.lambda(), bci=66
- drc.Main$Model$2.invoke(), bci=2
- drc.Main$Model$2.invoke(), bci=5
- javafx.io.http.HttpRequest.setInput$impl(), bci=48
- javafx.io.http.HttpRequest.setInput(), bci=2
- com.sun.javafx.io.http.impl.BaseTask$ReadNotifier.run(), bci=15
- com.sun.javafx.io.http.impl.msa.MsaProfile$1.invoke(), bci=4
- javafx.lang.FX$1.run(), bci=4
- com.sun.fxme.runtime.RunnableQueue$Worker.run(), bci=222
This exception appears to be thrown because of a bug in the JavaFX mobile runtime, and will be reported. Although somewhat annoying, the bug could be avoided by validating the textbox's input prior to invoking the Web service. Alternatively, I could avoid the Web service and just hardcode the calculations (but that wouldn't be as much fun).
Although it can be challenging to create skinnable components, I intend to present additional examples that complement the button component in future blog posts. Let me know if you have a favorite component that you'd like to see implemented next. In the meantime, perhaps Sun will provide additional skinnable components (and standardized support for themes, groups of logically related skins) in JavaFX 1.5.
Download a source file: cs j33109-src.zip
Like this blog? Subscribe to the CSJ Explorer RSS feed
getting problem at "scene.pressed = false"
hi, when i tried your code, i am getting problem at the code
action: function (): Void
{
scene.pressed = false
}
getting the following error
"pressed has protected write access in javafx.scene.Node"
RE: getting problem at "scene.pressed = false"
Hello,
The error you've mentioned also occurs in the line
scene.pressed = true;, which appears shortly after the code you've mentioned.I've discovered that this error occurs under JavaFX 1.0. However, it doesn't occur under JavaFX 1.1, which made some changes to the language and APIs.
All the best.
Jeff
Same error
I Get "pressed has protected write access in javafx.scene.Node" as well. Any ideas?
Great to see someone has
Great to see someone has finally designed a skinnable button componant for javaFX. Great work!
I want to try running this
I want to try running this code. I am very happy with java programming. currently developing java message. defeating other programming. I hope to master Java.
Getting upset to put this
Getting upset to put this code .
scene = Group
{
var rectRef: Rectangle
var textRef: Text
content:
[
rectRef = Rectangle
for realizable the pictures ??
Slick UI
The UI and the screenshots look pretty slick. The sample code looks readable and well structured. Though, I am interested in reading more on how to design the UI so that it automatically fits various screen sizes. I have been primarily involved in JSP and backend Java and am just starting on a project which requires Java FrontEnd UI.
Seems like
Seems like the method you posted about the skinnable button component is a little bit different from what I used before. So, I'll compare them and see which one is better for me. Thanks for the code.
I have also noticed The
I have also noticed The error you've mentioned it occurs in the line scene.pressed = true;, which appears after the code you've mentioned.
I love New Orleans Wedding photography and I wish I knew more about coding. Thanks alot!
great post
great post. I'll try that out right away.
All fair points apart from
All fair points apart from the bit about XML. The widespread use of XML alongside Java is precisely because Java is so bad for programming in a declarative style. JavaFX fixes that. Why would you use an irritatingly verbose document format to write programs instead of a purpose-built declarative programming language? His trivial example looks fine but doesn't contain any code to perform actions, only declarative layout code. How would he propose to encode that in XML? XML makes a very bad programming language as anyone who's worked on a moderately compliated Ant build will testify.- grace
If I use a skinnable button
If I use a skinnable button component made with JavaFX and a web user has java disabled in the web browser, will the buttons functionality be rendered useless?
Cali
How to change the color combination?
How we can change the skin ( color ). No i dont think so it will work efficiently .
help me with the error
I have tried this 5 times and I have been getting "pressed has protected write access in javafx.scene.Node" error.
Did anyone here with the solution?
In skinnable components, you
In skinnable components, you can’t change a component’s appearance and/or behavior without rewriting the component. Fortunately, Sun has addressed this limitation by providing the javafx.scene.control package with its two core classes..
Very good article. Thanks!
Very good article. Thanks!
Thanks
Very important, time management and the creative flow. A great way to get into the creative flow is by utilizing time wisely concentrating through meditation.
Coding in java are being meant for standard outputs
The Source we got from your post as exactly we meant to go for and it's really reliable.
CustomNode
Can I extend the CustomNode class to create a custom node with the content I need?
I am also agreed for this uniqueness
Among the verity of article. Your written article is unique one.This is appreciated. It is very nice and interesting. Continue to write. Thank you....
Good Presentation
Fantastic article. Thanks so much for your insight and example.
New to me!
I have not learnt about this yet. I am very glad to be a member of JavaWorld.com today.
Great Java Article ...
Thanks For a great article! I am in the process of adding Java to a moving companies site and this tool sounds very useful. Thumbs up!! I am having trouble though changing the apperance of the component. Any Tips?
dizi izle
Seems like the method you posted about the skinnable button component is a little bit different from what I used before. So, I'll compare them and see which one is better for me. Thanks for the code.
well worth the read.I found
well worth the read.I found it very informative as I have been researching a lot lately on practical matters such as button components for JavaFX...
Post new comment