有没有办法检测用户控件外的鼠标点击?

19
我正在创建一个自定义下拉框,并希望在鼠标单击下拉框外部时注册以隐藏它。是否可以检测控件外的单击事件?或者应该在包含表单上制作某种机制,并在任何下拉框打开时检查鼠标单击?

user control


你能不能不使用控件的“Leave”事件?我认为当你的新控件失去焦点时,它会被触发。 - IKEA Riot
检测下拉菜单失去焦点的情况,也许是一个好主意?如果你通过 Tab 键离开控件,你可能也想在那时关闭/隐藏下拉菜单。 - Jakob Möllås
7个回答

15

所以我终于明白,您只想在用户点击它之外的区域时关闭它。在这种情况下,Leave事件应该可以很好地工作... 不知何故,我有印象你希望它每当他们将鼠标移动到自定义下拉菜单之外时就关闭。当控件失去焦点时,会引发Leave事件,如果用户单击其他东西,控件就会失去焦点,因为他们单击的事物获得了焦点。

文档还指出,此事件会根据需要向上和向下级联控件链:

EnterLeave事件是分层的,并将级联到父级链上,直到到达适当的控件。例如,假设您有一个包含两个GroupBox控件的窗体,每个GroupBox控件都有一个TextBox控件。当光标从一个TextBox移动到另一个时,将为TextBox和GroupBox引发Leave事件,并为另一个GroupBox和TextBox引发Enter事件。

覆盖您的UserControl的OnLeave方法是处理此问题的最佳方法:

protected override void OnLeave(EventArgs e)
{
   // Call the base class
   base.OnLeave(e);

   // When this control loses the focus, close it
   this.Hide();
}

为了进行测试,我创建了一个表单,可以通过命令显示下拉UserControl:

public partial class Form1 : Form
{
   private UserControl1 customDropDown;

   public Form1()
   {
      InitializeComponent();

      // Create the user control
      customDropDown = new UserControl1();

      // Add it to the form's Controls collection
      Controls.Add(customDropDown);
      customDropDown.Hide();
   }

   private void button1_Click(object sender, EventArgs e)
   {         
      // Display the user control
      customDropDown.Show();
      customDropDown.BringToFront();   // display in front of other controls
      customDropDown.Select();         // make sure it gets the focus
   }
}

以上代码完美运行,但是有一个问题:如果用户单击表单的空白区域,UserControl不会关闭。为什么呢?因为表单本身不想获得焦点,只有控件能够获得焦点,而我们没有单击控件。并且由于没有其他控件窃取焦点,Leave事件从未被触发,这意味着UserControl不知道它应该关闭自己。

如果您需要在用户单击表单的空白区域时关闭UserControl,则需要对此进行特殊处理。由于您只关心单击,所以可以处理表单的Click事件,并将焦点设置为不同的控件:

protected override void OnClick(EventArgs e)
{
   // Call the base class
   base.OnClick(e);

   // See if our custom drop-down is visible
   if (customDropDown.Visible)
   {
      // Set the focus to a different control on the form,
      // which will force the drop-down to close
      this.SelectNextControl(customDropDown, true, true, true, true);
   }
}

是的,这个最后一部分感觉像是一个hack。更好的解决方案,正如其他人所提到的,是使用SetCapture函数指示Windows捕获鼠标在您的UserControl窗口上移动。控件的Capture属性提供了一种更简单的方法来完成相同的事情。


谢谢,我从未想过当鼠标离开用户控件时它会关闭,其中一位海报建议这是另一种解决方案。我将尝试实现你的答案。如果成功了,这将是完美的。 - Bildsoe

3
从技术上讲,您需要使用 p/invoke 来接收发生在您控件之外的单击事件,如 SetCapture()
但是在您的情况下,按照 @Martin 的建议处理 Leave 事件应该就足够了。 编辑:在寻找 SetCapture() 的用法示例时,我遇到了 Control.Capture 属性,这是我不知道的。使用该属性意味着您无需进行 p/invoke 操作,这对我来说总是一件好事。
因此,在显示下拉列表时,您必须将 Capture 设置为 true,然后在单击事件处理程序中确定鼠标指针是否位于控件内部,如果不是,则将 Capture 设置为 false 并关闭下拉列表。

更新: 您还可以使用Control.Focused属性来确定在使用键盘或鼠标时控件是否获得或失去了焦点,而不是使用与MSDN Capture页面中提供的相同示例的Capture


