在 WPF 中通过拖放绘制图表弧
我正在尝试执行拖放方法来在图表中创建关系,直接类似于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
绑定到视图模型,而 Canvas
上的实体和关系又绑定到各自的视图模型。一些与整体图相关的 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>
以及用于实体和关系:
<!-- 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+ Solution Builder 中实施更好的方法。
I'm trying to perform a drag and drop approach to creating relationships in a diagram, directly analagous to SQL Server Management Studio diagramming tools. For example, in the illustration below, the user would drag CustomerID
from the User
entity to the Customer
entity and create a foreign key relationship between the two.
The key desired feature is that a temporary arc path would be drawn as the user performs the drag operation, following the mouse. Moving entities or relationships once created isn't the issue I'm running into.
Some reference XAML corresponding to an entity on the diagram above:
<!-- 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>
My current approach to doing this is to:
1) Initiate the drag operation in a child control of the entity, such as:
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) Create a connector adorner when the drag operation leaves the entity, such as:
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) Draw the arc path as the mouse is being moved in the connector adorner, such as:
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();
}
}
The diagram Canvas
is bound to a view model, and the entities and relationships on the Canvas
are in turn bound to respective view models. Some XAML relating to the overall diagram:
<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>
and DataTemplate
s for the entites and relationships:
<!-- 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>
Issue: The issue is that once the drag operation begins, mouse moves are no longer tracked and the connector adorner is unable to draw the arc as it does in other contexts. If I release the mouse and click again, then the arc starts drawing, but then I've lost my source object. I'm trying to figure a way to pass the source object in conjunction with mouse movement.
Bounty: Circling back to this issue, I currently plan to not use drag and drop directly to do this. I currently plan to add a DragItem and IsDragging DependencyProperty
for the diagram control, which would hold the item being dragged, and flag if a drag operation is occuring. I could then use DataTrigger
s to change the Cursor
and Adorner
visibility based on IsDragging, and could use DragItem for the drop operation.
(But, I'm looking to award a bounty on another interesting approach. Please comment if more information or code is needed to clarify this question.)
Edit: Lower priority, but I'm still on the lookout for a better solution for a drag and drop diagramming approach. Want to implement a better approach in the open source Mo+ Solution Builder.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
这是一个相当复杂的答案。如果有任何部分不清楚,请告诉我。
我目前正在尝试解决类似的问题。就我而言,我想将 ListBox ItemsSource 绑定到一个集合,然后将该集合中的每个项目表示为一个节点(即可拖动对象)或一个连接(即一条线)在拖动节点时重新绘制自身的节点之间。我将向您展示我的代码和详细信息,我认为您可能需要进行更改以满足您的需求。
拖动
拖动是通过设置
Dragger
类拥有的附加属性来完成的。在我看来,这比使用MoveThumb
执行拖动具有优势,因为使对象可拖动不需要更改其控制模板。我的第一个实现实际上在控件模板中使用了MoveThumb
来实现拖动,但我发现这样做会使我的应用程序非常脆弱(添加新功能通常会破坏拖动)。这是拖动器的代码:我相信
Dragger
要求对象位于Canvas
或CustomCanvas
上,但没有任何好处这样做的原因,除了懒惰之外。您可以轻松修改它以适用于任何面板。 (它在我的积压工作中!)。Dragger
类还使用PavilionVisualTreeHelper.GetAncestor()
辅助方法,该方法只需爬上 Visual Tree 查找适当的元素。其代码如下。使用
Dragger
类非常简单。只需在相应控件的 xaml 标记中设置Dragger.IsDraggable = true
即可。或者,您可以注册Dragger.IsDragging
事件,该事件从被拖动的元素中冒出,以执行您可能需要的任何处理。更新连接位置
我的通知连接需要重新绘制的机制有点草率,并且肯定需要重新寻址。
Connection 包含两个 FrameworkElement 类型的 DependencyProperties:Start 和 End。在 PropertyChangedCallbacks 中,我尝试将它们转换为 DragAwareListBoxItems(我需要将其设为一个接口以获得更好的可重用性)。如果转换成功,我将注册到
DragAwareListBoxItem.ConnectionDragging
事件。 (坏名字,不是我的!)。当该事件触发时,连接将重新绘制其路径。DragAwareListBoxItem 实际上并不知道它何时被拖动,因此必须有人告诉它。由于 ListBoxItem 在我的可视化树中的位置,它永远不会听到
Dragger.IsDragging
事件。因此,为了告诉它它正在被拖动,ListBox 会侦听该事件并通知相应的 DragAwareListBoxItem。本打算发布
Connection
、DragAwareListBoxItem
和ListBox_IsDragging
的代码,但我认为这里的代码太多了,难以阅读。您可以在 http://code 查看该项目.google.com/p/pavilion/source/browse/#hg%2FPavilionDesignerTool%2FPavilion.NodeDesigner或使用 hg clone https://code.google.com/p/pavilion/ .它是 MIT 许可下的开源项目,因此您可以根据需要进行调整。作为警告,没有稳定的版本,因此它可能随时更改。
连接性
与连接更新一样,我不会粘贴代码。相反,我将告诉您要检查项目中的哪些类以及每个类中要查找的内容。
从用户的角度来看,创建连接的工作原理如下。用户右键单击节点。这将打开一个上下文菜单,用户可以从中选择“创建新连接”。该选项创建一条直线,其起点以所选节点为根,终点跟随鼠标。如果用户单击另一个节点,则会在两者之间创建连接。如果用户单击其他任何位置,则不会创建连接并且该线会消失。
这个过程涉及两个类。
ConnectionManager
(实际上并不管理任何连接)包含附加属性。使用控件将 ConnectionManager.IsConnectable 属性设置为 true,并将 ConnectionManager.MenuItemInvoker 属性设置为应启动该进程的菜单项。此外,可视化树中的某些控件必须侦听 ConnectionPending 路由事件。这是实际创建连接的地方。当选择菜单项时,ConnectionManager 将创建一个 LineAdorner。 ConnectionManager 侦听 LineAdorner LeftClick 事件。当该事件被触发时,我执行命中测试以查找所选的控件。然后,我引发 ConnectionPending 事件,将我想要在其间创建连接的两个控件传递到事件参数中。由事件的订阅者来实际完成这项工作。
This is a fairly involved answer. Let me know if any part of it isn't clear.
I’m currently trying to solve a similar problem. In my case, I want to bind my ListBox ItemsSource to a collection and then represent every item in that collection as either a node i.e a draggable object or a connection i.e a line between nodes that redraws itself when the nodes are dragged. I’ll show you my code and detail where I think you might need to make changes to fit your needs.
Dragging
Dragging is accomplished by setting attached properties owned by the
Dragger
class. In my opinion, this has an advantage over using theMoveThumb
to perform dragging in that making an object draggable does not involve changing its control template. My first implementation actually usedMoveThumb
in control templates to achieve dragging, but I found that doing so made my application very brittle (adding new features often broke the dragging). Here's the code for the Dragger:I believe that
Dragger
requires that the object be on aCanvas
orCustomCanvas
, but there isn't any good reason, besides lazyness, for this. You could easily modify it to work for any Panel. (It’s in my backlog!).The
Dragger
class is also using thePavilionVisualTreeHelper.GetAncestor()
helper method, which simply climbs the Visual Tree looking for the appropriate element. The code for that is below.Consuming the
Dragger
class is very simple. Simply setDragger.IsDraggable = true
in the appropriate control’s xaml markup. Optionally, you can register to theDragger.IsDragging
event, which bubbles up from the element being dragged, to perform any processing you might need.Updating the Connection Position
My mechanism for informing the connection that it needs to be redrawn is a little sloppy, and definitely needs readdressing.
The Connection contains two DependencyProperties of type FrameworkElement: Start and End. In the PropertyChangedCallbacks, I try to cast them as DragAwareListBoxItems (I need to make this an interface for better reusability). If the cast is successful, I register to the
DragAwareListBoxItem.ConnectionDragging
event. (Bad name, not mine!). When that event fires, the connection redraws its path.The DragAwareListBoxItem doesn’t actually know when it’s being dragged, so someone has to tell it. Because of the ListBoxItem’s position in my visual tree, it never hears the
Dragger.IsDragging
event. So to tell it that it’s being dragged, the ListBox listens to the event and and informs the appropriate DragAwareListBoxItem.The was going to post the code for the
Connection
, theDragAwareListBoxItem
, and theListBox_IsDragging
, but I think it's way too much to be readable here. You can check out the project at http://code.google.com/p/pavilion/source/browse/#hg%2FPavilionDesignerTool%2FPavilion.NodeDesigneror clone the respository with hg clone https://code.google.com/p/pavilion/ . It's an open source project under the MIT license, so you can adapt it as you see fit. As a warning, there is no stable release, so it can change at any time.
Connectability
As with the Connection Updating, I won't paste the code. Instead, I'll tell you which classes in the project to examine and what to look for in each class.
From a user perspective, here's how creating a connection works. The user right-clicks on a node. This brings up a context menu from which the user selects "Create New Connection". That option creates a straight line whose starting point is rooted to the selected node, and whose end point follows the mouse. If the user clicks on another node, then a connection is created between the two. If the user clicks anywhere else, no connection is created and the line disappears.
Two classes are involved in this process. The
ConnectionManager
(which doesn't actually manage any connections) houses Attached Properties. The consuming control sets the ConnectionManager.IsConnectable property to true and sets the ConnectionManager.MenuItemInvoker property to the menu item that should start the process. Additionally, some control in your visual tree has to listen to the ConnectionPending routed event. This is where the actual creation of the connection takes place.When the menu item is selected, the ConnectionManager creates a LineAdorner. The ConnectionManager listens to the LineAdorner LeftClick event. When that event is fired, I perform hit-testing to find the control that was selected. I then raise the ConnectionPending event, passing into the event args the two controls I want to create the connection between. It's up to the subscriber of the event to actually do the work.
我想您会想研究一下 WPF Thumb 控件。它将其中一些功能封装在一个方便的包中。
以下是 MSDN 文档:
http://msdn.microsoft .com/en-us/library/system.windows.controls.primitives.thumb.aspx
这是一个示例:
http://denisvuyka.wordpress.com/ 2007/10/13/wpf-draggable-objects-and-simple-shape-connectors/
不幸的是我在这方面没有很多经验区,但我确实认为这就是您正在寻找的。祝你好运!
I think you'll want to look into the WPF Thumb control. It wraps up some of this functionality in a convenient package.
Here's MSDN Documentation:
http://msdn.microsoft.com/en-us/library/system.windows.controls.primitives.thumb.aspx
Here's an example:
http://denisvuyka.wordpress.com/2007/10/13/wpf-draggable-objects-and-simple-shape-connectors/
Unfortunately I don't have a lot of experience in this area, but I do think that this is what you're looking for. Good luck!
如上所述,我当前的方法是不直接使用拖放,而是使用 DependencyProperties 和处理鼠标事件的组合来模拟拖放。
父图控件中的
DependencyProperties
为:IsDragging
DependencyProperty
用于在发生拖动时触发光标变化,例如:无论我需要在哪里执行
拖放
的圆弧绘制形式,我都会设置IsDragging = true
,而不是调用DragDrop.DoDragDrop
DragItem
到被拖动的源项目。在鼠标离开的实体控件内,启用在拖动期间绘制圆弧的连接器装饰器,例如:
图控件必须在拖动期间处理其他鼠标事件,例如:
图控件还必须处理“拖放”鼠标向上事件(并且它必须根据鼠标位置确定正在放置哪个实体),例如:
我仍在寻找更好的解决方案,以在拖动操作时在图表上绘制临时弧(跟随鼠标)正在发生。
As mentioned above, my current approach is to not use drag and drop directly, but to use a combination of
DependencyProperties
and handling mouse events to mimic a drag and drop.The
DependencyProperties
in the parent diagram control are:The
IsDragging
DependencyProperty
is used to trigger a cursor change when a drag is taking place, such as:Wherever I need to perform an arc drawing form of
drag and drop
, instead of callingDragDrop.DoDragDrop
, I setIsDragging = true
andDragItem
to the source item being dragged.Within the entity control on mouse leave, the connector adorner which draws the arc during the drag is enabled, such as:
The diagram control must handle additional mouse events during the drag, such as:
The diagram control must also handle the "drop" upon a mouse up event (and it must figure out which entity is being dropped on based on mouse position), such as:
I am still looking for a better solution to draw the temporary arc (following the mouse) on the diagram while a drag operation is taking place.