【WPF学习】第六十五章 建立无外观控件

  用户控件的目标是提供增补控件模板的设计表面,提供一种定义控件的快速方法,代价是失去了未来的灵活性。若是喜欢用户控件的功能,但须要修改使其可视化外观,使用这种方法就有问题了。例如,设想但愿使用相同的颜色拾取器,但但愿使用不一样的“皮肤”,将其更好地融合到已有的应用程序窗口中。能够经过样式来改变用户控件的某些方面,但该控件的一些部分是在内部锁定,并硬编码到标记中。例如,没法将预览矩形移动到滑动条的左边。ide

  解决方法是建立无外观控件——继承自控件基类,但没有设计表面的控件。相反,这个控件将其标记放到默认模板中,可替换默认模板而不会影响控件逻辑。函数

1、修改颜色拾取器的代码布局

  将颜色拾取器改为无外观控件并不难。第一步很容易——只须要改变类的声明,以下所示:this

public class ColorPicker:System.Windows.Controls.Control
    {

    }

  在这个示例中,ColorPicker类继承自Control类。继承自FrameworkElement类是不合适的,由于颜色拾取器容许与用户进行交互,并且其余高级的类不能准确地描述颜色拾取器的行为。例如,颜色拾取器不容许在内部嵌套其余内容,因此继承自ContentControl类也是不合适的。编码

  ColorPicker类中的代码与用于用户控件的代码是相同的(除了必须删除构造函数中的InitializeComponent()方法调用)。可以使用相同的方法定义依赖项属性和路由事件。惟一的区别是须要通知WPF,将为控件类提供新样式。该样式将提供新的控件模板(若是不执行该步骤,将继续使用在基类中定义的模板)。spa

  为通知WPF正在提供新的样式,须要在子弹女工艺控件类的静态构造函数中调用OverrideMetadata()方法。须要在DefaultStyleKeyProperty属性上调用该方法,该属性是为自定义控件定义默认样式的依赖性属性。须要的代码以下所示:设计

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

  若是但愿使用其余控件类的模板,可提供不一样的类型,但几乎老是为每一个自定义控件建立特定的样式。双向绑定

2、修改颜色拾取器的标记code

  添加对OverrideMetadata()方法的调用后,只须要插入正确的样式。须要将样式放在名为generic.xaml的资源字典中,该资源字典必须放在项目文件夹的Themes子文件夹中。这样,该样式就会被识别为自定义控件的默认样式。下面列出添加generic.xaml文件的具体步骤:component

  (1)在Solution Explorer中右键类库项目,并选择Add|New Folder菜单项。

  (2)将新建文件夹命名为Themes。

  (3)右击Themes文件夹,并选择Add|New Item菜单项。

  (4)在Add New Item对话框中选择资源字典,输入名称generic.xaml,并单击Add按钮。

  下图显示了Themes文件夹中的generic.xaml文件。

 

 

   一般,自定义控件库会包含几个控件。为了保持它们的样式相互独立以便编辑,generic.xaml文件一般使用资源字典合并功能。下面是标记显示了generic.xaml文件,该文件从ColorPicker.xaml资源字典中提取资源,该资源字典位于CustomControls控件库的Themes文件夹中:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="/CustomControls;component/Themes/ColorPicker.xaml">
        </ResourceDictionary>
    </ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

  自定义的控件样式必须使用TargetType特性来将自身自动关联到颜色拾取器。下面是ColorPicker.xaml文件中标记的基本结构:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:CustomControls">
    <Style TargetType="{x:Type local:ColorPicker}">
        ...
    </Style>