我的用户控件中还有其他控件,当它们获得焦点时,用户控件的 Leave 事件会被触发。 - Bildsoe
@Bildsoe,如果我理解正确的话,您希望在用户单击用户控件内的另一个控件时保持下拉菜单打开,但是如果他/她在用户控件外单击,则关闭它? - Frédéric Hamidi
@Bildsoe,如果您只是悬停在另一个控件上,则不应触发“Leave”事件。这些控件是否会在悬停时强制获取焦点? - Frédéric Hamidi
Bildsoe:听起来你可能在使用control.mouseleave而不是control.leave。后者只会在另一个控件被聚焦时触发-即被点击。前者则在鼠标不再悬停在控件上时触发。 - Martin
@Cody Gray - 我正在创建一个简单的下拉列表控件。当用户单击它时,它会打开并显示一系列选项。如果用户单击其中一个选项,则该选项将被选择。这个功能是有效的。如果用户在下拉列表外单击,它应该关闭。我已经按照我将在两分钟内包含的图像构建了它。 - Bildsoe
显示剩余9条评论

2

处理表单的 MouseDown 事件,或者重写表单的 OnMouseDown 方法:

enter code here

那么:

protected override void OnMouseDown(MouseEventArgs e)
{

    if (!theListBox.Bounds.Contains(e.Location)) 
    {
        theListBox.Visible = false;
    }
}

Contains方法旧的System.Drawing.Rectangle可以用于指示一个点是否包含在矩形内。控件的Bounds属性是由控件边缘定义的外部RectangleMouseEventArgs的Location属性是相对于接收MouseDown事件的控件而言的点。在窗体中,控件的Bounds属性是相对于窗体而言的。


这并不起作用,因为如果您单击另一个控件,则该表单不会接收到单击。 - Matthew

0

1
但是“普通”的下拉菜单会一直保持打开状态,直到鼠标被点击。你有什么想法如何解决这个问题吗? - Bildsoe

0

我只是想分享一下。这可能不是一个好的方法,但看起来它对于关闭虚假“MouseLeave”的下拉面板有效。我尝试在面板MouseLeave时隐藏它,但它不起作用,因为从面板到按钮移动会离开面板,因为按钮不是面板本身。可能有更好的方法,但我分享这个是因为我花了大约7个小时来找出如何使其工作。感谢@FTheGodfather

但如果鼠标移动到面板上,它只适用于在表单上移动。

    private void click_to_show_Panel_button_MouseDown(object sender, MouseEventArgs e)
    {
        item_panel1.Visible = true; //Menu Panel
    }



    private void Form1_MouseMove(object sender, MouseEventArgs e)
    {
        if (!item_panel1.Bounds.Contains(e.Location))
        {
            item_panel1.Visible = false; // Menu panel 
        }
    }

-1
如果您有表单,您可以像这样简单地使用“Deactivate”事件:
protected override void OnDeactivate(EventArgs e)
{
    this.Dispose();
}

此处理程序与其他事件相关,但不涉及鼠标单击。 - Serg Shevchenko

-1

我自己做过这个,以下是我的做法。

当下拉菜单被打开时,在控件的父表单上注册一个点击事件:

this.Form.Click += new EventHandler(CloseDropDown);

但这只能带你走一半的路。当当前窗口被取消激活时,您可能也希望关闭下拉列表。最可靠的检测方法对我来说是通过定时器检查哪个窗口当前处于活动状态:

[System.Runtime.InteropServices.DllImport("user32.dll")]
static extern IntPtr GetForegroundWindow();

并且

var timer = new Timer();
timer.Interval = 100;
timer.Tick += (sender, args) =>
{
    IntPtr f = GetForegroundWindow();
    if (this.Form == null || f != this.Form.Handle)
    {
        CloseDropDown();
    }
};

当下拉菜单可见时,您当然只应让计时器运行。此外,在打开下拉菜单时,父表单上可能还有其他事件需要注册:
this.Form.LocationChanged += new EventHandler(CloseDropDown);
this.Form.SizeChanged += new EventHandler(CloseDropDown);

别忘了在CloseDropDown方法中注销所有这些事件 :)

编辑:

我忘了,你还应该在控件上注册Leave事件,以查看是否有其他控件被激活/点击:

this.Leave += new EventHandler(CloseDropDown);

我想我现在明白了,这应该涵盖了所有的基础。如果我漏掉了什么,请告诉我。


1
哦,不要定期轮询当前的前景窗口。那肯定是滥用API。你有一个完美的方法来做这里要求的事情。它涉及捕获鼠标。查找“SetCapture”。 - Cody Gray
很好的建议@Cody,如果我需要更新此控件,我一定会查看它。我建议你自己也去看看@Bildsoe。我还想指出,我通常将我的下拉菜单实现为一个单独的表单,因此您需要考虑使用GetForegroundWindow方法。 - Kristoffer Lindvall

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接