Taking on GUI Builders with the Swing JavaBuilder
Join the DZone community and get the full member experience.
Join For FreeAs the author of the Swing JavaBuilder library, I'd like to present to you a simple tutorial that explains what it can do for you and how it can maximize Swing development productivity.
Since coding Swing by hand is extremely cumbersome, most developers have to fallback on IDE-specific GUI builders, such as NetBeans's Matisse or Eclipse's WindowBuilder. Although those are fine solutions, all visual GUI builders suffer from a set of common drawbacks:
- they are IDE-specific. Once you start using Matisse or WindowBuilder, your chances of easily moving your code from one IDE to another are next to nill
- they often generate extremely unmaintainable code (especially if using GroupLayout like Matisse, which is basically impossible for humans to code by hand)
- they've often hard to maintain if major changes to a screen need to be made (I've had a few fights with Matisse on that one where it seemed easier to code a panel from scratch than rework an existing one).
The Swing JavaBuilder library aims to fix all of this (and then some) by allowing you to code the whole UI declaratively in a YAML file (a JSON superset made popular by Ruby on Rails that relies on whitespace indentation for hierarchical structures, somewhat like Python).
Besides drastically reducing the amount of code required to actually create your controls, the Swing JavaBuilder offers also an innovative layout management DSL that runs on top of MigLayout (the layout manager that has made all JDK layout manager obsolete basically), which is basically something like a GUI builder, but in pure text.
All of this is covered in detail in our PDF book, so if this tutorial is of interest to you, please refer to it for more in-depth explanations. Unlike many open source projects, the Swing JavaBuilder comes with detailed developer documentation. You're not supposed to wonder through a maze of disconnected Javadoc pages or Java source files to figure out how it works.
With that introduction let's move on and create a simple, typical business application for entering a person's data.
Unlike JavaFX with it streaming video and shiny buttons that go "ping!" when you press on them (like that machine in Monty Python's "The Meaning of Life"), the Swing JavaBuilder is aimed squarely at those boring business applications with dozens of fields, validation rules and long running save processes. In short, the type of applications that probably 95% of us write for a living day to day.
The only thing you need to start is download the 0.3.FINAL release of Swing JavaBuilder and the YAML editor plugin for Eclipse. In NetBeans, a YAML editor is part of the base install and in IDEA I believe it is part of the Ruby on Rails plugin.
Let's create a new Java project called "PersonApp". Add to it the Swing JavaBuilder jar and all of its dependencies (located in the "/lib" folder). Createa new package "person.app". In it, let's create our model class that represents a person:
package person.app;
import java.text.MessageFormat;
public class Person {
private String firstName;
private String lastName;
private String emailAddress;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmailAddress() {
return emailAddress;
}
public void setEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
}
@Override
public String toString() {
return MessageFormat.format("{0} {1} : {2}", getFirstName(), getLastName(), getEmailAddress());
}
}
Next, let's create our resource file "PersonApp.properties" in the default package (did I mention that the Swing JavaBuilder comes with integrated support for resource bundles out of the box?):
button.save=Save
button.cancel=Cancel
label.firstName=First Name
label.lastName=Last Name
label.email=Email
frame.title=Enter Person Data
If in your main(), you add a resource bundle to the main Swing JavaBuilder configuration, it will automatically switch on its internationalization mode and from then all it will expect all relevant text properties to be in the form of resource keys, instead of actual text. Hence, no need for you to manually fetch resources from bundles, it all gets done automatically.
Next, let's create the actual application, namely PersonApp:
package person.app;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import org.javabuilders.BuildResult;
import org.javabuilders.annotations.DoInBackground;
import org.javabuilders.event.BackgroundEvent;
import org.javabuilders.event.CancelStatus;
import org.javabuilders.swing.SwingJavaBuilder;
public class PersonApp extends JFrame {
private Person person;
private BuildResult result;
public PersonApp() {
person = new Person();
person.setFirstName("John");
person.setLastName("Smith");
result = SwingJavaBuilder.build(this);
}
public Person getPerson() {
return person;
}
private void cancel() {
setVisible(false);
}
@DoInBackground(cancelable=true, indeterminateProgress=false, progressStart=1, progressEnd=100)
private void save(BackgroundEvent evt) {
//simulate a long running save to a database
for(int i = 0; i < 100; i++) {
//progress indicator
evt.setProgressValue(i + 1);
evt.setProgressMessage("" + i + "% done...");
//check if cancel was requested
if (evt.getCancelStatus() != CancelStatus.REQUESTED) {
//sleep
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
} else {
//cancel requested, let's abort
evt.setCancelStatus(CancelStatus.COMPLETED);
break;
}
}
}
//runs after successful save
private void done() {
JOptionPane.showMessageDialog(this, "Person data: " + person.toString());
}
/**
* @param args
*/
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
//activate internationalization
SwingJavaBuilder.getConfig().addResourceBundle("PersonApp");
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
new PersonApp().setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}
Let's break it down and focus on some of the key lines of code:
In the main()
//activate internationalization
SwingJavaBuilder.getConfig().addResourceBundle("PersonApp");
By passing in the resource bundle name, we've let it know that we want the app to be internationalized from now on.
In the constructor we have
private BuildResult result;
public PersonApp() {
...
result = SwingJavaBuilder.build(this);
}
This line of code performs all the actual parsing and reading in of the YAML file (which should be located in the same package and have the same name as the Java class, but with a ".yaml" extension, a convention over configuration approach we pinched from Apache Wicket, obviously).
That's all you need to actually create this JFrame from the YAML file. All the other Java methods in the class are pure business-logic methods:
private void cancel() {...}
@DoInBackground(cancelable=true, indeterminateProgress=false, progressStart=1, progressEnd=100)
private void save(BackgroundEvent evt) {
...}
The @DoInBackground annotation flags this method as a long running business process that should be run automatically on a background thread using SwingWorker.
Now that we've seen the simple and straightforward Java logic let's move on to our piece de resistance, i.e. the actual YAML file itself.
As I mentioned before, create a "PersonApp.yaml" file in the same person.app package where the PersonApp.java file is located:JFrame(name=frame, title=frame.title, size=packed, defaultCloseOperation=exitOnClose):
- JLabel(name=fNameLbl, text=label.firstName)
- JLabel(name=lNameLbl, text=label.lastName)
- JLabel(name=emailLbl, text=label.email)
- JTextField(name=fName)
- JTextField(name=lName)
- JTextField(name=email)
- JButton(name=save, text=button.save, onAction=($validate,save,done))
- JButton(name=cancel, text=button.cancel, onAction=($confirm,cancel))
- MigLayout: |
[pref] [grow,100] [pref] [grow,100]
fNameLbl fName lNameLbl lName
emailLbl email+*
>save+*=1,cancel=1
bind:
- fName.text: this.person.firstName
- lName.text: this.person.lastName
- email.text: this.person.emailAddress
validate:
- fName.text: {mandatory: true, label: label.firstName}
- lName.text: {mandatory: true, label: label.lastName}
- email.text: {mandatory: true, emailAddress: true, label: label.email}
[in case the JL forum software does not display the spacing in this file correctly, just check out the original source code directly in our GitHub repository: ttp://github.com/jacek99/javabuilders/blob/44cb39db6ec0a80c0cf060716082816dbbb2e963/PersonApp/src/person/app/PersonApp.yaml]
Some things are obvious from looking at it. The whitespace-based indentation gives us the parent-child hierarchy of all the controls. As you can see, all the "text" properties point to a resource key, so the actual text will be fetched at runtime from the resource bundle we selected at the beginning.
The MigLayout section contains our DSL for layout management. It basically aligns the control names in rows and columns and allows all the usual MigLayout keywords, plus a few simple characters to control alignment / cell & row spanning, etc. The whole DSL specs are in our PDF book, so please look it up for the full list of options.
The "bind" section specifies databinding between control properties and the Java-side properties. The regular Beans Binding (JSR 295) library is used under the hood to implement this. Unlike in other solutions, the data binding logic is not mixed with the control definitions, but in a separate node by itself. This enforces better clarity and separation of concerns.
The "validate" node defines basic input validation. Under the hood it is implemented using Apache Commons Validators (we don't like reinventing the wheel).
Now let's take this baby out for a spin and see what it can really do.
Upon running the app, we should see:
Notice that the layout is created, all controls are aligned by baseline (thanks to the wonders of MigLayout) and even the Save and Cancel buttons are the same size (all of this possible to our MigLayout DSL). Also, thanks to databinding the Person's first and last name have been progagated from the Java class to the UI automatically.
Let's enter an invalid email address and press Save:
The integrated validation checked the input against the email validator in Apache Commons Validators and automatically displayed an error msg to the user.
Let's enter a valid email:
and press Save. The Save button was wired to execute the save() method on the Java side (via the "onAction=save" section in YAML), which in turn was flagged as a method that should be run in the background using Swing Worker (with an integrated progress bar and a Cancel option, if required):
After successfully saving, the done() method is invoked that displays the person's data. Please note that the email address we entered in the UI was automatically propagated to the Person bean via data binding:
We can now press Cancel. Since the YAML file requested a confirmation using the global "$confirm" commmand before executing the cancel method:
- JButton(name=cancel, text=button.cancel, onAction=($confirm,cancel))
we get a standard confirmation prompt:
In total we have a perfectly working typical business application with integrated control creation / layout management / data binding / Swing Worker support / input validation in less than 25 lines of YAML and with a Java class where most of the logic is purely business-related and has little, if any, UI-specific code in it.
I hope this little example will get you interested in the Swing JavaBuilder. For fans of other GUI toolkits, we have some early dev builds of the SWT JavaBuilder and some work is being done on the GTK+ JavaBuilder (which will use the Java-GNOME bindings to allow building of native Linux apps with Java).
Cheers and happy (productive) building!
P.S. You can view the source files for this tutorial app in our GitHub repository:
Opinions expressed by DZone contributors are their own.
Comments