Wax primer: A simple editor (part 2)

[continued from part 1]

The current editor isn't very user-friendly. For example, it's hard to tell what it's doing. Is a filename set or not? If so, what is it? When I selected Save, nothing happened, did it actually save? Etc.

A status bar can make things clearer. It is easy to add one. Add this to the end of Body():

        self.statusbar = StatusBar(self, numpanels=2)
        self.SetStatusBar(self.statusbar)

(Note that it is added to a window using the SetStatusBar method, not using AddComponent.)

When you are in a pull-down menu, the help text will automatically show in the first panel of the status bar. (This is not a Wax feature; wxPython does this automatically.) Since the first panel already has a purpose, let's use the second one to display the current filename, if any.

First, we write a method SetFilename that sets the filename, and displays it in the status bar.

    def SetFilename(self, filename):
        self.filename = filename
        self.statusbar[1] = "Filename: " + str(self.filename)

(We can easily set text in a statusbar's Nth panel by assigning to statusbar[N]. The text has to be a string, of course. Indexing starts at 0, so [1] is the second panel.)

Second, replace all other instances of "self.filename = filename" in the code, with "self.SetFilename(filename)". In the New() method, use self.SetFilename(None).

When creating a new file, or opening or saving one, the filename should now appear in the second panel of the status bar.

Now, let's add some shortcut keys. Wouldn't it be useful if we could press Ctrl-S to save, and Ctrl-O to open a file, like in many (Windows) programs? Fortunately, this is easy too. Just replace the appropriate lines in CreateMenu() with:

        menu1.Append("&New", self.New, "Create a new file", hotkey="Ctrl-N")
        menu1.Append("&Open", self.Open, "Open a file", hotkey="Ctrl-O")
        menu1.Append("&Save", self.Save, "Save a file", hotkey="Ctrl-S")

The hotkey keyword argument sets the shortcut key, in text format. Many combinations are accepted, e.g. "Alt-F9" or "Shift-Ctrl-B", etc.

We now have two ways to handle common actions (new file, open, save). Adding a third one (a toolbar with buttons) may seem redundant, yet many programs use it. Also, it will illustrate two important Wax concepts: automatic sizing, and events. (These are of course really wxPython concepts, but the way Wax handles them is different.)

First, we create a method to make the toolbar. We use a Panel object, then add Buttons to it.

    def CreateToolbar(self, parent):
        p = Panel(parent, direction='horizontal')
        p.AddComponent(Button(p, "New", self.New))
        p.AddComponent(Button(p, "Open", self.Open))
        p.AddComponent(Button(p, "Save", self.Save))
        p.Pack()
        return p

The p.AddComponent(...) lines are really a concise version of:

b = Button(p, "New", self.New)
p.AddComponent(b)

Panels and Frames automatically stack the controls they contain in a simple way, either horizontal or vertical. direction='horizontal' means that this Panel will contain its buttons in a horizontal row. The call to Pack() makes the Panel resize itself around its buttons, so we won't see a panel of random size with buttons "somewhere" in it.

As for the Buttons... a call like Button(p, "New", self.New) obviously contains the parent, the text of the Button, and the event associated with clicking it. We can also write Button(parent, text, event=f). In other words, clicking the New button calls self.New, etc.

If we were to create a Button and add an event *later*, we could do:

b = Button(parent, text)
# later...
b.OnClick = some_function

This way of setting events applies to most Wax controls, by the way. There's no more need to play around with wxPython's EVT_* functions.

Now, we add CreateToolbar to the Body() method. However, since we create more than one control in our window, we have to lay out these controls properly, much like we did with the Panel. The first version of the new Body() method looks like this:

    def Body(self):
        self.filename = None
        self.CreateMenu()
        
        toolbar = self.CreateToolbar(self)
        self.AddComponent(toolbar)
        
        self.textbox = TextBox(self, multiline=1, wrap=0)
        self.textbox.SetFont(FIXED_FONT)
        self.AddComponent(self.textbox)
        
        self.Pack()
        
        self.statusbar = StatusBar(self, numpanels=2)
        self.SetStatusBar(self.statusbar)

We add the toolbar; then the textbox; then call self.Pack(). However, if we run this, it would look like a big old mess. The reason is that Panels and Frames use the horizontal layout by default. Since we want the toolbar on top, and then the TextBox under it, we must make the MainFrame use a vertical layout:

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

This is better, but it still doesn't look right. The size of the main window isn't right, and the TextBox should occupy the whole available space. To make that happen, the AddComponent() method takes a keyword arguments, expand. Let's also fix the TextBox's size, while we're at it.

        self.textbox.SetSize((600,400))
        self.AddComponent(self.textbox, expand='both')

(SetSize is derived from wxPython, and takes a tuple (width, height).) Note that expand='both' indicates that the control expands both horizontally and vertically.

This is more like it -- except that the toolbar doesn't expand like it should. We can fix that too:

        self.AddComponent(toolbar, expand='h')

(The 'h' stands for 'horizontal', to indicate that the toolbar should expand horizontally, but not vertically.) Finally, the window looks good, and when it's resized the toolbar and TextBox resize with it.

So, to summarize it... The Frame and Panel objects have wxPython sizers built in... an empty sizer is created when the object is created, and AddComponent adds controls to this sizer. The expand keyword argument can make controls stretch horizontally, vertically, or both. (There also used to be a stretch keyword argument, which still shows up in old code. It is now obsolete.)

In this second part, we added a status bar, a toolbar, shortcut keys, and learned about sizers, packing and events. The current code is still short, around 90 lines, including empty ones. What's more important, it's still quite readable and understandable.

This concludes the Wax primer. For the sake of completeness, here is the final code:



from wax import *

FIXED_FONT = ('Courier New', 10)

class MainFrame(Frame):

def Body(self):
self.filename = None
self.CreateMenu()

toolbar = self.CreateToolbar(self)
self.AddComponent(toolbar, expand='h')

self.textbox = TextBox(self, multiline=1, wrap=0)
self.textbox.SetFont(FIXED_FONT)
self.textbox.SetSize((600,400))
self.AddComponent(self.textbox, expand='both')

self.Pack()

self.statusbar = StatusBar(self, numpanels=2)
self.SetStatusBar(self.statusbar)

def CreateMenu(self):
menubar = MenuBar()

menu1 = Menu(self)
menu1.Append("&New", self.New, "Create a new file", hotkey="Ctrl-N")
menu1.Append("&Open", self.Open, "Open a file", hotkey="Ctrl-O")
menu1.Append("&Save", self.Save, "Save a file", hotkey="Ctrl-S")

menubar.Append(menu1, "&File")

self.SetMenuBar(menubar)

def CreateToolbar(self, parent):
p = Panel(parent, direction='horizontal')
p.AddComponent(Button(p, "New", self.New))
p.AddComponent(Button(p, "Open", self.Open))
p.AddComponent(Button(p, "Save", self.Save))
p.Pack()
return p

def New(self, event):
self.textbox.Clear()
self.SetFilename(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.SetFilename(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.SetFilename(filename)
self._SaveFile(filename)
finally:
dlg.Destroy()

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

def SetFilename(self, filename):
self.filename = filename
self.statusbar[1] = "Filename: " + str(self.filename)

app = Application(MainFrame, title="A simple editor", direction='vertical')
app.Run()

:code