One of the great things about Clojure is how it can make Java programming easier and less verbose.
Take Swing. It takes a ton of code to render even a simple GUI. Most tutorials don’t even tackle it without an IDE like NetBeans.
But we’ve got something Java lacks: macros!
In this post, I’ll build a simple counter application, using Clojure macros to make the code shorter and simpler.
Our app has two components: a label showing the current value of the counter, and a button to increment the counter. Here’s the basic structure:
(import '(javax.swing JLabel JButton JPanel JFrame)) (defn counter-app [] (let [label (JLabel. "Counter: 0") button (JButton. "Add 1") panel (JPanel.) frame (JFrame. "Counter App")] (.setOpaque panel true) (.add panel label) (.add panel button) (.setContentPane frame panel) (.setSize frame 300 100) (.setVisible frame true)))
You can run counter-app at the REPL and see the resulting GUI layout, although the button doesn’t do anything yet.
The annoying thing about this code is the imperative style Swing forces on us: construct local variables and hammer at them with method calls.
Fortunately, Clojure has a handy built-in macro for just this kind of situation: doto. The doto macro takes a body of expressions. It evaluates the first expression and saves it in a temporary variable, and then inserts that variable as the first argument in each of the following expressions. Finally, doto returns the value of the temporary variable. An example will make more sense:
;; This code: (doto (make-thing) (foo 1 2) (bar 3 4)) ;; Expands to this: (let [x (make-thing)] (foo x 1 2) (bar x 3 4) x)
The object created on the first line gets threaded through each of the following expressions and returned at the end.
We can use this to clean up our counter-app code:
(defn counter-app [] (let [label (JLabel. "Counter: 0") button (JButton. "Add 1") panel (doto (JPanel.) (.setOpaque true) (.add label) (.add button))] (doto (JFrame. "Counter App") (.setContentPane panel) (.setSize 300 100) (.setVisible true))))
Notice that we have eliminated one local variable (frame) entirely. Furthermore, all the expressions dealing with the JPanel are neatly grouped together.
For this to work out neatly, the order of definitions is important. We start at the inner-most components, JLabel and JButton, and move outward to the containing window.
Moving on! In my last post, I showed you how to proxy ActionListener to handle events like button clicks. Rather than typing out the proxy code each time, let’s make a macro:
(defmacro on-action [component event & body] `(. ~component addActionListener (proxy [java.awt.event.ActionListener] [] (actionPerformed [~event] ~@body))))
The on-action macro’s first argument is any component (such as JButton) that has an addActionListener method. The body of the macro is the code that will be executed when the ActionListener is triggered. The event argument is just a symbol; it will be bound to the ActionEvent that triggered the listener. (We could have used a fixed symbol like “this”, but that would be bad macro design.)
With this macro, the definition of our JButton can look like this:
(doto (JButton. "Add 1") (on-action event ;; ... code to run when button is clicked ... ))
We’re almost done! We just need a place to store the current value of the counter. Since we’re not worried about synchronization at this point, we can use an atom:
;; Initialize the counter (let [counter (atom 0)] ;; ... later on ... ;; Update the counter: (swap! counter inc))
Bringing it all together, our final app looks like this:
(defn counter-app [] (let [counter (atom 0) label (JLabel. "Counter: 0") button (doto (JButton. "Add 1") (on-action evnt ;; evnt is not used (.setText label (str "Counter: " (swap! counter inc))))) panel (doto (JPanel.) (.setOpaque true) (.add label) (.add button))] (doto (JFrame. "Counter App") (.setContentPane panel) (.setSize 300 100) (.setVisible true))))
Run counter-app at the REPL and you have a working GUI application. Not bad for about 50 lines!
[…] doto Swing with Clojure […]
Hello. Thank you very much for this Stuart. For my understanding, I tried to use the macro expended version of the code…. so:
(def button (doto (JButton. “Add 1”)
(on-action evnt ;; evnt is not used
(.repaint panel) (println “action jbuton click happened”))))
works fine, yet:
(def button (doto (JButton. “Add 1”)
(. evnt addActionListener
(proxy [java.awt.event.ActionListener] []
(actionPerformed [(.repaint panel)] (println “action jbuton click happened”))))))
fails, even though this is just the macroexpanded code of the macro…
any ideas why?
This tutorial is very helpful. The function:
(defn func-action [component event & f]
(.addActionListener component
(proxy [java.awt.event.ActionListener] []
(actionPerformed [event] (f))))))
works the same if “event” is passed in as “`event”. The problem is taking multiple functions for f (body in macro version) which works with the macro (save some exceptions). What is the advantage of using a macro, instead of a function, for cases like this?
How do I change size or alignment of the elements in the Panel?
Rik: check out the next post, Taming the GridBagLayout