UWP - 如何平铺背景图片?

8
在通用 Windows 应用程序中,我正在尝试使用背景图像(来自 ImageSource)并将其平铺在控件上。
XAML
<Grid x:Name="gridBackground">
  <ContentPresenter />
</Grid>

C#

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

根据MSDN,ImageBrush继承自TileBrush。它甚至说:
使用ImageBrush可以为文本添加装饰效果,或为控件或布局容器提供平铺的背景。
如果禁用了拉伸,我会认为这应该平铺图像,但遗憾的是,它只是将图像绘制在控件的中间。我没有看到任何实际的属性来使其平铺。
在WPF中,有一个TileMode属性和ViewPort可以设置以指定瓷砖的尺寸。但是,在通用平台下似乎不存在此功能。
一个之前的问题涉及WinRT(Windows 8),但我希望有一种基于画刷的解决方案,而不是用图片填充画布。
如何在UWP中平铺背景图像?
7个回答

5

之前的问题涉及WinRT(Windows 8),但我希望有一个基于刷子的解决方案,而不是用图像填充画布。

目前,在UWP应用程序中显示平铺模式的背景图像只有两种解决方案,第一种是使用画布进行填充,这是您已知的。

我正在使用的第二种方法是创建一个面板并在其上绘制图像,这个想法源自于this article

这种方法的作用是利用我们在矩形形状中重复绘制线条集的事实。首先,它尝试以与我们的瓷砖相同高度的块来绘制顶部的块。然后将该块复制到达底部。

我修改了一些代码并修复了一些问题:

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

使用方法:

<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

查看解决方案请访问 Github


不错,Franklin!为了填充区域而专门使用位图似乎有点浪费内存,但我已经将你的代码适应到我的自定义控件上,它确实有效。谢谢 :) - cleardemon

4

实际上,现在可以通过组合API和Win2D创建自定义画笔来实现平铺效果。此处提供代码示例: UWP TiledBrush

简而言之,只需子类化XamlCompositionBrushBase并重写其OnConnected方法:

public class TiledBrush : XamlCompositionBrushBase
{
  protected override void OnConnected()
  {
    var surface = LoadedImageSurface.StartLoadFromUri(ImageSourceUri);

    var surfaceBrush = Compositor.CreateSurfaceBrush(surface);

    surfaceBrush.Stretch = CompositionStretch.None;

    var borderEffect = new BorderEffect()
    {
        Source = new CompositionEffectSourceParameter("source"),
        ExtendX = Microsoft.Graphics.Canvas.CanvasEdgeBehavior.Wrap,
        ExtendY = Microsoft.Graphics.Canvas.CanvasEdgeBehavior.Wrap
    };

    var borderEffectFactory = Compositor.CreateEffectFactory(borderEffect);

    var borderEffectBrush = borderEffectFactory.CreateBrush();

    borderEffectBrush.SetSourceParameter("source", surfaceBrush);
  }
}

然后按照预期使用它:

<Grid>
    <Grid.Background>
        <local:TiledBrush ImageSourceUri="Assets/Texture.jpg" />
    </Grid.Background>
</Grid>

干得好,非常简单。 - sjb-sjb

4
所有这些变体对GPU来说都很重。您应该通过使用 BorderEffect ,通过 Composition API进行处理。
        var compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
        var canvasDevice = CanvasDevice.GetSharedDevice();
        var graphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(compositor, canvasDevice);

        var bitmap = await CanvasBitmap.LoadAsync(canvasDevice, new Uri("ms-appx:///YourProject/Assets/texture.jpg"));

        var drawingSurface = graphicsDevice.CreateDrawingSurface(bitmap.Size,
            DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied);
        using (var ds = CanvasComposition.CreateDrawingSession(drawingSurface))
        {
            ds.Clear(Colors.Transparent);
            ds.DrawImage(bitmap);
        }

        var surfaceBrush = compositor.CreateSurfaceBrush(drawingSurface);
        surfaceBrush.Stretch = CompositionStretch.None;

        var border = new BorderEffect
        {
            ExtendX = CanvasEdgeBehavior.Wrap,
            ExtendY = CanvasEdgeBehavior.Wrap,
            Source = new CompositionEffectSourceParameter("source")
        };

        var fxFactory = compositor.CreateEffectFactory(border);
        var fxBrush = fxFactory.CreateBrush();
        fxBrush.SetSourceParameter("source", surfaceBrush);

        var sprite = compositor.CreateSpriteVisual();
        sprite.Size = new Vector2(1000000);
        sprite.Brush = fxBrush;

        ElementCompositionPreview.SetElementChildVisual(YourCanvas, sprite);

我尝试了1000000x1000000大小的精灵,没有任何问题。

Win2d如果您的大小大于16386px,将会抛出异常。


1

请查看我对这个问题的回答:

你可以使用Win2D库进行平铺。他们还提供了示例代码;在“效果”下有一个平铺示例(EffectsSample.xaml.cs)。


这是一个实现Win2D解决方案的示例项目:https://github.com/r2d2rigo/Win2D-Samples/tree/master/TiledBackground - dex3703

0

在Windows社区工具包中,我们有TilesBrush:

<Border BorderBrush="Black" BorderThickness="1" VerticalAlignment="Center" HorizontalAlignment="Center" Width="400" Height="400">
  <Border.Background>
    <brushes:TilesBrush TextureUri="ms-appx:///Assets/BrushAssets/TileTexture.png"/>
  </Border.Background>
</Border>

我们还有TileControl,它允许进行动画。


0

评论说,当使用C#从Win2d时,必须注意内存泄漏。此外,如果您想动态更改位图资源,则有一些微妙之处。

查看此问题的答案,以获取解决这些问题的一种解决方案:在WinUI 3中重复使用图像的刷子或瓷砖


0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接