在 WPF 中通过拖放绘制图表弧

发布于 2024-11-15 12:01:25 字数 6526 浏览 3 评论 0原文

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

以及用于实体和关系:

<!-- 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.

Entity–relationship diagram

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 DataTemplates 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 DataTriggers 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

迷迭香的记忆 2024-11-22 12:01:25

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

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

拖动

拖动是通过设置 Dragger 类拥有的附加属性来完成的。在我看来,这比使用 MoveThumb 执行拖动具有优势,因为使对象可拖动不需要更改其控制模板。我的第一个实现实际上在控件模板中使用了 MoveThumb 来实现拖动,但我发现这样做会使我的应用程序非常脆弱(添加新功能通常会破坏拖动)。这是拖动器的代码:

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() 辅助方法,该方法只需爬上 Visual Tree 查找适当的元素。其代码如下。

 /// <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 事件,将我想要在其间创建连接的两个控件传递到事件参数中。由事件的订阅者来实际完成这项工作。

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 the MoveThumb to perform dragging in that making an object draggable does not involve changing its control template. My first implementation actually used MoveThumb 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:

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

I believe that Dragger requires that the object be on a Canvas or CustomCanvas, 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 the PavilionVisualTreeHelper.GetAncestor() helper method, which simply climbs the Visual Tree looking for the appropriate element. The code for that is below.

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

Consuming the Dragger class is very simple. Simply set Dragger.IsDraggable = true in the appropriate control’s xaml markup. Optionally, you can register to the Dragger.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, the DragAwareListBoxItem, and the ListBox_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.NodeDesigner
or 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.

一梦等七年七年为一梦 2024-11-22 12:01:25

我想您会想研究一下 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!

野稚 2024-11-22 12:01:25

如上所述,我当前的方法是不直接使用拖放,而是使用 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>

无论我需要在哪里执行拖放的圆弧绘制形式,我都会设置IsDragging = true,而不是调用DragDrop.DoDragDrop 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;
}

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

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:

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

The IsDragging DependencyProperty is used to trigger a cursor change when a drag is taking place, such as:

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

Wherever I need to perform an arc drawing form of drag and drop, instead of calling DragDrop.DoDragDrop, I set IsDragging = true and DragItem 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:

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

The diagram control must handle additional mouse events during the drag, such as:

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

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:

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

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文