Recursively draw equations in C#

The basic idea is simple. The following abstract Equation base class defines three methods: SetFontSizes, GetSize, and Draw.

abstract class Equation
{
    // The font size used by this equation.
    public float FontSize = 20;

    // Set font sizes for sub-equations.
    public abstract void SetFontSizes(float font_size);

    // Return the equation's size.
    public abstract SizeF GetSize(Graphics gr, Font font);

    // Draw the equation.
    public abstract void Draw(Graphics gr, Font font, Pen pen, Brush brush, float x, float y);
}

The SetFontSizes method sets the equation's FontSize value. If the equation is made up of sub-equations, as most of the equation classes are, this method calls the sub-equations' SetFontSizes methods passing them scaled font sizes.

The GetSize method calculates the equation's size. If the equation includes sub-equations, it calls their GetSize methods to get their sizes and then uses them to calculate this equation's size.

The Draw method draws the equation. If the equation includes sub-equations, it calls their Draw methods to draw them.

Subclasses of Equation implement these methods according to how they draw their particular kind of equation. The example program defines these classes:

StringEquation Draws a string.
BarEquation Draws vertical elements to the left and right of its contents. The elements can be bars, brackets, braces, pointy brackets, parentheses, or omitted.
FractionEquation Draws an equation above another with an optional line between.
IntegralEquation Draws an integral with equations specifying the value inside the integral and the values above and below the integral symbol.
MatrixEquation Draws equations in an array, optionally making all rows or columns the same size.
PowerEquation Draws an equation to the power of another equation.
RootEquation Draws a root of an equation where the root's base is another equation.
SigmaEquation Draws a sigma summation with equations specifying the value inside the summation and the values above and below the sigma.

The StringEquation class shown in the following code is the most basic. It simply draws a string.

// Draw some text.
class StringEquation : Equation
{
    // The text to draw.
    private string Text;

    // Initialize the text.
    public StringEquation(string text)
    {
        Text = text;
    }

    // Set font sizes for sub-equations.
    public override void SetFontSizes(float font_size)
    {
        FontSize = font_size;
    }

    // Return the equation's size.
    public override SizeF GetSize(Graphics gr, Font font)
    {
        using (Font new_font = new Font(font.FontFamily, FontSize, font.Style))
        {
            return gr.MeasureString(Text, new_font);
        }
    }

    // Draw the equation.
    public override void Draw(Graphics gr, Font font, Pen pen, Brush brush, float x, float y)
    {
        using (Font new_font = new Font(font.FontFamily, FontSize, font.Style))
        {
            gr.DrawString(Text, new_font, brush, x, y);
        }
    }
}

This class provides some constructors. Its SetFontSizes method simply saves the font size. GetSize creates a font of the correct size and then measures the object's string using that font. Draw creates a font of the correct size and then draws the string.

The following code shows a less trivial example: the FractionEquation class.

// Draw one item over another.
class FractionEquation : Equation
{
    // True to draw a separator line.
    public bool DrawSeparator;

    // The space between the top and bottom items.
    private const float Gap = 0;

    // Extra width for the separator (on each side).
    private const float ExtraWidth = 6;

    // The items to draw.
    private Equation Numerator, Denominator;

    // Initialize a new object.
    public FractionEquation(Equation top_item, Equation bottom_item, bool draw_separator)
    {
        Numerator = top_item;
        Denominator = bottom_item;
        DrawSeparator = draw_separator;
    }

    // Initialize a new object.
    public FractionEquation(string top_string, string bottom_string, bool draw_separator)
        : this(new StringEquation(top_string), new StringEquation(bottom_string), draw_separator)
    {
    }

    // Set font sizes for sub-equations.
    public override void SetFontSizes(float font_size)
    {
        FontSize = font_size;
        Numerator.SetFontSizes(font_size * 0.75f);
        Denominator.SetFontSizes(font_size * 0.75f);
    }

    // Return the object's size.
    public override SizeF GetSize(Graphics gr, Font font)
    {
        // Get the sizes of the items.
        SizeF top_size, bottom_size;
        float width, height;
        GetSizes(gr, font, out top_size, out bottom_size, out width, out height);

        // Calculate our size.
        return new SizeF(width, height);
    }

    // Draw the equation.
    public override void Draw(Graphics gr, Font font, Pen pen, Brush brush, float x, float y)
    {
        // Get the sizes of the items.
        SizeF top_size, bottom_size;
        float width, height;
        GetSizes(gr, font, out top_size, out bottom_size, out width, out height);

        // Draw the separator.
        if (DrawSeparator)
        {
            float separator_y = y + top_size.Height + Gap / 2;
            gr.DrawLine(pen,
                x, separator_y,
                x + width, separator_y);
        }

        // Draw the top.
        float top_x = x + (width - top_size.Width) / 2;
        Numerator.Draw(gr, font, pen, brush, top_x, y);

        // Draw the bottom.
        float bottom_x = x + (width - bottom_size.Width) / 2;
        float bottom_y = y + top_size.Height + Gap;
        Denominator.Draw(gr, font, pen, brush, bottom_x, bottom_y);
    }

    // Return various sizes.
    private void GetSizes(Graphics gr, Font font, out SizeF top_size, out SizeF bottom_size, out float width, out float height)
    {
        top_size = Numerator.GetSize(gr, font);
        bottom_size = Denominator.GetSize(gr, font);
        width = Math.Max(top_size.Width, bottom_size.Width) + 2 * ExtraWidth;
        height = top_size.Height + bottom_size.Height + Gap;
    }
}

This class begins with some parameters that help determine how it draws a fraction. Its constructors initialize those settings.

The class's Numerator and Denominator values hold Equation subclasses that should be drawn for the top and bottom of the fraction.

The SetFontSizes method saves the font size and then calls its sub-equations' SetFontSizes methods passing it the font size scaled by 0.75. That makes the font used by the sub-equations slightly smaller and produces a nicer result. (This is even more important for some of the other equation types such as IntegralEquation and SigmaEquation where the results look really weird if the integral or summation's limit equations are not in a smaller font.)

The GetSize method calls GetSizes to calculate the sizes of the Numerator and Denominator. It then uses those sizes to calculate the whole FractionEquation's size.

The Draw method also calls GetSizes to get the sizes of the Numerator and Denominator. It then draws the fraction. It calls the Numerator's and Denominator's Draw methods to make them draw themselves.

The GetSizes method calls the Numerator's and Denominator's GetSize methods to get their sizes. It then adds some room for the line between the two and returns the combined fraction's size.

The other equation sub-classes work similarly. In each sub-class:

  • The SetFontSizes method calls the sub-equations' SetFontSizes methods, possibly passing them a scaled down font size.
  • The GetSize method calls sub-equations' GetSize methods and then uses the geometry of this particular sub-class to decide how those sizes combine to make the whole equation's size.
  • The Draw method calls sub-equations' Draw methods and adds other symbols needed by this equation such as integral signs, brackets, or root signs.

The sub-classes are somewhat involved but the basic ideas are simple. The only real differences are in how each draws its sub-equations.

   

 

What did you think of this article?




Trackbacks
  • No trackbacks exist for this post.
Comments

  • 11/16/2012 6:00 AM Pieter Geerkens wrote:
    Not the prettiest I have ever seen, but definitely usable. Thank you.
    Reply to this
    1. 11/19/2012 3:07 PM Rod Stephens wrote:
      Yeah, you could do a lot of work to make it prettier. Particularly some of the symbols such as the integral and radical. But I wanted something simple to show the structure of the program and I figure everyone will have their own favorite drawing styles.

      You could also add a lot more flexibility to things like character scaling, which fonts to use, and colors.

      If anyone comes up with variations they want to show off, post here so everyone can see!
      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.