使用 MVVM 在 canvas 中移动项目

Move items in a canvas using MVVM

我希望用户能够在 canvas 中自由移动项目。
我的应用正在使用 Caliburn.Micro.

我的 MainViewModel 有一个集合,如果项目:

public BindableCollection<ItemViewModel> Items { get; set; }

我通过 ItemsControl 在 canvas 中显示:

<ItemsControl x:Name="Items">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas Background="#FFCADEEF" />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemContainerStyle>
        <Style TargetType="ContentPresenter">
            <Setter Property="Canvas.Left" Value="{Binding Path=X}" />
            <Setter Property="Canvas.Top" Value="{Binding Path=Y}" />
            <Setter Property="Width" Value="{Binding Path=Width}" />
            <Setter Property="Height" Value="{Binding Path=Height}" />
        </Style>
    </ItemsControl.ItemContainerStyle>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Border Background="{Binding Path=BackgroundColor}">
                <Rectangle>
                    <Rectangle.Fill>
                        <VisualBrush Visual="{StaticResource appbar_cursor_move}" />
                    </Rectangle.Fill>
                </Rectangle>
            </Border>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

我已经成功地将事件(现在什么都不做)绑定到 MouseLeftButtonDownMouseLeftButtonUpMouseMove 但我不知道如何从视图模型中获取光标的位置.

如果您想知道如何以一种好的方式做到这一点,那么我会说您正在寻找很长的几行:

<ItemsControl 
yourAttachedbehavior.MouseButtonMoved="{Binding YourViewModelCommand}"
x:Name="Items">

其中 MouseButtonMoved 是附加的 属性(ICommand 类型),它将自身挂接到 ItemsControl MouseButtonMove 事件中,然后执行它绑定的命令,通过 MouseButtonMoved.Execute(x, y).

传递两个双精度值 (x, y)

如果您想查看类似的实现,请检查 gongdrop 实现。我会用它作为解决方案,但如果您想从零开始重写它,请阅读源代码。

我有一个开源库,您可以从中复制此功能。

DragablzItemsControl 继承自 ItemsControl,DragablzItem 继承自 ContentControl。

DragablzItem 有一个 TemplatePart PART_Thumb,类型为 Thumb。在 OnApplyTemplate 中抓住拇指,订阅 .DragDelta,您可以在其中更新 X、Y 坐标。它还通过围绕边界的额外拇指来管理大小。

查看来源:http://github.com/ButchersBoy/Dragablz/blob/master/Dragablz/DragablzItem.cs

如果您从 http://github.com/ButchersBoy/Dragablz 下载演示项目,您将看到所有这些正在运行

我复制了 another GitHub account 的行为:

public class DragBehavior
{
    public readonly TranslateTransform Transform = new TranslateTransform();
    private Point _elementStartPosition2;
    private Point _mouseStartPosition2;
    private static DragBehavior _instance = new DragBehavior();
    public static DragBehavior Instance
    {
        get { return _instance; }
        set { _instance = value; }
    }

    public static bool GetDrag(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsDragProperty);
    }

    public static void SetDrag(DependencyObject obj, bool value)
    {
        obj.SetValue(IsDragProperty, value);
    }

    public static readonly DependencyProperty IsDragProperty =
      DependencyProperty.RegisterAttached("Drag",
      typeof(bool), typeof(DragBehavior),
      new PropertyMetadata(false, OnDragChanged));

    private static void OnDragChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        // ignoring error checking
        var element = (UIElement)sender;
        var isDrag = (bool)(e.NewValue);

        Instance = new DragBehavior();
        ((UIElement)sender).RenderTransform = Instance.Transform;

        if (isDrag)
        {
            element.MouseLeftButtonDown += Instance.ElementOnMouseLeftButtonDown;
            element.MouseLeftButtonUp += Instance.ElementOnMouseLeftButtonUp;
            element.MouseMove += Instance.ElementOnMouseMove;
        }
        else
        {
            element.MouseLeftButtonDown -= Instance.ElementOnMouseLeftButtonDown;
            element.MouseLeftButtonUp -= Instance.ElementOnMouseLeftButtonUp;
            element.MouseMove -= Instance.ElementOnMouseMove;
        }
    }

    private void ElementOnMouseLeftButtonDown(object sender, MouseButtonEventArgs mouseButtonEventArgs)
    {
        var parent = Application.Current.MainWindow;
        _mouseStartPosition2 = mouseButtonEventArgs.GetPosition(parent);
        ((UIElement)sender).CaptureMouse();
    }

    private void ElementOnMouseLeftButtonUp(object sender, MouseButtonEventArgs mouseButtonEventArgs)
    {
        ((UIElement)sender).ReleaseMouseCapture();
        _elementStartPosition2.X = Transform.X;
        _elementStartPosition2.Y = Transform.Y;
    }

    private void ElementOnMouseMove(object sender, MouseEventArgs mouseEventArgs)
    {
        var parent = Application.Current.MainWindow;
        var mousePos = mouseEventArgs.GetPosition(parent);
        var diff = (mousePos - _mouseStartPosition2);
        if (!((UIElement)sender).IsMouseCaptured) return;
        Transform.X = _elementStartPosition2.X + diff.X;
        Transform.Y = _elementStartPosition2.Y + diff.Y;
    }        
}

