WPF教学视频详解|WPF NavigationBar 如此丝滑的动画竟然这么简单?!GitHub源代码 |编程原视频已上传

-这篇文章是对 WPF《 Magic NavigationBar》教程视频的技术回顾。
-该控件编程的全过程教学视频中均已包含,视频时长为40分钟。
-BiliBili原视频请点击这里
-GitHub源代码 vickyqu115/navigationbar

在这里插入图片描述

Magic NavigationBar 控件介绍

WPF 应用程序传统上通过菜单构成将多个界面连接并集成到一个程序中。因此,菜单或称为导航的技术是 WPF 的核心实现之一。由于这与项目的架构(设计)直接相关,如果更仔细地实现这一技术,也可以预期项目的质量会得到显著提升。

此控件虽然在移动设备上以特殊设计和动画为特点,但如果在 WPF 中使用 ListBox 和动画技术,也可以实现结构优良且优雅的效果。此外,像 AvaloniaUI、Uno、OpenSilver、MAUI 等跨平台工具也可以用类似方式实现,因此希望这一项目能够在各种平台上得到研究和应用。

同时,推广 WPF 的灵活性和优越性,分享技术也是此项目的目的之一。通过这个项目,希望大家能深入体验 WPF 的魅力。

提供教程视频和 CodeProject 文章的学习资源

该控件编程的全过程教学视频中均已包含,视频时长为40分钟。的教程视频,配有英语和中文语音,并支持韩语字幕。制作教程视频需要花费大量时间和精力,虽然遇到了不少困难,但大家的支持和鼓励成为了我继续努力的动力。

可以通过以下平台进行进一步学习:

此外,还有 ThemeSwitch、Lol-PlayButton 等教程视频,也欢迎一同观看。

设计和结构的理念

在这里插入图片描述

该控件的实现方式是当前在网页和移动设备中广泛使用的一种导航结构。因此,通过 IOS、Android 或 HTML/CSS 技术实现这种结构在周围也很常见。通过 CSS/HTML 和 Javascript 技术实现时,结构和动画功能相对容易实现。而在 WPF 中,通过 XAML 从设计到事件和动画的实现则相对复杂。因此,此控件的实现核心是充分利用 WPF 的特点,提供结构优秀且能够充分展现 WPF 强大优势的高级实现方法。

该项目通过代码重构(Refactoring)极大地关注了质量。通过最小化/优化分层 XAML 结构,并通过利用 CustomControl 实现 XAML 和后台代码(Behind code)之间的交互,来提高代码质量。因此,该项目不仅仅提供简单功能的控件,还致力于传递技术灵感和广泛应用的结构性理念。

项目概述

MagicBar.cs

本项目的核心控件 MagicBar 是继承自 ListBox 的 CustomControl。在大多数开发场景中,选择 UserControl 是常见的做法,但对于包含复杂功能和动画以及重复元素的功能,选择比 UserControl 更小规模的 Control(CustomControl)单元实现更为有效。

如果尚未准备好使用 CustomControl,请仔细阅读以下内容:

CustomControl 的方式在技术上有一定难度,与 Windows Forms 环境等传统桌面方式在概念上有很大不同,因此初学时会有些困难。此外,寻找参考资料也较为困难。然而,为了提升 WPF 技术水平,这是一条必须走的重要道路。希望大家以开放的心态挑战 CustomControl 的实现方法。

Generic.xaml

CustomControl 将 XAML 设计区域分离管理,这是一大特点。因此,XAML 区域和控件(Class)之间不提供直接交互。而是通过另外的间接方式来支持这两者之间的交互。第一种方法是通过 OnApplyTemplate 时间点来探索 Template 区域。第二种方法是通过声明 DependencyProperty 扩展绑定。

通过这种结构特性,设计和代码可以完全分离,从而提高代码的可重用性和扩展性,并能深入理解 WPF 的传统结构。我们使用的所有 WPF 控件也采用相同方式。可以通过免费开放的 dotnet/wpf 仓库直接查看这些控件的实现方式。

XAML 结构

Geometry 介绍

Geometry 是 WPF 提供的设计元素之一,用于实现基于矢量的设计。在过去的传统开发方式中,更喜欢使用 png、jpeg 等位图图像,而现在更倾向于使用基于矢量的设计。这是由于计算机性能提升、显示器分辨率发展以及设计趋势变化所致。因此,此控件中也大量使用了 Geometry 元素。在后期的 Circle 实现过程中会详细介绍。

动画元素和 ItemsPresenter 分离

MagicBar 继承自 ListBox 控件,使用 ItemsControl 特性提供的 ItemsPresenter 元素。但是,ItemsPresenter 元素中的子项之间无法相互交互,这意味着子项之间的动画动作也是不可能的。

