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