How to avoid visual artifacts of colored border of zoomable UserControl with rounded corners?
Asked Answered
A

1

4

I have a Form which contains:

  1. a TrackBar (minimum = 1, maximum = 200, represents zoom percent);
  2. a UserControl with BorderStyle = BorderStyle.None.

Relevant code

Form1

From designer code

trackBar1.Value = 100;
BackColor = Color.Gray;

From code-behind

private void trackBar1_Scroll(object sender, EventArgs e)
{
    userControl11.SetZoomFactor(trackBar1.Value / 100F);
}

UserControl1

internal float MyBaseWidth;

public UserControl1()
{
    InitializeComponent();

    MyBaseWidth = Width;

    SetZoomFactor(1);
}

protected override void OnPaint(PaintEventArgs e)
{
    base.OnPaint(e);

    e.Graphics.SmoothingMode = SmoothingMode.HighQuality;
    e.Graphics.CompositingQuality = CompositingQuality.HighQuality;
    e.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;

    Pen p = new Pen(Color.Yellow);
    e.Graphics.DrawPath(p, GraphicsPathWithBorder);
}

internal GraphicsPath GraphicsPathWithBorder;

internal void SetZoomFactor(float z)
{
    Width = (int)(MyBaseWidth * z);

    GraphicsPathWithBorder = RoundedCornerRectangle(ClientRectangle);
    Region = new Region(GraphicsPathWithBorder);
}

internal static GraphicsPath RoundedCornerRectangle(Rectangle r)
{
    GraphicsPath path = new GraphicsPath();
    float size = 10 * 2F;

    path.StartFigure();

    path.AddArc(r.X, r.Y,
        size, size, 180, 90);
    path.AddArc((r.X + (r.Width - size)), r.Y,
        size, size, 270, 90);
    path.AddArc((r.X + (r.Width - size)), (r.Y + (r.Height - size)),
        size, size, 0, 90);
    path.AddArc(r.X, (r.Y + (r.Height - size)),
        size, size, 90, 90);

    path.CloseFigure();

    return path;
}

Initial screenshot

initially

Screenshot after using the trackbar

after zoom

The right side of the yellow border becomes invisible after zooming out, and when zooming in there are multiple yellow borders on the right side.

Update:

The answer Works, but there is a part of the control that goes beyond the border. Screenshot for top-right corner, for curveSize = 20:

curve 1

and for curveSize = 24:

curve 2

Abomination answered 20/2, 2019 at 13:40 Comment(4)
You need to invalidate the control after changing the size, manually by overriding OnSizeChanged and calling Invalidate or automatically by setting ResizeRedraw=true.Hylotheism
Are you looking for something like this?Hylotheism
@RezaAghaei ResizeRedraw=true Works but the border is not antialiased.Abomination
It's a different story. In fact for smooth edges you should forget about changing the region. Instead you need to create a control which supports transparent BackColor.Hylotheism
H
7

I suggest a slightly different method to draw the Border and the content of the User Control that should also cure the artifacts generated when the control is redrawn.

When you create a Region for a Control and then you paint the Region as it is, the outer borders of the painting are not anti-aliased: the aliased pixels fall outside the Region. The same effect of course is applied when a border is painted around the bounds of the Region.

Here, I apply a Scale Matrix and a Translate Matrix that scale and move the bounds of the Region on the inside of the outer Region that defines the control's bounds.
The size of the scale and the translate transformations are determined by the Pen size.
More information on the Matrix usage here: Flip the GraphicsPath

In this case, when the borders are painted, the outer, anti-aliased, section of the border is inside the Region bounds and the anti-aliasing is preserved.
The background color of the Control is set to Color.Transparent (a User Control supports color transparency on its own).

I've also added a couple of (non decorated) properties that allow to define the inner Color (the Control's BackColor) and Size and Color of the Border. The rest is more or less what it was before.

Sample results:

Rounded Zoomable UserControl


using System.Drawing;
using System.Drawing.Drawing2D;

public partial class RoundControl : UserControl
{
    private GraphicsPath GraphicsPathWithBorder;
    private float MyBaseWidth;
    private float m_PenSize = 2f;
    private Color m_BorderColor = Color.Yellow;
    private Color m_FillColor = Color.Green;

    public RoundControl()
    {
        ResizeRedraw = true;
        InitializeComponent();
        MyBaseWidth = Width;
    }

    public float BorderSize
    {
        get => m_PenSize;
        set {
            m_PenSize = value;
            Invalidate();
        }
    }

    public Color BorderColor
    {
        get => m_BorderColor;
        set {
            m_BorderColor = value;
            Invalidate();
        }
    }

    public Color FillColor
    {
        get => m_FillColor;
        set {
            m_FillColor = value;
            Invalidate();
        }
    }

    protected override void OnLayout(LayoutEventArgs e) {
        UpdateRegion();
        base.OnLayout(e);
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
        RectangleF rect = GraphicsPathWithBorder.GetBounds();
        float scaleX = 1 - ((m_PenSize + 1) / rect.Width);
        float scaleY = 1 - ((m_PenSize + 1) / rect.Height);
        using (Pen pen = new Pen(m_BorderColor, m_PenSize))
        using (Brush brush = new SolidBrush(m_FillColor))
        using (Matrix mx = new Matrix(scaleX, 0, 0, scaleY, pen.Width / 2, pen.Width / 2))
        {
            e.Graphics.Transform = mx;
            e.Graphics.FillPath(brush, GraphicsPathWithBorder);
            e.Graphics.DrawPath(pen, GraphicsPathWithBorder);
        }
        base.OnPaint(e);
    }

    internal void SetZoomFactor(float z) {
        int newWidth = (int)(MyBaseWidth * z);
        if (newWidth <= (30 + m_PenSize * 2)) return;
        Width = newWidth;
        UpdateRegion();
    }


    private void UpdateRegion() {
        GraphicsPathWithBorder = RoundedCornerRectangle(ClientRectangle);
        Region = new Region(GraphicsPathWithBorder);
        Invalidate();
    }

    private GraphicsPath RoundedCornerRectangle(Rectangle r)
    {
        GraphicsPath path = new GraphicsPath();
        // Fixed curve size since we only scale on X-dimension
        // Otherwise, adjust also considering the height
        float curveSize = 10 * 2.4F;

        path.StartFigure();
        path.AddArc(r.X, r.Y, curveSize, curveSize, 180, 90);
        path.AddArc(r.Right - curveSize, r.Y, curveSize, curveSize, 270, 90);
        path.AddArc(r.Right - curveSize, r.Bottom - curveSize, curveSize, curveSize, 0, 90);
        path.AddArc(r.X, r.Bottom - curveSize, curveSize, curveSize, 90, 90);
        path.CloseFigure();
        return path;
    }
}
Haste answered 20/2, 2019 at 19:41 Comment(1)
Have you set the BackColor = Color.Transparent as in the description? When coding for production, you should override/eliminate all those properties that a User must not tamper with. If it's for yourself, you can keep the property visible and set it in the designer. Or use the OnPaint method and Paint the Background yourself. Or override OnPaintBackground. Or use SetStyle(ControlStyles.Opaque, true);, You'll have no background so, again, you can paint it yourself to whatever you like.Haste

© 2022 - 2024 — McMap. All rights reserved.