0

I made a Custom Control with the functions of a Button.
The problem with this controls is that when you hover over it, the color of the button changes: when this action is repeated quickly, it glitches out.
To make it more clear, I recorded a video.

Update 09.07.2020; I updated the code, perhaps this is because I do not add the image correctly, although judging by the appearance, the original, everything seems to be correct, the image works, but what happens next, you have already seen in the video.

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

namespace Test_Project.SupportClass
{
    [DesignerCategory("Code")]
    public class button_check : Control
    {
        private int m_BorderSize = 2;
        private int m_ButtonRoundRadius = 15;

        private bool IsHighlighted = false;
        private bool IsPressed = false;

        private Image _image;
        private ImageLayout LautsCallBack { get; set; }

        public button_check()
        {
            SetStyle(ControlStyles.Opaque |
                     ControlStyles.AllPaintingInWmPaint |
                     ControlStyles.ResizeRedraw |
                     ControlStyles.UserPaint, true);
            // To be explicit...
            SetStyle(ControlStyles.OptimizedDoubleBuffer, false);
            this.DoubleBuffered = false;
            InitializeComponent();
        }

        private void InitializeComponent()
        {
            Size = new Size(100, 40);
            BackColor = Color.Tomato;
            BackColor2 = Color.Tomato;
            ButtonBorderColor = Color.Black;
            ButtonHighlightColor = Color.Orange;
            ButtonHighlightColor2 = Color.OrangeRed;
            ButtonHighlightForeColor = Color.Black;

            ButtonPressedColor = Color.Red;
            ButtonPressedColor2 = Color.Maroon;
            ButtonPressedForeColor = Color.White;
        }
        public ImageLayout LayoutImage
        {
            get
            {
                return LautsCallBack;
            }
            set
            {
                LautsCallBack = value;
                RecreateHandle();
            }
        }
        public Image ImageButtom
        {
            get
            {
                return _image;
            }
            set
            {
                _image = value;
                RecreateHandle();
            }
        }
        protected override CreateParams CreateParams
        {
            get
            {
                CreateParams cp = base.CreateParams;
                cp.ExStyle |= 0x00000020; // WS_EX_TRANSPARENT
                return cp;
            }
        }

        // Invalidate(rect) in Design-Mode to refresh the view
        public int BorderSize
        {
            get => m_BorderSize;
            set
            {
                m_BorderSize = Math.Max(Math.Min(value, 6), 1);
                RepaintControl();
            }
        }

        // Set Max = 44, Min = 1 to avoid quirks and exceptions
        public int ButtonRoundRadius
        {
            get => m_ButtonRoundRadius;
            set
            {
                m_ButtonRoundRadius = Math.Max(Math.Min(value, 44), 1);
                RepaintControl();
            }
        }

        public override string Text
        {
            get => base.Text;
            set
            {
                base.Text = value;
                RepaintControl();
            }
        }

        // You should Invalidate the Parent also when these change
        public Color BorderColor { get; set; } = Color.Tomato;
        public Color BackColor2 { get; set; } = Color.Tomato;

        public Color ButtonBorderColor { get; set; }
        public Color ButtonHighlightColor { get; set; }
        public Color ButtonHighlightColor2 { get; set; }
        public Color ButtonHighlightForeColor { get; set; }
        public Color ButtonPressedColor { get; set; }
        public Color ButtonPressedColor2 { get; set; }
        public Color ButtonPressedForeColor { get; set; }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
            e.Graphics.CompositingQuality = CompositingQuality.HighQuality;

