學過 WPF 的都知道,在 WPF 中,為控件添加一個陰影效果是相當容易的。
<Border Width="100" Height="100" Background="Red"> <Border.Effect> <DropShadowEffect /> </Border.Effect> </Border>
那么這樣就會顯示一個 100 寬、100 高,背景紅色,帶有陰影的矩形了。如下圖所示。
但是,在 WinRT 中,基於 Metro 教義和性能考慮,巨硬扼殺了陰影。但是,需求多多少少還是會有的,以致於部分開發者不得不用漸變來實現蹩腳的“陰影”效果,而且仔細看上去會發現很假,連 duang 一下的特效都沒,一眼看上去這陰影效果就是假的。
那么,真正的陰影效果真的沒法實現了嗎?以前是。但是現在,我們有了 Win2D,什么增強光照啊、高斯模糊啊,都不是問題。陰影當然也是。
先來看看怎么繪制一個陰影先吧。
前台 XAML:
<Page x:Class="App92.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:win2d="using:Microsoft.Graphics.Canvas.UI.Xaml" Unloaded="Page_Unloaded"> <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <win2d:CanvasControl x:Name="canvas" Width="300" Height="300" HorizontalAlignment="Left" VerticalAlignment="Top" Draw="canvas_Draw" /> </Grid> </Page>
后台代碼:
using Microsoft.Graphics.Canvas; using Microsoft.Graphics.Canvas.Effects; using Microsoft.Graphics.Canvas.UI.Xaml; using Windows.Foundation; using Windows.UI; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; namespace App92 { public sealed partial class MainPage : Page { public MainPage() { this.InitializeComponent(); } private void canvas_Draw(CanvasControl sender, CanvasDrawEventArgs args) { CanvasCommandList cl = new CanvasCommandList(sender); using (CanvasDrawingSession clds = cl.CreateDrawingSession()) { clds.FillRectangle(new Rect(100, 100, 100, 100), Colors.White); } ShadowEffect effect = new ShadowEffect() { Source = cl }; args.DrawingSession.DrawImage(effect); } private void Page_Unloaded(object sender, RoutedEventArgs e) { if (this.canvas != null) { this.canvas.RemoveFromVisualTree(); this.canvas = null; } } } }
Page_Unloaded 里面是釋放 Win2D 使用的資源。這點在我上次翻譯的《【Win2D】【譯】Win2D 快速入門》里面有說過。
Draw 方法的代碼則類似於《快速入門》里面對圖片施加高斯模糊。
編譯並運行后你應該會看見這樣的效果:
一坨黑乎乎的東西,而且是毛邊的。
在上面的代碼中,關鍵就是
ShadowEffect effect = new ShadowEffect() { Source = cl };
這一句聲明了一個陰影效果,並且源是上面那個命令列表,也就是表明對哪個對象施加陰影效果。在上面那個命令列表中繪制了一個在距離 canvas 左上角橫坐標 100、縱坐標 100,寬高 100 的矩形。
需要注意的是,盡管我們繪制的矩形是白色的,但是陰影效果是不關心的(詳細點說是不關心 RGB,A 通道還是有影響的),而且 ShadowEffect 有自己的顏色屬性。
在理清了如何編寫代碼顯示陰影之后,我們再來探究下如何實現控件陰影。
原理很簡單,無非就是在控件 z 軸下面顯示陰影。
於是乎我們新建一個模板控件,我就叫它 Shadow,並寫出以下代碼。
cs 代碼:
using Microsoft.Graphics.Canvas.UI.Xaml; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Markup; namespace App92 { [ContentProperty(Name = nameof(Content))] public class Shadow : Control { public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(nameof(Content), typeof(FrameworkElement), typeof(Shadow), new PropertyMetadata(null)); private CanvasControl _canvas; public Shadow() { this.DefaultStyleKey = typeof(Shadow); this.Unloaded += this.OnUnloaded; } public FrameworkElement Content { get { return (FrameworkElement)this.GetValue(ContentProperty); } set { this.SetValue(ContentProperty, value); } } protected override void OnApplyTemplate() { base.OnApplyTemplate(); this._canvas = (CanvasControl)this.GetTemplateChild("PART_Canvas"); this._canvas.Draw += this.Canvas_Draw; } private void Canvas_Draw(CanvasControl sender, CanvasDrawEventArgs args) { // TODO。 } private void OnUnloaded(object sender, RoutedEventArgs e) { if (this._canvas != null) { this._canvas.RemoveFromVisualTree(); this._canvas = null; } } } }
Generic.xaml 代碼:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:win2d="using:Microsoft.Graphics.Canvas.UI.Xaml" xmlns:local="using:App92"> <Style TargetType="local:Shadow"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="local:Shadow"> <Grid> <win2d:CanvasControl x:Name="PART_Canvas" /> <ContentControl Content="{TemplateBinding Content}" /> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
這里我不選擇繼承自 ContentControl 是因為 ContentControl 的 Content 屬性是 object,而后文中我們需要使用到 FrameworkElement。
接下來,開始考慮編寫 Draw 代碼。
第一個問題,ShadowEffect 的 Source 是哪里來的?對於大部分控件,就是一個矩形,但是,部分如 Border 之類的控件,可能是圓角的(因為有 CornerRadius 屬性)。那么該如何得到一個控件的形狀呢?這里我們使用 RenderTargetBitmap 這個類,它能夠捕獲一個在可視樹上的控件的外觀。對於控件透明的部分,RenderTargetBitmap 就是透明的。那么 RenderTargetBitmap 得到的就相當於控件的形狀。但是,RenderTargetBitmap 是異步的,因此我們要將該部分寫在其它方法當中。因為 Draw 方法是不能夠編寫異步代碼的。
第二個問題,應該何時重繪陰影?也就是應該何時重新調用 RenderTargetBitmap?這個問題很容易解決,我們使用 FrameworkElement 的 LayoutUpdated 事件好了。所以我們上面的 Content 的屬性需要為 FrameworkElement。
第三個問題,從上面 Generic.xaml 來看,CanvasControl 是跟 ContentControl 一樣大小的,假設我們的 Content 剛好占滿了 ContentControl,那么在下面的 CanvasControl 豈不是無法顯示?!也就是說,這時候我們的陰影是完全沒辦法顯示的。所以,就必須要確保 CanvasControl 必須永遠大於 ContentControl,以確保有足夠的空間顯示陰影。使用 ScaleTransform 可以,但是效果不是十分好。要注意一點,ShadowEffect 是會發散的!也就是說,經過 ShadowEffect 處理過的輸出是會比輸入要大,所以我們並不需要進行縮放,增大容納空間即可。對 CanvasControl 使用一個負數的 Margin 是一個相對較好的解決方案。至於負多少,我個人認為 10 個像素就足夠了,畢竟 ShadowEffect 的發散有限。
另外為了滿足實際需要,我們仿照下 WPF 的 DropShadowEffect 類,添加陰影顏色、陰影方向、陰影距離這些屬性。修改 cs 代碼如下:
using Microsoft.Graphics.Canvas; using Microsoft.Graphics.Canvas.Effects; using Microsoft.Graphics.Canvas.UI.Xaml; using System; using System.Numerics; using System.Runtime.InteropServices.WindowsRuntime; using Windows.ApplicationModel; using Windows.Foundation; using Windows.Graphics.DirectX; using Windows.Graphics.Display; using Windows.UI; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Markup; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Media.Imaging; namespace App92 { [ContentProperty(Name = nameof(Content))] public class Shadow : Control { public static readonly DependencyProperty ColorProperty = DependencyProperty.Register(nameof(Color), typeof(Color), typeof(Shadow), new PropertyMetadata(Colors.Black)); public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(nameof(Content), typeof(FrameworkElement), typeof(Shadow), new PropertyMetadata(null, ContentChanged)); public static readonly DependencyProperty DepthProperty = DependencyProperty.Register(nameof(Depth), typeof(double), typeof(Shadow), new PropertyMetadata(2.0d, DepthChanged)); public static readonly DependencyProperty DirectionProperty = DependencyProperty.Register(nameof(Direction), typeof(double), typeof(Shadow), new PropertyMetadata(270.0d)); private CanvasControl _canvas; private int _pixelHeight; private byte[] _pixels; private int _pixelWidth; public Shadow() { this.DefaultStyleKey = typeof(Shadow); this.Unloaded += this.OnUnloaded; } public Color Color { get { return (Color)this.GetValue(ColorProperty); } set { this.SetValue(ColorProperty, value); } } public FrameworkElement Content { get { return (FrameworkElement)this.GetValue(ContentProperty); } set { this.SetValue(ContentProperty, value); } } public double Depth { get { return (double)this.GetValue(DepthProperty); } set { this.SetValue(DepthProperty, value); } } public double Direction { get { return (double)this.GetValue(DirectionProperty); } set { this.SetValue(DirectionProperty, value); } } protected override void OnApplyTemplate() { base.OnApplyTemplate(); this._canvas = (CanvasControl)this.GetTemplateChild("PART_Canvas"); this._canvas.Draw += this.Canvas_Draw; this.ExpendCanvas(); } private static void ContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { Shadow obj = (Shadow)d; FrameworkElement oldValue = (FrameworkElement)e.OldValue; if (oldValue != null) { oldValue.LayoutUpdated -= obj.Content_LayoutUpdated; } FrameworkElement newValue = (FrameworkElement)e.NewValue; if (newValue != null) { newValue.LayoutUpdated += obj.Content_LayoutUpdated; } } private static void DepthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { Shadow obj = (Shadow)d; obj.ExpendCanvas(); } private void Canvas_Draw(CanvasControl sender, CanvasDrawEventArgs args) { if (this.Content == null || this._pixels == null || this._pixelWidth <= 0 || this._pixelHeight <= 0) { // 不滿足繪制條件,清除 Canvas。 args.DrawingSession.Clear(sender.ClearColor); } else { // 計算內容控件相對於 Canvas 的位置。 GeneralTransform transform = this.Content.TransformToVisual(sender); Vector2 location = transform.TransformPoint(new Point()).ToVector2(); using (CanvasCommandList cl = new CanvasCommandList(sender)) { using (CanvasDrawingSession clds = cl.CreateDrawingSession()) { using (CanvasBitmap bitmap = CanvasBitmap.CreateFromBytes(sender, this._pixels, this._pixelWidth, this._pixelHeight, DirectXPixelFormat.B8G8R8A8UIntNormalized, DisplayInformation.GetForCurrentView().LogicalDpi)) { // 在 Canvas 對應的位置中繪制內容控件的外觀。 clds.DrawImage(bitmap, location); } } float translateX = (float)(Math.Cos(Math.PI / 180.0d * this.Direction) * this.Depth); float translateY = 0 - (float)(Math.Sin(Math.PI / 180.0d * this.Direction) * this.Depth); Transform2DEffect finalEffect = new Transform2DEffect() { Source = new ShadowEffect() { Source = cl, BlurAmount = 2,// 陰影模糊參數,越大越發散,感覺 2 足夠了。 ShadowColor = this.GetShadowColor() }, TransformMatrix = Matrix3x2.CreateTranslation(translateX, translateY) }; args.DrawingSession.DrawImage(finalEffect); } } } private async void Content_LayoutUpdated(object sender, object e) { if (DesignMode.DesignModeEnabled || this.Visibility == Visibility.Collapsed || this.Content.Visibility == Visibility.Collapsed) { // DesignMode 不能調用 RenderAsync 方法。 // 控件自身隱藏或者內容隱藏時也不能調用 RenderAsync 方法。 this._pixels = null; this._pixelWidth = 0; this._pixelHeight = 0; } else { RenderTargetBitmap bitmap = new RenderTargetBitmap(); await bitmap.RenderAsync(this.Content); int pixelWidth = bitmap.PixelWidth; int pixelHeight = bitmap.PixelHeight; if (bitmap.PixelWidth > 0 && bitmap.PixelHeight > 0) { this._pixels = (await bitmap.GetPixelsAsync()).ToArray(); this._pixelWidth = pixelWidth; this._pixelHeight = pixelHeight; } else { // 內容寬或高為 0 時不能調用 GetPixelAsync 方法。 this._pixels = null; this._pixelWidth = pixelWidth; this._pixelHeight = pixelHeight; } } if (this._canvas != null) { // 請求重繪。 this._canvas.Invalidate(); } } private void ExpendCanvas() { if (this._canvas != null) { // 擴展 Canvas 以確保陰影能夠顯示。 this._canvas.Margin = new Thickness(0 - (this.Depth + 10)); } } private Color GetShadowColor() { if (this.Content.Visibility == Visibility.Collapsed) { return Colors.Transparent; } // 陰影透明度應該受內容的 Opacity 屬性影響。 double alphaProportion = Math.Max(0, Math.Min(1, this.Content.Opacity)); return Color.FromArgb((byte)(Color.A * alphaProportion), Color.R, Color.G, Color.B); } private void OnUnloaded(object sender, RoutedEventArgs e) { if (this._canvas != null) { this._canvas.RemoveFromVisualTree(); this._canvas = null; } } } }
然后在頁面上測試下吧。
<Page x:Class="App92.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:App92"> <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <local:Shadow HorizontalAlignment="Center" VerticalAlignment="Center"> <Border Background="Red" Width="100" Height="100"></Border> </local:Shadow> </Grid> </Page>
運行效果:
用在 Image 上也是不錯的說:
不過用在默認的 Button 上就比較難看了,因為 Button 本身默認的 Background 就是半透明的,然后背后一團黑乎乎的陰影。。。所以還是比較建議這個效果用在那些非透明的控件上。
最后放上項目源代碼:http://files.cnblogs.com/files/h82258652/ControlShadow.zip
Win2D 是個好東西,如果你覺得有些效果難以實現的話,可以嘗試一下 Win2D 的說。





