65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2024年1月10日

CPOL

13分钟阅读

viewsIcon

4436

该控件的设计和动画专为移动端优化,但可使用WPF提供的ListBox和Animation技术优雅且结构化地实现。

引言

WPF应用程序通常倾向于采用编程方法,通过菜单配置连接多个屏幕,并以统一的方式呈现。这种技术,通常被称为菜单或导航,是WPF的核心实现之一。它也与项目的架构(设计)直接相关,因此对其实现给予更多关注可以积极地影响项目质量。

该控件具有专为移动端设计和优化的动画,但可以使用WPF提供的ListBoxAnimation技术优雅且结构化地实现。此外,在AvaloniaUI、Uno、OpenSilver、MAUI等跨平台环境中也可以实现类似的功能,这使得该项目能够跨各种平台进行研究和应用。

目标也是广泛推广WPF实现的灵活性和卓越性,并分享相关技术。通过这个项目,我们希望您能深刻体验WPF的魅力。

通过教程视频和CodeProject文章学习

该控件附带一个大约30分钟的教程视频,提供英语和中文音频,配有韩文字幕。制作教程视频比想象中需要更多的时间和精力,虽然很具挑战性,但您的鼓励和支持是巨大的动力。

您可以通过以下平台了解更多信息

此外,请查看其他教程视频,如ThemeSwitch、Lol-PlayButton等。

设计和结构理念

这种控件风格广泛用于Web或移动导航配置。因此,它通常使用iOS、Android或HTML/CSS技术实现。使用CSS/HTML和JavaScript实现相对容易构建结构和动画功能。相比之下,WPF通过XAML在设计、事件和动画实现方面可能感觉比较复杂。因此,该控件实现的关​​键在于充分利用WPF的特性,并提供一种高级实现方法,让用户感受到WPF的结构优势。

通过重构,我们在源代码质量上投入了大量精力。该项目最小化/优化了XAML的层级结构,并通过CustomControl促进XAML和后台代码之间的交互来增强代码质量。该控件不仅仅是提供基本功能;它旨在通过其结构理念传达技术灵感并鼓励多样化应用。

项目概述

MagicBar.cs

该项目的核心控件MagicBar是一个继承自ListBox控件的CustomControl。在大多数开发场景中,UserControl是常见的选择,但对于涉及复杂功能、动画和重复元素的此情况,将其划分为更小的ControlCustomControl)单元来实现更为有效。

如果您不熟悉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>

如上所示,关键在于将ItemsPresenterCircle置于同一层级。这种排列允许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属性,允许从外部进行灵活的设计定义。关键属性包括IconWidthHeightFill等。

引用

基于矢量的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元素。

引用

这可以使用TriggerVisualStateManager模块来控制。对于该控件,采用了简单的Trigger模块方法来仅处理IsSelected操作。

Storyboard

对于ListBoxItem区域的动画行为,需要为IsSelectedtruefalse的情况准备场景。

<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/UnSelectedStoryboard,从而完成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)元素并执行(Beginstoryboard

引用

MagicBar控件作为源自ListBoxCustomControl,具有灵活实现覆盖功能的优势。

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结构简化了复杂的层级结构,便于查看。此外,即使是像StoryboardGeometryTextBlockJamesIcon这样的小元素,也以一种规则和系统的方式组织。

<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日:初始版本
© . All rights reserved.