并简单地将其附加到 ItemsControl DataTemplate :

<DataTemplate>
    <Border
        Background="{Binding Path=BackgroundColor}"
        behaviors:DragBehavior.Drag="True">
        <!-- whatever -->
    </Border>
</DataTemplate>

现在我需要找到如何在用户停止拖动时将消息从行为发送到视图模型(我假设它涉及新行为 属性)。

我已经使用了您的行为并做了一些改动以使其更符合 MVVM:

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525" DataContext="{Binding RelativeSource={RelativeSource Self}, Path=ViewModel}">
    <ItemsControl ItemsSource="{Binding Path=Shapes}">
        <ItemsControl.Resources>
            <DataTemplate DataType="{x:Type local:Rectangle}">
                <Rectangle Canvas.Top="{Binding Top, Mode=TwoWay}" Canvas.Left="{Binding Left, Mode=TwoWay}" Width="{Binding Width}" Height="{Binding Height}" Fill="Red">
                    <i:Interaction.Behaviors>
                        <local:DragBehavior/>
                    </i:Interaction.Behaviors>
                </Rectangle>
            </DataTemplate>
            <DataTemplate DataType="{x:Type local:Circle}">
                <Ellipse Width="{Binding Radius}" Height="{Binding Radius}" Fill="Blue" Canvas.Top="{Binding Top, Mode=TwoWay}" Canvas.Left="{Binding Left, Mode=TwoWay}">
                    <i:Interaction.Behaviors>
                        <local:DragBehavior/>
                    </i:Interaction.Behaviors>
                </Ellipse>
            </DataTemplate>
        </ItemsControl.Resources>
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <Canvas />
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.ItemContainerStyle>
            <Style TargetType="ContentPresenter">
                <Setter Property="Canvas.Top" Value="{Binding Path=Top, Mode=TwoWay}" />
                <Setter Property="Canvas.Left" Value="{Binding Path=Left, Mode=TwoWay}" />
            </Style>
        </ItemsControl.ItemContainerStyle>
    </ItemsControl>
</Window>

this 是我使用样式绑定到 Canvas.Top 和 Left 的原因。

这是我的 ViewModel。我对 IPropertyChanged 使用 ReactiveUI,但这并不重要。

 public class MainViewModel : ReactiveObject
    {
        private ReactiveList<IShape> _shapes;

        public MainViewModel()
        {
            Shapes = new ReactiveList<IShape>();

            Shapes.Add(new Rectangle { Top = 50, Left = 50, Height = 50, Width = 50 });
            Shapes.Add(new Circle{Top = 100, Left = 100, Radius = 50});
        }

        public ReactiveList<IShape> Shapes
        {
            get { return _shapes; }
            set { this.RaiseAndSetIfChanged(ref _shapes, value); }
        }
    }

    public interface IShape
    {
        int Top { get; set; }
        int Left { get; set; }
    }

    public abstract class Shape : ReactiveObject, IShape
    {
        private int _top;
        private int _left;

        public int Top
        {
            get { return _top; }
            set { this.RaiseAndSetIfChanged(ref _top, value); }
        }

        public int Left
        {
            get { return _left; }
            set { this.RaiseAndSetIfChanged(ref _left, value); }
        }
    }

    public class Circle : Shape
    {
        private int _radius;

        public int Radius
        {
            get { return _radius; }
            set { this.RaiseAndSetIfChanged(ref _radius, value); }
        }
    }

    public class Rectangle : Shape
    {
        private int _width;
        private int _height;

        public int Width
        {
            get { return _width; }
            set { this.RaiseAndSetIfChanged(ref _width, value); }
        }

        public int Height
        {
            get { return _height; }
            set { this.RaiseAndSetIfChanged(ref _height, value); }
        }
    }

我为 reactangle 和圆创建了 classes,因为 MVVM 的重点是区分层。在 ViewModel 中持有 UI 控件绝对反对这个想法。

最后,我不得不稍微更改一下您的 MouseLeftButtonUp:

 AssociatedObject.MouseLeftButtonUp += (sender, e) =>
            {
                AssociatedObject.ReleaseMouseCapture();

                var diff = e.GetPosition(parent) - mouseStartPosition;


                Canvas.SetTop(AssociatedObject, ElementStartPosition.Y + diff.Y);
                Canvas.SetLeft(AssociatedObject, ElementStartPosition.X + diff.X);

                transform.Y = 0;
                transform.X = 0;
            };

这从 RenderTransform 获取更改并将它们写入对象。然后,双向绑定将其放入我们的矩形 class。

只有当您想知道对象的位置时才需要这样做,例如检查它们是否在 VM 中相交。

它工作得很好,并且是你能得到的 MVVM。也许 var parent = Application.Current.MainWindow; 行除外 - 我认为这应该替换为绑定到 public 依赖性 属性 你的行为。