CS 421: Programming Languages
LablTK Tutorial

Introduction

[top]

OCaml comes with two graphical interface packages: a lowlevel graphics interface, discussed in the O'Reilly Objective Caml Book; and the higher level Tk graphics interface. (For those of you who are really interested, LablGtk exists as well, as does a raw X library for OCaml.)

Tk is a widget toolkit that features buttons, text boxes, entries, lists, menus, toplevel windows, etc. If you already know a little about Tk, either from working with the original Tcl/Tk or from working with some other language binding to Tk, that will help here. (There exist Tk bindings for Perl, Python, Pike, Ruby, etc. It's a popular toolkit.)

OCaml's Tk bindings comes in two flavors: the old style bindings, referred to as OCamlTk; and the new style bindings with more recent versions (at least 3.03 and on), referred to as LablTk. We will be discussing LablTk in this document (and if you're doing a project for this semester and need to use Tk, you will be using LablTk). LablTk introduces some interesting OCaml language extensions: labels, optional arguments, and variant types. I group these all under 'Label Extensions'. (OCaml has provisions for extending its own syntax on the fly. You can find out more from the OCaml Manual.)

First, we will talk about these language extensions used in LablTk. Then we will go over some of the key concepts in working with any Tk kit. Secondly, we will go through a few examples. In the final section we will include a link to a tarball of LablTk examples you can go through yourselves.

Label Extensions

[top]

Variant Types

[top]

Variant types are like ML datatypes, except that they can be used without a preceding type declaration. In other words, all you have to do is mention them and they exist. Variant tags, just like normal type constructors, must always be capitalized, but they must additionally be prefixed by a backquote:

