【问题标题】:WPF MVVM moveable shapes with itemscontrol带有项目控制的 WPF MVVM 可移动形状
【发布时间】:2016-08-11 22:17:15
【问题描述】:

在过去的两天里,我一直被困在一个(简单的?)问题上。我在互联网上搜索了很多,但我找不到一个完全解决我所遇到情况的示例(每次都忽略一个方面,这是我自己实现中的破坏因素)。

我想要什么:

创建我自己的 WPF 控件,该控件显示顶部矩形(或通常的实际形状)的图像,这些矩形在缩放和平移时保持固定。此外,这些矩形需要调整大小(待办事项)并且可以移动(现在就做)。

我希望此控件遵循 MVVM 设计模式。

我有什么:

我有一个带有 ItemsControl 的 XAML 文件。这显示了动态数量的矩形(来自我的 ViewModel)。它绑定到我的 ViewModel(一个 ObservableCollection)的 RectItems。我想将项目呈现为矩形。用户必须可以使用鼠标移动这些矩形。移动后,它应该会更新 ViewModel 中的模型实体。

XAML:

<ItemsControl ItemsSource="{Binding RectItems}">
<ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas IsItemsHost="True">

            </Canvas>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
    <Style>
        <Setter Property="Canvas.Left" Value="{Binding TopLeftX}"/>
        <Setter Property="Canvas.Top" Value="{Binding TopLeftY}"/>
    </Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
    <DataTemplate>
        <Rectangle Stroke="Black" StrokeThickness="2" Fill="Blue" Canvas.Left="0" Canvas.Top="0"
           Height="{Binding Height}" Width="{Binding Width}">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="MouseLeftButtonUp">
                    <i:InvokeCommandAction Command="{Binding ElementName=border,Path=DataContext.MouseLeftButtonUpCommand}" CommandParameter="{Binding}" />
                </i:EventTrigger>
                <i:EventTrigger EventName="MouseLeftButtonDown">
                    <i:InvokeCommandAction Command="{Binding ElementName=border,Path=DataContext.MouseLeftButtonDownCommand}" CommandParameter="{Binding}" />
                </i:EventTrigger>
                <i:EventTrigger EventName="MouseMove">
                    <i:InvokeCommandAction Command="{Binding ElementName=border,Path=DataContext.MouseMoveCommand}" CommandParameter="{Binding}" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Rectangle>
    </DataTemplate>
</ItemsControl.ItemTemplate>

视图模型:

public class PRDisplayViewModel : INotifyPropertyChanged
{
    private PRModel _prModel;

    private ObservableCollection<ROI> _ROIItems = new ObservableCollection<ROI>(); 

    public PRDisplayViewModel()
    {
        _prModel = new PRModel();

        ROI a = new ROI();
        a.Height = 100;
        a.Width = 50;
        a.TopLeftX = 50;
        a.TopLeftY = 150;

        ROI b = new ROI();
        b.Height = 200;
        b.Width = 200;
        b.TopLeftY = 200;
        b.TopLeftX = 200;

        _ROIItems.Add(a);
        _ROIItems.Add(b);

        _mouseLeftButtonUpCommand = new RelayCommand<object>(MouseLeftButtonUpInner);
        _mouseLeftButtonDownCommand = new RelayCommand<object>(MouseLeftButtonDownInner);
        _mouseMoveCommand = new RelayCommand<object>(MouseMoveInner);
    }

    public ObservableCollection<ROI> RectItems
    {
        get { return _ROIItems; }
        set { }
    }

    private bool isShapeDragInProgress = false;
    double originalLeft = Double.NaN;
    double originalTop = Double.NaN;
    Point originalMousePos;

    private ICommand _mouseLeftButtonUpCommand;

    public ICommand MouseLeftButtonUpCommand
    {
        get { return _mouseLeftButtonUpCommand; }
        set { _mouseLeftButtonUpCommand = value; }
    }

    public void MouseLeftButtonUpInner(object obj)
    {
        Console.WriteLine("MouseLeftButtonUp");

        isShapeDragInProgress = false;

        if (obj is ROI)
        {
            var shape = (ROI)obj;
            //shape.ReleaseMouseCapture();
        }
    }


