dotnet 如何將 Microsoft.Maui.Graphics 對接到 UNO 框架

本文將和大家介紹如何將 Microsoft.Maui.Graphics 對接到 UNO 框架裏面。一旦完成 Microsoft.Maui.Graphics 對接,即可讓 UNO 框架複用現有的許多繪製的基礎設施和現有基礎庫,且可以更進一步與 MAUI 打通

衆所周知,在 UNO 裏面有大量的項目類型都是基於 Skia 作爲底層渲染引擎構建出來的。在 Microsoft.Maui.Graphics 中有對 Skia 的一個實現,即 Microsoft.Maui.Graphics.Skia 實現方式。根據 UNO 的 Skia 例子 可以知道,在 UNO 裏面是可以採用 SKXamlCanvas 執行直接對 Skia 繪製的內容的

然而壞消息是當前在 2024.01.29 時,在 UNO 裏面的 SKXamlCanvas 是採用一個 Hack 的方式與 UNO 對接。在 SKXamlCanvas 裏面將會重新創建 SKSurface 用於讓開發者在此之上繪製,接着再將繪製的 Bitmap 通過 CPU 拷貝到 UNO 的 WriteableBitmap 上,讓 UNO 使用圖片繪製的形式將 WriteableBitmap 繪製出來。通過 WriteableBitmap 與 Skia 進行間接對接的方式,將會極大影響繪製性能,在繪製的中間存在很大的 CPU 壓力和繪製延遲。此問題的討論地址: https://github.com/unoplatform/uno/discussions/15097

爲了減少 SKXamlCanvas 的 Hack 方式的影響,本文這裏採用了 UNO 框架裏面未公開的 Visual 繪製方式。由於本方式用到的是 UNO 框架私有的 API 實現的功能,因此可能在後續的 UNO 更新版本,本文提供的方法將會無效

本文不是提供一個開箱即用的方法,代替的是,本文提供的是一套源代碼搭建對接的方法。閱讀完成本文,你將學會如何自行在自己的 UNO 項目裏面搭建與 Microsoft.Maui.Graphics 對接的代碼,且瞭解其中的細節實現邏輯,方便你進行更進一步的定製。在本文末尾你將找到本文用到的所有代碼的下載方法

本文以下提供了在 Uno.Skia.WPF 和 Uno.Skia.GTK 平臺下,使用與 UNO 的 Visual 直接對接的方式,而不是經過了 WriteableBitmap 的 SKXamlCanvas 方式,進行與 Microsoft.Maui.Graphics.Skia 對接,進而將 Microsoft.Maui.Graphics 對接到 UNO 框架

整體的架構引用關係圖如下

上圖的 Microsoft.Maui.Graphics.UnoAbstract 和 UnoHacker (SamplesApp) 就是接下來咱重點要工作的部分,額外的一部分工作則放在 Uno.Skia.WPF 和 Uno.Skia.GTK 平臺對接代碼上,平臺項目的對接工作量很小,所需的代碼量很少

先從 Microsoft.Maui.Graphics.UnoAbstract 項目的搭建開始,在 Microsoft.Maui.Graphics.UnoAbstract 項目裏面提供了一些用於 UnoHacker (SamplesApp) 使用的 Hack 接口,用於方便上層應用框架對接,其定義的代碼如下

using Microsoft.UI.Xaml;

namespace Microsoft.Maui.Graphics.UnoAbstract;

public interface IHack
{
    FrameworkElement Create();
}

public static class HackHelper
{
    public static IHack? Hack { set; get; }
}

通過以上的代碼,即可在 HackHelper 裏面注入 Hack 對象的值。另外的,爲了獲取到繪圖的通知,即與 FrameworkElement 的更特殊的實現,這裏也需要在 Microsoft.Maui.Graphics.UnoAbstract 項目裏面添加名爲 IDrawableNotify 的接口,代碼如下

namespace Microsoft.Maui.Graphics.UnoAbstract;

public interface IDrawableNotify
{
    event EventHandler<ICanvas>? Draw;
}

