Extending our PyGTK Application
In this tutorial we are going to extend our PyWine application to allowing you to edit the items that you have added to the list and save an load the wine lists you create so that you don’t have to keep entering them in all the time.
You can download the complete code for this tutorial here.
If you are not familiar with the PyWine application or with working with Glade and PyGTK I suggest you read my first two tutorial on the subject:
The GUI – Glade
The first thing that we are going to do is open up out PyWine glad project and add an edit button to our toolbar:
- Make space for the Edit button by selecting the toolbar and settings its size property to 2.
- Add a Toolbar Button in the newly created empty space.
- Call the button btnEditWine, set its label to be “Edit Wine”, and its icon to be the stock Edit icon. Next add a handler for the clicked event.
- We’re going to change the menu up a little bit, instead of a menu that says Add | Wine, we are going to set the menu up to read Wine | Add and Wine | Edit. Do this just like we did in the previous PyWine tutorial and make the Wine | Edit clicked handler the same function as your btnEditWine button’s clicked event handler.

The Code
Now lets get the Edit button working in the code, the first thing that we are going to have to do is get the information from whatever line in the gtk.TreeView is currently selected. There are two ways to go about his, the first way is to read all the of data from the four columns that we have visible and the second would be to actually add our Wine object to the gtk.ListStore (our model) but not display it in the gtk.TreeView.
Since it is simpler and may be more useful in the future if our wine class contains extra information or if we choose to let people add or remove columns from the gtk.TreeView I choose to use the later approach. This means that we need to change some of our code a little but.
First we have to add an additional column to our column definition variables in the pyWine __init__ function:
self.cWineObject = 0 self.cWine = 1 self.cWinery = 2 self.cGrape = 3 self.cYear = 4
You see that we put the actual wine object as position 0 in the list, so we have to adjust our gtk.ListStore creation code in the same function as follows:
#Create the listStore Model to use with the wineView self.wineList = gtk.ListStore(gobject.TYPE_PYOBJECT , gobject.TYPE_STRING , gobject.TYPE_STRING , gobject.TYPE_STRING , gobject.TYPE_STRING)
Everything is the same as it was before except that the first item in our gtk.ListStore will now be a python object. In order to get the above code to compile we must add the following code to the top of our file:
import gobject
Now the next thing that we need to change the way that we add our wine to the gtk.ListStore so that we actually include the Wine object. Luckily in our previous tutorial we added a getList() function to our Wine class that returns the list to add to the gtk.ListStore(), so all we have to do is edit that:
def getList(self): """This function returns a list made up of the wine information. It is used to add a wine to the wineList easily""" return [self, self.wine, self.winery, self.grape, self.year]
It’s not much of a change, we simple have to make it so that getList() puts the Wine class at the start of the list.
The next step is to actually allow the user to edit a wine entry but before we do that there is one more change that we need to make. In tutorial one the __init__ function of the wine dialog accepted all of the items that made up our wine class as initialization parameters.
def __init__(self, wine="", winery="", grape="", year=""):
This works alright if you have a small number or parameters, but if our Wine class was going to grow initializing the wineDialog class would become a pain. So all we are going to do is change the __init__ function to accept a Wine class object, rather then all of its parts:
def __init__(self, wine=None): """Initialize the class. wine - a Wine object""" #setup the glade file self.gladefile = "pywine.glade" #setup the wine that we will return if (wine): #They have passed a wine object self.wine = wine else: #Just use a blank wine self.wine = Wine()
The next step is to finally edit a wine entry, we will do this in function called on_EditWine(), it is hooked up to the Edit Wine button clicked event and the Wine | Edit menu item:
def on_EditWine(self, widget): """Called when the user wants to edit a wine entry""" # Get the selection in the gtk.TreeView selection = self.wineView.get_selection() # Get the selection iter model, selection_iter = selection.get_selected() if (selection_iter): """There is a selection, so now get the the value at column self.cWineObject, the Wine Object""" wine = self.wineList.get_value(selection_iter, self.cWineObject) # Create the wine dialog, based off of the current selection wineDlg = wineDialog(wine); result,newWine = wineDlg.run() if (result == gtk.RESPONSE_OK): """The user clicked Ok, so let's save the changes back into the gtk.ListStore""" self.wineList.set(selection_iter , self.cWineObject, newWine , self.cWine, newWine.wine , self.cWinery, newWine.winery , self.cGrape, newWine.grape , self.cYear, newWine.year)
The first thing we do is call gtk.TreeView.get_selection() to get the gtk.TreeSelection object that is associated with the gtk.TreeView. Then we call gtk.TreeSelection.get_selected() which returns our gtk.TreeModel (which we don’t care about) and a gtk.TreeIter that points to the currently selected node in out gtk.TreeView (which we do care about).
The gtk.TreeIter returned by the get_selected() function will be None if there is no selection, otherwise we use that gtk.TreeIter to get the Wine object at the currently selected position in our gtk.TreeView by calling the gtk.TreeModel.get_value() action. Once we have the Wine object the rest is pretty straight forward, we create our wineDialog object, show it, and if the used clicks the Ok button we update the selected item in the gtk.TreeView using the gtk.ListStore.set() function.
The gtk.ListStore.set() function is actually quite interesting since it takes a gtk.TreeIter as its first parameter (the position to set the values) and the rest of its parameters can be one or more column_number, new_value pairs! My only disappointment was not finding a function that used a list in the same way that the gtk.ListStore.append() function does.
So that’s it for editing a wine entry! Since we don’t want to always re-enter the wines that we like each time we start the application it’s high time that we start saving and loading our wine list.
Saving and Loading the Wine Lists
The first thing that we are going to do is borrow two helper functions from the WordPy offline blogging tool tutorial:
def show_error_dlg(self, error_string): """This Function is used to show an error dialog when an error occurs. error_string - The error string that will be displayed on the dialog. """ error_dlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR , message_format=error_string , buttons=gtk.BUTTONS_OK) error_dlg.run() error_dlg.destroy()
This function just provides us with an easy way to let the user know that an error has occurred by showing them an error dialog. We will add this to the pyWine class. For more information on how the function works please see the WordPy offline blogging tool tutorial.
We are also going to bring over the browse_for_image() function:
def browse_for_image(self): """This function is used to browse for an image. The path to the image will be returned if the user selects one, however a blank string will be returned if they cancel or do not select one.""" file_open = gtk.FileChooserDialog(title="Select Image" , action=gtk.FILE_CHOOSER_ACTION_OPEN , buttons=(gtk.STOCK_CANCEL , gtk.RESPONSE_CANCEL , gtk.STOCK_OPEN , gtk.RESPONSE_OK)) """Create and add the Images filter""" filter = gtk.FileFilter() filter.set_name("Images") filter.add_mime_type("image/png") filter.add_mime_type("image/jpeg") filter.add_mime_type("image/gif") filter.add_pattern("*.png") filter.add_pattern("*.jpg") filter.add_pattern("*.gif") file_open.add_filter(filter) """Create and add the 'all files' filter""" filter = gtk.FileFilter() filter.set_name("All files") filter.add_pattern("*") file_open.add_filter(filter) """Init the return value""" result = "" if file_open.run() == gtk.RESPONSE_OK: result = file_open.get_filename() file_open.destroy() return result
Except we are going to modify it so that it operates as a File Open and File Save dialog, and so that it browses for pyWine files (*.pwi) instead of images. We will control whether it is a File Open or File Save dialog by passing in an additional parameter called dialog_action, which will be the action that we use to set the action property of the gtk.FileChooserDialog:
def file_browse(self, dialog_action, file_name=""): """This function is used to browse for a pyWine file. It can be either a save or open dialog depending on what dialog_action is. The path to the file will be returned if the user selects one, however a blank string will be returned if they cancel or do not select one. dialog_action - The open or save mode for the dialog either gtk.FILE_CHOOSER_ACTION_OPEN, gtk.FILE_CHOOSER_ACTION_SAVE file_name - Default name when doing a save""" if (dialog_action==gtk.FILE_CHOOSER_ACTION_OPEN): dialog_buttons = (gtk.STOCK_CANCEL , gtk.RESPONSE_CANCEL , gtk.STOCK_OPEN , gtk.RESPONSE_OK) else: dialog_buttons = (gtk.STOCK_CANCEL , gtk.RESPONSE_CANCEL , gtk.STOCK_SAVE , gtk.RESPONSE_OK) file_dialog = gtk.FileChooserDialog(title="Select Project" , action=dialog_action , buttons=dialog_buttons) """set the filename if we are saving""" if (dialog_action==gtk.FILE_CHOOSER_ACTION_SAVE): file_dialog.set_current_name(file_name) """Create and add the pywine filter""" filter = gtk.FileFilter() filter.set_name("pyWine database") filter.add_pattern("*." + FILE_EXT) file_dialog.add_filter(filter) """Create and add the 'all files' filter""" filter = gtk.FileFilter() filter.set_name("All files") filter.add_pattern("*") file_dialog.add_filter(filter) """Init the return value""" result = "" if file_dialog.run() == gtk.RESPONSE_OK: result = file_dialog.get_filename() file_dialog.destroy() return result
FILE_EXT is simply defined as follows:
FILE_EXT = "pwi"
We are also going to want to add handlers for the File | Open and File | Save menu commands in our glad project using the same method that we did for the Wine | Add and Wine | Edit menu items. I called mine on_file_open and on_file_save:
#Create our dictionay and connect it
dic = {"on_mainWindow_destroy" : self.on_Quit
, "on_AddWine" : self.OnAddWine
, "on_EditWine" : self.on_EditWine
, "on_file_open" : self.on_file_open
, "on_file_save" : self.on_file_save}
self.wTree.signal_autoconnect(dic)To do the saving and loading of our Wine objects we are going to use the python shelve module. Which is a standard python module that can most (if not all) python object. There are of course other ways that we could have done this, we could have used xml files, or a straight pickle of all of our objects, but I thought that shelve made sense in this situation and it is easier to introduce them xml saving and loading.
From the documentation:
A “shelf” is a persistent, dictionary-like object. The difference with “dbm” databases is that the values (not the keys!) in a shelf can be essentially arbitrary Python objects — anything that the pickle module can handle. This includes most class instances, recursive data types, and objects containing lots of shared sub-objects. The keys are ordinary strings.
Saving
So to start lets setup the on_file_save() function. To start we will let the user browse for a location where they want to save their file, and let them specify the file’s name. Next we will ensure that that the file has our file extension on it and then we will loop through all the items in the gtk.TreeView and save each Wine object using the shelve module:
def on_file_save(self, widget): """Called when the user wants to save a wine list""" # Get the File Save path save_file = self.file_browse(gtk.FILE_CHOOSER_ACTION_SAVE, self.project_file) if (save_file != ""): # We have a path, ensure the proper extension save_file, extension = os.path.splitext(save_file) save_file = save_file + "." + FILE_EXT """ Now we have the "real" file save loction create the shelve file, use "n" to create a new file""" db = shelve.open(save_file,"n") """Get the first item in the gtk.ListStore, and while it is not None, move forward through the list saving each item""" # Get the first item in the list iter = self.wineList.get_iter_root() while (iter): # Get the wine at the current gtk.TreeIter wine = self.wineList.get_value(iter, self.cWineObject) # Use the iters position in the list as the key name db[self.wineList.get_string_from_iter(iter)] = wine # Get the next iter iter = self.wineList.iter_next(iter) #close the database and write changes to disk, we are done db.close(); #set the project file root, self.project_file = os.path.split(save_file)
After working with the gtk.TreeIter objects earlier this code should not be that difficult to understand. In fact the only real difficult part of the code is the following, the rest should be explained by the in-line comments:
while (iter): # Get the wine at the current gtk.TreeIter wine = self.wineList.get_value(iter, self.cWineObject) # Use the iters position in the list as the key name db[self.wineList.get_string_from_iter(iter)] = wine # Get the next iter iter = self.wineList.iter_next(iter)
Basically what we are doing is looping through each item in the gtk.ListStore and then setting the data at the current gtk.TreeIter position in the shelve file to be our wine object.
db[self.wineList.get_string_from_iter(iter)] = wine
The gtk.TreeModel.get_string_from_iter() function “returns a string representation of the path pointed to by iter. This string is a ‘:’ separated list of numbers. For example, “4:10:0:3″ would be an acceptable return value for this string.” (pyGTK Docs). Since we are using a gtk.ListStore the values returned will always be single values that increase as we move down the list from top to bottom.
So the first item will be “0″, the second “1″, the third “2″, and so on. This will be helpful for us when we open files, since keys in shelve files are not guaranteed to be in any particular order (as far as I could tell.)
When you close the shelve file, the data will be written to the disk.
You’ll also notice the inclusion of the self.project_file item as the default file name, this is a new addition to the class. It is the file name of the current project, it just lets us set the default name in the gtk.FileChooserDialog when we are doing a save. It’s defined in the __init__ function as follows:
self.project_file = ""
This just lets us have a dialog that pops up like so:

Loading
Now lets setup the on_file_open() function which, if you’ve understood the on_file_save function, should not be that difficult to understand:
def on_file_open(self, widget): """Called when the user wants to open a wine""" # Get the file to open open_file = self.file_browse(gtk.FILE_CHOOSER_ACTION_OPEN) if (open_file != ""): # We have a path, open it for reading try: db = shelve.open(open_file,"r") if (db): # We have opened the file, so empty out our gtk.TreeView self.wineList.clear() """ Since the shelve file is not gaurenteed to be in order we move through the file starting at iter 0 and moving our way up""" count = 0; while db.has_key(str(count)): newwine = db[str(count)] self.wineList.append(newwine.getList()) count = count +1 db.close(); #set the project file root, self.project_file = os.path.split(open_file) else: self.show_error_dlg("Error opening file") except: self.show_error_dlg("Error opening file")
You’ll notice that when loading items from the list we use a counter (count) and the has_key() function. As explained above we save each Wine object using the gtk.TreeIter path, which is a single number since we are using a gtk.ListStore. But since the order in the file cannot be guaranteed we use our own counter to get each item from the file in order starting at zero and working our way up until the key represented by our number is no longer in the file. (Note: we convert out integer to a string since the keys in the shelve files must be strings.)
To load a Wine object from the file we simply ask the shelve file for the item at the current count key:
newwine = db[str(count)]
Then we just append that wine to the list, and we have loaded a .pwi file. The Try except code, basically just catches any error that might occur if the user tries to open a file that is not a true pyWine project file.
Conclusion
That’s basically it for this tutorial, but if you’ve understood it you can see how easy to would be to hook in the File | New menu handler or add a Delete button to the toolbar, or even set the title of the Window to be the current project file.
You might even want to try playing around with different project file types and try implementing an XML file type. In the future I think it would be a neat option to allow the user to decide which type of file they want to use for their project files.
You can download the complete code for this tutorial here.
If you have any questions, or notice any problems with this tutorial please post a comment and let me know!



