Tip: Make parentheses matching more visible in C#

[parentheses]

Recently someone asked me if there was a Visual Studio add-in that highlighted parentheses, brackets, and braces so it would be easier to tell which closing parenthesis matched which opening parenthesis.

It turns out that Visual Studio already has a feature like this. When you place the cursor after a closing parenthesis (for example, the cursor is there right after you type the character), Visual Studio highlights that parenthesis and the matching opening parenthesis. By default the highlighting is light gray so it’s not easy to see, but if you take the following steps you can make it much more obvious.

  • In the Tools menu, select Options
  • In the Options dialog, open the Environment folder and select Fonts and Colors
  • Change “Brace Matching (Rectangle)” to the color you want to use for the highlight. For example, try Yellow or Lime.

The exact appearance of the Options dialog has changed in different versions of Visual Studio, but the general location of this option has remained the same: Tools > Options > Environment > Fonts and Colors, Brace Matching (Rectangle).

Now it will be obvious which parentheses form a pair.


Follow me on Twitter   RSS feed   Donate




Posted in coding, miscellany, tips | Tagged , , , , , , , , | 1 Comment

Display tips in a status bar instead of a tooltip in C#

[tooltip]

A tooltip provides information when a user needs it but remains unobtrusive when the user doesn’t need the information. For example, normally you can chug through a form filling in fields such as Name, Street, City, and State without any problem. However, if you get confused when you reach the “Oh hai!” field, you can hover the mouse over it to see a tooltip explaining what you should put in that field. Tooltips do such a good job of providing unobtrusive hints that they have been around virtually unchanged for decades.

Another method for providing hints even less obtrusively is to display tips message in a status label. When focus moves to a TextBox or the mouse moves over a button, you can display a tip in the status label. Experienced users can easily ignore that message but it’s instantly there for anyone who is confused.

In this example, the toolstrip buttons have tips stored in their Tag properties. The program uses the following code to display and remove the tips when the mouse enters or leaves a button.

// Set the button's status tip.
private void btn_MouseEnter(object sender, EventArgs e)
{
    ToolStripButton btn = sender as ToolStripButton;
    lblStatusTip.Text = btn.Tag.ToString();
}

// Remove the button's status tip.
private void btn_MouseLeave(object sender, EventArgs e)
{
    lblStatusTip.Text = "";
}

All of the example’s toolstrip buttons share these two event handlers. When the mouse moves over a button, the btn_MouseEnter event handler fires. It converts the sender parameter into the TooStripButton that raised the event. It then displays that button’s Tag value in the status strip label lblStatusTip.

When the mouse leaves a button, the btnNew_MouseLeave event handler clears the text in lblStatusTip.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in controls, tips, user interface | Tagged , , , , , , , , , , , , | 2 Comments

Make a sunburst chart in C#, Part 4


[sunburst chart]

The previous post showed how to make a sunburst chart with curved text. This example extends that one so it can draw multi-line text.

The previous post used the DrawTextOnArc method to draw curved text. The text is a distance radius from the chart’s center, and it is centered between the angles min_theta and max_theta. This example uses the DrawTextOnArc method to draw each of the lines of text needed for multiline descriptions.

The first step, however, is to represent multiline text in the XML data. The earlier examples use XML element names for the text. Unfortunately XML does not allow you to place line breaks inside an element name.

To work around that, I decided to add an optional Text attribute to the elements. If the attribute is present, the program uses its value for the node’s text. If the value is missing, the program uses the element’s name.

Within the Text value, the \n sequence indicates a new line. That’s my convention, not an XML convention. You can actually embed newlines within XML text, this just seemed simpler.

The DrawSunburstChild method uses the following code to find a node’s text.

// Get the node's text.
string text = node.Name;
if (node.Attributes["Text"] != null)
    text = node.Attributes["Text"].Value;

This code sets variable text equal to the node’s name. It then checks whether the node has the Text attribute and, if it does, it updates the text variable to hold its value.

After it finds the node’s text, the program calls the following new DrawNodeTextInWedge method to draw a node’s text.

// Draw multi-line text in a wedge.
private void DrawNodeTextInWedge(Graphics gr, Brush brush,
    Font font, string text, float cx, float cy,
    double min_theta, double max_theta, float mid_r)
{
    // Get the node's text.
    string[] separators = { @"\n" };
    string[] lines = text.Split(separators, StringSplitOptions.None);

    // See how tall each line should be.
    float hgt = font.Height * 0.8f;

    // Calculate the minimum and maximum radii,
    // and the change in radius per line dr.
    float min_r = mid_r - (lines.Length - 1) * hgt / 2f;
    float max_r = min_r + (lines.Length - 1) * hgt;
    float dr = (max_r - min_r) / (float)(lines.Length - 1);

    // Draw the lines of text.
    float radius = max_r;
    for (int i = 0; i < lines.Length; i++)
    {
        DrawTextOnArc(gr, brush, font, lines[i],
            cx, cy, min_theta, max_theta, radius);
        radius -= dr;
    }
}

The method first splits the text into lines. It calculates the font’s height and multiplies it by the fudge factor 0.8. It does that to make the lines of text sit closer together because the font’s normal height is a big larger than necessary for this kind of caption. You may need to fiddle with this value a bit to get the best result for your font.

[sunburst chart]

Next the code calculates the smallest and largest radii where the lines of text should be drawn. The picture on the right shows the relationships between the three radii used here. The value max_r is the distance from the center of the chart to the first line of text, min_r is the distance from the center to the last line of text, and mid_r is the distance from the center to the middle of the wedge.

The picture on the right uses three lines of text, but in general the node could have any number. For example, in the picture at the top of the post the wedge “All Kinds of Vegetables” uses three lines and the wedge “Dessert Choices” uses two lines. Note that the wedge “Various Sports” also uses three lines but the middle line is blank.

After it calculates the minimum and maximum radii, the code calculates the distance dr between adjacent lines. The code then loops through the lines of code and calls DrawTextOnArc to draw each of the lines at the appropriate radius.

That’s all I’m going to say about sunburst charts, for a while at least. There are plenty of enhancements you could make to the basic program. For example, I tried experimenting with colors that blend between parent and child nodes. It looked okay if the colors were related (blending from dark green to light green), but it looked terrible with unrelated colors (blending from red to blue).

It might also be nice to automatically size the font. Rotated text looks fuzzy or pixelated when the font is complicated and the characters are small. This example uses 10-point bold Arial, which is about the smallest font that looks nice. You could make the program adjust the font size to fit the available wedges.

Download the example and experiment with it. If you make any interesting enhancements, post them in the comments below.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in drawing, graphics, mathematics | Tagged , , , , , , , , , , , , , , , , , , , , | Leave a comment

Make a sunburst chart in C#, Part 3


[sunburst chart]

The post Make a sunburst chart in C#, Part 1 shows how to build a basic sunburst chart. The post Make a sunburst chart in C#, Part 2 enhances that example by drawing each node’s text at an angle so it fits within its wedge better.

This example draws node text along an arc so it fits its wedge better still.

The key is the following DrawTextOnArc method.

// Draw text along this arc.
private void DrawTextOnArc(Graphics gr, Brush brush,
    Font font, string text, float cx, float cy,
    double min_theta, double max_theta, float radius)
{
    // Use at most 32 characters.
    if (text.Length > 32) text = text.Substring(0, 32);

    // Make a CharacterRange for the string's characters.
    List<CharacterRange> range_list = new List<CharacterRange>();
    for (int i = 0; i < text.Length; i++)
    {
        range_list.Add(new CharacterRange(i, 1));
    }

    using (StringFormat sf = new StringFormat())
    {
        sf.SetMeasurableCharacterRanges(range_list.ToArray());

        // Measure the string's character ranges.
        RectangleF rect = new RectangleF(0, 0, 1000, 1000);
        Region[] regions =
            gr.MeasureCharacterRanges(text, font, rect, sf);

        // Get the total width.
        float total_width = regions.Sum(
            region => region.GetBounds(gr).Width);

        // Get the angle subtended by the text.
        double total_theta = total_width / radius;

        // Find the starting angle.
        double start_theta = (min_theta + max_theta - total_theta) / 2;

        // Find the angle per unit of character width.
        double theta_per_width = total_theta / total_width;
        
        // Draw the characters.
        for (int i = 0; i < regions.Length; i++)
        {
            float x = (float)(cx + radius * Math.Cos(start_theta));
            float y = (float)(cy + radius * Math.Sin(start_theta));
            float angle = (float)(start_theta / Math.PI * 180 + 90);
            PointF point = new PointF(x, y);
            DrawCenteredText(gr, font, brush,
                text[i].ToString(), point, angle);

            start_theta +=
                theta_per_width * regions[i].GetBounds(gr).Width;
        }
    }
}

This method uses the Graphics class’s MeasureCharacterRanges method to find the exact dimensions of each of the characters in the text that it will draw. The MeasureCharacterRanges method can only work for up to 32 characters, so the method starts by truncating the text to 32 characters if necessary. (If the text exceeds that length, the MeasureCharacterRanges method throws a non-descriptive “Overflow error” exception.)

To use MeasureCharacterRanges, the code makes a list of CharacterRange and adds CharacterRange structures to it. Each CharacterRange gives the starting position and length of a piece of the text that should be measured. That lets you measure pieces of the text that have different sizes. In this example, we want to measure the characters individually.

Next the code creates a StringFormat and calls its SetMeasurableCharacterRanges method to set the StringFormat object’s character ranges.

The code then creates a rectangle to hold the drawn text. If the rectangle were smaller, the StringFormat would use it to wrap the text. In this example we just want the sizes of the characters so the rectangle is large enough to prevent wrapping.

After all this set up, the code finally calls the Graphics object’s MeasureCharacterRanges method to measure the characters.

The code now uses a LINQ statement to add up the widths of the characters’ regions. It then uses the radius where it should draw the text to calculate the angle subtended by the text. It uses that angle together with the minimum and maximum angles of the text’s wedge to calculate where the first character should be drawn.

The code also calculates the number of radians per unit of character width so it can easily update the angle where the next character should be drawn after it draws each character.

Finally the code draws the characters. For each of the character regions, the code calculates the point where the text should be drawn and the angle it should have. It then uses the DrawCenteredText method described in the preceding post to draw the character at the desired location with the necessary rotation. The code then updates the value start_theta so it can draw the next character in the correct position.

There are plenty of enhancements you could make to the basic program. For example, I tried experimenting with colors that blend between parent and child nodes. It looked okay if the colors were related (blending between dark and light green), but looked terrible with unrelated colors (blending between red and blue).

It might also be nice to automatically size the font. Rotated text looks fuzzy or pixelated when the font is complicated and the characters are small. This example uses 10-point Arial, which is about the smallest font that looks nice. You could make the program adjust the font size to fit the available wedges.

Download the example and experiment with it. If you make any interesting enhancements, post them in the comments below.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in drawing, graphics, mathematics | Tagged , , , , , , , , , , , , , , , , , , , | 1 Comment

Make a sunburst chart in C#, Part 2

[sunburst chart]

The post Make a sunburst chart in C#, Part 1 explained how to build a simple sunburst chart. Unfortunately the node labels used by that chart didn’t fit their wedges very well because the text was all drawn horizontally. This post modifies the chart so it displays the text rotated to fit the node wedges better.

This change actually isn’t all that hard. The program simply uses the following new version of the DrawCenteredText method.

// Draw text centered at the position.
private void DrawCenteredText(Graphics gr, Font font, Color color,
    string text, PointF center, float angle)
{
    // Rotate.
    gr.RotateTransform(angle);

    // Translate.
    gr.TranslateTransform(center.X, center.Y, MatrixOrder.Append);

    // Draw the text.
    using (StringFormat sf = new StringFormat())
    {
        sf.Alignment = StringAlignment.Center;
        sf.LineAlignment = StringAlignment.Center;
        using (Brush brush = new SolidBrush(color))
        {
            gr.DrawString(text, font, brush, 0, 0, sf);
        }
    }

    // Reset the transformation.
    gr.ResetTransform();
}

This version of the method takes a new angle parameter that gives the angle at which the text should be drawn.

The method starts by adding a rotation transformation to the Graphics object that will draw the text. Now if the Graphics object draws text (or anything else), it will be rotated by the indicated angle.

Next the code appends a transformation to translate the drawing so the origin (0, 0) is moved to the text’s desired point center. The MatrixOrder.Append parameter is important because the default makes the Graphics object prepend the new transformation so it comes before the rotation. (I have never understood why Microsoft did it this way. It seems very nonintuitive to me.)

Now when you use the Graphics object to draw something, it is rotated and then translated.

The code then creates a StringFormat object and sets it up to center text. It creates a brush of the correct color and draws the text centered at the origin. The Graphics object’s transformations automatically rotate the text and translate it so it appears where it needs to be drawn.

The method finishes by resetting the Graphics object’s transformation. This is important. If you don’t do this, then the transformations for every piece of text pile on top of each other. The first piece of text is drawn where it should be, but later piece of text use stranger and stranger transformations so they don’t appear where they should. In fact, they probably will be mapped off of the image so they won’t appear at all.

The only other large change to the program is in the DrawSunburstChild method. When it is drawing text for a wedge with center angle mid_theta in radians, the method uses the following statement to calculate the text’s angle of rotation.

float text_angle = (float)(90 + mid_theta / Math.PI * 180);

This code converts mid_theta into degrees and then adds 90°. (If you don’t add 90°, then the text is oriented radially away from the center of the chart.)

After performing this calculation, the code passes the text’s angle into the DrawCenteredText method.

The final change to the program is that it uses a bold Arial font instead of Times New Roman. At the font size used here (10 point) and when rotated, Arial bold looks better than Times New Roman.

