dotnet DirectX 做一個簡單繪製折線筆跡的 D2D 應用

lindexi發表於2024-10-16

前置部落格: dotnet DirectX 透過 Vortice 控制檯使用 ID2D1DeviceContext 繪製畫面

本文屬於 D2D 系列部落格,更多 D2D 相關部落格,請參閱 部落格導航

在開始之前,我十分推薦大家先閱讀 分享一個在 dotnet 裡使用 D2D 配合 AOT 開發小而美的應用開發經驗 這篇部落格,透過閱讀此部落格,可以讓大家理解一些常用概念

本文實現的 D2D 應用,由於觸控資料是從 WM_Pointer 獲取的,這就限制了在 Win7 下是不可用的

依然按照 dotnet DirectX 透過 Vortice 控制檯使用 ID2D1DeviceContext 繪製畫面 部落格提供的方法,從控制檯開始建立 Win32 視窗,掛上交換鏈,初始化繪製上下文資訊

本文內容裡面只給出關鍵程式碼片段,如需要全部的專案檔案,可到本文末尾找到本文所有程式碼的下載方法

修改 NativeMethods.txt 檔案,替換為如下程式碼,以下為本文例子程式碼所需要用到的所有 Win32 方法和常量等內容

GetModuleHandle
PeekMessage
TranslateMessage
DispatchMessage
GetMessage
RegisterClassExW
DefWindowProc
LoadCursor
PostQuitMessage
CreateWindowExW
DestroyWindow
ShowWindow
GetSystemMetrics
AdjustWindowRectEx
GetClientRect
GetWindowRect
IDC_ARROW
WM_KEYDOWN
WM_KEYUP
WM_SYSKEYDOWN
WM_SYSKEYUP
WM_DESTROY
WM_QUIT
WM_PAINT
WM_CLOSE
WM_ACTIVATEAPP
VIRTUAL_KEY
GetPointerTouchInfo
ScreenToClient
GetPointerDeviceRects
ClientToScreen
WM_POINTERDOWN
WM_POINTERUPDATE
WM_POINTERUP

略過建立視窗和獲取 D2D 上下文相關程式碼,如對這部分程式碼感興趣,請參閱 dotnet DirectX 透過 Vortice 控制檯使用 ID2D1DeviceContext 繪製畫面

以下為已經獲取到 ID2D1RenderTarget 的程式碼,繼續新增對觸控資料的處理

        // 在視窗的 dxgi 的平面上建立 D2D 的畫布,如此即可讓 D2D 繪製到視窗上
        D2D.ID2D1RenderTarget d2D1RenderTarget =
            d2DFactory.CreateDxgiSurfaceRenderTarget(dxgiSurface, renderTargetProperties);
        d2D1RenderTarget.AntialiasMode = D2D.AntialiasMode.PerPrimitive;

        var renderTarget = d2D1RenderTarget;

定義一個基礎資料結構,用於記錄點的資訊

    readonly record struct Point2D(double X, double Y);

這些基礎資料結構我在很多個專案裡面都有定義,基礎數學相關型別我也重複定義了很多次,且受限於我的數學知識,有些型別定義還是不正確的。好在我的夥伴 SeWZC 在 GitHub 上開源了數學庫,這個數學庫是按照正確的數學實現,實現了許多數學相關的型別。詳細請看 https://github.com/dotnet-campus/DotNetCampus.Numerics

開個訊息迴圈等待,防止控制檯退出,順帶在此訊息迴圈裡面處理 Pointer 訊息

        // 開個訊息迴圈等待
        Windows.Win32.UI.WindowsAndMessaging.MSG msg;
        while (true)
        {
            ...
        }

根據 dotnet 讀 WPF 原始碼筆記 從 WM_POINTER 訊息到 Touch 事件 部落格提供的方法進行對 WM_POINTER 訊息的處理

處理邏輯如下

        // 開個訊息迴圈等待
        Windows.Win32.UI.WindowsAndMessaging.MSG msg;
        while (true)
        {
            if (PeekMessage(out msg, default, 0, 0, PM_REMOVE) != false)
            {
                if (msg.message is PInvoke.WM_POINTERDOWN or PInvoke.WM_POINTERUPDATE or PInvoke.WM_POINTERUP)
                {
                    ...
                }
            }
        }

