自定义 ListBox,实现 WPF 中平滑的动画导航栏





5.00/5 (6投票s)
该控件的设计和动画专为移动端优化,但可使用WPF提供的ListBox和Animation技术优雅且结构化地实现。
引言
WPF应用程序通常倾向于采用编程方法,通过菜单配置连接多个屏幕,并以统一的方式呈现。这种技术,通常被称为菜单或导航,是WPF的核心实现之一。它也与项目的架构(设计)直接相关,因此对其实现给予更多关注可以积极地影响项目质量。
该控件具有专为移动端设计和优化的动画,但可以使用WPF提供的ListBox
和Animation
技术优雅且结构化地实现。此外,在AvaloniaUI、Uno、OpenSilver、MAUI等跨平台环境中也可以实现类似的功能,这使得该项目能够跨各种平台进行研究和应用。
目标也是广泛推广WPF实现的灵活性和卓越性,并分享相关技术。通过这个项目,我们希望您能深刻体验WPF的魅力。
通过教程视频和CodeProject文章学习
该控件附带一个大约30分钟的教程视频,提供英语和中文音频,配有韩文字幕。制作教程视频比想象中需要更多的时间和精力,虽然很具挑战性,但您的鼓励和支持是巨大的动力。
您可以通过以下平台了解更多信息
- Youtube: 英语(韩语)教程视频
- Bilibili: 中文教程视频
- GitHub: 导航栏
此外,请查看其他教程视频,如ThemeSwitch、Lol-PlayButton等。
设计和结构理念
这种控件风格广泛用于Web或移动导航配置。因此,它通常使用iOS、Android或HTML/CSS技术实现。使用CSS/HTML和JavaScript实现相对容易构建结构和动画功能。相比之下,WPF通过XAML在设计、事件和动画实现方面可能感觉比较复杂。因此,该控件实现的关键在于充分利用WPF的特性,并提供一种高级实现方法,让用户感受到WPF的结构优势。
通过重构,我们在源代码质量上投入了大量精力。该项目最小化/优化了XAML的层级结构,并通过CustomControl
促进XAML和后台代码之间的交互来增强代码质量。该控件不仅仅是提供基本功能;它旨在通过其结构理念传达技术灵感并鼓励多样化应用。
项目概述
MagicBar.cs
该项目的核心控件MagicBar
是一个继承自ListBox
控件的CustomControl
。在大多数开发场景中,UserControl
是常见的选择,但对于涉及复杂功能、动画和重复元素的此情况,将其划分为更小的Control
(CustomControl
)单元来实现更为有效。
如果您不熟悉CustomControl
,请阅读以下内容
引用
CustomControl
方法本身在技术上具有挑战性,并且在概念上与Windows Forms等传统桌面方法不同,因此不易上手。此外,寻找指导性参考资料也很困难。然而,这是提升WPF技术技能的重要过程。我们鼓励您借此机会以开放的心态迎接CustomControl
实现的挑战。
Generic.xaml
CustomControl
的特点是其XAML设计区域的分离和管理。因此,它不提供XAML区域和控件(Class
)之间的直接交互。这些区域之间的交互通过其他间接方法支持。第一种方法是通过OnApplyTemplate
时机探索Template
区域。第二种方法通过DependencyProperty
声明扩展绑定。
这种结构特点实现了设计和代码的完美分离,提高了代码的可重用性和可扩展性,并深入理解了WPF的传统结构。WPF中使用的所有控件都遵循相同的方法。为验证这一点,您可以直接查看GitHub上可用的开源dotnet/WPF存储库。
XAML配置
几何图形(Geometry)简介
Geometry是WPF提供的设计元素之一,用于矢量图形设计。传统上,开发方法偏爱PNG或JPEG等位图图像,但近年来矢量图形设计越来越受欢迎。这种变化可以归因于计算机性能的提升、显示器分辨率的发展以及设计趋势的变化。因此,Geometry元素在此控件中起着重要作用。后面部分实现的Circle的过程将更详细地解释。
动画元素和ItemsPresenter的分离
MagicBar
继承自ListBox
控件,并独特地使用了ItemsControl
功能提供的ItemsPresenter
元素。然而,ItemsPresenter
内部的子元素之间无法进行交互,这意味着子项之间的Animation
操作也无法继续。
ListBoxItem
的行为由ItemsPresenter
元素中通过ItemsPanelTemplate
指定的Panel
类型决定。因此,Panel布局的选择对ListBoxItem
的行为有显著影响。对于StackPanel
,添加的子元素的顺序决定了它们的位置。对于Grid
,放置由Row
/Column
设置决定。
因此,在结构上链接子元素之间的动画操作是不可能的。
引用但是,也有例外。对于
Canvas
,可以通过坐标的概念实现与Animation
的交互,但这需要对所有控件进行复杂的计算和精确的实现。然而,存在更好的实现方法,因此在此上下文中省略了Canvas
控件内容。
ListBox ControlTemplate层级结构
通常,在实现ListBox
控件时,更侧重于子元素ListBoxItem
。然而,对于该控件,关键功能——Circle
结构——需要放置在ItemsPresenter
元素的外部区域。因此,在ListBox
控件中形成复杂的Template
至关重要。
ControlTemplate
的层级结构如下
引用为清晰起见,以下是简化表示,与实际源代码内容不同。
Circle
部分可在文本中轻松找到,标记为“PART_Circle
”。
<ControlTemplate TargetType="{x:Type ListBox}">
<Grid>
<Circle/>
<ItemsPresenter/>
</Grid>
</ControlTemplate>
如上所示,关键在于将ItemsPresenter
和Circle
置于同一层级。这种排列允许Circle
元素的Animation
范围看起来像是自由地跨越ItemsPresenter
的子元素。此外,重要的是将ItemsPresenter
元素放置在Circle
的前面,以便ListBoxItem
元素的图标和文本不会在视觉上覆盖Circle
。
在讨论了理论之后,现在让我们深入研究实际源代码以进行详细比较。
引用区域 x:Name="PART_Circle" 对应于 Circle。
<Style TargetType="{x:Type local:MagicBar}">
<Setter Property="ItemContainerStyle" Value="{StaticResource MagicBarItem}"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="UseLayoutRounding" Value="True"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Width" Value="440"/>
<Setter Property="Height" Value="120"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MagicBar}">
<Grid Background="{TemplateBinding Background}">
<Grid.Clip>
<RectangleGeometry Rect="0 0 440 120"/>
</Grid.Clip>
<Border Style="{StaticResource Bar}"/>
<Canvas Margin="20 0 20 0">
<Grid x:Name="PART_Circle" Style="{StaticResource Circle}">
<Path Style="{StaticResource Arc}"/>
<Ellipse Fill="#222222"/>
<Ellipse Fill="CadetBlue" Margin="6"/>
</Grid>
</Canvas>
<ItemsPresenter Margin="20 40 20 0"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<UniformGrid Columns="5"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Style>
ListBoxItem模板配置
与ListBox
控件的Template
不同,ListBoxItem
的配置相对简单。另外,由于它与Circle Animation
元素无关,因此仅包含菜单项的图标和文本。
<Style TargetType="{x:Type ListBoxItem}" x:Key="MagicBarItem">
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Grid Background="{TemplateBinding Background}">
<james:JamesIcon x:Name="icon" Style="{StaticResource Icon}"/>
<TextBlock x:Name="name" Style="{StaticResource Name}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
此外,还包括改变图标和文本位置和颜色的Animation
。如前所述,在这个ListBoxItem
元素中不需要实现特殊功能。
引用
JamesIcon
是通过NuGet可用的Jamesnet.Wpf
库提供的控件,该库提供各种图标。要替换它,您可以使用Path
控件直接实现Geometry
设计,或使用带有透明(Transparent
)背景的图像。
JamesIcon样式
JamesIcon
内部包含一个Path
控件,并提供各种DependencyProperty
属性,允许从外部进行灵活的设计定义。关键属性包括Icon
、Width
、Height
、Fill
等。
引用基于矢量的
Geometry
图标提供一致的设计,这是提高控件质量的一种方法。因此,仔细研究这些差异是值得的。
<Style TargetType="{x:Type james:JamesIcon}" x:Key="Icon">
<Setter Property="Icon" Value="{TemplateBinding Tag}"/>
<Setter Property="Width" Value="40"/>
<Setter Property="Height" Value="40"/>
<Setter Property="Fill" Value="#44333333"/>
</Style>
RelativeSource绑定
由于JamesIcon
样式与Template
分离,因此无法使用如下所示的TemplateBinding
标签绑定
// Binding method that's not possible</code>
<Setter Property="Icon" Value="{TemplateBinding Tag}"/>
因此,使用RelativeSource
绑定来搜索父元素ListBoxItem
,并绑定其Tag
属性,如下所示
<... Value="{Binding RelativeSource={RelativeSource AncestorType=ListBoxItem}, Path=Tag}"/>
使用RelativeSource
绑定,图标在ListBoxItem
区域内定义的原始TemplateBinding
可以单独移动到JamesIcon
区域。这种方法允许每个组件(JamesIcon
)拥有自己的定义和样式,使代码更具模块化、更易于维护和重用。将绑定和样式分离到各自的区域可以理清整体代码结构,使其更易于理解和修改。此外,这种分离提供了更大的灵活性,允许调整各个组件的样式和行为而不影响其他组件。
2. Microsoft Blend:几何图形设计
Microsoft Blend,Expression Blend的后续版本,尽管某些功能有所减少,但仍然保留其名称。此程序可以在Visual Studio的安装过程中添加。如果您找不到此程序,可以通过Visual Studio Installer添加。
尽管Microsoft Blend与Visual Studio共享大部分功能,但它包含一些专门针对设计的附加功能。其中与Geometry
相关的功能,部分类似于Adobe Illustrator中的功能。
在WPF开发中使用Microsoft Blend不是必需的,也不是专为设计师准备的。相反,它为开发人员提供了一个有价值的工具,让他们无需经过大量设计培训即可创建专业且吸引人的设计元素。
引用然而,Microsoft Blend提供的大部分设计功能可以在Figma和Illustrator等环境中更强大地利用,因此没有迫切需要学习它。但是,一些与Geometry相关的功能易于使用,无需单独培训,因此值得仔细研究。
Circle(🔵)设计分析
MagicBar
控件中的Circle
是该项目的一个关键点,在菜单更改时在视觉上起作用。它包含平滑的Animation
,增加了现代和时尚的设计元素。
Circle
元素不一定必须基于Geometry
实现。使用图像可能是更简单的方法。然而,在质量方面,Geometry
设计越来越受欢迎,因为它们可以更精细地处理由于尺寸变化引起的分辨率变化。
引用如下面的图像所示,
Geometry
的一个特点是您可以根据需要调整大小而不损失清晰度。
仔细观察Circle
设计,您会发现它通过叠加一个黑色圆圈和一个绿色圆圈来营造空间感。此外,将两侧的线条圆化使其自然地融入MagicBar
区域。这不仅看起来视觉流畅,而且在动画时也显得更加优雅。然而,实现这种弧形可能很困难,并且在实际实现中经常被放弃。
但这就是Microsoft Blend在轻松创建这些特殊形状方面发挥作用的地方。
绘制方法
设计过程包括绘制一个底部带有凸弧的大圆圈,然后在圆圈两侧添加相同高度的小圆圈。通过调整大圆圈的直径,确保大圆圈和小圆圈完美相交。
接下来,使用merge
函数剪切大圆圈中不需要的部分,并使用subtract
函数移除小圆圈中不需要的部分,仅留下交汇处的弧形。最后,添加一个矩形并移除不需要的部分,以创建独特而自然的弧形。
这种实现设计元素的方法不仅展示了如何使用Microsoft Blend创建复杂的图形,还提供了思考和解决设计问题的新视角。这种方法使圆圈不仅美观,而且在技术上具有创新性,提高了质量。
3. 动画:ListBoxItem
包含图标和文本的ListBoxItem
区域的动画行为相对简单。当IsSelected
设置为true
时,它会将组件向上移动并调整透明度。
引用请通过下图仔细观察动画路径和效果
如上图所示,每次ListBox
控件的IsSelected
值发生变化时,都会触发动画。此外,由于图标和文本的移动不会超出ListBoxItem
区域,因此最好直接在XAML中实现一个static Storyboard
元素。
引用这可以使用
Trigger
或VisualStateManager
模块来控制。对于该控件,采用了简单的Trigger
模块方法来仅处理IsSelected
操作。
Storyboard
对于ListBoxItem
区域的动画行为,需要为IsSelected
为true
和false
的情况准备场景。
<Storyboard x:Key="Selected">
<james:ThickItem Mode="CubicEaseInOut" TargetName="icon"
Duration="0:0:0.5" Property="Margin" To="0 -80 0 0"/>
<james:ThickItem Mode="CubicEaseInOut" TargetName="name"
Duration="0:0:0.5" Property="Margin" To="0 45 0 0"/>
<james:ColorItem Mode="CubicEaseInOut" TargetName="icon"
Duration="0:0:0.5" Property="Fill.Color" To="#333333"/>
<james:ColorItem Mode="CubicEaseInOut" TargetName="name"
Duration="0:0:0.5" Property="Foreground.Color" To="#333333"/>
</Storyboard>
<Storyboard x:Key="UnSelected">
<james:ThickItem Mode="CubicEaseInOut" TargetName="icon"
Duration="0:0:0.5" Property="Margin" To="0 0 0 0"/>
<james:ThickItem Mode="CubicEaseInOut" TargetName="name"
Duration="0:0:0.5" Property="Margin" To="0 60 0 0"/>
<james:ColorItem Mode="CubicEaseInOut" TargetName="icon"
Duration="0:0:0.5" Property="Fill.Color" To="#44333333"/>
<james:ColorItem Mode="CubicEaseInOut" TargetName="name"
Duration="0:0:0.5" Property="Foreground.Color" To="#00000000"/>
</Storyboard>
这里的关键是在'Selected
'中指定移动路径,在'UnSelected
'中指定返回路径。
触发器
最后,通过使用Trigger
声明BeginStoryboard
来激活相应的(Selected
/UnSelected
)Storyboard
,从而完成ListBoxItem
区域动画的实现。
引用与典型的
Trigger
属性更改不同,动画也需要返回场景。
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Trigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource Selected}"/>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard Storyboard="{StaticResource UnSelected}"/>
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
配置ListBoxItem
区域动画的方法相对简单。然而,实现接下来介绍的Circle
组件的移动需要更复杂的计算才能实现动态行为。
4. Circle组件的移动
现在是时候实现Circle
组件移动的动画了。下面是一个展示Circle
动态移动的视频。
Circle
组件的移动必须根据点击位置进行精确计算,因此无法在XAML中实现,需要在C#代码中动态处理。因此,需要一种连接XAML和Code Behind的方法。
OnApplyTemplate
此方法用于检索MagicBar
控件内部的Circle
区域。它在控件和模板的连接点处被内部调用。因此,它通过覆盖在MagicBar
类中实现。
然后,使用GetTemplateChild
方法搜索名为'PART_Circle
'的circle
元素。当与模板连接时,这个Grid
将成为显示动画效果的目标元素。
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
Grid grid = (Grid)GetTemplateChild("PART_Circle");
InitStoryboard(grid);
}
InitStoryboard
此方法初始化动画。首先创建ValueItem (_vi)
和Storyboard (_sb)
的实例。在ValueItem
中设置的动画效果是QuinticEaseInOut
,它在动画的开始和结束时放缓,使其看起来平滑自然。
Circle
的移动路径指定为Canvas.LeftProperty
,这意味着它会改变target
元素的水平位置。动画的持续时间设置为0.5秒。最后,动画目标设置为Circle
组件(Grid
),并将定义的动画添加到storyboard
中。
private void InitStoryboard(Grid circle)
{
_vi = new();
_sb = new();
_vi.Mode = EasingFunctionBaseMode.QuinticEaseInOut;
_vi.Property = new PropertyPath(Canvas.LeftProperty);
_vi.Duration = new Duration(new TimeSpan(0, 0, 0, 0, 500));
Storyboard.SetTarget(_vi, circle);
Storyboard.SetTargetProperty(_vi, _vi.Property);
_sb.Children.Add(_vi);
}
OnSelectionChanged
Circle
组件移动的场景现在已经实现。在MagicBar
类中,实现了OnSelectionChanged
事件方法来处理'PART_Circle
'(Grid
)元素并执行(Begin
)storyboard
。
引用
MagicBar
控件作为源自ListBox
的CustomControl
,具有灵活实现覆盖功能的优势。
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
_vi.To = SelectedIndex * 80;
_sb.Begin();
}
在此方法中,每当选定的菜单更改时,都会实现根据SelectedIndex
动态计算和更改To
值的逻辑。
5. 结论:检查CustomControl的完整源代码
最后,是时候看一下MagicBar
控件的XAML/Csharp代码的完整结构了。这是一个机会,可以看看控件如何在CustomControl
结构中以优雅而简洁的方式实现。
Generic.xaml
尽管实现了各种功能,但您可以看到XAML的结构已最大限度地简化。特别是,MagicBar
中包含的ControlTemplate
结构简化了复杂的层级结构,便于查看。此外,即使是像Storyboard
、Geometry
、TextBlock
和JamesIcon
这样的小元素,也以一种规则和系统的方式组织。
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:james="https://jamesnet.dev/xaml/presentation"
xmlns:local="clr-namespace:NavigationBar">
<Storyboard x:Key="Selected">
<james:ThickItem Mode="CubicEaseInOut" TargetName="icon"
Duration="0:0:0.5" Property="Margin" To="0 -80 0 0"/>
<james:ThickItem Mode="CubicEaseInOut" TargetName="name"
Duration="0:0:0.5" Property="Margin" To="0 45 0 0"/>
<james:ColorItem Mode="CubicEaseInOut" TargetName="icon"
Duration="0:0:0.5" Property="Fill.Color" To="#333333"/>
<james:ColorItem Mode="CubicEaseInOut" TargetName="name"
Duration="0:0:0.5" Property="Foreground.Color" To="#333333"/>
</Storyboard>
<Storyboard x:Key="UnSelected">
<james:ThickItem Mode="CubicEaseInOut" TargetName="icon"
Duration="0:0:0.5" Property="Margin" To="0 0 0 0"/>
<james:ThickItem Mode="CubicEaseInOut" TargetName="name"
Duration="0:0:0.5" Property="Margin" To="0 60 0 0"/>
<james:ColorItem Mode="CubicEaseInOut" TargetName="icon"
Duration="0:0:0.5" Property="Fill.Color" To="#44333333"/>
<james:ColorItem Mode="CubicEaseInOut" TargetName="name"
Duration="0:0:0.5" Property="Foreground.Color" To="#00000000"/>
</Storyboard>
<Style TargetType="{x:Type james:JamesIcon}" x:Key="Icon">
<Setter Property="Icon"
Value="{Binding RelativeSource={RelativeSource AncestorType=ListBoxItem},
Path=Tag}"/>
<Setter Property="Width" Value="40"/>
<Setter Property="Height" Value="40"/>
<Setter Property="Fill" Value="#44333333"/>
</Style>
<Style TargetType="{x:Type TextBlock}" x:Key="Name">
<Setter Property="Text"
Value="{Binding RelativeSource={RelativeSource AncestorType=ListBoxItem},
Path=Content}"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Foreground" Value="#00000000"/>
<Setter Property="Margin" Value="0 60 0 0"/>
</Style>
<Style TargetType="{x:Type ListBoxItem}" x:Key="MagicBarItem">
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Grid Background="{TemplateBinding Background}">
<james:JamesIcon x:Name="icon" Style="{StaticResource Icon}"/>
<TextBlock x:Name="name" Style="{StaticResource Name}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Trigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource Selected}"/>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard Storyboard="{StaticResource UnSelected}"/>
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Geometry x:Key="ArcData">
M0,0 L100,0 C95.167503,0 91.135628,3.4278221 90.203163,7.9846497 L90.152122,
8.2704506 89.963921,9.1416779 C85.813438,27.384438 69.496498,41 50,41 30.5035,
41 14.186564,27.384438 10.036079,9.1416779 L9.8478823,8.2704926 9.7968359,
7.9846497 C8.8643732,3.4278221 4.8324914,0 0,0 z
</Geometry>
<Style TargetType="{x:Type Path}" x:Key="Arc">
<Setter Property="Data" Value="{StaticResource ArcData}"/>
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="100"/>
<Setter Property="Fill" Value="#222222"/>
<Setter Property="Margin" Value="-10 40 -10 -1"/>
</Style>
<Style TargetType="{x:Type Border}" x:Key="Bar">
<Setter Property="Background" Value="#DDDDDD"/>
<Setter Property="Margin" Value="0 40 0 0"/>
<Setter Property="CornerRadius" Value="10"/>
</Style>
<Style TargetType="{x:Type Grid}" x:Key="Circle">
<Setter Property="Width" Value="80"/>
<Setter Property="Height" Value="80"/>
<Setter Property="Canvas.Left" Value="-100"/>
</Style>
<Style TargetType="{x:Type local:MagicBar}">
<Setter Property="ItemContainerStyle" Value="{StaticResource MagicBarItem}"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="UseLayoutRounding" Value="True"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Width" Value="440"/>
<Setter Property="Height" Value="120"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MagicBar}">
<Grid Background="{TemplateBinding Background}">
<Grid.Clip>
<RectangleGeometry Rect="0 0 440 120"/>
</Grid.Clip>
<Border Style="{StaticResource Bar}"/>
<Canvas Margin="20 0 20 0">
<Grid x:Name="PART_Circle" Style="{StaticResource Circle}">
<Path Style="{StaticResource Arc}"/>
<Ellipse Fill="#222222"/>
<Ellipse Fill="CadetBlue" Margin="6"/>
</Grid>
</Canvas>
<ItemsPresenter Margin="20 40 20 0"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<UniformGrid Columns="5"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
MagicBar.cs
通过OnApplyTemplate
定位分离的ControlTemplate
元素的过程是一项非常重要且基础的任务,堪称WPF的标志。每当菜单更改时,找到指定的PART_Circle
对象(Grid
)并动态组合和激活Circle
的移动(Move
)动画,生动地展示了WPF的活力和动态能力。
using Jamesnet.Wpf.Animation;
using Jamesnet.Wpf.Controls;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace NavigationBar
{
public class MagicBar : ListBox
{
private ValueItem _vi;
private Storyboard _sb;
static MagicBar()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MagicBar),
new FrameworkPropertyMetadata(typeof(MagicBar)));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
Grid grid = (Grid)GetTemplateChild("PART_Circle");
InitStoryboard(grid);
}
private void InitStoryboard(Grid circle)
{
_vi = new();
_sb = new();
_vi.Mode = EasingFunctionBaseMode.QuinticEaseInOut;
_vi.Property = new PropertyPath(Canvas.LeftProperty);
_vi.Duration = new Duration(new TimeSpan(0, 0, 0, 0, 500));
Storyboard.SetTarget(_vi, circle);
Storyboard.SetTargetProperty(_vi, _vi.Property);
_sb.Children.Add(_vi);
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
_vi.To = SelectedIndex * 80;
_sb.Begin();
}
}
}
因此,通过在CustomControl
方法中以控件级别实现通常通过UserControl
处理的功能,我们可以实现更高级、更有效的模块化。
至此,我结束了主要功能的解释。有关该控件的详细信息可在GitHub源代码中免费获取。此外,YouTube和Bilibili分别提供英语和中文的深度教程。我期待看到该控件在基于XAML的平台上的各种研究和应用。
历史
- 2024年1月10日:初始版本