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:

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:

SmallApp.pyw

   1 ### SmallApp.py/pyw:
   2 ###     Small test wxPython program along with py2Exe scripts to build a windows executable
   3 ###     ToDo:
   4 ###         - wrap RTB, menu items, etc in classes (see DemoA.py for a good example)
   5 
   6 import os
   7 import time
   8 from wxPython.wx import *
   9 
  10 #-------------------------------------------------------------------------------
  11 # Define booleans until Python ver 2.3
  12 True=1
  13 False=0
  14 
  15 APP_NAME = "SmallApp"
  16 
  17 # --- Menu and control ID's
  18 ID_NEW=101
  19 ID_OPEN=102
  20 ID_SAVE=103
  21 ID_SAVEAS=104
  22 ID_EXIT=109
  23 ID_ABOUT=141
  24 ID_RTB=201
  25 
  26 SB_INFO = 0
  27 SB_ROWCOL = 1
  28 SB_DATETIME = 2
  29 
  30 #-------------------------------------------------------------------------------
  31 # --- our frame class
  32 class smallAppFrame(wxFrame):
  33     """ Derive a new class of wxFrame. """
  34 
  35 
  36     def __init__(self, parent, id, title):
  37         # --- a basic window frame/form
  38         wxFrame.__init__(self, parent = None, id = -1, 
  39                          title = APP_NAME + " - Greg's 1st wxPython App", 
  40                          pos = wxPoint(200, 200), size = wxSize(379, 207), 
  41                          name = '', style = wxDEFAULT_FRAME_STYLE)
  42 
  43         # --- real windows programs have icons, so here's ours!
  44         # XXX see about integrating this into our app or a resource file
  45         try:            # - don't sweat it if it doesn't load
  46             self.SetIcon(wxIcon("test.ico", wxBITMAP_TYPE_ICO))
  47         finally:
  48             pass
  49 
  50         # --- add a menu, first build the menus (with accelerators
  51         fileMenu = wxMenu()
  52 
  53         fileMenu.Append(ID_NEW, "&New\tCtrl+N", "Creates a new file")
  54         EVT_MENU(self, ID_NEW, self.OnFileNew)
  55         fileMenu.Append(ID_OPEN, "&Open\tCtrl+O", "Opens an existing file")
  56         EVT_MENU(self, ID_OPEN, self.OnFileOpen)
  57         fileMenu.Append(ID_SAVE, "&Save\tCtrl+S", "Save the active file")
  58         EVT_MENU(self, ID_SAVE, self.OnFileSave)
  59         fileMenu.Append(ID_SAVEAS, "Save &As...", "Save the active file with a new name")
  60         EVT_MENU(self, ID_SAVEAS, self.OnFileSaveAs)
  61 
  62         fileMenu.AppendSeparator()
  63         fileMenu.Append(ID_EXIT, "E&xit\tAlt+Q", "Exit the program")
  64         EVT_MENU(self, ID_EXIT, self.OnFileExit)
  65 
  66         helpMenu = wxMenu()
  67         helpMenu.Append(ID_ABOUT, "&About", "Display information about the program")
  68         EVT_MENU(self, ID_ABOUT, self.OnHelpAbout)
  69 
  70         # --- now add them to a menubar & attach it to the frame
  71         menuBar = wxMenuBar()
  72         menuBar.Append(fileMenu, "&File")
  73         menuBar.Append(helpMenu, "&Help")
  74         self.SetMenuBar(menuBar)
  75 
  76         #  Not needed!, just put them in text form after tab in menu item!
  77         # --- add accelerators to the menus
  78         #self.SetAcceleratorTable(wxAcceleratorTable([(wxACCEL_CTRL, ord('O'), ID_OPEN), 
  79         #                          (wxACCEL_ALT, ord('Q'), ID_EXIT)]))
  80 
  81         # --- add a statusBar (with date/time panel)
  82         sb = self.CreateStatusBar(3)
  83         sb.SetStatusWidths([-1, 65, 150])
  84         sb.PushStatusText("Ready", SB_INFO)
  85         # --- set up a timer to update the date/time (every 5 seconds)
  86         self.timer = wxPyTimer(self.Notify)
  87         self.timer.Start(5000)
  88         self.Notify()       # - call it once right away
  89 
  90         # --- add a control (a RichTextBox) & trap KEY_DOWN event
  91         self.rtb = wxTextCtrl(self, ID_RTB, size=wxSize(400,200), 
  92                               style=wxTE_MULTILINE | wxTE_RICH2)
  93         ### - NOTE: binds to the control itself!
  94         EVT_KEY_UP(self.rtb, self.OnRtbKeyUp)
  95 
  96         # --- need to add a sizer for the control - yuck!
  97         self.sizer = wxBoxSizer(wxVERTICAL)
  98         # self.sizer.SetMinSize(200,400)
  99         self.sizer.Add(self.rtb, 1, wxEXPAND)
 100         # --- now add it to the frame (at least this auto-sizes the control!)
 101         self.SetSizer(self.sizer)
 102         self.SetAutoLayout(True)
 103         self.sizer.SetSizeHints(self)
 104 
 105         # --- initialize other settings
 106         self.dirName = ""
 107         self.fileName = ""
 108 
 109         # - this is ugly, but there's no static available 
 110         #   once we build a class for RTB, move this there
 111         self.oldPos = -1
 112         self.ShowPos()
 113 
 114         # --- finally - show it!
 115         self.Show(True)
 116 
 117 #---------------------------------------
 118     def __del__(self):
 119         """ Class delete event: don't leave timer hanging around! """
 120         self.timer.stop()
 121         del self.timer
 122 
 123 #---------------------------------------
 124     def Notify(self):
 125         """ Timer event """
 126         t = time.localtime(time.time())
 127         st = time.strftime(" %b-%d-%Y  %I:%M %p", t)
 128         # --- could also use self.sb.SetStatusText
 129         self.SetStatusText(st, SB_DATETIME)
 130 
 131 #---------------------------------------
 132     def OnFileExit(self, e):
 133         """ File|Exit event """
 134         self.Close(True)
 135 
 136 #---------------------------------------
 137     def OnFileNew(self, e):
 138         """ File|New event - Clear rtb. """
 139         self.fileName = ""
 140         self.dirName = ""
 141         self.rtb.SetValue("")
 142         self.PushStatusText("Starting new file", SB_INFO)
 143         self.ShowPos()
 144 
 145 #---------------------------------------
 146     def OnFileOpen(self, e):
 147         """ File|Open event - Open dialog box. """
 148         dlg = wxFileDialog(self, "Open", self.dirName, self.fileName, 
 149                            "Text Files (*.txt)|*.txt|All Files|*.*", wxOPEN)
 150         if (dlg.ShowModal() == wxID_OK):
 151             self.fileName = dlg.GetFilename()
 152             self.dirName = dlg.GetDirectory()
 153 
 154             ### - this will read in Unicode files (since I'm using Unicode wxPython
 155             #if self.rtb.LoadFile(os.path.join(self.dirName, self.fileName)):
 156             #    self.SetStatusText("Opened file: " + str(self.rtb.GetLastPosition()) + 
 157             #                       " characters.", SB_INFO)
 158             #    self.ShowPos()
 159             #else:
 160             #    self.SetStatusText("Error in opening file.", SB_INFO)
 161 
 162             ### - but we want just plain ASCII files, so:
 163             try:
 164                 f = file(os.path.join(self.dirName, self.fileName), 'r')
 165                 self.rtb.SetValue(f.read())
 166                 self.SetTitle(APP_NAME + " - [" + self.fileName + "]")
 167                 self.SetStatusText("Opened file: " + str(self.rtb.GetLastPosition()) + 
 168                                    " characters.", SB_INFO)
 169                 self.ShowPos()
 170                 f.close()
 171             except:
 172                 self.PushStatusText("Error in opening file.", SB_INFO)
 173         dlg.Destroy()
 174 
 175 #---------------------------------------
 176     def OnFileSave(self, e):
 177         """ File|Save event - Just Save it if it's got a name. """
 178         if (self.fileName != "") and (self.dirName != ""):
 179             try:
 180                 f = file(os.path.join(self.dirName, self.fileName), 'w')
 181                 f.write(self.rtb.GetValue())
 182                 self.PushStatusText("Saved file: " + str(self.rtb.GetLastPosition()) + 
 183                                     " characters.", SB_INFO)
 184                 f.close()
 185                 return True
 186             except:
 187                 self.PushStatusText("Error in saving file.", SB_INFO)
 188                 return False
 189         else:
 190             ### - If no name yet, then use the OnFileSaveAs to get name/directory
 191             return self.OnFileSaveAs(e)
 192 
 193 #---------------------------------------
 194     def OnFileSaveAs(self, e):
 195         """ File|SaveAs event - Prompt for File Name. """
 196         ret = False
 197         dlg = wxFileDialog(self, "Save As", self.dirName, self.fileName, 
 198                            "Text Files (*.txt)|*.txt|All Files|*.*", wxSAVE)
 199         if (dlg.ShowModal() == wxID_OK):
 200             self.fileName = dlg.GetFilename()
 201             self.dirName = dlg.GetDirectory()
 202             ### - Use the OnFileSave to save the file
 203             if self.OnFileSave(e):
 204                 self.SetTitle(APP_NAME + " - [" + self.fileName + "]")
 205                 ret = True
 206         dlg.Destroy()
 207         return ret
 208 
 209 #---------------------------------------
 210     def OnHelpAbout(self, e):
 211         """ Help|About event """
 212         title = self.GetTitle()
 213         d = wxMessageDialog(self, "About " + title, title, wxICON_INFORMATION | wxOK)
 214         d.ShowModal()
 215         d.Destroy()
 216 
 217 #---------------------------------------
 218     def OnRtbKeyUp(self, e):
 219         """ Update Row/Col indicator based on position """
 220         self.ShowPos()
 221         e.Skip()
 222 
 223 #---------------------------------------
 224     def ShowPos(self):
 225         """ Update Row/Col indicator """
 226         (bPos,ePos) = self.rtb.GetSelection()
 227         if (self.oldPos != ePos):
 228             (c,r) = self.rtb.PositionToXY(ePos)
 229             self.SetStatusText(" " + str((r+1,c+1)), SB_ROWCOL)
 230         self.oldPos = ePos
 231 
 232 # --- end [testFrame] class
 233 
 234 
 235 #-------------------------------------------------------------------------------
 236 # --- Program Entry Point
 237 app = wxPySimpleApp()
 238 # --- note: Title never gets used!
 239 frame = smallAppFrame(NULL, -1, "Small wxPython Application")
 240 # frame.Show(True)  # - now shown in class __init__
 241 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:

setup.py

   1 # setup.py
   2 from distutils.core import setup
   3 import py2exe
   4 
   5 setup(name="SmallApp",
   6       scripts=["SmallApp.pyw"],
   7       data_files=[(".",
   8                    ["SmallApp.ico"])],
   9 )

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

SmallApp (last edited 2008-03-11 10:50:28 by localhost)