Common Lisp/External libraries/Ltk

From Wikibooks, open books for an open world
Jump to navigation Jump to search

Ltk is a portable set of Tk bindings for Common Lisp. Tk is a graphical ‘toolkit’, a library that makes GUI building easier by providing widgets, i.e. graphical elements such as buttons an drop-down menus. Tk originated and gained great popularity as part of Tcl/Tk, the dynamic Tcl language paired with the simple to use Tk toolkit. Due to Tcl/Tk's popularity, implementations exist for most systems and Ltk is portable to any such system.

Ltk is not truly a binding in the usual sense, it is really an interface to the wish shell, a shell that interprets commands to build a window. This allows Ltk to operate via a simple stream of commands to an external process running wish, rather than the linking needed in most other GUI builders. While passing commands to the wish interpreter may produce slower programs, it makes the Ltk package very simple to build, use, extend, and debug, and it removes the need for any FFI. These features stand in stark contrast to most other GUI builders for Common Lisp.

Basic Ltk[edit | edit source]

A program that uses Ltk to build its GUI needs three main parts. It needs to create widgets, place the widgets on the window, and then associate actions for when the widgets are activated. An Ltk program typically has the following basic format:

(with-ltk ()
  (let* (;; Creating widgets
         (widget1 (make-instance 'widget))
         ...more widgets... )
    ;; Associating actions with widgets
    (bind widget1 event (lambda (evt) ...do something...))
    ;; Placing widgets on the window
    (pack widget1) ))

Creating Widgets[edit | edit source]

Ltk operation is based on the instantiation of CLOS objects. To create any particular widget, you would make and instance of that class. This instance does not need to be retained, but if it is, it can then be used to alter the properties of the widget.

Ltk includes the following common widgets:[1]

  • Buttons
  • Check boxes
  • Radio buttons
  • Text entry fields and text editor boxes
  • Listboxes
  • Menu bars
  • Frames and scrolled frames
  • Canvases and scrolled canvases

If this list seems quite short, you are not wrong. These widgets will allow you to create many useful programs, but you will find them lacking for certain tasks (such as a progress indicator for a download). Latter we will see how to add new widgets.

Laying out Widgets[edit | edit source]

With Ltk (as with Tk) once a widget is created, it is not visible until you tell wish where to put it in the window. Ltk has two geometry mangers, pack and grid. Pack treats widgets as boxes and packs them horizontally or vertically. Grid lays out widgets on a regular grid. Of the two, pack is much more commonly used.

Events and Binding[edit | edit source]

Like most graphical interfaces, all computation in Ltk is event driven in nature. This means that any event that happens inside your window, like a key press or a mouse click, is sent to your program by the windowing system. Ltk then looks up what handler functions, typically defined by you, are associated with this event and calls them. Event handlers can be established in two ways, by the command slot in many widgets and bind function. The bind function is more complex than the command slot method, but bind is a general and more flexible way to associate functions with events.

A Simple Example[edit | edit source]

(with-ltk ()
  (let* ((button (make-instance
                   'button :text "Press Me!"
                   :command (lambda ()
                              (do-msg "Hello World!") ))))
    (pack button :pady 30 :padx 30) ))

Examples[edit | edit source]

Placing Widgets[edit | edit source]

For widgets to be visible, they must be placed on the window. This is done with one of three functions: ‘place’, ‘pack’, or ‘grid’. Each is used for different circumstances. In some simple cases, widgets may be placed by specifying the placement method as a keyword argument to make-instance. We will deal with each of these methods in turn.

Placing Widgets[edit | edit source]

The most rudimentary way of placing widgets on a window is to specify an x and y position along with a width and height. The ‘place’ function will perform this task for you. However, this is typically harder than it needs to be (compared to the methods we will explore next) so its use is not advised for any purpose.

(with-ltk ()
  (let ((button (make-instance 'button :text "A Button")))
    ;; Place BUTTON at x=50, y=40
    (place button 50 40 :width 100 :height 50) ))

Pack Geometry[edit | edit source]

Grid Geometry[edit | edit source]

Here is a mock up of the video game Lights Out. We use the grid geometry manager to assemble an NxM array of buttons where each button is either ‘on’ or ‘off’. For simplicity sake we will represent on by an ‘X’ and off by no text. Whenever you click a button, it toggles the button and the four adjacent buttons between the blank and ‘X’ state. The goal is to change all of the buttons to blanks.

(defun button-on? (button)
  "Tell me if the button is 'on' or not."
  (equal (text button) "X") )

(defun toggle-button (b)
  "Toggle one button from an X to a Space or a Space to an X."
  (if (button-on? b)
      ;; To change a widget's text, ltk doesn't use CONFIGURE, but
      ;; instead you just SETF the text
      (setf (text b) " ")
      (setf (text b) "X") ))

(defun toggle-block (buttons i j n m)
  "Change the button, and the buttons neighboring it \(if they are in
bounds)."
  (toggle-button (aref buttons i j))
  (when (< (1+ i) n) (toggle-button (aref buttons (1+ i) j)))
  (when (< (1+ j) m) (toggle-button (aref buttons i (1+ j))))
  (when (>= (1- i) 0) (toggle-button (aref buttons (1- i) j)))
  (when (>= (1- j) 0) (toggle-button (aref buttons i (1- j)))) )

(defun lights-out (n m)
  "Create an N by M lights out game."
  (with-ltk ()
    (let* ( ;; Create button widgets and store them for later
           (buttons
            (coerce
             (iter (for i below (* n m))
                   (collect (make-instance 'button
                                           :text " " )))
             'vector ))
           ;; Displace the button vector into a 2D array representing
           ;; the n by m grid.  With this we can reference buttons by
           ;; a pair of indicies, i and j.
           (b-array
            (let ((array (make-array (list n m) :displaced-to buttons)))
              ;; Some iteration stuff to build a random (yet solvable)
              ;; selection of `X's and spaces.
              (iter (for i below n)
                    (iter (for j below m)
                          (when (= 1 (random 2))
                            (toggle-block array i j n m) )))
              array )))
      (iter
        ;; Iterate over buttons
        (for b in-vector buttons)
        ;; Keep a numerical index of what button we are on.
        (for tot below (* n m))
        ;; Calculate the indicies, i and j, on the grid
        (for i = (mod tot m)) (for j = (floor tot m))
        ;; Bind a button press on one of the buttons to flip its `X'
        ;; to a space (or vice verse) and toggle its neighbors as well.
        (bind b "<Button-1>"
              (let ((i i)
                    (j j) )
                (lambda (evt)
                  (declare (ignore evt))
                  (toggle-block b-array j i n m)
                  ;; Check if you have solved the problem
                  (when (iter (for button in-vector buttons)
                              (never (button-on? button)) )
                    (do-msg "You have won!!!" "You a winner") ))))
        ;; Here, we will use the GRID geometry manager.  This makes
        ;; sense, we are building a grid...
        (grid b i j) ))))

Modifying Widgets with ‘configure[edit | edit source]

The function configure is used to change various attributes of a widget such as color and font.

Binding[edit | edit source]

Binding, in the Tk sense, is associating actions with events. We do this by calling the function bind which modifies a widgets list of event handlers.

The Canvas Widget[edit | edit source]

The canvas widget allows you to draw arbitrary vector graphics.

Modifying canvas items[edit | edit source]

  • itemconfigure
  • itemmove
  • itembind
  • itemdelete
  • itemlower’ and ‘itemraise

Bitmaps[edit | edit source]

Ltk can display bitmaps in widgets. By default, Tk (and thus Ltk) supports GIF, PBM, and PPM images, all of which are rarely used these days. In order to use modern formats like PNG and JPEG you will need to install the libimg extension library for Tcl/Tk (e.g. in Ubuntu, you need to install the libtk-img package) and require the package before you use it (see the code below).

The following piece of code creates an Ltk window with a canvas object in it. It then loads the image specified by the pathname filename and places it on the canvas. Anything outside of the 300x300 pixel canvas is merely not drawn.

(defun display-image (filename)
  (with-ltk ()
    (format-wish "package require Img")
    (let* ((img (make-image))
           (c (make-instance 'canvas :height 300 :width 300)) )
      ;; Pack the canvas
      (pack c)
      ;; Load the image from the file
      (image-load img filename)
      ;; Draw the image on the canvas
      (create-image c 0 0 :image img) )))

The return value of create-image is a canvas item designator. It could be saved so the image may be modified later (i.e. moved around like in the canvas item modification examples).

Bitmaps are not limited to canvas widgets. They can also be placed on buttons instead of text.

(defun click-change (filename1 filename2)
  "Make a button that switches between two different images whenever pressed."
  (with-ltk ()
    (format-wish "package require Img")
    (let* ((img (make-image))
           ;; We specify text here, but it is overridden by the :image keyword
           (b (make-instance 'button :text "hello!" :image img
                             :command
                             (let ((imgs (make-circular-list 2 :initial-element filename2)))
                               (setf (car imgs) filename1)
                               (lambda ()
                                 (setf imgs (cdr imgs))
                                 (image-load img (car imgs)) )))))
      ;; Load the image from the file
      (image-load img filename1)
      ;; Pack the button
      (pack b) )))

Threads, lengthy calculations and ‘process-events[edit | edit source]

The basic structure of an event driven system might look something like the following:

(loop
  (let ((event (get-next-event)))
    (case (name-of event)
      (:window-resize (funcall *window-resize-handler* event))
      (:button-click (funcall *button-click-handler* event))
      (:keypress (funcall *keypress-handler* event))
      ... )))

This is called an event loop, or event pump. The purpose of the event loop is to map the actions that a user performs onto the functions defined to handle them. Any action the user makes causes new event to be inserted in a queue. The function get-next-event pulls the next event out of the queue and the case statement dispatches it to the correct location. If a handler doesn't return in a timely manner, this can lead to an unresponsive program.

In order to use Ltk for more applications, we need methods of avoiding this unresponsive behavior, so we can do things like have cancel button to stop a lengthy calculation. There are several methods of dealing with this issue; we will discuss three:

  1. Set up a timer event in the Tk event loop.
  2. Periodically pause your calculations to explicitly process events via process-events.
  3. Use the Lisp implementations internal threads to separate computations from the thread running the event loop. Of course this only works on threaded Lisp implementations.

Timers and ‘after[edit | edit source]

In some cases, the handler is slow to return because you are waiting for something to happen, not computing at all. In these cases, the correct way for the handler to behave is to return immediately, but not before telling the event loop to retry at some later time. This is done by inserting a timer event which will come off the queue after your specified delay. A timer event is set up by using the after function.

process-events’ and ‘after-idle[edit | edit source]

In order to use after, it was necessary that our task was only waiting (in our example above, waiting for input on a socket) and easily suspended and resumed. If this is not the case, then you have another option available, process-events. With process-event, instead of inserting a new event and allowing LTK to process other event in the mean time, it asks the main loop to process all events currently waiting in the queue then return and allow your code to continue.

(defun count-down (n)
  (with-ltk ()
    (let* ((cvs (make-instance 'canvas :height 400 :width 400))
           (text (create-text cvs 100 100 "Count down: ")) )
      (pack cvs)
      (force-focus cvs)
      (bind cvs "<q>"
            (lambda (evt)
              (declare (ignore evt))
              (return-from count-down) ))
      (iter (for i from n downto 0)
            (itemconfigure cvs text
                           :text (format nil "Count down: ~A" i) )
            ;; We must explicitly process events since the event loop isn't
            ;; running yet (not until after the body of WITH-LTK)
            (process-events)
            ;; Pretend this sleep command is actually doing some work...
            (sleep 1/100) ))))

Similarly, after-idle behaves like after except it gets processed once the event queue is flushed but no intrinsic delay. This has the basic behavior of process-events but instead of resuming in the middle of a function, it triggers a new function call. It is useful if your code is not just waiting, but is easily suspended and resumed.

Threaded Ltk applications[edit | edit source]

Using threads is by far the most powerful solution to this problem. However, with that power, there is a great deal of added complexity. All of the issues with threaded programs come up. There is an added catch with Ltk, though. The with-ltk form sets up a special environment for your Ltk commands. However, dynamic variables by design are not lexically captured. This means that your function that is bound to an event is not necessarily executed in an environment where the dynamic Ltk environment is set up (although it seems like it should be). Although this comes up in single threaded Ltk programs as well, this happens all the time in multithreaded programs. The solution is to force the function to lexically close over the desired environment and remake the environment inside the function.

Remote Ltk sessions[edit | edit source]

Tk (and hence Ltk) provides for remote sessions. In a remote session, a program is run on a server but the display happens elsewhere, on a client computer. The display information is transmitted as a series of wish commands, so efficiency of such a system is not bad. To X11 users, this is quite similar to X11 forwarding.

Ltk Internals[edit | edit source]

Extending Ltk[edit | edit source]

Notes[edit | edit source]

  1. For the up to date and complete list of supported widgets check the ltk.lisp file.

Further reading[edit | edit source]