    /**** More Commands for MouseLeftButtonDown and MouseMove ****/

类 ROI(稍后将驻留在 PRModel 中):

public class ROI
{
    public double Height { get; set; }

    public double TopLeftX { get; set; }

    public double TopLeftY { get; set; }

    public double Width { get; set; }
}

所以我的看法是这样的:

ItemsControl 将 ROI 对象渲染为 Rectangle。矩形上的鼠标事件由 ViewModel 中的命令处理。 ViewModel 在接收到鼠标事件后,直接处理 ROI 对象的更新。然后,视图应该重绘(假设 ROI 对象已更改),并因此生成新的 Rectangle 等。

有什么问题?

在鼠标事件处理程序中,我需要调用发生鼠标事件的矩形的 CaptureMouse() 方法。如何访问此矩形?

最有可能的实际问题是我对 MVVM 的看法是错误的。我真的应该尝试更新 ViewModel 中鼠标事件处理程序中的 ROI 对象吗?还是我应该只更新 Rectangle 对象?如果是后者,那么更新如何传播到实际的 ROI 对象?

我检查了许多其他问题,其中包括以下问题,但我仍然没有解决我的问题:

Add n rectangles to canvas with MVVM in WPF

Dragable objects in WPF in an ItemsControl?


编辑: 谢谢大家的回复。您的意见帮助很大。不幸的是,我仍然无法让这个工作(我是 WPF 的新手,但我无法想象这会这么难)。

两次新尝试,我都在 ROI 类中实现了 INotifyPropertyChanged 接口。

尝试 1: 我用 MouseDragElementBehavior 实现了拖动,如下所示:

                <ItemsControl ItemsSource="{Binding RectItems}">
                <ItemsControl.ItemsPanel>
                        <ItemsPanelTemplate>
                            <Canvas IsItemsHost="True">
                            </Canvas>
                        </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemContainerStyle>
                    <Style TargetType="ContentPresenter">
                        <!-- //-->
                        <Setter Property="Canvas.Left" Value="{Binding TopLeftX, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
                        <Setter Property="Canvas.Top" Value="{Binding TopLeftY, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
                    </Style>
                </ItemsControl.ItemContainerStyle>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Grid>
                        <Rectangle Stroke="Black" StrokeThickness="2" Canvas.Left="0" Canvas.Top="0"
                           Height="{Binding Height}" Width="{Binding Width}">
                                <i:Interaction.Behaviors>
                                    <ei:MouseDragElementBehavior/>
                                </i:Interaction.Behaviors>
                            </Rectangle>
                        </Grid>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>

效果很好!一切都是免费的,即使我放大边框(这是所有这些的父元素)。

这里的问题: 拖动一个矩形后,我在 UI 中看到了这个,但是我的 ROI 对象(链接到这个矩形)没有更新?我用 PropertyChanged UpdateSourceTrigger 指定了 TwoWay 绑定,但这仍然不起作用。

此处的问题:在这种情况下,如何更新我的 ROI 对象?我可以实现 MouseDragElementBehavior 的 DragFinished 事件,但是这里我没有得到对应的 ROI 对象? @Chris W.,我可以在哪里绑定 UIElement.Drop 处理程序?我可以在那里得到相应的 ROI 对象吗?

尝试 2: 与尝试 1 相同,但现在我还实现了 EventTriggers(就像我原来的帖子中一样)。在那里,我没有做任何更新 UI 中的矩形,但我只更新了相应的 ROI 对象。

这里的问题:这不起作用,因为我们将矩形移动了它们应该移动的向量“两倍”。可能第一个动作来自拖动本身,第二个动作是手动更新(来自我)相应的 ROI。

尝试 3: 不是使用 MouseDragElementBehavior,而是“简单地”使用 EventTriggers 实现自己的拖动(就像在我的原始帖子中一样)。我没有更新任何 Rectangle (UI),而只更新了 ROI(移动了它们的 TopLeftX 和 TopLeftY)。

这里的问题:这确实有效,除了在缩放的情况下。此外,拖动并不好,因为它有点“闪烁”,当鼠标移动得太快时,它会失去它的矩形。很明显,在 MouseDragElementBehavior 中,已经添加了更多的逻辑来使这一过程更加顺畅。

@Mark Feldman:谢谢你的回答。它还没有解决我的问题,但我喜欢在我的 ViewModel 中不使用 GUI 类的想法(比如 Mouse.GetPosition 来自己实现拖动)。使用转换器解耦,如果功能正常,我将在稍后实现。

@Will:你是对的(MVVM != 后面没有代码)。不幸的是,我现在看不到如何利用它,因为来自 MouseDragElementBehavior 的事件处理程序不知道 ROI(我需要更新 ViewModel)。


编辑 2:

正在工作的东西(至少看起来如此)如下(使用 MouseDragElementBehavior):

<ItemsControl ItemsSource="{Binding RectItems}">
                <ItemsControl.ItemsPanel>
                        <ItemsPanelTemplate>
                            <Canvas IsItemsHost="True">
                            </Canvas>
                        </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemContainerStyle>
                    <Style TargetType="ContentPresenter">

                    </Style>
                </ItemsControl.ItemContainerStyle>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Grid>
                        <Rectangle Stroke="Black" StrokeThickness="2" Canvas.Left="0" Canvas.Top="0"
                           Height="{Binding Height}" Width="{Binding Width}">
                                <i:Interaction.Behaviors>
                                    <ei:MouseDragElementBehavior X="{Binding TopLeftX, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Y="{Binding TopLeftY, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">

                                    </ei:MouseDragElementBehavior>
                                </i:Interaction.Behaviors>
                            </Rectangle>
                        </Grid>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>

我将 MouseDragElementBehavior 的 X 和 Y 属性绑定到 ViewModel 的相应属性。当我拖动矩形时,我看到相应 ROI 中的值已更新!此外,拖动时不会出现“闪烁”或其他问题。

问题仍然存在:我不得不删除 ItemContainerStyle 中用于初始化矩形位置的代码。可能 MouseDragElementBehavior 绑定中的更新也会导致此处的更新。这在拖动时可见(矩形在两个位置之间快速跳转)。

问题:使用这种方法,如何初始化矩形的位置?此外,这感觉像是一种 hack,那么有没有更合适的方法?


编辑 3:

以下代码还将在适当的位置初始化矩形:

<ItemsControl ItemsSource="{Binding RectItems}">
                <ItemsControl.ItemsPanel>
                        <ItemsPanelTemplate>
                            <Canvas IsItemsHost="True">
                            </Canvas>
                        </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemContainerStyle>
                    <Style TargetType="ContentPresenter">
                        <!-- //-->
                        <Setter Property="Canvas.Left" Value="{Binding TopLeftX, Mode=OneTime}"/>
                        <Setter Property="Canvas.Top" Value="{Binding TopLeftY, Mode=OneTime}"/>
                    </Style>
                </ItemsControl.ItemContainerStyle>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Grid>
                        <Rectangle Stroke="Black" StrokeThickness="2" Canvas.Left="0" Canvas.Top="0"
                           Height="{Binding Height}" Width="{Binding Width}">
                                <i:Interaction.Behaviors>
                                    <ei:MouseDragElementBehavior X="{Binding TopLeftX, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Y="{Binding TopLeftY, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">

                                    </ei:MouseDragElementBehavior>
                                </i:Interaction.Behaviors>
                            </Rectangle>
                        </Grid>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>

问题仍然存在:这感觉很“骇人听闻”。有没有“更好”的方法来做到这一点(到目前为止它似乎有效)?


编辑 4: 我最终使用了拇指。使用 DataTemplates 我能够从我的 ViewModel 中定义项目的外观(以便使用例如拇指),并通过 ControlTemplates 我能够定义拇指的实际外观(例如矩形)。 Thumbs 的优点是已经实现了拖放!

【问题讨论】:

  • 在你深入这个兔子洞之前。你看过MouseDragElementBehavior 并使用UIElement.Drop 事件吗?
  • 从克里斯点找到这个link。可能会有所帮助。
  • MVVM != 没有代码隐藏。你的 UI 应该控制 UI 中发生的事情,包括移动元素。像这样进行叠加的最佳方法是创建一个用户控件,其中包含一个 Canvas,负责显示和移动矩形。
  • 您能否提供完整的代码(包括您使用的行为)来展示您来之不易的解决方案?

标签: wpf xaml mvvm itemscontrol


【解决方案1】:

您的第一个问题是 ROI 不支持 INotifyPropertyChange,因此更改它们不会更新您的图形。这需要先解决。

关于事件问题,看看我的Perfy 项目。更具体地说,查看MouseCaptureBehavior 类,它负责拦截鼠标消息并将它们打包以供视图模型使用,包括提供捕获和释放功能。在我更复杂的应用程序中,我在视图和视图模型之间创建了一个契约,通常看起来像这样:

public interface IMouseArgs
{
    Point Pos { get; }
    Point ParentPos { get; }
    void Capture(bool parent = false);
    void Release(bool parent = false);
    bool Handled { get; set; }
    object Data { get;}
    bool LeftButton { get; }
    bool RightButton { get; }
    bool Shift { get; }
    void DoDragDrop(DragDropEffects allowedEffects);
}

关于这个界面有几点需要注意。首先,视图模型不关心实现是什么,这完全取决于视图。其次,视图模型需要调用 Capture/Release 处理程序,同样由视图提供。最后还有一个 Data 字段,我通常将其设置为包含单击的任何对象的 DataContext,这对于传递回视图以让它知道您正在谈论的对象是有用的。

这涵盖了视图模型方面的内容,现在我们需要实现这个视图方面。我通常使用事件触发器来执行此操作,该触发器直接绑定到视图模型中的命令处理程序,但使用转换器获取特定于视图的事件参数并将它们转换为支持我的视图模型所期望的 IMouseArgs 对象的东西:

<!--- this is in the resources block -->
<conv:MouseEventsConverter x:Key="ClickConverter" />

<!-- and this is in the ItemControl's ItemTemplate -->
<ContentControl>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="PreviewMouseDown">
            <cmd:EventToCommand
                Command="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:CanvasView},
                Path=DataContext.MouseDownCommand}"
                PassEventArgsToCommand="True"
                EventArgsConverter="{StaticResource ClickConverter}"
                EventArgsConverterParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:SchedulePanel}}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</ContentControl>