September 2nd, 2006 at 4:17 pm
[...] The next installment of the PyWine tutorial: Extending our PyGTK Application is available. [...]
September 3rd, 2006 at 12:56 pm
Save in XML format! that would be great.
I love the way you make things look so easy

selsine Says:September 6th, 2006 at 9:11 am
Thanks for the compliment Fred! Everyone loves that old XML format don’t they? I will be sure to write a tutorial on how to save in XML format!
October 3rd, 2006 at 2:06 am
Thanks for another excellent tutorial! Its great the way it follows on from the previous two.

selsine Says:October 3rd, 2006 at 9:04 am
Hey Peter, thanks for saying that! I’m hoping that these tutorials allow people to progress with python and PyGTK at a comfortable rate.
October 24th, 2006 at 9:57 pm
[...] We will also need to do some file browsing for the save, open, and save as events so we’ll borrow the file_browse() function from the Extending our PyGTK application tutorial (to learn more about the function and how it works please read the tutorial): # Borrowed from the PyWine project[...]
December 3rd, 2006 at 4:40 pm
[...] Extending our PyGTK Application [...]
February 18th, 2007 at 2:03 pm
[...] Extending our PyGTK Application [...]
March 29th, 2007 at 11:34 pm
[...] http://www.learningpython.com/2006/09/02/extending-our-pygtk-application/ [...]
April 8th, 2007 at 9:29 pm
[...] Tercer tutorial que traduzco perteneciente a la web Learning Python y realizados por Mark Mruss. Este en particular se titulaba originalmente Extending our PyGTK application. La traducciĂłn se complica cada vez mĂĄs, porque voy llegando a sitios que me cuesta pillar un poco, espero que haya quedado todo mĂĄs o menos inteligible, y no haber metido la gamba en ningĂşn sitio. Si fuera asÄ‚Â, por favor, decÄ‚Âdmelo En este tutorial vamos a extender nuestra aplicaciĂłn PyWine para permitirte editar los elementiso que has aĂąadido a la lista y guardar y cargar las listas de vino que crees para que no tengas que estar insertando datos todo el tiempo. [...]
April 8th, 2007 at 9:44 pm
This is the third tutorial that I’ve translated to spanish. Soon the fourth one!