As always, download the example to see additional details. In my next post I’ll show how you can make the text curve to fit inside the wedges even better.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in drawing, graphics, mathematics | Tagged , , , , , , , , , , , , , , , | 2 Comments

Make a sunburst chart in C#, Part 1

[sunburst chart]

A sunburst chart displays hierarchical data in a circular diagram. The hierarchy’s root is drawn in the center. Its children sit in a ring around the root. After that, each node’s wedge in a ring is divided among its children in the next ring.

In the picture above, you can compare the data in the TreeView control on the left with the sunburst chart on the right.

This is a fairly complicated program so I’m going to start with a simple version and then add more features in the following posts. In this post I’ll explain how the program stores its data, how it loads the TreeView control, and how it draws the basic sunburst chart.

Storing Data

There are many ways you can store hierarchical data. For this example I decided to store the data in an XML file. The example uses elements for the values displayed on the chart. The elements can have two attributes, BgColor and FgColor, which give en element’s background and foreground colors.

The following code shows the example’s XML data.

<Items>
  <Food>
    <Vegetables BgColor="Green">
      <Beans BgColor="SandyBrown" />
      <Peas BgColor="LightGreen" />
      <Lettuce BgColor="Lime" />
    </Vegetables>
    <Desserts>
      <Tart FgColor="Blue" />
      <Cookie FgColor="Brown" />
      <Cake FgColor="Green" />
    </Desserts>
    <Fruit BgColor="Orange">
      <Banana />
      <Peach />
    </Fruit>
  </Food>
  <Sports BgColor="LightBlue" FgColor="Blue">
    <Volleyball />
    <Baseball />
  </Sports>
</Items>

Loading the TreeView

When the program starts, it executes the following code.

private XmlDocument XmlDoc = new XmlDocument();

private void Form1_Load(object sender, EventArgs e)
{
    // Load the XML document.
    XmlDoc.Load("test.xml");

    // Load the TreeView.
    LoadTreeViewFromXmlDoc(XmlDoc, trvItems);
    trvItems.ExpandAll();

    // Make the sun burst chart.
    MakeSunburst();
}

This code declares the variable XmlDoc to hold the XML document. It saves the data at the class level so the program can reread the data as many times as necessary.

The form’s Load event handler loads the XML document. It then calls the LoadTreeViewFromXmlDoc method to load the TreeView control. The event handler finishes by expanding all of the TreeView control’s nodes and then calling MakeSunburst, which is described later.

// Load a TreeView control from an XML file.
private void LoadTreeViewFromXmlDoc(XmlDocument xml_doc, TreeView trv)
{
    // Add the root node's children to the TreeView.
    trv.Nodes.Clear();
    AddTreeViewNode(trv.Nodes, xml_doc.DocumentElement);
}

// Add the children of this XML node 
// to this child nodes collection.
private void AddTreeViewNode(
    TreeNodeCollection parent_nodes, XmlNode xml_node)
{
    // Make the new TreeView node.
    TreeNode new_node = parent_nodes.Add(xml_node.Name);

    // Add child nodes.
    foreach (XmlNode child_node in xml_node.ChildNodes)
    {
        // Recursively make this node's descendants.
        AddTreeViewNode(new_node.Nodes, child_node);
    }
}

The LoadTreeViewFromXmlDoc method clears the TreeView control and then calls AddTreeViewNode to add the root node to the trv.Nodes collection of nodes.

The AddTreeViewNode method does all of the interesting work. It adds a node to the parent_nodes collection that it receives as a parameter. It then loops through the XML node’s children and recursively calls AddTreeViewNode to add them to the newly created TreeView node’s child collection.

Note that the program also calls MakeSunburst when it resizes so you can enlarge the form to make things fit better. It doesn’t resize the font, however. You’ll have to do that yourself in the code.

Drawing the Sunburst Chart

The following MakeSunburst method starts drawing the sunburst chart.

// Make a sunburst chart from the XML data.
private Bitmap MakeSunburst(int wid, int hgt, int margin,
    XmlDocument xml_doc, Color bm_color, Pen arc_pen, Font font)
{
    Bitmap bm = new Bitmap(wid, hgt);
    using (Graphics gr = Graphics.FromImage(bm))
    {
        gr.Clear(bm_color);
        gr.SmoothingMode = SmoothingMode.AntiAlias;

        // See how deep we must go.
        int depth = FindDepth(xml_doc.DocumentElement);

        // Calculate geometry.
        float cx = wid / 2f;
        float cy = hgt / 2f;
        wid -= 2 * margin;
        hgt -= 2 * margin;
        float dr = (Math.Min(wid, hgt) / 2f / depth);

        // Draw the root.
        RectangleF rect = new RectangleF(
            cx - dr, cy - dr, 2 * dr, 2 * dr);
        Color bg_color = GetNodeColor(XmlDoc.DocumentElement,
            "BgColor", Color.Transparent);
        using (Brush brush = new SolidBrush(bg_color))
        {
            gr.FillEllipse(brush, rect);
        }
        gr.DrawEllipse(arc_pen, rect);

        Color fg_color = GetNodeColor(XmlDoc.DocumentElement,
            "FgColor", Color.Black);
        DrawCenteredText(gr, font, fg_color,
            XmlDoc.DocumentElement.Name,
            new PointF(cx, cy));

        // Draw the other nodes.
        DrawSunburstChildren(gr, cx, cy, dr, 1,
            XmlDoc.DocumentElement.ChildNodes,
            0, 360, font, bg_color, fg_color);
    }
    return bm;
}

