Yorick GUI Tutorial

This page is still in construction. It contains step-to-step instructions to build a GUI for yorick using pygtk (python gtk bindings) and glade.

Go to:

Overview

Building GUIs (Graphical User Interface) for yorick applications is now easy, as we hope we will demonstrate here. The learning curve is not that steep, although a little bit of python syntax will have to be learned on the way. The new method is based on gtk (the gnome tool kit), and as such will work better on linux. Gtk is certainly available for OsX (although we have had trouble building up dependencies) and I have heard it is available on windows too (untested). You will need the gnome libraries, gtk, python and pygtk, and glade and its libraries libglade. You will also need the yorick-yutils package (for pyk.i).

The architecture we propose can be simply described as follow:

The communication between yorick and python is done through a spawn() call from yorick, so is basically made through pipes (fast). spawn() imposes uses a slightly different syntax than the regular yorick syntax: the command's arguments are separated by spaces instead of comas, and there can be no ommited arguments, nor keywords (see help page for yorick's spawn() and funcdef()). This often imposes the need for yorick wrapper functions, that will in turn call more elaborate yorick (often pre-existing) functions.

Now let's get to work and build up our first yorick gtk GUI.

Hello world

Create the GUI with Glade

Open up a terminal, and type,

$ glade-3

Some distro still have glade-2, you'll have to slightly adapt the instructions below.

You should be presented with this interface:

Follow the instructions below step-by-step:

  1. Create the top level window:
    left menu bar -> toplevels -> window (upper left icon) click
  2. By default, top-level widgets are not visible. Let's make it visible: go to the right set of panes:
    Common -> visible click
  3. Add a button:
    left menu bar -> control-and-display -> button (upper left icon) click
    and place the button (click) on the recently created window widget
  4. Define the properties of the button: on the right set of panes:
    • Go to General
    • Enter Hello World! as a label
    • Enter helloworld as the widget name (type return to make it register)
  5. Let's put some space around our button:
    Common -> Border width (toward the end): 40 type return
  6. Then we have to define the callback function when this button is pressed:
    Signals -> clicked double click on the "handler" column of the "clicked" line
    Enter "on_helloworld_clicked"
    Note that when you type the first "o", it should complete the field with the name of the widget. Just type return.
  7. Save it:
    File -> Save as helloworld.glade
  8. Quit

The python file

This is going to look complicated/overwhelming, but in fact is rather simple. What you want to do is basically copy this file over and adapt it for future use. Below is the python file. I have added ample comments, so read through the code. Most of this will stays identical from one application to another. In fact, I have color coded (in red) what need to be changed if you decide to re-use this to code another GUI.

So copy and paste this file (or get it from here) and save it as helloworld.py alongside your newly created helloworld.glade. One more thing: the python file has to be executable! Just do:

$ chmod 755 helloworld.py
  

helloworld.py

#!/usr/bin/env python
# authors: Matthieu Bec, Francois Rigaut
# helloworld.py

# import necessary modules
import gtk
import gtk.glade
import sys
import gobject
import os, fcntl, errno

# define the class
class helloworld:
   
   def destroy(self, wdg, data=None):
      self.py2yo('quit')
#     gtk.main_quit() # we'll call this from yorick before it quits.
      
   def __init__(self,helloworldtop):
      self.helloworldtop = helloworldtop

      # read GUI definition
      self.glade = gtk.glade.XML(os.path.join(self.helloworldtop,'helloworld.glade')) 
      
      # handle destroy event (so that we can kill the window through window bar)
      self.window = self.glade.get_widget('window1')
      if (self.window):
         self.window.connect('destroy', self.destroy)
         
      # autoconnect to callback functions
      # this will automatically connect the event handler "on_something_event"
      # to the here-defined function of the same name. This avoid defining a
      # long dictionary that serves the same purpose
      self.glade.signal_autoconnect(self)

      # set stdin non blocking, this will prevent readline to block
      # stdin is coming from yorick (yorick spawned this python process)
      fd = sys.stdin.fileno()
      flags = fcntl.fcntl(fd, fcntl.F_GETFL)
      fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
      
      # ... and add stdin to the event loop (yorick input pipe by spawn)
      gobject.io_add_watch(sys.stdin,gobject.IO_IN|gobject.IO_HUP,self.yo2py,None)

      # run: realize the interface, start event management
      gtk.main()

   # callback functions
   # This is the important, user-defined heart of this file.
   # here, you define the function that will be called when an event is raised
   # (read: happens): a button is pressed, a slider is moved, you enter a
   # drawingarea or move the cursor, etc... Just define a handler (callback) of
   # the same name in the glade UI definition (see signals) and you're done.

   def on_helloworld_clicked(self,wdg):
      #string/command to send to yorick
      #function parameters (separated with spaces)
      self.py2yo('write "Hello World!"')
      
   # minimal wrapper for yorick/python communication
   # This is really internal to the yorick/python communication and you should
   # not have to fiddle with it
   
   def py2yo(self,msg):
      # sends string command to yorick
      sys.stdout.write(msg+'\n')
      sys.stdout.flush()
   
   def yo2py(self,cb_condition,*args):
      if cb_condition == gobject.IO_HUP:
         raise SystemExit, "lost pipe to yorick"
      # handles string command from yorick
      # note: inidividual message needs to end with \n for proper ungarbling
      while 1:
         try:
            msg = sys.stdin.readline()
            msg = "self."+msg
            exec(msg)
         except IOError, e:
            if e.errno == errno.EAGAIN:
               # the pipe's empty, good
               break
            # else bomb out
            raise SystemExit, "yo2py unexpected IOError:" + str(e)
         except Exception, ee:
            raise SystemExit, "yo2py unexpected Exception:" + str(ee)
         return True


# check calling syntax (should not be a problem as it is always called
# from yorick
#
if len(sys.argv) != 2:
   print 'Usage: helloworld.py path_to_glade'
   raise SystemExit

# get path to glade file
helloworldtop = str(sys.argv[1])
# execute it
top = helloworld(helloworldtop)

Yorick

We're almost done. Now we will create the yorick side, which will spawn python.
Let's call it (unsurprisingly) helloworld.i, with the simple content:

/* helloworld.i
   main function to call the pygtk GUI to helloworld.
   syntax: yorick -i helloworld.i
   Authors: F.Rigaut, Dec 2007
*/

require,"pyk.i";

helloworldtop = get_cwd();

// build command to spawn:
python_exec = helloworldtop+"/helloworld.py";
pyk_cmd=[python_exec,helloworldtop];

// spawn it and attach to _pyk_callback (see pyk.i):
// that starts python and pass it the path to the glade file
_pyk_proc = spawn(pyk_cmd, _pyk_callback);

Run it!

cd to the directory where you should now have helloworld.i, helloworld.py and helloworld.glade. Make sure helloworld.py is executable. Run the example with

$ yorick -i helloworld.i

The following window should pop up:

Click on it, and you should see "Hello world!" in the yorick terminal.

That's it. You have done it. This might appear anticlimatic, but you have to realize that you have now the keys to building GUIs for yorick. The whole structure is here, and adding functionalities is just a question of adding widgets in glade, python callbacks in the python routine, and yorick wrappers (or not), to get GUIs like the ones below (which are, you'll have to admit, very clean, thanks to gtk2.0+).

As a simple exercize, you could upgrade your GUI to have the button create a graphical window (easy), and add second button to make it plot or display something (beware of the funcdef syntax).

yao GUI (adaptive optics simulations)

spydr image viewer interface

We will see below how to include graphics in the GUI.

Adding graphics

A yorick GUI would probably not be complete without a graphic window. Fortunately, since 2.1.04x (about 08/2007), yorick has the capability to display its graphic window within another window id. The trick here is to reserve space within the gtk window and display the yorick graphics window in there. We will modify the three helloworld files from the previous section. You can cheat and get the 3 modified files in that tarball.
  1. Modify the GUI in glade to include somewhere a drawingarea.
    • For instance, you can edit helloworld.glade and add a vertical box parent by right clicking in the existing button (glade-3 only), then add a drawingarea widget (colorful icon bottom of the "control and display" in the left menu bar). Once done, you have to set the size of this drawingarea widget. You can do that within the "Common" tab at the right of the interface. Set for instance the width to 450 and the height to 471.
    • Alternatively, you can start from scratch: start with a vertical box, and add drawingarea and a button. Don't forget to set the visibility of the window, and to set the name, label and callback function for the button.
    • If yorick tries to create its window within a wid (window id) that does not exist yet, it will create an error. We have therefore to trigger the window creation after the main gtk window is created. To do that, add a callback to the drawingarea: select the drawingarea either from the main gui display or from the right pane widget selector, go to "signals", scroll down and add a handler at "map-event". Call it the default name, "on_drawingarea_map_event".
    • Save the file.
  2. Add the following in the python script, within the "callback functions" definition:
    def on_drawingarea_map_event(self,wdg,*args):   
       mwid = wdg.window.xid;
       self.py2yo('ywindow_init %d' % mwid)
       # optionaly (but this is a good place to
       # do it), update GUI parameters from yorick:
       # self.py2yo('gui_update')
    This is a function that will be called when the drawingarea is *realized* (mapped). What this does is sending a command to yorick to initialize the graphic windows (ywindow_init, see below). As it says, this is also the place to update the various widgets that may display numbers, etc...(not in this gui yet, but e.g. in the complex GUIs that you have seen above).
  3. Add the following lines in helloworld.i:
    func ywindow_init(parent_id)
    {
      window,0,wait=1,parent=parent_id;
    }

Go ahead now and try it: yorick -i helloworld.i. You should be rewarded with a display like this one:

helloworld with a yorick graphic window

Now we can readily link the button to display something:

  1. Change the py2yo call within the python script "on_helloworld_clicked" to, for instance 'pli_random':
  2. In helloworld.i, add a the corresponding function:
    func pli_random(void)
    {
      fma;
      pli,random([2,128,128]);
    }

Start it, and click on the helloworld button at will.

You should have gotten the drill by now. You can add all kinds of widgets, and connect them to yorick functions to do all kinds of things.

Tips and tricks