Click here to Skip to main content
12,625,319 members (37,849 online)
Click here to Skip to main content
Add your own
alternative version

Stats

5K views
66 downloads
3 bookmarked
Posted

Tiled Background for Windows Store Applications

, 24 May 2014 CPOL
Rate this:
Please Sign up or sign in to vote.
Creating a replacement for missing TileBrush known from WPF.

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;

    //Stage 1: Go line by line and create a block of our pattern
    //Stop when tile image height or required height is reached
    while ((y < height) && (y < tileImageHeight))
    {
        int tileIndex = y * tileImageWidth * bytesPerPixel;
        int dataIndex = y * width * bytesPerPixel;

        //Copy the whole line from tile at once
        for (int i = 0; i < fullTileInRowCount; i++)
        {
            Array.Copy(tileImagePixels, tileIndex, data, dataIndex, tileRowLength);
            dataIndex += tileRowLength;
        }

        //Copy the rest - if there is any
        //Length will evaluate to 0 if all lines were copied without remainder
        Array.Copy(tileImagePixels, tileIndex, data, dataIndex,
                   (width - fullTileInRowCount * tileImageWidth) * bytesPerPixel);
        y++; //Next line
    }

    //Stage 2: Now let's copy those whole blocks from top to bottom
    //If there is not enough space to copy the whole block, skip to stage 3
    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;
    }

    //Copy the rest line by line
    //Use previous lines as source
    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:

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Matej Pavlů
Software Developer
Slovakia Slovakia
No Biography provided

You may also be interested in...

Pro
Pro

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.161128.1 | Last Updated 24 May 2014
Article Copyright 2014 by Matej Pavlů
Everything else Copyright © CodeProject, 1999-2016
Layout: fixed | fluid