【问题标题】:Drawing diagram arcs with drag and drop in WPF在 WPF 中通过拖放绘制图表弧
【发布时间】:2011-06-15 17:38:08
【问题描述】:

我正在尝试使用拖放方法在图表中创建关系,直接类似于SQL Server Management Studio 图表工具。例如,在下图中,用户将CustomerIDUser 实体拖到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>

DataTemplates 表示实体和关系:

<!-- 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,它将保存被拖动的项目,并标记是否正在发生拖动操作。然后我可以使用DataTriggers 来更改基于 IsDragging 的 CursorAdorner 可见性,并且可以使用 DragItem 进行放置操作。

(但是,我希望奖励另一种有趣的方法。如果需要更多信息或代码来澄清这个问题,请发表评论。)

编辑:优先级较低,但我仍在寻找更好的拖放图表方法解决方案。想在开源Mo+ Solution Builder 中实现更好的方法。

【问题讨论】:

    标签: wpf mvvm drag-and-drop diagramming mo+


    【解决方案1】:

    这是一个相当复杂的答案。如果有任何部分不清楚,请告诉我。

    我目前正在尝试解决类似的问题。就我而言,我想将我的 ListBox ItemsSource 绑定到一个集合,然后将该集合中的每个项目表示为 node 即可拖动对象或 connection 即一条线在节点被拖动时重绘自身的节点之间。我将向您展示我的代码和详细信息,我认为您可能需要进行更改以满足您的需求。

    拖动

    拖动是通过设置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 要求对象位于CanvasCustomCanvas 上,但除了懒惰之外没有任何好的理由。您可以轻松修改它以适用于任何面板。 (它在我的积压中!)。

    Dragger 类也使用了PavilionVisualTreeHelper.GetAncestor() 辅助方法,它只是爬上可视树来寻找合适的元素。代码如下。

     /// <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 事件,该事件会从被拖动的元素中弹出,以执行您可能需要的任何处理。

    更新连接位置

    我通知连接需要重绘的机制有点草率,肯定需要重新寻址。

    Connection 包含两个 FrameworkElement 类型的 DependencyProperties:Start 和 End。在 PropertyChangedCallbacks 中,我尝试将它们转换为 DragAwareListBoxItems(我需要将其设为接口以实现更好的可重用性)。如果转换成功,我将注册到DragAwareListBoxItem.ConnectionDragging 事件。 (坏名声,不是我的!)。当该事件触发时,连接会重新绘制其路径。

    DragAwareListBoxItem 实际上并不知道它何时被拖动,因此必须有人告诉它。由于 ListBoxItem 在我的可视化树中的位置,它永远不会听到 Dragger.IsDragging 事件。因此,要告诉它它正在被拖动,ListBox 会监听该事件并通知相应的 DragAwareListBoxItem。

    本打算发布ConnectionDragAwareListBoxItemListBox_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 事件,将我想要在其之间创建连接的两个控件传递给事件 args。由事件的订阅者实际完成工作。

    【讨论】:

    • +1 表示有趣的拖动方法,明天我将深入研究您的链接项目。例如,您正在从节点 A 拖动到节点 B(不移动任何一个节点)并且您在 drop 上创建连接(并且该点的连接被添加到 ItemsSource)。您能否在此处添加到您的帖子并解释当您从 A 拖动到 B 时如何绘制或将绘制弧线,以及如何在 drop 上创建连接(将 A 连接到 B)?谢谢。
    • +50 用于回答尝试。如果你能涵盖拖动时画出临时弧线的关键问题场景,我可以接受答案。
    • 当然可以。期待本周末的某个时候。
    【解决方案2】:

    我想你会想看看 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/

    很遗憾,我在这方面没有太多经验,但我确实认为这正是您所寻找的。祝你好运!

    【讨论】:

    • 谢谢亚当。我确实使用 Thumb 控件来移动实体和关系,并在渲染后调整实体的大小。在这种情况下,我认为我不能使用拇指,因为弧形路径在拖动过程中是动态绘制的,然后在放置时呈现为控件。除非你知道诀窍!
    【解决方案3】:

    如上所述,我目前的做法是不直接使用拖放,而是使用DependencyProperties 和处理鼠标事件的组合来模拟拖放。

    父图表控件中的DependencyProperties是:

    public static readonly DependencyProperty IsDraggingProperty = DependencyProperty.Register("IsDragging", typeof(bool), typeof(SolutionDiagramControl));
    public bool IsDragging
    {
        get
        {
            return (bool)GetValue(IsDraggingProperty);
        }
        set
        {
            SetValue(IsDraggingProperty, value);
        }
    }
    
    public static readonly DependencyProperty DragItemProperty = DependencyProperty.Register("DragItem", typeof(IWorkspaceViewModel), typeof(SolutionDiagramControl));
    public IWorkspaceViewModel DragItem
    {
        get
        {
            return (IWorkspaceViewModel)GetValue(DragItemProperty);
        }
        set
        {
            SetValue(DragItemProperty, value);
        }
    }
    

    IsDraggingDependencyProperty用于在拖动发生时触发光标变化,例如:

    <Style TargetType="{x:Type lib:SolutionDiagramControl}">
        <Style.Triggers>
            <Trigger Property="IsDragging" Value="True">
                <Setter Property="Cursor" Value="Pen" />
            </Trigger>
        </Style.Triggers>
    </Style>
    

    无论何时我需要执行drag and drop 的弧形绘制形式,而不是调用DragDrop.DoDragDrop,我将IsDragging = trueDragItem 设置为正在拖动的源项。

    在鼠标离开实体控件中,启用拖拽过程中绘制圆弧的连接器装饰器,如:

    protected override void OnMouseLeave(MouseEventArgs e)
    {
        base.OnMouseLeave(e);
        if (ParentSolutionDiagramControl.DragItem != null)
        {
            CreateConnectorAdorner();
        }
    }
    

    图表控件必须在拖动过程中处理额外的鼠标事件,例如:

    protected override void OnMouseMove(MouseEventArgs e)
    {
        base.OnMouseMove(e);
        if (e.LeftButton != MouseButtonState.Pressed)
        {
            IsDragging = false;
            DragItem = null;
        }
    }
    

    图表控件还必须处理鼠标向上事件时的“放置”(并且它必须根据鼠标位置确定要放置在哪个实体上),例如:

    protected override void OnMouseUp(MouseButtonEventArgs e)
    {
        base.OnMouseUp(e);
        if (DragItem != null)
        {
            Point currentPosition = MouseUtilities.GetMousePosition(this);
            DiagramEntityViewModel diagramEntityView = GetMouseOverEntity(currentPosition );
            if (diagramEntityView != null)
            {
                // Perform the drop operations
            }
        }
        IsDragging = false;
        DragItem = null;
    }
    

    我仍在寻找更好的解决方案,以便在进行拖动操作时在图表上绘制临时弧(跟随鼠标)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-09-12
      • 2015-02-18
      • 2023-03-22
      • 1970-01-01
      • 2011-01-14
      相关资源
      最近更新 更多