CL-TK

CL-TK provides a very basic bridge to Tcl/Tk. It will fire up a Tk window, handle events, and allow you to evaluate Tcl code.

Contents

News

25-11-2011: Version 0.2: Fixes several small issues in escaping and backend spawning.

16-03-2009: Version 0.1: The first public release of this library.

Download and installation

This library is released under a zlib-style license, which approximately means you can use the code in whatever way you like, except for passing it off as your own or releasing a modified version without indication that it is not the original. See the LICENSE file in the distribution.

CL-TK requires Tcl/Tk 8.5 to be installed. It also has an optional dependence on CFFI (on platforms other than Allegro Common Lisp). Not having CFFI installed means the FFI backend is not available, and you have to use the wish based backend, which is somewhat slower.

The current release of CL-TK can be downloaded from http://marijnhaverbeke.nl/cl-tk/cl-tk.tgz, or installed with asdf-install.

A git repository with the most recent changes can be checked out with:

> git clone http://marijnhaverbeke.nl/git/cl-tk

The code is also available on github.

Support

There is currently no mailing list, so mail me directly. I'll set something up when there is enough traffic.

Examples

To use CL-TK, a basic understanding of Tcl and Tk is indispensable. If you are completely new to those, you might want to read up on them first.

When working with Tk, the *tk* special variable has to be bound to a back-end object. The simplest way to do this is:

(toplevel-tk)

This will automatically create a suitable Tk back-end. See below for details on what these back-ends are.

You should now see an empty window. Next, we'll want to put something into this window:

(tcl "pack [ttk::label .label -text {Hello world}]")

Or, if you don't want to pass the whole command as one big string:

(tcl "pack" (tcl[ "ttk::label" ".label" :text "Hello world"))

See the reference for information about the way in which arguments to tcl and tcl[ are treated.

Depending on your platform and back-end, it's possible that you don't see your label in the window yet. That's because it is not reacting to events. mainloop will run Tk's event pump until the window is closed.

(mainloop)

Another capability that this library provides is running Lisp code when Tk events occur. For example:

(with-tk ()
  (tcl "pack" (tcl[ "ttk::button" ".exit" :text "Exit" :command (event-handler #'destroy)))
  (mainloop))

with-tk locally binds *tk*. The destroy function (predictably) destroys the Tk instance. But what does event-handler do? Tk event handlers are pieces of Tcl code, not Lisp functions. event-handler returns a piece of code that, when executed, will notify the CL-TK event pump, which will call the function that was passed to event-handler. More about this below.

Reference

Back-ends

There are two types of back-ends. It is never obligatory to specify a back-end, and when you don't the sensible thing happens: The system tries to use the faster FFI back-end, and falls back on wish when that fails.

class ffi-tk

Only available when you either have CFFI installed, or are using Allegro Common Lisp. A back-end that tries to load the Tcl and Tk shared libraries, and talk to them through a foreign-function interface.

class wish-tk

This back-end starts a wish (windowing shell) process, and communicates with that through its standard in and output channels. You can specify a :binary initarg to specify the name (and optionally path) of the executable to run. This can be a single string or pathname, or a list of strings and pathnames.

variable *tk*

Holds the 'current' back-end. This is used by most of the function in the library. Bind with toplevel-tk and with-tk.

function toplevel-tk (&optional back-end)

Set *tk* to a back-end. When no argument is given, the library tries to create a suitable back-end.

macro with-tk ((&optional back-end) &body body)

Execute body with *tk* bound to a back-end. Wraps an unwind-protect that makes sure the back-end is destroyed when the body exits.

function destroy ()

Destroy the current back-end. This will close the top-level window, and dispose of any resources that were held.

function alive-p ()

Test whether the current back-end is still alive. After the user manually closes or kills the top-level window, this will return nil.

Tcl evaluation

The main thing to do with this library is sending Tcl commands to an interpreter, in order to make some windowing interface do what you want. The following commands try to make this easy.

function tcl (command &rest args)

Run a piece of Tcl code in the current back-end. The first argument is sent as-is, the rest are converted to strings by the rules below, separated by spaces, and appended to the first argument.

string
Strings simply escaped.
number
Converted to a string as per princ-to-string.
keyword
Lowercased and prepended by a dash, so that named arguments to Tcl operators can look like Lisp keyword arguments.
list
Treated as if the elements of the list occurred separatly in the argument list.
Tcl literal
These are created with the lit function. They are inserted into the command as they are, without any escaping.

So, for example, the following packs the .foo.scroll widget into the right side of its container.

(tcl "pack" ".foo.scroll" :side "right" :expand 1 :fill "y")

The return value of tcl is the string produced by evaluating the given expression. If Tcl signals an error, a tcl-error condition is raised.

function lit (string)

Wraps the given string to be inserted literally into a Tcl command.

function tcl[ (command &rest args)

Like tcl, but instead of sending the result to the back-end, it is wrapped in '[' and ']' and returned as a literal, so that it can be inserted into another command. For example...

(tcl "grid" (tcl[ "ttk::frame" ".baz" :padding 10) :column 0 :row 1 :sticky "news")

function tcl{ (command &rest args)

Just like tcl[, but wraps its result in '{' and '}'.

function tcl-escape (string)

Escape a string so that it can be passed as an argument to a Tcl operator. Disables all $ and [ ] magic by adding backslashes.

Event handling

A Tcl application, or at least its interaction with Lisp (depending on your back-end) only runs when its event pump is running.

function doevent (&optional block)

Handle one event. When block is true, this will wait until an event becomes available, otherwise it will return immediately. Returns a boolean indicating whether an event was handled (always true in blocking mode).

function doevents ()

Handle events (without blocking) until no more events are available.

function mainloop ()

Handle events (blocking) until the current back-end is no longer alive.

CL-TK allows you to associate 'magic' Tcl commands with pieces of Lisp code. These commands are then registered as Tcl event handlers, and the CL-TK message pump detects when they are fired, running your Lisp code.

function event-handler (function &optional fields)

Return a string containing a Tcl command associated with calling the given function. When fields is given (usually you don't need it), it should be a list of characters, which will be put into the Tcl command as %c markers (see bind), with their substitutions passed as arguments to the function. Returns the handler ID (see below) as a second value. For example:

(tcl "ttk:button" :text "Exit" :command (event-handler #'destroy))

macro event-handler* (&body body)

Wraps the given body in a lambda, and passes it to event-handler.

macro bind-event (tag event (&rest fields) &body body)

Binds the given event for the given tag to a function containing the macro body. If fields is given, it should hold (variable #\c) pairs, where the second element is the character to put into the Tcl event handler, and the first element is the variable to which the substitution of this character will be bound in the body. Returns an event ID.

(bind-event "." "<1>" ((x #\x) (y #\y))
  (format t "You clicked at ~a,~a.~%" x y))

The tricky thing with associating Lisp code with Tcl strings is that the two systems have separate garbage collectors, and as such it is unclear how long the Lisp closures should live. In toy applications, or applications that don't dynamically register events, you can ignore this fact. But usually, you should take care to clean up your handlers, or you are leaking memory.

function unregister-event (id)

Release the closure stored in the current back-end under the given event ID.

function event-snapshot ()

Retrieve a 'snapshot' of the current event handler set. At a later time, you can use clear-events to release all event handlers registered after this snapshot was taken.

function clear-events (snapshot)

Release all events registered since the given snapshot.

macro with-local-events (&body body)

Register an event snapshot before running the macro body, and unwind-protect the body to release that snapshot when it unwinds.

Window names

Tk requires you to come up with a unique 'window name' (as in .main.foo.label20) for every widget you create. This gets pretty tiresome, but these functions try to ease the pain a little.

variable *wname*

If you decide to use CL-TK's wname facilities, you'll use this to store the window name of the widget you're currently populating.

macro with-wname (wname &body body)

Binds *wname* to the given value in body.

function wname (name &optional id)

Create a new window name, based on *wname*. If *wname* is .foo, (wname "bar") gives you .foo.bar, and (wname "bar" 10) gives .foo.bar10.

function wname-cons (name base)

Extend a base window name. For example, (wname-cons "button" ".frame") yields ".frame.button".

function wname-car (wname)

Take the inner element of a window name.

function wname-cdr (wname)

Return the outer element of window name. For example (wname-cdr ".dialog.top.scroll") gives .dialog.top.

Errors

All errors raised by CL-TK have the type tcl-error, which is a subclass of simple-error without extra slots.