Cross Platform Desktop Applications with Python

Nathan R. Yergler

Creative Commons

Today's Presentation

Note that this presentation is using new fe self.Bind(wx.EVT_BUTTON, self.onCalculate, XRCCTRL(self, "CMD_CONVERT")) atures found in wxPython 2.5. You can do everything you see today in wxPython 2.4 (the "stable" branch), but it's often less "Python-ic".

ccPublisher Today

We've had an overwhelming response to ccPublisher, and I believe it's a testament to the power of Python and the community that we've been able to build an application of this size with a single developer, and with far fewer headaches than other projects I've worked on in the past.

Why wxPython?

wxPython provides...

In preparation for this presentation I took another look at the tkinter module documentation, and I'm embarassed to admit that I don't see anything (at a glance) that tkinter can't do that we needed. So maybe personal experience played into the decision more than it should have. I think that some of the issues we're going to talk about are applicable regardless of toolkit, especially the packaging issues.

Of course, wxPython isn't without it's problems. It has a tendancy to show it's C roots, although this is becoming less of a problem. I recommend using wxPython 2.5; it's technically the development branch, but lots of things are mapped in a more Python-ic manner.

wxPython will also occasionally it will let you do things the "wrong" way. And they'll work -- on some platforms. These bugs are the hardest to track down and I'll try to point out possible problem areas to you that I repeatedly ran into.

I've also taken a look at PyObjC for OS X, and while it definitly produces applications that behave better on OS X (in subtle ways), we don't have the resources to manage two code bases. It's possible that the ccPublisher 2 code base will support different UI front ends, but it's not a driving need.

10,000' View

In the first iteration of ccPublisher we (I) totally ignored the Model, rolling all the logic into the top-level window and application classes. While I could attempt to justify that decision, I've since begun working on a major refactoring effort which attempts to enforce a stronger separation between UI and logic. While this has resulted in some tougher "engineering" decisions, it's also resulted in cleaner, more maintainable code.

This leads me to the corrolarry to one of Jim Fulton's Laws of Engineering. One of Jim's Laws states "You can't solve a problem until you know the answer." My corrolary is "You can solve a problem if you know what the answer isn't."

Top-Level Window

wxPython provides a passable tool called XRCED for editting XRC files. It's far from perfect, but does allow you to do most editting tasks. Note that on OS X, you'll need to use the most recent version. Older versions had problems with properly displaying the widget properties.

XRCED

Do demo of creating XRC file, show structure.

Loading XRC

Sanity and Cross-Platform XRC

The GridBagSizer is a tempting widget, since it allows lots of flexibility in organizing widgets into columns/rows, and also supports spanning rows and columns. However, my experience has been that it has layout problems on OS X, especially when nested within another sizer. Therefore, it's often easier to use nested FlexGridSizers to accomplish the same task.

When inserting a static bitmap, the XRC specification requires you specify the explicit path to the image. Since the images and resources may be stored in different places depending on platform, I've found it easier to use a placeholder Panel in the XRC, and then load the image at run time. See my wiki for sample code.

The App Class

It seems to make sense to perform application-level "janitorial" tasks in the Application setup and tear-down code. Things such as loading/saving preferences, configuring platform specific paths and loading translation catalogs. For this reason we're starting to use XpApp, a specialized App class that just provides more hooks for customizing specific parts of the startup process.

The Currency Converter App

ConverterApp

Demo running the application.

Making Stuff Happen

Note that the Bind method, in it's simplest form, just connects an event type (in this case EVT_BUTTON) with a callable (in this case, self.onCalculate). Specifying a target widget, CMD_CONVERT, causes the event handling machinery to filter the events passed to the callable. If no target is specified, all events of the given type will be passed to the callable.

The Event Handler

    def onCalculate(self, event):

        dollars = XRCCTRL(self, "TXT_DOLLARS").GetValue()
        rate = XRCCTRL(self, "TXT_RATE").GetValue()
        
        euros = float(dollars) * float(rate)

        XRCCTRL(self, "TXT_EUROS").SetValue(str(euros))
Items of Note: * XRCCTRL * type casting (yes, it matters) After discussing, demo

Internationalization

The Message Catalog

Loading the Catalog

    def initI18n(self):
        # initialize the wxPython translation interface
        i18n_path = os.path.join(os.getcwd(), 'locale')

        wx.Locale.AddCatalogLookupPathPrefix(i18n_path)
        self.i18n = wx.Locale(wx.LANGUAGE_DEFAULT)
        
        self.i18n.AddCatalog('converter')
        
        # initialize the gettext interface
        gettext.install('converter', os.path.join(os.getcwd(), 'locale'), unicode=True)

Win32 GUI

The taskbar icon for both Windows and Linux is handled using the wxTaskBarIcon class. This class has a SetIcon method, which takes a wxIcon instance. This class can also manage the tooltip and popup menu. Note that on Linux, this only works with window managers which support the freedesktop.org System Tray Protocol. Gnome and KDE both do.

The Desktop Icon is set using the setup.py, and should contain multiple images (16x16, 32x32, 48x48). The png2ico utility is very useful for creating the appropriate file.

Mac OS X GUI

The Application Icon is actually specified in the setup.py call to py2app. We'll cover that later. If you install the Apple Developer Tools, you get the IcnsEditor application, which allows you to construct icon resource files from PNGs, etc.

In order to specify the About Box and other Application menu items, we create a menubar with the appropriate IDs. These IDs are fairly static, but can be retrieved dynamically from the wx.App class. For example, wx.App.GetMacAboutMenuItemId() will return the Menu ID which wxPython will dynamicaly use for the About entry on the Application Menu.

To establish the top of screen menu, simply create a menu and use the top window's SetMenuBar method. This will become the top of screen menu.

When dropping files on the application icon for Windows or Linux, the file names come in as sys.argv. For OS X, this only occurs if argv_emulation is enabled. Finally, to enable dropping files on the dock icon when the app is running, we need to implement the MacOpenFile method on the App class.

Linux GUI

Handling Bugs

Packaging

Freezing

Py2Exe

You can download Py2Exe from http://starship.python.net/crew/theller/py2exe/.

Building a Distribution

WiX is one of Microsoft's first open source projects, and is licensed under the CPL. It depends on the .NET framework.

WiX Subtleties

Py2App

As anyone who's developed for OS X knows, .app "files" are really bundles; that is, directories with a set structure that contain the entire application and it's dependencies.

Distributing your App

Cross-Platform setup.py

Unified setup.py


# check for win32 support
if sys.platform == 'win32':
    # win32 allows building of executables
    import py2exe

# check for OS X support
if sys.platform == 'darwin':
    import py2app
    pkg_data.append( ('../Frameworks', 
       ['/usr/local/lib/wxPython-unicode-2.5.3.1/lib/libwx_macud-2.5.3.rsrc'])
    )

setup.py


setup(name='CurrencyConverter',
      version='1.0',
      description = "",
      long_description="",
      url='http://creativecommons.org',
      author='Nathan R. Yergler',
      author_email='nathan@creativecommons.org',
      windows=[{'script':'converter_4.py',
               }],
      app = ['converter_4.py'],
      scripts=['converter_4.py',],
      options={"py2exe": {"packages": ["encodings", 'rdflib'], },
               "py2app": {"argv_emulation": True,
                          },
               },
      )

Cross-Platform Wishlist

Conclusion

Thanks!