5

As part of a project I'm working on I have to store and restore magic wand regions from an image. To obtain the data for storage, I'm utilizing the GetRegionData method. As the specification specifies, this method:

Returns a RegionData that represents the information that describes this Region.

I store the byte[] kept in the RegionData.Data property in a base64 string, so I can retrieve the RegionData later through a somewhat unconventional method:

// This "instantiates" a RegionData object by simply initiating an object and setting the object type pointer to the specified type.
// Constructors don't run, but in this very specific case - they don't have to. The RegionData's only member is a "data" byte array, which we set right after.
var regionData =
    (RegionData)FormatterServices.GetUninitializedObject(typeof(RegionData));
regionData.Data = bytes;

I then create a Region and pass the above RegionData object in the constructor and call GetRegionScans to obtain the rectangle objects which comprise the region:

var region = new Region(regionData);
RectangleF[] rectangles = region.GetRegionScans(new Matrix());

This way I end up with a collection of rectangles that I use to draw and reconstruct the region. I have isolated the entire drawing process to a WinForms application, and I'm using the following code to draw this collection of rectangles on an image control:

using (var g = Graphics.FromImage(picBox.Image))
{
    var p = new Pen(Color.Black, 1f);
    var alternatePen = new Pen(Color.BlueViolet, 1f);
    var b = new SolidBrush(picBox.BackColor);
    var niceBrush = new SolidBrush(Color.Orange);

    foreach (var r in rectangles)
    {
        g.DrawRectangle(p,
            new Rectangle(new Point((int)r.Location.X, (int)r.Location.Y),
                new Size((int)r.Width, (int)r.Height)));
    }
}

The above code results in the following being rendered in my picture control:

Rectangles rendered

The outline here is correct - that's exactly what I originally marked with my magic wand tool. However, as I'm drawing rectangles, I also end up with horizontal lines, which weren't a part of the original magic wand selection. Because of this, I can't view the actual image anymore, and my wand tool now makes the image useless.

I figured I'd only draw the left and right edges of each of the rectangles on the screen, so I'd end up with a bunch of points on the screen which outline the image for me. To do this, I tried the following code:

var pointPair = new[]
{
    new Point((int) r.Left, (int) r.Y),
    new Point((int) r.Right, (int) r.Y)
};

g.DrawRectangle(p, pointPair[0].X, pointPair[0].Y, 1, 1);
g.DrawRectangle(p, pointPair[1].X, pointPair[1].Y, 1, 1);

And the result can be observed below:

Points Rendered

Though closer, it's still no cigar.

I'm looking for a way to connect the dots in order to create an outline of the rectangles in my region. Something that's trivial for humans to do, but I can not figure out for the life of me how to instruct a computer to do this for me.

I've already tried creating new points from each of the rectangles by calculating the most adjacent points on both the Left and Right points of each rectangle, and rendering those as individual rectangles, but to no avail.

Any help would be greatly appreciated, as I'm really at a loss here.

Thanks!

Solution

Thanks to Peter Duniho's answer, I managed to solve this problem. I'm including the SafeHandle wrapper classes and the code I've used to make this work below, for the sake of completeness.

Drawing code

The code below is what I've used to draw the region outline.

    private void DrawRegionOutline(Graphics graphics, Color color, Region region)
    {
        var regionHandle = new SafeRegionHandle(region, region.GetHrgn(graphics));
        var deviceContext = new SafeDeviceContextHandle(graphics);
        var brushHandle = new SafeBrushHandle(color);

        using (regionHandle)
        using (deviceContext)
        using (brushHandle)
            FrameRgn(deviceContext.DangerousGetHandle(), regionHandle.DangerousGetHandle(), brushHandle.DangerousGetHandle(), 1, 1);
    }

SafeHandleNative

Small wrapper around SafeHandleZeroOrMinusOneIsInvalid to ensure cleanup after we're done with the handles.

[HostProtection(MayLeakOnAbort = true)]
[SuppressUnmanagedCodeSecurity]
public abstract class SafeNativeHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    #region Platform Invoke

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    protected internal static extern bool CloseHandle(IntPtr hObject);

    #endregion

    /// <summary>
    /// Initializes a new instance of the <see cref="SafeNativeHandle"/> class.
    /// </summary>
    protected SafeNativeHandle() : base(true)
    {}

    /// <summary>
    /// Initializes a new instance of the <see cref="SafeNativeHandle"/> class.
    /// </summary>
    /// <param name="handle">The handle.</param>
    protected SafeNativeHandle(IntPtr handle)
        : base(true)
    {
        SetHandle(handle);
    }
}

I've created three other wrappers, one for the Region object, one for the Brush and one for the device context handles. These all inherit from SafeNativeHandle, but in order not to spam I'll only provide the one I've used for the region below. The other two wrappers are virtually identical, but use the respective Win32 API required to clean up their own resources.

public class SafeRegionHandle : SafeNativeHandle
{
    private readonly Region _region;

