0

I have a window with a canvas (that is smaller than the window itself). Now I want to draw a rectangle whenever I click on the canvas, and it should appear where I clicked. I got it working, but whenever I click on an existing rectangle, it seems to take the mouse coordinates relative to the upper left corner of the existing rectangle and not of the canvas itself. Below you see my XAML:

<ItemsControl ItemsSource="{Binding CanvasItems}" Grid.Column="1" >
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas Background="White">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="MouseLeftButtonDown">
                        <i:InvokeCommandAction Command="{Binding MouseButtonDownCommand}" PassEventArgsToCommand="True"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </Canvas>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemContainerStyle>
        <Style TargetType="Shape">
            <Setter Property="Canvas.Left" Value="{Binding ModelLeft}"/>
            <Setter Property="Canvas.Top" Value="{Binding ModelTop}"/>
        </Style>
    </ItemsControl.ItemContainerStyle>
</ItemsControl>

And here you can see the according command method in my ViewModel:

private void OnMouseButtonDown(object obj)
    {
        var mouseArgs = obj as MouseButtonEventArgs;
        var point = mouseArgs.GetPosition((IInputElement)mouseArgs.Source);
        

        ModelLeft = (int)point.X;
        ModelTop = (int)point.Y;

        Rectangle rect = new Rectangle();
        
        
        rect.Stroke = new SolidColorBrush(Colors.Black);
        rect.Fill = new SolidColorBrush(Colors.Red);
        rect.Width = 200;
        rect.Height = 200;

        CanvasItems.Add(rect);
    }

For a better uderstanding, ModelLeft and ModelTop are two int properties and CanvasItems is an ObservableCollection<object>. Also, I'm using System.Windows.Shapes; for the rectangles.

How can I get it working as expected, that it will always take the mouse position relative to the canvas and not an already existing shape on the canvas?

Turwaith
  • 67
  • 6
  • 1
    Better use an ordinary MouseLeftButtonDown event handler, where you get the Canvas via the sender argument. Then invoke the view model command from there. A view model shouldn't be aware of things like MouseButtonEventArgs. – Clemens Aug 17 '22 at 15:13
  • @Clemens I'm afraid I don't fully understand what you mean. Would I have to pass the canvas itself here: `` ? How would I do that, I do not see a property of `InvokeCommandAction` that lets me pass anything else but the raw event args. And as I understood, the event passes its "top most" source, which is not always the canvas. – Turwaith Aug 18 '22 at 06:53
  • 1
    No, do not pass any view related items to the view model. Add a MouseLeftButtonDown event handler in the view, i.e. in the view's code behind. Then retrieve the coordinates in that handler method and pass them to a property or method or command in the view model. – Clemens Aug 18 '22 at 07:40
  • 1
    Also be aware that with your ItemContainerStyle ModelLeft and ModelTop are supposed to be properties of the item class, i.e. Shape here. This is obviously not the case. – Clemens Aug 18 '22 at 07:43
  • 1
    You might consider an approach where you do not create UI elements in code behind, but only an abstract representation of their visual appearance, e.g. like shown here: https://stackoverflow.com/a/40190793/1136211 – Clemens Aug 18 '22 at 07:45
  • @Clemens Alright thank you! I will try and implement what you proposed. Could take a while though as I'm pretty much a bloody beginner. – Turwaith Aug 18 '22 at 08:15

2 Answers2

1

GetPosition returns the mouse coordinates relative to the top-left angle of the element passed in. So rather than the Shape element hit and referred to by mouseArgs.Source you likely just have to pass the canvas itself to mouseArgs.GetPosition.

var point = mouseArgs.GetPosition(myCanvas);

lidqy
  • 1,891
  • 1
  • 9
  • 11
1

Your could also make your rectangles "invisible" to the mouse clicks, so that the source of the event will always be your canvas.

rect.Width = 200;
rect.Height = 200;
rect.IsHitTestVisible = false;

Also, mouse clicks come in both bubbling and tunneling versions, and to catch the tunneling version, handle the PreviewMouseDown event. That way, the canvas will always have a chance to handle the event first. But, you will still have to have the canvas object as a parameter in your GetPosition call.