            var foreColor = IsPressed ? ButtonPressedForeColor : IsHighlighted ? ButtonHighlightForeColor : ForeColor;
            var backColor = IsPressed ? ButtonPressedColor : IsHighlighted ? ButtonHighlightColor : BackColor;
            var backColor2 = IsPressed ? ButtonPressedColor2 : IsHighlighted ? ButtonHighlightColor2 : BackColor2;
            var rect = Path.GetBounds();
            using (var pen = new Pen(ButtonBorderColor, m_BorderSize))
            using (var pathBrush = new LinearGradientBrush(rect, backColor, backColor2, LinearGradientMode.Vertical))
            using (var textBrush = new SolidBrush(foreColor))
            using (var sf = new StringFormat())
            {
                sf.Alignment = StringAlignment.Center;
                sf.LineAlignment = StringAlignment.Center;

                e.Graphics.FillPath(pathBrush, Path);
                if (m_BorderSize > 0) e.Graphics.DrawPath(pen, Path);

                if (_image != null)
                {
                    switch (LayoutImage)
                    {
                        case ImageLayout.Stretch:
                            e.Graphics.DrawImage(_image, this.ClientRectangle);
                            break;
                        case ImageLayout.Center:
                            int left = (this.ClientSize.Width - _image.Width) / 2;
                            int top = (this.ClientSize.Height - _image.Height) / 2;
                            e.Graphics.DrawImage(_image, left, top);
                            break;
                        case ImageLayout.Tile:
                            using (var texture = new TextureBrush(_image))
                            {
                                e.Graphics.FillRectangle(texture, this.ClientRectangle);
                            }
                            break;
                        case ImageLayout.Zoom:
                            double xr = (double)this.ClientSize.Width / _image.Width;
                            double yr = (double)this.ClientSize.Height / _image.Height;
                            if (xr > yr)
                            {
                                rect.Width = (int)(_image.Width * yr);
                                rect.X = (this.ClientSize.Width - rect.Width) / 2;
                            }
                            else
                            {
                                rect.Height = (int)(_image.Height * xr);
                                rect.Y = (this.ClientSize.Height - rect.Height) / 2;
                            }
                            e.Graphics.DrawImage(_image, rect);
                            break;
                    }
                }


                rect.Inflate(-4, -4);
                e.Graphics.DrawString(Text, Font, textBrush, rect, sf);
            }
        }

        protected override void OnMouseEnter(EventArgs e)
        {
            base.OnMouseEnter(e);
            IsHighlighted = true;
            RepaintControl();
        }

        protected override void OnMouseLeave(EventArgs e)
        {
            base.OnMouseLeave(e);
            IsHighlighted = false;
            IsPressed = false;
            RepaintControl();
        }

        protected override void OnMouseDown(MouseEventArgs e)
        {
            base.OnMouseDown(e);
            IsPressed = true;
            RepaintControl();
        }

        protected override void OnMouseUp(MouseEventArgs e)
        {
            base.OnMouseUp(e);
            IsPressed = false;
            RepaintControl();
        }

        private void RepaintControl()
        {
            Parent?.Invalidate(this.Bounds, true);
            Invalidate();
        }

        private GraphicsPath Path
        {
            get
            {
                var rect = ClientRectangle;
                int scaleOnBorder = -((m_BorderSize / 2) + 2);
                rect.Inflate(scaleOnBorder, scaleOnBorder);
                return GetRoundedRectangle(rect, m_ButtonRoundRadius);
            }
        }

        private GraphicsPath GetRoundedRectangle(Rectangle rect, int d)
        {
            var gp = new GraphicsPath();
            gp.StartFigure();
            gp.AddArc(rect.X, rect.Y, d, d, 180, 90);
            gp.AddArc(rect.X + rect.Width - d, rect.Y, d, d, 270, 90);
            gp.AddArc(rect.X + rect.Width - d, rect.Y + rect.Height - d, d, d, 0, 90);
            gp.AddArc(rect.X, rect.Y + rect.Height - d, d, d, 90, 90);
            gp.CloseFigure();
            return gp;
        }
    }
}
  • For a slightly different drawing method, which usually provides *stable* anti-aliasing of the external Borders, see: [How to avoid visual artifacts of colored border of zoomable UserControl with rounded corners?](https://stackoverflow.com/a/54794097/7444103) – Jimi Jul 06 '20 at 14:33
  • @Jimi Something I didn't understand, I changed `Parent.Invalidate(Bounds, true);` and nothing happened.You can use an example of my code to show how it will look in the final result I would be very grateful to you – Сергей Маерович Jul 06 '20 at 15:42
  • That change is only needed to prevent the Control from being invalidated more than necessary. In the Designer, it's required, otherwise the custom control won't be repainted when you change something. At run-time, it's redundant and will cause flickering. You need all those changes. I see whether I have the time to post a modified version. -- You need to add a written description of what the expected results are and what you actually get. A **written description**, nobody should need to see a Video to understand what this is all about, assuming a Video can actually describe what's wrong. – Jimi Jul 06 '20 at 15:52
  • @Jimi well, after you publish the new version, I will describe exactly what has changed and what has not – Сергей Маерович Jul 06 '20 at 17:55
  • @Jimi Can you help me get rid of these highlights in the button? – Сергей Маерович Jul 07 '20 at 09:38

1 Answers1

1

All right, try your Button with these modifications:

► SetStyle(ControlStyles.Opaque, true) is set. Combined with WS_EX_TRANSPARENT, you have a fully transparent Control that supports TransparentColor. No need to also set ControlStyles.SupportsTransparentBackColor, it's understood.

► Double buffering is explicitly disable, since we're painting the Control ourselves when required (ControlStyles.AllPaintingInWmPaint, ControlStyles.UserPaint and ControlStyles.ResizeRedraw are all set).

► Moved the invalidating functions to the RepaintControl() method, including invalidating the parent Control, if available, when in design-mode. At run-time, Invalidate() is enough.

► Recalculated the GraphicsPath bounds when the BorderSize is changed, so the border is always painted inside the Control's bounds (to preserve anti-aliasing, which requires at least one pixel to render correctly. When too close to the Control's bounds, the rendering is incomplete).

