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:

  1. Make space for the Edit button by selecting the toolbar and settings its size property to 2.
  2. Add a Toolbar Button in the newly created empty space.
  3. 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.
  4. 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.

GLADE Window PyWine

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:

GLADE Window PyWine

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!

selsine

del.icio.us del.icio.us

25 Responses to “Extending our PyGTK Application”

  1. learning python » Blog Archive » Building an Application with PyGTK and Glade
    Says:

    [...] The next installment of the PyWine tutorial: Extending our PyGTK Application is available. [...]

  2. Fred.cpp
    Says:

    Save in XML format! that would be great.
    I love the way you make things look so easy ;-)

  3. selsine

    selsine
    Says:

    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!

  4. Peter
    Says:

    Thanks for another excellent tutorial! Its great the way it follows on from the previous two.

  5. selsine

    selsine
    Says:

    Hey Peter, thanks for saying that! I’m hoping that these tutorials allow people to progress with python and PyGTK at a comfortable rate.

  6. learning python » Blog Archive » WordPy 0.2 - Using XML to Save and Load Data
    Says:

    [...] 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[...]

  7. learning python » Blog Archive » Translating your Python/PyGTK application
    Says:

    [...] Extending our PyGTK Application [...]

  8. learning python » Blog Archive » PyLan a GTD todo application written in python and PyGTK - part one
    Says:

    [...] Extending our PyGTK Application [...]

  9. Nação Livre » Blog Archive » VocÄ‚Åž jÄ‚Ä„ Garimpou hoje?
    Says:

    [...] http://www.learningpython.com/2006/09/02/extending-our-pygtk-application/ [...]

  10. Las Noyas de Taran
    Says:

    [...] 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. [...]

  11. Lord Taran
    Says:

    This is the third tutorial that I’ve translated to spanish. Soon the fourth one!

  12. selsine

    selsine
    Says:

    Lord Taran, keep up the great work. Please be sure to let the people at pyGTK.org know about your translations!

  13. David
    Says:

    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 !!!

  14. selsine

    selsine
    Says:

    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:

    #They have passed a wine object
    self.wine = wine

    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:

    #They have passed a wine object
    self.wine = Wine(wine.wine
    					, wine.winery
    					, wine.grape
    					, wine.year)

    This will create a copy of the wine object in self.wine, and stop the cancel problem from happening.

  15. Steve
    Says:

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

  16. Matt Nichols
    Says:

    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.

  17. selsine

    selsine
    Says:

    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.

  18. selsine

    selsine
    Says:

    Hi Matt,

    Thanks for the semi-colon catch! I’ve spent way too much time using C++!

    Also thanks for the kind words!

  19. Dave H
    Says:

    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!

  20. Joono
    Says:

    i want some help on Validation in this example
    I tried some but none of them works….

  21. selsine

    selsine
    Says:

    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?

  22. Aaron Calderon
    Says:

    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.

  23. duanedesign
    Says:

    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.

  24. parkoros
    Says:

    Where is the following (in your source code file), I do not see it…

    ::HLIGHT_BLOCK_1::

  25. parkoros
    Says:

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

Leave a Reply

 

Popular Posts