Go graphical with Gtk

From LXF Wiki

It's only part five, but Paul Hudson can't wait any longer to show you the magic of Gtk#...

Let me rephrase that: I could quite happily wait to show you how Gtk# works, but it's been requested many times by readers and who am I to disagree with the hoi polloi? Mono is great fun to program with, as I hope you've found so far. But - at least in my opinion - it's when you start mixing in Gtk that the first problems start to come out. You see, Gtk was written by people who dislike the idea of object-oriented programming, but then try to recreate it in C. It is also hopelessly overengineered, making it almost impenetrable to the newcomer. So, for a change, you're lucky to have me around!

In the first four issues you have created a stock "Hello world!" program, a file locator, an RSS reader and a program that interfaces with Beagle. This issue, so that we can focus entirely on Gtk, we're going to go back to the RSS project and re-use some of its code to build a graphical RSS reader. Gtk isn't really /hard/, it's just thoroughly opaque - if you read something here you don't quite understand, you may need to go back a few paragraphs and try again, because it's very easy to lose track of where you are.


Table of contents

Stetic shock

As I've said already, Gtk is written in C, but there's a Mono/C# binding for it called, predictably, Gtk#. This is a very thin wrapper around the C code, which means it's pretty ugly to work with. But one part isn't ugly at all, and that's Stetic - the drag-and-drop GUI toolkit builder that comes as part of MonoDevelop. If you recall from the first part of this tutorial series, I'm using Fedora Core 6, so if you're using that too you're all set to use MonoDevelop with Stetic. If you're using a different distro, make sure you have v0.12 or later of MonoDevelop.

The first thing we're going to do is draw out our GUI using Stetic. While this doesn't give us any functionality, it does at least give you a good idea of how the finished program will look, and I've always thought it encouraging to make big leaps forward at the beginning of a new project. Another good reason for starting with the GUI first is that you can see fairly quickly whether your project will end up looking unwieldy or scary, and take steps to redesign it early on. Remember, the best projects are one that have very careful user interface design - don't just dive into writing the code and hope the UI will come together all by itself!

Make sure you have fully updated your Fedora install before trying this out, because Stetic is relatively new and it's best to have the latest MonoDevelop package just to minimise the chances of failure. You should also try to save regularly, because Stetic isn't terrifically stable, and it's also pretty unforgiving if you make mistakes.

So, please carefully follow the walkthrough on the opposite page, then continue reading on pXXXX.


Stetic vs Glade

Stetic and Glade are very similar: both are drag-and-drop ways of creating GUIs for Gtk. But Stetic is tied directly into MonoDevelop, meaning that it can generate code for you that attaches widgets to variables. You can use Glade in Mono if you want to, but Stetic is the recommended GUI designer at this time, and there's really no speed hit in using it.


TO DO - insert walkthrough here!


OK, that's the drag and drop part out of the way, but there's still more to be done before the GUI is complete. Specifically, there are three things that need to be done:

  1. We need to assign nice names to some widgets. Right now, all the widgets are named button1, textview2, and so on.
  2. We need to bind some widgets to fields. Doing this lets us access the widget in our C# code.
  3. We need to change the properties of some widgets to make them look and work as we want.

Widget names are really just properties, so 1) and 3) are both done using the Widget Properties pane in the bottom-right corner of the MonoDevelop window. You'll see the Bind To Field button just above the window you designed - I'll let you know when you need to use that.

So, here's the list of changes to make:

  1. Click the New button in the toolbar (not the toolbar itself!) Change its name to btnNew.
  2. Change the Refresh button's name to btnRefresh
  3. Change the Delete button's name to btnDelete
  4. Change the Tree View's name to tvFeeds. Click Bind To Field.
  5. Deselect Show Day Names for the Calendar (this makes it a bit smaller). Click Bind To Field.
  6. Change the Text View's name to be txtFeed. Click Bind To Field. Change its Height Request value to 300, and its Width Request value to 400 (this stops it taking over the whole screen)
  7. Change the name of the button beneath the notebook to Refresh All. Give it the name btnRefreshAll.
  8. Change the text for the notebook's pages to be By Feed for the tab with the Tree View, and By Date for the tab with the Calendar.

That's it: the GUI design is now finished, which means we can focus on plugging in functionality behind the scenes. This is partly done by tapping code straight into MainWindow.cs, but also by setting up signal handlers in Stetic. Signals are internal Gtk messages that get sent when something happens, such as a user clicking a button or presses a key. By default most of these signals do nothing at all, but we can subscribe to them with Mono event handlers by using Stetic - it even creates a basic method for us!

