zoukankan      html  css  js  c++  java
  • [WPF 自定义控件]了解如何自定义ItemsControl

    1. 前言

    对WPF来说ContentControl和ItemsControl是最重要的两个控件。

    顾名思义,ItemsControl表示可用于呈现一组Item的控件。大部分时候我们并不需要自定义ItemsControl,因为WPF提供了一大堆ItemsControl的派生类:HeaderedItemsControl、TreeView、Menu、StatusBar、ListBox、ListView、ComboBox;而且配合Style或DataTemplate足以完成大部分的定制化工作,可以说ItemsControl是XAML系统灵活性的最佳代表。不过,既然它是最常用的控件,那么掌握一些它的原理对所有WPF开发者都有好处。

    我以前写过一篇文章介绍如何模仿ItemsControl,并且博客园也已经很多文章深入介绍ItemsControl的原理,所以这篇文章只介绍简单的自定义ItemsControl知识,通过重写GetContainerForItemOverride和IsItemItsOwnContainerOverride、PrepareContainerForItemOverride函数并使用ItemContainerGenerator等自定义一个简单的IItemsControl控件。

    2. 介绍作为例子的Repeater

    作为教学我创建了一个继承自ItemsControl的控件Repeater(虽然简单,用来展示资料的话好像还真的有点用)。它的基本用法如下:

    <local:Repeater>
        <local:RepeaterItem Content="1234999"
                            Label="Product ID" />
        <local:RepeaterItem Content="Power Projector 4713"
                            Label="IGNORE" />
        <local:RepeaterItem Content="Projector (PR)"
                            Label="Category" />
        <local:RepeaterItem Content="A very powerful projector with special features for Internet usability, USB"
                            Label="Description" />
    </local:Repeater>
    

    也可以不直接使用Items,而是绑定ItemsSource并指定DisplayMemberPath和LabelMemberPath。

    public class Product
    {
        public string Key { get; set; }
    
        public string Value { get; set; }
    
        public static IEnumerable<Product> Products
        {
            get
            {
                return new List<Product>
                {
                    new Product{Key="Product ID",Value="1234999" },
                    new Product{Key="IGNORE",Value="Power Projector 4713" },
                    new Product{Key="Category",Value="Projector (PR)" },
                    new Product{Key="Description",Value="A very powerful projector with special features for Internet usability, USB" },
                    new Product{Key="Price",Value="856.49 EUR" },
                };
    
            }
        }
    }
    
    
    <local:Repeater ItemsSource="{x:Static local:Product.Products}"
                    DisplayMemberPath="Value"
                    LabelMemberPath="Key"/>
    

    运行结果如下图:

    3. 实现

    确定好需要实现的ItemsControl后,通常我大致会使用三步完成这个ItemsControl:

    1. 定义ItemContainer
    2. 关联ItemContainer和ItemsControl
    3. 实现ItemsControl的逻辑

    3.1 定义ItemContainer

    派生自ItemsControl的控件通常都会有匹配的子元素控件,如ListBox对应ListBoxItem,ComboBox对应ComboBoxItem。如果ItemsControl的Items内容不是对应的子元素控件,ItemsControl会创建对应的子元素控件作为容器再把Item放进去。

    <ListBox>
        <system:String>Item1</system:String>
        <system:String>Item2</system:String>
    </ListBox>
    

    例如这段XAML中,Item1和Item2是ListBox的LogicalChildren,而它们会被ListBox封装到ListBoxItem,ListBoxItem才是ListBox的VisualChildren。在这个例子中,ListBoxItem可以称作ItemContainer

    ItemsControl派生类的ItemContainer控件要使用父元素名称做前缀、-Item做后缀,例如ComboBox的子元素ComboBoxItem,这是WPF约定俗成的做法(不过也有TabControl和TabItem这种例外)。Repeater也派生自ItemsControl,Repeatertem即为Repeater的ItemContainer控件。

    public RepeaterItem()
    {
        DefaultStyleKey = typeof(RepeaterItem);
    }
    
    public object Label
    {
        get => GetValue(LabelProperty);
        set => SetValue(LabelProperty, value);
    }
    
    public DataTemplate LabelTemplate
    {
        get => (DataTemplate)GetValue(LabelTemplateProperty);
        set => SetValue(LabelTemplateProperty, value);
    }
    
    <Style TargetType="local:RepeaterItem">
        <Setter Property="Padding"
                Value="8" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:RepeaterItem">
                    <Border BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            Background="{TemplateBinding Background}">
                        <StackPanel Margin="{TemplateBinding Padding}">
                            <ContentPresenter Content="{TemplateBinding Label}"
                                              ContentTemplate="{TemplateBinding LabelTemplate}"
                                              VerticalAlignment="Center"
                                              TextBlock.Foreground="#FF777777" />
                            <ContentPresenter x:Name="ContentPresenter" />
                        </StackPanel>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    

    上面是RepeaterItem的代码和DefaultStyle。RepeaterItem继承ContentControl并提供Label、LabelTemplate。DefaultStyle的做法参考ContentControl。

    3.2 关联ItemContainer和ItemsControl

    <Style TargetType="{x:Type local:Repeater}">
        <Setter Property="ScrollViewer.VerticalScrollBarVisibility"
                Value="Auto" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:Repeater}">
                    <Border BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            Background="{TemplateBinding Background}">
                        <ScrollViewer Padding="{TemplateBinding Padding}">
                            <ItemsPresenter />
                        </ScrollViewer>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    

    如上面XAML所示,Repeater的ControlTemplate中需要提供一个ItemsPresenter,用于指定ItemsControl中的各Item摆放的位置。

    [StyleTypedProperty(Property = "ItemContainerStyle", StyleTargetType = typeof(RepeaterItem))]
    public class Repeater : ItemsControl
    {
        public Repeater()
        {
            DefaultStyleKey = typeof(Repeater);
        }
    
        protected override bool IsItemItsOwnContainerOverride(object item)
        {
            return item is RepeaterItem;
        }
    
        protected override DependencyObject GetContainerForItemOverride()
        {
            var item = new RepeaterItem();
            return item;
        }
    }
    

    Repeater的基本代码如上所示。要将Repeater和RepeaterItem关联起来,除了使用约定俗成的命名方式告诉用户,还需要使用下面两步:

    重写 GetContainerForItemOverride
    protected virtual DependencyObject GetContainerForItemOverride () 用于返回Item的Container。Repeater返回的是RepeaterItem。

    重写 IsItemItsOwnContainer
    protected virtual bool IsItemItsOwnContainerOverride (object item),确定Item是否是(或者是否可以作为)其自己的Container。在Repeater中,只有RepeaterItem返回True,即如果Item的类型不是RepeaterItem,就将它作使用RepeaterItem包装起来。

    完成上面几步后,为Repeater设置ItemsSource的话Repeater将会创建对应的RepeaterItem并添加到自己的VisualTree下面。

    使用 StyleTypedPropertyAttribute

    最后可以在Repeater上添加StyleTypedPropertyAttribute,指定ItemContainerStyle的类型为RepeaterItem。添加这个Attribute后在Blend中选择“编辑生成项目的容器(ItemContainerStyle)”就会默认使用RepeaterItem的样式。

    3.3 实现ItemsControl的逻辑

    public string LabelMemberPath
    {
        get => (string)GetValue(LabelMemberPathProperty);
        set => SetValue(LabelMemberPathProperty, value);
    }
    
    /*LabelMemberPathProperty Code...*/
    
    protected virtual void OnLabelMemberPathChanged(string oldValue, string newValue)
    {
        // refresh the label member template.
        _labelMemberTemplate = null;
        var newTemplate = LabelMemberPath;
    
        int count = Items.Count;
        for (int i = 0; i < count; i++)
        {
            if (ItemContainerGenerator.ContainerFromIndex(i) is RepeaterItem RepeaterItem)
                PrepareRepeaterItem(RepeaterItem, Items[i]);
        }
    }
    
    private DataTemplate _labelMemberTemplate;
    
    private DataTemplate LabelMemberTemplate
    {
        get
        {
            if (_labelMemberTemplate == null)
            {
                _labelMemberTemplate = (DataTemplate)XamlReader.Parse(@"
                <DataTemplate xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
                            xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"">
                		<TextBlock Text=""{Binding " + LabelMemberPath + @"}"" VerticalAlignment=""Center""/>
                </DataTemplate>");
            }
    
            return _labelMemberTemplate;
        }
    }
    
    protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
    {
        base.PrepareContainerForItemOverride(element, item);
    
        if (element is RepeaterItem RepeaterItem )
        {
            PrepareRepeaterItem(RepeaterItem,item);
        }
    }
    
    private void PrepareRepeaterItem(RepeaterItem RepeaterItem, object item)
    {
        if (RepeaterItem == item)
            return;
    
        RepeaterItem.LabelTemplate = LabelMemberTemplate;
        RepeaterItem.Label = item;
    }
    

    Repeater本身没什么复杂的逻辑,只是模仿DisplayMemberPath添加了LabelMemberPathLabelMemberTemplate属性,并把这个属性和RepeaterItem的Label和'LabelTemplate'属性关联起来,上面的代码即用于实现这个功能。

    LabelMemberPath和LabelMemberTemplate
    Repeater动态地创建一个内容为TextBlock的DataTemplate,这个TextBlock的Text绑定到LabelMemberPath

    XamlReader相关的技术我在如何使用代码创建DataTemplate这篇文章里讲解了。

    ItemContainerGenerator.ContainerFromIndex
    ItemContainerGenerator.ContainerFromIndex(Int32)返回ItemsControl中指定索引处的Item,当Repeater的LabelMemberPath改变时,Repeater首先强制更新了LabelMemberTemplate,然后用ItemContainerGenerator.ContainerFromIndex找到所有的RepeaterItem并更新它们的Label和LabelTemplate。

    PrepareContainerForItemOverride
    protected virtual void PrepareContainerForItemOverride (DependencyObject element, object item) 用于在RepeaterItem添加到UI前为其做些准备工作,其实也就是为RepeaterItem设置LabelLabelTemplate而已。

    4. 结语

    实际上WPF的ItemsControl很强大也很复杂,源码很长,对初学者来说我推荐参考Moonlight中的实现(Moonlight, an open source implementation of Silverlight for Unix systems),上面LabelMemberTemplate的实现就是抄Moonlight的。Silverlight是WPF的简化版,Moonlight则是很久没维护的Silverlight的简陋版,这使得Moonlight反而成了很优秀的WPF教学材料。

    当然,也可以参考Silverlight的实现,使用JustDecompile可以轻松获取Silverlight的源码,这也是很好的学习材料。不过ItemsControl的实现比Moonlight多了将近一倍的代码。

    5. 参考

    ItemsControl Class (System.Windows.Controls) Microsoft Docs
    moon_ItemsControl.cs at master
    ItemContainer Control Pattern - Windows applications _ Microsoft Docs

  • 相关阅读:
    CodeForces 980 E The Number Games
    CodeForces 980 D Perfect Groups
    【动态规划】The Triangle
    【动态规划】矩形嵌套
    金块问题-排序-找最大最小
    猪八戒吃西瓜(wmelon)-排序-查找
    【贪心】取数游戏
    【贪心】排队接水
    桐桐的贸易--WA
    【贪心】智力大冲浪
  • 原文地址:https://www.cnblogs.com/dino623/p/Custom-ItemsControl.html
Copyright © 2011-2022 走看看