如何在Canvas 上拖动移动、resize/scale、旋转控件元素?

How to drag move, resize/scale, rotate a control element on a Canvas?

我正在尝试使用 WPF 创建图表设计器并使用 MVVM 模式,我从本指南中获取信息和一些提示:https://www.codeproject.com/Articles/22952/WPF-Diagram-Designer-Part-1

一会儿,我的项目看起来像:

WPF: Hydrate Canvas with Draggable Controls at Runtime

当然我遇到了与上述作者类似的问题:当我绘制 ContentControl 时,它使用随机坐标正确绘制,但是当我尝试移动它时,它不会移动!当我调试 class MoveThumb 时,我发现我的 ContentControl 没有得到它的 Parent。但在我看来,它应该有 Canvas 作为 Parent。我知道我应该覆盖一些 system/basic 方法,但我不明白我应该覆盖什么以及如何覆盖它。也许有人有想法?

现在我尝试描述我的实现,首先我创建 BaseShapeViewModel

abstract public class BaseShapeViewModel : BaseViewModel
{

    public BaseShapeViewModel()
    {

    }

    private double left;
    public double Left
    {
        get => left;
        set => SetField(ref left, value, nameof(Left));
    }

    private double top;
    public double Top
    {
        get => top;
        set => SetField(ref top, value, nameof(Top));
    }

    private int width;
    public int Width
    {
        get => width;
        set => SetField(ref width, value, nameof(Width));
    }

    private int height;
    public int Height
    {
        get => height;
        set => SetField(ref height, value, nameof(Height));
    }

    private string fill;
    public string Fill
    {
        get => fill;
        set => SetField(ref fill, value, nameof(Fill));
    }

    private string text;
    public string Text
    {
        get => text;
        set => SetField(ref text, value, nameof(Text));
    }

}

其他ViewModel EllipseViewModel、RectangleViewModel 继承自BaseShapeViewModel。 我的 MainViewModel 看起来像

class MainViewModel : BaseViewModel
{
    public MainViewModel()
    {          
        BaseShapeViewModels = new ObservableCollection<BaseShapeViewModel>();                    
    }

    public ObservableCollection<BaseShapeViewModel> BaseShapeViewModels { get; set; }

    //public Canvas DesignerCanvas;
   
    private RelayCommand createUseCase;
    private RelayCommand createRectangle;

    public ICommand CreateUseCase
    {
        get
        {
            return createUseCase ?? 
                (
                    createUseCase = new RelayCommand(() => { AddUseCase(); })                    
                );
        }
    }
    public ICommand CreateRectangle
    {
        get
        {
            return createRectangle ??
                (
                    createRectangle = new RelayCommand(() => { AddRectangle(); })
                );
        }
    }

    private void AddUseCase()
    {
        Random rnd = new Random();
        
        int valueLeft = rnd.Next(0, 200);
        int valueTop = rnd.Next(0, 200);
        
        EllipseViewModel useCaseViewModel = new EllipseViewModel {Left=valueLeft,Top=valueTop, Height = 100, Width = 200, Fill="Blue"};
        BaseShapeViewModels.Add(useCaseViewModel);
  
    }

    private void AddRectangle()
    {
        Random rnd = new Random();
        
        int valueLeft = rnd.Next(0, 200);
        int valueTop = rnd.Next(0, 200);

        RectangleViewModel rectangleViewModel = new RectangleViewModel { Left = valueLeft, Top = valueTop, Height = 100, Width = 200, Fill = "Blue" };
        BaseShapeViewModels.Add(rectangleViewModel);
    }
}

我的MoveThumb.cs长得像

 public class MoveThumb : Thumb
{
  
    public MoveThumb()
    {
        DragDelta += new DragDeltaEventHandler(this.MoveThumb_DragDelta);
    }

    private void MoveThumb_DragDelta(object sender, DragDeltaEventArgs e)
    {
        ContentControl designerItem = DataContext as ContentControl;
       
        if (designerItem != null)
        {
            Point dragDelta = new Point(e.HorizontalChange, e.VerticalChange);
            
            RotateTransform rotateTransform = designerItem.RenderTransform as RotateTransform;
            if (rotateTransform != null)
            {
                dragDelta = rotateTransform.Transform(dragDelta);
            }
            double left = Canvas.GetLeft(designerItem);
            double top = Canvas.GetTop(designerItem);
           
            Canvas.SetLeft(designerItem, left + dragDelta.X);
            Canvas.SetTop(designerItem, top + dragDelta.Y);       
        }

    }
}

并且知道我想说我在 Xaml 没有盈利,但我从第一个 link 检查 material 并像这样创建 MoveThumb.xaml

<ResourceDictionary 

<ControlTemplate x:Key="MoveThumbTemplate" TargetType="{x:Type s:MoveThumb}">
    <Rectangle Fill="Transparent"/>
</ControlTemplate>

在我创建 ResizeDecorator 和 RotateDecorator 之后,但现在没关系,然后创建 DesignerItem.xaml

ResourceDictionary.MergedDictionaries>
    <ResourceDictionary Source="MoveThumb.xaml"/>
    <ResourceDictionary Source="ResizeDecorator.xaml"/>
    <ResourceDictionary Source="RotateDecorator.xaml"/>
</ResourceDictionary.MergedDictionaries>

<!-- ContentControl style to move, resize and rotate items -->
<Style x:Key="DesignerItemStyle" TargetType="ContentControl">
    <Setter Property="MinHeight" Value="50"/>
    <Setter Property="MinWidth" Value="50"/>
    <Setter Property="RenderTransformOrigin" Value="0.5,0.5"/>
    <Setter Property="SnapsToDevicePixels" Value="true"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ContentControl">
                <Grid DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}">
                    <Control Name="RotateDecorator"
                             Template="{StaticResource RotateDecoratorTemplate}"
                             Visibility="Collapsed"/>
                    <s:MoveThumb Template="{StaticResource MoveThumbTemplate}"
                                 Cursor="SizeAll"/>
                    <Control x:Name="ResizeDecorator"
                             Template="{StaticResource ResizeDecoratorTemplate}"
                             Visibility="Collapsed"/>
                    <ContentPresenter Content="{TemplateBinding ContentControl.Content}"/>
                </Grid>
                <ControlTemplate.Triggers>
                    <Trigger Property="Selector.IsSelected" Value="True">
                        <Setter TargetName="ResizeDecorator" Property="Visibility" Value="Visible"/>
                        <Setter TargetName="RotateDecorator" Property="Visibility" Value="Visible"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

并且我尝试在我的 MainWindow.xaml 中为我的 ContentControls