base.OnPaint(e) is still called, but you only need it if you want to allow User customization of the Control. Otherwise, you can remove it (or place it after your code, at the bottom of the OnPaint method).

► Added, as example, Min/Max checkes on some properties:

  • The BorderSize is limited in the range (0, 6) with Math.Max(Math.Min(value, 6), 0)
  • The ButtonRoundRadius is limited to range (1, 44) with Math.Max(Math.Min(value, 44), 1): the GraphicsPath cannot add arcs with a null angle and, above 45, it'll generate re-entrant curves.

I don't see any flickering at run-time.
Note that this is tested with .Net Framework 4.8


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

[DesignerCategory("Code")]
public class l1_Button : Control
{
    private int m_BorderSize = 2;
    private int m_ButtonRoundRadius = 15; 

    private bool IsHighlighted = false;
    private bool IsPressed = false;

    public l1_Button()
    {
        SetStyle(ControlStyles.Opaque | 
                 ControlStyles.AllPaintingInWmPaint | 
                 ControlStyles.ResizeRedraw | 
                 ControlStyles.UserPaint, true);
        // To be explicit...
        SetStyle(ControlStyles.OptimizedDoubleBuffer, false);
        this.DoubleBuffered = false;
        InitializeComponent();
    }

    private void InitializeComponent()
    {
        Size = new Size(100, 40);
        BackColor = Color.Tomato;
        BackColor2 = Color.Tomato;
        ButtonBorderColor = Color.Black;
        ButtonHighlightColor = Color.Orange;
        ButtonHighlightColor2 = Color.OrangeRed;
        ButtonHighlightForeColor = Color.Black;

        ButtonPressedColor = Color.Red;
        ButtonPressedColor2 = Color.Maroon;
        ButtonPressedForeColor = Color.White;
    }

    protected override CreateParams CreateParams {
        get {
            CreateParams cp = base.CreateParams;
            cp.ExStyle |= 0x00000020; // WS_EX_TRANSPARENT
            return cp;
        }
    }

    // Invalidate(rect) in Design-Mode to refresh the view
    public int BorderSize {
        get => m_BorderSize;
        set {
            m_BorderSize = Math.Max(Math.Min(value, 6), 1);
            RepaintControl();
        }
    }

    // Set Max = 44, Min = 1 to avoid quirks and exceptions
    public int ButtonRoundRadius {
        get => m_ButtonRoundRadius;
        set {
            m_ButtonRoundRadius = Math.Max(Math.Min(value, 44), 1);
            RepaintControl();
        }
    }

    public override string Text {
        get => base.Text;
        set {
            base.Text = value;
            RepaintControl();
        }
    }

    // You should Invalidate the Parent also when these change
    public Color BorderColor { get; set; } = Color.Tomato;
    public Color BackColor2 { get; set; } = Color.Tomato;

