This page contains a practical tutorial on using XRCed, a nice visual tool for editing XRC files. What is XRC? Other pages on this wiki describe it in detail; the short version is XRC allows you to create an XML file describing the widgets on every dialog/window/frame of your program. Rather than creating the widgets manually in dialog constructors, you tell XRC to create the widgets automatically by reading the XML file. While you can create this XML manually, most people use graphical tools because they are faster. XRCed is one of several tools that allow you to create your dialog graphically. The XML is created when you save your design.
Why use XRC? Many reasons: 1) it makes your code a lot shorter and more readable; 2) it speeds up the creation of dialogs; 3) it makes your code less buggy by doing the GUI grunt work for you.
My impression is that many programmers see XRCed as a basic visual GUI development tool; other tools like wxGlade and wxDesigner are often seen as more "professional" tools with more capability. I'll agree that other tools have more capability, but XRCed provides everything you need; sometimes simple is better. As a proof that XRCed can be used for "real" projects, look to my Picalo project. It is a full-featured, large GUI application written entirely in wxPython. Nearly every dialog in the program was done with XRCed.
I prefer XRCed for several reasons:
It is simple and straightforward. Because it does less, XRCed has less bugs and problems than some of the other designers.
- You get exactly what you design. The use of the XML tree to design the GUI allows exact precision in designing the dialog. I like this predictability compared with the other design tools.
Copy and paste are really easy with XRCed. Just grab the an entire XML subtree from one dialog and paste into another. While your first dialog might take a while to create, the second and third are really quick because so much can be copied (basic skeleton, OK and Cancel buttons, etc.)
The other main tools, like wxGlade and wxDesigner are also excellent projects. One reason I use Python is because it allows choice and competition between modules and tools.
I'll assume that you have XRCed running and know basic Python and wxPython coding practices. If you don't have it yet, XRCed comes in the wxPython "Docs, Demos, Samples, etc." distribution on the main download page. You obviously need to have Python and wxPython working on your machine to complete this tutorial.
XML, XRC, XRCed, huh?
Just to review for those that were confused by the previous section:
XML is a set of rules to write stuff in. The XRC information is written in this language.
XRC is the language you describe a dialog in. It contains your description of the hierarchy of frames, buttons, combo boxes, and other widgets in a dialog for your program. Look at the dialogs.xrc attachment further down this tutorial for an example of XRC. XRC is what your program actually reads and uses to make your dialogs.
XRCed is a visual creator for XRC. You could write the XRC by hand in a regular text editor, but you'd be crazy to do so (IMO). XRCed gives you a very nice, easy way to create the XRC. XRCed is only used by you, the programmer. It's output, XRC, is what your running program uses.
The Scenario
In the following paragraphs, I'll walk you through the design of my NatGeoDesktopBackground program (public domain license). This is a simple little program that scrapes National Geographic's web site for the picture of the day. It downloads, resizes, adds a description, and places the picture on your desktop each day. I won't go into the program logic but will concentrate on the creation of the dialog and skeleton application. Let me state up front that this GUI is not one I'm particularly proud of, but it works for what I need it to do.
The following is a screen shot of the running program:
Our job today is to create the above GUI using XRCed, write a skeleton wxPython program to display it, and attach events to some of the widgets.
Step 1: Create the GUI in XRCed
Every GUI is essentially a tree of components: the top-most frame (a component itself) contains components (like wxPanels and wxSizers), which in turn may contain even more components (like wxButtons and wxTextCtrls). While some designers allow you to "graphically" design the actual form, XRCed has you modify the tree of components. After making changes to the tree, you click the "test" icon at the top. You then get to see and interact with your actual dialog. In fact, the "test" button works by generating the XML code and invoking the XRC system to create the dialog -- in the exact way your program does at runtime. I like this model because I get to see the exact dialog in real time. The dialog looks the same in XRCed as it does in your program because it is the same. Again, predictability is a big time saver.
To start, click the "XML tree" item. Then click the wxDialog button near the top of the left-side icons. You'll get a new child in the tree with XML ID "DIALOG1". You can change this ID, the dialog title, or other parameters on the right side of the XRCed window. Change the XRC ID to "mainframe". Then ensure that the item is selected.
Sizers
To continue, you need to understand how wx components are organized on the screen. In the "old" days of programming, we just set components on dialogs in static pixel locations (and with exact pixel sizes). This worked well when every monitor and operating system had the same resolution and font size. Since wx is a cross-platform widget set, more modern layout paradigms are more appropriate. In wx, we generally use sizer components to specify widget locations in a dynamic fashion (wx actually supports other paradigms as well, but we'll use sizers here). A telltale sign of a good program is the use of dynamic widget locations rather than static locations.
XRCed lists six different sizer components in its icon panel. The most basic one, wxBoxSizer, splits the available space into one or more equal portions. Suppose you use a wxBoxSizer on the main dialog and the dialog is 100 pixels wide and 100 pixels high. A horizontal box sizer with two components would lay out the two components left and right. Each component would be sized at 50 pixels wide and 100 pixels high. If the user resized the dialog at runtime, the two components would be resized proportionally.
With the mainframe item selected, click the wxBoxSizer (it's orientation doesn't matter). With the new wxBoxSizer item selected, add another wxBoxSizer to it. Set the second wxBoxSizer to have the options shown in the graphic below:
Note the property group called sizeritem. The items in this group pertain to how any widget behaves in its parent sizer item. The reason we placed one sizer within another is we want to take advantage of its border property. A border of 10 places some spacing around the four sides of the main dialog of our program. The items in this group are described as follows:
proportion: How much of the parent's space this widget will be given. If one widget has a proportion of 1 and another has a proportion of 2, one widget will be twice as big as the other. As far as I know, this setting only has effect when you have a wxBoxSizer as the parent item.
flag: This is a set of flags that set how the widget expands into the space its parent gives it. You can look up a description of all the options elsewhere, but the two we'll use a lot are wxAll and wxEXPAND. wxAll tells the border setting to be applied to all four sides of the widget. wxEXPAND tells the widget to expand (horizontally and vertically) to fill the entire space given to it by its parent.
border: How much spacing around the widget should be left. Be sure to combine this with wxALL (or wxTOP, wxBOTTOM, wxLEFT, and/or wxRIGHT) to specify where the border is activated. Note that this is not a graphical border, just invisible spacing around the borders of the widget.
minsize: The minimum size of the component, if you want to set it. Use 0,0 format.
I always begin my dialogs and frames with two wxBoxSizer items to be able to control the border, even if I set the border to 0 at first (meaning I wanted widgets to go right up to the sides of the dialog). Who knows when I'll want to add border space at a later time.
Widgets
Next, add widgets to the dialog as you desire--using additional sizers to lay out widgets. The spacer widget can be used to provide spacing between components (borders can also be used). Be sure to test your dialog frequently to ensure you are getting the results you expect. I look at the test dialog after adding nearly every new control. Sizers can be extremely frustrating because they often act differently than you expect. My favorite part of XRCed is it allows you to immediately see the effect of different sizer settings -- effectively shortening the debugging process. As you gain experience, you'll be able to use sizers effectively and quickly; dynamic GUIs are worth the effort. In my view, static GUIs (with hard coded pixel locations for each widget) are not acceptable in today's programming world.
Rather than post screen shots of each item (and its properties) in the tree, load the following XML code into XRCed. If you start two copies of XRCed, you can have this one as a reference as you create your new one to match it. Pay special attention to my use of the proportion property and the Grid sizer to ensure things line up correctly on the left and right sides.
Please take some time now to make your dialog. I highly suggest you try to match mine because you have a working example (dialogs.xrc) to help you when things don't act the way you expect (which will absolutely happen).
Hint: XRC ID
The most important part of the dialog design is specifying exact, unique XRC IDs for any item you want to access from your Python code. Be sure to note the IDs of the widgets in my example XRC code above. All of the user-editable controls (widgets like wxComboBox, wxTextCtrl, wxButton, and wxRadioButton) on the dialog have unique IDs. These are used in MainFrame.py below.
Hint: Flags
You'll notice that every widget and sizer in XRCed has a number of flags you can set. Let me call attention to two flags in particular. I check these on almost every control I place on the form:
- wxALL - this specifies the whatever border you set applies to all sides of the widget. If you don't set a border, the wxALL does nothing (so why not flag it every time so you won't have to worry about it later).
wxEXPAND - this specifies that the control should fill up its entire space. Note that this has to be set throughout a tree hierarchy to work on the lowest-level control. In other words, if you set a ListBox to wxEXPAND but don't set it's parent sizer to wxEXPAND, the ListBox will expand into its parent; the parent sizer will not. And since sizers essentially take up as little space as possible, your ListBox will not expand into the space you expect it to. In addition, you often need to check the "proportion" option and set it to 1. Do this only when things don't expand as they should.
Hint: Additional Widgets
You'll notice that only a subset of the wx controls are listed in XRCed (24 as of this writing). There are a lot of controls that seem to be missing. A trick to adding the additional controls is to add a button, then right-click it in the tree (once you've added it), and replace it with another control. More controls are listed on the right-click replace menu!
Step 2: Create the Python Program
I assume that you now have a working dialog that matches mine above. In particular, be sure your XRC IDs for each user-editable control has the unique names that I assigned in my dialog. When you save the XRCed dialog, XRCed creates an XML document for you. Mine is called dialogs.xrc. We'll create two Python files: Main.py and MainFrame.py. Note that there's no specific reason to have these classes in two files; it's just my preference.
Main.py is the primary application script (the one you run). It really has nothing to do with XRC. It implements the rules of a general wx application. I'll first list the source code and then go through the important parts.
1 #!/usr/bin/env python
2 import MainFrame, wx
3
4 #############################
5 ### The main wx App
6
7 class NatGeoApp(wx.App):
8 '''Main National Geographic application class'''
9 def __init__(self):
10 '''Constructor.'''
11 wx.App.__init__(self)
12
13
14 def OnInit(self):
15 '''Sets everything up'''
16 # set up the main frame of the app
17 self.SetAppName('National Geographic Desktop Background')
18 self.mainframe = MainFrame.MainFrame()
19 self.mainframe.dlg.Show()
20 return True
21
22
23 #############################
24 ### Main startup code
25
26 app = NatGeoApp()
27 app.MainLoop()
The main class here, NatGeoApp, extends wx.App in the regular wx fashion. Per wx rules, it does setup in the OnInit method rather than in the constructor. The wx system calls the OnInit method automatically, so you can almost see it as a constructor. The method creates the MainFrame object and makes it visible. The OnInit method must return True so wx knows the dialog created successfully.
The final part of the file creates a NatGeoApp object and runs its MainLoop method (which is actually contained in the parent object, wx.App). MainLoop turns control over to the wx subsystem so it can respond to GUI events.
Step 3: Create the MainFrame Class
MainFrame.py is a wrapper around the XRC-based mainframe dialog. Since the XRC system only sets up passive components within the dialog, this class customizes widgets, attaches events, and adds processing where needed. The code is as follows:
1 #!/usr/bin/env python
2 import os, os.path, sys, wx
3 from wx import xrc
4
5 ########################################
6 ### Figure out the app directory and
7 ### set up the dialogxrc global
8
9 APPDIR = sys.path[0]
10 if os.path.isfile(APPDIR): #py2exe/py2app
11 APPDIR = os.path.dirname(APPDIR)
12 dialogxrc = None
13
14
15 ######################################
16 ### The main frame of the app
17
18 class MainFrame:
19 def __init__(self):
20 '''Constructor'''
21 global dialogxrc
22 # load the dialog
23 dialogxrc = xrc.XmlResource(os.path.join(APPDIR, 'dialogs.xrc'))
24 # note: sometimes doing the os.path.join with APPDIR seems to kill xrc.
25 # I have no idea why but try without it if you're having problems.
26 self.dlg = dialogxrc.LoadDialog(None, 'mainframe')
27
28 # customize widgets
29 self.getControl('deleteyes').SetValue(True)
30 self.getControl('textwidth').SetValue('100')
31 self.getControl('size').SetSelection(0)
32
33 # events
34 self.dlg.Bind(wx.EVT_CLOSE, self.onClose)
35 self.dlg.Bind(wx.EVT_IDLE, self.onIdle)
36 self.getControl('closeButton').Bind(wx.EVT_BUTTON, self.onClose)
37 self.getControl('exitButton').Bind(wx.EVT_BUTTON, self.onExit)
38 self.getControl('refreshButton').Bind(wx.EVT_BUTTON, self.onRefresh)
39 self.getControl('savetoButton').Bind(wx.EVT_BUTTON, self.onSaveToButton)
40 # here's an example of a menu item, although we don't have any in this app
41 # self.dlg.Bind(wx.EVT_MENU, self.eventHandlerMethod, self.getControl('menuItemName'))
42
43
44 def getControl(self, xmlid):
45 '''Retrieves the given control (within a dialog) by its xmlid'''
46 control = self.dlg.FindWindowById(xrc.XRCID(xmlid))
47 if control == None and self.dlg.GetMenuBar() != None: # see if on the menubar
48 control = self.dlg.GetMenuBar().FindItemById(xrc.XRCID(xmlid))
49 assert control != None, 'Programming error: a control with xml id ' + xmlid + ' was not found.'
50 return control
51
52
53 def onIdle(self, event):
54 '''Responds to idle time in the system'''
55 # when the timer says it's time, we do the actual downloading in the main thread (wx doesn't work well in secondary threads)
56
57
58 def onClose(self, event):
59 '''Closes the application'''
60 dlg = wx.MessageDialog(self.dlg, "Exit the program?", "Exit", wx.YES_NO | wx.ICON_QUESTION)
61 if dlg.ShowModal() == wx.ID_YES:
62 self.dlg.Destroy() # frame
63 dlg.Destroy()
64
65
66 def onExit(self, event):
67 '''Exit the app'''
68 self.Close(True)
69
70
71 def onSaveToButton(self, event):
72 '''Responds to the 'Browse...' button'''
73 saveto = wx.DirSelector('Please select the directory to save to:', self.getControl('saveto').GetValue())
74 if saveto:
75 self.getControl('saveto').SetValue(saveto)
76
77
78 def onRefresh(self, event):
79 '''Handles the refresh button'''
80 print "I should be refreshing the picture now, but the app logic is omitted in this tutorial"
Topmost Code
The APPDIR variable allows the program to find the dialogs.xrc file in a dynamic way (rather than hard coding the file location). It assumes that Main.py and dialogs.xrc exist in the same directory. The logic at the top of the file allows it to adjust to py2exe and py2app (see below for more on this).
The dialogxrc variable is declared (as None) at the top of the file, but the XRC file is not loaded until the constructor. See the next section for information on why I do this.
MainFrame Constructor
The constructor starts by loading the dialog into memory. In these lines, the XRC subsystem reads our dialogs.xrc XML file and creates every widget on the dialog. Awesome, huh? Rather than cluttering up the constructor with widget and sizer creation and layout, a single call creates the entire thing. I always use a variable called self.dlg to refer to the dialog the class is wrapping. This allows me to refer to the dialog throughout all the methods of the class.
If the main window had been a wx.Frame object, the code would have used dialogxrc.LoadFrame instead of dialogxrc.LoadDialog.
If you create the xrc.XmlResource object before the wxApp is initialized (i.e. before the MainFrame constructor), you'll get a warning message printed to the console. The error message seems to come directly from the wxWidgets C++ code, and there is no way to prevent or circumvent it other than to delay the dialog loading. Be sure to load your dialogs after the wxApp has had a chance to initialize.
The second portion of the constructor customizes widgets programatically. You can use this section to perform customization that cannot be done in the XRCed program. For example, this program reads a preferences file and sets the user controls to have the same values as last time the program was run. For this tutorial, I've simply set a few options statically to provide an example.
The third and final portion of the constructor attaches events to widgets on the screen. One of the most important methods in the dialog is getControl. This method uses the difficult-to-type FindWindowById method to get a reference to the named control. By simply sending in the unique XRC ID of a control you want to access, you can refer directly to the controls on the dialog. In larger applications, I often make this method a global method used throughout the program. Since this application has only one dialog, the function can be a method of the one and only wrapper class, MainFrame.
The events, such as wx.EVT_CLOSE and wx.EVT_BUTTON, are the wx constants to refer to different types of events. These can be found in the wxPython documentation for each type of widget (like wx.EVT_BUTTON for wxButtons) or in the "Event Handling Overview" topic in the wxPython documentation. After attaching the events in the constructor, the wx subsystem will call the appropriate methods when the events occur.
MainFrame onIdle
This really doesn't have anything to do with the example. However, I've bloodied my nose on this one enough that I though it might be useful to mention it. wxPython does not like being accessed from multiple threads. It must be run and used in the main program thread. If you use traditional python threading (like the threading module), your application will become unstable and eventually crash.
A partial workaround is to use the onIdle method. Note how I attach the wx.EVT_IDLE event constant to the onIdle method of the MainFrame wrapper class. The wxPython system will call this method repeatedly whenever it has free time (which is a lot). It allows you to do things that you might otherwise do in a regular thread. Carry on.
Event Handlers
The remainder of the class is the event handlers for button pushes, window hiding, and application close. These are straightforward, so I'll just refer you to the code for examples.
Getting Help
The wxPython documentation is pretty good once you know how to use it. Here's some hints:
The wxPython Demo application contains some of the best documentation because it shows you how to do most things. Be sure to use it as a primary resource.
The wxPython documentation is actually a modification of the wxWidgets C++ documentation. While the C++ programmers call the wxDirSelector() function (as shown in the docs), Python programmers would use wx.DirSelector() since we used the 'import wx' statement. In addition, sometimes the function names or parameters change a bit for the Python version of wx. This is always noted in the documentation in bold at the bottom of a function explanation. Watch for it.
Step 4: Run and Test!
The program is now ready to run. Run it with "python Main.py". Hopefully everything works as expected!
Hint: If the program starts, shows a quick dialog, and immediately exists, something is wrong with your code? You don't see any error messages because wx actually takes over the Python error stream and shows it in a GUI window. I think it does this because most GUI apps are run without a console, so anything printed to the regular Python console would never be seen by the user. In our case, though, it makes things difficult because the dialog closes too fast for us to see the error.
The temporary solution is to turn off the GUI error console. By sending False to the wx.App constructor, you tell wx not to take over the error console. In the example above, use the following constructor in Main.py:
Just be sure to remember to remove the False when you get the error fixed.
Step 5: Compile with py2exe and/or py2app
This step is not required, but most of your users will not have (or want to have) Python, wxPython, or any other dependencies your program requires. I highly recommend the excellent py2exe module on Windows and py2app module on Macintosh. PyInstaller on Windows is also supposed to be a great alternative to py2exe. I'll show py2exe here because I have more experience with it.
These Python modules wrap up your program into a nice executable for the two operating systems, respectively. For example, on Windows, py2exe creates a nice .exe program that users are familiar with. It embeds Python within the executable bundle, ensuring your program can run on any Windows system. Your users will never even know your program is written in Python.
The following is a very basic py2exe setup file:
1 #!/usr/bin/env python
2 from distutils.core import setup
3 import sys
4
5 # Generic options
6 options = {
7 'name': 'NatGeoDesktopBackground',
8 'version': '1',
9 'description': 'NatGeoDesktopBackground',
10 'long_description': 'National Geographic Desktop Background',
11 'author': 'http://warp.byu.edu/',
12 'author_email': 'conan@warp.byu,edu',
13 'url': 'http://warp.byu.edu/',
14 'packages': [
15 ],
16 'scripts': [
17 'Main.py',
18 ],
19 'data_files': [
20 ('', [ 'dialogs.xrc' ]),
21 ]
22 }
23
24 # this manifest enables the standard Windows XP/Vista-looking theme
25 manifest = """
26 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
27 <assembly xmlns="urn:schemas-microsoft-com:asm.v1"
28 manifestVersion="1.0">
29 <assemblyIdentity
30 version="0.64.1.0"
31 processorArchitecture="x86"
32 name="Controls"
33 type="win32"
34 />
35 <description>Picalo</description>
36 <dependency>
37 <dependentAssembly>
38 <assemblyIdentity
39 type="win32"
40 name="Microsoft.Windows.Common-Controls"
41 version="6.0.0.0"
42 processorArchitecture="X86"
43 publicKeyToken="6595b64144ccf1df"
44 language="*"
45 />
46 </dependentAssembly>
47 </dependency>
48 </assembly>
49 """
50
51 # windows specific for py2exe
52 if len(sys.argv) >= 2 and sys.argv[1] == 'py2exe':
53 try:
54 import py2exe
55 except ImportError:
56 print 'Could not import py2exe. Windows exe could not be built.'
57 sys.exit(0)
58 # windows-specific options
59 options['windows'] = [
60 {
61 'script':'Main.py',
62 'other_resources': [
63 ( 24, 1, manifest ),
64 ],
65 },
66 ]
67
68 # mac specific
69 if len(sys.argv) >= 2 and sys.argv[1] == 'py2app':
70 try:
71 import py2app
72 except ImportError:
73 print 'Could not import py2app. Mac bundle could not be built.'
74 sys.exit(0)
75 # mac-specific options
76 options['app'] = ['Main.py']
77 options['options'] = {
78 'py2app': {
79 'argv_emulation': True,
80 }
81 }
82
83 # run the setup
84 setup(**options)
Windows
Note that this can only be done on Windows. You cannot build a Windows .exe on Mac or Linux right now.
First install py2exe. To create the .exe file and support files, run python setup.py py2exe. After a bit of work, you'll have a dist/ subdirectory that contains almost everything you need. Try running the Main.exe program and see if Windows reports that it needs any .dll support files. On my Vista build, it requires msvcp71.dll in the same directory as the Main.exe file (this doesn't come with py2exe because of licensing issues, but it's easy to find on any Windows installation).
The manifest section above tells Windows to use the standard Windows controls and themes. This makes your program look like a real Windows XP or Vista program (which it is). There are also options you can include to set the program icon, add support libraries, and a host of other things. See http://www.py2exe.org/ for more information.
Now that you have a .exe program, why not go the next step and make a setup or msi installer? I like the "Inno Setup" free installation creator. With this free program, users of your program can install using the regular Windows methods, and they'll get an uninstall option for your program in the Add/Remove Programs section of the control panel. See the Inno Setup web site for more information.
Macintosh
Note that this can only be done on Macintosh. You cannot build a Macintosh app bundle on Windows or Linux right now.
First install py2app. To create the app bundle and support files, run python setup.py py2app. After a bit of work, you'll have a dist/ subdirectory that contains the application. It should run without any problems.
Use the hdiutil program (comes with your Mac) to wrap the app bundle up into a .dmg file for distribution. Most Mac users know how to copy your program from the .dmg file to their /Applications directory.
Linux/Unix
Distribution on Linux or BSD is a little more difficult because you need to wrap it up differently for each distribution. My understanding is that the freeze module (included with Python) is the right way to do it. If anyone wants to provide some insights into how to do this, please add to the tutorial. I am a big *nix (Debian, actually) fan, but I haven't looked much into the creation of deb or rpm or ports files for the different distros.
Author Information
This tutorial was written by Conan C. Albrecht in early 2008. You can contact me through my home page at http://warp.byu.edu/.