== 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 == {{{ #!python #!/usr/bin/env python # -*- coding: iso-8859-15 -*- # generated by wxGlade 0.6.2 on Sun Sep 21 16:28:55 2008 from "OrganizeYourCode.wxg" import wx # begin wxGlade: extracode # end wxGlade class myNotebook(wx.Notebook): def __init__(self, *args, **kwds): # begin wxGlade: myNotebook.__init__ kwds["style"] = 0 wx.Notebook.__init__(self, *args, **kwds) self.notebook_1_pane_1 = wx.Panel(self, -1) self.button_1 = wx.Button(self.notebook_1_pane_1, -1, "button_1") self.button_2 = wx.Button(self.notebook_1_pane_1, -1, "button_2") self.text_ctrl_1 = wx.TextCtrl(self.notebook_1_pane_1, -1, "", style=wx.TE_MULTILINE) self.notebook_1_pane_2 = wx.Panel(self, -1) self.__set_properties() self.__do_layout() # end wxGlade def __set_properties(self): # begin wxGlade: myNotebook.__set_properties self.AddPage(self.notebook_1_pane_1, "tab1") self.AddPage(self.notebook_1_pane_2, "tab2") # end wxGlade def __do_layout(self): # begin wxGlade: myNotebook.__do_layout sizer_2 = wx.BoxSizer(wx.VERTICAL) sizer_3 = wx.BoxSizer(wx.HORIZONTAL) sizer_3.Add(self.button_1, 0, 0, 0) sizer_3.Add((20, 20), 0, 0, 0) sizer_3.Add(self.button_2, 0, 0, 0) sizer_2.Add(sizer_3, 0, wx.TOP|wx.BOTTOM|wx.ALIGN_CENTER_HORIZONTAL, 4) sizer_2.Add(self.text_ctrl_1, 1, wx.EXPAND, 0) self.notebook_1_pane_1.SetSizer(sizer_2) # end wxGlade # end of class myNotebook class MyFrame1(wx.Frame): def __init__(self, *args, **kwds): # begin wxGlade: MyFrame1.__init__ kwds["style"] = wx.DEFAULT_FRAME_STYLE wx.Frame.__init__(self, *args, **kwds) # Menu Bar self.frame1_menubar = wx.MenuBar() global ID_EXIT; ID_EXIT = wx.NewId() global ID_BLURB; ID_BLURB = wx.NewId() wxglade_tmp_menu = wx.Menu() wxglade_tmp_menu.Append(ID_EXIT, "exit", "", wx.ITEM_NORMAL) wxglade_tmp_menu.Append(ID_BLURB, "blurb", "", wx.ITEM_NORMAL) self.frame1_menubar.Append(wxglade_tmp_menu, "File") self.SetMenuBar(self.frame1_menubar) # Menu Bar end self.frame1_statusbar = self.CreateStatusBar(1, 0) self.notebook_1 = myNotebook(self, -1) self.__set_properties() self.__do_layout() # end wxGlade def __set_properties(self): # begin wxGlade: MyFrame1.__set_properties self.SetTitle("OrganizeYourCode") self.SetSize((402, 524)) self.frame1_statusbar.SetStatusWidths([-1]) # statusbar fields frame1_statusbar_fields = ["Created with wxGlade!"] for x, item in enumerate(frame1_statusbar_fields): self.frame1_statusbar.SetStatusText(item, x) # end wxGlade def __do_layout(self): # begin wxGlade: MyFrame1.__do_layout sizer_1 = wx.BoxSizer(wx.VERTICAL) sizer_1.Add(self.notebook_1, 1, wx.EXPAND, 0) self.SetSizer(sizer_1) self.Layout() # end wxGlade # end of class MyFrame1 if __name__ == "__main__": app = wx.PySimpleApp(0) wx.InitAllImageHandlers() frame1 = MyFrame1(None, -1, "") app.SetTopWindow(frame1) frame1.Show() 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: {{{ #!python # In GUI.PY self.b = wxButton(self, -1, "Test") # In MAIN.PY EVT_BUTTON(self, self.frame.b.GetId(), self.SomeFunction) }}} == FUNCTIONS.PY == {{{ #!python #! /usr/bin/env python from __future__ import with_statement # needed for Python before version 2.6 def openfile(fileobj, mode = "", list=None): """Generic file opener, adds newlines to list and returns lines or string""" try: with open(fileobj, mode or "r") as file: # Add newline if we write a list to a file if mode in ('w', 'a'): file.writelines(line + '\n' for line in list) # Read and return a list if we read a file elif mode == 'r': return file.read().splitlines() # If we want a single, big-ass string instead else: return file.read() except IOError: print "No such file: %s" % fileobj def stringify(list): """Turn list into string object and return""" return "".join(map(str, list)) def test_openfile(): """ Test function for trying out our code shenanigans before using them in the actual application. """ lines = openfile("functions.py") string = stringify(lines) print string if __name__ == '__main__': 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 == {{{ #!python #! /usr/bin/env python import wx import wx.lib.dialogs class Popup: """Class for handling all our fancy GUI stuff""" def msg(self, frame, string, title): """Display STRING in a wx.MessageDialog""" dlg = wx.lib.dialogs.ScrolledMessageDialog(frame, string, title) dlg.ShowModal() 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 == {{{ #!python #! /usr/bin/env python import wx import gui import popups import functions ---- /!\ '''Edit conflict - other version:''' ---- # You'll find images.py [OrganizingYourCodeImages|here] - just copy it to your ---- /!\ '''Edit conflict - your version:''' ---- # You'll find images.py [[OrganizingYourCodeImages|here]] - just copy it to your ---- /!\ '''End of edit conflict''' ---- # path/current directory - it's for the toolbar. import images ID_COMBO = wx.NewId() class Toolbar: def __init__(self, frame, list): # Now we can call ID_COMBO from outside the class... self.ID_COMBO = ID_COMBO self.tb = frame.CreateToolBar(wx.TB_HORIZONTAL|wx.NO_BORDER|wx.TB_FLAT) # Change the wx.NewId() as in gui.py when you're ready to play # with them. self.tb.AddSimpleTool(wx.NewId(), images.getOpenBitmap(), "Open","file") self.tb.AddSimpleTool(wx.NewId(),images.getPasteBitmap(), "Save","Save file") self.tb.AddSeparator() self.tb.AddSimpleTool(wx.NewId(), images.getCopyBitmap(), "Slice", "Cut out slice of out of a file") self.tb.AddSimpleTool(wx.NewId(), images.getTestBitmap(), "Edit", "Edit word") self.tb.AddSimpleTool(wx.NewId(), images.getNewBitmap(), "Reset", "Reset list") self.tb.AddSeparator() # Notice ID_COMBO. It's declared exactly the way we did in gui.py self.cb = wx.ComboBox(self.tb, ID_COMBO, "", choices=list,size=(150,-1), style=wx.CB_DROPDOWN | wx.CB_READONLY) self.tb.AddControl(self.cb) self.tb.Realize() ##class MyApp(wx.App): class MyApp(wx.PySimpleApp): def OnInit(self): wx.InitAllImageHandlers() frame = gui.MyFrame1(None, -1, "") frame.Show(True) self.SetTopWindow(frame) self.frame = frame # This is how you can pass the frame around and add more glitz # at liberty! self.list = ["one", "two", "three", "four", "five"] self.toolbar = Toolbar(self.frame, self.list) self.popup = popups.Popup() # Menu Events wx.EVT_MENU(self, gui.ID_EXIT, self.MenuExit) wx.EVT_MENU(self, gui.ID_BLURB, self.MenuBlurb) self.frame.SetAcceleratorTable(wx.AcceleratorTable([ (wx.ACCEL_NORMAL, wx.WXK_F1, gui.ID_EXIT), (wx.ACCEL_NORMAL, wx.WXK_F2, gui.ID_BLURB), ])) return True def MenuExit(self, event): self.frame.Close(true) def MenuBlurb(self, event): list = functions.openfile("main.py") string = functions.stringify(list) self.popup.msg(self.frame, string, "Blurb Title") def main(): # app = wx.PySimpleApp(0) app = MyApp(0); app.MainLoop() if __name__ == '__main__': 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. {{attachment:Organize_Your_Code_pics_wxGlade_allows_NAME.png}} 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''' ----