在使用WPF編寫客戶端代碼時,我們會在VM下解耦業務邏輯,而剩下與功能無關的內容比如動畫、視覺效果,布局切換等等在數量和復雜性上都超過了業務代碼。而如何更好的簡化這些編碼,WPF設計人員使用了Style和Behavior來幫助我們構建一致性、組織性好的代碼。
這一章的目的是理解我們使用行為和資源的目標。使用這2個內容使我們創建封裝一些通用用戶界面功能的行為。比如啟動故事板,加入重力的動畫效果,我們要把思維給打開,我們做的東西是為了通用,而不是為了業務,因為業務在這個時刻只存在於VM中。(即使個人能力所限,或者實際情況所限,V下面還是有業務代碼。但是我們心中要有這個自信,我做WPF開發,那么在未來我也能設計出來堪比WPF這種優秀的的框架,如果沒有自信和信心,別人一說就受到了打擊,那么什么時間才能成為大佬,別說成為大佬了,可能自己慢慢的就放棄了把),跑題了,簡單來說就是我們使用行為和樣式設計出來可以添加到各種控件的通用效果。這里不想考慮更多的內容,比如自定義控件。
先講樣式和觸發器,我們設計窗體只有暗色風格,在此風格下的按鈕都是黑底白字。
1)什么是樣式,先來段代碼:
<Window x:Class="StyleAndBehavior.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:StyleAndBehavior" mc:Ignorable="d" Topmost="True" Background="#000000" Title="MainWindow" Height="450" Width="800"> <Grid> <StackPanel Height="30" Orientation="Horizontal"> <Button Margin="3" Content="我是按鈕A" Foreground="#F5FFFA" Background="#696969" BorderBrush="#2F4F4F"/> <Button Margin="3" Content="我是按鈕B" Foreground="#F5FFFA" Background="#696969" BorderBrush="#2F4F4F"/> </StackPanel> </Grid> </Window>
實際效果如下圖:
我們看到如果這里有N個按鈕,那么所有的代碼上都要寫自己屬性對應的樣式。我們使用資源可以規划一些統一的樣式。而統一的樣式,就被我們放到了資源里面。我們一點一點改進我們的代碼,修改代碼如下。
<Window x:Class="StyleAndBehavior.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:StyleAndBehavior" mc:Ignorable="d" Topmost="True" Background="#000000" Title="MainWindow" Height="450" Width="800"> <Window.Resources> <Style TargetType="Button"> <Setter Property="Foreground" Value="#F5FFFA" /> <Setter Property="Background" Value="#696969"/> <Setter Property="BorderBrush" Value="#2F4F4F"/> <Setter Property="Margin" Value="3"/> </Style> </Window.Resources> <Grid> <StackPanel VerticalAlignment="Top" Height="30" Orientation="Horizontal"> <Button Content="我是按鈕A"/> <Button Content="我是按鈕B"/> </StackPanel> </Grid> </Window>
我們看到了我們在Window節點的Resources下添加了一個Style,並且設置了TargetType為Button。在Button元素內,我刪除了對應的代碼。這個時候我們啟動程序。發現程序的效果是一樣的。那么這個時候我們在添加其他按鈕,就自動使用了這個樣式。
如果在使用Style的時候,不指定Key,那么所有加載了資源的元素都會默認使用這個資源。我們給Style指定一個Key,並設置一個Button的Style觀察效果,代碼如下:
<Window x:Class="StyleAndBehavior.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:StyleAndBehavior" mc:Ignorable="d" Topmost="True" Background="#000000" Title="MainWindow" Height="450" Width="800"> <Window.Resources> <Style x:Key="DarkButtonStyle" TargetType="Button"> <Setter Property="Foreground" Value="#F5FFFA" /> <Setter Property="Background" Value="#696969"/> <Setter Property="BorderBrush" Value="#2f4f4f"/> <Setter Property="Margin" Value="3"/> </Style> </Window.Resources> <Grid> <StackPanel VerticalAlignment="Top" Height="30" Orientation="Horizontal"> <Button Style="{StaticResource DarkButtonStyle}" Content="我是按鈕A"/> <Button Content="我是按鈕B"/> </StackPanel> </Grid> </Window>
·
我們發現沒有樣式添加了Key之后,沒有缺少Key的TargetType等於Button的資源后,沒有引用Style的Button被修改回系統默認的了。而我們使用Style={StaticResource }資源的樣式的Button外觀就變成了我們資源中定義的。
樣式中還有一個關鍵的點,是樣式的繼承。從一個樣式中繼承公共的部分后,可以實現自己特殊部分的樣式,比如我們在繼承DarkButtonStyle的樣式實現一個警告的按鈕的樣式。假設統一的警告按鈕風格是字體會更粗。我們需要添加一個新的樣式繼承自DarkButtonStyle並FontWeight屬性,同時使警告的控件引用該樣式,代碼如下:
<Window x:Class="StyleAndBehavior.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:StyleAndBehavior" Background="#000000" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Window.Resources> <Style x:Key="DarkButtonStyle" TargetType="Button"> <Setter Property="Foreground" Value="#F5FFFA" /> <Setter Property="Background" Value="#696969"/> <Setter Property="BorderBrush" Value="#2f4f4f"/> <Setter Property="Margin" Value="3"/> </Style> <Style x:Key="WarningDarkButtonStyle" TargetType="Button" BasedOn="{StaticResource DarkButtonStyle}"> <Setter Property="FontWeight" Value="Bold"/> </Style> </Window.Resources> <Grid> <StackPanel VerticalAlignment="Top" Height="30" Orientation="Horizontal"> <Button Content="我是按鈕A" Style="{StaticResource DarkButtonStyle}"/> <Button Content="我是按鈕B" Style="{StaticResource WarningDarkButtonStyle}"/> </StackPanel> </Grid> </Window>
這樣我們就實現了樣式的繼承,但是代碼中,為了通用,還是盡量減少樣式的繼承,因為要改動代碼的話,涉及的一旦包含繼承關系,在修改外觀時就需要考慮改動樣式資源帶來的影響,但是會讓長期穩定迭代的代碼更加結構化。一般都是一個控件的幾種形態,建議用樣式的繼承。
2)什么是觸發器。
我們在控件引用資源后,我們發現雖然外觀修改了,但是鼠標經過,等其他事件時,控件依然沒有對應我們要的風格。為了簡化對應的事件代碼,WPF提出了觸發器的概念,在這里我們可以使用觸發器來方便的維護控件的外觀。
我們在前面代碼的基礎上添加觸發器,如果按鈕被禁用,則修改前景色為紅色:
<Window x:Class="StyleAndBehavior.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:StyleAndBehavior" Background="#000000" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Window.Resources> <Style x:Key="DarkButtonStyle" TargetType="Button"> <Setter Property="Foreground" Value="#F5FFFA" /> <Setter Property="Background" Value="#696969"/> <Setter Property="BorderBrush" Value="#2f4f4f"/> <Setter Property="Margin" Value="3"/> <Style.Triggers> <Trigger Property="IsEnabled" Value="False"> <Setter Property="Foreground" Value="Red" /> </Trigger> </Style.Triggers> </Style> <Style x:Key="WarningDarkButtonStyle" TargetType="Button" BasedOn="{StaticResource DarkButtonStyle}"> <Setter Property="FontWeight" Value="Bold"/> </Style> </Window.Resources> <Grid> <StackPanel VerticalAlignment="Top" Height="30" Orientation="Horizontal"> <Button x:Name="AButton" Content="我是按鈕A" Style="{StaticResource DarkButtonStyle}"/> <Button Content="我是按鈕B" Style="{StaticResource WarningDarkButtonStyle}" Click="SetButtonADisableButton_OnClick"/> </StackPanel> </Grid> </Window>
using System.Windows; namespace StyleAndBehavior { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void SetButtonADisableButton_OnClick(object sender, RoutedEventArgs e) { AButton.IsEnabled = false; } } }
從這個代碼中,我們看到了當我們點擊按鈕B時,按鈕A的被設置了Disable無法使用,同時前景色被改成了白色(背景色的變化我們目前先不關注。后面會講樣式的重寫,這里只關注我們前景色的變化)。
我們在資源上嘗試添加其他觸發器,完整代碼如下,就會發現觸發器可以幫助我們通過監聽屬性的變化直接修改樣式。我們的Button獲取焦點,和單擊按下后,前景色都會發生變化。
<Window x:Class="StyleAndBehavior.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:StyleAndBehavior" Background="#000000" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Window.Resources> <Style x:Key="DarkButtonStyle" TargetType="Button"> <Setter Property="Foreground" Value="#F5FFFA" /> <Setter Property="Background" Value="#696969"/> <Setter Property="BorderBrush" Value="#2f4f4f"/> <Setter Property="Margin" Value="3"/> <Style.Triggers> <Trigger Property="IsEnabled" Value="False"> <Setter Property="Foreground" Value="Red" /> </Trigger> <Trigger Property="IsPressed" Value="True"> <Setter Property="Foreground" Value="Blue"/> </Trigger> <Trigger Property="IsFocused" Value="True"> <Setter Property="Foreground" Value="Yellow"/> </Trigger> </Style.Triggers> </Style> <Style x:Key="WarningDarkButtonStyle" TargetType="Button" BasedOn="{StaticResource DarkButtonStyle}"> <Setter Property="FontWeight" Value="Bold"/> </Style> </Window.Resources> <Grid> <StackPanel VerticalAlignment="Top" Height="30" Orientation="Horizontal"> <Button x:Name="AButton" Content="我是按鈕A" Style="{StaticResource DarkButtonStyle}"/> <Button Content="我是按鈕B" Style="{StaticResource WarningDarkButtonStyle}" Click="SetButtonADisableButton_OnClick"/> </StackPanel> </Grid> </Window>
還有一種是滿足多個屬性同時變更要求的觸發器,MultiTriggers。使用這個可以監聽多個屬性的變化滿足條件時設置對應觸發器綁定的屬性。
<Window x:Class="StyleAndBehavior.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:StyleAndBehavior" Background="#000000" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Window.Resources> <Style x:Key="DarkButtonStyle" TargetType="Button"> <Setter Property="Foreground" Value="#F5FFFA" /> <Setter Property="Background" Value="#696969"/> <Setter Property="BorderBrush" Value="#2f4f4f"/> <Setter Property="Margin" Value="3"/> <Style.Triggers> <Trigger Property="IsEnabled" Value="False"> <Setter Property="Foreground" Value="Red" /> </Trigger> <Trigger Property="IsPressed" Value="True"> <Setter Property="Foreground" Value="Blue"/> </Trigger> <Trigger Property="IsFocused" Value="True"> <Setter Property="Foreground" Value="Yellow"/> </Trigger> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="IsFocused" Value="True"/> <Condition Property="IsMouseOver" Value="True"/> </MultiTrigger.Conditions> <Setter Property="Foreground" Value="Orange"/> </MultiTrigger> </Style.Triggers> </Style> <Style x:Key="WarningDarkButtonStyle" TargetType="Button" BasedOn="{StaticResource DarkButtonStyle}"> <Setter Property="FontWeight" Value="Bold"/> </Style> </Window.Resources> <Grid> <StackPanel VerticalAlignment="Top" Height="30" Orientation="Horizontal"> <Button x:Name="AButton" Content="我是按鈕A" Style="{StaticResource DarkButtonStyle}"/> <Button Content="我是按鈕B" Style="{StaticResource WarningDarkButtonStyle}" Click="SetButtonADisableButton_OnClick"/> </StackPanel> </Grid> </Window>
我們使用了MultiTrigger來實現多屬性變化的觸發器,用來設置對應場景下的UI變化。我們這里設置了前景色為橙色。
因為還沒有講到MVVM所以還有一個DataTrigger這里就先不講了。后面寫自定義控件時通過MVVM會講到這個DataTrigger的使用。原理是一樣的。只是使用DataTrigger綁定時監聽的時VM對象下的屬性。
接下來是事件觸發器。事件觸發器需要傳入一個故事板對象,我們可以使用事件觸發器來實現一個鼠標移入時字體慢慢變大, 鼠標移出時字體慢慢變小的動畫效果。
代碼已經實現了,但是因為最近搬家寫代碼用的電腦不一樣,這個電腦沒有錄屏軟件,所以實際效果沒法錄屏,復制代碼跑起來看看啦。
<Window x:Class="StyleAndBehavior.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:StyleAndBehavior" Background="#000000" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Window.Resources> <Style x:Key="DarkButtonStyle" TargetType="Button"> <Setter Property="Foreground" Value="#F5FFFA" /> <Setter Property="Background" Value="#696969"/> <Setter Property="BorderBrush" Value="#2f4f4f"/> <Setter Property="Margin" Value="3"/> <Style.Triggers> <Trigger Property="IsEnabled" Value="False"> <Setter Property="Foreground" Value="Red" /> </Trigger> <Trigger Property="IsPressed" Value="True"> <Setter Property="Foreground" Value="Blue"/> </Trigger> <Trigger Property="IsFocused" Value="True"> <Setter Property="Foreground" Value="Yellow"/> </Trigger> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="IsFocused" Value="True"/> <Condition Property="IsMouseOver" Value="True"/> </MultiTrigger.Conditions> <Setter Property="Foreground" Value="Orange"/> </MultiTrigger> <EventTrigger RoutedEvent="Mouse.MouseEnter"> <EventTrigger.Actions> <BeginStoryboard > <Storyboard> <DoubleAnimation Duration="0:0:1.0" Storyboard.TargetProperty="FontSize" To="22"> </DoubleAnimation> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger> <EventTrigger RoutedEvent="Mouse.MouseLeave"> <EventTrigger.Actions> <BeginStoryboard> <Storyboard> <DoubleAnimation Duration="0:0:1.0" Storyboard.TargetProperty="FontSize" To="12"></DoubleAnimation> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger> </Style.Triggers> </Style> <Style x:Key="WarningDarkButtonStyle" TargetType="Button" BasedOn="{StaticResource DarkButtonStyle}"> <Setter Property="FontWeight" Value="Bold"/> </Style> </Window.Resources> <Grid> <StackPanel VerticalAlignment="Top" Height="30" Orientation="Horizontal"> <Button x:Name="AButton" Content="我是按鈕A" Style="{StaticResource DarkButtonStyle}"/> <Button Content="我是按鈕B" Style="{StaticResource WarningDarkButtonStyle}" Click="SetButtonADisableButton_OnClick"/> </StackPanel> </Grid> </Window>
到了這一個章節更為關鍵的內容了,行為的使用。
對於行為,很多人學的很迷糊,我之前也是。就是拿行為綁定幾個命令到后台的VM上。其他的大部分場景都沒有用過了。導致無法發揮出來WPF設計人員設計行為的優勢,這里我們也嘗試自己寫一下行為。
對行為的支持被放到了System.Windows.Interactivity.dll中。他是使用行為的基礎。行為主要是為了封裝一些UI功能,從而可以不必編寫代碼就能夠把行為應用到元素上。舉個例子,我們實現一個TextBox的輸入水印效果。
我們新建一個類庫工程起名叫做CustomBehaviorLibrary。來存放我們的行為,通過在該工程上右鍵=》管理Nuget程序包=》搜索System.Windows.Interactivity.WPF並安裝。如果使用WPF下的控件,注意必須要同時有PresentationCore、PresentationFramework、WindwsBase這三個庫的引用。缺少的可以Alt+Enter手動引用一下。
我們創建TextBoxWatermarkBehavior類,並繼承自Behavior類,我們在Behavior上右鍵F12,看到里面有一個AssociatedObject名字的對象,這個就是我們要用來添加行為的對象。我們先使用propdp添加名字為Watermark的string類型的依賴項屬性。用來作為我們的水印顯示文本。
using System.Windows; using System.Windows.Controls; using System.Windows.Interactivity; using System.Windows.Media; namespace CustomBehaviorLibrary { public class TextBoxWatermarkBehavior : Behavior<TextBox> { private bool _hasContent = true; public string Watermark { get { return (string)GetValue(WatermarkProperty); } set { SetValue(WatermarkProperty, value); } } // Using a DependencyProperty as the backing store for Watermark. This enables animation, styling, binding, etc... public static readonly DependencyProperty WatermarkProperty = DependencyProperty.Register("Watermark", typeof(string), typeof(TextBoxWatermarkBehavior), new PropertyMetadata(default(string))); protected override void OnAttached() { base.OnAttached(); var textbox = AssociatedObject; textbox.Loaded += Textbox_Loaded; } protected override void OnDetaching() { base.OnDetaching(); } private void Textbox_Loaded(object sender, RoutedEventArgs e) { var textbox = sender as TextBox; if (string.IsNullOrEmpty(textbox.Text)) { textbox.Foreground = Brushes.Gray; textbox.Text = Watermark; _hasContent = false; } textbox.GotFocus -= Textbox_GotFocus; textbox.LostFocus -= Textbox_LostFocus; textbox.GotFocus += Textbox_GotFocus; textbox.LostFocus += Textbox_LostFocus; } private void Textbox_LostFocus(object sender, RoutedEventArgs e) { var textbox = sender as TextBox; if (string.IsNullOrEmpty(textbox.Text)) { _hasContent = false; textbox.Text = Watermark; textbox.Foreground = Brushes.Gray; } else { _hasContent = true; } } private void Textbox_GotFocus(object sender, RoutedEventArgs e) { var textbox = sender as TextBox; if (!_hasContent) { textbox.Text = ""; textbox.Foreground = Brushes.Black; } } } }
這樣我們的行為就創建好了,這個時候,我們在主工程下使用這個行為。
1)主工程添加對CustomBehaviorLibrary工程的引用;
2)主工程在NuGet添加對System.Windows.Interactivity.WPF的引用。
3)注意在使用的窗體下添加命名空間
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:customBehavior="clr-namespace:CustomBehaviorLibrary;assembly=CustomBehaviorLibrary"
4)添加TextBox控件,並添加Interactivity下的Behaviors。 在Behaviors中添加我們自定義的TextBoxWatermarkBehavior 並設置我們添加的依賴項屬性。設置水印內容。代碼如下:
<Window x:Class="StyleAndBehavior.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:StyleAndBehavior" mc:Ignorable="d" Topmost="True" Background="#000000" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:customBehavior="clr-namespace:CustomBehaviorLibrary;assembly=CustomBehaviorLibrary" Title="MainWindow" Height="450" Width="800"> <Grid> <StackPanel VerticalAlignment="Top" Height="30" Orientation="Horizontal"> <TextBox MinWidth="200"> <i:Interaction.Behaviors> <customBehavior:TextBoxWatermarkBehavior Watermark="我是水印,請輸入內容"/> </i:Interaction.Behaviors> </TextBox> <TextBox MinWidth="200"> <i:Interaction.Behaviors> <customBehavior:TextBoxWatermarkBehavior Watermark="我是另外一個TextBox水印,請輸入內容"/> </i:Interaction.Behaviors> </TextBox> </StackPanel> </Grid> </Window>
這樣我們就完成了對行為的使用。這里寫的比較簡單,其實還有很多相關的知識可以擴展,因為行為是一個比較獨立的內容,所以單獨在行為中可以擴展的通用的東西特別多。而i:Interaction.Triggers也是在這里的,但是我之前都是直接綁定VM下的Command所以這個等講到VM和Command的時候在講這個吧用法是一樣的。目前這一章就講這么多,行為這里配置和引用稍微復雜了一些,但是學習是一個持續的過程,每天進步一點,掌握這個知識點,不要急,WPF的知識就那么多,每天投入一點,幾年時間慢慢的也就精通了。