使用itemscontrol

时间:2016-04-19 15:50:30

标签: wpf xaml mvvm itemscontrol

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

我想要什么:

创建我自己的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。 Rectangle上的鼠标事件由ViewModel中的命令处理。 ViewModel在收到鼠标事件后,直接处理ROI对象上的更新。然后,视图应该重绘(假设ROI对象已更改),因此生成新的矩形等。

有什么问题?

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

最有可能的实际问题是我对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绑定,但仍然不起作用。

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

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

问题在这里:这不起作用,因为我们移动的矩形两次&#39;他们应该的向量。可能第一个动作来自拖动本身,第二个动作是相应ROI中的手动更新(来自我)。

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

问题:这实际上有效,除了缩放的情况。此外,拖动并不好,因为它是闪烁的&#39;有点,当移动鼠标太快时,它会松开它的矩形。显然,在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>

我将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>

问题仍然存在:这感觉很糟糕&#39;。有没有更好的&#39;这样做的方式(到目前为止似乎有效)?

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

2 个答案:

答案 0 :(得分: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);
}

有关此界面的一些注意事项。首先,视图模型并不关心实现是什么,它完全取决于视图。其次,视图模型可以调用Capture / Release处理程序,由视图再次提供。最后是一个数据字段,我通常将其设置为包含单击任何对象的DataContext,对于传回视图以使其知道您正在谈论的对象非常有用。

这涵盖了视图模型方面的内容,现在我们需要实现此视图方面。我通常使用事件触发器来直接绑定到视图模型中的命令处理程序,但使用转换器来获取特定于视图的事件args并将它们转换为支持我的视图模型所期望的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)实际上存储了Framework元素,如果视图模型将此对象传递回任何地方的视图,您总是可以使用强制转换。

有意义吗?它看起来很复杂,但一旦你编码就很简单。

答案 1 :(得分:0)

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