带有itemscontrol的WPF MVVM可移动形状

use*_*266 6 wpf xaml itemscontrol mvvm

在过去的两天里,我一直坚持一个(简单?)问题.我在互联网上搜索了很多,但我找不到一个能够解决我完全情况的例子(每次遗漏一个方面,这是我自己实现的突破因素).

我想要什么:

创建我自己的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>
Run Code Online (Sandbox Code Playgroud)

视图模型:

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 ****/
Run Code Online (Sandbox Code Playgroud)

类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; }
}
Run Code Online (Sandbox Code Playgroud)

所以我看到它的方式如下:

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

问题是什么?

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

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

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

在WPF中使用MVVM将n个矩形添加到画布

在ItemsControl中WPF中的可拖动对象?


编辑: 谢谢大家的回复.你的意见很有帮助.不幸的是,我仍然无法使这个工作(我是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>
Run Code Online (Sandbox Code Playgroud)

这很完美!一切都是免费的,即使我放大边框(这是所有这一切的父元素).

问题在于:拖动矩形后,我在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自己实现draggin).如果功能正常,我将在稍后实现解耦使用转换器.

@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>
Run Code Online (Sandbox Code Playgroud)

我将MouseDragElementBehavior的X和Y属性绑定到ViewModel的相应属性.当我拖动矩形时,我看到相应ROI中的值已更新!此外,拖动时没有"闪烁"或其他问题.

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

问题:使用这种方法,我如何初始化矩形的位置?此外,这感觉就像一个黑客,所以有更合适的方法吗?


编辑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>
Run Code Online (Sandbox Code Playgroud)

问题仍然存在:这感觉"很烂".有没有"更好"的方式来做到这一点(到目前为止似乎有效)?


编辑4:我最终使用了Thumbs.使用DataTemplates我能够从我的ViewModel定义一个项目的外观(所以使用例如Thumbs),并通过ControlTemplates我能够定义拇指实际上应该是什么样子(例如,一个矩形).Thumbs的优势在于已经实现了拖放操作!

Mar*_*man 0

那么您的第一个问题是 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);
}
Run Code Online (Sandbox Code Playgroud)

关于这个界面有几点需要注意。首先,视图模型不关心实现是什么,这完全取决于视图。其次,还有供视图模型调用的捕获/释放处理程序,同样由视图提供。最后有一个数据字段,我通常将其设置为包含单击的任何对象的 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>
Run Code Online (Sandbox Code Playgroud)

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

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

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;
    }
Run Code Online (Sandbox Code Playgroud)

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

合理?它看起来很复杂,但实际上一旦编写代码就非常简单。