    public Color ButtonBorderColor { get; set; }
    public Color ButtonHighlightColor { get; set; }
    public Color ButtonHighlightColor2 { get; set; }
    public Color ButtonHighlightForeColor { get; set; }
    public Color ButtonPressedColor { get; set; }
    public Color ButtonPressedColor2 { get; set; }
    public Color ButtonPressedForeColor { get; set; }

    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);
        e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
        e.Graphics.CompositingQuality = CompositingQuality.HighQuality;

        var foreColor = IsPressed ? ButtonPressedForeColor : IsHighlighted ? ButtonHighlightForeColor : ForeColor;
        var backColor = IsPressed ? ButtonPressedColor : IsHighlighted ? ButtonHighlightColor : BackColor;
        var backColor2 = IsPressed ? ButtonPressedColor2 : IsHighlighted ? ButtonHighlightColor2 : BackColor2;
        var rect = Path.GetBounds();

        using (var pen = new Pen(ButtonBorderColor, m_BorderSize))
        using (var pathBrush = new LinearGradientBrush(rect, backColor, backColor2, LinearGradientMode.Vertical))
        using (var textBrush = new SolidBrush(foreColor))
        using (var sf = new StringFormat()) {
            sf.Alignment = StringAlignment.Center;
            sf.LineAlignment = StringAlignment.Center;

            e.Graphics.FillPath(pathBrush, Path);
            if (m_BorderSize > 0) e.Graphics.DrawPath(pen, Path);

            rect.Inflate(-4, -4);
            e.Graphics.DrawString(Text, Font, textBrush, rect, sf);
        }
    }

    protected override void OnMouseEnter(EventArgs e)
    {
        base.OnMouseEnter(e);
        IsHighlighted = true;
        RepaintControl();
    }

    protected override void OnMouseLeave(EventArgs e)
    {
        base.OnMouseLeave(e);
        IsHighlighted = false;
        IsPressed = false;
        RepaintControl();
    }

    protected override void OnMouseDown(MouseEventArgs e)
    {
        base.OnMouseDown(e);
        IsPressed = true;
        RepaintControl();
    }

    protected override void OnMouseUp(MouseEventArgs e)
    {
        base.OnMouseUp(e);
        IsPressed = false;
        RepaintControl();
    }

    private void RepaintControl() {
        Parent?.Invalidate(this.Bounds, true);
        Invalidate();
    }

    private GraphicsPath Path {
        get {
            var rect = ClientRectangle;
            int scaleOnBorder = -((m_BorderSize / 2) + 2);
            rect.Inflate(scaleOnBorder, scaleOnBorder);
            return GetRoundedRectangle(rect, m_ButtonRoundRadius);
        }
    }

    private GraphicsPath GetRoundedRectangle(Rectangle rect, int d)
    {
        var gp = new GraphicsPath();
        gp.StartFigure();
            gp.AddArc(rect.X, rect.Y, d, d, 180, 90);
            gp.AddArc(rect.X + rect.Width - d, rect.Y, d, d, 270, 90);
            gp.AddArc(rect.X + rect.Width - d, rect.Y + rect.Height - d, d, d, 0, 90);
            gp.AddArc(rect.X, rect.Y + rect.Height - d, d, d, 90, 90);
        gp.CloseFigure();
        return gp;
    }
}
Jimi
  • 29,621
  • 8
  • 43
  • 61
  • It works fine, no flickering was noticed, only that it broke, I click on the button, the default color is red, and when you click on the button, its color does not change... Thank you for doing such a great job – Сергей Маерович Jul 07 '20 at 17:51
  • @Сергей Маерович It's not broken: in `OnMouseDown`, the call to `RepaintControl();` is missing (forgot to add it back). Now corrected. – Jimi Jul 07 '20 at 18:02
  • If you put m_BorderSize 0 and many times to restore order, and take the mouse from our button, it is the result zabaletta, and if you choose figuratively speaking primary color 78; 184; 206, but all other colors remain unchanged with the exception of ButtonBorderColor, he set the Transparent color, if a lot of time to drive the mouse back and forth, you will see that the backside is formed orange in color, and if you add on the button the picture is even more clearly seen if even pushing the button @Jimi – Сергей Маерович Jul 07 '20 at 19:07
  • I don't really understand what you're saying here (what is *zebaletta*? And *primary color*? Which one is it?). Anyway, from what I can gather, try changing `using (var pen = new Pen(ButtonBorderColor, m_BorderSize))` in `using (var pen = new Pen(m_BorderSize == 0 ? Color.Transparent : ButtonBorderColor, m_BorderSize))`. But, did you use (all) this code, or you just changed something here and there? I don't have any problem with the Control's background. No matter where I add it to. The edit I propose here, can - eventually - eliminate an empty background when the border is set to `0`. – Jimi Jul 07 '20 at 20:19
  • I think it works just fine. BUT as soon as I try to make an image, everything immediately breaks down and stops working as it should @Jimi – Сергей Маерович Jul 09 '20 at 13:34
  • What does *make an image* mean and imply? This control doesn't support images. You didn't post any code that deals with Bitmaps, so you're asking me to debug code that I've never seen. – Jimi Jul 09 '20 at 14:42