UI Construction with JavaFX
When constructing a UI then on the technical level I am concerned with
- creating the respective widgets
- putting them in the right place, i.e. layouting
- making them look nice and consistent, i.e. styling
- hooking them up to the data they should display, i.e. binding
- and putting them to action, i.e. attaching handlers.
Creating the widgets
The usual way of creating widgets like labels, buttons, menus, tables, charts and the likes is by calling their respective constructors.
Alternatively, one can use factory methods like in Swing to either set some common attributes consistently on each widget or to even construct compound components like a labelled text field.
JavaFx provides additional Builders for creating preconfigured widgets by following the classical GOF Builder pattern with additional create() and build() methods.
Layouting
Where Swing puts its components inside containers, JavaFX adds its nodes to a parent node in the SceneGraph. Either way, layouts define how the added widgets are placed inside the available area: side-by-side, stacked on top of each other, in the cell of a grid, in equally sized tiles, or else.
Layouts also have their own strategies of reacting to changes in the available area and how they deal with missing or excessive space.
Selecting a layout and putting each widget in its proper place can be done by API calls or via an external description in XML format (FXML).
GroovyFX adds the additional capability of defining the layout with tree-like builders as part of the usual code.
Styling
Swing styling was confined to API calls that would set visual properties per-widget (plus rules for proliferation of these property values down the containment tree) and the Look&Feel concept.
JavaFX builds on top of the API approach with CSS-like styles. This adds a fully new magnitude of power to the visual design capabilities but also a new dimension of complexity. Without going into too much detail, here are some points to consider:
- CSS has its own notion of cascading and how to “inherit” values
- JavaFX supports multiple stylesheets per scene plus “inline” styles per node
- selectors are per id, node type, and/or styleClass plus some pseudo-classes
- styles, styleSheets, styleClasses, and API-controlled properties may change at any time
Layout styles
To make things even more complex, it is not only the widgets that have visual properties like a Label bearing Font. Even layouts have styles, e.g. values for paddings, margins, gaps, alignments, etc. (but not for positioning ATM).
Binding
The binding story in JavaFX follows the same approach as Swing does by registering listeners that get notified for value changes of any bindable source. Such a source is typically a data model or some other widget. The latter case has a special support in JavaFX through accessor methods that end with “Property” and provide a very efficient update mechanism.
Projects like GroovyFX add on top of that concept further capabilities for declarative binding.
Event handlers
Value changes are not the only events that one can listen for. A long list of registration methods for callbacks that start with “on” are at our disposal for setting the application in action: onAction, onKeyReleased, onMouseOver, etc.
Quite surprisingly has JavaFX chosen to only support one such event handler per registration. We cannot add to an existing list of those unless we build such a facility ourselves.
Organizing the codebase
With all the work of creating, layouting, styling, binding, and event handling comes the need for some organization.
- there are sequence dependencies between these tasks
- there is scope to be managed to keep reference usages traceable
- styles, layouts, and whole building blocks may be shared for faster building, consistency and less maintenance effort
In other words, we need some grouping that keeps these aspects together while making them distinct enough such that each part is easily recognizable, potentially reusable, and subject to proper lifecycle management.
Use Griffon when you can
The Griffon framework provides a full solution for this challenge with its organization into MVC groups and the rules and conventions that it imposes on the code structure.
For those of us who do not have such a luxury: down below is a proposal for a simple structure that can help keeping track of the various aspects.
Example: a simple feedback form
We start with a simple example of a feedback form that looks like this:
We can implement this form easily in GroovyFX by putting all the UI code (widgets, layout, style, behaviour) in one single place.
Here is a possible solution.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
class Email1 { @FXBindable String name, address, feedback String toString() { "<$name> $address : $feedback" } } start { app -> def email = new Email1() stage title: "All-in-one app demo", visible: true, { scene { gridPane hgap: 5, vgap: 10, padding: 25, alignment: "top_center", style: "-fx-background-color: groovyblue", { columnConstraints minWidth: 50, halignment: "right" columnConstraints prefWidth: 250, hgrow: 'always' effect innerShadow() label "Please Send Us Your Feedback", style: "-fx-font-size: 18px;", row: 0, textFill: white, columnSpan: GridPane.REMAINING, halignment: "center", margin: [0, 0, 10] label "Name", hgrow: "never", row: 1, column: 0, textFill: white textField id: 'name', row: 1, column: 1 label "Email", row: 2, column: 0, textFill: white textField id: 'address', row: 2, column: 1 label "Message", row: 3, column: 0, valignment: "baseline", textFill: white textArea id: 'feedback', prefRowCount: 8, row: 3, column: 1, vgrow: 'always' button "Send Message", row: 4, column: 1, halignment: "right", { onAction { println "preparing and sending the mail: $email" } } } } } email.nameProperty().bind(name.textProperty()) email.addressProperty().bind(address.textProperty()) email.feedbackProperty().bind(feedback.textProperty()) } |
Having everything in one place has its advantages: at least you know where to start looking.
But it comes at the expense of clarity and consistency, e.g. the initial example makes all labels to be of white color.
Is that by accident or shall that be a consistent rule – a common style? This is most likely.
Separating the various aspects into their own methods also provides a better overview about
- what widgets we are dealing with
- how they are arranged on the screen
- what dependencies they have (binding)
- and what they are supposed to do.
The better structured code becomes this:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
class Email2 { @FXBindable String name, address, feedback String toString() { "<$name> $address : $feedback" } } start { app -> SceneGraphBuilder builder = delegate layoutFrame builder DemoStyle.style builder def model = new Email2() bindModelToViews model, builder attachHandlers model, builder primaryStage.show() } def layoutFrame(SceneGraphBuilder sgb) { sgb.stage { scene { gridPane { label id:'header', row: 0, column: 1, 'Please Send Us Your Feedback' label 'Name', row: 1, column: 0 textField id: 'name', row: 1, column: 1 label 'Address', row: 2, column: 0 textField id: 'address', row: 2, column: 1 label 'Feedback', row: 3, column: 0, valignment: BASELINE textArea id: 'feedback', row: 3, column: 1 button id: 'submit', row: 4, column: 1, halignment: RIGHT, "Send Feedback" } } } } void bindModelToViews(Email2 email, SceneGraphBuilder sgb) { sgb.with { email.nameProperty() .bind name.textProperty() email.addressProperty() .bind address.textProperty() email.feedbackProperty().bind feedback.textProperty() } } void attachHandlers(Email2 email, SceneGraphBuilder sgb) { sgb.submit.onAction = { println "preparing and sending the mail: $email" } as EventHandler } |
Pretty much all styling information is out of the way and can be maintained in an extra class or css file – and used in multiple places.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class DemoStyle { static style(SceneGraphBuilder sgb) { Stage frame = sgb.primaryStage Scene scene = frame.scene def groovyblue = sgb.groovyblue scene.fill = sgb.radialGradient(stops: [ groovyblue.brighter(), groovyblue.darker()] ).build() // a scene fill cannot be set via css atm GridPane grid = scene.root grid.styleClass << 'form' grid.hgap = 5 // for some reason, the gaps are not taken from the css atm grid.vgap = 10 grid.columnConstraints << sgb.columnConstraints(halignment: RIGHT, hgrow: ALWAYS) grid.columnConstraints << sgb.columnConstraints(halignment: LEFT, hgrow: ALWAYS, prefWidth:300) sgb.translateTransition(1.s, node: grid, fromY: -100, toY: 0).play() scene.stylesheets << 'demo.css' } } |
http://github.com/groovyfx-project/groovyfx/blob/develop/groovyfx/src/demo/resources/demo.css
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
.label { -fx-text-fill: white; } #header { -fx-font-size: 18; -fx-padding: 0 0 10 0; } .submit { -fx-spacing: 10; } .form { -fx-padding: 25 25 25 25; -fx-alignment: top-center; } |
Having the styles in one place makes it often more worthwhile to improve the style. Here we worked a little on the background gradient and added an additional feat that makes the form scroll-in at startup – just as if you would be handed over a feedback form from someone who sits in front of you.
This is how it looks now:
This is the end of the story for today but not for tomorrow. Further improvements of this structure are likely to be needed for any project of size.
stay tuned
Dierk König, @mittie















url submission said,
February 21, 2012 @ 11:35
Very descriptive post, I enjoyed that bit. Will there be a part 2?