let one = `END
You only need to know how to use variant types to work with LablTk.

Labeled Arguments

[top]

Labels, or labeled arguments, allow us to consistently label arguments when we define and when we use functions. This means we can use labeled arguments in any order we wish (though any unlabeled arguments must appear in the correct order). Functions can have both labeled and unlabeled arguments. Labels also do interesting things to the type checking system, as you can imagine.

Here is the type of a function with a mix of labeled and unlabeled arguments:

StdLabels.String.sub;;
-: string -> pos:int -> len:int -> string = <fun>
Here, the function sub expects three arguments and returns a final string: the first is anonymous, and the next two are called pos and len. You can call this function in two different ways to take "Hell" out of "Hello":
String.sub "Hello" ~pos:0 ~len:4
or
String.sub "Hello" ~len:4 ~pos:0
Note that the position of the anonymous string argument demands that we put that argument first; but String.sub could just have easily defined the anonymous argument to go last instead.

Optional Arguments

[top]

Labeled arguments that look like pos:int in the type of the function are required. However, labeled arguments with default values are optional arguments. The type of a function with optional arguments may look something like this:

:- ?pos:int -> length:int -> string -> string
Here, pos is an optional argument.

As you've probably already figured out, you don't have to supply optional arguments.

For more information on label extensions, refer to Appendix B of the OCaml Manual.

Tk Concepts

[top]

A widget is a GUI element, such as a button or a text entry, or even an application window. Tk is a library of such widgets. Tk has been ported to almost every operating system, including all Unix-type, Windows-type, and Mac-type systems; thus Tk gives you the ability to create very portable graphical interfaces for your applications.

Every program that uses Tk starts out with a root window, often called either 'top' or 'main' in many applications. The root will be the parent or grandparent or great-grandparent etc. (you get the idea) of all the other widgets you create later, except for toplevel windows.

When a widget P is the immediate parent of another widget w, and w is not a window, then w will be drawn inside of P. In order for w to actually appear inside P, w has to be packed. This just means that w is placed inside P using a very simple widget layout algorithm. (More complicated ones, such as grid, are available; you can read about this and other things in any Tk guide.)

Widgets have various attributes that you can set to change their appearance and behaviour. There are some attributes that are common to every widget, such as background (background color); and some that are common to a group of widgets, like text (labels, buttons, etc). Attributes can be configured on the fly. In OCaml, attributes are associated with optional labeled arguments.

You can bind actions (in the case of OCaml, actions are functions) to widgets and/or events. Some widgets have 'command' attributes that are called for a specific widget event; for instance, a button has a command that is called whenever it is pressed. Events occur when certain things happen, like gaining/losing focus, key presses/releases, mouse button presses/releases, etc.

Running LablTk Apps

[top]

Running a LablTk program requires setting the paths to necessary libraries and so on that are not usually set. On Unix, for this reason, the labltk command is provided. In all respects it acts just like ocaml, but with the proper library include paths set, Tk stuff loaded, and so on. On Windows, the labltk top-level interpreter does not exist. However, you may run the program file this way:

ocaml -I +labltk labltk.cma ...

Compiling With LablTk

[top]

Since LablTk isn't on the usual include/library path of the ocamlc or ocamlopt compilers, we need to add it with the following flag:

ocamlc -I +labltk labltk.cma ...

LablTk Documentation

[top]

OCaml comes with its own module browser, called ocamlbrowser. In its recent versions, ocamlbrowser looks an awful lot like the SmallTalk browser. If you're working from home, you either need a ssh-tunneled X-connection to use ocamlbrowser in the labs, or -- if you're running Windows and don't have an X server for it -- use your own installation of OCaml. Unfortunately, labltk doesn't automatically load into ocamlbrowser. You'll need to edit the path in this manner:

  1. Pull down the Modules menu and select the Path editor... item.
  2. A new window should come up called "Edit Load Path". There is a Load path list on the right of the window. Double-click on the first entry to change the Directories list on the left of the window to list the directories in OCaml's system library directory.
  3. You should see the Path text entry along the top change to something like blah/blah/blah/ocaml-3.04/lib/ocaml.
  4. In the Directories list, select the labltk item with ONE click (we don't want to change into that directory, just select it).
  5. Press the Add to Path button below the Directories list.
  6. Now the labltk directory has been added to the search path. Click Okay to close the dialog.

Now the Tk modules have all been loaded into ocamlbrowser. For instance, we can browse the Button module. Double-clicking on one of the Tk.blah types, such as Tk.color, will cross-reference you to the type definition.

A Simple Example

[top]

[The GCD app window]
We are going to write a very simple graphical application that takes two numbers and displays their greatest common divisor. Hopefully, this will illustrate the basic stuff you need to know to get your project done. The code is here, and a walkthrough follows:
open Tk
Here we open the Tk module for convenience, so we don't have to keep typing Tk.blah all the time.
let rec gcd n m = 
  if n = 0 then
    m
  else if n >= m then gcd (n - m) m
       else gcd m n;;
This is just the code for computing the greatest common divisor of two integers. It's usually a good idea to put all your non-GUI code somewhere apart from your GUI code.
let compute entry_n entry_m label_r = 
  fun () ->
compute actually returns a callback function whose environment contains the values of the arguments given to compute. We'll see this in action later on.
    let n = int_of_string(Entry.get entry_n) in
    let m = int_of_string(Entry.get entry_m) in
Entry.get returns what's in a text entry as a string.
    let result = gcd n m in
      Label.configure label_r ~text:(string_of_int result);;
We can use the configure methods of various widget modules to change widget attributes on the fly.
let root = openTk() in
This initializes the Tk library and our root window.
let top_frame = Frame.create root in
Here we create a Frame widget whose parent is root. This is typically how all Tk widgets are created -- with the first argument being the parent of the widget we're about to create.
let gcd_begin = Label.create top_frame ~text:"gcd(" in
Now we've created a label widget. Labels just hold non-editable text; you can change what a label shows by configuring it on the fly with Label.configure.
let n = Entry.create top_frame ~width:4 ~relief:`Sunken in
let m = Entry.create top_frame ~width:4 ~relief:`Sunken in
Now we create two entries for our two numbers. Note that we see the first use of variant types here with `Sunken.
let gcd_end = Label.create top_frame ~text:")=" in
let result = Label.create top_frame in
result is going to be used to display the gcd of what's in n and m.
let button = Button.create root 
                ~text: "Compute" 
                ~command: (compute n m result) in
Now we call compute to "load up" a closure with our two entries and the result display label, and bind the resulting closure to button.
    pack [top_frame] ~side:`Top ~expand:true ~fill:`Both ;
Now we begin a lot of imperative stuff. First we pack our top_frame. top_frame's parent was root, so we're telling it to display itself on the `Top side of the root window. We also tell the pack layout algorithm that top_frame is allowed expand when the window is resized, filling both the X and Y directions (across and vertical).

Note that pack takes a list of widgets to pack.

    pack [gcd_begin] ~side:`Left ~anchor:`E ;
    pack [n; m] ~side: `Left ~expand:true ~fill:`X ;
    pack [gcd_end; result] ~side: `Left ~anchor: `W ;
    pack [button] ~side: `Bottom ~expand:true ~fill:`X ;
In this way, we can pack multiple widgets at a time, as long as they're the same type.
    mainLoop();;
We call this particular function to start the event processing loop. Now we can run labltk on the .ml file and ask it for the gcd of 12 and 34.

GCD of 12 and 34

More Examples

[top]

Here are even more source examples for you to play with and look at, usually fairly heavily commented. These are taken from a somewhat hidden directory in the OCaml 3.04 distribution:

Braindead Does-nothing Hello App
A Calculator
A Clock
Eyes that Follow You
Demo of Almost All Tk Widgets
Tetris, which needs this background.

Contents
Introduction
Label Extensions
   Variant Types
   Labeled Arguments
   Optional Arguments
Tk Concepts
Running LablTk Apps
Compiling With LablTk
LablTk Documentation
A Simple Example
More Examples