18. jul 2005 08:03, chp
Although Swing has a very rich set of good components it is very often necessary to create a custom component to display your data just right. When creating a custom component there are many things to consider. I have created a small example of a custom component that is usable for a very specific need, but of course it is always a good idea to evaluate if your custom component has a broader use than just your current app (i.e. a Calendar pop up or something). In this example we ignore all this common sense and assume that our custom component will only be used for this specific purpose.
Preface: For demonstration purposes I have created a very simple implementation of Buffons needle test. This is an old (16th century) way to determine the value of Mathematical constant PI. In theory it is very precise, but in practise it takes a LOT of trials to get even a basic value of PI. The experiment uses the Monte Carlo simulation method which uses random numbers to compute a value. Monte Carlo algorithms can succeed where there are no simple computational formulas.
This description contains some math and assumes some pre-existing understanding of geometry. If all you care about is the swing stuff you can skip this section. :-)
Description of Buffons needle test:
Suppose we dropped needles, each of length 1, at random on to a piece of paper ruled with parallel lines 1 unit apart. What is the probability that a needle crosses a line?
We use three uniformly distributed random values to represent a needle drop:
· X the x coordinate of needles midpoint
· Y the y coordinate of needles midpoint
· Θ (theta) the needles angle of rotation
Using X and Y we can deduct the perpendicular distance d to the nearest line.
Here is a small sketch of the setup:

We can see that the needle crosses the line whenever: 
The value of d can range from 0 to ½ and the value of Θ can range from 0 to π. This is the rectangle below which has an area of:


The curve inside the rectangle is the function:

The grey area under the curve is where:

The area of the grey region can be determined:

The probability that a needle crosses a line is the ratio of the shaded area under the curve to the total area of the rectangle:
This means that if continue to drop needles the ratio of the number of needles that cross a line to the total number of needle will approach

from which we can compute the value of π.
Very smart right? Personally I find this little algorithm very elegant (although it is neither terribly precise nor fast).
The dropping of needles (i.e. generation of random numbers and determination of line cross) is handled by a very basic class called BuffonNeedle. Every instance of this class models a single needle, and some static variables keep track of results. Please bear in mind that this is neither a Monte Carlo demo nor a PI finding demo. Therefore the BuffonNeedle class is somewhat naive in its design and is not at all expected to perform well. J In practice we will spend most of our time doing GC.
Swing stuff
We do however want to display our needle dropping in swing, and since none of the standard components seems appropriate we will make our own. The first thing we have to do is to decide which class should be our super class. We need to extend an existing swing (or AWT) class in order to be able to use our custom component as any other swing component. Since our component is to only draw our needles as they are dropped JPanel seems like a good choice. We create the following class:
public class CustomComponent extends javax.swing.JPanel
When customizing our subclass we have two choices - we can override either paint(Graphics gr) or paintComponent(Graphics gr). The paint() is to be considered as a more "raw" way of doing it. If your current look and feel specifies a special background color (almost all LF do this), you will have to set this your self whereas paintComponent is executed after super has taken care of such things. We therefore choose to override paintComponent(Graphics gr).
We also create two other panels, one to display results of our calculations and one to allow us to play a little with the way of paintComponent method draws.
Screenshot of application running:

The CustomComponent class also contains a static void main method that will create and show a JFrame containg all three panels.
The source code for CustomComponent.java:
*************************************
import java.awt.*;
import java.util.LinkedList;
import java.util.Vector;
import javax.swing.JFrame;
import javax.swing.JPanel;
/**
* CustomComponentTest is used to demonstrate various painting techniques.
*
* Creation Date: 15/07-2005
* @author Christian Petersen
* @version 1.0
*/
public class CustomComponent extends JPanel {
private boolean antiAliased = true;
private Color lineColor = Color.BLACK;
private Color needleColor = Color.GRAY;
//We use a Vector since this is threadsafe.
private Vector<BuffonNeedle> needles = new Vector<BuffonNeedle>();
//Since we do not what to meat Mr. OutOfMemoryException we limit the number of needles we keep and paint.
private int maximumNeedleCount = 1000;
private int unit;
private int height;
private int width;
private int needles_to_generate;
private BuffonResultsPanel results;
public CustomComponent(BuffonResultsPanel res) {
super();
results = res;
}
/**
* Adds a needle to list and removes oldest if max size has been reached.
* @param bn The needle to add
*/
public void addNeedle(BuffonNeedle bn) {
needles.add(bn);
if (needles.size() > maximumNeedleCount) {
needles.remove(0);
}
}
public Runnable NEEDLE_GENERATOR = new Runnable() {
public void run() {
//Generate and add needles.
for (int i = 0;i < needles_to_generate;i++) {
addNeedle(new BuffonNeedle(width, height, unit));
}
repaint();
results.updateValues();
}
};
/**
* Paints the current paper with lines and any needles currently in the list.
*/
public void paintComponent(Graphics gr) {
//First we let super draw itself, this will draw background color and borders
super.paintComponent(gr);
//We cast the given java.awt.Graphics objet to a java.awt.Graphics2D object.
Graphics2D g2d = (Graphics2D) gr;
//Set anti aliasing
if (antiAliased) {
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
}
//Get size of panel
Dimension dimension = getSize();
width = (int) dimension.getWidth();
height = (int) dimension.getHeight();
//Calculate and store the value of 1 unit
unit = height / 11;
//Draw horizontal lines
g2d.setColor(lineColor);
for (int i = 0;i < 10;i++) {
int y = unit + (i*unit);
g2d.drawLine(0, y, width, y);
}
//Set needle color and draw all needles in list
g2d.setColor(needleColor);
for (BuffonNeedle bn : needles) {
g2d.drawLine(bn.getX1(), bn.getY1(), bn.getX2(), bn.getY2());
}
}
public static void main(String[] args) {
JFrame frame = new JFrame();
frame.getContentPane().setLayout(new BorderLayout());
BuffonResultsPanel res = new BuffonResultsPanel();
CustomComponent cmb = new CustomComponent(res);
frame.getContentPane().add(new PaintController(cmb), BorderLayout.NORTH);
frame.getContentPane().add(cmb, BorderLayout.CENTER);
frame.getContentPane().add(res, BorderLayout.SOUTH);
frame.setSize(800, 600);
frame.setTitle("Test of custom swing component");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
public boolean isAntiAliased() {
return antiAliased;
}
public void setAntiAliased(boolean antiAliased) {
this.antiAliased = antiAliased;
}
public Color getLineColor() {
return lineColor;
}
public void setLineColor(Color lineColor) {
this.lineColor = lineColor;
}
public int getMaximumNeedleCount() {
return maximumNeedleCount;
}
public void setMaximumNeedleCount(int maximumNeedleCount) {
this.maximumNeedleCount = maximumNeedleCount;
}
public Color getNeedleColor() {
return needleColor;
}
public void setNeedleColor(Color needleColor) {
this.needleColor = needleColor;
}
public int getUnit() {
return unit;
}
public int getNeedles_to_generate() {
return needles_to_generate;
}
public void setNeedles_to_generate(int needles_to_generate) {
this.needles_to_generate = needles_to_generate;
}
}
*************************************
The source code for BuffonNeedle.java
*************************************
import java.util.Random;
/**
* This class implements the BuffonNeedle, which uses the MonteCarlo simulation method to determine the value of PI.
* This implementation has been created for demonstration purposes only and is NOT designed to perform well.
*
* Creation Date: 18/07-2005
*
* @author Christian Petersen (christian.petersen@ciber.dk)
* @version 1.0
*/
public class BuffonNeedle {
/** Generator used to get uniform distributed values */
private static final Random RANDOM = new Random(System.currentTimeMillis());
/** Total number of generated needles */
public static int TOTAL_NEEDLE_COUNT = 0;
/** Total number of needles that cross a line */
public static int TOTAL_CROSSINGS = 0;
/** x coord of one end of a needle */
private double x1;
/** x coord of the other end of a needle */
private double x2;
/** y coord of one end of a needle */
private double y1;
/** y coord of the other end of a needle */
private double y2;
/** Angle of rotation about the midpoint */
private double theta;
/** Indication of line cross */
private boolean crossingLine;
/**
* Generate new needle. This will drop the needle randomly at paper, and compute if line is crossed.
* NOTE: Paper is assumed to start at (0,0).
* @param xWidth The width of paper
* @param yHeight The height of paper
*/
public BuffonNeedle(double xWidth, double yHeight, double unit) {
TOTAL_NEEDLE_COUNT++;
// Compute random values for the x and y coordinates of the
// needle's midpoint, and for the needle's angle of rotation.
double xCenter = xWidth*RANDOM.nextDouble();
double yCenter = yHeight*RANDOM.nextDouble();
double theta = Math.PI*RANDOM.nextFloat();
double sin = Math.sin(theta);
double cos = Math.cos(theta);
// Rotate about the origin a 1-unit length needle on
// the x axis with endpoints at -0.5 and +0.5.
x1 = (-0.5d * unit) * cos;
x2 = (+0.5d * unit) * cos;
y1 = (-0.5d * unit) * (-sin);
y2 = (+0.5d * unit) * (-sin);
// Translate the needle to its location.
x1 += xCenter;
x2 += xCenter;
y1 += yCenter;
y2 += yCenter;
// Does the needle cross a line?
if (Math.floor((y1 / unit)) != Math.floor((y2 / unit))) {TOTAL_CROSSINGS++;}
}
/**
* Return the current computed value of pi
* @return the value
*/
public static double getPi() {
return (2d * TOTAL_NEEDLE_COUNT) / TOTAL_CROSSINGS;
}
public boolean isCrossingLine() {
return crossingLine;
}
public void setCrossingLine(boolean crossingLine) {
this.crossingLine = crossingLine;
}
public double getTheta() {
return theta;
}
/**
* Returned as an int so value can be used directly by .draw methods
* @return
*/
public int getX1() {
return (int) x1;
}
/**
* Returned as an int so value can be used directly by .draw methods
* @return
*/
public int getX2() {
return (int) x2;
}
/**
* Returned as an int so value can be used directly by .draw methods
* @return
*/
public int getY1() {
return (int) y1;
}
/**
* Returned as an int so value can be used directly by .draw methods
* @return
*/
public int getY2() {
return (int) y2;
}
}
*************************************
The source code for PaintController.java
*************************************
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.*;
public class PaintController extends JPanel {
private CustomComponent component;
public PaintController(CustomComponent cmp) {
super();
component = cmp;
JCheckBox jch = new JCheckBox("Draw Anti-aliased");
jch.setSelected(component.isAntiAliased());
jch.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
//We just change value - assuming JCheckBox has been initialized correct
component.setAntiAliased(!component.isAntiAliased());
component.repaint();
}
});
add(jch);
JButton lcolor = new JButton("Line Color");
lcolor.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
//We use static convenience method on JColorChooser to show a modal dialog
component.setLineColor(JColorChooser.showDialog(PaintController.this,
"Choose color of lines",
component.getLineColor()));
component.repaint();
}
});
add(lcolor);
JButton ncolor = new JButton("Needle Color");
ncolor.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
//We use static convenience method on JColorChooser to show a modal dialog
component.setNeedleColor(JColorChooser.showDialog(PaintController.this,
"Choose color of needles",
component.getNeedleColor()));
component.repaint();
}
});
add(ncolor);
final JSpinner spinner = new JSpinner(new SpinnerNumberModel(10000, 1, 100000000, 10000));
add(spinner);
JButton dropNeedles = new JButton("Add needles");
dropNeedles.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
//Set number of needles to add
component.setNeedles_to_generate(((Number) spinner.getValue()).intValue());
//Start new thread to generate needles. This will allow this method to return and the AWTEventQueue to continue
new Thread(component.NEEDLE_GENERATOR).start();
}
});
add(dropNeedles);
}
}
*************************************
The soure code for BuffonResultsPanel.java
*************************************
import javax.swing.*;
public class BuffonResultsPanel extends JPanel {
private JLabel pi = new JLabel("\u03C0" + " = ");
private JLabel pi_value = new JLabel("no data");
private JLabel needles = new JLabel("Total needles = ");
private JLabel needles_value = new JLabel("0");
private JLabel crossings = new JLabel("Crossings = ");
private JLabel crossings_value = new JLabel("0");
public BuffonResultsPanel() {
super();
add(pi);
add(pi_value);
add(needles);
add(needles_value);
add(crossings);
add(crossings_value);
}
public void updateValues() {
pi_value.setText(Double.toString(BuffonNeedle.getPi()));
needles_value.setText(Integer.toString(BuffonNeedle.TOTAL_NEEDLE_COUNT));
crossings_value.setText(Integer.toString(BuffonNeedle.TOTAL_CROSSINGS));
repaint();
}
}
*************************************