65.9K
CodeProject 正在变化。 阅读更多。
Home

使用全新的 Windows 7 功能创建计时器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (88投票s)

2009年11月30日

CPOL

7分钟阅读

viewsIcon

160354

downloadIcon

6044

本文介绍如何使用 Windows 7 的新功能创建一个简单的煮蛋计时器。

TaskbarTimer screenshot

目录

引言

Microsoft 在 Windows 7 中引入了一些令人惊叹的新功能。我已使用这些功能创建了这个小型应用程序:任务栏、跳转列表、任务对话框和 Aero Glass。

该应用程序只是一个煮蛋计时器:您告诉它需要经过多长时间,它会显示剩余时间。时间到后,您会收到通知

  • 通过声音;
  • 通过窗口闪烁。

在本文中,您将了解如何在计时器应用程序中使用 Windows 7 功能。

背景

我一直想,为什么标准 Windows 应用程序中没有计时器?我创建了这个应用程序,以便让我在 Windows 7 下的生活和工作更加轻松。

代码

环境

首先,关于环境。开发此应用程序,我使用了

我对 Windows API Code Pack 做了一个小的补丁:请查看Shell/Ext文件夹,其中包含我添加的文件。该补丁允许您创建一个可以在需要时更新其任务栏截图的玻璃窗口。补丁的详细信息将在下一节中介绍。

任务栏

TaskbarTimer 的主窗体只是一个显示时间的 Aero Glass 窗口(我在本文后面进行了描述)。实际上,该应用程序根本不需要它,所有功能都来自任务栏。

这是计时器的任务栏缩略图和预览

Timer preview

每秒,应用程序都会更新缩略图和预览。它还设置任务栏进度

/// Handles timer tick event. Invalidates form image and thumbnails 
private void Timer_Tick(object sender, EventArgs e) {
    _timeLeft = _timeLeft.Subtract(new TimeSpan(0, 0, 0, 1));
 
    // Invalidate the image 
    Invalidate();
    // Tell the taskbar to invalidate the thumbnail and the preview 
    InvalidateThumbnails();
 
    // Calculate the percent of completion 
    int percent = _timeLeft.TotalMilliseconds > 0 ? 
      (100 - (int)(_timeLeft.TotalMilliseconds / _totalMilliseconds * 100d)) : 100;
    if (percent < 0 || percent > 100) {
        percent = 0;
    }
    TaskbarManager.Instance.SetProgressValue(percent, 100);
    // ... the rest of code is below

InvalidateControl 的标准方法。在我们的情况下,我们需要调用它,因为窗体可能已打开。在这种情况下,Windows 将重新绘制窗体并更新其上的时间。

InvalidateThumbnailsGlassFormWithCustomThumbnails 类的方法,该类是我作为补丁创建的。它非常简单

/// Invalidates the thumbnails 
protected void InvalidateThumbnails() {
    TabbedThumbnailNativeMethods.DwmInvalidateIconicBitmaps(Handle);
}

我将其放入该类中的唯一原因是 TabbedThumbnailNativeMethods 类被声明为 protected,因此我们只能从 Shell 程序集使用它。

在此方法之后,Windows 会检查该窗口是否启用了自定义预览。我们在 GlassFormWithCustomThumbnails 类的 OnLoad 方法中启用自定义预览。

/// Enables custom thumbnails for the form 
protected override void OnLoad(EventArgs e) {
    if (!DesignMode) {
        TabbedThumbnailNativeMethods.EnableCustomWindowPreview(Handle, true);
    }
    base.OnLoad(e);
}

因此,在我们的应用程序中启用了预览。这意味着在调用 InvalidateThumbnails 后,Windows 会执行以下操作。

Invalidating taskbar thumbnails

如您所见,如果任务栏缩略图关闭,Windows 不会执行任何操作。如果已打开,Windows 会将事件发送到窗口。这就是为什么我们必须实现 WndProc 方法。

/// Handles taskbar-related messages 
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);
}

这是我们 GlassFormWithCustomThumbnails 类的 WndProc 方法。如您所见,派生类必须处理以下事件。

/// Event to set peek bitmap 
protected event GetPeekBitmapDelegate GetPeekBitmap;
 
/// Event to set the iconic thumbnail 
protected event GetIconicThumbnailDelegate GetIconicThumbnail;

我们在 FrmMain 类中实现它们。

