The state of JVM desktop frameworks: SWT

The state of JVM desktop frameworks: SWT

This series is dedicated to the state of JVM desktop frameworks. After having had a look at Swing the previous week, this post focuses on the Standard Widget Toolkit.

  1. The state of JVM desktop frameworks: introduction
  2. The state of JVM desktop frameworks: Swing

What is SWT?

SWT originates from Eclipse project, an IDE. For Eclipse, the developers built a dedicated framework to build their graphic components upon. Swing and SWT have widely different designs. Swing implements the drawing of widgets in Java from scratch. On the opposite, SWT is a thin wrapper API that relies on native graphic objects. This has two main benefits:

  1. Widgets look native to the platform
  2. Rendering is faster

SWT APIs

There's one guiding principle behind SWT: because it depends on native graphic objects, every component requires a "parent" object as its first parameter. The parent is the object the child will be drawn onto. Every SWT component's constructor takes the parent as its first argument.

Overview of the SWT API

Fun with SWT

SWT has some peculiarities, most of them related to its design based on system libraries.

Native dependency

SWT provides a JAR for each mainstream operating system e.g. Windows, Mac OSX, etc. For example, this is the Maven dependency for my laptop:

<dependency>
  <groupId>org.eclipse.platform</groupId>
  <artifactId>org.eclipse.swt.cocoa.macosx.x86_64</artifactId> <!--1-->
  <version>3.114.100</version>
  <scope>runtime</scope>                                       <!--2-->
</dependency>
  1. JAR coordinates are platform-dependent. It contains the required native libraries in the form of JNI bindings.
  2. The JAR is only required at runtime

Structure of the SWT JAR on Mac OSX

SWT event control loop

Swing provides an event control loop out-of-the-box. This is not the case with SWT. We need to copy-paste the following code into each of our applications:

val display = Display()                           // 1
val shell = Shell(display)                        // 2
shell.open()                                      // 3
while (!shell.isDisposed) {                       // 4
  if (!display.readAndDispatch()) display.sleep() // 5
}
display.dispose()                                 // 6
  1. Bridge between SWT and the OS
  2. Create the top-level window
  3. Display it
  4. While the system native resources of the window have not been released
  5. Handle queued events. If nothing needs to be done... do nothing
  6. Free all system native resources

No-arg constructors

Both windows and dialogs are represented as Shell instances in SWT. The top-level window requires no parent and thus Shell offers a no-arg constructor. But since Shell is a graphical control, all its parent classes do also offer such a constructor. Those constructors have an empty body and calling them doesn't do anything.

Component creation order

The order in which components are instantiated on a parent is the order in which they will be added to the layout of that parent. If you need to decouple them, you need to be creative e.g. wrap the call to the constructor in a lambda.

Here's an SWT sample displaying a label, a text field, and a button in this order:

val label = Label(shell, SWT.LEFT)
val text = Text(parent, SWT.SINGLE or SWT.LEFT or SWT.BORDER)
val button = Button(shell, SWT.PUSH)

Styling

As seen in the previous snippet, the styling of widgets happens during their instantiation. Those styles are coded in the SWT class in the form of style bits:

  • LEAD = 1 << 14
  • LEFT = LEAD
  • SINGLE = 1 << 2
  • BORDER = 1 << 11
  • PUSH = 1 << 3
  • etc.

Circular dependency

Note that the constructor of Control takes a Composite instance, which itself is a subclass of Control. This circular dependency is bound to within the same package.

Displaying tabular data

SWT concerns itself only with the widgets and their rendering. As opposed to Swing and JavaFX, it has no concept of data model: you need to manage the data yourself. It's manageable for 0-D data e.g. text fields and even for 1-D data e.g. list boxes. For 2-D data i.e. tables, it's a lot of trouble.

For this reason, most graphic frameworks introduce a model abstraction between the component and the data it manages. For example, Swing has JTable and a TableModel.

Eclipse delivers the JFace library, which provides a data model abstraction over the SWT API among others. For example, for tables, JFace has the TableViewer class. At its core, every JFace viewer class wraps an SWT control.

JFace SWT integration

The wrapping applies at a deep level: SWT's TableColumn is wrapped by JFace's TableColumnViewer.

The Viewer class has a rich type hierarchy to handle data of different dimensions. IStructuredContentProvider provides multiple rows of data such as those found in tables. Because the API was designed before generics, there's a runtime check at the StructuredViewer level to verify the type of the set IContentProvider. Moreover, StructuredViewer also provides sorting, filtering, and "decorating" capabilities.

JFace TableViewer capabilities

Note that there's a library that manages two-way data bindings between the model and the control: JFace Data Binding. I couldn't find a compatible version though.

Conclusion

There's no denying SWT success. Not only Eclipse uses it, but some software also do.

SWT provides a fully native look-and-feel GUI. On the good side, it means that applications behave natively. On the flip side, there's a cost associated with this capability though:

  1. A dependency to the platform library, which breaks the Compile Once Run Anywhere promise
  2. Runtime checks instead of compile-time checks because of the lack of generics
  3. For those two reasons above, the API feels unwieldy at times

Thanks a lot to Benjamin Muskalla for his offer to review this post.

The complete source code for this post can be found on Github in Maven format:

To go further:

Originally published at A Java Geek on January 24th 2021