cyanide
cyanide

Reputation: 3964

Windows Store App: set an image as a background for an UI element

Sorry for asking a really basic question, but it's probably the first time for a number of years I feel really confused.

Windows provides two set of controls: Windows.UI.Xaml namespace (I thinks this is referred as Metro), used for Windows Store Apps, and System.Windows (WPF) for Desktop.

Since I am going to develop Windows Store Apps mainly for Windows 8.1 and Windows 10 phones, I will have to stick to Windows.UI.Xaml, and this has not only a separate set of UI elements, but also separate set of bitmaps, brushes, etc. (Windows.UI.Xaml.Media vs System.Windows.Media).

I found that Windows.UI.Xaml provides a very limited support for graphics, much less than provided by WPF, Android or (even!) iOS platform. To start with, I got stuck with a simple task: tiling a background!

Since Windows.UI.Xaml.Media.ImageBrush do not support tiling, I wanted to to do that "manually". Some sites suggest making numerous number of children, each holding a tile. Honestly, it looks as a rather awkward approach to me, so I decided to do it in what appears a more natural way: create an off-screen tiled image, assign it to a brush and assign the brush as the background for a panel.

The tiling code is rather straightforward (it probably has mistakes, possibly won't even not run on a phone, because of some unavailable classes used).

int panelWidth = (int) contentPanel.Width;
int panelHeight = (int) contentPanel.Height;

Bitmap bmpOffscreen = new Bitmap(panelWidth, panelHeight);

Graphics gOffscreen = Graphics.FromImage(bmpOffscreen);

string bmpPath = Path.Combine(Windows.ApplicationModel.Package.Current.InstalledLocation.Path, "Assets/just_a_tile.png");
 System.Drawing.Image tile = System.Drawing.Image.FromFile(bmpPath,  true);
 int tileWidth = tile.Width;
 int tileHeight = tile.Height;

 for (int y = 0; y < panelHeight; y += tileHeight)
     for (int x = 0; x < panelWidth; x += tileWidth)
         gOffscreen.DrawImage(tile, x, y);

Now I presumably have the tiled image in bmpOffscreen. But how assign it to a brush? To do that I need to convert Bitmap to BitmapSource, while I couldn't find something similar to System.Windows.Imaging.CreateBitmapSourceFromHBitmap available for WPF structure!

Upvotes: 0

Views: 263

Answers (2)

cyanide
cyanide

Reputation: 3964

Thank you, Arkadiusz. Since Australian time goes slightly ahead of Europe, I had an advantage and seen the code before you posted it. I downloaded MSDN XAML images sample and it helped me a lot. I gave a +1 to you but someone apparently put -1, so it compensated each other. Don't be upset I get -1 so often, that I stopped paying attention on that :)

So I've managed to do tiling with Windows Universal Platform! On my Lumia 532 phone it works magnifique. I felt like re-inventing a wheel, because all this stuff must be handled by SDK, not by a third-party developer.

                public static async Task<bool> setupTiledBackground(Panel panel, string tilePath)
                {
                    Brush backgroundBrush = await createTiledBackground((int)panel.Width, (int)panel.Height, TilePath);
                    if (backgroundBrush == null) return false;
                    panel.Background = backgroundBrush;
                    return true;
                }



                private static async Task<Brush> createTiledBackground(int width, int height, string tilePath)
                {
                    StorageFile file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///" + tilePath));


                    byte[] sourcePixels;
                    int tileWidth, tileHeight;

                    using (IRandomAccessStream inputStream = await file.OpenAsync(Windows.Storage.FileAccessMode.Read))
                    {
                        if (inputStream == null) return null;

                        BitmapDecoder tileDecoder = await BitmapDecoder.CreateAsync(inputStream);
                        if (tileDecoder == null) return null;


                        tileWidth = (int)tileDecoder.PixelWidth;
                        tileHeight = (int) tileDecoder.PixelHeight;

                        PixelDataProvider pixelData = await tileDecoder.GetPixelDataAsync(
                                BitmapPixelFormat.Bgra8,    // WriteableBitmap uses BGRA format
                                BitmapAlphaMode.Straight,
                                new BitmapTransform(),
                         ExifOrientationMode.IgnoreExifOrientation,
                         ColorManagementMode.DoNotColorManage);

                        sourcePixels = pixelData.DetachPixelData();
                        //            fileStream.Dispose();

                    }

                    WriteableBitmap backgroundBitmap = new WriteableBitmap(width, height);

                    int tileBmpWidth = tileWidth << 2;
                    int screenBmpWidth = width << 2;
                    int tileSize = tileBmpWidth * tileHeight;
                    int sourceOffset = 0;

                    using (Stream outputStream = backgroundBitmap.PixelBuffer.AsStream())
                    {
                        for (int bmpY=0; bmpY < height; bmpY++) {
                           for (int bmpX = 0; bmpX < screenBmpWidth; bmpX += tileBmpWidth)
                                await outputStream.WriteAsync(sourcePixels, sourceOffset, Math.Min(screenBmpWidth - bmpX, tileBmpWidth));

                            if ((sourceOffset += tileBmpWidth) >= tileSize)
                                sourceOffset -= tileSize;
                        } 
                    }

                    ImageBrush backgroundBrush = new ImageBrush();
                    backgroundBrush.ImageSource = backgroundBitmap;   // It's very easy now!
                    return backgroundBrush;  // Finita la comédia!
                }

Just one remark: if you do it on form start, you should not wait for it. This doesn't work:

   public MainPage()
   {
       this.InitializeComponent();
       bool result = setupTiledBackground(contextPanel, TilePath).Result;
   }

This works:

  private Task<bool> backgroundImageTask;

  public MainPage()
  {
       this.InitializeComponent();
       backgroundImageTask = setupTiledBackground(contextPanel, TilePath);
  }       

Upvotes: 0

Arkadiusz Szechlicki
Arkadiusz Szechlicki

Reputation: 66

Well, first of all System.Drawing namespace is not available in Windows Universal Platform, so you won't be able to use Bitmap class

But, all hope is not lost - you can use Windows.UI.Xaml.Media.Imaging.WriteableBitmap

If you look at example included on this page, you will see that at one point image data is extracted to a byte array - all you need to do is copy it according to your needs

Please let me know if you want me to include a complete code sample.

Edit:

StorageFile file = await StorageFile.GetFileFromPathAsync(filePath);
Scenario4WriteableBitmap = new WriteableBitmap(2000, 2000);
// Ensure a file was selected
if (file != null)
{
    using (IRandomAccessStream fileStream = await file.OpenAsync(Windows.Storage.FileAccessMode.Read))
    {
        int columns = 4;
        int rows = 4;
        BitmapDecoder decoder = await BitmapDecoder.CreateAsync(fileStream);

        // Scale image to appropriate size
        BitmapTransform transform = new BitmapTransform()
        {
            ScaledHeight = Convert.ToUInt32(Scenario4ImageContainer.Height),
            ScaledWidth = Convert.ToUInt32(Scenario4ImageContainer.Width)
        };

        PixelDataProvider pixelData = await decoder.GetPixelDataAsync(
            BitmapPixelFormat.Bgra8,    // WriteableBitmap uses BGRA format
            BitmapAlphaMode.Straight,
            transform,
            ExifOrientationMode.IgnoreExifOrientation, // This sample ignores Exif orientation
            ColorManagementMode.DoNotColorManage);

        // An array containing the decoded image data, which could be modified before being displayed
        byte[] sourcePixels = pixelData.DetachPixelData();

        // Open a stream to copy the image contents to the WriteableBitmap's pixel buffer
        using (Stream stream = Scenario4WriteableBitmap.PixelBuffer.AsStream())
        {
            for (int i = 0; i < columns * rows; i++)
            {
                await stream.WriteAsync(sourcePixels, 0, sourcePixels.Length);
            }
        }
     }

     // Redraw the WriteableBitmap
     Scenario4WriteableBitmap.Invalidate();
     Scenario4Image.Source = Scenario4WriteableBitmap;
     Scenario4Image.Stretch = Stretch.None;
 }

Upvotes: 1

Related Questions