Im Geschäft habe ich auf dem Firmenblog drei Artikel geschrieben, welche Clojure/Swing kurz vorstellen.
Clojure ist eine Lisp Sprache, welche auf der Java Virtual Machine läuft. Wir können alle Java Klassen verwenden. Ein Beispiel:
können alle Java Klassen verwenden
(import '(javax.swing JLabel JPanel JFrame ImageIcon) '(java.awt GridBagLayout GridBagConstraints)) (def EMPTY_TILE (ImageIcon. "empty.png")) (defn empty-tile [] (JLabel. EMPTY_TILE)) (defn simple-grid [panel width height] "Creates a grid of WIDTH + 1 columns and HEIGHT + 1 rows where each cell contains the result of a call to (EMPTY-TILE) and adds it to the PANEL." (let [constraints (GridBagConstraints.)] (loop [x 0 y 0] (set! (. constraints gridx) x) (set! (. constraints gridy) y) (. panel add (empty-tile) constraints) (cond (and (= x width) (= y height)) panel (= y height) (recur (+ x 1) 0) true (recur x (+ y 1)))))) (defn app [] (let [frame (JFrame. "Grid Mapper") panel (doto (JPanel. (GridBagLayout.)) (simple-grid 5 5))] (doto frame (.setContentPane panel) (.pack) (.setDefaultCloseOperation JFrame/EXIT_ON_CLOSE) (.setVisible true)))) (app)
In einem Arbeitsverzeichnis muss folgendes vorhanden sein:
1. der obige Code in einem File namens *gridmapper-1.clj*
2. aus dem aktuellen Zip muss man das clojure.jar extrahieren
3. das kleine Bild empty.png muss im gleichen Verzeichnis liegen
Aufruf nun wie folgt: java -cp clojure.jar clojure.main gridmapper.clj
Es erscheint ein kleines schwarzes Quadrat in einem Fenster, das man wieder schliessen kann.
Was ich hier interessant finde:
1. wie ganz natürlich FRame, JPanel, und JLabel integriert werden
2. wie man doto verwendet, um sich die ständige Wiederholung des Objektes zu sparen (beispielsweise ganz am Schluss wo diverse Funktionen auf dem frame aufgerufen werden
3. wie man mit loop und recur eine Schlaufe aufbaut, die Tail Recursion simuliert (eigentlich wird der Code so in seine iterative Form umgeschrieben)
4. wie Clojure eigentlich unveränderliche Objekte hat, mittels set! aber die Eigenschaften der Java Objekte gesetzt werden
PS: Wer keinen Emacs verwendet, kann ja mal das Eclipse Plugin Counterclockwise ausprobieren und im Kommentar Bericht erstatten. 😄
Nun wollen wir drauf zeichnen! Hierfür brauchen wir ein zweites Bild: floor.png.
Und hier der Source Code für gridmapper-2.clj:
(import '(javax.swing JLabel JPanel JFrame ImageIcon) '(javax.swing.event MouseInputAdapter) '(java.awt GridBagLayout GridBagConstraints) '(java.awt.event InputEvent)) (def EMPTY_TILE (ImageIcon. "empty.png")) (def FLOOR_TILE (ImageIcon. "floor.png")) (defn edit-grid [e] "Draw a tile at the position of the event E." (let [cell (.getComponent e) here (.getIcon cell)] (if (.equals here EMPTY_TILE) (.setIcon cell FLOOR_TILE) (.setIcon cell EMPTY_TILE)))) (defn mouse-input-adapter [] "A MouseInputAdapter that will call EDIT-GRID when the mouse is clicked or dragged into a grid cell." (proxy [MouseInputAdapter] [] (mousePressed [e] (edit-grid e)) (mouseEntered [e] (let [mask InputEvent/BUTTON1_DOWN_MASK] (if (= mask (bit-and mask (.getModifiersEx e))) (edit-grid e)))))) (let [mouse (mouse-input-adapter)] ;; share the mouse input adapter with every other tile (defn empty-tile [] (doto (JLabel. EMPTY_TILE) (.addMouseListener mouse) (.addMouseMotionListener mouse)))) (defn simple-grid [panel width height] "Creates a grid of WIDTH + 1 columns and HEIGHT + 1 rows where each cell contains the result of a call to (EMPTY-TILE) and adds it to the PANEL." (let [constraints (GridBagConstraints.)] (loop [x 0 y 0] (set! (. constraints gridx) x) (set! (. constraints gridy) y) (. panel add (empty-tile) constraints) (cond (and (= x width) (= y height)) panel (= y height) (recur (+ x 1) 0) true (recur x (+ y 1)))))) (defn app [] (let [frame (JFrame. "Grid Mapper") panel (doto (JPanel. (GridBagLayout.)) (simple-grid 5 5))] (doto frame (.setContentPane panel) (.pack) (.setDefaultCloseOperation JFrame/EXIT_ON_CLOSE) (.setVisible true)))) (app)
Was ist neu?
1. empty-tile ist nun eine Closure, welche jedem JLabel eine Kopie des immer gleichen MouseInputAdapters hinzufügt
2. der MouseInputAdapter reagiert auf Button drücken und herumfahren (aber nicht auf das traditionelle Drag & Drop)
3. der Code für den MouseInputAdapter zeigt, wie man in Clojure eine Klasse erstellt, welche den MouseInputAdapter implementiert und die beiden Methoden mousePressed und mouseEntered überschreibt
4. der MouseInputAdapter bestimmt die betroffenen JLabel über den InputEvent und ruft edit-grid auf
5. edit-grid wechselt nun das ImageIcon aus
Resultat:
Wir brauchen ein drittes Bild: trap.png.
Und hier der Source Code für gridmapper-3.clj (nun schon 90 Zeilen):
(import '(javax.swing JLabel JPanel JFrame ImageIcon) '(javax.swing.event MouseInputAdapter) '(java.awt GridBagLayout GridBagConstraints) '(java.awt.event KeyAdapter InputEvent)) (def EMPTY_TILE (ImageIcon. "empty.png")) (def FLOOR_TILE (ImageIcon. "floor.png")) (def TRAP_TILE (ImageIcon. "trap.png")) (def current-tile (atom FLOOR_TILE)) (defn get-tile [] "Return the tile the user wants to place. By default this will be a FLOOR_TILE." @current-tile) (defn set-tile [tile] "Set the tile the user wants to place. This must be an ImageIcon." (println tile) (reset! current-tile tile)) (defn edit-grid [e] "Draw a tile at the position of the event E." (let [cell (.getComponent e) here (.getIcon cell) tile (get-tile)] (cond (.equals here EMPTY_TILE) (.setIcon cell FLOOR_TILE) (and (.equals here FLOOR_TILE) (.equals tile FLOOR_TILE)) (.setIcon cell EMPTY_TILE) (.equals here FLOOR_TILE) (.setIcon cell tile) true (.setIcon cell FLOOR_TILE)))) (defn mouse-input-adapter [] "A MouseInputAdapter that will call EDIT-GRID when the mouse is clicked or dragged into a grid cell." (proxy [MouseInputAdapter] [] (mousePressed [e] (edit-grid e)) (mouseEntered [e] (let [mask InputEvent/BUTTON1_DOWN_MASK] (if (= mask (bit-and mask (.getModifiersEx e))) (edit-grid e)))))) (let [mouse (mouse-input-adapter)] ;; share the mouse input adapter with every other tile (defn empty-tile [] (doto (JLabel. EMPTY_TILE) (.addMouseListener mouse) (.addMouseMotionListener mouse)))) (defn simple-grid [panel width height] "Creates a grid of WIDTH + 1 columns and HEIGHT + 1 rows where each cell contains the result of a call to (EMPTY-TILE) and adds it to the PANEL." (let [constraints (GridBagConstraints.)] (loop [x 0 y 0] (set! (. constraints gridx) x) (set! (. constraints gridy) y) (. panel add (empty-tile) constraints) (cond (and (= x width) (= y height)) panel (= y height) (recur (+ x 1) 0) true (recur x (+ y 1)))))) (defn keyboard-adapter [] (proxy [KeyAdapter] [] (keyTyped [e] (let [c (.getKeyChar e)] (condp = c \t (set-tile TRAP_TILE) \n (set-tile FLOOR_TILE) true))))) (defn app [] (let [frame (JFrame. "Grid Mapper") panel (doto (JPanel. (GridBagLayout.)) (simple-grid 5 5))] (doto frame (.addKeyListener (keyboard-adapter)) (.setContentPane panel) (.pack) (.setDefaultCloseOperation JFrame/EXIT_ON_CLOSE) (.setVisible true)))) (app)
Neu sind:
1. current-tile ist ein Clojure Atom; Clojure kennt verschiedene Referenztypen, mit denen Werte gespeichert und geändert werden können (was ja sonst nicht geht weil Clojure nur unveränderliche Werte kennt); Atom verwendet man für einfache, synchronisierte Zugriffe innerhalb desselben Threads
2. Mit set-tile und get-tile wird auf das Atom zugegriffen. Insfern nichts neues.
3. edit-grid enthält nun etwas Logik, mit der das passende Element gemäss get-tile gewählt wird
4. Neu ist der KeyboardAdapter auf dem JFrame, welcher set-tile aufruft; mit T kann man “trap” auf den “floor” zeichnen
Das aktuelle Projekt ist auf GitHub erhältlich.
#Clojure