绑定此样式
<Window.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Resources/DesignerItem.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Window.Resources>

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="100"/>
        <ColumnDefinition Width="*"/>
         </Grid.ColumnDefinitions>

    <StackPanel Background="Gray" Grid.RowSpan="2">
        <TextBlock Text="Shapes"                      
                   FontSize="18"
                   TextAlignment="Center" />
        <Button Content="Initial Node" />
        <Button Content="Final Node" />
        <Button Content="Line" />
        <Button Content="Action" />
        <Button Content="Decision Node" />
        <Button Content="Actor" />
        <Button Content="Class" Command="{Binding Path=CreateRectangle}"/>
        <Button Content="Use Case" Command="{Binding Path = CreateUseCase}"/>
    </StackPanel>

    <Grid Grid.Column="2">

        <ItemsControl ItemsSource="{Binding Path= BaseShapeViewModels}">

            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>

                    <Canvas/>

                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>

            <ItemsControl.Resources>
                <DataTemplate DataType="{x:Type viewModel:EllipseViewModel}">
                    <ContentControl
                                           Selector.IsSelected="True"
                                           Style="{StaticResource DesignerItemStyle}">

                        <Ellipse 
                                           Fill="{Binding Fill}"                                               
                                           IsHitTestVisible="False"/>

                    </ContentControl>
                </DataTemplate>

                <DataTemplate DataType="{x:Type viewModel:RectangleViewModel}">
                    <ContentControl             
                                           Selector.IsSelected="True"
                                           Style="{StaticResource DesignerItemStyle}">

                        <Rectangle 
                                           Fill="{Binding Fill}"                                              
                                           IsHitTestVisible="False"/>
                    </ContentControl>
                </DataTemplate>
            </ItemsControl.Resources>

            <ItemsControl.ItemContainerStyle>
                <Style TargetType="ContentPresenter">
                    <Setter Property="Canvas.Left" Value="{Binding Left, Mode=TwoWay}"/>
                    <Setter Property="Canvas.Top" Value="{Binding Top, Mode=TwoWay}"/>
                    <Setter Property="Width" Value="{Binding Width, Mode=TwoWay}"/>
                    <Setter Property="Height" Value="{Binding Height,Mode=TwoWay}"/>
                </Style>
            </ItemsControl.ItemContainerStyle>
        </ItemsControl>
        
    </Grid>
</Grid>
               

您不能将项目拖过 Canvas,因为您使用的是 ItemsControl。此控件将每个项目包装到一个容器中。您当前正在尝试拖动此容器的内容,而不是容器。它是实际位于 Canvas.

内的容器

为了降低复杂性,我建议实施扩展 ItemsControl 的自定义控件。这样您就可以使项目容器本身可拖动。或者,您可以实施附加行为。

要提供调整大小和旋转用户界面(例如调整手柄大小),我建议使用 Adorner

以下示例实现了一个支持拖动、缩放和旋转的自定义项目容器,以及一个将此容器用于其项目的扩展 ItemsControl
由于可拖动元素是 ContentControl,因此不需要任何额外的包装。这将简化用法,如下所示。

实施DraggableContentControl

DraggableContentControl.cs
DraggableContentControl class 用作 ItemsCanvas 控件的项目容器(专门的 ItemsControl - 见下文)。

此外,DraggableContentControl可以用作stand-alone控件:DraggableContentControl是一种非常方便且可重复使用的方式,可以将鼠标拖动、缩放和旋转添加到任何UIElementCanvas 上。例如,要允许将 Rectangle 拖过 Canvas,只需将 Rectangle 分配给 DraggableContentControl.Content 属性 并将 DraggableContentControl 放置在 Canvas:

<Canvas Width="1000" Height="1000">

 <!-- Ad mouse drag, rotation and scaling to a Rectangle -->
  <DraggableContentControl Canvas.Left="50" 
                           Canvas.Top="50" 
                           Angle="45">
        <Rectangle Height="100" 
                   Width="100" 
                   Fill="Coral" />
  </DraggableContentControl>
</Canvas>

Rotation : 设置 DraggableContentControl.Angle 允许旋转托管元素。
缩放:设置DraggableContentControl.WidthDraggableContentControl.Height允许scale/resize内容(自动,因为DraggableContentControl.Content值被包装成Viewbox - 请参阅下面的默认值 Style)。
默认情况下,当大小设置为 Auto 时,DraggableContentControl 将动态采用其内容的大小。

public class DraggableContentControl : ContentControl
{
  public static readonly DependencyProperty AngleProperty = DependencyProperty.Register(
    "Angle",
    typeof(double),
    typeof(DraggableContentControl),
    new PropertyMetadata(default(double), DraggableContentControl.OnAngleChanged));

  public double Angle
  {
    get => (double) GetValue(DraggableContentControl.AngleProperty);
    set => SetValue(DraggableContentControl.AngleProperty, value);
  }

  private RotateTransform RotateTransform { get; set; }
  private IInputElement ParentInputElement { get; set; }
  private bool IsDragActive { get; set; }
  private Point DragOffset { get; set; }

