Building a Viewer Parcel
Chandler Tutorial, Part 1: Building MrMenus
In this tutorial, we'll look at how to create basic elements
of a simple viewer parcel named
MrMenus?.
MrMenus? isn't representative of a
useful application, but he does allow us to demonstrate key concepts using a
brief amount of code--specifically, we'll look at how to create a parcel, then
give it some simple viewer UI, a new menu, and custom items in an existing
Chandler menu. We'll also show how data persists while the parcel isn't running,
and how user events set the persistent data model and update the view or user
interface.
Viewer Parcel Basics
In Chandler, a parcel is a generalized plug-in module. This
could be a spelling checker, interactive agent, data schema, email client, or
any other chunk of code that extends Chandler. The type of parcel we're going
to build here is a
viewer parcel--an area of the user-interface
where users view
Information items. These are any user-centric
data, such as emails, documents, addresses, or anything else users work with
directly. We'll work more with items in later tutorials, but for now, we'll
demonstrate some key concepts using interface data.
- Get familiar with
Python and WxWindows
Chandler parcels are written in wxPython (Python +
wxWindows), which you should be familiar with to follow this tutorial. Python,
because of its interpretive nature, is easier to work with than static languages
like C++ or Java, and it's powerful enough that you can still do everything you
want. The wxWindows cross-platform toolkit allows writing the majority of your
user interface code only once then deploying it on Windows, Macintosh, and
Linux. It also does a good job of using each platform's native widgets. This is
important to us, as we've noticed that most, if not all best-of-breed
applications have interfaces that fit the native platform.
- Use a good debugger!
Here's a link to information on some of my favorites:
http://wiki.osafoundation.org/bin/view/Chandler/DebuggingChandler
- Download and Run MrMenus?
Download the source code and sample files we'll be referring
to in this tutorial from http://wiki.osafoundation.org/bin/view/Chandler/GettingChandlerSource#cvs.
When you fire up MrMenus?, you'll notice:
A radio button array with options named Lunch and Dinner.
A MrMenus? parcel menu containing two corresponding menu items,
one of which has a check mark next to it, according to the selection in the
radio button array.
Two custom menu items in Chandler's Edit menu: one whose label
toggles between two different names, and one that's enabled or disabled--all
based on the current radio selection.
Set up the directory structure
The directory structure for your viewer parcel must contain:
- A subdirectory structure for your parcels
- A Python
__init__.py
file at each level of the hierarchy
- A Python module (ParcelName.py file)
- An XRC resources file (ParcelName.xrc file), whose name
matches that of the ParcelName.py file.
Take Home Point: Throughout this example, ParcelName is
a placeholder for text representing your parcel (in our case, MrMenus?),
and in many cases, this text must be the same.
For starters, the prefixes (file names minus extension) of your parcel's
ParcelName.py and ParcelName.xrc
files must be the same text.
- Create your parcel's directory
A parcel is a Python package, which in Chandler lives in the
parcels/
directory, the path to which is shown below. Within that, put your parcels in
their own subdirectory. You can make as deep a directory structure as you want.
osaf/chandler/Chandler/parcels/YourParcels/ParcelName/
All our parcels are in an OSAF folder, so our path
looks like this:
osaf/chandler/Chandler/parcels/OSAF/MrMenus/
Take Home Point: Putting your parcel's directory (ParcelName)
in a subdirectory separate from everyone else's (YourParcels)
avoids
conflict between parcels with the same name, say, if you're building a Calendar
parcel in addition to ours.
- Add a Python "init" file to each level of the hierarchy
Every directory under parcels/ has to contain a
__init__.py
file. That's how Python recognizes Python packages.
In our case, there's one in
parcels/,
OSAF/,
MrMenus/.
Note that at higher levels of the hierarchy (parcels/ and
OSAF/,
this file doesn't have to have anything in it.
parcels/
__init__.py
OSAF/
__init__.py
MrMenus/
__init__.py
- Define your parcel class
Now it's time to define your parcel class, and tell Chandler
that it's in your Python module. You do this in the
__init__.py
file at the level of your actual parcel (in our case, the
MrMenus/ directory).
This "init" file gets
executed when the parcel is loaded and must contain at least the following:
parcelClass = "ParcelName.ParcelClass"
where ParcelName
matches the filename-minus-extension of the Python module file
(ParcelName.py) and
ParcelClass
is the name of the parcel class you're creating, which you'll be defining in
ParcelName.py.
In our case, the
definition is: parcelClass="MrMenus.MrMenus"
where the first MrMenus identifies our Python module (MrMenus.py)
and
the second MrMenus
identifies the class we're about to create in that module.
Take Home Point:ParcelName
in the parcelClass definition
must match ParcelName
in
your ParcelName.py file name.
Build the Python Module
Viewer parcels follow an MVC (Model, Viewer, Controller)
architecture, which, not surprisingly, consists of a:
- Model--an object that contains data, which in Chandler persists
even when the parcel isn't running
- Viewer--an object that displays the model, which in Chandler is
not persistent
- Controller--code that ties all of these things together
Here, we'll show you
MrMenus?' model and viewer objects (he's
simple enough that he doesn't have any separate controller code).
For simplicity, the model and viewer objects
both reside in the
MrMenus.
py
Python module.
While reading this section, look at the code discussed in
parcels/OSAF/MrMenus/MrMenus.py.
- Create the Python module file
This is the ParcelName.py
file. As we said before, the stem, ParcelName must be the same
text as ParcelName in the "init" file's
parcel class definition. In our case, the module name is MrMenus.py.
It goes in the same directory as
the "init" file with the class definition.
- Include relevant imports
As with any Python project, you have to import the relevant
pieces for your project. For this
example, you need wxPython, the ViewerParcel?, the application object, and a
splash screen:
from wxPython.wx import *
from application.ViewerParcel import *
from application.Application import app
from application.SplashScreen import SplashScreen
- Create the Model object (MrMenus?)
The model object, MrMenus?, is a subclass of the ViewerParcel?
class. We override the superclass's Python __init__ to initialize
data that should persist across runs of the application. Most parcels need to
remember some user interface state so the user's world is the same when they
return to it as it was when they left (we think best of breed applications
should work this way). Our example includes a radio button array, whose
selection will be remembered across runs of the application. For now, we
initialize the selection to zero. Notice that before initializing the data, we
must call the parcel superclass's __init__.
class MrMenus(ViewerParcel):
def __init__(self):
ViewerParcel.__init__(self)
self.radioSelection = 0
Note that this value gets set here only once--the very first
time the parcel is loaded. After that, the last user setting will persist and
be loaded automatically each time the parcel is run.
Take Home Point: Before
doing anything else, you must call your superclass's __init__
- Create the ViewerObject? (wxMrMenus)
This object is the wxWindows viewer counterpart to the model
object and contains the user interface that displays the model. We suggest naming the object wxParcelName (notice
the wxWindows convention of using the prefix wx in the object name):
class wxMrMenus(wxViewerParcel):
- Initialize the Viewer
Notice that general initialization for the viewer goes
inside OnInit,
which is called afterthe Viewer Parcel's __init__. This is where we associate menu items and
radio buttons with the XRC resources file, and define user events.
def OnInit(self):
"""
General initialization goes here, e.g. wiring up menus, etc.
"""
EVT_RADIOBOX(self, XRCID ('RadioBox'),self.OnRadioBox)
etc...
Take Home Point: In general, the viewer has to be initialized
in OnInit,
after the Viewer Parcel's __init__
gets called. This is because wxPython objects aren't completely wired up in __init__.
- Override Activate to synchronize UI to the model
Unlike the model, which persists, the viewer object isn't
created until it's displayed, at which point you must synchronize it to
the value of the data in the model. You do
this by overriding the superclass's Activate method. The
system calls Activate
each time the
user goes to a viewer parcel, but just before the parcel is displayed, so you
can synchronize the UI to the model. In
our example, Activate
sets MrMenus?' parcel menu and viewer UI. Notice again that you must call the
superclass's Activate
as the first thing you do in your override:
def Activate(self):
wxViewerParcel.Activate(self)
self.SynchronizeParcelMenu()
self.SynchronizeParcelViewer()
Take Home Point: The system calls Activate each time the user goes to a viewer parcel. You can override Activate to
synchronize UI to the model before anything gets displayed in the viewer.
- Using events to change UI
The remaining methods in MrMenus.py define how
events change the model and update the user interface. Within the viewer, you
can access the model as self.model. The method below, for
example, which is called when the user makes a radio button selection, gets the
new selection, sets the model (self.model.radioSelection),
and updates the current selection in the parcel menu:
def OnRadioBox(self, event):
self.model.radioSelection = self.radioBox.GetSelection()
self.SynchronizeParcelMenu()
By the way, we're assuming that you're familiar with
wxPython events. If you're not, check
out the wxPython documentation before continuing this tutorial.
Take Home Point: Use self.model to access data in
the model.
Create Viewer Parcel Resources
Now let's look at the wxWindows XRC file that defines the UI
elements named in our Python code.
For the code discussed in this section, see
parcels/OSAF/MrMenus/MrMenus.xrc.
- Create your XRC file
You can create and edit the XRC file directly in a text
editor. Or, use one of the many
available tools that let you lay out user interface graphically and export to
XRC. In either case, name the file ParcelName.xrc,
where ParcelName is the same as ParcelName in both
your parcel
class definition (parcelClass="ParcelName.ParcelClass")
and your Python module's file name (ParcelName.py).
In our case, it's MrMenus.xrc.
Put this file in the same directory as the Python module.
- Define the ViewerObject (wxMrMenus)
Now you need to define the UI elements for the viewer to
display, as a panel in the XRC file. Specify the class to associate with the
panel by using the subclass
tag. This subclass
definition must include the path, starting under the parcel's directory, to the
wxViewerObject in MrMenus.py.
In the path below, OSAF.MrMenus
is the directory path, the second MrMenus is the MrMenus.py
file,
and wxMrMenus
is the wxViewerObject:
<object class="wxPanel"
subclass="OSAF.MrMenus.MrMenus.wxMrMenus"
name="MrMenus">
You can now go ahead and define the contents of the
viewer--in our case, a radio button array with the options "Lunch" and "Dinner,"
beneath a label "Please Choose a Menu".
Take Home Point: The subclass definition of your Viewer
object in ParcelName.xrc
must include the path to the wxViewerObject in ParcelName.py.
- Add a parcel menu
Every parcel can have its own wxMenu added to Chandler's
regular menu structure. You create this
menu using name="ViewerParcelMenu".
We've labeled our parcel menu "MrMenus," and created two
menu items, "Lunch" and "Dinner":
<object class="wxMenu" name="ViewerParcelMenu">
<label>MrMenus</label>
<object class="wxMenuItem" name="ParcelMenu0">
<label>Lunch</label>
<radio>1</radio>
</object>
<object class="wxMenuItem" name="ParcelMenu1">
<label>Dinner</label>
<radio>1</radio>
</object>
</object>
Notice that for the Python module to define events and
methods that act on UI elements created here, code in both places must refer to
these same elements. You do this in the XRC file using name="UI_element",
where UI_element (e.g., Parcel1Menu0)
is also named in any Python code acting on that element.
To see how this works, go back to MrMenus.py
and check out how EVT_RADIOBOX
calls onRadioBox to
update the model and change which of the menu items created here (either ParcelMenu0 or
ParcelMenu1) becomes
the current selection.
Take Home Points: name="ViewerParcelMenu" creates your
parcel menu. You use other name="UI_element" values
throughout the XRC file to refer to the viewer's UI elements in ParcelName.py.
- Insert menu items in
an existing Chandler menu
Now we'll insert two new menu items in Chandler's Edit
menu--one before Cut (along with a separator), and the other at the end of the
menu. Initially, these each have the same label but show two different ways to
update menu state.
First, create a menu with the same label as the existing
menu. You can indicate where to insert new items using the insertbefore and insertafter XRC tags. Insert
separator lines using class="separator".
The only trick here is that you can't refer to a separator when inserting, but
you can insert one before or after items with labels, so we insert our menu
item first, followed by the separator.
<object class="wxMenu" name="EditMenu">
<label>Edit</label>
<object class="wxMenuItem" name="EditMenu0">
<label></label>
<insertBefore>Cut</insertBefore>
</object>
<object class="separator">
<insertBefore>Cut</insertBefore>
</object>
If you don't indicate where to insert an item, it
automatically goes at the end of the menu, as is the case with our second menu
item.
<object class="wxMenuItem" name="EditMenu1">
<label>Set to Dinner</label>
</object>
Remember again that you use the names of menu items here to
refer to UI elements in your ParcelName.py code.
Take
Home Point: You can't refer to a separator when
inserting menu items, but you can insert one before or after an item with a
label.
Did you notice that the label for EditMenu0 is initially
empty? That's because we set its label in the Python code, using UPDATE_UI
events, which is what we'll look at next.
- Update custom Edit menu items
As you can see in the wxMrMenus class code, choosing a menu
item generates an EVT_MENU
event that can be associated with methods in the Python
code. In the case of our custom Edit
menu items, EVT_MENU
events in MrMenus.py
get handled by the OnEditMenu0
or OnEditMenu1 method to set the model and update
the parcel menu and viewer.
To update our Edit menu items themselves, however, we take a
different approach that comes in handy when you have a lot of menu item states
that become hard to keep track of. Instead of having widgets themselves set
menu labels, you can wait until the menu is opened to respond to an
UPDATE_UI
event, as demonstrated in MrMenus.py:
EVT_UPDATE_UI(self, XRCID('EditMenu0'), self.OnEditMenu0UpdateUI)
EVT_UPDATE_UI(self, XRCID('EditMenu1'), self.OnEditMenu1UpdateUI)
The first of these associates the update event with the OnEditMenu0UpdateUI method, which gets the current state
from the model, then uses event.SetText to set EditMenu0's
label to either "Set to
Dinner" or "Set to Lunch", overriding the blank label defined in
the XRC file.
def OnEditMenu0UpdateUI(self, event):
if self.model.radioSelection == 0:
event.SetText(_('Set to Dinner'))
elif self.model.radioSelection == 1:
event.SetText(_('Set to Lunch'))
The second EVT_UPDATE_UI
associates the update event with
the OnEditMenu1UpdateUI
method, which enables or disables EditMenu1
.
def OnEditMenu1UpdateUI(self,event):
event.Enable(self.model.radioSelection == 0)
Take Home Point: Keeping menu logic in one place, rather than
peppering the rest of your code with it, reduces the risk that you'll forget to
think about updating menus when you're writing the backend and not thinking
about menus at all.
What Else?
Persistence, and Three Lifespans of an Object
In Chandler, an object can have one of three different life
spans, depending on the type of object.
Forever Persistent objects, such as the model
object, are created only once. They
then persist forever, even when the application isn't running.
Run to Quit Python objects are created when the
application is started, and destroyed when the user quits. They stay around as long as the application
is running, even when the user leaves the parcel and comes back.
View to View wxViewerParcel objects are created when
the user displays the parcel, then completely destroyed when the viewer leaves
it -- even if only to briefly look at another parcel. This prevents parcels
that aren't being displayed from consuming too much memory, which is the same
reason we don't load a parcel until it's needed.
Sometimes, however, you want to keep viewer data around
between views of the parcel, say if your parcel opens files that you want to
stay around when users leave the parcel and be there when they return. Such
data can't persist in the model because Python can't pickle it, in this case
because it contains pointers to memory, so you can't store opened files in the
model.
To store such non-persistent data with the viewer without
having it destroyed between sessions, you can override
OnInitData and place it in a dictionary accessed as
self.data
in your code.
OnInitData is called the very first time your parcel is
initialized. When the user leaves the
parcel, the dictionary is saved, and then the viewer is destroyed. When the
user returns,
self.data
gets restored before the viewer is displayed.
Contributors
(wikified by
DuckySherwood)
Excellent reading!
Some thoughts:
About:
from wxPython.wx import *
I had the impression Robin and others are working hard to remove this "namespace pollution" from wxPython?
About:
Naming the Viewer class in every parcel with the wx prefix.
Doesn't this have the potential of creating name collisions with wxPython names, especially given the import method above?
About:
"Such data can't persist in the model because Python can't pickle it..."
Does this mean that adding any object that can't be pickled to the Model object, will make it fail? If so, maybe some guidance of what you can't add may be useful. I've read about nested functions and classes, as well as new style class instances using __slots__, but without __getstate__ and __setstate__.
--
NilsR - 31 Jul 2003