Introduction
One of the biggest hurdles I have had to overcome as a newbie in the whacky world of codeslinging, python and wxPython, is how to wrap my head around writing effective code that is easy to manage and extend. It's hard enough to get stuff chugging along in a satisfactory manner without the flashy GUI glitter, but when you add OOP with classes, selfs and import statements galore to the cocktail, you have a recipe for disaster for us newcomers.
Take my first project. I hashed out an interface in wxGlade, then sprinkled a whole bunch of ID_* identifiers with a generous hand over the stew, seasoned with EVENTS for all the widgets, tossed in functions for handling popups and toolbars and other GUI mojo, and mixed in the whole stuffed enchilada consisting of networking, configuration parser and database functions and other code to get the App From Hell to actually do something interesting. The result was 1000 lines of spaghetti code that more looked like something the cat dragged in than anything else. That's when I discovered my life just wouldn't be complete without an extra box of widgets crammed in there as well as a couple of more pages to the wxNotebook, but needless to say it was impossible to make heads or tails of the mess.
Let's see if we can remedy this binary horror.
The General Idea
First we clean things up a tad, by dividing everything into neat little packages. The application I'll use for demonstration purposes is a wxFrame with a wxMenu and wxstatusbar slapped on for good measure. The menu has two functions, one calling a standard onExit() function, and the other triggers an event which calls a function to read in the sourcecode and launches a popup in the form of a wxmessageDialog to display it. Not exactly a marvel of an application, but at least it's on a level where even granny can follow along.
Here's an outline of the files we'll be using:
- gui.py - this one is generated by wxGlade (just do the tutorial under the help menu in wxGlade. It's easy as pie)
- functions.py - the meat and potatoes of the application (could also have been database.py or networking.py or something more exhilarating)
- popups.py - a class holding the scrolledmessageDialog just to demonstrate how to integrate OOP. It's the in-thing, you know.
- main.py - the file which actually launches the GUI and where we write the EVENTS. We'll add an acceleratorTable just for the sheer heck of it so you can see just how easy it is to add stuff outside of gui.py so that you don't mess up the precious wxGlade code.
GUI.PY
1 #!/usr/bin/env python
2 # -*- coding: iso-8859-15 -*-
3 # generated by wxGlade 0.6.2 on Sun Sep 21 16:28:55 2008 from "OrganizeYourCode.wxg"
4
5 import wx
6
7 # begin wxGlade: extracode
8 # end wxGlade
9
10
11
12 class myNotebook(wx.Notebook):
13 def __init__(self, *args, **kwds):
14 # begin wxGlade: myNotebook.__init__
15 kwds["style"] = 0
16 wx.Notebook.__init__(self, *args, **kwds)
17 self.notebook_1_pane_1 = wx.Panel(self, -1)
18 self.button_1 = wx.Button(self.notebook_1_pane_1, -1, "button_1")
19 self.button_2 = wx.Button(self.notebook_1_pane_1, -1, "button_2")
20 self.text_ctrl_1 = wx.TextCtrl(self.notebook_1_pane_1, -1, "", style=wx.TE_MULTILINE)
21 self.notebook_1_pane_2 = wx.Panel(self, -1)
22
23 self.__set_properties()
24 self.__do_layout()
25 # end wxGlade
26
27 def __set_properties(self):
28 # begin wxGlade: myNotebook.__set_properties
29 self.AddPage(self.notebook_1_pane_1, "tab1")
30 self.AddPage(self.notebook_1_pane_2, "tab2")
31 # end wxGlade
32
33 def __do_layout(self):
34 # begin wxGlade: myNotebook.__do_layout
35 sizer_2 = wx.BoxSizer(wx.VERTICAL)
36 sizer_3 = wx.BoxSizer(wx.HORIZONTAL)
37 sizer_3.Add(self.button_1, 0, 0, 0)
38 sizer_3.Add((20, 20), 0, 0, 0)
39 sizer_3.Add(self.button_2, 0, 0, 0)
40 sizer_2.Add(sizer_3, 0, wx.TOP|wx.BOTTOM|wx.ALIGN_CENTER_HORIZONTAL, 4)
41 sizer_2.Add(self.text_ctrl_1, 1, wx.EXPAND, 0)
42 self.notebook_1_pane_1.SetSizer(sizer_2)
43 # end wxGlade
44
45 # end of class myNotebook
46
47
48 class MyFrame1(wx.Frame):
49 def __init__(self, *args, **kwds):
50 # begin wxGlade: MyFrame1.__init__
51 kwds["style"] = wx.DEFAULT_FRAME_STYLE
52 wx.Frame.__init__(self, *args, **kwds)
53
54 # Menu Bar
55 self.frame1_menubar = wx.MenuBar()
56 global ID_EXIT; ID_EXIT = wx.NewId()
57 global ID_BLURB; ID_BLURB = wx.NewId()
58 wxglade_tmp_menu = wx.Menu()
59 wxglade_tmp_menu.Append(ID_EXIT, "exit", "", wx.ITEM_NORMAL)
60 wxglade_tmp_menu.Append(ID_BLURB, "blurb", "", wx.ITEM_NORMAL)
61 self.frame1_menubar.Append(wxglade_tmp_menu, "File")
62 self.SetMenuBar(self.frame1_menubar)
63 # Menu Bar end
64 self.frame1_statusbar = self.CreateStatusBar(1, 0)
65 self.notebook_1 = myNotebook(self, -1)
66
67 self.__set_properties()
68 self.__do_layout()
69 # end wxGlade
70
71 def __set_properties(self):
72 # begin wxGlade: MyFrame1.__set_properties
73 self.SetTitle("OrganizeYourCode")
74 self.SetSize((402, 524))
75 self.frame1_statusbar.SetStatusWidths([-1])
76 # statusbar fields
77 frame1_statusbar_fields = ["Created with wxGlade!"]
78 for x, item in enumerate(frame1_statusbar_fields):
79 self.frame1_statusbar.SetStatusText(item, x)
80 # end wxGlade
81
82 def __do_layout(self):
83 # begin wxGlade: MyFrame1.__do_layout
84 sizer_1 = wx.BoxSizer(wx.VERTICAL)
85 sizer_1.Add(self.notebook_1, 1, wx.EXPAND, 0)
86 self.SetSizer(sizer_1)
87 self.Layout()
88 # end wxGlade
89
90 # end of class MyFrame1
91
92
93 if __name__ == "__main__":
94 app = wx.PySimpleApp(0)
95 wx.InitAllImageHandlers()
96 frame1 = MyFrame1(None, -1, "")
97 app.SetTopWindow(frame1)
98 frame1.Show()
99 app.MainLoop()
To add more wizardry, just insert more ID_*. I keep mine in a separate file for now, and then simply use C-X-i in Emacs to insert it. That way I can trash the interface if I get sick of it or find out it demands pulling off an anatomic impossibility just to navigate. See Sept 21, 2003 note at the bottom for an alternate solution to these wxNewId() edits
For items which don't have a ID_* value, just do as follows:
FUNCTIONS.PY
1 #! /usr/bin/env python
2 from __future__ import with_statement # needed for Python before version 2.6
3
4 def openfile(fileobj, mode = "", list=None):
5 """Generic file opener, adds newlines to list and returns lines or string"""
6 try:
7 with open(fileobj, mode or "r") as file:
8
9 # Add newline if we write a list to a file
10 if mode in ('w', 'a'):
11 file.writelines(line + '\n' for line in list)
12
13 # Read and return a list if we read a file
14 elif mode == 'r':
15 return file.read().splitlines()
16
17 # If we want a single, big-ass string instead
18 else:
19 return file.read()
20
21 except IOError:
22 print "No such file: %s" % fileobj
23
24
25 def stringify(list):
26 """Turn list into string object and return"""
27 return "".join(map(str, list))
28
29 def test_openfile():
30 """
31 Test function for trying out our code shenanigans before using them in
32 the actual application.
33 """
34
35 lines = openfile("functions.py")
36 string = stringify(lines)
37 print string
38
39 if __name__ == '__main__':
40 test_openfile()
Not the kinkiest example ever deviced, but it gets the job done. You can call it as a stand-alone file like this: % python functions.py and ka-pow, it reads its own sourcecode. It's a rather spiffy way of testing your code without getting bogged down with the fancy GUI gizmos. (If I do say so myself)
POPUPS.PY
1 #! /usr/bin/env python
2
3 import wx
4 import wx.lib.dialogs
5
6 class Popup:
7 """Class for handling all our fancy GUI stuff"""
8 def msg(self, frame, string, title):
9 """Display STRING in a wx.MessageDialog"""
10 dlg = wx.lib.dialogs.ScrolledMessageDialog(frame, string, title)
11 dlg.ShowModal()
12 dlg.Destroy()
The frame is the parent of the widget, and should show you how easy it is to pass an entire object. You could also add from functions import * and have msg() call openfile() and stringify() if you're into that sorta thing.
MAIN.PY
1 #! /usr/bin/env python
2
3 import wx
4 import gui
5 import popups
6 import functions
7
8
9 ---- /!\ '''Edit conflict - other version:''' ----
10 # You'll find images.py [OrganizingYourCodeImages|here] - just copy it to your
11
12 ---- /!\ '''Edit conflict - your version:''' ----
13 # You'll find images.py [[OrganizingYourCodeImages|here]] - just copy it to your
14
15 ---- /!\ '''End of edit conflict''' ----
16 # path/current directory - it's for the toolbar.
17 import images
18
19 ID_COMBO = wx.NewId()
20
21 class Toolbar:
22 def __init__(self, frame, list):
23 # Now we can call ID_COMBO from outside the class...
24 self.ID_COMBO = ID_COMBO
25
26 self.tb = frame.CreateToolBar(wx.TB_HORIZONTAL|wx.NO_BORDER|wx.TB_FLAT)
27
28 # Change the wx.NewId() as in gui.py when you're ready to play
29 # with them.
30 self.tb.AddSimpleTool(wx.NewId(), images.getOpenBitmap(),
31 "Open","file")
32 self.tb.AddSimpleTool(wx.NewId(),images.getPasteBitmap(),
33 "Save","Save file")
34 self.tb.AddSeparator()
35 self.tb.AddSimpleTool(wx.NewId(), images.getCopyBitmap(),
36 "Slice", "Cut out slice of out of a file")
37 self.tb.AddSimpleTool(wx.NewId(), images.getTestBitmap(),
38 "Edit", "Edit word")
39 self.tb.AddSimpleTool(wx.NewId(), images.getNewBitmap(),
40 "Reset", "Reset list")
41 self.tb.AddSeparator()
42
43 # Notice ID_COMBO. It's declared exactly the way we did in gui.py
44 self.cb = wx.ComboBox(self.tb, ID_COMBO, "", choices=list,size=(150,-1),
45 style=wx.CB_DROPDOWN | wx.CB_READONLY)
46 self.tb.AddControl(self.cb)
47
48 self.tb.Realize()
49
50
51 ##class MyApp(wx.App):
52 class MyApp(wx.PySimpleApp):
53 def OnInit(self):
54 wx.InitAllImageHandlers()
55 frame = gui.MyFrame1(None, -1, "")
56 frame.Show(True)
57 self.SetTopWindow(frame)
58 self.frame = frame
59
60 # This is how you can pass the frame around and add more glitz
61 # at liberty!
62 self.list = ["one", "two", "three", "four", "five"]
63 self.toolbar = Toolbar(self.frame, self.list)
64
65 self.popup = popups.Popup()
66
67 # Menu Events
68 wx.EVT_MENU(self, gui.ID_EXIT, self.MenuExit)
69 wx.EVT_MENU(self, gui.ID_BLURB, self.MenuBlurb)
70 self.frame.SetAcceleratorTable(wx.AcceleratorTable([
71 (wx.ACCEL_NORMAL, wx.WXK_F1, gui.ID_EXIT),
72 (wx.ACCEL_NORMAL, wx.WXK_F2, gui.ID_BLURB),
73 ]))
74 return True
75
76 def MenuExit(self, event):
77 self.frame.Close(true)
78
79 def MenuBlurb(self, event):
80 list = functions.openfile("main.py")
81 string = functions.stringify(list)
82 self.popup.msg(self.frame, string, "Blurb Title")
83
84 def main():
85 # app = wx.PySimpleApp(0)
86 app = MyApp(0);
87 app.MainLoop()
88
89 if __name__ == '__main__':
90 main()
Ah, *so* much nicer than a single, everything-but-the-kitchen-sink file, don't you think so? You can of course put class Toolbar in yet another file.
TODO LIST
This should give other newbies in the same boat a leg up, but I still need to figure out the follow to achieve perfect Appness. Help out if you can!
- Find a nicer way to add self.VERBOSE = True to a class than I usually do. This is for debugging purposes. Usually I just use print statements, but maybe a dialog is better? Or would it be smarter to just use the statusbar and print oneliners there?
CHANGE LOG
April 21, 2003:
- Added the acceleratorTable to main.py.
- corrected a bug in the parameter for openfile().
- corrected a bug in button.getId() example.
- replaced messageDialog with the much sexier scrolledmessageDialog.
- figured out that I could add ID_* via wxGlade's menu-interface.
- added the whacky toolbar class to main.py to show how to pass objects + lists to a class.
- correct 'self' error in Toolbar class
Sept 21, 2003:
wxGlade allows NAME=? for object IDs, which will have the same effect without the need for inserting a separate file. Search http://wxglade.sourceforge.net/tutorial.php for 'name=?' for more info.
September 21, 2008:
- updated the presented code (to meet actual wxPython naming and now use of explicit name space)
included missing code snippets to enable a complete cut&paste example
- inserted screenshot that show how to automatically generate object IDs with wxGlade
May 2010, split images onto its own page
Edit conflict - other version:
May 2010, split images onto its own page
Edit conflict - your version:
End of edit conflict
KUDOS
My heartfelt thanks and appreciation goes out to Alberto Griggio and Chris Munchenberg from the imitable wxPython mailing list for wetnursing me through the above examples - I wouldn't have been able to catch another Z without their support on the list. (Please direct your lawsuits and other legal shenanigans due to this page to them too, hehehe). Peace out, you Pyrates out there!
Edit conflict - other version:
Edit conflict - your version:
End of edit conflict