Access Grid Enabled Image Viewer

Introduction

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.

How I did it…

Images and WxPython

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:

 

Back to top

A Python Image Viewer

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()

The Frame Object

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.

The Window Class

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.

 

 

Back to top