The method first creates a bitmap of the desired size, creates an associated Graphics object, and clears it. It then calls the following FindDepth method to see how deep the hierarchical data is.

// Return the depth of the XML sub-document rooted at this node.
private int FindDepth(XmlNode node)
{
    int depth = 1;
    foreach (XmlNode child in node.ChildNodes)
    {
        int child_depth = FindDepth(child);
        if (depth < 1 + child_depth) depth = 1 + child_depth;
    }
    return depth;
}

This method finds the depth of the sub-tree rooted at the indicated node. It sets variable depth equal to 1. It then loops over the node’s children, recursively calling itself to find the depth of the child sub-trees. If a child sub-tree’s depth plus 1 is greater than the current value of depth, the method updates depth.

After it finishes visiting the children, the method returns the final value of depth.

After it knows the depth of the hierarchical data, the MakeSunburst method calculates the X and Y coordinates of the center of the sunburst chart. It also calculates the chart’s width and height, and dr, the width of the rings.

Next the method draws the root node. This node is special because it is drawn in a circle but the other nodes are drawn in wedges within the rings.

To draw the root, the method makes a rectangle to define the inner circle. It calls GetNodeColor (described shortly) to get the root element’s background color, makes a brush of that color, and fills the center circle. It then uses GetNodeColor again to get the node’s foreground color and calls DrawCenteredText to draw the root’s text in the center of the sunburst chart.

The following code shows the GetNodeColor method.

// Return a node's Color attribute or
// the default value if there is no color.
private Color GetNodeColor(XmlNode node, string color_name,
    Color default_color)
{
    if (node.Attributes[color_name] == null) return default_color;
    try
    {
        return Color.FromName(node.Attributes[color_name].Value);
    }
    catch
    {
        return default_color;
    }
}

This method uses node.Attributes to see if the XML node has a particular attribute. The attribute will be either BgColor or FgColor in this example. If the attribute is not present, then the method returns the default color.

If the attribute is present, the method uses Color.FromName to see if this is a known color name. If the value is a known color, such as Red or LightBlue, then the method returns the resulting color. If the values isn’t a known color, such as Plaid or Coquelicot, the method returns the default color.

The following code shows the DrawCenteredText method.

// Draw text centered at the position.
private void DrawCenteredText(Graphics gr, Font font, Color color,
    string text, PointF center)
{
    using (StringFormat sf = new StringFormat())
    {
        sf.Alignment = StringAlignment.Center;
        sf.LineAlignment = StringAlignment.Center;
        using (Brush brush = new SolidBrush(color))
        {
            gr.DrawString(text, font, brush, center, sf);
        }
    }
}

This method creates a StringFormat object, sets its Alignment and LineAlignment properties to center the text vertically and horizontally, and then draws the text at the indicated location.

Now, back to the MakeSunburst method. The method ends by calling the following DrawSunburstChildren method to recursively draw the root node’s children.

// Draw the children of this node.
private void DrawSunburstChildren(Graphics gr,
    float cx, float cy, float dr, int level,
    XmlNodeList children, float min_angle, float max_angle,
    Font font, Color parent_bg_color, Color parent_fg_color)
{
    // Draw child arcs.
    int num_children = children.Count;
    float angle = min_angle;
    float dangle = (max_angle - min_angle) / num_children;
    foreach (XmlNode child in children)
    {
        // Draw this child.
        Color child_bg_color, child_fg_color;
        DrawSunburstChild(gr,
            cx, cy, dr, level,
            child, angle, angle + dangle, font,
            parent_bg_color, parent_fg_color,
            out child_bg_color, out child_fg_color);

        // Draw this child's children.
        DrawSunburstChildren(gr, cx, cy, dr, level + 1,
            child.ChildNodes, angle, angle + dangle, font,
            child_bg_color, child_fg_color);

        // Move to the next child's section.
        angle = angle + dangle;
    }
}

This method gets the number of children and uses that to calculate the angle subtended by each child. For example, if the parent’s arc goes from 90 to 120 degrees and it has 3 children, then each child gets (120 – 90) / 3 = 30 / 3 = 10 degrees of arc.

The method then loops through the children. For each child, the method calls DrawSunburstChild (described next) to draw the child. It then calls itself recursively to draw the child’s children.

After processing each child, the method increases the angle variable to position the next child.

The following code shows the DrawSunburstChild method.

// Draw a single node.
private void DrawSunburstChild(Graphics gr,
    float cx, float cy, float dr, int level,
    XmlNode node, float min_angle, float max_angle, Font font,
    Color default_bg_color, Color default_fg_color,
    out Color bg_color, out Color fg_color)
{
    // Draw the outline.
    double min_theta = min_angle / 180f * Math.PI;
    double max_theta = max_angle / 180f * Math.PI;
    float inner_r = level * dr;
    float outer_r = inner_r + dr;
    RectangleF outer_rect = new RectangleF(
        cx - outer_r, cy - outer_r,
        2 * outer_r, 2 * outer_r);
    RectangleF inner_rect = new RectangleF(
        cx - inner_r, cy - inner_r,
        2 * inner_r, 2 * inner_r);

    float inner_min_x = (float)(cx + inner_r * Math.Cos(min_theta));
    float inner_min_y = (float)(cy + inner_r * Math.Sin(min_theta));
    float outer_min_x = (float)(cx + outer_r * Math.Cos(min_theta));
    float outer_min_y = (float)(cy + outer_r * Math.Sin(min_theta));

    float inner_max_x = (float)(cx + inner_r * Math.Cos(max_theta));
    float inner_max_y = (float)(cy + inner_r * Math.Sin(max_theta));
    float outer_max_x = (float)(cx + outer_r * Math.Cos(max_theta));
    float outer_max_y = (float)(cy + outer_r * Math.Sin(max_theta));
    
    GraphicsPath path = new GraphicsPath();
    path.AddArc(outer_rect, min_angle, max_angle - min_angle);
    path.AddLine(outer_max_x, outer_max_y, inner_max_x, inner_max_y);
    path.AddArc(inner_rect, max_angle, min_angle - max_angle);
    path.AddLine(inner_min_x, inner_min_y, outer_min_x, outer_min_y);
    path.CloseFigure();

    bg_color = GetNodeColor(node, "BgColor", default_bg_color);
    using (Brush brush = new SolidBrush(bg_color))
    {
        gr.FillPath(brush, path);
    }
    gr.DrawPath(Pens.Black, path);

    // Draw the text.
    double mid_theta = (min_theta + max_theta) / 2;
    float mid_r = (inner_r + outer_r) / 2;
    float text_x = (float)(cx + mid_r * Math.Cos(mid_theta));
    float text_y = (float)(cy + mid_r * Math.Sin(mid_theta));
    string text = node.Name;
    fg_color = GetNodeColor(node, "FgColor", default_fg_color);
    DrawCenteredText(gr, font, fg_color, text,
        new PointF(text_x, text_y));
}

[sunburst chart]

This method draws a single node’s data in a sunburst chart ring. Basically it uses some mathematics to calculate the four corners of the wedge and then makes a GraphicsPath to outline the wedge. In the picture on the right, the code adds the arc AB, the segment BC, the arc CD, and finally the segment DA to the GraphicsPath.

The method then fills and outlines the GraphicsPath.

Next the method finds the point E at the center of the wedge. It is at the angle halfway between the wedge’s minimum and maximum angles. Its distance from the center of the sunburst chart is half of the distances between the wedge’s inner and outer arcs.

After it finds the center point E, the method uses DrawCenteredText to draw the node’s text centered at that point.

Next Time…

That’s enough for now. This is a pretty long post because it needs to cover a certain minimum amount to draw a basic sunburst chart. Despite its length, this post still glosses over a number of points. Download the example to see additional details.

In my next post, I’ll explain how to draw rotated text so the node labels fit inside the wedges better.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in drawing, graphics, mathematics | Tagged , , , , , , , , , , , , , | 3 Comments

Make a status strip label that sticks to the right side of the form in C#

[status strip label]

To create a status strip label, add a StatusStrip control to the form. When you select the StatusStrip, it should display an icon with a dropdown arrow. Click the arrow and select the kind of control you want to add to the StatusStrip. This can be StatusLabel, ProgressBar, DropDownButton, and SplitButton.

I recommend that you avoid the buttons and reserve the StatusStrip for status information and not commands.

If you set a status strip label control’s Spring property to true, then that label takes up any space not used by other controls in the StatusStrip. If the status strip label is the last one in the StatusStrip, then it extends to the right edge of the StatusStrip. If you set its TextAlign property to MiddleRight, then the label places its text at the right edge of the StatusStrip.

