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
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:
- 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
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