wxPython 2.8: Window Layout and Design

Exclusive offer: get 50% off this eBook here
wxPython 2.8 Application Development Cookbook

wxPython 2.8 Application Development Cookbook — Save 50%

Over 80 practical recipes for developing feature-rich applications using wxPython

$23.99    $12.00
by Cody Precord | December 2010 | Open Source

In today's world of desktop applications, there is a great amount of incentive to be able to develop applications that can run in more than one environment. Currently, there are a handful of options available for cross-platform frameworks to develop desktop applications in Python. wxPython is one such cross-platform GUI toolkit for the Python programming language. It allows Python programmers to create programs with a complete, highly-functional graphical user interface, simply and easily.

In this article by Cody Precord, author of the book wxPython 2.8 Application Development Cookbook, we will cover:

  • Using a BoxSizer
  • Understanding proportions, flags, and borders
  • Laying out controls with the GridBagSizer
  • Standard dialog button layout
  • Using XML resources
  • Making a custom resource handler
  • Using the AuiFrameManager

 

wxPython 2.8 Application Development Cookbook

wxPython 2.8 Application Development Cookbook

Over 80 practical recipes for developing feature-rich applications using wxPython

  • Develop flexible applications in wxPython.
  • Create interface translatable applications that will run on Windows, Macintosh OSX, Linux, and other UNIX like environments.
  • Learn basic and advanced user interface controls.
  • Packed with practical, hands-on cookbook recipes and plenty of example code, illustrating the techniques to develop feature rich applications using wxPython.
        Read more about this book      

(For more resources on Python, see here.)

Introduction

Once you have an idea of how the interface of your applications should look, it comes the time to put it all together. Being able to take your vision and translate it into code can be a tricky and often tedious task. A window's layout is defined on a two dimensional plane with the origin being the window's top-left corner. All positioning and sizing of any widgets, no matter what it's onscreen appearance, is based on rectangles. Clearly understanding these two basic concepts goes a long way towards being able to understand and efficiently work with the toolkit.

Traditionally in older applications, window layout was commonly done by setting explicit static sizes and positions for all the controls contained within a window. This approach, however, can be rather limiting as the windows will not be resizable, they may not fit on the screen under different resolutions, trying to support localization becomes more difficult because labels and other text will differ in length in different languages, the native widgets will often be different sizes on different platforms making it difficult to write platform independent code, and the list goes on.

So, you may ask what the solution to this is. In wxPython, the method of choice is to use the Sizer classes to define and manage the layout of controls. Sizers are classes that manage the size and positioning of controls through an algorithm that queries all of the controls that have been added to the Sizer for their recommended best minimal sizes and their ability to stretch or not, if the amount of available space increases, such as if a user makes a dialog bigger. Sizers also handle cross-platform widget differences, for example, buttons on GTK tend to have an icon and be generally larger than the buttons on Windows or OS X. Using a Sizer to manage the button's layout will allow the rest of the dialog to be proportionally sized correctly to handle this without the need for any platform-specific code.

So let us begin our adventure into the world of window layout and design by taking a look at a number of the tools that wxPython provides in order to facilitate this task.

Using a BoxSizer

A BoxSizer is the most basic of Sizer classes. It supports a layout that goes in a single direction—either a vertical column or a horizontal row. Even though it is the most basic to work with, a BoxSizer is one of the most useful Sizer classes and tends to produce more consistent cross-platform behavior when compared to some of the other Sizers types. This recipe creates a simple window where we want to have two text controls stacked in a vertical column, each with a label to the left of it. This will be used to illustrate the most simplistic usage of a BoxSizer in order to manage the layout of a window's controls.

How to do it...

Here we define our top level Frame, which will use a BoxSizer to manage the size of its Panel:

class BoxSizerFrame(wx.Frame):
    def __init__(self, parent, *args, **kwargs):
        super(BoxSizerFrame, self).__init__(*args, **kwargs)

        # Attributes
        self.panel = BoxSizerPanel(self)

        # Layout
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.panel, 1, wx.EXPAND)
        self.SetSizer(sizer)
        self.SetInitialSize()

The BoxSizerPanel class is the next layer in the window hierarchy, and is where we will perform the main layout of the controls:

class BoxSizerPanel(wx.Panel):
    def __init__(self, parent, *args, **kwargs):
        super(BoxSizerPanel, self).__init__(*args, **kwargs)

        # Attributes
        self._field1 = wx.TextCtrl(self)
        self._field2 = wx.TextCtrl(self)

        # Layout
        self._DoLayout()

Just to help reduce clutter in the __init__ method, we will do all the layout in a separate _DoLayout method:

def _DoLayout(self):
    """Layout the controls"""
    vsizer = wx.BoxSizer(wx.VERTICAL)
    field1_sz = wx.BoxSizer(wx.HORIZONTAL)
    field2_sz = wx.BoxSizer(wx.HORIZONTAL)

    # Make the labels
    field1_lbl = wx.StaticText(self, label="Field 1:")
    field2_lbl = wx.StaticText(self, label="Field 2:")

    # Make the first row by adding the label and field
    # to the first horizontal sizer
    field1_sz.AddSpacer(50)
    field1_sz.Add(field1_lbl)
    field1_sz.AddSpacer(5) # put 5px of space between
    field1_sz.Add(self._field1)
    field1_sz.AddSpacer(50)
  
    # Do the same for the second row
    field2_sz.AddSpacer(50)
    field2_sz.Add(field2_lbl)
    field2_sz.AddSpacer(5)
    field2_sz.Add(self._field2)
    field2_sz.AddSpacer(50)

    # Now finish the layout by adding the two sizers
    # to the main vertical sizer.
    vsizer.AddSpacer(50)
    vsizer.Add(field1_sz)
    vsizer.AddSpacer(15)
    vsizer.Add(field2_sz)
    vsizer.AddSpacer(50)

    # Finally assign the main outer sizer to the panel
    self.SetSizer(vsizer)

How it works...

The previous code shows the basic pattern of how to create a simple window layout programmatically, using sizers to manage the controls. First let's start by taking a look at the BoxSizerPanel class's _DoLayout method, as this is where the majority of the layout in this example takes place.

First, we started off by creating three BoxSizer classes: one with a vertical orientation, and two with a horizontal orientation. The layout we desired for this window requires us to use three BoxSizer classes and this is why. If you break down what we want to do into simple rectangles, you will see that:

  1. We wanted two TextCtrl objects each with a label to the left of them which can simply be thought of as two horizontal rectangles.
  2. We wanted the TextCtrl objects stacked vertically in the window which is just a vertical rectangle that will contain the other two rectangles.

This is illustrated by the following screenshot (borders are drawn in and labels are added to show the area managed by each of Panel's three BoxSizers):

In the section where we populate the first horizontal sizer (field1_sz), we use two of the BoxSizer methods to add items to the layout. The first is AddSpacer, which does simply as its named and adds a fixed amount of empty space in the left-hand side of the sizer. Then we use the Add method to add our StaticText control to the right of the spacer, and continue from here to add other items to complete this row. As you can see, these methods add items to the layout from left to right in the sizer. After this, we again do the same thing with the other label and TextCtrl in the second horizontal sizer.

The last part of the Panel's layout is done by adding the two horizontal sizers to the vertical sizer. This time, since the sizer was created with a VERTICAL orientation, the items are added from top to bottom. Finally, we use the Panel's SetSizer method to assign the main outer BoxSizer as the Panel's sizer.

The BoxSizerFrame also uses a BoxSizer to manage the layout of its Panel. The only difference here is that we used the Add method's proportion and flags parameters to tell it to make the Panel expand to use the entire space available. After setting the Frame's sizer, we used its SetInitialSize method, which queries the window's sizer and its descendents to get and set the best minimal size to set the window to. We will go into more detail about these other parameters and their effects in the next recipe.

There's more...

Included below is a little more additional information about adding spacers and items to a sizer's layout.

Spacers

The AddSpacer will add a square-shaped spacer that is X pixels wide by X pixels tall to the BoxSizer, where X is the value passed to the AddSpacer method. Spacers of other dimensions can be added by passing a tuple as the first argument to the BoxSizer's Add method.

someBoxSizer.Add((20,5))

This will add a 20x5 pixel spacer to the sizer. This can be useful when you don't want the vertical space to be increased by as much as the horizontal space, or vice versa.

AddMany

The AddMany method can be used to add an arbitrary number of items to the sizer in one call. AddMany takes a list of tuples that contain values that are in the same order as the Add method expects.

someBoxSizer.AddMany([(staticText,),
                      ((10, 10),),
                      (txtCtrl, 0, wx.EXPAND)]))

This will add three items to the sizer: the first two items only specify the one required parameter, and the third specifies the proportion and flags parameters.

wxPython 2.8 Application Development Cookbook Over 80 practical recipes for developing feature-rich applications using wxPython
Published: December 2010
eBook Price: $23.99
Book Price: $39.99
See more
Select your format and quantity:
        Read more about this book      

(For more resources on Python, see here.)

Understanding proportions, flags, and borders

Through the use of the optional parameters in a sizer's various Add methods, it is possible to control the relative proportions, alignment, and padding around every item that is managed by the sizer. Without using these additional settings, all the items in the sizer will just use their "best" minimum size and will be aligned to the top-left of the rectangle of space that the sizer provides. This means that the controls will not stretch or contract when the window is resized. Also, for example, if in a horizontal row of items in a BoxSizer one of the items has a greater height than some of the other items in that same row, they may not be aligned as desired (see the following diagram).

This diagram illustrates an alignment issue that can occur when some controls have a different-sized rectangle than the one next to it. This is a realistic example of a problem that can occur on GTK (Linux), as its ComboBoxes tend to be much taller than a StaticTextCtrl. So where on other platforms these two controls may appear to be properly center-aligned, they will look like this on Linux.

This recipe will re-implement the previous recipe's BoxSizerPanel, using these additional Add parameters to improve its layout, in order to show how these parameters can be used to influence how the sizer manages each of the controls that have been added to it.

Getting Started

Before getting started on this recipe, make sure you have reviewed the previous recipe, Using a BoxSizer, as we will be modifying its _DoLayout method in this recipe to define some additional behaviors that the sizers should apply to its layout.

How to do it...

Here, we will make some modifications to the SizerItems proportions, flags, and borders to change the behavior of the layout:

def _DoLayout(self):
    """Layout the controls"""
    vsizer = wx.BoxSizer(wx.VERTICAL)
    field1_sz = wx.BoxSizer(wx.HORIZONTAL)
    field2_sz = wx.BoxSizer(wx.HORIZONTAL)

    # Make the labels
    field1_lbl = wx.StaticText(self, label="Field 1:")
    field2_lbl = wx.StaticText(self, label="Field 2:")

    # 1) HORIZONTAL BOXSIZERS
    field1_sz.Add(field1_lbl, 0,
                  wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, 5)
    field1_sz.Add(self._field1, 1, wx.EXPAND)

    field2_sz.Add(field2_lbl, 0,
                  wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, 5)
    field2_sz.Add(self._field2, 1, wx.EXPAND)

    # 2) VERTICAL BOXSIZER
    vsizer.AddStretchSpacer()
    BOTH_SIDES = wx.EXPAND|wx.LEFT|wx.RIGHT
    vsizer.Add(field1_sz, 0, BOTH_SIDES|wx.TOP, 50)
    vsizer.AddSpacer(15)
    vsizer.Add(field2_sz, 0, BOTH_SIDES|wx.BOTTOM, 50)
    vsizer.AddStretchSpacer()

    # Finally assign the main outer sizer to the panel
    self.SetSizer(vsizer)

How it works...

This recipe just shows what we changed in the previous recipe's __DoLayout method to take advantage of some of these extra options. The first thing to notice in the section where we add the controls to the horizontal sizers is that we no longer have the AddSpacer calls. These have been replaced by specifying a border in the Add calls. When adding each of the labels we added two sizer flags, ALIGN_CENTER_VERTICAL and RIGHT. The first flag is an alignment flag that specifies the desired behavior of the alignment and the second is a border flag that specifies where we want the border parameter to be applied. In this case, the sizer will align the StaticText in the center of the vertical space and add a 5px padding to the right side of it.

Next, where we add the TextCtrl objects to the sizer, we specified a 1 for the proportion and EXPAND for the sizer flag. Setting the proportion greater than the default of 0 will tell the sizer to give that control proportionally more of the space in the sizer's managed area. A proportion value greater than 0 in combination with the EXPAND flag which tells the control to get bigger as space is available will let it stretch as the dialog is resized to a bigger size. Typically you will only need to specify 0 or 1 for the proportion parameter, but under some complex layouts it may be necessary to give different controls a relatively different amount of the total available space. For example, in a layout with two controls if both are given a proportion of 1, they would each get 50 percent of the space. Changing the proportion of one of the controls to 2 would change the space allocation to a 66/33 percent balance.

We also made some changes to the final layout with the vertical sizer. First, instead of using the regular AddSpacer function to add some static spacers to the layout, we changed it to use AddStretchSpacer instead. AddStretchSpacer is basically the equivalent of doing Add((-1,-1), 1, wx.EXPAND), which just adds a spacer of indeterminate size that will stretch as the window size is changed. This allows us to keep the controls in the center of the dialog as its vertical size changes.

Finally, when adding the two horizontal sizers to the vertical sizer, we used some flags to apply a static 50px of spacing around the LEFT, RIGHT, and TOP or BOTTOM of the sizers. It's also important to notice that we once again passed the EXPAND flag. If we did not do this, the vertical sizer would not allow those two items to expand which in turn would nullify us adding the EXPAND flag for the TextCtrl objects. Try running this and the previous sample side-by-side and resizing each window to see the difference in behavior.

The previous screenshot has had some lines drawn over it to show the five items that are managed by the main top level VERTICAL sizer vsizer.

There's more...

There are a number of flags that can be used to affect the layout in various ways. The following three tables list the different categories of these flags that can be combined in the flag's bitmask:

Alignment flags

This table shows a listing of all the alignment flags and a description of what each one does:

Alignment flags

Description

wx.ALIGN_TOP

Align the item to the top of the available space

wx.ALIGN_BOTTOM

Align the item to the bottom of the available space

wx.ALIGN_LEFT

Align the item to the left of the available space

wx.ALIGN_RIGHT

Align the item to the right of the available space

wx.ALIGN_CENTER_VERTICAL

wx.ALIGN_CENTRE_VERTICAL

Align the item in the center of the vertical space

wx.ALIGN_CENTER_HORIZONTAL

wx.ALIGN_CENTRE_HORIZONTAL

Align the item in the center of the horizontal space

Border flags

The following flags can be used to control which side(s) of the control the border argument of the Sizer's Add method is applied to:

Border flags

Description

wx.TOP

Apply the border to the top of the item

wx.BOTTOM

Apply the border to the bottom of item

wx.LEFT

Apply the border to the left of the item

wx.RIGHT

Apply the border to the right of the item

wx.ALL

Apply the border to all sides of the item

Behavior flags

The sizer flags in this table can be used to control how a control is resized within a sizer:

Behaviour flags

Description

wx.EXPAND

Item will expand to fill the space provided to it (wx.GROW is the same)

wx.SHAPED

Similar to EXPAND but maintains the item's aspect ratio

wx.FIXED_MINSIZE

Don't let the item become smaller than its initial minimum size

wx.RESERVE_SPACE_EVEN_IF_HIDDEN

Don't allow the sizer to reclaim an item's space when it is hidden

Laying out controls with the GridBagSizer

There are a number of other types of sizers in wxPython, besides the BoxSizer, that are designed to help simplify different kinds of layouts. The GridSizer, FlexGridSizer, and GridBagSizer can be used to lay items out in a grid-like manner. The GridSizer provides a fixed grid layout where items are added into different "cells" in the grid. The FlexGridSizer is just like the GridSizer, except that the columns in the grid can be different widths. Finally, the GridBagSizer is similar to the FlexGridSizer but also allows items to span over multiple "cells" in the grid, which makes it possible to achieve layouts that can usually only be achieved by nesting several BoxSizers. This recipe will discuss the use of the GridBagSizer, and use it to create a dialog that could be used for viewing the details of a log event.

How to do it...

Here we will create a custom DetailsDialog that could be used for viewing log messages or system events. It has two fields in it for displaying the type of message and the verbose message text:

class DetailsDialog(wx.Dialog):
    def __init__(self, parent, type, details, title=""):
        """Create the dialog
        @param type: event type string
        @param details: long details string
        """

        super(DetailsDialog, self).__init__(parent, title=title)

        # Attributes
        self.type = wx.TextCtrl(self, value=type,
                                style=wx.TE_READONLY)
        self.details = wx.TextCtrl(self, value=details,
                                   style=wx.TE_READONLY|
                                         wx.TE_MULTILINE)
        # Layout
        self.__DoLayout()
        self.SetInitialSize()

    def __DoLayout(self):
        sizer = wx.GridBagSizer(vgap=8, hgap=8)

        type_lbl = wx.StaticText(self, label="Type:")
        detail_lbl = wx.StaticText(self, label="Details:")

        # Add the event type fields
        sizer.Add(type_lbl, (1, 1))
        sizer.Add(self.type, (1, 2), (1, 15), wx.EXPAND)

        # Add the details field
        sizer.Add(detail_lbl, (2, 1))
        sizer.Add(self.details, (2, 2), (5, 15), wx.EXPAND)

        # Add a spacer to pad out the right side
        sizer.Add((5, 5), (2, 17))
        # And another to the pad out the bottom
        sizer.Add((5, 5), (7, 0))

        self.SetSizer(sizer)

How it works...

The GridBagSizer's Add method of GridBagSizer takes some additional parameters compared to the other types of sizers. It is necessary to specify the grid position and optionally the number of columns and rows to span. We used this in our details dialog in order to allow the TextCtrl fields to span multiple columns and multiple rows in the case of the details field. The way this layout works can get a little complicated, so let's go over our __DoLayout method line-by-line to see how each of them affect the dialog's layout.

First, we create out GridBagSizer, and in its constructor we specify how much padding we want between the rows and columns. Next, we start adding our items to the sizer. The first item that we add is the type StaticText label, which we added at row 1, column 1. This was done to leave some padding around the outside edge. Next, we added the TextCtrl to the right of the label at row 1, column 2. For this item, we also specified the span parameter to tell the item to span 1 row and 15 columns. The column width is proportionally based upon the size of the first column in the grid.

Next we add the details fields, starting with the details label, which is added at row 2, column 1, in order to line up with the type StaticText label. Since the details text may be long, we want it to span multiple rows. Hence, for its span parameter we specified for it to span 5 rows and 15 columns.

Finally, so that the padding around our controls on the bottom and right-hand side matches the top and left, we need to add a spacer to the right and bottom to create an extra column and row. Notice that for this step we need to take into account the span parameters of the previous items we added, so that our items do not overlap. Items cannot occupy the same column or row as any other item in the sizer. So first we add a spacer to row 2, column 17, to create a new column on the right-hand side of our TextCtrl objects. We specified column 17 because the TextCtrl objects start at column 2 and span 15 columns. Likewise, we did the same when adding one to the bottom, to take into account the span of the details text field. Note that instead of offsetting the first item in the grid and then adding spacers, it would have been easier to nest our GridBagSizer inside of a BoxSizer and specify a border. The approach in this recipe was done just to illustrate the need to account for an item's span when adding additional items to the grid:

Standard dialog button layout

Each platform has different standards for how different dialog buttons are placed in the dialog. This is where the StdDialogButtonSizer comes into play. It can be used to add standard buttons to a dialog, and automatically take care of the specific platform standards for where the button is positioned. This recipe shows how to use the StdDialogButtonSizer to quickly and easily add standard buttons to a Dialog.

How to do it...

Here is the code for our custom message box class that can be used as a replacement for the standard MessageBox in cases where the application wants to display custom icons in their pop-up dialogs:

class CustomMessageBox(wx.Dialog):
    def __init__(self, parent, message, title="",
                 bmp=wx.NullBitmap, style=wx.OK):
        super(CustomMessageBox, self).__init__(parent, title=title)

        # Attributes
        self._flags = style
        self._bitmap = wx.StaticBitmap(self, bitmap=bmp)
        self._msg = wx.StaticText(self, label=message)

        # Layout
        self.__DoLayout()
        self.SetInitialSize()
        self.CenterOnParent()

    def __DoLayout(self):
        vsizer = wx.BoxSizer(wx.VERTICAL)
        hsizer = wx.BoxSizer(wx.HORIZONTAL)
        # Layout the bitmap and caption
        hsizer.AddSpacer(10)
        hsizer.Add(self._bitmap, 0, wx.ALIGN_CENTER_VERTICAL)
        hsizer.AddSpacer(8)
        hsizer.Add(self._msg, 0, wx.ALIGN_CENTER_VERTICAL)
        hsizer.AddSpacer(10)

        # Create the buttons specified by the style flags
        # and the StdDialogButtonSizer to manage them
        btnsizer = self.CreateButtonSizer(self._flags)

        # Finish the layout
        vsizer.AddSpacer(10)
        vsizer.Add(hsizer, 0, wx.ALIGN_CENTER_HORIZONTAL)
        vsizer.AddSpacer(8)
        vsizer.Add(btnsizer, 0, wx.EXPAND|wx.ALL, 5)

        self.SetSizer(vsizer)

How it works...

Here, we created a custom MessageBox clone that can accept a custom Bitmap to display instead of just the standard icons available in the regular MessageBox implementation. This class is pretty simple, so let's jump into the __DoLayout method to see how we made use of the StdDialogButtonSizer.

In __DoLayout, we first created some regular BoxSizers to do the main part of the layout, and then in one single line of code we created the entire layout for our buttons. To do this, we used the CreateButtonSizer method of the base wx.Dialog class. This method takes a bitmask of flags that specifies the buttons to create, then creates them, and adds them to a StdDialogButtonSizer that it returns. All we need to do after this is to add the sizer to our dialog's main sizer and we are done!

The following screenshots show how the StdDialogButtonSizer handles the differences in platform standards.

For example, the OK and Cancel buttons on a dialog are ordered as OK/Cancel on Windows:

On Macintosh OS X, the standard layout for the buttons is Cancel/OK:

There's more...

Here is a quick reference to the flags that can be passed as a bitmask to the CreateButtonSizer method in order to create the buttons that the button sizer will manage:

Flags

 

Description

 

Wx.OK

Creates an OK button

 

wx.CANCEL

 

Creates a Cancel button

 

wx.YES

 

Creates a Yes button

 

wx.NO

 

Creates a No button

 

wx.HELP

 

Creates a Help button

 

wx.NO_DEFAULT

 

Sets the No button as the default

 

wxPython 2.8 Application Development Cookbook Over 80 practical recipes for developing feature-rich applications using wxPython
Published: December 2010
eBook Price: $23.99
Book Price: $39.99
See more
Select your format and quantity:
        Read more about this book      

(For more resources on Python, see here.)

Using XML resources

XRC is a way of creating and design window layouts with XML resource files. The hierarchical nature of XML parallels that of an application's window hierarchy, which makes it a very sensible data format to serialize a window layout with. This recipe shows how to create and load a simple dialog with two CheckBoxe objects and two Button objects on it, from an XML resource file.

How to do it...

Here is the XML for our dialog that we have in a file called xrcdlg.xrc:

<?xml version="1.0" ?>
<resource>
  <object class="wxDialog" name="xrctestdlg">
    <object class="wxBoxSizer">
      <orient>wxVERTICAL</orient>
      <object class="spacer">
        <option>1</option>
        <flag>wxEXPAND</flag>
      </object>
      <object class="sizeritem">
        <object class="wxCheckBox">
          <label>CheckBox Label</label>
        </object>
        <flag>wxALL|wxALIGN_CENTRE_HORIZONTAL</flag>
        <border>5</border>
      </object>
      <object class="spacer">
        <option>1</option>
        <flag>wxEXPAND</flag>
      </object>
      <object class="sizeritem">
        <object class="wxBoxSizer">
          <object class="sizeritem">
            <object class="wxButton" name="wxID_OK">
              <label>Ok</label>
            </object>
            <flag>wxALL</flag>
            <border>5</border>
          </object>
          <object class="sizeritem">
            <object class="wxButton" name="wxID_CANCEL">
              <label>Cancel</label>
            </object>
            <flag>wxALL</flag>
            <border>5</border>
          </object>
          <orient>wxHORIZONTAL</orient>
        </object>
        <flag>wxALIGN_BOTTOM|wxALIGN_CENTRE_HORIZONTAL</flag>
        <border>5</border>
      </object>
    </object>
    <title>Xrc Test Dialog</title>
    <style>wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER</style>
  </object>
</resource>

When loaded, the above XML will generate the following dialog:

This is a minimal program to load this XML resource to make and show the dialog it represents:

import wx
import wx.xrc as xrc
app = wx.App()
frame = wx.Frame(None)
resource = xrc.XmlResource("xrcdlg.xrc")
dlg = resource.LoadDialog(frame, "xrctestdlg")
dlg.ShowModal()
app.MainLoop()

How it works...

The XML in this recipe was created with the help of xrced, which is an XML resource editor tool that is a part of the wxPython tools package. The object tag is used to represent a class object. Nesting other objects inside is how the parent child relationship is represented with the XML. The class attribute of the object tag is what is used to specify the type of class to create. The values should be a class name and in the case of wxPython provided classes, they use the wxWidgets names, which are prefixed with "wx". To work with XmlResource classes, it is highly recommended to use a tool like xrced to generate the XML.

In order to load the XML to create the object(s) that are used for representation, you need to import the wx.xrc package, which provides the XmlResource class. There are a few ways to use XmlResource to perform the transformations on the XML. In this example, we created our XmlResource object by passing the path to our xrc file in its constructor. This object has a number of load methods for instantiating different types of objects. We want to load a dialog, so we called its LoadDialog method, passing a parent window as the first argument and then the name of the dialog we want to load from the XML. It will then instantiate an instance of that dialog and return it so that we can show it.

There's more...

Included below are some additional references to features available when using the XRC library.

Loading other types of resources

The XmlResource object has methods for loading many different kinds of resources from XML. Here is quick reference to some of the additional methods:

Methods

 

Description

 

LoadBitmap(name)

 

Loads and returns the Bitmap identified by name

 

LoadDialog(parent, name)

 

Loads and returns the Dialog identified by name

 

LoadFrame(parent, name)

 

Loads and returns the Frame identified by name

 

LoadIcon(name)

 

Loads and returns the Icon identified by name

 

LoadMenu(name)

 

Loads and returns the Menu identified by name

 

LoadMenuBar(parent, name)

 

Loads and returns the MenuBar identified by name

 

LoadPanel(parent, name)

 

Loads and returns the Panel identified by name

 

LoadToolBar(parent, name)

 

Loads and returns the ToolBar identified by name

 

Specifying standard IDs

In order to give an object a standard ID in XRC, it should be specified in the object tag's name attribute, using the wxWidgets naming for the ID (that is, wxID_OK without the '.').

Making a custom resource handler

Although XRC has built-in support for a large number of the standard controls, any non-trivial application will use its own subclasses and/or custom widgets. Creating a custom XmlResource class will allow these custom classes to be loaded from an XML resource file. This recipe shows how to create an XML resource handler for a custom Panel class and then use that handler to load the resource.

Getting Started

This recipe discusses how to customize and extend the handling of XML resources. Please review the Using XML resources recipe in this article to learn the basics of how XRC works.

How to do it...

In the following code, we will show how to create a custom XML resource handler for a Panel and then how to use XRC to load that resource into a Frame:

import wx
import wx.xrc as xrc

 

# Xml to load our object
RESOURCE = r"""<?xml version="1.0"?>
<resource>
<object class="TextEditPanel" name="TextEdit">
</object>
</resource>
"""

Here, in our Frame subclass, we simply create an instance of our custom resource handler and use it to load our custom Panel:

class XrcTestFrame(wx.Frame):
  def __init__(self, *args, **kwargs):
      super(XrcTestFrame, self).__init__(*args, **kwargs)

      # Attributes
      resource = xrc.EmptyXmlResource()
      handler = TextEditPanelXmlHandler()
      resource.InsertHandler(handler)
      resource.LoadFromString(RESOURCE)
      self.panel = resource.LoadObject(self,
                                       "TextEdit",
                                       "TextEditPanel")
      # Layout
      sizer = wx.BoxSizer(wx.VERTICAL)
      sizer.Add(self.panel, 1, wx.EXPAND)
      self.SetSizer(sizer)

Here is the Panel class that our custom resource handler will be used to create. It is just a simple Panel with a TextCtrl and two Buttons on it:

class TextEditPanel(wx.Panel):
    """Custom Panel containing a TextCtrl and Buttons
    for Copy and Paste actions.
    """
    def __init__(self, *args, **kwargs):
        super(TextEditPanel, self).__init__(*args, **kwargs)

        # Attributes
        self.txt = wx.TextCtrl(self, style=wx.TE_MULTILINE)
        self.copy = wx.Button(self, wx.ID_COPY)
        self.paste = wx.Button(self, wx.ID_PASTE)

        # Layout
        self._DoLayout()
 
        # Event Handlers
        self.Bind(wx.EVT_BUTTON, self.OnCopy, self.copy)
        self.Bind(wx.EVT_BUTTON, self.OnPaste, self.paste)
    def _DoLayout(self):
        """Layout the controls"""
        vsizer = wx.BoxSizer(wx.VERTICAL)
        hsizer = wx.BoxSizer(wx.HORIZONTAL)

        vsizer.Add(self.txt, 1, wx.EXPAND)
        hsizer.AddStretchSpacer()
        hsizer.Add(self.copy, 0, wx.RIGHT, 5)
        hsizer.Add(self.paste)
        hsizer.AddStretchSpacer()
        vsizer.Add(hsizer, 0, wx.EXPAND|wx.ALL, 10)

        # Finally assign the main outer sizer to the panel
        self.SetSizer(vsizer)

    def OnCopy(self, event):
        self.txt.Copy()
  
    def OnPaste(self, event):
        self.txt.Paste()

Finally, here is our custom XML resource handler class, where we just have to override two methods to implement the handling for our TextEditPanel class:

class TextEditPanelXmlHandler(xrc.XmlResourceHandler):
    """Resource handler for our TextEditPanel"""
    def CanHandle(self, node):
        """Required override. Returns a bool to say
        whether or not this handler can handle the given class
        """
        return self.IsOfClass(node, "TextEditPanel")

    def DoCreateResource(self):
        """Required override to create the object"""
        panel = TextEditPanel(self.GetParentAsWindow(),
                              self.GetID(),
                              self.GetPosition(),
                              self.GetSize(),
                              self.GetStyle("style",
                                            wx.TAB_TRAVERSAL),
                              self.GetName())
        self.SetupWindow(panel)
        self.CreateChildren(panel)
        return panel

How it works...

The TextEditPanel is our custom class that we want to create a custom resource handler for. The TextEditPanelXmlHandler class is a minimal resource handler that we created to be able to load our class from XML. This class has two required overrides that need to be implemented for it to function properly. The first is CanHandle, which is called by the framework to check if the handler can handle a given node type. We used the IsOfClass method to check if the node was of the same type as our TextEditPanel. The second is DoCreateResource, which is what is called to create our class. To create the class, all of its arguments can be retrieved from the resource handler.

The XrcTestFrame class is where we made use of our custom resource handler. First, we created an EmptyXmlResource object and used its InsertHandler method to add our custom handler to it. Then we loaded the XML from the RESOURCE string that we defined using the handler's LoadFromString method. After that, all there was to do was load the object using the resource's LoadObject method, which takes three arguments: the parent window of the object to be loaded, the name of the object in the XML resource, and the classname.

Using the AuiFrameManager

The AuiFrameManager is part of the Advanced User Interface (wx.aui) library added to wxPython in 2.8. It allows a Frame to have a very user customizable interface. It automatically manages children windows in panes that can be undocked and turned into separate floating windows. There are also some built-in features to help with persisting and restoring the window's layout during running the application. This recipe will create a Frame base class that has AUI support and will automatically save its perspective and reload it when the application is next launched.

How to do it...

The following code will define a base class that encapsulates some of the usage of an AuiManager:

import wx
import wx.aui as aui

class AuiBaseFrame(wx.Frame):
    """Frame base class with builtin AUI support"""
    def __init__(self, parent, *args, **kwargs):
        super(AuiBaseFrame, self).__init__(*args, **kwargs)

        # Attributes
        auiFlags = aui.AUI_MGR_DEFAULT
        if wx.Platform == '__WXGTK__' and \
           aui.AUI_MGR_DEFAUL & aui.AUI_MGR_TRANSPARENT_HINT:
           # Use venetian blinds style as transparent can
           # cause crashes on Linux when desktop compositing
           # is used. (wxAUI bug in 2.8)
           auiFlags -= aui.AUI_MGR_TRANSPARENT_HINT
           auiFlags |= aui.AUI_MGR_VENETIAN_BLINDS_HINT
       self._mgr = aui.AuiManager(self, flags=auiFlags)

       # Event Handlers
       self.Bind(wx.EVT_CLOSE, self.OnAuiBaseClose)

OnAuiBaseClose will be called when the Frame closes. We use this as the point to get the current window layout perspective and save it for the next time the application is launched:

def OnAuiBaseClose(self, event):
    """Save perspective on exit"""
    appName = wx.GetApp().GetAppName()
    assert appName, "No App Name Set!"
    config = wx.Config(appName)
    perspective = self._mgr.SavePerspective()
    config.Write("perspective", perspective)
    event.Skip() # Allow event to propagate

AddPane simply wraps getting access to the Frame's AuiManager and adds the given pane and auiInfo to it:

def AddPane(self, pane, auiInfo):
    """Add a panel to be managed by this Frame's
    AUI Manager.
    @param pane: wx.Window instance
    @param auiInfo: AuiInfo Object
    """
    # Delegate to AuiManager
    self._mgr.AddPane(pane, auiInfo)
    self._mgr.Update() # Refresh the layout

The next method is simply a convenience method for creating and adding the main center pane to the managed window:

def SetCenterPane(self, pane):
    """Set the main center pane of the frame.
    Convenience method for AddPane.
    @param pane: wx.Window instance
    """
    info = aui.AuiPaneInfo()
    info = info.Center().Name("CenterPane")
    info = info.Dockable(False).CaptionVisible(False)
    self._mgr.AddPane(pane, info)

This final method is used to load the last saved window layout from the last time the window was opened:

def LoadDefaultPerspective(self):
    appName = wx.GetApp().GetAppName()
    assert appName, "Must set an AppName!"
    config = wx.Config(appName)
    perspective = config.Read("perspective")
    if perspective:
        self._mgr.LoadPerspective(perspective)

How it works...

In this recipe, we created a class to help encapsulate some of the AuiManager's functionality. So let's take a look at some of the functionality that this class provides, and how it works.

The __init__ method is where we create the AuiManager object that will manage the panes that we want to add to the Frame. The AuiManager accepts a number of possible flags to dictate its behavior. We employed a small workaround for a bug on Linux platforms that use desktop compositing. Using the transparent docking hints can cause an AUI application to crash in this scenario, so we replaced it with the venetian blind style instead.

OnAuiBaseClose is used as an event handler for when the Frame closes. We use this as a hook to automatically store the current layout of the AuiManager, which is called a perspective, for the next application launch. To implement this feature, we have created a requirement that the App object's SetName method was called to set the application name because we need this in order to use wx.Config. The wx.Config object is simply an interface used to access the Registry on Windows or an application configuration file on other platforms. SavePerspective returns a string encoded with all of the information that the AuiManager needs in order to restore the current window layout. The application can then simply call our LoadDefaultPerspective method when the application starts up, in order to restore the user's last window layout.

The other two methods in this class are quite simple and are provided simply for convenience to delegate to the AuiManager of the Frame. The AddPane method of the AuiManager is how to add panes to be managed by it. The pane argument needs to be a window object that is a child of the Frame. In practice, this is usually some sort of Panel subclass. The auiInfo argument is an AuiPaneInfo object. This is what the AuiManager uses to determine how to manage the pane. See the sample code that accompanies this recipe for an example of this class in action.

There's more...

Here is a quick reference to the flags that can be used in the flags bitmask for the AuiManager in order to customize its behavior and the styles of some of its components:

Flags

 

Description

 

AUI_MGR_DEFAULT

 

Equivalent of

AUI_MGR_ALLOW_FLOATING| AUI_MGR_TRANSPARENT_HINT| AUI_MGR_HINT_FADE| AUI_MGR_NO_VENETIAN_BLINDS_FADE

 

AUI_MGR_ALLOW_FLOATING

 

Allow for floating panes

 

AUI_MGR_ALLOW_ACTIVE_PANE

 

Highlight the caption bar of the currently-active pane

 

AUI_MGR_HINT_FADE

 

Fade docking hints out of view

 

AUI_MGR_LIVE_RESIZE

 

Resize panes while the sash between them is being dragged

 

AUI_MGR_NO_VENETIAN_BLINDS_FADE

 

Disable the venetian blind fade in/out

 

AUI_MGR_RECTANGLE_HINT

 

Show a simple rectangle docking hint when dragging floating panes

 

AUI_MGR_TRANSPARENT_DRAG

 

Make floating panes partially-transparent when they are being dragged

 

AUI_MGR_TRANSPARENT_HINT

 

Show a partially-transparent light blue docking hint when dragging floating panels

 

AUI_MGR_VENETIAN_BLINDS_HINT

 

Use a venetian blind style docking hint for floating panels

 

Summary

This article introduced to you a number of concepts and techniques for designing your user interfaces in wxPython. The majority of this article explained the use of Sizers to allow you to quickly implement cross-platform user interfaces.


Further resources on this subject:


About the Author :


Cody Precord

Cody Precord is a Software Engineer based in Minneapolis, MN, USA. He has been designing and writing systems and application software for AIX, Linux, Windows, and Macintosh OSX for the last 10 years using primarily C, C++, Perl, Bash, Korn Shell, and Python. The constant need to be working on multiple platforms naturally led Cody to the wxPython toolkit, which he has been using intensely for that last 5 years. Cody has been primarily using wxPython for his open source project Editra which is a cross platform development tool. He is interested in promoting cross platform development practices and improving usability in software.

Books From Packt


Python 2.6 Text Processing Beginners Guide
Python 2.6 Text Processing Beginners Guide

Python Text Processing with NLTK 2.0   Cookbook
Python Text Processing with NLTK 2.0 Cookbook

Python Geo-Spatial Development
Python Geo-Spatial Development

Python 3 Object Oriented Programming
Python 3 Object Oriented Programming

Python 2.6 Graphics Cookbook
Python 2.6 Graphics Cookbook

Spring Python 1.1
Spring Python 1.1

Python Multimedia
Python Multimedia

MySQL for Python
MySQL for Python


No votes yet

Post new comment

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
1
C
w
s
S
2
Enter the code without spaces and pay attention to upper/lower case.
Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software