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

WPF 友好的 Shell_NotifyIcon 包装类 - 第一部分:通知图标包装

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2020 年 12 月 2 日

CPOL

12分钟阅读

viewsIcon

7821

downloadIcon

506

本系列文章探讨了一个新的 WPF 友好的 Shell_NotifyIcon 包装器类。

引言

本系列文章介绍了一个新的 Shell_NotifyIcon 包装器类,这是一个臭名昭著的棘手的 Win32 API。尽管 WinForms 提供了 NotifyIcon 类,但其 API 仅比 Win32 API 略好,文档也是如此。我希望它独立于 WinForms,所以我创建了这个包装器类。

本文(第 1 部分:通知图标包装器)描述了包装器库公开的 API,并提供了一些简单的示例来展示其功能。

背景

通知图标

在研究了网上找到的几个相关项目后,我决定尽可能消除对 WinForms 的依赖是有价值的。最大的障碍是 WinForms 的 NotifyIcon 类。这个类有一个不友好的 API。它基于一个 Win32 API (Shell_NotifyIcon),它的 API 更糟糕,而且两者可用的文档都很稀少且糟糕。作为参考,我查找了 Win32 API 使用的一个示例。我找到的叫做“StealthDialog”。目前我找不到我的来源(用于归属),但我有原始代码,它没有任何指纹。我没有直接使用任何代码,但它直接是旧的 C++,让我感到我正在近距离接触 Win32 API。我在这段代码和 NotifyIcon 的 Reference Source 之间进行了一些交叉检查,这帮助我理解了任何细微差别,并给了我用 C# 和 PInvoke 创建自己的代码的信心。

从 Windows 10 用户体验的角度来看,似乎有两种不同类型的通知图标:一种是出现在系统托盘中并迁移到溢出区域的传统图标,另一种是出现在屏幕右侧边缘一段时间然后迁移到点击屏幕右下角图标时弹出的手动操作的通知面板的现代通知。从程序员的角度来看,这两种概念的区别仅在于设置了哪些参数以及哪些未设置。

我为 NotifyIconWrappper 类创建了一个 API,并将其放置在一个库 (NotifyIconLibrary) 中。我尝试将其打包为 NuGet 包并确认可行,但我决定不想承担更新它的负担。NotifyIconWrapper 位于 NotifyIconLibrary 命名空间中。

为了能够处理任意数量的进程、线程和通知图标而不使用面向对象的架构,Shell_NotifyIcon Win32 API 依赖其客户端来跟踪执行其功能所需的许多状态信息。当前窗口会话中所有进程的通知图标有效地包含在 Win32 API 可以搜索的某个集合中。每一次交互要么创建一个新的通知图标,要么处理一个之前创建的图标。NotifyIconWrapper 维护此状态,因此您不必这样做。

每个通知图标都必须与一个有效的 Win32 窗口句柄 (hWnd) 相关联。Win32 API 要求其客户端提供这样一个句柄以及与之关联的窗口。该窗口充当通知图标的代理。如果通知图标是一个普通窗口,它将接收到的消息会被重新打包并以不同类型的消息路由到代理窗口。处理这些重新路由的消息需要客户端为此目的提供一个窗口过程 (WndProc)。NotifyIconWrapper 为您完成了这个工作。它会创建一个带有 WndProc 的隐藏窗口,这样您就不必这样做了。在创建示例应用程序时,我尝试探索了相关的消息类型。我在 NotifyIconWrapper API 中公开了许多这些消息作为事件。如果我的猜测有所偏差,我将考虑修改 API。我会关注评论和建议。

有两种方法可以识别通知图标

  1. 您可以使用 hWnd 和一个任意数字,该数字唯一地区分与该 hWnd 相关联的每个通知图标。
  2. 您可以使用 GUID 来防止通知图标被欺骗。您仍然需要提供 hWnd

    互联网上关于为此目的使用 GUID 的混乱很多。当您第一次在特定计算机上添加具有特定 GUID 的通知图标时,操作系统会记录应用程序的完整路径并永久关联它。我不知道他们把它放在哪里。也许他们使用了系统注册表受保护的部分。据称您无法删除这些条目。我不想深究这个问题,但 SysInternals(微软的一个部门)的 ProcMon 可能会揭示一些东西。您只能从名称和绝对路径都相同的程序添加具有该 GUID 的通知图标。我建议使用合适的注册表项或程序的设置文件来存储 GUID 和程序的完整路径。当您希望在给定的程序运行时添加一个特定的 NotifyIcon 时,如果 GUID 和路径尚未存储,或者路径不匹配,请使用 System.Guid.NewGuid() 随机生成一个新 GUID 并存储它,以及您程序的绝对路径,以供将来参考。如果出现不匹配,请确保将新信息覆盖旧信息。如果路径匹配,请使用您与之一起存储的 GUID 作为添加通知图标的 GUID。

    除非通知图标欺骗对您来说是一个真正的安全风险,否则我建议您像躲避瘟疫一样避开使用 GUID 标识方法。您还应该注意,因此,我对 API 的这部分测试并不多。

Shell 本质上是伪装的 Explorer.exe。当 Shell 失效时,您可以通过终止并重新启动 Explorer.exe 来修复它。Shell 维护任务栏以及与之相关的大部分(如果不是全部)内容。当 Shell 崩溃并恢复或手动重新启动时,它会向所有窗口发送一个动态注册的唯一窗口消息。如果您的应用程序处理此消息,它可以调用 Recover() 来恢复通知图标的先前状态。

Using the Code

这是 NotifyIconDemo1 的主窗口

Main window of NotifyIconDemo1

这是 NotifyIconDemo1 显示的 Balloon 通知图标

BalloonNotifyIcon of NotifyIconDemo1

NotifyIconDemo1 的主窗口构造函数很简单,所以让我们看看主窗口的非平凡初始化在哪里发生

/// <summary>
/// This is a good place to initialize things that depend on the window being
/// connected to its source.
/// </summary>
/// <param name="e">This is the standard <c>EventArgs</c>.</param>
protected override void OnSourceInitialized(EventArgs e)
{
    base.OnSourceInitialized(e);
    _notifyIconWrapper = new NotifyIconWrapper();
    _notifyIconWrapper.Update();
    _notifyIconWrapper.BalloonTipClicked += NotifyIconWrapper_BalloonTipClicked;
    _notifyIconWrapper.BalloonTipClosed += NotifyIconWrapper_BalloonTipClosed;
    _notifyIconWrapper.BalloonTipShown += NotifyIconWrapper_BalloonTipShown;
}        

请注意,OnSourceInitialized 是一个晦涩的重写,在使用 Win32 Interop 时会发挥作用。在调用基类实现后,此方法会创建一个新的 NotifyIconWrapper,然后订阅 NotifyIconWrapper 提供的三个事件。这些事件的处理很简单,因此在此不予显示。有关详细信息,请参阅源代码。

现在让我们看看点击 **Notify** 按钮时会发生什么

/// <summary>
/// The Notify button has been clicked.
/// </summary>
/// <param name="sender">This is the source of the event (<c>NotifyButton</c>).</param>
/// <param name="e">This is the <c>RoutedEventArgs</c> used with most routed events.</param>
private void NotifyButton_Click(object sender, RoutedEventArgs e)
{
    if (_notifyIconWrapper != null)
    {
        NotifyButton.IsEnabled = false;
        _notifyIconWrapper.Info = Properties.LocalizedResources.DemoInfo;
        _notifyIconWrapper.InfoTitle = Properties.LocalizedResources.DemoInfoTitle;
        
        if (NoIcon.IsChecked.HasValue && NoIcon.IsChecked.Value)
        {
            _notifyIconWrapper.IconType = NotifyIconType.None;
        }
        
        if (InfoIcon.IsChecked.HasValue && InfoIcon.IsChecked.Value)
        {
            _notifyIconWrapper.IconType = NotifyIconType.Info;
        }
        
        if (WarningIcon.IsChecked.HasValue && WarningIcon.IsChecked.Value)
        {
            _notifyIconWrapper.IconType = NotifyIconType.Warning;
        }
        
        if (ErrorIcon.IsChecked.HasValue && ErrorIcon.IsChecked.Value)
        {
            _notifyIconWrapper.IconType = NotifyIconType.Error;
        }
        
        if (UserIcon.IsChecked.HasValue && UserIcon.IsChecked.Value)
        {
            _notifyIconWrapper.IconType = NotifyIconType.User;
        }
        
        _notifyIconWrapper.BalloonIcon =
            _notifyIconWrapper.IconType == NotifyIconType.User
          ? Properties.NonLocalizedResources.TwoTone
          : null;
        _notifyIconWrapper.LargeIcon = 
                   LargeIcon.IsChecked.HasValue && LargeIcon.IsChecked.Value;
        _notifyIconWrapper.NoSound = Silent.IsChecked.HasValue && Silent.IsChecked.Value;
        _notifyIconWrapper.Update();
    }
}        

