On Tue, Aug 4, 2009 at 10:33 PM, Joe Van Dyk <joevan...@gmail.com> wrote:

>
> Hey,
>
> New to Java and Clojure.  My possibly relevant experience is with Gtk
> and Ruby and C++ programming.
>
> I'd like to develop a GUI in Clojure.  I'm guessing I want to use
> Swing.  This application will be targeted towards scientists who are
> used to working with the most horribly-designed Tk UI known to man, so
> I'm sure they will be fine with Swing.
>
> So, where's the best place to start?
>
> What I've been doing:
>
> - Watched the peepcode
> - Working my way through Stuart's book
> - Playing with netbean's GUI designer
>
> Is it possible to use the netbean designer and clojure at the same
> time?  What's the best way of doing that?  I'm used to writing GUIs in
> C++, would Clojure have a drastically different approach (as far as
> application logic and event handling)?


I've not done much GUI work in Clojure yet, but what I have done has not
involved any GUI builders. Just Clojure functions, some calling Swing
methods.

A lot simplifies once you have some useful utility functions and macros. For
example:

(defmacro jbutton
  "Creates a JButton with the specified text, which when clicked executes
the body with an ActionEvent bound to event."
  [text [event] & body]
  `(let [b# (JButton. ~text)]
     (.addActionListener b#
       (proxy [ActionListener] []
         (actionPerformed [~event] ~...@body)))))

(defn add-all
  "Adds the specified things, in order to obj. Useful for obj = a Swing
JPanel with a BoxLayout or a FlowLayout and things = Swing components."
  [obj & things]
  (doseq [thing things] (.add obj thing)))

(defn box-layout-panel
  "Given a JPanel or other component that can have a layout, gives it a
BoxLayout with the specified orientation."
  [panel orientation]
  (.setLayout panel (BoxLayout. panel orientation)))

(defn horizontal-box-layout-panel
  "Given a JPanel or other component that can have a layout, gives it a
BoxLayout with horizontal orientation."
  ([panel]
    (box-layout-panel panel BoxLayout/LINE_AXIS)
    panel)
  ([]
    (horizontal-box-layout-panel (JPanel.))))

(defn horizontal-glue
  []
  (Box/createHorizontalGlue))

(defn left-component
  "Given a JComponent, returns a component that will try to display it at
its
preferred size on the left side of its container."
  [component]
  (let [result (horizontal-box-layout-panel)]
    (.add result component)
    (.add result (horizontal-glue))
    result))

(defn combine-borders
  "Combines multiple borders into one. The first argument will be the
outermost,
followed by the next argument, and the next; the last argument will be the
innermost."
  ([border]
    border)
  ([outer inner]
    (BorderFactory/createCompoundBorder outer inner))
  ([outermost nxt & more]
    (reduce combine-borders (cons outermost (cons nxt more)))))

and so forth.

I've even got:

(def *instruction-columns* 60)

(def *instruction-inset* 10)

(defn instructions
  "Turns strings, or other objects, using str, into instruction text and
returns
a component suitable for displaying these in a Swing UI."
  [& strings]
  (doto (JTextArea. (apply str strings))
    (.setEditable false)
    (.setLineWrap true)
    (.setWrapStyleWord true)
    (.setColumns *instruction-columns*)
    (.setOpaque false)
    (.setBorder (combine-borders
                  (lowered-bevel-border)
                  (empty-border *instruction-inset*)))))

which produces a component that displays essentially a multi-line label with
a 10-pixel space around its perimeter; as the name says, useful for putting
instruction strings in a dialog box or whatever without worrying about where
to put manual line breaks. I've used this in wizards, which often have large
chunks of textual instructions as well as a few form fields and back, next,
cancel buttons.

Naturally, other code does things like take a passed-in JPanel and pop up a
JDialog with a specified title, the JPanel front and center, and OK and
Cancel buttons or similar at the bottom, with a supplied block of code to
execute when OK is pressed and the behavior that both Cancel and the close
button close and dispose the JDialog without doing anything; the macro for
this expands into code that evaluates to nil on cancel and whatever the OK
code returns on OK.

It can take as little as ten minutes to slap something like one of these
things together and another ten to test it. With these kinds of reusable UI
fragments, both "framed" elements (like jbutton and instructions generate)
and "framing" ones (like the JDialog generator macro), slapping together a
GUI in code becomes easy.

You might have noticed indications of my preference for using nested
BoxLayouts to build things. I like this because BoxLayout respects a
component's preferred size and nested BoxLayouts tend to be well behaved
when subjected to window or component resizings. GridBagLayout would be the
only other contender for flexibility and size-control of components, but
BoxLayout nesting lends itself much better to a compositional style of
assembly of the GUI component tree, and thus to a functional style of code
and to use of reusable fragments like the above examples.

Last but not least, do keep in mind the need for Swing actions to take place
on the EDT. SwingWorker macro in the works, and someone else posted code
here a while ago that I shamelessly copied that invokes actions on the EDT.
I may have made some modifications:

(defmacro do-on-edt
  "Evaluates body on the event dispatch thread. Does not block. Returns
nil."
  [& body]
  `(SwingUtilities/invokeLater (fn [] ~...@body)))

(defmacro get-on-edt
  "Evaluates body on the event dispatch thread, and returns whatever body
evaluates to. Blocks until body has been evaluated."
  [& body]
  `(let [ret# (LinkedBlockingDeque.)]
     (SwingUtilities/invokeLater (fn [] (.put ret# (do ~...@body))))
     (.take ret#)))

(defmacro future-on-edt
  "Evaluates body on the event dispatch thead and returns a future that will
eventually hold the result of evaluating that body."
  [& body]
  `(future (get-on-edt ~...@body)))

The REPL does NOT run on the EDT, so GUI code tests from the REPL should be
wrapped in do-on-edt. That returns nil; the other two furnish the result of
evaluating the body code back to the calling thread. All but get-on-edt can
be invoked from the EDT safely; get-on-edt will deadlock if called on the
EDT, since invokeLater won't do anything until pending events are processed,
but one of those pending events will be the code that's blocking on the
LinkedBlockingDeque waiting for the invokeLater. It can be called from
SwingWorkers, futures, and agents safely though, and directly at the REPL. I
should make get-on-edt smarter actually:

(defmacro get-on-edt
  "Evaluates body on the event dispatch thread, and returns whatever body
evaluates to. Blocks until body has been evaluated."
  [& body]
  `(if (SwingUtilities/isEventDispatchThread)
     (do ~...@body)
     (let [ret# (LinkedBlockingDeque.)]
       (SwingUtilities/invokeLater (fn [] (.put ret# (do ~...@body))))
       (.take ret#))))

There, fixx0red.

I have a .clj file full of the most reusable bits of Swing interop code like
these. The (ns ...) at the top is chock full of Swing classes. The code that
uses this .clj file tends to only need to import one or two, on top of
:use-ing that .clj file, so building up a load of little utilities like
these in a file even saves on big complex (ns ...) work at the top of your
other UI-related source files.

By the way, feel free to use all of the above as public domain code. Well,
unless the earlier posted of the foo-on-edt functions complains anyway, but
I think he offered them in the same spirit, and I'm not sure code snippets
that short are copyrightable anyway.

--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clojure@googlegroups.com
Note that posts from new members are moderated - please be patient with your 
first post.
To unsubscribe from this group, send email to
clojure+unsubscr...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to