しっぽを追いかけて

ぐるぐるしながら考えています

Unity と猫の話題が中心   掲載内容は個人の私見であり、所属組織の見解ではありません

UWP で画像をサイズに応じて繰り返し敷き詰めて表示したい

f:id:matatabi_ux:20150913154758p:plain

上記のような市松模様の塗りつぶしをする場合、小さな画像だけを用意して上下左右に繰り返して敷き詰めて表示させるようにしたいところ

しかし、Windows ランタイムにはこうした塗りつぶしオプションは用意されていません・・・作るしかない!

ソースコード全体は下記にアップしています


以前は下記の記事のように Canvas を継承して実現していました

今回はさらに処理負荷が下がりそうな方法を試してみます

というわけで用意したのが次のクラス

/// <summary>
/// Tiled sourceImage rendering panel
/// </summary>
public class ImageTile : Panel
{
    /// <summary>
    /// Image source dependency property
    /// </summary>
    public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(
        "Source",
        typeof(ImageSource),
        typeof(ImageTile),
        new PropertyMetadata(null, OnSourceChanged));

    /// <summary>
    /// Image source CLR property
    /// </summary>
    public ImageSource Source
    {
        get { return (ImageSource)this.GetValue(SourceProperty); }
        set { this.SetValue(SourceProperty, value); }
    }

    /// <summary>
    /// Image source changed event handler
    /// </summary>
    /// <param name="d">dependency object</param>
    /// <param name="e">event argument</param>
    private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var panel = d as ImageTile;
        if (panel == null)
        {
            return;
        }
        panel.sourceImage = new Image
        {
            Source = panel.Source,
            UseLayoutRounding = false,
            Stretch = Stretch.None,
        };
        panel.sourceImage.ImageOpened += panel.OnSourceImageOpened;
        panel.sourceImage.ImageFailed += panel.OnSourceImageFailed;

        // Open sourceImage by add a sourceImage on visual tree
        panel.Children.Add(panel.sourceImage);
    }

    /// <summary>
    /// Image open failed event handler
    /// </summary>
    /// <param name="sender">event sender</param>
    /// <param name="e">event arguments</param>
    private void OnSourceImageFailed(object sender, ExceptionRoutedEventArgs e)
    {
        this.sourceImage.ImageOpened -= this.OnSourceImageOpened;
        this.sourceImage.ImageFailed -= this.OnSourceImageFailed;
        this.sourceImage = null;
        this.Children.Clear();
    }

    /// <summary>
    /// Image opened event handler
    /// </summary>
    /// <param name="sender">event sender</param>
    /// <param name="e">event arguments</param>
    private void OnSourceImageOpened(object sender, RoutedEventArgs e)
    {
        this.sourceImage.ImageOpened -= this.OnSourceImageOpened;
        this.sourceImage.ImageFailed -= this.OnSourceImageFailed;
        this.sourceImage = null;

        // Children Images layout update
        this.InvalidateArrange();
    }

    /// <summary>
    /// Source sourceImage
    /// </summary>
    private Image sourceImage;

    /// <summary>
    /// Constructor
    /// </summary>
    public ImageTile()
    {
        this.Unloaded += this.OnUnloaded;
    }

    /// <summary>
    /// Unloaded event handler
    /// </summary>
    /// <param name="sender">event sender</param>
    /// <param name="e">event arguments</param>
    private void OnUnloaded(object sender, RoutedEventArgs e)
    {
        this.Unloaded -= this.OnUnloaded;
        if (this.sourceImage == null)
        {
            return;
        }
        this.sourceImage.ImageFailed -= this.OnSourceImageFailed;
        this.sourceImage.ImageOpened -= this.OnSourceImageOpened;
        this.sourceImage = null;
    }

    /// <summary>
    /// Arrange children elements layout
    /// </summary>
    /// <param name="finalSize">final layout size</param>
    /// <returns>decided layout size</returns>
    protected override Size ArrangeOverride(Size finalSize)
    {
        var bmp = this.Source as BitmapSource;
        if (bmp == null)
        {
            return base.ArrangeOverride(finalSize);
        }

        var width = bmp.PixelWidth;
        var height = bmp.PixelHeight;
        if (width == 0 || height == 0)
        {
            return base.ArrangeOverride(finalSize);
        }

        // Put images at tiled
        var index = 0;
        for (double x = 0; x < finalSize.Width; x += width)
        {
            for (double y = 0; y < finalSize.Height; y += height)
            {
                Image image;
                if (this.Children.Count > index)
                {
                    image = (Image)this.Children[index];
                    image.Source = bmp;
                }
                else
                {
                    image = new Image
                    {
                        Source = bmp,
                        UseLayoutRounding = false,
                        Stretch = Stretch.None
                    };
                    this.Children.Add(image);
                }
                image.Measure(new Size(width, height));
                image.Arrange(new Rect(x, y, width, height));
                index++;
            }
        }

        // Remove unnecessary images
        var count = this.Children.Count;
        for (var i = index; i < count; i++)
        {
            this.Children.RemoveAt(index);
        }

        // Clip pushed out images
        this.Clip = new RectangleGeometry
        {
            Rect = new Rect(0, 0, finalSize.Width, finalSize.Height)
        };
        return base.ArrangeOverride(finalSize);
    }
}

今回は Canvas ではなく、その基底クラスの Panel を継承しました

素材画像を指定するための Source という依存関係プロパティを用意し、Source が変更されたら下記の処理を行います

    /// <summary>
    /// Image source changed event handler
    /// </summary>
    /// <param name="d">dependency object</param>
    /// <param name="e">event argument</param>
    private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var panel = d as ImageTile;
        if (panel == null)
        {
            return;
        }
        panel.sourceImage = new Image
        {
            Source = panel.Source,
            UseLayoutRounding = false,
            Stretch = Stretch.None,
        };
        panel.sourceImage.ImageOpened += panel.OnSourceImageOpened;
        panel.sourceImage.ImageFailed += panel.OnSourceImageFailed;

        // Open sourceImage by add a sourceImage on visual tree
        panel.Children.Add(panel.sourceImage);
    }

Image は表示されるまでサイズがわからないため、ImageOpened イベント後に敷き詰めるようにいろいろ準備しています

ImageOpened イベント後には

    /// <summary>
    /// Image opened event handler
    /// </summary>
    /// <param name="sender">event sender</param>
    /// <param name="e">event arguments</param>
    private void OnSourceImageOpened(object sender, RoutedEventArgs e)
    {
        this.sourceImage.ImageOpened -= this.OnSourceImageOpened;
        this.sourceImage.ImageFailed -= this.OnSourceImageFailed;
        this.sourceImage = null;

        // Children Images layout update
        this.InvalidateArrange();
    }

最終的に InvalidateArrange() を実行して強制的に Panel のレイアウト処理を行っています

このとき間接的に実行される下記の部分が重要

    /// <summary>
    /// Arrange children elements layout
    /// </summary>
    /// <param name="finalSize">final layout size</param>
    /// <returns>decided layout size</returns>
    protected override Size ArrangeOverride(Size finalSize)
    {
        var bmp = this.Source as BitmapSource;
        if (bmp == null)
        {
            return base.ArrangeOverride(finalSize);
        }

        var width = bmp.PixelWidth;
        var height = bmp.PixelHeight;
        if (width == 0 || height == 0)
        {
            return base.ArrangeOverride(finalSize);
        }

        // Put images at tiled
        var index = 0;
        for (double x = 0; x < finalSize.Width; x += width)
        {
            for (double y = 0; y < finalSize.Height; y += height)
            {
                Image image;
                if (this.Children.Count > index)
                {
                    image = (Image)this.Children[index];
                    image.Source = bmp;
                }
                else
                {
                    image = new Image
                    {
                        Source = bmp,
                        UseLayoutRounding = false,
                        Stretch = Stretch.None
                    };
                    this.Children.Add(image);
                }
                image.Measure(new Size(width, height));
                image.Arrange(new Rect(x, y, width, height));
                index++;
            }
        }

        // Remove unnecessary images
        var count = this.Children.Count;
        for (var i = index; i < count; i++)
        {
            this.Children.RemoveAt(index);
        }

        // Clip pushed out images
        this.Clip = new RectangleGeometry
        {
            Rect = new Rect(0, 0, finalSize.Width, finalSize.Height)
        };
        return base.ArrangeOverride(finalSize);
    }

Panel 全体のサイズが finalSize として引数に渡されるので、素材となる画像のサイズを取得したら、縦横に繰り返して Image を配置するようにしています

image.Measure(new Size(width, height));
image.Arrange(new Rect(x, y, width, height));

Measure で Image 自体のサイズを設定し、Arrange で Panel 内の表示領域を設定しています

その後は余計な子要素を削除したり、Panel からはみ出た画像をクリッピングで表示しないようにしたりしています

おためしで下記のような XAML に組み込んでみました

<Page
    x:Class="TiledImageSample.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:TiledImageSample"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    RequestedTheme="Dark"
    mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="20"/>
        </Grid.RowDefinitions>
        
        <local:ImageTile Source="ms-appx:///Assets/transparent.png"
                         HorizontalAlignment="Center"
                         VerticalAlignment="Center"
                         Width="{Binding Value, ElementName=widthBar, Mode=OneWay}"
                         Height="{Binding Value, ElementName=heightBar, Mode=OneWay}"/>
        
        <Slider x:Name="widthBar"
                Grid.Row="1"
                Header="Width"
                Minimum="20"
                Maximum="400"
                MaxWidth="320"
                Value="48"/>
        
        <Slider x:Name="heightBar"
                Grid.Row="2"
                Header="Height"
                Minimum="20"
                Maximum="400"
                MaxWidth="320"
                Value="48"/>
    </Grid>
</Page>

実行してみた様子はこんな感じ

f:id:matatabi_ux:20150913172534g:plain

きれいに敷き詰められましたね