ItemsPresenter 元素的行为由 ItemsPanelTemplate 指定的 Panel 类型决定。例如,StackPanel 会通过 Children 集合中的顺序来确定子元素的位置,而 Grid 则通过 Row/Column 设置来决定布局。因此,无法实现子元素之间的动画动作。

但也有例外情况。例如 Canvas 使用坐标概念,可以通过坐标实现动画的交互,但需要处理所有控件,因此需要复杂的计算和精细的实现。不过,由于有更好的实现方法,此处不讨论 Canvas 控件的内容。

ListBox ControlTemplate 层次结构

通常实现 ListBox 控件时,更多地利用其子项 ListBoxItem 控件,但此控件的核心功能 Circle 结构需要位于 ItemsPresenter 元素的区域之外,因此在 ListBox 控件中构建复杂的 Template 是关键。

因此,ControlTemplate 的层次结构如下:

<ControlTemplate TargetType="{x:Type ListBox}">
    <Grid>
        <Circle/>
    	<ItemsPresenter/>
    </Grid>
</ControlTemplate>

如上所示,ItemsPresenter 和 Circle 的位置在层次上同级,这是关键。通过这种方式,Circle 元素的动画范围可以自由地跨越 ItemsPresenter 的子元素。此外,为了不让 ListBoxItem 元素的图标和文本遮挡 Circle 元素,需要将 ItemsPresenter 元素放在 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 Template 结构

与 ListBox 控件的 Template 不同,ListBoxItem 的实现相对简单。而且 Circle 动画元素也无关紧要,只需简单实现菜单项的图标和文本。

<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>

此外,还包括图标和文本位置及颜色的动画。在 ListBoxItem 元素中,无需实现特殊功能。

JamesIcon 样式

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 Tag 绑定。

// 无法绑定方式
<Setter Property="Icon" Value="{TemplateBinding Tag}"/>

因此,通过 RelativeSource 绑定,搜索上级父元素 ListBoxItem 并绑定 Tag 属性。

<... Value="{Binding RelativeSource={RelativeSource AncestorType=ListBoxItem}, Path=Tag}"/>

使用 RelativeSource 绑定,将 ListBoxItem 区域内最初定义的图标的 TemplateBinding 动态迁移到 JamesIcon 区域。此方法使每个组件(JamesIcon)能够拥有自己的定义和样式,从而模块化代码,便于维护和重用。通过将绑定和样式分别管理,整体代码结构更清晰,易于理解和修改。此外,这种分离提供了更大的灵活性,可以在不影响其他组件的情况下调整各组件的样式和行为。

Microsoft Blend: Geometry 设计

在这里插入图片描述

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 是菜单更改时视觉上动作用的重要部分。通过平滑的动画实现现代和潮流的设计元素。

Circle 元素并不一定需要基于 Geometry 实现。使用图像可以更轻松地实现。然而,从质量的角度来看,使用 Geometry 设计元素不受尺寸变化的分辨率影响,可以更细致地实现,因此对其需求日益增加。

如下图所示,无论如何改变尺寸,结果都非常清晰。

在这里插入图片描述

Circle 设计仔细观察,发现是通过叠加黑色圆和绿色圆来表现视觉上的空间感。此外,通过在 MagicBar 区域两侧的圆角处理,使其看起来更柔和,并通过动画动作实现更加优雅。然而,Arc 的实现并不容易,因此在实际引入过程中经常会被放弃。

此时,Microsoft Blend 可以轻松绘制此特殊形状。

绘制方法:

设计过程包括绘制一个下部凸起弧的较大圆,然后在大圆两侧同一高度添加较小圆。通过调整大圆直径,使大圆和小圆完全交叉。
在这里插入图片描述

然后,使用合并功能首先切掉大圆的多余部分,使用减去功能去除小圆不需要的部分,最后只保留交点处的弧形。最后,添加一个矩形并移除不需要的部分,生成独特且自然的弧形。
在这里插入图片描述

这种设计元素的实现方法不仅适用于处理复杂图形时的 Microsoft Blend 使用方法,还提供了思考和解决设计问题的新视角。通过这种方式,Circle 不仅在美学上具有吸引力,在技术上也实现了创新的质量提升。

动画:ListBoxItem

构成图标和文本的 ListBoxItem 区域动画相对简单。IsSelected=true 时,移动组件到上方并调整透明度。

请通过下图仔细观察动画的路径和效果。

在这里插入图片描述

如下图所示,每当 ListBox 控件的 IsSelected 值更改时,动画都会触发。此外,由于图标和文本的动作范围不会超出 ListBoxItem 区域,因此建议在 XAML 中直接实现静态 Storyboard 元素。

