- A+
在使用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 Style="{StaticResource DarkButtonStyle}" Content="我是按钮A"/> <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="buttonA" Style="{StaticResource DarkButtonStyle}" Content="我是按钮A"/> <Button Content="我是按钮B" Click="SetButtonADisableButton_OnClick" Style="{StaticResource WarningDarkButtonStyle}"/> </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) { buttonA.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="buttonA" Style="{StaticResource DarkButtonStyle}" Content="我是按钮A"/> <Button Content="我是按钮B" Click="SetButtonADisableButton_OnClick" Style="{StaticResource WarningDarkButtonStyle}"/> </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="buttonA" Style="{StaticResource DarkButtonStyle}" Content="我是按钮A"/> <Button Content="我是按钮B" Click="SetButtonADisableButton_OnClick" Style="{StaticResource WarningDarkButtonStyle}"/> </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="buttonA" Style="{StaticResource DarkButtonStyle}" Content="我是按钮A"/> <Button Content="我是按钮B" Click="SetButtonADisableButton_OnClick" Style="{StaticResource WarningDarkButtonStyle}"/> </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的知识就那么多,每天投入一点,几年时间慢慢的也就精通了。