Eric Arther
Eric Arther

Reputation: 65

How can I make a Control lose focus clicking anywhere outside of it?

enter image description here

I want my UserControl (at the middle-top in the image) to lose focus when I click outside of it, anywhere.

I have tried this:

private void Form1_Click(object sender, EventArgs e)
{
    ActiveControl = null;
}

It only works when I click the Form itself, the TrackBar and the MediaPlayer. It doesn't work when I click other controls, even the FlowLayoutPanel.
What can I do about it?

Upvotes: 4

Views: 2092

Answers (3)

Jimi
Jimi

Reputation: 32298

You can use a general purpose Message handler/dispatcher:

  • Make your Forms that need to provide control over Mouse events implement IMessageFilter, so these Forms will know beforehand what messages are sent to which child Control, what kind of event is generated and any other details that may be useful, such as the position of the Mouse pointer when the event is generated. Mouse related messages are generated in any case, no matter whether you click on a Control that usually cannot get Focus, as the Form itself, or a Panel etc.
  • Your Forms also implement another Interface that just defines a public event that interested parties can subscribe to in order to receive fresh notifications about these events.
    This also allows Control to detect whether the Parent Form is actually a Mouse Event notifier guy.
  • When a Mouse event is notified (here, just the event generated when a WM_LBUTTONDOWN message is received), subscribers of the event(s) can decide to act upon it. In your case, you can call the SelectNextControl() method of the Parent Form to set a different ActiveControl, only when your UserControl has the Focus and a MouseDown event is generate outside its bounds.

Call Application.AddMessageFilter() before the Form is constructed, passing the Form instance itself, since this Form implements the IMessageFilter interface.
Remove the message filter when the Form closes.

Form side:
add a message filter to capture a mouse down event (WM_LBUTTONDOWN) and rise an event to notify the subscribers of the location where the event is generated and which is the Control that will be affected (passing its Handle).

public partial class SomeForm : Form, IMessageFilter, IMouseHandler {
    private const int WM_LBUTTONDOWN = 0x0201;
    public event EventHandler<MouseDownEventArgs> MouseEvent;

    public SomeForm() => InitializeComponent();

    public bool PreFilterMessage(ref Message m)
    {
        if (m.Msg == WM_LBUTTONDOWN) {
            var pos = MousePosition;
            MouseEvent?.Invoke(this, new MouseDownEventArgs(m.HWnd, pos));
        }
        return false;
    }

    protected override void OnHandleCreated(EventArgs e)
    {
        base.OnHandleCreated(e);
        if (!DesignMode) Application.AddMessageFilter(this);
    }

    protected override void OnHandleDestroyed(EventArgs e)
    {
        base.OnHandleDestroyed(e);
        if (!DesignMode) Application.RemoveMessageFilter(this);
    }
}

UserControl part:
if the Parent Form implements IMouseHandler, subscribe to its MouseEvent. When the event is raised, verify that the UserControl is the current ActiveControl (it contains the Focus) and that the Mouse event is generated outside its bounds. If these conditions are met, call the Parent Form's SelectNextControl() method to move the Focus elsewhere.

The bool m_MouseEventSubscribed is there because a UserControl may regenerate its Handle more than once in its life-time.

public partial class MyUserControl : UserControl
{
    private bool m_MouseEventSubscribed = false;

    public MyUserControl() => InitializeComponent();

    protected override void OnHandleCreated(EventArgs e)
    {
        base.OnHandleCreated(e);

        var form = this.ParentForm;
        if (form != null && form is IMouseHandler && !m_MouseEventSubscribed) {
            m_MouseEventSubscribed = true;
            ((IMouseHandler)form).MouseEvent += (s, a) => {
                if (this.ContainsFocus && !this.ClientRectangle.Contains(PointToClient(a.Position))) {
                    form.SelectNextControl(this, true, true, false, true);
                }
            };
        }
    }
}

IMouseHandler Interface:

public interface IMouseHandler
{
    event EventHandler<MouseDownEventArgs> MouseEvent;
}

Custom EventArgs class:

using System;
using System.Drawing;

public class MouseDownEventArgs : EventArgs
{
    public MouseDownEventArgs() { }
    public MouseDownEventArgs(IntPtr hWnd, Point point)
    {
        this.ControlHandle = hWnd;
        this.Position = point;
    }
    public IntPtr ControlHandle { get; }
    public Point Position { get; }
}

Upvotes: 2

Jim Simson
Jim Simson

Reputation: 2872

Eric,

To remove the focus from an item in your custom control, you will need some other focusable control to pass the focus to. I would do something like this:

private void StealFocus() => lbl_SomeLabel.Focus();

And then drop StealFocus() in your form's click event handler:

private void Form1_Click(object sender, EventArgs e) => StealFocus();

and any other control the user might click that doesn't execute a command.

Note that you cannot set focus to the Form. Container controls like Form and Panel will pass the Focus on to their first child control. Which could be the custom control you wanted to remove focus from.

Upvotes: 4

Flot2011
Flot2011

Reputation: 4671

When you click on controls, the click event is eaten by them and doesn't go to the form itself. To notify the form that one of its controls was clicked, use something like this:

 private void control_Click(object sender, EventArgs e)
    {
        // you control click code here
        this.OnClick(new EventArgs());
    }

Upvotes: 1

Related Questions