//Deprecated
Keeping it here as a reference for devs stuck on Win8 (probably none) but for everyone targeting Windows 10 UWP apps this is a way to go:
https://www.codeproject.com/Tips/1236452/UWP-TiledBrush
Introduction
When I was developing my latest Windows store application, I had a requirement to use a tiled background with texture. WPF has a TileBrush
which does exactly that, but unfortunately this brush was not ported to WinRT. After Googling around for a few minutes, I found a suggestion to use a large background image which seemed a bit impractical (and with those 4K displays around the corner even inefficient).
Then I found a code snippet creating a lot of Image controls with the same image and placing them one to another. That's a lot better but I still didn't like the idea of 16 000 controls in XAML visual tree, so I decided to roll out my own implementation.
Background
The basic idea was to convert the small tiled image into an array of pixel information which will later be used to generate a single background image of desired resolution.
Getting those Pixels
Pixels can be retrieved from image with the help of a BitmapDecoder
class. Just remember to use the same bitmap pixel format (order of RGBA) for decoding and encoding. I'm using the default Bgra8
which results in 4 bytes per pixel. Pixels returned in single dimensional array are ordered line by line from left to right (pretty much the way one would expect). Also remember to check if the code is not executed in design mode, because designer process can't access app package files. Since I've not found a way to overcome this limitation, I am resorting to generate a design time image data (simple checker board).
private async Task RebuildTileImageData()
{
BitmapImage image = BackgroundImage as BitmapImage;
if ((image != null) && (!DesignMode.DesignModeEnabled))
{
var imageSource = new Uri
(image.UriSource.OriginalString.Replace("ms-appx:/", "ms-appx:///"));
StorageFile storageFile = await StorageFile.GetFileFromApplicationUriAsync(imageSource);
using (var imageStream = await storageFile.OpenAsync(FileAccessMode.Read))
{
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(imageStream);
var pixelDataProvider =
await decoder.GetPixelDataAsync(this.bitmapPixelFormat, this.bitmapAlphaMode,
this.bitmapTransform, this.exifOrientationMode, this.coloManagementMode
);
this.tileImagePixels = pixelDataProvider.DetachPixelData();
this.tileImageHeight = (int)decoder.PixelHeight;
this.tileImageWidth = (int)decoder.PixelWidth;
}
}
else LoadDesignTile();
}
Creating the Background - Proof of Concept
Possible nicknames: simple, easy to understand, brute force, SLOW AS HELL.
But mainly the most easy way to get it right. What this method does is that it iterates over every pixel of our new background and arithmetically maps it to a pixel of our small tile. This is good if you want to find out color of one single pixel, but it's not suited for repeated calls. Certainly not for double for
loop.
private byte[] CreateBackground(int width, int height)
{
int bytesPerPixel = this.tileImagePixels.Length / (this.tileImageWidth * this.tileImageHeight);
int tx, ty, tileIndex, dataIndex = 0;
byte[] data = new byte[width * height * bytesPerPixel];
for (int y = 0; y < height; y++)
{
ty = y % tileImageHeight;
for (int x = 0; x < width; x++)
{
tx = x % tileImageWidth;
tileIndex = ((ty * tileImageWidth) + tx) * bytesPerPixel;
dataIndex = ((y * width) + x) * bytesPerPixel;
Array.Copy(tileImagePixels, tileIndex, data, dataIndex, bytesPerPixel);
}
}
return data;
}
Creating the Background - Less Elegant == way Faster
Okay, so it's not exactly a brain teaser, but the code is a little harder to read. What this method does is that it abuses the fact that we are drawing repeated sets of lines in a rectangular shape. First, it tries to draw a block at the top with the same height as our tile. Then it copies that block down until it reaches the bottom.
private byte[] CreateBackgroud(int width, int height)
{
int bytesPerPixel = this.tileImagePixels.Length / (this.tileImageWidth * this.tileImageHeight);
byte[] data = new byte[width * height * bytesPerPixel];
int y = 0;
int fullTileInRowCount = width / tileImageWidth;
int tileRowLength = tileImageWidth * bytesPerPixel;
while ((y < height) && (y < tileImageHeight))
{
int tileIndex = y * tileImageWidth * bytesPerPixel;
int dataIndex = y * width * bytesPerPixel;
for (int i = 0; i < fullTileInRowCount; i++)
{
Array.Copy(tileImagePixels, tileIndex, data, dataIndex, tileRowLength);
dataIndex += tileRowLength;
}
Array.Copy(tileImagePixels, tileIndex, data, dataIndex,
(width - fullTileInRowCount * tileImageWidth) * bytesPerPixel);
y++;
}
int rowLength = width * bytesPerPixel;
int blockLength = this.tileImageHeight * rowLength;
while (y <= (height - tileImageHeight))
{
int dataBaseIndex = y * width * bytesPerPixel;
Array.Copy(data, 0, data, dataBaseIndex, blockLength);
y += tileImageHeight;
}
for (int row = y; row < height; row++)
Array.Copy(data, (row - tileImageHeight) * rowLength, data, row * rowLength, rowLength);
return data;
}
Wrapping It Up - Setting the Background
Practically the opposite of method RebuildImageData
. One thing to note however: at first I was using the standard MemoryStream
and AsRandomAccessStream
extension to get the RandomAccessStream
. Code crashed on the line with await for encoders FlushAsync
method. Switching to InMemoryRandomAccessStream
resolved the issue.
private async void TiledBackground_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (tileImageDataRebuildNeeded)
await RebuildTileImageData();
using (var randomAccessStream = new InMemoryRandomAccessStream())
{
var backgroundWidth = (int)e.NewSize.Width;
var backgroundHeight = (int)e.NewSize.Height;
var backgroundPixels = CreateBackgroud(backgroundWidth, backgroundHeight);
BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, randomAccessStream);
encoder.SetPixelData(this.bitmapPixelFormat, this.bitmapAlphaMode,
(uint)backgroundWidth, (uint)backgroundHeight, 96, 96, backgroundPixels);
await encoder.FlushAsync();
if (this.backgroundImageBrush.ImageSource == null)
{
BitmapImage bitmapImage = new BitmapImage();
randomAccessStream.Seek(0);
bitmapImage.SetSource(randomAccessStream);
this.backgroundImageBrush.ImageSource = bitmapImage;
}
else ((BitmapImage)this.backgroundImageBrush.ImageSource).SetSource(randomAccessStream);
}
}
Using the Code
Pretty straight forward, just paste the control on the surface and set its BackgroundImage
property. A small tip: If all you need is a simple checkerboard pattern, then you don't need an image at all. Just set the following design properties (DesignTileBlockSize
, DesignColorA
, DesignColorB
), they will also be used at runtime if there is no image set.
<Grid Background="White">
<Border HorizontalAlignment="Left" Height="100"
VerticalAlignment="Top" Width="100"
Background="#FFDC1111"/>
<local:TiledBackground HorizontalAlignment="Left" VerticalAlignment="Top"
DesignTileBlockSize="10"
DesignColorA="Gold" DesignColorB="Yellow"
Width="100" Height="100" Margin="100,0"/>
<local:TiledBackground HorizontalAlignment="Left" VerticalAlignment="Top"
BackgroundImage="Assets/Background2.png"
Width="100" Height="100" Margin="50"/>
</Grid>
The XAML code should result in something like this: