将 Direct2D 呈现重定向到 WPF 控件

Redirect Direct2D rendering to a WPF Control

我正在 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)来表示我的新渲染目标(因此它将替换主 window m_hwnd),以便我可以创建一个新的 (C#) WPF 项目,其主要 window 具有我的自定义面板作为子项,而渲染部分保留在本机 C++/CLI DLL 项目中。

我该怎么做?我必须将什么设置为我的新渲染目标?

我想使用我的 WPF 句柄 window:

IntPtr windowHandle = new WindowInteropHelper(MyMainWindow).Handle;

但我需要在我的面板上绘图,而不是在我的 window 上绘图。

请注意,我不想在渲染部分使用任何 WPF 类 (Shapes, DrawingVisuals...)

WINFORMS 或许可以,但 WPF 不行。 这是一份讨论 WPF 如何使用 HWND 的文档:

How WPF Uses Hwnds

To make the most of WPF "HWND interop", you need to understand how WPF uses HWNDs. For any HWND, you cannot mix WPF rendering with DirectX rendering or GDI / GDI+ rendering. This has a number of implications. Primarily, in order to mix these rendering models at all, you must create an interoperation solution, and use designated segments of interoperation for each rendering model that you choose to use. Also, the rendering behavior creates an "airspace" restriction for what your interoperation solution can accomplish. The "airspace" concept is explained in greater detail in the topic Technology Regions Overview. All WPF elements on the screen are ultimately backed by a HWND. When you create a WPF Window, WPF creates a top-level HWND, and uses an HwndSource to put the Window and its WPF content inside the HWND. The rest of your WPF content in the application shares that singular HWND. An exception is menus, combo box drop downs, and other pop-ups. These elements create their own top-level window, which is why a WPF menu can potentially go past the edge of the window HWND that contains it. When you use HwndHost to put an HWND inside WPF, WPF informs Win32 how to position the new child HWND relative to the WPF Window HWND. A related concept to HWND is transparency within and between each HWND. This is also discussed in the topic Technology Regions Overview.

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

我建议您研究一种跟踪渲染区域的方法,并可能在其前面创建一个 "hideous" 子项 window。 您可以做的其他研究是尝试 find/get WPF 图形缓冲区,并使用指针和一些高级内存编程将渲染场景直接注入其中。

您必须实现一个 class 来托管您的 Win32 Window m_hwnd。这个 class 继承自 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
}

请按照本教程进行操作:Walkthrough: Hosting a Win32 Control in WPF

我会根据 WPF 4.5 Unleashed 第 19 章尽力回答这个问题。如果您想查找它,可以在子部分 "Mixing DirectX Content with WPF Content" 中找到所有信息。

您的 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 内容的好方法。不过,您可以随意使用图片。

要初始化渲染,请获取当前 window 的句柄并使用它调用 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();
    }
}

基本上就是这样,希望对您有所帮助。现在只是一些重要的旁注:

  • 始终锁定您的图像,以避免 WPF 部分绘制帧
  • 不要在 Direct 3D 设备上调用 Present。 WPF 根据您传递给 d3dImage.SetBackBuffer() 的图面显示其自己的后台缓冲区。
  • 应该处理事件 IsFrontBufferAvailableChanged,因为有时前台缓冲区可能变得不可用(例如,当用户进入锁定屏幕时)。您应该根据缓冲区可用性释放或获取资源。

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

https://github.com/SonyWWS/ATF/ 可能有帮助

这是一个包含 direct2d 视图的关卡编辑器