How to draw a shape using GraphicsPath to create the Region of a Custom Control?

A few pointers and an example on the use of Regions to define the visible area of a Custom Control that represents a non rectangular shape.

  • You’re converting most floating point values to integer values: don’t do that when drawing, unless you have no other immediate choice. Drawing requires floating point measures (float) most of the time. Always to produce correct calculations.
    For example: Rotate Point around pivot Point repeatedly, to see the difference.

  • You’re using what appear to be fixed measures of some type, not defined in the OP and apparently never modified: DEFAULT_WIDTH and DEFAULT_HEIGHT. Since Controls can be resized at any time, both at Design-Time and Run-Time, using fixed measures is not really useful (assuming this is what these values represent). In any case, you need to use the current client area of your Control as the main reference: this value is returned by the Control.ClientRectangle property.

  • The Region of a Control is not set in the OnPaint() override, but either in the OnResize() or OnLayout() override, depending on the functionality of the Control you’re building.
    Setting properties, as FlatStyle = FlatStyle.Flat; that you have there (are you deriving your Control from a Label?), also does not belong in the drawing procedures: you may generate cascading events, causing the Control to constantly repaint itself (until it crashes, that is).

  • Using a GraphicsPath, the Pen.Alignment property is not exactly useful. See also the Remarks section in the documentation.


When you set the Region of a Control, to modify its shape, you need to consider that the Region doesn’t support anti-aliasing, hence you cannot draw along the borders it creates. You need to deflate the drawing area, so you always draw inside the Region you have defined. Or, you can create a completely transparent / translucent Control and draw whatever you need (shapes and/or Bitmaps) inside the transparent area. But this is a different matter, let’s stick to the Region and the GraphicsPath that creates it.

In the sample code, the GetRegionPath() method generates a GraphicsPath object, created passing PointF coordinates that defines a shape, then building the shape using the AddLines() method.

The Custom Control show here, uses SetStyle() to set in its Constructor:

SetStyle(ControlStyles.OptimizedDoubleBuffer | 
        ControlStyles.AllPaintingInWmPaint | 
        ControlStyles.ResizeRedraw, true);

This enables double-buffering and causes the Control to repaint when it’s resized.
The Region is then reset in the OnResize() override.

In OnPaint(), the GetRegionPath() method is called again to get a GraphicsPath object, based on the current client area.
It then scales the GraphicsPath using a simple Matrix:
(see a description of the Matrix functionality in Flip the GraphicsPath that draws the text/string)

float scaleX = 1.0f - ((border * 2.5f) / rect.Width);
float scaleY = 1.0f - ((border * 2.0f) / rect.Height);
var mx = new Matrix(scaleX, 0, 0, scaleY, border, border);
[GraphicsPath].Transform(mx);

This scales the GraphicsPath, based on the size of the Border (values above 1.0f scale up, values below it scale down).
It’s then moved (translated) right and down by the measure of the Border.
If the Border is not set, the GraphicsPath is not scaled or moved: e.g.

 1.0f + (((border * 2.5f) / rect.Width)) = 1.0f + 0.0f

This allows to draw the shape and its Border, if any, all inside the Region.
In this case, anti-aliasing can be applied and the borders of the shape appear smooth.

This is how it looks like, at Design-Time:

Region Control Design-time

And at Run-Time:

Region Control Run-time


See also:
How to avoid visual artifacts of colored border of zoomable UserControl with rounded corners?
How can I draw a rounded rectangle as the border for a rounded Form?
A shadow created with PathGradientBrush shows undesired thorn results


Custom Control:

using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;

[ToolboxItem(true)]
[DesignerCategory("code")]
public class NavigationShape : Control
{
    private Color m_ArrowColor = Color.SteelBlue;
    private Color m_BorderColor = Color.Orange;
    private float m_BorderSize = 1.5f;
    private bool m_BorderVisible = true;

    public NavigationShape() {
        SetStyle(ControlStyles.OptimizedDoubleBuffer | 
                 ControlStyles.AllPaintingInWmPaint | 
                 ControlStyles.ResizeRedraw, true);
        MinimumSize = new Size(40, 20);
    }

    protected override void OnResize(EventArgs e)
    {
        base.OnResize(e);
        Region = new Region(GetRegionPath());
    }

    private GraphicsPath GetRegionPath()
    {
        // The arrow shape begins at 3/4 or the current width of the container
        float arrowSection = ClientSize.Width * .75f;
        PointF[] arrowPoints = new PointF[] {
            new PointF (0, 0), 
            new PointF (arrowSection, 0),
            new PointF(ClientSize.Width, ClientSize.Height / 2.0f),
            new PointF (arrowSection, ClientSize.Height),
            new PointF (0, ClientSize.Height),
            new PointF (0, 0)
        };
        var path = new GraphicsPath();
        path.AddLines(arrowPoints);
        path.CloseFigure();
        return path;
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
        float border = m_BorderVisible ? m_BorderSize : .5f;

        using (var path = GetRegionPath()) {
            var rect = path.GetBounds();
            float scaleX = 1.0f - ((border * 2.5f) / rect.Width);
            float scaleY = 1.0f - ((border * 2.0f) / rect.Height);

            using (var mx = new Matrix(scaleX, 0, 0, scaleY, border, border))
            using (var brush = new SolidBrush(m_ArrowColor)) {
                path.Transform(mx);
                e.Graphics.FillPath(brush, path);
                if (m_BorderVisible) {
                    using (Pen pen = new Pen(m_BorderColor, m_BorderSize)) {
                        e.Graphics.DrawPath(pen, path);
                    }
                }
            }
        }
        base.OnPaint(e);
    }


    [DefaultValue(typeof(Color), "SteelBlue")]
    [Description("Color of the shape")]
    public Color ArrowColor {
        get => m_ArrowColor;
        set {
            if (m_ArrowColor != value) {
                m_ArrowColor = value;
                Invalidate();
            }
        }
    }

    [DefaultValue(true)]
    [Description("Show or hide the Border")]
    public bool BorderVisible {
        get => m_BorderVisible;
        set {
            m_BorderVisible = value;
            Invalidate();
        }
    }

    [DefaultValue(typeof(Color), "Orange")]
    [Description("Color of the Border")]
    public Color BorderColor {
        get => m_BorderColor;
        set {
            if (m_BorderColor != value) {
                m_BorderColor = value;
                Invalidate();
            }
        }
    }

    [DefaultValue(1.5f)]
    [Description("Size of the Border [1.0, 6.0]")]
    public float BorderSize {
        get => m_BorderSize;
        set {
            if (m_BorderSize != value) {
                m_BorderSize = Math.Max(Math.Min(value, 6.0f), 1.0f);
                Invalidate();
            }
        }
    }

    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public BorderStyle BorderStyle{ get; set; }  // Implement if needed
}

Leave a Comment