1. 程式人生 > >【Win10】【Win2D】實現控制元件陰影效果

【Win10】【Win2D】實現控制元件陰影效果

原文: 【Win10】【Win2D】實現控制元件陰影效果

學過 WPF 的都知道,在 WPF 中,為控制元件新增一個陰影效果是相當容易的。

<Border Width="100"
        Height="100"
        Background="Red">
    <Border.Effect>
        <DropShadowEffect />
    </Border.Effect>
</Border>

那麼這樣就會顯示一個 100 寬、100 高,背景紅色,帶有陰影的矩形了。如下圖所示。

QQ截圖20151105202037

 

但是,在 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 方法的程式碼則類似於《快速入門》裡面對圖片施加高斯模糊。

編譯並執行後你應該會看見這樣的效果:

QQ截圖20151105205053

一坨黑乎乎的東西,而且是毛邊的。

 

在上面的程式碼中,關鍵就是

ShadowEffect effect = new ShadowEffect()
{
    Source = cl
};

這一句聲明瞭一個陰影效果,並且源是上面那個命令列表,也就是表明對哪個物件施加陰影效果。在上面那個命令列表中繪製了一個在距離 canvas 左上角橫座標 100、縱座標 100,寬高 100 的矩形。

需要注意的是,儘管我們繪製的矩形是白色的,但是陰影效果是不關心的(詳細點說是不關心 RGB,A 通道還是有影響的),而且 ShadowEffect 有自己的顏色屬性。

 

在理清了如何編寫程式碼顯示陰影之後,我們再來探究下如何實現控制元件陰影。

未命名1

原理很簡單,無非就是在控制元件 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>

執行效果:

QQ截圖20151105221151

用在 Image 上也是不錯的說:

QQ截圖20151105221418

不過用在預設的 Button 上就比較難看了,因為 Button 本身預設的 Background 就是半透明的,然後背後一團黑乎乎的陰影。。。所以還是比較建議這個效果用在那些非透明的控制元件上。

 

最後放上專案原始碼:http://files.cnblogs.com/files/h82258652/ControlShadow.zip

Win2D 是個好東西,如果你覺得有些效果難以實現的話,可以嘗試一下 Win2D 的說。