5

我正在尝试使用拖放方法在图表中创建关系,直接类似于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>

问题:问题在于,一旦开始拖动操作,鼠标移动将不再被跟踪,并且连接器装饰器无法像在其他上下文中那样绘制弧线。如果我释放鼠标并再次单击,则弧线开始绘制,但随后我丢失了源对象。我试图想办法结合鼠标移动来传递源对象。

Bounty:回到这个问题,我目前计划不直接使用拖放来执行此操作。我目前计划DependencyProperty为图表控件添加一个 DragItem 和 IsDragging,它将保存被拖动的项目,并标记是否发生拖动操作。然后我可以使用DataTriggers 来更改基于 IsDragging 的可见性,并且可以使用 DragItem 进行放置操作CursorAdorner

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

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

4

3 回答 3

4

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

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

拖动

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

该类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类非常简单。只需Dragger.IsDraggable = true在相应控件的 xaml 标记中进行设置。或者,您可以注册到Dragger.IsDragging事件,该事件从被拖动的元素中冒出,以执行您可能需要的任何处理。

更新连接位置

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

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

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

The 将发布Connection、 theDragAwareListBoxItem和 the的代码ListBox_IsDragging,但我认为这里的可读性太强了。您可以在http://code.google.com/p/pavilion/source/browse/#hg%2FPavilionDesignerTool%2FPavillion.NodeDesigner查看项目 或使用 hg clone https://code.google.com/克隆存储库p/亭/。它是 MIT 许可下的开源项目,因此您可以根据需要对其进行调整。作为警告,没有稳定的版本,所以它可以随时更改。

可连接性

与连接更新一样,我不会粘贴代码。相反,我将告诉您要检查项目中的哪些类以及要在每个类中查找什么。

从用户的角度来看,这是创建连接的工作原理。用户右键单击一个节点。这将打开一个上下文菜单,用户可以从中选择“创建新连接”。该选项创建一条直线,其起点以选定节点为根,终点跟随鼠标。如果用户单击另一个节点,则在两者之间创建连接。如果用户单击其他任何地方,则不会创建连接并且该行消失。

这个过程涉及两个类。(ConnectionManager实际上并不管理任何连接)包含附加属性。消费控件将 ConnectionManager.IsConnectable 属性设置为 true,并将 ConnectionManager.MenuItemInvoker 属性设置为应该启动进程的菜单项。此外,您的可视化树中的某些控件必须侦听 ConnectionPending 路由事件。这是实际创建连接的地方。

选择菜单项时,ConnectionManager 会创建一个 LineAdorner。ConnectionManager 侦听 LineAdorner LeftClick 事件。当该事件被触发时,我执行命中测试以找到被选中的控件。然后我引发 ConnectionPending 事件,将我想要在其之间创建连接的两个控件传递给事件 args。由事件的订阅者实际完成工作。

于 2011-08-14T15:10:12.483 回答
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/

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

于 2011-06-16T02:49:40.637 回答
2

如上所述,我目前的做法是不直接使用拖放,而是使用组合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);
    }
}

用于在IsDragging DependencyProperty拖动发生时触发光标更改,例如:

<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 = true和设置DragItem为被拖动的源项目。

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

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;
}

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

于 2011-08-17T21:04:46.643 回答