These few pages describe the
construction of a shared image viewer. The image viewer uses the AG2.0
framework to provide collaborative capability. This application is the
predecessor of a shared image white board tool that is still under development.
The shared image white board (sort of a misnomer) will allow the collaborative
sharing and mark up of images via the access grid. At present, one can only
load images into the viewer and have them appear on other remote viewers. The
application works both under Windows and Linux.
The
first part of this documentation describes the build of a simple image viewer
using the WxPython window system. This was my first experience with WxPython.
Windows programmers will feel they are in familiar territory with this system.
Having done most of my GUI development in Tcl/Tk I was less familiar. However,
all of these systems provide similar functionality so the concepts of
programming in an event environment are all the same. This was also my first
deep immersion into the Python world and I must say I like it. There is nothing
wrong with Tcl/Tk but Python has a better look and feel than Tcl. My advice to
Tcl programmers out there is don’t fear the Python. Try it, you will like it.
I would like to thank Ivan Judson and Tom Uram from Argonne National Lab for the great deal of assistance they gave me in understanding the AG system in general and how to write shared applications in particular.
The first thing I had to do
was figure out how WxPython ( and WxWindows for that matter ) worked. I had
some small bit of experience with Windows programming. This helped but is not
entirely necessary. It is helpful to know what event programming is and how it
works. This knowledge can be gained by playing with Tcl/Tk or FLTK or some
other gui system.
So having said all that the
first thing I did was to create an application that displays images. I wrote
the program using WxPython. The way I usually approach tasks of this sort is to
find an existing solution. Goggling for what I was looking for turned up
several possibilities but none that did exactly what I wanted. Most did much
more than I wanted. I was able to glean much interesting info from these
sources. The key sources of information I used for this phase are:
Lets
write an image viewer in Python first and modify it later to work in the AG
framework. The code for this step can be found below. The code is adequately
commented to reflect what is going on (no really it is). We will describe some
highlights never the less. First notice that we are doing double buffering to
avoid flicker when the window is resized or moved. I hate flicker. Next it
should be noted that there are several ways to accomplish what we are trying to
do. Your way may differ from my way. Lets start at the bottom of the file.
class MyApp(wxApp):
def OnInit(self):
wxInitAllImageHandlers()
frame = ImageFrame(None,-1)
frame.Show(true)
self.SetTopWindow(frame)
return true
app = MyApp(0)
app.MainLoop()
This is
basically how all WxPython applications work. One defines a class derived from
the wxApp class, instantiates it and calls the MainLoop function. Inside MyApp
the image handlers are initialized. This step is required by the image handling
functions. Next a frame is created and displayed. The event handlers in the
frame object and the window object it contains do all the work. Lets have a
look at the frame object.
class ImageFrame(wxFrame):
def __init__(self,parent,ID):
wxFrame.__init__(self,parent,ID,"AGImage: no image
loaded",size=(800,600),
style=wxDEFAULT_FRAME_STYLE | wxNO_FULL_REPAINT_ON_RESIZE)
menu = wxMenu()
menu.Append(ID_OPEN,"&Open","Open an
image file")
menu.AppendSeparator()
menu.Append(ID_EXIT,"&Exit","Terminate
with extreme prejudice")
menubar = wxMenuBar()
menubar.Append(menu,"&File")
self.SetMenuBar(menubar)
EVT_MENU(self,ID_OPEN, self.On_Open)
EVT_MENU(self,ID_EXIT, self.On_Exit)
self.wind = ImageWindow(self,-1)
dt = BIFileDropTarget(self)
self.SetDropTarget(dt)
The ImageFrame class is
derived from the wxFrame class. In the initialization step the frame creates a
menu with two entries. One entry allows the user to open an image file and the
other exits the program. Pretty basic stuff really. There are loads of sources
of documentation for this sort of basic stuff so I will not belabor the point.
The interesting bit in this code snippet is the instantiation of the
ImageWindow object. We will
Become familiar with the
ImageWindow object shortly. At this point notice that we have a handle to this
object. The last two lines in the code
segment above set up this frame as a drop target. This functionality lets one
load an image into the application by dragging the file from a file manager.
Now we can get on to
examining the events that are bound to this frame. They are fired by selecting
the menu options or by dropping a file into the frame. The code for the event
handlers looks like this.
def On_Open(self,event):
dlg = wxFileDialog(self,"Select An Image",
os.getcwd(), "",wildcard,wxOPEN)
if dlg.ShowModal() == wxID_OK:
self.wind.LoadImageFromFilename(dlg.GetPath())
self.SetTitle(dlg.GetPath())
def On_Exit(self,event):
self.Close(true)
The On_Open method handles
events from the open menu item. It opens a dialog, retrieves a file name from
the dialog and calls the LoadImageFromFilename method on the window object we
created before. We will see what that method does with this information when we
examine the window class in the next section. The other handler simply exits
the application by closing the window when an exit event is fired from the
menu.
This is where the meat of the
program is to be found. This is where all the sick convoluted windows stuff
happens. I will describe what is going on. A more thorough explanation of what
is happening can be found in the double buffering reference sited above. This
class handles all the window drawing and such stuff. In order to understand
what is going on you should know how device contexts work. That topic is
covered extensively in the literature, see the links on background info above,
so we will not go into it here.
We already know of the
existence of the LoadImageFromFilename method that belongs to this class. There
are others of course. The contents of a window will need to be redrawn if the
window is resized or the contents damaged in some way. This is done via two
event handlers that the user must define. The On_Size and On_Paint handlers
take care of dealing with resize and repaint events. One other handler is
created in this class. The idle handler does its work when no other events are
occurring. Lets have a look at the code.
class ImageWindow(wxWindow):
def __init__(self,parent,ID):
wxWindow.__init__(self,parent,ID,style=wxNO_FULL_REPAINT_ON_RESIZE)
self.imagefile = None
self.image = None
self.SetBackgroundColour("WHITE")
self.InitBuffer()
EVT_IDLE(self,self.OnIdle)
EVT_SIZE(self,self.OnSize)
EVT_PAINT(self,self.OnPaint)
The class initialization
looks innocent enough. There are the bindings for the event handlers and one
other method call to note. The InitBuffer call initializes the bitmap that
holds the image. That method looks like this.
def InitBuffer(self):
size = self.GetClientSize()
if self.image == None:
self.buffer = wxEmptyBitmap(size.width,size.height)
dc = wxBufferedDC(None,self.buffer)
dc.SetBackground(wxBrush(self.GetBackgroundColour()))
dc.Clear()
else:
self.Clear()
self.buffer = self.image.ConvertToBitmap()
dc = wxBufferedDC(None,self.buffer)
dc.SetBackground(wxBrush(self.GetBackgroundColour()))
self.reInitBuffer = false
Two things can happen here.
If an image exists in the class when the method is called then the window is
cleared, a bitmap is created from the image, a wxBufferedDC is created with the
bitmap, the background for the DC is set to white. If no image is present when
the call is made then an empty bitmap is created and used to create the
wxBufferedDC. The background is set as before. Finally and in either case, the
reInitBuffer variable is set to false and the method exits. This method does
all the work of this class. The other methods just handle the state and cause
this method to be called. This method is called when the class is instantiated
and since there is no image at that time an empty window is created. Lets have
a closer look at how things happen by examining the LoadImageFromFile method.
def LoadImageFromFilename(self,imagefilename):
self.imagefile = imagefilename
self.image = wxImage(self.imagefile)
self.reInitBuffer = true
self.Refresh(true)
This method is called when
an image file is to be loaded. It creates a wxImage object from the file name,
sets the reInitBuffer flag to true and causes a repaint event via the call to
Refresh. The true argument to Refresh means the background should be erased.
The window is then repainted via the onpaint method. Onpaint has only one line
of code in it.
def OnPaint(self,event):
dc = wxBufferedPaintDC(self,self.buffer)
This method doesn’t look
like much. It creates a buffered paintDC that blits itself to the screen when
it is destroyed. It creates the dc using the buffer. But wait, the buffer is
empty at this point because there has been no call to the InitBuffer routine
yet. OnIdle to the rescue!
def OnIdle(self,event):
if self.reInitBuffer:
self.InitBuffer()
self.Refresh(FALSE)
Recall we set the
reInitBuffer flag when we loaded the image from a file. Now we are done
repainting the window and idle. The idle process is called and since we set the
reInitBuffer flag it calls the InitBuffer method and refreshes the window yet
again. (seems redundant doesn’t it) Now the buffer variable contains something
and the image appears in the window. If the reInitBuffer flag isn’t set OnIdle
doesn’t do anything. Now we have an image in the window that refreshes
correctly when a new image is loaded or when a hidden piece of the window is
revealed. We have not done anything earth shaking here. There are other ways to
do what I have done. Your mileage may vary. I choose to skip the drag and drop
description at this time. It is just a variation on what I have already
described. The entire application script is located here. Download it and give
it a try.