Saturday, April 23, 2011

Clicking away from FrameworkElement when mouse is still captured

This post is about a very specific problem about mouse capturing. There are certain situations in which we need to be notified when mouse is clicked outside the bounds of an element. As all of you know that this is classical mouse capturing situation used for various tasks including Drag & Drop. When we search around for a solution for this then we find out an attached event Mouse.PreviewMouseDownOutsideCapturedElement. This is documented in msdn as follows:

“Occurs when the primary mouse button is pressed outside the element that is capturing mouse events.”
<Window x:Class="WpfApplication3.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Window1" Height="483" Width="553"      
Mouse.PreviewMouseDownOutsideCapturedElement="Window_PreviewMouseDownOutsideCapturedElement" >
    <StackPanel>
        <StackPanel Height="33">
            <TextBox Name="textBox1" />
        </StackPanel>
        <StackPanel  >           
            <Button Content="Button" Height="32" HorizontalAlignment="Left"
                    Margin="126,209,0,0" Name="button1" VerticalAlignment="Top"
                    Width="297" Click="button1_Click" />
        </StackPanel>
    </StackPanel>
</Window>
This definitely does what it is supposed to do. In order to get this to work we must have a captured element. Let’s capture mouse on textBox1. So, clicking a mouse outside textBox1 (captured element) should result in causing this handler to get called.
public partial class Window1 : Window
{
    public Window1()
    {
        InitializeComponent();       
    }

    private void Window_PreviewMouseDownOutsideCapturedElement(object sender, MouseButtonEventArgs e)
    {

    }

    private void button1_Click(object sender, RoutedEventArgs e)
    {
        Mouse.Capture(this.textBox1);
    }
}
Let’s run this. We have the display as presented in XAML. It has a TextBox and a Button. When we click the button textBox1 captures the mouse using static Capture method on Mouse. We have subscribed PreviewMouseDownOutsideCapturedElement event in XAML. We could have easily done that in code behind as well. Obviously we would need AddHandler… mechanism for registering with an attached event. This is similar to XAML attached properties. When implemented in WPF, they work as Dependency properties. Similarly Attached events from XAML are implemented as routed events in WPF.

Now the issue is that this event gets fired even when we click inside textBox1. This seems awkwardly strange as this is purely not expected behavior when we read the msdn description. Now we know the behavior. How can we fix this? We just need to find out where the mouse was clicked and we should be good to go. You might be wondering we can get around that we can do that either through the sender parameter or Source / OriginalSource from MouseButtonEventArgs. But the strange thing is that sender is always the element on which the event is registered. So if were registering it on textBox1, it would be textBox1. Currently this would always be the Window object no matter where we click on the Window after capturing the mouse by clicking the button. On top of that, Source and OriginalSource are always the captured element. The other properties which could inform us if the mouse is directly over the textBox1 are always true.

The only way to fix it seems to be to do it ourselves. We can find out the position of mouse. If the position of mouse is within the bounds of captured element then we can just ignore this. Otherwise, we can execute the same logic as we were supposed to execute. We might need to register the event again.

private void Window_PreviewMouseDownOutsideCapturedElement(object sender, MouseButtonEventArgs e)
{
    bool isClickedWithin =  IsMouseClickWithin(this.textBox1, e.MouseDevice.GetPosition(this.textBox1));

    if(isClickedWithin)
    {
        //execute some logic
    }
}

private bool IsMouseClickWithin(FrameworkElement element, Point point)
{
    return (element.ActualWidth > point.X && element.ActualHeight > point.Y) || point.X < 0 || point.Y < 0;
}
In the above example, it is checking if the mouse position is within the actual bounds of textBox1. IsMouseClickWithin returns true for this. It returns false otherwise.

2 comments:

Unknown said...

use CaptureMode like this "Mouse.Capture(grid, CaptureMode.SubTree);" and it'll work properly. I had same problem and after your decision I found this: http://stackoverflow.com/questions/6761786/how-can-a-control-handle-a-mouse-click-outside-of-that-control

Muhammad Shujaat Siddiqi said...

Thanks Roman. I will definitely try that.