Building an Application with PyGTK and Glade

Article by Mark Mruss, originally posted on www.learningpython.com

After working with PyGTK and Glade for my first tutorial, I decided to write another more complex tutorial but I couldn't think of a topic (Note: If you have any suggestions for any tutorials or posts I'd love to hear them) so I decided to work on creating a simple application and hopefully useful topics would arise from there.

The idea I came up with (which will hopefully be simple) is to create a program that will allow me to keep track of the different types of wine that I drink and how much I like them. It's something I've wanted to write for a while so I thought it would be good to combine learning PyGTK and doing it.

I'm going to call the project PyWine. The full source and Glade file for this tutorial can be found here

The first thing to do is start a new Glade project in the projects directory called PyWine. Create a new Window, call it "mainWindow", and set the title to be "PyWine". Then add a handler to the destroy signal just like in the first tutorial.

Next I add a Vertical Box to the Window with 4 rows, (from top to bottom) one row for a Menu bar, one row for a Toolbar, one row for a Tree or List View, and the final row for a Status Bar. Name the Tree View "wineView"

Python Glade

The Tree or List View that we added to the window will be used to display the wines information to the user. So the first thing that we are going to want to do is allow the user to add a wine to the dialog. To do this we are going to use a menu command and a Toolbar button.

The first thing to add is a toolbar button. To do this simply select the toolbar button in the Pallet and click on the toolbar that we added to the window. This should add a strange looking button to your toolbar. Now we are going to edit the toolbar buttons properties on the Widget tab. We'll set it's name to be "tbAddWine", it's label to be "Add Wine", and it's icon to be the stock add icon.

Python Glade

Now we'll add a handler for the tbAddWine button's click event but this time instead of calling it the default "on_tbAddWine_clicked" we'll call the handler "on_AddWine".

Adding to the Menu Bar

Now we are going to work on the menu, since we want people to be able to add items from the taskbar button or from the Menu Bar. So click on the Menu Bar in the window and go to the Widget tab in the properties window. Then click on the "Edit Menus..." button to edit the Menu Bar.

Now click the Add button to add a new top level menu item, and sets it's label to be "_Add". Then select the Add item in the menu list and click the "Add Child" button, this will create a sub-menu withing the Add menu. Set the new items label to be "_Wine" and set it's handler to be: "on_AddWine".

You'll notice that the handled for the Add toolbar button clicked event and the Add | Wine menu activation are the same. This is because both of those two widgets will be performing the exact same task, adding a wine to our wine list.

Python Glade Menu Editor

So what's going to happen when the user clicks on the Add Wine button or selects Add | Wine from the menu, is a dialog will pop up allowing them to enter in details about the wine. If they choose the "Ok" button to end the dialog the wine information that they entered will be added to the ListView.

Creating a dialog

So the next thing that we need to do is create the dialog that the user will use to add a new wine. For this example we'll keep things pretty simple and let the user enter the wine name, the winery, the year it was bottled, and the grape variety.

So to create a new dialog simply click the Dialog button on the Glade Palette window. This should bring up the New Dialog window, in it choose the "Standard button layout" option with Cancel and Ok as the buttons. Now set the dialogs name to be "wineDlg" and it's window title to be "Add Wine".

Next we will use a table to lay things out in our wineDlg, add the table to your dialog (in the normal way that you add widgets to a window) and set the number or rows to be 4 and the number of columns to be 2. Then we'll fill in the spaces of the table with Label and Text Entry widgets until the dialog looks something like this:

Python Glade Menu Editor

I added three pixels of spacing between the rows of the table, this setting can be found on the widget tab of the table's properties. If you are having trouble selecting the table you can select View | Show Widget Tree from the menu in the main Glade window and select the table in the widget tree. Or you can hold down the SIFT key and left click on the widgets in the table, this will allow you to cycle through the widgets on the dialog.

Now we are going to have to name all of the edit fields on the dialog, name then: enWine, enWinery, enGrape, and enYear.

The Python Code

Now we are going to take that code and get it working, we'll call our python file pywine.py and create it in our /projects/PyWine directory (the same directory where we created our glade files). The following is the basic code that we will use (most of it taken form the first PyGTk/Glade tutorial):

#!/usr/bin/env python

import sys
try:
 	import pygtk
  	pygtk.require("2.0")
except:
  	pass
try:
	import gtk
  	import gtk.glade
except:
	sys.exit(1)

