zoukankan      html  css  js  c++  java
  • WPF学习(10)模板

    在前面一篇我们粗略说了Style和Behaviors,如果要自定义一个个性十足的控件,仅仅用Style和Behaviors是不行的,Style和Behaviors只能通过控件的既有属性来简单改变外观,还需要有ControlTemplate来彻底定制,这是改变Control的呈现,也可以通过DataTemplate来改变Data的呈现,对于ItemsControl,还可以通过ItemsPanelTemplate来改变Items容器的呈现。

    1.模板

    WPF模板有三种:ControlTemplate、DataTemplate和ItemsPanelTemplate,它们都继承自FrameworkTemplate抽象类。在这个抽象类中有一个FrameworkElementFactory类型的VisualTree变量,通过该变量可以设置或者获取模板的根节点,包含了你想要的外观元素树。

    先来看下ControlTemplate的例子:

    <Window x:Class="TemplateDemo.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" Height="350" Width="525">
        <Window.Resources>
            <ControlTemplate x:Key="buttonTemplate" TargetType="{x:Type Button}">
                <Grid>
                    <Ellipse Width="100" Height="100">
                        <Ellipse.Fill>
                            <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                                <GradientStop Offset="0" Color="Cyan" />
                                <GradientStop Offset="1" Color="LightCyan" />
                            </LinearGradientBrush>
                        </Ellipse.Fill>
                    </Ellipse>
                    <Ellipse Width="80" Height="80">
                        <Ellipse.Fill>
                            <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                                <GradientStop Offset="0" Color="Yellow" />
                                <GradientStop Offset="1" Color="Transparent" />
                            </LinearGradientBrush>
                        </Ellipse.Fill>
                    </Ellipse>
                    <ContentPresenter Content="{TemplateBinding Content}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
                </Grid>
            </ControlTemplate>
        </Window.Resources>
        <Grid>
            <StackPanel>
                <Button Content="Hi,WPF" Template="{StaticResource buttonTemplate}" Click="Button_Click"/>
            </StackPanel>
        </Grid>
    </Window>
    View Code

    这里是将ControlTemplate作为资源的方式共享的,当然也可以通过Style的Setter来设置Button的Template属性来做。

    效果如下:

    在该ControlTemplate的VisualTree中,Button是被作为TemplatedParent的,这个属性定义在FrameworkElement和FrameworkContentElement中。关于TemplatedParent的介绍,这里可以看Mgen这篇文章。

    这里完成了一个自定义风格的Button,然后在很多时候,我们只是想稍微修改下Button的外观,仍然像保留其阴影特性等功能,这时候我们就要"解剖"Button来了解其内部结构,VS2012自带的Expression Blend 5就具有这样的解剖功能。

     

    生成了这样的代码:

    <Style x:Key="FocusVisual">
                <Setter Property="Control.Template">
                    <Setter.Value>
                        <ControlTemplate>
                            <Rectangle Margin="2" SnapsToDevicePixels="true" Stroke="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" StrokeThickness="1" StrokeDashArray="1 2"/>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
            <SolidColorBrush x:Key="Button.Static.Background" Color="#FFDDDDDD"/>
            <SolidColorBrush x:Key="Button.Static.Border" Color="#FF707070"/>
            <SolidColorBrush x:Key="Button.MouseOver.Background" Color="#FFBEE6FD"/>
            <SolidColorBrush x:Key="Button.MouseOver.Border" Color="#FF3C7FB1"/>
            <SolidColorBrush x:Key="Button.Pressed.Background" Color="#FFC4E5F6"/>
            <SolidColorBrush x:Key="Button.Pressed.Border" Color="#FF2C628B"/>
            <SolidColorBrush x:Key="Button.Disabled.Background" Color="#FFF4F4F4"/>
            <SolidColorBrush x:Key="Button.Disabled.Border" Color="#FFADB2B5"/>
            <SolidColorBrush x:Key="Button.Disabled.Foreground" Color="#FF838383"/>
            <Style x:Key="ButtonStyle1" TargetType="{x:Type Button}">
                <Setter Property="FocusVisualStyle" Value="{StaticResource FocusVisual}"/>
                <Setter Property="Background" Value="{StaticResource Button.Static.Background}"/>
                <Setter Property="BorderBrush" Value="{StaticResource Button.Static.Border}"/>
                <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
                <Setter Property="BorderThickness" Value="1"/>
                <Setter Property="HorizontalContentAlignment" Value="Center"/>
                <Setter Property="VerticalContentAlignment" Value="Center"/>
                <Setter Property="Padding" Value="1"/>
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="{x:Type Button}">
                            <Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="true">
                                <ContentPresenter x:Name="contentPresenter" Focusable="False" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                            </Border>
                            <ControlTemplate.Triggers>
                                <Trigger Property="IsDefaulted" Value="true">
                                    <Setter Property="BorderBrush" TargetName="border" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
                                </Trigger>
                                <Trigger Property="IsMouseOver" Value="true">
                                    <Setter Property="Background" TargetName="border" Value="{StaticResource Button.MouseOver.Background}"/>
                                    <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource Button.MouseOver.Border}"/>
                                </Trigger>
                                <Trigger Property="IsPressed" Value="true">
                                    <Setter Property="Background" TargetName="border" Value="{StaticResource Button.Pressed.Background}"/>
                                    <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource Button.Pressed.Border}"/>
                                </Trigger>
                                <Trigger Property="IsEnabled" Value="false">
                                    <Setter Property="Background" TargetName="border" Value="{StaticResource Button.Disabled.Background}"/>
                                    <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource Button.Disabled.Border}"/>
                                    <Setter Property="TextElement.Foreground" TargetName="contentPresenter" Value="{StaticResource Button.Disabled.Foreground}"/>
                                </Trigger>
                            </ControlTemplate.Triggers>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>

     看的出来,是由一个Border里面放了一个ContentPresenter构成的,然后是触发器定义的默认行为,相对比较简单,像ScrollBar等控件内部是很复杂的。关于ContentPresent,我们将在第二小节详细描述。

    接下来,我们以Selector中的ListBox为例,来说明DataTemplate和ItemsPanelTemplate。

            <!--ItemsPanelTemplate-->
            <ItemsPanelTemplate x:Key="itemspanel">
                <StackPanel Orientation="Vertical" />
            </ItemsPanelTemplate>
            <!--DataTemplate-->
            <DataTemplate x:Key="datatemplate">
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding ID}" Width="30"/>
                    <TextBlock Text="{Binding Name}" Width="60"/>
                    <Image Source="{Binding imgPath}" Width="30"/>
                </StackPanel>
            </DataTemplate>    

    cs代码:

            List<Student> studentList = new List<Student>()
                {
                    new Student(){ID=1,Name="Rethinker",imgPath="/TemplateDemo;component/Images/1.png"},
                    new Student(){ID=2,Name="Jello",imgPath="/TemplateDemo;component/Images/2.png"},
                    new Student(){ID=3,Name="Taffy",imgPath="/TemplateDemo;component/Images/3.png"}
                };
                this.lbStudentList.ItemsSource = studentList;    

     注意:这里Image的Source采用的是Pack Uri,详细内容请查看WPF中的Pack Uri

     效果如下:

    2.ContentPresenter

    在第一节,我们发现在解剖的Button内部有个叫ContentPresenter的东东,根据名字也许你已经猜到它是干嘛的了,它就是呈现ContentControl的内容的。这里,当我们将ContentPresenter换成TextBlock好像效果也没变化。

    <TextBlock Text="{TemplateBinding Content}" HorizontalAlignment="Center" VerticalAlignment="Center"/>

     我们知道TextBlock的Text属性是String类型,这就限制了其显示的丰富性。有人会说,既然这样那换成ContentControl算了,它可以显示更丰富的东西,类似这样:

    <ContentControl Content="{TemplateBinding Content}" HorizontalAlignment="Center" VerticalAlignment="Center"/>

     看起来也没什么问题,我们知道Button本身就是一个ContentControl,它是一个很重量级的Control,它的Content其实也是通过ContentPresenter来表现的,看起来ContentPresenter是一个更轻量级的Control。另外,ContentPresenter还有一个比较特别的地方,当你未指定它的Content时,它会默认去取该模板的使用者的Content。

    在继承自ItemsControl的控件内部也有个类似的ItemsPresenter,它是来负责Item的展示。

    在ItemsPresenter内部会以ItemsPanelTemplate中的容器作为自己的容器,会以ItemTemplate中的布局作为ListBoxItem的布局。当然,在具体显示内容的地方,还是要用到ContentTemplate的。归根结底,我们可以将Presenter看作是一个占位符,设置了Button的Content,它就获取,否则默认。

    3.TemplatePart机制

    TemplatePart机制是某些WPF控件,如ProgressBar等,通过内建的逻辑来控制控件的可是行为的方式。我们先来解剖下ProgressBar一探究竟。

    我们发现里面有几个很特别的东西,一个名为PART_Track的Rectangle,一个名为PART_Indicator的Grid。这并不是偶然,实际上在ComboBox和TextBox中也有这样类似PART_×××这样命名的元素。在这些类的定义中,我们也会发现一些端倪,例如ProgressBar类的定义:

        [TemplatePart(Name = "PART_GlowRect", Type = typeof(FrameworkElement))]
        [TemplatePart(Name = "PART_Indicator", Type = typeof(FrameworkElement))]
        [TemplatePart(Name = "PART_Track", Type = typeof(FrameworkElement))]
        public class ProgressBar : RangeBase
        {
            
        }

    ProgressBar用了TemplatePart这个Attribute,那它到底如何有何作用呢?实际上,如果在ControlTemplate中找到了这样的元素,就会应用一些附加的行为。

    例如,在ComboBox的空间模板中有个名为PART_Popup的Popup,当它关闭时,ComboBox的DropDownClosed事件会自动触发,如果ComboBox控件模板中有名为PART_EditableTextBox的TextBox,它就会将用户的选项作为它的显示项。在后面的自定义控件这一篇中,我们也将使用它。

    4.如何找Template中的控件

    在Template的基类FrameworkTemplate中有FindName方法,通过它我们可以找到模板中的控件。这个方法对于ControlTemplate和ItemsPanelTemplate很直接有效,但是,在ItemsControl的DataTemplate中,因为展示的数据是集合,所以相对复杂些。在前面我们已经剖析了ListBox内部,这对于找控件是最本质的。我们将前面的DataTemplate稍微修改下,如下:

    <!--DataTemplate-->
            <DataTemplate x:Key="datatemplate">
                <StackPanel x:Name="sp" Orientation="Horizontal">
                    <TextBlock x:Name="tbID" Text="{Binding ID}" Width="30"/>
                    <TextBlock x:Name="tbName" Text="{Binding Name}" Width="60"/>
                    <Image x:Name="tbImgPath" Source="{Binding imgPath}" Width="30"/>
                    <TextBlock x:Name="tbNameLen" Text="{Binding Path=Name.Length}" />
                </StackPanel>
            </DataTemplate>

    要查找由某个ListBoxItem的DataTemplate生成的TextBlock元素,需要获得ListBoxItem,在该ListBoxItem内查找ContentPresenter,然后对在该 ContentPresenter 上设置的 DataTemplate 调用 FindName,在由ListBoxItem查找ContentPresenter时,需要遍历VisualTree,这里给出遍历方法:

    class CommonHelper
        {
            public static T ChildOfType<T>(DependencyObject Parent) where T : DependencyObject
            {
                for (int i = 0; i < VisualTreeHelper.GetChildrenCount(Parent); i++)
                {
                    DependencyObject obj = VisualTreeHelper.GetChild(Parent, i);
                    if (obj != null && obj is T)
                    {
                        return (T)obj;
                    }
                    else
                    {
                        T child = ChildOfType<T>(obj);
                        if (child != null)
                            return child;
                    }
                }
                return default(T);
            }
        }

    在这里通过监听ListBox的SelectionChanged事件来展示效果,cs代码:

    private void lbStudentList_SelectionChanged(object sender, SelectionChangedEventArgs e)
            {
                //第一步:找到ListBoxItem
                ListBoxItem item = this.lbStudentList.ItemContainerGenerator.ContainerFromIndex(this.lbStudentList.SelectedIndex) as ListBoxItem;
                if (item == null) return;
                //第二步:遍历找到ContentPresenter,这里需要写个辅助方法
                ContentPresenter cp = CommonHelper.ChildOfType<ContentPresenter>(item);
                if (cp == null) return;
                //第三步:找到DataTemplate
                DataTemplate dt = cp.ContentTemplate;
                //第四步:通过DataTemplate的FindName方法
                TextBlock tb = dt.FindName("tbName", cp) as TextBlock;
                if (tb != null)
                    MessageBox.Show(tb.Text);
            }

    效果如下:

    最后,推荐几篇比较好的文章:

    1)Creating WPF Data Templates in Code: The Right Way

    2)Customizing WPF Expander with ControlTemplate

  • 相关阅读:
    MySQL数据库时间查询
    MySQL函数转储存(当前月数据同步)
    字节数截取字符串
    JAVA 内部静态类--解析静态内部类的使用目的与限制
    Java集合框架学习总结
    JDBC中的Statement和PreparedStatement的区别
    java jdbc的优化之BeanUtils组件
    jdbc java数据库连接 11)中大文本类型的处理
    jdbc java数据库连接 10)批处理
    jdbc java数据库连接 9)事务编程
  • 原文地址:https://www.cnblogs.com/jellochen/p/3561200.html
Copyright © 2011-2022 走看看