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

zz2j4svz  于 2022-12-20  发布在  其他
关注(0)|答案(3)|浏览(200)

我发现很难理解如何简单地用位图的重复副本覆盖矩形XAML元素!我使用的是带有Windows App SDK的WinUI 3。我希望使用重复图像作为应用程序中的背景元素。
这似乎涉及到组合API。Deiderik KrohlsJetChopper给出了一些诱人的线索......然而(a)似乎没有针对所需接口的稳定发布的NuGet包,以及(b)这似乎是一个非常复杂的方法来做一些应该是简单的事情(c)这些解决方案似乎需要额外的工作才能与WinUI 3类(如ImageSource和BitmapImage)集成。
有什么建议吗?

tzxcd3kk

tzxcd3kk1#

您可以使用社区工具包中的TilesBrush
安装CommunityToolkit.WinUI.UI.MediaNuGet软件包并尝试以下代码:

<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>
czfnxgou

czfnxgou2#

你可以使用Direct2D效果,Tile Effect就是这样的效果。这种效果是硬件加速的。微软提供了一个名为Win2D的nuget,它可以在WinUI上实现这种效果:Microsoft.Graphics.Win2D
创建标准WinUI3应用程序项目后,添加此nuget,并为此XAML:

<StackPanel
      HorizontalAlignment="Center"
      VerticalAlignment="Center"
      Orientation="Horizontal">
      <canvas:CanvasControl
          x:Name="myCanvas"
          Width="128"
          Height="128"
          CreateResources="myCanvas_CreateResources"
          Draw="myCanvas_Draw" />
  </StackPanel>

您可以使用如下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的瓷砖。

mwngjboj

mwngjboj3#

@simon-mourier的回答是我最终完成这项工作的关键。
我创建了一个TiledContentControl,它在平铺背景前面有一个ContentControl,并且当TileUriString属性更改时(例如,由于绑定),它会重新加载其位图图像。
还有属性TileWidth、TileHeight来控制平铺位图的绘制大小,以及AlignRight和AlignBottom来使位图与右边缘或下边缘对齐,而不是与左边缘或上边缘对齐。对齐参数对于在两个紧邻的TiledContentControl之间获得无缝连续性非常有用。
我把这个反馈给社区,感谢所有的帮助,我已经得到了各种编码挑战在过去。我已经做了一些基本测试,但没有广泛的测试。
使用的关键包是Microsoft.Graphics.Win2D 1.0.4和Microsoft.WindowsAppSDK 1.2。我在代码的注解中讨论了一些有趣的编码挑战。例如,在从WinUI 3 C#代码订阅Win 2D 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
    }

}

相关问题