[UWP]テキストに影をつける

WPFでは添付プロパティとしてEffectがサポートされているので簡単に影付きの表示ができましたが、UWPだとそうはいかないようです。（要出典）

仕方がないのでUIElementを地道に画像処理をして、影を作り出す方法でやっていきましょう。

UWPでTextBlock等のUIElementを画像処理するのに、ここでは[Win2D](https://github.com/Microsoft/Win2D)を使っていきます。
Microsoftの内部チームが開発しているXAMLと親和性のあるDirext2Dベースのライブラリなので安心して利用できそうです。

VisualStudioであればnugetからWin2Dで検索すればすぐに見つかると思います（[Win2D.UWP - NuGet](https://www.nuget.org/packages/Win2D.uwp)）



Win2Dの準備ができたら、次はプログラムの概要を考えます。
影付きテキストを作成するまでの概念的な流れは

1. TextBlock(UIElement) の表示内容をWin2Dで扱えるCanvasBitmapとして書き出す
2. CanvasBitmapをソースにして黒でぼかした画像を作成する
3. 黒でぼかした画像をTextBlockの背景として配置する

です。

## UIElementからCanvasBitmapを作る

```csharp:Win2DBitmapExtention.cs
using System;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media.Imaging;
using Microsoft.Graphics.Canvas;

namespace MyApp.Views.UIEffects
{
	public static class Win2DBitmapExtention
	{
		public static async Task<CanvasBitmap> CreateBitmapFromUIElement(this ICanvasResourceCreator sender, UIElement ui)
		{
			using (var stream = new InMemoryRandomAccessStream())
			{
				// get the stream from the background image
				var target = new RenderTargetBitmap();
				await target.RenderAsync(ui);

				var pixelBuffer = await target.GetPixelsAsync();
				var pixels = pixelBuffer.ToArray();

				var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream);
				encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied, (uint)target.PixelWidth, (uint)target.PixelHeight, 96, 96, pixels);

				await encoder.FlushAsync();
				stream.Seek(0);

				// load the stream into our bitmap
				return await CanvasBitmap.LoadAsync(sender, stream);
			}
		}
	}
}


```

参考元：http://stackoverflow.com/questions/32582698/how-to-save-contents-of-canvascontrol-as-an-image-file

## 発光エフェクトを表現するUserControlを作る


```xml:GlowEffectHostControl.xaml
<ContentControl
    x:Class="MyApp.Views.UIEffects.GlowEffectHostControl"
    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"
    >

    <ContentControl.Template>
        <ControlTemplate>
            <Grid x:Name="RootPanel">

                <!-- CanvasControl add to here from code behind -->

                <ContentPresenter x:Name="ShadowTargetContentPresenter" />
                
            </Grid>
        </ControlTemplate>
    </ContentControl.Template>
    
</ContentControl>
```

```csharp:GlowEffectHostControl.xaml.cs
using System;
using Windows.Foundation;
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.UI;
using Microsoft.Graphics.Canvas.UI.Xaml;

namespace NicoPlayerHohoema.Views.UIEffects
{
	public sealed partial class GlowEffectHostControl : ContentControl
	{
		public GlowEffectHostControl()
		{
			this.InitializeComponent();

			this.Loaded += OnLoaded;
			this.Unloaded += OnUnloaded;
		}

		private void OnLoaded(object sender, RoutedEventArgs e)
		{
			if (TargetUI != null)
			{
				var canvasControl = new CanvasControl();

				canvasControl.CreateResources += BGCanvas_CreateResources;
				canvasControl.Draw += BGCanvas_Draw;

				Canvas.SetZIndex(canvasControl, -1);

				var rootPanel = GetTemplateChild("RootPanel") as Panel;
				rootPanel.Children.Add(canvasControl);
			}
		}

		private void OnUnloaded(object sender, RoutedEventArgs e)
		{
			_shadowEffect?.Dispose();
			_bitmap?.Dispose();
		}

		/// <summary>
		/// 影Bitmapの作成
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="args"></param>
		void BGCanvas_CreateResources(CanvasControl sender, CanvasCreateResourcesEventArgs args)
		{
			args.TrackAsyncAction(sender.CreateBitmapFromUIElement(TargetUI)
				.ContinueWith(x =>
				{
					_bitmap = x.Result;

					_shadowEffect = new ShadowEffect
					{
						Source = _bitmap
					};
				}).AsAsyncAction());
		}


		/// <summary>
		/// ContentPresenter読み込み後に動的に追加したCanvasControlへの描画
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="args"></param>
		void BGCanvas_Draw(CanvasControl sender, CanvasDrawEventArgs args)
		{
			_shadowEffect.BlurAmount = (float)GlowEffectBlurAmount;
			_shadowEffect.ShadowColor = GlowEffectColor;

			using (var session = args.DrawingSession)
			{
				session.DrawImage(_shadowEffect
					// Canvas上に描画する範囲
					, new Rect(GlowEffectTranslate.X, GlowEffectTranslate.Y, sender.ActualWidth, sender.ActualHeight)

					// Bitmapの参照範囲
					, new Rect(0, 0, _bitmap.SizeInPixels.Width, _bitmap.SizeInPixels.Height)

					, (float)GlowEffectOpacity
					);
			}
		}


		#region field value

		CanvasBitmap _bitmap;

		ShadowEffect _shadowEffect;

		#endregion


		#region GlowEffectTargetName Property

		public static readonly DependencyProperty GlowEffectTargetNameProperty =
			DependencyProperty.Register("GlowEffectTargetName"
				, typeof(string)
				, typeof(GlowEffectHostControl)
				, new PropertyMetadata("")
				);


		public string GlowEffectTargetName
		{
			get { return (string)GetValue(GlowEffectTargetNameProperty); }
			set { SetValue(GlowEffectTargetNameProperty, value); }
		}

		#endregion

		#region GlowEffectColor Property

		public static readonly DependencyProperty GlowEffectColorProperty =
			DependencyProperty.Register("GlowEffectColor"
				, typeof(Color)
				, typeof(GlowEffectHostControl)
				, new PropertyMetadata(Windows.UI.Colors.Black)
				);


		public Color GlowEffectColor
		{
			get { return (Color)GetValue(GlowEffectColorProperty); }
			set { SetValue(GlowEffectColorProperty, value); }
		}

		#endregion

		#region GlowEffectOpacity Property

		public static readonly DependencyProperty GlowEffectOpacityProperty =
			DependencyProperty.Register("GlowEffectOpacity"
				, typeof(double)
				, typeof(GlowEffectHostControl)
				, new PropertyMetadata(1.0)
				);


		public double GlowEffectOpacity
		{
			get { return (double)GetValue(GlowEffectOpacityProperty); }
			set { SetValue(GlowEffectOpacityProperty, value); }
		}

		#endregion

		#region GlowEffectBlurAmount Property

		public static readonly DependencyProperty GlowEffectBlurAmountProperty =
			DependencyProperty.Register("GlowEffectBlurAmount"
				, typeof(double)
				, typeof(GlowEffectHostControl)
				, new PropertyMetadata(1.0)
				);


		public double GlowEffectBlurAmount
		{
			get { return (double)GetValue(GlowEffectBlurAmountProperty); }
			set { SetValue(GlowEffectBlurAmountProperty, value); }
		}

		#endregion

		#region GlowEffectTranslate Property

		public static readonly DependencyProperty GlowEffectTranslateProperty =
			DependencyProperty.Register("GlowEffectTranslate"
				, typeof(TranslateTransform)
				, typeof(GlowEffectHostControl)
				, new PropertyMetadata(new TranslateTransform())
				);


		public TranslateTransform GlowEffectTranslate
		{
			get { return (TranslateTransform)GetValue(GlowEffectTranslateProperty); }
			set { SetValue(GlowEffectTranslateProperty, value); }
		}

		#endregion



		private UIElement TargetUI
		{
			get
			{
				return GetTemplateChild("ShadowTargetContentPresenter") as UIElement;
			}
		}
	}
}
```



## 発光エフェクトの発光色を黒にしてテキストの背景に配置する
```xml:TextWithShadow.xaml
<UserControl
    x:Class="MyApp.Views.TextWithShadow"
    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:myeffect="using:MyApp.Views.UIEffects"
>
    <Grid>
        <myeffect:GlowEffectHostControl
            GlowEffectTargetName="TextUI"
            GlowEffectBlurAmount="2.0"
            GlowEffectColor="Black"
            >
            <TextBlock 
                x:Name="TextUI"
                Text="影付きテキスト"
                Foreground="White"
                />
        </myeffect:GlowEffectHostControl>
    </Grid>
</UserControl>
```



## 補足

GlowEffectBlurAmountに大きめの値を設定すると、表示が崩れます。これはテキストの描画サイズ以上に表示領域を拡張するよう計算していないためです。（2.0はOK、5.0で崩れました）（Translateの設定でも同様に崩れそう（未検証））
これを改善したい場合には以下を参考にしてみてください。

https://github.com/Microsoft/Win2D-Samples/blob/master/ExampleGallery/Shared/GlowTextCustomControl.cs
（リンク先で"Expand"でページ内検索）


#### 蛇足
~~実利用を考えるとGlowEffectHostControlにUIElementを入れ子させる（下のコード）だけでエフェクトが掛けられると便利なんですが、TemplateControlのControlPresenterの読み込み機序とCanvasControlのCreateResource/Drawの機序への理解が足りず断念しました。ぐぬぬ…。~~ 


## 追記１
ContentControl+ContentPresenterでの実装ができたのでいろいろ修正。
記事は一晩寝かせよう（反省）


## 追記２
まだ開発段階のようですが、Microsoft.UI.Composition.Toolkitを使うと、この記事のエフェクトよりも柔軟なことがやれそうです。
https://github.com/Microsoft/WindowsUIDevLabs/tree/master/Demos/Reference%20Demos/Microsoft.UI.Composition.Toolkit

参考記事（英語）
https://mareinsula.wordpress.com/2015/12/09/xaml-attached-properties-composition-effects/

ただ、ShadowEffectなどの重い画像処理は[NoComposition]とマークされていてコンポジションとして扱えないようです。
http://microsoft.github.io/Win2D/html/T_Microsoft_Graphics_Canvas_Effects_ShadowEffect.htm

画像に対するリッチなエフェクトとして使えそうですね。小回りのあるXAMLのUIとの親和性のあるエフェクトとはいかないようなので、この記事とは趣が少し違ってしまいますが、よかったら参考にしてみてください。