Simple Extentions to Java AWT and Swing Classes for Mathematical Display

Kevin Neelands
March 14, 2008

Introduction

   Java AWT and Swing provide a very full suite of functionality to layout screens and display information. But sometimes they lack the full power of we need. In these cases the object oriented concept of inheritance allows us to extend and tweak the existing capabilities to suit our needs. This article examines a bag of such tricks for mathematics used in a Completing the Square tutorial written by the author.

Educational Concepts

   As you run this tutorial, please note a few instructional methods it uses. First, it puts the user in control, both by allowing the user to enter the values to use, and by allowing the user to control the pace of the presentation with the slider. Using the slider, the student can even easily have steps repeated. Second, it uses multiple modalities. The process is explained in general terms AND specific terms, as well as in symbolic equations AND graphically. The process is shown in one panel in general terms, carried through using only variable names and not numeric values, to prove the point that concepts, not numbers, are important. It is also carried out in specific terms with numerical values, to prove the point that nothing is more important than numbers. If you think that's confusing, just wait a minute or two. (Not that these have anything to do with the topic, I just wanted to point that out.) The important thing is to display each of the steps as students are used to seeing them done, in a format they are familiar with.

Completing the Square

   From an algebra standpoint, Completing the Square has it all. moving terms, canceling terms, squares and square roots, finding common denominators and adding fractions. It is to algebra what an obstacle course is to boot camp. It is the Ronco Slice-&-Dice Vegematic of arithmetic. It is the Pandoras box of mathematics. If you are smart enough to complete the square to find the roots of a quadratic equation, you can probably do your own taxes, or vote in South Florida. Before we go much further, please take a moment to look at the C.T.S. tutorial. Simply move the slider on the bottom from the left to the right to watch the computer perform and explain every step of the process.
Applet 1. - Completing the Square Tutorial

(If nothing appears above you may need to upgrade the Java on your computer. Click here to install Java 6.)
You probably noticed a number of things that you can't display with with Java as it comes out of the box:
  1. Square roots symbols
  2. Fractions with horizonatal bars
  3. Exponents (squares)
  4. Parentheses that vertically span fractions
  5. Crossed-out terms
Before we delve into how to perform these little feats of magic, here's some background information.

Representing mathematical expressions as trees

   It is standard practice to represent a mathematical expression as a tree. Operands - addition, subtraction, mutiplication, division and exponentiation are nodes with left and right branches, while numbers and variables are terminals, or leaves, on the tree. An expression can be evaluated with a simple recursive routine. In this case we create a layout in the same recursive manner. Each node will be a panel, so it can be a container for left and right nodes. It will contain a label that will show the variable or value if the node is a terminal or the operand if the node has branches. This picture shows the tree structure used to hold the expression:

A*X2 + B*X + C

Figure 1. - Tree structure of Mathematical Expressions
   At first, it looks as if a simple FlowLayout will lay out each node as we wish - and indeed it works for addition, subtraction, multiplication and terminals. The FlowLayouts left-to-right arrangement fits in with expressions like 3 * x + 4, but we have a problem with division if we want to show fractions. Teachers and students alike complain if you display one half of X as 1/2 X. The fix is rather obvious - division nodes will use a grid layout instead of a flowlayout to position the numerator over the denominator instead of beside it. We still have to put the horizontal bar in place. We'll get to that later. But another problem comes up with exponents. Teachers prefer to see exponents the same way they draw them on the blackboard - as superscripts - and are tired of seeing X squared written x^2. The Flowlayout has no provision for superscripts, and exponents will appear vertically centered. We will have to fine tune Flowlayout to get the results we want.

Extending FlowLayout to FlowLayoutPlus

   The Flowlayout places one component after another, left to right, each component vertically centered in its row. Each component is laid out with its upper left corner specified by and X and Y distance from the upper left corner of the container, the Y distance being (Container Height - Component Height)/2. So if we have the phrase 5 ft3 of h2o composed of labels, and we reduce the font size of the '3' and the '2', this image shows what we'll get as opposed to what we want:
Figure 2. - FlowLayout
So you can see we want the superscript '3' to have a y distance of 0 relative to the container top, and the '2' subscript to have a y distance of (Container Height-Component Height). In pseudo-code:
	yDelta = (ContainerHeight - componentHeight);
	if ( isaSuperscript )
		y = 0;
	else if ( isaSubscript )
		y = yDelta;
	else
		y = yDelta/2; // 'normal' case
   The changes we want are fairly simple - designate some components as superscripts, and when they are laid out their top should align with the top of the container they are in instead of being vertically centered. So first, we add a HashMap to the class to save the component/String combinations and then add this function:
public void setComponentVALign(Component comp, String name)
setComponentVAlign will add Component & String pairs to the hashMap. Components that are superscripts will have the string "super" associated with them via the hashmap, substring components will have "sub" associated with them, and "normal" components won't be entered into the collection. Then we modify the function that does the bulk of the work in FlowLayout:
private void moveComponents(Container target, int x, int y, int width, int height, int rowStart, int rowEnd, boolean ltr) 
so that the Y coordinate of components designated as 'super' is the same as the parents' top. But we test it and it doesn't work! At first it looks as if this whole inheritance concept is a cruel hoax! On further investigation it turns out the function moveComponents() is a private function, so it can be only called from within its own class. When the calling function in the base class calls moveComponents() it is restricted to calling the moveComponents() within it, for all practical purposes negating the benefits of inheritance. Initially it appears declaring a function private is the same as declaring it final, but on closer look we see we just have to include the calling function in our class. This seems to be an unneccesary bother and indicates you should declare a function private only when you have a very good reason. The astute reader may observe that we could simply use HTML to display the entire line of text, but in our case we need all the terminals and nodes in our tree structure to be discrete and separate entities. However, this class does have the toString() function overwritten to return just such an HTML string for those cases where we don't need to keep them separate. The source to the FlowLayoutPlus class is included later.

   The square root radical is pretty cool, and those parentheses that vertically span fractions are spiffy. They're certainly not the parentheses [ (,) ] that are provided by the font. Those shapes can be very easily created in a graphics context with some simple function calls - indeed, a radical is simply three straight lines, but keeping their position straight so they position themselves around the text must be pretty difficult, right? Wrong! It's easier than spilling beer on your dates' lap when your pizza arrives.

Extending Borders

   The radical and the parentheses are borders! Pretty clever... the Border class makes positioning the graphics calls needed for the symbols and keeping them out of the way of the inner and surrounding text incredibly simple, as you'll see later in the code. (Source code is included at the end of this file.)
   The AbstractBorder class simply requires that you specify the insets - the distances from the top, left, right, and bottom sides of the component that the system will leave blank and not use to display the component's text, leaving you free to draw in those areas as you see fit. In the case of the parentheses a left and right offset are specified, while the top and bottom insets are left at 0. Arcs are drawn in the left and right areas, resulting in pleasant-looking parenthesis. In the case of the square root, a thin top offset and a left offset are specified, the bottom and the right left at 0. Three line segments are all we need for the radical. That's all you have to do - the AbstractBorder class and the LayoutManager take care of the rest!
Figure 3. - Math Borders
   That leaves two features that need explaining - crossing terms out and fractions - three things if you include the highlighting, but you've probably already figured out that's done by setting the background color. The 'node' class extends from JPanel so it can lay out its children, but we are going to enhance it to draw the cross-outs and fractions. The fractions are done with a grid layout, but they still need that horizontal bar. Similarily, the crossed out items need the diagonal bar. All we need to do is extend JPanel and write our own paint() function. Sorta like this:
public void paint ( Graphics g ) {		
	super.paint(g);
	if ( isaFraction )
		drawHorizontalBar(g);
	if ( isCrossedOut )
		drawDiagonalBar(g);
}
   Well, its a little more complicated than that. But not much. But isn't that slick? - extend a class and overwrite one of its functions, and then that new function first calls the very function it is replacing in the base class, and then does only the needed additional extra work after that.

Inheritance for its own sake?

   While on the topic of using inheritance to make your life easier take a look at this, I declare these inner classes:
    class JTextFieldHex extends JTextField {}
    class JTextFieldDec extends JTextField {}
    class JTextFieldFloat extends JTextField{}
   WHY would I declare new derived classes and not add any new functionality to them? Well, let's examine a function in the applet that gets added as a KeyListener to these guys:
protected static final String strValidDecChars = "+-0123456789";	
protected static final String strValidHexChars = "+-0123456789abcdefABCDEF";	
protected static final String strValidFloatChars = "+-.eE0123456789";

