【发布时间】: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