效果圖:
由於整個控件是實現之后才寫的教程,因此這里記錄的代碼是最終實現后的,前后會引用到其他的一些依賴屬性或者代碼,需要閱讀整篇文章。
1、確定Timeline繼承的基類
從效果圖中可以看到,時間軸都是由一節一節的子節點組成的,這個很容易聯想到我們應該將Timeline繼承自ItemsControl。之外仔細觀察效果圖,可以發現第一項的時間軸節點與其他都不同,而且拆解每一個子項,發現都是由一個圓圈和一個豎線組成,但是最后一項和上面的都不同,少了一個豎線,因此為了控制這些樣式,我們需要重新定義一個TimelineItem,將其繼承自ContentControl,來重新定義邏輯。
2、具體實現
2.1、TimelineItem的具體實現
2.1.1、設計思路
2.1.1.1、為了能確定當前子項所處的位置(是第一項、中間項還是最后一項),我能想到的有下面2種實現方式
①、使用Converter轉換器,將當前Item傳入后台的轉換器種,通過ItemsControl自帶的【ItemContainerGenerator.IndexFromContainer】方法獲取到當前Item的Index,然后將Index與ItemsControl的Items進行比較,判斷當前Item的所處位置。
②、直接在TimelineItem類里面定義3個依賴屬性:IsFirstItem、IsMiddleItem、IsLastItem,來定義TimelineItem的身份,這樣我們就可以在觸發器里面根據這些屬性來進行一些樣式上面的設置,就像使用IsMouseOver屬性一樣。
接下來的代碼示例中,我采用了第二種實現方式(因為第一種我已經用過了
^_^
)
2.1.1.2、為了控件的靈活性以及用戶可能需要自定義第一項、中間項、最后一項的樣式,或者更極端一點,用戶可能會自定義每一個Item的外觀,因此在Timeline類里面定義了好幾個依賴屬性:FirstSlotTemplate、MiddleSlotTemplate、LastSlotTemplate、IsCustomEverySlot以及SlotTemplate。
2.1.2、具體代碼實現
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.ComponentModel; namespace ZdfFlatUI { public class TimelineItem : ContentControl { #region DependencyProperty #region IsFirstItem /// <summary> /// 獲取或者設置該項在列表中是否是第一個 /// </summary> [Bindable(true), Description("獲取或者設置該項在列表中是否是第一個")] public bool IsFirstItem { get { return (bool)GetValue(IsFirstItemProperty); } set { SetValue(IsFirstItemProperty, value); } } public static readonly DependencyProperty IsFirstItemProperty = DependencyProperty.Register("IsFirstItem", typeof(bool), typeof(TimelineItem), new PropertyMetadata(false)); #endregion #region IsMiddleItem /// <summary> /// 獲取或者設置該項在列表中是否是中間的一個 /// </summary> [Bindable(true), Description("獲取或者設置該項在列表中是否是中間的一個")] public bool IsMiddleItem { get { return (bool)GetValue(IsMiddleItemProperty); } set { SetValue(IsMiddleItemProperty, value); } } public static readonly DependencyProperty IsMiddleItemProperty = DependencyProperty.Register("IsMiddleItem", typeof(bool), typeof(TimelineItem), new PropertyMetadata(false)); #endregion #region IsLastItem /// <summary> /// 獲取或者設置該項在列表中是否是最后一個 /// </summary> [Bindable(true), Description("獲取或者設置該項在列表中是否是最后一個")] public bool IsLastItem { get { return (bool)GetValue(IsLastItemProperty); } set { SetValue(IsLastItemProperty, value); } } public static readonly DependencyProperty IsLastItemProperty = DependencyProperty.Register("IsLastItem", typeof(bool), typeof(TimelineItem), new PropertyMetadata(false)); #endregion #endregion #region Constructors static TimelineItem() { DefaultStyleKeyProperty.OverrideMetadata(typeof(TimelineItem), new FrameworkPropertyMetadata(typeof(TimelineItem))); } #endregion } }
Timeline代碼
1 using System; 2 using System.Collections.Generic; 3 using System.Collections.Specialized; 4 using System.Linq; 5 using System.Text; 6 using System.Windows; 7 using System.Windows.Controls; 8 using System.ComponentModel; 9 namespace ZdfFlatUI 10 { 11 /// <summary> 12 /// 時間軸 13 /// </summary> 14 /// <remarks>add by zhidanfeng 2017.5.29</remarks> 15 public class Timeline : ItemsControl 16 { 17 #region private fields 18 #endregion 19 #region DependencyProperty 20 #region FirstSlotTemplate 21 /// <summary> 22 /// 獲取或者設置第一個時間軸點的樣子 23 /// </summary> 24 [Bindable(true), Description("獲取或者設置第一個時間軸點的樣子")] 25 public DataTemplate FirstSlotTemplate 26 { 27 get { return (DataTemplate)GetValue(FirstSlotTemplateProperty); } 28 set { SetValue(FirstSlotTemplateProperty, value); } 29 } 30 31 public static readonly DependencyProperty FirstSlotTemplateProperty = 32 DependencyProperty.Register("FirstSlotTemplate", typeof(DataTemplate), typeof(Timeline)); 33 #endregion 34 #region MiddleSlotTemplate 35 /// <summary> 36 /// 獲取或者設置中間的時間軸點的樣子 37 /// </summary> 38 [Bindable(true), Description("獲取或者設置中間的時間軸點的樣子")] 39 public DataTemplate MiddleSlotTemplate 40 { 41 get { return (DataTemplate)GetValue(MiddleSlotTemplateProperty); } 42 set { SetValue(MiddleSlotTemplateProperty, value); } 43 } 44 45 public static readonly DependencyProperty MiddleSlotTemplateProperty = 46 DependencyProperty.Register("MiddleSlotTemplate", typeof(DataTemplate), typeof(Timeline)); 47 #endregion 48 #region LastItemTemplate 49 /// <summary> 50 /// 獲取或者設置最后一個時間軸點的樣子 51 /// </summary> 52 [Bindable(true), Description("獲取或者設置最后一個時間軸點的樣子")] 53 public DataTemplate LastSlotTemplate 54 { 55 get { return (DataTemplate)GetValue(LastSlotTemplateProperty); } 56 set { SetValue(LastSlotTemplateProperty, value); } 57 } 58 59 public static readonly DependencyProperty LastSlotTemplateProperty = 60 DependencyProperty.Register("LastSlotTemplate", typeof(DataTemplate), typeof(Timeline)); 61 #endregion 62 #region IsCustomEverySlot 63 /// <summary> 64 /// 獲取或者設置是否自定義每一個時間軸點的外觀。 65 /// </summary> 66 [Bindable(true), Description("獲取或者設置是否自定義每一個時間軸點的外觀。當屬性值為True時,FirstSlotTemplate、MiddleSlotTemplate、LastSlotTemplate屬性都將失效,只能設置SlotTemplate來定義每一個時間軸點的樣式")] 67 public bool IsCustomEverySlot 68 { 69 get { return (bool)GetValue(IsCustomEverySlotProperty); } 70 set { SetValue(IsCustomEverySlotProperty, value); } 71 } 72 73 public static readonly DependencyProperty IsCustomEverySlotProperty = 74 DependencyProperty.Register("IsCustomEverySlot", typeof(bool), typeof(Timeline), new PropertyMetadata(false)); 75 #endregion 76 #region SlotTemplate 77 /// <summary> 78 /// 獲取或者設置每個時間軸點的外觀 79 /// </summary> 80 [Bindable(true), Description("獲取或者設置每個時間軸點的外觀。只有當IsCustomEverySlot屬性為True時,該屬性才生效")] 81 public DataTemplate SlotTemplate 82 { 83 get { return (DataTemplate)GetValue(SlotTemplateProperty); } 84 set { SetValue(SlotTemplateProperty, value); } 85 } 86 87 public static readonly DependencyProperty SlotTemplateProperty = 88 DependencyProperty.Register("SlotTemplate", typeof(DataTemplate), typeof(Timeline)); 89 #endregion 90 #endregion 91 #region Constructors 92 static Timeline() 93 { 94 DefaultStyleKeyProperty.OverrideMetadata(typeof(Timeline), new FrameworkPropertyMetadata(typeof(Timeline))); 95 } 96 #endregion 97 #region Override 98 protected override void PrepareContainerForItemOverride(DependencyObject element, object item) 99 { 100 int index = this.ItemContainerGenerator.IndexFromContainer(element); 101 TimelineItem timelineItem = element as TimelineItem; 102 if(timelineItem == null) 103 { 104 return; 105 } 106 if(index == 0) 107 { 108 timelineItem.IsFirstItem = true; 109 } 110 if(index == this.Items.Count - 1) 111 { 112 timelineItem.IsLastItem = true; 113 } 114 base.PrepareContainerForItemOverride(timelineItem, item); 115 } 116 protected override DependencyObject GetContainerForItemOverride() 117 { 118 return new TimelineItem(); 119 } 120 public override void OnApplyTemplate() 121 { 122 base.OnApplyTemplate(); 123 } 124 protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e) 125 { 126 base.OnItemsChanged(e); 127 //以下代碼是為了新增項或者移除項時,正確設置每個Item的外觀 128 switch (e.Action) 129 { 130 case NotifyCollectionChangedAction.Add: 131 if (e.NewStartingIndex == 0) //如果新添加項是放在第一位,則更改原來的第一位的屬性值 132 { 133 this.SetTimelineItem(e.NewStartingIndex + e.NewItems.Count); 134 } 135 //如果新添加項是放在最后一位,則更改原來的最后一位的屬性值 136 if (e.NewStartingIndex == this.Items.Count - e.NewItems.Count) 137 { 138 this.SetTimelineItem(e.NewStartingIndex - 1); 139 } 140 break; 141 case NotifyCollectionChangedAction.Remove: 142 if(e.OldStartingIndex == 0) //如果移除的是第一個,則更改更新后的第一項的屬性值 143 { 144 this.SetTimelineItem(0); 145 } 146 else 147 { 148 this.SetTimelineItem(e.OldStartingIndex - 1); 149 } 150 break; 151 } 152 } 153 #endregion 154 #region private function 155 /// <summary> 156 /// 設置TimelineItem的位置屬性 157 /// </summary> 158 /// <param name="index"></param> 159 private void SetTimelineItem(int index) 160 { 161 if(index > this.Items.Count || index < 0) 162 { 163 return; 164 } 165 TimelineItem timelineItem = this.ItemContainerGenerator.ContainerFromIndex(index) as TimelineItem; 166 if(timelineItem == null) 167 { 168 return; 169 } 170 timelineItem.IsFirstItem = index == 0; 171 timelineItem.IsLastItem = index == this.Items.Count - 1; 172 timelineItem.IsMiddleItem = index > 0 && index < this.Items.Count - 1; 173 } 174 #endregion 175 } 176 }
樣式代碼:
1 <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 2 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ZUI="clr-namespace:ZdfFlatUI"> 3 <PathGeometry x:Key="Icon_Gou" Figures="M378.410667 850.450963C364.491852 850.450963 350.610963 845.293037 340.02963 834.939259L20.920889 523.529481C-0.279704 502.821926-0.279704 469.295407 20.920889 448.587852 42.121481 427.880296 76.48237 427.880296 97.682963 448.587852L378.410667 722.526815 925.75763 188.491852C946.958222 167.784296 981.319111 167.784296 1002.519704 188.491852 1023.720296 209.161481 1023.720296 242.688 1002.519704 263.395556L416.791704 834.939259C406.172444 845.293037 392.291556 850.450963 378.410667 850.450963L378.410667 850.450963Z" /> 4 <DataTemplate x:Key="FirstSlotTemplate"> 5 <Grid> 6 <Ellipse x:Name="Slot1" Width="15" Height="15" Fill="#30AAADAF" /> 7 <Ellipse x:Name="Slot2" Width="7" Height="7" Fill="#FF6501" /> 8 </Grid> 9 </DataTemplate> 10 <DataTemplate x:Key="LastSlotTemplate"> 11 <Grid> 12 <Ellipse x:Name="Slot1" Width="15" Height="15" Fill="#AAADAF" /> 13 <Path x:Name="path" Width="9" 14 Data="{StaticResource Icon_Gou}" 15 Fill="#FFFFFF" Stretch="Uniform" /> 16 </Grid> 17 </DataTemplate> 18 <Style TargetType="{x:Type ZUI:TimelineItem}"> 19 <Setter Property="Background" Value="Transparent" /> 20 <Setter Property="BorderBrush" Value="{Binding BorderBrush, RelativeSource={RelativeSource AncestorType={x:Type ZUI:Timeline}}}" /> 21 <Setter Property="BorderThickness" Value="0" /> 22 <Setter Property="Foreground" Value="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type ZUI:Timeline}}}" /> 23 <Setter Property="Padding" Value="15,0,15,0" /> 24 <Setter Property="MinHeight" Value="50" /> 25 <Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ZUI:Timeline}}}" /> 26 <Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ZUI:Timeline}}}" /> 27 <Setter Property="SnapsToDevicePixels" Value="True" /> 28 <Setter Property="UseLayoutRounding" Value="True" /> 29 <Setter Property="Template"> 30 <Setter.Value> 31 <ControlTemplate TargetType="{x:Type ZUI:TimelineItem}"> 32 <Grid> 33 <Grid.RowDefinitions> 34 <RowDefinition Height="auto" /> 35 <RowDefinition Height="*" /> 36 </Grid.RowDefinitions> 37 <Grid.ColumnDefinitions> 38 <ColumnDefinition Width="auto" /> 39 <ColumnDefinition Width="*" /> 40 </Grid.ColumnDefinitions> 41 <ContentPresenter x:Name="Slot" ContentTemplate="{Binding MiddleSlotTemplate, RelativeSource={RelativeSource AncestorType={x:Type ZUI:Timeline}}}" /> 42 <Rectangle x:Name="Line" Grid.Row="1" Width="1" 43 Fill="{TemplateBinding BorderBrush}" /> 44 <ContentPresenter Grid.RowSpan="2" Grid.Column="1" 45 Margin="{TemplateBinding Padding}" 46 HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 47 VerticalAlignment="{TemplateBinding VerticalContentAlignment}" /> 48 </Grid> 49 <ControlTemplate.Triggers> 50 <!-- 51 當IsCustomEverySlot為True時,FirstSlotTemplate、MiddleSlotTemplate、LastSlotTemplate都將失效, 52 只能設置SlotTemplate來定義每一個時間軸點的樣式 53 --> 54 <MultiDataTrigger> 55 <MultiDataTrigger.Conditions> 56 <Condition Binding="{Binding IsFirstItem, RelativeSource={RelativeSource Self}}" Value="True" /> 57 <Condition Binding="{Binding IsCustomEverySlot, RelativeSource={RelativeSource AncestorType={x:Type ZUI:Timeline}}}" Value="False" /> 58 </MultiDataTrigger.Conditions> 59 <Setter TargetName="Slot" Property="ContentTemplate" Value="{Binding FirstSlotTemplate, RelativeSource={RelativeSource AncestorType={x:Type ZUI:Timeline}}}" /> 60 </MultiDataTrigger> 61 <MultiDataTrigger> 62 <MultiDataTrigger.Conditions> 63 <Condition Binding="{Binding IsLastItem, RelativeSource={RelativeSource Self}}" Value="True" /> 64 <Condition Binding="{Binding IsCustomEverySlot, RelativeSource={RelativeSource AncestorType={x:Type ZUI:Timeline}}}" Value="False" /> 65 </MultiDataTrigger.Conditions> 66 <Setter TargetName="Slot" Property="ContentTemplate" Value="{Binding LastSlotTemplate, RelativeSource={RelativeSource AncestorType={x:Type ZUI:Timeline}}}" /> 67 </MultiDataTrigger> 68 <MultiDataTrigger> 69 <MultiDataTrigger.Conditions> 70 <Condition Binding="{Binding IsMiddleItem, RelativeSource={RelativeSource Self}}" Value="True" /> 71 <Condition Binding="{Binding IsCustomEverySlot, RelativeSource={RelativeSource AncestorType={x:Type ZUI:Timeline}}}" Value="False" /> 72 </MultiDataTrigger.Conditions> 73 <Setter TargetName="Slot" Property="ContentTemplate" Value="{Binding MiddleSlotTemplate, RelativeSource={RelativeSource AncestorType={x:Type ZUI:Timeline}}}" /> 74 </MultiDataTrigger> 75 <DataTrigger Binding="{Binding IsCustomEverySlot, RelativeSource={RelativeSource AncestorType={x:Type ZUI:Timeline}}}" Value="True"> 76 <Setter TargetName="Slot" Property="ContentTemplate" Value="{Binding SlotTemplate, RelativeSource={RelativeSource AncestorType={x:Type ZUI:Timeline}}}" /> 77 </DataTrigger> 78 <Trigger Property="IsLastItem" Value="True"> 79 <Setter TargetName="Line" Property="Visibility" Value="Collapsed" /> 80 </Trigger> 81 <Trigger Property="IsMiddleItem" Value="True"> 82 <Setter TargetName="Line" Property="Visibility" Value="Visible" /> 83 </Trigger> 84 </ControlTemplate.Triggers> 85 </ControlTemplate> 86 </Setter.Value> 87 </Setter> 88 </Style> 89 <Style TargetType="{x:Type ZUI:Timeline}"> 90 <Setter Property="Background" Value="Transparent" /> 91 <Setter Property="BorderBrush" Value="#F0F0F0" /> 92 <Setter Property="BorderThickness" Value="0" /> 93 <Setter Property="Foreground" Value="Black" /> 94 <Setter Property="HorizontalContentAlignment" Value="Left" /> 95 <Setter Property="VerticalContentAlignment" Value="Top" /> 96 <Setter Property="SnapsToDevicePixels" Value="True" /> 97 <Setter Property="UseLayoutRounding" Value="True" /> 98 <Setter Property="FirstSlotTemplate" Value="{StaticResource FirstSlotTemplate}" /> 99 <Setter Property="MiddleSlotTemplate" Value="{StaticResource LastSlotTemplate}" /> 100 <Setter Property="LastSlotTemplate" Value="{StaticResource LastSlotTemplate}" /> 101 <Setter Property="Template"> 102 <Setter.Value> 103 <ControlTemplate TargetType="{x:Type ZUI:Timeline}"> 104 <Border Background="{TemplateBinding Background}" 105 BorderBrush="{TemplateBinding BorderBrush}" 106 BorderThickness="{TemplateBinding BorderThickness}" 107 SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" 108 UseLayoutRounding="{TemplateBinding UseLayoutRounding}"> 109 <ZUI:ZScrollViewer> 110 <ItemsPresenter /> 111 </ZUI:ZScrollViewer> 112 </Border> 113 </ControlTemplate> 114 </Setter.Value> 115 </Setter> 116 </Style> 117 </ResourceDictionary>
在Timeline類中,有幾處關鍵代碼
①、為了能將TimelineItem和Timeline關聯起來,需要重寫【GetContainerForItemOverride
】方法
protected override DependencyObject GetContainerForItemOverride() { return new TimelineItem(); }
②、在控件第一次初始化以及在后來的新增時,需要設置TimelineItem的幾個依賴屬性值,即設置Item具體是第一個、中間項還是最后一項,因此需要重寫【PrepareContainerForItemOverride】
protected override void PrepareContainerForItemOverride(DependencyObject element, object item) { int index = this.ItemContainerGenerator.IndexFromContainer(element); TimelineItem timelineItem = element as TimelineItem; if(timelineItem == null) { return; } if(index == 0) { timelineItem.IsFirstItem = true; } if(index == this.Items.Count - 1) { timelineItem.IsLastItem = true; } base.PrepareContainerForItemOverride(timelineItem, item); }
③、Timeline控件在運行過程中,可能會涉及到新增或者刪除節點,這時同樣需要實時的設置每個TimelineItem的IsFirstItem、IsMiddleItem、IsLastItem,這樣呈現正確的外觀
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e) { base.OnItemsChanged(e); //以下代碼是為了新增項或者移除項時,正確設置每個Item的外觀 switch (e.Action) { case NotifyCollectionChangedAction.Add: if (e.NewStartingIndex == 0) //如果新添加項是放在第一位,則更改原來的第一位的屬性值 { this.SetTimelineItem(e.NewStartingIndex + e.NewItems.Count); } //如果新添加項是放在最后一位,則更改原來的最后一位的屬性值 if (e.NewStartingIndex == this.Items.Count - e.NewItems.Count) { this.SetTimelineItem(e.NewStartingIndex - 1); } break; case NotifyCollectionChangedAction.Remove: if(e.OldStartingIndex == 0) //如果移除的是第一個,則更改更新后的第一項的屬性值 { this.SetTimelineItem(0); } else { this.SetTimelineItem(e.OldStartingIndex - 1); } break; } }
最終效果圖:

代碼下載:https://github.com/zhidanfeng/WPF.UI