== SmallApp Introduction == (last updated 03/06/2003) This is a brief sample program and supporting files which builds a small but complete Windows application using Python, wxPython, and py2exe. It demonstrates a number of things that seem to be left out of other introductory tutorials, but that I consider important in order to produce a 'real' Windows application. This does not try to teach Python or any of the supporting tools, so you need to review other tutorials for that. After you feel comfortable with the individual tools, you should be able to follow what is being done here. == Background == In trying to learn Python and be able to reproduce what I can accomplish with Visual Basic, it's been difficult to piece together all the items necessary to package a complete application. This is an attempt to demonstrate all the parts needed to build a small self-contained Python GUI app. It has been tested on Windows XP Pro, but should work on other platforms, The items I used for this include: * Python (!ActiveState's !ActivePython 2.2.1; 2.2.2 available) [[http://www.activestate.com/Products/ActivePython/]] * wxPython (2.3.3.1-unicode; 2.4.0.2 available ) [[http://www.wxpython.org/download.php]] * py2exe (0.3.3) [[http://starship.python.net/crew/theller/py2exe/]] * NSIS (1.98; 2.0b1 available) [[http://nsis.sourceforge.net/]] I also investigated the Boa, !PythonCard, and wxGlade products. The developers of all of those tools are doing some very neat things, but as with many open source products, there are a number of rough edges still present, and I decided to just drop down to the underlying toolkit (wxPython) to see if I could accomplish what I knew I could do in VB. This exercise has allowed me to verify that I can at least build a basic Windows application (that can probably also run on other platforms). My next test will be to do a slightly more complex application involving database interaction (a simple browse & data entry screen). == How to build the app == 1) I installed Python, wxPython, and py2exe on my PC. 2) I also installed wxGlade, !PythonCard, and Boa Constructor, though they aren't needed for this. You could use the !PythonCard or Boa Constructor editors for your wxPython programming, since they won't conflict with wxPython like PythonIDE does. This program was small enough that I just used a stand-alone editor (!EditPlus), but even if I don't use Boa for building my next app, I may still use the editor built into it, since it has some very nice interactive debugging support. 3) I wrote and tested the basic application. Many items are from the wxPython Wiki Getting Started tutorial, and some are from the DemoA.py sample. Other things are simple from digging into the wxPython documentation. A few things that I didn't see in other examples: * I consider accelerator keys critical to a 'real' Windows app. While some folks will provide menu accelerators (like "&File", or "E&xit"), I didn't see examples of "&Open" = "Ctrl-O". wxPython has wxAccelerator Entry and Table's, but those don't show up in the menu items. After I adjusted the menu items by manually placing the descriptions in place (tab separated), I found that I was able to comment out the wxAccelerator stuff! It's interesting that it should work this way. It really isn't described as behaving that way in the manual, and it would appear to require 'them' to parse the menu item text instead of just being able to get the actual flags & keycodes, but hey, if someone's going to make life easy for me, I'll say thanks and move on. I did check out both Alt & Ctrl accelerators, and they both seem to work, so it seems great! * Another thing that I want is my forms and application to have a meaningful icon associated with it. In order to get an icon on the form, you simply need to use the !SetIcon method of the wxFrame class. I just loaded the icon from an icon file in the current directory. It should be able to load it from a resource, but I didn't take the time to look into that method (or what might be different in working with resources in Python). (For one possible technique, ugly though it is, see the recipe LoadIconFromWin32Resources.) * I really like VB.NET's new min/max form settings and the way that controls can be anchored & grow/shrink with the form. From what I understand of wxPython sizers, it seems like they can do the same thing, but in a bit more convoluted fashion. This app only has one control on the main part of the form, so using sizers, it defines the minimum form size, and automatically grows with the form. We'll see how easy it is to handle a more typical form with a lot of controls on it in my next test application. === SmallApp.pyw === {{{ #!python ### SmallApp.py/pyw: ### Small test wxPython program along with py2Exe scripts to build a windows executable ### ToDo: ### - wrap RTB, menu items, etc in classes (see DemoA.py for a good example) import os import time from wxPython.wx import * #------------------------------------------------------------------------------- # Define booleans until Python ver 2.3 True=1 False=0 APP_NAME = "SmallApp" # --- Menu and control ID's ID_NEW=101 ID_OPEN=102 ID_SAVE=103 ID_SAVEAS=104 ID_EXIT=109 ID_ABOUT=141 ID_RTB=201 SB_INFO = 0 SB_ROWCOL = 1 SB_DATETIME = 2 #------------------------------------------------------------------------------- # --- our frame class class smallAppFrame(wxFrame): """ Derive a new class of wxFrame. """ def __init__(self, parent, id, title): # --- a basic window frame/form wxFrame.__init__(self, parent = None, id = -1, title = APP_NAME + " - Greg's 1st wxPython App", pos = wxPoint(200, 200), size = wxSize(379, 207), name = '', style = wxDEFAULT_FRAME_STYLE) # --- real windows programs have icons, so here's ours! # XXX see about integrating this into our app or a resource file try: # - don't sweat it if it doesn't load self.SetIcon(wxIcon("test.ico", wxBITMAP_TYPE_ICO)) finally: pass # --- add a menu, first build the menus (with accelerators fileMenu = wxMenu() fileMenu.Append(ID_NEW, "&New\tCtrl+N", "Creates a new file") EVT_MENU(self, ID_NEW, self.OnFileNew) fileMenu.Append(ID_OPEN, "&Open\tCtrl+O", "Opens an existing file") EVT_MENU(self, ID_OPEN, self.OnFileOpen) fileMenu.Append(ID_SAVE, "&Save\tCtrl+S", "Save the active file") EVT_MENU(self, ID_SAVE, self.OnFileSave) fileMenu.Append(ID_SAVEAS, "Save &As...", "Save the active file with a new name") EVT_MENU(self, ID_SAVEAS, self.OnFileSaveAs) fileMenu.AppendSeparator() fileMenu.Append(ID_EXIT, "E&xit\tAlt+Q", "Exit the program") EVT_MENU(self, ID_EXIT, self.OnFileExit) helpMenu = wxMenu() helpMenu.Append(ID_ABOUT, "&About", "Display information about the program") EVT_MENU(self, ID_ABOUT, self.OnHelpAbout) # --- now add them to a menubar & attach it to the frame menuBar = wxMenuBar() menuBar.Append(fileMenu, "&File") menuBar.Append(helpMenu, "&Help") self.SetMenuBar(menuBar) # Not needed!, just put them in text form after tab in menu item! # --- add accelerators to the menus #self.SetAcceleratorTable(wxAcceleratorTable([(wxACCEL_CTRL, ord('O'), ID_OPEN), # (wxACCEL_ALT, ord('Q'), ID_EXIT)])) # --- add a statusBar (with date/time panel) sb = self.CreateStatusBar(3) sb.SetStatusWidths([-1, 65, 150]) sb.PushStatusText("Ready", SB_INFO) # --- set up a timer to update the date/time (every 5 seconds) self.timer = wxPyTimer(self.Notify) self.timer.Start(5000) self.Notify() # - call it once right away # --- add a control (a RichTextBox) & trap KEY_DOWN event self.rtb = wxTextCtrl(self, ID_RTB, size=wxSize(400,200), style=wxTE_MULTILINE | wxTE_RICH2) ### - NOTE: binds to the control itself! EVT_KEY_UP(self.rtb, self.OnRtbKeyUp) # --- need to add a sizer for the control - yuck! self.sizer = wxBoxSizer(wxVERTICAL) # self.sizer.SetMinSize(200,400) self.sizer.Add(self.rtb, 1, wxEXPAND) # --- now add it to the frame (at least this auto-sizes the control!) self.SetSizer(self.sizer) self.SetAutoLayout(True) self.sizer.SetSizeHints(self) # --- initialize other settings self.dirName = "" self.fileName = "" # - this is ugly, but there's no static available # once we build a class for RTB, move this there self.oldPos = -1 self.ShowPos() # --- finally - show it! self.Show(True) #--------------------------------------- def __del__(self): """ Class delete event: don't leave timer hanging around! """ self.timer.stop() del self.timer #--------------------------------------- def Notify(self): """ Timer event """ t = time.localtime(time.time()) st = time.strftime(" %b-%d-%Y %I:%M %p", t) # --- could also use self.sb.SetStatusText self.SetStatusText(st, SB_DATETIME) #--------------------------------------- def OnFileExit(self, e): """ File|Exit event """ self.Close(True) #--------------------------------------- def OnFileNew(self, e): """ File|New event - Clear rtb. """ self.fileName = "" self.dirName = "" self.rtb.SetValue("") self.PushStatusText("Starting new file", SB_INFO) self.ShowPos() #--------------------------------------- def OnFileOpen(self, e): """ File|Open event - Open dialog box. """ dlg = wxFileDialog(self, "Open", self.dirName, self.fileName, "Text Files (*.txt)|*.txt|All Files|*.*", wxOPEN) if (dlg.ShowModal() == wxID_OK): self.fileName = dlg.GetFilename() self.dirName = dlg.GetDirectory() ### - this will read in Unicode files (since I'm using Unicode wxPython #if self.rtb.LoadFile(os.path.join(self.dirName, self.fileName)): # self.SetStatusText("Opened file: " + str(self.rtb.GetLastPosition()) + # " characters.", SB_INFO) # self.ShowPos() #else: # self.SetStatusText("Error in opening file.", SB_INFO) ### - but we want just plain ASCII files, so: try: f = file(os.path.join(self.dirName, self.fileName), 'r') self.rtb.SetValue(f.read()) self.SetTitle(APP_NAME + " - [" + self.fileName + "]") self.SetStatusText("Opened file: " + str(self.rtb.GetLastPosition()) + " characters.", SB_INFO) self.ShowPos() f.close() except: self.PushStatusText("Error in opening file.", SB_INFO) dlg.Destroy() #--------------------------------------- def OnFileSave(self, e): """ File|Save event - Just Save it if it's got a name. """ if (self.fileName != "") and (self.dirName != ""): try: f = file(os.path.join(self.dirName, self.fileName), 'w') f.write(self.rtb.GetValue()) self.PushStatusText("Saved file: " + str(self.rtb.GetLastPosition()) + " characters.", SB_INFO) f.close() return True except: self.PushStatusText("Error in saving file.", SB_INFO) return False else: ### - If no name yet, then use the OnFileSaveAs to get name/directory return self.OnFileSaveAs(e) #--------------------------------------- def OnFileSaveAs(self, e): """ File|SaveAs event - Prompt for File Name. """ ret = False dlg = wxFileDialog(self, "Save As", self.dirName, self.fileName, "Text Files (*.txt)|*.txt|All Files|*.*", wxSAVE) if (dlg.ShowModal() == wxID_OK): self.fileName = dlg.GetFilename() self.dirName = dlg.GetDirectory() ### - Use the OnFileSave to save the file if self.OnFileSave(e): self.SetTitle(APP_NAME + " - [" + self.fileName + "]") ret = True dlg.Destroy() return ret #--------------------------------------- def OnHelpAbout(self, e): """ Help|About event """ title = self.GetTitle() d = wxMessageDialog(self, "About " + title, title, wxICON_INFORMATION | wxOK) d.ShowModal() d.Destroy() #--------------------------------------- def OnRtbKeyUp(self, e): """ Update Row/Col indicator based on position """ self.ShowPos() e.Skip() #--------------------------------------- def ShowPos(self): """ Update Row/Col indicator """ (bPos,ePos) = self.rtb.GetSelection() if (self.oldPos != ePos): (c,r) = self.rtb.PositionToXY(ePos) self.SetStatusText(" " + str((r+1,c+1)), SB_ROWCOL) self.oldPos = ePos # --- end [testFrame] class #------------------------------------------------------------------------------- # --- Program Entry Point app = wxPySimpleApp() # --- note: Title never gets used! frame = smallAppFrame(NULL, -1, "Small wxPython Application") # frame.Show(True) # - now shown in class __init__ app.MainLoop() }}} 4) After I built & tested the application, I read the py2exe information and built a Setup.py file as per their example. I also created a Setup.cfg file which allows you to save file & version information in the resulting EXE file. I also wrote a little batch file (Build.cmd) which provides the command line parameters for py2exe. A couple of items to note here too: * The icon you can specify on the command line will be placed in the EXE, so it will show up in Windows Explorer, but I haven't figured out how to read that same icon from within the actual script. So for now, I need to still include the ICO file in my distribution. (use the [--icon] switch) * Instead of having to create a !SamplewxApp.pyw to prevent a console window from appearing in Windows, use the [--windows] switch in py2exe. * I found that if you use the Unicode version of the wxPython library, file operations will fail due to codec/encoding problems. This is noted on the py2exe page, along with the fix: use the [--packages encodings] switch - unfortunately, this increases the size of the EXE by about 160k! * py2exe generates a number of warnings for underlying wxWindows routines. I saw a message in one of the archives that stated that this was normal behavior due to the way that it is linked in. * You may be a bit shocked by the size of the distribution at first. The EXE is quite large due to being used to hold various scripts being used. The Python and wxPython supporting files add another 6 meg of files, but at least those can be packaged separately, or shared among multiple programs. We are definitely paying a price for the EXE package, and it might be nice to be able to specify which programs/scripts should be included in the EXE, and which could be put in a separate package in order to reduce the build size. === setup.py === {{{ #!python # setup.py from distutils.core import setup import py2exe setup(name="SmallApp", scripts=["SmallApp.pyw"], data_files=[(".", ["SmallApp.ico"])], ) }}} === setup.cfg === {{{ [py2exe] version-companyname=Semper Software, Inc. version-legalcopyright=Copyright (c) 2002 Semper Software, Inc. version-filedescription=File Description version-fileversion=0.10.1 version-legaltrademarks=Legal Trademarks version-productname=SmallApp version-productversion=ProdVer 0.10.1 }}} === build.cmd === {{{ python setup.py py2exe --icon SmallApp.ico }}} 5) The final part of writing an application is making it easy for your users to install it. After looking at a number of installers, I thought that !NullSoft's NSIS would be an appropriate one to use here. It's fairly easy to use, and there are a number of front ends available to help you generate and edit installation scripts if you like. One that I thought was promising is NSIS Workbench [[http://www.techmarc.co.uk/fnsis.htm]] Here's the script that I ended up with: === SmallwxApp.nsi === {{{ ;NSIS Script For SmallwxApp ;Background Colors BGGradient 0000FF 000000 FFFFFF ;Title Of Your Application Name "SmallwxApp" ;Do A CRC Check CRCCheck On ;Output File Name OutFile "SmallwxAppSetup.exe" ;The Default Installation Directory InstallDir "$PROGRAMFILES\SmallwxApp" ;The text to prompt the user to enter a directory DirText "Please select the folder below" Section "Install" ;Install Files SetOutPath $INSTDIR SetCompress Auto SetOverwrite IfNewer File "D:\Dev\Python22\Apps\SmallwxApp\dist\SmallwxApp\SmallwxApp.exe" File "D:\Dev\Python22\Apps\SmallwxApp\dist\SmallwxApp\_sre.pyd" File "D:\Dev\Python22\Apps\SmallwxApp\dist\SmallwxApp\python22.dll" File "D:\Dev\Python22\Apps\SmallwxApp\dist\SmallwxApp\SmallwxApp.ico" File "D:\Dev\Python22\Apps\SmallwxApp\dist\SmallwxApp\wxc.pyd" File "D:\Dev\Python22\Apps\SmallwxApp\dist\SmallwxApp\wxmsw233uh.dll" File "D:\Dev\Python22\Apps\SmallwxApp\dist\SmallwxApp\zlib.pyd" ; Write the uninstall keys for Windows WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\SmallwxApp" "DisplayName" "SmallwxApp (remove only)" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\SmallwxApp" "UninstallString" "$INSTDIR\Uninst.exe" WriteUninstaller "Uninst.exe" SectionEnd Section "Shortcuts" ;Add Shortcuts CreateDirectory "$SMPROGRAMS\SmallwxApp" CreateShortCut "$SMPROGRAMS\SmallwxApp\Small wxApp.lnk" "$INSTDIR\SmallwxApp.exe" "" "$INSTDIR\SmallwxApp.exe" 0 CreateShortCut "$DESKTOP\Small wxApp.lnk" "$INSTDIR\SmallwxApp.exe" "" "$INSTDIR\SmallwxApp.exe" 0 SectionEnd UninstallText "This will uninstall SmallwxApp from your system" Section Uninstall ;Delete Files Delete "$INSTDIR\SmallwxApp.exe" Delete "$INSTDIR\_sre.pyd" Delete "$INSTDIR\python22.dll" Delete "$INSTDIR\SmallwxApp.ico" Delete "$INSTDIR\wxc.pyd" Delete "$INSTDIR\wxmsw233uh.dll" Delete "$INSTDIR\zlib.pyd" Delete "$DESKTOP\Small wxApp.lnk" ;Delete Start Menu Shortcuts Delete "$SMPROGRAMS\SmallwxApp\*.*" RmDir "$SMPROGRAMS\SmallwxApp" ;Delete Uninstaller And Unistall Registry Entries Delete "$INSTDIR\Uninst.exe" DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\SmallwxApp" DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\SmallwxApp" RMDir "$INSTDIR" SectionEnd }}} It's not fancy, just installs all of the files into the target directory and creates a couple of shortcuts. As I mentioned above, the final set of files is over 6 meg, though NSIS gets this down to under 3 meg. The DLLs and wxc.pyd make up most of this, and could be packaged in a separate runtime setup to reduce the setup file size (The DLLs, and I expect the wxc.pyd file, could also be copied into the appropriate windows system directory if you would like to share them between applications). That's about it for this project. The main thing that I was testing here was whether I could create a stand-alone app that, looks behaves, and installs like a regular Windows app. I believe that this demonstrates that it can be done. Good luck on your Python projects. == Comments == Hope that this can help others trying to build a complete, self-contained Python app on Windows. Good luck! Page originally created by:<
> -- Greg Brunet<
> [gbrunet(at)sempersoft.com]<
> http://www.SemperSoftware.com