Thursday, September 2, 2010

Scala: SwingBuilder Essentials




A distinguishing feature of the Groovy programming language is its builders.
A Groovy builder lets you imbed hierarchical structures directly into your
code. Groovy supports a range of builders including XML builder, Ant builder
and Swing builder. The Grails web application development framework, built on
Groovy, also supports JSON and Hibernate criteria builders. Interestingly, this
range of builders is developed from a single common framework.


Builders offer a convenient way to construct hierarchical models with a direct
correspondence between the hierarchy and the generated code. The following
snippet demonstrates how, using a Scala SwingBuilder, we might specify a
simple UI:


frame('title -> "Centigrade to Fahrenheit conversion", 'location -> new Point(100, 100), 'size -> new Dimension(500, 300)){
  panel('layout -> new MigLayout("fill")){
      
    panel('constraints -> "dock north", 'layout -> new MigLayout("fill")) {
      label('text -> "Centigrade to Fahrenheit conversion", 'constraints -> "alignx right")
    }
      
    panel('layout -> new MigLayout("fill")) {
      label('text -> "Centigrade")
      label('text -> "Fahrenheit", 'constraints -> "wrap")
      textField('text -> "", 'columns -> 20)
      textField('text -> "", 'columns -> 20, 'editable -> false)
    }
      
    panel('constraints -> "dock south", 'opaque -> false) {
        button('text -> "Close")
    }
      
  }
}
Widgets are created by factory method calls into the SwingBuilder object.
Simple widgets such as label initialize their properties through a series
of named parameters. The property is identified with a Symbol and its initial
value. Container widgets such as frame and panel also include a code block
of the sub-components managed by it.


The widgets are named after their Swing counterpart. Hence the method call
frame constructs a JFrame object while label constructs a JLabel object. The
methods have two variants as illustrated by label:


label('property -> value, ...)
label(new MyLabel(...), 'property -> value, ...)
The first version creates a JLabel and initializes it properties. The second
version initializes the given JLabel. This can be used to introduce specialized
widgets. Both calls are also supplied with an implicit SwingBuilder object
responsible for the creation an initialization of the widgets. For example, the main panel
above might use the custom class RadialGradientPanel to deliver a panel decorated
with a gradient painter:


panel(new RadialGradientPanel(0.0F, 0.0F, 400.0F, List(0.0F, 0.5F, 1.0F), List(royalBlue, royalBlue4, black)), 'layout -> new MigLayout("fill")) { ... }
Running this code produces the application:


To interact with the application we introduce event handlers through the same
named parameter mechanism. The names are the event handlers of the corresponding
Swing listener class. Pressing a button or entering RETURN into a text field
produces an Action event processed by the actionPerformed method in the
registered ActionListener object.


In the above application we introduce the synthetic property id into the
second text field. The implicit SwingBuilder object maintains a mapping between
the id and the widget which can be retrieved with the method getWidgetID


frame('title -> "Centigrade to Fahrenheit conversion", 'location -> new Point(100, 100), 'size -> new Dimension(500, 300)){
  panel(new RadialGradientPanel(0.0F, 0.0F, 400.0F, List(0.0F, 0.5F, 1.0F), List(royalBlue, royalBlue4, black)), 'layout -> new MigLayout("fill")) {
      
    panel('opaque -> false, 'constraints -> "dock north", 'layout -> new MigLayout("fill")) {
      label('text -> "Centigrade to Fahrenheit conversion", 'foreground -> Color.WHITE, 'constraints -> "alignx right")
    }
      
    panel('opaque -> false, 'layout -> new MigLayout("fill")) {
      label('text -> "Centigrade", 'foreground -> Color.WHITE)
      label('text -> "Fahrenheit", 'foreground -> Color.WHITE, 'constraints -> "wrap")
      textField('text -> "", 'columns -> 20, 'actionPerformed -> doConvert)
      textField('id -> "fahrenheitTF", 'text -> "", 'columns -> 20, 'editable -> false)
    }
      
    panel('opaque -> false, 'constraints -> "dock south") {
        button(new GlossButton(), 'text -> "Close", 'actionPerformed -> doClose)
    }
      
  }
}

The first text field includes the synthetic property actionPerformed.
The value for this property will refer to a Scala function. The SwingBuilder
creates an instance of a class that refers to this function. The class implements
the Swing ActionListener interface and its actionPerformed method calls the function.
The instance of the class is registered as a listener on the text field.


The event handlers are simple Scala functions:


val doClose = (event: ActionEvent) => System.exit(0)

val doConvert = (event: ActionEvent) => {
  val centigradeTF = event.getSource().asInstanceOf[JTextField]
  val centigrade = centigradeTF.getText().toDouble
  val fahrenheitTF = builder.getWidgetID("fahrenheitTF").asInstanceOf[JTextField]
  fahrenheitTF.setText(((9 * centigrade + 160)/5).toString)
}

The builder code is, of course, just Scala. We are, therefore, free to mix in
any other Scala code. The factory methods return a reference to the widget it
creates so we could, for example, say:


val centigrade = textField('text -> "", 'columns -> 20)

then use this identifier elsewhere, providing we respect the usual scoping
rules.


The project is very much in its infancy. Presently the library supports
a sizeable subset of the Swing components: label, textField, button, toggleButton,
checkBox, radioButton (buttonGroup), list, scrollPane, panel, tabbedPane
and frame. More will follow.


In the next blog I will show how to introduce an MVC architecture into an
application.

1 comment:

draco said...

Hi Ken, I'm reading your book "Groovy Programming" but I cannot access the website you mentioned for the software distribution. Could you please upload them somewhere again? Thanks!