wpfcanvasitemscontrol

how to change Canvas.left of items in a ItemsControl?


I want to wrap some Rectangles on a Canvas.The details of rectangles are in a list in my viewmodel.I succeeded drawing the rectangles,but failed to make them movable.In my eventhandler Canvas.GetLeft() and Canvas.SetLeft dosen't work.

here is my ItemsControl:

<ItemsControl Width="1920"
                Height="1080"
                ItemsSource="{Binding Controls,Mode=TwoWay}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <UniformGrid Rows="2"
                            PreviewMouseUp="Container_Control_MouseUp"
                            PreviewMouseMove="Container_Control_MouseMove"
                            PreviewMouseDown="Container_Control_MouseDown">
                <Rectangle Width="{Binding Width}"
                            Height="{Binding Height}"
                            Fill="Blue" />
                <TextBlock Grid.Row="1"
                            Text="move it" />
            </UniformGrid>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
    <ItemsControl.ItemContainerStyle>
        <Style>
            <Setter Property="Canvas.Top"
                    Value="{Binding X}" />
            <Setter Property="Canvas.Left"
                    Value="{Binding Y}" />
        </Style>
    </ItemsControl.ItemContainerStyle>
</ItemsControl>        

and here is my eventhandlers,they don't work with elements in a itemscontrol:

public partial class ControlsView : UserControl
    {
        bool _isMouseDown = false;
        Point _mouseDownPosition;
        Point _mouseDownControlPosition;
        public ControlsView(ControlsViewModel vm)
        {
            this.DataContext = vm;
            InitializeComponent();
        }
        private void Container_Control_MouseUp(object sender, MouseButtonEventArgs e)
        {
            var c = sender as UIElement;
            _isMouseDown = false;
            c.ReleaseMouseCapture();
        }
        private void Container_Control_MouseMove(object sender, MouseEventArgs e)
        {
            if (_isMouseDown)
            {
                var c = sender as UIElement;
                var pos = e.GetPosition(this);
                var dp = pos - _mouseDownPosition;
                Canvas.SetLeft(c, _mouseDownControlPosition.X + dp.X);
                Canvas.SetTop(c, _mouseDownControlPosition.Y + dp.Y);
            }
        }

        private void Container_Control_MouseDown(object sender, MouseButtonEventArgs e)
        {
            var c = sender as UIElement;
            _isMouseDown = true;
            _mouseDownPosition = e.GetPosition(this);
            _mouseDownControlPosition = new Point(double.IsNaN(Canvas.GetLeft(c)) ? 0 : Canvas.GetLeft(c), double.IsNaN(Canvas.GetTop(c)) ? 0 : Canvas.GetTop(c));
            c.CaptureMouse();
        }
    }

Solution

  • The UniformGrid is not the direct child of the Canvas. You must attach the mouse event handlers to the item container. You can use the ItemsControl.ItemContainerStyle to register them using an EventSetter:

    <ItemsControl Width="1920"
                  Height="1080"
                  ItemsSource="{Binding Controls}">
      <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
          <Canvas />
        </ItemsPanelTemplate>
      </ItemsControl.ItemsPanel>
    
      <ItemsControl.ItemTemplate>
        <DataTemplate>
          <UniformGrid Rows="2">
            <Rectangle Width="{Binding Width}"
                       Height="{Binding Height}"
                       Fill="Blue" />
            <TextBlock Grid.Row="1"
                       Text="move it" />
          </UniformGrid>
        </DataTemplate>
      </ItemsControl.ItemTemplate>
    
      <ItemsControl.ItemContainerStyle>
        <Style TargetType="ContentPresenter">
          <EventSetter Event="PreviewMouseLeftButtonDown"
                       Handler="ItemContainer_PreviewMouseLeftButtonDown" />
          <EventSetter Event="PreviewMouseMove"
                       Handler="ItemContainer_PreviewMouseMove" />
          <Setter Property="Canvas.Top"
                  Value="{Binding X}" />
          <Setter Property="Canvas.Left"
                  Value="{Binding Y}" />
        </Style>
      </ItemsControl.ItemContainerStyle>
    </ItemsControl>
    

    You can also simplify your algorithm:

    // The initial mouse offset relative to the container position
    private Point mousePositionOffset;
    
    private void ItemContainer_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
      var itemContainer = sender as IInputElement;
      _ = Mouse.Capture(itemContainer);
      this.mousePositionOffset = e.GetPosition(itemContainer);
    }
    
    private void ContentPresenter_PreviewMouseMove(object sender, MouseEventArgs e)
    {
      if (e.LeftButton is not MouseButtonState.Pressed 
        || sender is not UIElement itemContainer)
      {
        Mouse.Capture(null);
        return;
      }
    
      var canvas = (Canvas)VisualTreeHelper.GetParent(itemContainer);
      Point currentMousePosition = e.GetPosition(canvas);
      Canvas.SetLeft(itemContainer, currentMousePosition.X - this.mousePositionOffset.X);
      Canvas.SetTop(itemContainer, currentMousePosition.Y - this.mousePositionOffset.Y);
    }