How do I draw the outline of a collection of rectangles?

ⅰ亾dé卋堺 提交于 2020-01-02 06:38:44

问题


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:

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:

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;
    }
}

回答1:


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).



来源:https://stackoverflow.com/questions/26846813/how-do-i-draw-the-outline-of-a-collection-of-rectangles

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!