當前位置: 妍妍網 > 碼農

會魔法的導航欄 #WPF 走你!

2024-05-28碼農

會魔法的導航欄 #WPF 走你!

控制項名稱: NavigationBar
作者:WPFDevelopersOrg - Vicky&James

源碼連結 [1] :https://github.com/vickyqu115/navigationbar

教學視訊 [2] (【小李趣味多】https://bit.ly/3UvaOsl)

這篇文章是對 WPF Magic NavigationBar 教程視訊的技術回顧。

Magic NavigationBar 控制項介紹

WPF 應用程式傳統上透過選單構成將多個界面連線並整合到一個程式中。因此,選單或稱為導航的技術是 WPF 的核心實作之一。由於這與計畫的架構(設計)直接相關,如果更仔細地實作這一技術,也可以預期計畫的品質會得到顯著提升。

此控制項雖然在行動裝置上以特殊設計和動畫為特點,但如果在 WPF 中使用 ListBox 和動畫技術 ,也可以實作結構優良且優雅的效果。此外,像 AvaloniaUI、Uno、OpenSilver、MAUI 等跨平台工具也可以用類似方式實作,因此希望這一計畫能夠在各種平台上得到研究和套用。

同時,推廣 WPF 的靈活性和優越性,分享技術也是此計畫的目的之一。透過這個計畫,希望大家能深入體驗 WPF 的魅力。

設計和結構的理念

Magic NavigationBar

該控制項的實作方式是當前在網頁和行動裝置中廣泛使用的一種導航結構。因此,透過 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 的階層如下:

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

如上所示,ItemsPresenter 和 Circle 的位置在層次上同級,這是關鍵。透過這種方式,Circle 元素的動畫範圍可以自由地跨越 ItemsPresenter 的子元素。此外,為了不讓 ListBoxItem 元素的圖示和文本遮擋 Circle 元素,需要將 ItemsPresenter 元素放在 Circle 之前。

在討論了理論後,接下來透過實際實作的原始碼來詳細對比。

x:Name="PART_Circle" 區域即為 Circle。

< styleTargetType="{x:Type local:MagicBar}">
<SetterProperty="ItemContainer style"Value="{StaticResource MagicBarItem}"/>
<SetterProperty="SnapsToDevicePixels"Value="True"/>
<SetterProperty="UseLayoutRounding"Value="True"/>
<SetterProperty="Background"Value="Transparent"/>
<SetterProperty="Width"Value="440"/>
<SetterProperty="Height"Value="120"/>
<SetterProperty="Template">
<Setter.Value>
<ControlTemplateTargetType="{x:Type local:MagicBar}">
<GridBackground="{TemplateBinding Background}">
<Grid.Clip>
<RectangleGeometryRect="0 0 440 120"/>
</Grid.Clip>
<Border style="{StaticResource Bar}"/>
<CanvasMargin="20 0 20 0">
<Gridx:Name="PART_Circle" style="{StaticResource Circle}">
<Path style="{StaticResource Arc}"/>
<EllipseFill="#222222"/>
<EllipseFill="CadetBlue"Margin="6"/>
</Grid>
</Canvas>
<ItemsPresenterMargin="20 40 20 0"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
<SetterProperty="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<UniformGridColumns="5"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</ style>

ListBoxItem Template 結構

與 ListBox 控制項的 Template 不同, ListBoxItem 的實作相對簡單 。而且 Circle 動畫元素也無關緊要,只需簡單實作選單項的圖示和文本。

< styleTargetType="{x:Type ListBoxItem}"x:Key="MagicBarItem">
<SetterProperty="FocusVisual style"Value="{x:Null}"/>
<SetterProperty="Background"Value="Transparent"/>
<SetterProperty="Template">
<Setter.Value>
<ControlTemplateTargetType="{x:Type ListBoxItem}">
<GridBackground="{TemplateBinding Background}">
<james:JamesIconx:Name="icon" style="{StaticResource Icon}"/>
<TextBlockx: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 圖示提供一致的設計,可以提高控制項的品質。因此,建議仔細觀察這些差異。


< styleTargetType="{x:Type james:JamesIcon}"x:Key="Icon">
<SetterProperty="Icon"Value="{TemplateBinding Tag}"/>
<SetterProperty="Width"Value="40"/>
<SetterProperty="Height"Value="40"/>
<SetterProperty="Fill"Value="#44333333"/>
</ style>

RelativeSource 繫結

JamesIcon 樣式與 Template 分離,因此無法使用 TemplateBinding Tag 繫結。

// 無法繫結方式
<SetterProperty="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 兩種情況的場景。

<Storyboardx:Key="Selected">
<james:ThickItemMode="CubicEaseInOut"TargetName="icon"Duration="0:0:0.5"Property="Margin"To="0 -80 0 0"/>
<james:ThickItemMode="CubicEaseInOut"TargetName="name"Duration="0:0:0.5"Property="Margin"To="0 45 0 0"/>
<james:ColorItemMode="CubicEaseInOut"TargetName="icon"Duration="0:0:0.5"Property="Fill.Color"To="#333333"/>
<james:ColorItemMode="CubicEaseInOut"TargetName="name"Duration="0:0:0.5"Property="Foreground.Color"To="#333333"/>
</Storyboard>
<Storyboardx:Key="UnSelected">
<james:ThickItemMode="CubicEaseInOut"TargetName="icon"Duration="0:0:0.5"Property="Margin"To="0 0 0 0"/>
<james:ThickItemMode="CubicEaseInOut"TargetName="name"Duration="0:0:0.5"Property="Margin"To="0 60 0 0"/>
<james:ColorItemMode="CubicEaseInOut"TargetName="icon"Duration="0:0:0.5"Property="Fill.Color"To="#44333333"/>
<james:ColorItemMode="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>
<TriggerProperty="IsSelected"Value="True">
<Trigger.EnterActions>
<BeginStoryboardStoryboard="{StaticResource Selected}"/>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboardStoryboard="{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 是互動中顯示動畫效果的目標元素。

publicoverridevoidOnApplyTemplate()
{
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。

privatevoidInitStoryboard(Grid circle)
{
_vi = new();
_sb = new();
_vi.Mode = EasingFunctionBaseMode.QuinticEaseInOut;
_vi.Property = new PropertyPath(Canvas.LeftProperty);
_vi.Duration = new Duration(new TimeSpan(0000500));
Storyboard.SetTarget(_vi, circle);
Storyboard.SetTargetProperty(_vi, _vi.Property);
_sb.Children.Add(_vi);
}

OnSelectionChanged

現在需要實作 Circle 元素的移動動畫場景。因此,在 MagicBar 類中實作 OnSelectionChanged 事件方法,並編寫程式碼以啟動(Begin)

Storyboard。

MagicBar 控制項是 CustomControl 形式的 ListBox ,可以靈活實作來自 ListBox 的 override 功能。

protectedoverridevoidOnSelectionChanged(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">

<Storyboardx:Key="Selected">
<james:ThickItemMode="CubicEaseInOut"TargetName="icon"Duration="0:0:0.5"Property="Margin"To="0 -80 0 0"/>
<james:ThickItemMode="CubicEaseInOut"TargetName="name"Duration="0:0:0.5"Property="Margin"To="0 45 0 0"/>
<james:ColorItemMode="CubicEaseInOut"TargetName="icon"Duration="0:0:0.5"Property="Fill.Color"To="#333333"/>
<james:ColorItemMode="CubicEaseInOut"TargetName="name"Duration="0:0:0.5"Property="Foreground.Color"To="#333333"/>
</Storyboard>
<Storyboardx:Key="UnSelected">
<james:ThickItemMode="CubicEaseInOut"TargetName="icon"Duration="0:0:0.5"Property="Margin"To="0 0 0 0"/>
<james:ThickItemMode="CubicEaseInOut"TargetName="name"Duration="0:0:0.5"Property="Margin"To="0 60 0 0"/>
<james:ColorItemMode="CubicEaseInOut"TargetName="icon"Duration="0:0:0.5"Property="Fill.Color"To="#44333333"/>
<james:ColorItemMode="CubicEaseInOut"TargetName="name"Duration="0:0:0.5"Property="Foreground.Color"To="#00000000"/>
</Storyboard>
< styleTargetType="{x:Type james:JamesIcon}"x:Key="Icon">
<SetterProperty="Icon"Value="{Binding RelativeSource={RelativeSource AncestorType=ListBoxItem},Path=Tag}"/>
<SetterProperty="Width"Value="40"/>
<SetterProperty="Height"Value="40"/>
<SetterProperty="Fill"Value="#44333333"/>
</ style>
< styleTargetType="{x:Type TextBlock}"x:Key="Name">
<SetterProperty="Text"Value="{Binding RelativeSource={RelativeSource AncestorType=ListBoxItem},Path=Content}"/>
<SetterProperty="HorizontalAlignment"Value="Center"/>
<SetterProperty="FontWeight"Value="Bold"/>
<SetterProperty="FontSize"Value="14"/>
<SetterProperty="Foreground"Value="#00000000"/>
<SetterProperty="Margin"Value="0 60 0 0"/>
</ style>
< styleTargetType="{x:Type ListBoxItem}"x:Key="MagicBarItem">
<SetterProperty="FocusVisual style"Value="{x:Null}"/>
<SetterProperty="Background"Value="Transparent"/>
<SetterProperty="Template">
<Setter.Value>
<ControlTemplateTargetType="{x:Type ListBoxItem}">
<GridBackground="{TemplateBinding Background}">
<james:JamesIconx:Name="icon" style="{StaticResource Icon}"/>
<TextBlockx:Name="name" style="{StaticResource Name}"/>
</Grid>
<ControlTemplate.Triggers>
<TriggerProperty="IsSelected"Value="True">
<Trigger.EnterActions>
<BeginStoryboardStoryboard="{StaticResource Selected}"/>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboardStoryboard="{StaticResource UnSelected}"/>
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</ style>
<Geometryx: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>
< styleTargetType="{x:Type Path}"x:Key="Arc">
<SetterProperty="Data"Value="{StaticResource ArcData}"/>
<SetterProperty="Width"Value="100"/>
<SetterProperty="Height"Value="100"/>
<SetterProperty="Fill"Value="#222222"/>
<SetterProperty="Margin"Value="-10 40 -10 -1"/>
</ style>
< styleTargetType="{x:Type Border}"x:Key="Bar">
<SetterProperty="Background"Value="#DDDDDD"/>
<SetterProperty="Margin"Value="0 40 0 0"/>
<SetterProperty="CornerRadius"Value="10"/>
</ style>
< styleTargetType="{x:Type Grid}"x:Key="Circle">
<SetterProperty="Width"Value="80"/>
<SetterProperty="Height"Value="80"/>
<SetterProperty="Canvas.Left"Value="-100"/>
</ style>
< styleTargetType="{x:Type local:MagicBar}">
<SetterProperty="ItemContainer style"Value="{StaticResource MagicBarItem}"/>
<SetterProperty="SnapsToDevicePixels"Value="True"/>
<SetterProperty="UseLayoutRounding"Value="True"/>
<SetterProperty="Background"Value="Transparent"/>
<SetterProperty="Width"Value="440"/>
<SetterProperty="Height"Value="120"/>
<SetterProperty="Template">
<Setter.Value>
<ControlTemplateTargetType="{x:Type local:MagicBar}">
<GridBackground="{TemplateBinding Background}">
<Grid.Clip>
<RectangleGeometryRect="0 0 440 120"/>
</Grid.Clip>
<Border style="{StaticResource Bar}"/>
<CanvasMargin="20 0 20 0">
<Gridx:Name="PART_Circle" style="{StaticResource Circle}">
<Path style="{StaticResource Arc}"/>
<EllipseFill="#222222"/>
<EllipseFill="CadetBlue"Margin="6"/>
</Grid>
</Canvas>
<ItemsPresenterMargin="20 40 20 0"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
<SetterProperty="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<UniformGridColumns="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;
namespaceNavigationBar
{
public classMagicBar : ListBox
{
private ValueItem _vi;
private Storyboard _sb;
staticMagicBar()
{
Default styleKeyProperty.OverrideMetadata(typeof(MagicBar), new FrameworkPropertyMetadata(typeof(MagicBar)));
}
publicoverridevoidOnApplyTemplate()
{
base.OnApplyTemplate();
Grid grid = (Grid)GetTemplateChild("PART_Circle");
InitStoryboard(grid);
}
privatevoidInitStoryboard(Grid circle)
{
_vi = new();
_sb = new();
_vi.Mode = EasingFunctionBaseMode.QuinticEaseInOut;
_vi.Property = new PropertyPath(Canvas.LeftProperty);
_vi.Duration = new Duration(new TimeSpan(0000500));
Storyboard.SetTarget(_vi, circle);
Storyboard.SetTargetProperty(_vi, _vi.Property);
_sb.Children.Add(_vi);
}
protectedoverridevoidOnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
_vi.To = SelectedIndex * 80;
_sb.Begin();
}
}
}








透過這種方式,通常透過 UserControl 實作的功能規模透過控制項級別的 CustomControl 方式實作,可以更加優雅高效地模組化。

至此,主要功能的介紹已經完成。此控制項的詳細資訊可以透過 GitHub 原始碼免費下載,同時也可以透過BiliBili 提供的中文詳細教程進行研究和套用。期待在 XAML 基礎的平台上廣泛套用和研究。

使用模型繫結實作動態導航欄自訂

本指南解釋了如何透過將模型繫結到 ItemsSource 而不是直接在 XAML 中建立 ListBoxItem 元素來自訂導航欄。這種方法增強了應用程式的靈活性和可延伸性。

第一步:建立模型

首先, 定義一個模型來表示導航項 。該模型包括顯示名稱和圖示。

public classNavigationModel
{
publicstring DisplayName { getset; }
public IconType MenuIcon { getset; }
}

第二步:更新 Generic.xaml 中的繫結

修改 Generic.xaml 中的繫結以反映模型內容。這允許導航欄顯示每個計畫的適當文本和圖示。

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

第三步:更新 MainWindow.xaml

MainWindow.xaml 中移除手動定義的 ListBoxIte m 元素,並確保 MagicBar 控制項已準備好繫結到資料來源。

<navigation:MagicBarx:Name="bar"/>

第四步:在程式碼後置或 ViewModel 中填充 ItemsSource

MainWindow.xaml.cs 或 ViewModel 檔中,建立一個 NavigationModel 計畫列表,並將其設定為 MagicBar 的 ItemsSource

privatevoidPopulateNavigationItems()
{
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>
<UniformGridColumns="{Binding RelativeSource={RelativeSource AncestorType=ListBox}, Path=Items.Count}"/>
</ItemsPanelTemplate>

結論

按照這些步驟操作,你可以動態建立具有自訂計畫的導航欄。這種方法提供了一種更具可延伸性和可維護性的方法來管理應用程式中的導航元素。

Magic NavigationBar

溝通與支持

我們隨時保持溝通渠道開放。大家可以透過以下方式與我們互動:

GitHub: 關註、Fork、Stars
BiliBili: 一鍵三連
信箱: [email protected]

參考資料

[1]

源碼連結: https://github.com/vickyqu115/navigationbar

[2]

教學視訊: https://bit.ly/3UvaOsl