WPF自定義控件第一 - 進度條控件

本文主要針對WPF新手,高手能夠直接忽略,更但願高手們能給出一些更好的實現思路。html

前期一個小任務須要實現一個相似含步驟進度條的控件。雖然對於XAML的瞭解還不是足夠深刻,仍是摸索着作了一個。這篇文章介紹下實現這個控件的步驟,最後會放出代碼。還請高手們給出更好的思路。同時也但願這裏的思路能給同道中人一些幫助。話很少說,開始正題。node

實現中的一些代碼採用了網上現有的方案,代碼中經過註釋標記了來源,再次對代碼做者一併表示感謝。git

 

首先放一張最終效果圖。github

 

節點能夠被點擊c#

 

控件會根據綁定的集合數據生成一系列節點,根據集合中的數據還能夠按比例放置節點的位置。app

節點的實體代碼以下:ide

public class FlowItem
{
    public FlowItem()
    {
    }

    public FlowItem(int id, string title,double offsetRate)
    {
        Id = id;
        Title = title;
        OffsetRate = offsetRate;
    }

    public int Id { get; set; }

    public string Title { get; set; }

    public double OffsetRate { get; set; }
}

其中三個屬性分別表明了節點的編號,標題和偏移量(用來肯定節點在整個條中的位置)。函數

 

控件的實現

忘了好久之前在哪看到過一句話,說設計WPF控件時不必定按照MVVM模式來設計,但必定要確保設計的控件能夠按照MVVM模式來使用。本控件也是本着這麼目標來完成。佈局

控件實現爲TemplatedControl,我的認爲這種方式更爲靈活,作出來的控件可複用度更高。反之UserControl那種組合控件的方式更適用於一個項目內複用的須要。動畫

遵循通常的原則,咱們將控件單獨放於一個項目中。在TemplatedControl項目中,「模板」即XAML內容通常都放置在一個名爲Generic.xaml文件中,這個文件應該放置在解決方案Themes文件夾下。

若是要使用Themes/Generic.xaml這個默認的模板樣式地址,要保證AssemblyInfo.cs中以下語句:

[assembly: ThemeInfo(ResourceDictionaryLocation.None, ResourceDictionaryLocation.SourceAssembly)]

另外也不要試圖修改Themes/Generic.xaml這個文件位置了。雖然聽說是能夠改,但不知道會不會有潛在問題。RoR流行時常說「約定大於配置」,就把這個路徑看成一個約定就行了。

通常來講控件的模板也不宜直接放到Generic.xaml而是每一個控件都定義到一個單獨的xaml文件,而後在Generic中用以下方式進行引用。這樣能夠有效的防止Generic.xaml文件變的過大,也能夠更利於項目模板的查找和修改(直接定位到相關文件便可,博主經常使用Ctrl+T鍵定位文件,也不知道這個是VS的功能仍是Resharper的功能)。

<ResourceDictionary Source="/Zq.Control;component/Flow/FlowControl.xaml"></ResourceDictionary>

 這樣控件的模板就能夠移入FlowControl.xaml中,接着咱們就看一下這裏面控件模板的定義:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:system="clr-namespace:System;assembly=mscorlib"
                    xmlns:flow="clr-namespace:Zq.Control.Flow">

    <flow:MultiThicknessConverter x:Key="FlowMultiThicknessConverter"></flow:MultiThicknessConverter>
    <flow:MultiWidthAnimationConverter x:Key="FlowMultiWidthAnimationConverter"></flow:MultiWidthAnimationConverter>
    <system:Double x:Key="FlowDoubleZero">0</system:Double>
    <Duration x:Key="FlowDuration">0:0:1.5</Duration>

    <Style TargetType="{x:Type flow:FlowControl}">
        <Setter Property="NodeWidth" Value="30"></Setter>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type flow:FlowControl}">
                    <Grid VerticalAlignment="Top">
                        <Grid.Triggers>
                            <EventTrigger RoutedEvent="SizeChanged">
                                <BeginStoryboard>
                                    <Storyboard >
                                        <DoubleAnimation Storyboard.TargetName="Bar" Storyboard.TargetProperty="Tag"
                                             From="0" To="1" Duration="{StaticResource FlowDuration}"/>
                                    </Storyboard>
                                </BeginStoryboard>
                            </EventTrigger>
                        </Grid.Triggers>
                        <Rectangle x:Name="Bar" Panel.ZIndex="0" StrokeThickness="0" Fill="#61d0b3" 
                                   HorizontalAlignment="Left" VerticalAlignment="Top"
                                   Height="{TemplateBinding BarHeight}">
                            <Rectangle.Margin>
                                <MultiBinding Converter="{StaticResource FlowMultiThicknessConverter}">
                                    <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="BarMarginLeft"></Binding>
                                    <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="BarMarginTop"></Binding>
                                    <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="BarMarginLeft"></Binding>
                                    <Binding Source="{StaticResource FlowDoubleZero}"></Binding>
                                </MultiBinding>
                            </Rectangle.Margin>
                            <Rectangle.Tag>
                                <system:Double>0.0</system:Double>
                            </Rectangle.Tag>
                            <Rectangle.Width>
                                <MultiBinding Converter="{StaticResource FlowMultiWidthAnimationConverter}">
                                    <Binding Path="ShadowWidth" RelativeSource="{RelativeSource TemplatedParent}" />
                                    <Binding Path="Tag" RelativeSource="{RelativeSource Self}" />
                                </MultiBinding>
                            </Rectangle.Width>
                        </Rectangle>

                        <ItemsPresenter />
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Setter Property="ItemsPanel">
            <Setter.Value>
                <ItemsPanelTemplate>
                    <flow:FlowControlPanel AnimationDuration="{StaticResource FlowDuration}" />
                </ItemsPanelTemplate>
            </Setter.Value>
        </Setter>
    </Style>