/// Constuctor 
public FrmMain(int minutes, int elapsedMinutes, TimerOptions options) {
    // ... (constructor logic) 
    GetPeekBitmap += OnGetPeekBitmap;
    GetIconicThumbnail += OnGetIconicThumbnail;
}

我们不需要处理这些项返回的位图;它们会在 GlassFormWithCustomThumbnails 类中自动处理。

也许您会问:“为什么您没有全部使用 Windows API Code Pack 来完成?

首先,我们进行一个小调查。

让我们使用 Visual Studio 查找 TabbedThumbnailNativeMethods.DwmInvalidateIconicBitmaps 方法的所有引用:右键单击该方法,然后单击“查找所有引用”菜单项。我们将看到它仅在一个地方使用(TaskbarWindowManager.cs: 632)。

internal void InvalidatePreview(TaskbarWindow taskbarWindow)
{
    if (taskbarWindow != null)
        TabbedThumbnailNativeMethods.DwmInvalidateIconicBitmaps(
                         taskbarWindow.WindowToTellTaskbarAbout);
}

它是 TaskbarWindowManager 类的方法。如果我们查找该方法的所有引用,我们将看到为了使用它,我们必须创建一个 TabbedThumbnail 类的实例。

不幸的是,我们无法获取默认窗口的预览。好的,这是我们可以编写的代码。

  1. 创建一个新项目并引用 Windows API Code Pack 库:您需要CoreShell。还要引用PresentationCoreWindowsBase库。
  2. 在窗体的 Load 事件中编写此代码。
  3. 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;
    }

我使用了 Visual Studio 截图作为示例图像。这是结果。

Unwanted frame

Windows 显示了一个框架,尽管我们已经指定了 DisplayFrameAroundBitmap = false!而且,没有办法摆脱它。

如果 Microsoft 实现这一点,我将非常高兴。

// this.Handle here is a form for which we want to get the default thumbnail: 
TabbedThumbnail thumb = 
  TaskbarManager.Instance.TabbedThumbnail.GetThumbnailPreview(this.Handle);

不幸的是,此方法返回 null。这就是为什么我为 Windows API Code Pack 实现补丁的原因。

任务栏按钮

现在,让我们在任务栏缩略图附近添加两个小的暂停关于按钮。

/// A button in the taskbar to stop the timer 
private readonly ThumbnailToolbarButton _buttonPause = 
   new ThumbnailToolbarButton(Resources.Pause, Resources.PauseTimerTooltip);

/// A button in the taskbar show About dialog 
private readonly ThumbnailToolbarButton _buttonAbout = 
   new ThumbnailToolbarButton(Resources.About, 
       Resources.AboutTooltip) { DismissOnClick = true };

稍后,在初始化它之后。

_buttonPause.Click += PauseTimer_Clicked;
_buttonAbout.Click += About_Clicked;
ThumbnailToolbarButton[] buttons = new[] {_buttonPause, _buttonAbout};
TaskbarManager.Instance.ThumbnailToolbars.AddButtons(Handle, buttons.ToArray());

稍后,我们可以禁用此按钮(在我们的情况下,当时间到期时,点击该按钮没有任何用处)。

_buttonPause.Enabled = false;

在此调用之后,当您将鼠标悬停在按钮上时,它不会变为蓝色。当然,处理程序也不会被调用。

我们还更改了按钮的图像和工具提示(当用户单击它时,播放图像将变为暂停图像,反之亦然)。

private void PauseTimer_Clicked(object sender, ThumbnailButtonClickedEventArgs e) {
    // ... (event logic) 
    e.ThumbnailButton.Icon = Resources.Play;
    e.ThumbnailButton.Tooltip = Resources.StartTimerTooltip;
    // ... (event logic continues)
}

如您所见,我们在 ThumbnailButtonClickedEventArgs 中获取了对我们按钮的引用。

请注意 DismissOnClick 属性(我们将其设置为“关于”按钮)。此属性决定单击按钮时任务栏缩略图会发生什么。

  • 如果 DismissOnClick 设置为 true,则任务栏缩略图将消失。
  • 如果 DismissOnClick 设置为 false,则不会发生任何事情。这是任务栏按钮的默认行为。

在我们的例子中,当用户单击关于按钮时,任务栏缩略图将消失。

Taskbar-Buttons

Aero Glass 窗口

为了创建 Aero Glass 窗口,我将我的窗体派生自 GlassFormWithCustomThumbnails。这是我的类,它派生自 GlassWindow

