Wax primer: A simple editor

This document illustrates some of the basics of Wax. As an example program, we're going to make a simple editor. I'm going to build it step by step; you are encouraged to apply the changes incrementally and run the code after every step.

Every Wax application needs at least an Application object, using a Frame. A skeleton app would look like this:

from wax import *

class MainFrame(Frame):
    pass
    
app = Application(MainFrame)
app.Run()

  • This is a working application. (Granted, it doesn't do much yet. :-)

  • Like Tkinter, Wax is designed to play well with "from .. import *". Of course, you can also do "import wax", but you will then have the additional overhead of writing wax.Frame, wax.Application, etc.

  • A Frame object is a window. Unlike Tkinter, it cannot be used for panes inside windows. For that, use the Panel object.

  • An Application takes a Frame *class* as its first argument. It automatically creates an instance of that class for internal use. You can pass additional parameters that will be applied to the Frame. For example, if we want to set a title for the application, we can do:

app = Application(MainFrame, title="A simple editor")

For our editor widget, we're going to use a simple TextBox, which is based on wxPython's wxTextCtrl. You add controls to a Frame by overriding its Body() method. Let's start simple:

class MainFrame(Frame):
    
    def Body(self):
        self.textbox = TextBox(self, multiline=1, wrap=0)
        self.AddComponent(self.textbox)

Normally a little bit more code is needed, but since the TextBox is currently our only control, we get away with coding it like this. The TextBox automatically fills up the whole window and resizes if the window resizes.

Like most (all?) GUI toolkits, when a control is created, a reference to its parent is passed. In this case, controls that belong to MainFrame are passed self as the first parameter. Hence TextBox(self, ...). Calling AddComponent() is necessary for the layout; more about this later.

I don't like the font, though. Let's set a fixed-width font for the TextBox. Add this code after the import statement:

FIXED_FONT = ('Courier New', 10)
# note that this used to be Font('Courier New', 10) in older
# versions, but this isn't allowed anymore in recent versions
# of wxPython

and change the code in Body() to this:

    def Body(self):
        self.textbox = TextBox(self, multiline=1, wrap=0)
        self.textbox.SetFont(FIXED_FONT)
        self.AddComponent(self.textbox)

OK, now we have an editor. But it's not very useful. It cannot load or save anything. It would be a good idea to add a menu with these options.

This is the code that creates the menu (CreateMenu is a method of MainFrame, of course):

    def CreateMenu(self):
        menubar = MenuBar()
        
        menu1 = Menu(self)
        menu1.Append("&New", self.New, "Create a new file")
        menu1.Append("&Open", self.Open, "Open a file")
        menu1.Append("&Save", self.Save, "Save a file")
        
        menubar.Append(menu1, "&File")

        self.SetMenuBar(menubar)

Create dummy methods for New, Open and Save (again in MainFrame):

    def New(self, event): pass
    def Open(self, event): pass
    def Save(self, event): pass

And change Body() to:

    def Body(self):
        self.CreateMenu()
        self.textbox = TextBox(self, multiline=1, wrap=1)
        self.textbox.SetFont(FIXED_FONT)
        self.AddComponent(self.textbox)

A line like menu1.Append("&New", self.New, "Create a new file") means, append a menu item with name "New", associated with the method self.New, and help text "Create a new file". The & means that the character that follows can be used as a shortcut. In this case, you can press Alt-F, N (on Windows at least) to select the menu item.

The resulting code works, and you will notice that the window now sports a menu bar. Clicking on the items doesn't do anything yet, because we associated the menu items with empty methods. That's going to change, though. For this example, I will use simple methods:

  • New should clear the TextBox and reset the internally stored filename.
  • Open should bring up a dialog to select a file, and, upon success, load that file and set the internal filename.
  • Save should store the contents of the TextBox to a file. If the filename is already known, it will use that name, otherwise it should bring up a dialog to set the filename (and directory).

First, we need a filename attribute. This can be set in Body():

    def Body(self):
        self.filename = None
        # other code here...

Implementing the New() method is easy:

    def New(self, event):
        self.textbox.Clear()
        self.filename = None

Note that most Wax controls are derived from wxPython controls, and as such have the same methods (plus some more, usually). Here, TextBox.Clear() is the same as wxTextCtrl.Clear().

Writing the Open() method will be a bit more challenging, since it requires a dialog. In this case, the FileDialog class. Fortunately, it's easy to use. Like in wxPython and many other GUI toolkits, this is how you use dialogs:

  1. create an instance
  2. call the ShowModal() method and store its result
  3. if the result is "OK", do something, possibly using the dialog instance itself (for example, get an input value that you typed in the dialog, etc)
  4. destroy the dialog instance

The actual code looks like this:

    def Open(self, event):
        dlg = FileDialog(self, open=1)
        result = dlg.ShowModal()
        if result == 'ok':
            filename = dlg.GetPaths()[0]
            self._OpenFile(filename)
        dlg.Destroy()

(We cannot run this yet until we have written the _OpenFile method.) Some comments:

  • dlg = FileDialog(self, open=1) creates the dialog (but doesn't show it yet). The open=1 indicates that this is a dialog for opening files. We can, and will, use save=1 instead to use FileDialog for saving files.

  • dlg.ShowModal() actually shows the dialog. No surprises here.

  • Wax dialogs don't return some obscure constant; rather, they return a string, e.g. 'ok', 'cancel', 'yes', 'no'. The string should always be lowercase.

  • The GetPaths() methods returns the fully qualified names of the files chosen. In this case, we can only select one file, so we take the first argument of the list. It's possible, though, to have FileDialog accept a selection of multiple files.

Note that in case something goes wrong in, say, _OpenFile, an exception will occur and the dialog will not be destroyed. To prevent this from happening, people usually write such code like this:

    def Open(self, event):
        dlg = FileDialog(self, open=1)
        try:
            result = dlg.ShowModal()
            if result == 'ok':
                filename = dlg.GetPaths()[0]
                self._OpenFile(filename)
        finally:
            dlg.Destroy()

Now for the implementation of _OpenFile. It should simply take a filename, load that file into the TextBox, and set the internal filename. This uses the Clear() and AppendText() methods, well known from wxTextCtrl:

    def _OpenFile(self, filename):
        self.filename = filename
        f = open(filename, 'r')
        data = f.read()
        f.close()
        self.textbox.Clear()
        self.textbox.AppendText(data)

The code should now work. Try it, select Open in the File menu, select a file, and its contents should appear in the TextBox.

Now that this is done, the code for Save is not difficult:

    def Save(self, event):
        if self.filename:
            self._SaveFile(self.filename)
        else:
            dlg = FileDialog(self, save=1)
            try:
                result = dlg.ShowModal()
                if result == 'ok':
                    filename = dlg.GetPaths()[0]
                    self.filename = filename
                    self._SaveFile(filename)
            finally:
                dlg.Destroy()
                
    def _SaveFile(self, filename):
        f = open(filename, 'w')
        f.write(self.textbox.GetValue())
        f.close()

Ta-da! A working editor, in ~70 lines of code. Nothing fancy, but it illustrates the power of Wax. (And of wxPython, of course.)

In part 2, we will soup up this editor, by adding a few thingies, like a statusbar, toolbar, shortcut keys, and more.

Your code should look like this:



from wax import *

FIXED_FONT = ('Courier New', 10)

class MainFrame(Frame):

def Body(self):
self.filename = None
self.CreateMenu()
self.textbox = TextBox(self, multiline=1, wrap=0)
self.textbox.SetFont(FIXED_FONT)
self.AddComponent(self.textbox)

def CreateMenu(self):
menubar = MenuBar()

menu1 = Menu(self)
menu1.Append("&New", self.New, "Create a new file")
menu1.Append("&Open", self.Open, "Open a file")
menu1.Append("&Save", self.Save, "Save a file")

menubar.Append(menu1, "&File")

self.SetMenuBar(menubar)

def New(self, event):
self.textbox.Clear()
self.filename = None

def Open(self, event):
dlg = FileDialog(self, open=1)
try:
result = dlg.ShowModal()
if result == 'ok':
filename = dlg.GetPaths()[0]
self._OpenFile(filename)
finally:
dlg.Destroy()

def _OpenFile(self, filename):
self.filename = filename
f = open(filename, 'r')
data = f.read()
f.close()
self.textbox.Clear()
self.textbox.AppendText(data)

def Save(self, event):
if self.filename:
self._SaveFile(self.filename)
else:
dlg = FileDialog(self, save=1)
try:
result = dlg.ShowModal()
if result == 'ok':
filename = dlg.GetPaths()[0]
self.filename = filename
self._SaveFile(filename)
finally:
dlg.Destroy()

def _SaveFile(self, filename):
f = open(filename, 'w')
f.write(self.textbox.GetValue())
f.close()

app = Application(MainFrame, title="A simple editor")
app.Run()
:code