提到樣式應該要追溯到 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等);樣式觸發器高於模板觸發器。