将Direct2D渲染重定向到WPF控件

12

我正在使用 Visual C++Direct2D 开发一款绘图应用程序。 我有一个演示应用程序,其中:

// create the ID2D1Factory
D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &m_pDirect2dFactory);

// create the main window
HWND m_hwnd = CreateWindow(...);

// set the render target of type ID2D1HwndRenderTarget
m_pDirect2dFactory->CreateHwndRenderTarget(
            D2D1::RenderTargetProperties(),
            D2D1::HwndRenderTargetProperties(m_hwnd, size),
            &m_pRenderTarget
            );

当我接收到WM_PAINT消息时,我会绘制我的形状。
现在我需要开发一个WPF控件(一种Panel),它代表我的新渲染目标(因此它将替换主窗口m_hwnd),这样我就可以创建一个新的(C#)WPF项目,其中主窗口具有我的自定义面板作为子级,而渲染部分仍位于本地C++/CLI DLL项目中。
我该怎么做?我该将什么设置为我的新渲染目标?
我想使用我的WPF窗口句柄:
IntPtr windowHandle = new WindowInteropHelper(MyMainWindow).Handle;

但我需要在我的面板上绘制而不是窗口。 请注意,我不想使用任何WPF类进行渲染部分(ShapesDrawingVisuals...)

2
我想查找一个如何在WPF应用程序中托管Direct2D的示例。 好问题。 - eran otzap
你有DLL的访问权限吗?你能改变它的代码吗?我认为调用CreateWindow()存在问题。 - Domysee
WPF非常接近WIC,因此我建议使用d2d1.CreateWicBitmapRenderTarget和您自己的IWicBitmap。然后可以在派生自WPF抽象BitmapSource的类中使用此IWicBitmap(就像InteropBitmap一样,但是使用您自己的类,因为WPF不公开其WIC位图...)。这个BitmapSource现在可以成为Panel的子级。如果您有一个示例应用程序可以使用,我们可以进一步开发它。 - Simon Mourier
@Dominik 是的,我可以更改我的本地C++项目的代码。 - Nick
4个回答

3

您需要实现一个类来托管Win32窗口m_hwnd。该类继承自HwndHost

此外,您需要重写HwndHost.BuildWindowCoreHwndHost.DestroyWindowCore方法:

HandleRef BuildWindowCore(HandleRef hwndParent) 
{
  HWND parent = reinterpret_cast<HWND>(hwndParent.Handle.ToPointer());

  // here create your Window and set in the CreateWindow function
  // its parent by passing the parent variable defined above
  m_hwnd = CreateWindow(..., 
                        parent, 
                        ...);

  return HandleRef(this, IntPtr(m_hwnd));
}


void DestroyWindowCore(HandleRef hwnd) 
{
  DestroyWindow(m_hwnd);  // hwnd.Handle
}

请按照以下教程操作:演练:在WPF中托管Win32控件

3
根据《WPF 4.5 Unleashed》第19章的内容,我会尽力回答这个问题。如果您想查找它,请在“将DirectX内容与WPF内容混合使用”子部分中找到所有信息。
您的C++ DLL应该有3个公开方法:Initialize()、Cleanup()和Render()。其中有趣的方法是Initialize()和InitD3D(),后者由Initialize()调用。
extern "C" __declspec(dllexport) IDirect3DSurface9* WINAPI Initialize(HWND hwnd, int width, int height)
{
    // Initialize Direct3D
    if( SUCCEEDED( InitD3D( hwnd ) ) )
    {
        // Create the scene geometry
        if( SUCCEEDED( InitGeometry() ) )
        {
            if (FAILED(g_pd3dDevice->CreateRenderTarget(width, height, 
                D3DFMT_A8R8G8B8, D3DMULTISAMPLE_NONE, 0, 
                true, // lockable (true for compatibility with Windows XP.  False is preferred for Windows Vista or later)
                &g_pd3dSurface, NULL)))
            {
                MessageBox(NULL, L"NULL!", L"Missing File", 0);
                return NULL;
            }
            g_pd3dDevice->SetRenderTarget(0, g_pd3dSurface);
        }
    }
    return g_pd3dSurface;
}


HRESULT InitD3D( HWND hWnd )
{
    // For Windows Vista or later, this would be better if it used Direct3DCreate9Ex:
    if( NULL == ( g_pD3D = Direct3DCreate9( D3D_SDK_VERSION ) ) )
        return E_FAIL;

    // Set up the structure used to create the D3DDevice. Since we are now
    // using more complex geometry, we will create a device with a zbuffer.
    D3DPRESENT_PARAMETERS d3dpp;
    ZeroMemory( &d3dpp, sizeof( d3dpp ) );
    d3dpp.Windowed = TRUE;
    d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
    d3dpp.BackBufferFormat = D3DFMT_UNKNOWN;
    d3dpp.EnableAutoDepthStencil = TRUE;
    d3dpp.AutoDepthStencilFormat = D3DFMT_D16;

    // Create the D3DDevice
    if( FAILED( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
                                      D3DCREATE_SOFTWARE_VERTEXPROCESSING,
                                      &d3dpp, &g_pd3dDevice ) ) )
    {
        return E_FAIL;
    }

    // Turn on the zbuffer
    g_pd3dDevice->SetRenderState( D3DRS_ZENABLE, TRUE );

    // Turn on ambient lighting 
    g_pd3dDevice->SetRenderState( D3DRS_AMBIENT, 0xffffffff );

    return S_OK;
}

