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.
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.
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.
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 are subclasses of the Control class. There are some general methods in Control that most objects can use:
Here is a list of graphical objects you can create (these are all subclasses of Control):
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.
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: