CLIM Walkthrough

These are the notes of my exploration of CLIM. I'm using McCLIM and LispWorks to try it out. Mail rudi at constantly.at with comments, if you want. The source file is available as well (right click and save; at least my browser displays it a bit funky).

Fire up your editor, load McCLIM (see its INSTALL files), then evaluate forms one after another, redisplay the frame and watch the effects.

First, let's create a package.

(defpackage :tags-ui
  (:use :clim  :clim-lisp))

(in-package :tags-ui)

This form and the next one are commented out. Whenever you want to see the frame we work with in this file, evaluate it or paste it into your listener. (The symbols here have package prefixes so you don't have to change the package at your REPL.)

#+nil
(clim:run-frame-top-level (clim:make-application-frame 'tags-ui::tags-frame))

When McCLIM doesn't want to let go of your windows, evaluate this form. It shouldn't be a problem once we created our example command below, though.

#+nil
(progn (loop for port in climi::*all-ports*
          do (clim:destroy-port port))
       (setq climi::*all-ports* nil))

The first window!

The simplest possible frame: just an interactor pane. I added :width and :height, or the window becomes quite small with McCLIM. Evaluate the "run-frame-top-level ..." form to see it.

(define-application-frame tags-frame ()
  ()
  (:pane (make-pane 'interactor-pane :width 400 :height 300)))

Please make it go away! or, the first command

A quick command to exit the frame. define-tags-frame-comand was defined automagically by define-application-frame. *application-frame* is bound to the current frame. The command line name is "Quit Frame" (com-quit, hyphens replaced by spaces, "com-" prefix removed). Because of tab completion, you'll only have to type q <tab>, though.

Just for laughs, we display it in a menu with another name (the menu bar displays in McCLIM automatically, but not in Lispworks).

(define-tags-frame-command (com-quit-frame :name t :menu "Quit")
   ()
 (frame-exit *application-frame*))

Panes within panes

The same pane, with scroll bars around it. This showed a window of reasonable size both in McCLIM and LispWorks, so I omitted :width and :height arguments.

(define-application-frame tags-frame ()
  ()
  (:pane (scrolling () (make-pane 'interactor-pane))))

This time, display only vertical scrollbar.

(define-application-frame tags-frame ()
  ()
  (:pane (scrolling (:scroll-bar :vertical) (make-pane 'interactor-pane))))

MORE LAYOUT

define-application-frame takes either a :pane argument or arguments :panes and :layouts. We define some panes and a layout. vertically added scroll bars automatically.

(define-application-frame tags-frame ()
  ()
  (:panes
   (some-pane :application :background +blue+)
   (my-interactor :interactor))
  (:layouts
   (default
       (vertically (:height 500 :width 400)
                   some-pane
                   my-interactor))))

Tell the horizontal layout how to divide its space, and wrap the panes in other panes..

(define-application-frame tags-frame ()
  ()
  (:panes
   (some-pane :application :background +blue+)
   (my-interactor :interactor))
  (:layouts
   (default
       (horizontally (:height 400 :width 800)
                     (4/5 (spacing (:thickness 20) some-pane))
                     (+fill+ (outlining (:thickness 10) my-interactor))))))

Now, can we see something?

Let's display something. Note the use of 'display-some-pane instead of #'display-some-pane — that way, a redefinition of display-some-pane will be picked up. Also note that the application frame has grown an attribute. This is nothing unusual; frames are CLOS objects, after all.

As you can see, panes can be used both as streams and as canvases. Panes can be accessed via get-frame-pane from their frame.

(defmethod display-some-pane ((frame tags-frame) stream)
  (format stream (tags-message frame)))

(defmethod display-another-pane ((frame tags-frame) stream)
  (declare (ignore stream))
  (let ((pane (get-frame-pane *application-frame* 'another-pane)))
    (window-clear pane)
    (draw-rectangle* pane 10 10 200 150 :filled nil :line-thickness 2)
    (draw-ellipse* pane 150 100 10 0 0 30)))

(define-application-frame tags-frame ()
  ((message :initform "Message" :accessor tags-message))
  (:panes
   (some-pane :application :display-function 'display-some-pane)
   (another-pane :application :display-function 'display-another-pane)
   (my-interactor :interactor))
  (:layouts
   (default
       (vertically (:height 600 :width 800)
                     (2/3 (horizontally () some-pane another-pane))
                     (:fill my-interactor)))))

Pull-down Menus

Each item in a pull-down menu represents a command. Commands are organized in command tables (one of these is generated by default with define-application-frame). Submenus are collections of commands, so it makes sense to organize them as command tables.

First, let's define two commands that change the application frame's state. We don't use define-tags-frame-command, because we want the two commands callable only from the submenu, not from the listener. So, they don't belong in the default command table of tags-frame.

(define-command com-hello ()
  (setf (tags-message *application-frame*) "Hello there!"))

(define-command com-hi ()
  (setf (tags-message *application-frame*) "Hi there!"))

Now we make a command table for the pull-down menu and one for the menu bar.

(define-command-table menu-command-table
    :menu (("Say Hello" :command com-hello)
           ("Say Hi" :command com-hi)))

(define-command-table menubar-command-table
    :menu (("Menu" :menu menu-command-table)
           ("Quit" :command com-quit-frame)))

Finally, we make the frame use the menu bar. Clicking the commands changes the message slot of the frame; the display changes accordingly.

(define-application-frame tags-frame ()
  ((message :initform "Message" :accessor tags-message))
  (:menu-bar menubar-command-table)
  (:panes
   (some-pane :application :display-function 'display-some-pane)
   (my-interactor :interactor)
   (mouse :pointer-documentation))
  (:layouts
   (default
       (vertically (:height 500 :width 400)
                   (1/3 some-pane)
                   (:fill my-interactor)
                   mouse))))