绑定本身需要一些解释...

1) 命令。此绑定是在用户单击的对象(例如您的矩形)上进行的,但命令处理程序通常位于父 ItemsControl 的 DataContext 对象中。所以我们需要使用RelativeBinding 来查找该对象并绑定到它的处理程序。

2)Path=DataContext.MouseDownCommand。应该很简单。

3)PassEventArgsToCommand="True"。这告诉 EventToCommand 我们的处理程序需要一个 MouseArgs 类型的参数。如果我们愿意,我们可以这样做,但是我们的视图模型会弄乱 GUI 对象,所以我们需要将其转换为我们自己的类型 IMouseArgs。

4) EventArgsConverter="{StaticResource ClickConverter}" 这个转换器会帮我们翻译,我会在下面展示代码。

5) EventArgsConverterParameter="... 有时我们需要将其他数据作为附加参数传递给我们的转换器。我不会讨论所有需要这样做的具体情况,但请记住这一点。在这种特定情况下,我需要找到相对于 ItemControl 父级而不是 ItemControl 本身的点。

转换器类本身可以有任何你喜欢的东西,一个简单的实现是这样的:

public class MouseEventsConverter : IEventArgsConverter
{
    public object Convert(object value, object parameter)
    {           
        var args = value as MouseEventArgs;
        var element = args.Source as FrameworkElement;
        var parent = parameter as IInputElement;
        var result = new ConverterArgs
        {
            Args = args,
            Element = element,
            Parent = parent,
            Data = element.DataContext
        };
        return result;
    }

请注意,我在这里的具体实现(ConverterArgs)实际上存储了框架元素,如果视图模型将此对象传递回任何位置的视图,您始终可以通过强制转换来获取它。

有意义吗?它看起来很复杂,但编写代码后实际上非常简单。

【讨论】:

    【解决方案2】:

    这里使用行为的完美解决方案:Move items in a canvas using MVVM。我对其进行了测试,因此它可以正常工作。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-07-10
      • 1970-01-01
      • 1970-01-01
      • 2023-03-10
      • 1970-01-01
      • 1970-01-01
      • 2011-02-19
      相关资源
      最近更新 更多