Introduction
When I first started using Windows 7, I knew there had to be a way to use some of the cool new features to help me keep up to date with all my Gmail accounts. But alas, I could not find any existing applications that worked the way I wanted. So, I decided to write an application that would allow me to fulfill two desires at the same time. Those desires were to learn more about these new Windows 7 features and to get a Gmail notification application that behaved the way I wanted it to. A win-win proposition.
In this article, I'll tell you how to use the Windows 7 features this application uses as well as tell you about some of the things I learned the hard way. Hopefully this will be useful to some of you out there.
Even though this application is called Gmail for Windows 7,
it will run in other versions of Windows. It does this by checking for Windows 7 with the TaskbarManager.IsPlatformSupported
property before executing any Windows 7 specific code.
Here are some screen shots
Main Screen
Taskbar Notifications
Custom Thumbnails
Background
I first took an inventory of the new Windows 7 features that might be useful in an application such as this. The following were on my initial list of things to consider:
- Jump Lists
- Application Preview Thumbnails
- Icon Overlays
- Taskbar icon progress indicator
In the end, I decided to use all of these. I had not used the progress indicator at first because the check was so fast. However, I decided it was worth putting in for the teaching aspect if nothing else.
I quickly found out about the Windows® API Code Pack for Microsoft® .NET Framework and decided to use that.
So, in the end this application uses the following things that may be of interest to other developers.
- Gmail's atom feed
- In memory bitmap/icon generation
- Windows® API Code Pack
- Popup Balloon Notification window
- Windows 7 Features
- Jump Lists
- Application Preview Thumbnails
- Icon Overlays
- Progress Indicator
Using the Code
The Visual Studio 2008 solution is included in the source zip file. You should be able to just unzip this to any directory and open the GmailForWin7.sln file.
Points of Interest
There are several areas that are worthy of mention in this project:
- Retrieving Unread Messages From Gmail
- Main form
- JumpLists
- Thumbnails
- Balloon notification window
- Settings dialogs
- Encryption
- Progress Indicator
1. Retrieving Unread Messages From Gmail
The thing I did not like about some Gmail notifiers was that they used POP or IMAP to retrieve messages from Gmail. The problem with these was that they move the messages out of your inbox when retrieved this way. I really just wanted to know if I had any interesting mail at anytime. If I did, then I could easily fire up Gmail to take care of those messages. I also wanted to be able to do this from multiple locations. So, I wanted a way of retrieving my unread messages without affecting the status of those messages. It turns out that Gmail has an ATOM feed that fits this bill very well. The only thing I do not like about this feed is that you can only get a summary line of text about each email message and not the entire content. I could not find a way to query this for the entire text. However, I really only needed the subject line and the first bit of a message to know if I had anything worth going to read anyway. So, this was good enough and I moved forward using the ATOM feed.
The code to retrieve these messages into an XmlDocument
turned out to be surprising easy:
string GmailAtomUrl = @"https://mail.google.com/mail/feed/atom";
XmlUrlResolver xmlResolver = new XmlUrlResolver();
xmlResolver.Credentials = new NetworkCredential(account.UserName, account.Password);
XmlTextReader xmlReader = new XmlTextReader(GmailAtomUrl);
xmlReader.XmlResolver = xmlResolver;
try
{
XNamespace ns = XNamespace.Get("http://purl.org/atom/ns#");
XDocument xmlFeed = XDocument.Load(xmlReader);
var emailItems = from item in xmlFeed.Descendants(ns + "entry")
select new
{
Author = item.Element(ns + "author").Element(ns +
"name").Value + "(" + item.Element(ns +
"author").Element(ns + "email").Value + ")",
Title = item.Element(ns + "title").Value,
Link = item.Element(ns + "link").Attribute("href").Value,
Summary = item.Element(ns + "summary").Value,
Date = item.Element(ns + "issued").Value
};
XmlDocument document = new XmlDocument();
document.Load(xmlReader);
2. Main Form
The main form has a tab for each Gmail account configured. You may have up to 5 accounts set up. These tabs will contain a list of all unread messages in that account's Gmail inbox.
The main things of interest about this form are that it controls JumpList
creation and updating, handles messages generated by the JumpList
items, updates the taskbar overlay icon, and controls displaying balloon notification messages.
3. JumpLists
The Windows API code pack makes working with JumpList
s very easy. The biggest thing I learned about JumpList
s was that a JumpList
is not hosted by your application but is hosted by the operating system itself. Therefore, an application does not have as much control over a JumpList
as I thought it would. It really just controls what items go into it. A JumpList
item can refer to a URL, a file location, or an application filename. Clicking on an item in the JumpList
results in that item being executed by Windows 7. It does not result in a message being sent back to your application. Basically, clicking on an item in a JumpList
does the same thing that typing the string at the command prompt would do.
One of the predefined categories in a JumpList
is the Task category. This application adds two tasks to this group: Check Now and Settings. Since a message is not sent back to the application when a JumpList
item is selected, then a means has to be devised to communicate that event to the instance that is currently running. Like most other applications that do something similar, I chose to fire up a new instance of the program and then send a message to the currently running instance to let it know it needed to perform some action.
The WindowsMessageHelper
class was very helpful for this purpose. It registers two custom windows messages with the OS that are used to communicate between the two application instances. When a JumpList
task link is clicked, it starts up another instance of the application, with one of two program arguments to tell it that a given task is desired. The second instance looks to find the currently running program and sends a windows message to that instance based on the action desired.
However, before we can send messages from a JumpList
we have to create one. Creating a jump list is very simple:
JumpList.CreateJumpList();
This is how I created the task links:
List tasks = new List();
tasks.Add(new JumpListLink(Assembly.GetEntryAssembly().Location, "Check Now")
{ Arguments = "CheckNow" });
tasks.Add(new JumpListLink(Assembly.GetEntryAssembly().Location, "Settings...")
{ Arguments = "Settings" });
_jumpList.AddUserTasks(tasks.ToArray());
Limited Number of Items
I learned the hard way, that you can only have a fixed number of items in a JumpList
. This is a system wide setting and cannot be changed without effecting all JumpList
s. You can get the maximum number of non-task items with the read-only property JumpList.MaxSlotsInList
. It seems you can have up to 2 * JumpList.MaxSlotsInList
total items, both tasks and non-task category related items, in a JumpList
. If you have more tasks than JumpList.MaxSlotsInList
, then your non-task items start losing slots.
For example, I have JumpList.MaxSlotsInList
set to the default of 10
. I added 11 tasks and it took away 1 slot from my non-task items. I added 12 tasks and it took away 2 user non-task slots.
Once a JumpList
is created, you can add categories like this:
IconReference iconRef = new IconReference
(Assembly.GetEntryAssembly().Location, IconBaseIndex + account.IconIndex);
JumpListCustomCategory category = new JumpListCustomCategory(account.AccountName);
categories.Add(category);
_jumpList.AddCustomCategories(categories.ToArray());
I had to go to a lot of trouble to get that simple IconReference
to work. The OS cannot link to icons in a managed application. So, I had to create an old fashioned RC file that referred to these icons. I then linked those resources into the application in the Application section of the properties for the project.
Once you have a category created, you can add items to it like this:
List<JumpListLink> listItems = new List<JumpListLink>();
JumpListLink link = new JumpListLink("http://gmail.google.com", "Gmail");
category.AddJumpListItems(listItems.ToArray());
If you want to fire up an application and give it some arguments, then you can use something like this:
JumpListLink link = new JumpListLink(Assembly.GetEntryAssembly().Location, "Error")
{ Arguments = "Settings" }
An important point to remember is that JumpList
changes are not shown to the user until Refresh
is called on the JumpList
like this:
_jumpList.Refresh();
4. Thumbnails
Another cool new feature of Windows 7 is the ability to have tabbed previews (called Thumbnails) which are accessible from the taskbar icons. I really liked this when I noticed I could see all my open tabs in Internet Explorer 8 and select which I wanted to bring up, all from the taskbar. I really wanted to incorporate that into this application. So, I figured each account could be its own tab and have its own preview thumbnail.
I first added these just like most applications do, they grab a screen shot of the tab/screen to use as a preview and display a scaled down version as the thumbnail. There is a nice built in method that makes this very easy called TabbedThumbnailScreenCapture.GrabWindowBitmap
. Things were going great.
But, I quickly ran into a problem. I start this Gmail notification program in minimized mode from my Startup Folder. This was a problem because before you can grab a screenshot the application has to have been displayed at least once, and starting minimized does not count. So, the only way around that was to have some hack that quickly displayed the window and then hide it again. I didn't like that. But, there was a bigger reason that made me not like the whole approach of a scaled down screen shot, and that was that it just was not of any use. All the tabs looked pretty much the same and you could not really tell them apart. So, I decided to delve a little deeper into what was behind these thumbnails.
It turns out that these thumbnail images are just bitmap images. So, you can generate any bitmap you want to display for the thumbnail. I decided that it would be very useful if you could actually read the subjects of your new email messages without having to open up the main screen. So, I generated a bitmap that was simply a list of the email messages drawn on a bitmap. The hardest part of this was getting the scaling to work out so that the text displayed nicely. It turns out the code to set this up is pretty simple:
TabbedThumbnail preview =
TaskbarManager.Instance.TabbedThumbnail.GetThumbnailPreview(tab);
using (Bitmap = new Bitmap(300, 300))
{
preview.SetImage(bitmap);
}
Updating Thumbnails
Once created, these thumbnails typically reflect the current state of the window they represent. So, if changes happen to the window then its associated thumbnail needs to be updated. This is accomplished by replacing the bitmap assigned to the thumbnail. The bitmap is set using the TabbedThumbnail.SetImage
method. You have two choices here about when to update the thumbnail images. You can listen for the TabbedThumbnail.TabbedThumbnailBitmapRequested
event and call SetImage
in the event handler. Or you can chose to update the images on your own schedule when it makes sense to update the images. For this application, it was a pretty easy choice, since the content of the thumbnail is pretty static. The thumbnail is updated after each mail check and I did not listen for the TabbedThumbnail.TabbedThumbnailBitmapRequested
event.
Closing Thumbnails
If you notice, on all thumbnails displayed there is a little X in the upper left corner. This is to allow you to close the window from the taskbar without having to bring up the window. This is fine for most applications however the thumbnails in our application represent Gmail accounts and it makes no sense to close an account.
So, I went about finding out how to remove or disable that little close button. I was disappointed to learn that there is no way to disable that button. Well then, I thought, if I cannot disable it I'll just prevent it from doing anything when clicked on. So, I listened for the TabbedThumbnail.TabbedThumbnailClosed
event and tried to prevent the thumbnail from being removed. However, there was nothing to be done at that point as the preview object had already been removed when this event is called. So, I decided to try and fire a cancelable event before the thumbnail was removed. I had to include the source for the Windows API code pack in the solution to do that. I then modified the TaskbarWindowManager.DispatchMessage
method and fire a new event before it removed the preview from its list of windows. The event worked just fine and I cancelled any further action from the TaskbarWindowManager
class. However, that is when the fact that the JumpList
is hosted by the OS came back to bite me. The message the .NET code was handling in regard to the thumbnail being clicked was merely to tell it that the thumbnail was already closed. There was nothing I could do to prevent the OS from closing the thumbnail. So, I backed out all of my modifications to the code pack and decide to try and think up another solution.
Something to think about: I mentioned that the OS hosts the JumpList
and notifies your application of actions taken by the user in the JumpList
by executing commands not by sending messages back to your application. However, the TabbedThumbnails
are able to send messages back to your application. So it appears that your application is at least partially responsible for handling the thumbnail manipulation. This seems to be true since the TaskbarWindowManager
looks for and dispatches windows messages for these thumbnails. However, it does not appear to draw these thumbnails, so I still am not sure where that takes place.
The solution that I finally came up with, was to not try and stop the thumbnail from being closed at all, but to let it close and then simply open up a new one with the same info. Yes, this is kind of kludgy, but I could find no other way to get this to work. There may be a way if you hook into the COM APIs relating to JumpList
s but the solution I came up with was workable so I stopped there.
Selecting a Thumbnail
I mentioned an event that is fired when a thumbnail is closed, well there is also an event that is fired when a thumbnail is selected called TabbedThumbnail.TabbedThumbnailActivated
. This allows your application to display the appropriate window when its thumbnail is clicked on.
5. Balloon Notifications
I wanted to be notified of new mail messages with balloon popups similar to Outlook or other applications. I could find no built in support for this in Windows 7. So, I wrote a simple window that looks like other balloon notifications I have seen. The most interesting thing about this class is that I create some nice rounded edges using the CreateRoundRectRgn
native function. It calls this function from within the overridden OnSizeChanged
method.
This window also uses the Graphics.MeasureString
method to help it compute the size it needs to be and then positions itself in the lower right corner of the primary monitor. The dimensions and location of the primary monitor are determined by the Screen.PrimaryScreen.WorkingArea
property.
6. Settings Dialogs
Nothing really special here. Just a place to tweak settings and edit Gmail accounts. The most interesting thing in these dialogs would be in the Account settings dialog where I use an owner drawn drop down list to display the icons to chose from. I manage this by listening for the DrawItem
event on the ComboBox
control.
7. Encryption
Since part of the account information is the password, then I wanted to store this securely. I also didn't want to go overboard in trying to secure this information. I decided that it would be sufficient to encrypt these passwords using a key generated from the settings of the current user that are not likely to change. The code to encrypt and decrypt the password is as follows (I left out the GetKeyBytes
method but it generates a key based on computer and user settings):
private const int KeySize = 24;
private static string EncryptPassword(string password)
{
byte[] toEncryptArray = UTF8Encoding.UTF8.GetBytes(password);
byte[] keyArray = GetKeyBytes(KeySize);
TripleDESCryptoServiceProvider tdes = new TripleDESCryptoServiceProvider();
tdes.Key = keyArray;
tdes.Mode = CipherMode.ECB;
tdes.Padding = PaddingMode.PKCS7;
ICryptoTransform cTransform = tdes.CreateEncryptor();
byte[] resultArray = cTransform.TransformFinalBlock(toEncryptArray,
0, toEncryptArray.Length);
return Convert.ToBase64String(resultArray, 0, resultArray.Length);
}
private static string DecryptPassword(string encryptedPassword)
{
byte[] toEncryptArray = Convert.FromBase64String(encryptedPassword);
TripleDESCryptoServiceProvider tdes = new TripleDESCryptoServiceProvider();
tdes.Key = GetKeyBytes(KeySize);
tdes.Mode = CipherMode.ECB;
tdes.Padding = PaddingMode.PKCS7;
ICryptoTransform cTransform = tdes.CreateDecryptor();
byte[] resultArray = cTransform.TransformFinalBlock(toEncryptArray,
0, toEncryptArray.Length);
return UTF8Encoding.UTF8.GetString(resultArray);
}
8. Progress Indicator
Even though the mail check is very fast and you do not see the progress indicator for long, I thought it was worth putting it in this application just to show how easy it was to use. Since I have no idea how long it will take to perform the mail check I use the version of the progress indicator that merely indicates the application is busy, I do not use the progress bar that shows the % complete. To start the progress indicator do the following:
TaskbarManager.Instance.SetProgressState(TaskbarProgressBarState.Indeterminate);
To turn it off simply call the same method with TaskbarProgressBarState.NoProgress
.
TaskbarManager.Instance.SetProgressState(TaskbarProgressBarState.NoProgress);
Left To Accomplish
First Thing Left to Accomplish
I tried very hard to automate logging into different Gmail accounts. I wanted you to be able to click on an email link, from any account, and have that email message come up in the browser. However, after many fruitless (and sleepless) hours I finally gave up. There are a lot of articles talking about how to accomplish this, but Google seems to have stopped supporting the ability to perform automated login through a URL link.
So, this means that if you click on the Open in Gmail link, that it will only bring up that email message if you happen to already be logged into that Gmail account. So, if you are running this application with a single Gmail account, you should not have any problems.
Second Thing Left to Accomplish
The settings are currently stored in the application's config file and not on a per user basis. I will probably come back and change this, as it does bug me. I did start to use the Properties settings stuff for the C# project, where it generates a class with the given properties for you. However, when you set something to be stored in the user scope, it tends to change which config file it wants to load ALL THE TIME on you. It was very frustrating to have to re-enter my settings OVER AND OVER again during development.
History
- Version 1.0.0.0 - 17 Jan 10
- Version 1.0.0.1 - 25 Jan 10
- Added Open Gmail task
- Now uses Progress indicator