|
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(); |
|
} |
|
} |