我发现很难理解如何用位图的重复副本简单地覆盖矩形 XAML 元素!我正在使用 WinUI 3 和 Windows App SDK。我想在我的应用程序中使用重复图像作为背景元素。
好像涉及到composition API。 Deiderik Krohls 和 JetChopper 给出了一些诱人的线索...但是(a)似乎没有针对所需接口的稳定发布的 NuGet 包,并且(b)这似乎是一种非常复杂的方法一些应该很简单的东西,并且 (c) 这些解决方案似乎需要额外的工作才能与 WinUI 3 类(例如 ImageSource 和 BitmapImage)集成。
有什么建议吗?
您可以使用 Direct2D 效果,即 Tile Effect。此效果是硬件加速的。 Microsoft 提供了一个名为 Win2D 的 nuget,可以为 WinUI 启用此功能:Microsoft.Graphics.Win2D
创建标准 WinUI3 应用程序项目后,添加此 nuget,对于此 XAML:
<Window
x:Class="Win2DApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:canvas="using:Microsoft.Graphics.Canvas.UI.Xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal">
<canvas:CanvasControl
x:Name="myCanvas"
Width="128"
Height="128"
CreateResources="myCanvas_CreateResources"
Draw="myCanvas_Draw" />
</StackPanel>
</Window>
您可以使用如下 C# 代码显示图像的重复部分:
public sealed partial class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
}
// handle canvas' CreateResources event for Win2D (Direct2D) resources
private void myCanvas_CreateResources(CanvasControl sender, CanvasCreateResourcesEventArgs args)
=> args.TrackAsyncAction(CreateResources(sender).AsAsyncAction());
// create all needed resources async (here a bitmap)
CanvasBitmap _canvasBitmap;
private async Task CreateResources(CanvasControl sender)
{
// this is my 32x32 image downloaded from https://i.stack.imgur.com/454HU.jpg?s=32&g=1
_canvasBitmap = await CanvasBitmap.LoadAsync(sender, @"c:\downloads\smo.jpg");
}
// handle canvas' Draw event
// check quickstart https://microsoft.github.io/Win2D/WinUI3/html/QuickStart.htm
private void myCanvas_Draw(CanvasControl sender, CanvasDrawEventArgs args)
{
// create an intermediate command list as a feed to the Direct2D effect
using var list = new CanvasCommandList(sender);
using var session = list.CreateDrawingSession();
session.DrawImage(_canvasBitmap);
// create the Direct2D effect (here Tile effect https://learn.microsoft.com/en-us/windows/win32/direct2d/tile)
using var tile = new TileEffect();
tile.Source = list;
// use image size as source rectangle
tile.SourceRectangle = _canvasBitmap.Bounds;
// draw the effect (using bitmap as input)
args.DrawingSession.DrawImage(tile);
}
}
这是使用我的 StackOverflow 头像作为位图源的结果:
图像为 32x32,画布为 128x128,因此我们有 4x4 块。
您可以使用 CommunityToolkit 中的 TilesBrush。
安装 CommunityToolkit.WinUI.UI.Media NuGet 包并尝试以下代码:
<Window
x:Class="TileBrushes.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:toolkit="using:CommunityToolkit.WinUI.UI.Media"
mc:Ignorable="d">
<Grid ColumnDefinitions="*,*">
<Border Grid.Column="0">
<TextBlock Text="No tiles" />
</Border>
<Border Grid.Column="1">
<Border.Background>
<toolkit:TilesBrush TextureUri="ms-appx:///Assets/StoreLogo.png" />
</Border.Background>
<TextBlock Text="Tiles" />
</Border>
</Grid>
</Window>
@simon-mourier 的答案是我最终完成这项工作的关键。
我创建了一个 TiledContentControl,它在平铺背景前面有一个 ContentControl,并且当 TileUriString 属性更改时(例如由于绑定),它会重新加载其位图图像。
还有 TileWidth、TileHeight 属性用于控制平铺位图的绘制大小,以及 AlignRight 和 AlignBottom 属性使位图与右边缘或下边缘对齐,而不是与左边缘或上边缘对齐。对齐参数对于在两个相邻的 TiledContentControl 之间获得无缝连续性非常有用。
我将此回馈给社区,感谢我过去在各种编码挑战中获得的所有帮助。注意:我做了一些基本测试,但没有进行广泛的测试。
使用的关键 nuget 包是 Microsoft.Graphics.Win2D 1.0.4 和 Microsoft.WindowsAppSDK 1.2。我在代码的评论中讨论了一些有趣的编码挑战。例如,从 WinUI3 C# 代码订阅 Win2D C++ 事件时需要防止内存泄漏。
这是 TiledContentControl.xaml:
<UserControl
x:Class="Z.Framework.TiledContentControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:win2d="using:Microsoft.Graphics.Canvas.UI.Xaml"
mc:Ignorable="d"
Padding="0"
>
<Grid
RowDefinitions="*"
ColumnDefinitions="*"
>
<win2d:CanvasControl
x:Name="CanvasControl"
Grid.Row="0"
Grid.Column="0"
>
</win2d:CanvasControl>
<ContentPresenter
Name="ContentPresenter"
Grid.Row="0"
Grid.Column="0"
Background="Transparent"
Foreground="{x:Bind Foreground, Mode=OneWay}"
HorizontalContentAlignment="{x:Bind HorizontalContentAlignment, Mode=OneWay}"
VerticalContentAlignment="{x:Bind VerticalContentAlignment, Mode=OneWay}"
Padding="{x:Bind Padding, Mode=OneWay}"
Content="{x:Bind Content, Mode=OneWay}"
>
</ContentPresenter>
</Grid>
</UserControl>
这是 TiledContentControl.xaml.cs:
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Brushes;
using Microsoft.Graphics.Canvas.UI;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup;
using System;
using System.Diagnostics;
using System.Numerics;
using System.Threading.Tasks;
using Windows.Foundation;
namespace Z.Framework
{
/// <summary>
/// A control that has a tiled (repeating) bitmap background behind a content control.
///
/// Setting the TileUriString will change the tiled bitmap. Setting the drawing parameters
/// (TileWidth, TileHeight, AlignRight, AlignBottom) will scale the bitmap or offset it so
/// that it is right or bottom aligned.
/// </summary>
[ContentProperty(Name="Content")]
public sealed partial class TiledContentControl : UserControl
{
#region Discussion
// There are a number of necessary objectives to achieve the Win2D tiling with post-Load updates.
// Goal: to trigger an async load-resources when a resource-related property of the control
// changes. This is accomplished by calling StartLoadingResources when the TileUriString changes.
// Goal: cancel any resource loads that are in progress when the new load is requested.
// This is done in StartNewLoadResourcesTaskAndCleanupOldTaskAsync.
// To do it, one must store the resource-loading task (LoadResourcesTask).
// Goal: to store the resources that have been loaded, and dispose them timely.
// The LoadResourcesTask contains the loaded resources in the Result property.
// They are kept around indefinitely, except if we start a new resource load task
// then any resources in the old load task are disposed. Also, when loading several
// resources, if one of the resource loads fails then we dispose of the others.
// The CanvasResourcesRecord and LoadResourcesAsync provide a generalizable way of
// storing resources in the task result.
// Goal: make sure that any exceptions from resource creation are thrown to Win2D, so that
// Win2D can handle device-lost events (which includes Win2D triggering a new CreateResources).
// It is accomplished by only throwing load-resource exceptions from the Win2d draw handler.
// Goal: prevent Draw from being called before resources are loaded. Resource loads that are
// triggered by Win2D go through the CreateResources event handler, allowing the use of
// CanvasCreateResourcesEventArgs.TrackAsyncAction which will postpone the Draw call -- not
// until the resources are loaded but at least while the load task is started. A Draw
// callback may then occur before the load completes, but then when the load completes
// it will invalidate the CanvasControl and another Draw callback will occur.
// It does not appear to be necessary from a Win2D perspective to prevent Draw calls
// while subsequent (post-CreateResources) resource loads are being done.
// Goal: to prevent memory leaks due to .NET not being able to detect the reference cycle
// between the main control and the CanvasControl. This is accomplished by only subscribing
// to CanvasControl events while the main control is loaded.
// References:
// https://microsoft.github.io/Win2D/WinUI2/html/M_Microsoft_Graphics_Canvas_UI_CanvasCreateResourcesEventArgs_TrackAsyncAction.htm
// https://stackoverflow.com/questions/74527783/repeating-brush-or-tile-of-image-in-winui-3-composition-api
// https://microsoft.github.io/Win2D/WinUI2/html/RefCycles.htm
// https://english.r2d2rigo.es/
// https://microsoft.github.io/Win2D/WinUI3/html/M_Microsoft_Graphics_Canvas_UI_CanvasCreateResourcesEventArgs_TrackAsyncAction.htm
// https://learn.microsoft.com/en-us/windows/win32/direct2d/tile
#endregion
#region ctor
public TiledContentControl()
{
this.InitializeComponent();
this.Loaded += this.TiledContentControl_Loaded; // OK, same lifetime
this.Unloaded += this.TiledContentControl_Unloaded; // OK, same lifetime
}
private void TiledContentControl_Loaded(object sender, RoutedEventArgs e)
{
this.CanvasControl.Draw += this.CanvasControl_Draw; // OK, matched in Unloaded
this.CanvasControl.CreateResources += this.CanvasControl_CreateResources;
}
private void TiledContentControl_Unloaded(object sender, RoutedEventArgs e)
{
this.CanvasControl.Draw -= this.CanvasControl_Draw;
this.CanvasControl.CreateResources -= this.CanvasControl_CreateResources;
}
#endregion
#region CanvasResourcesRecord, LoadResourcesAsync, LoadResourcesTask
private record class CanvasResourcesRecord(
CanvasBitmap TileBitmap,
CanvasImageBrush TileBrush
): IDisposable
{
public void Dispose()
{
this.TileBitmap.Dispose();
this.TileBrush.Dispose();
}
}
static private async Task<CanvasResourcesRecord> LoadResourcesAsync(CanvasControl canvasControl, string tileUriString)
{
object[] resources = new object[2];
try {
Uri tileUri = new Uri(tileUriString);
Task<CanvasBitmap> loadTileBitmap = CanvasBitmap.LoadAsync(canvasControl, tileUri).AsTask();
CanvasBitmap tileBitmap = await loadTileBitmap;
resources[0] = tileBitmap;
CanvasImageBrush tileBrush = new CanvasImageBrush(canvasControl, tileBitmap);
tileBrush.ExtendX = CanvasEdgeBehavior.Wrap;
tileBrush.ExtendY = CanvasEdgeBehavior.Wrap;
resources[1] = tileBrush;
} catch {
// Cleanup from partial/incomplete creation
foreach (object? resource in resources) {
(resource as IDisposable)?.Dispose();
}
throw;
}
canvasControl.Invalidate(); // now that resources are loaded, we trigger an async Draw.
return new CanvasResourcesRecord(
TileBitmap: (CanvasBitmap)resources[0],
TileBrush: (CanvasImageBrush)resources[1]
);
}
private Task<CanvasResourcesRecord>? LoadResourcesTask {
get { return this._loadResourcesTask; }
set { this._loadResourcesTask = value; }
}
private Task<CanvasResourcesRecord>? _loadResourcesTask;
#endregion
#region CanvasControl_CreateResources
private void CanvasControl_CreateResources(CanvasControl sender, CanvasCreateResourcesEventArgs args)
{
Debug.Assert(sender == this.CanvasControl);
args.TrackAsyncAction(this.StartNewLoadResourcesTaskAndCleanupOldTaskAsync().AsAsyncAction());
}
#endregion
#region StartLoadingResources, StartNewLoadResourcesTaskAndCleanupOldTaskAsync
private void StartLoadingResources()
{
if (this.CanvasControl.IsLoaded) {
Task _ = this.StartNewLoadResourcesTaskAndCleanupOldTaskAsync();
}
}
private async Task StartNewLoadResourcesTaskAndCleanupOldTaskAsync()
{
// Start new task, if the necessary input properties are available.
string? tileUriString = this.TileUriString;
Task<CanvasResourcesRecord>? oldTask = this.LoadResourcesTask;
if (tileUriString != null) {
this.LoadResourcesTask = LoadResourcesAsync(this.CanvasControl, tileUriString);
} else {
this.LoadResourcesTask = null;
}
// Cleanup old task.
if (oldTask != null) {
oldTask.AsAsyncAction().Cancel();
try {
await oldTask;
} catch {
// ignore exceptions from the cancelled task
} finally {
if (oldTask.IsCompletedSuccessfully) {
oldTask.Result.Dispose();
}
}
}
}
#endregion
#region CanvasControl_Draw, ActuallyDraw
private void CanvasControl_Draw(CanvasControl sender, CanvasDrawEventArgs args)
{
Debug.Assert(sender == this.CanvasControl);
if (!this.DrawingParameters.AreFullyDefined) { return; }
if (!this.DrawingParameters.AreValid) { throw new InvalidOperationException($"Invalid drawing parameters (typically width or height)."); }
Task<CanvasResourcesRecord>? loadResourcesTask = this.LoadResourcesTask;
if (loadResourcesTask == null) { return; }
if (loadResourcesTask.IsCompletedSuccessfully) {
CanvasResourcesRecord canvasResources = loadResourcesTask.Result;
this.ActuallyDraw( args, canvasResources);
} else if (loadResourcesTask.IsFaulted) {
// Throw exceptions to Win2D, for example DeviceLostException resulting in new CreateResoures event
loadResourcesTask.Exception?.Handle(e => throw e);
} else {
return;
}
}
private void ActuallyDraw( CanvasDrawEventArgs args, CanvasResourcesRecord canvasResources)
{
Debug.Assert(this.DrawingParameters.AreFullyDefined && this.DrawingParameters.AreValid);
Debug.Assert(this.DrawingParameters.AlignRight != null && this.DrawingParameters.AlignBottom != null);
CanvasControl canvasControl = this.CanvasControl;
float scaleX = (float)(this.DrawingParameters.TileWidth / canvasResources.TileBitmap.Bounds.Width);
float scaleY = (float)(this.DrawingParameters.TileHeight / canvasResources.TileBitmap.Bounds.Height);
float translateX = ((bool)this.DrawingParameters.AlignRight) ? (float)((canvasControl.RenderSize.Width % this.DrawingParameters.TileWidth) - this.DrawingParameters.TileWidth) : (float)0;
float translateY = ((bool)this.DrawingParameters.AlignBottom) ? (float)((canvasControl.RenderSize.Height % this.DrawingParameters.TileHeight) - this.DrawingParameters.TileHeight) : (float)0;
Matrix3x2 transform = Matrix3x2.CreateScale( scaleX, scaleY);
transform.Translation = new Vector2(translateX, translateY);
canvasResources.TileBrush.Transform = transform;
Rect rectangle = new Rect(new Point(), canvasControl.RenderSize);
args.DrawingSession.FillRectangle(rectangle, canvasResources.TileBrush);
}
#endregion
#region Content
new public UIElement? Content {
get { return (UIElement?)this.GetValue(ContentProperty); }
set { this.SetValue(ContentProperty, value); }
}
new public static DependencyProperty ContentProperty { get; } = DependencyProperty.Register(nameof(TiledContentControl.Content), typeof(UIElement), typeof(TiledContentControl), new PropertyMetadata(default(UIElement)));
#endregion
#region TileUriString
public string? TileUriString {
get { return (string?)this.GetValue(TileUriStringProperty); }
set { this.SetValue(TileUriStringProperty, value); }
}
public static readonly DependencyProperty TileUriStringProperty = DependencyProperty.Register(nameof(TiledContentControl.TileUriString), typeof(string), typeof(TiledContentControl), new PropertyMetadata(default(string), new PropertyChangedCallback(OnTileUriStringChanged)));
private static void OnTileUriStringChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
TiledContentControl @this = (TiledContentControl)sender;
@this.StartLoadingResources();
}
#endregion
#region TileWidth, TileHeight, AlignRight, AlignBottom; OnDrawingParameterChanged, DrawingParametersRecord, DrawingParameters
public double TileWidth {
get { return (double)this.GetValue(TileWidthProperty); }
set { this.SetValue(TileWidthProperty, value); }
}
public static readonly DependencyProperty TileWidthProperty = DependencyProperty.Register(nameof(TileWidth), typeof(double), typeof(TiledContentControl), new PropertyMetadata(double.NaN, new PropertyChangedCallback(OnDrawingParameterChanged)));
public double TileHeight {
get { return (double)this.GetValue(TileHeightProperty); }
set { this.SetValue(TileHeightProperty, value); }
}
public static readonly DependencyProperty TileHeightProperty = DependencyProperty.Register(nameof(TileHeight), typeof(double), typeof(TiledContentControl), new PropertyMetadata(double.NaN, new PropertyChangedCallback(OnDrawingParameterChanged)));
public bool? AlignRight {
get { return (bool?)this.GetValue(AlignRightProperty); }
set { this.SetValue(AlignRightProperty, value); }
}
public static readonly DependencyProperty AlignRightProperty = DependencyProperty.Register(nameof(AlignRight), typeof(bool?), typeof(TiledContentControl), new PropertyMetadata(default(bool?), new PropertyChangedCallback(OnDrawingParameterChanged)));
public bool? AlignBottom {
get { return (bool?)this.GetValue(AlignBottomProperty); }
set { this.SetValue(AlignBottomProperty, value); }
}
public static readonly DependencyProperty AlignBottomProperty = DependencyProperty.Register(nameof(AlignBottom), typeof(bool?), typeof(TiledContentControl), new PropertyMetadata(default(bool?), new PropertyChangedCallback(OnDrawingParameterChanged)));
private static void OnDrawingParameterChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
TiledContentControl @this = (TiledContentControl)sender;
@this.DrawingParameters = new DrawingParametersRecord(@this.TileWidth, @this.TileHeight, @this.AlignRight, @this.AlignBottom);
@this.CanvasControl.Invalidate(); // trigger an async redraw using the new parameters.
}
private record struct DrawingParametersRecord(
double TileWidth,
double TileHeight,
bool? AlignRight,
bool? AlignBottom
)
{
public bool AreFullyDefined => !double.IsNaN(this.TileWidth) && !double.IsNaN(this.TileHeight) && this.AlignBottom != null && this.AlignRight != null;
public bool AreValid => this.TileWidth > 0 && this.TileHeight > 0;
}
private DrawingParametersRecord DrawingParameters { get; set; }
#endregion
}
}