在 WinUI 3 中重复画笔或平铺图像

问题描述 投票:0回答:3

我发现很难理解如何用位图的重复副本简单地覆盖矩形 XAML 元素!我正在使用 WinUI 3 和 Windows App SDK。我想在我的应用程序中使用重复图像作为背景元素。

好像涉及到composition API。 Deiderik KrohlsJetChopper 给出了一些诱人的线索...但是(a)似乎没有针对所需接口的稳定发布的 NuGet 包,并且(b)这似乎是一种非常复杂的方法一些应该很简单的东西,并且 (c) 这些解决方案似乎需要额外的工作才能与 WinUI 3 类(例如 ImageSource 和 BitmapImage)集成。

有什么建议吗?

xaml repeat bitmapimage winui-3 brush
3个回答
3
投票

您可以使用 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 块。


0
投票

您可以使用 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>

0
投票

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

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