I guess everybody who knows Java and Swing also knows the Swing Tutorial. It is a great source of information if you want to learn Swing. It is also a major catastrophy when it comes to structuring code. The problem is: Lots of people miss critical information contained in the tutorial like 'Everything concerned with Swing must happen in the EDT' but they seem to suck up the messy way to structure code like a sponge.
This might result in code like the one below. It is actually Scala code, but that shouldn't matter much. The only piece that is a little special are the calls to Binder, which is a little Swing Binding Framework which I introduced a couple of weeks ago.
private def createPersonPanel(p : PersonEditor) = {
val panel = new JPanel()
val layout = new GridBagLayout()
val c = new GridBagConstraints()
c.gridx = 0
c.gridy = 0
panel.setLayout(layout)
c.fill = 0
panel.add(new JLabel("firstname"), c)
c.fill = 1
c.gridx += 1
c.weightx = 1
val firstnameTF = new JTextField()
panel.add(firstnameTF, c)
Binder.bind(p.firstname, firstnameTF)
c.fill = 0
c.weightx = 0
c.gridx = 0
c.gridy += 1
panel.add(new JLabel("lastname"), c)
c.fill = 1
c.gridx += 1
val lastnameTF = new JTextField()
Binder.bind(p.lastname, lastnameTF)
panel.add(lastnameTF, c)
c.fill = 0
c.weightx = 1
c.gridy += 1
c.anchor = GridBagConstraints.SOUTHEAST
val button = new JButton("save")
Binder.bind(p.save, button)
panel.add(button, c)
panel
}
What is so bad about this piece of crap ... ähm ... code?
The method is long. 38 lines is about 10 times longer than healthy for a method.
There is tons of code duplication.
The method does lots of different things: creating components, adding them to a panel, configuring the layout.
There is a strong dependency on the order of commands. We can't just move stuff up or down in the method and still hope the result will be something reasonable, even if we stick to rearrangements allowed by the compiler.
All this together makes the method extremely hard to understand. How long does it take to understand what kind of GUI results? Don't bother to much, I help. It looks like this:
and if you resize it, it looks like this
Arguably the result looks just as ugly as the code, but once the code is clean we might be able to improve on the visual design as well.
If you don't see it in the code, you might see it in the images: There are three different ways JComponents are handled by the method: JLabels are in the left column and don't resize. The JTextFields are in the right column and do resize and the JButton doesn't resize and is in the buttom right. In the code this is completely hidden in the manipulation of the GridBagConstraint. So lets make it explicit in the code:
private def addToLabelColumn(
panel : JPanel,
component : JComponent,
row : Int) {
val c = new GridBagConstraints()
c.gridx = 0
c.gridy = row
c.weightx = 0
c.fill = 0
panel.add(component, c)
}
private def addToComponentColumn(
panel : JPanel,
component : JComponent,
row : Int) {
val c = new GridBagConstraints()
c.gridx = 1
c.gridy = row
c.weightx = 1
c.fill = 1
panel.add(component, c)
}
private def addButton(
panel : JPanel,
component : JButton,
row : Int) {
val c = new GridBagConstraints()
c.weightx = 1
c.gridx = 1
c.gridy = row
c.fill = 0
c.anchor = GridBagConstraints.SOUTHEAST
panel.add(component, c)
}
private def createPersonPanel(p : PersonEditor) = {
val panel = new JPanel()
val layout = new GridBagLayout()
panel.setLayout(layout)
addToLabelColumn(panel, new JLabel("firstname"), 0)
val firstnameTF = new JTextField()
Binder.bind(p.firstname, firstnameTF)
addToComponentColumn(panel, firstnameTF, 0)
addToLabelColumn(panel, new JLabel("lastname"), 1)
val lastnameTF = new JTextField()
Binder.bind(p.lastname, lastnameTF)
addToComponentColumn(panel, lastnameTF, 1)
val button = new JButton("save")
Binder.bind(p.save, button)
addButton(panel, button, 2)
panel
}
I introduced three methods. One for adding a JLabel, one for adding a JComponent and one for adding JButtons. These handle the arrangement of components on a JPanel. The total length of the code increased because we create the GridBagConstraints insided the methods and have to set all properties and don't rely anymore on the previous step to leave the constraint in a specific state.
If we now look at the createPersonPanel we'll get a strong fealing of repetition in various places:
- each call to add* methods takes the same JPanel as an argument. We can improve on this by creating a Builder wich crates the panel, contains the add* methods and can return the fully configured panel at the end.
- for each property we create a JLabel, a JTextField, bind the property to the later and add both to the JPanel. We can fix this by encapsulating it in a seperate method.
The result might look like this:
case class PanelBuilder() {
val panel = new JPanel()
val layout = new GridBagLayout()
panel.setLayout(layout)
def add(components : (JLabel, JComponent), row : Int) {
addToLabelColumn(components._1, row)
addToComponentColumn(components._2, row)
}
def add(
component : JButton,
row : Int) {
val c = new GridBagConstraints()
c.weightx = 1
c.gridx = 1
c.gridy = row
c.fill = 0
c.anchor = GridBagConstraints.SOUTHEAST
panel.add(component, c)
}
private def addToLabelColumn(
component : JComponent,
row : Int) {
val c = new GridBagConstraints()
c.gridx = 0
c.gridy = row
c.weightx = 0
c.fill = 0
panel.add(component, c)
}
private def addToComponentColumn(
component : JComponent,
row : Int) {
val c = new GridBagConstraints()
c.gridx = 1
c.gridy = row
c.weightx = 1
c.fill = 1
panel.add(component, c)
}
}
private def create(name : String, property : Property[String]) : (JLabel, JComponent) = {
val textField = new JTextField()
Binder.bind(property, textField)
(new JLabel(name), textField)
}
private def create(name : String, action : => Unit) = {
val button = new JButton("save")
Binder.bind(action, button)
button
}
private def createPersonPanel(p : PersonEditor) = {
val builder = PanelBuilder()
builder.add(create("firstname", p.firstname), 0)
builder.add(create("lastname", p.lastname), 1)
builder.add(create("save", p.save), 2)
builder.panel
}
The PanelBuilder has now two simple public add methods. In order to imitate the method signitures in Java we would have to create a couple of helper classes and interfaces. It would make the code less compact but this shouldn't be a serious problem. Creation of the various components is just as the binding extracted in two create Methods. The createPersonPanel has now only 5 lines of code. Adding another property to the form should be trivial. Changing the extremely simplistic layout should be trivial and is at least limited to a single small class. I think this is pretty much OK for a first step toward clean swing code. So I leave it like it is right now. Although I do have further plans for this.
I hope most of you agree that the code is much easier to understand and maintain in the form it is right now. But is it really worth the effort? Some might say no. If the whole application would consist of only this little panel. I would agree. But a Swing application typically does not have a single panel with two textfields and a button. But tens or even hundreds of panels. Many consisting of large collections of components. If you have to maintain such a monster, would you prefer createPersonPanel methods like the last one, or would you prefer the first version? What if the customer if finally fed up with your crappy layout and insists on proper spacing between the labels and the JTextFields or other components?
All this is pretty nice when you start a new Swing application. But what if you have an existing Swing application? One with convoluted code just as the Swing Tutorial taught you? Well ... start changing it now. Don't sit down for two months and rewrite all your code, but find pieces of code duplication and extract them. It will be a long way, but you wont reach the end if you don't start walking.
But what if you are not using Swing, but writing a web application? Well it really shouldn't matter much. Your PanelBuilder might be written in JavaScript, or create HTML, but the principle is the same: Seperate creation of components, layout of components and binding of components to properties and actions.
Talks
Wan't to meet me in person to tell me how stupid I am? You can find me at the following events: