Roses
The wxPython Usage within Roses
As I discussed in my main Roses page, one of my
motivations was to learn something about wxPython, a Python interface
to the wxWidgets graphics package. My interest was piqued sometime after
writing my first Python Roses variant that used the Tkinter package when
I read praise like "Tkinter is dead, Java is dead, wxPython rules! That's all
there is to say."
A few years later I had time to check it out and realized that much of
the praise was due to the number of tools available in wxPython and not
so much for the ease of use or the mindset of someone who spent most
of his career in Unix kernel networking protocols.
Using wxPython was not easy, was not intuitive, and not all that
pleasant, at least at first. This it shares with every other graphics
package I've used except for pre-raster, pre-windowing systems when I could write my
own display lists. Eventually I began to figure out the system, I'm
sure people more experienced than I would have had much less trouble.
This is especially true for applications that have the typical menu
and status bar design instead of the uncommon layout I use in Roses.
One thing that is very useful is the demo system manifested
by wxPython-src-2.8.1.1/wxPython/demo/demo.py. It serves as
a good "card catalog" of the various widgets that are available
and as a source of boilerplate code to adapt into your application.
A number of demo programs have bugs or crash, I think the problems are
more with the demos than the library as the demos tend to demonstrate the
widget with as little code as possible. Some of the failures may work
okay on other platforms.
The book wxPython in
Action
is well regarded with the publisher's marketing page saying it's "a complete
guide to the wxPython toolkit." It's not. If it were, it would be three
times longer. However, it is a good introduction and fits well with the
demo programs and online documentation.
General Structure of a wxPython (or any wxWidgets Application)
Graphical toolkits generally produce tree structures of nested widgets,
and that's what we have here. The uppermost object is called a frame. While
you can put various objects that you can see directly on the frame, that
normally isn't done. Instead, you create panel objects and put things on
them. When in doubt, making a new panel seems to help.
Widgets and other objects used in wxRoses
The visible widgets used in wxRoses include:
- StaticBox
This creates a labeled box in which other objects are placed.
An early version of wxRoses used one in each corner, the current version
uses five stacked vertically in a single panel on the side.
- Button
This displays a labeled button. When one is clicked, an event is triggered
and that's used to call application code to handle the button press.
These are used in the "Command" box in the upper right.
- StaticText
This just shows a handful of text, in this application, just a word to make
a label. There isn't much you can do with these, you can't even select
the text and copy it somewhere. You can change the text under program
control, but the user can't change it directly.
- SpinCtrl
This has both a editable area that normally displays a number and a pair
of arrows that you can click on to increase and decrease the value.
- SpinPanel
This is a widget I created. It is a subclass of Panel, and
places a StaticText widget and a SpinCtrl widget side-by-side.
These are used for all the numeric controls in wxRoses.
- CheckBox
This has a checkbox you can click on to toggle a checkmark and some
static text to describe it.
- cs.ColourSelect
This is a rather nice color, err, colour selection tool. I'm not certain
if it's a GTK+ widget or part of wxWidgets. At any rate, you can enter
a color by RGB or HSV decimal values in SpinCtrl widgets (click and hold on
the increment/decrement arrows is interesting), entering a color value or
name, selecting points on a color wheel, using an eyedropper picker anywhere
on the screen, or by clicking some preset colors. Whew.
Sizers
The placement of the controls is generally done by "sizers." It can be done
manually, and I do that with in SpinCtrl. For anything but the simplest and
most complex cases, sizers make things easy. They also deal with resizing the
windows and other hassles. A sizer isn't a widget, but is some object that
you create, tell what widgets are to be tracked, and then attach to a
a panel widget. Then you just forget about it and the right stuff seems to
happen. There are five sizers with multiple options and wxroses uses
three of them:
- BoxSizer
This simple sizer is used arrange multiple widgets horizontally or vertically.
Sizers can be nested, so this can be used to make grids of widgets.
- StaticBoxSizer
This only works (only works well?) when it's linked to a StaticBox widget.
I use the sizer to place SpinPanels or Buttons vertically. If I hadn't used
the static boxes, I would have used the BoxSizer instead.
- GridSizer
This creates a grid where each widget is the same size. It's used for the
Command buttons, where I wanted a 2x2 grid of same-sized buttons.
Events and callbacks
Any interactive graphical application is event driven. This means you
never really feel in contol of the system. Most of your code is called
from C++ code you've never seen and really don't want to. Fortunately, I
haven't really needed to except to see if I could intercept alphabetic
characters typed into SpinCtrl widgets. (Answer: I don't think so.)
There are a lot of events scattered through wxPython, wxRoses only uses
these:
- EVT_BUTTON
Triggered when a button is clicked on.
- EVT_CHECKBOX
Triggered when a check box is clicked on to toggle the checkmark on or off.
- EVT_COLOURSELECT
Triggered when a colour is decided upon in a colour dialog.
- EVT_SPINCTRL
Triggered when the number in a SpinCtrl changes, either by clicking on an
arrow to typing something like Tab or Enter, not for a digit.
- EVT_TIMER
Roses uses a timer both for controlling the rate a pattern is drawn and
pausing between drawing patterns when in automatic mode.
- EVT_SIZE
This is triggered when a window is resized. Window managers differ how
they handle gradual resizing, i.e. what happens when you resize a window
by dragging a handle with the mouse. Some produce a flood of resize
events, others produce one when you release the mouse button. wxRoses
redraws the pattern after a bit of a delay to reduce the flickering quick
redraws create.
- EVT_PAINT
This is triggered when a window is partially or completely exposed. Only
the widgets affect are signaled. wxRoses immediately redraws the whole
pattern.
- EVT_IDLE
This is triggered when there are no events left to processed. It provides
a convenient place to do things "when you get around to it" or do things
that might change the appearance of something at an inopportune time.
You bind an event to a callback method and when the event happens, that method
executes within the context of the object that did the binding.
All together
All these bits and pieces fit into a tree-like hierarchy:
--- New ---
- Frame, BoxSizer(horizontal), EVT_TIMER -> timer() -> OnTimer() -> timer_callback()
- Panel rose_panel, EVT_PAINT -> OnRepaint()
- Panel side_panel, BoxSizer(vertical)
- Panel cmd_panel, StaticBoxSizer
- StaticBox "Command"
- Panel sub_panel, GridSizer(rows = 2, cols = 2), EVT
- Button "Go", EVT_BUTTON -> OnGoStop()
- Button "Redraw", EVT_BUTTON -> OnRedraw()
- Button "Backward", EVT_BUTTON -> OnBackward()
- Button "Forward", EVT_BUTTON -> OnForward()
- Panel coe_panel, StaticBoxSizer
- StaticBox "Coefficient"
- SpinPanel "Style", EVT_SPINCTRL -> OnSpin() -> OnSpinback()
- SpinPanel "Sincr" (all SpinPanels are like the above)
- SpinPanel "Petal"
- SpinPanel "Pincr"
- Panel vec_panel, StaticBoxSizer
- StaticBox "Vector"
- SpinPanel "Vectors"
- SpinPanel "Minimum"
- SpinPanel "Maximum"
- SpinPanel "Skip first"
- SpinPanel "Draw only"
- StaticText "Takes"
- Panel tim_panel, StaticBoxSizer
- StaticBox "Timing"
- SpinPanel "Vec/tick"
- SpinPanel "msec/tick"
- SpinPanel "Delay"
- Panel opt_panel, StaticBoxSizer
- StaticBox "Options"
- CheckBox "Use GCDC", EVT_CHECKBOX -> OnCombo
- CheckBox "Use buffering", EVT_CHECKBOX -> OnBuffer
- BoxSizer "Foreground"
- cs.ColourSelect, EVT_COLOURSELECT -> OnSetFG
- StaticText "Foreground"
- BoxSizer "Background"
- cs.ColourSelect, EVT_COLOURSELECT -> OnSetBG
- StaticText "Background"
Oh, yeah, drawing vectors
Ultimately, the whole point of roses is to draw patterns. wxPython seems a
little odd in that you do that with the aid of an ephemeral object called
a device context. As always, there are several types that do things as
extreme as drawing on the raw screen, printer spooler, or bitmap buffers.
wxRoses just uses ClientDC which draws on the main area of a widget, in
this case the top_panel.
The only two things it needs to do is clear the widget and draw lines
on the widget. The former uses a "Brush" on the "Background" to paint the
area with "Clear". The latter uses a "Pen" and a connect-the-dots method
called "DrawLines".
A ClientDC can't be used within EVT_SIZE and EVT_PAINT handlers (I think
PaintDC is for that), but wxRoses just arranges for a new pattern to be drawn
through the usual time-based event.
I had some trouble getting resize and repaint events handled properly.
I may be missing something simple, or sizers don't let me have the hooks
I need. The vectors are drawn on my top_panel widget. The FlexGridSizer
there apparently uses EVT_SIZE to position the control panels and I can't
figure out how to share that event with both the sizer and the vector redraw
code. Binding both EVT_SIZE and EVT_PAINT on the se_panel mostly worked,
but EVT_PAINT events were only generated when part of the se_panel was
exposed. By triggering a redraw on EVT_PAINT events on top_panel and
EVT_SIZE on se_panel, I seem to have things working. Ugly and kludgy,
but it works.
So, does wxPython rule?
In playing around with the wxPython demo.py tool, it's
clear that there are a lot of applications built with wxWidgets. Since that's
in C++, wxPython provides some low-overhead hooks to get into native code.
WxRoses needs about a page and a half of code to set up the display window
and add all the hooks it needs, and about as much to interact with the roses
engine after that. For all the tiny little details that need to be handled,
I can't complain about that.
Speedwise, it may be as fast or faster than my PDP-11 version of roses
running on a PDP-11/45 back in 1973. That's not really a criticism - raster
graphics have far more overhead than using a vector drawing engine,
windowing systems have much more overhead than when I was the only
application drawing on the display, and the pipe between the application and
X11 has far more overhead than the PDP-11 system. That needed merely two
16 bit memory writes to add a vector to the pattern being displayed.
I'm also calling multiple trig functions for each vector, the PDP-11 used
a table lookup with interpolation and fixed point (integer) arithmetic.
So keeping up with a PDP-11 is actually quite a good accomplishment!
Perhaps the best test to answer "Does it rule?" is to ask "What's next?"
I'd love to see a Python program that display GPS tracks and deals with
waypoints, maps, etc. I have a Python code to talk to a serial port,
experience dealing with one serial line protocol, C code for dealing with
Garmin GPS receivers that can be converted to Python, MySQL, and wxPython. Way
to go! I'd also love to see a wxPython version of Banshee. Banshee started
out nice, but doesn't handle large libraries well and has several annoyances.
While it uses Mono, it sure likes like wxWidgets is behind it. I don't care
about Mono or .net, and I think I could write a Banshee-like thing that
executes various Linux apps to play or rip CDs.
So, wxPython has me enthusiastic about writing new applications, and that
seems as good a test as any.
If only there were a wxPython widget to add a few hours to the day....
Contact Ric Werme or
return to his home page.
Last updated 2007 June 13, originally written May 2007.