dotnet C# 從控制檯開始 關聯 Win2D 和 WinUI 3 應用

lindexi發表於2024-08-25

本文將告訴大家如何從最簡單的控制檯開始搭建,讓 Win2D 和 WinUI 3 關聯起來,讓 Win2D 可以將內容渲染到 WinUI 3 應用上

本文適合想了解 WinUI 3 基礎機制以及 Win2D 與 WinUI 3 協同的方式的夥伴。閱讀本文將可以瞭解到一個簡單的方式,簡單到使用控制檯專案即可進行搭建整個簡單應用

上一篇部落格 裡,告訴大家可以如何簡單從控制檯搭建起一個 WinUI 3 應用。本文將在此基礎上告訴大家如何關聯上 Win2D 進行基礎介面繪製

大概製作出來的應用的介面如下圖

上圖裡面的左上角的灰色矩形就是使用 Win2D 繪製出來的內容,中間的文字則是 TextBlock 控制元件所提供的介面內容

本文的重點都在於如何讓 Win2D 繪製出上圖的左上角的灰色矩形

當然了,只要 Win2D 能在上面繪製出灰色矩形,自然也就能繪製出更多有趣的介面內容了

按照 dotnet 的慣例,在開始之前,咱需要安裝 Win2D 的 NuGet 庫。如按照 dotnet WinUI3 Win2D 翻轉圖片 部落格提供的方法,快速編輯 csproj 專案檔案,替換為如下程式碼即可完成初始化部署邏輯

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0-windows10.0.19041</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PlatformTarget>x86</PlatformTarget>
    <RuntimeIdentifiers>win10-x86;win10-x64</RuntimeIdentifiers>
    <TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    <UseWinUI>true</UseWinUI>
    <WindowsPackageType>None</WindowsPackageType>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Graphics.Win2D" Version="1.2.0" />
  </ItemGroup>

</Project>

接下來按照 上一篇部落格 提供的方法建立一個簡單 WinUI 3 應用,程式碼如下

public class App : Application
{
    protected override void OnLaunched(LaunchActivatedEventArgs args)
    {
        var window = new Window()
        {
            Title = "控制檯建立應用"
        };
        window.Content = new Grid()
        {
            Children =
            {
                new TextBlock()
                {
                    Text = "控制檯應用",
                    HorizontalAlignment = HorizontalAlignment.Center,
                    VerticalAlignment = VerticalAlignment.Center
                }
            }
        };

        window.Activated += (sender, eventArgs) =>
        {
            ... // 忽略其他程式碼
        };

        window.Activate();

        base.OnLaunched(args);
    }
}

internal class Program
{
    static void Main(string[] args)
    {
        global::WinRT.ComWrappersSupport.InitializeComWrappers();

        global::Microsoft.UI.Xaml.Application.Start(p =>
        {
            var app = new App();
            _ = app;
        });
    }
}

將 Win2D 關聯到 WinUI 3 應用,核心的一點就是讓 Win2D 能夠繪製到 WinUI 3 應用的平面上

下面程式碼寫到 Window 的 Activated 事件裡面,更具體來說這裡只是隨便找一個事件,確保視窗等初始化完成之後執行關聯的程式碼而已

先建立或獲取共享的裝置,按照 DirectX 的使用概念裡面,需要先有明確的裝置

            var canvasDevice = new CanvasDevice();
            // 或
            var canvasDevice = CanvasDevice.GetSharedDevice();

接著取出視窗的 Compositor 用於呼叫 Win2D 的 CreateCompositionGraphicsDevice 方法,建立出 CompositionGraphicsDevice 裝置

            var compositor = window.Compositor;

            CompositionGraphicsDevice compositionGraphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(compositor, canvasDevice);

此時的 CompositionGraphicsDevice 即可打通 Win2D 和 WinUI 3 之間的渲染關聯

為了使用 Win2D 繪製內容,需要建立出一個平面讓 Win2D 繪製。這裡的平面大家可以理解為畫布,即建立一個畫布讓 Win2D 在上面繪製內容

            CompositionDrawingSurface compositionDrawingSurface = compositionGraphicsDevice.CreateDrawingSurface(new Windows.Foundation.Size(200, 200),
                DirectXPixelFormat.B8G8R8A8UIntNormalized,
                DirectXAlphaMode.Premultiplied);

建立畫布的時候,最重要的兩個引數就是畫布的尺寸以及使用的顏色格式。這裡的 B8G8R8A8UIntNormalized 的意思就是顏色格式採用 Blue 藍色 8 個 bit 長度,和 Green 綠色 8 個 bit 長度,和 Red 紅色 8 個 bit 長度,和 Alpha 透明度 8 個 bit 長度。這是十分標準且通用性非常好,且 GPU 友好的顏色畫素格式

最後一個 Premultiplied 引數的意思就是是否進行 Alpha 預乘,這屬於 WinUI 3 渲染層所要求。儘管預乘可能會造成精度丟失問題,但是可以減少後續步驟重複的計算過程。詳細請看 圖片Alpha預乘的作用[轉] - 孤海傲月 - 部落格園

拿到了 CompositionDrawingSurface 之後,即可在此平面上繪製,如以下程式碼

            using (CanvasDrawingSession drawingSession =
                   CanvasComposition.CreateDrawingSession(compositionDrawingSurface))
            {
                drawingSession.FillRectangle(new Rect(10, 10, 100, 100),
                    Windows.UI.Color.FromArgb(0xFF, 0x56, 0x56, 0x56));
            }

從上面程式碼可以在 CanvasComposition.CreateDrawingSession 返回咱十分熟悉的 CanvasDrawingSession 物件

所有的 Win2D 繪製邏輯都可在 CanvasDrawingSession 的輔助下進行。大家可以將以上的 FillRectangle 方法換成自己的使用 Win2D 繪製複雜的介面的程式碼,如此即可畫出好看的介面內容

完成上述步驟,只是將 Win2D 繪製的內容放在一個平面上,接下來需要將這個平面放入到 WinUI 3 的框架裡面進行顯示

本文選用的方式是走貼圖 Brush 的方式,將 Brush 貼到 SpriteVisual 上,再讓 SpriteVisual 加入到視窗的內容裡面

如此即可使用 SpriteVisual 的貼圖顯示出 Win2D 繪製的內容

實現的邏輯程式碼如下,先將 CompositionDrawingSurface 建立為畫刷作為貼圖

            // 在 Win2d 渲染到平面完成之後,將這個平面作為一個畫刷用於在之後的效果
            CompositionSurfaceBrush surfaceBrush = compositor.CreateSurfaceBrush(compositionDrawingSurface);

換回 Compositor 建立出 SpriteVisual 且應用畫刷

            SpriteVisual visual = compositor.CreateSpriteVisual();
            visual.Brush = surfaceBrush;
            visual.Size = new Vector2(200, 200);
            visual.Offset = new Vector3(20, 20, 0);

上述程式碼核心只是 visual.Brush = surfaceBrush; 但是新增尺寸是必須的,否則介面將看不見 Win2D 的內容。至於 Offset 只是為了控制在哪顯示,可以不寫

再將 SpriteVisual 放到視窗內容的最上方,程式碼如下

            Visual elementVisual = ElementCompositionPreview.GetElementVisual(window.Content);
            if (elementVisual is ContainerVisual containerVisual)
            {
                containerVisual.Children.InsertAtTop(visual);
            }

如此即完成了邏輯,可以將 Win2D 繪製的內容作為 WinUI 3 介面的一部分,這個過程全從控制檯開始搭建,減少了許多中間的封裝

整個建立 Win2D 和 WinUI 3 關聯的核心程式碼全部如下

        window.Activated += (sender, eventArgs) =>
        {
            var canvasDevice = new CanvasDevice();

            var compositor = window.Compositor;

            var compositionGraphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(compositor, canvasDevice);

            var compositionDrawingSurface = compositionGraphicsDevice.CreateDrawingSurface(
                new Windows.Foundation.Size(200, 200),
                DirectXPixelFormat.B8G8R8A8UIntNormalized,
                DirectXAlphaMode.Premultiplied);
            using (CanvasDrawingSession? drawingSession =
                   CanvasComposition.CreateDrawingSession(compositionDrawingSurface))
            {
                drawingSession.FillRectangle(new Rect(10, 10, 100, 100),
                    Windows.UI.Color.FromArgb(0xFF, 0x56, 0x56, 0x56));
            }

            // 在 Win2d 渲染到平面完成之後,將這個平面作為一個畫刷用於在之後的效果
            CompositionSurfaceBrush surfaceBrush = compositor.CreateSurfaceBrush(compositionDrawingSurface);

            SpriteVisual visual = compositor.CreateSpriteVisual();
            visual.Brush = surfaceBrush;
            visual.Size = new Vector2(200, 200);
            visual.Offset = new Vector3(20, 20, 0);

            Visual elementVisual = ElementCompositionPreview.GetElementVisual(window.Content);
            if (elementVisual is ContainerVisual containerVisual)
            {
                containerVisual.Children.InsertAtTop(visual);
            }
        };

可以看到關聯的程式碼不多,整體過程如下圖

先建立或獲取共享裝置,透過 CanvasComposition.CreateCompositionGraphicsDevice
混合 Compositor 和 CanvasDevice 建立出 CompositionGraphicsDevice 物件。這個 CompositionGraphicsDevice 物件同樣也是 Win2D 的裝置物件概念,只是加上了 Composition 的能力,可以和 WinUI 3 互動。使用 CompositionGraphicsDevice 建立出 CompositionDrawingSurface 平面。將 CompositionDrawingSurface 作為畫布,呼叫 CanvasComposition.CreateDrawingSession 建立出 CanvasDrawingSession 用於呼叫 Win2D 的繪製邏輯。再將 CompositionDrawingSurface 作為 CompositionSurfaceBrush 畫刷,貼到 SpriteVisual 上。最後將 SpriteVisual 放在視窗內容的最上層

全部的程式碼如下

public class App : Application
{
    protected override void OnLaunched(LaunchActivatedEventArgs args)
    {
        var window = new Window()
        {
            Title = "控制檯建立應用"
        };
        window.Content = new Grid()
        {
            Children =
            {
                new TextBlock()
                {
                    Text = "控制檯應用",
                    HorizontalAlignment = HorizontalAlignment.Center,
                    VerticalAlignment = VerticalAlignment.Center
                }
            }
        };

        window.Activated += (sender, eventArgs) =>
        {
            var canvasDevice = new CanvasDevice();

            var compositor = window.Compositor;

            var compositionGraphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(compositor, canvasDevice);

            var compositionDrawingSurface = compositionGraphicsDevice.CreateDrawingSurface(
                new Windows.Foundation.Size(200, 200),
                DirectXPixelFormat.B8G8R8A8UIntNormalized,
                DirectXAlphaMode.Premultiplied);
            using (CanvasDrawingSession? drawingSession =
                   CanvasComposition.CreateDrawingSession(compositionDrawingSurface))
            {
                drawingSession.FillRectangle(new Rect(10, 10, 100, 100),
                    Windows.UI.Color.FromArgb(0xFF, 0x56, 0x56, 0x56));
            }

            // 在 Win2d 渲染到平面完成之後,將這個平面作為一個畫刷用於在之後的效果
            CompositionSurfaceBrush surfaceBrush = compositor.CreateSurfaceBrush(compositionDrawingSurface);

            SpriteVisual visual = compositor.CreateSpriteVisual();
            visual.Brush = surfaceBrush;
            visual.Size = new Vector2(200, 200);
            visual.Offset = new Vector3(20, 20, 0);

            Visual elementVisual = ElementCompositionPreview.GetElementVisual(window.Content);
            if (elementVisual is ContainerVisual containerVisual)
            {
                containerVisual.Children.InsertAtTop(visual);
            }
        };

        window.Activate();

        base.OnLaunched(args);
    }
}

internal class Program
{
    static void Main(string[] args)
    {
        global::WinRT.ComWrappersSupport.InitializeComWrappers();

        global::Microsoft.UI.Xaml.Application.Start(p =>
        {
            var app = new App();
            _ = app;
        });
    }
}

本文程式碼放在 githubgitee 上,可以使用如下命令列拉取程式碼。我整個程式碼倉庫比較龐大,使用以下命令列可以進行部分拉取,拉取速度比較快

先建立一個空資料夾,接著使用命令列 cd 命令進入此空資料夾,在命令列裡面輸入以下程式碼,即可獲取到本文的程式碼

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

以上使用的是國內的 gitee 的源,如果 gitee 不能訪問,請替換為 github 的源。請在命令列繼續輸入以下程式碼,將 gitee 源換成 github 源進行拉取程式碼。如果依然拉取不到程式碼,可以發郵件向我要程式碼

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

獲取程式碼之後,進入 Workbench/HuremluhuhaChilejelawlai 資料夾,即可獲取到原始碼

更多技術部落格,請參閱 部落格導航

相關文章