Handle generic TreeNode mouse events in C#

[generic TreeNode mouse events]

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 Example   Follow me on Twitter   RSS feed   Donate








This entry was posted in Uncategorized and tagged , , , , , , , , , , , , , , . Bookmark the permalink.

5 Responses to Handle generic TreeNode mouse events in C#

  1. CodeWarrior says:

    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?

  2. Rod Stephens says:

    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.

  3. Jay says:

    This was an excellent post that has helped me to understand the basics of how to code tree-like structures. I want to thank you very much for it!

    I am attempting to improve on this by having all of the subtrees meet at an end node.

    I have been editing the Arrange method to add a subtree end point where all of the subtree nodes will meet to then meet with the other nodes, but I get some overlap in some cases and can’t quite figure out how to avoid this.

    Any suggestions/comments would be appreciated! And thank you again for your helpful posts!

  4. Pingback: Map between nodes and node numbers in a binary tree in C# - C# HelperC# Helper

  5. Pingback: Draw trees vertically or horizontally in C# - C# HelperC# Helper

Leave a Reply

Your email address will not be published. Required fields are marked *