class pyWine:
	"""This is the PyWine application"""

	def __init__(self):
		
		#Set the Glade file
		self.gladefile = "pywine.glade"  
		self.wTree = gtk.glade.XML(self.gladefile, "mainWindow") 

		#Create our dictionay and connect it
		dic = {"on_mainWindow_destroy" : gtk.main_quit
				, "on_AddWine" : self.OnAddWine}
		self.wTree.signal_autoconnect(dic)
		
	def OnAddWine(self, widget):
		"""Called when the use wants to add a wine"""
		
		print "OnAddWine"

if __name__ == "__main__":
	wine = pyWine()
	gtk.main()

There are a few new additions to this code, one is the handler for the on_AddWine signal, if you run this code you'll notice that "OnAddWine" will be printed out when you click the Add Wine button and when you select Add | Wine from the menu. The other new addition to the code is that we pass the name of the main window to gtk.glade.XML. This lets us load only that window and it's children.

The next thing that we are going to create is a Wine class that we will use to store the wines information:

class Wine:
	"""This class represents all the wine information"""
	
	def __init__(self, wine="", winery="", grape="", year=""):
		
		self.wine = wine
		self.winery = winery
		self.grape = grape
		self.year = year

Pretty simple, the next thing that we are going to create is a class that we'll use for our wineDlg dialog, we'll call it wineDialog:

class wineDialog:
	"""This class is used to show wineDlg"""
	
	def __init__(self, wine="", winery="", grape="", year=""):
	
		#setup the glade file
		self.gladefile = "pywine.glade"
		#setup the wine that we will return
		self.wine = Wine(wine,winery,grape,year)

The next thing we need to do is add a function to our wineDialog class that will load the wineDialog widget from the glade file and show it. We will also want this function to return the result of the dialog, this will be a gtk.RESPONSE, you can read more about these at the PyGTK website.

Here is the run function:

def run(self):
	"""This function will show the wineDlg"""	
	
	#load the dialog from the glade file	  
	self.wTree = gtk.glade.XML(self.gladefile, "wineDlg") 
	#Get the actual dialog widget
	self.dlg = self.wTree.get_widget("wineDlg")
	#Get all of the Entry Widgets and set their text
	self.enWine = self.wTree.get_widget("enWine")
	self.enWine.set_text(self.wine.wine)
	self.enWinery = self.wTree.get_widget("enWinery")
	self.enWinery.set_text(self.wine.winery)
	self.enGrape = self.wTree.get_widget("enGrape")
	self.enGrape.set_text(self.wine.grape)
	self.enYear = self.wTree.get_widget("enYear")
	self.enYear.set_text(self.wine.year)	

	#run the dialog and store the response		
	self.result = self.dlg.run()
	#get the value of the entry fields
	self.wine.wine = self.enWine.get_text()
	self.wine.winery = self.enWinery.get_text()
	self.wine.grape = self.enGrape.get_text()
	self.wine.year = self.enYear.get_text()
	
	#we are done with the dialog, destroy it
	self.dlg.destroy()
	
	#return the result and the wine
	return self.result,self.wine

You'll notice that we load the dialog form the glade file in the same way that we load the main window. We call gtk.glade.XML() and pass it the name of the widget that we want to load. This will automatically show the Dialog (in the same way that it does our main window) but that's not good enough for us, we want the run function to wait until the user has exited the dialog before we return to the caller. To do that we call get the dialog widget from the widget tree (self.dlg = self.wTree.get_widget("wineDlg")) and we call the GTkDialogs run function. I'll let the PyGTk documentation describe what this function does:

The run() method blocks in a recursive main loop until the dialog either emits the "response" signal, or is destroyed. If the dialog is destroyed, the run() method returns gtk.RESPONSE_NONE; otherwise, it returns the response ID from the "response" signal emission. Before entering the recursive main loop, the run() method calls the gtk.Widget.show() on the dialog for you. Note that you still need to show any children of the dialog yourself.

During the run() method, the default behavior of "delete_event" is disabled; if the dialog receives a "delete_event", it will not be destroyed as windows usually are, and the run() method will return gtk.RESPONSE_DELETE_EVENT. Also, during the run() method the dialog will be modal. You can force the run() method to return at any time by calling response() to emit the "response" signal. Destroying the dialog during the run() method is a very bad idea, because your post-run code won't know whether the dialog was destroyed or not.

After the run() method returns, you are responsible for hiding or destroying the dialog as needed.

The Ok button will return gtk.RESPONSE_OK and the Cancel button will return gtk.RESPONSE_CANCEL, for the most part we only care about the wine that is returned by this dialog if the user clicks the Ok button.

You can also see that we get the GTKEntry widgets from the dialog in order to get and set their text. All-in-all this function is pretty simple.

Tree Views and List Stores

Now that we have the wine the the user wanted to add to the list we need actually add it to the gtk.TreeView.

The main feature if CTKTreeViews is that they display their data in whatever way their model tells them to. They can use a gkt.ListStore, gtk.TreeStore, gtk.TreeModelSort, or a gtk.GenericTreeModel. For this example we're going to be using a gtk.ListStore.

The relationships between Tree View's and Models is a bit complicated but once you start using it you'll begin to understand why they did it this way. In a very basic form the Model represents the Data, and the Tree View is simply a way to display the data. So you can have multiple views display the same data (model) in totally different ways. From the GTk+ reference manual:

To create a tree or list in GTK+, use the GtkTreeModel interface in conjunction with the GtkTreeView widget. This widget is designed around a Model/View/Controller design and consists of four major parts:
The tree view widget (GtkTreeView)
The view column (GtkTreeViewColumn)
The cell renderers (GtkCellRenderer etc.)
The model interface (GtkTreeModel)

The View is composed of the first three objects, while the last is the Model. One of the prime benefits of the MVC design is that multiple views can be created of a single model. For example, a model mapping the file system could be created for a file manager. Many views could be created to display various parts of the file system, but only one copy need be kept in memory.

The first thing we need to do is add some code to the __init__ function of the pyWine class right after we auto-connect the dictionary to the widget tree:

#Here are some variables that can be reused later
self.cWine = 0
self.cWinery = 1
self.cGrape = 2
self.cYear = 3
		
self.sWine = "Wine"
self.sWinery = "Winery"
self.sGrape = "Grape"
self.sYear = "Year"		
				
#Get the treeView from the widget Tree
self.wineView = self.wTree.get_widget("wineView")
#Add all of the List Columns to the wineView
self.AddWineListColumn(self.sWine, self.cWine)
self.AddWineListColumn(self.sWinery, self.cWinery)
self.AddWineListColumn(self.sGrape, self.cGrape)
self.AddWineListColumn(self.sYear, self.cYear)

This code is pretty straight forward, first we create a few variables that act as defines for us (so that we can easily change things around later) then we get our gtk.TreeView from the widget tree. After that we call a new function to add the columns that we need in the list. AddWineListColumn just a quick function that stops us from having to replicate the column create code each time:

[code language="python"]
def AddWineListColumn(self, title, columnId):
"""This function adds a column to the list view.
First it create the gtk.TreeViewColumn and then set
some needed properties"""

column = gtk.TreeViewColumn(title, gtk.CellRendererText()
, text=columnId)
column.set_resizable(True)
column.set_sort_column_id(columnId)
self.wineView.append_column(column)
[/code]

This code is a bit more complicated, first we create a new gtk.TreeViewColumn that uses a gtk.CellRendererText as it's gtk.CellRenderer. Here is a bit more general information from the GTK+ reference manual:

Once the GtkTreeView widget has a model, it will need to know how to display the model. It does this with columns and cell renderers.

Cell renderers are used to draw the data in the tree model in a way. There are a number of cell renderers that come with GTK+ 2.x, including the GtkCellRendererText, GtkCellRendererPixbuf and the GtkCellRendererToggle. It is relatively easy to write a custom renderer.

A GtkTreeViewColumn is the object that GtkTreeView uses to organize the vertical columns in the tree view. It needs to know the name of the column to label for the user, what type of cell renderer to use, and which piece of data to retrieve from the model for a given row.

So basically what we are doing is creating a column with a specific title, specifying that it will use the gtk.CellRendererText (to display simply text), and telling it which item in the model it is attached to. The we make it resizable and allow the user to sort the list by clicking on a columns header. After that all we do is add the column to the View.

Now that that's done we need to create our Model, we'll do this back in the __init__ function of the pyWine class:

#Create the listStore Model to use with the wineView
self.wineList = gtk.ListStore(str, str, str, str)
#Attatch the model to the treeView
self.wineView.set_model(self.wineList)

Basically we simply create a gtk.ListStore and tell it that it will have four items, all of which will be strings. Then we attach the model to the view and that's all the initialization that we need to do for out gtk.TreeView.

Putting it all Together

The last thing that we need to do is write the OnAddWine function (called from the menu or the toolbar button) in the pyWine class. It's a pretty simple function:

def OnAddWine(self, widget):
	"""Called when the use wants to add a wine"""
	#Create the dialog, show it, and store the results
	wineDlg = wineDialog();		
	result,newWine = wineDlg.run()
	
	if (result == gtk.RESPONSE_OK):
		"""The user clicked Ok, so let's add this
		wine to the wine list"""
		self.wineList.append(newWine.getList())

So we create an instance of our wineDialog and then we run it storing the result and the wine information that the user entered. Then we check to see if the result was gtk.RESPONSE_OK (the user clicked on the Ok button) and if it is we then add the wine information to our gtk.ListStore which will automatically be displayed in our gtk.TreeView since the two are connected.

