wxserpent = serpent + wxWidgets

Roger B. Dannenberg

Introduction

Serpent has no built-in graphics or user interface classes, but it has been interfaced to parts of the wxWidgets library. Unlike wxPython, wxserpent is not intended to include all of wxWidgets, so there is only a very limited subset available. However, this is enough to construct simple but effective graphical interfaces. For example, wxserpent was used to create a graphical editor for audio synthesis algorithms, including pop-up help text, icons with hot-spots, icons that drag, lines that snap into place, menus, and file browsers.

This document is likely to be incomplete, so you are encouraged to read the sources for details, constants, and recent additions.

When you are getting started, you should run wxserpent in the serpent/wxs directory. The init.srp file here will then be loaded automatically at startup and create a little window where you can type text and have it evaluated by Serpent. You can even type

load "filename"

to load a file. You can of course read init.srp and modify it or install your own version here or in another directory. Serpent will read from the current directory first, and then use the search path.

Two-level Implementation

Level 1

wxserpent interfaces to wxWidgets through a fairly small set of C functions. Because wxWidget objects are not primitives in serpent (like files for example), the wxWidget objects are accessed via "handles" that are simply integers. For example, a window might be designated by the integer 1, and a menu might be named by integer 27. As you can imagine, the C interface code has a simple table that maps integers to wxWindows objects and then calls methods on these objects.

This is the low-level, direct interface to wxWindows, and you can use it if you like, but one of the problems is that when wxWindows events occur (e.g. the user selects a menu item), a single serpent method, wxs_handler, is called to handle the event. It is up to this function to figure out which graphical object, based on the integer identifier, generated the event, and what to do about it.

Level 2

The standard way to impose some structure on a user interface system like this is to let objects represent things like menus, windows, sliders, number boxes, etc. In this scheme, the integer handles are hidden, and serpent maintains a table to map integers, not to wxWidget objects, but to Serpent objects that serve as representations of the graphical objects.

When a menu item is selected, the wxs_handler method finds the corresponding Serpent object and invokes the handle() method. Thus, if you use this level, implemented in wxslib/wxserpent.srp, creating a graphical object is just a matter of instantiating a class.

Example

Let's go through a simple example. Suppose you want a slider control. Your serpent program should say something like:

// parameters are: parent, min, max, initial, x, y, w, h
myslider = Slider(default_window.id, 0, 100, 50, 10, 30, 200, 20)

The first parameter tells where to put the slider: this must be the integer "handle" for a window. When wxserpent initializes, there is a default window created automatically, and serpent creates a corresponding sepent object called default_window. The id field contains the integer handle. The next parameters specify the minimum, maximum, and initial values for the slider. The last four parameters are coordinates: left, top, width, and height. Coordinates are in integer pixels measured from the upper left corner of the window.

Now we have a slider, and you should see a slider in the window. How do we get values from the slider? One way is to start over and make a subclass of Slider where we override the handle method, but this is so common, there's an easier way. Let's look at some code to get a thorough understanding. Here's wxs_handler:

def wxs_handler(id, event, x, y):
    var obj = control_map[id]
    if obj:
        if event == WXS_PAINT:
            obj.paint()
        else:
            obj.handle(obj, event, x, y)

When an event occurs (e.g. when the slider button is moved), wxs_handler is called with the number of the slider, a code indicating the type of event, and the value of the slider as x. (The y parameter is not used in slider update events.) The wxs_handler function maps id to a Serpent object. If the event is a paint request, which is valid for some objects, the paint message is sent to the object. Otherwise, the parameters are forwarded to the handle method of the object.

Most objects, including Sliders, inherit the handle method from their superclass, Control. Here is the method:

    def handle(obj, event, x, y):
        if method:
            if target:
                send(target, method, obj, event, x, y)
            else:
                funcall(method, obj, event, x, y)
        elif parent:
            control_map[parent].handle(obj, event, x, y)

