WPF 實作 Message 消息提醒控件
控 件:Message
作 者:WPFDevelopersOrg - 驚鏵
原文連結[1]:https://github.com/WPFDevelopersOrg/WPFDevelopers
- 架構使用
;.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
-
方法用于建立并傳回一個新的容器對象,用于承載清單框中的項。在這裡,它建立并傳回一個GetContainerForItemOverride
的執行個體作為容器對象。MessageListBoxItem
-
方法用于确定給定的項是否應該作為其自己的容器。在這裡,它檢查傳入的IsItemItsOwnContainerOverride
對象是否是item
的執行個體。如果是,則傳回MessageListBoxItem
,表示該項是其自己的容器;否則,傳回true
。false
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
-
屬性傳回裝飾器的可視化子元素數量。VisualChildrenCount
-
方法用于調整裝飾器的布局。在這裡,根據裝飾元素的寬度和子元素的期望大小,計算出子元素的水準位置,并将其排列在裝飾器的頂部。ArrangeOverride
-
方法用于擷取指定索引處的可視化子元素。在這裡,如果索引為0且子元素不為 ,則傳回子元素;否則,調用基類的方法傳回對應索引的可視化子元素。GetVisualChild
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 == )
{
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 == )
{
RemoveVisualChild(_child);
_child = value;
return;
}
AddVisualChild(value);
_child = value;
}
}
protected override int VisualChildrenCount
{
get
{
return _child != ? 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 != ) 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 = , string message = , MessageBoxImage type = MessageBoxImage.Information, bool center = false)
{
try
{
if (messageAdorner != )
{
messageAdorner.Push(message, type, center);
return;
}
if (owner == )
owner = ControlsHelper.GetDefaultWindow();
var layer = ControlsHelper.GetAdornerLayer(owner);
if (layer == )
throw new Exception("not AdornerLayer is ");
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
,持續時間為1
秒。這會使0.2
在加載時以漸變的方式顯示出來。PART_SmallPanel
- 第二個
動畫将目标元素DoubleAnimation
的不透明度屬性PART_SmallPanel
從Opacity
變化到0.1
,持續時間為1
秒。這也是為了使0.2
在加載時以漸變的方式顯示出來。PART_SmallPanel
- 第三個
動畫将目标元素DoubleAnimation
的PART_SmallPanel
軸縮放屬性Y
從ScaleTransform.ScaleY
變化到 ,持續時間為1
秒。這個動畫在0.2
秒後開始執行,會使10
以漸變的方式從可見狀态變為不可見狀态。PART_SmallPanel
- 第四個
動畫将目标元素DoubleAnimation
的不透明度屬性PART_SmallPanel
從Opacity
變化到 ,持續時間為1
秒。同樣地,在10秒後開始執行,使0.2
以漸變的方式從可見狀态變為不可見狀态。PART_SmallPanel
- 定義一個名為
的故事闆Storyboard_Close
,其中包含了兩個Storyboard
動畫。DoubleAnimation
- 第一個
動畫将目标元素DoubleAnimation
的Y軸縮放屬性PART_SmallPanel
從(UIElement.RenderTransform).(ScaleTransform.ScaleY)
變化到 ,持續時間為1
秒。這會使0.2
以漸變的方式從可見狀态變為不可見狀态。PART_SmallPanel
- 第二個
動畫将目标元素DoubleAnimation
的不透明度屬性(PART_SmallPanel
)從Opacity
變化到0,持續時間為 秒。該動畫在1
秒後開始執行,使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
小于0.1時,該處理程式将執行以下操作:Opacity
- 擷取父級
,即包含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 != )
{
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 != )
{
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 != )
{
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;
}
}
碼雲[2]
參考資料
[1]
原文連結: https://github.com/WPFDevelopersOrg/WPFDevelopers
[2]
碼雲: https://github.com/WPFDevelopersOrg/WPFDevelopers