在对 NotifyIconWrapper 引用进行 null 检查后,此方法会禁用 **Notify** 按钮。然后将 Info 属性设置为“Put your message here.”,将 InfoTitle 属性设置为“Put your title here.”。然后,该方法会遍历一系列复选框。对于每个勾选的复选框,IconType 参数都会设置为相应的值。一次只能有一个复选框处于勾选状态。如果 IconType 属性为 NotifyIconType.User,则 BalloonIcon 属性将设置为一个小的红色图标,大的绿色图标。这有助于您确认显示的图标大小。然后检查另外两个复选框,以确定 LargeIconNoSound 的属性值。调用 Update() 以将 Win32 通知图标的状态与 NotifyIconWrapper 的状态同步。这也会触发 Balloon 通知。

最后,让我们看看主窗口关闭时会发生什么

/// <summary>
/// This window has been closed.
/// </summary>
/// <param name="e">This is the standard <c>EventArgs</c>.</param>
protected override void OnClosed(EventArgs e)
{
    if (_notifyIconWrapper != null)
    {
        _notifyIconWrapper.BalloonTipClicked -= NotifyIconWrapper_BalloonTipClicked;
        _notifyIconWrapper.BalloonTipClosed -= NotifyIconWrapper_BalloonTipClosed;
        _notifyIconWrapper.BalloonTipShown -= NotifyIconWrapper_BalloonTipShown;
        _notifyIconWrapper.Close();
        _notifyIconWrapper = null;
    }

    base.OnClosed(e);
}        

在对 NotifyIconWrapper 引用进行 null 检查后,将取消订阅之前使用的三个 NotifyIconWrapper 事件,关闭 NotifyIconWrapper 并将其引用设置为 null。在任何情况下,最后一步都是调用基类实现。

NotifyIconWrapper 类

public sealed class NotifyIconWrapper : IDisposable

命名空间:NotifyIconLibrary

程序集:NotifyIconLibrary.dll

备注

此类使用 System.Windows.Interop.HwndSourceSystem.Windows.Interop.HwndTarget 来创建 Shell_NotifyIcon 的包装器。NotifyIconLibrary 依赖于 DefinitionLibrarySystemInformationLibrary

Win32 API 中的位图标志和打包字段通常在 NotifyIconWrapper API 中实现为 boolint 属性,分别。

构造函数

NotifyIconWrapper

public NotifyIconWrapper(int callbackMessage = WindowMessages.User)

这是 NotifyIconWrapper 类的构造函数。

callbackMessage 是用于重新打包从通知图标接收的消息的消息编号,以便可以将它们重新路由到通知图标窗口。除非您有理由不这样做,否则应使用默认值。

属性

图标

public System.Drawing.Icon Icon { get; set; }

获取或设置要显示为经典通知图标的 Icon

ShowTip

public bool ShowTip { get; set; }

获取或设置显示工具提示文本的样式。设置为 true 以显示原始工具提示样式。设置为 false 以显示气球工具提示样式。

提示

public String Tip { get; set; }

获取或设置悬停在通知图标上时显示的工具提示文本。string 限制为 63 个字符。

隐藏

public bool Hidden { get; set; }

获取或设置一个值,指示通知图标是否隐藏。

SharedIcon

public bool SharedIcon { get; set; }

获取或设置一个值,指示通知图标是否共享。

Info(信息)

public string Info { get; set; }

获取或设置气球通知的信息文本。string 限制为 255 个字符。

InfoTitle

public string InfoTitle { get; set; }

获取或设置气球通知的标题文本。string 限制为 63 个字符。

IconType

public int IconType { get; set; }

获取或设置气球图标类型。图标类型必须是 NotifyIconType 类的 static 成员。所用图标的大小由 LargeIcon 属性确定。

NoSound

public bool NoSound { get; set; }

获取或设置一个值,指示气球通知是静音还是有声。

LargeIcon

public bool LargeIcon { get; set; }

获取或设置一个值,指示在气球通知中显示大图标还是小图标。

RespectQuietTime

public bool RespectQuietTime { get; set; }

获取或设置一个值,指示在显示气球通知时是否应尊重或忽略静音时间。

GuidItem

public Guid GuidItem { get; set; }

获取或设置一个 Guid,用于唯一标识此通知图标。

BalloonIcon

public System.Drawing.Icon BalloonIcon { get; set; }

获取或设置当 IconType 属性等于 NotifyIconType.User 时,用于气球通知的图标。此图标的大小由 LargeIcon 属性确定。

方法

SetFocusOnNotifyIcon

public void SetFocusOnNotifyIcon()

当通过按 escape 键关闭上下文菜单时使用此选项。它将焦点返回到通知图标,以便可以使用键盘命令,例如按 enter 键或 escape 键。

