Skip to content

Instantly share code, notes, and snippets.

@christophevg
Last active February 16, 2017 21:35
Show Gist options
  • Save christophevg/5b730c0cae90e2bf756b215f1c30e900 to your computer and use it in GitHub Desktop.
Save christophevg/5b730c0cae90e2bf756b215f1c30e900 to your computer and use it in GitHub Desktop.
Connecting nodes of two TreeViews using C#/WinForms

This Gist is a combination a many different solutions found around the net. It shows how to create two TreeViews and visually connect two nodes in each of them.

Most "problems" are covered:

  • Invisible nodes (e.g. when collapsed or scrolled out of sight)
  • Back- and forth notifications of updates between TreeViews and parent Form
  • Two LinkedTreeViews are combined in LinkedTreeViews Panel
  • Drag and Drop support allows to create/remove links
using System;
using System.IO;
using System.Drawing;
using System.Windows.Forms;
public class LinkedTreeViewsDemo : Form {
[STAThread]
static public void Main () {
Application.Run(new LinkedTreeViewsDemo());
}
public LinkedTreeViewsDemo() {
this.MinimumSize = new Size(600, 200);
this.Width = 600;
var trees = new LinkedTreeViews();
this.Controls.Add(trees);
// populate left tree with some nodes
addNode("Node 1", trees.LeftTree);
this.addNode("Node 2", trees.LeftTree);
var group1 = this.addNode("Node 3", trees.LeftTree);
this.addNode("Node 3/1", group1);
this.addNode("Node 3/2", group1);
this.addNode("Node 4", trees.LeftTree);
var group2 = this.addNode("Node 5", trees.LeftTree);
this.addNode("Node 5/1", group2);
this.addNode("Node 5/2", group2);
this.addNode("Node 5/3", group2);
// populate right tree with some nodes
this.addNode("Node 1", trees.RightTree);
this.addNode("Node 2", trees.RightTree);
var group3 = this.addNode("Node 3", trees.RightTree);
this.addNode("Node 3/1", group3);
this.addNode("Node 3/2", group3);
this.addNode("Node 4", trees.RightTree);
var group4 = this.addNode("Node 5", trees.RightTree);
this.addNode("Node 5/1", group4);
this.addNode("Node 5/2", group4);
this.addNode("Node 5/3", group4);
}
private LinkedTreeNode addNode(string label, LinkedTreeView tree=null) {
var node = new LinkedTreeNode(label);
if( tree != null ) { tree.Nodes.Add(node); }
return node;
}
private LinkedTreeNode addNode(string label, LinkedTreeNode parent=null) {
var node = new LinkedTreeNode(label);
if( parent != null ) { parent.Nodes.Add(node); }
return node;
}
}
// TreeNode with a link to another TreeNode
public class LinkedTreeNode : TreeNode {
public LinkedTreeNode(string label) : base(label) {}
public LinkedTreeNode OtherNode { get; private set; }
public bool IsLinked { get { return this.OtherNode != null; } }
public bool HasVisibleLink { get {
return this.IsLinked && this.IsVisible && this.OtherNode.IsVisible;
}}
// utility function to manage links
public LinkedTreeNode LinkTo(LinkedTreeNode otherNode) {
this.Unlink();
otherNode.Unlink();
this.OtherNode = otherNode;
this.OtherNode.OtherNode = this;
return this;
}
public void Unlink() {
if( this.OtherNode == null ) { return; }
this.OtherNode.OtherNode = null;
this.OtherNode = null;
}
// the EndPoint at the outside of the TreeView
public Point ExternalEndPoint {
get {
LinkedTreeView tree = this.TreeView as LinkedTreeView;
return new Point(
tree.IsLeft ? tree.Right : tree.Left,
this.TreeView.Top + this.Bounds.Top + this.Bounds.Height/2
);
}
}
// the starting Point next to the Label of the TreeNode
public Point InternalLabelPoint {
get {
LinkedTreeView tree = this.TreeView as LinkedTreeView;
return new Point(
tree.IsLeft ? this.Bounds.Right : this.Bounds.Left,
this.Bounds.Top + this.Bounds.Height/2
);
}
}
// the internal counterpart of ExternalEndPoint
public Point InternalEndPoint {
get {
LinkedTreeView tree = this.TreeView as LinkedTreeView;
return new Point(
tree.IsLeft ? tree.ClientRectangle.Width : 0,
this.Bounds.Top + this.Bounds.Height/2
);
}
}
}
// A TreeView with links to another TreeView
public class LinkedTreeView : TreeView {
// boolean to indicate left/right position vs other LinkedTreeView
public bool IsLeft { get; set; }
// the other treeview (to check if other end has a selected node)
private LinkedTreeView otherTree;
public LinkedTreeView LinkTo(LinkedTreeView otherTree) {
this.otherTree = otherTree;
this.otherTree.otherTree = this;
return this;
}
internal const int WM_PAINT = 0xF;
internal const int WM_VSCROLL = 0x0115;
protected override void WndProc(ref Message m) {
base.WndProc(ref m);
if( m.Msg == WM_PAINT ) {
// draw links for all linked tree nodes
foreach( LinkedTreeNode node in this.Nodes ) {
this.drawAllVisibles(node);
}
// invalidate parent to allow it to re-render the middle part of the link
this.Parent.Invalidate();
}
// if we scroll, we might have made our selected node invisible, make sure
// the other party is also notified of this
if( m.Msg == WM_VSCROLL ) {
if( this.otherTree != null ) { this.otherTree.Invalidate(); }
}
}
// render the end point for this node, and its subnodes
private void drawAllVisibles(LinkedTreeNode node) {
// only draw when we AND the other end are visible
if( node.HasVisibleLink ) {
this.drawLink(node);
}
// recurse down
foreach(LinkedTreeNode subnode in node.Nodes) {
this.drawAllVisibles(subnode);
}
}
public LinkedTreeView() {
// drag-drop support
this.AllowDrop = true;
this.DragDrop += new DragEventHandler(this.handleDragDrop);
this.DragOver += new DragEventHandler(this.handleDragOver);
this.MouseDown += new MouseEventHandler(this.handleMouseDown);
this.AfterSelect += new TreeViewEventHandler(this.handleAfterSelect);
}
private void handleDragOver(object sender, DragEventArgs e) {
e.Effect = DragDropEffects.Copy;
}
private void handleAfterSelect(object sender, TreeViewEventArgs e) {
this.Capture = false;
this.otherTree.Capture = false;
}
private void handleMouseDown(object sender, MouseEventArgs e) {
TreeNode node = this.HitTest(e.Location).Node;
if( node != null ) {
this.DoDragDrop(node, DragDropEffects.All);
}
}
private void handleDragDrop(object sender, DragEventArgs e) {
var source = e.Data.GetData(typeof(LinkedTreeNode)) as LinkedTreeNode;
if( source != null ) {
Point location = this.PointToClient(Cursor.Position);
LinkedTreeNode target = this.HitTest(location).Node as LinkedTreeNode;
if( target == null ) {
// seems we're trying to unlink ?
source.Unlink();
} else {
if( source.TreeView == this ) {
// dropping a node on same treeview could mean: "change this side of
// the link ;-)
target.LinkTo(source.OtherNode);
} else {
target.LinkTo(source);
}
}
this.Invalidate();
this.otherTree.Invalidate();
}
}
private void drawLink(LinkedTreeNode node) {
Graphics g = this.CreateGraphics();
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
Pen pen = new Pen(Color.FromArgb(255, 128, 128, 128), 3);
g.DrawLine(pen, node.InternalLabelPoint, node.InternalEndPoint);
}
// fake transparancy by inheriting parent background color
protected override void OnParentChanged(EventArgs e) {
if( this.Parent != null ) { this.BackColor = this.Parent.BackColor; }
base.OnParentChanged(e);
}
protected override void OnParentBackColorChanged(EventArgs e) {
this.BackColor = this.Parent.BackColor;
base.OnParentBackColorChanged(e);
}
}
// two LinkedTreeViews together on a Panel
public class LinkedTreeViews : Panel {
public LinkedTreeView LeftTree { get; private set; }
public LinkedTreeView RightTree { get; private set; }
public LinkedTreeViews() {
// create and link the two TreeViews
this.LeftTree = this.createLeftTree();
this.RightTree = this.createRightTree().LinkTo(this.LeftTree);
// we're a Panel, stretch us to our parents size
this.Dock = DockStyle.Fill;
// resizing might cause scroll bars to appear and make nodes invisible
this.Resize += new EventHandler(this.UpdateTrees);
// make sure links are redrawn
this.Paint += new PaintEventHandler(this.drawAllVisibleLinks);
}
// draw a link between the two TreeViews to link the linked Nodes
// start by going thgough the links list from one side, e.g. LeftTree
private void drawAllVisibleLinks(object sender, PaintEventArgs e)
{
foreach(LinkedTreeNode node in LeftTree.Nodes) {
this.drawAllVisibleLinks(node);
}
}
private void drawAllVisibleLinks(LinkedTreeNode node) {
if( node.HasVisibleLink ) {
this.drawLink((LinkedTreeNode)node);
}
// recurse down
foreach(LinkedTreeNode subnode in node.Nodes) {
this.drawAllVisibleLinks(subnode);
}
}
private void drawLink(LinkedTreeNode node) {
Graphics g = this.CreateGraphics();
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
Pen pen = new Pen(Color.FromArgb(255, 128, 128, 128), 3);
g.DrawLine(pen, node.ExternalEndPoint, node.OtherNode.ExternalEndPoint);
}
private LinkedTreeView createLeftTree() {
var tree = this.createDefaultTree();
tree.IsLeft = true;
tree.Dock = DockStyle.Left;
tree.RightToLeft = System.Windows.Forms.RightToLeft.Yes;
return tree;
}
private LinkedTreeView createRightTree() {
var tree = this.createDefaultTree();
tree.Left = 300;
tree.IsLeft = false;
tree.Dock = DockStyle.Right;
return tree;
}
private LinkedTreeView createDefaultTree() {
var tree = new LinkedTreeView();
tree.BorderStyle = BorderStyle.None;
tree.Width = 250;
tree.NodeMouseClick += new TreeNodeMouseClickEventHandler(this.UpdateTrees);
this.Controls.Add(tree);
return tree;
}
// make sure all three parts of the link are redrawn
private void UpdateTrees(object sender, EventArgs e) {
this.LeftTree.Invalidate();
this.RightTree.Invalidate();
this.Invalidate();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment