始终如一地设计应用程序
让我们的应用程序脱颖而出的最简单方法之一是让它们看起来独一无二。这可以通过为我们在其中使用的控件定义自定义样式来实现。 然而,如果我们决定为我们的控件设置样式,那么我们必须为我们使用的所有控件设置样式,因为半样式的应用程序通常看起来比仅使用默认样式的应用程序更糟糕。
因此,我们必须始终如一地设计我们的应用程序控制样式,以使我们的应用程序具有专业的外观。 在本节中,我们将讨论一些技巧和窍门来帮助我们实现这些应用程序样式。
覆盖默认控件样式
在为我们的应用程序控件提供自定义样式时,这通常需要我们为每个控件定义一个新的 ControlTemplate 元素。 由于这些通常非常大,因此习惯于在单独的资源文件中声明它们,并将其与 App.xaml 文件中的应用程序资源合并,如第 5 章,为作业使用正确的控件中所示。
在开始此任务之前,我们需要计划我们希望控件的外观,然后将相同的外观应用到每个控件。 另一个错误是自定义具有不同样式的不同控件,因为一致性是提供专业外观的关键。 例如,如果我们希望我们的单行文本框具有一定的高度,那么我们还应该将其他控件定义为相同的高度。
我们为控件声明的自定义样式可以是我们应用程序框架的一部分。 如果我们在不通过 x:Key 指令命名它们的情况下定义它们,它们将被隐式应用,因此,使用我们的应用程序框架的开发人员无需关心每个控件的外观,有效地释放它们以专注于将它们聚合到 各种意见。
在开始设计我们的自定义样式之前要做的第一件事是定义我们将在应用程序中使用的小范围颜色。 在一个应用程序中使用过多的颜色会使其看起来不那么专业,所以我们应该选择少数几个深浅不一的颜色 使用的颜色。 有许多在线工具可以帮助我们选择要使用的调色板。
一旦我们选择了我们的应用程序颜色,我们应该首先将它们声明为 App.xaml 文件中的 Color 对象,然后声明使用它们的画笔元素,因为大多数控件使用画笔而不是颜色。 这有两个好处; 仅使用这些颜色将提高一致性,如果我们需要更改颜色,我们只需要在一个地方更改它:
<Color x:Key="ReadOnlyColor">#FF585858</Color>
...
<SolidColorBrush x:Key="ReadOnlyBrush"
Color="{StaticResource ReadOnlyColor}" />
为最常见的控件类型定义多个命名样式通常是一个好主意。 例如,为 TextBlock 元素设置 Label 样式,使它们右对齐并添加适当的边距,或者设置更大的字体大小和更重的字体粗细的 Heading 样式。 为开发人员提供一组预定义样式有助于使应用程序看起来一致。
在定义多个命名样式时,通常会在其他样式中重用其中的一些。 例如,如果我们有一个 TextBox 控件的默认样式,我们可以基于它的其他样式变化。 让我们看一些 XAML 示例:
<Style x:Key="TextBoxStyle" TargetType="{x:Type TextBox}">
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Margin" Value="0,0,0,5" />
<Setter Property="Padding" Value="1.5,2" />
<Setter Property="MinHeight" Value="25" />
<Setter Property="TextWrapping" Value="Wrap" />
...
</Style>
<Style x:Key="Max2LineTextBoxStyle" TargetType="{x:Type TextBox}"
BasedOn="{StaticResource TextBoxStyle}">
<Setter Property="MaxHeight" Value="44" />
<Setter Property="VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="ToolTip"
Value="{Binding Text, RelativeSource={RelativeSource Self}}" />
</Style>
<Style x:Key="Max3LineTextBoxStyle" TargetType="{x:Type TextBox}"
BasedOn="{StaticResource Max2LineTextBoxStyle}">
<Setter Property="MaxHeight" Value="64" />
</Style>
<Style x:Key="ReadOnlyTextBoxStyle" TargetType="{x:Type TextBox}"
BasedOn="{StaticResource TextBoxStyle}">
<Setter Property="Background" Value="{StaticResource ReadOnlyBrush}" />
<Setter Property="IsReadOnly" Value="True" />
<Setter Property="Cursor" Value="Arrow" />
</Style>
在这里,简化的 TextBoxStyle 样式定义了所有 TextBox 控件的大部分属性。 Max2LineTextBoxStyle 样式继承了该样式的所有属性设置,并设置了更多以确保垂直滚动条可以在需要时出现并强制控件的最大高度。
Max3LineTextBoxStyle 样式扩展了 Max2LineTextBoxStyle 样式,因此继承了它的所有属性设置,以及 TextBoxStyle 样式的设置。 它覆盖了以前样式中设置的 MaxHeight 属性。 ReadOnlyTextBoxStyle 样式还扩展了 TextBoxStyle 样式并设置属性以确保控件是只读的。 以这种方式定义样式可确保每个 View 中的控件保持一致。
除了为我们的应用程序控件定义默认样式外,为应用程序中的每个数据模型提供默认数据模板资源通常也是一个好主意。 与控件类似,预定义这些数据模板可以提高一致性。 我们还可以定义一些命名模板来覆盖默认模板并在不同场景中使用。
如果应用程序中有大量数据模型,那么在单独的资源文件中声明它们的数据模板并将其与 App.xaml 文件中的应用程序资源合并会很有帮助,就像默认控件模板一样。 因此,在应用程序资源文件中合并多个资源文件并不罕见。
分层视觉效果
到目前为止,我们只是通过更改形状、大小、边框和其他常见属性来查看标准控件的简单重新定义。 然而,我们可以用 WPF 做的远不止这些。 在继续本节之前,重要的是要知道每个控件包含的视觉效果越多,渲染它们所需的时间就越长,因此这会对性能产生负面影响。
因此,如果我们的应用程序将在缓慢的旧计算机上运行,重要的是不要过度使用我们的控件的视觉方面。 相反,如果我们知道我们的最终用户将拥有大量 RAM 和/或显卡,那么我们就可以更进一步,开发视觉上令人惊叹的控件。 让我们看一下可以用来改善控件外观的一些技术。
投掷阴影
让我们的 UI 元素从屏幕中弹出的最简单方法之一就是为它们添加阴影。 每个控件都有一个从 UIElement 类继承的 Effect 属性。 我们可以为这个属性设置一个 DropShadowEffect 类型的对象来为我们的控件添加阴影。
但是,我们必须对在 DropShadowEffect 元素上使用的设置保持保守,因为这种效果很容易被过度渲染。 我们也不想将此效果应用于每个控件,因为这会破坏整体效果。 在包含其他控件的面板或围绕此类面板的边框上进行设置时,它最有用。 让我们看一个应用此效果的简单示例:
<Button Content="Click Me" Width="140" Height="34" FontSize="18">
<Button.Effect>
<DropShadowEffect Color="Black" ShadowDepth="6" BlurRadius="6"
Direction="270" Opacity="0.5" />
</Button.Effect>
</Button>
让我们看看这段代码的输出是什么样子的:
在此示例中,我们有一个标准按钮,其中包含一个 DropShadowEffect 元素,该元素设置为它的 Effect 属性。 正如我们将在本章后面看到的,DropShadowEffect 类有很多用途,但它的主要用途是创建阴影效果。
当将此元素用于阴影效果时,我们通常希望将其 Color 属性设置为黑色,并将其 Opacity 属性设置为至少半透明的值,以获得最佳或最真实的结果。 ShadowDepth 属性决定了阴影应该落在元素多远的地方。 与 BlurRadius 属性一起,此属性用于为元素添加高度感。
BlurRadius 属性分散了阴影区域,同时也降低了它的密度。 与 ShadowDepth 属性一样,此属性的默认值为 5。 Direction 属性指定阴影应该落在哪个方向,值为 0 时使阴影向右下落,增加值使阴影角度逆时针移动。
请注意,值 270 使阴影直接落在应用的控件下方,通常最适合在业务应用程序中使用。 使用这个角度会导致一个元素似乎悬停在屏幕上方或前面,光源来自上方,这是光线最自然的方向。
与此相反,例如 45 度的角度会将阴影放置在元素的右上角,这会告诉大脑左下角有光源。 然而,这种特殊的效果看起来不自然,可能会减损而不是增加应用程序的样式。
声明多个边界
另一种使控件突出的简单技术是为每个控件声明多个 Border 元素。 通过在外部边界内声明一个或多个边界,我们可以使我们的控件具有专业的外观。 稍后我们将看到当用户的鼠标光标悬停在按钮上时如何以不同的方式对这些边框进行动画处理,但现在,让我们看看如何创建这种效果:
<Grid Width="160" Height="68">
<Grid.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="Red" />
<GradientStop Color="Yellow" Offset="1" />
</LinearGradientBrush>
</Grid.Background>
<Button Content="Click Me" Width="120" Height="28" FontSize="14"
Margin="20">
<Button.Template>
<ControlTemplate TargetType="{x:Type Button}">
<Border BorderBrush="Black" BorderThickness="1"
Background="#7FFFFFFF" Padding="1" CornerRadius="5"
SnapsToDevicePixels="True">
<Border BorderBrush="#7F000000" BorderThickness="1"
Background="White" CornerRadius="3.5"
SnapsToDevicePixels="True">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
</Border>
</ControlTemplate>
</Button.Template>
</Button>
</Grid>
在此示例中,我们为 Button 控件声明了一个简单的 ControlTemplate 元素,以演示双边框技术。 请注意,我们通常会在 App.xaml 文件的 Application.Resources 部分中声明此模板,以便可以重用它,但我们已在本地声明它以节省空间。
请注意,我们需要调整内边框的圆角半径以准确地适合外边框。 如果我们为两者使用相同的尺寸,它们将无法正确组合在一起。 此外,我们在两个边框上将 SnapsToDevicePixels 属性设置为 true,以确保它们不会被抗锯齿伪影模糊。
还有一点需要注意的是,我们使用#7FFFFFFF作为外边框的背景值和内边框的边框画笔。 此值中的 Alpha 通道设置为 7F,相当于不透明度值为 0.5。 这意味着这些元素将部分透明,因此来自背景的颜色将部分通过边框边缘显示。
我们将按钮添加到 Grid 面板中,并设置一个 LinearGradientBrush 对象作为其背景,以展示这种半透明效果。 渲染后,我们的背景渐变和按钮将如下图所示:
重用复合视觉效果
下一个技术涉及定义将在我们的控件背景中呈现的特定主题。 这可能是公司标志的全部或部分,一个特定的形状,甚至只是一个简单的、位置合适的曲线。 这将形成我们控制视觉效果的最底层,并且可以在顶部具有额外的视觉效果。 让我们看一下我们可以实现这种设计的一种方式,从定义一些资源开始:
<RadialGradientBrush x:Key="LayeredButtonBackgroundBrush" RadiusX="1.85"
RadiusY="0.796" Center="1.018,-0.115" GradientOrigin="0.65,- 0.139">
<GradientStop Color="#FFCACACD" />
<GradientStop Color="#FF3B3D42" Offset="1" />
</RadialGradientBrush>
<LinearGradientBrush x:Key="LayeredButtonCurveBrush" StartPoint="0,0"
EndPoint="1,1">
<GradientStop Color="#FF747475" Offset="0" />
<GradientStop Color="#FF3B3D42" Offset="1" />
</LinearGradientBrush>
<Grid x:Key="LayeredButtonBackgroundElements">
<Rectangle Fill="{StaticResource LayeredButtonBackgroundBrush}" />
<Path StrokeThickness="0"
Fill="{StaticResource LayeredButtonCurveBrush}">
<Path.Data>
<CombinedGeometry GeometryCombineMode="Intersect">
<CombinedGeometry.Geometry1>
<EllipseGeometry Center="-20,50.7" RadiusX="185" RadiusY="46" />
</CombinedGeometry.Geometry1>
<CombinedGeometry.Geometry2>
<RectangleGeometry Rect="0,0,106,24" />
</CombinedGeometry.Geometry2>
</CombinedGeometry>
</Path.Data>
</Path>
</Grid>
<VisualBrush x:Key="LayeredButtonBackground"
Visual="{StaticResource LayeredButtonBackgroundElements}" />
这个设计有几个元素,所以让我们分别看一下每个元素。 我们首先声明一个带有键LayeredButtonBackgroundBrush 的RadialGradientBrush 元素和一个带有LayeredButtonCurveBrush 键的LinearGradientBrush。
RadialGradientBrush 元素的 RadiusX 和 RadiusY 属性指定包含径向渐变的最外层椭圆的 X 和 Y 半径,而 Center 和 GradientOrigin 属性指定径向渐变的中心和焦点,使我们能够将其精确定位在我们的 长方形。
LinearGradientBrush 元素的 StartPoint 值为 0,0,EndPoint 值为 1,1,这会产生对角渐变。 通过这种特殊的设计,我们的想法是在中心的两个渐变之间形成鲜明的对比,并在边缘将它们稍微混合在一起。
接下来,我们使用键 LayeredButtonBackgroundElements 声明一个 Grid 面板,其中包含一个 Rectangle 和一个 Path 元素。 默认情况下,矩形被拉伸以填充面板,并使用 LayeredButtonBackgroundBrush 资源进行绘制。 Path 元素使用 LayeredButtonCurveBrush 资源绘制。
Path 对象的 Data 属性是我们定义路径形状的地方。 我们可以通过多种方式指定路径数据; 但是,在此示例中,我们使用了一个具有 GeometryCombineMode 值为 Intersect 的 CombinedGeometry 元素,它输出一个表示两个指定几何形状相交的形状。
在 CombinedGeometry 元素内,我们有 Geometry1 和 Geometry2 属性,我们根据 GeometryCombineMode 属性指定的 Intersect 模式组合两个几何形状。
我们的第一个形状定义了我们设计中的曲线,它来自一个 EllipseGeometry 元素,使用 Center 属性来定位椭圆,并使用 RadiusX 和 RadiusY 属性来塑造它。 第二个形状是来自 RectangleGeometry 元素的矩形,由其 Rect 属性定义。
这两个形状的交集是这条路径的结果,大致覆盖了我们整体形状的底部,直到曲线。 其后面部分被遮挡的矩形元素完成了整体形状的其余部分。
带有 LayeredButtonBackground 键的 VisualBrush 元素的 Visual 属性设置为 LayeredButtonBackgroundElements 面板,因此使用此画笔绘制的任何 UI 元素现在都将印上此设计。 一旦我们将这些资源添加到 App.xaml 文件的 Application.Resources 部分,我们就可以通过 VisualBrush 元素使用它们,如下所示:
<Button Background="{StaticResource LayeredButtonBackground}" Width="200"
Height="40" SnapsToDevicePixels="True" />
这将在按钮背景中呈现渐变,如下所示:
在此示例中,我们手动指定对视觉画笔的引用来绘制 Button 对象的背景。 但是,以这种方式设置背景需要使用我们的应用程序框架的开发人员在每次添加按钮时都这样做。 更好的解决方案是重新设计默认按钮模板,以便将视觉画笔自动应用于每个按钮。 当我们将这些技术结合在一起时,我们将在本章后面看到一个这样的例子。
反射光
另一种技术涉及在我们的控件顶部添加一个渐变为透明的半透明层,以呈现光源反射的外观。 这可以使用简单的 Border 元素和 LinearGradientBrush 实例轻松实现。 让我们看看我们如何做到这一点:
<Button Content="Click Me" Width="140" Height="34" FontSize="18"
Foreground="White" Margin="20">
<Button.Template>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="#FF007767" CornerRadius="5"
SnapsToDevicePixels="True">
<Grid>
<Rectangle RadiusX="4" RadiusY="4" Margin="1,1,1,7"
SnapsToDevicePixels="True">
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#BFFFFFFF" />
<GradientStop Color="#00FFFFFF" Offset="0.8" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
</Border>
</ControlTemplate>
</Button.Template>
</Button>
运行时,此示例将生成一个如下所示的按钮:
让我们来看看这个例子。 我们首先声明带有一些样式属性的 Button 元素。 我们没有像在真实应用程序中那样在资源部分定义单独的样式或控件模板,而是再次声明模板内联以节省空间。
在控件模板中,我们首先声明了一个带有翡翠绿色背景和 CornerRadius 值为 5 的 Border 元素。我们再次将 SnapsToDevicePixels 属性设置为 true 以确保边缘保持清晰。
在边框内,我们在 Grid 面板中定义了两个元素。 第一个是产生反射效果的 Rectangle 元素,第二个是必需的 ContentPresenter 对象。 该矩形在 RadiusX 和 RadiusY 属性中使用值 4 并适当设置 Margin 属性以确保反射边缘周围有一个微小的间隙。
它还将其 SnapsToDevicePixels 属性设置为 true 以确保不会模糊这个微小的间隙。 请注意,底部边距的值为 7,因为我们不希望反射效果覆盖按钮的下半部分。 Fill 属性是实际创建反射效果的位置。
在矩形的 Fill 属性中,我们通过将 StartPoint 和 EndPoint 属性的 X 值以及 StartPoint.Y 属性都设置为 0 并将 Endpoint.Y 属性设置为 1 来定义一个垂直的 LinearGradientBrush 元素; 在图表上绘制这些点将产生一条垂直线,因此这会产生一个垂直梯度。
在 LinearGradientBrush 对象的 GradientStops 集合中,我们定义了两个 GradientStop 元素。 第一个偏移量为零,设置为白色,十六进制 Alpha 通道值为 BF,不透明度值接近 0.7。 第二个的偏移量为 0.8,并设置为白色,其十六进制 Alpha 通道值为 00,这会产生完全透明的颜色,并且可以用透明颜色替换。
因此,生成的渐变从顶部开始略微透明,在底部完全透明,底部边距和偏移值实际上位于按钮的中间。 与我们的其他示例一样,ContentPresenter 对象是在之后声明的,以便在反射效果之上呈现。
创建发光效果
我们可以为控件创建的另一个效果是发光的外观,就好像一盏灯从控件内部向外发光。 我们需要另一个 LinearGradientBrush 实例和 UI 元素来绘制它。 矩形元素非常适合这个角色,因为它非常轻巧。 我们应该在 App.xaml 文件的应用程序资源中定义这些资源,以使每个 View 都可以使用它们:
<TransformGroup x:Key="GlowTransformGroup">
<ScaleTransform CenterX="0.5" CenterY="0.85" ScaleY="1.8" />
<TranslateTransform Y="0.278" />
</TransformGroup>
<RadialGradientBrush x:Key="GreenGlow" Center="0.5,0.848"
GradientOrigin="0.5,0.818" RadiusX="-1.424" RadiusY="-0.622"
RelativeTransform="{StaticResource GlowTransformGroup}">
<GradientStop Color="#CF65FF00" Offset="0.168" />
<GradientStop Color="#4B65FF00" Offset="0.478" />
<GradientStop Color="#0065FF00" Offset="1" />
</RadialGradientBrush>
<Style x:Key="GlowingButtonStyle" TargetType="{x:Type Button}">
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border BorderBrush="White" BorderThickness="1"
Background="DarkGray" CornerRadius="3">
<Grid>
<Rectangle IsHitTestVisible="False" RadiusX="2"
RadiusY="2" Fill="{StaticResource GreenGlow}" />
<ContentPresenter Content="{TemplateBinding Content}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
<Border.Effect>
<DropShadowEffect Color="#FF65FF00" ShadowDepth="4"
Opacity="0.4" Direction="270" BlurRadius="10" />
</Border.Effect>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
我们首先声明一个 TransformGroup 元素,该元素使我们能够将一个或多个变换对象组合在一起。 在其中,我们定义了一个 ScaleTransform 元素,该元素将应用的元素垂直缩放默认因子 1,水平缩放因子 1.8。 我们使用其 CenterX 和 CenterY 属性指定此转换的中心。 接下来,我们声明一个 TranslateTransform 元素,它将应用的元素向下移动少量。
在此之后,我们定义了一个 RadialGradientBrush 对象,它将代表我们设计中的发光。 我们使用 RadiusX 和 RadiusY 属性来塑造画笔元素,并指定 Center 和 GradientOrigin 属性来指示径向渐变的中心和焦点。
然后我们将 TransformGroup 元素设置为画笔的 RelativeTransform 属性,以对其应用变换。 请注意,三个 GradientStop 元素都使用相同的 R、G 和 B 值,只是 alpha 通道或不透明度值不同。
接下来,我们为 Button 类型声明 GlowingButtonStyle 样式,将 SnapsToDevicePixels 属性设置为 true,以保持其线条清晰锐利。 在 Template 属性中,我们定义了一个 ControlTemplate 元素,它带有一个带有略微圆角的白色 Border 元素。
在边框内,我们声明了一个 Grid 面板,其中包含一个 Rectangle 和一个 ContentPresenter 元素。 同样,矩形的 RadiusX 和 RadiusY 属性设置为小于父边框控件的 CornerRadius 属性的值,以确保它均匀地适合其中。 我们的 RadialGradientBrush 资源被指定为矩形的 Fill 属性。
ContentPresenter 对象居中以确保按钮的内容将呈现在其中心。 返回到 Border 元素,我们看到在其 Effect 属性中声明了 DropShadowEffect。 但是,这个元素不是在这里创建阴影 影响; 这个类是多功能的,还可以渲染发光效果和阴影效果。
诀窍是将其 Color 属性设置为黑色以外的颜色,并将其 BlurRadius 属性设置为比我们通常在创建阴影效果时使用的更大的值。 在这种特殊情况下,我们将 Direction 属性设置为 270,ShadowDepth 属性设置为 4,以便将发光效果定位在边框底部,即光线应该来自的位置。
不幸的是,这种效果不能很好地转化为灰度和纸张,因此当不在彩色和屏幕上查看时,发光效果会有所损失。 对于本书电子书版本的读者,以下是我们示例中的发光效果:
把它们放在一起
虽然这些不同的效果可以单独改善我们控件的外观,但最大的改进可以在将其中的一些合并到一个设计中时找到。 在下一个示例中,我们将这样做。 我们首先需要添加更多的资源来使用:
<SolidColorBrush x:Key="TransparentWhite" Color="#7FFFFFFF" />
<SolidColorBrush x:Key="VeryTransparentWhite" Color="#3FFFFFFF" />
<SolidColorBrush x:Key="TransparentBlack" Color="#7F000000" />
<SolidColorBrush x:Key="VeryTransparentBlack" Color="#3F000000" />
<VisualBrush x:Key="SemiTransparentLayeredButtonBackground"
Visual="{StaticResource LayeredButtonBackgroundElements}"
Opacity="0.65" />
这里没有什么太复杂的。 我们只是定义了一些颜色,它们具有不同的透明度和一个稍微透明的视觉画笔版本,它引用了我们的分层背景元素。 现在让我们转到包含样式:
<Style TargetType="{x:Type Button}">
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border CornerRadius="3"
BorderBrush="{StaticResource TransparentBlack}"
BorderThickness="1"
Background="{StaticResource TransparentWhite}">
<Border Name="InnerBorder" CornerRadius="2"
Background="{StaticResource LayeredButtonBackground}"
Margin="1">
<Grid>
<Rectangle IsHitTestVisible="False" RadiusX="2"
RadiusY="2" Fill="{StaticResource GreenGlow}" />
<ContentPresenter Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding
HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding
VerticalContentAlignment}" />
</Grid>
</Border>
<Border.Effect>
<DropShadowEffect Color="Black" ShadowDepth="6"
BlurRadius="6" Direction="270" Opacity="0.5" />
</Border.Effect>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="InnerBorder"
Property="Background" Value="{StaticResource
SemiTransparentLayeredButtonBackground}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="InnerBorder" Property="Background"
Value="{StaticResource LayeredButtonBackground}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
查看示例 XAML,我们可以看到 SnapsToDevicePixels 属性设置为 true,以避免抗锯齿伪影模糊按钮边缘,并且 Cursor 属性设置为在用户鼠标悬停时显示指向手指光标 按钮。
在控件模板中,我们看到了两个嵌套的 Border 元素。 请注意,外边框使用了透明黑色和透明白色画笔资源,因此它是半透明的。 另外,请注意,白色内边框实际上来自外边框的背景而不是内边框,这会将 Margin 属性设置为 1 以给人一种内边框的印象。
在这个例子中,内边框元素只负责显示来自视觉画笔的分层按钮元素,并且没有自己的显示边框。 再次,我们调整了它的 CornerRadius 属性,使它整齐地适合外边框。 我们可以在 WPF 设计器中放大放大倍数,以帮助我们决定在这里应该使用什么值。
在内部边框内,我们声明了一个 Grid 面板,以便我们可以添加所需的 ContentPresenter 和使用资源中的 GreenGlow 画笔绘制的 Rectangle 元素。 同样,我们将其 IsHitTestVisible 属性设置为 false,以便用户无法与其交互,并设置 RadiusX 和 RadiusY 属性以匹配内边框的 CornerRadius 值。
我们使用 TemplateBinding 元素将 ContentPresenter 对象的属性映射到模板对象中的合适属性,以便在我们的按钮上设置属性可以影响其定位和内容。 接下来,我们将之前显示的 DropShadowEffect 元素设置为外边框的 Effect 属性,并汇总模板中包含的 UI 元素。
为了使模板更有用,我们在 ControlTemplate.Triggers 集合中设置了一些 Trigger 对象,这将为我们的按钮添加鼠标悬停效果。 第一个触发器以 IsMouseOver 属性为目标,并在为 true 时将内边框的背景设置为稍微透明的分层按钮元素视觉画笔版本。
第二个触发器以 IsPressed 属性为目标,并在该属性为 true 时重新应用原始视觉画笔。 请注意,这两个触发器必须按此顺序定义,以便当两个条件都为真时,以 IsPressed 属性为目标的触发器将覆盖另一个触发器。 当然,点击时按钮是亮起还是熄灭,或者甚至改变颜色,这都是个人喜好问题。
请注意,我们省略了此样式的 x:Key 指令,因此它将隐式应用于所有没有显式应用不同样式的 Button 元素。 因此,我们可以在不指定样式的情况下声明 Button 元素,如下面的代码片段:
<Button Content="Click Me" Width="200" Height="40" FontSize="20"
Foreground="White" />
这会产生以下视觉输出:
我们也可以通过定义许多不同的颜色资源并使用数据模板中的数据触发器来更改发光的颜色以指示数据对象的不同状态,从而将这个发光的想法更进一步。 除了通常的文本反馈方法之外,这使我们能够向用户提供更多的视觉信息。
例如,数据模型对象上的蓝色发光可以指定未更改的对象,而绿色可以表示具有有效更改的对象,红色可以突出显示错误的对象。 我们将在下一章中看到如何实现这个想法,但现在,让我们继续寻找不同的方法来让我们的应用程序脱颖而出。
远离平凡
一般来说,绝大多数业务应用程序看起来相当普通,具有包含标准矩形表单字段库的各种表单页面。 另一方面,视觉上吸引人的应用程序从人群中脱颖而出。 因此,为了创建视觉上吸引人的应用程序,我们需要远离平凡。
这是否意味着简单地为我们的控件添加带有圆角的控件模板或更多内容取决于您。 有许多不同的方法可以增强控件的外观,我们将在本节中介绍其中的一些想法。 让我们从最适合与徽标或启动和背景图像一起使用的反射效果开始。
投射反射
所有 FrameworkElement 派生的类都有一个 RenderTransform 属性,我们可以利用该属性以各种方式转换它们的渲染输出。 ScaleTransform 元素使我们能够在水平和垂直方向上缩放每个对象。 ScaleTransform 对象的一个有用方面是我们也可以负向缩放,因此可以反转视觉输出。
我们可以用这个特定的面创建一个视觉上令人愉悦的效果是对象的镜像或反射。 为了增强这种效果,我们可以使用不透明蒙版在反射从对象后退时淡出反射。 这可以给人一种物体在光亮表面上反射的视觉印象,如下图所示:
让我们看看如何实现这个结果:
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"
Width="348">
<TextBlock Name="TextBlock" FontFamily="Candara"
Text="APPLICATION NAME" FontSize="40" FontWeight="Bold">
<TextBlock.Foreground>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="Orange" />
<GradientStop Color="Red" Offset="0.5" />
<GradientStop Color="Orange" Offset="1" />
</LinearGradientBrush>
</TextBlock.Foreground>
</TextBlock>
<Rectangle Height="31" Margin="0,-11.6,0,0">
<Rectangle.Fill>
<VisualBrush Visual="{Binding ElementName=TextBlock}">
<VisualBrush.RelativeTransform>
<ScaleTransform ScaleY="-1.0" CenterX="0.5" CenterY="0.5" />
</VisualBrush.RelativeTransform>
</VisualBrush>
</Rectangle.Fill>
<Rectangle.OpacityMask>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#DF000000" />
<GradientStop Color="Transparent" Offset="0.8" />
</LinearGradientBrush>
</Rectangle.OpacityMask>
</Rectangle>
</StackPanel>
在此示例中,我们使用 StackPanel 对象将 TextBlock 元素定位在 Rectangle 元素上方。 文本将是要反射的对象,并且反射将在矩形中生成。 面板的宽度受到限制,以确保反射完全适合文本元素。 我们首先命名 TextBlock 元素并设置一些字体属性以及要输出的文本。
我们设置了一个 LinearGradientBrush 对象作为文本的颜色以使其更有趣,尽管这对创建反射效果没有任何作用。 接下来,请注意 Rectangle 元素的大小和位置正好适合 TextBlock 元素中文本的大小。 我们当然可以使用这种技术来反映任何东西,而不仅限于反映文本元素。
矩形的背景是使用 VisualBrush 对象绘制的,其中 Visual 属性是使用 ElementName 属性绑定到 TextBlock 元素的可视输出的数据。 注意 VisualBrush 对象的 RelativeTransform 属性,它使我们能够以某种方式转换视觉对象,并设置为 ScaleTransform 类的实例。
这是创建这种效果的最重要的组成部分之一,因为这个元素是在垂直平面上反转相关视觉的元素。 将 ScaleY 属性设置为 -1 将为我们垂直反转视觉效果,而将 ScaleX 属性设置为 -1 将水平反转视觉效果。 请注意,我们在此处省略了 ScaleX 属性,因为我们希望将其设置为默认值 1。
接下来,我们看到 OpacityMask 属性,它允许我们设置渐变画笔以映射到矩形的不透明度。 当画笔的alpha通道为1时,矩形是不透明的,为0时,矩形是透明的,介于两者之间时,矩形是半透明的。 这是此效果的另一个重要部分,会创建反射图像的淡入淡出。
在我们的示例中,我们有一个垂直渐变,它在顶部几乎是纯黑色,并且变得越来越透明,直到它向下达到五分之四时,它变得完全透明。 当设置为矩形的 OpacityMask 时,仅使用 Alpha 通道值,这会导致它在顶部完全可见,然后在向下五分之四处淡入不可见,如上图所示。
探索无边界窗口
使用 WPF,可以创建没有边框的窗口、标题栏以及标准的最小化、恢复和关闭按钮。 也可以创建不规则形状的窗户和带有透明区域的窗户,以显示下面的任何东西。 虽然让我们的主应用程序窗口无边框有点不合常规,但我们仍然可以利用这种能力。
例如,我们可以为自定义消息框、扩展工具提示或任何其他向最终用户提供信息的弹出控件创建一个无边框窗口。 只需几个简单的步骤即可创建无边框窗口。 让我们从基础开始,假设我们将其添加到现有的应用程序框架中。
在这种情况下,我们已经有了 MainWindow 类,需要添加一个额外的窗口。 正如我们在第 6 章“调整内置控件”中看到的,我们可以通过在项目中添加一个新的 UserControl 并在 XAML 文件及其关联的代码隐藏文件中用单词 Window 替换单词 UserControl 来做到这一点。 两者都更改失败将导致设计时错误,抱怨类不匹配。
或者,我们可以右键单击启动项目并选择添加,然后选择窗口…,然后将其剪切并粘贴到您希望它驻留的任何位置。 不幸的是,Visual Studio 没有提供其他方法来将 Window 控件添加到我们的其他项目中。
一旦我们有了 Window 对象,我们需要做的就是将其 WindowStyle 属性设置为 None 并将其 AllowsTransparency 属性设置为 true。 这将导致我们的窗口出现白色背景:
<Window
x:Class="CompanyName.ApplicationName.Views.Controls.BorderlessWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="100" Width="200" WindowStyle="None" AllowsTransparency="True">
</Window>
...
using System.Windows;
namespace CompanyName.ApplicationName.Views.Controls
{
public partial class BorderlessWindow : Window
{
public BorderlessWindow()
{
InitializeComponent();
}
}
}
然而,虽然这移除了我们都习惯的默认窗口镶边并为我们提供了无边框窗口,但它也移除了标准按钮,因此我们无法直接关闭、调整大小甚至移动窗口。 幸运的是,使我们的窗口可移动是一件非常简单的事情。 在调用 InitializeComponent 方法后,我们只需在窗口的构造函数中添加以下代码行:
MouseLeftButtonDown += (o, e) => DragMove();
这个 DragMove 方法在 Window 类中声明,使我们能够从其边界内的任何位置单击和拖动窗口。 通过添加我们自己的标题栏并将这个匿名事件处理程序附加到该对象的 MouseLeftButtonDown 事件,我们可以轻松地重新创建只能从标题栏移动窗口的普通窗口功能。
如果我们希望我们的无边框窗口可以调整大小,Window 类中有一个 ResizeMode 属性,它为我们提供了一些选项。 我们可以在无边框窗口中使用的一个值是 CanResizeWithGrip 值。 此选项添加所谓的调整大小夹点,由窗口右下角的三角形点图案指定,用户可以使用它调整窗口大小。
如果我们将 ResizeMode 属性设置为此值并将背景设置为与此调整大小的夹点形成对比的颜色,我们将以以下视觉输出结束:
但是,我们仍然没有办法关闭窗口。 为此,我们可以添加自己的按钮,或者通过按 Esc 键或键盘上的其他键来关闭窗口。 无论哪种方式,无论触发什么,关闭窗口都是调用窗口的 Close 方法的简单问题。
与其实现一个可以通过几个边框轻松实现的替换窗口镶边,让我们专注于开发一个形状不规则的无边框窗口,我们可以用它来为用户弹出有用的信息。 通常,我们需要将窗口的背景设置为透明来隐藏它,但是我们将替换它的控件模板,所以我们不需要这样做。
对于此示例,我们也不需要调整夹点大小,因此让我们将 ResizeMode 属性设置为 NoResize。 我们也不需要通过鼠标移动这个标注窗口,所以我们不需要添加调用 DragMove 方法的匿名事件处理程序。
由于这个窗口只会向用户提供信息,我们还应该设置一些其他的窗口属性。 要设置的一个重要属性是 ShowInTaskbar 属性,它指定应用程序图标是否应出现在 Windows 任务栏中。 由于此窗口将成为我们主应用程序的组成部分,因此我们将此属性设置为 false,以便隐藏其图标。
对于这种情况,另一个有用的属性是 WindowStartupLocation 属性,它可以使用 Window.Top 和 Window.Left 属性来定位窗口。 通过这种方式,标注窗口可以以编程方式定位在屏幕上需要的任何位置。 在继续之前,让我们看看这个窗口的代码:
<Window x:Class="CompanyName.ApplicationName.Views.Controls.CalloutWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Controls=
"clr-namespace:CompanyName.ApplicationName.Views.Controls"
WindowStartupLocation="Manual">
<Window.Resources>
<Style TargetType="{x:Type Controls:CalloutWindow}">
<Setter Property="ShowInTaskbar" Value="False" />
<Setter Property="WindowStyle" Value="None" />
<Setter Property="AllowsTransparency" Value="True" />
<Setter Property="ResizeMode" Value="NoResize" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Controls:CalloutWindow}">