Passing User Events to Clojure from Java GUIs

copyright 2010 by Terrance Gene Davis

So you want to create a complex GUI to stick on the front end of your Clojure application. You might have noticed that there are not a lot of Clojure GUI development tools out there. That’s fine. Clojure plays real nice with Java, and Java has lots of GUI creation tools.

My favorite Java GUI creation tool is JFormDesigner. It doesn’t cost much, and has a lot of power. After creating a GUI with JFromDesigner (or your favorite GUI design tool) it is relatively painless to integrate your Java GUI with your Clojure application.

If you are writing a Java program that includes Clojure code, the process of integrating Clojure is slightly different than if you are writing a Clojure program that integrates Java.

If your program is Java based, then you need to load the Clojure scripts with a command such as:


RT.loadResourceScript("my_script.clj");

Then to call the script you need to first get a reference to the function, and then invoke the reference to the function. For instance,


Var ref = RT.var("my-ns", "my-method");

Object returnValue = ref.invoke(“the”, “method”, “args”);

However, if you have a Clojure program that includes a Java GUI, the Java code needs access to Clojure for processing user events, but loading the clojure scripts is no longer necessary. After all, the Clojure scripts are already loaded. In this case, do not call RT.loadResourceScript(). Skip directly to obtaining the method reference and invoking it.

It is important to remember that the loadResourceScript() method should only be called once, not every time an event is passed to Clojure. Also, when the application started from Clojure instead of Java, it is unlikely that you need to call loadResourceScript() at all.

Now for an example. The example I created is a GUI palindrome detector, as shown below. The application is a Clojure application that happens to use a Java based GUI. The GUI is a JFrame. The JFrame is generated using JFromDesigner. The code to process user events from the JFrame is written in Clojure.

Palindrome Checker Frame

Here is the Clojure code:

#!/usr/bin/env clj

(ns palindrome)

(import (com.genedavis PalindromeCheckerFrame))

;; take input from in a frame and determine if it is a palindrome

(def frame (new PalindromeCheckerFrame))
(. frame setVisible true)

(defn check [the-string]
(def original (seq the-string))
(def reversed (reverse original))
(if (= original reversed)
(.. frame getResultOfCheckTF (setText “Yes! You have a Palindrome.”))
(.. frame getResultOfCheckTF (setText “Nope! Not a Palindrome.”))))

(defn clear []
(.. frame getString2CheckTF (setText “”))
(.. frame getResultOfCheckTF (setText “”)))

The Clojure code imports the com.genedavis.PalindromeCheckerFrame using the command,


(import '(com.genedavis PalindromeCheckerFrame))

Notice the single quote (only one), and the space instead of a period between the package name and the class name.

Instantiating the JFrame and making it visible is done the same way it is in Java, with different syntax. First, create the PalindromeCheckerFrame with the new command and then assign it to a variable. Then show the JFrame with the setVisible() method.

The actual Clojure code for creating and showing the JFrame looks like this:


(def frame (new PalindromeCheckerFrame))

(. frame setVisible true)

I have explained the Java end of things already. But seeing code may make understanding the explanation a bit more clear. Look specifically at the two methods checkButtonClicked() and clearButtonClicked().

Here is the Java code:

/*
 * Created by JFormDesigner on Sat Feb 13 08:43:59 MST 2010
 */

package com.genedavis;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;

import clojure.lang.RT;
import clojure.lang.Var;


/**
 * @author T. Gene Davis
 */
public class PalindromeCheckerFrame extends JFrame {
    public PalindromeCheckerFrame() {
        initComponents();
    }

    public JTextField getString2CheckTF() {
        return string2CheckTF;
    }

    public JTextField getResultOfCheckTF() {
        return resultOfCheckTF;
    }

    /**
     * Click the "check" button, and call a Clojure function
     */
    private void checkButtonClicked(ActionEvent ae) {

      try
      {
         // Get a reference to the check() function 
         // in the palindrome namespace.
         Var foo = RT.var("palindrome", "check");

         // call check() with a String as the argument
         foo.invoke(string2CheckTF.getText().toLowerCase());
      }
      catch (Exception e)
      {
         e.printStackTrace();
      }

    }

   /**
    * Click the "clear" button, and call a Clojure function
    */
   private void clearButtonClicked(ActionEvent ae) {

      try
      {
         // Get a reference to the clear function 
         // in the palindrome namespace.
         Var foo = RT.var("palindrome", "clear");

         // call clear() with no arguments
         foo.invoke();
      }
      catch (Exception e)
      {
         e.printStackTrace();
      }

    }

    private void initComponents() {
        // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
        dialogPane = new JPanel();
        contentPanel = new JPanel();
        label3 = new JLabel();
        checkStringLabel = new JLabel();
        string2CheckTF = new JTextField();
        label2 = new JLabel();
        resultLabel = new JLabel();
        resultOfCheckTF = new JTextField();
        buttonBar = new JPanel();
        checkButton = new JButton();
        clearButton = new JButton();

        //======== this ========
        setTitle("Palindrome Checker");
        setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        Container contentPane = getContentPane();
        contentPane.setLayout(new BorderLayout());

        //======== dialogPane ========
        {
            dialogPane.setBorder(new EmptyBorder(12, 12, 12, 12));
            dialogPane.setBackground(new Color(238, 238, 238));
            dialogPane.setLayout(new BorderLayout());

            //======== contentPanel ========
            {
                contentPanel.setLayout(new GridBagLayout());
                ((GridBagLayout)contentPanel.getLayout()).columnWidths = new int[] {120, 247, 0};
                ((GridBagLayout)contentPanel.getLayout()).rowHeights = new int[] {0, 0, 0, 19, 0};
                ((GridBagLayout)contentPanel.getLayout()).columnWeights = new double[] {0.0, 0.0, 1.0E-4};
                ((GridBagLayout)contentPanel.getLayout()).rowWeights = new double[] {0.0, 0.0, 0.0, 0.0, 1.0E-4};

                //---- label3 ----
                label3.setText("   ");
                contentPanel.add(label3, new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0,
                    GridBagConstraints.CENTER, GridBagConstraints.BOTH,
                    new Insets(0, 0, 5, 5), 0, 0));

                //---- checkStringLabel ----
                checkStringLabel.setText("String to check:");
                checkStringLabel.setHorizontalAlignment(SwingConstants.RIGHT);
                contentPanel.add(checkStringLabel, new GridBagConstraints(0, 1, 1, 1, 0.0, 0.0,
                    GridBagConstraints.CENTER, GridBagConstraints.BOTH,
                    new Insets(0, 0, 5, 5), 0, 0));
                contentPanel.add(string2CheckTF, new GridBagConstraints(1, 1, 1, 1, 0.0, 0.0,
                    GridBagConstraints.CENTER, GridBagConstraints.BOTH,
                    new Insets(0, 0, 5, 0), 0, 0));

                //---- label2 ----
                label2.setText("   ");
                contentPanel.add(label2, new GridBagConstraints(0, 2, 1, 1, 0.0, 0.0,
                    GridBagConstraints.CENTER, GridBagConstraints.BOTH,
                    new Insets(0, 0, 5, 5), 0, 0));

                //---- resultLabel ----
                resultLabel.setText("Result:");
                resultLabel.setHorizontalAlignment(SwingConstants.RIGHT);
                contentPanel.add(resultLabel, new GridBagConstraints(0, 3, 1, 1, 0.0, 0.0,
                    GridBagConstraints.CENTER, GridBagConstraints.BOTH,
                    new Insets(0, 0, 0, 5), 0, 0));

                //---- resultOfCheckTF ----
                resultOfCheckTF.setBorder(null);
                resultOfCheckTF.setText("Not Palindrome");
                resultOfCheckTF.setBackground(new Color(238, 238, 238));
                contentPanel.add(resultOfCheckTF, new GridBagConstraints(1, 3, 1, 1, 0.0, 0.0,
                    GridBagConstraints.CENTER, GridBagConstraints.BOTH,
                    new Insets(0, 0, 0, 0), 0, 0));
            }
            dialogPane.add(contentPanel, BorderLayout.CENTER);

            //======== buttonBar ========
            {
                buttonBar.setBorder(new EmptyBorder(12, 0, 0, 0));
                buttonBar.setLayout(new GridBagLayout());
                ((GridBagLayout)buttonBar.getLayout()).columnWidths = new int[] {0, 85, 80};
                ((GridBagLayout)buttonBar.getLayout()).columnWeights = new double[] {1.0, 0.0, 0.0};

                //---- checkButton ----
                checkButton.setText("Check");
                checkButton.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        checkButtonClicked(e);
                    }
                });
                buttonBar.add(checkButton, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0,
                    GridBagConstraints.CENTER, GridBagConstraints.BOTH,
                    new Insets(0, 0, 0, 5), 0, 0));

                //---- clearButton ----
                clearButton.setText("Clear");
                clearButton.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        clearButtonClicked(e);
                    }
                });
                buttonBar.add(clearButton, new GridBagConstraints(2, 0, 1, 1, 0.0, 0.0,
                    GridBagConstraints.CENTER, GridBagConstraints.BOTH,
                    new Insets(0, 0, 0, 0), 0, 0));
            }
            dialogPane.add(buttonBar, BorderLayout.SOUTH);
        }
        contentPane.add(dialogPane, BorderLayout.CENTER);
        setSize(400, 235);
        setLocationRelativeTo(null);
        // JFormDesigner - End of component initialization  //GEN-END:initComponents
    }

    // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables
    private JPanel dialogPane;
    private JPanel contentPanel;
    private JLabel label3;
    private JLabel checkStringLabel;
    public JTextField string2CheckTF;
    private JLabel label2;
    private JLabel resultLabel;
    public JTextField resultOfCheckTF;
    private JPanel buttonBar;
    private JButton checkButton;
    private JButton clearButton;
    // JFormDesigner - End of variables declaration  //GEN-END:variables
}