【问题标题】:UWP - How to tile a background image?UWP - 如何平铺背景图像?
【发布时间】:2016-02-17 12:08:54
【问题描述】:

在通用 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 来指定 tile 的尺寸。但这在通用平台下似乎不存在。

previous question 指的是 WinRT (Windows 8),但我希望有一个基于画笔的解决方案,而不是用图像填充画布。

如何使用 UWP 平铺背景图像?

【问题讨论】:

    标签: c# .net xaml win-universal-app


    【解决方案1】:

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

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

    我使用的第二个是创建一个Panel并在上面绘制图像,这个想法来自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>
    

    查看解决方案 Github

    【讨论】:

    • 好一个富兰克林!拥有一个特定的位图来填充该区域似乎有点浪费内存,但我已经将您的代码调整为我的自定义控件并且它确实有效。谢谢:)
    【解决方案2】:

    所有这些变体对于 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 如果您的尺寸大于 16386 像素,则会抛出异常。

    【讨论】:

      【解决方案3】:

      实际上,现在可以创建自定义画笔(借助 Composition 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>
      

      【讨论】:

        【解决方案4】:

        查看我对this question的回复:

        您可以使用Win2D 库进行平铺。他们也有sample code;在“效果”(EffectsSample.xaml.cs) 下有一个平铺示例。

        【讨论】:

        【解决方案5】:

        我们在 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 允许动画。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2023-02-10
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多