此时,动画的控制可以通过 Trigger 或 VisualStateManager 模块实现,此控件仅处理简单的 IsSelected 动作,因此选择便于使用的 Trigger 模块。

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

最终,通过 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 元素的动画实现需要动态计算,因此实现更为复杂。

Circle(圆形)元素的动画

接下来是实现 Circle 元素动画。以下是动态 Circle 位置移动的视频。

在这里插入图片描述

Circle 元素的移动需根据点击位置精确计算,因此无法在 XAML 中实现,而需在 C# 代码中处理。因此,需要一种方法来连接 XAML 和后台代码。

OnApplyTemplate

此方法用于获取 MagicBar 控件内部的 Circle 区域。此方法在控件与 Template 连接时被内部调用。因此,通过在 MagicBar 类中提前 override 来实现功能。

然后,使用 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 目标属性,表示更改目标元素的水平位置。动画持续时间设置为 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 事件方法,并编写代码以启动(Begin)

Storyboard。

MagicBar 控件是 CustomControl 形式的 ListBox,可以灵活实现来自 ListBox 的 override 功能。

protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
    base.OnSelectionChanged(e);

    _vi.To = SelectedIndex * 80;
    _sb.Begin();
}

此方法在菜单更改时通过 SelectedIndex 值动态计算位置并更改 To 值。

完整代码查看:CustomControl 总体代码

最后,查看 MagicBar 控件的 XAML 和 C# 代码的总体结构,了解该控件在 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 的移动动画,使 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();
        }
    }
}

通过这种方式,通常通过 UserControl 实现的功能规模通过控件级别的 CustomControl 方式实现,可以更加优雅高效地模块化。

至此,主要功能的介绍已经完成。此控件的详细信息可以通过 GitHub 源代码免费下载,同时也可以通过 YouTube 或 BiliBili 提供的英语/中文详细教程进行研究和应用。期待在 XAML 基础的平台上广泛应用和研究。

使用模型绑定实现动态导航栏自定义

本指南解释了如何通过将模型绑定到 ItemsSource 而不是直接在 XAML 中创建 ListBoxItem 元素来自定义导航栏。这种方法增强了应用程序的灵活性和可扩展性。

第一步:创建模型

首先,定义一个模型来表示导航项。该模型包括显示名称和图标。

public class NavigationModel
{
    public string DisplayName { get; set; }
    public IconType MenuIcon { get; set; }
}

第二步:更新 Generic.xaml 中的绑定

修改 Generic.xaml 中的绑定以反映模型属性。这允许导航栏显示每个项目的适当文本和图标。

<Setter Property="Text" Value="{Binding DisplayName}"/>
<Setter Property="Icon" Value="{Binding MenuIcon}"/>

第三步:更新 MainWindow.xaml

从 MainWindow.xaml 中移除手动定义的 ListBoxItem 元素,并确保 MagicBar 控件已准备好绑定到数据源。

<navigation:MagicBar x:Name="bar"/>

第四步:在代码后置或 ViewModel 中填充 ItemsSource

在 MainWindow.xaml.cs 或 ViewModel 文件中,创建一个 NavigationModel 项目列表,并将其设置为 MagicBar 的 ItemsSource。

private void PopulateNavigationItems()
{
    List<NavigationModel> items = new List<NavigationModel>
    {
        new NavigationModel { DisplayName = "Microsoft", MenuIcon = IconType.Microsoft },
        new NavigationModel { DisplayName = "Apple", MenuIcon = IconType.Apple },
        new NavigationModel { DisplayName = "Google", MenuIcon = IconType.Google },
        new NavigationModel { DisplayName = "Facebook", MenuIcon = IconType.Facebook },
        new NavigationModel { DisplayName = "Instagram", MenuIcon = IconType.Instagram }
    };

    bar.ItemsSource = items;
}

第五步:调整 ItemsPanel 模板

最后,定制 Generic.xaml 中的 ItemsPanel 模板,使用 UniformGrid 根据项目数动态调整列数。

<ItemsPanelTemplate>
    <UniformGrid Columns="{Binding RelativeSource={RelativeSource AncestorType=ListBox}, Path=Items.Count}"/>
</ItemsPanelTemplate>

结论

按照这些步骤操作,您可以动态创建具有自定义项目的导航栏。这种方法提供了一种更具可扩展性和可维护性的方法来管理应用程序中的导航元素。

沟通与支持

我们随时保持沟通渠道开放。大家可以通过以下方式与我们互动:

GitHub: 关注、Fork、Stars
BiliBili: 一键三连
邮箱: james@jamesnet.dev

  • 11
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值