</ResourceDictionary>

  可以使用样式设置控件类中的任意属性(不管是继承自基类的属性仍是新增属性)。但在此,样式最有用的任务是应用新目标,新目标定义了控件的默承认视化外观。

  很容易就能将普通标记(如颜色拾取器使用的标记)转换到控件目标中。但要注意如下几点:

  •   当建立连接到父控件类属性的绑定表达式时,不能使用ElementName属性。而须要使用RelativeSource属性指示但愿绑定到父控件。若是单向绑定彻底可以知足须要,一般可使用轻量级的TemplateBinding标记表达式,而不须要使用功能完备的数据绑定。
  •   不能在控件模板中关联事件处理程序。相反,须要为元素提供可以识别的名称,并在控件构造函数中经过代码为他们关联处理程序。
  •   除非但愿关联事件处理程序或经过代码与它进行交互,不然不要在控件模板中命名元素。当命名但愿使用的元素时,使用“PART_元素名”的形式进行命名。

  遵循上面几点,可为颜色拾取器建立如下模板:

 <Style TargetType="{x:Type local:ColorPicker}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:ColorPicker}">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition></ColumnDefinition>
                            <ColumnDefinition Width="Auto"/>
                        </Grid.ColumnDefinitions>
                        <Slider Minimum="0" Maximum="255"
                                Margin="{TemplateBinding Padding}"
                                Value="{Binding Path=Red,
                             RelativeSource={RelativeSource TemplatedParent}}"/>
                        <Slider Grid.Row="1" Minimum="0" Maximum="255"
                                Margin="{TemplateBinding Padding}"
                                Value="{Binding Path=Green,
                             RelativeSource={RelativeSource TemplatedParent}}"/>
                        <Slider Grid.Row="2" Minimum="0" Maximum="255"
                                Margin="{TemplateBinding Padding}"
                                Value="{Binding Path=Blue,
                             RelativeSource={RelativeSource TemplatedParent}}"/>
                        <Rectangle Grid.Column="1" Grid.RowSpan="3"
                                   Margin="{TemplateBinding Padding}" Width="50"
                                   Stroke="Black" StrokeThickness="1">
                            <Rectangle.Fill>
                                <SolidColorBrush
                                    Color="{Binding Path=Color,RelativeSource={RelativeSource TemplatedParent}}"></SolidColorBrush>
                            </Rectangle.Fill>
                        </Rectangle>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

  正如上面看到的,本例已用TemplateBinding扩展提到一些绑定表达式。其余一些绑定表达式仍使用Binding扩展,但将RelativeSource设置为指向模板的父元素(自定义控件)。尽管TemplateBinding和将RelativeSource属性设置为TemplatedParent值得Binding的做用相同——从自定义控件的属性中提取数据——可是使用量级更轻的TemplateBinding老是合适的。若是须要双向绑定(与滑动条同样)或绑定到继承自Freezable的类(如SolidColorBrush类)的属性,TemplateBinding就不能工做了。

3、精简控件模板

  经过上面设计,颜色拾取器控件模板填充了须要的所有内容,可按与使用颜色拾取器相同的方式来使用。然而,仍可经过移除一些细节来简化模板。

  如今,全部但愿提供自定义模板的控件使用这必须添加大量的绑定表达式,已确保控件可以继续工做。这并不难,可是很繁琐。另外一种选择是,在控件自身的初始化代码中配置全部绑定表达式。这样,模板就不须要指定这些细节了。

  一、添加部件名称

  为了让这一系统可以工做,代码要能找到所需的元素。WPF控件经过名称定为它们须要的元素。因此,元素的名称变成自定义控件公有接口的一部分,并且须要恰当的描述性名称。根据约定,这些名称以PART_开头,后跟元素名称。元素名称的首字母要大写,就像数学名称。对于须要的元素名称,PART_RedSlider是合适的选择,而PART_sldRed、PART_redSlider以及RedSlider等名称都不合适。

  例如,下面的标记演示了如何经过删除三个滚动条的Value数学的绑定表达式,并为三个滑动条添加PART_名称,从而为经过代码设置绑定作好准备。

<Slider Name="PART_RedSlider" Minimum="0" Maximum="255"
                                Margin="{TemplateBinding Padding}"
                               />
<Slider Name="PART_GreemSlider" Grid.Row="1" Minimum="0" Maximum="255"
                                Margin="{TemplateBinding Padding}"
                                />
<Slider Name="PART_BlueSlider" Grid.Row="2" Minimum="0" Maximum="255"
                                Margin="{TemplateBinding Padding}"
                                />

  注意,Margin数学仍使用绑定表达式添加内边距,但这是一个可选的细节,能够很容易地从自定义模板中去掉该细节(可选择硬编码内边距或者使用不一样的布局),

  为确保得到更大的灵活性,这是没有为Rectangle元素提供名称,而是为其内部的SolidColorBrush指定了名称。这样,可根据模板为颜色预览功能使用任何形状或任意元素。