我们只需要重写 OnPaint 方法来绘制计时器状态。

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);
}

如前所述,应用程序的主窗口非常简单,因为它几乎从不使用。但使用 Aero Glass,它看起来不错(Windows 徽标是我的壁纸,它不在窗口上)。

Aero Glass window

任务对话框

当您启动计时器时,它会显示以下窗口。

Task-dialog

这种窗口称为任务对话框。让我们详细检查其各个部分。

Task-dialog

在 Windows API Code Pack 的 TaskDialog 类中可以设置所有内容。

/// Constructor. Initlalizes the _dialog 
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);
    
    // ... (create other buttons here) 
 
    _dialog.Controls.Add(btn5Munutes);
    // ... (add other buttons to the dialog here) 
}

这是一个处理按钮单击事件的函数。

/// Set the interval to N minutes and close the dialog
/// minutes - N, number of minutes to set
void SetInterval(int minutes) {
    _minutes = minutes;
    _dialog.Close(TaskDialogResult.CustomButtonClicked);
}

请注意,要在应用程序中使用任务对话框,您必须添加app.manifest文件。您可以在 Windows API Code Pack 示例或本文代码中找到示例。如果缺少清单,您将收到一个异常:“TaskDialog 功能需要加载 comctl32.dll 的版本 6,但当前内存中加载的是不同的版本。

跳转列表

跳转列表是 Windows 7 的另一个惊人功能。我已在此应用程序中使用它:您可以从列表中选择预定义的时间间隔之一。

此外,还有一个指向上次选择的链接。

所有项都已添加到Tasks部分。这是代码。

/// Configures the jump list 
public void ConfigureJumpList() {
    JumpList.AddUserTasks(new[] { GetMinutesLink(5, null, Icon), 
          GetMinutesLink(10, null, Icon), GetMinutesLink(30, null, Icon) });
}
 
/// Adds link "N minutes timer"
/// 
/// minutes - The number of minutes
/// timerName - Name of the timer
/// icon - The icon for the jump-list 
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};
}

请注意:我们不能直接向任务列表添加图标——我们必须传递 IconReference 对象。加载图标有两种选择。

  • .ico文件
  • 从 exe 文件中的资源

这是获取 IconReference 的代码。

/// Gets the icon for the jump-link 
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;
    }
}

此代码首先尝试查找.ico文件并用它添加 IconReference。如果磁盘上没有.ico文件,它将使用shell32.dll文件中的图标添加任务项。

请注意我们如何获取shell32.dll文件的路径:我们使用 Windows API Code Pack for Windows 7 中引入的 KnownFolders 类。

此外,我们每次启动计时器时都会添加一个自定义任务。

if (_totalMinutes != 5 && _totalMinutes != 10 && _totalMinutes != 30) {
    jumpListHelper.AddCustomLink(_totalMinutes, _options.TimerName);
}

将所有项添加到跳转列表后,我们必须保存它。

/// Saves the jump-list
public void Save() {
    JumpList.Refresh();
}

当您右键单击任务栏中的计时器图标时,会显示一个包含上次计时器设置的跳转列表。

Jump-list

跳转列表中的所有项的作用都是使用参数启动计时器应用程序。

<path_to_the_app> -minutes=<number_of_minutes> -name="<name_of_the_timer>"

原生方法

不幸的是,.NET Framework 中并非所有内容都已实现。我已导入这些例程(如果您不熟悉这些函数,可以通过单击以下链接查看其描述)。

您将在 NativeMethods 类中找到它们。

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);
}

FlashWindow 函数用于通过将窗口图标高亮显示为橙色来通知用户超时。

Flash window

如果您用鼠标打开任务栏缩略图,窗口将如下图所示。

Flash window expanded

当计时器到期时,我们只需使用这些方法并移除任务栏进度。

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);
    }
}

如您所见,声音是异步播放的(使用标志 SND_ASYNC),因此函数会立即返回。

关注点

在本文中,我们学习了

  • 如何创建没有框架的可更新缩略图;
  • 如果要创建使用所有者绘制的任务栏缩略图的窗口,必须处理哪些事件;
  • 如何向任务栏添加按钮;
  • 任务对话框的组成部分;
  • 如何创建跳转列表。

感谢您的阅读 :)

修订历史

  • 2009 年 11 月 - 初次修订。
  • 2010 年 1 月 - 添加了对任务栏按钮更详细的描述。
© . All rights reserved.