Hello,
I want to implement snap and glue functionality for nodes. My project is basicly similar to the “Dynamic Ports” sample. Can you tell me how I can implement such a snapping function in the best way?
Thank you very much!
Kind Regards,
Benedikt
Could you describe/show what you want users to do?
As I understand it, I do not see how that would work for the nodes in the Dynamic Ports sample.
For example if there are two nodes and I´m moving one of them with an edge near to an edge of the other node, the moving node shall snap to the specified x or y coordinate of the other node. Also I want to show a temporary snap line, which tells the user where the current moving node snaps.
Following there is a picture of the scenario:
Ah, you’re talking about guidelines. Here’s a sample app that implements guidelines by using a custom DraggingTool.
/* Copyright © Northwoods Software Corporation, 2008-2014. All Rights Reserved. */</p><p>using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;
using Northwoods.GoXam;
using Northwoods.GoXam.Model;
using Northwoods.GoXam.Tool;</p><p>namespace Guidelines {
public partial class Guidelines : Window {
public Guidelines() {
InitializeComponent();</p><p> var model = new GraphLinksModel<NodeInfo, String, String, UniversalLinkData>();
model.NodesSource = new ObservableCollection<NodeInfo>() {
new NodeInfo() { Key="A", Color="LightBlue" },
new NodeInfo() { Key="B", Color="LightYellow" },
new NodeInfo() { Key="C", Color="LightGreen" },
new NodeInfo() { Key="D", Color="Pink" },
new NodeInfo() { Key="E" },
};
model.LinksSource = new ObservableCollection<UniversalLinkData>() {
};
model.Modifiable = true;
model.HasUndoManager = true;
myDiagram.Model = model;</p><p> myDiagram.DraggingTool = new GuidedDraggingTool();
}
}</p><p> public class NodeInfo : GraphLinksModelNodeData<String> {
public String Color {
get { return _Color; }
set { if (_Color != value) { String old = _Color; _Color = value; RaisePropertyChanged("Color", old, value); } }
}
private String _Color = "Fuchsia";</p><p> public double Width {
get { return _Width; }
set { if (_Width != value) { double old = _Width; _Width = value; RaisePropertyChanged("Width", old, value); } }
}
private double _Width = 60;</p><p> public double Height {
get { return _Height; }
set { if (_Height != value) { double old = _Height; _Height = value; RaisePropertyChanged("Height", old, value); } }
}
private double _Height = 60;
}</p><p>
public class GuidedDraggingTool : DraggingTool {
public GuidedDraggingTool() {
this.GuidelineSnapDistance = 6;
this.GuidelineVicinity = 1000;
}</p><p> public override void DoActivate() {
if (this.HorizontalGuideline == null) CreateGuidelines();
base.DoActivate();
}
public override void DoDeactivate() {
this.Diagram.PartsModel.RemoveNode(this.HorizontalGuideline);
this.Diagram.PartsModel.RemoveNode(this.VerticalGuideline);
this.HorizontallyAlignedNode = null;
this.VerticallyAlignedNode = null;
base.DoDeactivate();
}</p><p> public double GuidelineSnapDistance { get; set; }
public double GuidelineVicinity { get; set; }</p><p> private Node HorizontalGuideline { get; set; }
private Node VerticalGuideline { get; set; }</p><p> private Node HorizontallyAlignedNode { get; set; }
private Node VerticallyAlignedNode { get; set; }</p><p> private void CreateGuidelines() {
Node n = new Node();
n.Id = "HorizontalGuideline";
n.LayerName = "Tool";
n.InDiagramBounds = false;
var line = new Line();
line.Stroke = new SolidColorBrush(Colors.Red);
line.StrokeThickness = 1;
line.X1 = 0;
line.Y1 = 0;
line.X2 = this.GuidelineVicinity;
line.Y2 = 0;
n.Content = line;
this.HorizontalGuideline = n;</p><p> n = new Node();
n.Id = "VerticalGuideline";
n.LayerName = "Tool";
n.InDiagramBounds = false;
line = new Line();
line.Stroke = new SolidColorBrush(Colors.Red);
line.StrokeThickness = 1;
line.X1 = 0;
line.Y1 = 0;
line.X2 = 0;
line.Y2 = this.GuidelineVicinity;
n.Content = line;
this.VerticalGuideline = n;
}</p><p> protected override void DragOver(Point pt, bool moving, bool copying) {
// Just guide the first dragged Node
Node node = null;
if (moving && this.DraggedParts != null) node = this.DraggedParts.Keys.First(p => p is Node) as Node;
if (copying && this.CopiedParts != null) node = this.CopiedParts.Keys.First(p => p is Node) as Node;
ShowGuidelines(node);
base.DragOver(pt, moving, copying);
}</p><p> protected override void DropOnto(Point pt) {
base.DropOnto(pt);
// Just shift the first dragged Node
Node node = this.Diagram.SelectedParts.OfType<Node>().FirstOrDefault();
if (node != null) {
double dx = 0;
double dy = 0;
if (this.VerticallyAlignedNode != null) {
dx = this.VerticallyAlignedNode.Bounds.X + this.VerticallyAlignedNode.Bounds.Width/2 - (node.Bounds.X + node.Bounds.Width/2);
}
if (this.HorizontallyAlignedNode != null) {
dy = this.HorizontallyAlignedNode.Bounds.Y + this.HorizontallyAlignedNode.Bounds.Height/2 - (node.Bounds.Y + node.Bounds.Height/2);
}
var pos = node.Position;
pos.Offset(dx, dy);
node.Move(pos, true);
}
}</p><p> private void ShowGuidelines(Node node) {
if (node == null) return;
// just align centers, for now
double cx = node.Bounds.X + node.Bounds.Width/2;
double cy = node.Bounds.Y + node.Bounds.Height/2;
this.HorizontallyAlignedNode = null;
this.VerticallyAlignedNode = null;
double bestdx = this.GuidelineSnapDistance + 0.5;
double bestdy = this.GuidelineSnapDistance + 0.5;
foreach (Node n in this.Diagram.Nodes) { // this could be more efficient by only looking at nodes within the viewport?
// ignore invisible and dragged Nodes
if (!n.Visible || !n.IsBoundToData) continue;
if (this.DraggedParts != null && this.DraggedParts.ContainsKey(n)) continue;
if (this.CopiedParts != null && this.CopiedParts.ContainsKey(n)) continue;
// now see if it's the best aligned within GuidelineSnapDistance
Rect b = n.Bounds;
double nx = b.X + b.Width/2;
double ny = b.Y + b.Height/2;
double dx = Math.Abs(nx - cx);
double dy = Math.Abs(ny - cy);
if (dx < bestdx && dy < this.GuidelineVicinity) {
this.VerticallyAlignedNode = n;
bestdx = dx;
}
if (dy < bestdy && dx < this.GuidelineVicinity) {
this.HorizontallyAlignedNode = n;
bestdy = dy;
}
}
if (this.HorizontallyAlignedNode != null) {
double minx = Math.Min(this.HorizontallyAlignedNode.Bounds.X, node.Bounds.X) - 10;
double maxx = Math.Max(this.HorizontallyAlignedNode.Bounds.Right, node.Bounds.Right) + 10;
this.HorizontalGuideline.Position = new Point(minx, this.HorizontallyAlignedNode.Bounds.Y + this.HorizontallyAlignedNode.Bounds.Height/2);
((Line)this.HorizontalGuideline.Content).X2 = maxx-minx;
this.Diagram.PartsModel.AddNode(this.HorizontalGuideline);
} else {
this.Diagram.PartsModel.RemoveNode(this.HorizontalGuideline);
}</p><p> if (this.VerticallyAlignedNode != null) {
double miny = Math.Min(this.VerticallyAlignedNode.Bounds.Y, node.Bounds.Y) - 10;
double maxy = Math.Max(this.VerticallyAlignedNode.Bounds.Bottom, node.Bounds.Bottom) + 10;
this.VerticalGuideline.Position = new Point(this.VerticallyAlignedNode.Bounds.X + this.VerticallyAlignedNode.Bounds.Width/2, miny);
((Line)this.VerticalGuideline.Content).Y2 = maxy-miny;
this.Diagram.PartsModel.AddNode(this.VerticalGuideline);
} else {
this.Diagram.PartsModel.RemoveNode(this.VerticalGuideline);
}
}
}
}
<!-- Copyright © Northwoods Software Corporation, 2008-2014. All Rights Reserved. --></p><p><Window x:Class="Guidelines.Guidelines"
xmlns="<a href="http://schemas.microsoft.com/winfx/2006/xaml/presentation" target="_blank" rel="nofollow">http://schemas.microsoft.com/winfx/2006/xaml/presentation</a>"
xmlns:x="<a href="http://schemas.microsoft.com/winfx/2006/xaml" target="_blank" rel="nofollow">http://schemas.microsoft.com/winfx/2006/xaml</a>"
xmlns:go="<a href="http://schemas.nwoods.com/GoXam" target="_blank" rel="nofollow">http://schemas.nwoods.com/GoXam</a>"
Title="Guidelines" Height="600" Width="600"></p><p> <Window.Resources>
<DataTemplate x:Key="NodeTemplate">
<go:NodePanel Width="{Binding Path=Data.Width, Mode=TwoWay}"
Height="{Binding Path=Data.Height, Mode=TwoWay}"
Background="{Binding Path=Data.Color}"
go:Part.SelectionAdorned="True"
go:Part.Resizable="True"
go:Node.LocationSpot="Center"
go:Node.Location="{Binding Path=Data.Location, Mode=TwoWay}">
<TextBlock Text="{Binding Path=Data.Key}" />
</go:NodePanel>
</DataTemplate>
</Window.Resources></p><p> <Grid>
<go:Diagram x:Name="myDiagram"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
NodeTemplate="{StaticResource NodeTemplate}">
</go:Diagram>
</Grid>
</Window>
This GuidedDraggingTool just implements center-to-center guidelines. If you want guidelines for the top/bottom/left/right sides, you’ll need to adapt this code appropriately.
I modified the above solution for my scenario and it works very well :) Thank you very much. There is just one little problem. I´m using the “IsRealtime = false” option within my DraggingTool. If I set this option the current location of the node is not updated. How can I get the current location of a node during moving if IsRealtime is false?
I’m not sure. I suppose in DoActivate you could remember the difference between the DraggingTool.StartPoint and the original selected Node’s center point. The overrides above find the first Node being dragged and using its center point. Instead you use the current mouse point (this.Diagram.FirstMousePointInModel) plus the remembered offset.
Hm no it does not work very well. I got a new idea. I override the SnapTo method within the dragging tool and deactivate the function if “snap to grid” is disabled.