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.
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:
- Widgets look native to the platform
- 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.
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>
- JAR coordinates are platform-dependent. It contains the required native libraries in the form of JNI bindings.
- The JAR is only required at runtime
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
- Bridge between SWT and the OS
- Create the top-level window
- Display it
- While the system native resources of the window have not been released
- Handle queued events. If nothing needs to be done... do nothing
- 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.
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.
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:
- A dependency to the platform library, which breaks the Compile Once Run Anywhere promise
- Runtime checks instead of compile-time checks because of the lack of generics
- 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