让我们开始看XAML代码:
xmlns:interop="clr-namespace:System.Windows.Interop;assembly=PresentationCore"
<Button.Background>
    <ImageBrush>
        <ImageBrush.ImageSource>
            <interop:D3DImage x:Name="d3dImage" />
        </ImageBrush.ImageSource>
    </ImageBrush>
</Button.Background>

我在这里使用了一个ImageBrush将其设置为按钮的背景。我相信将其添加为背景是展示DirectX内容的好方法。然而,您可以按任何方式使用该图像。
要初始化渲染,请获取当前窗口的句柄,并调用DLL的Initialize()方法:
private void initialize()
{
    IntPtr surface = DLL.Initialize(new WindowInteropHelper(this).Handle,
            (int)button.ActualWidth, (int)button.ActualHeight);

    if (surface != IntPtr.Zero)
    {
        d3dImage.Lock();
        d3dImage.SetBackBuffer(D3DResourceType.IDirect3DSurface9, surface);
        d3dImage.Unlock();

        CompositionTarget.Rendering += CompositionTarget_Rendering;
    }
}

CompositionTarget.Rendering事件在UI呈现之前被触发。您应该在其中呈现DirectX内容:
private void CompositionTarget_Rendering(object sender, EventArgs e)
{
    if (d3dImage.IsFrontBufferAvailable)
    {
        d3dImage.Lock();
        DLL.Render();
        // Invalidate the whole area:
        d3dImage.AddDirtyRect(new Int32Rect(0, 0, d3dImage.PixelWidth, d3dImage.PixelHeight));
        d3dImage.Unlock();
    }
}

那基本就是这样,希望有所帮助。现在只有几个重要的侧记:
  • Always lock your image, to avoid that WPF draws frames partially
  • Dont call Present on the Direct 3D device. WPF presents its own backbuffer, based on the surface you passed to d3dImage.SetBackBuffer().
  • The event IsFrontBufferAvailableChanged should be handled because sometimes the frontbuffer can become unavailable (for example when the user enters the lock screen). You should free or acquire the resources based on the buffer availability.

    private void d3dImage_IsFrontBufferAvailableChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
         if (d3dImage.IsFrontBufferAvailable)
         {
             initialize();
         }
         else
         {
             // Cleanup:
             CompositionTarget.Rendering -= CompositionTarget_Rendering;
             DLL.Cleanup();
         }
     }
    

我无法使用Direct3D,我需要使用Direct2D。 - Nick
1
据我所知,D3DImage 也可以托管 Direct2D 内容。 - Domysee
1
@Nick Direct2D是一个针对Direct3D子集的库。https://msdn.microsoft.com/zh-cn/library/windows/desktop/dd370966(v=vs.85).aspx - Aron

0

使用WINFORMS可能是可行的,但使用WPF则不行。 这里有一份关于WPF如何使用HWND的文档:

WPF 如何使用 HWND
为了最大限度地利用 WPF “HWND 互操作”,您需要了解 WPF 如何使用 HWND。对于任何 HWND,都不能将 WPF 渲染与 DirectX 渲染或 GDI / GDI+ 渲染混合。这有许多影响。首先,为了在所有情况下混合这些渲染模型,您必须创建一个互操作解决方案,并为每个渲染模型选择使用指定的互操作部分。此外,渲染行为为您的互操作解决方案创建了“空间”限制。该 “空间” 概念在主题技术区域概述中有更详细的解释。屏幕上的所有 WPF 元素最终都由 HWND 支持。当您创建 WPF 窗口时,WPF 创建一个顶级 HWND,并使用 HwndSource 将窗口及其 WPF 内容放置在 HWND 中。应用程序中的其余 WPF 内容共享该单一 HWND。一个例外是菜单、组合框下拉列表和其他弹出窗口。这些元素创建自己的顶级窗口,这就是为什么 WPF 菜单可能会超出包含它的窗口 HWND 的边缘的原因。当您使用 HwndHost 将 HWND 放入 WPF 中时,WPF 会告诉 Win32 如何相对于 WPF 窗口 HWND 定位新的子 HWND。HWND 的相关概念是每个 HWND 内部和之间的透明度。这也在主题技术区域概述中进行了讨论。

复制自 https://msdn.microsoft.com/en-us/library/ms742522%28v=vs.110%29.aspx

我建议你学习一种跟踪渲染区域的方法,或许创建一个“丑陋”的子窗口放在它前面。 你可以进行其他研究,尝试找到/获取WPF图形缓冲区,并使用指针和一些高级内存编程将渲染场景直接注入其中。


如何通过使用Windows Forms实现呢? - Nick
在 Windows 表单中,每个控件都有一个句柄,格式为 C/C++ win32 的 HWND。你可以通过调用 Control.Handle 属性来获取它,它返回一个 IntPtr,我相信你可以将其转换为 HWND。参考: https://msdn.microsoft.com/library/system.windows.forms.control.handle%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396 - Felype

-3

2
它是如何回答这个问题的? - Roman R.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接