删除

public void Delete()

Delete 方法准备通知图标数据结构并执行它。这通常不需要后跟对 Update 方法的调用。

Recover

public void Recover()

从命令 shell (explorer.exe) 重启中恢复。这通常不需要后跟对 Update 方法的调用。

更新

public void Update()

如果图标已显示在系统托盘中,则使用累积的任何更改对其进行修改。否则,使用所有指定的选项在系统托盘中显示图标。

Close

public void Close()

关闭通知图标窗口。

处置

public void(Dispose)

实现 IDisposible 模式。

事件

Closed

public event EvenHandler Closed;

NotifyIconWindow 已关闭。

BalloonTipClicked

public event EventHandler BalloonTipClicked;

气球提示已被点击。

BalloonTipClosed

public event EventHandler BalloonTipClosed;

气球提示已关闭。

BalloonTipShown

public event EventHandler BalloonTipShown;

气球提示已显示。

MouseMove

public event EventHandler<MouseLocationEventArgs> MouseMove;

鼠标已移动。

LeftMouseButtonDown

public event EventHandler<MouseLocationEventArgs> LeftMouseButtonDown;

已按下鼠标左键。

LeftMouseButtonClick

public event EventHandler<MouseLocationEventArgs> LeftMouseButtonClick;

已点击鼠标左键。

LeftMouseButtonDoubleClick

public event EventHandler<MouseLocationEventArgs> LeftMouseButtonDoubleClick;

已双击鼠标左键。

LeftMouseButtonUp

public event EventHandler<MouseLocationEventArgs> LeftMouseButtonUp;

已释放鼠标左键。

MiddleMouseButtonDown

public event EventHandler<MouseLocationEventArgs> MiddleMouseButtonDown;

已按下鼠标中键。

MiddleMouseButtonClick

public event EventHandler<MouseLocationEventArgs> MiddleMouseButtonClick;

已点击鼠标中键。

MiddleMouseButtonDoubleClick

public event EventHandler<MouseLocationEventArgs> MiddleMouseButtonDoubleClick;

已双击鼠标中键。

MiddleMouseButtonUp

public event EventHandler<MouseLocationEventArgs> MiddleMouseButtonUp;

已释放鼠标中键。

RightMouseButtonDown

public event EventHandler<MouseLocationEventArgs> RightMouseButtonDown;

已按下鼠标右键。

RightMouseButtonClick

public event EventHandler<MouseLocationEventArgs> RightMouseButtonClick;

已点击鼠标右键。

RightMouseButtonDoubleClick

public event EventHandler<MouseLocationEventArgs> RightMouseButtonDoubleClick;

已双击鼠标右键。

RightMouseButtonUp

public event EventHandler<MouseLocationEventArgs> RightMouseButtonUp;

已释放鼠标右键。

ShowContextMenu

public event EventHandler<MouseLocationEventArgs> ShowContextMenu;

显示通知图标的上下文菜单。

NotifyIconSelectedViaKeyboard

public event EventHandler NotifyIconSelectedViaKeyboard;

通知图标已通过键盘选择。

NotifyIconDemo1 项目

我将此项目作为 NotifyIconLibrary 的第一个演示。它允许您探索使用气球图标时几乎所有可用的选项。

一项警告:根据您计算机上安装的其他软件和您的计算机设置,气球图标通知可能已被禁用,或者可能在不显示为弹出窗口的情况下立即路由到通知面板。在我的情况下,我必须确保我的任何窗口都没有最大化。

关注点

尽管我已尝试移除所有对 WinForms 的依赖,但仍有一个领域是肯定的。几乎我考虑过的每种本地化方案都依赖于 .resx 文件,而这些文件在历史上依赖于 WinForms。我已经为此类本地化实现了基础工作,但将其细节留给任何发现我的代码有用的人。

每个可执行项目都提供了基本的异常处理。在每种情况下,您都会在名为 program.cs 的文件中找到它。此文件还包含项目的入口点 (Main)。我没有使用默认的启动代码,因为我认为它不适合我的目的,特别是它没有提供处理异常的单一入口点。

在个人技能方面,我的技能仅限于图形艺术。如果阅读此文的人有技能和兴趣,我的代码确实需要一些更花哨的图标。

我还没有进行过任何 .NET Core 项目。我可能会尝试将本文系列涵盖的项目转换为 .NET Core。如果我这样做,我会尝试相应地更新文章。

本系列的下一篇文章将讨论如何将屏幕最小化到通知图标托盘(也称为系统托盘)。

历史

  • 2020 年 12 月 1 日:初始版本
© . All rights reserved.