WPF之樣式和控件模板


提到樣式應該要追溯到 Html中的CSS樣式了,通常是為了使多個元素(控件)達到一個效果(外觀或者擁有相同的功能);當然,在WPF中也有類似於CSS這樣,不過肯定是比CSS更要強大的哦。
基本使用

 <StackPanel>
            <Button Height="50" Margin="0 10 0 0" Background="Beige"  Width="100">
                <Button.RenderTransform>
                    <RotateTransform Angle="45"></RotateTransform>
                </Button.RenderTransform>
            </Button>
            <Button Height="50" Margin="0 10 0 0" Background="Beige"  Width="100">
                <Button.RenderTransform>
                    <RotateTransform Angle="45"></RotateTransform>
                </Button.RenderTransform>
            </Button>
            <Button Height="50" Margin="0 10 0 0" Background="Beige"  Width="100">
                <Button.RenderTransform>
                    <RotateTransform Angle="45"></RotateTransform>
                </Button.RenderTransform>
            </Button>
        </StackPanel>

上邊的三個按鈕使用了同樣的傾斜規則,好在我們只有3個按鈕,如果我們有幾十個或者更多呢,實在是看着不舒服,也是對資源的極其浪費.如果我們提取出樣式會是怎樣的呢:

 <UserControl.Resources>
        <Style TargetType="Button">
            <Setter Property="Height" Value="50"></Setter>
            <Setter Property="Width" Value="100"></Setter>
            <Setter Property="Margin" Value="0 10 0 0"></Setter>
            <Setter Property="Background" Value="Beige"></Setter>
            <Setter Property="RenderTransform">
                <Setter.Value>
                    <RotateTransform Angle="45"></RotateTransform>
                </Setter.Value>
            </Setter>
        </Style>
    </UserControl.Resources>

添加了這樣一個Style樣式,切忌要放到Resource中哦(當前是Usercontrol,也可以是Grid或者其他元素的),使用如下:

  <Grid x:Name="LayoutRoot" Background="White">
        <StackPanel>
            <Button>
            </Button>
            <Button>
            </Button>
            <Button>
            </Button>
        </StackPanel>

哈哈,是不是很奇怪呢,我對Button什么也沒有做,看到的效果依然如此

為什么呢,當添加了Style之后,並且沒有對Style設置Key,同時呢設置了TargetType,那么此TargetType的控件就會自動用上你給的樣式哦.

 

樣式的繼承
上邊的使用是最簡單的樣式使用方式,下面有一個新的需求,我們添加了一個RadioButton,同樣也希望旋轉45度,但是呢RadioButton也有自己不同的地方,這時候呢又想重用之前的樣式代碼,怎么辦呢?還好我們有樣式繼承功能BaseOn屬性,看看下邊怎么做.

 
        
 <Window.Resources>
        <Style TargetType="{x:Type Control}">
            <Setter Property="Height" Value="50"></Setter>
            <Setter Property="Width" Value="100"></Setter>
            <Setter Property="Margin" Value="0 10 0 0"></Setter>
            <Setter Property="Background" Value="Beige"></Setter>
            <Setter Property="RenderTransform">
                <Setter.Value>
                    <RotateTransform Angle="45"></RotateTransform>
                </Setter.Value>
            </Setter>
        </Style>
        <Style BasedOn="{StaticResource {x:Type Control}}" TargetType="RadioButton">
            <Setter Property="Foreground" Value="Blue"></Setter>
            <Setter Property="FontSize" Value="50"></Setter>
        </Style>
        <Style BasedOn="{StaticResource {x:Type Control}}" TargetType="Button">
            <Setter Property="FontSize" Value="22"></Setter>
            <Setter Property="Foreground" Value="Red"></Setter>
        </Style>
    </Window.Resources>

 在上邊中主要就是使用了BaseOn屬性來設置一個看起來比較費解的值.第一個Style標簽是我們樣式的基類,基類沒有名字,只指定了TargetType="{x:Type Control}",即樣式應用到所有的控件;兩個子樣式都BaseOn了這個基樣式,BaseOn="{StaticResource {x:Type Control}}",這個式子蠻奇怪,那是因為我們的基類樣式沒有指定Key,如果指定了,那么我們的寫法就是 BaseOn="{StaticResource baseStyleName}",如果樣式沒有指定key,那么就認為是TargetType的值了。效果如下:

    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
        <Button  Content="Test"></Button>
        <Button  Content="Test"></Button>
        <RadioButton ></RadioButton>
    </StackPanel>

三個按鈕都應用了樣式,當然有公共的和自己的樣式.

樣式觸發器
樣式中還有一個很好玩的功能,就是Triggers集合,這個是專門的觸發器集合.首先來看如何通過觸發器實現按鈕懸浮時候改變字體顏色的功能.

        <Style BasedOn="{StaticResource {x:Type Control}}" TargetType="{x:Type Button}">
            <Setter Property="FontSize" Value="22"></Setter>
            <Setter Property="Foreground" Value="Red"></Setter>
            <Style.Triggers>
                <Trigger Property="IsPressed" Value="true">
                    <Setter Property = "Foreground" Value="Green"/>
                </Trigger>
            </Style.Triggers>
        </Style>

 
WPF中有3種觸發器:

1.屬性觸發器:即上例子;

2.數據觸發器(DataTrigger):普通的.net屬性或者是依賴屬性改變時觸發;

3.事件觸發器(EventTrigger):觸發路由事件時會被調用。

 

事件觸發器:

            <ListBox.Triggers>
                <EventTrigger RoutedEvent="ListBox.Loaded">
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetProperty="Opacity" From="0" To="1" Duration="0:0:5"></DoubleAnimation>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </ListBox.Triggers>

上邊的代碼制作了一個ListBox的動畫效果,使ListBox的透明度從0變到1,通過EventTrigger來實現.EventTrigger的RoutedEvent屬性表示發生的事件,上邊代碼表示ListBox的Loaded事件.與屬性觸發器不同,EventTrigger中是TriggerAction的集合,即發生的事件和執行的操作.BeginStroyboard是一種操作,用於開始動畫效果.

數據觸發器:
DataTrigger之所以能夠支持普通.Net屬性,是因為它比屬性觸發器多了一個屬性Binding,通過綁定來指定相關屬性。

   public class Student
    {
        private int _age;

        public int Age
        {
            get { return _age; }
            set { _age = value; }
        }
        private string _name;

        public string Name
        {
            get { return _name; }
            set { _name = value; }
        }
    }

一個自定義的類,用於綁定ListBox.

 List<Student> students = new List<Student> {
                new Student{ Age=18, Name="Listen"},
                new Student{ Age=12, Name="Fly"},
                new Student{ Age=18, Name="Colors"},
                new Student{ Age=9, Name="Blue"},
            };
            this.lstStudent.ItemsSource = students;

在后置代碼中進行初始化List集合,並賦值給ListBox.

<Window.Resources>
        <Style TargetType="ListBoxItem">
            <Setter Property="Margin" Value="0 , 2, 0, 2"></Setter>
            <Setter Property="Padding" Value="0 , 2, 0, 2"></Setter>
            <Setter Property="FontFamily" Value="微軟雅黑"></Setter>
            <Style.Triggers>
                <DataTrigger Binding="{Binding Path=Age}" Value="18">
                    <Setter Property="Background" Value="LightBlue"></Setter>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>

添加針對ListBoxItem的樣式,其中的DataTrigger為數據觸發器,Bindding就是綁定的屬性,當Age的值為18的時候,就設置ListBoxItem的背景色為LightBlue。

        <ListBox x:Name="lstStudent" DisplayMemberPath="Name">
        </ListBox>

效果如下:

第一行和第三行的Age為18,背景色也發生了變化.

 

模板完整示例:

<Grid.Resources>
            <ControlTemplate x:Key="buttonTemplate">
                <Grid Width="100" Height="100">
                    <Ellipse x:Name="outerCircle" Width="100" Height="100">
                        <Ellipse.Fill>
                            <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                                <GradientStop Offset="0" Color="Blue"/>
                                <GradientStop Offset="1" Color="Red"/>
                            </LinearGradientBrush>
                        </Ellipse.Fill>
                    </Ellipse>
                    <Ellipse Width="80" Height="80">
                        <Ellipse.Fill>
                            <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                                <GradientStop Offset="0" Color="White"/>
                                <GradientStop Offset="1" Color="Transparent"/>
                            </LinearGradientBrush>
                        </Ellipse.Fill>
                    </Ellipse>
                </Grid>
                <ControlTemplate.Triggers>
                    <Trigger Property="Button.IsMouseOver" Value="True">
                        <Setter TargetName="outerCircle" Property="Fill" Value="Orange"/>
                    </Trigger>
                    <Trigger Property="Button.IsPressed" Value="True">
                        <Setter Property="RenderTransform">
                            <Setter.Value>
                                <ScaleTransform ScaleX="0.9" ScaleY="0.9"/>
                            </Setter.Value>
                        </Setter>
                        <Setter Property ="RenderTransformOrigin" Value=".5,.5"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Grid.Resources>

此模板實現了,嵌套了兩個Ellipse,最主要的是懸浮到按鈕上,外層Ellipse的顏色會變為Orange,點擊按鈕則會縮放按鈕的大小為90%.

 

模板工作原理:
WPF中的模板分別為ControlTemplate(控件模板)、DataTemplate(數據模板)、HierarchicalDataTemplate(表示支持 HeaderedItemsControl(表示包含多個項目並具有標頭的控件)的 DataTemplate ,例如 TreeViewItem 或 MenuItem)和ItemsPanelTemplate(項容器模板,例如ListBox的Items的容器模板),它們均繼承自FrameworkTemplate,其中HierarchicalDataTemplate繼承自DataTemplate。

 模板通過改變控件的可視化樹(visual Tree)來徹底改變其外觀,比如上邊代碼將按鈕的可視化樹變為兩個嵌套的Ellipse。 控件均派生自Control的類,只有Control及其派生類才有Template 這樣的屬性

模板綁定和模板觸發器:

 <Grid>
        <Grid.Resources>
            <ControlTemplate x:Key="buttonTemplate" TargetType="{x:Type Button}">    
                    <Grid >
                        <Ellipse x:Name="outerCircle">
                            <Ellipse.Fill>
                                <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                                    <GradientStop Offset="0" Color="{Binding RelativeSource={RelativeSource TemplatedParent},Path=Background.Color}"/>
                                    <GradientStop Offset="1" Color="Red"/>
                                </LinearGradientBrush>
                            </Ellipse.Fill>
                        </Ellipse>
                        <Ellipse RenderTransformOrigin=".5,.5">
                        <Ellipse.RenderTransform>
                            <ScaleTransform ScaleX=".8" ScaleY=".8"/>
                        </Ellipse.RenderTransform>
                        <Ellipse.Fill>
                                <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                                    <GradientStop Offset="0" Color="White"/>
                                    <GradientStop Offset="1" Color="Transparent"/>
                                </LinearGradientBrush>
                            </Ellipse.Fill>
                        </Ellipse>
                        <Viewbox>
                            <ContentPresenter Margin="20" Content="{TemplateBinding Content}"/>
                        </Viewbox>
                    </Grid>
                
                <ControlTemplate.Triggers>
                    <Trigger Property="Button.IsMouseOver" Value="True">
                        <Setter TargetName="outerCircle" Property="Fill" Value="{Binding RelativeSource={RelativeSource TemplatedParent},Path=BorderBrush}"/>
                    </Trigger>
                    <Trigger Property="Button.IsPressed" Value="True">
                        <Setter Property="RenderTransform">
                            <Setter.Value>
                                <ScaleTransform ScaleX="0.9" ScaleY="0.9"/>
                            </Setter.Value>
                        </Setter>
                        <Setter Property ="RenderTransformOrigin" Value=".5,.5"/>
                    </Trigger>
                    <Trigger Property="IsEnabled" Value="False">
                        <Setter TargetName="outerCircle" Property="Fill" Value="Gray">
                        </Setter>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Grid.Resources>
        <StackPanel>
            <Button Width="100" Height="100" Template="{StaticResource buttonTemplate}" Click="Button_Click" Content="OK" Background="CadetBlue" BorderBrush="BurlyWood"
                />
            <TextBlock Text="IsEnable = True" HorizontalAlignment="Center"/>
            <Button Width="100" Height="100" Template="{StaticResource buttonTemplate}" Click="Button_Click" Content="Disabled" IsEnabled="False"></Button>
            <TextBlock Text="IsEnable = False" HorizontalAlignment="Center"/>
        </StackPanel>
    </Grid>

