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

Display the Current Temperature in the System Tray

0.00/5 (No votes)
11 Nov 2004 1  
Use a web service to retrieve the temperature for a given zipcode and display it in the taskbar system tray

screenshot

For a given zip code, display the current temperature in the taskbar system tray:

Introduction

This article will take you through the major steps necessary to build the sample application and touches on a variety of topics, including how to:

  • Consume a web service
  • Draw a system tray icon at runtime
  • Hide the application when minimized
  • Access the native Win32 API (DllImport)
  • Use a timer
  • Write and read user settings to/from an XML file
  • Obtain the user's application data directory

Accessing the Web Service

To access weather data for a given zip code, I use a web service from EJSE Inc., which I found on http://www.xmethods.com/. After creating your C# Forms application, you will need to add a proxy class which accesses the web service. You can do this in a couple of ways. The easiest way is to right click on your project in the Solution Explorer and select "Add Web Reference..." and type in the URL of the .asmx file or the WSDL file (Web Service Description Language) for the service. In this case the URL is: http://www.ejseinc.com/WeatherService/Service.asmx. This will add a namespace to your project containing a proxy class, which exposes the methods of the web service, and several data structures that hold the returned data.

The other way to generate the proxy class is from the command line (this is the route I chose for this project):

wsdl /l:CS /protocol:SOAP http://www.ejse.com/WeatherService/Service.asmx

This tells the wsdl tool to generate a C# class that accesses the web service using SOAP (Simple Object Access Protocol) (as opposed to HTTP GET or HTTP POST protocols). You can then add the generated .cs file to your project. The wsdl tool generates the file and class names based on the service name specified in the WSDL file. Since this service was given a rather generic name, "Service", I manually changed the generated file name and class name to "WeatherService".

Tip: To run the wsdl command, don't start a regular command prompt since that most likely won't have wsdl.exe on your path. Instead, start the Visual Studio .NET command prompt found in the Visual Studio .Net tools folder off the start menu. This will set all the environment variables necessary to run command-line tools.

Once the WeatherService class has been included in the project, create an instance in the Form's constructor and assign it to a member variable:

public class TemperatureForm : System.Windows.Forms.Form
{
    private WeatherService m_weatherService;
    ...
    public TemperatureForm()
    {
        ...
        m_weatherService = new WeatherService();
        ...
        

To get the weather data, all that you need to do is invoke the desired method on the m_weatherService object. It's important to put the call to the web service in a try/catch block since any number of things could go wrong. For example, the web service may be temporarily unavailable. Note in the code snippet below, the data is returned in a WeatherInfo instance. This is one of the data types that was automatically defined when we generated code from the wsdl file.

// Get the temperature for the current zipcode using a web service

private void UpdateTemperature()
{
    try
    {
        if (zipcodeBox.Text.Length > 0)
        {
            int zipcode = Int32.Parse(zipcodeBox.Text);
            WeatherInfo info = m_weatherService.GetWeatherInfo(zipcode);
            ...
            (show the relavant weather info data on the 
              form and call UpdateTaskBar)
            ...
        }
    }
    catch (Exception e)
    {
        // don't show message when we're minimized

        if (this.WindowState == FormWindowState.Normal)
        {
            String messageStr = String.Format(
                     "Couldn't retrieve weather data for zipcode {0}", 
                     zipcodeBox.Text);
            MessageBox.Show(this, messageStr, "Temperature", 
                            MessageBoxButtons.OK, 
                            MessageBoxIcon.Warning);
        }
        Console.WriteLine(e.ToString());
    }
}

Displaying the Temperature in a System Tray Icon

Now that we have the WeatherInfo data, we can display the temperature in the taskbar system tray. Adding a system tray icon for your application is trivial: drag a NotifyIcon from the Toolbox onto your form. To change the icon at runtime so that it shows the current temperature, create a Graphics instance by using Graphics.FromImage(). This allows you to draw the temperature string to any subclass of Image (e.g. a bitmap or metafile). Since Icon is not a subclass of Image, we must first draw to a bitmap and then convert that to an Icon, which can then be used for the system tray icon.

// Draw the current temperature to a bitmap to show 

// in the taskbar system tray

private void UpdateTaskBar()
{
    if (temperatureLabel.Text.Length == 0)
        return;

    // Create a graphics instance that draws to a bitmap

    Bitmap bitmap = new Bitmap(16, 16);
    SolidBrush brush = new SolidBrush(fontDialog1.Color);
    Graphics graphics = Graphics.FromImage(bitmap);

    // Draw the temperature and the degrees symbol

    // to the bitmap using the user selected font

    int index = temperatureLabel.Text.IndexOf('�');  
           // don't want 'F' after the degree symbol

    String temperatureOnlyStr = temperatureLabel.Text.Substring(0, index+1);
    graphics.DrawString(temperatureOnlyStr, fontDialog1.Font, brush, 0, 0);

    // Convert the bitmap into an icon and use it for the system tray icon

    IntPtr hIcon = bitmap.GetHicon();
    Icon icon = Icon.FromHandle(hIcon);
    notifyIcon1.Icon = icon;
    
    // unfortunately, GetHicon creates an 

    // unmanaged handle which must be manually destroyed

    DestroyIcon(hIcon);
}

As noted in the comment in the code above, the icon handle that is used to convert from a bitmap to an icon must be manually destroyed (according to several newsgroup messages I have read). If you don't, the handle will leak every time the taskbar icon is updated. This is easily verified using the Windows Task Manager and inspecting the GDI Objects column. The only way to destroy the handle is by using the DestroyIcon function from the Win32 API. Gaining access to the API is fairly straightforward (this technique is also know as platform invoke, or P/Invoke):

[DllImport("user32.dll", EntryPoint="DestroyIcon")]
static extern bool DestroyIcon(IntPtr hIcon);

Hiding When Minimized

The user has the option of hiding the application when it gets minimized so that it won't appear on the taskbar (although it will still appear in the system tray). To do this, override the OnResize method, which will get invoked when the user minimizes the form:

protected override void OnResize(EventArgs e)
{
    if (hideCB.Checked && this.WindowState == FormWindowState.Minimized)
        this.Hide();
    base.OnResize(e);
}

To redisplay the form, add a double click handler for the notify icon:

private void notifyIcon1_MouseDoubleClick(object sender, System.EventArgs e)
{
    this.Show();
    this.WindowState = FormWindowState.Normal;
}

Tip: The forms designer makes it easy to add "OnClicked" handlers to your code by double clicking on any control, but in this case we want a double click handler. To do this, select the notifyIcon at the bottom of the form designer screen, then select the lightening bolt icon at the top of the property browser. This will show you a list of events for the selected control. To create a new handler, double click on the desired event.

Setting up the Timer

In order to get awakened at a specified interval of time to update the temperature, we use the System.Windows.Forms.Timer class. Again, to set this up is trivial. First, drag the Timer from the Toolbox onto your form. Then, double click on the timer1 icon at the bottom of the form design page. This creates an event handler method for the timer's "Tick" event, which gets fired when the timer expires. In this case, all our handler needs to do is call the UpdateTemperature() method that we saw above.

We also have to set the timer interval and its start/stop state, which are based on values from the form's controls:

private void UpdateTimer()
{
    if (minutesBox.Text.Length > 0)
    {
        int minutes = Int32.Parse(minutesBox.Text);
        timer1.Interval = minutes * 60 * 1000; // milliseconds


        if (autoUpdateCB.Checked && zipcodeBox.Text.Length > 0)
            timer1.Start();
        else
            timer1.Stop();
    }
}

Saving and Reading the Settings

To save the user's settings such as zipcode and font, I experimented with XML and the BinaryFormatter. The code currently uses XMLTextWriter and XMLTextReader to save and load the settings to/from an XML file, however I would not recommend this approach in its current form. I include it because it served as a good exercise to learn how these classes work, and hopefully is useful to you in that regard as well. The Load function in particular is messy since it includes the logic to walk through the XML file. I think a better approach would be to come up with something that is more reusable and hides the XML parsing, for example see XML Serlialization - a Settings Class. See also Using Dynamic Properties and the AppSettingsReader for yet another approach. The BinaryFormatter approach worked well (code is included in the project but commented out in a separate #region), but of course the resulting file is not human readable.

// save values to the defaults file

private void SaveSettings()
{
    XmlTextWriter writer = new XmlTextWriter(
          m_settingsPath + "\\settings.xml", null);
    writer.Formatting = Formatting.Indented;
    writer.Indentation = 4;
    writer.WriteComment("Settings for Temperature program");     
    writer.WriteStartElement("settings");

    writer.WriteElementString("zipcode", zipcodeBox.Text);
    
    writer.WriteStartElement("font");
    writer.WriteElementString("name", fontDialog1.Font.Name);
    writer.WriteElementString("size", 
          fontDialog1.Font.SizeInPoints.ToString());
    writer.WriteElementString("style", fontDialog1.Font.Style.ToString());
    writer.WriteElementString("color", fontDialog1.Color.Name);
    writer.WriteEndElement();

    writer.WriteElementString("minutes", minutesBox.Text);
    writer.WriteElementString("autoupdate", autoUpdateCB.Checked.ToString());
    writer.WriteElementString("hide", hideCB.Checked.ToString());

    writer.WriteEndElement();
    writer.Flush();
    writer.Close();
}

// init values from the the defaults file

private void LoadSettings()
{
    XmlTextReader reader = null;
    try
    {
        reader = new XmlTextReader(m_settingsPath + "\\settings.xml");
        reader.WhitespaceHandling = WhitespaceHandling.None;
        
        string elementName = null;
        string fontName = null;
        float fontSize = 0;
        FontStyle fontStyle = FontStyle.Regular;
        while (reader.Read())
        {
 //Console.WriteLine("type = {0}, name = {1}, value = {2}", reader.NodeType, 

 //                  reader.Name, reader.Value);

            if (reader.NodeType == XmlNodeType.Element)
                elementName = reader.Name;
            else if (reader.NodeType == 
                XmlNodeType.EndElement && reader.Name == "font")
                fontDialog1.Font = new Font(fontName, fontSize, fontStyle);
            else if (reader.NodeType == XmlNodeType.Text)
            {
                switch (elementName)
                {
                    case "zipcode": zipcodeBox.Text = reader.Value;  break;
                    case "name": fontName = reader.Value;  break;
                    case "size": 
                   fontSize = float.Parse(reader.Value);  break;
                    case "style": fontStyle = 
                         (FontStyle)Enum.Parse(
                               fontDialog1.Font.Style.GetType(), 
                               reader.Value);  break;
                    case "color": fontDialog1.Color = 
             Color.FromName(reader.Value);  break;
                    case "minutes": minutesBox.Text = reader.Value;  break;
                    case "autoupdate": autoUpdateCB.Checked = 
             bool.Parse(reader.Value);break;
                    case "hide": hideCB.Checked = 
             bool.Parse(reader.Value);  break;
                }
            }
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e.ToString());
    }

    if (reader != null)
        reader.Close();
}

The settings are saved to a file in the user's application data directory:

private string BuildSettingsPath()
{
    string pathName;
    try
    {
        pathName = Environment.GetFolderPath(
System.Environment.SpecialFolder.ApplicationData);
        pathName += "\\Serlio\\Temperature";
        if (!Directory.Exists(pathName))
            Directory.CreateDirectory(pathName);
    }
    catch (Exception e)
    {
        Console.WriteLine(e.ToString());
        pathName = "";
    }

    return pathName;
}

Starting the Application Minimized

I have a shortcut in my startup folder so the temperature application starts every time I start my computer. Tip: on the properties page for the shortcut, set "Run" to Minimized. This starts the application in the minimized state.

Conclusion

Writing this application served as a good introduction to VS.NET for me: I was able to explore and learn about several different topics and it was fun to write.

History

  • 10 Nov 2002 - updated downloads
  • 12 Nov 2004 - updated downloads

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