Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Browsing xkcd in a Windows 7 way

0.00/5 (No votes)
25 Jan 2010 2  
A desktop application with new Windows 7 features for browsing xkcd.

Contents

Introduction

In this article, I will present a Windows desktop application which allows to browse xkcd comics. When you launch the application, it shows the latest xkcd that is published at the website. You can browse other xkcds as you would do it at the website. It also utilizes features new in Windows 7 such as jump lists, thumbnail previews, and thumbnail toolbar to provide better user experience.

How the Application Works

Parsing xkcd.com

In order to display comics and related information from xkcd.com, we first need to parse the page. We need to retrieve the comic title, ID, image location, and the mouse-over text. This can be easily done by using the HTML Agility Pack library. The HTML Agility Pack also provides an application called HAPExplorer where you can load the source of a webpage and build the XPath expression you need. As you can see from the image below, we need the following XPath expression:

/html[1]/body[1]/div[1]/div[2]/div[1]/div[2]/div[1]/div[1]

HAPExplorer

After that, we need the h3 and img elements to retrieve the necessary information. Here is the corresponding code:

public xkcd(string xkcdUrl)
{
    //Get the source of the page
    HtmlWeb loader = new HtmlWeb();
    HtmlDocument doc = loader.Load(xkcdUrl);

    //Extract the part we need
    HtmlNode mainnode = doc.DocumentNode.
      SelectSingleNode("/html[1]/body[1]/div[1]/div[2]/div[1]/div[2]/div[1]/div[1]");
    HtmlNode img = mainnode.SelectSingleNode("img");
    HtmlNode h3 = mainnode.SelectSingleNode("h3");

    //Finally get the details we need
    ImagePath = img.Attributes[0].Value;
    MouseOver = img.Attributes[1].Value;
    Title = img.Attributes[2].Value;

    int temp = 0;
    int.TryParse(h3.InnerText.Replace("Permanent link to this comic: http://xkcd.com",
                                              string.Empty).Replace("/", string.Empty),
                           out temp);
    ID = temp;
    Url = string.Format("http://xkcd.com/{0}/", ID);

    //And the image too
    using (WebClient client = new WebClient())
    {
      using (Stream imagestream = client.OpenRead(ImagePath))
        {
          Image = Image.FromStream(imagestream);
        }
    }
}

Communication with the Form

As the previous source code snippet shows, we have a class called xkcd which knows how to get the required information. However, the form which displays it never instantiates the class directly. Instead, all the interaction goes through the xkcdService class. This class exposes the following methods for getting an xkcd:

  • GetLast
  • GetFirst
  • GetRandom
  • GetPrevious
  • GetNext

As getting the source of a webpage requires some time, all the above methods are executed asynchronously. When an xkcd object is available, the xkcdService class fires the xkcdLoaded event and passes the downloaded object.

Apart from this, the xkcdService class keeps track of the current xkcd ID, and exposes two properties indicating whether there is a previous and next xkcd or not. Also, it keeps a cache of the loaded objects so that if the object is requested for a second time, it is returned instantly. The code snippet below shows how all this is done:

public void GetNext()
{
    //If it is cache return it, otherwise download it.
    if (cache.ContainsKey(currentID + 1))
    {
      OnxkcdLoaded(cache[currentID + 1]);
    }
    else
    {
      worker.RunWorkerAsync(string.Format("http://xkcd.com/{0}/", currentID + 1));
    }
}

private void worker_DoWork(object sender, DoWorkEventArgs e)
{
    string arg = e.Argument as string;

    if (!string.IsNullOrEmpty(arg))
    {
      xkcd temp = new xkcd(arg);

      if (!cache.ContainsKey(temp.ID))
      {
        cache.Add(temp.ID, temp);
      }

      if (arg.Equals("http://xkcd.com/"))
      {
        lastID = temp.ID;
      }

      currentID = temp.ID;
      e.Result = temp;
    }
}

private void OnxkcdLoaded(xkcd result)
{
    currentID = result.ID;

    //Specify if previous and next comic exist
    HasPrevious = result.ID > 1;
    HasNext = result.ID < lastID;

    if (xkcdLoaded != null)
    {
      xkcdLoaded(this, new ExtendedEventArgs<xkcd>(result));
    }
}

The form is subscribed to the xkcdLoaded event, and displays the new comic.

Windows 7 New Features

Taskbar Progress Bar

In order to report progress when a comic is being loaded, the program uses the taskbar progress bar. As the time for loading a comic is not known, the progress bar is in an indeterminate state. To achieve the desired effect, only a single line of code is needed:

TaskbarManager.Instance.SetProgressState(TaskbarProgressBarState.Indeterminate);

The result we get looks like this:

Toolbar Buttons

The program also uses toolbar buttons for easier navigation between the comics. Toolbar buttons are ordinary buttons placed at the thumbnail preview. As we will see below, creating them is easy, and consists of three steps:

//Step 1: Create the buttons.
first = new ThumbnailToolbarButton(Properties.Resources.First, "First");
prev = new ThumbnailToolbarButton(Properties.Resources.Prev, "Previous");
random = new ThumbnailToolbarButton(Properties.Resources.Random, "Random");
next = new ThumbnailToolbarButton(Properties.Resources.Next, "Next");
last = new ThumbnailToolbarButton(Properties.Resources.Last, "Last");

//Step 2: Subscribe to events.
first.Click += new EventHandler<thumbnailbuttonclickedeventargs>(firstButton_Click);
prev.Click += new EventHandler<ThumbnailButtonClickedEventArgs>(prevButton_Click);
random.Click += new EventHandler<ThumbnailButtonClickedEventArgs>(randomButton_Click);
next.Click += new EventHandler<ThumbnailButtonClickedEventArgs>(nextButton_Click);
last.Click += new EventHandler<ThumbnailButtonClickedEventArgs>(lastButton_Click);

