Рисование диаграмм дуг с помощью перетаскивания в WPF
Я пытаюсь использовать метод перетаскивания для создания связей на диаграмме, прямо аналогичный инструментам построения диаграмм в http://en.wikipedia.org/wiki/SQL_Server_Management_Studio. Например, на рисунке ниже пользователь будет перетаскивать CustomerID
от User
сущность к Customer
сущность и создать отношения внешнего ключа между ними.
Ключевой желаемой особенностью является то, что временная траектория дуги будет нарисована, когда пользователь выполняет операцию перетаскивания, следуя за мышью. Перемещение сущностей или отношений после их создания не является проблемой, с которой я сталкиваюсь.
Некоторые ссылки XAML, соответствующие сущности на диаграмме выше:
<!-- Entity diagram control -->
<Grid MinWidth="10" MinHeight="10" Margin="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*" ></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid Grid.Row="0" Grid.Column="0" IsHitTestVisible="False" Background="{StaticResource ControlDarkBackgroundBrush}">
<Label Grid.Row="0" Grid.Column="0" Style="{DynamicResource LabelDiagram}" Content="{Binding DiagramHeader, Mode=OneWay}" />
</Grid>
<ScrollViewer Grid.Row="1" Grid.Column="0" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" Background="{StaticResource ControlBackgroundBrush}" >
<StackPanel VerticalAlignment="Top">
<uent:EntityDataPropertiesDiagramControl DataContext="{Binding EntityDataPropertiesFolder}" />
<uent:CollectionEntityPropertiesDiagramControl DataContext="{Binding CollectionEntityPropertiesFolder}" />
<uent:DerivedEntityDataPropertiesDiagramControl DataContext="{Binding DerivedEntityDataPropertiesFolder}" />
<uent:ReferenceEntityPropertiesDiagramControl DataContext="{Binding ReferenceEntityPropertiesFolder}" />
<uent:MethodsDiagramControl DataContext="{Binding MethodsFolder}" />
</StackPanel>
</ScrollViewer>
<Grid Grid.RowSpan="2" Margin="-10">
<lib:Connector x:Name="LeftConnector" Orientation="Left" VerticalAlignment="Center" HorizontalAlignment="Left" Visibility="Collapsed"/>
<lib:Connector x:Name="TopConnector" Orientation="Top" VerticalAlignment="Top" HorizontalAlignment="Center" Visibility="Collapsed"/>
<lib:Connector x:Name="RightConnector" Orientation="Right" VerticalAlignment="Center" HorizontalAlignment="Right" Visibility="Collapsed"/>
<lib:Connector x:Name="BottomConnector" Orientation="Bottom" VerticalAlignment="Bottom" HorizontalAlignment="Center" Visibility="Collapsed"/>
</Grid>
</Grid>
Мой текущий подход к этому заключается в следующем:
1) Инициируйте операцию перетаскивания в дочернем элементе управления сущности, например:
protected override void OnPreviewMouseMove(MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed)
{
dragStartPoint = null;
}
else if (dragStartPoint.HasValue)
{
Point? currentPosition = new Point?(e.GetPosition(this));
if (currentPosition.HasValue && (Math.Abs(currentPosition.Value.X - dragStartPoint.Value.X) > 10 || Math.Abs(currentPosition.Value.Y - dragStartPoint.Value.Y) > 10))
{
DragDrop.DoDragDrop(this, DataContext, DragDropEffects.Link);
e.Handled = true;
}
}
}
2) Создайте элемент оформления коннектора, когда операция перетаскивания покидает объект, например:
protected override void OnDragLeave(DragEventArgs e)
{
base.OnDragLeave(e);
if (ParentCanvas != null)
{
AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(ParentCanvas);
if (adornerLayer != null)
{
ConnectorAdorner adorner = new ConnectorAdorner(ParentCanvas, BestConnector);
if (adorner != null)
{
adornerLayer.Add(adorner);
e.Handled = true;
}
}
}
}
3) Нарисуйте траекторию дуги при перемещении мыши в соединителе, например:
protected override void OnMouseMove(MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
if (!IsMouseCaptured) CaptureMouse();
HitTesting(e.GetPosition(this));
pathGeometry = GetPathGeometry(e.GetPosition(this));
InvalidateVisual();
}
else
{
if (IsMouseCaptured) ReleaseMouseCapture();
}
}
Диаграмма Canvas
связан с моделью представления, а сущности и отношения на Canvas
в свою очередь связаны с соответствующими моделями представления. Некоторые XAML, относящиеся к общей диаграмме:
<ItemsControl ItemsSource="{Binding Items, Mode=OneWay}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<lib:DesignerCanvas VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.VirtualizationMode="Recycling"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Left" Value="{Binding X}"/>
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
<Setter Property="Canvas.Width" Value="{Binding Width}"/>
<Setter Property="Canvas.Height" Value="{Binding Height}"/>
<Setter Property="Canvas.ZIndex" Value="{Binding ZIndex}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
а также DataTemplate
s для энтузиастов и отношений:
<!-- 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 для операции удаления.
(Но я жду награды за другой интересный подход. Пожалуйста, прокомментируйте, если для уточнения этого вопроса требуется дополнительная информация или код.)
Изменить: с более низким приоритетом, но я все еще в поисках лучшего решения для схемы перетаскивания мышью. Хотите реализовать лучший подход в Mo + Solution Builder с открытым исходным кодом.
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);
}
}
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;
}
Я все еще ищу лучшее решение, чтобы нарисовать временную дугу (следуя за мышью) на диаграмме во время операции перетаскивания.
Это довольно сложный ответ. Дайте мне знать, если какая-то часть этого не ясна.
Я сейчас пытаюсь решить подобную проблему. В моем случае я хочу связать свой 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
событие, которое всплывает из перетаскиваемого элемента, чтобы выполнить любую обработку, которая может вам понадобиться.
Обновление позиции соединения
Мой механизм информирования соединения о том, что он должен быть перерисован, немного неаккуратен и определенно требует переадресации.
Соединение содержит два свойства DependencyProperties типа 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 clone https://code.google.com/p/pavilion/. Это проект с открытым исходным кодом под лицензией MIT, так что вы можете адаптировать его по своему усмотрению. Как предупреждение, стабильной версии нет, поэтому она может измениться в любое время.
Подключаемость
Как и при обновлении соединения, я не буду вставлять код. Вместо этого я расскажу, какие классы в проекте нужно изучить и что искать в каждом классе.
С точки зрения пользователя, вот как работает создание соединения. Пользователь щелкает правой кнопкой мыши по узлу. Это вызывает контекстное меню, из которого пользователь выбирает "Создать новое соединение". Эта опция создает прямую линию, начальная точка которой укоренена в выбранном узле, а конечная точка следует за мышью. Если пользователь нажимает на другой узел, между ними создается соединение. Если пользователь щелкает где-либо еще, соединение не создается и линия исчезает.
В этом процессе участвуют два класса. ConnectionManager
(который фактически не управляет никакими связями) содержит Прикрепленные Свойства. Управляющий элемент управления устанавливает для свойства ConnectionManager.IsConnectable значение true и устанавливает для свойства ConnectionManager.MenuItemInvoker пункт меню, который должен запустить процесс. Кроме того, некоторый элемент управления в вашем визуальном дереве должен прослушивать перенаправленное событие ConnectionPending. Здесь происходит фактическое создание соединения.
Когда элемент меню выбран, ConnectionManager создает LineAdorner. ConnectionManager прослушивает событие LineAdorner LeftClick. Когда это событие срабатывает, я выполняю тестирование попаданий, чтобы найти выбранный элемент управления. Затем я вызываю событие ConnectionPending, передавая в событие аргументы двух элементов управления, между которыми я хочу создать соединение. На самом деле, подписчик мероприятия должен выполнить работу.
Я думаю, вы захотите взглянуть на элемент управления 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/
К сожалению, у меня нет большого опыта в этой области, но я думаю, что это то, что вы ищете. Удачи!