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





5.00/5 (5投票s)
本系列文章探讨了一个新的 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。我会关注评论和建议。
有两种方法可以识别通知图标
- 您可以使用
hWnd
和一个任意数字,该数字唯一地区分与该hWnd
相关联的每个通知图标。 - 您可以使用 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
的主窗口
这是 NotifyIconDemo1
显示的 Balloon 通知图标
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
属性将设置为一个小的红色图标,大的绿色图标。这有助于您确认显示的图标大小。然后检查另外两个复选框,以确定 LargeIcon
和 NoSound
的属性值。调用 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.HwndSource
和 System.Windows.Interop.HwndTarget
来创建 Shell_NotifyIcon
的包装器。NotifyIconLibrary
依赖于 DefinitionLibrary
和 SystemInformationLibrary
。
Win32 API 中的位图标志和打包字段通常在 NotifyIconWrapper
API 中实现为 bool
和 int
属性,分别。
构造函数
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 日:初始版本