C# Programming

Introduction

Summary

This article will demonstrate how to create a screen saver in C# and how to work with the Outlook object model. The complete source code and application are available way at the bottom. Both are free software under the GNU GPL.

Idea

It seems to me that programmers always get stuck with the bad cubicles. At a now defunct software company we were seated in an "open environment". This was a polite way of saying "cubicles without any walls". It was supposed to increase collaboration or some such, oddly none of the higher-ups felt the need to increase their collaboration in this manner. A result of this arrangement is that you'd have a cubicle-buddy, someone you spent 6-8 hours a day roughly 18 inches away from. I was quite fortunate to have a buddy who was away from his desk a lot. However, this resulted in me being asked for his whereabouts on a regular basis. The conversations went remarkably like:

Coworker: Hey, where's Skyler?

Me: I don't know.

Coworker: Do you know where he went?

Me: I don't know.

Coworker: Do you know when he'll be back?

Me: Look, I'm really not his secretary.

Coworker: Jerk.

After experiencing this delight a few dozen times I had a revelation. What Skyler needs is a screen saver that displays where he is. Something that goes out and reads his Outlook calendar and displays his public schedule. Then next time someone comes by and inquires "Where's Skyler?" I can grunt and point at his monitor.

Setting up the project

Project

The first step is to create a new console application. A Windows screen saver is essentially a .exe with a different extension.

New project

Configuring post-build rename

By default console applications are compiled into a .exe. Since there's really no difference between a .exe and a .scr a simple rename task is all that's needed. However, Visual Studio likes to lock files so a copy is used instead.

Post build rename

Add references

The next step is to add a reference to the Outlook interop library.

Add reference

This example uses Outlook 9 (AKA Outlook 2000) and has not been tested against other versions (but will probably work with Outlook XP/2003).

Main

The main program is pretty brief; the release portion of the code is the most interesting. This section checks for command-line arguments and either displays the configuration page or the screen saver. The supported command-lines are:

/c - show the configuration form

/p - preview the screen saver (called "Test" from the Windows context menu)

/s - show the screen saver

In this screen saver /p and /s do the same thing.

if((args.Length<1)||(string.Compare(args[0],"/c",true)==0))

{

    Application.Run(new ConfigurationForm());

}

else if((string.Compare(args[0],"/p",true)==0)||(string.Compare(args[0],"/s",true)==0))

    Application.Run(new SaverForm());

}

else

{

    MessageBox.Show("Unsupported command line argument: "+args[0]);

}

Configuration form

Overview

The configuration form is used to set the options for the screen saver. The configurable options are:

Font: The name of the font to use.

Default Message: What gets displayed if no appointments are occurring at the specified time.

Show Errors: There's a region on the screen saver that displays any errors that occurred loading the appointments. If this box is checked they will be displayed.

ConfigurationForm_Load method

Two things happen on the load event. First, a list of all installed fonts is loaded into the font dropdown list. Next, any previously saved settings are loaded from the registry.

private void ConfigurationForm_Load(object sender, System.EventArgs e)

{

    Cursor.Current=Cursors.WaitCursor;

    //load the list of installed fonts

    InstalledFontCollection installedFonts=new InstalledFontCollection();

    foreach(FontFamily fontFamily in installedFonts.Families)

    {

       

this.fontDropdown.Items.Add(fontFamily.Name);

    }

    //load saved values

    RegistryKey key=Registry.CurrentUser.OpenSubKey("Software",false);

    RegistryKey subKey=key.OpenSubKey(Application.ProductName,false);

    if(subKey!=null)

    {

        this.fontDropdown.Text=(String)subKey.GetValue("fontName",string.Empty);

        this.defaultMessageTextbox.Text=(String)subKey.GetValue("defaultMessage","I'll be right back.");

       this.displayErrorsCheckbox.Checked=((String)subKey.GetValue("showErrors","True")).Equals("True");

    }

    Cursor.Current=Cursors.Default;

}

Saving the settings

All settings chosen by the user are saved to the registry.

private void okButton_Click(object sender, System.EventArgs e)

{

    Cursor.Current=Cursors.WaitCursor;

    //save settings

    RegistryKey key=Registry.CurrentUser.OpenSubKey("Software",true);

    RegistryKey subKey=key.OpenSubKey(Application.ProductName,true);

    if(subKey==null)

    {

        subKey=key.CreateSubKey(Application.ProductName);

    }

    subKey.SetValue("fontName",this.fontDropdown.Text);

    subKey.SetValue("defaultMessage",this.defaultMessageTextbox.Text);

    subKey.SetValue("showErrors",this.displayErrorsCheckbox.Checked.ToString());

    //exit

    Cursor.Current=Cursors.Default;

    this.Close();

}

SimpleAppointment.cs

Purpose

This class is used to shield the main application from having to work directly with the Outlook object model. There were a couple of motivations for doing this. It theoretically helps the application support other calendar programs. All the "business rules" work with SimpleAppointment and not the vendor-specific object model. Also by creating a custom appointment class comparison logic can be implemented.

[note: xml documentation snipped, some code re-formatted to reduce printed length]

public class SimpleAppointment

{

    #region private members

    private DateTime startTime;

    private DateTime endTime;

    private string description;

    #endregion

    #region public properties

    public DateTime StartTime

    {

        set{ this.startTime=value; }

        get{ return(this.startTime); }

    }

    public DateTime EndTime

    {

        set{ this.endTime=value; }

    get{ return(this.endTime); }

    }

    public string Description

    {

        set{ this.description=value; }

        get{ return(this.description); }

    }

    #endregion

    #region constructors

    public SimpleAppointment(DateTime startTime,DateTime endTime,string description)

    {

        this.StartTime=startTime;

        this.EndTime=endTime;

        this.Description=description;

    }

    public SimpleAppointment Clone()

    {

       return((SimpleAppointment)this.MemberwiseClone());

    }

    public override bool Equals(Object obj)

    {

        //Check for null and compare run-time types.

       if((Object.ReferenceEquals(obj,null))||(GetType()!=obj.GetType()))

        {

           return(false);

        }

        else

        {

            SimpleAppointment task=(SimpleAppointment)obj;

            return(

                (task.StartTime.Equals(this.StartTime))&&

                (task.EndTime.Equals(this.EndTime))&&

                (task.Description.Equals(this.Description))

            );

        }

    }

    public override int GetHashCode(){

return(base.GetHashCode()); }

    public static bool operator ==(SimpleAppointment left,SimpleAppointment right)

    {

       if(Object.ReferenceEquals(left,right)){ return(true); }

        else if(Object.ReferenceEquals(left,null)){ return(false); }

        else{ return(left.Equals(right));

}

    }

    public static bool operator !=(SimpleAppointment left,SimpleAppointment right)

    {

        return(!(left==right));

    }

    public static bool operator <(SimpleAppointment left,SimpleAppointment right)

    {

       return(left.StartTime.CompareTo(right.StartTime)<0);

    }

    public static bool operator >(SimpleAppointment left,SimpleAppointment right)

    {

       return(left.StartTime.CompareTo(right.StartTime)>0);

    }

    public static bool operator <=(SimpleAppointment left,SimpleAppointment right)

    {

        return((left<right)||(left==right));

    }

    public static bool operator >=(SimpleAppointment left,SimpleAppointment right)

    {

        return((left>right)||(left==right));

    }

    #endregion

}

public class SimpleAppointmentComparer:IComparer

{

    public int Compare(object x,object y)

    {

        if((x is SimpleAppointment)&&(y is SimpleAppointment))

        {

           return(CompareSA((SimpleAppointment)x,(SimpleAppointment)y));

        }

        else

        {

           return(x.GetHashCode()-y.GetHashCode());

        }

    }

    private static int Compare(SimpleAppointment x,SimpleAppointment y)

    {

        return(CompareSA(x,y));

    }

    private static int CompareSA(SimpleAppointment x,SimpleAppointment y)

    {

        if(x<y){ return(-1); }

        else if(x>y){ return(1); }

        else{ return(0); }

    }

}

OutlookUtil.cs

Purpose

This class serves as a static utility class for working with Outlook. It would be good practice to go back and have this inherit some common "ICalendar" interface to better support the theoretical addition of other calendar programs. 

Members and properties

Neither of those should really be pluralized. There's only one member, it's a static reference to the Outlook application instance. There's only one property, it's a static reference to the last error encountered.

#region private static members

private static Application app;

private static Application App

{

    get

    {

        if(app==null)

        {

            app=new Application();

        }

        return(app);

    }

}

#endregion

#region publc static properties

private static System.Exception lastError;

public static System.Exception LastError

