Handle mouse events for a generic TreeNode class in C#

This example adds handling for MouseMove and MouseDown events on the tree's nodes to the following entries:

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's 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 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 child in Children)
    {
        TreeNode hit_node = child.NodeAtPoint(gr, target_pt);
        if (hit_node != null) return hit_node;
    }

    return null;
}

The NodeAtPoint method calls the DataObject's IsAtPoint method to see if the node's drawing is at the target point. The IsAtPoint method is a new one 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'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.) It then displays the 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 child =
            new TreeNode(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

   

 

What did you think of this article?




Trackbacks
  • No trackbacks exist for this post.
Comments

  • 11/24/2010 1:05 AM CodeWarrior wrote:
    Though i was able to understand your code, there is no way i could have come up with it on my own because recursive functions are my weak point. How do you manage to keep track of recursive functions? The debugger is of no use. Logging to a text file maybe?
    Reply to this
  • 11/24/2010 9:35 AM Rod Stephens wrote:
    Recursion confuses a lot of people. It's not the way people think naturally. Recursion on trees is easier because you can think of a method as doing something to its node and the recursive part does the same thing on all of the nodes in its subtree.

    As for debugging, you're right that it can be hard. You can use the debugger but you have to be careful because it will stop at every node and make things confusing. On technique is to set a breakpoint in the code, stop there, and then remove the breakpoint so you can step over the recursive calls.

    Another technique is to use a test to see if you're at the node where you want to stop. For example:

    if (this.Data.ToString() == "X")
    {
    int i = 10;
    }

    Now set a breakpoint on "int i = 10" and the program will stop at that node so you can see what's going on inside the recursion.

    Another technique is to log to a text file or use the Console window. If you pass a depth parameter into the method so you know how deep in the tree you are, you can indent the output that number of spaces (or 4 times that number) so you can more easily see the depth of recursion. (In the recursive call, you add 1 to the depth.)

    All in all, it can be tricky. A bit of practice can be a big help.
    Reply to this
  • 4/22/2011 12:16 AM ernesto wrote:
    My friend, i really really really need to thank you, this is such and amazing and educational program; seriously, you have done a very good job and i want to thank you for it.

    THANK YOU!!!!!!!!!!!!!!!
    Reply to this
Leave a comment

Submitted comments are subject to moderation before being displayed.

 Name

 Email (will not be published)

 Website

Your comment is 0 characters limited to 3000 characters.