重新修改了之前的示例代碼,主要是修改了Ellipse的顏色,使用了LinearGradientBrush線性畫刷,同時使用了RelativeResource相對資源綁定方式,此處的TemplatedParent應為Button本身,使用ContentPresenter來綁定按鈕的Content,切記是使用TemplateBinding來綁定 。

模板綁定(TemplateBinding)類似一般的數據綁定,與一般的綁定相比有如下限制:
1.僅在模板的可視化樹內部有效,在模板外部,甚至模板的Trigger中都無效。

2.不能應用在Freezable派生對象的屬性上,如果你嘗試綁定Brush的Color屬性,則會失敗哦。

我們上述代碼有兩處的數據綁定,一個是Ellipse的Fill屬性中。因為Color屬於Freezable派生的對象的屬性,因此不能使用模板綁定。二是在Trigger中對IsMouseOver的處理,這事因為Trigger不屬於控件模板的可視化樹內容,因此使用模板無效。
模板和樣式觸發器比較類似,但是有區別哦:

1.樣式觸發器無法應用於模板的某個元素,而模板的觸發器可以。比如上例子中可以在IsMouseOver為true的時候設置第一個Ellipse的Fill屬性,而在樣式中只能設置整個控件的某個屬性。Setter的TargeName和Trigger的SourceName屬性均用來指定模板中的某個子元素,該子元素必須有一個名字。
2.樣式觸發器優先級高於模板的觸發器。

接下來和大家分享一個小程序,控件模板的瀏覽器程序(來自WPF葵花寶典一書):

<Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="20"/>
        </Grid.RowDefinitions>
        <Grid Grid.Row="0" Name="grid" >
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"></ColumnDefinition>
                <ColumnDefinition Width="2"></ColumnDefinition>
                <ColumnDefinition Width="3*"></ColumnDefinition>
            </Grid.ColumnDefinitions>
            <GridSplitter Grid.Column="1" ResizeDirection="Columns" VerticalAlignment="Stretch" Width="2" Background="Black" HorizontalAlignment="Center" ShowsPreview="True"/>
            <TreeView DisplayMemberPath="Name" Name="lstTypes" SelectedItemChanged="lstTypes_SelectedItemChanged"></TreeView>
            <TextBox Grid.Column="2" Name="txtTemplate" TextWrapping="Wrap" VerticalScrollBarVisibility="Visible" FontFamily="Consolas"></TextBox>    
        </Grid>
        <TextBlock x:Name="txtbar" Grid.Row="1" Height="18" HorizontalAlignment="Left" Margin="10,0,0,0"  Text="Wait"></TextBlock>
    </Grid>

