[WinRT] 画像を丸く切り抜いて表示する方法

アプリ開発企画 Spotlight スタート! – 高橋 忍のブログ – Site Home – MSDN Blogs
http://blogs.msdn.com/b/shintak/archive/2013/09/03/10445963.aspx

の中に「艦これ」の単語があるので、そのまま戦略ボードを作るのはつまらないので、昨晩つくってみたのがこれです。

戦略ボードとはなんか違う…というか、全く違うものになってしまったのでアレですし、そのままで版権の問題があるからストアには出せない(と思う)ので、テクニック的に晒しておきます。
元画像から円形でクリッピングして、それを覗いてみる、というサーチライトみたな効果ですね。ひとつだけのサーチライトの場合、黒の透過画像を使ってクリッピングするほうが楽なのですが、今回のように複数の円をクリッピングして表示させる場合には、直接クリッピングが必要になります。でも、需要はあるのか?

■WinRTではClipが四角しかできない

画像を丸く切り抜くためには、WPF では EllipseGeometry を使うのですが残念ながらストアアプリの WinRT にはこれがありません。RectangleGeometry という矩形のクリッピングしかできないので、矩形以外のクリップには ImageBrush を使います。

クイック スタート: Image と ImageBrush (Windows)
http://msdn.microsoft.com/ja-jp/library/windows/apps/hh868203.aspx

WFP では、できていたものが、WinRT で使えないのはどうなの?という気もしますが、まあ、描画速度を考えた処置なのでしょう。ちなみに、8.1 でも RectangleGeometry 以外は使えないので、実装漏れというわけではなさそうです。

■円でクリップするためにはFillを使う

ここでは、動的に画像を設定したいので、コードで書いていますが、XAML だけで書くとこんな感じになりあmす。

<Ellipse StrokeThickness="4" Stroke="Green"
         Canvas.Left="60" Canvas.Top="54" Width="200" Height="200">
    <Ellipse.Fill>
        <ImageBrush
            Stretch="None"
            ImageSource="ms-appx:///images/64[8].png">
        </ImageBrush>
    </Ellipse.Fill>
</Ellipse>

Stretch=”None” にしてあるのは、元のサイズのまま表示させるためなのですが、None の時は画像の中心で表示されるのが曲者ですね。これを左上を原点にするために、画像のサイズが必要になります。

■拡大縮小のためにManipulationDeltaを使う

コントロールの移動とサイズ変更には、ManipulationDelta イベントを使います。拡大縮小の場合は、コントロールの中心を原点にして行いたいので、こんな風に x,y 座標を調節する必要あります。Canvas をつかうと計算が多少楽になります。

private void Grid_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
{
    var el = e.OriginalSource as UIElement;

    if (e.Delta.Scale == 1.0)
    {
        double x = Canvas.GetLeft(el) + e.Delta.Translation.X;
        double y = Canvas.GetTop(el) + e.Delta.Translation.Y;
        Canvas.SetLeft(el, x);
        Canvas.SetTop(el, y);
    }
    else
    {
        double w = ((Circle)el).Width;
        double h = ((Circle)el).Height;
        double x = Canvas.GetLeft(el);
        double y = Canvas.GetTop(el);
        x -= (e.Delta.Scale-1.0) * w /2.0;
        y -= (e.Delta.Scale-1.0) * h /2.0;
        w *= e.Delta.Scale;
        h *= e.Delta.Scale;
        Canvas.SetLeft(el, x);
        Canvas.SetTop(el, y);
        ((Circle)el).Width = w;
        ((Circle)el).Height = h;
    }
}

■画像の位置合わせをするためにTranslateTransformを使う

移動させるためのコントロールは、ユーザーコントロールとして作っておきます。
参照させたいところ(TranslateTransform の X,Y の座標)は、あらかじめ名前を付けておきます。TranslateTransform というのは、先の ImageBrush で描画するときの原点を設定する方法です。

    x:Class="SearchLight.Circle"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:SearchLight"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="200"
    d:DesignWidth="200"
    ManipulationDelta="UserControl_ManipulationDelta" SizeChanged="UserControl_SizeChanged"
    >
    <Grid>
        <Ellipse StrokeThickness="4" Stroke="Red" >
            <Ellipse.Fill>
                <ImageBrush
                    x:Name="ib"
                    Stretch="None">
                    <ImageBrush.Transform>
                        <TranslateTransform
                            x:Name="tr"
                            X="0" Y="0"></TranslateTransform>
                    </ImageBrush.Transform>
                </ImageBrush>
            </Ellipse.Fill>
        </Ellipse>
    </Grid>
</UserControl>

そんな訳で、コントロールを移動しても、元の画像を移動せずにクリッピングする、というイベントが作れます。原点からのコントロールの位置は、TransformPoint を使って取っていますが、実際は画像の原点から合わせたほうがよいので、もうちょっと違う書き方が必要かも。

private void UserControl_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
{
    var pt = this.TransformToVisual(null).TransformPoint(new Point(0, 0));
    marX = (_imageSize.Width - this.Width) / 2.0;
    marY = (_imageSize.Height - this.Height) / 2.0;
    tr.X = -pt.X + marX;
    tr.Y = -pt.Y + marY;
}

そんな訳で、戦略ボードのようにコントロールを追加して、円を移動させたり、拡大縮小させたりすることができたのですが、移動させているときに画像がずれるのが気に食わないですよね。ええ、このあたりは、ClipとImageBrushによるFillの違いかと。たぶん、WPFでClipを使ったほうがスムースに動くと思います。

20130915_03

■サンプルコード

サンプルコードは、以下からどうぞ。
http://sdrv.ms/1eAP7yp

 

カテゴリー: 開発, C#, WinRT パーマリンク