Note that you can place controls both to the left and right of a label with Spring = true. In that case the label with Spring = true will take up whatever space is available.

You can also set Spring = true on more than one status strip label to make them share the available space.

The final point of interest in this example is the “Normal label” on the StatusStrip control’s left side. That label’s BorderSides property is set to All and its BorderStyle property is set to Sunken so it appears sunken below the surface of the form.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in controls, user interface | Tagged , , , , , , , , , , | Leave a comment

Parse file sizes in KB, MB, GB, and so forth in C#

[parse]

The example Format file sizes in KB, MB, GB, and so forth in C# shows how to convert a number into a string formatted in KB, MB, etc. This example does the opposite: it parses a value such as “1.23 TB” and returns a number.

The following ParseFileSize method does all of the interesting work.

// Parse a file size.
private string[] SizeSuffixes =
    { "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" };
private double ParseFileSize(string value, int kb_value)
{
    // Remove leading and trailing spaces.
    value = value.Trim();

    try
    {
        // Find the last non-alphabetic character.
        int ext_start = 0;
        for (int i = value.Length - 1; i >= 0; i--)
        {
            // Stop if we find something other than a letter.
            if (!char.IsLetter(value, i))
            {
                ext_start = i + 1;
                break;
            }
        }

        // Get the numeric part.
        double number = double.Parse(value.Substring(0, ext_start));

        // Get the extension.
        string suffix;
        if (ext_start < value.Length)
        {
            suffix = value.Substring(ext_start).Trim().ToUpper();
            if (suffix == "BYTES") suffix = "bytes";
        }
        else
        {
            suffix = "bytes";
        }

        // Find the extension in the list.
        int suffix_index = -1;
        for (int i = 0; i < SizeSuffixes.Length; i++)
        {
            if (SizeSuffixes[i] == suffix)
            {
                suffix_index = i;
                break;
            }
        }
        if (suffix_index < 0)
            throw new FormatException(
                "Unknown file size extension " + suffix + ".");

        // Return the result.
        return Math.Round(number * Math.Pow(kb_value, suffix_index));
    }
    catch (Exception ex)
    {
        throw new FormatException("Invalid file size format", ex);
    }
}

The SizeSuffixes array holds suffixes such as KB and GB that represent powers of 1024 in a file size value.

The ParseFileSize method starts by finding the last non-alphabetic character in the string and separating the numeric beginning of the string from the alphabetic suffix at the end.

The method then uses double.Parse to parse the numeric piece.

It then finds the index of the suffix in the suffix list and uses Math.Pow to raise the value kb_value to the power of the index.

The parameter kb_value should be either 1,000 or 1,024 depending on whether the program wants to parse the value with 1,000 byte kilobytes or 1,024 byte kilobytes. Disk drive manufacturers usually use 1,000 byte kilobytes to make you think you’re getting a bigger disk drive than you really are.

For example, suppose the string is “1.2 MB” and kb_value is 1,024. Then the index of the suffix MB is 2 so the result is 1.2 * Math.Pow(1024, 2) = 1.2 * 10242 = 1,258,291.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in algorithms, files, mathematics | Tagged , , , , , , , , , | Leave a comment

Highlight the DataGridView row that is under the mouse in C#


[DataGridView]

Someone recently asked me how to change the style of the row under the mouse in a DataGridView control. This example does that. When the program starts, the following code prepares the DataGridView for use.

// The style to use when the mouse is over a row.
private DataGridViewCellStyle HighlightStyle;

private void Form1_Load(object sender, EventArgs e)
{
    // Define the highlight style.
    HighlightStyle = new DataGridViewCellStyle();
    HighlightStyle.ForeColor = Color.Red;
    HighlightStyle.BackColor = Color.Yellow;
    HighlightStyle.Font = new Font(dgvValues.Font, FontStyle.Bold);

    // Make some data items.
    dgvValues.Rows.Add(new object[]
        { "Interview Puzzles Dissected", 15.95m, 1 });
    dgvValues.Rows.Add(new object[]
        { "C# 24-Hour Trainer", 45.00m, 2 });
    dgvValues.Rows.Add(new object[]
        { "Beginning Software Engineering", 45.00m, 5 });
    dgvValues.Rows.Add(new object[]
        { "Essential Algorithms", 60.00m, 3 });
    dgvValues.Rows.Add(new object[]
        { "C# 5.0 Programmer's Reference", 49.99m, 1 });
    dgvValues.Rows.Add(new object[]
        { "Beginning Database Design Solutions", 44.99m, 2 });

    // Calculate totals.
    CalculateTotals();
}

