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:

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:

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:

natgeo.png

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:

sizer.png

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:

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:

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:

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:

   1   def __init__(self):
   2     '''Constructor.'''
   3     wx.App.__init__(self, False)

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/.

XRCed Tutorial (last edited 2010-09-30 15:45:13 by c-98-246-90-205)