**WPF 實現 Message 消息提醒控制項** > 控 件:Message > > 作 者:WPFDevelopersOrg - **驚鏵** > >[原文鏈接](https://github.com/WPFDevelopersOrg/WPFDevelopers "原文鏈接"):https:// ...
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;
}
}