The first thing we're going to handle is the user selecting something in the Tree View. This view will be used to list all the RSS feeds the user has subscribed to, so when they select a feed from the list Chomp should download the latest RSS file and display it in the Text View on the right. We can act upon user clicks by catching the RowActivated signal - click the Tree View, then look in the Widget Properties pane and change the tab to Signals rather than Properties. Look in there for the RowActivated signal, and double click where it says "Click here to add a new handler". Give it the name OnRowActivated, and press Enter. If you now click the Source Code button immediately below the window designer, you'll see that Stetic has created the following method for you:

protected virtual void OnRowActivated(object o, Gtk.RowActivatedArgs args)
{
}

So far, so good, but unfortunately this is the first point at which you will come face-to-face with some of Gtk's ugliness. Specifically: pulling data out of a Tree View is far more complicated than you might think!


Adding signals

OK, so Chomp needs to handle feeds in two ways. If we're in the By Feed tab, double-clicking on one of the feeds in the Tree View ought to load that individual feed. If we're in the By Date tab, double-clicking on a date in the Calendar ought to load all the feeds, but only show entries that match the clicked date. To make our code a bit more unified, this is all going to be accomplished by creating a ReadFeed() method that loads a URL, then filters it by date. In the case of loading all the entries in a given feed, we'll be passing in the special value DateTime.MinValue, which we'll be using to mean "just ignore the date".

Here's the code you'll need to put into OnRowActivated:

TreeSelection select = tvFeeds.Selection;
TreeIter iter;
TreeModel model;
select.GetSelected(out model, out iter);
string val = (string)model.GetValue(iter, 0);
		
txtFeed.Buffer.Clear();
ReadFeed(val, DateTime.MinValue);

The first five lines are what I was referring to as Gtk's ugliness: it's how you extract a value from a Tree View widget. Yes, it's very messy, but it's designed to cope with all the different ways a Tree View can be used.

Once we have the selected value in the "val" variable, the Text View buffer gets cleared to wipe whatever is in there, then ReadFeed() is called to load and display the RSS. Ignore the ReadFeed() method call for now; I want to plug in the calendar first. To do that, look for the DaySelectedDoubleClick signal and give it the handler DayClicked. In the Source Code view MonoDevelop should have created an empty DayClick() method that looks like this:

protected virtual void DayClicked(object sender, System.EventArgs e)
{
}

That tells us when a user double clicks on the calendar, and - perhaps surprisingly - it's very easy to read the selected date from a Calendar widget. But before you start liking Gtk, let me show you how you actually read through all the items in a Tree View. Here's the code you need to put into DayClicked():

txtFeed.Buffer.Clear();
tvFeeds.Model.Foreach(FeedByDate);

So, that clears the text buffer to prepare for incoming feeds, then calls the Foreach() method of the Tree View's data model. That means the FeedByDate() method (which I haven't shown you yet - hold your horses!) gets called once for each item in the Tree View. Now, because the FeedByDate() method gets called by Foreach(), it needs to fit a very specific function prototype. That is, it needs to accept a precise list of parameters, otherwise it won't work.

What we want the FeedByDate() function to do is to read each feed it gets passed, then send that into ReadFeed() along with the date on the Calendar widget. This is pretty easy:

bool FeedByDate(TreeModel model, TreePath path, TreeIter iter)
{
   string url = (string)model.GetValue(iter, 0);
   ReadFeed(url, new DateTime(calendar.Year, calendar.Month + 1, calendar.Day));
   return false;
}

The first line there is the magic to get the URL to the feed out from the Tree View, then it gets passed into the ReadFeed() method along with the year, month and day selected in the calendar. But wait: notice that it's calendar.Month + 1? That's because Gtk very cunningly counts its months from zero. To make matters more interesting, it counts the days and years from 1. No, I've no idea why either, but that's just how it is!


Reading feeds, part 2

A couple issues ago we looked at the code needed to read RSS feeds. This issue we're using roughly the same code, with a few modifications:

  1. Output gets written to the text buffer, rather than the console.
  2. GUIDs aren't cached - we want Chomp to load all entries for each feed, rather than just entries we haven't seen before.
  3. If DateTime.MinValue is specified for the time, we take that to mean "don't filter by date".
  4. Otherwise, get the publication date of each news item, and convert it into a DateTime.
  5. Then compare each DateTime with the filter date that got passed in, and print the entry if we have a match.

There's one minor hiccup here: some feeds, including the LXF blog feed, provide their publication dates in the format Wed, 24 Jan 2007 11:02:43 +0000, whereas others use Wed, 24 Jan 2007 11:02:43 GMT. Both are supported by .NET, but, probably due to a bug, only the latter is supported by Mono. As a quick workaround for this, we need to spot any times that contain a + symbol, then strip out that whole part of the time.

Here's the code. If you read the previous XML tutorial you should recognise quite a bit of it!

protected virtual void ReadFeed(string feed, DateTime filter) {
   XmlDocument doc = new XmlDocument();
   doc.Load(feed);
   TextBuffer text = txtFeed.Buffer;

   XmlNodeList items = doc.SelectNodes("//item");
   foreach (XmlNode item in items) {
      if (filter == DateTime.MinValue) {
         text.Text += (item.SelectSingleNode("title").InnerText) + "\n";
         text.Text += ("   " + item.SelectSingleNode("description").InnerText) + "\n\n";
      } else {
         string time = item.SelectSingleNode("pubDate").InnerText;
         if (time.Contains("+")) time = time.Substring(0, time.IndexOf("+"));        
         DateTime thisdate = DateTime.Parse(time);
            
         if (filter.Day == thisdate.Day && filter.Month == thisdate.Month && filter.Year == thisdate.Year) {
            text.Text += (item.SelectSingleNode("title").InnerText) + "\n";
            text.Text += ("   " + item.SelectSingleNode("description").InnerText) + "\n\n";
         }
      }
   }
}

The two lines that might cause confusion in there are:

if (time.Contains("+")) time = time.Substring(0, time.IndexOf("+"));        
DateTime thisdate = DateTime.Parse(time);

The first line means "if this string contains a +, then set the time variable to be everything from the 0th character (the start of the string) up to the + sign", effectively stripping of the +0000 part. The second line means "take this time string, and convert it to a DateTime type." Having the date as a DateTime rather than a string is good because it lets us read the day, month and year as numbers, rather than trying to parse the string by hand.


The final straight

The first version of Chomp is almost complete, but there's one missing feature. Can you tell what it is yet? I'll give you a clue: double-clicking feeds loads them in. Double-clicking a date loads all the feeds and filters entries by the selected date. But at what point do we actually load feeds into the program? The answer is that we don't!

The previous XML reader loaded a list of feeds too, so we really just need to take that and modify it for our Gtk GUI. Specifically, rather than just reading the list and loading the feeds, we need to place each URL into the Tree View so that users can click on them. This requires a bit more Gtk ugliness, so strap yourself in:

string[] sitelist;
		
if (File.Exists("sitelist.txt")) {
   sitelist = File.ReadAllLines("sitelist.txt");
} else {
   sitelist = new string[0];
}
		
// this means we want to store a string and a string in the Tree View, but really we only need the URL!
TreeStore store = new TreeStore (typeof(string), typeof(string));
		
foreach(string site in sitelist) {
// the "Foo" is a placeholder
   store.AppendValues(site, "Foo");		
}
		
tvFeeds.Model = store;
tvFeeds.AppendColumn("URLs", new CellRendererText(), "text", 0);

To have the Tree View actually show the data, it requires at least one column. You're welcome to disable headers in the Stetic widget properties, because they are unnecessary. Now, create the sitelist.txt file in your program's working directory (probably /path/to/solution/bin/Debug), fill it with URLs to feeds, and give it a try!

All the code for this tutorial is on your coverdisc, along with a few extra features (see the box "Chomp 0.2"). But there's still a lot more that Chomp needs to do if it ever wants to appear on Sourceforge. I leave it in your capable hands...

///BODY COPY ENDS///

Chomp 0.2

The version of Chomp on your coverdisc builds upon what we've done here by letting users temporarily add new feeds, but there are still many more things you can do to build upon this base in your own time:

Make Delete Feed work Cache feeds and GUIDs, so that the cached versions are loaded from memory rather than always redownloading Let users search through the cached feed data Make Refresh and Refresh All work Add something to the status bar! Fill out the menu bar with meaningful actions Have user-added feeds get added to sitelist.txt

This tutorial was really about teaching you how to use Stetic, and I think we've accomplished that pretty well. From here, it's mostly about writing lots of code to hook up all the different parts of the interface and make it do cool things. Save your work regularly and you'll be fine - have a play!

Quick tip

Gtk is designed to let GUIs scale smoothly as they are resized, and also to allow things to grow and shrink as needed to allow for other languages, larger fonts and other run-time options. That's why we use VBox and HBox widgets rather than trying to place widgets pixel by pixel onto the window.

Quick tip

Make sure the sitelist.txt file contains URLs to feeds, one per line, and that it exists in the same location as your chomp.exe file. Usually this is in the bin/Debug directory, or bin/Release if you've done a release build.


Quick tip

By the time you read this it's very likely that a 1.0 release of MonoDevelop will be generally available, albeit probably in Beta. Stetic is one of the areas that's heavily under construction, so you'll probably find the newer release fixes many existing bugs and may well even add new features. Worth checking out!