为 WPF 和 Silverlight 创建内阴影效果
在 WPF 和 Silverlight 中创建 Photoshop 风格的内阴影效果的一些技巧。
引言
如果你曾被要求将 Photoshop 设计转换为 WPF UI,你可能已经尝试过 Expression Blend 的 Photoshop 导入功能。因此你可能知道,尽管它在导入简单的 Photoshop 文件方面做得相当不错,但当要求它转换设计师们喜欢用以点缀其杰作的小细节时,它就会力不从心。
比如内阴影。
在本文中,我将向你展示几种在 WPF 中创建内阴影效果的方法,其中一种也适用于 Silverlight。
很高兴认识你,内阴影先生
如果你以前从未见过内阴影,请允许我为你介绍一下。
这是一个灰色矩形
这是一个带有内阴影的灰色矩形
很微妙,不是吗?但它为渲染增添了一丝真实感。
剪裁区域和不透明度蒙版
问题是,我们如何在 WPF 中创建内阴影?查看 System.Windows.Media.Effects
命名空间,你会找到 DropShadowEffect
类,但没有 InnerShadowEffect
。别让这迷惑了你。DropShadowEffect
可以用来创建内阴影,利用 WPF 的两个巧妙功能:剪裁区域和不透明度蒙版。
剪裁区域和不透明度蒙版都实现了类似的效果,尽管方式不同。它们指示 WPF 在将视觉元素渲染到屏幕时,修剪或隐藏其某些部分。剪裁区域是几何图形——矩形、椭圆或任意路径,它们指定了形状的边界。几何图形内的一切都被渲染,几何图形外的一切都被忽略。它们告诉 WPF,你只能渲染到此,不能再远。
不透明度蒙版就像 WPF 覆盖在你的元素顶部的模板。你的元素中的每个像素都被赋予与不透明度蒙版中对应像素相同的不透明度。如果不透明度蒙版中的像素是透明的,那么 WPF 会忽略你的元素中对应的像素。如果不透明度蒙版中的像素是不透明的或半透明的,那么 WPF 会渲染你的元素中的该像素。
使用 ClipToBounds 的内阴影
我将从最简单的绘制内阴影的技术开始:使用 ClipToBounds
属性应用剪裁区域。将其设置为 true
,WPF 将确保元素或其子元素的任何部分都不会溢出其边界之外。这是使用剪裁区域最简单的方法,因为你不必考虑它们的几何形状,也不必担心元素大小改变时需要调整大小:WPF 将元素的自然边框矩形用作剪裁区域。
以下是我创建上面所示内阴影的方法
<Border Background="LightGray" BorderBrush="DarkGray"
BorderThickness="1" ClipToBounds="True">
<Border Background="Transparent" BorderBrush="Black"
BorderThickness="1" Margin="-2">
<Border.Effect>
<DropShadowEffect ShadowDepth="0" BlurRadius="10">
</Border.Effect>
</Border>
</Border>
这里有几点需要注意
- 我将
DropShadowEffect
应用到一个具有实心边框但透明填充的元素(本例中为Border
)上。这会在边框的两侧产生阴影效果。 DropShadowEffect
的ShadowDepth
设置为 0,以便边框和阴影起点之间没有间隙。这也能确保阴影在所有方向上都是均匀的。如果你想改变阴影的亮度,可以调整DropShadowEffect
的BlurRadius
和Opacity
属性。还有Color
属性也可以尝试。- 使此阴影成为内阴影而非全方位阴影的关键部分是:带有
DropShadowEffect
的Border
嵌套在另一个具有ClipToBounds="True"
的边框内。这就是剪裁掉阴影外部的部分。 - 我在内部边框上使用负边距来将其边缘推出,以便它被外部边框的剪裁区域剪裁,从而有效地隐藏它。
你可以尝试的另一件事是改变内部边框的厚度。这可以让你改变内阴影的密度。例如,BorderThickness
为 10(并用 –12 的 Margin
来补偿)会给你
只想在几个边缘上加阴影?这很简单:只需使用内部 Border
的 BorderThickness
属性关闭相应的线条即可。
这是一个矩形,其顶部边缘有一个内阴影
这是标记
<Border Background="LightGray" BorderBrush="DarkGray"
BorderThickness="1" ClipToBounds="True" Width="400" Height="100">
<Border Background="Transparent" BorderBrush="Black"
BorderThickness="0,10,0,0" Margin="0,-11,0,0">
<Border.Effect>
<DropShadowEffect ShadowDepth="0" BlurRadius="10"/>
</Border.Effect>
</Border>
</Border>
遗憾的是,这种易于使用的方法在 Silverlight 中不起作用,因为它没有 ClipToBounds
属性。但不要灰心。我还有另一个技巧。
使用 Clip 属性的内阴影
在 Silverlight 和 WPF 中同样有效的一种技术是使用 Clip
属性设置剪裁区域的几何形状。这是一个例子
<Grid>
<Rectangle Width="400" Height="100" Fill="LightYellow"
Stroke="Orange" StrokeThickness="2" RadiusX="8" RadiusY="8"/>
<Rectangle Width="400" Height="100" Fill="Transparent"
Stroke="Orange" StrokeThickness="2" RadiusX="8" RadiusY="8">
<Rectangle.Effect>
<DropShadowEffect ShadowDepth="0" BlurRadius="15" Color="Orange"/>
</Rectangle.Effect>
<Rectangle.Clip>
<RectangleGeometry Rect="0,0,400,100" RadiusX="8" RadiusY="8"/>
</Rectangle.Clip>
</Rectangle>
</Grid>
渲染后看起来像这样
正如你所看到的,我使用的是相同的技巧:将 DropShadowEffect
应用于具有实心边框但透明填充的形状,然后剪裁掉外部阴影,这次是通过在同一形状上设置剪裁区域。为了获得背景填充,我在后面放置了形状的另一个副本(使用 Grid
将两者分层),并将其 Fill
设置为我想要的颜色。
使这种方法不如 ClipToBounds
方法容易使用的原因是,Clip
属性中的几何图形必须与你所应用元素的形状完全匹配。如果元素大小发生变化,几何图形也必须更新——WPF 不会自动完成此操作。因此,这种技术在代码中可能工作得很好,但在 XAML 中就不那么直接了,除非你使用简单、静态的形状。
如果你确实选择了这条路线,这里有一个提示:Expression Blend 对将 剪裁路径应用于元素 有很好的支持。
使用不透明度蒙版的内阴影
我想向你展示的最后一种技术是使用 不透明度蒙版 绘制内阴影。这就是我们想要达到的效果
这是如何在 XAML 中实现它
<Grid Width="400" Height="200">
<Grid.OpacityMask>
<VisualBrush Visual="{Binding ElementName=Shape}" Stretch="None" />
</Grid.OpacityMask>
<Path x:Name="Shape"
Data="M98.765432,136.43836 L180.41152,1.6438356 262.05761,103.56164 343.7037,
21.369863 470.12346,126.57534 596.54321,54.246575 551.76955,231.78082 638.68313,
340.27397 528.06584,353.42466 525.4321,478.35616 391.11111,373.15068 285.76132,
461.91781 217.28395,350.13699 135.63786,475.06849 114.5679,346.84932 1.3168724,
251.50685 140.90535,212.05479 z"
Fill="#FFF8F93F" Stroke="#FFAB6600" Stretch="Fill" />
<Path Data="M100.57143,137.83784 L181.55102,4.8648649 262.53061,105.40541 343.5102,
24.324324 468.89796,128.10811 594.28571,56.756757 549.87755,231.89189 636.08163,
338.91892 526.36735,351.89189 523.7551,475.13514 390.53061,371.35135 286.04082,
458.91892 218.12245,348.64865 137.14286,471.89189 116.2449,345.40541 3.9183673,
251.35135 142.36735,212.43243 z"
Stroke="#FFAB6600" StrokeThickness="3" Stretch="Fill" >
<Path.Effect>
<DropShadowEffect ShadowDepth="0" BlurRadius="20" Color="Orange"/>
</Path.Effect>
</Path>
</Grid>
同样,我们在一个 Grid
中分层放置了两个形状的副本,第一个用于提供背景颜色,上面的一个用于创建阴影效果。
这次的区别在于我们设置了 Grid
的 OpacityMask
属性。我们利用 WPF 的 VisualBrush
从实心填充的形状创建蒙版。这意味着当 Grid
及其内容被渲染并应用由实心形状制成的不透明度蒙版时,该基本形状之外的像素将不会被渲染,从而巧妙地剪裁掉由顶部形状创建的阴影的外部部分。请注意,务必将 VisualBrush
上的 Stretch="None"
设置为获取蒙版与其所蒙版的形状的精确对齐。
这种技术的好处是 Grid
会为我们处理所有的尺寸调整,这使得它比设置剪裁区域更容易使用。一个缺点是性能不会那么好,因为使用不透明度蒙版和 VisualBrush 将需要 WPF 创建中间渲染目标,它首先在其中绘制元素,然后将它们与场景的其余部分组合。这可以在一定程度上通过明智地使用 位图缓存 来抵消。
Silverlight 和不透明度蒙版
虽然 Silverlight 支持不透明度蒙版,但它不支持 VisualBrush。因此,不幸的是,这排除了在 Silverlight 中使用最后一种技术。
致谢
我不能声称我是第一个想到这些技术的人。灵感来自 Timo Pijnappel 和另一个我目前因为 Google Foo 耗尽而无法找到的帖子。如果你认为那可能是你的,请告诉我!