</ResourceDictionary>

這個xaml文件的根節點是ResourceDictionary,表示其中內容是各類資源:樣式,模板等等..

最開始的部分定義了模板中用到的一些Conveter及常量值。

而後就是TemplatedControl最核心的部分,Control Template的定義:

<Style TargetType="{x:Type flow:FlowControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type flow:FlowControl}">
                   ...控件模板內容...
                </ControlTemplate>
            </Setter.Value>
        </Setter>
<Style>

除了模板的定義還定義一些控件依賴屬性的默認值,這些值也能夠被用戶顯示設置的值所覆蓋:

<Setter Property="NodeWidth" Value="30"></Setter>

這裏咱們定義了節點寬度的默認值。

 

控件的主體分兩部分,一個是背景中綠色的矩形條,另外一個是節點。節點是放置在Item中,經過ItemsPresenter顯示出來的。這個後面會詳細說。

模板是須要配合代碼使用的,正如Petzold的第一本WPF書的標題Applications = Code + Markup。咱們有了「標記」了,下面來看看「代碼」:

public class FlowControl : ItemsControl
{

    static FlowControl()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(FlowControl), new FrameworkPropertyMetadata(typeof(FlowControl)));
    }

    #region dependency property

    private const double NodeWidthDefault = 30;

    public static readonly DependencyProperty NodeWidthProperty = DependencyProperty.Register(
        "NodeWidth", typeof(double), typeof(FlowControl),
        new PropertyMetadata(NodeWidthDefault));

    public double NodeWidth
    {
        get { return (double)GetValue(NodeWidthProperty); }
        set
        {
            SetValue(NodeWidthProperty, value);
        }
    }

    private const double BarHeightDefault = 10;

    public static readonly DependencyProperty BarHeightProperty = DependencyProperty.Register(
        "BarHeight", typeof(double), typeof(FlowControl), new PropertyMetadata(BarHeightDefault));

    public double BarHeight
    {
        get { return (double)GetValue(BarHeightProperty); }
        set { SetValue(BarHeightProperty, value); }
    }

    public static readonly DependencyProperty BarMarginLeftProperty = DependencyProperty.Register(
        "BarMarginLeft", typeof(double), typeof(FlowControl), new PropertyMetadata(0.0));

    public double BarMarginLeft
    {
        get { return (double)GetValue(BarMarginLeftProperty); }
        set { SetValue(BarMarginLeftProperty, value); }
    }

    public static readonly DependencyProperty BarMarginTopProperty = DependencyProperty.Register(
        "BarMarginTop", typeof(double), typeof(FlowControl), new PropertyMetadata(default(double)));

    private double BarMarginTop
    {
        get { return (double)GetValue(BarMarginTopProperty); }
        set { SetValue(BarMarginTopProperty, value); }
    }

    public static readonly DependencyProperty ShadowWidthProperty = DependencyProperty.Register(
        "ShadowWidth", typeof(double), typeof(FlowControl), new PropertyMetadata(default(double)));

    private double ShadowWidth
    {
        get { return (double)GetValue(ShadowWidthProperty); }
        set { SetValue(ShadowWidthProperty, value); }
    }

    public static readonly DependencyProperty AnimationDurationProperty = DependencyProperty.Register(
        "AnimationDuration", typeof(Duration), typeof(FlowControl), new PropertyMetadata(default(Duration)));

    public Duration AnimationDuration
    {
        get { return (Duration)GetValue(AnimationDurationProperty); }
        set { SetValue(AnimationDurationProperty, value); }
    }
	
	#endregion

    protected override Size MeasureOverride(Size constraint)
    {
        SetValue(BarMarginLeftProperty, NodeWidth / 2);
        SetValue(BarMarginTopProperty, (NodeWidth - BarHeight) / 2);
        SetValue(ShadowWidthProperty, constraint.Width - BarMarginLeft * 2);

        return base.MeasureOverride(new Size(constraint.Width, NodeWidth * 3));
    }

    protected override Size ArrangeOverride(Size arrangeBounds)
    {
        return base.ArrangeOverride(arrangeBounds);
    }

    #region route event

    //route event
    public static readonly RoutedEvent NodeSelectedEvent =
        FlowNodeControl.NodeSelectedEvent.AddOwner(typeof(FlowControl));

    public event RoutedEventHandler NodeSelected
    {
        add { AddHandler(FlowNodeControl.NodeSelectedEvent, value, false); }
        remove { RemoveHandler(FlowNodeControl.NodeSelectedEvent, value); }
    }

    #endregion

}

