UWP で画像をサイズに応じて繰り返し敷き詰めて表示したい
上記のような市松模様の塗りつぶしをする場合、小さな画像だけを用意して上下左右に繰り返して敷き詰めて表示させるようにしたいところ
しかし、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>
実行してみた様子はこんな感じ
きれいに敷き詰められましたね