Рисование дуг диаграммы с помощью перетаскивания в WPF - PullRequest
5 голосов
/ 15 июня 2011

Я пытаюсь выполнить метод перетаскивания для создания связей на диаграмме , прямо аналогичный SQL Server Management Studio инструментам построения диаграмм. Например, на рисунке ниже пользователь перетаскивает CustomerID из объекта User в объект Customer и создает отношение внешнего ключа между ними.

Ключевой требуемой особенностью является то, что временная траектория дуги будет нарисована, когда пользователь выполняет операцию перетаскивания, следуя за мышью. Перемещение сущностей или отношений после их создания не является проблемой, с которой я сталкиваюсь.

Entity–relationship diagram

Некоторая ссылка на 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>

и 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 s, чтобы изменить видимость Cursor и Adorner на основе IsDragging, и мог бы использовать DragItem для операции удаления.

(Но я жду награды за другой интересный подход. Пожалуйста, прокомментируйте, если для уточнения этого вопроса требуется дополнительная информация или код.)

Редактировать: Более низкий приоритет, но я все еще в поисках лучшего решения для подхода с перетаскиванием диаграмм. Хотите реализовать лучший подход в open source Mo + Solution Builder.

Ответы [ 3 ]

3 голосов
/ 14 августа 2011

Это довольно сложный ответ.Дайте мне знать, если какая-то часть этого не ясна.

В настоящее время я пытаюсь решить подобную проблему.В моем случае я хочу привязать мой 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 требует, чтобы объект был на Canvas или 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, которое всплывает от перетаскиваемого элемента, чтобы выполнить любую обработку, которая вам может понадобиться.

Обновление позиции соединения

Мой механизм информированиясоединение, которое нужно перерисовать, немного неаккуратно и определенно требует переадресации.

Соединение содержит два свойства DependencyProperty типа FrameworkElement: 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, так что вы можете адаптировать его по своему усмотрению.Как предупреждение, стабильной версии нет, поэтому она может измениться в любое время.

Возможность подключения

Как и при обновлении подключения, я не буду вставлять код.Вместо этого я расскажу, какие классы в проекте нужно изучить и что искать в каждом классе.

С точки зрения пользователя, вот как работает создание соединения.Пользователь щелкает правой кнопкой мыши по узлу.Это вызывает контекстное меню, из которого пользователь выбирает «Создать новое соединение».Эта опция создает прямую линию, начальная точка которой укоренена в выбранном узле, а конечная точка следует за мышью.Если пользователь нажимает на другой узел, то между ними создается соединение.Если пользователь щелкает где-либо еще, соединение не создается и линия исчезает.

Два классаs участвуют в этом процессе. ConnectionManager (который фактически не управляет какими-либо соединениями) содержит присоединенные свойства. Управляющий элемент управления устанавливает для свойства ConnectionManager.IsConnectable значение true и устанавливает для свойства ConnectionManager.MenuItemInvoker пункт меню, который должен запустить процесс. Кроме того, некоторый элемент управления в вашем визуальном дереве должен прослушивать перенаправленное событие ConnectionPending. Здесь происходит фактическое создание соединения.

Когда выбран элемент меню, ConnectionManager создает LineAdorner. ConnectionManager прослушивает событие LineAdorner LeftClick. Когда это событие срабатывает, я выполняю тестирование попаданий, чтобы найти выбранный элемент управления. Затем я вызываю событие ConnectionPending, передавая в событие аргументы двух элементов управления, между которыми я хочу создать соединение. На самом деле, подписчик мероприятия должен выполнить работу.

2 голосов
/ 18 августа 2011

Как упомянуто выше, мой текущий подход заключается в том, чтобы не использовать перетаскивание напрямую, а использовать комбинацию 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;
}

Я все еще ищу лучшееРешение нарисовать временный арc (после мыши) на диаграмме во время операции перетаскивания.

1 голос
/ 16 июня 2011

Думаю, вам захочется взглянуть на элемент управления 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/

К сожалению, у меня нет большого опыта в этой области, но я думаю, что это то, что вы ищете.Удачи!

...