  static DraggableContentControl()
  {
    // Remove this line if you don't plan to define the default Style
    // inside the Generic.xaml file.
    DefaultStyleKeyProperty.OverrideMetadata(typeof(DraggableContentControl), new FrameworkPropertyMetadata(typeof(DraggableContentControl)));
  }

  public DraggableContentControl()
  {
    this.PreviewMouseLeftButtonDown += InitializeDrag_OnLeftMouseButtonDown;
    this.PreviewMouseLeftButtonUp += CompleteDrag_OnLeftMouseButtonUp;
    this.PreviewMouseMove += Drag_OnMouseMove;
    this.RenderTransformOrigin = new Point(0.5, 0.5);

    var transformGroup = new TransformGroup();
    this.RotateTransform = new RotateTransform();
    transformGroup.Children.Add(this.RotateTransform);
    this.RenderTransform = transformGroup;
  }

  #region Overrides of FrameworkElement

  public override void OnApplyTemplate()
  {
    base.OnApplyTemplate();

    // Parent is required to calculate the relative mouse coordinates.
    DependencyObject parentControl = this.Parent;
    if (parentControl == null
        && !TryFindParentElement(this, out parentControl)
        && !(parentControl is IInputElement))
    {
      return;
    }

    this.ParentInputElement = parentControl as IInputElement;
  }

  #endregion

  private void InitializeDrag_OnLeftMouseButtonDown(object sender, MouseButtonEventArgs e)
  {
    // Do nothing if disabled
    this.IsDragActive = this.IsEnabled;
    if (!this.IsDragActive)
    {
      return;
    }

    Point relativeDragStartPosition = e.GetPosition(this.ParentInputElement);

    // Calculate the drag offset to allow the content to be dragged 
    // relative to the clicked coordinates (instead of the top-left corner)
    this.DragOffset = new Point(
      relativeDragStartPosition.X - Canvas.GetLeft(this),
      relativeDragStartPosition.Y - Canvas.GetTop(this));

    // Prevent other controls from stealing mouse input while dragging
    CaptureMouse();
  }

  private void CompleteDrag_OnLeftMouseButtonUp(object sender, MouseButtonEventArgs e)
  {
    this.IsDragActive = false;
    ReleaseMouseCapture();
  }

  private void Drag_OnMouseMove(object sender, MouseEventArgs e)
  {
    if (!this.IsDragActive)
    {
      return;
    }

    Point currentPosition = e.GetPosition(this.ParentInputElement);

    // Apply the drag offset to drag relative to the 
    // initial mouse down coordinates (instead of the top-left corner)
    currentPosition.Offset(-this.DragOffset.X, -this.DragOffset.Y);
    Canvas.SetLeft(this, currentPosition.X);
    Canvas.SetTop(this, currentPosition.Y);
  }

  private static void OnAngleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
    (d as DraggableContentControl).RotateTransform.Angle = (double) e.NewValue;
  }

  private bool TryFindParentElement<TParent>(DependencyObject child, out TParent resultElement)
    where TParent : DependencyObject
  {
    resultElement = null;

    if (child == null)
    {
      return false;
    }

    DependencyObject parentElement = VisualTreeHelper.GetParent(child);

    if (parentElement is TParent)
    {
      resultElement = parentElement as TParent;
      return true;
    }

    return TryFindParentElement(parentElement, out resultElement);
  }
}

实施ItemsCanvas

ItemsCanvas.cs
ItemsCanvas 是一个 ItemsControl 配置为使用 DraggableContentControl 作为项目容器。

class ItemsCanvas : ItemsControl
{
  static ItemsCanvas()
  {
    // Remove this line if you don't plan to define the default Style
    // inside the Generic.xaml file.
    DefaultStyleKeyProperty.OverrideMetadata(typeof(ItemsCanvas), new FrameworkPropertyMetadata(typeof(ItemsCanvas)));
  }

  #region Overrides of ItemsControl

  protected override bool IsItemItsOwnContainerOverride(object item) => item is DraggableContentControl;

  protected override DependencyObject GetContainerForItemOverride() => new DraggableContentControl();

  #endregion
}

Generic.xaml

DraggableContentControlItemsCanvas 的默认样式:

<Style TargetType="DraggableContentControl">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="DraggableContentControl">
        <Border Background="{TemplateBinding Background}"
                BorderBrush="{TemplateBinding BorderBrush}"
                BorderThickness="{TemplateBinding BorderThickness}">

          <!-- 
            Optional: wrapping the content into a Viewbox
            allows to automatically resize/scale the content 
            based on the container's (DraggableContentControl) size.
          -->
          <Viewbox Stretch="Fill">
            <ContentPresenter />
          </Viewbox>
        </Border>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>


<Style TargetType="ItemsCanvas">
  <Setter Property="ItemsPanel">
    <Setter.Value>
      <ItemsPanelTemplate>
        <Canvas />
      </ItemsPanelTemplate>
    </Setter.Value>
  </Setter>
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="ItemsCanvas">
        <Border Background="{TemplateBinding Background}"
                BorderBrush="{TemplateBinding BorderBrush}"
                BorderThickness="{TemplateBinding BorderThickness}">
          <ScrollViewer>
            <ItemsPresenter />
          </ScrollViewer>
        </Border>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

例子
此示例基于您的数据模型。

<Window>
  <Window.Resources>
    <DataTemplate DataType="{x:Type RectangleViewModel}">
      <Rectangle Height="{Binding Height}" 
                 Width="{Binding Width}"
                 Fill="{Binding Fill}" />
    </DataTemplate>
    <DataTemplate DataType="{x:Type EllipseViewModel}">
      <Ellipse Height="{Binding Height}" 
               Width="{Binding Width}"
               Fill="{Binding Fill}" />
    </DataTemplate>
  </Window.Resources>

  <ItemsCanvas ItemsSource="{Binding BaseShapeViewModels}" 
               Height="500" 
               Width="500">
    <ItemsCanvas.ItemContainerStyle>

      <!-- Optional Style that adds the possibility to position items on the Canvas 
           using the e.g., Top and Left properties of the data model. 
      -->
      <Style TargetType="main:DraggableContentControl">
        <Setter Property="Canvas.Left" Value="{Binding Left, Mode=TwoWay}" />
        <Setter Property="Canvas.Top" Value="{Binding Top, Mode=TwoWay}" />
      </Style>
    </ItemsCanvas.ItemContainerStyle>
  </ItemsCanvas>
</Window>

BionocCode 解决方案工作正常,但我找到了另一种方法,我只更改了两种方法

private void MoveThumb_DragDelta(object sender, DragDeltaEventArgs e)
    {
        ContentControl designerItem = DataContext as ContentControl;
       

        if (designerItem != null)
        {
            Point dragDelta = new Point(e.HorizontalChange, e.VerticalChange);
            
            RotateTransform rotateTransform = designerItem.RenderTransform as RotateTransform;
            if (rotateTransform != null)
            {
                dragDelta = rotateTransform.Transform(dragDelta);
            }

            /*use this*/
            var model = designerItem.DataContext as BaseShapeViewModel;
            model.Left += dragDelta.X;
            model.Top += dragDelta.Y;

            /* instead of this*/
            //double left = Canvas.GetLeft(item);
            //double top = Canvas.GetTop(item);
            //Canvas.SetLeft(item, left + e.HorizontalChange);
            //Canvas.SetTop(item, top + e.VerticalChange);
        }

    }

查看最后 3 行,我能够从我的 ContentControl 中获取我的 ViewModel 并更改它们的属性,当我更改它们时,ObservableCollection 中的 ViewModel 对象的属性也会更改并且一切都呈现得很好