Virtuous Programmer Adventures of an Autodidact

22Nov/101

Python 4/4: GUI Libraries

Posted by Frank Berthold

This is the last article in a series, to read the entire series go here.

Writing a GUI for our RSS Aggregator

The Plan

I'll be finishing up the Python set with an overview of the Tkinter GUI library. There are numerous other GUI libraries available for Python, the best known of which is wxPython. I chose Tkinter because it comes built into Python and the primary goal here is to, as much as possible, look at Python as it comes out of the box.

What to Expect

When this article is complete:

  • You will have:
    • A basic GUI for last week's RSS reader.
  • You will know:
    • How to write a GUI in Python's Tkinter.
    • How to use lambda expressions to create functions on the fly.

The final GUI will look like this:

RSS Gui

Files Used in this Project

  • RssReader.txt: A library for reading RSS information, that I wrote in the previous project. Rename it to "RssReader.py" after downloading.
  • RssReaderGui.txt: The sourcecode for this project. Rename it to "RssReaderGui.py" after downloading.
  • feeds.txt: A sample input file.

The Code

Libraries

  1. from RssReader import generateRsses
  2. from Tkinter import *
  3. from tkFileDialog import askopenfilename
  4.  

Global Variables

  1. currentFeeds = {}
  2. rssDisplay = None
  3. chooseChannel = None
  4. currentChannel = None
  5. currentFeeds = None
  6.  

I've seen two ways to organizing the code for a simple one window GUI in Python. One is to wrap it all in a single large class. The other is to write a group of functions that share a set of global variables. In this case a single class would have the same problems as globals along with the overhead of adding self. to every method call and class variable.

The variables that are set to None above are place holders for global variables that will be assigned later. They don't have to be assigned here, but it's useful to know what your globals are at the front end.

Toplevel Window Creation

  1. def rssWindow():
  2.   root = Tk()
  3.   root.title("Rss Reader")
  4.   root.geometry("750x500")
  5.   root = channelFrame(root)
  6.   root = buttonFrame(root)
  7.   return root
  8.  

Although you can define your functions in arbitrary order, I've written these in a, more or less, heirarchical order. I'll walk through what I did here line by line:

  1. At the top level we create a Tk object.
  2. Give the window a title
  3. and an initial size.
  4. The frame that contains the drop down menu and list of articles.
  5. The frame that contains the select and load buttons.

The Upper Window with the Dropdown and Listbox Layout

RSS Gui

  1. def channelFrame(parent):
  2.   channelFrame = Frame(parent)
  3.   channelSelectFrame = Frame(channelFrame)
  4.   channelSelectFrame = channelLabel(channelSelectFrame)
  5.   channelSelectFrame = channelSelect(channelSelectFrame)
  6.   channelSelectFrame.pack(side=LEFT)
  7.   channelFrame = rssDisplay(channelFrame)
  8.   channelFrame.pack(side=TOP, expand=YES, fill=BOTH)
  9.   return parent
  10.  

Here we create the window that contains textual information from the RSS feeds. To accomplish this, I create frames which contain either other frames or GUI elements. Once the elements are created and organized by frame they are in, then the window behavior is set with .pack

  1. def rssDisplay(parent):
  2.   global rssDisplay
  3.   rssDisplay = Listbox(parent)
  4.   rssDisplay.pack(side=RIGHT, expand=YES, fill=BOTH)
  5.   return parent
  6.  
  7. def channelLabel(parent):
  8.   label = Label(parent, text="Select a channel:")
  9.   label.pack(side=TOP)
  10.   return parent
  11.  
  12. def channelSelect(parent):
  13.   global chooseChannel, currentChannel
  14.   currentChannel = StringVar(parent)
  15.   channelList = ["None"]
  16.   currentChannel.set(channelList[0])
  17.   chooseChannel = OptionMenu(parent, currentChannel, *channelList)
  18.   chooseChannel.pack(side=BOTTOM)
  19.   return parent
  20.  

Here the individual components are generated, the behaviors of each are described later. On line 2 I used the global keyword. You only need to use this keyword if you plan on changing the global variable, not if you're going to access it.

