Losing anti aliasing when sharing Graphics object between managed and unmanaged code
Asked Answered
H

2

6

Passing Graphics object between native C++ and C#

I'm currently working on a Paint .NET-like application. I have mulitple types of layers which are implemented in C#. These layers are drawn into a .NET Graphics object that is provided by a WinForms user control - it is similar to the WPF canvas control. The layer base class has a Draw method that is implemented as follows:

public void Draw(IntPtr hdc)
{
    using (var graphics = Graphics.FromInternalHDC(hdc)
    {
         // First: Setup rendering settings like SmoothingMode, TextRenderingHint, ...
         // Layer specific drawing code goes here...
    }
}

For performance and decompiling issues I'm doing the composition of the layers in a mixed mode assembly since I'm also applying effects like bevel or drop shadow. The wrapper, of course written in C++/CLI, gets directly called from the canvas control and forwards the metadata of each layer and the target Graphics object (Graphics object from my C# written canvas user control) to a native C++ class.

C++/CLI Wrapper:

public ref class RendererWrapper
{
   public:
        void Render(IEnumerable<Layer^>^ layersToDraw, Graphics^ targetGraphics)
        {
            // 1) For each layer get metadata (position, size AND Draw delegate)
            // 2) Send layer metadata to native renderer
            // 3) Call native renderer Render(targetGraphics.GetHDC()) method
            // 4) Release targetGraphics HDC
         };
}

Native C++ Renderer:

class NativeRenderer
{
  void NativeRenderer::Render(vector<LayerMetaData> metaDataVector, HDC targetGraphicsHDC)
  {
     Graphics graphics(targetGraphicsHDC);
     // Setup rendering settings (SmoothingMode, TextRenderingHint, ...)

     for each metaData in metaDataVector
     {
        // Create bitmap and graphics for current layer
        Bitmap* layerBitmap = new Bitmap(metaData.Width, metaData.Height, Format32bppArgb);
        Graphics* layerGraphics = new Graphics(layerBitmap);

        // Now the interesting interop part
        // Get HDC from layerGraphics
        HDC lgHDC = layerGraphics->GetHDC();

        // Call metaData.Delegate and pass the layerGraphics HDC to C#
        // By this call we are ending up in the Draw method of the C# Layer object
        metaData.layerDrawDelegate(lgHDC);     

        // Releasing HDC - leaving interop...
        layerGraphics->ReleaseHDC(lgHDC);

        // Apply bevel/shadow effects
        // Do some other fancy stuff

        graphics.DrawImage(layerBitmap, metaData.X, metaData.Y, metaData.Width, metaData.Height);        
     }
  }
}

So far so good. The above code works nearly as expected, but...

Problem

The only thing is that my current implementation is lacking of anti aliasing and semi transparency when rendering PNG with shadows for example. So I just have only 2 values for the Alpha channel: Transparent or full visible Color at 255. This side effect makes drawing PNGs with alpha channel and fonts looking very ugly. I cannot get the same smooth and nice semi transparent anti aliasing any more like before when I worked with pure C# code.

BUT: When drawing a string in the native Graphics object directly,

 layerGraphics->DrawString(...);

anti aliasing and semi transparency are back for good. So the problem is only evident when passing the Graphics HDC to .NET.

Questions

Is there any solution/workaround for this problem? I've tried to create the Bitmap directly in the C# Layer class and return the IntPtr for the HBITMAP to the native code. This approach is working, but in this case I have another problem since I cannot find a perfect solution for converting HBITMAP to GDI+ Bitmap with alpha channel (white pixel noise is surrounding the edges when drawing fonts).

Thanks for your input! :)

Demo Solution

Attached you'll find a demo solution here: Sources

In this demo solution I'm testing 3 different rendering methods (all implemented in NativeRenderer.cpp), while the FIRST ONE shows the described problems:

Demo output

1) RenderViaBitmapFromCSharp() - a) Creates a new bitmap in C++, creates a new Graphics object in C++, calls the C# drawing code by passing the C++ Graphics object HDC - Fails

But: b) Drawing directly from C++ works via the created bitmap too

2) RenderDirectlyFromCSharp() - Creates a new Graphics object from C# Graphics handle in C++ , calls the C# drawing code by passing the C++ Graphics object HDC - Works

3) RenderDirectlyFromCPP() - Creates a new Graphics object from C# Graphics handle in C++, draws the text directly in C++ - Works

Halla answered 24/5, 2012 at 8:4 Comment(4)
C++/CLI code looks a little bit...C# code! Anyway I do not see the code you use for writing the string in C++ so I just guess a little bit: if you're using native GDI functions (for metaData.layerDrawDelegate(lgHDC)) then the string will not be rendered with settings you had with GDI+ (= transparency, antialias and whatever else from GDI+ won't be applied to a GDI call).Toname
I've changed the pseudo code to be more C++/CLI. The method for writing the string is just copied and pasted snipped from the MSDN explanation of DrawString, nothing special. In the Draw method of the layer I'm setting all those settings like SmoothingMode or TextRenderHint to the same as on the native GDI+ object.Halla
GDI functions such as ExtTextOut don't support an alpha channel, and simply passing a DC with a selected bitmap with an alpha channel won't make them alpha aware.Compiler
I'm aware of that, but there are a few solutions on the web that keep the alpha channel when copying from HBITMAP to Bitmap.Halla
H
1

I've ended up creating the Bitmap in C# and passing the object to C++/CLI. As already mentioned by Hans and Vincent you have to avoid GetHDC. So my workaround reads in pseudo code as follows:

Layer.cs C#:

public Bitmap Draw()
{
     var bitmap = new Bitmap(Width, Height, PixelFormat.Format32bppArgb);
     using (var graphics = Graphics.FromBitmap(bitmap)
     {
          // First: Setup rendering settings like SmoothingMode, TextRenderingHint, ...
          // Layer specific drawing code goes here...
     }
     return bitmap;
}

NativeRenderer.cs C++:

void NativeRenderer::RenderFromBitmapCSharp(System::Drawing::Bitmap^ bitmap)
{
    // Create and lock empty native bitmap
    Bitmap *gdiBitmap = new Bitmap(bitmap->Width, bitmap->Height, PixelFormat32bppARGB);
    Rect rect(0, 0, bitmap->Width, bitmap->Height);
    BitmapData bitmapData;
    gdiBitmap->LockBits(&rect, Gdiplus::ImageLockModeRead | Gdiplus::ImageLockModeWrite, PixelFormat32bppARGB, &bitmapData);

    // Lock managed bitmap
    System::Drawing::Rectangle rectangle(0, 0, bitmap->Width, bitmap->Height);
    System::Drawing::Imaging::BitmapData^ pBitmapData = bitmap->LockBits(rectangle, System::Drawing::Imaging::ImageLockMode::ReadOnly, System::Drawing::Imaging::PixelFormat::Format32bppArgb);

    // Copy from managed to unmanaged bitmap
    ::memcpy(bitmapData.Scan0, pBitmapData->Scan0.ToPointer(), bitmap->Width * bitmap->Height * 4);
    bitmap->UnlockBits(pBitmapData);
    gdiBitmap->UnlockBits(&bitmapData);

    // Draw it
    _graphics->DrawImage(gdiBitmap, 0, 0, bitmap->Width, bitmap->Height);
}

Hope that is helpful to others - have not found any code snippet on the web which actually does converting managed to unmanaged GDI+ Bitmap.

Thank you all for your comments.

Cheers, P

Halla answered 25/5, 2012 at 6:35 Comment(0)
J
1
 Graphics graphics(targetGraphicsHDC);

You are creating a new Graphics object. So it won't have its properties setup like its original did. Properties like TextRenderingHint are not properties of a GDI device context, they are specific to Graphics.

Fairly ugly problem, you'll need to re-initialize the Graphics object the way it was done in the calling code. That's two chunks of code that are far removed from each other. Avoiding the conversion to HDC and back is the only really decent way to side-step the problem.

Joselow answered 24/5, 2012 at 12:20 Comment(5)
How can I avoid the conversion to HDC, since I can't work natively with the initial .NET Graphics object? Could you please give an example how to re-initialize the the Gdiplus Graphics in the .NET way? Thank you!Halla
Furthermore: Graphics graphics(targetGraphicsHDC) is indeed not a problem as my source code shows, the problem is the line Graphics layerGraphics(layerBitmap)...Halla
There's no simple solution. Rick Brewster of Paint.NET fame writes a lot of C++/CLI code. That's having your cake and eat it too, it can use the managed Graphics object and make native function calls when needed for speed.Joselow
Tested the issue once again and you are absolutely right about the additional HDC when creating new Graphics. I've discovered that same behavior in a .NET only solution. BUT: When doing it the other way round from .NET to native GDI+ the conversion via HDC works. Not sure about the next steps, but as soon as I come up with a solution I let you know.Halla
It's actually the GetHDC method that's causing this problem. GetHDC makes a new bitmap filled with a solid color and selects that into a new memory DC. ReleaseHDC will copy the pixels that changed in the bitmap to the Graphics object. It's not a way to access the original HDC used to create a Graphics object, and the process loses any partial alpha. But that doesn't change the solution: avoid GetHDC.Bojorquez
H
1

I've ended up creating the Bitmap in C# and passing the object to C++/CLI. As already mentioned by Hans and Vincent you have to avoid GetHDC. So my workaround reads in pseudo code as follows:

Layer.cs C#:

public Bitmap Draw()
{
     var bitmap = new Bitmap(Width, Height, PixelFormat.Format32bppArgb);
     using (var graphics = Graphics.FromBitmap(bitmap)
     {
          // First: Setup rendering settings like SmoothingMode, TextRenderingHint, ...
          // Layer specific drawing code goes here...
     }
     return bitmap;
}

NativeRenderer.cs C++:

void NativeRenderer::RenderFromBitmapCSharp(System::Drawing::Bitmap^ bitmap)
{
    // Create and lock empty native bitmap
    Bitmap *gdiBitmap = new Bitmap(bitmap->Width, bitmap->Height, PixelFormat32bppARGB);
    Rect rect(0, 0, bitmap->Width, bitmap->Height);
    BitmapData bitmapData;
    gdiBitmap->LockBits(&rect, Gdiplus::ImageLockModeRead | Gdiplus::ImageLockModeWrite, PixelFormat32bppARGB, &bitmapData);

    // Lock managed bitmap
    System::Drawing::Rectangle rectangle(0, 0, bitmap->Width, bitmap->Height);
    System::Drawing::Imaging::BitmapData^ pBitmapData = bitmap->LockBits(rectangle, System::Drawing::Imaging::ImageLockMode::ReadOnly, System::Drawing::Imaging::PixelFormat::Format32bppArgb);

    // Copy from managed to unmanaged bitmap
    ::memcpy(bitmapData.Scan0, pBitmapData->Scan0.ToPointer(), bitmap->Width * bitmap->Height * 4);
    bitmap->UnlockBits(pBitmapData);
    gdiBitmap->UnlockBits(&bitmapData);

    // Draw it
    _graphics->DrawImage(gdiBitmap, 0, 0, bitmap->Width, bitmap->Height);
}

Hope that is helpful to others - have not found any code snippet on the web which actually does converting managed to unmanaged GDI+ Bitmap.

Thank you all for your comments.

Cheers, P

Halla answered 25/5, 2012 at 6:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.