My GUIs suck. They are hard to use and are a nightmare behind the scenes. In particular, I find it hard to separate the business logic from the GUI code. However I managed to divide these parts in a recent project into 100% disjoint namespaces. Let's see how.
GUI code is all about handling state. So we have to cope with that. Luckily Clojure gives us a variety of tools here. I decided to use atoms for my little project.
The state is modified by the user via UI interaction. So we need a way to change the atom once some interaction happens. This can be easily done via the huge listener machinery of Swing. Here is an example of a button modifying the atom.
(defn process
[stuff canceled?]
(doseq [x (take-while (fn [_] (not @canceled?)) stuff)]
(process-item x)))
(defn some-click-handler
[evt]
(let [stuff (get-eg.-selected-stuff-from-gui)
canceled? (atom false)
cancel (JButton. "Cancel")
...]
(add-action-listener
cancel
(fn [_]
(reset! canceled? true)))
...
(future (process stuff canceled?))))
add-action-listener
is a small helper from clojure.contrib.swing-utils.
Otherwise the idea should be pretty straight-forward. The future
call
is necessary, so that the processing doesn't run on the Swing Event
Dispatch Thread.
We first retrieve some stuff we want to process from our GUI. Then we set up a Dialog with a cancel button and fire off the processing. Pressing the cancel button should stop the processing of our input data. In order for this to work, our processing function has to take the atom carrying the state of whether the computation should be stopped. And in fact it has to check the atom from time to time. Here we do this after every item. A bit hacky but worked for me. Beware chunked seqs, though.
So far this is not very exciting. To keep the user entertained let's add a progress bar to our dialog.
(defn process
[stuff canceled? progress]
(doseq [x (take-while (fn [_] (not @canceled?)) stuff)]
(process-item x)
(do-swing
(.setValue progress (inc (.getValue progress))))))
(defn some-click-handler
[evt]
(let [stuff (get-eg.-selected-stuff-from-gui)
canceled? (atom false)
cancel (JButton. "Cancel")
progress (JProgressBar. 0 (count stuff))
...]
(add-action-listener
cancel
(fn [_]
(reset! canceled? true)))
...
(future (process stuff canceled? progress))))
Again, we have to pass the progress bar down to our processing function in order to be able to update it after an item was processed.
But wait! Now our processing function has to know, what a progress bar
is! And it has to know, that updating a progress bar has to be done on
the Swing EDT. What the heck is an EDT? Things like do-swing
from
clojure.contrib.swing-utils help a bit. But why should our
processing function be bothered with such stuff. Again GUI stuff leaked
into my logic. Bleh.
But Clojure to the rescue: we can simply fix the situation using a feature of Clojure's reference types: a watch. We can wire-up the state of the progress bar with a Clojure atom. Updating the atom will then also update the progress bar. That way the processing function does not have to know how the GUI works. It just updates the atom.
(defn process
[stuff canceled? progress]
(doseq [x (take-while (fn [_] (not @canceled?)) stuff)]
(process-item x)
(swap! progress inc)))
(defn some-click-handler
[evt]
(let [stuff (get-eg.-selected-stuff-from-gui)
canceled? (atom false)
cancel (JButton. "Cancel")
progress (atom 0)
bar (JProgressBar. 0 (count stuff))
...]
(add-action-listener
cancel
(fn [_]
(reset! canceled? true)))
(add-watch
progress
::update-progress-bar
(fn [_ _ _ new-val]
(do-swing
(.setValue bar new-val))))
...
(future
(process stuff canceled? progress))))
Ah. Much better the progress bar update code moved back to the other GUI stuff again. And the processing function is independent of the GUI code again.
Using watches, we were able to completely separate the GUI code from the business logic. Neither is dependent on the implementation of the other.
Here a full example which shows the described technique.
(ns example.gui
(:import
javax.swing.JButton
javax.swing.JFrame
javax.swing.JProgressBar
com.jgoodies.forms.layout.CellConstraints
com.jgoodies.forms.layout.FormLayout
com.jgoodies.forms.factories.ButtonBarFactory
com.jgoodies.forms.builder.PanelBuilder)
(:use
[clojure.contrib.swing-utils
:only (do-swing do-swing* add-action-listener)]))
(defn processing
[items done? canceled? progress]
(doseq [item (take-while (fn [_] (not @canceled?)) items)]
(println item)
(Thread/sleep 5000)
(swap! progress inc))
(reset! done? true))
(defn startup
[]
(let [items (take 5 (iterate inc 0))
frame (JFrame. "Example GUI")
layout (FormLayout. "fill:default:grow" "pref,3dlu,pref")
cc (CellConstraints.)
progress (atom 0)
pbar (JProgressBar. @progress (count items))
done? (atom false)
done (JButton. "Done")
canceled? (atom false)
cancel (JButton. "Cancel")
bbar (ButtonBarFactory/buildCenteredBar (into-array [done cancel]))
panel (-> (PanelBuilder. layout)
(doto
(.setDefaultDialogBorder)
(.add pbar (.xy cc 1 1))
(.add bbar (.xy cc 1 3)))
.getPanel)]
; Wire up the done button.
(.setEnabled done false)
(add-action-listener
done
(fn [_]
(do-swing
(doto frame
(.setVisible false)
(.dispose)))))
(add-watch
done?
::toggle-buttons-on-done
(fn [_ _ _ done?]
(do-swing
(when done?
(.setEnabled cancel false)
(.setEnabled done true)))))
; Wire up cancel button.
(add-action-listener
cancel
(fn [_]
(reset! canceled? true)))
; Wire up progress bar.
(add-watch
progress
::update-progress-bar
(fn [_ _ _ v]
(do-swing
(.setValue pbar v))))
(doto frame
(.setDefaultCloseOperation JFrame/DISPOSE_ON_CLOSE)
(-> .getContentPane (.add panel))
(.pack)
(.setVisible true))
(-> #(processing items done? canceled? progress) Thread. .start)))
(defn main
[]
(do-swing* :now startup))
Published by Meikel Brandmeyer on .
I'm a long-time Clojure user and the developer of several open source projects mostly involving Clojure. I try to actively contribute to the Clojure community.
My most active projects are at the moment VimClojure, Clojuresque and ClojureCheck.
Copyright © 2009-2014 All Right Reserved. Meikel Brandmeyer