UWP - 如何平铺背景图像?

问题描述 投票:0回答: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 平铺背景图像?

c# .net xaml win-universal-app
8个回答
5
投票
之前的问题涉及 WinRT (Windows 8),但我希望有一个基于画笔的解决方案,而不是用图像填充画布。

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

我使用的第二个是创建一个Panel并在上面绘制图像,这个想法源自

这篇文章

这种方法的作用是滥用我们在矩形中绘制重复的线条组这一事实。首先,它尝试在顶部绘制一个与我们的图块高度相同的块。然后它向下复制该块,直到到达底部。

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

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


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 大小的精灵,并且毫不费力地工作了。

如果您的尺寸大于 16386px,

Win2d 会抛出异常。


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

1
投票
查看我对

这个问题的回答

您可以使用

Win2D 库进行平铺。他们也有示例代码; “效果”下有一个平铺示例(EffectsSample.xaml.cs

)。


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
投票
WindowsCompositorSamples 中的“边框”示例也展示了如何执行此操作,以及旋转和缩放。

链接:

https://github.com/microsoft/WindowsCompositionSamples/tree/master/SampleGallery/Samples/SDK%2015063/BorderPlayground


© www.soinside.com 2019 - 2024. All rights reserved.