//Step 3: Add buttons to toolbar.
TaskbarManager.Instance.ThumbnailToolbars.AddButtons(this.Handle, first, prev, 
                                            random, next, last);

Here is how these buttons appear:

Thumbnail Previews

Thumbnail preview appears when you hover the mouse over the window in the taskbar. Your application gets a default thumbnail without a single line of code, but to get the most out of them, you can customize it according to your needs. As the application displays comics, I decided that it would be better if the thumbnail displayed only the current comic and nothing else. To achieve the desired behaviour, I wrote an extension method which returns a rectangle corresponding to the current image. After that, using it as a thumbnail preview is as easy as:

TaskbarManager.Instance.TabbedThumbnail.SetThumbnailClip(
          this.Handle, xkcdPictureBox.GetImageRectangle());

As a result, we get a thumbnail of the current comic which is displayed on the image above.

Jump List

As the name suggests, a jump list is a list of tasks where you can jump to. They appear when you right-click a window in a taskbar and provide access to frequently used tasks. This program has two tasks: one for visiting http://xkcd.com/about/, and another for saving the current comic to disk. Tasks do not expose an event when they are clicked. Instead, they launch an external application. As a result, to create a task, you need to supply the path of the application that will be launched when the task is clicked and an icon displayed by the taskbar. For the first task, the application path needs to point to the default browser path. For the second task, we need to launch our program a second time, but we need to save an image from the first one. Let's see what we can do to overcome these problems.

Getting the Default Browser Path

As it turns out, we can retrieve the default browser path by using the AssocQueryString function. According to MSDN, the function "Searches for and retrieves a file or protocol association-related string from the Registry." With the help of pinvoke.net, I wrote the following method to get the browser path:

private string GetDefaultBrowser()
  {
    uint pcchOut = 0;

    //First get the string length
    NativeMethods.AssocQueryString(NativeMethods.AssocF.Verify,
                NativeMethods.AssocStr.Executable, ".html", 
                null, null, ref pcchOut);
    StringBuilder pszOut = new StringBuilder((int)pcchOut);

    //Then retrieve the path
    NativeMethods.AssocQueryString(NativeMethods.AssocF.Verify, 
                NativeMethods.AssocStr.Executable, ".html", 
                null, pszOut, ref pcchOut);
    return pszOut.ToString();
  }

Communication Between Program Instances

As we cannot access the memory of the first instance from the second instance, we need to notify it that the user wants to save an image and then quit. We can achieve it by sending a Windows message by using the SendMessage function. The message that we send is registered by the program with a call to the RegisterWindowMessage function. The first instance listens to messages by overriding the WndProc method. So, when the second instance is launched, it checks for command arguments, and if found, sends the message to the first instance. Before it sends the message, it first needs to find the handle of the window which will receive it. This is achieved by the FindWindow function. It sounds a little bit complicated, but as we can see, it is quite easy:

static void Main(string[] args)
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    if (args.Length == 0)
    {
      Application.Run(new MainForm());
    }
    else
    {
      //Find first instance window and send the message.
      IntPtr handle = NativeMethods.FindWindow(null, "xkcd Browser");
      NativeMethods.SendMessage(handle, NativeMethods.WM_SAVE, 
                                IntPtr.Zero, IntPtr.Zero);
    }
}

class NativeMethods
{
    public static readonly uint WM_SAVE;

    static NativeMethods()
    {
      WM_SAVE = RegisterWindowMessage("savexkcd");
    }
}

protected override void WndProc(ref Message m)
{
    //If it is the message we need
    if (m.Msg == NativeMethods.WM_SAVE)
    {
      using (CommonSaveFileDialog save = new CommonSaveFileDialog())
      {
        save.DefaultExtension = "png";
        save.DefaultFileName = titleLabel.Text;
        save.AlwaysAppendDefaultExtension = true;
        save.Filters.Add(CommonFileDialogStandardFilters.PictureFiles);

        //Show the SaveFileDialog
        if (save.ShowDialog() == CommonFileDialogResult.OK)
        {
          if (xkcdPictureBox.Image != null)
          {
            xkcdPictureBox.Image.Save(save.FileName, ImageFormat.Png);
          }
        }
      }

      return;
    }

    base.WndProc(ref m);
}

Creating the Jump List

As we already know how to retrieve the browser path and communicate between different instances, we are now ready to create the tasks. The process is easy and straightforward. As you can see, the default browser path is used for the task icon too.

private void InitializeJumpList()
{
    string browser = GetDefaultBrowser();

    //Create jump list
    JumpList jumpList = JumpList.CreateJumpList();

    //Add tasks to the newly created jump list
    jumpList.AddUserTasks(new JumpListLink(browser, "About xkcd")
    {
      IconReference = new IconReference(browser, 0),
        Arguments = "http://xkcd.com/about/"
    });

    jumpList.AddUserTasks(new JumpListLink(
       Application.ExecutablePath,"Save current comic")
    {
      IconReference =new IconReference(
        Path.Combine(Application.StartupPath,"program.ico"), 0),
        Arguments = "save"
    });

    //Finally commit the changes we made.
    jumpList.Refresh();
}

I guess it's time to see the result:

Final Notes

If you are wondering why I built this application, the answer is simple: It was fun. The problems were challenging, and playing with the Windows 7 API is also interesting.

Comments, ideas, suggestions, and votes are welcome.

References

History

  • January 25, 2010 - Initial release.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here