- A+
WPF 实现 Message 消息提醒控件
控 件:Message
作 者:WPFDevelopersOrg - 驚鏵
-
框架使用
.NET4 至 .NET6
; -
Visual Studio 2022
; -
接着上一篇
1)新增 MessageListBoxItem.cs
代码如下:
-
新增了名为
MessageType
的依赖属性,类型为MessageBoxImage
,默认值为MessageBoxImage.Information
-
新增了名为
IsCenter
的依赖属性,默认值为false
,为true
则内容居中显示。
using System.Windows; using System.Windows.Controls; namespace WPFDevelopers.Controls { public class MessageListBoxItem : ListBoxItem { public MessageBoxImage MessageType { get { return (MessageBoxImage)GetValue(MessageTypeProperty); } set { SetValue(MessageTypeProperty, value); } } public static readonly DependencyProperty MessageTypeProperty = DependencyProperty.Register("MessageType", typeof(MessageBoxImage), typeof(MessageListBoxItem), new PropertyMetadata(MessageBoxImage.Information)); public bool IsCenter { get { return (bool)GetValue(IsCenterProperty); } set { SetValue(IsCenterProperty, value); } } public static readonly DependencyProperty IsCenterProperty = DependencyProperty.Register("IsCenter", typeof(bool), typeof(MessageListBoxItem), new PropertyMetadata(false)); } }
2)新增 MessageListBox.cs
代码如下:
-
自定义
MessageListBox
继承ListBox
,其中重写了两个方法:IsItemItsOwnContainerOverride
和GetContainerForItemOverride
。-
IsItemItsOwnContainerOverride
方法用于确定给定的项是否应该作为其自己的容器。在这里,它检查传入的item
对象是否是MessageListBoxItem
的实例。如果是,则返回true
,表示该项是其自己的容器;否则,返回false
。
-
GetContainerForItemOverride
方法用于创建并返回一个新的容器对象,用于承载列表框中的项。在这里,它创建并返回一个MessageListBoxItem
的实例作为容器对象。
-
using System.Windows; using System.Windows.Controls; namespace WPFDevelopers.Controls { public class MessageListBox : ListBox { protected override bool IsItemItsOwnContainerOverride(object item) { return item is MessageListBoxItem; } protected override DependencyObject GetContainerForItemOverride() { return new MessageListBoxItem(); } } }
3)新增 MessageAdorner.cs
代码如下:
-
MessageAdorner
是一个继承自Adorner
的自定义类。它用于在装饰元素上显示消息的附加装饰器。 -
构造函数
MessageAdorner(UIElement adornedElement)
接受一个UIElement
类型的参数,作为要进行装饰的元素。 -
Push
方法用于将消息添加到装饰器中。它接受消息内容、消息类型和是否居中显示的参数。如果装饰器尚未创建,则会创建一个MessageListBox
并将其设置为装饰器的子元素。然后,根据传入的参数创建一个新的MessageListBoxItem
,并将其插入到列表框的顶部。 -
Child
属性用于获取或设置装饰器的子元素。当设置子元素时,会将子元素添加到装饰器的可视化子元素集合中;当子元素为null
时,会从可视化子元素集合中移除子元素。 -
VisualChildrenCount
属性返回装饰器的可视化子元素数量。 -
ArrangeOverride
方法用于调整装饰器的布局。在这里,根据装饰元素的宽度和子元素的期望大小,计算出子元素的水平位置,并将其排列在装饰器的顶部。 -
GetVisualChild
方法用于获取指定索引处的可视化子元素。在这里,如果索引为0且子元素不为null
,则返回子元素;否则,调用基类的方法返回对应索引的可视化子元素。
using System.Windows; using System.Windows.Documents; using System.Windows.Media; namespace WPFDevelopers.Controls { public class MessageAdorner : Adorner { private MessageListBox listBox; private UIElement _child; private FrameworkElement adornedElement; public MessageAdorner(UIElement adornedElement) : base(adornedElement) { this.adornedElement = adornedElement as FrameworkElement; } public void Push(string message, MessageBoxImage type = MessageBoxImage.Information, bool center = false) { if (listBox == null) { listBox = new MessageListBox(); Child = listBox; } var mItem = new MessageListBoxItem { Content = message, MessageType = type, IsCenter = center }; listBox.Items.Insert(0, mItem); } public UIElement Child { get => _child; set { if (value == null) { RemoveVisualChild(_child); _child = value; return; } AddVisualChild(value); _child = value; } } protected override int VisualChildrenCount { get { return _child != null ? 1 : 0; } } protected override Size ArrangeOverride(Size finalSize) { var x = (adornedElement.ActualWidth - _child.DesiredSize.Width) / 2; _child.Arrange(new Rect(new Point(x, 0), _child.DesiredSize)); return finalSize; } protected override Visual GetVisualChild(int index) { if (index == 0 && _child != null) return _child; return base.GetVisualChild(index); } } }
4)新增 Message.cs
代码如下:
-
CreateMessageAdorner
方法用于创建消息装饰器MessageAdorner
。它接受一些参数,如窗口所有者Window owner
、消息内容string message
、消息类型MessageBoxImage type
和是否居中显示bool center
。在方法内部,它首先检查是否已经存在消息装饰器实例messageAdorner
,如果存在,则调用装饰器的Push
方法将新的消息添加到装饰器中,并直接返回。如果不存在消息装饰器实例,则根据提供的窗口所有者或获取默认窗口ControlsHelper.GetDefaultWindow()
,然后获取对应的装饰层AdornerLayer
。如果装饰层为空,则抛出异常。接着,创建一个新的消息装饰器实例,并将其添加到装饰层中。最后,调用装饰器的Push
方法将消息添加到装饰器中。 -
Push
方法是一个扩展方法,用于在指定的窗口上显示消息。它接受窗口对象Window owner
作为第一个参数,以及其他参数,如消息内容string message
、消息类型MessageBoxImage type
和是否居中显示bool center
。在方法内部,它调用CreateMessageAdorner
方法,并传递相应的参数。 -
另一个重载的
Push
方法是直接在静态上下文中调用的。它接受消息内容string message
、消息类型MessageBoxImage type
和是否居中显示bool center
作为参数,并调用CreateMessageAdorner
方法。
using System; using System.Linq; using System.Windows; using WPFDevelopers.Helpers; namespace WPFDevelopers.Controls { public static class Message { private static MessageAdorner messageAdorner; static void CreateMessageAdorner(Window owner = null, string message = null, MessageBoxImage type = MessageBoxImage.Information, bool center = false) { try { if (messageAdorner != null) { messageAdorner.Push(message, type, center); return; } if (owner == null) owner = ControlsHelper.GetDefaultWindow(); var layer = ControlsHelper.GetAdornerLayer(owner); if (layer == null) throw new Exception("not AdornerLayer is null"); messageAdorner = new MessageAdorner(layer); layer.Add(messageAdorner); messageAdorner.Push(message, type, center); } catch (Exception) { throw; } } public static void Push(this Window owner, string message, MessageBoxImage type = MessageBoxImage.Information, bool center = false) { CreateMessageAdorner(owner, message, type, center); } public static void Push(string message, MessageBoxImage type = MessageBoxImage.Information, bool center = false) { CreateMessageAdorner(message: message, type: type, center: center); } } }
5)新增 MessageListBoxItem.xaml
代码如下:
-
XAML
代码定义一个名为Storyboard_Loaded
的故事板(Storyboard
),其中包含了四个DoubleAnimation
动画-
第一个
DoubleAnimation
动画将目标元素PART_SmallPanel
的Y
轴缩放属性ScaleTransform.ScaleY
从0
变化到1
,持续时间为0.2
秒。这会使PART_SmallPanel
在加载时以渐变的方式显示出来。 -
第二个
DoubleAnimation
动画将目标元素PART_SmallPanel
的不透明度属性Opacity
从0.1
变化到1
,持续时间为0.2
秒。这也是为了使PART_SmallPanel
在加载时以渐变的方式显示出来。 -
第三个
DoubleAnimation
动画将目标元素PART_SmallPanel
的Y
轴缩放属性ScaleTransform.ScaleY
从1
变化到0
,持续时间为0.2
秒。这个动画在10
秒后开始执行,会使PART_SmallPanel
以渐变的方式从可见状态变为不可见状态。 -
第四个
DoubleAnimation
动画将目标元素PART_SmallPanel
的不透明度属性Opacity
从1
变化到0
,持续时间为0.2
秒。同样地,在10秒后开始执行,使PART_SmallPanel
以渐变的方式从可见状态变为不可见状态。
-
-
定义一个名为
Storyboard_Close
的故事板Storyboard
,其中包含了两个DoubleAnimation
动画。-
第一个
DoubleAnimation
动画将目标元素PART_SmallPanel
的Y轴缩放属性(UIElement.RenderTransform).(ScaleTransform.ScaleY)
从1
变化到0
,持续时间为0.2
秒。这会使PART_SmallPanel
以渐变的方式从可见状态变为不可见状态。 -
第二个
DoubleAnimation
动画将目标元素PART_SmallPanel
的不透明度属性(Opacity
)从1
变化到0,持续时间为0
秒。该动画在0.2
秒后开始执行,使PART_SmallPanel
以立即消失的方式从可见状态变为不可见状态。
-
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="clr-namespace:WPFDevelopers.Controls" xmlns:helpers="clr-namespace:WPFDevelopers.Helpers"> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="Basic/ControlBasic.xaml" /> </ResourceDictionary.MergedDictionaries> <Style x:Key="WD.MessageListBoxItem" BasedOn="{StaticResource WD.ControlBasicStyle}" TargetType="{x:Type controls:MessageListBoxItem}"> <Setter Property="FontSize" Value="{DynamicResource WD.NormalFontSize}" /> <Setter Property="Foreground" Value="{DynamicResource WD.RegularTextSolidColorBrush}" /> <Setter Property="Background" Value="{DynamicResource WD.BackgroundSolidColorBrush}" /> <Setter Property="helpers:ListBoxItemExtensions.AutoRemoveOnOpacityZero" Value="True" /> <Setter Property="Width" Value="300" /> <Setter Property="Height" Value="Auto" /> <Setter Property="Padding" Value="10" /> <Setter Property="Margin" Value="4,2" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type controls:MessageListBoxItem}"> <ControlTemplate.Resources> <Storyboard x:Key="Storyboard_Close"> <DoubleAnimation Storyboard.TargetName="PART_SmallPanel" Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)" From="1" To="0" Duration="0:0:0.2" /> <DoubleAnimation BeginTime="0:0:0.2" Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="0:0:0" /> </Storyboard> <Storyboard x:Key="Storyboard_Loaded"> <DoubleAnimation Storyboard.TargetName="PART_SmallPanel" Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)" From="0" To="1" Duration="0:0:0.2" /> <DoubleAnimation Storyboard.TargetName="PART_SmallPanel" Storyboard.TargetProperty="Opacity" From=".1" To="1" Duration="0:0:0.2" /> <DoubleAnimation BeginTime="0:0:10" Storyboard.TargetName="PART_SmallPanel" Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)" From="1" To="0" Duration="0:0:0.2" /> <DoubleAnimation BeginTime="0:0:10" Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="0:0:0.2" /> </Storyboard> </ControlTemplate.Resources> <controls:SmallPanel x:Name="PART_SmallPanel" Margin="{TemplateBinding Margin}" RenderTransformOrigin=".5,0"> <controls:SmallPanel.RenderTransform> <ScaleTransform /> </controls:SmallPanel.RenderTransform> <Border x:Name="PART_Border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{Binding Path=(helpers:ElementHelper.CornerRadius), RelativeSource={RelativeSource TemplatedParent}}" Effect="{StaticResource WD.NormalShadowDepth}" SnapsToDevicePixels="True" UseLayoutRounding="True" /> <Border Padding="{TemplateBinding Padding}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <DockPanel x:Name="PART_DockPanel" Grid.Column="0"> <Path x:Name="PART_Path" Width="15" Height="15" Data="{StaticResource WD.InformationGeometry}" Fill="{DynamicResource WD.PrimaryNormalSolidColorBrush}" Stretch="Fill" /> <TextBlock Grid.Column="1" Margin="5,0,0,0" VerticalAlignment="Center" FontSize="{TemplateBinding FontSize}" Foreground="{TemplateBinding Foreground}" Text="{TemplateBinding Content}" TextWrapping="Wrap" /> </DockPanel> <Button x:Name="PART_CloseButton" Grid.Column="2" Width="30" Height="28" Padding="0" HorizontalAlignment="Right" Style="{StaticResource WD.PathButton}"> <Path Width="10" Height="10" HorizontalAlignment="Center" VerticalAlignment="Center" Data="{DynamicResource WD.WindowCloseGeometry}" Fill="{DynamicResource WD.SecondaryTextSolidColorBrush}" Stretch="Fill" /> </Button> </Grid> </Border> </controls:SmallPanel> <ControlTemplate.Triggers> <Trigger Property="MessageType" Value="Warning"> <Setter TargetName="PART_Path" Property="Data" Value="{StaticResource WD.WarningGeometry}" /> <Setter TargetName="PART_Path" Property="Fill" Value="{StaticResource WD.WarningSolidColorBrush}" /> <Setter Property="Foreground" Value="{StaticResource WD.WarningSolidColorBrush}" /> </Trigger> <Trigger Property="MessageType" Value="Error"> <Setter TargetName="PART_Path" Property="Data" Value="{StaticResource WD.ErrorGeometry}" /> <Setter TargetName="PART_Path" Property="Fill" Value="{StaticResource WD.DangerSolidColorBrush}" /> <Setter Property="Foreground" Value="{StaticResource WD.DangerSolidColorBrush}" /> </Trigger> <Trigger Property="MessageType" Value="Information"> <Setter TargetName="PART_Path" Property="Data" Value="{StaticResource WD.InformationGeometry}" /> <Setter TargetName="PART_Path" Property="Fill" Value="{StaticResource WD.SuccessSolidColorBrush}" /> <Setter Property="Foreground" Value="{StaticResource WD.SuccessSolidColorBrush}" /> </Trigger> <Trigger Property="MessageType" Value="Question"> <Setter TargetName="PART_Path" Property="Data" Value="{StaticResource WD.QuestionGeometry}" /> <Setter TargetName="PART_Path" Property="Fill" Value="{StaticResource WD.NormalSolidColorBrush}" /> <Setter Property="Foreground" Value="{StaticResource WD.NormalSolidColorBrush}" /> </Trigger> <Trigger Property="IsCenter" Value="True"> <Setter TargetName="PART_DockPanel" Property="HorizontalAlignment" Value="Center" /> </Trigger> <EventTrigger RoutedEvent="Button.Click" SourceName="PART_CloseButton"> <BeginStoryboard x:Name="BeginStoryboardClose" Storyboard="{StaticResource Storyboard_Close}" /> </EventTrigger> <EventTrigger RoutedEvent="Loaded"> <BeginStoryboard x:Name="BeginStoryboardLoaded" Storyboard="{StaticResource Storyboard_Loaded}" /> </EventTrigger> <EventTrigger RoutedEvent="Unloaded"> <StopStoryboard BeginStoryboardName="BeginStoryboardLoaded" /> <StopStoryboard BeginStoryboardName="BeginStoryboardClose" /> </EventTrigger> <Trigger Property="Opacity" Value="0"> <Setter Property="Visibility" Value="Collapsed" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> <Style x:Key="WD.MessageListBox" TargetType="{x:Type controls:MessageListBox}"> <Setter Property="BorderThickness" Value="0" /> <Setter Property="Background" Value="Transparent" /> <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Hidden" /> </Style> <Style BasedOn="{StaticResource WD.MessageListBoxItem}" TargetType="{x:Type controls:MessageListBoxItem}" /> <Style BasedOn="{StaticResource WD.MessageListBox}" TargetType="{x:Type controls:MessageListBox}" /> </ResourceDictionary>
6)新增 ListBoxItemExtensions
代码如下:
-
在方法内部,首先将
sender
转换为ListBoxItem
类型,并进行空引用检查。然后,创建一个绑定对象binding
,将其源设置为当前的item
,并将绑定模式设置为单向。 -
接下来,使用
DependencyPropertyDescriptor
从UIElement.OpacityProperty
属性获取一个依赖属性描述符dpd
。通过调用AddValueChanged
方法,将一个值更改事件的处理程序添加到item
上。当item
的不透明度Opacity
小于0.1时,该处理程序将执行以下操作:- 获取父级
ItemsControl
,即包含item
的列表框。 - 如果找到了父级
ItemsControl
,则使用ItemContainerGenerator
从item
获取关联的数据项(selectedItem
)。 - 从父级
ItemsControl
的Items
集合中移除selectedItem
。 - 调用父级
ItemsControl
的Refresh
方法,以刷新列表框的显示。
- 获取父级
using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Data; namespace WPFDevelopers.Helpers { public static class ListBoxItemExtensions { public static readonly DependencyProperty AutoRemoveOnOpacityZeroProperty = DependencyProperty.RegisterAttached("AutoRemoveOnOpacityZero", typeof(bool), typeof(ListBoxItemExtensions), new PropertyMetadata(false, OnAutoRemoveOnOpacityZeroChanged)); public static bool GetAutoRemoveOnOpacityZero(DependencyObject obj) { return (bool)obj.GetValue(AutoRemoveOnOpacityZeroProperty); } public static void SetAutoRemoveOnOpacityZero(DependencyObject obj, bool value) { obj.SetValue(AutoRemoveOnOpacityZeroProperty, value); } private static void OnAutoRemoveOnOpacityZeroChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) { ListBoxItem item = obj as ListBoxItem; if (item != null) { if ((bool)e.NewValue) item.Loaded += Item_Loaded; else item.Loaded -= Item_Loaded; } } private static void Item_Loaded(object sender, RoutedEventArgs e) { var item = sender as ListBoxItem; if (item != null) { var binding = new Binding("Opacity"); binding.Source = item; binding.Mode = BindingMode.OneWay; var dpd = DependencyPropertyDescriptor.FromProperty(UIElement.OpacityProperty, typeof(UIElement)); dpd.AddValueChanged(item, (s, args) => { if (item.Opacity < 0.1) { var parent = ItemsControl.ItemsControlFromItemContainer(item); if (parent != null) { object selectedItem = parent.ItemContainerGenerator.ItemFromContainer(item); parent.Items.Remove(selectedItem); parent.Items.Refresh(); } } }); } } } }
7)新增 示例
代码如下:
<wd:Window x:Class="MessageSample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:MessageSample" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:wd="https://github.com/WPFDevelopersOrg/WPFDevelopers" Title="WPFDevelopers - Message" Width="800" Height="450" mc:Ignorable="d"> <Grid> <StackPanel Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Bottom" Orientation="Horizontal"> <Button Click="AddButton_Click" Content="Info Message" Style="{StaticResource WD.SuccessPrimaryButton}" Tag="Info" /> <Button Click="AddButton_Click" Content="Error Message" Style="{StaticResource WD.DangerPrimaryButton}" Tag="Error" /> <Button Click="AddButton_Click" Content="Warning Message" Style="{StaticResource WD.WarningPrimaryButton}" Tag="Warning" /> <Button Click="AddButton_Click" Content="Question Message" Style="{StaticResource WD.PrimaryButton}" Tag="Question" /> <Button Click="AddButton_Click" Content="Long Message" Style="{StaticResource WD.SuccessPrimaryButton}" Tag="Long" /> </StackPanel> </Grid> </wd:Window>
8) 示例
代码如下:
private void AddButton_Click(object sender, RoutedEventArgs e) { var btn = sender as Button; switch (btn.Tag) { case "Info": Message.Push(App.Current.MainWindow, "This is a info message", MessageBoxImage.Information); break; case "Error": Message.Push("This is a error message", MessageBoxImage.Error, true); break; case "Warning": Message.Push("This is a warning message", MessageBoxImage.Warning, true); break; case "Question": Message.Push("This is a question message", MessageBoxImage.Question); break; default: Message.Push("这是一条很长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长长消息", MessageBoxImage.Information); break; } }