Mouse handlers in WPF/Caliburn Micro
Asked Answered
M

2

7

I've got what I consider a simple situation, but can't seem to figure out how to get it done. I'm very new to WPF and the Caliburn Micro framework. In my View, I have a Canvas and on this Canvas I have an Image. I want to write some mouse event handlers (MouseLeftButtonDown, MouseLeftButtonUp, and MouseMove) that are called when the cursor is inside the Image. I have successfully been able to create event handlers in the ViewModel, but I can't seem to figure out how to get the current cursor position inside the handlers. The MouseEventArgs.GetPosition requires a IInputElement as an argument...not sure how to get that.

Here's my XAML (view):

<Canvas x:Name="ImageCanvas" >    
    <Image Source="{Binding DataImage}" 
           x:Name="ContainerImage"
              cal:Message.Attach=
              "[Event MouseLeftButtonDown] = [Action MouseDown_Image($source, $eventArgs)];
               [Event MouseLeftButtonUp] = [Action MouseUp_Image($source, $eventArgs)];
               [Event MouseMove] = [Action MouseMove_Image($source, $eventArgs)]">
    </Image>
</Canvas>

Here's my C# (viewmodel)

public void MouseDown_Image(object sender, MouseEventArgs e)
    {
      // How do I get cursor position here??

      // convert to Image coordinates??
    }

public void MouseUp_Image(object sender, MouseEventArgs e)
    {
      // How do I get cursor position here??

      // convert to Image coordinates??
    }

public void MouseMove_Image(object sender, MouseEventArgs e)
    {
      // How do I get cursor position here??

      // convert to Image coordinates??
    }

Once I do this, I'll need to convert the mouse coordinates to coordinates on the Image...but first things first.

Thanks!!

Manhandle answered 1/7, 2013 at 22:35 Comment(1)
That's not what MVVM is about. You NEVER create event handlers in the ViewModel. If you want to handle mouse events, do it in the view. If you want to convert a mouse event to a specific ACTION, create a converter to convert the event to that action.Proprietary
W
6

It may be useful to make a SpecialValues dictionary entry in order to process the coords for an image - this way you could add functionality in a central location, or even take just the X/Y position within the image or another scaled input control:

e.g. in your CM Bootstrapper configure:

            MessageBinder.SpecialValues.Add("$scaledmousex", (ctx) =>
            {
                var img = ctx.Source as Image;
                var input = ctx.Source as IInputElement;
                var e = ctx.EventArgs as MouseEventArgs;

                // If there is an image control, get the scaled position
                if (img != null && e != null)
                {
                    Point position = e.GetPosition(img);
                    return (int)(img.Source.Width * (position.X / img.ActualWidth));
                }

                // If there is another type of of IInputControl get the non-scaled position - or do some processing to get a scaled position, whatever needs to happen
                if (e != null && input != null)
                    return e.GetPosition(input).X;

                // Return 0 if no processing could be done
                return 0;
            });

This dictionary is used to parse the input parameters in your action message binding and will run the above anonymous delegates, returning the value you specify (you will need to also add $scaledmousey)

Then you use this value in your binding:

cal:Message.Attach="[Event MouseMove] = [Action MouseMove_Image($scaledmousex, $scaledmousey)]"

This way, if you decide you want to change the source type, you can still get the position without much/any refactoring and your code goes through a central processing path

This also ensures that the view/viewmodel are decoupled - part of the aim of the MVVM pattern

Wafer answered 2/7, 2013 at 10:54 Comment(4)
Charleh - thank you for the response. I see from other responses I've recieved that I've broken the MVVM by passing eventArgs in the Action. Your suggestion seems to solve that. I'm quickly realizing that I need to backup and study a lot more on WPF and MVVM. Thank you again for your kind and detailed response.Manhandle
There are times when it seems like the only way to do something is to reference a known typed view from the VM - but most of the time there is a mechanism to avoid this, and often with CM it's built in. I always ask myself "how does this bit of code ensure that we aren't coupling these components" and usually talking through it helps me reach an alternative - often a cleaner simpler implementation with better separation! Good luck with it anyway!Wafer
I do have a question, however, :-) does the code you supplied above go inside the CM bootstrapper class? When I do this, I get an error: "Caliburn.Micro.MessageBinder.SpecialValues is a field but is used like a type". What am I missing?Manhandle
Should go inside your Bootstrapper.Configure method - SpecialValues is a static field on the MessageBinder class (iirc it's a Dictionary<string, Func>) so the code just adds a key and a callback to the dictionaryWafer
M
0

I believe that I've solved it!

In my XAML, I attached event handlers to events on the Image object. Therefore my sender inside my event handlers can be cast to type Image. You can then pass the sender as the argument to GetPosition.

Also, I was able to convert the coordinates sent to Actual image coordinates. Knowing the pixel size of the original image, it's just some simple scaling math.

Here is the code from one of my mouse handlers:

public void MouseDown_Image(object sender, MouseEventArgs e)
   {

      // Get the x and y coordinates of the mouse pointer.
       System.Windows.Point position = e.GetPosition((Image)sender);
       int pX = (int)position.X;
       int pY = (int)position.Y;

     // DataImage is the actual Image object that is being shown in the view
       int X_ImageCoordinates = (int)(DataImage.PixelWidth * (position.X/img.ActualWidth));
       int Y_ImageCoordinates = (int)(DataImage.PixelHeight * (position.Y / img.ActualHeight));                
    }

Turned out to be simple! As it should be.

Please let me know if there's a better solution, or if for some reason I've made a mistake.

Thanks!!

Manhandle answered 1/7, 2013 at 23:29 Comment(2)
You just invalidated the MVVM model. The whole point of the MVVM model is to separate the view from the model. By passing the events to the model you destroyed that. If you actually have to handle mouse events, do it in the viewProprietary
Understood. Thank you. Looks like I still have lots to learn about MVVM, CM, and WPF.Manhandle

© 2022 - 2024 — McMap. All rights reserved.