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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM