如何从旧版 .NET Framework 的托管代码将 Direct2D 渲染器初始化为 GDI 上下文

How initialize Direct2D renderer into GDI context from managed code for older version of .NET Framework

给定 Windows 形式的遗留桌面应用程序,托管代码(C# 和 VB 项目)运行ning 在 .NET Framework 3.5 上(由于超出此问题范围的原因,无法迁移到较新的 .NET), 如何逐步将代码从 GDI+ 转换为 Direct2D?或者可能到 Direct3D?

另一个限制是生成的应用程序在 Windows7 上工作,但我们将迁移到 Windows8 或 Windows10,如果这是让它发挥作用的唯一方法。

(推动力是与 Graphics.FillPath 一起使用时 GDI+ 纹理处理中的错误和较小的纹理缩放因子;但无论如何我们最终还是想转移到 Direct2D 或 Direct3D。)

如果我们的目标是 .NET Framework 4.0+ 和 Windows8+,那么我们想要做的事情就很简单了,如此处所述:
Direct2D and GDI Interoperability Overview

不幸的是,尝试使这些指令适应我们较旧的目标规范 运行 遇到了一系列障碍。

第一步是使用一些托管包装器访问 Direct2D。
(不确定 Microsoft 和其他地方的 wrappers/code examples/tutorials 是否针对 Direct2D 1.0 或 1.1。)

我知道的选项:
A. 用于托管代码的 Microsoft DirectX 9.0 (MDX)(最后更新 2006):
我看到了关于这个长期不受支持的软件包的令人沮丧的评论,以及使用 SlimDX 或 SharpDX 代替的建议 [或迁移到受支持但与我们指定的旧平台不兼容的更新的 Microsoft 技术]。似乎不是一个好的长期方向。所以我还没有尝试过这个。
B. Win2D - 不支持 Windows 7,也不支持 .NET Framework 3.5.
C. SharpDX(开源,积极维护):
试图使用这个。不幸的是,直到 v.3.0.0 才添加 Direct2D,这需要 .NET Framework 4.0+。所以这不是一个选择,直到我们准备好对我们的应用程序进行更重大的改革。

D. SlimDX(开源,2012 年最后更新): 已成功安装并渲染到独立的 Direct2D window。
坚持调整此以呈现 "GDI context",如上文 "Interoperability Overview" link 中所述。

C++ 代码来自 "interoperability" link:

// Create a DC render target.
D2D1_RENDER_TARGET_PROPERTIES props = D2D1::RenderTargetProperties(
    D2D1_RENDER_TARGET_TYPE_DEFAULT,
    D2D1::PixelFormat(
        DXGI_FORMAT_B8G8R8A8_UNORM,
        D2D1_ALPHA_MODE_IGNORE),
    0,
    0,
    D2D1_RENDER_TARGET_USAGE_NONE,
    D2D1_FEATURE_LEVEL_DEFAULT
    );

hr = m_pD2DFactory->CreateDCRenderTarget(&props, &m_pDCRT);

正在尝试编写 VB 代码:

Dim factory As New Direct2D.Factory

' --- THIS WORKS using SlimDX, SlimDX.Direct2D ---
'   (But it is not what I need; taken from SlimDX sample code)
' Stand-alone D2D window (NOT to GDI)
' "IntPtr handle" is "The window handle to associate with the device.".
Dim windowProperties As New WindowRenderTargetProperties(handle, New Size(600, 600))
Dim target As New WindowRenderTarget(factory, windowProperties)

' --- Hand-Translation of C++ code from "interoperability" link ---
Dim targetProperties As New RenderTargetProperties
targetProperties.Type = RenderTargetType.Default
targetProperties.PixelFormat = New PixelFormat(Format.B8G8R8A8_UNorm, AlphaMode.Ignore)
' *** How invoke "ID2D1Factory::CreateDCRenderTarget"? ***
'  (There aren't many methods on SlimDX.Direct2D.Factory "factory" above,
'   so if it is possible at all, SlimDX must do this some other way.)
' TODO

如何前进

第一个问题:上面 link 中描述的 D2D/GDI 互操作性是否适用于指定的目标平台(.NET 3.5,Windows 7)?

如果没有,那么我的尝试是不可能的。尽管如果 Windows 7 是问题所在,那么“Windows 10 上的 .NET 3.5”的解决方案将值得了解。

第二个问题假设互操作性是可能的,那么我面临着 SlimDX 的限制?或者我忽略了什么? 我不希望将 C++ 项目添加到此解决方案,但如果可以预编译自定义 C++ dll,然后使用 [除了 SlimDX dll],那将是一个(勉强)可以容忍的解决方案。

而不是 C++ 代码,手动编写托管包装器来访问需要的东西[但我在 SlimDX 中找不到]来初始化 D2D/GDI 互操作性?如何从上面的 link 转换 C++ 代码?

更新

在 SlimDX 中找到了需要的调用。详情见我的回答。

刚刚在 SlimDX 中发现 DeviceContextRenderTarget class:

' Equivalent to "ID2D1Factory::CreateDCRenderTarget".
    Dim target2 As New DeviceContextRenderTarget(factory, targetProperties)

要完成初始化,需要绑定那个DC。

来自互操作性的 C++ link:

HRESULT DemoApp::OnRender(const PAINTSTRUCT &ps)
{

    HRESULT hr;
    RECT rc;

    // Get the dimensions of the client drawing area.
    GetClientRect(m_hwnd, &rc);

    // Create the DC render target.
    hr = CreateDeviceResources();

    if (SUCCEEDED(hr))
    {
        // Bind the DC to the DC render target.
        hr = m_pDCRT->BindDC(ps.hdc, &rc);


        // Draw with Direct2D.

        m_pDCRT->BeginDraw();

        m_pDCRT->SetTransform(D2D1::Matrix3x2F::Identity());

        m_pDCRT->Clear(D2D1::ColorF(D2D1::ColorF::White));

        m_pDCRT->DrawEllipse(
            D2D1::Ellipse(
                D2D1::Point2F(150.0f, 150.0f),
                100.0f,
                100.0f),
            m_pBlackBrush,
            3.0
            );

        hr = m_pDCRT->EndDraw();

        // Draw some GDI content.
        if (SUCCEEDED(hr))
        {
         ...
        }
    }

    if (hr == D2DERR_RECREATE_TARGET)
    {
        hr = S_OK;
        DiscardDeviceResources();
    }

    return hr;
}

VB 翻译:

' "canvas" is the Windows control (tested with Panel) that I wish to draw D2D in.
Private Sub canvas_Paint(sender As Object, e As System.Windows.Forms.PaintEventArgs) Handles canvas.Paint
    ' Render GDI content that is below D2D content
    '... existing GDI calls ...

    ' Render Direct2D content.
    cDirect2DRenderer.TestRendering(e.Graphics, canvas.ClientSize)

    ' Render GDI content that is above D2D content.
    '... existing GDI calls ...
End Sub

其中使用 VB class:

Imports System.Drawing

Imports SlimDX
Imports SlimDX.Direct2D
Imports SlimDX.DXGI

Public Class cDirect2DRenderer

#Region "=== Shared ==="
    Public Shared Sub TestRendering(gr As Graphics, canvasSize As System.Drawing.Size)
        Dim renderer As New cDirect2DRenderer

        ' CAUTION: After this, must call EndDraw or ReleaseHDC when done drawing.
        Dim success As Boolean = renderer.BeginDraw(gr, canvasSize)

        ' Render some Direct2D content.
        success = renderer.Test_Render(success)

        success = renderer.EndDraw(gr, success)
        If Not success Then
            'TODO: Log error.
        End If

        renderer.Dispose() : renderer = Nothing
    End Sub
#End Region


#Region "=== Fields, Constructor, Dispose ==="
    Private Ready As Boolean
    Private _factory As New Direct2D.Factory
    Private Target As DeviceContextRenderTarget
    Private Bounds As Rectangle
    Private Hdc As IntPtr

    Public Sub New()

    End Sub

    Public Sub Dispose()
        If Target IsNot Nothing Then
            Target.Dispose() : Target = Nothing
        End If

        Ready = False
    End Sub
#End Region


#Region "=== BeginDraw, Test_Render, EndDraw ==="
    Public Property Factory As Direct2D.Factory
        Get
            Return _factory
        End Get
        Set(value As Direct2D.Factory)
            If Exists(_factory) Then
                _factory.Dispose()
                '_factory = Nothing
            End If

            _factory = value
        End Set
    End Property

    ' True if Ready to draw.
    ' CAUTION: Even if returns False, Caller must call EndDraw, so that ReleaseHDC is called.
    Public Function BeginDraw(g As Graphics, canvasSize As System.Drawing.Size) As Boolean
        ' CAUTION: After this, must call EndDraw or ReleaseHDC when done drawing.
        EnsureReady(g, canvasSize)
        If Not Ready Then
            ' Initialization failed.
            Return False
        End If

        Try
            Dim success As Boolean = True
            Target.BeginDraw()

            Return success

        Catch ex As Exception
            Return False
        End Try
    End Function

    Public Function Test_Render(success As Boolean) As Boolean
        Try
            Target.Transform = Matrix3x2.Identity
            Target.Clear(New Color4(Color.BlueViolet))
            Dim brush As Direct2D.Brush = New SolidColorBrush(Target, New Color4(Color.Black))
            Dim ellipse As Direct2D.Ellipse = New Ellipse() With {
                    .Center = New PointF(100, 100),
                    .RadiusX = 80, .RadiusY = 80}
            Target.DrawEllipse(brush, ellipse)
            Target.FillEllipse(brush, ellipse)

        Catch ex As Exception
            success = False
        End Try

        Return success
    End Function

    ' True if rendering succeeds.
    ' "success" is accumulation, included in the return value.
    Public Function EndDraw(g As Graphics, success As Boolean) As Boolean
        ' Wrap EndDraw in Try, because "ReleaseHDC" must always be called.
        Try
            ' EndDraw is always called (even if "success" is already False).
            success = success And Target.EndDraw().IsSuccess
        Catch ex As Exception
            success = False
        End Try

        ReleaseHDC(g)
        ' TBD: This could be moved out elsewhere.
        EnsureFactoryReleased()

        If Not success Then
            Trouble()
        End If
        Return success
    End Function

    ' CAUTION: Caller must call EndDraw or ReleaseHDC when done drawing.
    Private Sub EnsureReady(g As Graphics, canvasSize As System.Drawing.Size)
        Dim newBounds As New Rectangle(0, 0, canvasSize.Width, canvasSize.Height)

        If Not Ready OrElse Not SameBounds(newBounds) Then
            If Ready Then
                Dispose()
            End If

            Me.Bounds = newBounds

            Me.Ready = InitializeDevice(g)
        End If
    End Sub

    ' AFTER set Me.Bounds.
    ' CAUTION: Caller must call g.ReleaseHdc(Me.Hdc) when done drawing.
    Private Function InitializeDevice(g As Graphics) As Boolean
        Try
            '' Stand-alone D2D window (NOT to GDI)
            ' ...width As Integer, height As Integer
            'Dim windowProperties As New WindowRenderTargetProperties(handle, New Size(600, 600))
            'Dim target1 As New WindowRenderTarget(factory, windowProperties)

            Dim targetProperties As New RenderTargetProperties
            targetProperties.Type = RenderTargetType.Default
            targetProperties.PixelFormat = New PixelFormat(Format.B8G8R8A8_UNorm, AlphaMode.Ignore)
            ' Equivalent to "ID2D1Factory::CreateDCRenderTarget".
            Me.Target = New DeviceContextRenderTarget(Me.Factory, targetProperties)

            ' CAUTION: Caller must call g.ReleaseHdc(Me.Hdc) when done drawing.
            Me.Hdc = g.GetHdc()
            Try
                'TestStr = Me.Hdc.ToString()
                Dim result As SlimDX.Result = Target.BindDeviceContext(Me.Hdc, Me.Bounds)

                If Not result.IsSuccess Then
                    ReleaseHDC(g)
                End If
                Return result.IsSuccess

            Catch ex As Exception
                ReleaseHDC(g)
                Return False
            End Try

        Catch ex As Exception
            Return False
        End Try
    End Function

    Private Sub ReleaseHDC(g As Graphics)
        Try
            g.ReleaseHdc(Me.Hdc)
        Finally
            Me.Hdc = Nothing
        End Try
    End Sub

    Private Sub EnsureFactoryReleased()
        Me.Factory = Nothing
    End Sub

    Private Function SameBounds(newBounds As Rectangle) As Boolean
        ' TBD: Does Equals do what we need?
        Return (newBounds.Equals(Me.Bounds))
    End Function
#End Region

End Class