public void keyTyped(KeyEvent e) {
	Object src = e.getSource();
	char ch = e.getKeyChar();
		
	if ( src instanceof JTextFieldHex ) {
		if (strValidHexChars.indexOf(ch) ==-1 && ch!=KeyEvent.VK_BACK_SPACE )
			e.consume();
	} else if ( src instanceof JTextFieldDec ) {
		if (strValidDecChars.indexOf(ch) ==-1 && ch!=KeyEvent.VK_BACK_SPACE )
			e.consume();
	} else if ( src instanceof JTextFieldFloat ) {
		if (strValidFloatChars.indexOf(ch) ==-1 && ch!=KeyEvent.VK_BACK_SPACE )
			e.consume()	
	}
}
Do you see what happens? Depending on the class of the source, invalid keystrokes are disregarded. Talk about a quick and easy way to restrict keyboard input!

Summary:

   It can't be overemphasized: Always look to see if an existing class already does the bulk of the work you want done, and save time and effort by extending that class. In this paper we have seen these examples:
  1. We created mathematical symbols by extending the BorderClass. This saved a ton of effort. I know- it did it the hard way first time through.
  2. We provided for easy superscripting and subscripting with a simple extension to the FlowLayout class.
  3. We added the mathematical concepts of fractions and "crossing-out" terms with simple extensions to the JPanel class.
  4. We even used inheritance WITHOUT any code in the new class to simplify keyboard input.
Here is the source code I promised earlier:

import java.awt.*;
import java.util.HashMap;

public class FlowLayoutPlus extends FlowLayout {
	  private static final long serialVersionUID = 1L;
	  
    /**
     * VertAlignMap stores optional info about vertical orientation
     */

     HashMap<Component,String> VerticalAlignMap;

     /*
      * our constructor
      */
     
     public FlowLayoutPlus(int align, int hgap, int vgap) {
    	super(align,hgap,vgap);
    	VerticalAlignMap = new HashMap<Component,String>();
    	}
     
     /**
      * Specifies whether a component should be a superscript or subscript.
      * This is the only function we are ADDING to flowlayout
      * 
      * @param comp the component to be added
      * @param name the name vertical alignment ( super or sub )
      */
     public void setComponentVALign(Component comp, String name) {
       name = name.toLowerCase();
       if ( name.equalsIgnoreCase("plain") || 
            name.equalsIgnoreCase("center"))
          VerticalAlignMap.remove(comp);
       else
 	      VerticalAlignMap.put(comp, name);
     }
     
