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

27Dec/102

Scala 4/4: GUI

Posted by Frank Berthold

Writing GUI Code in Scala

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

The Plan

For the last week of Scala I've put together a GUI for week 3's RSS aggregator using the Swing library. Scala's Swing library is a fairly thin wrapper around Java's Swing library. The primary difference between them is that Scala's library takes advantage of its more powerful type system.

What to Expect

When this article is complete:

  • You will have:
    • A front end for the RSS agregator I wrote in week 3.
  • You will know:
    • How to write a basic Swing application in Scala.
Screenshot:

RSS GUI

Files Used in this Project

The Code

Libraries

  1. import RssReader._
  2. import swing._
  3.  

You can use the libraries that you've written in the same way you call any other Java class library. Remember that you must compile RssReader.scala before you try to compile RssGui.scala.

A Top Level Object

  1. object RssGui extends SimpleSwingApplication {
  2.  

The SimpleSwingApplication incorporates all of the basics that you need for a standard windowed application in order to: create, draw and destroy the window when it's closed.

Container for RSS Data

  1.   var rssFeeds = List(("None Loaded", Seq("")))
  2.  

This will hold the data for the RSS feeds. I've chosen to initialize it with "None Loaded" and an empty string because it gives the GUI something to display until a config file is selected and loaded, and it let's Scala know what the rssFeeds's type is.

File Chooser

  1.   var configFileChooser = new FileChooser(new java.io.File("./"))
  2.   var configFileName = new String
  3.  

The configFileChooser is a FileChooser that will open, defaulting to the current working directory. configFileName will hold the value selected, the details for how this will work are in the "Defining Behaviors" section.

Layout

For the most part the code here stands for itself, which is wonderful. Once you understand the basics for creation and layout for one component, they're all the same. As a result I've be described each new idea in detail when I encountered it and let the rest alone.

Channel Panel Construction
Channel Select
  1.   val channelLabel = new Label {
  2.     text = "Select a channel:"
  3.   }
  4.  
  5.   val channelSelect = new ComboBox(rssFeeds(0)._1) { }
  6.  
  7.   val channelSelectPanel = new BoxPanel(Orientation.Vertical) {
  8.     contents += channelLabel
  9.     contents += channelSelect
  10.     border = Swing.EmptyBorder(0, 0, 0, 30)
  11.   }
  12.  

Lines 1-3: Create a new label with "Select a channel:" for its value.
Line 5: Create a ComboBox that is initialized to the first tuple value in the first list value: "None Loaded"
Line 7: Create a new panel that is filled top to bottom.
Line 8-9: Add the Label and ComboBox to the panel.
Line 10: Surround the panel in an empty border with 30 pixels on the right side (to separate it from the RssDisplay).

Rss Display
  1.   val rssDisplay = new ListView(rssFeeds(0)._2)
  2.  

This creates a nearly empty ListView. It has one line with an empty string. Until it's populated with something more complex, it won't display at all.

Combining channelPanel
  1.   val channelPanel = new BoxPanel(Orientation.Horizontal) {
  2.     contents += channelSelectPanel
  3.     contents += rssDisplay
  4.     border = Swing.EmptyBorder(0, 0, 30, 0)
  5.   }
  6.  

This creates a new panel with Orientation.Horizontal so each element will be added left to right. The channelSelectPanel and rssDisplay are added to complete the channelPanel.

Button Panel Construction
  1.   val setConfigFileButton = new Button {
  2.     text = "Set Config File"
  3.   }
  4.  
  5.   val loadRssButton = new Button {
  6.     text = "Load Feeds"
  7.   }
  8.  
  9.   val buttonPanel = new BoxPanel(Orientation.Horizontal) {
  10.     contents += setConfigFileButton
  11.     contents += loadRssButton
  12.   }
  13.  

Create the two buttons and lays them out in a panel.

Top Window
  1.   val topWindow = new BoxPanel(Orientation.Vertical) {
  2.     contents += channelPanel
  3.     contents += buttonPanel
  4.     border = Swing.EmptyBorder(10, 10, 10, 10)
  5.   }
  6.  

Add the two top level panels together and put a border around them to make it neat.

Defining Behaviors

The Last Bit of Layout

  1.   def top = new MainFrame {
  2.     title = "Rss Reader"
  3.  
  4.     // Overall construction
  5.     contents = topWindow
  6.  

This creates the overall window and assigns topWindow as its contents. The title "Rss Reader" will appear in the title bar of the window.

Listening

  1.     listenTo(channelSelect.selection, setConfigFileButton, loadRssButton)
  2.  

Each of the GUI elements that you want to be able to have act as controllers must be listed in listenTo. I've chosen to add them all at once, you can also add them one at a time as in:

  1.    listenTo(channelSelect.selection)
  2.    listenTo(setConfigFileButton)
  3.    listenTo(loadRssButton)
  4.  

To determine when a value has been selected we are listening to channelSelect.selection instead of channelSelect.

Reacting

For each action performed on one of the listenTo'ed elements it will run through each of the listed reactions to see if any of them are appropriate for the action/object combination.

  1.     reactions += {
  2.       case swing.event.SelectionChanged(`channelSelect`) =>
  3.         rssDisplay.listData = rssFeeds(channelSelect.selection.index)._2
  4.         pack
  5.       case swing.event.ButtonClicked(`setConfigFileButton`) =>
  6.         if(configFileChooser.showOpenDialog(channelSelectPanel) ==
  7.             FileChooser.Result.Approve) {
  8.           configFileName = configFileChooser.selectedFile.getAbsolutePath
  9.         }
  10.       case swing.event.ButtonClicked(`loadRssButton`) =>
  11.         if(configFileName != "") {
  12.           rssFeeds = combineFeeds(getRssFeeds(configFileName))
  13.           channelSelect.peer.setModel(ComboBox.newConstantModel(rssFeeds.map(_._1)))
  14.           rssDisplay.listData = rssFeeds(0)._2
  15.           pack
  16.         }
  17.     }
  18.   }
  19. }
  20.  

For readability I've left the entire set intact above, I'll look at them for each case here.

A New Channel is Selected
  1.       case swing.event.SelectionChanged(\`channelSelect\`) =>
  2.         rssDisplay.listData = rssFeeds(channelSelect.selection.index)._2
  3.         pack
  4.  

Line 1: The case looks for the event, SelectionChanged and to which element that event is applied, channelSelect, the backquotes above are important.
Line 2: When the selection changes, set the display to contain the story titles from the rssFeeds value with the same index as the value selected.
Line 3: Once the values have been changed, packs the window so everything fits.

The Config File Button is Clicked
  1.       case swing.event.ButtonClicked(\`setConfigFileButton\`) =>
  2.         if(configFileChooser.showOpenDialog(channelSelectPanel) ==
  3.             FileChooser.Result.Approve) {
  4.           configFileName = configFileChooser.selectedFile.getAbsolutePath
  5.         }
  6.  

Line 2-3: Opens the configFileChooser and checks to make sure that a value has come back.
Line 4: Assigns the absolute path to the selected file to configFileName.

The Load Rss Button is Clicked
  1.       case swing.event.ButtonClicked(\`loadRssButton\`) =>
  2.         if(configFileName != "") {
  3.           rssFeeds = combineFeeds(getRssFeeds(configFileName))
  4.           channelSelect.peer.setModel(ComboBox.newConstantModel(rssFeeds.map(_._1)))
  5.           rssDisplay.listData = rssFeeds(0)._2
  6.           pack
  7.         }
  8.  

Line 2: Verifies that some value has been assigned to configFileName.
Line 3: Uses combineFeeds from RssReader to read in the RSS values.
Line 4: Set's the channelSelect ComboBox to contain the names of each of the RSS feeds.
Line 5. Set's the rssDisplay to contain the titles from teh first feed.

Final Summary

Scala has been an extraordinarily interesting language. The XML package alone makes it worth adding to your toolbox and, as can be seen by how clean the GUI design code is, it's not a one trick pony. The type system, which sadly I haven't been able to go into nearly enough detail here, has many of the nice features you'll find in Haskell. And of course it's great to be able to make use of all of Java's libraries for free.

I do have a couple of small complaints:

  1. The error messages are sometimes a bit obscure, but this is a common flaw in all but the most mature open source projects.
  2. When code compiles, each anonymous function gets its own class. For anyone coding in a functional style, this quickly leads to a deeply cluttered directory.

These complaints are minor though, and the first can certainly be fixed. Overall it's been a pleasure to work with and I look forward to using it for real tasks in the near future.

Coming Up

Next week I'll be starting on Prolog. Prolog is a declarative language that is centered on formal logic.

Resources

Filed under: gui, scala 2 Comments
24Jan/110

Prolog 4/4: GUI

Posted by Frank Berthold

Prolog 4/4: GUI

The Plan

I will write a GUI for last week's RSS reader in order to explore how to write a GUI in a logic based language using Prolog's XPCE toolkit.

The XPCE toolkit is:

  • An IDE for SWI-Prolog.
  • A library for writing GUI in Prolog.
  • An object layer for Prolog.

What to Expect

When this article is complete:

  • You will have:
    • A GUI front end for the RSS reader from last week
  • You will know:
    • How to use SWIPL-Win.
    • How to write a GUI in Prolog.

Files Used in this Project

  • rssGui.txt: The GUI code. (Change the extension from txt to pl)
  • rss.txt: The original RSS reader file, used here as a library. (Change the extension from txt to pl)
  • feeds.txt: Sample RSS feed config file.

The Code

Compiling the Code... More or Less

I discovered that compiling GUI code for XPCE is easier said than done. I made several attempts, read a couple of tutorials and got nowhere. If there are any Prolog guru's reading who wouldd like to set me straight, please leave a comment.

On the other hand, running the code on the is pretty easy:

  1. Download the files for this project and re-name them as described above.
  2. Double click on "rssGui.pl"
  3. In the SWIPL window, type, rssGui.

At that point, you should have a window with the RSS reader in it.

Loading Libraries And Other Activities

  1. :- consult(rss).
  2. :- pce_autoload(finder, library(find_file)).
  3. :- pce_global(@finder, new(finder)).
  4.  

To load the rss library from last week, tell Prolog to consult it. The two lines afterward load the file finder into memory. This makes it possible to open a file finder to select the rss config file.

Laying Out the GUI, and Adding Hooks

  1. rssGui :-
  2.   new(Frame, frame('RSS Reader')),
  3.   new(RssFeeds, browser),
  4.   send(Frame, append(RssFeeds)),
  5.   send(new(NewsList, browser(news)), right, RssFeeds),
  6.   send(RssFeeds, select_message, message(@prolog, newsDisplay, RssFeeds, NewsList)),
  7.   send(new(Buttons, dialog), below(RssFeeds)),
  8.   send(Buttons, append(button(load, message(@prolog, load, RssFeeds)))),
  9.   send(Frame, open).
  10.  

XPCE makes it possible to access objects in Prolog by adding three predicates:

  • new: Instantiates a new object.
  • send: Sends information to the object.
  • get: Gets information from an object.

To make a new GUI window, first a frame is created(Line 2) which contains all the GUI elements.
Lines 3 and 4 create a List Box to contain the channel names and adds it to the Frame.
Line 5 adds a List Box to the right side of the channel list box for displaying news titles.
Line 6 adds an action so that when a value is selected in RssFeeds, the newsDisplay predicate is fired with RssFeeds and NewsList for arguments.
Line 7 adds a container for a button. Line 8 adds a button to the dialog which has the text "load" and fires load when clicked.
Line 9 opens the frame.

Read the Config and Load the RSS Feeds

  1. load(Browser) :-
  2.   get(@finder, file, exists := @on, FileName),
  3.   readConfig(FileName, URLs),
  4.   send(Browser, clear),
  5.   foreach(member(URL, URLs), readAndListChannels(Browser, URL)).
  6.  

Line 2 opens up a standard file dialog and unifies the selected file with FileName, from there the code is much like the display code from last week.

I used a separate predicate to write each of the URLs to avoid potential logical contradictions in the foreach.

Fetch the Feed and Add it to the RssFeeds Browser

  1. readAndListChannels(Browser, URL) :-
  2.   catch((rssFetch(URL, XML),
  3.         rssRead(XML, RSS),
  4.         foreach(member(channel(Name, News), RSS),
  5.           send(Browser, append, create(dict_item, Name, Name, News)))),
  6.       _,
  7.       readAndListChannels(Browser, URL)).
  8.  

Again, much of this is similar to the code to display the feeds on the command line. The only real change is in Line 5, where the data from each channel is wrapped into a dictionary item with:

  1. identifier: Name
  2. key value: Name
  3. object: News

As before, this will keep trying until it succeeds or blows the stack.

Display the News Titles in the NewsList Browser

  1. newsDisplay(RssFeeds, NewsList) :-
  2.   get(RssFeeds, selection, Channel),
  3.   get(Channel, object, News),
  4.   get(News, size, SizeVal),
  5.   send(NewsList, clear),
  6.   foreach(between(1, SizeVal, LineNumber),
  7.       sendVector(NewsList, News, LineNumber)).
  8.  

Line 2 unifies Channel with the currently selected item in RssFeeds, the dictionary item from readAndListChannels.
Line 3 pulls the object from the dictionary item, a vector.
Line 4 determines the length of the vector.
Line 5 clears the NewsList so we don't just keep adding to the end.
Line 6-7 iterates over each of the values in the vector and appends them to NewsList with sendVector.

Append a Cell from a Vector to a Browser

  1. sendVector(Browser, Vector, Index) :-
  2.   get(Vector, element, Index, Value),
  3.   send(Browser, append, Value).
  4.  

Given a Browser, Vector and Index, sendVector pulls the Indexth element from the Vector and appends it to the Browser.

Final Summary

Prolog's logic based semantics make for an interesting programming experience. Up to this point I had trouble imagining how you'd deal with dynamic situations, like user input or a file being read in, in context to a language that is based on logic. Now that I've spent a month working with it, it's an incredibly elegant way to solve problems. That being said, I think I'm going to stick with Python for my day to day scripting needs.

Coming Up

Next week is a fifth Monday, so I'll be working with a tool, Robot Framework. Robot Framework is a Python based glue language for writing test cases in a clear format which is readable to non-technical users.

In February, I'll be working with Squeak. Squeak several neat features, it is:

  • generally considered the first full object oriented language
  • where unit tests frameworks came from (in the form that most of us are used to today)
  • contained entirely in its own design environment
  • designed to be useful as an education language
  • host to Seaside a powerful web framework

Resources

Tagged as: No Comments