我正在尝试执行拖放方法以在图表中创建关系,该方法直接类似于SQL Server Management Studio图表工具。例如,在下图中,用户将CustomerID
从User
实体拖动到Customer
实体,并在两者之间创建外键关系。
关键的期望功能是在用户跟随鼠标执行拖动操作时绘制一条临时弧形路径。创建实体后移动实体或关系并不是我遇到的问题。
上图中与实体相对应的一些引用XAML:
<!-- Entity diagram control -->
<Grid MinWidth="10" MinHeight="10" Margin="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*" ></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid Grid.Row="0" Grid.Column="0" IsHitTestVisible="False" Background="{StaticResource ControlDarkBackgroundBrush}">
<Label Grid.Row="0" Grid.Column="0" Style="{DynamicResource LabelDiagram}" Content="{Binding DiagramHeader, Mode=OneWay}" />
</Grid>
<ScrollViewer Grid.Row="1" Grid.Column="0" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" Background="{StaticResource ControlBackgroundBrush}" >
<StackPanel VerticalAlignment="Top">
<uent:EntityDataPropertiesDiagramControl DataContext="{Binding EntityDataPropertiesFolder}" />
<uent:CollectionEntityPropertiesDiagramControl DataContext="{Binding CollectionEntityPropertiesFolder}" />
<uent:DerivedEntityDataPropertiesDiagramControl DataContext="{Binding DerivedEntityDataPropertiesFolder}" />
<uent:ReferenceEntityPropertiesDiagramControl DataContext="{Binding ReferenceEntityPropertiesFolder}" />
<uent:MethodsDiagramControl DataContext="{Binding MethodsFolder}" />
</StackPanel>
</ScrollViewer>
<Grid Grid.RowSpan="2" Margin="-10">
<lib:Connector x:Name="LeftConnector" Orientation="Left" VerticalAlignment="Center" HorizontalAlignment="Left" Visibility="Collapsed"/>
<lib:Connector x:Name="TopConnector" Orientation="Top" VerticalAlignment="Top" HorizontalAlignment="Center" Visibility="Collapsed"/>
<lib:Connector x:Name="RightConnector" Orientation="Right" VerticalAlignment="Center" HorizontalAlignment="Right" Visibility="Collapsed"/>
<lib:Connector x:Name="BottomConnector" Orientation="Bottom" VerticalAlignment="Bottom" HorizontalAlignment="Center" Visibility="Collapsed"/>
</Grid>
</Grid>
我目前执行此操作的方法是:
1)在实体的子控件中启动拖动操作,例如:
protected override void OnPreviewMouseMove(MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed)
{
dragStartPoint = null;
}
else if (dragStartPoint.HasValue)
{
Point? currentPosition = new Point?(e.GetPosition(this));
if (currentPosition.HasValue && (Math.Abs(currentPosition.Value.X - dragStartPoint.Value.X) > 10 || Math.Abs(currentPosition.Value.Y - dragStartPoint.Value.Y) > 10))
{
DragDrop.DoDragDrop(this, DataContext, DragDropEffects.Link);
e.Handled = true;
}
}
}
2)当拖动操作离开实体时,创建一个连接器装饰物,例如:
protected override void OnDragLeave(DragEventArgs e)
{
base.OnDragLeave(e);
if (ParentCanvas != null)
{
AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(ParentCanvas);
if (adornerLayer != null)
{
ConnectorAdorner adorner = new ConnectorAdorner(ParentCanvas, BestConnector);
if (adorner != null)
{
adornerLayer.Add(adorner);
e.Handled = true;
}
}
}
}
3)在连接器装饰器中移动鼠标时绘制弧形路径,例如:
protected override void OnMouseMove(MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
if (!IsMouseCaptured) CaptureMouse();
HitTesting(e.GetPosition(this));
pathGeometry = GetPathGeometry(e.GetPosition(this));
InvalidateVisual();
}
else
{
if (IsMouseCaptured) ReleaseMouseCapture();
}
}
图
Canvas
绑定(bind)到 View 模型,并且Canvas
上的实体和关系又绑定(bind)到相应的 View 模型。与整个图表有关的一些XAML:<ItemsControl ItemsSource="{Binding Items, Mode=OneWay}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<lib:DesignerCanvas VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.VirtualizationMode="Recycling"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Left" Value="{Binding X}"/>
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
<Setter Property="Canvas.Width" Value="{Binding Width}"/>
<Setter Property="Canvas.Height" Value="{Binding Height}"/>
<Setter Property="Canvas.ZIndex" Value="{Binding ZIndex}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
和
DataTemplate
的实体和关系:<!-- diagram relationship -->
<DataTemplate DataType="{x:Type dvm:DiagramRelationshipViewModel}">
<lib:Connection />
</DataTemplate>
<!-- diagram entity -->
<DataTemplate DataType="{x:Type dvm:DiagramEntityViewModel}">
<lib:DesignerItem>
<lib:EntityDiagramControl />
</lib:DesignerItem>
</DataTemplate>
问题:问题是,一旦开始拖动操作,就不再跟踪鼠标移动,并且连接器装饰器无法像在其他情况下那样绘制弧线。如果释放鼠标并再次单击,则弧开始绘制,但随后丢失了源对象。我试图找到一种方法来结合鼠标移动传递源对象。
赏金:回过头来看这个问题,我目前不打算直接使用拖放操作来完成此任务。我目前计划为图表控件添加一个DragItem和IsDragging
DependencyProperty
,它们将保留要拖动的项目,并在发生拖动操作时进行标记。然后,我可以使用DataTrigger
更改基于IsDragging的Cursor
和Adorner
可见性,并可以使用DragItem进行放置操作。(但是,我希望奖励另一种有趣的方法。请发表评论,如果需要更多信息或代码来澄清此问题。)
编辑:较低的优先级,但我仍在寻找更好的拖放图表方法解决方案。希望在开源Mo+解决方案生成器中实现更好的方法。
最佳答案
这是一个相当复杂的答案。让我知道其中的任何部分不清楚。
我目前正在尝试解决类似的问题。就我而言,我想将我的ListBox ItemsSource绑定(bind)到一个集合,然后将该集合中的每个项目表示为节点(即可拖动对象)或连接(即节点之间的线,当节点被拖动时会重绘自身)。我将向您展示我的代码和详细信息,我认为您可能需要根据自己的需要进行更改。
拖曳
拖动是通过设置Dragger
类拥有的附加属性来完成的。我认为,这比使用MoveThumb
进行拖动具有一个优势,因为使对象成为可拖动对象并不涉及更改其控制模板。我的第一个实现实际上是在控制模板中使用MoveThumb
来实现拖动,但是我发现这样做使我的应用程序非常脆弱(添加新功能通常会破坏拖动)。这是Dragger的代码:
public static class Dragger
{
private static FrameworkElement currentlyDraggedElement;
private static FrameworkElement CurrentlyDraggedElement
{
get { return currentlyDraggedElement; }
set
{
currentlyDraggedElement = value;
if (CurrentlyDraggedElement != null)
{
CurrentlyDraggedElement.MouseMove += new MouseEventHandler(CurrentlyDraggedElement_MouseMove);
CurrentlyDraggedElement.MouseLeftButtonUp +=new MouseButtonEventHandler(CurrentlyDraggedElement_MouseLeftButtonUp);
}
}
}
private static ItemPreviewAdorner adornerForDraggedItem;
private static ItemPreviewAdorner AdornerForDraggedItem
{
get { return adornerForDraggedItem; }
set { adornerForDraggedItem = value; }
}
#region IsDraggable
public static readonly DependencyProperty IsDraggableProperty = DependencyProperty.RegisterAttached("IsDraggable", typeof(Boolean), typeof(Dragger),
new FrameworkPropertyMetadata(IsDraggable_PropertyChanged));
public static void SetIsDraggable(DependencyObject element, Boolean value)
{
element.SetValue(IsDraggableProperty, value);
}
public static Boolean GetIsDraggable(DependencyObject element)
{
return (Boolean)element.GetValue(IsDraggableProperty);
}
#endregion
#region IsDraggingEvent
public static readonly RoutedEvent IsDraggingEvent = EventManager.RegisterRoutedEvent("IsDragging", RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(Dragger));
public static event RoutedEventHandler IsDragging;
public static void AddIsDraggingHandler(DependencyObject d, RoutedEventHandler handler)
{
UIElement uie = d as UIElement;
if (uie != null)
{
uie.AddHandler(Dragger.IsDraggingEvent, handler);
}
}
public static void RemoveIsDraggingEventHandler(DependencyObject d, RoutedEventHandler handler)
{
UIElement uie = d as UIElement;
if (uie != null)
{
uie.RemoveHandler(Dragger.IsDraggingEvent, handler);
}
}
#endregion
public static void IsDraggable_PropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if ((bool)args.NewValue == true)
{
FrameworkElement element = (FrameworkElement)obj;
element.PreviewMouseLeftButtonDown += new MouseButtonEventHandler(itemToBeDragged_MouseLeftButtonDown);
}
}
private static void itemToBeDragged_MouseLeftButtonDown(object sender, MouseEventArgs e)
{
var element = sender as FrameworkElement;
if (element != null)
{
CurrentlyDraggedElement = element;
}
}
private static void CurrentlyDraggedElement_MouseMove(object sender, MouseEventArgs e)
{
var element = sender as FrameworkElement;
if (element.IsEnabled == true)
{
element.CaptureMouse();
//RaiseIsDraggingEvent();
DragObject(sender, new Point(Mouse.GetPosition(PavilionVisualTreeHelper.GetAncestor(element, typeof(CustomCanvas)) as CustomCanvas).X,
Mouse.GetPosition(PavilionVisualTreeHelper.GetAncestor(element, typeof(CustomCanvas)) as CustomCanvas).Y));
}
}
private static void CurrentlyDraggedElement_MouseLeftButtonUp(object sender, MouseEventArgs e)
{
FrameworkElement element = sender as FrameworkElement;
element.MouseMove -= new MouseEventHandler(CurrentlyDraggedElement_MouseMove);
element.ReleaseMouseCapture();
CurrentlyDraggedElement = null;
}
private static void DragObject(object sender, Point startingPoint)
{
FrameworkElement item = sender as FrameworkElement;
if (item != null)
{
var canvas = PavilionVisualTreeHelper.GetAncestor(item, typeof(CustomCanvas)) as CustomCanvas;
double horizontalPosition = Mouse.GetPosition(canvas).X - item.ActualWidth/2;
double verticalPosition = Mouse.GetPosition(canvas).Y - item.ActualHeight/2;
item.RenderTransform = ReturnTransFormGroup(horizontalPosition, verticalPosition);
item.RaiseEvent(new IsDraggingRoutedEventArgs(item, new Point(horizontalPosition, verticalPosition), IsDraggingEvent));
}
}
private static TransformGroup ReturnTransFormGroup(double mouseX, double mouseY)
{
TransformGroup transformGroup = new TransformGroup();
transformGroup.Children.Add(new TranslateTransform(mouseX, mouseY));
return transformGroup;
}
}
public class IsDraggingRoutedEventArgs : RoutedEventArgs
{
public Point LocationDraggedTo { get; set;}
public FrameworkElement ElementBeingDragged { get; set; }
public IsDraggingRoutedEventArgs(DependencyObject elementBeingDragged, Point locationDraggedTo, RoutedEvent routedEvent)
: base(routedEvent)
{
this.ElementBeingDragged = elementBeingDragged as FrameworkElement;
LocationDraggedTo = locationDraggedTo;
}
}
我相信
Dragger
要求对象位于Canvas
或CustomCanvas
上,但是除了懒惰之外,没有其他任何理由。您可以轻松地对其进行修改以使其适用于任何面板。 (这是我的待办事项!)。Dragger
类还使用PavilionVisualTreeHelper.GetAncestor()
helper方法,该方法只是爬上可视树以查找适当的元素。下面的代码。 /// <summary>
/// Gets ancestor of starting element
/// </summary>
/// <param name="parentType">Desired type of ancestor</param>
public static DependencyObject GetAncestor(DependencyObject startingElement, Type parentType)
{
if (startingElement == null || startingElement.GetType() == parentType)
return startingElement;
else
return GetAncestor(VisualTreeHelper.GetParent(startingElement), parentType);
}
消费
Dragger
类非常简单。只需在相应控件的xaml标记中设置Dragger.IsDraggable = true
。 (可选)您可以注册到Dragger.IsDragging
事件,该事件从被拖动的元素起泡,以执行您可能需要的任何处理。更新连接位置
我通知连接需要重新绘制的机制有些草率,而且绝对需要重新处理。
该连接包含两个FrameworkElement类型的DependencyProperty:开始和结束。在PropertyChangedCallbacks中,我尝试将它们强制转换为DragAwareListBoxItems(我需要将其设置为一个接口(interface),以实现更好的可重用性)。如果转换成功,我将注册到
DragAwareListBoxItem.ConnectionDragging
事件。 (坏名字,不是我的!)。当该事件触发时,连接将重绘其路径。DragAwareListBoxItem实际上并不知道何时拖动它,因此必须有人告诉它。由于ListBoxItem在我的可视树中的位置,因此它从未听到
Dragger.IsDragging
事件。为了告诉它正在被拖动,ListBox会监听该事件并通知适当的DragAwareListBoxItem。将会发布
Connection
,DragAwareListBoxItem
和ListBox_IsDragging
的代码,但我认为在这里难以理解。您可以在http://code.google.com/p/pavilion/source/browse/#hg%2FPavilionDesignerTool%2FPavilion.NodeDesigner上 checkout 项目或使用hg clone https://code.google.com/p/pavilion/克隆存储库。这是MIT许可下的一个开源项目,因此您可以根据需要进行调整。作为警告,没有稳定的发行版,因此它可以随时更改。
连通性
与连接更新一样,我不会粘贴代码。相反,我将告诉您项目中要检查的类以及在每个类中要查找的内容。
从用户的角度来看,这是创建连接的工作方式。用户在节点上单击鼠标右键。这将弹出一个上下文菜单,用户可以从中选择“创建新连接”。该选项将创建一条直线,其起点以选定节点为根,终点跟随鼠标。如果用户单击另一个节点,则会在两者之间创建连接。如果用户单击其他任何地方,则不会创建任何连接,并且该行也会消失。
此过程涉及两个类。
ConnectionManager
(实际上不管理任何连接)包含附加属性。使用方控件将ConnectionManager.IsConnectable属性设置为true,并将ConnectionManager.MenuItemInvoker属性设置为应启动该过程的菜单项。此外,视觉树中的某些控件必须监听ConnectionPending路由事件。这是实际创建连接的地方。选择菜单项后,ConnectionManager将创建LineAdorner。 ConnectionManager监听LineAdorner LeftClick事件。当该事件被触发时,我执行 HitTest 以找到所选的控件。然后,我引发ConnectionPending事件,将要在其之间创建连接的两个控件传递给事件args。事件的订阅者必须实际完成工作。