本文這裡先不考慮多指,也不考慮多筆,直接就是相鄰點連線為折線。先按照 dotnet 讀 WPF 原始碼筆記 從 WM_POINTER 訊息到 Touch 事件 部落格提供的方法對收到的 Pointer 點進行處理,這裡將使用的是高精度的點

                    var wparam = msg.wParam;
                    var pointerId = (uint)(ToInt32((IntPtr)wparam.Value) & 0xFFFF);
                    PInvoke.GetPointerTouchInfo(pointerId, out var info);
                    POINTER_INFO pointerInfo = info.pointerInfo;

                    global::Windows.Win32.Foundation.RECT pointerDeviceRect = default;
                    global::Windows.Win32.Foundation.RECT displayRect = default;

                    PInvoke.GetPointerDeviceRects(pointerInfo.sourceDevice, &pointerDeviceRect, &displayRect);

                    var point2D = new Point2D(
                        pointerInfo.ptHimetricLocationRaw.X / (double)pointerDeviceRect.Width * displayRect.Width +
                        displayRect.left,
                        pointerInfo.ptHimetricLocationRaw.Y / (double)pointerDeviceRect.Height * displayRect.Height +
                        displayRect.top);

                    point2D = new Point2D(point2D.X - screenTranslate.X, point2D.Y - screenTranslate.Y);

    private static int ToInt32(IntPtr ptr) => IntPtr.Size == 4 ? ptr.ToInt32() : (int)(ptr.ToInt64() & 0xffffffff);

以上拿到的 Point2D 就是 Pointer 訊息收到的觸控點

為了簡單起見,咱這裡不獲取歷史點,只獲取最新的點即可。將最新的點和上一個點連線做折線在螢幕上顯示出來,如此即可獲取很高的效能,很低的延遲

有雙快取的存在,推薦每次都是重新繪製,在實際使用中,即使每次都繪製整個介面,對整理的效能影響也幾乎可以忽略。但為了方便演示,本文這裡限制了點的數量,如果超過了一定數量,則將記錄的部分點刪掉

        var pointList = new List<Point2D>();

        var screenTranslate = new Point(0, 0);
        PInvoke.ClientToScreen(hWnd, ref screenTranslate);

        // 開個訊息迴圈等待
        Windows.Win32.UI.WindowsAndMessaging.MSG msg;
        while (true)
        {
            if (PeekMessage(out msg, default, 0, 0, PM_REMOVE) != false)
            {
                if (msg.message is PInvoke.WM_POINTERDOWN or PInvoke.WM_POINTERUPDATE or PInvoke.WM_POINTERUP)
                {
                    ...

                    point2D = new Point2D(point2D.X - screenTranslate.X, point2D.Y - screenTranslate.Y);

                    pointList.Add(point2D);
                    if (pointList.Count > 200)
                    {
                        // 不要讓點太多,導致繪製速度太慢
                        pointList.RemoveRange(0, 100);
                    }

                    ...
                }
            }
        }

為了在螢幕顯示出筆跡折線,這裡需要先建立畫刷。按照 dotnet C# 使用 Vortice 建立 Direct2D1 的 ID2D1SolidColorBrush 純色畫刷 部落格介紹的方法建立簡單的純色畫刷,程式碼如下

                    var color = new Color4(0xFF0000FF);
                    using var brush = renderTarget.CreateSolidColorBrush(color);

接著開始構成折線,開始之前和結束之後別忘了呼叫 renderTarget.BeginDraw();renderTarget.EndDraw(); 方法

                    renderTarget.BeginDraw();
                    renderTarget.AntialiasMode = AntialiasMode.Aliased;

                    renderTarget.Clear(new Color4(0xFFFFFFFF));

                    for (var i = 1; i < pointList.Count; i++)
                    {
                        var previousPoint = pointList[i - 1];
                        var currentPoint = pointList[i];

                        renderTarget.DrawLine(new Vector2((float)previousPoint.X, (float)previousPoint.Y),
                            new Vector2((float)currentPoint.X, (float)currentPoint.Y), brush, 5);
                    }

                    renderTarget.EndDraw();

以上程式碼透過多次 DrawLine 的方式完成筆跡折線的。完成繪製之後,呼叫一下 swapChain.Present 切換交換鏈,從而在介面顯示筆跡折線

                    renderTarget.EndDraw();
                    swapChain.Present(1, DXGI.PresentFlags.None);
                    // 等待重新整理
                    d3D11DeviceContext.Flush();

以上就是使用 Vortice 輔助呼叫 Direct2D1 的功能,配合 WM_Pointer 訊息,製作一個簡單繪製觸控折線筆跡的 D2D 應用的核心邏輯

