wxPython: Design Approaches and Techniques

0
157
10 min read

 

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

Programming is all about patterns. There are patterns at every level, from the programming language itself, to the toolkit, to the application. Being able to discern and choose the optimal approach to use to solve the problem at hand can at times be a difficult task. The more patterns you know, the bigger your toolbox, and the easier it will become to be able to choose the right tool for the job.

Different programming languages and toolkits often lend themselves to certain patterns and approaches to problem solving. The Python programming language and wxPython are no different, so let’s jump in and take a look at how to apply some common design approaches and techniques to wxPython applications.

Creating Singletons

In object oriented programming, the Singleton pattern is a fairly simple concept of only allowing exactly one instance of a given object to exist at a given time. This means that it only allows for only one instance of the object to be in memory at any given time, so that all references to the object are shared throughout the application. Singletons are often used to maintain a global state in an application since all occurrences of one in an application reference the same exact instance of the object. Within the core wxPython library, there are a number of singleton objects, such as ArtProvider , ColourDatabase , and SystemSettings . This recipe shows how to make a singleton Dialog class, which can be useful for creating non-modal dialogs that should only have a single instance present at a given time, such as a settings dialog or a special tool window.

How to do it…

To get started, we will define a metaclass that can be reused on any class that needs to be turned into a singleton. We will get into more detail later in the How it works section. A metaclass is a class that creates a class. It is passed a class to it’s __init__ and __call__ methods when someone tries to create an instance of the class.

class Singleton(type):
def __init__(cls, name, bases, dict):
super(Singleton, cls).__init__(name, bases, dict)
cls.instance = None

def __call__(cls, *args, **kw):
if not cls.instance:
# Not created or has been Destroyed
obj = super(Singleton, cls).__call__(*args, **kw)
cls.instance = obj
cls.instance.SetupWindow()

return cls.instance

Here we have an example of the use of our metaclass, which shows how easy it is to turn the following class into a singleton class by simply assigning the Singleton class as the __metaclass__ of SingletonDialog. The only other requirement is to define the SetupWindow method that the Singleton metaclass uses as an initialization hook to set up the window the first time an instance of the class is created.

Note that in Python 3+ the __metaclass__ attribute has been replaced with a metaclass keyword argument in the class definition.

class SingletonDialog(wx.Dialog):
__metaclass__ = Singleton

def SetupWindow(self):
"""Hook method for initializing window"""
self.field = wx.TextCtrl(self)
self.check = wx.CheckBox(self, label="Enable Foo")

# Layout
vsizer = wx.BoxSizer(wx.VERTICAL)
label = wx.StaticText(self, label="FooBar")
hsizer = wx.BoxSizer(wx.HORIZONTAL)
hsizer.AddMany([(label, 0, wx.ALIGN_CENTER_VERTICAL),
((5, 5), 0),
(self.field, 0, wx.EXPAND)])
btnsz = self.CreateButtonSizer(wx.OK)
vsizer.AddMany([(hsizer, 0, wx.ALL|wx.EXPAND, 10),
(self.check, 0, wx.ALL, 10),
(btnsz, 0, wx.EXPAND|wx.ALL, 10)])
self.SetSizer(vsizer)
self.SetInitialSize()

How it works…

There are a number of ways to implement a Singleton in Python. In this recipe, we used a metaclass to accomplish the task. This is a nicely contained and easily reusable pattern to accomplish this task. The Singleton class that we defined can be used by any class that has a SetupWindow method defined for it. So now that we have done it, let’s take a quick look at how a singleton works.

The Singleton metaclass dynamically creates and adds a class variable called instance to the passed in class. So just to get a picture of what is going on, the metaclass would generate the following code in our example:

class SingletonDialog(wx.Dialog):
instance = None

Then the first time the metaclass’s __call__ method is invoked, it will then assign the instance of the class object returned by the super class’s __call__ method, which in this recipe is an instance of our SingletonDialog. So basically, it is the equivalent of the following:

SingletonDialog.instance = SingletonDialog(*args,**kwargs)

Any subsequent initializations will cause the previously-created one to be returned, instead of creating a new one since the class definition maintains the lifetime of the object and not an individual reference created in the user code.

Our SingletonDialog class is a very simple Dialog that has TextCtrl, CheckBox, and Ok Button objects on it. Instead of invoking initialization in the dialog’s __init__ method, we instead defined an interface method called SetupWindow that will be called by the Singleton metaclass when the object is initially created. In this method, we just perform a simple layout of our controls in the dialog. If you run the sample application that accompanies this topic, you can see that no matter how many times the show dialog button is clicked, it will only cause the existing instance of the dialog to be brought to the front. Also, if you make changes in the dialog’s TextCtrl or CheckBox, and then close and reopen the dialog, the changes will be retained since the same instance of the dialog will be re-shown instead of creating a new one.

Implementing an observer pattern

The observer pattern is a design approach where objects can subscribe as observers of events that other objects are publishing. The publisher(s) of the events then just broadcasts the events to all of the subscribers. This allows the creation of an extensible, loosely-coupled framework of notifications, since the publisher(s) don’t require any specific knowledge of the observers. The pubsub module provided by the wx.lib package provides an easy-to-use implementation of the observer pattern through a publisher/subscriber approach. Any arbitrary number of objects can subscribe their own callback methods to messages that the publishers will send to make their notifications. This recipe shows how to use the pubsub module to send configuration notifications in an application.

How to do it…

Here, we will create our application configuration object that stores runtime configuration variables for an application and provides a notification mechanism for whenever a value is added or modified in the configuration, through an interface that uses the observer pattern:

import wx
from wx.lib.pubsub import Publisher

# PubSub message classification
MSG_CONFIG_ROOT = ('config',)

class Configuration(object):
"""Configuration object that provides
notifications.
"""
def __init__(self):
super(Configuration, self).__init__()
# Attributes
self._data = dict()

def SetValue(self, key, value):
self._data[key] = value
# Notify all observers of config change
Publisher.sendMessage(MSG_CONFIG_ROOT + (key,),
value)

def GetValue(self, key):
"""Get a value from the configuration"""
return self._data.get(key, None)

Now, we will create a very simple application to show how to subscribe observers to configuration changes in the Configuration class:

class ObserverApp(wx.App):
def OnInit(self):
self.config = Configuration()
self.frame = ObserverFrame(None,

title="Observer Pattern")
self.frame.Show()
self.configdlg = ConfigDialog(self.frame,
title="Config Dialog")
self.configdlg.Show()
return True

def GetConfig(self):
return self.config

This dialog will have one configuration option on it to allow the user to change the applications font:

class ConfigDialog(wx.Dialog):
"""Simple setting dialog"""
def __init__(self, *args, **kwargs):
super(ConfigDialog, self).__init__(*args, **kwargs)

# Attributes
self.panel = ConfigPanel(self)

# Layout
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.panel, 1, wx.EXPAND)
self.SetSizer(sizer)
self.SetInitialSize((300, 300))
class ConfigPanel(wx.Panel):
def __init__(self, parent):
super(ConfigPanel, self).__init__(parent)

# Attributes
self.picker = wx.FontPickerCtrl(self)

# Setup
self.__DoLayout()

# Event Handlers
self.Bind(wx.EVT_FONTPICKER_CHANGED,
self.OnFontPicker)

def __DoLayout(self):
vsizer = wx.BoxSizer(wx.VERTICAL)
hsizer = wx.BoxSizer(wx.HORIZONTAL)

vsizer.AddStretchSpacer()
hsizer.AddStretchSpacer()
hsizer.AddWindow(self.picker)
hsizer.AddStretchSpacer()
vsizer.Add(hsizer, 0, wx.EXPAND)
vsizer.AddStretchSpacer()
self.SetSizer(vsizer)

Here, in the FontPicker‘s event handler, we get the newly-selected font and call SetValue on the Configuration object owned by the App object in order to change the configuration, which will then cause the (‘config’, ‘font’) message to be published:

def OnFontPicker(self, event):
"""Event handler for the font picker control"""
font = self.picker.GetSelectedFont()
# Update the configuration
config = wx.GetApp().GetConfig()
config.SetValue('font', font)

Now, here, we define the application’s main window that will subscribe it’s OnConfigMsg method as an observer of all (‘config‘,) messages, so that it will be called whenever the configuration is modified:

class ObserverFrame(wx.Frame):
"""Window that observes configuration messages"""
def __init__(self, *args, **kwargs):
super(ObserverFrame, self).__init__(*args, **kwargs)

# Attributes
self.txt = wx.TextCtrl(self, style=wx.TE_MULTILINE)
self.txt.SetValue("Change the font in the config "
"dialog and see it update here.")

# Observer of configuration changes
Publisher.subscribe(self.OnConfigMsg, MSG_CONFIG_ROOT)

def __del__(self):
# Unsubscribe when deleted
Publisher.unsubscribe(self.OnConfigMsg)

Here is the observer method that will be called when any message beginning with ‘config‘ is sent by the pubsub Publisher. In this sample application, we just check for the (‘config’, ‘font’) message and update the font of the TextCtrl object to use the newly-configured one:

def OnConfigMsg(self, msg):
"""Observer method for config change messages"""
if msg.topic[-1] == 'font':
# font has changed so update controls
self.SetFont(msg.data)
self.txt.SetFont(msg.data)

if __name__ == '__main__':
app = ObserverApp(False)
app.MainLoop()

How it works…

This recipe shows a convenient way to manage an application’s configuration by allowing the interested parts of an application to subscribe to updates when certain parts of the configuration are modified. Let’s start with a quick walkthrough of how pubsub works.

Pubsub messages use a tree structure to organize the categories of different messages. A message type can be defined either as a tuple (‘root’, ‘child1’, ‘grandchild1’) or as a dot-separated string (‘root.child1.grandchild1’). Subscribing a callback to (‘root’,) will cause your callback method to be called for all messages that start with (‘root’,). This means that if a component publishes (‘root’, ‘child1’, ‘grandchild1’) or (‘root’, ‘child1’), then any method that is subscribed to (‘root’,) will also be called

Pubsub basically works by storing the mapping of message types to callbacks in static memory in the pubsub module. In Python, modules are only imported once any other part of your application that uses the pubsub module shares the same singleton Publisher object.

In our recipe, the Configuration object is a simple object for storing data about the configuration of our application. Its SetValue method is the important part to look at. This is the method that will be called whenever a configuration change is made in the application. In turn, when this is called, it will send a pubsub message of (‘config’,) + (key,) that will allow any observers to subscribe to either the root item or more specific topics determined by the exact configuration item.

Next, we have our simple ConfigDialog class. This is just a simple example that only has an option for configuring the application’s font. When a change is made in the FontPickerCtrl in the ConfigPanel, the Configuration object will be retrieved from the App and will be updated to store the newly-selected Font. When this happens, the Configuration object will publish an update message to all subscribed observers.

Our ObserverFrame is an observer of all (‘config’,) messages by subscribing its OnConfigMsg method to MSG_CONFIG_ROOT. OnConfigMsg will be called any time the Configuration object’s SetValue method is called. The msg parameter of the callback will contain a Message object that has a topic and data attribute. The topic attribute will contain the tuple that represents the message that triggered the callback and the data attribute will contain any data that was associated with the topic by the publisher of the message. In the case of a (‘config’, ‘font’) message, our handler will update the Font of the Frame and its TextCtrl.

LEAVE A REPLY

Please enter your comment!
Please enter your name here