<Rectangle Grid.Column="1" Grid.RowSpan="3"
                                   Margin="{TemplateBinding Padding}" Width="50"
                                   Stroke="Black" StrokeThickness="1">
                            <Rectangle.Fill>
                                <SolidColorBrush
                                    x:Name="PART_PreviewBrush"></SolidColorBrush>
                            </Rectangle.Fill>
</Rectangle>

  二、操做模板部件

  在初始化控件后,可链接绑定表达式,但有一种更好的方法。WPF有一个专用的OnApplyTemplate()方法,若是须要在模板中查找元素并关联事件处理程序或添加数据绑定表达式,应重写该方法。在该方法中,能够经过GetTemplateChild()方法查找所需的元素。

  若是没有找到但愿处理的元素,推荐的模式就不起做用。也可添加代码来检索该元素,若是元素存在,在检查类型是否正确;若是类型不正确,就引起异常。

  下面的代码演示了OnApplyTemplate()方法使用:

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            RangeBase slider = GetTemplateChild("PART_RedSlider") as RangeBase;
            if (slider != null)
            {
                Binding binding = new Binding("Red");
                binding.Source = this;
                binding.Mode = BindingMode.TwoWay;
                slider.SetBinding(RangeBase.ValueProperty, binding);
            }
            slider = GetTemplateChild("PART_GreenSlider") as RangeBase;
            if (slider != null)
            {
                Binding binding = new Binding("Green");
                binding.Source = this;
                binding.Mode = BindingMode.TwoWay;
                slider.SetBinding(RangeBase.ValueProperty, binding);
            }
            slider = GetTemplateChild("PART_BlueSlider") as RangeBase;
            if (slider != null)
            {
                Binding binding = new Binding("Blue");
                binding.Source = this;
                binding.Mode = BindingMode.TwoWay;
                slider.SetBinding(RangeBase.ValueProperty, binding);
            }

            SolidColorBrush brush = GetTemplateChild("PART_PreviewBrush") as SolidColorBrush;
            if (brush != null)
            {
                Binding binding = new Binding("Color");
                binding.Source = brush;
                binding.Mode = BindingMode.OneWayToSource;
                this.SetBinding(ColorPicker.ColorProperty, binding);
            } 
        }

  注意,上面代码使用的是System.Windows.Controls.Primitives.RangeBase类(Slider类继承自该类)而不是Slider类。由于RangeBase类提供了须要的最小功能——在本例中是中Value属性。经过尽量提升代码的通用性,控件使用者可得到更大自由。例如,如今可提供自定义模板,使用不一样的派生自RangeBase类的控件代替颜色滑动条。

  绑定SolidColorBrush画刷的代码稍有区别,由于SolidColorBrush画刷美誉包含SetBinding()方法(该方法是在FrameworkElement类中定义的)。一个比较容易得变通方法是为ColorPicker.Color属性建立绑定表达式,使用指向源方向的单向绑定。这样,当颜色拾取器的颜色改变后,将自动更新画刷。

  为查看这种设计变化的优势,须要建立一个使用颜色拾取器的控件,并提供一个新的控件模板。

  三、记录模板部件

  对于上面的示例,还有最后一处应予改进。良好的设计指导原则建议为控件声明添加TemplatePart特性,以记录在控件模板中使用了哪些部件名称,以及为每一个部件使用了什么类型的控件。从技术角度看,这一步不是必须的,但该文档可为其余使用自定义类的用户提供帮助。

  下面是应当为ColorPicker控件类添加的TemplatePart特性:

[TemplatePart(Name = "PART_RedSlider", Type = typeof(RangeBase))]
[TemplatePart(Name = "PART_BlueSlider", Type = typeof(RangeBase))]
[TemplatePart(Name = "PART_GreenSlider", Type = typeof(RangeBase))]
[TemplatePart(Name = "PART_PreviewBrush", Type = typeof(SolidColorBrush))]
public class ColorPicker:System.Windows.Controls.Control
{
}

  本实例源码:CustomControlsV2.0.zip

相关文章
相关标签/搜索