WPF自定义控件第一 - 进度条控件

WPF自定义控件第一 - 进度条控件
原文: WPF自定义控件第一 - 进度条控件

本文主要针对WPF新手,高手可以直接忽略,更希望高手们能给出一些更好的实现思路。

前期一个小任务需要实现一个类似含步骤进度条的控件。虽然对于XAML的了解还不是足够深入,还是摸索着做了一个。这篇文章介绍下实现这个控件的步骤,最后会放出代码。还请高手们给出更好的思路。同时也希望这里的思路能给同道中人一些帮助。话不多说,开始正题。

实现中的一些代码采用了网上现有的方案,代码中通过注释标记了来源,再次对代码作者一并表示感谢。

 

首先放一张最终效果图。

 

节点可以被点击

WPF自定义控件第一 - 进度条控件_第1张图片

 

控件会根据绑定的集合数据生成一系列节点,根据集合中的数据还可以按比例放置节点的位置。

节点的实体代码如下:

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的功能)。

 这样控件的模板就可以移入FlowControl.xaml中,接着我们就看一下这里面控件模板的定义:



    
    
    0
    0:0:1.5

    
        
        
            
                
                    
                        
                            
                                
                                    
                                        
                                    
                                
                            
                        
                        
                            
                                
                                    
                                    
                                    
                                    
                                
                            
                            
                                0.0
                            
                            
                                
                                    
                                    
                                
                            
                        

                        
                    
                
            
        
        
            
                
                    
                
            
        
    

这个xaml文件的根节点是ResourceDictionary,表示其中内容是各种资源:样式,模板等等..

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

然后就是TemplatedControl最核心的部分,Control Template的定义:


        
            
                
                   ...控件模板内容...
                
            
        

其中就是一个RadioButton和一个TextBlock,分别用来表示绿色的节点圆圈和下面的的进度文本。另外给RadioButton定义了一套新的控件模板,用来实现进度节点被按下时的不同样式。


    
        
            
                
                    
                        
                            
                                
                                
                            
                        
                    
                    
                        
                            
                        
                    


                    
                        
                            
                            
                                
                                    
                                        
                                    
                                
                            
                            
                                
                                    
                                        
                                    
                                
                            
                            
                                
                                    
                                        
                                    
                                    
                                        
                                    
                                
                            
                        
                        
                            
                                
                                    
                                        
                                    
                                
                            
                            
                            
                        
                    
                
            
        
    

节点控件的代码:

[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就可以了:


    
        
    

在NodeClickCommand中可以获取被点击的节点(节点就是事件的原始触发源):

private RelayCommand _nodeClickCommand;

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

 

基本上上面这些就把整个控件设计实现使用介绍清楚了,希望能给WPF新手以帮助,也希望WPF大神能给与更好的解决方案拓展下博主的眼界。

 

代码下载

Github 

 

版权说明:本文版权归博客园和hystar所有,转载请保留本文地址。文章代码可以在项目随意使用,如果以文章出版物形式出现请表明来源,尤其对于博主引用的代码请保留其中的原出处尊重原作者劳动成果。

 

posted on 2018-08-08 09:18 NET未来之路 阅读( ...) 评论( ...) 编辑 收藏

转载于:https://www.cnblogs.com/lonelyxmas/p/9440841.html

你可能感兴趣的:(WPF自定义控件第一 - 进度条控件)