// Calculate the total costs.
private void CalculateTotals()
{
    // Calculate the total costs.
    foreach (DataGridViewRow row in dgvValues.Rows)
    {
        // Calculate total cost.
        decimal total_cost =
            (decimal)row.Cells["PriceEach"].Value *
            (int)row.Cells["Quantity"].Value;

        // Display the value.
        row.Cells["Total"].Value = total_cost;
    }
}

The HighlightStyle variable will hold the style used to highlight the row under the mouse. The form’s Load event handler defines the style. It then adds some items to the DataGridView control and calls the CalculateTotals method.

The CalculateTotals method loops through the DataGridView control’s rows and displays a total price times quantity for each row.

The program uses the following SetRowStyle method to set a row’s style to a particular DataGridViewCellStyle.

// Set the cell Styles in the given row.
private void SetRowStyle(DataGridViewRow row,
    DataGridViewCellStyle style)
{
    foreach (DataGridViewCell cell in row.Cells)
    {
        cell.Style = style;
    }
}

This code loops through the row’s cells and sets their Style properties.

The real fun takes place in the DataGridView cell’s mouse events. The following code executes when the mouse enters one of the DataGridView control’s cells.

// The currently highlighted cell.
private int HighlightedRowIndex = -1;

// Highlight this cell's row.
private void dgvValues_CellMouseEnter(object sender,
    DataGridViewCellEventArgs e)
{
    if (e.RowIndex == HighlightedRowIndex) return;

    // Unhighlight the previously highlighted row.
    if (HighlightedRowIndex >= 0)
    {
        SetRowStyle(dgvValues.Rows[HighlightedRowIndex], null);
    }

    // Highlight the row holding the mouse.
    HighlightedRowIndex = e.RowIndex;
    if (HighlightedRowIndex >= 0)
    {
        SetRowStyle(dgvValues.Rows[HighlightedRowIndex],
            HighlightStyle);
    }
}

The program stores the index of the currently highlighted row in the variable HighlightedRowIndex. When the mouse enters a cell, the event handler compares the cell’s row to HighlightedRowIndex. If the cell is in the currently highlighted row, the correct row is already highlighted so the event handler does nothing else.

If the cell is not in the currently highlighted row, the program calls SetRowStyle to set the currently highlighted row’s style to null, effectively unhighlighting the row (if any row is highlighted). The program then updates HighlightedRowIndex and uses SetRowStyle to highlight the new row.

The last piece of code executes when the mouse leaves a cell.

// Unhighlight the currently highlighted row.
private void dgvValues_CellMouseLeave(object sender,
    DataGridViewCellEventArgs e)
{
    if (HighlightedRowIndex >= 0)
    {
        SetRowStyle(dgvValues.Rows[HighlightedRowIndex], null);
        HighlightedRowIndex = -1;
    }
}

If there is a currently highlighted row, this code simply unhighlights it.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in controls, user interface | Tagged , , , , , , , , , , , | Leave a comment

Use accelerators on labels and buttons in C#

[accelerators]

To place an accelerator on a menu item, you put an ampersand in front of the letter in the menu item’s caption that you want to be the accelerator. At run time, the caption is displayed with the accelerator letter underlined. For example, you would set the File menu’s caption to be &File at design time and it would display as File at run time. (Note that in some operating systems the accelerator isn’t underlined until the user presses the Alt key.)

Most programmers know that you can place accelerators on menu items to let the user access them by pressing the Alt key. For example, pressing Alt+F opens the File menu in most applications. However, many programmers don’t know that you can also use accelerators on labels and buttons.

If you place an accelerator on a label and the user triggers the accelerator, focus moves to the next control after the label that can have the focus. In the example program the Street label’s S is an accelerator. If you press Alt+S, the input focus moves to the next control in the tab order, which is the Street text box.

If you place an accelerator on a button, the user can use the accelerator to immediately fire the button. If you press Alt+O while running the example program, the OK button immediately fires.

Note that if you press and release the Alt key by itself, the program moves into a sort of accelerator mode. All of the accelerators become visible and you can press accelerator keys such as F and S to trigger various accelerators without pressing Alt again. This lets you easily navigate menus but it makes using labels and buttons with accelerators a bit more confusing. For example, you could press Alt, S to move to the Street text box. You might then like to start typing a street address but the program is still in accelerator mode so it treats any keys that you press as accelerators. You need to press Alt again or Escape to end accelerator mode.

You can avoid accelerator mode if you press Alt and the accelerator key at the same time as in Alt+Z. Then focus moves to the appropriate control without starting accelerator mode.

To make using your programs easier, you can add accelerators to labels before text boxes as in this example and to buttons.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in miscellany, productivity | Tagged , , , , , , , , , , , | Leave a comment