Good day,
We have implemented such nodes in one of our project as a combination of an elliptical node and a custom drawn link. The DrawLink handler draws the callout arrow like this:
void OnDrawLink(object sender, DrawLinkEventArgs e)
{
RectangleF bounds = e.Link.Origin.Bounds;
if (bounds.Width == 0 || bounds.Height == 0)
return;
PointF startPoint = e.Points[0];
PointF endPoint = e.Points[e.Points.Count - 1];
float distance = Distance(startPoint, endPoint);
if (distance == 0)
return;
float openingAngle = (float)(Math.Atan(1 / distance) * 360 / Math.PI) * 1.5f;
float angle = 0;
float radius = 0;
GeometryConvert.DekartToPolar(endPoint, startPoint, ref angle, ref radius);
PointF leftKnee = startPoint;
PointF rightKnee = startPoint;
GeometryConvert.PolarToDekart(endPoint, angle - openingAngle, radius, ref leftKnee);
GeometryConvert.PolarToDekart(endPoint, angle + openingAngle, radius, ref rightKnee);
PointF leftInter1 = PointF.Empty;
PointF leftInter2 = PointF.Empty;
PointF rightInter1 = PointF.Empty;
PointF rightInter2 = PointF.Empty;
GetEllipseIntr(bounds, endPoint, leftKnee, ref leftInter1, ref leftInter2);
GetEllipseIntr(bounds, endPoint, rightKnee, ref rightInter1, ref rightInter2);
PointF leftInter = startPoint;
if (!float.IsNaN(leftInter1.X) && !float.IsNaN(leftInter1.Y))
{
leftInter = Distance(leftInter1, endPoint) < Distance(leftInter2, endPoint) ? leftInter1 : leftInter2;
}
PointF rightInter = startPoint;
if (!float.IsNaN(rightInter2.X) && !float.IsNaN(rightInter2.Y))
{
rightInter = Distance(rightInter1, endPoint) < Distance(rightInter2, endPoint) ? rightInter1 : rightInter2;
}
float leftKneeAngle = 0;
float rightKneeAngle = 0;
PointF center = new PointF(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2);
GeometryConvert.DekartToPolar(center, leftInter, ref leftKneeAngle, ref radius);
GeometryConvert.DekartToPolar(center, rightInter, ref rightKneeAngle, ref radius);
leftKneeAngle = (-leftKneeAngle + 360) % 360;
rightKneeAngle = (-rightKneeAngle + 360) % 360;
GraphicsPath path = new GraphicsPath();
path.AddLine(endPoint, leftInter);
path.AddArc(bounds, leftKneeAngle, (rightKneeAngle - leftKneeAngle +
360) % 360);
path.AddLine(rightInter, endPoint);
// Use the same brush as the origin node's
using (Brush brush =
e.Link.Origin.Brush.CreateGdiBrush(path.GetBounds()))
e.Graphics.FillPath(brush, path);
path.Dispose();
}
static void GetEllipseIntr(RectangleF rcBox, PointF pt1, PointF pt2, ref PointF out1, ref PointF out2)
{
RectangleF rc = new RectangleF(Math.Min(pt1.X, pt2.X), Math.Min(pt1.Y, pt2.Y),
Math.Abs(pt1.X - pt2.X), Math.Abs(pt1.Y - pt2.Y));
float x1 = pt1.X;
float y1 = pt1.Y;
float x2 = pt2.X;
float y2 = pt2.Y;
if (Math.Abs(x1 - x2) > 0.0001)
{
float cx = (rcBox.Left + rcBox.Right) / 2;
float cy = (rcBox.Top + rcBox.Bottom) / 2;
float ea = (rcBox.Right - rcBox.Left) / 2;
float eb = (rcBox.Bottom - rcBox.Top) / 2;
float a = (y1 - y2) / (x1 - x2);
float b = (x1 * y2 - x2 * y1) / (x1 - x2);
double A = eb * eb + a * a * ea * ea;
double B = 2 * a * (b - cy) * ea * ea - 2 * cx * eb * eb;
double C = eb * eb * cx * cx + ea * ea * (b - cy) * (b - cy) - ea * ea * eb * eb;
float X1, X2, Y1, Y2;
double D = Math.Sqrt(B * B - 4 * A * C);
X1 = (float)((-B + D) / (2 * A));
X2 = (float)((-B - D) / (2 * A));
Y1 = a * X1 + b;
Y2 = a * X2 + b;
out1.X = X1; out1.Y = Y1;
out2.X = X2; out2.Y = Y2;
}
else
{
float cx = (rcBox.Left + rcBox.Right) / 2;
float cy = (rcBox.Top + rcBox.Bottom) / 2;
float ea = (rcBox.Right - rcBox.Left) / 2;
float eb = (rcBox.Bottom - rcBox.Top) / 2;
float X = x1;
float Y1 = cy - (float)Math.Sqrt((1 - (X - cx) * (X - cx) / (ea * ea)) * eb * eb);
float Y2 = cy + (float)Math.Sqrt((1 - (X - cx) * (X - cx) / (ea * ea)) * eb * eb);
out1.X = X; out1.Y = Y1;
out2.X = X; out2.Y = Y2;
}
}
If you also set the link.Dynamic property, the arrow will automatically adjust its position between the nodes. The code that finds intersection points works for Shape.Ellipse. You will have to modify it if you need rectangular nodes.
I hope that helps,
Stoyan