[C# Helper]
Index Books FAQ Contact About Rod
[Beginning Database Design Solutions, Second Edition]

[Beginning Software Engineering, Second Edition]

[Essential Algorithms, Second Edition]

[The Modern C# Challenge]

[WPF 3d, Three-Dimensional Graphics with WPF and C#]

[The C# Helper Top 100]

[Interview Puzzles Dissected]

[C# 24-Hour Trainer]

[C# 5.0 Programmer's Reference]

[MCSD Certification Toolkit (Exam 70-483): Programming in C#]

Title: See which section is under the mouse in a sunburst chart in C#

[See which section is under the mouse in a sunburst chart in C#]

My earlier post Make a sunburst chart in C#, Part 4 shows how you can draw a sunburst chart. This example shows how you can tell which section the mouse is over when you move the mouse over the chart. It also shows how you can draw a wedge for a "missing" element.

Finding the Wedge Under the Mouse

To keep track of the wedges, the program uses the following Wedge class.

public class Wedge { public GraphicsPath Path; public Color FgColor, BgColor; public string Text; public bool IsHidden; public Wedge(GraphicsPath path, Color fg_color, Color bg_color, string text, bool is_hidden) { Path = path; FgColor = fg_color; BgColor = bg_color; Text = text; IsHidden = is_hidden; } // Return true if the Wedge contains this point. public bool ContainsPoint(Point point) { return Path.IsVisible(point); } // Return the Wedge's text. public override string ToString() { return Text; } }

This class stores the GraphicsPath object used to draw the wedge. It also stores the wedge's foreground and background colors, text, and an IsHidden value that indicates whether the wedge should be drawn. I'll say more about that in the following section, which deals with hidden edges.

The wedge-finding code really only needs the Wedge class's GraphicsPath object. The other values are just there so the program can display something. You could add other pieces of information to identify the wedge if you like. For example, you could add a Name property to the wedges in the XML data and then make the program save that data in the Wedge objects.

The Wedge class's ContainsPoint method returns true if a given point lies within the wedge. To do that, it simply calls the GraphicsPath object's IsVisible method and returns the result.

The last piece of the class overrides its ToString method to return the wedge's text. Overriding this method is useful because it allows the debugger to display a Wedge object by using its text instead of the default, which is the class name.

The program uses the following code to define two objects used to keep track of the wedges.

// The items' wedges. private List Wedges = null; // The Wedge that is currently under the mouse. private Wedge WedgeUnderMouse = null;

The Wedges list holds a Wedge object for each of the wedges. As you can probably guess, the WedgeUnderMouse field holds a reference to the wedge that is currently under the mouse.

When the program starts or is resized, it draws the wedges needed to make the sunburst chart. The previous version of the program just drew the chart's wedges on a bitmap. The new version also creates a new Wedge object to represent each wedge. For example, it uses the following statement to create a Wedge for the chart's root.

wedges.Add(new Wedge(path, fg_color, bg_color, XmlDoc.DocumentElement.Name, IsHidden(XmlDoc.DocumentElement)));

The program uses the following code to create a Wedge object for a child wedge.

// Make the item's wedge. wedges.Add(new Wedge(path, fg_color, bg_color, text, is_hidden));

Most of the parameters to the Wedge class's constructor were already found by the program's previous version. The final parameter indicates whether the wedge should be drawn. I'll say more about how that is used and the IsHidden method in the following section

When the mouse moves over the sunburst chart's PictureBox, the following event handler executes.

// Display information about the wedge under the mouse. private void picSunburst_MouseMove(object sender, MouseEventArgs e) { // Find the wedge under the mouse. foreach (Wedge wedge in Wedges) { if (wedge.ContainsPoint(e.Location)) { DisplayWedgeInfo(wedge); return; } } // We didn't find a wedge containing // the mouse. Clear the info. DisplayWedgeInfo(null); }

This method simply loops through the objects in the Wedges list calling their ContainsPoint methods. If it finds an object that contains the mouse's location, the method calls the DisplayWedgeInfo method to display that wedge's information and then returns. If none of the Wedge objects contains the mouse's location, the method calls the DisplayWedgeInfo method passing it null to clear any previously displayed information.

The following code shows the DisplayWedgeInfo method.

// If this is a new Wedge under the mouse, // display its information. private void DisplayWedgeInfo(Wedge wedge) { // If the Wedge under the mouse has // not changed, do nothing. if (wedge == WedgeUnderMouse) return; WedgeUnderMouse = wedge; // See if the FgColor is Transparent. if ((wedge == null) || (wedge.IsHidden)) { // It's null or Transparent. Clear the label. lblWedgeUnderMouse.Text = ""; } else { // It's not Transparent. // Display the Wedge's information. lblWedgeUnderMouse.Text = wedge.Text.Replace("\\n", " "); } }

This method compares its parameter to the previously displayed Wedge stored in the WedgeUnderMouse variable. If the new object is the same as the old one, then that object's information is already shown so the method returns.

If the new Wedge object is different from the previously displayed one, the code saves the new object in the variable WedgeUnderMouse. Next, if the wedge is null or hidden, the method clears the text in the lblWedgeUnderMouse label.

If the wedge is not null and not hidden, the code displays its text in the label. The code also replaces the \n escape sequence with a space so it can display multi-line text.

Drawing Empty Wedges

One approach you can use to draw an empty wedge is to make an entry that has the same FgColor and BgColor values. Then the entry's text and background have the same color, so you can't see the text.

Unfortunately the program makes descendant elements inherit the foreground and background colors of their parents. If you give an element matching foreground and background colors, then its descendants inherit those colors. To make them visible, you would need to explicitly assign them new colors. That's not the end of the world, but it removes the advantage of inherited colors and clutters the XML file.

To avoid those problems, I modified the program to recognize a new IsHidden attribute. For example, the following code shows the part of the example program's XML data that defines the fruit section of the sunburst chart.

<Fruit BgColor="Orange" IsHidden="true"> <Banana /> <Peach /> <Frog /> </Fruit>

The first line defines the Fruit element. It gives that element an orange background color so its children inherit that color. The Fruit element has IsHidden="true", so that element is marked as hidden.

When the program is loading an element, it uses the following IsHidden method to determine whether the wedge should be hidden.

// Return true if the wedge should be hidden. private bool IsHidden(XmlNode node) { if (node.Attributes["IsHidden"] == null) return false; return (bool.Parse(node.Attributes["IsHidden"].Value)); }

This method gets an XML node's IsHidden attribute. If the attribute is not present, then the method returns false to indicate that the node should not be hidden. If the attribute is present, then the method parses it ass a Boolean value and returns the result.

The last interesting new piece in the example is where it decides whether it should draw a wedge. The DrawSunburstChild method fills the wedge's GraphicsPath and draws its text. Before it does so, it checks whether the wedge should be hidden. For example, the following code snippet fills and outlines the wedge's GraphicsPath.

// See if this wedge should be hidden. bool is_hidden = IsHidden(node); bg_color = GetNodeColor(node, "BgColor", default_bg_color); if (!is_hidden) { using (Brush brush = new SolidBrush(bg_color)) { gr.FillPath(brush, path); } gr.DrawPath(Pens.Black, path); }

This code calls the IsHidden method to see if the wedge should be hidden. It then gets the wedge's background color. If the wedge should not be hidden, the code fills the wedge's GraphicsPath and outlines it in black.

Summary

To determine the wedge under the mouse, the program uses a list of Wedge objects. When the mouse moves, it simply loops through the objects to see if any contain the mouse's location. This is a simple and very versatile strategy that you can use to determine what the mouse is above in many programs.

To avoid drawing a wedge, the program allows its XML elements to have a new IsHidden attribute. When it processes an element, the code now saves that value in the Wedge class's IsHidden field. It also uses the value to decide whether it should draw the wedge. You can use a similar technique to add other attributes to the XML elements so they can hold other information.

Because the program now uses Wedge objects to store information about the wedges, you could modify it to do other things to the wedges. For example, you could redraw any individual wedge to give it new colors, perhaps when the mouse moves over it, when the user clicks on it, or when the user selects the corresponding item in the TreeView control on the program's left side.

This example only includes a few snippets of code because most of the code is the same as in the earlier post. See that post and download this example to see all of the code, to experiment with the program, and to learn about other details.

Download the example to experiment with it and to see additional details.

© 2009-2023 Rocky Mountain Computer Consulting, Inc. All rights reserved.