完成基礎的接口定義之後,即可在 Microsoft.Maui.Graphics.UnoAbstract 項目編寫用於給通用跨平臺的 UNO 業務項目直接使用的 GraphicsCanvas 類型。此 GraphicsCanvas 類型將繼承 Microsoft.UI.Xaml.Controls.Canvas 類型,可以直接在 XAML 裏面加入,且在開始繪製時觸發 Draw 事件,方便業務代碼使用 Draw 事件編寫 Microsoft.Maui.Graphics 的繪製業務代碼

using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;

namespace Microsoft.Maui.Graphics.UnoAbstract;

public class GraphicsCanvas : Canvas, IDrawableNotify
{
    public GraphicsCanvas()
    {
        SizeChanged += OnSizeChanged;
        var frameworkElement = HackHelper.Hack?.Create();

        if (frameworkElement != null)
        {
            IDrawableNotify drawableNotify = (IDrawableNotify) frameworkElement;
            drawableNotify.Draw += OnDraw;
            Children.Add(frameworkElement);
            FrameworkElement = frameworkElement;
        }
        else
        {
            var textBlock = new TextBlock()
            {
                Text = "Not Supported"
            };

            FrameworkElement = textBlock;
        }
    }

    private FrameworkElement FrameworkElement { get; }

    private void OnDraw(object? sender, ICanvas e)
    {
        Draw?.Invoke(this, e);
    }

    private void OnSizeChanged(object sender, SizeChangedEventArgs e)
    {
        FrameworkElement.Width = e.NewSize.Width;
        FrameworkElement.Height = e.NewSize.Height;
    }

    public event EventHandler<ICanvas>? Draw;
}

接下來創建 UnoHacker (SamplesApp) 項目,這個項目需要設置項目的程序集名爲 SamplesApp 纔可以使用到 UNO 框架裏面的一些內部成員,方便起見可以將此項目直接命名爲 SamplesApp 項目。之所以叫這個名字是因爲在 UNO 裏面添加了 Internal 對名爲 SamplesApp 程序集可見

再次說明,由於 UnoHacker (SamplesApp) 項目將使用 UNO 的一些不公開的類型,可能在後續的 UNO 版本里將會失效

在 UnoHacker (SamplesApp) 項目裏面只需一個簡單的類型,此類型將與 UNO 的 Visual 進行對接,代碼如下

public class GraphicsCanvasElement : FrameworkElement
{
    public GraphicsCanvasElement()
    {
        Visual.Children.InsertAtBottom(new GraphicsCanvasVisual(Visual.Compositor, this));
    }

    public event EventHandler<ICanvas>? Draw;

    internal void InvokeDraw(ICanvas canvas)
    {
        Draw?.Invoke(this, canvas);
    }

    class GraphicsCanvasVisual : Visual
    {
        public GraphicsCanvasVisual(Compositor compositor, GraphicsCanvasElement owner) : base(compositor)
        {
            _owner = new WeakReference<GraphicsCanvasElement>(owner);
        }

        private readonly WeakReference<GraphicsCanvasElement> _owner;

        internal override void Draw(in DrawingSession session)
        {
            if (_owner.TryGetTarget(out var graphicsCanvasElement))
            {
                using var skiaCanvas = new SkiaCanvas();
                skiaCanvas.Canvas = session.Surface.Canvas;
                graphicsCanvasElement.InvokeDraw(skiaCanvas);
            }
        }
    }
}

如此就完成了與 Microsoft.Maui.Graphics 對接的基礎代碼了,剩餘的工作就需要在具體的 Uno.Skia 平臺項目裏面編寫對接代碼,通過在 Uno.Skia 平臺項目裏面編寫對接代碼將 Microsoft.Maui.Graphics.UnoAbstract 與 UnoHacker (SamplesApp) 項目進行對接。額外說明的是爲什麼不能將 Microsoft.Maui.Graphics.UnoAbstract 與 UnoHacker (SamplesApp) 項目合併,原因是爲了讓 Microsoft.Maui.Graphics.UnoAbstract 項目還可以在 UNO 的非 Skia 實現平臺上使用,只讓 UnoHacker (SamplesApp) 項目強引用 UNO 的 Skia 實現,如此即可讓 Microsoft.Maui.Graphics.UnoAbstract 項目同時被 UNO 的 WinUI3 等非 Skia 的實現的項目進行對接

