Thursday, December 22, 2011

Building SWT User Interfaces with Xtend

Xtend is a programming language that compiles to Java, but adds new features, such as closures. This article applies Xtend to implement SWT UIs and shows how the resulting code can be more readable and concise compared to Java.

When you’ve been programming desktop applications with Eclipse before, you probably have used the Standard Widget Toolkit (SWT), since it provides the fundamental UI elements (aka Widgets), such as windows, buttons, edit fields, checkboxes, etc.

The first thing you probably did was to create a window with several nicely arranged widgets. This task includes choosing the proper parent widget for all widgets, configuring layouts, etc. Then, the next step usually is to implement behavior for some widgets, so that the user can interact with the UI. This includes for example implementing selection listeners for buttons.

These two tasks have different characteristics:

  • To arrange widgets nicely, the developer's code needs to creates trees of objects. In such trees, windows are the root nodes, composites etc. the intermediate nodes and buttons, text fields, etc. are the leaf nodes. In the context of the web and HTML, an equivalent tree is the DOM (Document Object Model). For SWT, this tree becomes very visible in the XML-based approaches to specify UIs, such as CookSwt and swtxml.
  • To implement behavior, the developer usually creates anonymous or nested classes in Java. These classes implement event handlers and are registered as event listeners.

SWT User Interfaces are usually implemented using Java. However, the way to implement both patterns in Java bears room for improvement, because:

  • Java doesn’t have a special pattern to create trees. The standard pattern is to imperatively create objects and establish references between them. Whether the resulting objects form a tree or a graph is not obvious to the reader of the code, since for him/her it is just a linear sequence of statements.
  • Anonymous classes in Java have a particularly noisy syntax. The extreme scenario (which is fairly common) is when the actual code that handles the event is only a single Java statement. In this scenario, the majority of the code is not the execution logic of the handler, but the declaration of the class and the handlers method.

Designing User Interfaces using Trees: Builder Syntax

Xtend offers to improve the situation due to its support for a builder syntax and its support for closures.

  • Builder syntax is a pattern that creates an object tree and allows the code to be formatted like a tree (using indentation an brackets).
  • Closures allow to define instructions that can be executed later. You may perceive it as similar to an anonymous class with a single method, but without the syntactic overhead of a method declaration.

The following snippet illustrates the builder syntax in Xtend. As indicated already, the style of the code reflects the tree structure of the SWT widgets. This example creates a shell with one label, one text filed and one button. Additionally, it arranges them using a GridLayout.

Builder Syntax

val shell = newShell(display) [ 

    layout = new GridLayout(3, false)

    newLabel(SWT::NONE) [ 

        text = "To:"

    ]      

    newText(SWT::BORDER) [

        layoutData = newGridData() [

            grabExcessHorizontalSpace = true

            horizontalAlignment = SWT::FILL

        ]

    ]

    newButton(SWT::PUSH) [

        text = "Send"

    ]

]

To understand the code from the Xtend code snippet above and to compare it with Java, the following things may be noteworthy:

  • All referenced elements in the snippet are Java elements from SWT, except for the new*()-methods. Those are methods implemented in a custom Java factory class. They all conform to the following pattern illustrated in the next code snippet.

    public static Button newButton(Composite parent, int style, Procedure1<Button> init) {

        Button btn = new Button(parent, style);

        init.apply(btn);

        return btn;

    }

    The new*()-methods can be invoked from the snippet like member methods since they have been imported as extension methods. Extension methods are methods defined in a different class which can be invoked like member methods on any kind of object that the method accepts as its first parameter. In the snippet, the extension methods are invoked on the implicit variable it.
  • You probably have noticed that all invocations of new*()-methods are followed by a code block surrounded by brackets "[]". This code block defines a closure which is passed as the last parameter to the new*()-method. You could as well pass in the closure explicitly as the last parameter to the method. However, this syntax can improve readability of the code. Since the closure has one parameter and no return value, its class implements interface Procedure1 (src).
  • No closures in this example declare parameters. However, they do have one implicit parameter. An example of a closure with parameters is [param | param.doSomething() ]. Xtend supports an implicit variable named it. If a closure has a parameter but does not declare the parameter, the parameters' value can be accessed through the variable it. Usage of the variable it is implicit in the same way as you know it from this, with the difference that you can not assign new values to this. When accessing members of it (e.g. horizontalAlignment), stating the name it is optional. Furthermore, you can invoke extension methods on it: In the code snippet, the method newText(Composite, int) has two parameters. The value for Composite is the value of it from the surrounding closure, which is in fact the shell (the window).
  • Xtend allows to use Getter/Setter-pairs like properties: Instead of setText("Send") you may write text = "Send"

Using Closures to make User Interfaces Interactive

To implement event handlers it is common in Java to use anonymous or nested classes. The snippet below uses Xtends closures to do the same in a syntactically more concise way.

Event Handling

newButton(SWT::PUSH) [

    text = "Send"

    addListener(SWT::Selection) [

        newMessageBox((widget as Control).shell, SWT::OK) [ 

            message = "Hello World"

        ].open()

    ]

]

The snippet above uses the Builder Syntax (as described in the last section) to create a Button. When the button is clicked, a MessageBox will be created and shown to the user.

The interesting part here is addListener(int, Listener) . This method accepts two parameters with the second parameter being an instance of interface Listener (src) . In the snippet the second parameter for addListener is a closure (the code block surrounded with [] in addListener(SWT::Selection) [ ... ] ). When the Xtend code is compiled to Java, the closure is compiled into an anonymous Java class which implements the interface Listener . This, however, is only possible for interfaces that have just a single method.

A Comprehensive Example

Builder Syntax and Event Handling

package blog_001_xtend_and_swt


import org.eclipse.swt.SWT

import org.eclipse.swt.layout.GridLayout

import org.eclipse.swt.widgets.Display


import static extension blog_001_xtend_and_swt.XtendSWTLib.*


class MessageForm {


    def static void main(String[] args) {

        new MessageForm().run(args)

    }

    

    def void run(String[] args) {

        val display = new Display()

        val shell = newShell(display) [ 

            setSize(400, 400)

            layout = new GridLayout(3, false)

            newLabel(SWT::NONE) [ 

                text = "To:"

            ]      

            val to = newText(SWT::BORDER) [

                layoutData = newGridData() [

                    grabExcessHorizontalSpace = true

                    horizontalAlignment = SWT::FILL

                ]

            ]

            val send = newButton(SWT::PUSH) [

                text = "Send"

            ]

            val msg = newText(SWT::BORDER) [

                layoutData = newGridData() [

                    grabExcessHorizontalSpace = true

                    grabExcessVerticalSpace = true

                    horizontalSpan = 3

                    horizontalAlignment = SWT::FILL

                    verticalAlignment = SWT::FILL 

                ]

            ]

            send.addListener(SWT::Selection) [

                newMessageBox(send.shell, SWT::OK) [ 

                    message = to.text + "\n" + msg.text

                ].open()

            ]

        ]

        

        shell.open()

        while (!shell.isDisposed()) {

            if (!display.readAndDispatch())

                display.sleep()

        }

        display.dispose()

    }

}