    /// <summary>
    /// Initializes a new instance of the <see cref="SafeRegionHandle" /> class.
    /// </summary>
    /// <param name="region">The region.</param>
    /// <param name="handle">The handle.</param>
    public SafeRegionHandle(Region region, IntPtr handle)
    {
        _region = region;
        base.handle = handle;
    }

    /// <summary>
    /// When overridden in a derived class, executes the code required to free the handle.
    /// </summary>
    /// <returns>
    /// true if the handle is released successfully; otherwise, in the event of a catastrophic failure, false. In this case, it generates a releaseHandleFailed MDA Managed Debugging Assistant.
    /// </returns>
    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
    protected override bool ReleaseHandle()
    {
        try
        {
            _region.ReleaseHrgn(handle);
        }
        catch
        {
            return false;
        }

        return true;
    }
}
aevitas
  • 3,753
  • 2
  • 28
  • 39
  • 1
    it's not clear that you want some WPF solution here? you tagged WPF but all your code is about Winforms. I don't understand what you want here. Note that something in WPF can be done much more easily than in Winforms, so don't assume they are the same (I mean don't think that once you know how to do it in winforms, you can also know how to do it in WPF). – King King Nov 10 '14 at 16:33
  • @KingKing You're right, I mistagged the question. The eventual solution would be implemented in a third party viewer from LeadTools, which is WinForms inside a WPF wrapper (it uses many of the `System.Drawing` components). Your point stands however, the question is mistagged and I'll correct that. – aevitas Nov 10 '14 at 19:10
  • I don't understand this question at all. You start with a `Region`. Presumably that was originally just fine. But then you have to serialize the data (which is fine). But when you deserialize it, getting a new `Region` instance that is identical to the `Region` object you started with, you no longer want to use it as a `Region`, but instead want to come up with some scheme to draw the data. Why? How is the serialization/deserialization relevant at all here? Don't you have the same drawing issue with the original `Region`? If not, why can't you just draw the new `Region` as you did the first? – Peter Duniho Nov 10 '14 at 19:30
  • @PeterDuniho Because the first Region isn't drawn by me, it's whatever is selected by a magic wand tool. Sorry if my explanation is a bit ambiguous. The problem is in recreating what the magic wand tool original does for me. – aevitas Nov 10 '14 at 19:43
  • So what does the serialization/deserialization have to do with anything here? – Peter Duniho Nov 10 '14 at 20:39

1 Answers1

2

I'm still not entirely sure I understand the question. However, it sounds to me as though you simply want to draw the given region by outlining it, rather than filling it.

Unfortunately, as far as I know the .NET API does not support this. However, the native Windows API does. Here is some code that should do what you want:

[DllImport("gdi32")]
static extern bool FrameRgn(System.IntPtr hDC, System.IntPtr hRgn, IntPtr hBrush, int nWidth, int nHeight);

[DllImport("gdi32")]
static extern IntPtr CreateSolidBrush(uint colorref);

[DllImport("gdi32.dll")]
static extern bool DeleteObject([In] IntPtr hObject);

[StructLayout(LayoutKind.Explicit)]
struct COLORREF
{
    [FieldOffset(0)]
    public uint colorref;
    [FieldOffset(0)]
    public byte red;
    [FieldOffset(1)]
    public byte green;
    [FieldOffset(2)]
    public byte blue;

    public COLORREF(Color color)
        : this()
    {
        red = color.R;
        green = color.G;
        blue = color.B;
    }
}

void DrawRegion(Graphics graphics, Color color, Region region)
{
    COLORREF colorref = new COLORREF(color);
    IntPtr hdc = IntPtr.Zero, hbrush = IntPtr.Zero, hrgn = IntPtr.Zero;

    try
    {
        hrgn = region.GetHrgn(graphics);
        hdc = graphics.GetHdc();
        hbrush = CreateSolidBrush(colorref.colorref);

        FrameRgn(hdc, hrgn, hbrush, 1, 1);
    }
    finally
    {
        if (hrgn != IntPtr.Zero)
        {
            region.ReleaseHrgn(hrgn);
        }

        if (hbrush != IntPtr.Zero)
        {
            DeleteObject(hbrush);
        }

        if (hdc != IntPtr.Zero)
        {
            graphics.ReleaseHdc(hdc);
        }
    }
}

Call the DrawRegion() method from your Paint event handler or other appropriate context where you have a Graphics instance, such as drawing into an Image object as in your example.

Obviously you could make this an extension method for more convenience. Also, while in this example I am dealing with the initialization and releasing of the handles directly, a better implementation would wrap the handles in appropriate SafeHandle subclasses, so that you can conveniently use using instead of try/finally, and to get the backup of finalization (in case you forget to dispose).

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
  • This is spot on, thanks a lot. I'll edit my question with the `SafeHandle` wrappers I've used and the code I've ultimately used to draw the region outline. Thanks for your answer! – aevitas Nov 11 '14 at 09:45