能夠看到這個控件由ItemsControl繼承而來,像是咱們節點集合這種數據很適合用ItemsControl來展現,固然咱們也能夠直接繼承自Control本身添加處理Items的一些功能,能實現一樣的效果。

大部分代碼主要定義依賴屬性,路由事件以及重寫了父類的佈局方法。構造函數中那句將代碼與咱們的XAML模板作了關聯:

DefaultStyleKeyProperty.OverrideMetadata(typeof(FlowControl), new FrameworkPropertyMetadata(typeof(FlowControl)));

這樣控件的大致結構就有了。下面對其中的一些細節進行解釋。

先來講說那個綠色進度條的實現,其最主要的一點就是要實現距離左上右三部分有適當的距離,並且這個距離應該隨着節點小圓球半徑的變化自動變化從而始終保持在節點圓球中心部位穿過。

這裏的實現辦法仍是比較簡陋的,但我沒找到更好的辦法:

代碼中定義了2個依賴屬性BarMarginLeft和BarMarginTop分別用來存儲背景進度條左(右)上3部分的Margin值。這兩個值是在重寫的控件的佈局方法MeasureOverride中根據節點的寬度進行計算得出的。

protected override Size MeasureOverride(Size constraint)
{
    SetValue(BarMarginLeftProperty, NodeWidth / 2);
    SetValue(BarMarginTopProperty, (NodeWidth - BarHeight) / 2);

    return base.MeasureOverride(new Size(constraint.Width, NodeWidth * 3));
}

而後使用了一個MultiBinding和轉換器(和MultiBinding配合須要實現IMultiValueConverter的多值轉換器)將上面的值綁定到進度條的Margin屬性:

<Rectangle.Margin>
    <MultiBinding Converter="{StaticResource FlowMultiThicknessConverter}">
        <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="BarMarginLeft"></Binding>
        <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="BarMarginTop"></Binding>
        <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="BarMarginLeft"></Binding>
        <Binding Source="{StaticResource FlowDoubleZero}"></Binding>
    </MultiBinding>
</Rectangle.Margin>

用到的多值轉換器來自網上,代碼以下:

//來源http://stackoverflow.com/questions/6249518/binding-only-part-of-the-margin-property-of-wpf-control
public class MultiThicknessConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return new Thickness(System.Convert.ToDouble(values[0]),
                             System.Convert.ToDouble(values[1]),
                             System.Convert.ToDouble(values[2]),
                             System.Convert.ToDouble(values[3]));
    }

    public object[] ConvertBack(object value, Type[] targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return null;
    }
}

接着來看看進度條的動畫(節點動畫後文另說)是怎樣實現的。WPF中實現動畫無非就是經過Trigger觸發一個BeginStoryboard,裏面放一個Storyboard包裝的動畫。以下:

<Grid.Triggers>
    <EventTrigger RoutedEvent="SizeChanged">
        <BeginStoryboard>
            <Storyboard >
                <DoubleAnimation Storyboard.TargetName="Bar" Storyboard.TargetProperty="Tag"
                     From="0" To="1" Duration="{StaticResource FlowDuration}"/>
            </Storyboard>
        </BeginStoryboard>
    </EventTrigger>
</Grid.Triggers>

咱們經過EventTrigger觸發動畫,而這個Event就是控件Size發生變化。可能你會比較奇怪爲啥動畫修改的不是Width屬性而是一個名爲Tag的屬性。

真相是因爲不能將動畫的To的值設置爲進度條的寬度(這個From和To的值只能是一個常量值),因此在網上找到這種變通的方案(出處見下面代碼的註釋),動畫控制一個比例值。而後進度條的Width綁定到其寬度可能的最大值*比例值。From和To設置的是這個比例的最大最小值。

這個進度條寬度的最大值經過一個名爲ShadowWidth的屬性來存儲。其也是在控件佈局時被計算:

SetValue(ShadowWidthProperty, constraint.Width - BarMarginLeft * 2);

有了最大值和比例值,只需的經過一個多值綁定和轉換器變爲進度條的實際尺寸就能夠了。

<Rectangle.Width>
    <MultiBinding Converter="{StaticResource FlowMultiWidthAnimationConverter}">
        <Binding Path="ShadowWidth" RelativeSource="{RelativeSource TemplatedParent}" />
        <Binding Path="Tag" RelativeSource="{RelativeSource Self}" />
    </MultiBinding>
</Rectangle.Width>

多值轉換器實現很簡單,就是把傳入參數相乘並返回:

// stackoverflow.com/questions/2186933/wpf-animation-binding-to-the-to-attribute-of-storyboard-animation
public class MultiWidthAnimationConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        double result = 1.0;
        for (int i = 0; i < values.Length; i++)
        {
            if (values[i] is double)
                result *= (double)values[i];
        }

        return result;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new Exception("Not implemented");
    }
}

進度條基本上就這些內容了。下面看看節點的實現。

 

節點的佈局主要經過一個自定義的Panel實現:

public class FlowControlPanel : Panel
{
    public static readonly DependencyProperty AnimationDurationProperty = DependencyProperty.Register(
        "AnimationDuration", typeof (Duration), typeof (FlowControlPanel), new PropertyMetadata(default(Duration)));

    public Duration AnimationDuration
    {
        get { return (Duration) GetValue(AnimationDurationProperty); }
        set { SetValue(AnimationDurationProperty, value); }
    }

    protected override Size MeasureOverride(Size availableSize)
    {
        var s = base.MeasureOverride(availableSize);
        foreach (UIElement element in this.Children)
        {
            element.Measure(availableSize);
        }
        return availableSize;
    }
    protected override Size ArrangeOverride(Size finalSize)
    {
        const double y = 0;
        double margin = 0;

        foreach (UIElement child in Children)
        {
            var newMargin = child.DesiredSize.Width / 2;
            if (newMargin > margin)
            {
                margin = newMargin;
            }
        }

        //double lastX = 0; todo
        foreach (ContentPresenter element in Children)
        {
            var node = element.Content as FlowItem;
            var x = Convert.ToDouble(node.OffsetRate) * (finalSize.Width - margin * 2);
            element.Arrange(new Rect(0, y, element.DesiredSize.Width, element.DesiredSize.Height));

            //方法來自http://www.mgenware.com/blog/?p=326
            var transform = element.RenderTransform as TranslateTransform;
            if (transform == null)
                element.RenderTransform = transform = new TranslateTransform();

            transform.BeginAnimation(TranslateTransform.XProperty, new DoubleAnimation(0, x, AnimationDuration));

        }
        return finalSize;
    }
}

給節點進行佈局主要發生在ArrangeOverride中。取出每一個節點對象中存儲的OffsetRate的值乘以節點能夠佔據的最終寬度即節點的最終位置(x值,y值固定爲0)。這個節點佔據的寬度不是使用的進度條的寬度,而是用控件(面板)的最終尺寸減去一個最寬節點的寬度的一半乘二。由於節點標題的存在這個節點可分佈的寬度要比進度條的寬度小。並且節點標題的寬度還不能太寬。

