Title: Recursively draw equations in C#
The basic idea for recursively drawing equations is simple. Well, sort of simple.
Classes represent different kinds of equations. A particular class knows how to draw its kind of equation. It uses other classes to draw its pieces.
For example, the FractionEquation class draws a horizontal line with a numerator on top and a denominator on the bottom. It uses other objects to draw the numerator and denominator.
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. This is the end of the chain of recursion. |
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 horizontal line in between. |
IntegralEquation | Draws an integral symbol with equations specifying the value inside the integral and the values above and below the 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 with equations for the index and radicand. |
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. The GetSize method creates a font of the correct size and then measures the object's string using that font. The Draw method 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 them 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 object'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 object's and the Denominator object's Draw methods to make them draw themselves.
The GetSizes method calls the Numerator object's and Denominator object'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 subclasses work similarly. In each subclass:
- 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 the particular subclass 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 symbols.
The subclasses are somewhat involved, but the basic ideas are simple. The only real differences are in how each draws its sub-equations.
Download the example to experiment with it and to see additional details.
|