Table of Contents
Introduction
Microsoft has introduced some amazing new features in Windows 7. I have used these features to create this small app: taskbar, jump-list, task-dialog, and Aero Glass.
The application is just an egg-timer: you tell the time after which the timer must elapse, and it shows you the time left. After the time has elapsed, you are notified:
- by sound;
- by window flash.
In this article, you will see how Windows 7 features can be used for a timer application.
Background
I always thought, why is there no timer in standard Windows applications? I've created this one to make my life and work easier with Windows 7.
The Code
The Environment
First of all, about the environment. To develop this application, I've used:
I've made a small patch for the Windows API Code Pack: please see the Shell/Ext folder with the files I've added. The patch allows you to create a glass-window that updates its taskbar screenshot on-demand. The details of the patch are described in the following section.
Taskbar
TaskbarTimer's main form is just an Aero Glass window with the time displayed on it (I've described it later in this article). Actually, the application doesn't need it at all, all functionality is used from the taskbar.
Here's a taskbar thumbnail and a preview for the timer:
Every second, the application updates the thumbnail and the preview. It also sets the taskbar progress:
private void Timer_Tick(object sender, EventArgs e) {
_timeLeft = _timeLeft.Subtract(new TimeSpan(0, 0, 0, 1));
Invalidate();
InvalidateThumbnails();
int percent = _timeLeft.TotalMilliseconds > 0 ?
(100 - (int)(_timeLeft.TotalMilliseconds / _totalMilliseconds * 100d)) : 100;
if (percent < 0 || percent > 100) {
percent = 0;
}
TaskbarManager.Instance.SetProgressValue(percent, 100);
Invalidate
is a standard method of Control
. In our case, we need to call it because the form may be open. In this case, Windows will re-paint the form and update the time on it.
InvalidateThumbnails
is a method of the GlassFormWithCustomThumbnails
class, which I have created as a patch. It's pretty simple:
protected void InvalidateThumbnails() {
TabbedThumbnailNativeMethods.DwmInvalidateIconicBitmaps(Handle);
}
The only reason I have put it in that class is that the TabbedThumbnailNativeMethods
class is declared as protected
, so we can use it only from the Shell assembly.
After this method, Windows checks whether the custom previews are enabled for the window. We enable custom previews in the OnLoad
method of the GlassFormWithCustomThumbnails
class:
protected override void OnLoad(EventArgs e) {
if (!DesignMode) {
TabbedThumbnailNativeMethods.EnableCustomWindowPreview(Handle, true);
}
base.OnLoad(e);
}
So, the previews are enabled in our application. This means that Windows behaves in such a way after calling InvalidateThumbnails
:
As you can see, if the taskbar thumbnail is closed, Windows doesn't do anything. If it's open, Windows sends the events to the window. That's why we have to implement the WndProc
method:
protected override void WndProc(ref System.Windows.Forms.Message m) {
if (m.Msg == (int)TaskbarNativeMethods.WM_DWMSENDICONICTHUMBNAIL) {
int width = (int)((long)m.LParam >> 16);
int height = (int)(((long)m.LParam) & (0xFFFF));
Size requestedSize = new Size(width, height);
using (Bitmap iconicThumb = GetIconicThumbnail(requestedSize)) {
TabbedThumbnailNativeMethods.SetIconicThumbnail(Handle,
iconicThumb.GetHbitmap());
}
} else if (m.Msg == (int)TaskbarNativeMethods.WM_DWMSENDICONICLIVEPREVIEWBITMAP) {
using (Bitmap peekBitmap = GetPeekBitmap()) {
TabbedThumbnailNativeMethods.SetPeekBitmap(Handle,
peekBitmap.GetHbitmap(), false);
}
}
base.WndProc(ref m);
}
This is a method of our GlassFormWithCustomThumbnails
class. As you can see, the derived class must handle the following events:
protected event GetPeekBitmapDelegate GetPeekBitmap;
protected event GetIconicThumbnailDelegate GetIconicThumbnail;
We implement them in the FrmMain
class:
public FrmMain(int minutes, int elapsedMinutes, TimerOptions options) {
GetPeekBitmap += OnGetPeekBitmap;
GetIconicThumbnail += OnGetIconicThumbnail;
}
We don't need to dispose bitmaps returned from these items; they are disposed automatically in the GlassFormWithCustomThumbnails
class.
Perhaps you will ask: Why haven't you done everything using the Windows API Code Pack???
First, we'll do a small investigation:
Let's find all the references to the TabbedThumbnailNativeMethods.DwmInvalidateIconicBitmaps
method using Visual Studio: right-click the method, and click the Find all references menu item. We'll see that it's used only in one place (TaskbarWindowManager.cs: 632):
internal void InvalidatePreview(TaskbarWindow taskbarWindow)
{
if (taskbarWindow != null)
TabbedThumbnailNativeMethods.DwmInvalidateIconicBitmaps(
taskbarWindow.WindowToTellTaskbarAbout);
}
It's a method of the TaskbarWindowManager
class. If we find all references to this method, we'll see that in order to use it, we must create an instance of the TabbedThumbnail
class.
Unfortunately, we cannot get the preview for the default window. Okay, here's the code we can write:
- Create a new project and reference the Windows API Code Pack libraries: you need Core and Shell. Also reference the PresentationCore and WindowsBase libraries.
- Write this code in the form
Load
event:
private void Form1_Load(object sender, EventArgs e) {
TabbedThumbnail thumb = new TabbedThumbnail(Handle, Handle);
thumb.DisplayFrameAroundBitmap = false;
TaskbarManager.Instance.TabbedThumbnail.AddThumbnailPreview(thumb);
thumb.SetImage(Bitmap.FromFile(@"d:\temp\vs.png") as Bitmap);
thumb.DisplayFrameAroundBitmap = false;
}
I've used a Visual Studio screenshot as a sample image. Here's the result:
Windows displays a frame, although we have specified DisplayFrameAroundBitmap = false
! And, there's no way to get rid of it.
I would be really glad if Microsoft implemented this:
TabbedThumbnail thumb =
TaskbarManager.Instance.TabbedThumbnail.GetThumbnailPreview(this.Handle);
Unfortunately, we get null
in this method. That's why I've implemented the patch for the Windows API Code Pack.
Now, let's add two small pause and about buttons near the taskbar thumbnail:
private readonly ThumbnailToolbarButton _buttonPause =
new ThumbnailToolbarButton(Resources.Pause, Resources.PauseTimerTooltip);
private readonly ThumbnailToolbarButton _buttonAbout =
new ThumbnailToolbarButton(Resources.About,
Resources.AboutTooltip) { DismissOnClick = true };
And later, after we have initialized it:
_buttonPause.Click += PauseTimer_Clicked;
_buttonAbout.Click += About_Clicked;
ThumbnailToolbarButton[] buttons = new[] {_buttonPause, _buttonAbout};
TaskbarManager.Instance.ThumbnailToolbars.AddButtons(Handle, buttons.ToArray());
Later, we can disable this button (in our case, when the time has elapsed, it's no use to click the button):
_buttonPause.Enabled = false;
After this call, the button doesn't become blue when you put your mouse over it. And of course, the handler is not called.
We also change the image and the tooltip of the button (when the user clicks it, the play image becomes the pause image, and vice versa):
private void PauseTimer_Clicked(object sender, ThumbnailButtonClickedEventArgs e) {
e.ThumbnailButton.Icon = Resources.Play;
e.ThumbnailButton.Tooltip = Resources.StartTimerTooltip;
}
As you can see, we get the reference to our button in ThumbnailButtonClickedEventArgs
.
Please pay your attention to the DismissOnClick
property (we set it for the About button). This property determines what happens to the taskbar thumbnail when the button is clicked:
- If
DismissOnClick
is set to true
, the taskbar thumbnail will disappear. - If
DismissOnClick
is set to false
, nothing will happen. This is the default behavoir of taskbar buttons.
In our case, the taskbar thumbnail will disappear when the user clicks the About button.
Aero Glass Window
To create the Aero Glass window, I've derived my form from GlassFormWithCustomThumbnails
. This is my class, it's derived from GlassWindow
.
All we need is to just override the OnPaint
method to draw the timer state:
protected override void OnPaint(PaintEventArgs e) {
base.OnPaint(e);
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.TextRenderingHint = TextRenderingHint.AntiAlias;
e.Graphics.DrawString(GetTimeLeftText(), _textFont,
Brushes.Black, ClientRectangle, _stringFormat);
}
As I've mentioned before, the main window of the application is very simple because it's almost never used. But with Aero Glass, it looks fine (the Windows logo is my wallpaper, it's not on the window):
Task-dialog
When you start the timer, it shows you the following window:
Such windows are called task-dialogs. Let's examine its parts in detail:
Everything can be set in the TaskDialog
class from the Windows API Code Pack:
public TimerTaskDialog() {
_dialog = new TaskDialog();
_dialog.Caption = "Taskbar Timer";
_dialog.Cancelable = true;
_dialog.InstructionText = "Please select a time interval for taskbar timer";
_dialog.FooterText = "TaskbarTimer 1.0 Copyright (C) Dmitry Vitkovsky 2009";
_dialog.HyperlinksEnabled = true;
_dialog.StandardButtons = TaskDialogStandardButtons.Close;
TaskDialogCommandLink btn5Munutes = new TaskDialogCommandLink("btn5min",
"5 minutes",
"Set timer to 5 minutes and start the timer");
btn5Munutes.Default = true;
btn5Munutes.Click += (sender, e) => SetInterval(5);
_dialog.Controls.Add(btn5Munutes);
}
Here's a function that handles the button click event:
void SetInterval(int minutes) {
_minutes = minutes;
_dialog.Close(TaskDialogResult.CustomButtonClicked);
}
Please note, to use the task-dialog in your application, you must add the app.manifest file. You can find a sample in the Windows API Code Pack samples, or in my code for this article. If the manifest is absent, you will get an exception: TaskDialog feature needs to load version 6 of comctl32.dll, but a different version is currently loaded in memory.
Jump-list
Jump-list is another amazing feature of Windows 7. I've used it in this application: you can select one of a pre-defined time intervals from the list.
Also, there's a link for the last selection.
All the items are added to the Tasks section. Here's the code:
public void ConfigureJumpList() {
JumpList.AddUserTasks(new[] { GetMinutesLink(5, null, Icon),
GetMinutesLink(10, null, Icon), GetMinutesLink(30, null, Icon) });
}
private static JumpListLink GetMinutesLink(int minutes,
string timerName, IconReference icon) {
string title = string.Format("New {0} minute{1} timer",
minutes, IsPlural(minutes) ? "s" : "");
string arguments = " -minutes=" + minutes;
if (!string.IsNullOrEmpty(timerName) &&
timerName != TimerOptions.Default.TimerName) {
arguments += string.Format(" -name=\"{0}\"", timerName);
title += string.Format(" ({0})", timerName);
}
return new JumpListLink(Application.ExecutablePath, title)
{ IconReference = icon, Arguments = arguments};
}
Please pay attention: we cannot directly add an icon to the task list - we must pass the IconReference
object. There are two options for loading icons:
- From an .ico file
- From a resource in an exe file
Here's the code to obtain the IconReference
:
private static IconReference Icon {
get {
if (_icon == null) {
string pathToIcon = Path.Combine(
Path.GetDirectoryName(Application.ExecutablePath),
"Timer.ico");
int iconNum = 0;
if (!File.Exists(pathToIcon)) {
pathToIcon = Path.Combine(
KnownFolders.System.Path, "shell32.dll");
iconNum = 20;
}
_icon = new IconReference(pathToIcon, iconNum);
}
return _icon.Value;
}
}
This code first tries to find the .ico file and adds the IconReference
with it. If there's no .ico file on the disk, it will add the task items with the icon in the shell32.dll file.
Please pay your attention on how we get the path to the shell32.dll file: we use the KnownFolders
class introduced in the Windows API Code Pack for Windows 7.
Also, we add a custom task every time the timer is started:
if (_totalMinutes != 5 && _totalMinutes != 10 && _totalMinutes != 30) {
jumpListHelper.AddCustomLink(_totalMinutes, _options.TimerName);
}
After we have added all the items to the jump-list, we have to save it:
public void Save() {
JumpList.Refresh();
}
Here's a jump-list with the last timer settings which appear when you right-click the timer icon in the taskbar:
All that the items in the jump-list do is start the timer app with parameters:
<path_to_the_app> -minutes=<number_of_minutes> -name="<name_of_the_timer>"
Native Methods
Unfortunately, not everything is already implemented in the .NET Framework. I have imported these routines (if you are not familiar with these functions, you can see their description by clicking the following links):
You will find them in the NativeMethods
class:
internal static class NativeMethods {
[DllImport("user32.dll", CallingConvention = CallingConvention.Winapi)]
public static extern bool FlashWindow(IntPtr hwnd, bool bInvert);
[DllImport("winmm.dll", SetLastError = true,
CallingConvention = CallingConvention.Winapi)]
public static extern bool PlaySound(string pszSound,
IntPtr hMod, PlaySoundFlags sf);
}
The FlashWindow
function is used to notify the user about the timeout by highlighting the window icon in orange color:
If you open a taskbar thumbnail with a mouse, the windows will look like as shown in the following picture:
When the timer has elapsed, we simply use these methods and remove the taskbar progress:
if (_timeLeft.TotalMilliseconds <= 0) {
TaskbarManager.Instance.SetProgressState(TaskbarProgressBarState.NoProgress);
timer.Stop();
_buttonPause.Enabled = false;
NativeMethods.FlashWindow(Handle, true);
if (!_options.DisableSound) {
NativeMethods.PlaySound(Path.Combine(KnownFolders.Windows.Path,
"Media\\Windows Default.wav"), IntPtr.Zero,
PlaySoundFlags.SND_FILENAME | PlaySoundFlags.SND_ASYNC);
}
}
As you can see, the sound is played asynchronously (using the flag SND_ASYNC
), so the function returns immediately.
Points of Interest
In this article, we have learned:
- How to create updateable thumbnails without a frame;
- Which events must be handled if we want to create a window that uses owner-draw taskbar thumbnails;
- How to add a button to a taskbar;
- Parts of the task-dialog;
- How to create a jump-list.
Thank you for reading :)
Revision History
- Nov. 2009 - Initial revision.
- Jan. 2010 - Added a more detailed description of the taskbar buttons.