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:
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.
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)
{
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.
private void UpdateTaskBar()
{
if (temperatureLabel.Text.Length == 0)
return;
Bitmap bitmap = new Bitmap(16, 16);
SolidBrush brush = new SolidBrush(fontDialog1.Color);
Graphics graphics = Graphics.FromImage(bitmap);
int index = temperatureLabel.Text.IndexOf('�');
String temperatureOnlyStr = temperatureLabel.Text.Substring(0, index+1);
graphics.DrawString(temperatureOnlyStr, fontDialog1.Font, brush, 0, 0);
IntPtr hIcon = bitmap.GetHicon();
Icon icon = Icon.FromHandle(hIcon);
notifyIcon1.Icon = icon;
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;
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.
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();
}
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())
{
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