How to make a custom Button responsiveness smoother?

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;
        }
    }
}
c#
winforms
button
gdi+

1 Answer

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;
    }
}
answered on Stack Overflow Jul 7, 2020 by Jimi • edited Jul 8, 2020 by Jimi

User contributions licensed under CC BY-SA 3.0