本文將和大家介紹如何將 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);
}
運行項目,將可以看到如下界面
可以通過如下方式獲取本文的源代碼,先創建一個空文件夾,接着使用命令行 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 文件夾