     /**
      * Centers the elements in the specified row, if there is any slack.
      * 
      * This function overwrites the same function in FlowLayout
      * -- but --
      * since it is a PRIVATE function it can only be called from within its own class,
      * so it does not get called as automatically as we'd like
      * we have to turn around and include the public function that calls it
      * 
      * @param target the component which needs to be moved
      * @param x the x coordinate
      * @param y the y coordinate
      * @param width the width dimensions
      * @param height the height dimensions
      * @param rowStart the beginning of the row
      * @param rowEnd the the ending of the row
      */
     private void moveComponents(Container target, int x, int y, int width, int height,
                                 int rowStart, int rowEnd, boolean ltr) {
       String strVert;
       int yOffset;
       int iDeltaHeight;

       synchronized (target.getTreeLock()) {
    	   switch (getAlignment()) {
    	   case LEFT:
    		   x += ltr ? 0 : width;
    		   break;
    	   case CENTER:
    		   x += width / 2;
    		   break;
    	   case RIGHT:
    		   x += ltr ? width : 0;
    		   break;
    	   case LEADING:
    		   break;
    	   case TRAILING:
    		   x += width;
    		   break;
    	   }
 	for (int i = rowStart ; i < rowEnd ; i++) {
 	    Component m = target.getComponent(i);
 	    strVert = (String)VerticalAlignMap.get(m);
 	    iDeltaHeight = height-m.getHeight();
 	    yOffset = y + iDeltaHeight/2;
 	    if (strVert!=null ) {
             if ( strVert.equals("sup") || strVert.equals("super")) 
                 yOffset = 0; 
             if ( strVert.equals("sub"))
                 yOffset = y + iDeltaHeight;
             }
 	    if (m.isVisible()) {
 	        if (ltr) {
         	    m.setLocation(x, yOffset );
 	        } else {
 	            m.setLocation(target.getWidth() - x - m.getWidth(), yOffset );
                 }
                 x += m.getWidth() + getHgap();
 	    		}
 	    	}
 		}
     }
     /**
      * Lays out the container. This method lets each 
      * visible component take
      * its preferred size by reshaping the components in the
      * target container in order to satisfy the alignment of
      * this FlowLayout object.
      *
      * This function overwrites the function of the same name in FlowLayout.
      * It is include only because we need it to call our version of moveComponents.
      * We have to do a little extra work to properly access private variables.
      * 
      * @param target the specified component being laid out
      * @see Container
      * @see       java.awt.Container#doLayout
      */
     public void layoutContainer(Container target) {
       synchronized (target.getTreeLock()) {
 	Insets insets = target.getInsets();
 	int hgap = getHgap(); // hgap is private, need to use local variable and call 'get'
 	int vgap = getVgap(); // same deal with vgap
 	int maxwidth = target.getWidth() - (insets.left + insets.right + hgap*2);
 	int nmembers = target.getComponentCount();
 	int x = 0, y = insets.top + vgap;
 	int rowh = 0, start = 0;

         boolean ltr = target.getComponentOrientation().isLeftToRight();

 	for (int i = 0 ; i < nmembers ; i++) {
 	    Component m = target.getComponent(i);
 	    if (m.isVisible()) {
 		Dimension d = m.getPreferredSize();
 		m.setSize(d.width, d.height);

 		if ((x == 0) || ((x + d.width) <= maxwidth)) {
 		    if (x > 0) {
 			x += hgap;
 		    }
 		    x += d.width;
 		    rowh = Math.max(rowh, d.height);
 		} else {
 		    moveComponents(target, insets.left + hgap, y, maxwidth - x, rowh, start, i, ltr);
 		    x = d.width;
 		    y += vgap + rowh;
 		    rowh = d.height;
 		    start = i;
 		}
 	    }
 	}
 	moveComponents(target, insets.left + hgap, y, maxwidth - x, rowh, start, nmembers, ltr);
       }
     }
}

and here is the code for MathBorders:

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.RenderingHints;

import javax.swing.border.AbstractBorder;

public class MathBorder extends AbstractBorder {
	static final long serialVersionUID = 1;
	
	public static final int NO_BORDER   = 0;
	public static final int PARENTHESES	= 1;
	public static final int SQUARE_ROOT	= 2;
	public static final int ABS_VALUE 	= 3;
	public static final int FLOOR 		= 4;
	public static final int CEILING 	= 5;
	
	private static final String[] strBorderNames = { 
	"NO_BORDER",
	"PARENTHESES",
	"SQUARE_ROOT",
	"ABS_VALUE",
	"FLOOR",
	"CEILING" };
	
	
	private static final int PWIDTH = 6;
	private static final int SR_TOP = 4;
	
	private static final RenderingHints qualityHints = new
	RenderingHints(RenderingHints.KEY_ANTIALIASING,
	RenderingHints.VALUE_ANTIALIAS_ON);
	private BasicStroke stroke2 = (new BasicStroke(2.0f));
	protected int iLineWidth = 2;
	protected boolean visible = true;
	protected Color clr;
	protected int iType;
	protected Insets theInsets;
	
	public MathBorder () {
		this(PARENTHESES);
	}
	
	public MathBorder ( int iType ) {
		
		if ( !isValidBorderID(iType) ) {
			System.out.println ( "Bad type " + iType + " passed to MathBorder");
			iType = NO_BORDER;
		}
		this.iType = iType;
		switch ( iType ) {
		case PARENTHESES: theInsets = new Insets (0,PWIDTH,0,PWIDTH); break;
		case SQUARE_ROOT: theInsets = new Insets (SR_TOP, PWIDTH,0 ,0); break;
		default: theInsets = new Insets (0,3,0,3); break;
		}
		clr = Color.black;	
	} 
	
	public void paintBorder (Component c, Graphics g, int x, int y, int w, int h ) {
		double theta;
		int hyp;
		int h2 = h/2;
		int startAngle;
		int ptx;
		int pty;
		int iWidth = PWIDTH;
		int iCompWidth;
		int iCompHeight;
		Graphics2D g2 = (Graphics2D)g;
		
		g.setColor(c.getBackground());
		g2.setRenderingHints(qualityHints);
		g2.setStroke(stroke2);
		iCompWidth = c.getWidth();
		iCompHeight = c.getHeight();
		 
		switch (iType ) {
		case ABS_VALUE:
		case FLOOR:
		case CEILING:
			 g2.fillRect(0,0,theInsets.left,iCompHeight);
			 g2.fillRect(iCompWidth-theInsets.right, 0, iCompWidth, iCompHeight);
			 if ( !visible )
				 break;
			 g2.setColor(Color.black);
			 g2.drawLine(2,0,2,iCompHeight);
			 g2.drawLine(iCompWidth-2,0, iCompWidth-2, iCompHeight);
			 if ( iType==FLOOR ) {
				 g2.drawLine( 2, iCompHeight-1, theInsets.left, iCompHeight-1);
				 g2.drawLine( iCompWidth-theInsets.right, iCompHeight-1, iCompWidth, iCompHeight-1);
			 } else if ( iType==CEILING ) {
				 g2.drawLine( 2, 2, theInsets.left, 2);
				 g2.drawLine( iCompWidth-theInsets.right, 2, iCompWidth, 2);
			 }
			 break;
		case SQUARE_ROOT:
			 g2.fillRect(0,0,iCompWidth, theInsets.top);
			 g2.fillRect(0,0,theInsets.left, iCompHeight);
			 if ( !visible )
				 break;
			 g2.setColor(Color.black);
			 g2.drawLine( 0,iCompHeight/4, iCompHeight/4, iCompHeight-4);
			 g2.drawLine(iCompHeight/4, iCompHeight-4, (iCompHeight-4)/2,2);
			 g2.drawLine((iCompHeight-4)/2,2, iCompWidth,2);
			 break;
		case PARENTHESES:
			g2.fillRect(x,y,PWIDTH,h-1);
			g2.fillRect(x+w-PWIDTH,y,PWIDTH,h-1);
			 if ( !visible )
				 break;
			g2.setColor(clr);
			hyp = (iWidth*iWidth + h2*h2)/(2*iWidth);
			theta = Math.atan((double)h2/(double)(hyp-iWidth));
			startAngle = (int)((180.0*theta)/Math.PI);
			startAngle *= 3;
			startAngle /= 4;
			ptx = x+w;
			pty = y+h2;
			g2.drawArc(ptx-2*hyp-1, pty-hyp, 2*hyp, 2*hyp, -startAngle, 2*startAngle);
			ptx = x;
			g2.drawArc(ptx+1, pty-hyp, 2*hyp, 2*hyp, 180-startAngle, 2*startAngle);
			break;
		}	
	} 
	
	public Insets getBorderInsets(Component c) {
		if ( iType==SQUARE_ROOT )  {
			theInsets.left = c.getHeight()/2;
			theInsets.top = SR_TOP;
			theInsets.right = theInsets.bottom = 0;
			// theInsets.set (SR_TOP, c.getHeight()/2,0 ,0);
		    }
		return theInsets;
	}
	
	public Insets getBorderInsets(Component c, Insets i) {
		if ( iType==SQUARE_ROOT )  {
			theInsets.left = c.getHeight()/2;
			theInsets.top = SR_TOP;
			theInsets.right = theInsets.bottom = 0;
			// theInsets.set (SR_TOP, c.getHeight()/2,0 ,0);
			}
		i.set(theInsets.top, theInsets.left, theInsets.bottom, theInsets.right);
		return i;
	}
	
	public boolean isBorderOpaque() { return true;}
	
	public void setLineWidth ( int iValue ) {
		iLineWidth = iValue;
		stroke2 = (new BasicStroke(iLineWidth));
	}
	
	public int getLineWidth() { return(iLineWidth);}
	public void setVisible(boolean bv) {
		visible = bv;
	}
	public boolean getvisible() {
		return(visible);
	}
	
	static public boolean isValidBorderID ( int iValue ) {
		if ( iValue>=PARENTHESES  &&  iValue<=CEILING )
			return(true);
		return(false);
	}	
}

>

Please feel free to copy and paste these classes for your own use. Please tell me about anything you do that's worth bragging about!
Kevin Neelands, kcn32605@yahoo.com