本文的例子程式碼非常簡單,可以全部在一個 Program.cs 檔案完成,所有程式碼如下

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
using static Windows.Win32.PInvoke;
using static Windows.Win32.UI.WindowsAndMessaging.PEEK_MESSAGE_REMOVE_TYPE;
using static Windows.Win32.UI.WindowsAndMessaging.WNDCLASS_STYLES;
using static Windows.Win32.UI.WindowsAndMessaging.WINDOW_STYLE;
using static Windows.Win32.UI.WindowsAndMessaging.WINDOW_EX_STYLE;
using static Windows.Win32.UI.WindowsAndMessaging.SYSTEM_METRICS_INDEX;
using static Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD;
using Vortice.Mathematics;
using AlphaMode = Vortice.DXGI.AlphaMode;
using D3D = Vortice.Direct3D;
using D3D11 = Vortice.Direct3D11;
using DXGI = Vortice.DXGI;
using D2D = Vortice.Direct2D1;
using System.Drawing;
using Vortice.Direct2D1;
using System.Numerics;
using Windows.Win32;
using Windows.Win32.UI.Input.Pointer;

namespace QalberegejeaJawchejoleawerejea;

class Program
{
    // 設定可以支援 Win7 和以上版本。如果用到 WinRT 可以設定為支援 win10 和以上。這個特性只是給 VS 看的,沒有實際影響執行的邏輯
    [SupportedOSPlatform("Windows7.0")]
    static unsafe void Main(string[] args)
    {
        // 準備建立視窗
        // 使用 Win32 建立視窗需要很多引數,這些引數系列不是本文的重點,還請自行了解
        SizeI clientSize = new SizeI(1000, 600);

        // 視窗標題
        var title = "QalberegejeaJawchejoleawerejea";
        var windowClassName = "lindexi doubi";

        // 視窗樣式,視窗樣式含義請執行參閱官方文件,樣式只要不離譜,自己隨便寫,影響不大
        WINDOW_STYLE style = WS_CAPTION |
                             WS_SYSMENU |
                             WS_MINIMIZEBOX |
                             WS_CLIPSIBLINGS |
                             WS_BORDER |
                             WS_DLGFRAME |
                             WS_THICKFRAME |
                             WS_GROUP |
                             WS_TABSTOP |
                             WS_SIZEBOX;

        var rect = new RECT
        {
            right = clientSize.Width,
            bottom = clientSize.Height
        };

        // Adjust according to window styles
        AdjustWindowRectEx(&rect, style, false, WS_EX_APPWINDOW);

        // 決定視窗在哪顯示,這個不影響大局
        int x = 0;
        int y = 0;
        int windowWidth = rect.right - rect.left;
        int windowHeight = rect.bottom - rect.top;

        // 隨便,放在螢幕中間好了。多個顯示器?忽略
        int screenWidth = GetSystemMetrics(SM_CXSCREEN);
        int screenHeight = GetSystemMetrics(SM_CYSCREEN);

        x = (screenWidth - windowWidth) / 2;
        y = (screenHeight - windowHeight) / 2;

        var hInstance = GetModuleHandle((string?)null);

        fixed (char* lpszClassName = windowClassName)
        {
            PCWSTR szCursorName = new((char*)IDC_ARROW);

            var wndClassEx = new WNDCLASSEXW
            {
                cbSize = (uint)Unsafe.SizeOf<WNDCLASSEXW>(),
                style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC,
                // 核心邏輯,設定訊息迴圈
                lpfnWndProc = new WNDPROC(WndProc),
                hInstance = (HINSTANCE)hInstance.DangerousGetHandle(),
                hCursor = LoadCursor((HINSTANCE)IntPtr.Zero, szCursorName),
                hbrBackground = (Windows.Win32.Graphics.Gdi.HBRUSH)IntPtr.Zero,
                hIcon = (HICON)IntPtr.Zero,
                lpszClassName = lpszClassName
            };

            ushort atom = RegisterClassEx(wndClassEx);

            if (atom == 0)
            {
                throw new InvalidOperationException(
                    $"Failed to register window class. Error: {Marshal.GetLastWin32Error()}"
                );
            }
        }

        // 建立視窗
        var hWnd = CreateWindowEx
        (
            WS_EX_APPWINDOW,
            windowClassName,
            title,
            style,
            x,
            y,
            windowWidth,
            windowHeight,
            hWndParent: default,
            hMenu: default,
            hInstance: default,
            lpParam: null
        );

        // 建立完成,那就顯示
        ShowWindow(hWnd, SW_NORMAL);
        RECT windowRect;
        GetClientRect(hWnd, &windowRect);
        clientSize = new SizeI(windowRect.right - windowRect.left, windowRect.bottom - windowRect.top);

        // 開始建立工廠建立 D3D 的邏輯
        var dxgiFactory2 = DXGI.DXGI.CreateDXGIFactory1<DXGI.IDXGIFactory2>();

        var hardwareAdapter = GetHardwareAdapter(dxgiFactory2)
            // 這裡 ToList 只是想列出所有的 IDXGIAdapter1 在實際程式碼裡,大部分都是獲取第一個
            .ToList().FirstOrDefault();
        if (hardwareAdapter == null)
        {
            throw new InvalidOperationException("Cannot detect D3D11 adapter");
        }
        else
        {
            Console.WriteLine($"使用顯示卡 {hardwareAdapter.Description1.Description}");
        }

        // 功能等級
        // [C# 從零開始寫 SharpDx 應用 聊聊功能等級](https://blog.lindexi.com/post/C-%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E5%86%99-SharpDx-%E5%BA%94%E7%94%A8-%E8%81%8A%E8%81%8A%E5%8A%9F%E8%83%BD%E7%AD%89%E7%BA%A7.html)
        D3D.FeatureLevel[] featureLevels = new[]
        {
            D3D.FeatureLevel.Level_11_1,
            D3D.FeatureLevel.Level_11_0,
            D3D.FeatureLevel.Level_10_1,
            D3D.FeatureLevel.Level_10_0,
            D3D.FeatureLevel.Level_9_3,
            D3D.FeatureLevel.Level_9_2,
            D3D.FeatureLevel.Level_9_1,
        };

        DXGI.IDXGIAdapter1 adapter = hardwareAdapter;
        D3D11.DeviceCreationFlags creationFlags = D3D11.DeviceCreationFlags.BgraSupport;
        var result = D3D11.D3D11.D3D11CreateDevice
        (
            adapter,
            D3D.DriverType.Unknown,
            creationFlags,
            featureLevels,
            out D3D11.ID3D11Device d3D11Device, out D3D.FeatureLevel featureLevel,
            out D3D11.ID3D11DeviceContext d3D11DeviceContext
        );

        if (result.Failure)
        {
            // 如果失敗了,那就不指定顯示卡,走 WARP 的方式
            // http://go.microsoft.com/fwlink/?LinkId=286690
            result = D3D11.D3D11.D3D11CreateDevice(
                IntPtr.Zero,
                D3D.DriverType.Warp,
                creationFlags,
                featureLevels,
                out d3D11Device, out featureLevel, out d3D11DeviceContext);

            // 如果失敗,就不能繼續
            result.CheckError();
        }

        // 大部分情況下,用的是 ID3D11Device1 和 ID3D11DeviceContext1 型別
        // 從 ID3D11Device 轉換為 ID3D11Device1 型別
        var d3D11Device1 = d3D11Device.QueryInterface<D3D11.ID3D11Device1>();
        var d3D11DeviceContext1 = d3D11DeviceContext.QueryInterface<D3D11.ID3D11DeviceContext1>();

        // 後續還要建立 D2D 裝置,就先不考慮釋放咯
        //// 轉換完成,可以減少對 ID3D11Device1 的引用計數
        //// 呼叫 Dispose 不會釋放掉剛才申請的 D3D 資源,只是減少引用計數
        //d3D11Device.Dispose();
        //d3D11DeviceContext.Dispose();

        // 建立裝置,接下來就是關聯視窗和交換鏈
        DXGI.Format colorFormat = DXGI.Format.B8G8R8A8_UNorm;

        const int FrameCount = 2;

        DXGI.SwapChainDescription1 swapChainDescription = new()
        {
            Width = (uint)clientSize.Width,
            Height = (uint)clientSize.Height,
            Format = colorFormat,
            BufferCount = FrameCount,
            BufferUsage = DXGI.Usage.RenderTargetOutput,
            SampleDescription = DXGI.SampleDescription.Default,
            Scaling = DXGI.Scaling.Stretch,
            SwapEffect = DXGI.SwapEffect.FlipSequential,
            AlphaMode = AlphaMode.Ignore,
            // https://learn.microsoft.com/zh-cn/windows/win32/api/dxgi/nf-dxgi-idxgiswapchain-present
            // 可變重新整理率顯示 啟用撕裂是可變重新整理率顯示器的要求
            //Flags = DXGI.SwapChainFlags.AllowTearing,
        };
        // 設定是否全屏
        DXGI.SwapChainFullscreenDescription fullscreenDescription = new DXGI.SwapChainFullscreenDescription
        {
            Windowed = true,
        };

        // 給建立出來的視窗掛上交換鏈
        DXGI.IDXGISwapChain1 swapChain =
            dxgiFactory2.CreateSwapChainForHwnd(d3D11Device1, hWnd, swapChainDescription, fullscreenDescription);

        // 不要被按下 alt+enter 進入全屏
        dxgiFactory2.MakeWindowAssociation(hWnd, DXGI.WindowAssociationFlags.IgnoreAltEnter);

        D3D11.ID3D11Texture2D backBufferTexture = swapChain.GetBuffer<D3D11.ID3D11Texture2D>(0);

        // 獲取到 dxgi 的平面,這個螢幕就約等於視窗渲染內容
        DXGI.IDXGISurface dxgiSurface = backBufferTexture.QueryInterface<DXGI.IDXGISurface>();

        // 對接 D2D 需要建立工廠
        D2D.ID2D1Factory1 d2DFactory = D2D.D2D1.D2D1CreateFactory<D2D.ID2D1Factory1>();

        // 方法1:
        var renderTargetProperties = new D2D.RenderTargetProperties(Vortice.DCommon.PixelFormat.Premultiplied);

        // 在視窗的 dxgi 的平面上建立 D2D 的畫布,如此即可讓 D2D 繪製到視窗上
        D2D.ID2D1RenderTarget d2D1RenderTarget =
            d2DFactory.CreateDxgiSurfaceRenderTarget(dxgiSurface, renderTargetProperties);
        d2D1RenderTarget.AntialiasMode = D2D.AntialiasMode.PerPrimitive;

        var renderTarget = d2D1RenderTarget;

        // 方法2:
        // 建立 D2D 裝置,透過設定 ID2D1DeviceContext 的 Target 輸出為 dxgiSurface 從而讓 ID2D1DeviceContext 渲染內容渲染到視窗上
        // 如 https://learn.microsoft.com/en-us/windows/win32/direct2d/images/devicecontextdiagram.png 圖
        // 獲取 DXGI 裝置,用來建立 D2D 裝置
        //DXGI.IDXGIDevice dxgiDevice = d3D11Device.QueryInterface<DXGI.IDXGIDevice>();
        //ID2D1Device d2dDevice = d2DFactory.CreateDevice(dxgiDevice);
        //ID2D1DeviceContext d2dDeviceContext = d2dDevice.CreateDeviceContext();

        //ID2D1Bitmap1 d2dBitmap = d2dDeviceContext.CreateBitmapFromDxgiSurface(dxgiSurface);
        //d2dDeviceContext.Target = d2dBitmap;

        //var renderTarget = d2dDeviceContext;

        var pointList = new List<Point2D>();

        var screenTranslate = new Point(0, 0);
        PInvoke.ClientToScreen(hWnd, ref screenTranslate);

        // 開個訊息迴圈等待
        Windows.Win32.UI.WindowsAndMessaging.MSG msg;
        while (true)
        {
            if (PeekMessage(out msg, default, 0, 0, PM_REMOVE) != false)
            {
                if (msg.message is PInvoke.WM_POINTERDOWN or PInvoke.WM_POINTERUPDATE or PInvoke.WM_POINTERUP)
                {
                    var wparam = msg.wParam;
                    var pointerId = (uint)(ToInt32((IntPtr)wparam.Value) & 0xFFFF);
                    PInvoke.GetPointerTouchInfo(pointerId, out var info);
                    POINTER_INFO pointerInfo = info.pointerInfo;

                    global::Windows.Win32.Foundation.RECT pointerDeviceRect = default;
                    global::Windows.Win32.Foundation.RECT displayRect = default;

                    PInvoke.GetPointerDeviceRects(pointerInfo.sourceDevice, &pointerDeviceRect, &displayRect);

                    var point2D = new Point2D(
                        pointerInfo.ptHimetricLocationRaw.X / (double)pointerDeviceRect.Width * displayRect.Width +
                        displayRect.left,
                        pointerInfo.ptHimetricLocationRaw.Y / (double)pointerDeviceRect.Height * displayRect.Height +
                        displayRect.top);

                    point2D = new Point2D(point2D.X - screenTranslate.X, point2D.Y - screenTranslate.Y);

                    pointList.Add(point2D);
                    if (pointList.Count > 200)
                    {
                        // 不要讓點太多,導致繪製速度太慢
                        pointList.RemoveRange(0, 100);
                    }

                    var color = new Color4(0xFF0000FF);
                    using var brush = renderTarget.CreateSolidColorBrush(color);

                    renderTarget.BeginDraw();
                    renderTarget.AntialiasMode = AntialiasMode.Aliased;

                    renderTarget.Clear(new Color4(0xFFFFFFFF));

                    for (var i = 1; i < pointList.Count; i++)
                    {
                        var previousPoint = pointList[i - 1];
                        var currentPoint = pointList[i];

                        renderTarget.DrawLine(new Vector2((float)previousPoint.X, (float)previousPoint.Y),
                            new Vector2((float)currentPoint.X, (float)currentPoint.Y), brush, 5);
                    }

                    renderTarget.EndDraw();
                    swapChain.Present(1, DXGI.PresentFlags.None);
                    // 等待重新整理
                    d3D11DeviceContext.Flush();
                }

                _ = TranslateMessage(&msg);
                _ = DispatchMessage(&msg);

                if (msg.message is WM_QUIT or WM_CLOSE)
                {
                    return;
                }
            }
        }
    }

    private static int ToInt32(IntPtr ptr) => IntPtr.Size == 4 ? ptr.ToInt32() : (int)(ptr.ToInt64() & 0xffffffff);

    private static IEnumerable<DXGI.IDXGIAdapter1> GetHardwareAdapter(DXGI.IDXGIFactory2 factory)
    {
        DXGI.IDXGIFactory6? factory6 = factory.QueryInterfaceOrNull<DXGI.IDXGIFactory6>();
        if (factory6 != null)
        {
            // 先告訴系統,要高效能的顯示卡
            for (uint adapterIndex = 0;
                 factory6.EnumAdapterByGpuPreference(adapterIndex, DXGI.GpuPreference.Unspecified,
                     out DXGI.IDXGIAdapter1? adapter).Success;
                 adapterIndex++)
            {
                if (adapter == null)
                {
                    continue;
                }

                DXGI.AdapterDescription1 desc = adapter.Description1;

                if ((desc.Flags & DXGI.AdapterFlags.Software) != DXGI.AdapterFlags.None)
                {
                    // Don't select the Basic Render Driver adapter.
                    adapter.Dispose();
                    continue;
                }

                Console.WriteLine($"列舉到 {adapter.Description1.Description} 顯示卡");
                yield return adapter;
            }

            factory6.Dispose();
        }

        // 如果列舉不到,那系統返回啥都可以
        for (uint adapterIndex = 0;
             factory.EnumAdapters1(adapterIndex, out DXGI.IDXGIAdapter1? adapter).Success;
             adapterIndex++)
        {
            DXGI.AdapterDescription1 desc = adapter.Description1;

            if ((desc.Flags & DXGI.AdapterFlags.Software) != DXGI.AdapterFlags.None)
            {
                // Don't select the Basic Render Driver adapter.
                adapter.Dispose();

                continue;
            }

            Console.WriteLine($"列舉到 {adapter.Description1.Description} 顯示卡");
            yield return adapter;
        }
    }

    private static LRESULT WndProc(HWND hWnd, uint message, WPARAM wParam, LPARAM lParam)
    {
        return DefWindowProc(hWnd, message, wParam, lParam);
    }

    readonly record struct Point2D(double X, double Y);
}

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

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

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

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

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

獲取程式碼之後,進入 DirectX/D2D/QalberegejeaJawchejoleawerejea 資料夾,即可獲取到原始碼。歡迎大家拉下來程式碼跑跑看效能,這個簡單的應用能夠追得上 WPF 的筆跡應用的效能。本文介紹的這個應用還不能達到 D2D 的最優效能,還有很多最佳化空間。預計極限效能,筆跡的延遲能和 WPF 追平,部分特殊情況下能夠超越 WPF 的效能。本文繪製的筆跡比較粗糙,只是簡單的折線,沒有帶任何筆跡路徑平滑和邊緣取樣最佳化。如果大家對從觸控收到的點集轉換為筆跡路徑好奇,請參閱 WPF 筆跡演算法 從點集轉筆跡輪廓

更多渲染和觸控部落格,請參閱 部落格導航

相關文章