One common way of building GUI code is to have one class for one type of widget. Slots (fields) of these classes are child widgets or some data related to the state of the widget. This approach might not be the best approach or adequate at all cases, but it is simple and quite common to many programmers.

To use this approach, we create the class as a subclass of desired widget:

(defclass custom-window (gtk-window)
  ()
  (:metaclass gobject-class))

In CL-GTK2, GObject classes and their subclasses must have gobject-class as their metaclass. The absence of :g-type-name class option specifies that this class is mapped to GObject class GtkWindow. This means that (make-instance 'custom-window) creates the GObject class GtkWindow and returns the custom-window instance as its wrapper. CL-GTK2 maintains references to created object instances, so you will always get back the instance of custom-window from all Gtk+ functions, methods, properties. This means that this works as you would expect it work.

Often, we want to customize the creation of widgets (by specifying their properties at construction time). This is possible with :default-initargs class option:

(defclass custom-window (gtk-window)
  ()
  (:metaclass gobject-class)
  (:default-initargs :title "Custom window"
                     :default-width 320
                     :default-height 240))

Specified default-initargs will be passed into GtkWindow‘s constructor.

Also it is possible to specify the :after method for initialize-instance that will initialize the the widget.

For example, we will have a GtkLabel and a GtkButton inside the custom-window packed into GtkVBox.

(defclass custom-window (gtk-window)
  ((label :initform (make-instance 'label :label "A label text")
          :reader custom-window-label)
   (button :initform (make-instance 'button :label "Click me!")
           :reader custom-window-button))
  (:metaclass gobject-class)
  (:default-initargs :title "Custom window"
                     :default-width 320
                     :default-height 240))

(defmethod initialize-instance :after
    ((window custom-window) &key &allow-other-keys)
  (connect-signal (custom-window-button window) "clicked"
                  (lambda (button)
                    (declare (ignore button))
                    (format t "Button clicked~%")))
  (let ((box (make-instance 'v-box)))
    (container-add window box)
    (box-pack-start box (custom-window-label window))
    (box-pack-start box (custom-window-button window) :expand nil)))

Now we can use the custom-window as a composite widget:

(within-main-loop
  (let ((w (make-instance 'custom-window)))
    (widget-show w)))

Screenshot

GtkGLExt binding

August 29, 2009

Thanks to the code by Vitaly Mayatskikh (http://13-49.blogspot.com/) that was adopted from cells-gtk3, I’ve added binding for GtkGLExt library. This binding allows create OpenGL contexts for Gtk+ widgets and draw on them with cl-opengl.

That’s the screenshot of a demo:

Let’s create a simple stand-alone GUI application. The program will be able to launch and show a window with “Hello, world” label.

To build GUI, we’ll use the CL-GTK2. The process will be described step by step.

Start up Slime and load CL-GTK2:

(asdf:oos 'asdf:load-op :cl-gtk2-gtk)

Create a source file (call it hello-world.lisp) with the following content:

(defpackage :hello-world
  (:use :cl :gobject :gtk)
  (:export :main :run))

(in-package :hello-world)

(defun main ()
  (within-main-loop
    (let ((w (make-instance 'gtk-window :title "Hello, world"))
          (l (make-instance 'label :label "Hello, world!")))
      (container-add w l)
      (connect-signal w "destroy" (lambda (w)
                                    (declare (ignore w))
                                    (gtk-main-quit)))
      (widget-show w))))

(defun run ()
  (main)
  (join-main-thread))

In this source file, hello-world package is defined that exports two functions: main and run. The ‘main’ function is used during development – it launches the program in a background thread (the within-main-loop macro does that) and returns immdeiately; and the ‘run’ function is used when launching the program not from Slime (it returns only when the application is closed).

The join-main-thread function waits until the Gtk+ main loop finishes. It finishes when the window is closed and the gtk-window widget receives the “destroy” signal.

We can start the program from the Slime’s REPL:

(hello-world:main)

After you enter this expression at the REPL, Gtk+ background thread will be started and the control returns to the REPL.

Now let’s create the stand-alone program that will not require Lisp compiler and Lisp libraries on the user’s machine.

First, we define the ASDF system.

To do this, we create the hello-world.asd file at the same directory where hello-world.lisp file is located with the following contents:

(defsystem :hello-world
  :name "hello-world"
  :components ((:file "hello-world"))
  :depends-on (:cl-gtk2-gtk))

This definition specifies the way of building the program: first load the cl-gtk2-gtk system and load the hello-world.lisp source file.

Usually ASDF system definitions are used to define systems that contain code of libraries and are placed (in Linux) into system-wide directory /usr/share/common-lisp/systems. But this time we define a system for a particular application and do not place it into this directory.

Now we can use cl-launch to create and executable file that contains all the Lisp code that is necessary.

First we install cl-launch. In Gentoo Linux with lisp-overlay this can be achieve with the simple command at shell prompt:

emerge dev-lisp/cl-launch

In other Linux distributions you can use the ASDF-INSTALL to install cl-launch into system-wide or used directory:

(require :asdf-install)
(asdf-install:install :cl-launch)

Next we should create a Lisp image (a “core” file) that contains the cl-gtk2-gtk system and newly created hello-world application. If we will not create the image, the all code will be loaded from source files or from FASLs. Neither is fast. For example, loading cl-gtk2-gtk from FASLs takes as much as 30 seconds on my machine. During development, this is neglibible as it is loaded only once at startup.

Enter the following command to create the Lisp image:

cl-launch.sh -s hello-world -d hello-world-image

cl-launch loads the hello-world system (from hello-world.asd file from current directory) and saves the image as the ‘hello-world-image’ file.

If several Lisp implementations are installed, you can choose which of them is used:

cl-launch.sh --lisp sbcl -s hello-world -d hello-world-image

(But at the moment, cl-gtk2-gtk supports image dumping only with SBCL)

Next we use cl-launch to create a shell script that starts the program from a newly created image:

cl-launch.sh -m hello-world-image -i '(hello-world:run)' -o hello-world

Now, we should have a ‘hello-world’ shell script created.

Let’s run it:

./hello-world

In an instant, a window appears.

The application startup time is acceptable.

Now the only things that are needed to redistribute the application are hello-world-image file and hello-world shell script (and Gtk+ libraries, of course).

But the image size is quite big. On my machine, the image produced by 64-bit Linux version of SBCL takes 64 megabytes. Of it, SBCL takes about 42 megabytes, and the rest is cl-gtk2. SBCL is a native code generating compiler, so it produces large images. Byte-code compilers (like clisp) should have smaller sizes.

If this size is too much, there is an option of using gzexe program to compress the image.

gzexe hello-world-image

On my machine, this decreases the image size produced by SBCL from 64 megabytes to 12 megabytes and somewhat increases the startup time (but it is still quite fast).

It seems that one possible way of redistributing Lisp applications is to compile them from sources on target machine.

New GBoxed type mapping

August 8, 2009

I’ve finished designing and implementing a new type mapping for GBoxed types. The code is now in git repository.

This type mapping brings features that are essential to cl-gtk2 binding.

  • GObject structures are now type-safe. You basically can not have an invalid pointer inside wrapper class for opaque structures
  • Declarative definition of foreign functions, callbacks and vtables using GBoxed structures
  • GBoxed structures are more easier to user from user code
  • Callbacks and foreign functions can now correctly modify their arguments and have changes in GBoxed instances propagate to caller. This is required e.g., for implementing GtkTreeModelInterface or using GtkTreeModel, GtkTextIter, etc.

Given these changes, binding Gtk+ and libraries it uses (Gdk, Pango, Cairo, GdkPixbuf) will be easier.

I’ve mostly finished the hard parts of mapping GBoxed types (the test code is on gboxed branch of repository). Properly mapping foreign structure types requires a change in CFFI regarding callbacks. The new type mapping for boxed types will simplify implementation of interfaces and overriding methods and will make GObject binding more type-safe and more lispy.

When the callback defined with defcallback is called, CFFI uses the generic function translate-to-foreign to convert foreign values to Lisp values. So we can create Lisp structures from C structures or create instances of wrapper classes around opaque pointers.

Operations with wrappers involve calling foreign functions with the wrapped pointer. We want to be sure that we never call foreign functions with dangling or freed pointer. Often, callbacks are called with pointers to stack-allocated structures or with pointers to structures whose lifetime is short and does not extend beyond the callback. To ensure that operations on wrappers will not result in memory corruptions, wrapper should be invalidated when callback returns.

The other aspect is that callbacks are sometimes called with pointers to structures that should be modified by callbacks. For example, to implement GObject interface, various callbacks should be provided that correspond to methods of this interface. Method gtk_tree_model_get_iter_first of GtkTreeModel interface accepts a pointer to GtkTreeIter structure that will hold result. The most intuitive way of implementing the gtk_tree_model_get_iter_first method is to have a Lisp structure passed into the callback; then the method will modify the structure; and the structure modification are reflected in the foreign structure.

This requires being able to define actions that are performed on callback arguments when the callback finishes execution. I’ve written a patch that adds such support, but CFFI developers haven’t responded to it yet. I hope that the patch will eventually get merged into CFFI, otherwise I’ll have to make workarounds.

July 24, 2009

The latest commit to the cl-gtk2’s git repository adds gtk:tree-lisp-store class. This class implements the GtkTreeModel interface and can be used as a data model for GtkTreeView (gtk:tree-view in cl-gtk2). This is addition for already existing array-list-store that can be used as a list data model for GtkTreeView.

This screenshot shows the working of tree demo (located in gtk-demo:demo-treeview-tree).

The tree-lisp-store API is pretty rough at the moment. For tree manipulation it provides a few DOM-like operations: make-tree-node, tree-node-insert-at, tree-node-remove-node-at, tree-node-children.

Introduction

July 17, 2009

This blog is intended for status updates and news of cl-gtk2 project.

Ramarren in a forum post said:

Anyway, it seems that every GTK binding has its own GObject, or at least its subset, binding. Someone should create a stand alone complete one to be used for binding of systems using it. Someone who is not me, because I don’t really understand that well how it works and I am not sure if I really want to, the entire thing is a poster child for Greenspun’s Tenth Rule. I’m just happy to see rotating triangles which don’t leak memory all over the place. I hope they don’t, anyway.

This message provoked me to devote some more time in my Gtk+ binding (in hope that it will be useful).

Anyhow, I’ve written and uploaded documentation for GObject binding.

While preparing documentation, numerous flaws in GObject binding were fixed (and more were discovered) and refactoring was done. I never learned to write documentation and it definitely needs improvement. But it is a still useful place to start. At least GObject part of cl-gtk2 should be useful.