WPF Timeline簡易時間軸控件的實現

效果圖:

因爲整個控件是實現以後才寫的教程,所以這裏記錄的代碼是最終實現後的,先後會引用到其餘的一些依賴屬性或者代碼,須要閱讀整篇文章。

 一、肯定Timeline繼承的基類

從效果圖中能夠看到,時間軸都是由一節一節的子節點組成的,這個很容易聯想到咱們應該將Timeline繼承自ItemsControl。以外仔細觀察效果圖,能夠發現第一項的時間軸節點與其餘都不一樣,並且拆解每個子項,發現都是由一個圓圈和一個豎線組成,可是最後一項和上面的都不一樣,少了一個豎線,所以爲了控制這些樣式,咱們須要從新定義一個TimelineItem,將其繼承自ContentControl,來從新定義邏輯。

二、具體實現

2.一、TimelineItem的具體實現
2.1.一、設計思路
2.1.1.一、爲了能肯定當前子項所處的位置(是第一項、中間項仍是最後一項),我能想到的有下面2種實現方式
①、使用Converter轉換器,將當前Item傳入後臺的轉換器種,經過ItemsControl自帶的【ItemContainerGenerator.IndexFromContainer】方法獲取到當前Item的Index,而後將Index與ItemsControl的Items進行比較,判斷當前Item的所處位置。
②、直接在TimelineItem類裏面定義3個依賴屬性:IsFirstItem、IsMiddleItem、IsLastItem,來定義TimelineItem的身份,這樣咱們就能夠在觸發器裏面根據這些屬性來進行一些樣式上面的設置,就像使用IsMouseOver屬性同樣。
接下來的代碼示例中,我採用了第二種實現方式(由於第一種我已經用過了 ^_^
2.1.1.二、爲了控件的靈活性以及用戶可能須要自定義第一項、中間項、最後一項的樣式,或者更極端一點,用戶可能會自定義每個Item的外觀,所以在Timeline類裏面定義了好幾個依賴屬性:FirstSlotTemplate、MiddleSlotTemplate、LastSlotTemplate、IsCustomEverySlot以及SlotTemplate。
2.1.二、具體代碼實現
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代碼git

  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 }

樣式代碼:github

  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類中,有幾處關鍵代碼ide

①、爲了能將TimelineItem和Timeline關聯起來,須要重寫【GetContainerForItemOverride 】方法
protected override DependencyObject GetContainerForItemOverride()
{
	return new TimelineItem();
}

②、在控件第一次初始化以及在後來的新增時,須要設置TimelineItem的幾個依賴屬性值,即設置Item具體是第一個、中間項仍是最後一項,所以須要重寫【PrepareContainerForItemOverridethis

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,這樣呈現正確的外觀spa

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
相關文章
相關標籤/搜索