Hello welcome to the long-time-coming third article in this tutorial. I apologize to everyone (anyone?) that was waiting for it. I have been very busy as of late and have had much of my time taken up by a few other python projects, that I hope to be able to show you all soon.
You can download the full source to this tutorial here.
This tutorial will teach you the following things:
- How to construct a simple theme engine, or at least how I would, hopefully it will give you some ideas!
- How to display icons in a gtk.TreeView
- How to catch the selection event in a gtk.TreeView.
- How to enable or disable widgets.
- How to remove the selection from a gtk.TreeView if the user clicks on the gtk.TreeView but not on a tree item.
This tutorial is organized into the following sections:
- A Simple Theme Engine
- Showing Icons
- Enabling and Disabling Widgets
- Removing Unwanted Selection
As with other tutorial in this series this will not go into all details and changes that were made in the code, instead it will deal with a few main subjects that will hopefully provide ideas and sample code to others interested in python and PyGtk programming.
Now a while back I had an interesting idea that I would like to create a simple theme engine for the PyLan application. I thought that it would be easy and let me (or other people) change the icons or colours that are used.
So my idea was to use a dictionary object indexed by constant keys, whose values would be “theme objects.”
I thought that this would be a simple enough idea that I could implement quickly and then expand in the future to have more features like the ability to save and load the theme from xml.
So to start we create a file called theme.py and filled it with all of the basics and some imports and some constants:
try: import gtk import helper except: print _("import Error") PIXMAP = 0 COLOUR = 1
Next we create our Theme object that will serve as the applications theme. In this version the Theme object is still very simple, here is it in its entirety:
class Theme(dict): """This object represents all of the necessary information needed to properly present information in a pyLan application...or any application """ def __init__(self, theme_dict = None): """initialize the theme @param theme_dict - dictionary of values. If None it is ignored, if not none, it's values will be stored in the internal theme_dictionary.""" self.load_from_dictionary(theme_dict) def load_from_dictionary(self, theme_dict, initialize = False): """This function loads all of the theme values from the theme_dict into the internal theme dictionary. @param theme_dict - dictionary - A theme dictionary that describes theme items. @param initialize = False - boolean, do we want to blank out the internal dictionary before we load the items? """ #Make sure that the dict is not None if (not theme_dict): return #clear yourself out dict.clear(self) for key,value in theme_dict.iteritems(): self[key] = value
So what is happening here? Not that much, the Theme object uses the dict object as a base class and then lets you initialize the Theme using a dictionary, which is done using a helper function. This was put for future usage and is not used yet.
The next item in the Theme setup is the ThemeItem object. This is the object that will be used as the value for each item in our Theme dictionary.
class ThemeItem(object): """An item in the theme. This is used to store all of the necessary theme information. """ """Type property - Cannot be set, only set in the initializer.""" def __get_type(self): return self._m_type type = property(__get_type) """Data Property""" def __get_data(self): return self._m_data data = property(__get_data) def __init__(self, type): """Initialize yourself. @param type - number - The type of theme item. """ self._m_type = type self._m_data = None #now load the data self.load_data() def load_data(self): """This function is call when the Theme item should load any data that it needs. i.e. if it is a pixmap it should load the image file. it should create the data. """ pass
First you can see that there are two date members in the ThemeItem base class, the _m_type and _m_data. The _m_type is simply used to let interested parties know what the type of theme object is. I’m not sure if the _m_type is still needed, originally I was using it but after changing a few things it doesn’t really do much (anything?) anymore.
The next data member is that _m_data item that is meant to be private and accessed through the data property. As you can see the data property cannot be set it can only be read. This was done this way so that only the ThemeItem would be able to set its data.
Now the data member can be anything, it could be an image, a colour, whatever you want it to be. The data member will generally be set by a subclass of the ThemeItem. You’ll also notice that the data is “loaded” in the __init__ function. By default the load_data() function does nothing, and should be overridden in the base class.
The only type that I have in use so far is the ThemePixmap object. This is a ThemeItem that sets its data to be a gtk.gdk.pixbuf:
class ThemePixmap(ThemeItem): """A Them item that represents a pixmap to be displayed.""" def __init__(self, file_name = ""): """Initialize the pixmap theme item. @param file_name - string - the path to the pixmap file. """ self.file_name = file_name ThemeItem.__init__(self, PIXMAP) def load_data(self): """This is where we load the pixmap based on the file name""" try: self._m_data = gtk.gdk.pixbuf_new_from_file(self.file_name) except gerror, e: helper.show_error_dlg(_("Error loading pixmap %s = %s") % (self.file_name, e)) self._m_data = None
Pretty straightforward here, a simple subclass of the ThemeItem object that overrides the load_data() function and adds a new class member. The load_data function simply loads a pixmap into the data using the gtk.gdk.pixbuf_new_from_file function.
Now how do we use this?
In the main pyLan.py file I added the following function to initalize the theme, it is called from the __init__ function and could theoretically be called from other locations:
def initialize_theme(self): """This function is used to initialise the theme.""" self.app_theme = theme.Theme() self.app_theme[CATEGORY_ICON] = theme.ThemePixmap( os.path.join(self.pixmaps, "stock_book_blue.png")) self.app_theme[TASK_ICON] = theme.ThemePixmap( os.path.join(self.pixmaps, "stock_task.png"))
So this is how I use the theme in the main PyLan application, I’m using it to determine what icon to display for tasks and what icon to use for categories.
As you can see I simply create a theme member in the PyLan class and the add two ThemePixmap items to the Theme, each index by constants that will be used to identify them later. In the future I hope to use this function to load the information from a file.
The constants used in the function are defined as follows:
CATEGORY_ICON = 1 TASK_ICON = 2
I will show you how the theme is used in the next section.
So now that we have a simple theme engine that load graphics for us we need to figure out how to show images in our gtk.TreeView.
What we will do is we will use the gtk.CellRendererPixbuf to render our image. We will also pack the image into the same column as the Title column. We do this so that the icon isn’t in a column of it’s own, which would seem quite strange.
Basically what we will do is create the gtk.TreeViewColumn in our normal way, using the gtk.CellRenberPixbuf, then when we use the gtk.TreeViewColumn.pack_start function to add the gtk.CellRendererText (representing the Title Text) to add the text to column. This means that we will have two cell renderers in the same column.
The actual code to accomplish this in the pyLan program is quite complicated as I attempted to use the todoColumn object as a generic way to order the columns. This worked fine before but now it has become a more complicated and slightly hackish now that we want to be able to pack items into the same column.
Sufficed to say I accomplished this by adding a new share_column member to the totoColum that allows you to Pack Widgets.
We also have to update the get_column_list() function in the todo.Task and todo.Category items so that they returns the pixbuf from the theme. This also means that we need to add a new member to the todo.todoBase object:
def __init__(self, type, app_theme): """The only way to set the type is through the initialization""" self.__m_type = type self.app_theme = app_theme
Where app_theme is an instance of our Theme object described in the previous section.
Then in the get_column_list() the todo.Category objects need to add the following:
elif (item_column.ID == pyLan.COL_ICON): lst_return.append(self.app_theme[pyLan.CATEGORY_ICON].data)
and the todo.Theme object needs to add the following:
elif (item_column.ID == pyLan.COL_ICON): lst_return.append(self.app_theme[pyLan.TASK_ICON].data)
Pretty simple, all we do is we get that Theme item in the applications theme and then return its data. If this seems strange or you cannot understand it, please take a look at the previous tutorial and you you will be able to understand what the get_column_list() function does.
One of the things that has been bothering me about the PyLan application was that all of the buttons on the toolbar were always enabled even when they cannot perform their function.
Now I’m used to the Enable/Disabled terminology except in GTK the terms sensitive and insensitive are used, so I will try to use them from now on, but if an enabled or a disabled slips in there please forgive me.
The first thing that we need to add is an initialize_widgets() function to the PyLan class, this function will gather widgets that we need and will set their default state. This needs to be called early in the __init__ function before initialize_todo_tree() is called. The function is as follows:
def initialize_widgets(self): """Initialize any widgets that we want. Basically grab widgets that you want to have access to later on in the program. """ #Get the widgets self.edit_button = self.wTree.get_widget("btnEdit") self.delete_button = self.wTree.get_widget("btnRemoveCategory") self.add_task_button = self.wTree.get_widget("btnAddTask") #disable them self.edit_button.set_sensitive(False) self.delete_button.set_sensitive(False) self.add_task_button.set_sensitive(False)
The code is pretty simple, we grab the widgets from the widget tree, and then we simply call the set_sensitive function and tell them to be insensitive by passing False to the function.
The next step is to connect the application to the “changed” signal that is emitted by the gtk.TreeSelection that is associated with our gtk.TreeView. To do so we need to add the following code to the initialize_todo_tree() function:
#Enable the selection callback selection = self.todoTreeView.get_selection() selection.connect('changed', self.on_tree_selection_changed)
Pretty simple, we get the gtk.TreeSelection from the gtk.TreeView and then we connect the on_tree_selection_changed() function with the changed signal.
The on_tree_selection_changed() function is relatively simple as well:
def on_tree_selection_changed(self, selection): """Called when the selection has changed in the tree. @param selection - gtk.TreeSelection - The selection object associated with the gtk.TreeView. """ model, selection_iter = selection.get_selected() #If there is a selection then enable these self.edit_button.set_sensitive((selection_iter != None)) self.delete_button.set_sensitive((selection_iter != None)) self.add_task_button.set_sensitive((selection_iter != None))
First we get the selection_iter from the gtk.TreeSelection and then we enable the edit, delete, and add_task buttons based on whether or not there is a selection in the gtk.TreeView. If nothing is selected then we make the buttons insensitive.
One feature that I wanted to implement in the application was the ability to remove selection from the tree by clicking on an empty space in the tree. As far as I can tell there is no simple method to enable a feature like this in PyGTK, which I found quite surprising.
As a result what I needed to do was connect to the gtk.TreeView’s button-press-event. Then we use the location where the user clicked and see if they clicked on an item in the tree. If they did not click on an item in the tree then we remove the selection from the tree. If they did click on something then we leave the selection as is.
def on_todoTree_button_press_event(self, widget, event): """There has been a button press on the TodoTree for now we use this as a quick hack to remove the selection. Perhaps there is a better way? @param widget - gtk.TreeView - The Tree View @param event - gtk.gdk.event - Event information """ #Get the path at the specific mouse position path = widget.get_path_at_pos(int(event.x), int(event.y)) if (path == None): """If we didn't get apath then we don't want anything to be selected.""" selection = widget.get_selection() selection.unselect_all()
As you can see, we take position of the cursor where the button was pressed and then call the get_path_at_pos() function in the gtk.TreeView which returns a tuple containing information if an item in the tree can be found at the specified x,y position, and None if nothing in the tree was hit.
It’s not pretty but it works!
You can download the full source to this tutorial here.
Now one of the problems that I had writing this tutorial is that it’s getting very hard to detail all of the changes that are made to the project. So I have tried to leave out as much extraneous detail as I possibly could, but then I start to wonder how much use anyone could get out of these articles if the details are left out.
Hopefully there is still enough in there so that people can get ideas and at least some hints as to how things work in some of the slightly more advanced areas of PyGTK.
I’m not sure if I am going to continue detailing everything in this test application, I might skip a few phases since detailing everything is getting to be more of a problem. But what I really want to focus on soon is working with things like Preferences and distributions. Sadly time is always a factor!
If you have any questions please ask away as always!