WPF 吐司通知 - 深入研究。






4.85/5 (13投票s)
WPF 应用程序的精美吐司通知,
- 下载 WPFToastNotification_1.1.0.zip - 4.6 MB
- 下载 WPFToastNotification-1.0.2.zip - 140.4 KB
- 下载 WPFToastNotification-1.0.1.zip - 345.8 KB
只需下载项目演示,使用 Nuget 恢复丢失的包即可尽情体验。
引言
在本文中,我将完成我之前的文章 WPF 弹出通知。在那篇文章中,我们介绍了如何在 WPF 应用程序中使用弹出通知包。在本文中,我们将解释 WPFNotification 包的问题以及背后的代码。
Github
您可以从 这里 在 Github 上查看该项目,下载源代码和演示应用程序,尽情体验。
演示展示了如何使用弹出通知,可以按默认实现显示,也可以按自定义实现显示。
Nuget
WPF 弹出通知可在 NuGet 上获取,您可以使用 NuGet 管理器安装它,或者在程序包管理器控制台中运行以下命令。
PM> Install-Package WPFNotification
问题
最近我需要在 WPF 应用程序中使用一个弹出通知来向用户显示一些信息。于是我搜索了 WPF 通知,看看是否有符合我需求的搜索结果,以及从哪里开始。我发现了一个名为 Elysium 的优秀开源项目。它具有通知框功能。但存在一些问题。
- 通知的 UI 与我的应用程序 UI 不匹配。
- 我需要显示许多通知,每个通知都有不同的 UI。
- 它是一个大型库,而我不需要其他控件。
- 我需要一次显示一个通知,任何其他通知都将被放入队列。
于是我从这里开始,并调整了通知框,试图使其成为一个轻量级且时尚的可重用组件,易于使用并符合我的需求。
使用代码
WPFNotification 项目结构包含六个文件夹
- 模型
- Notification.cs
- 资产
- CloseButton.xaml
- NotificationItem.xaml
- NotificationUI.xaml
- 核心
- 配置
- NotificationConfiguration.cs
- NotificationFlowDirection.cs
- Interactivity
- FadeBehavior.cs
- SlideBehavior.cs
- NotifyBox.cs
- 配置
- 服务
- INotificationDialogService.cs
- NotificationDialogService.cs
- Converters
- BaseConverter.cs
- EmptyStringConverter.cs
- 资源
- 图像
- notification-icon.png
- 图像
现在我们将一步一步地探讨结构。所以我们将看看
模型
包含通知模型。它只有 Title、Message 和 Image URL
public class Notification
{
public string Title { get; set; }
public string Message { get; set; }
public string ImgURL { get; set; }
}
资产
此文件夹包含通知数据模板,通知默认样式和关闭按钮样式。
- CloseButton.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="SystemButtonBase" TargetType="ButtonBase">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Padding" Value="1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ButtonBase}">
<Border Name="Chrome"
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
SnapsToDevicePixels="true">
<ContentPresenter Margin="{TemplateBinding Padding}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="SystemButton" TargetType="ButtonBase" BasedOn="{StaticResource SystemButtonBase}">
<Setter Property="Width" Value="32" />
<Setter Property="Height" Value="24" />
<Setter Property="Foreground" Value="#d1d1d1"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#3e3e42" />
<Setter Property="Foreground" Value="#d1d1d1"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#1ba1e2" />
<Setter Property="Foreground" Value="#d1d1d1" />
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="#515151" />
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="SystemCloseButton" TargetType="ButtonBase" BasedOn="{StaticResource SystemButton}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#3e3e42" />
<Setter Property="Foreground" Value="#d1d1d1"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#e51400" />
<Setter Property="Foreground" Value="White" />
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="#515151" />
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
- NotificationItem.xaml
<UserControl x:Class="WPFNotification.Assets.NotificationItem"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:converters="clr-namespace:WPFNotification.Converters"
mc:Ignorable="d"
d:DesignHeight="150" d:DesignWidth="300"
x:Name="NotificationWindow"
Background="Transparent">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/WPFNotification;component/Assets/CloseButton.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid Background="Transparent">
<Border Name="border" Background="#2a3345" BorderThickness="0" CornerRadius="10" Margin="10">
<Border.Effect>
<DropShadowEffect ShadowDepth="0" Opacity="0.8" BlurRadius="10"/>
</Border.Effect>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Image Grid.RowSpan="2"
Source="{Binding ImgURL, Converter={converters:EmptyStringConverter}, ConverterParameter='pack://application:,,,/WPFNotification;component/Resources/Images/notification-icon.png'}"
Margin="4" Width="80"></Image>
<TextBlock Grid.Column="1" Text="{Binding Path=Title}" TextOptions.TextRenderingMode="ClearType" TextOptions.TextFormattingMode="Display" Foreground="White"
FontFamily="Arial" FontSize="14" FontWeight="Bold" VerticalAlignment="Center" Margin="2,4,4,2" TextWrapping="Wrap" TextTrimming="CharacterEllipsis" />
<Button x:Name="CloseButton"
Width="16"
Height="16"
Grid.Column="1"
HorizontalAlignment="Right"
Margin="0,0,12,0"
Click="CloseButton_Click"
Style="{StaticResource SystemCloseButton}">
<Button.Content>
<Grid Width="10" Height="12" RenderTransform="1,0,0,1,0,1">
<Path Data="M0,0 L8,7 M8,0 L0,7 Z" Width="8" Height="7"
VerticalAlignment="Center" HorizontalAlignment="Center"
Stroke="{Binding Foreground, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Button}}"
StrokeThickness="1.5" />
</Grid>
</Button.Content>
</Button>
<TextBlock Grid.Row="1"
Grid.Column="1"
Text="{Binding Path=Message}"
TextOptions.TextRenderingMode="ClearType"
TextOptions.TextFormattingMode="Display"
Foreground="White"
FontFamily="Arial"
VerticalAlignment="Stretch"
Margin="5"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"/>
</Grid>
</Border>
</Grid>
</UserControl>
在此文件中,我们仅创建默认通知窗口并将其绑定到通知模型。窗口包含
- 一个图像以显示通知图像。如果 notification ImageURL 为空,它将显示 images 文件夹中的默认预定义图像,这要归功于
- 两个文本框,一个用于显示通知标题,另一个用于显示通知消息。
- 关闭按钮,用于立即关闭通知窗口。要实现此功能,我们需要实现
CloseButton_Click
事件。
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
Window parentWindow = Window.GetWindow(this);
this.Visibility = Visibility.Hidden;
parentWindow.Close();
}
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Model="clr-namespace:WPFNotification.Model"
xmlns:NotificationView="clr-namespace:WPFNotification.Assets">
<DataTemplate x:Key="notificationTemplate" DataType="{x:Type Model:Notification}">
<NotificationView:NotificationItem/>
</DataTemplate>
</ResourceDictionary>
一个资源字典,其中包含 DataTemplate 以定义通知模型的呈现。我们将使用它的名称作为配置对象的默认值。您必须在 App.xaml
文件中引用此文件。您可以在 WPF 弹出通知文章 - 入门部分 中看到这一点。
核心
配置
它包含
NotificationConfiguration
类NotificationFlowDirection
枚举。
NotificationConfiguration
public class NotificationConfiguration
{
#region Configuration Default values
/// <summary>
/// The default display duration for a notification window.
/// </summary>
private static readonly TimeSpan DefaultDisplayDuration = TimeSpan.FromSeconds(2);
/// <summary>
/// The default notifications window Width
/// </summary>
private const int DefaultWidth = 300;
/// <summary>
/// The default notifications window Height
/// </summary>
private const int DefaultHeight = 150;
/// <summary>
/// The default template of notification window
/// </summary>
private const string DefaultTemplateName = "notificationTemplate";
#endregion
#region constructor
/// <summary>
/// Initialises the configuration object.
/// </summary>
/// <param name="displayDuration">The notification display duration. set it TimeSpan.
/// Zero to use default value </param>
/// <param name="width">The notification width. set it to null to use default value</param>
/// <param name="height">The notification height. set it to null to use default value</param>
/// <param name="templateName">The notification template name.
/// set it to null to use default value</param>
/// <param name="notificationFlowDirection">The notification flow direction.
/// set it to null to use default value (RightBottom)</param>
public NotificationConfiguration(TimeSpan displayDuration, int? width, int? height,
string templateName,
NotificationFlowDirection? notificationFlowDirection)
{
DisplayDuration = displayDuration > TimeSpan.Zero ? displayDuration : DefaultDisplayDuration;
Width = width.HasValue ? width : DefaultWidth;
Height = height.HasValue ? height : DefaultHeight;
TemplateName = !string.IsNullOrEmpty(templateName) ? templateName : DefaultTemplateName;
NotificationFlowDirection = notificationFlowDirection ?? NotificationFlowDirection.RightBottom;
}
#endregion
#region public Properties
/// <summary>
/// The default configuration object
/// </summary>
public static NotificationConfiguration DefaultConfiguration
{
get
{
return new NotificationConfiguration(DefaultDisplayDuration,DefaultWidth,
DefaultHeight, DefaultTemplateName,
NotificationFlowDirection.RightBottom);
}
}
/// <summary>
/// The display duration for a notification window.
/// </summary>
public TimeSpan DisplayDuration { get; private set; }
/// <summary>
/// Notifications window Width
/// </summary>
public int? Width { get; private set; }
/// <summary>
/// Notifications window Height
/// </summary>
public int? Height { get; private set; }
/// <summary>
/// The template of notification window
/// </summary>
public string TemplateName { get; private set; }
/// <summary>
/// The notification window flow direction
/// </summary>
public NotificationFlowDirection NotificationFlowDirection { get; set; }
#endregion
}
使用此类,您可以配置通知
- 宽度。默认值为 300
- 高度。默认值为 150
- 显示时长。默认值为 2 秒。
- 模板名称。默认值为“notificationTemplate”,此值是 NotificationUI.xaml 文件中数据模板的名称。
- NotificationFlowDirection. 这设置了新通知窗口出现的方向。默认值为 RightBottom。
NotificationFlowDirection
代表通知窗口方向的枚举
public enum NotificationFlowDirection
{
RightBottom,
LeftBottom,
LeftUp,
RightUp,
}
Interactivity
包含两个行为
- 用于通知窗口淡入/淡出的淡入淡出行为
- 用于通知窗口滑动进出/出的滑动行为
NotifyBox
在我们开始解释 NotifyBox
类之前,让我先介绍一下 WindowInfo
类。此类将保存窗口的元数据。
private sealed class WindowInfo
{
public int ID { get; set; }
public TimeSpan DisplayDuration { get; set; }
public Window Window { get; set; }
}
NotifyBox
是一个依赖于 Reactive Extensions 的静态类。它包含一些私有字段
public static class NotifyBox
{
private const int MAX_NOTIFICATIONS = 1;
private static int notificationWindowsCount;
private const double Margin = 5;
private static List<WindowInfo> notificationWindows;
private static List<WindowInfo> notificationsBuffer;
static NotifyBox()
{
notificationWindows = new List<WindowInfo>();
notificationsBuffer = new List<WindowInfo>();
notificationWindowsCount = 0;
}
.......
.......
}
MAX_NOTIFICATIONS
是您希望同时显示的通知数量。
注意:在当前版本中,MAX_NOTIFICATIONS
设置为一,且不可配置,我们一次只显示一个通知。
-
notificationWindowsCount
一个累积数字,表示通知的数量 -
notificationWindows
要显示的通知列表 -
notificationsBuffer
一个缓冲区列表,将放置待处理的通知。
现在让我们解释公共方法
public static class NotifyBox
{
.....
.....
public static void Show(object content, NotificationConfiguration configuration)
{
DataTemplate notificationTemplate = (DataTemplate)Application.Current.Resources[configuration.TemplateName];
Window window = new Window()
{
Title = "",
Width = configuration.Width.Value,
Height = configuration.Height.Value,
Content = content,
ShowActivated = false,
AllowsTransparency = true,
WindowStyle = WindowStyle.None,
ShowInTaskbar = false,
Topmost = true,
Background = Brushes.Transparent,
UseLayoutRounding = true,
ContentTemplate = notificationTemplate
};
Show(window, configuration.DisplayDuration);
}
public static void Show(object content)
{
Show(content, NotificationConfiguration.DefaultConfiguration);
}
public static void Show(Window window, TimeSpan displayDuration,
NotificationFlowDirection notificationFlowDirection )
{
BehaviorCollection behaviors = Interaction.GetBehaviors(window);
behaviors.Add(new FadeBehavior());
behaviors.Add(new SlideBehavior());
SetWindowDirection(window, notificationFlowDirection);
notificationWindowsCount += 1;
WindowInfo windowInfo = new WindowInfo()
{
ID = notificationWindowsCount,
DisplayDuration = displayDuration,
Window = window
};
if (notificationWindows.Count + 1 > MAX_NOTIFICATIONS)
{
notificationsBuffer.Add(windowInfo);
}
else
{
Observable
.Timer(displayDuration)
.ObserveOnDispatcher()
.Subscribe(x => OnTimerElapsed(windowInfo));
notificationWindows.Add(windowInfo);
window.Show();
}
}
/// <summary>
/// Remove all notifications from notification list and buffer list.
/// </summary>
public static void ClearNotifications()
{
notificationWindows.Clear();
notificationsBuffer.Clear();
notificationWindowsCount = 0;
}
......
......
我们有 ClearNotifications
方法
在此方法中,我们只是将所有内容重置为零状态
- 我们将 notificationWindowsCount 设置为零。
- 我们清空 notificationWindows 和 notificationsBuffer 列表。
然后我们有三个重载的 Show
方法
public static void Show(object content)
public static void Show(object content, NotificationConfiguration configuration)
在此方法中,我们执行以下步骤
- 使用
configuration
对象中的templateName
,获取notificationTemplate
。 - 使用此模板和配置对象的其他值创建
window
对象。 - 调用
Show
方法,并传入window
对象和配置的显示时长值。
public static void Show(Window window, TimeSpan displayDuration, NotificationFlowDirection notificationFlowDirection)
在此方法中,我们执行以下步骤
- 我们获取与
window
对象关联的BehaviorCollection
,并向其中添加slide
和fade
行为。 - 我们调用
SetWindowDirection
方法,根据notificationFlowDirection
值将通知窗口放置在正确的位置,默认值为屏幕的 (RightBottom) 角。 - 我们创建一个
WindowInfo
对象,并检查当前显示的通知数量是否大于MAX_NOTIFICATIONS
。如果是,我们将此通知添加到缓冲区列表,否则我们显示通知。显示时长结束后,我们调用回调函数OnTimerElapsed
。
现在让我们讨论 OnTimerElapsed
方法
private static void OnTimerElapsed(WindowInfo windowInfo)
{
if (windowInfo.Window.IsMouseOver)
{
Observable
.Timer(windowInfo.DisplayDuration)
.ObserveOnDispatcher()
.Subscribe(x => OnTimerElapsed(windowInfo));
}
else
{
BehaviorCollection behaviors = Interaction.GetBehaviors(windowInfo.Window);
FadeBehavior fadeBehavior = behaviors.OfType<FadeBehavior>().First();
SlideBehavior slideBehavior = behaviors.OfType<SlideBehavior>().First();
fadeBehavior.FadeOut();
slideBehavior.SlideOut();
EventHandler eventHandler = null;
eventHandler = (sender2, e2) =>
{
fadeBehavior.FadeOutCompleted -= eventHandler;
notificationWindows.Remove(windowInfo);
windowInfo.Window.Close();
if (notificationsBuffer != null && notificationsBuffer.Count > 0)
{
var BufferWindowInfo = notificationsBuffer.First();
Observable
.Timer(BufferWindowInfo.DisplayDuration)
.ObserveOnDispatcher()
.Subscribe(x => OnTimerElapsed(BufferWindowInfo));
notificationWindows.Add(BufferWindowInfo);
BufferWindowInfo.Window.Show();
notificationsBuffer.Remove(BufferWindowInfo);
}
};
fadeBehavior.FadeOutCompleted += eventHandler;
}
}
通知显示时长结束后将调用此方法。在此方法中,我们检查鼠标指针是否仍然位于通知窗口上方。在某些情况下
- 是,我们将通知窗口再显示一个显示时长。
- 否,我们获取窗口的相关行为,然后调用
FadeOut()
和SlideOut
方法,并订阅FadeOutCompleted
回调函数。在此方法中- 我们关闭通知窗口,并将其从显示的通知列表中移除。
- 如果缓冲区列表(队列)不为空,我们从队列中取出第一个通知并在屏幕上显示它。
最后,在 NotifyBox 类中,让我们讨论 SetWindowDirection
方法
private static void SetWindowDirection(Window window, NotificationFlowDirection notificationFlowDirection)
{
var workingArea = System.Windows.Forms.Screen.PrimaryScreen.WorkingArea;
var transform = PresentationSource.FromVisual(Application.Current.MainWindow).CompositionTarget.TransformFromDevice;
var corner = transform.Transform(new Point(workingArea.Right, workingArea.Bottom));
switch (notificationFlowDirection)
{
case NotificationFlowDirection.RightBottom:
window.Left = corner.X - window.Width - window.Margin.Right - Margin;
window.Top = corner.Y - window.Height - window.Margin.Top;
break;
case NotificationFlowDirection.LeftBottom:
window.Left = 0;
window.Top = corner.Y - window.Height - window.Margin.Top;
break;
case NotificationFlowDirection.LeftUp:
window.Left = 0;
window.Top = 0;
break;
case NotificationFlowDirection.RightUp:
window.Left = corner.X - window.Width - window.Margin.Right - Margin;
window.Top = 0;
break;
default:
window.Left = corner.X - window.Width - window.Margin.Right - Margin;
window.Top = corner.Y - window.Height - window.Margin.Top;
break;
}
}
在此方法中,我们仅计算通知窗口的坐标,以便根据 notificationFlowDirection
值在正确的位置显示通知,而不考虑监视器分辨率或计算机是否使用默认 DPI 设置。有关 WPF 中 DPI 值的更多信息,请参阅这篇 文章。
服务
包含一个 INotificationDialogService
接口,该接口有两个方法
public interface INotificationDialogService
{
/// <summary>
/// Show notification window.
/// </summary>
/// <param name="content">The notification object.</param>
void ShowNotificationWindow(object content);
/// <summary>
/// Show notification window.
/// </summary>
/// <param name="content">The notification object.</param>
/// <param name="configuration">The notification configuration object.</param>
void ShowNotificationWindow(object content, NotificationConfiguration configuration);
/// <summary>
/// Remove all notifications from notification list and buffer list.
/// </summary>
void ClearNotifications();
}
要显示通知窗口,您必须在项目中实现此接口。或者使用默认实现。让我们看看 NotificationDialogService
;在此类中,我们仅实现 ShowNotificationWindow
方法。我认为代码是不言自明的。
public class NotificationDialogService : INotificationDialogService
{
/// <summary>
/// Show notification window.
/// </summary>
/// <param name="content">The notification object.</param>
public void ShowNotificationWindow(object content)
{
NotifyBox.Show(content);
}
/// <summary>
/// Show notification window.
/// </summary>
/// <param name="content">The notification object.</param>
/// <param name="configuration">The notification configuration object.</param>
public void ShowNotificationWindow(object content, NotificationConfiguration configuration)
{
NotifyBox.Show(content, configuration);
}
/// <summary>
/// Remove all notifications from notification list and buffer list.
/// </summary>
public void ClearNotifications()
{
NotifyBox.ClearNotifications();
}
}
Converters
包含 EmptyStringConverter
,当将通知图像绑定到通知对象中的 ImgURL
属性时,我们使用此转换器。因此,如果 ImageURL 为空,转换器将分配默认图像 URL。
public class EmptyStringConverter : BaseConverter, IValueConverter
{
public EmptyStringConverter()
{ }
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
return string.IsNullOrEmpty(value as string) ? parameter : value;
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
资源
它包含 Images 文件夹,其中包含默认通知图像。只要您 没有 覆盖 <span class="sac" id="spans0e3">notification</span>
对象中的 ImgURL
属性,该图像将用于通知 窗口。
历史
- 2016 年 2 月 12 日 发布了原始帖子。
- 2016 年 7 月 3 日 添加了新功能
- 添加了对 Windows 7 及更高版本的支持。
- 添加了对在不同屏幕分辨率上显示通知的支持。
- 添加了通知流方向。
- 允许删除缓冲区列表中的所有通知