Taming the GridBagLayout

GUI layout is hard. You’d be crazy to do it without a GUI designer like Netbeans.

Well, I’m pretty crazy. So I’m going to do some GUI layout in Clojure. And I’m going to use the most intimidating of Java’s GUI layout classes, the GridBagLayout.

Conceptually, GridBagLayout is pretty straightforward. It places components on a flexible grid, in which each column and row is automatically sized to fit the largest component. A set of constraints placed on each component determines its exact placement, size, and alignment.

The devil, as usual, is in the details. GridBagConstraints has 11 fields and 32 constants to control the layout of each component. I won’t try to explain how it works; read the tutorial a few times instead.

The problem with GridBagConstraints, from a Clojure point of view, is that it’s a mutable object with public instance fields. This isn’t a problem for concurrency, because the object is used in only one place, but the syntax is awkward:

(import 'java.awt.GridBagConstraints)
(def c (GridBagConstraints.))
(set! (. c gridx) 1)
(set! (. c gridy) GridBagConstraints/RELATIVE)

Since I will have to do a lot of set!s, I can try to pare it down a bit with a macro:

(defmacro set-grid! [constraints field value]
  `(set! (. ~constraints ~(symbol (name field)))
         ~(if (keyword? value)
            `(. java.awt.GridBagConstraints
                ~(symbol (name value)))
            value)))

This macro takes a field name as a keyword and sets the value of that field. I used keywords instead of bare symbols because they tend to signal names or constants in Clojure code.

The macro has one other trick up its sleeve: if the value of a field is a keyword, it gets used as the name of a static field (constant) in GridBagConstraints.

With this macro, the set!s in the previous example become:

(set-grid! c :gridx 1)
(set-grid! c :gridy :RELATIVE)

So far, so good. But we’ve only replaced a bunch of set!s with a bunch of set-grid!s.

What we really want is a terse syntax for specifying constraints. There are many ways to do this. I could write a macro that creates a new GridBagConstraints for each component. But that would require a lot of repetition.

Often one wants to reuse the same constraint for several components in a row. The tutorial examples achieve this by using a single instance of GridBagConstraints and modifying its fields for each component. (The definition of GridBagLayout explicitly permits this.)

So what I want is a macro that allows me to add components to a container, in any order, specifying only the constraints that need to change for each component. Here’s what it will look like:

(grid-bag-layout container
  :gridx 0, :gridy 0
  component-one
  :gridx :RELATIVE, :gridwidth 2
  component-two
  ;; ... more components & constraints ...
  )

The first argument to the macro is a container with a GridBagLayout as its layout manager. What follows is a mixture of components and constraints. Each component is added to the layout with the current set of constraints. A keyword signals a change to a constraint field, and is immediately followed by its value. Once a field is set, it retains its value until it is set again.

The line breaks and commas are just whitespace; what matters is the order.

Here’s the macro:

(defmacro grid-bag-layout [container & body]
  (let [c (gensym "c")
        cntr (gensym "cntr")]
    `(let [~c (new java.awt.GridBagConstraints)
           ~cntr ~container]
       ~@(loop [result '() body body]
           (if (empty? body)
             (reverse result)
             (let [expr (first body)]
               (if (keyword? expr)
                 (recur (cons `(set-grid! ~c ~expr
                                          ~(second body))
                              result)
                        (next (next body)))
                 (recur (cons `(.add ~cntr ~expr ~c)
                              result)
                        (next body)))))))))

You’ll have to take my word for it that this macro was not especially difficult to write. As for what it does, it loops through the body and recursively constructs code for the result. When it encounters a keyword, it uses the set-grid! macro I defined earlier. Otherwise, it assumes the expression is a component, and adds it to the container. (The manual gensyms are necessary because of the nested backquotes.)

Here’s an example that creates a layout with three buttons, as in this screenshot:

GridBagLayout example

(import '(javax.swing JFrame JPanel JButton)
        '(java.awt GridBagLayout Insets))

(def panel
     (doto (JPanel. (GridBagLayout.))
       (grid-bag-layout
        :fill :BOTH, :insets (Insets. 5 5 5 5)
        :gridx 0, :gridy 0
        (JButton. "One")
        :gridy 1
        (JButton. "Two")
        :gridx 1, :gridy 0, :gridheight 2
        (JButton. "Three"))))

(def frame
     (doto (JFrame. "GridBagLayout Test")
       (.setContentPane panel)
       (.pack)
       (.setVisible true)))

Admittedly, this is not all that much shorter than the equivalent Java code.  But it’s still shorter, without diminishing the power or flexibility of the original API.

This is the kind of mini-language for which Lisp is famous.  And it’s only the beginning.  We could go on to define a more complete, expressive, and functional language for designing GUIs.  But that will have to wait for another post.

7 thoughts on “Taming the GridBagLayout

  1. AOL John

    Every time you post something about Clojure I learn something. Thank you for taking the time to blog.

  2. Joe

    I had to add a
    (.setDefaultCloseOperation JFrame/EXIT_ON_CLOSE)
    to make the window close correctly.

    Very educational. Thanks.

  3. Stuart Post author

    Joe wrote: “I had to add a (.setDefaultCloseOperation JFrame/EXIT_ON_CLOSE)”

    Yes, that is necessary if you run the example as a script. But if you use that in the REPL, closing the window will kill the REPL process.

  4. Jeff Schwab

    Re. JFrame/EXIT_ON_CLOSE: The right solution is to use JFrame/DISPOSE_ON_CLOSE instead. I don’t know why so many tutorials (including the Sun Swing trails) use EXIT_ON_CLOSE.

  5. Pingback: Starting a Clojure/Swing Project | To Dream of Magick

Comments are closed.