带超链接和淡入淡出的 .NET 系统托盘气泡
.NET C# 系统托盘气泡组件,支持超链接和淡入淡出。
引言
.NET Framework 提供了丰富的用户通知功能,但缺少一个可用于消息驱动型应用程序的系统托盘气泡。本文将为您提供一个现成的组件,并解释如何对其进行自定义和使用。
背景
有时在编写用户交互式应用程序时,尤其是那些处理互联网资源的应用程序,我们需要向用户显示一些信息气泡。消息框或底部的备忘录不是很方便。最方便的通知方式是出现在屏幕右下角的气泡(就像 MSN、Skype 和 ICQ 那样)。
自从我第一次开发这个组件以来,它已经经历了多次修改,以满足客户的所有要求。以下是一些最显著的改进:
- 它必须是可配置的(背景、声音)
- 它必须能在扩展显示器上工作
- 出现时不能抢占输入焦点
- 不能出现在 Alt-Tab 切换列表中
- 它必须支持超链接,并在单击链接时打开浏览器
- 当屏幕上已显示其他气泡时,它必须堆叠显示
- 它必须支持多线程异步模型,能够处理来自任何线程(而不仅仅是 GUI 线程)的服务
- 当鼠标悬停在气泡区域时,它必须保持可见
要点
我将在下面解释一些最有趣的地方。
如何让系统托盘气泡出现但不抢占输入焦点
当您在程序中输入文本时,如果一个气泡出现并抢占了输入焦点,那是非常不方便的。如果我们使用常规的 `Form.Show` 方法,就会出现这种糟糕的行为。要显示一个气泡而不改变输入焦点,我们必须使用 `PInvoke ShowWindow` 方法。
[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter,
int x, int y, int cx, int cy, int flags);
SetWindowPos(Frm.Handle, (IntPtr)(-1), 0, 0, 0, 0, 0x50);
在上面的代码片段中,我调用了原生的 Windows API 函数,将我们的窗口显示在屏幕顶部,高于任何其他窗口。此外,根据最后一个参数,它将以非激活状态显示。这正是我们所需要的。因此,正在输入文本的用户不会被重定向到我们的窗口。
如何解析输入文本并创建超链接
使用 `LinkLabel` 设置多个链接非常简单,并且 MSDN 上有详细说明。但是,我们需要使用输入中的 HTML 文本来做到这一点(我们应该能够从某个地方复制链接并将其粘贴到 `.config` 文件或其他任何地方,然后代码必须进行解析,并将所有 `` 标签转换为链接)。完成此任务的最优方法是使用正则表达式。通常,与基于代码的替代方案相比,Regex 能以更优雅的方式解决基于文本的问题。因此,我们只需使用 Regex 解析文本并剥离所有 `` 标签,然后将它们用作 `LinkLabel` 控件中的链接。
static readonly System.Text.RegularExpressions.Regex A =
new System.Text.RegularExpressions.Regex(
"\\<a\\w+href=\"(?<href>[^\"]*)\"\\W*\\>(?<text>[^\\<]*)\\",
System.Text.RegularExpressions.RegexOptions.IgnoreCase |
System.Text.RegularExpressions.RegexOptions.Multiline);
private void SetupText()
{
TitleLabel.Text = Title;
string msg = Message ?? string.Empty;
var matches = A.Matches(msg);
if (matches == null || matches.Count == 0)
{
MessageLabel.Text = msg;
MessageLabel.LinkArea = new LinkArea(msg.Length, 0);
}
else
{
StringBuilder sb = new StringBuilder();
int last_index = 0;
foreach (System.Text.RegularExpressions.Match match in matches)
{
var href = match.Groups["href"].Value;
var text = match.Groups["text"].Value;
sb.Append(msg, last_index, match.Index - last_index);
MessageLabel.Links.Add(new LinkLabel.Link(sb.Length,
text.Length) { LinkData = href });
sb.Append(text);
last_index = match.Index + match.Length;
}
if (last_index < msg.Length)
sb.Append(msg.Substring(last_index));
MessageLabel.Text = sb.ToString();
}
}
工作原理
创建并调整 `TrayBalloon` 对象后,您应该通过调用 `Run` 方法来显示它。此方法将立即返回,因为它旨在供想要通知用户但不与其交互的多线程使用。调用此方法后,您可以继续执行,而不必关心它是如何工作的。
现在,让我们看一下 `TrayBalloon` 经过的关键步骤。首先,它将窗体的显示排队到线程池。之后,线程池线程调用 `TrayBalloon` 窗体的主入口点。在此方法中,`TrayBalloon` 会检查其他可见实例,以便在屏幕上选择一个可用空间(请记住,它必须堆叠显示)。如果没有其他实例,它将在底部显示;如果已显示其他 `TrayBalloon`,它将选择它们之间或上方的可用空间。如果屏幕上没有可用空间,它将覆盖最顶层的 `TrayBalloon` 窗口。另外,在出现时,它会使用动画和淡入淡出效果(但可以根据您显示之前设置的选项跳过)。
`TrayBalloon` 会在屏幕上显示几秒钟,然后开始淡出动画。但是,如果用户将鼠标保留在窗体上方,它将不会隐藏。`TrayBalloon` 隐藏后,它会在可见队列中取消注册自己,以释放屏幕空间,并允许其他 `TrayBalloon` 重用其空间。
这是内部结构的非常简短的描述;请阅读源代码以更详细地了解其工作原理。
结果
这是一个测试应用程序,可用于详细了解 `TrayBalloon.dll` 组件。
这是最终定制的气泡窗口
关注点
在测试许多带有系统托盘气泡的应用程序(不仅仅是这个)时,我偶然发现了一个非常有趣的 bug。如果您使 Windows 任务栏可编辑,并在系统托盘气泡出现时将其重新固定到屏幕的另一个角落,Explorer 可能会挂起 :) 请小心。
在使用此组件之前,我们需要设置一些属性,例如 `Title`、`Message` 等。请查看窗体示例,了解必须设置的字段的最低要求。如果您需要为出现的气泡指定声音或自定义背景,请这样做。还有许多其他不同的调整可以应用以获得性能、美观等等。
历史
这是文章的初稿。未来所有新的改进都将发布在这里。