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


效果圖:

由於整個控件是實現之后才寫的教程,因此這里記錄的代碼是最終實現后的,前后會引用到其他的一些依賴屬性或者代碼,需要閱讀整篇文章。

 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM