Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
365 views
in Technique[技术] by (71.8m points)

c# - UWP - How to tile a background image?

In a Universal Windows app, I am trying to use a background image (from an ImageSource) and tile it across a control.

XAML

<Grid x:Name="gridBackground">
  <ContentPresenter />
</Grid>

C#

void UpdateBackground(ImageSource source)
{
// ...
  gridBackground.Background = new ImageBrush {
    ImageSource = source,
    Stretch = Stretch.None
  };
}

According to MSDN, ImageBrush inherits from TileBrush. It even says:

Use for an ImageBrush include decorative effects for text, or tiled backgrounds for controls or layout containers.

I would assume that this should tile the image, if stretching is disabled, but alas, it just draws the image in the middle of the control. I don't see any actual properties to make it tile.

In WPF, there is a TileMode property and ViewPort can be set to specify the dimensions of the tile. But this seems absent under the Universal Platform.

A previous question refers to WinRT (Windows 8), but I'm hoping for a brush based solution, rather than filling a canvas with images.

How do I tile a background image with UWP?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

A previous question refers to WinRT (Windows 8), but I'm hoping for a brush based solution, rather than filling a canvas with images.

Currently, there are only two solution for showing background image in Tile mode in UWP app, the first one of which you are aware is filling a canvas.

The second one I'm using is to create a Panel and draw the image on it, this idea is derived from this article

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.

I've modified some code and fix some issues:

public class TiledBackground : Panel
{
        public ImageSource BackgroundImage
        {
            get { return (ImageSource)GetValue(BackgroundImageProperty); }
            set { SetValue(BackgroundImageProperty, value); }
        }

        // Using a DependencyProperty as the backing store for BackgroundImage.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty BackgroundImageProperty =
            DependencyProperty.Register("BackgroundImage", typeof(ImageSource), typeof(TiledBackground), new PropertyMetadata(null, BackgroundImageChanged));


        private static void BackgroundImageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((TiledBackground)d).OnBackgroundImageChanged();
        }
        private static void DesignDataChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((TiledBackground)d).OnDesignDataChanged();
        }

        private ImageBrush backgroundImageBrush = null;

        private bool tileImageDataRebuildNeeded = true;
        private byte[] tileImagePixels = null;
        private int tileImageWidth = 0;
        private int tileImageHeight = 0;

        private readonly BitmapPixelFormat bitmapPixelFormat = BitmapPixelFormat.Bgra8;
        private readonly BitmapTransform bitmapTransform = new BitmapTransform();
        private readonly BitmapAlphaMode bitmapAlphaMode = BitmapAlphaMode.Straight;
        private readonly ExifOrientationMode exifOrientationMode = ExifOrientationMode.IgnoreExifOrientation;
        private readonly ColorManagementMode coloManagementMode = ColorManagementMode.ColorManageToSRgb;

        public TiledBackground()
        {
            this.backgroundImageBrush = new ImageBrush();
            this.Background = backgroundImageBrush;

            this.SizeChanged += TiledBackground_SizeChanged;
        }

        private async void TiledBackground_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            await this.Render((int)e.NewSize.Width, (int)e.NewSize.Height);
        }

        private async void OnBackgroundImageChanged()
        {
            tileImageDataRebuildNeeded = true;
            await Render((int)this.ActualWidth, (int)this.ActualHeight);
        }

        private async void OnDesignDataChanged()
        {
            tileImageDataRebuildNeeded = true;
            await Render((int)this.ActualWidth, (int)this.ActualHeight);
        }

        private async Task RebuildTileImageData()
        {
            BitmapImage image = BackgroundImage as BitmapImage;
            if ((image != null) && (!DesignMode.DesignModeEnabled))
            {
                string imgUri = image.UriSource.OriginalString;
                if (!imgUri.Contains("ms-appx:///"))
                {
                    imgUri += "ms-appx:///";
                }
                var imageSource = new Uri(imgUri);
                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;
                }
            }
        }

        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;
        }

        private async Task Render(int width, int height)
        {
            Stopwatch fullsw = Stopwatch.StartNew();

            if (tileImageDataRebuildNeeded)
                await RebuildTileImageData();

            if ((height > 0) && (width > 0))
            {
                using (var randomAccessStream = new InMemoryRandomAccessStream())
                {
                    Stopwatch sw = Stopwatch.StartNew();
                    var backgroundPixels = CreateBackgroud(width, height);
                    sw.Stop();
                    Debug.WriteLine("Background generation finished: {0} ticks - {1} ms", sw.ElapsedTicks, sw.ElapsedMilliseconds);

                    BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, randomAccessStream);
                    encoder.SetPixelData(this.bitmapPixelFormat, this.bitmapAlphaMode, (uint)width, (uint)height, 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);
                }
            }
            else this.backgroundImageBrush.ImageSource = null;

            fullsw.Stop();
            Debug.WriteLine("Background rendering finished: {0} ticks - {1} ms", fullsw.ElapsedTicks, fullsw.ElapsedMilliseconds);
        }
}

Usage:

<Grid x:Name="rootGrid" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <tileCtrl:TiledBackground
                               BackgroundImage="Assets/avatar1.png"
                               Width="{Binding ActualWidth, ElementName=rootGrid}" Height="{Binding ActualHeight, ElementName=rootGrid}"/>
    </Grid>

Screenshot

Check the solution in Github


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...