Search code examples
c#.netwinformsevent-handling

How to handle mouse click or mouse down over ErrorProvider icon in WinForms


I would like to display an error provider icon over a button at certain times, like this:

enter image description here

However, I also don't want the icon to obscure/prevent the button click if the user happens to click on that when intending to click on the button, perhaps because the error message on hover says something like "click here to fix".

ErrorProvider has no events (well it has one irrelevant one). I tried using the methodology described in this post, but the problem is that no pre-filter message even occurs when I click on the error icon. Here's my sample code:

public partial class Form1 : Form, IMessageFilter
{
    public Form1()
    {
        InitializeComponent();
        errorProvider1.SetIconPadding(button1, -27);
        errorProvider1.SetError(button1, "error");
        Application.AddMessageFilter(this);
    }

    public bool PreFilterMessage(ref Message m)
    {
        var ctrl = Control.FromHandle(m.HWnd);
        var msg = m.Msg;

        // the values in the "ignore" list below are all movement/paint events
        if (ctrl is Button b && (!(new[] { 15, 512, 675, 280, 673 }.Any((v) => msg == v))))
        {
            // only gets here when un-obscured button surface is clicked.
        }

        // this only ever prints form and button
        if (ctrl != null) System.Diagnostics.Debug.Print($"{ctrl.GetType().Name}"); 

        return false;
    }
}

Is there any way to do this?


Solution

  • As you want to display the error icon inside the button, you can easily fake the ErrorProvider by drawing the icon manually and using a ToolTip explicitly:

    public partial class FakeErrorProvider : Form
    {
        private static Icon _errorIcon = new ErrorProvider().Icon;
        private static int _errorIconPadding = 4;
    
        private ToolTip _toolTip;
        private bool _hasError;
    
        public FakeErrorProvider()
        {
            InitializeComponent();
    
            _toolTip = new ToolTip();
            button1.Click += Button1_Click;
            button1.Paint += Button1_Paint;
            button1.MouseMove += Button1_MouseMove;
        }
    
        private void Button1_Click(object? sender, EventArgs e)
        {
            // fixing the error only when the error icon is clicked
            if (!_hasError || GetErrorRectangle(button1).Contains(button1.PointToClient(Cursor.Position)))
                _hasError = !_hasError;
            button1.Invalidate();
            if (!_hasError)
                _toolTip.SetToolTip(button1, String.Empty);
        }
    
        private void Button1_Paint(object? sender, PaintEventArgs e)
        {
            // showing the error icon
            if (_hasError)
                e.Graphics.DrawIcon(_errorIcon, GetErrorRectangle(button1));
        }
    
        private void Button1_MouseMove(object? sender, MouseEventArgs e)
        {
            // displaying the error tooltip only when the cursor is above the error icon
            bool isVisible = _toolTip.GetToolTip(button1).Length > 0;
            if (_hasError && !isVisible && GetErrorRectangle(button1).Contains(e.Location))
                _toolTip.SetToolTip(button1, "click here to fix");
            else if (isVisible && (!_hasError || !GetErrorRectangle(button1).Contains(e.Location)))
                _toolTip.SetToolTip(button1, String.Empty);
        }
    
        private Rectangle GetErrorRectangle(Button button)
        {
            var rect = new Rectangle(Point.Empty, _errorIcon.Size);
            rect.Offset(button.Width - rect.Width - _errorIconPadding, button.Height / 2 - rect.Height / 2);
            return rect;
        }
    }
    

    And the result:

    enter image description here


    Edit:

    If clicking any part of the button should fix the issue, the solution is even simpler because you don't need to bother with the icon area at all and the tooltip can be displayed when you hover any part of the button just like a normal tooltip.

    Just set button1.TextImageRelation = TextImageRelation.TextBeforeImage, and then simply adjust button1.Image along with the tooltip.