后置代碼如下:

  private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            // 獲得Control的程序集
            Assembly asbly = Assembly.GetAssembly(typeof(Control));

            // 獲得該程序集里的所有類型
            Type[] atype = asbly.GetTypes();

            // 使用該列表存儲
            SortedList<string, TreeViewItem> sortlst =
                                    new SortedList<string, TreeViewItem>();
            
            TreeViewItem item = new TreeViewItem();
            item.Header = "Control";
            item.Tag = typeof(Control);
            sortlst.Add("Control", item);
            lstTypes.Items.Add(item);

            // 遍歷所有的類型,然后將派生自contorl的類型添加到列表當中
            foreach (Type typ in atype)
            {
                if (typ.IsPublic && (typ.IsSubclassOf(typeof(Control))))
                {
                    item = new TreeViewItem();
                    item.Header = typ.Name;
                    item.Tag = typ;
                    sortlst.Add(typ.Name, item);
                }
            }

            // 構建樹
            foreach (KeyValuePair<string, TreeViewItem> kvp in sortlst)
            {
                if (kvp.Key != "Control")
                {
                    string strParent = ((Type)kvp.Value.Tag).BaseType.Name;
                    TreeViewItem itemParent = sortlst[strParent];
                    itemParent.Items.Add(kvp.Value);
                }
            }
        }

        private void lstTypes_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
        {
            try
            {
                Cursor oldcur = this.Cursor;
                this.Cursor = Cursors.Wait;
                // 獲得選中的類型
                TreeViewItem selectedItem = (TreeViewItem)lstTypes.SelectedItem;
                if (selectedItem.HasItems)
                {
                    this.Cursor = oldcur;
                    //wndBar.Hide();
                    return;
                }
                Type type = (Type)selectedItem.Tag;
                // 實例化該type
                ConstructorInfo info = type.GetConstructor(System.Type.EmptyTypes);
                Control control = (Control)info.Invoke(null);

                // 添加該控件 但是將屬性狀態設置為Collapsed.
                control.Visibility = Visibility.Collapsed;
                grid.Children.Add(control);

                // 獲得模板
                ControlTemplate template = control.Template;
                // 獲得模板的XAML文件
                XmlWriterSettings settings = new XmlWriterSettings();
                settings.Indent = true;
                StringBuilder sb = new StringBuilder();
                XmlWriter writer = XmlWriter.Create(sb, settings);
                XamlWriter.Save(template, writer);

                // 顯示模板
                txtTemplate.Text = sb.ToString();
                txtbar.Text = type.Name + "Control Template";
                // 移出該控件
                grid.Children.Remove(control);
                this.Cursor = oldcur;
            }
            catch (Exception err)
            {
                txtTemplate.Text = "<< Error generating template: " + err.Message + ">>";
            }
        }

效果圖如下:

 

樣式和主題:
其實WPF中的主題或者是換膚其實就是通過樣式和模板來實現的,當然通常會建立一個個xaml資源字典存放不同風格的樣式,一般是在App.xaml中引入需要的樣式字典。如下代碼:

    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Resources/button.xaml"></ResourceDictionary>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

窗體代碼:

  <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Menu Grid.Row="0" VerticalAlignment="Top">
            <MenuItem Header="皮膚" >
                <MenuItem Header="簡單按鈕風格" Name="simpleSkin" Click="MenuItem_Click" IsChecked="True"></MenuItem>
                <MenuItem Header="個性化按鈕風格" Name="fancySkin" Click="MenuItem_Click"></MenuItem>
            </MenuItem>
            
        </Menu>
        <Button Grid.Row="1" Content="OK" VerticalAlignment="Center" HorizontalAlignment="Center"/>
    </Grid>

后置代碼:

  private void MenuItem_Click(object sender, RoutedEventArgs e)
        {
            MenuItem menuItem = (MenuItem)sender;
            if(menuItem == null) return;
            
            if (menuItem == simpleSkin)
            {
                ResourceDictionary newDictionary = new ResourceDictionary();
                newDictionary.Source = new Uri("Resources/button.xaml", UriKind.Relative);
                Application.Current.Resources.MergedDictionaries[0] = newDictionary;
                menuItem.IsChecked = true;
                fancySkin.IsChecked = false;
            }
            else if (menuItem == fancySkin)
            {
                ResourceDictionary newDictionary = new ResourceDictionary();
                newDictionary.Source = new Uri("Resources/fancyButton.xaml", UriKind.Relative);
                Application.Current.Resources.MergedDictionaries[0] = newDictionary;
                menuItem.IsChecked = true;
                simpleSkin.IsChecked = false;
            }
        }

主要是通過Application.Current.Resources.MergedDictionaries.Add添加一個ResourceDictionary或者是直接修Application.Current.Resources.MergedDictionaries[0] 的值即可,主要代碼是,通過設置ResourceDictionary的Source來指定需要的資源文件;這樣在切換選中的菜單項之后就可以看到按鈕樣式的不同哦。

希望大家多多交流討論。

 

Tip:WPF中的樣式優先級: 模板中的樣式優先級高於外部樣式(Resource中的style等);樣式觸發器高於模板觸發器。


免責聲明!

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



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