Title: Handle generic TreeNode mouse events in C#
This example shows how you can make a generic TreeNode class handle MouseMove and MouseDown events. It adds those events to the example described in the following posts:
I've restructured the main program slightly so it draws the tree in a PictureBox instead of directly on the form. The program then captures two of the PictureBox events: MouseMove and MouseDown. There are several steps but they're all fairly simple.
Both the MouseMove and MouseDown event handlers use the following FindNodeUnderMouse method.
// Set SelectedNode to the node under the mouse.
private void FindNodeUnderMouse(PointF pt)
{
using (Graphics gr = picTree.CreateGraphics())
{
SelectedNode = root.NodeAtPoint(gr, pt);
}
}
The FindNodeUnderMouse method calls the root node's NodeAtPoint method shown in the following code to see which node (if any) is at the mouse's position.
The TreeNode class's NodeAtPoint method is shown in the following code.
// Return the TreeNode at this point
// (or null if there isn't one there).
public TreeNode<T> NodeAtPoint(Graphics gr, PointF target_pt)
{
// See if the point is under this node.
if (Data.IsAtPoint(gr, MyFont, Center, target_pt)) return this;
// See if the point is under a node in the subtree.
foreach (TreeNode<T> child in Children)
{
TreeNode<T> hit_node = child.NodeAtPoint(gr, target_pt);
if (hit_node != null) return hit_node;
}
return null;
}
The NodeAtPoint method calls the Data object's IsAtPoint method to see if the node's drawing is at the target point. The IsAtPoint method is a new one that I added to the IDrawable interface so the tree could figure out where the node representations are. If the point isn't at this particular node, the code recursively calls the NodeAtPoint method for each of the node's children.
The following code shows the CircleNode class's implementation of IsAtPoint. This kind of node draws its text inside an ellipse, so the code uses the equation of an ellipse to see if the point lies under the node.
// Return true if the node is above this point.
// Note: The equation for an ellipse with half
// width w and half height h centered at the origin is:
// x*x/w/w + y*y/h/h <= 1.
bool IDrawable.IsAtPoint(Graphics gr, Font font,
PointF center_pt, PointF target_pt)
{
// Get our size.
SizeF my_size = GetSize(gr, font);
// translate so we can assume the
// ellipse is centered at the origin.
target_pt.X -= center_pt.X;
target_pt.Y -= center_pt.Y;
// Determine whether the target point is under our ellipse.
float w = my_size.Width / 2;
float h = my_size.Height / 2;
return
target_pt.X * target_pt.X / w / w +
target_pt.Y * target_pt.Y / h / h
<= 1;
}
To summarize so far, the main program includes a FindNodeUnderMouse method that calls the root node's NodeAtpoint method. That method uses the CircleNode class's IsAtPoint method to see which node contains the mouse's location.
Now to the main program's events. When the mouse moves over the PictureBox, the following event handler executes.
// Display the text of the node under the mouse.
private void picTree_MouseMove(object sender, MouseEventArgs e)
{
// Find the node under the mouse.
FindNodeUnderMouse(e.Location);
// If there is a node under the mouse,
// display the node's text.
if (SelectedNode == null)
{
lblNodeText.Text = "";
}
else
{
lblNodeText.Text = SelectedNode.Data.Text;
}
}
This code calls the FindNodeUnderMouse method to see if there's a node under the mouse. The code clears or sets the lblNodeText label in the form's status strip to show the node's text.
When the user presses the mouse down on the PictureBox, the following event handler executes.
// If this is a right button down and the
// mouse is over a node, display a context menu.
private void picTree_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Right) return;
// Find the node under the mouse.
FindNodeUnderMouse(e.Location);
// If there is a node under the mouse,
// display the context menu.
if (SelectedNode != null)
{
// Don't let the user delete the root node.
// (The TreeNode class can't do that.)
ctxNodeDelete.Enabled = (SelectedNode != root);
// Display the context menu.
ctxNode.Show(this, e.Location);
}
}
This code calls FindNodeUnderMouse to find the node at the mouse's position. If there is a node there, the program checks whether it's the root node. If this is the root node, the program disables the context menu's Delete Node item because the TreeNode class cannot delete the root node. (That would make the whole tree disappear and would make the program pretty hard to use.) It then displays a context menu.
The context menu uses the following code to let the user add or remove tree nodes.
// Add a child to the selected node.
private void ctxNodeAddChild_Click(object sender, EventArgs e)
{
NodeTextDialog dlg = new NodeTextDialog();
if (dlg.ShowDialog() == DialogResult.OK)
{
TreeNode<CircleNode> child =
new TreeNode<CircleNode>(
new CircleNode(dlg.txtNodeText.Text));
SelectedNode.AddChild(child);
// Rearrange the tree to show the new node.
ArrangeTree();
}
}
// Delete this node from the tree.
private void ctxNodeDelete_Click(object sender, EventArgs e)
{
if (MessageBox.Show(
"Are you sure you want to delete this node?",
"Delete Node?", MessageBoxButtons.YesNo,
MessageBoxIcon.Question) == DialogResult.Yes)
{
// Delete the node and its subtree.
root.DeleteNode(SelectedNode);
// Rearrange the tree to show the new structure.
ArrangeTree();
}
}
Some things you might like to add to this example include:
- Handling right mouse clicks
- Right-click to select a node and keep it selected
- Know when the mouse moves over or clicks a link
- Color different nodes or links differently (for example to show the one that's currently selected)
- Using a drag rectangle to let the user select multiple nodes or links at the same time
Download the example to experiment with it and to see additional details.
|