简体   繁体   English

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

[英]Repeating brush or tile of image in WinUI 3

I'm finding it awfully hard to see how to simply cover a rectangular XAML element with repeating copies of a bitmap.我发现很难理解如何用位图的重复副本简单地覆盖矩形 XAML 元素。 I am using WinUI 3 with Windows App SDK.我正在使用带有 Windows App SDK 的 WinUI 3。 I would like to use the repeating image as a background element in my app.我想在我的应用程序中使用重复图像作为背景元素。

It would seem to involve the composition API.它似乎涉及组合 API。 Some tantalizing clues are given by Deiderik Krohls and by JetChopper ... however (a) there does not seem to be a stable released NuGet package for the required interface and (b) this seems like a very complicated way to do something that should be simple and (c) these solutions would seem to require extra work to integrate with WinUI 3 classes such as ImageSource and BitmapImage. Deiderik KrohlsJetChopper给出了一些诱人的线索......但是(a)似乎没有针对所需接口的稳定发布的 NuGet 包,并且(b)这似乎是一种非常复杂的方式来做一些应该是简单且 (c) 这些解决方案似乎需要额外的工作才能与 WinUI 3 类(例如 ImageSource 和 BitmapImage)集成。

Any suggestions?有什么建议么?

You can use the TilesBrush from the CommunityToolkit .您可以使用CommunityToolkit中的TilesBrush

Install the CommunityToolkit.WinUI.UI.Media NuGet package and try this code:安装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>

You can use a Direct2D effect, the Tile Effect for that.您可以使用Direct2D效果,即 平铺效果 This effect is hardware accelerated.此效果是硬件加速的。 Microsoft provides a nuget called Win2D that enables that for WinUI: Microsoft.Graphics.Win2D Microsoft 提供了一个名为 Win2D 的 nuget,它为 WinUI 启用了它: Microsoft.Graphics.Win2D

Once you have created a standard WinUI3 application project, add this nuget, and for this XAML:创建标准 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>

You can display a repetition of an image with a C# code like this:您可以使用如下 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);
      }
  }

Here is the result with my StackOverflow avatar as the bitmap source:这是我的 StackOverflow 头像作为位图源的结果:

在此处输入图像描述

The image is 32x32 and the canvas is 128x128 so we have 4x4 tiles.图像是 32x32,画布是 128x128,所以我们有 4x4 块。

The answer from @simon-mourier was the key for me in finally getting this done. @simon-mourier 的回答是我最终完成这项工作的关键。

I created a TiledContentControl that has a ContentControl in front of the tiled background, and which reloads its bitmap image when the TileUriString property is changed (eg due to a binding).我创建了一个 TiledContentControl,它在平铺背景前面有一个 ContentControl,当 TileUriString 属性发生变化时(例如,由于绑定),它会重新加载其位图图像。

There are also properties TileWidth, TileHeight to control the drawn size of the tile bitmap, as well as AlignRight and AlignBottom to make the bitmap align with the right edge or bottom edge instead of the left or top edge.还有属性 TileWidth、TileHeight 来控制图块位图的绘制大小,以及 AlignRight 和 AlignBottom 使位图与右边缘或底部边缘对齐,而不是与左边缘或顶部边缘对齐。 The alignment parameters are useful to get a seamless continuity between two TiledContentControls that are right next to each other.对齐参数对于在彼此相邻的两个 TiledContentControl 之间获得无缝连续性很有用。

I am providing this back to the community with thanks for all of the help I've gotten on various coding challenges in the past.感谢我过去在各种编码挑战中获得的所有帮助,我将此回馈给社区。 Note: I have done some basic testing but not extensive testing.注意:我做了一些基本测试,但没有进行广泛测试。

The key nuget packages used are Microsoft.Graphics.Win2D 1.0.4 and Microsoft.WindowsAppSDK 1.2.使用的关键 nuget 包是 Microsoft.Graphics.Win2D 1.0.4 和 Microsoft.WindowsAppSDK 1.2。 There are some interesting coding challenges that I discuss in a comment in the code.我在代码的评论中讨论了一些有趣的编码挑战。 For example the need to prevent memory leakage when subscribing to Win2D C++ events from WinUI3 C# code.例如,在从 WinUI3 C# 代码订阅 Win2D C++ 事件时需要防止内存泄漏。

Here is TiledContentControl.xaml:这是 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>

Here is TiledContentControl.xaml.cs:这是 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
    }

}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM