Monday, November 21, 2011

Bringing a Custom Swing Component to Life

I am sometimes down on the Java documentation, but in the sand plains of lugubrious and often confusing material there is the occasional gem. One example is a lesson from The Java Tutorial entitled Performing Custom Painting. I was directed to it by a reply to this thread in the Oracle Java Desktop forum.

When I first began my efforts to create a rainbow colored Gaussian distribution curve I began with one of the Tutorial lessons on colors. I have unfortunately lost the URL for the lesson but the code began something like this:

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;

import javax.swing.JFrame;
import javax.swing.JPanel;

public class Colors extends JPanel {

public void paintComponent(Graphics g) {
super.paintComponent(g);

Graphics2D g2d = (Graphics2D) g;

g2d.setColor(new Color(255, 0, 0));//vivid red
g2d.fillRect(10, 15, 90, 60);

...

}

public static void main(String[] args) {

JFrame frame = new JFrame("Colors");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new Colors());
frame.setSize(360, 300);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}

It looked as shown below.

I modified this by putting the rectangles side by side and end on to produce a crude histogram as shown below:

I reduced the width of the rectangles (to one pixel) and their number (to 800), and made their height and color the subject of mathematical functions. The colors were produced by three out of phase sine waves. That idea from that came from this article by Jim Bumgardner. His explanation is very thorough, so I shall not repeat it here, but in recognition of the idea, my first rainbow colored curve was a sine wave, as shown below:

The mathematical function for a sine wave in Java is really simple:

y = Math.sin(x);

There is, alas, no inbuilt function for a Normal/Gaussian distribution curve, but Wikipedia gives the function as:

I used the middle part of this expression to produce the rainbow colored Gaussian distribution curve shown at the bottom of my previous post. But as I said there, it did nothing. I could not send messages to it or make it change.

I will admit that when I first read the reply to my forum post recommending the Custom Painting tutorial, I was not that optimistic, and I didn't look at it properly until after I had tried all the articles described in my previous blog post. But my cynicism was misplaced, and I should have started there.

The essential code construction from the tutorial begins as follows:

import javax.swing.SwingUtilities;
import javax.swing.JFrame;

public class SwingPaintDemo1 {

public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGUI();
}
});
}

private static void createAndShowGUI() {
System.out.println("Created GUI on EDT? "+
SwingUtilities.isEventDispatchThread());
JFrame f = new JFrame("Swing Paint Demo");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setSize(250,250);
f.setVisible(true);
}

And the first essential point to note is that it uses:

The SwingUtilities helper class to construct this GUI on the Event Dispatch Thread.

I don't fully understand this, but I've been told to do it before, and it was only when I used this structure for my "custom swing component" that I could get it to work properly.

The second slightly strange thing that the tutorial did was to create its own "custom" JPanel:

class MyPanel extends JPanel {

public MyPanel() {
setBorder(BorderFactory.createLineBorder(Color.black));
}

public Dimension getPreferredSize() {
return new Dimension(250,200);
}

public void paintComponent(Graphics g) {
super.paintComponent(g);

// Draw Text
g.drawString("This is my custom Panel!",10,20);
}
}

This was all in the same source file, which was modified by replacing:

f.setSize(250,250);

with

f.add(new MyPanel());
f.pack();

It looks trivial, but by following this structure, I was able to use the .pack() command in my applet. Using my original code construction (as shown above), the first time I inserted the new component, I thought it hadn't worked at all because it did not show, and it was only after adding padding to the gridbaglayout and manually resizing the applet that I could see it.

Another point worth noting is the line which inherits functionality from the parent component. This is described in the tutorial as follows:

Most of the standard Swing components have their look and feel implemented by separate "UI Delegate" objects. The invocation of super.paintComponent(g) passes the graphics context off to the component's UI delegate, which paints the panel's background.

This avoided me having to mimic the code structure of the standard swing components. I could focus on the code that made my component different.

The third thing that the tutorial did was to create a "sprite" and code to move it around. My "sprite" is my rainbow colored histogram, and I didn't need "event" code to drag it around the page. But I did need code to alter one or more of the parameters used to build the histogram. The code used by the tutorial was:

... previous imports
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseMotionAdapter;

... previous unchanged code

public MyPanel() {

setBorder(BorderFactory.createLineBorder(Color.black));

addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
moveSquare(e.getX(),e.getY());
}
});

addMouseMotionListener(new MouseAdapter() {
public void mouseDragged(MouseEvent e) {
moveSquare(e.getX(),e.getY());
}
});

}

private void moveSquare(int x, int y) {
int OFFSET = 1;
if ((squareX!=x) || (squareY!=y)) {
repaint(squareX,squareY,squareW+OFFSET,squareH+OFFSET);
squareX=x;
squareY=y;
repaint(squareX,squareY,squareW+OFFSET,squareH+OFFSET);
}
}


public Dimension getPreferredSize() {
return new Dimension(250,200);
}

protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawString("This is my custom Panel!",10,20);
g.setColor(Color.RED);
g.fillRect(squareX,squareY,squareW,squareH);
g.setColor(Color.BLACK);
g.drawRect(squareX,squareY,squareW,squareH);
}
}

The fourth slightly strange thing that the tutorial did was to put the "sprite" is its own class:

class RedSquare{

private int xPos = 50;
private int yPos = 50;
private int width = 20;
private int height = 20;

public void setX(int xPos){
this.xPos = xPos;
}

public int getX(){
return xPos;
}

... more set/get functions

public void paintSquare(Graphics g){
g.setColor(Color.RED);
g.fillRect(xPos,yPos,width,height);
g.setColor(Color.BLACK);
g.drawRect(xPos,yPos,width,height);
}
}

I'm not sure whether this was strictly necessary, but I followed the same structure, putting my histogram into the paintSquare(Graphics g) method, although I called it rainbowHist(Graphics g). I also renamed MyPanel() to MyHist(). The six set/get functions I replaced with two:

public void setMaxValue(int MaxValue){
this.MaxValue = MaxValue;
}

public void setActValue(int ActValue){
this.ActValue = ActValue;
}

The moveSquare(int x, int y) function I made as follows:

public void moveSquare(int MaxValue, int ActValue){
myHist.setMaxValue(MaxValue);
myHist.setActValue(ActValue);
repaint();
}

I didn't need the mouse listeners, so I removed them altogether.

Already my custom component was beginning to look a bit like a JProgressBar, with a MaxValue and an ActValue, and "progress" indicated by the relationship between the two. In my initial version I made the number of columns in the histogram a linear function of ActValue as a proportion of MaxValue, just like the colored bit in an ordinary JProgressBar.

But I needed to revise this, because I wanted "progress" to be indicated by the area under the curve, not the distance along the x axis.

Wikipedia gives the area under the curve, or "Cumulative distribution function" as:

In this expression "erf" is an abbreviation for "error function", and I was interested to read that much of the work around and even the name of the function derives from measurement theory. Indeed, one of the approximation expressions is:

And this could almost have been lifted straight from the Rasch book. I must say, I've never liked that expression (in fact it gives me the heebie jeebies), so I moved straight down to the Abramowitz and Stegun approximations, and used the first, because I'm aiming for a visual impression here, and don't need seven decimal places of accuracy:

Because we are talking about probability here, the theoretical total area under the graph is 1. So while in the initial version, the critical parameter was number of columns:

for (double i=0; i<ActNoofColumns; i++){

I now set the histogram to build completely by default:

for (double i=0; i<MaxNoofColumns; i++){

and inserted a break to trigger when the area approximation equates to ActValue as a proportion of MaxValue:

if (jonathan > ProportionofMax) break;

I used the variable jonathan, because Wikipedia was a bit vague about the left hand portion of the curve when the mean is zero (and x<0). I guess the measurement theorists who did this work didn't care, because they were only interested in the extreme right hand end of the curve. Wiki suggested:

erf(x)=-erf(-x)

This is correct, but I missed the leading minus sign on the right hand side of the expression, so I did a bit of fiddling around. Anyway, it eventually worked. The illustration below shows my custom component under the JprogressBar it will replace (as well as the curve shown in full for an instance of the component not yet callibrated).

No comments: