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:

  • 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

 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)

NOTE: To edit pages in this wiki you must be a member of the TrustedEditorsGroup.

AltStyle によって変換されたページ (->オリジナル) /