Search code examples
delphiclipboarddirect2d

How to copy a selection from Direct2DCanvas to clipboard?


I made a small program to draw geometry figures on a D2DBox (with Direct2DCanvas and RenderTarget) and I need to be able to copy a rectangle from it to the clipboard. Tried this, but I'm stuck with the source handle parameter:

bm := TBitmap.Create;
try
  bm.SetSize(maxx-minx, maxy-miny);
  BitBlt(bm.Canvas.Handle, minx, miny, maxx, maxy, ??? , 0, 0, SRCCOPY);
  Clipboard.Assign(bm);
finally
  bm.Free;
end;

Any idea where to get a handle from? Or the whole thing is done a different way? Thanx!


Solution

  • BitBlt() requires a GDI HDC to copy from, but TDirect2DCanvas does not have an HDC of its own, and it cannot directly render to an off-screen HDC/TCanvas, such as TBitmap.Canvas, per its documentation:

    TDirect2DCanvas will only work for on-screen device contexts. You cannot use the TDirect2DCanvas to draw on a printer device context, for example.

    And you can't associate a custom RenderTarget (such as one created with ID2D1Factory.CreateDCRenderTarget() and ID2D1DCRenderTarget.BindDC()) since the TDirect2DCanvas.RenderTarget property is read-only.

    So, you will likely have to go a long way to get what you want. Based on code I found in Direct2d Desktop printing C++, which demonstrates copying an arbitrary ID2D1RenderTarget to an arbitrary HDC, you can try the following:

    • Create a Direct2D ID2D1Bitmap that is bound to the canvas's current RenderTarget using one of the target's CreateBitmap() methods.

    • Copy pixels from the canvas into the bitmap using the bitmap's CopyFromRenderTarget() method.

    • Create an IWICBitmap (you can probably use the VCL's TWICImage for this) and render the Direct2D bitmap to it using one of the ID2D1Factory.CreateWicBitmapRenderTarget() methods with one of the ID2D1RenderTarget.DrawBitmap() methods.

    • Create a GDI DIB bitmap and render the IWICBitmap to it using the WIC bitmap's CopyPixels() method.

    • Finally, you use the DIB however you needed, such as select/copy it to your final HDC, or you can simply place its contents directly on the clipboard using the CF_DIB format.

    Here is the code (sorry, it is in C++, I'm not going to translate it to Delphi):

    void Direct2DRender::RenderToDC(HDC hDC, UINT uiWidth, UINT uiHeight)
    {
      HRESULT hr = S_OK;
    
      IWICImagingFactory *pImageFactory = WICImagingFactory::GetInstance().GetFactory();
    
      CComPtr<IWICBitmap> wicBitmap;
      hr = pImageFactory->CreateBitmap( uiWidth, uiHeight, GUID_WICPixelFormat32bppBGR, WICBitmapCacheOnLoad, &wicBitmap);
    
      D2D1_SIZE_U bitmapPixelSize = D2D1::SizeU( uiWidth, uiHeight);
    
      float dpiX, dpiY;
      m_pRenderTarget->GetDpi( &dpiX, &dpiY);
    
      CComPtr<ID2D1Bitmap> d2dBitmap;
      hr = m_pRenderTarget->CreateBitmap( bitmapPixelSize, D2D1::BitmapProperties(
        D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_IGNORE),
        dpiX, dpiY), &d2dBitmap );
    
      D2D1_POINT_2U dest = D2D1::Point2U(0,0);
      D2D1_RECT_U src = D2D1::RectU(0, 0, uiWidth, uiHeight);
    
      hr = d2dBitmap->CopyFromRenderTarget(&dest, m_pRenderTarget, &src);
    
      D2D1_RENDER_TARGET_PROPERTIES rtProps = D2D1::RenderTargetProperties();
        rtProps.pixelFormat = D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_IGNORE);
        rtProps.type = D2D1_RENDER_TARGET_TYPE_DEFAULT;
        rtProps.usage = D2D1_RENDER_TARGET_USAGE_NONE;
    
      CComPtr<ID2D1RenderTarget> wicRenderTarget;
      hr = m_pDirect2dFactory->CreateWicBitmapRenderTarget( wicBitmap, rtProps, &wicRenderTarget);
    
      wicRenderTarget->BeginDraw();
      wicRenderTarget->DrawBitmap(d2dBitmap);
      hr = wicRenderTarget->EndDraw();
    
      // Render the image to a GDI device context
      HBITMAP hDIBBitmap = NULL;
      try
      {
        // Get a DC for the full screen
        HDC hdcScreen = GetDC(NULL);
        if (!hdcScreen)
          throw 1;
    
        BITMAPINFO bminfo;
        ZeroMemory(&bminfo, sizeof(bminfo));
        bminfo.bmiHeader.biSize         = sizeof(BITMAPINFOHEADER);
        bminfo.bmiHeader.biWidth        = uiWidth;
        bminfo.bmiHeader.biHeight       = -(LONG)uiHeight;
        bminfo.bmiHeader.biPlanes       = 1;
        bminfo.bmiHeader.biBitCount     = 32;
        bminfo.bmiHeader.biCompression  = BI_RGB;     
    
        void* pvImageBits = nullptr;  // Freed with DeleteObject(hDIBBitmap)
        hDIBBitmap = CreateDIBSection(hdcScreen, &bminfo, DIB_RGB_COLORS, &pvImageBits, NULL, 0);
        if (!hDIBBitmap)
          throw 2;
    
        ReleaseDC(NULL, hdcScreen);
    
        // Calculate the number of bytes in 1 scanline
        UINT nStride = DIB_WIDTHBYTES(uiWidth * 32);
        // Calculate the total size of the image
        UINT nImage = nStride * uiHeight;
        // Copy the pixels to the DIB section
        hr = wicBitmap->CopyPixels(nullptr, nStride, nImage, reinterpret_cast<BYTE*>(pvImageBits));
    
        // Copy the bitmap to the target device context
        ::SetDIBitsToDevice(hDC, 0, 0, uiWidth, uiHeight, 0, 0, 0, uiHeight, pvImageBits, &bminfo, DIB_RGB_COLORS);
    
        DeleteObject(hDIBBitmap);
      }
      catch (...)
      {
        if (hDIBBitmap)
          DeleteObject(hDIBBitmap);
        // Rethrow the exception, so the client code can handle it
        throw;
      }
    }
    

    In that same discussion, another alternative is described:

    • Create a DIB section, and select it into a DC

    • Create a DC render target

    • Bind the render target to the DC that corresponds to the DIB section

    • Draw using Direct2D. After calling EndDraw the DIB contains what was rendered.

    • The final step is to draw the dib where you need it.

    So, try moving your drawing code to its own function that takes an ID2D1RenderTarget as input and draws on it as needed. Then, you can create an HDC-based RenderTarget when you want to place a bitmap on the clipboard, and use TDirect2DCanvas.RenderTarget when you want to draw on your D2DBox.