當前位置: 妍妍網 > 碼農

使用 D2D 繪制界面以及使用 AOT 釋出的套用

2024-02-10碼農

這是我用不到 370 行程式碼,從零開始控制台建立 Win32 視窗,再掛上交換鏈,在視窗上使用 D2D 繪制界面內容。最後使用 AOT 方式釋出的測試套用。成品檔體積不超過 10MB 且執行記憶體穩定在 60MB 以內,滿幀率執行但 CPU 近乎不動

整個測試套用采用了 .NET 8 的框架,用於更好的支持 AOT 釋出

使用了 Vortice 系列庫用於對 DirectX 的封裝,方便讓編寫呼叫 DirectX 的程式碼

使用了 Microsoft.Windows.CsWin32 方便進行 Win32 方法的呼叫

所有的程式碼都寫在 Program.cs 檔裏面,程式碼長度不到 370 行,更有趣的是,可以強行算是都寫在 Main 方法裏面,由 Main 方法以及放在 Main 方法裏面的局部方法構成。整體實作非常簡單。我將會在本文末尾告訴大家本文的程式碼的下載方法

本文僅僅是分享我的開發經驗,不包含 DirectX 的前置知識。如果不熟悉 D2D 和 DirectX 還請以看著玩的心態閱讀本文

一開始采用了 DirectX 使用 Vortice 從零開始控制台建立 Direct2D1 視窗修改顏色 和 dotnet DirectX 透過 Vortice 控制台使用 ID2D1DeviceContext 繪制畫面 部落格提供的方法搭建了基礎的套用框架

為了讓界面更加的豐富,我準備在界面添加多個圓形。然後為了讓界面動起來,我添加了名為 DrawingInfo 的結構體,用於存放每個圓形的座標和大小等資訊

readonlyrecordstructDrawingInfo(System.Numerics.Vector2Offset,SizeSize,D2D.ID2D1SolidColorBrushBrush);

先在繪制的迴圈外對 DrawingInfo 進行隨機設定值

varellipseInfoList=newList<DrawingInfo>();
for(inti=0;i<3000;i++)
{
// 隨意建立顏色
varcolor=newColor4((byte)Random.Shared.Next(255),(byte)Random.Shared.Next(255),(byte)Random.Shared.Next(255));
D2D.ID2D1SolidColorBrushbrush=renderTarget.CreateSolidColorBrush(color);
ellipseInfoList.Add(newDrawingInfo(newSystem.Numerics.Vector2(Random.Shared.Next(clientSize.Width),Random.Shared.Next(clientSize.Height)),newSize(Random.Shared.Next(10,100)),brush));
}

進入迴圈之後,再每次修改 Offset 的值,這樣就可以讓每次繪制的圓形動起來

while(true)
{
// 開始繪制邏輯
renderTarget.BeginDraw();
// 清空畫布
renderTarget.Clear(newColor4(0xFF,0xFF,0xFF));
// 在下面繪制漂亮的界面
for(vari=0;i<ellipseInfoList.Count;i++)
{
vardrawingInfo=ellipseInfoList[i];
varvector2=drawingInfo.Offset;
vector2.X+=Random.Shared.Next(200)-100;
vector2.Y+=Random.Shared.Next(200)-100;
while(vector2.X<100||vector2.X>clientSize.Width-100)
{
vector2.X=Random.Shared.Next(clientSize.Width);
}
while(vector2.Y<100||vector2.Y>clientSize.Height-100)
{
vector2.Y=Random.Shared.Next(clientSize.Height);
}
ellipseInfoList[i]=drawingInfowith{Offset=vector2};
// 忽略其他程式碼
}
// 忽略其他程式碼
}






以上的修改座標程式碼只是為了讓圓形每次都在其附近移動

附帶就在裏層迴圈將每個圓形繪制,程式碼如下

// 在下面繪制漂亮的界面
for(vari=0;i<ellipseInfoList.Count;i++)
{
// 忽略其他程式碼
renderTarget.FillEllipse(newD2D.Ellipse(vector2,drawingInfo.Size.Width,drawingInfo.Size.Height),drawingInfo.Brush);
}

大概的改動如此,接下來咱需要改造一下 csproj 計畫檔,讓此計畫可以構建出 AOT 版本的套用

先修改 TargetFramework 為 net8.0 使用 .NET 8 可以更好構建 AOT 套用

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

接著為了減少不斷提示的平台警告,添加以下程式碼忽略 CA1416 警告

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<NoWarn>CA1416</NoWarn>
</PropertyGroup>

接著再添加 PublishAot 內容,這樣呼叫釋出命令之後,就可以自動建立 AOT 套用的檔

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<NoWarn>CA1416</NoWarn>
<PublishAot>true</PublishAot>
</PropertyGroup>

此時執行起來將不會成功,將會提示大概如下的錯誤

Unhandled Exception: System.MissingMethodException: No parameterless constructor defined for type 'Vortice.DXGI.IDXGIFactory2'.
at System.ActivatorImplementation.CreateInstance(Type, BindingFlags, Binder, Object[], CultureInfo, Object[]) + 0x348
at SharpGen.Runtime.MarshallingHelpers.FromPointer[T](IntPtr) + 0x8c
at Vortice.DXGI.DXGI.CreateDXGIFactory1[T]() + 0x55
at Program.<<Main>$>g__CreateD2D|0_2(Program.<>c__Display class0_0&) + 0x90
at Program.<Main>$(String[] args) + 0x23e
at CedageawhakairnerewhalNaibiferenagifee!<BaseAddress>+0x17a3c0

或者是如下的錯誤

Unhandled Exception: System.MissingMethodException: No parameterless constructor defined for type 'Vortice.Direct3D11.ID3D11Device1'.
at System.ActivatorImplementation.CreateInstance(Type, BindingFlags, Binder, Object[], CultureInfo, Object[]) + 0x348
at SharpGen.Runtime.MarshallingHelpers.FromPointer[T](IntPtr) + 0x8c
at SharpGen.Runtime.ComObject.QueryInterface[T]() + 0x64
at Program.<<Main>$>g__CreateD2D|0_2(Program.<>c__Display class0_0&) + 0x1c7
at Program.<Main>$(String[] args) + 0x23e
at CedageawhakairnerewhalNaibiferenagifee!<BaseAddress>+0x335cf0

這是因為這些參照的柯瑞面的型別在 AOT 的裁剪過程被丟掉

修復的方法很簡單,那就是將 Vortice 添加到 TrimmerRootAssembly 裏面,防止在 AOT 過程被裁剪

<ItemGroup>
<TrimmerRootAssemblyInclude="Vortice.Win32"/>
<TrimmerRootAssemblyInclude="Vortice.DXGI"/>
<TrimmerRootAssemblyInclude="Vortice.Direct3D11"/>
<TrimmerRootAssemblyInclude="Vortice.Direct2D1"/>
<TrimmerRootAssemblyInclude="Vortice.D3DCompiler"/>
<TrimmerRootAssemblyInclude="Vortice.DirectX"/>
<TrimmerRootAssemblyInclude="Vortice.Mathematics"/>
</ItemGroup>

修改之後的 csproj 程式碼如下

<ProjectSdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<NoWarn>CA1416</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReferenceInclude="Vortice.Direct2D1"Version="2.1.32"/>
<PackageReferenceInclude="Vortice.Direct3D11"Version="2.1.32"/>
<PackageReferenceInclude="Vortice.DirectX"Version="2.1.32"/>
<PackageReferenceInclude="Vortice.D3DCompiler"Version="2.1.32"/>
<PackageReferenceInclude="Vortice.Win32"Version="1.6.2"/>
<PackageReferenceInclude="Microsoft.Windows.CsWin32"PrivateAssets="all"Version="0.2.63-beta"/>
</ItemGroup>
<ItemGroup>
<TrimmerRootAssemblyInclude="Vortice.Win32"/>
<TrimmerRootAssemblyInclude="Vortice.DXGI"/>
<TrimmerRootAssemblyInclude="Vortice.Direct3D11"/>
<TrimmerRootAssemblyInclude="Vortice.Direct2D1"/>
<TrimmerRootAssemblyInclude="Vortice.D3DCompiler"/>
<TrimmerRootAssemblyInclude="Vortice.DirectX"/>
<TrimmerRootAssemblyInclude="Vortice.Mathematics"/>
</ItemGroup>
</Project>