We make use of the simple getList function in our wine class to make this slightly easier to read:

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.wine, self.winery, self.grape, self.year]

Python PyGTK PyWine Glade

That's about it for this sample application, granted it doesn't save any of the information or anything like that yet, but it does outline some of the initial steps to creating a full pyGTk application.

The full source and Glade file can be found here you can also browse the full source below:

#!/usr/bin/env python

import sys
try:
 	import pygtk
  	pygtk.require("2.0")
except:
  	pass
try:
	import gtk
  	import gtk.glade
except:
	sys.exit(1)

class pyWine:
	"""This is an Hello World GTK application"""

	def __init__(self):
			
		#Set the Glade file
		self.gladefile = "pywine.glade"  
		self.wTree = gtk.glade.XML(self.gladefile, "mainWindow") 
			
		#Create our dictionay and connect it
		dic = {"on_mainWindow_destroy" : gtk.main_quit
				, "on_AddWine" : self.OnAddWine}
		self.wTree.signal_autoconnect(dic)
		
		#Here are some variables that can be reused later
		self.cWine = 0
		self.cWinery = 1
		self.cGrape = 2
		self.cYear = 3
		
		self.sWine = "Wine"
		self.sWinery = "Winery"
		self.sGrape = "Grape"
		self.sYear = "Year"		
				
		#Get the treeView from the widget Tree
		self.wineView = self.wTree.get_widget("wineView")
		#Add all of the List Columns to the wineView
		self.AddWineListColumn(self.sWine, self.cWine)
		self.AddWineListColumn(self.sWinery, self.cWinery)
		self.AddWineListColumn(self.sGrape, self.cGrape)
		self.AddWineListColumn(self.sYear, self.cYear)
	
		#Create the listStore Model to use with the wineView
		self.wineList = gtk.ListStore(str, str, str, str)
		#Attache the model to the treeView
		self.wineView.set_model(self.wineList)	
		
	def AddWineListColumn(self, title, columnId):
		"""This function adds a column to the list view.
		First it create the gtk.TreeViewColumn and then set
		some needed properties"""
						
		column = gtk.TreeViewColumn(title, gtk.CellRendererText()
			, text=columnId)
		column.set_resizable(True)		
		column.set_sort_column_id(columnId)
		self.wineView.append_column(column)
		
	def OnAddWine(self, widget):
		"""Called when the use wants to add a wine"""
		#Cteate the dialog, show it, and store the results
		wineDlg = wineDialog();		
		result,newWine = wineDlg.run()
		
		if (result == gtk.RESPONSE_OK):
			"""The user clicked Ok, so let's add this
			wine to the wine list"""
			self.wineList.append(newWine.getList())
				
class wineDialog:
	"""This class is used to show wineDlg"""
	
	def __init__(self, wine="", winery="", grape="", year=""):
	
		#setup the glade file
		self.gladefile = "pywine.glade"
		#setup the wine that we will return
		self.wine = Wine(wine,winery,grape,year)
		
	def run(self):
		"""This function will show the wineDlg"""	
		
		#load the dialog from the glade file	  
		self.wTree = gtk.glade.XML(self.gladefile, "wineDlg") 
		#Get the actual dialog widget
		self.dlg = self.wTree.get_widget("wineDlg")
		#Get all of the Entry Widgets and set their text
		self.enWine = self.wTree.get_widget("enWine")
		self.enWine.set_text(self.wine.wine)
		self.enWinery = self.wTree.get_widget("enWinery")
		self.enWinery.set_text(self.wine.winery)
		self.enGrape = self.wTree.get_widget("enGrape")
		self.enGrape.set_text(self.wine.grape)
		self.enYear = self.wTree.get_widget("enYear")
		self.enYear.set_text(self.wine.year)	
	
		#run the dialog and store the response		
		self.result = self.dlg.run()
		#get the value of the entry fields
		self.wine.wine = self.enWine.get_text()
		self.wine.winery = self.enWinery.get_text()
		self.wine.grape = self.enGrape.get_text()
		self.wine.year = self.enYear.get_text()
		
		#we are done with the dialog, destory it
		self.dlg.destroy()
		
		#return the result and the wine
		return self.result,self.wine
		

class Wine:
	"""This class represents all the wine information"""
	
	def __init__(self, wine="", winery="", grape="", year=""):
		
		self.wine = wine
		self.winery = winery
		self.grape = grape
		self.year = year
		
	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.wine, self.winery, self.grape, self.year]		
		
if __name__ == "__main__":
	wine = pyWine()
	gtk.main()