selsine Says:April 9th, 2007 at 8:20 am
Lord Taran, keep up the great work. Please be sure to let the people at pyGTK.org know about your translations!
June 4th, 2007 at 3:16 pm
There is a bug
I create an entry
I edit it,make change on a field, but I click on the cancel button
The entry is not modified in the view
but if I edit it again, the changes I have canceled appear !!!

selsine Says:June 4th, 2007 at 6:30 pm
Hi David,
Thanks for the great catch!
I figured out the problem, it’s what I’ve heard referred to as a “sticky-variable” problem in the past, which means that it’s simply a code error.
The problem is in these lines in the windDialog class:
What this does is set self.wine to be a reference to wine (a shallow copy), so when we change self.wine we are also changing wine itself. So even if we cancel the operation, wine (which is stored in the list) gets changed.
To fix this you can use the following code instead of the above lines:
This will create a copy of the wine object in self.wine, and stop the cancel problem from happening.
September 28th, 2007 at 11:38 am
Great tutorials! I got up and going and extending the examples in very short order.
I found that I wanted to double-click the entries to edit them.
To do it, I added a handler for the row_activated event using glade (which was just what I wanted) and called the event on_EditRow. Turns out that row_activated gives more arguments that specify what was selected (oddly enough), but I wasn’t sure how to use them so I just wrapped them up and called your existing on_EditWine function.
So I added the handler to the dictionary with a
“on_EditRow” : self.on_EditRow
in the middle there, then wrote the wrapper,
def on_EditRow(self, widget, path, column):
“”" Called when the user double-clicks a row “”"
self.on_EditWine(widget)
Since the double-clicked row is also selected, it’s just a little extra work that could be avoided in the on_EditWine function but it doesn’t hurt (I don’t think).
Anyway, keep up the good tutorials. I just found your site and have been learning a bunch.
(Any thoughts on a widget I could embed that would do 2-D plots of data? Ideally, I just pass it a couple of data sets and it does all the hard work of plotting.)
(One more, how about a tutorial about rubber-banding items? It’s integral to the GUI experience and used extensively, but there seem to be a fair amount of complexity in both the graphical part and using the bounding box for item selection that I’ve never seen one.)
September 30th, 2007 at 9:50 pm
Hey, I really enjoyed this tutorial, and learned a lot. Thanks!
A few suggestions:
-Look for semicolons at the end of some lines, I think some ended up in your code
-Remember to remind people to import the “shelve” and “os” modules
-Keep blogging! You are great, dude.

selsine Says:October 2nd, 2007 at 8:53 am
Hi Steve,
Thanks for the information and the great tip! If you want to take a loot at more Tree processing take a look at the PyLan tutorials as I do a little bit more in there.
For plotting you might want to take a look at Matplotlib
I will take a look into the rubber banding, that’s an interesting feature that I have not looked into yet.

selsine Says:October 2nd, 2007 at 8:55 am
Hi Matt,
Thanks for the semi-colon catch! I’ve spent way too much time using C++!
Also thanks for the kind words!
October 9th, 2007 at 2:34 pm
Excellent! I’m new to Linux (Ubuntu) and your tutorials are exactly what I needed to get a conceptual and practical grip on GUI coding.
It seems to me there is a lot of code blocks that could be auto-generated for all the default events and such. Are there any developments ‘out there’ that pull Python and Glade together into one development environment (IDE)?
Keep up the great work, I’ll be back!
October 22nd, 2007 at 1:38 am
i want some help on Validation in this example
I tried some but none of them works….

selsine Says:October 22nd, 2007 at 8:33 am
Dave,
Not that I know of, although I have seen Python scripts out there that do it for you.
Joono,
What sort of validation are you looking for?
December 18th, 2009 at 4:12 pm
I am getting my feet wet with GTK+, GLade and Python. This is such a grate example. The fact that it is a working example that one can build upon is grate.
I see that you have expressed interest to write a follow up tutorial to save in xml. That would be grate. I wait to see if you can work something out.
In the mean time I will look around the web for some similar xml saving tutorials.
Thanks for your grate job.
December 7th, 2010 at 11:28 pm
Just wanted to take a second to let you know how much your page has helped me. I have referred to it countless times over the last few months as I learn py.GTK. Thank you for your work on this.
January 2nd, 2011 at 6:06 pm
Where is the following (in your source code file), I do not see it…
::HLIGHT_BLOCK_1::
January 2nd, 2011 at 6:07 pm
Ugghh sorry I don’t know the code tags for comments here…
Where is the following (in your source code file), I do not see it…
def __init__(self, wine=None):
“”"Initialize the class.
wine – a Wine object”"”
#setup the glade file
self.gladefile = “pywine.glade”
#setup the wine that we will return
if (wine):
#They have passed a wine object
self.wine = wine
else:
#Just use a blank wine
self.wine = Wine()