I started writing an article about how to write new UI controls for OpenJFX using all the internal APIs and architecture and so forth. But then I discovered that the control I was writing as a proof of concept was not using any private API at all, and actually was implementing the Skin differently than I had imagined previously, and I thought I ought to blog about it. Behold, the MoneyField!
The design space for a MoneyField is quite interesting. I did a bunch of research and tried out various different representations of “money” in Java, including Joda-Money, but in the end the parsing was not satisfactory and I concluded to just use a good old fashioned BigDecimal to represent a monetary value. The best blog I found talking about the issues with BigDecimal for representing Money is this one titled BigDecimal and Your Money. I think this same technique that I’m going to show works equally well whatever the data type is.
The most complex part of implementing this control was using the DecimalFormat for parsing the result back. In fact, I had a lot of trouble parsing the text in the MoneyField and producing a reliable BigDecimal out of it. I’m sure the implementation is lousy compared to what an expert in parsing such things could do, and I would welcome any feedback on my present implementation.
Writing the Control itself was trivial. I extended MoneyField from Control, and added a value
property for holding the BigDecimal that would represent the monetary value of this control. I added a few other properties such as editable
, promptText
, and prefColumnCount
. I also added an onAction event to MoneyField.
Now, there are some interesting things to talk about for you aspiring UI controls authors. Lets take a brief walk through the code.
public class MoneyField extends Control { /** * Creates a new MoneyField. The style class is set to "money-field". */ public MoneyField() { getStyleClass().setAll("money-field"); } @Override protected String getUserAgentStylesheet() { return getClass().getResource("MoneyField.css").toExternalForm(); } }
Every third party UI control must define the essential skeletal structure that you see in the above code snippet. You should provide a no-arg constructor (or it may not work in FXML). In your constructor, you should set the style class of the control to be the “css-ized” name of your control. Just toLowerCase and separate words by dashes. Then, because I want my skin to be entirely defined by CSS, I will override the getUserAgentStylesheet method and have it return a reference to a css file that I am going to define which acts as the “user agent stylesheet” for this control. That is, it provides the default CSS style for this control. Here is what my CSS file looks like:
#money-text-field { -fx-skin: "com.sun.javafx.scene.control.skin.TextFieldSkin"; } .money-field { -fx-skin: "com.fxexperience.javafx.scene.control.skin.MoneyFieldSkin"; -fx-background-color: -fx-shadow-highlight-color, -fx-text-box-border, -fx-control-inner-background; -fx-background-insets: 0, 1, 2; -fx-background-radius: 3, 2, 2; -fx-padding: 3 5 3 5; -fx-prompt-text-fill: derive(-fx-control-inner-background,-30%); -fx-cursor: text; } .money-field:focused { -fx-background-color: -fx-focus-color, -fx-text-box-border, -fx-control-inner-background; -fx-background-insets: -0.4, 1, 2; -fx-background-radius: 3.4, 2, 2; } .money-field:disabled { -fx-opacity: -fx-disabled-opacity; }
There really isn’t a lot to say here, other than you can see I have a little trickery around the #money-text-field. Basically, I want the developer to be able to style the money field from CSS using .money-field, but I need to make sure the Skin used with the inner TextField is still a normal TextField. You’ll see how this fits together in a minute when I describe how the MoneyFieldSkin works.
Back in the MoneyField.java file, I also defined the above mentioned properties and events. First, here is what a “normal” property looks like. It is fully observable, readable, and writable.
/** * The value of the MoneyField. If null, the value will be treated as "0", but * will still actually be null. */ private ObjectProperty<BigDecimal> value = new SimpleObjectProperty<BigDecimal>(this, "value"); public final BigDecimal getValue() { return value.get(); } public final void setValue(BigDecimal value) { this.value.set(value); } public final ObjectProperty<BigDecimal> valueProperty() { return value; }
I am using the SimpleObjectProperty, which takes as parameters a reference to the bean that defined it, and the name of the property. The value of the value
property defaults to null. Our doclet produces javadoc on the getter and setter without having to actually write it manually, so that’s why docs are missing for those. The editable property is likewise defined as a SimpleBooleanProperty.
The onAction is a very straightforward implementation. If you write your own controls and want to include your own action events, it is very easy to do:
/** * The action handler associated with this MoneyField, or * <tt>null</tt> if no action handler is assigned. * * The action handler is normally called when the user types the ENTER key. */ private ObjectProperty<EventHandler<ActionEvent>> onAction = new ObjectPropertyBase<EventHandler<ActionEvent>>() { @Override protected void invalidated() { setEventHandler(ActionEvent.ACTION, get()); } @Override public Object getBean() { return MoneyField.this; } @Override public String getName() { return "onAction"; } }; public final ObjectProperty<EventHandler<ActionEvent>> onActionProperty() { return onAction; } public final EventHandler<ActionEvent> getOnAction() { return onActionProperty().get(); } public final void setOnAction(EventHandler<ActionEvent> value) { onActionProperty().set(value); } }
You’ll notice that in the invalidated method of the property I call setEventHandler. This method exists for the registration of such event handlers with the system. Another interesting thing here is that I’m not extending SimpleObjectProperty, but rather I’m extending ObjectPropertyBase.
Every property must implement the getBean() and getName() method. SimpleXXX properties have fields which hold the bean & name, and you supply them in the constructor. But XXXPropertyBase properties have no such fields, and you must implement these methods. Why chose one over the other? Well, SimpleXXX properties will have smaller static footprint because they are using a class instead of defining a class, but will have larger dynamic footprint because they have two additional fields which must be saved for each instance. XXXPropertyBase uses larger static footprint because it defines a class, but smaller dynamic footprint because it doesn’t have to save these two fields. Since I had to subclass an object property anyway for the sake of implementing the invalidated method, I might as well extend ObjectPropertyBase and save a couple fields at runtime.
The Skin for MoneyField was quite interesting. At first I thought I would extend from TextFieldSkin (an internal implementation in JavaFX) but then discovered this wasn’t necessary. Instead, I could just implement the Skin interface and return a TextField as the “node” of the Skin. So the MoneyField defines my UI, and the TextField is actually what I end up using to represent the MoneyField. And then there is a pile of glue code which ties the TextField to the MoneyField. The other nice thing about this approach was that I could implement the entirety of MoneyField and MoneyFieldSkin without using any private API (which, when I started writing this, I didn’t think was possible).
public class MoneyFieldSkin implements Skin<MoneyField> { /** * The {@code Control} that is referencing this Skin. There is a * one-to-one relationship between a {@code Skin} and a {@code Control}. * When a {@code Skin} is set on a {@code Control}, this variable is * automatically updated. */ private MoneyField control; /** * This textField is used to represent the MoneyField. */ private TextField textField; /** * Create a new MoneyFieldSkin. * @param control The MoneyField */ public MoneyFieldSkin(final MoneyField control) { this.control = control; // Create the TextField that we are going to use to represent this MoneyFieldSkin. // The textField restricts input so that only valid digits that contribute to the // Money can be input. textField = new TextField() { // ... }; // ... and a lot more stuff! } @Override public MoneyField getSkinnable() { return control; } @Override public Node getNode() { return textField; } /** * Called by a Skinnable when the Skin is replaced on the Skinnable. This method * allows a Skin to implement any logic necessary to clean up itself after * the Skin is no longer needed. It may be used to release native resources. * The methods {@link #getSkinnable()} and {@link #getNode()} * should return null following a call to dispose. Calling dispose twice * has no effect. */ @Override public void dispose() { textField = null; } // ... and a lot more stuff! }
The above code lays out the very basic skeletal structure of a Skin. This skin implements the Skin interface directly. The constructor takes the MoneyField it will be working with, and creates the TextField which will represent the skin (or “draw” the skin). In the dispose method, we throw away the TextField.
If you ran the code above, you would find a MoneyField that looked and felt like a TextField. None of the properties on the MoneyField would work, but from a visual perspective and a text entry perspective you’d have, essentially, a TextField. The rest of MoneyFieldSkin is devoted to wiring up the MoneyField API to the TextField and implementing all the parsing and formatting necessary to make it look like a money field.
The remainder of the MoneyFieldSkin isn’t worth going through in this blog post, though you can download the Intellij project and check it out for yourself.
When Jonathan tried this out last night he was getting exceptions with the New Zealand currency. I suspect there may be problems in Australia too. I’m hoping one of our readers from down under will give it a look and see where my bug is :-).
Using the MoneyField is really very straightforward. I hope it is actually useful to boot. I’m wondering if anybody industrious out there might take this design strategy and write an EmailField and UrlField control, maybe a NumberField control too! It seems eminently doable using only the public API and would make a great addition to the ecosystem (as well as a great addition to the toolkit!).
Very nice post. As with my last comment in the”Restricting Input on a TextField” post though, I urge JavaFX developers to check out the Qt GUI Toolkit and how they handle these things. They have a proven technology used my many developers and is the main toolkit of KDE which drives its development.
You suggested more fields like e-mail field, number field, etc. In Qt, this can be accomplished without having tons of subclasses or classes that implement some interface. For currency, one can have a QDoubleSpinner control with a currency symbol prefixed. (No comma’s that I know of in that control though for values like $1,000.00)
For others like a phone number control, you can use a input mask on a text box to make the control be prefilled with something like “(___)___-____”. The user is allowed to enter the phone without typing the parenthesis or dash. E-mail can be achieved using a QValidator which is a class for validating input on controls which has subclasses like QDoubleValidator and QRegexValidator.
@Tamul, I think you are missing the point. Qt is a very mature and stable tool, that has been in development for a long time. JavaFX is still incomplete, and still in hard and continous development. I think the core theme of the article is to show how to create our own controls.
Certainly in the near future things like JFormattedTextField for JavaFX APIs is going to appear and simplify all this.
Qt have their own philosophy and way to make things. JavaFX is a very different and newer tool, that with time is going to stablish their own philosophy and way to make things the most efficiently.
2 years later, here we are, and still no FormattedTextField or something like that
how to use MoneyFieldSkin ?
How can I clear the value entered by the user?
Example: In a normal TextField it could be: textfield.setText(“”);
MoneyField has the setValue method, but it does not clear the text in the input box.
This is still very relevant and useful, but under Java11/OpenJFX you need to change one thing.
#money-text-field {
– -fx-skin: “com.sun.javafx.scene.control.skin.TextFieldSkin”;
+ -fx-skin: “javafx.scene.control.skin.TextFieldSkin”;
}