Every Control has two fields used for message handling: method and target. If method is set, then this method will be called to handle the event. If target is set to a Serpent object, then method will be sent to this target. If target is not set, then method is regarded as a global function, and the function is called. If method is not set, there is no handler defined for this object, so the event is sent to the parent of the object. In the case of our slider, recall that the parent is the window containing the slider. This forward-to-parent mechanism could be used if a parent has many controls as children and wants to handle all of them in one place (although in practice, it is simple to direct each child object's events to a designated target.)

OK, so now we're ready to handle some slider events. Try this:

myslider.method = 'print_slider_value'
def print_slider_value(obj, event, x, y):
    display "print_slider_value", obj, event, x, y

Notice that this is a global function, not a method. It will be called because myslider.target is nil (the default). Also notice that the handler gets called with 4 parameters even though the y parameter will never be used. When you move the slider, you should see text that tells you the value.

You now know just about everything you need to know except for the details about what objects you can create and how to create a timer. These are described in the next section. You should also be warned about error-handling. It's not pretty. See below.

Graphical Objects

Graphical objects are subclasses of the Control class. There are some general methods in Control that most objects can use:

delete()
Delete the wxWidgets control this object represents. You should not use a Control after calling delete().
get_width()
Get the width in pixels.
get_height():
Get the height in pixels.
set_size(width, height)
Set the size in pixels.
set_width(w)
Set the width in pixels.
set_height(w)
Set the height in pixels.
set_position(x, y)
Set the position in pixels, relative to the upper left of the parent window.
set_font(size, family, style, weight, underline, name)
Set font parameters (for text objects only)
set_color(color)
Set the color of an object. Colors are designated by strings, e.g. "RED". See colors.txt for a list of available colors and their interpretation. Also, see colorchart.png for a color chart (image).
set_value(x)
Set the value as an integer (works only with Gauge and Slider)
set_string(string)
Set the value as a string (works only with Textctrl, Multitext, and Statictext)
value()
Get the value of this control (not actually defined in Control, but defined by most subclasses).
 

Here is a list of graphical objects you can create (these are all subclasses of Control):

Button(parent, label, x, y, w, h)
Creates a labeled button. label is a string.
Choice(parent, x, y, w, h)
Creates a choice box. Use the append(label) method to add choices (strings) to the choice box.
Checkbox(parent, label, x, y, w, h)
Similar to Button, but this creates a two-state check box.
Combobox(parent, x, y, w, h)
Similar to a Choice, but you can type in choices as well as select them. Use append(label) to add choices. The combo box event reports the string value through the 4th ("y") parameter.
Listbox(parent, x, y, w, h)
Creates a list box. Use the append(label) method to add to the list.
Multitext(parent, x, y, w, h)
Creates a multi-line editable text box. The value() method returns the text.
Textctrl(parent, text, x, y, w, h)
Creates a one-line text box. The value() method returns the text.
Spinctrl(parent, min, max, initial, x, y, w, h)
Creates a spin control.
Statictext(parent, text, x, y, w, h)
Creates non-editable text.
Radiobox(parent, label, items, x, y, w, h)
Creates a radio box, where label is a text label and items is an array of strings to label the buttons.
Panel(parent, x, y, w, h)
Creates a panel, essentially a sub-window with its own local coordinate system.
Slider(parent, min, max, initial, x, y, w, h)
Creates a horizontal slider.
Gauge(parent, range, x, y, w, h)
Creates a gauge, used to display values or progress as a horizontal bar. Use set_value(x) to change the display. Values of range and x are integer.
Menu(parent, label)
Add a menu with the given (string) label. Use the item(label, help, checkable) method to add menu items with the given (string) label, the associated help string, and the checkable attribute (use 0 or 1, do not use nil or true). Use the separator() method to insert a separator. Use the is_checked(n) method to see if the nth menu item is checked.
Canvas(parent, x, y, w, h)
Create a canvas, a drawable region. There are many methods, but first a word about how this all works. To create a Canvas, you must make a subclass and define the paint() method, which may call any of the following methods. Except for refresh, the following methods can only be called after a paint() method begins and before it ends. The affected canvas will be the one who's paint method is active. You cannot simply call draw_line(0, 0, 100, 100) and expect to see a line on your canvas. Instead, you must first call refresh(erase). At some point in the future, paint() will be called, and then you will have the opportunity to draw on your canvas.
draw_line(x, y, x2, y2)
draw a line from x,y to x2,y2
draw_point(x, y)
draw a 1-pixel point at x,y
draw_rectangle(x, y, w, h)
draw a rectangle
draw_text(x, y, text)
draw a string
draw_ellipse(x, y, w, h)
draw an ellipse
draw_polygon(points)
draw a closed polygon. points are an array: [x0, y0, x1, y1, x2, y2, ...]
set_pen_color(color)
color is a string color name (see set_color() above). Future lines, points, etc. are in this color.
set_pen_rgb(r, g, b)
set the color using integer RGB values from 0 to 255.
set_pen_width(w)
set the pen width for lines, rectangles, etc.
set_brush_color(color)
set the color (a string) with which to fill rectangles, ellipses, etc. See set_color() above.
set_brush_rgb(r, g, b)
specify the brush color with integer RGB values from 0 to 255.
clear(color)
fill the entire canvas with a solid color
refresh(erase)
notify wxWidgets that a change has occurred requiring a graphical update. If erase is true, the canvas will be erased first.
Window(title, x, y, w, h)
Creates a top-level window with title (a string) as title.

Timer

In wxserpent, the graphic interface does not really become operational until you finish loading the initial program. Recall that the default startup action is to load init.srp which may load other files. After loading, control is passed to the wxWindows library. Depending on how graphical objects were created and initialized, various functions and methods will be called in response to user actions. If you want other processing to take place, your only option is to have wxWindows call a function periodically. This can be started by calling wxs_timer_start, as described below.

wxs_timer_start(interval, function)
Starts calling the function named by the function parameter, a symbol, every interval ticks. Under Windows (at least), a tick is 1/100 seconds. The function should be defined and should take no parameters.

Error Handling

Error handling in Serpent is generally handled by a simple debugger that displays some information and prompts the user for a command. In wxserpent, we do not have a command line and output is directed to a window, so when the debugger waits for input, what happens? Blocking input in wxserpent is implemented by calling a pop-up dialog box to retrieve type-in. This usually puts a dialog box right in front of your application, which gets in your way, and if you are in the debugger, typing something to the dialog box will only encourage it to ask you another question.

Aside from a few debugging commands that might be useful, you basically have two choices: