|Read more about this book|
(For more resources on Python, see here.)
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:
def __init__(self, parent, *args, **kwargs):
super(BoxSizerFrame, self).__init__(*args, **kwargs)
self.panel = BoxSizerPanel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.panel, 1, wx.EXPAND)
The BoxSizerPanel class is the next layer in the window hierarchy, and is where we will perform the main layout of the controls:
def __init__(self, parent, *args, **kwargs):
super(BoxSizerPanel, self).__init__(*args, **kwargs)
self._field1 = wx.TextCtrl(self)
self._field2 = wx.TextCtrl(self)
Just to help reduce clutter in the __init__ method, we will do all the layout in a separate _DoLayout method:
"""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(5) # put 5px of space between
# Do the same for the second row
# Now finish the layout by adding the two sizers
# to the main vertical sizer.
# Finally assign the main outer sizer to the panel
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:
- We wanted two TextCtrl objects each with a label to the left of them which can simply be thought of as two horizontal rectangles.
- 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.
Included below is a little more additional information about adding spacers and items to a sizer’s layout.
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.
This will add a 20×5 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.
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.
(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.