{

    get{ return(lastError); }

    set{ lastError=value; }

}

#endregion

GetCalendar method

This method finds the default calendar from the current Outlook session.

private static MAPIFolder GetCalendar()

{

    MAPIFolder calendar=null;

    NameSpace session=App.Session;

    try

    {

        calendar=session.GetDefaultFolder(OlDefaultFolders.olFolderCalendar);

    }

    catch(System.Exception x)

    {

        LastError=new System.Exception("Error getting the calendar folder",x);

    }

    return(calendar);

}

FilterItems method

The method takes a collection of Outlook items and filters them using the built-in Restrict method. The Restrict method takes a SQL-like parameter containing the filter statement. In this case, the items are restricted to ones that end during the current date, do not have a status of "free", and are public (obviously a user doesn't want their private appointments showing up).

private static Outlook.Items FilterItems(Outlook.Items items)

{

    System.Text.StringBuilder builder=new System.Text.StringBuilder();

    builder.Append("[End]>='");

    builder.Append(DateTime.Now.ToShortDateString());

    builder.Append(" 12:00 AM'");

    builder.Append(" and [BusyStatus]<>0"); //0=olFree

    builder.Append(" and [Sensitivity]=0"); //0=olNormal

    string filter=builder.ToString();

    try

    {

        Outlook.Items filteredItems=items.Restrict(filter);

        filteredItems.Sort("[Start]",false);

        return(filteredItems);

    }

    catch(System.Exception x)

    {

        LastError=new System.Exception("Error filtering items.",x);

        return(items);

    } 

}

GetAppointments method

This method grabs a reference to the default calendar and filters the items (see previous two methods). It then loops through all the items and converts them to SimpleAppointment objects.

public static ArrayList GetAppointments()

{

    ArrayList list=new ArrayList();

    MAPIFolder calendar=GetCalendar();

    if(calendar!=null)

    {

        Outlook.Items appointments=FilterItems(calendar.Items);

        DateTime now=DateTime.Now;

        object obj;

        //try getting the first item

        try 

        {

            obj=appointments.GetFirst();

        }

        catch(System.Exception x)

        {

            LastError=new System.Exception("Error getting first calendar item.",x);

            obj=null;

        }

        while(obj!=null)

        {

            if(obj is AppointmentItem)

            {

                AppointmentItem appointment=(AppointmentItem)obj;

                SimpleAppointment simpleAppointment=new SimpleAppointment(appointment.Start,appointment.End,appointment.Subject);

                list.Add(simpleAppointment);

            }

            //try getting the next item

            try

            {

                obj=appointments.GetNext();

            }

            catch(System.Exception x)

            {

                LastError=new System.Exception("Error getting next calendar item.",x);

                obj=null;

            }

        }

        //sort the list

        list.Sort(new SimpleAppointmentComparer());

    }

    return(list);

}

Saver Form

Purpose

This is the form that displays the actual screen saver. It loads the list of appointments and displays the current/future ones. On a timed basis it moves the text to a random location.

Members

startingPoint: The initial location of the mouse, used to track if it was moved.

appointments: The list of appointments. 

defaultMessage & fontName: Defaults to use if no user-defined settings are present.

apptColors & errorColors: Arrays of colors to use when displaying items. Yeah, there's certainly a better way to do this but I was going for quick.

SaverForm_Load method

The first thing this method does is resize the form to fill the entire screen. Then it loads saved settings from the registry. Next-up is loading the list of appointments, this call takes a few seconds. Finally, it sets the startingPoint member to (-1,-1) and kicks-off a timer tick.

private void SaverForm_Load(object sender, System.EventArgs e)

{

    Cursor.Current=Cursors.WaitCursor;

    //fill the entire screen

    this.Bounds=Screen.PrimaryScreen.Bounds;

    //load saved values

    try

    {

        RegistryKey key=Registry.CurrentUser.OpenSubKey("Software",false);

        RegistryKey subKey=key.OpenSubKey(Application.ProductName,false);

        if(subKey!=null)

        {

            this.displayErrors=((String)subKey.GetValue("showErrors","True")).Equals("True");

            this.fontName=(String)subKey.GetValue("fontName",string.Empty);

            this.defaultMessage=(String)subKey.GetValue("defaultMessage","I'll be right back.");

        }

    }

    catch(Exception x)

    {

        if(this.displayErrors)

        {

            this.errorTextbox.Text=x.Message+Environment.NewLine+x.StackTrace;

            this.errorTextbox.Visible=true;

            Random random=new Random();

            Color foreColor=this.errorColors[random.Next(0,this.errorColors.Length)];

            this.errorTextbox.ForeColor=foreColor;

            this.errorTextbox.Font=new Font(this.fontName,random.Next(4,6)*2);

        }

    }

    //load the appointments

    ArrayList appointmentList=OutlookUtil.GetAppointments();

    this.appointments=(SimpleAppointment[])appointmentList.ToArray(typeof(SimpleAppointment));

    //set startingPoint to (-1,-1), see OnMouseMove event

    this.startingPoint=new Point(-1,-1);

    //force a tick

    this.timer_Tick(null,null);

    Cursor.Current=Cursors.Default;

}

OnMouseMove & OnKeyPress methods

This method checks if the mouse has moved, if so the screen saver is exited. Ditto if a key is pressed. The (-1,-1) check is used to prevent the application from exiting immediately.

private void OnMouseMove(object sender,MouseEventArgs args)

{

    if((this.startingPoint.X==-1)&&(this.startingPoint.Y==-1))

    {

        this.startingPoint=new Point(args.X,args.Y);

    }

    else if((this.startingPoint.X!=args.X)||(this.startingPoint.Y!=args.Y))

    {

        this.Close();

    }

}

private void OnKeyPress(object sender,KeyPressEventArgs args)

{

    this.Close();

}

timer_Tick method

When the timer event fires its time to update the screen saver. The current & future appointments are loaded, random colors are chosen for the text, and the text is moved to a random location on the screen.

private void timer_Tick(object sender, System.EventArgs e)

{

    //stop in case this runs long

    this.timer.Stop();

    //clear the last error

    OutlookUtil.LastError=null;

    //random number generator used to pick colors

    Random random=new Random();

    #region show the current appointments

    SimpleAppointment[] currentAppointments=this.getCurrentAppointments();

    int length=currentAppointments.Length;

    StringBuilder builder=new StringBuilder();

    if(length<=0)

    {

        builder.Append(this.defaultMessage);

    }

    else

    {

        for(int index=0;index<length;index++)

        {

            builder.Append(currentAppointments[index].StartTime.ToString());

            builder.Append("-");

            builder.Append(currentAppointments[index].EndTime.ToString());

            builder.Append(" ");

            builder.Append(currentAppointments[index].Description);

            builder.Append(Environment.NewLine);

        }

    }

    this.currentAppointmentsTextbox.Text=builder.ToString();

    this.currentAppointmentsTextbox.SelectionLength=0;

    Color foreColor=this.apptColors[random.Next(0,this.apptColors.Length)];

    this.whereNowLabel.ForeColor=foreColor;

    this.currentAppointmentsTextbox.ForeColor=foreColor;

    this.whereNowLabel.Font=new Font(this.fontName,random.Next(8,10)*2);

    this.currentAppointmentsTextbox.Font=new Font(this.fontName,random.Next(4,6)*2);

    #endregion

    #region show the future appointments

    SimpleAppointment[] futureAppointments=this.getFutureAppointments();

    length=futureAppointments.Length;

    if(length>0)

    {

        builder=new StringBuilder();

        for(int index=0;index<length;index++)

        {

            builder.Append(futureAppointments[index].StartTime.ToString());

            builder.Append("-");

            builder.Append(futureAppointments[index].EndTime.ToString());

            builder.Append(" ");

            builder.Append(futureAppointments[index].Description);

            builder.Append(Environment.NewLine);

        }

        this.futureAppointmentsTextbox.Text=builder.ToString();

        this.futureAppointmentsTextbox.Visible=true;

        this.whereLaterLabel.Visible=true;

        foreColor=this.apptColors[random.Next(0,this.apptColors.Length)];

        this.whereLaterLabel.ForeColor=foreColor;

        this.whereLaterLabel.Font=new Font(this.fontName,random.Next(8,10)*2);

        this.futureAppointmentsTextbox.ForeColor=foreColor;

        this.futureAppointmentsTextbox.Font=new Font(this.fontName,random.Next(4,6)*2);

    }

    else

    {

        //hide the future appointments

        this.futureAppointmentsTextbox.Visible=false;

        this.whereNowLabel.Visible=false;

    }

    #endregion

    #region check for errors

    if((this.displayErrors==false)||(OutlookUtil.LastError==null))

    {

        this.errorTextbox.Visible=false;

    }

    else

    {

        builder=new StringBuilder();

        builder.Append(OutlookUtil.LastError.Message);

        builder.Append(Environment.NewLine);

        builder.Append(OutlookUtil.LastError.StackTrace);

        if(OutlookUtil.LastError.InnerException!=null)

        {

            builder.Append(Environment.NewLine);

            builder.Append("Inner Exception: ");

            builder.Append(OutlookUtil.LastError.InnerException.Message);

            builder.Append(Environment.NewLine);

            builder.Append(OutlookUtil.LastError.InnerException.StackTrace);

        }

        this.errorTextbox.Text=builder.ToString();

        this.errorTextbox.Visible=true;

        foreColor=this.errorColors[random.Next(0,this.errorColors.Length)];

        this.errorTextbox.ForeColor=foreColor;

        this.errorTextbox.Font=new Font(this.fontName,random.Next(4,6)*2);

    }

    #endregion

    #region now move everything to a random location

    int maxTop=this.Height-this.contentPanel.Height;

    int maxLeft=this.Width-this.contentPanel.Width;

    int top=random.Next(0,maxTop);

    int left=random.Next(0,maxLeft);

    this.contentPanel.Location=new Point(top,left);

    #endregion

    //restart

    this.timer.Start();

}

getCurrentAppointments & getFutureAppointments methods

These two methods are used to sort out the current & future appointments. These methods are roughly the same, there's probably some way to accomplish this in one method, again, going for quick on this.

private SimpleAppointment[] getCurrentAppointments()

{

    ArrayList list=new ArrayList();

    DateTime now=DateTime.Now;

    int length=this.appointments.Length;

    int index=0; 

    int apptCount=0;

    while((index<length)&&(apptCount<MAX_APPT_COUNT)) //this assumes that appointments is sorted by start date

    {

        int compareStart=now.CompareTo(this.appointments[index].StartTime);

        int compareEnd=now.CompareTo(this.appointments[index].EndTime);

        if(compareStart>=0)

        {

            if(compareEnd<0)

            {

                list.Add(this.appointments[index]);

                apptCount++;

            }

            else

            {

                index=length;

            }

        }

        index++;

    }

    return((SimpleAppointment[])list.ToArray(typeof(SimpleAppointment)));

}

private SimpleAppointment[] getFutureAppointments()

{

    ArrayList list=new ArrayList();

    DateTime now=DateTime.Now;

    int length=this.appointments.Length;

    int index=0; 

    int apptCount=0;

    while((index<length)&&(apptCount<MAX_APPT_COUNT)) //this assumes that appointments is sorted by start date

    {

        int compareStart=now.CompareTo(this.appointments[index].StartTime);

        if(compareStart<0)

        {

            list.Add(this.appointments[index]);

            apptCount++;

        }

        index++;

    }

    return((SimpleAppointment[])list.ToArray(typeof(SimpleAppointment)));

}

The Final Product

The final product looks like this:

After a few tests it seems to working as expected. The initial startup is kinda slow because of the time needed to load up the calendar items. After writing this article and reflecting on it there are a few other lingering issues..

Issues

The reason I didn't post this in the software section of this site was because it's an incomplete project. It does what I needed it to do so I stopped working on it. However, the basic framework would probably be useful to someone else so I released it as a "How To" tutorial instead. Here are a few of the things that need to be done to make this a "complete" application:

Reoccurring appointments don't display

This is a very painful issue to fix. A reoccurring appointment has the start/end date of the first instance. This causes it to appear as a past event that isn't displayed on the screen saver.

Linking to unmanaged code

This screen saver links to the Outlook 9 libraries. COM/COM+ interop always seems to have problems so it would be nice to avoid this binding. There's not much I can do to fix this though.

Registry

I'm not a big fan of storing things in the registry. It would be preferable to use an xml configuration file instead.

If Outlook isn't running this probably doesn't work

Yeah, it's easy enough to kick-off an Outlook session if it's not. I pretty much always have Outlook running so this didn't bother me too much.

Only tested against Outlook 2000

I have a feeling that this won't work with other versions of Outlook because it's bound to the Outlook 9 libraries. Well, it will probably work once the references are updated and the code recompiled.

Portability

It would be nice to if this could support different calendar programs.


Tweet