接下來開始編寫 Uno.Skia.WPF 和 Uno.Skia.GTK 平臺對接代碼,這兩個部分的平臺對接代碼內容都是相同的。先定義一個名爲 HackElement 的繼承 GraphicsCanvasElement 的控件,代碼如下,定義此類型控件僅僅只是爲了方便統一對接代碼而已

public partial class HackElement : GraphicsCanvasElement, IDrawableNotify
{
}

接着創建名爲 Hack 的類型,讓此類型繼承 Microsoft.Maui.Graphics.UnoAbstract 的 IHack 接口,代碼如下

public class Hack : IHack
{
    public FrameworkElement Create()
    {
        return new HackElement();
    }
}

最後創建 HackInitializer 類型,用於將 Hack 類型放入到 Microsoft.Maui.Graphics.UnoAbstract 的 HackHelper 靜態類型裏面

static class HackInitializer
{
    public static void Init()
    {
        HackHelper.Hack = new Hack();
    }
}

通過以上的封裝代碼,即可在各平臺項目裏面通過調用 HackInitializer 的 Init 方法,即可完成所有的對接邏輯。比如在 Uno.Skia.Wpf 的默認 App 構造函數裏面,調用 HackInitializer.Init(); 代碼。比如在 Uno.Skia.GTK 項目裏面的 Program 裏面,使用如下面代碼調用對接方法

public class Program
{
    public static void Main(string[] args)
    {
        ExceptionManager.UnhandledException += delegate (UnhandledExceptionArgs expArgs)
        {
            Console.WriteLine("GLIB UNHANDLED EXCEPTION" + expArgs.ExceptionObject.ToString());
            expArgs.ExitApplication = true;
        };

        HackInitializer.Init();

        var host = new GtkHost(() => new AppHead());

        host.Run();
    }
}

完成所有對接邏輯之後,接下來即可在 UNO 的全平臺項目裏面,使用 GraphicsCanvas 繪製業務代碼的界面。比如在 MainPage.xaml 裏面添加以下代碼

      xmlns:graphics="using:Microsoft.Maui.Graphics.UnoAbstract"

  <StackPanel x:Name="StackPanel"
        HorizontalAlignment="Center"
        VerticalAlignment="Center">
    <TextBlock x:Name="TextBlock" AutomationProperties.AutomationId="HelloTextBlock"
          Text="Hello Uno Platform"
          HorizontalAlignment="Center" />
    <Border Background="AliceBlue">
      <graphics:GraphicsCanvas Draw="GraphicsCanvas_OnDraw"/>
    </Border>
  </StackPanel>

在後臺代碼即可在 GraphicsCanvas_OnDraw 方法的 Microsoft.Maui.Graphics.ICanvas 參數裏面獲取到與整個 Microsoft.Maui.Graphics 對接的開始,如下面代碼簡單繪製界面

    private void GraphicsCanvas_OnDraw(object? sender, ICanvas e)
    {
        e.StrokeSize = 5;
        e.StrokeColor = Colors.Red;
        e.DrawRectangle(0, 0, 100, 100);
    }

運行項目,將可以看到如下界面

本文以上代碼放在githubgitee 歡迎訪問

可以通過如下方式獲取本文的源代碼,先創建一個空文件夾,接着使用命令行 cd 命令進入此空文件夾,在命令行裏面輸入以下代碼,即可獲取到本文的代碼

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin daf3e44a853177d55e9ebb15d989e27b1e497591

以上使用的是 gitee 的源,如果 gitee 不能訪問,請替換爲 github 的源。請在命令行繼續輸入以下代碼

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin daf3e44a853177d55e9ebb15d989e27b1e497591

獲取代碼之後,進入 KefalurcilaybelJallbuderenajel 文件夾

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章