完成以上配置之後,即可使用命令列 dotnet publish 將計畫進行釋出,如果在釋出的控制台可以看到 Generating native code 輸出,那就證明配置正確,正在構建 AOT 檔

完成構建之後,即可在 bin\Release\net8.0\win-x64\publish 資料夾找到構建輸出的檔,在我這裏看到的輸出檔大小大概在 10MB 以下,大家可以嘗試使用本文末尾的方法拉取我的程式碼自己構建一下,試試效果

執行起來的工作管理員所見記憶體大小大約是 30MB 左右,透過 VMMap 工具檢視 WorkingSet 和 Private Bytes 都在 60MB 以內。雖然 Committed 的記憶體高達 300MB 但是絕大部份都是 Image 共享部份占用記憶體,如顯卡驅動等部份的占用,這部份占用大約在 250MB 以上,實際的 Image 的 private 的占用不到 10MB 大小

我認為這個技術可以用來制作一些小而美的工具,甚至是不用考慮 x86 的,只需考慮 x64 的機器上執行的套用的安裝包制作程式。要是拿著 D2D 繪制的界面去當安裝包的界面,那估計安裝包行業會卷起來

以下是所有的程式碼

usingD3D=Vortice.Direct3D;
usingD3D11=Vortice.Direct3D11;
usingDXGI=Vortice.DXGI;
usingD2D=Vortice.Direct2D1;
usingSystem.Runtime.CompilerServices;
usingSystem.Runtime.InteropServices;
usingWindows.Win32.Foundation;
usingWindows.Win32.UI.WindowsAndMessaging;
usingstaticWindows.Win32.PInvoke;
usingstaticWindows.Win32.UI.WindowsAndMessaging.PEEK_MESSAGE_REMOVE_TYPE;
usingstaticWindows.Win32.UI.WindowsAndMessaging.WND class_ styleS;
usingstaticWindows.Win32.UI.WindowsAndMessaging.WINDOW_ style;
usingstaticWindows.Win32.UI.WindowsAndMessaging.WINDOW_EX_ style;
usingstaticWindows.Win32.UI.WindowsAndMessaging.SYSTEM_METRICS_INDEX;
usingstaticWindows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD;
usingVortice.DCommon;
usingVortice.Mathematics;
usingAlphaMode=Vortice.DXGI.AlphaMode;
usingSystem.Diagnostics;
unsafe
{
SizeIclientSize=newSizeI(1000,1000);
// 視窗標題
vartitle="lindexi D2D AOT";
varwindow className=title;
WINDOW_ style style=WS_CAPTION|WS_SYSMENU|WS_MINIMIZEBOX|WS_CLIPSIBLINGS|WS_BORDER|WS_DLGFRAME|WS_THICKFRAME|WS_GROUP|WS_TABSTOP|WS_SIZEBOX;
varrect=newRECT
{
right=clientSize.Width,
bottom=clientSize.Height
};
AdjustWindowRectEx(&rect, style,false,WS_EX_APPWINDOW);
intx=0;
inty=0;
intwindowWidth=rect.right-rect.left;
intwindowHeight=rect.bottom-rect.top;
// 隨便,放在螢幕中間好了。多個顯視器?忽略
intscreenWidth=GetSystemMetrics(SM_CXSCREEN);
intscreenHeight=GetSystemMetrics(SM_CYSCREEN);
x=(screenWidth-windowWidth)/2;
y=(screenHeight-windowHeight)/2;
varhInstance=GetModuleHandle((string)null);
fixed(char*lpsz className=window className)
{
PCWSTRszCursorName=new((char*)IDC_ARROW);
varwnd classEx=newWND classEXW
{
cbSize=(uint)Unsafe.SizeOf<WND classEXW>(),
style=CS_HREDRAW|CS_VREDRAW|CS_OWNDC,
// 核心邏輯,設定訊息迴圈
lpfnWndProc=newWNDPROC(WndProc),
hInstance=(HINSTANCE)hInstance.DangerousGetHandle(),
hCursor=LoadCursor((HINSTANCE)IntPtr.Zero,szCursorName),
hbrBackground=(Windows.Win32.Graphics.Gdi.HBRUSH)IntPtr.Zero,
hIcon=(HICON)IntPtr.Zero,
lpsz className=lpsz className
};
ushortatom=Register classEx(wnd classEx);
if(atom==0)
{
thrownewInvalidOperationException($"Failed to register window class. Error: {Marshal.GetLastWin32Error()}");
}
}
// 建立視窗
varhWnd=CreateWindowEx
(
WS_EX_APPWINDOW,
window className,
title,
style,
x,
y,
windowWidth,
windowHeight,
hWndParent:default,
hMenu:default,
hInstance:default,
lpParam:null
);
// 建立完成,那就顯示
ShowWindow(hWnd,SW_NORMAL);
CreateD2D();
// 開個訊息迴圈等待
Windows.Win32.UI.WindowsAndMessaging.MSGmsg;
while(true)
{
if(GetMessage(outmsg,hWnd,0,0)!=false)
{
_=TranslateMessage(&msg);
_=DispatchMessage(&msg);
if(msg.messageisWM_QUITorWM_CLOSEor0)
{
return;
}
}
}
voidCreateD2D()
{
RECTwindowRect;
GetClientRect(hWnd,&windowRect);
clientSize=newSizeI(windowRect.right-windowRect.left,windowRect.bottom-windowRect.top);
// 開始建立工廠建立 D3D 的邏輯
vardxgiFactory2=DXGI.DXGI.CreateDXGIFactory1<DXGI.IDXGIFactory2>();
varhardwareAdapter=GetHardwareAdapter(dxgiFactory2)
// 這裏 ToList 只是想列出所有的 IDXGIAdapter1 方便偵錯而已。在實際程式碼裏,大部份都是獲取第一個
.ToList().FirstOrDefault();
if(hardwareAdapter==null)
{
thrownewInvalidOperationException("Cannot detect D3D11 adapter");
}
// 功能等級
// [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.IDXGIAdapter1adapter=hardwareAdapter;
D3D11.DeviceCreationFlagscreationFlags=D3D11.DeviceCreationFlags.BgraSupport;
varresult=D3D11.D3D11.D3D11CreateDevice
(
adapter,
D3D.DriverType.Unknown,
creationFlags,
featureLevels,
outD3D11.ID3D11Deviced3D11Device,outD3D.FeatureLevelfeatureLevel,
outD3D11.ID3D11DeviceContextd3D11DeviceContext
);
if(result.Failure)
{
// 如果失敗了,那就不指定顯卡,走 WARP 的方式
// http://go.microsoft.com/fwlink/?LinkId=286690
result=D3D11.D3D11.D3D11CreateDevice(
IntPtr.Zero,
D3D.DriverType.Warp,
creationFlags,
featureLevels,
outd3D11Device,outfeatureLevel,outd3D11DeviceContext);
// 如果失敗,就不能繼續
result.CheckError();
}
// 大部份情況下,用的是 ID3D11Device1 和 ID3D11DeviceContext1 型別
// 從 ID3D11Device 轉換為 ID3D11Device1 型別
vard3D11Device1=d3D11Device.QueryInterface<D3D11.ID3D11Device1>();
vard3D11DeviceContext1=d3D11DeviceContext.QueryInterface<D3D11.ID3D11DeviceContext1>();
// 轉換完成,可以減少對 ID3D11Device1 的參照計數
// 呼叫 Dispose 不會釋放掉剛才申請的 D3D 資源,只是減少參照計數
d3D11Device.Dispose();
d3D11DeviceContext.Dispose();
// 建立裝置,接下來就是關聯視窗和交換鏈
DXGI.FormatcolorFormat=DXGI.Format.B8G8R8A8_UNorm;
constintFrameCount=2;
DXGI.SwapChainDescription1swapChainDescription=new()
{
Width=clientSize.Width,
Height=clientSize.Height,
Format=colorFormat,
BufferCount=FrameCount,
BufferUsage=DXGI.Usage.RenderTargetOutput,
SampleDescription=DXGI.SampleDescription.Default,
Scaling=DXGI.Scaling.Stretch,
SwapEffect=DXGI.SwapEffect.FlipDiscard,
AlphaMode=AlphaMode.Ignore,
};
// 設定是否全螢幕
DXGI.SwapChainFullscreenDescriptionfullscreenDescription=newDXGI.SwapChainFullscreenDescription
{
Windowed=true
};
// 給建立出來的視窗掛上交換鏈
DXGI.IDXGISwapChain1swapChain=
dxgiFactory2.CreateSwapChainForHwnd(d3D11Device1,hWnd,swapChainDescription,fullscreenDescription);
// 不要被按下 alt+enter 進入全螢幕
dxgiFactory2.MakeWindowAssociation(hWnd,DXGI.WindowAssociationFlags.IgnoreAltEnter);
D3D11.ID3D11Texture2DbackBufferTexture=swapChain.GetBuffer<D3D11.ID3D11Texture2D>(0);
// 獲取到 dxgi 的平面,這個平面就約等於視窗渲染內容
DXGI.IDXGISurfacedxgiSurface=backBufferTexture.QueryInterface<DXGI.IDXGISurface>();
// 對接 D2D 需要建立工廠
D2D.ID2D1Factory1d2DFactory=D2D.D2D1.D2D1CreateFactory<D2D.ID2D1Factory1>();
// 方法1:
//var renderTargetProperties = new D2D.RenderTargetProperties(PixelFormat.Premultiplied);
//// 在視窗的 dxgi 的平面上建立 D2D 的畫布,如此即可讓 D2D 繪制到視窗上
//D2D.ID2D1RenderTarget d2D1RenderTarget =
// d2DFactory.CreateDxgiSurfaceRenderTarget(dxgiSurface, renderTargetProperties);
//var renderTarget = d2D1RenderTarget;
// 方法2:
// 建立 D2D 裝置,透過設定 ID2D1DeviceContext 的 Target 輸出為 dxgiSurface 從而讓 ID2D1DeviceContext 渲染內容渲染到視窗上
// 如 https://learn.microsoft.com/en-us/windows/win32/direct2d/images/devicecontextdiagram.png
// 獲取 DXGI 裝置,用來建立 D2D 裝置
DXGI.IDXGIDevicedxgiDevice=d3D11Device1.QueryInterface<DXGI.IDXGIDevice>();
D2D.ID2D1Deviced2dDevice=d2DFactory.CreateDevice(dxgiDevice);
D2D.ID2D1DeviceContextd2dDeviceContext=d2dDevice.CreateDeviceContext();
D2D.ID2D1Bitmap1d2dBitmap=d2dDeviceContext.CreateBitmapFromDxgiSurface(dxgiSurface);
d2dDeviceContext.Target=d2dBitmap;
varrenderTarget=d2dDeviceContext;
// 開啟後台渲染執行緒,無限重新整理
varstopwatch=Stopwatch.StartNew();
varcount=0;
Task.Factory.StartNew(()=>
{
varellipseInfoList=newList<DrawingInfo>();
for(inti=0;i<100;i++)
{
// 隨意建立顏色
varcolor=newColor4((byte)Random.Shared.Next(255),(byte)Random.Shared.Next(255),(byte)Random.Shared.Next(255));
D2D.ID2D1SolidColorBrushbrush=renderTarget.CreateSolidColorBrush(color);
ellipseInfoList.Add(newDrawingInfo(newSystem.Numerics.Vector2(Random.Shared.Next(clientSize.Width),Random.Shared.Next(clientSize.Height)),newSize(Random.Shared.Next(10,100)),brush));
}
while(true)
{
// 開始繪制邏輯
renderTarget.BeginDraw();
// 清空畫布
renderTarget.Clear(newColor4(0xFF,0xFF,0xFF));
// 在下面繪制漂亮的界面
for(vari=0;i<ellipseInfoList.Count;i++)
{
vardrawingInfo=ellipseInfoList[i];
varvector2=drawingInfo.Offset;
vector2.X+=Random.Shared.Next(200)-100;
vector2.Y+=Random.Shared.Next(200)-100;
while(vector2.X<100||vector2.X>clientSize.Width-100)
{
vector2.X=Random.Shared.Next(clientSize.Width);
}
while(vector2.Y<100||vector2.Y>clientSize.Height-100)
{
vector2.Y=Random.Shared.Next(clientSize.Height);
}
ellipseInfoList[i]=drawingInfowith{Offset=vector2};
renderTarget.FillEllipse(newD2D.Ellipse(vector2,drawingInfo.Size.Width,drawingInfo.Size.Height),drawingInfo.Brush);
}
renderTarget.EndDraw();
swapChain.Present(1,DXGI.PresentFlags.None);
// 等待重新整理
d3D11DeviceContext1.Flush();
// 統計重新整理率
count++;
if(stopwatch.Elapsed>=TimeSpan.FromSeconds(1))
{
Console.WriteLine($"FPS: {count/stopwatch.Elapsed.TotalSeconds}");
stopwatch.Restart();
count=0;
}
}
},TaskCreationOptions.LongRunning);
}
}
staticIEnumerable<DXGI.IDXGIAdapter1>GetHardwareAdapter(DXGI.IDXGIFactory2factory)
{
DXGI.IDXGIFactory6?factory6=factory.QueryInterfaceOrNull<DXGI.IDXGIFactory6>();
if(factory6!=null)
{
// 先告訴系統,要高效能的顯卡
for(intadapterIndex=0;
factory6.EnumAdapterByGpuPreference(adapterIndex,DXGI.GpuPreference.HighPerformance,
outDXGI.IDXGIAdapter1?adapter).Success;
adapterIndex++)
{
if(adapter==null)
{
continue;
}
DXGI.AdapterDescription1desc=adapter.Description1;
if((desc.Flags&DXGI.AdapterFlags.Software)!=DXGI.AdapterFlags.None)
{
// Don't select the Basic Render Driver adapter.
adapter.Dispose();
continue;
}
//factory6.Dispose();
Console.WriteLine($"列舉到 {adapter.Description1.Description} 顯卡");
yieldreturnadapter;
}
factory6.Dispose();
}
// 如果列舉不到,那系統返回啥都可以
for(intadapterIndex=0;
factory.EnumAdapters1(adapterIndex,outDXGI.IDXGIAdapter1?adapter).Success;
adapterIndex++)
{
DXGI.AdapterDescription1desc=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} 顯卡");
yieldreturnadapter;
}
}
staticLRESULTWndProc(HWNDhWnd,uintmessage,WPARAMwParam,LPARAMlParam)
{
returnDefWindowProc(hWnd,message,wParam,lParam);
}
readonlyrecordstructDrawingInfo(System.Numerics.Vector2Offset,SizeSize,D2D.ID2D1SolidColorBrushBrush);

































































本文以上程式碼放在github 和 gitee 歡迎存取

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

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

以上使用的是 gitee 的源,如果 gitee 不能存取,請替換為 github 的源。請在命令列繼續輸入以下程式碼

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

獲取程式碼之後,進入 CedageawhakairnerewhalNaibiferenagifee 資料夾

更多關於 DirectX 和 D2D 相關技術請參閱我的 部落格導航

交流 Vortice 技術,歡迎加群: 622808968