標題寬度經過Converter作了限制,由於進度條只能根據節點圓球的寬度進行適應,而沒法根據節點實際寬度--即算上標題的寬度--進行適應,若是不限制標題長度,太長的標題會致使兩頭節點位置與進度條不匹配。

獲得節點最終位置後,還經過一個小技巧把這個佈局過程變成一個動畫。動畫的持續時間經過自定義模板中的依賴屬性獲取。傳遞給自定義模板的動畫時間和傳遞給進度條動畫的時間是同一個XAML常量值,這樣更改持續時間時,能夠很方便的讓兩個不一樣位置的動畫保持一致。

經過以下的XAML將自定義Panel設置給控件的ItemsPanel屬性(繼承自ItemsControl控件)。

<Setter Property="ItemsPanel">
    <Setter.Value>
        <ItemsPanelTemplate>
            <flow:FlowControlPanel AnimationDuration="{StaticResource FlowDuration}" />
        </ItemsPanelTemplate>
    </Setter.Value>
</Setter>

這樣設置給控件的節點項就能夠按咱們但願的方式顯示出來。(下面代碼是調用控件的代碼,咱們經過MVVM方式使用控件)

<flow:FlowControl HorizontalAlignment="Stretch" Margin="0 0 0 0"
                      Padding="30 0" AnimationDuration="0:0:0.5"
                      ItemsSource="{Binding Nodes}" >
                      ...

其中Nodes的聲明和初始化(ViewModel中須要完成的):

private ObservableCollection<FlowItem> _nodes;

public ObservableCollection<FlowItem> Nodes
{
    get { return _nodes; }
    set { Set(() => Nodes, ref _nodes, value); }
}

_dataService.GetData(
    (item, error) =>
    {
        Nodes = new ObservableCollection<FlowItem>(

            new List<FlowItem>()
            {
                new FlowItem() {Id = 1, OffsetRate = 0, Title = "接到報修"},
                new FlowItem() {Id = 2, OffsetRate = 0.5, Title = "派工完成"},
                new FlowItem() {Id = 3, OffsetRate = 0.75, Title = "維修完成"},
                new FlowItem() {Id = 3, OffsetRate = 1, Title = "客戶確認(我是特別長的標題)"},
            }
            );
    });

能夠看到從ItemsControl繼承的好處就是咱們馬上有了ItemsSource屬性,給其賦值後就能夠在Panel中訪問到這些item,進行佈局等操做。另外咱們也獲得了經過ItemTemplate設置item模板的能力,這些都無需本身另外實現:

<flow:FlowControl.ItemTemplate>
    <DataTemplate>
        <flow:FlowNodeControl Id="{Binding Id}"
            NodeTitle="{Binding Title}"
            OffsetRate="{Binding OffsetRate}"></flow:FlowNodeControl>
    </DataTemplate>
</flow:FlowControl.ItemTemplate>

能夠看到咱們給Item賦的模板是另外一個 TemplatedControl,這個控件用來表示一個進度節點:

這個控件模板結構很簡單:

<Style TargetType="flow:FlowNodeControl">
    <Setter Property="NodeWidth" Value="30"></Setter>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="flow:FlowNodeControl">
                <StackPanel Orientation="Vertical">
                    <RadioButton x:Name="PART_NodeRadioButton" GroupName="FlowNodeGroup" Width="{TemplateBinding NodeWidth}" Height="{TemplateBinding NodeWidth}" Style="{StaticResource FlowNodeRadioButton}"></RadioButton>
                    <TextBlock Text="{TemplateBinding NodeTitle}" TextWrapping="Wrap" MaxWidth="{TemplateBinding NodeWidth,Converter={StaticResource FlowTitleMaxWidthConverter}}"></TextBlock>
                </StackPanel>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

其中就是一個RadioButton和一個TextBlock,分別用來表示綠色的節點圓圈和下面的的進度文本。另外給RadioButton定義了一套新的控件模板,用來實現進度節點被按下時的不一樣樣式。

<Style x:Key="FlowNodeRadioButton" TargetType="RadioButton">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate>
                <Grid>
                    <Ellipse x:Name="Border" StrokeThickness="1">
                        <Ellipse.Fill>
                            <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                                <GradientStop Color="#91c885" Offset="0" />
                                <GradientStop Color="#65b254" Offset="1" />
                            </LinearGradientBrush>
                        </Ellipse.Fill>
                    </Ellipse>
                    <Ellipse x:Name="CheckMark" Margin="4" Visibility="Collapsed">
                        <Ellipse.Fill>
                            <SolidColorBrush Color="#20830a" />
                        </Ellipse.Fill>
                    </Ellipse>


                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="CommonStates">
                            <VisualState x:Name="Normal" />
                            <VisualState x:Name="MouseOver">
                                <Storyboard>
                                    <ColorAnimationUsingKeyFrames 
                                        Storyboard.TargetName="Border" 
                                        Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)">
                                        <EasingColorKeyFrame KeyTime="0" Value="#399c24" />
                                    </ColorAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Pressed">
                                <Storyboard>
                                    <ColorAnimationUsingKeyFrames 
                                        Storyboard.TargetName="Border"
                                        Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)">
                                        <EasingColorKeyFrame KeyTime="0" Value="#20830a" />
                                    </ColorAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Disabled">
                                <Storyboard>
                                    <ColorAnimationUsingKeyFrames Storyboard.TargetName="Border"
                                            Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[0].(GradientStop.Color)">
                                        <EasingColorKeyFrame KeyTime="0" Value="#c1cbcb" />
                                    </ColorAnimationUsingKeyFrames>
                                    <ColorAnimationUsingKeyFrames Storyboard.TargetName="Border"
                                            Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)">
                                        <EasingColorKeyFrame KeyTime="0" Value="#a0abab" />
                                    </ColorAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                        <VisualStateGroup x:Name="CheckStates">
                            <VisualState x:Name="Checked">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="CheckMark"
                                        Storyboard.TargetProperty="(UIElement.Visibility)">
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Visible}" />
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Unchecked" />
                            <VisualState x:Name="Indeterminate" />
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

節點控件的代碼:

[TemplatePart(Name = "PART_NodeRadioButton", Type = typeof(RadioButton))]
public class FlowNodeControl : System.Windows.Controls.Control
{
    static FlowNodeControl()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(FlowNodeControl), new FrameworkPropertyMetadata(typeof(FlowNodeControl)));
    }

    #region Dependency Property

    public static readonly DependencyProperty OffsetRateProperty = DependencyProperty.Register(
        "OffsetRate", typeof(double), typeof(FlowNodeControl), new PropertyMetadata(default(double)));

    public double OffsetRate
    {
        get { return (double)GetValue(OffsetRateProperty); }
        set { SetValue(OffsetRateProperty, value); }
    }

    public static readonly DependencyProperty NodeTitleProperty = DependencyProperty.Register(
        "NodeTitle", typeof(string), typeof(FlowNodeControl), new PropertyMetadata(string.Empty));

    public string NodeTitle
    {
        get { return (string)GetValue(NodeTitleProperty); }
        set { SetValue(NodeTitleProperty, value); }
    }

    //用於向上通知哪一個Node被點擊
    public static readonly DependencyProperty IdProperty = DependencyProperty.Register(
        "Id", typeof(int), typeof(FlowNodeControl), new PropertyMetadata(default(int)));

    public int Id
    {
        get { return (int)GetValue(IdProperty); }
        set { SetValue(IdProperty, value); }
    }

    private const double NodeWidthDefault = 30;
    public static readonly DependencyProperty NodeWidthProperty = DependencyProperty.Register(
        "NodeWidth", typeof(double), typeof(FlowNodeControl), new PropertyMetadata(NodeWidthDefault));

    public double NodeWidth
    {
        get { return (double)GetValue(NodeWidthProperty); }
        set { SetValue(NodeWidthProperty, value); }
    }

    #endregion

    private RadioButton nodeRadioButton;

    public override void OnApplyTemplate()
    {
        if (nodeRadioButton != null)
        {
            nodeRadioButton.Click -= nodeRadioButton_Click;
        }

        base.OnApplyTemplate();

        nodeRadioButton = GetTemplateChild("PART_NodeRadioButton") as RadioButton;

        if (nodeRadioButton != null)
        {
            nodeRadioButton.Click += nodeRadioButton_Click;
        }
    }

    void nodeRadioButton_Click(object sender, RoutedEventArgs e)
    {
        RaiseEvent(new RoutedEventArgs(NodeSelectedEvent,this));
    }

    //route event
    public static readonly RoutedEvent NodeSelectedEvent = EventManager.RegisterRoutedEvent(
            "NodeSelected", RoutingStrategy.Bubble,
            typeof(RoutedEventHandler),
            typeof(FlowNodeControl));

    public event RoutedEventHandler NodeSelected
    {
        add { AddHandler(NodeSelectedEvent, value); }
        remove { RemoveHandler(NodeSelectedEvent, value); }
    }
}

其中這行:

[TemplatePart(Name = "PART_NodeRadioButton", Type = typeof(RadioButton))]

說明控件模板中須要定義一個名爲PART_NodeRadioButton的RadioButton,由於WPF容許控件使用者自行替換控件模板,這樣的聲明能夠提示模板建立者模板中這個元素對於控件必不可少必定要存在。

最後一個須要介紹的功能就是點擊進度節點觸發控件中訂閱事件的方法。

事件的來源是咱們這個節點控件FlowNodeControl中的RadioButton。爲了讓事件能夠向上傳播在FlowNodeControl中定義了一個路由事件NodeSelected:

//route event
public static readonly RoutedEvent NodeSelectedEvent = EventManager.RegisterRoutedEvent(
        "NodeSelected", RoutingStrategy.Bubble,
        typeof(RoutedEventHandler),
        typeof(FlowNodeControl));

public event RoutedEventHandler NodeSelected
{
    add { AddHandler(NodeSelectedEvent, value); }
    remove { RemoveHandler(NodeSelectedEvent, value); }
}

爲了能在RadioButton被點擊時觸發這個路由事件,在代碼獲取RadioButton對象並手動給它關聯事件處理(事件處理即觸發路由事件):

public override void OnApplyTemplate()
{
    if (nodeRadioButton != null)
    {
        nodeRadioButton.Click -= nodeRadioButton_Click;
    }

    base.OnApplyTemplate();

    nodeRadioButton = GetTemplateChild("PART_NodeRadioButton") as RadioButton;

    if (nodeRadioButton != null)
    {
        nodeRadioButton.Click += nodeRadioButton_Click;
    }
}

如代碼所示,OnApplyTemplate方法通常是獲取模板中元素對應的對象的引用的地方。獲取對象後給起Click事件添加處理。

接下來還須要把FlowNodeControl中的路由事件向上傳遞到FlowControl中,咱們須要在FlowControl中定義路由事件,但不一樣於FlowNodeControl中,這裏不是新註冊一個路由事件,而是經過下面的語法告知系統FlowControl也能夠處理NodeSelectedEvent事件,這樣若是FlowNodeControl沒有處理事件,事件將向上傳播。

//route event
public static readonly RoutedEvent NodeSelectedEvent =
    FlowNodeControl.NodeSelectedEvent.AddOwner(typeof(FlowControl));

public event RoutedEventHandler NodeSelected
{
    add { AddHandler(FlowNodeControl.NodeSelectedEvent, value, false); }
    remove { RemoveHandler(FlowNodeControl.NodeSelectedEvent, value); }
}

這樣咱們在使用FlowControl控件時給其NodeSelected事件綁定一個Command就能夠了:

<i:Interaction.Triggers>
    <i:EventTrigger EventName="NodeSelected">
        <command:EventToCommand Command="{Binding NodeClickCommand, Mode=OneWay}" PassEventArgsToCommand="True" />
    </i:EventTrigger>
</i:Interaction.Triggers>

在NodeClickCommand中能夠獲取被點擊的節點(節點就是事件的原始觸發源):

private RelayCommand<RoutedEventArgs> _nodeClickCommand;

public RelayCommand<RoutedEventArgs> NodeClickCommand
{
    get
    {
        return _nodeClickCommand
            ?? (_nodeClickCommand = new RelayCommand<RoutedEventArgs>(
                                  p =>
                                  {
                                      var aa = p;
                                      MessageBox.Show(((FlowNodeControl)aa.OriginalSource).NodeTitle);
                                  }));
    }
}

 

基本上上面這些就把整個控件設計實現使用介紹清楚了,但願能給WPF新手以幫助,也但願WPF大神能給與更好的解決方案拓展下博主的眼界。

 

代碼下載

Github 

 

版權說明:本文版權歸博客園和hystar全部,轉載請保留本文地址。文章代碼能夠在項目隨意使用,若是以文章出版物形式出現請代表來源,尤爲對於博主引用的代碼請保留其中的原出處尊重原做者勞動成果。

相關文章
相關標籤/搜索