The Lower Window with the 'Set Config File' and 'Load Feeds' Buttons

RSS Gui

  1. def buttonFrame(parent):
  2.   buttonFrame = Frame(parent)
  3.   buttonFrame = setConfigFileButton(buttonFrame)
  4.   buttonFrame = loadRssButton(buttonFrame)
  5.   buttonFrame.pack(side=RIGHT, expand=YES, fill=X)
  6.   return parent
  7.  
  8. def setConfigFileButton(parent):
  9.   button = Button(parent, command=setConfigFile)
  10.   button["text"] = "Set Config File"
  11.   button.pack(side=LEFT)
  12.   return parent
  13.  
  14. def loadRssButton(parent):
  15.   button = Button(parent, command=loadRss)
  16.   button["text"] = "Load Feeds"
  17.   button.pack(side=RIGHT)
  18.   return parent
  19.  

Here I create a top level layout for each of the buttons then describe the specific behavior of each one. We describe the buttons behavior by passing the function to be called via the command argument.

Commands called by various elements

Lambda, functions are first class objects

  1. def loadRss():
  2.   global chooseChannel, currentChannel
  3.   chooseChannel["menu"].delete(0, END)
  4.   channelList = currentFeeds.keys()
  5.   for channelName in channelList:
  6.     chooseChannel["menu"].add_command(label=channelName,
  7.         command=lambda (temp = channelName): selectChannel(temp))
  8.   selectChannel(channelList[0])
  9.  

The loadRss function clears the values from the drop down menu then adds each of the current channels to it. It also uses a Lambda function to set the behavior of the drop down box when a given channel is selected.

Lambda functions, as used in line 7 of the above code are extraordinarily useful. It comes in handy, as in the above case, when you need to create a function for which some of the internal values are not known until runtime. Here the Lambda function's purpose is to assign the default value, channelName to the selectChannel function and assigns it as chooseChannel's command.

In general Lambda functions come in the form lambda arg, arg: arg + arg. For people who aren't used to functional it's important to remember that there's only ever one line in a lambda function and the value of that line is always returned without any need to use the return keyword.

  1. def selectChannel(channelName):
  2.   global chooseChannel, rssDisplay
  3.   chooseChannel.setvar(chooseChannel.cget("textvariable"), value = channelName)
  4.   rssDisplay.delete(0, END)
  5.   for feed in currentFeeds[channelName]:
  6.     rssDisplay.insert(END, feed)
  7.  

When one of the channels is selected from the drop down, this function fires and populates the display window with the names of the current articles.

  1. def setConfigFile():
  2.   global currentFeeds
  3.   currentFeedFile = askopenfilename(filetypes=[("allfiles", "*"),
  4.                                                 ("textfiles","*.txt")])
  5.   currentFeeds = combineFeeds(currentFeedFile)
  6.   loadRss()
  7.  

Here the program opens a file dialog with askopenfilename which will return the flie selected. It then combines all of the feeds with the same channel name and loads them into the select box to display them with loadRss.

  1. def combineFeeds(fileName):
  2.   feeds = {}
  3.   for feed in generateRsses(fileName):
  4.     for channelName in feed.keys():
  5.       if feeds.has_key(channelName):
  6.         feeds[channelName] += feed[channelName]
  7.       else:
  8.         feeds[channelName] = feed[channelName]
  9.   return feeds
  10.  

combineFeeds reads the RSS sources from the given filename, downloads the feeds using the library from the last article then combines any channels that happen to have the same name.

  1. if __name__ == "__main__":
  2.   rssWindow().mainloop()
  3.  

To actually create the window and make it useful, the program calls rssWindow(), described at top of the file, then runs the mainloop() method on it.

Final Summary

I've thoroughly enjoyed touring through Python, my observations are that Python has: * A gentle learning curve. * Many libraries with good documentation. * An easy to read syntax. * Flexible semantics.

This has been an interesting run, next week I'll be taking a look at Markdown, a lightweight markup language which allows you to create nicely typset documents from the comfort of your text editor. Starting in December I'll be taking a look through Scala a functional/object oriented language that runs in the Java VM.

Resources