HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML
 

This month we have a colorful widget for you. While the JFC provides a pretty nice color picker, it doesn't seem to go the extra mile that users of imaging software have come to expect. Once exposed to that software, some users have become pretty sophisticated and you have to use a well-designed color selection control - should you need one in your application - to make a good impression on them.

This column explores a widget called JColor that provides six views on the color spectrum in a single, easy-to-use interface. Figure 1 shows the JColor control in action, with the Blue view activated. The six views correspond directly with the red, green and blue of the RGB model and the hue, saturation and brightness of the HSB model.

Figure 1
Figure 1:

The RGB and HSB color models are based on three dimensions, something often difficult to visualize on a flat display. Our approach uses the two dimensions of a rectangle and an additional dimension in a vertical gradient.

The view selected by the radio controls of our interface determine which dimension is displayed in the vertical gradient. The remaining two dimensions are represented by the two axes of the rectangle. You can pick colors graphically in any model, or type the RGB or HSB values directly in the fields.

Image Producers
Both the ColorSliderImage and ColorMatrixImage classes are implementations of the ImageProducer interface. An image producer implements a minimum interface that allows it to dynamically produce arbitrary images that can be read by an ImageConsumer and displayed by an ImageObserver.

The JFC offers a simple abstraction to the ImageProducer class called SyntheticImage, which provides a constructor that needs to know the width and height of the image and a computeRow method to actually produce the pixels one row at a time. This is very convenient, since little coding is actually required to produce an image.

We implement two image producers. Both of them calculate a color gradient vertically and one of them calculates an additional horizontal gradient. In both cases, we use a set of constants that tell us which of the six view modes we're in. The constants, presented in Listing 1, are RED, GREEN, BLUE, HUE, SATURATION and BRIGHTNESS. The images are produced dynamically through the interface.

Listing 2 shows the source code for ColorSliderImage, which produces a vertical gradient based on the current style. For the RED, GREEN and BLUE styles we create a gradient between zero and the specified color, representing a value between zero and 255. The HUE, SATURATION and BRIGHTNESS values are determined by using the static HSB to RGB method in the Java Color class.

Listing 3 shows the code for ColorMatrixImage, which produces a vertical and horizontal gradient covering the full two-dimensional range specified by the current style. Notice that the ColorSliderImage actually represents the selected style, so ColorMatrixImage produces the remaining two dimensions. If, for example, the RED style is selected, the ColorMatrixImage will actually represent the green and blue color components.

Color Selectors
Two widgets need to be implemented to support interactive color selection. The JColorSlider and JColorMatrix controls use the image producers we developed to display the range of values available for each selection. Two visual cues are employed to show the user the current selection - arrows on the outside of the color area and a crosshair surrounding the current position.

The arrows are drawn as triangular polygons on the border of the control. The JColorSlider control provides arrows on the left and right of the current position. The JColorMatrix control draws left and right arrows, along with top and bottom arrows. To make all this easy to follow, we provide separate methods for each of the arrow drawing routines.

The crosshair is designed to handle the unpredictable underlying color spectrum. If the underlying color is dark, a black crosshair would be ineffective. If we make the crosshair white, the same problem occurs when the underlying color is too bright. The solution is to use a combination of black and white with a black central crosshair and a white edge on each side of the black lines.

Listing 4 shows the AbstractColorSelector class from which both JColorSlider and JColorMatrix inherit. It encapsulates basic code for handling common member variables along with action listener registration and event handling. Empty MouseListener and KeyListener methods are also provided; since we're primarily interested in the mousePressed and keyPressed events, we can ignore the others.

Listing 5 shows the code for JColorSlider. We set the style and register to receive mouse, key and focus events. Besides setting the style member variable, the setStyle method sets the image to null before repainting, forcing a new image to be generated by the ColorSliderImage producer. The paintComponent method draws the image, arrows and crosshair, and the focus rectangle when appropriate.

The setYValue and getYValue methods handle setting and getting the vertical position. Internally, we calculate the actual pixel location. From the outside these values are expected to be between zero and 255. We also set the preferred size so that window packing will create ideal dimensions for the control, with a pixel for each discrete position. The object can be scaled as needed, however, so this is considered a guideline.

Listing 6 shows the code for JColorMatrix, which extends the JColorSlider control. All the vertical handling is identical, so we extend the JColorSlider behavior to handle horizontal positions. We add the setXValue and getXValue methods along with top and bottom arrows, and modify the code for paintMethod and drawCrosshair to accommodate the additional dimension. The getPreferredSize method returns symmetrical, ideal dimensions for a square area with a single pixel for each discrete unit. Note: If you make it smaller, you'll lose some resolution, and making it larger repeats pixels, so the preferred size is highly recommended.

The JColor Control
The main JColor control is implemented as an extension to JPanel for flexibility. Using this strategy allows us to place it in any component or window. This is the most complex class in this collection, primarily because the JColor panel handles all the button, field, and selector events, and coordinates the six views provided to display more than 16 million (24 bit) colors in the spectrum.

Figure 2 shows how the internal panels and components are arranged. The JColor constructor creates each of the instances and stores a reference to the buttons, fields and color selectors in member variables.

Figure 2
Figure 2:

Let's quickly review behavior in the JColor widget. If the user selects one of the radio buttons, the matrix and slider view images are updated to reflect the color selection model. When the user types in a value directly in one of the fields, we clip values within the zero to 255 range. If the field being edited is in the RGB range, we automatically switch to one of those views if one isn't already active. To be consistent, we pick the view associated with the current field. The same is true of entering values in the HSB fields if one of the views isn't active already. A value changed in the matrix or slider component is immediately reflected in the field values and the swatch panel, both of which keep in mind which model is active at the time.

Most of the work is done when action events are triggered. Listing 7 shows the code for the actionPerformed method, with most of the field handlers omitted. The red field handler is sufficiently indicative of how the others work to get the idea.

The radio buttons are handled first. In each case the style value changes and a condition that checks for RadioButton events follows immediately to switch the the slider and matrix styles dynamically. No matter what the source of the event, we call setColorText to update the field values and repaint the swatch to reflect the current color.

Listing 7 shows the condition that handles the red field events. We first check to see if one of the RGB radio selections is active. If none of them are, we switch views by setting the style value and updating the matrix and slider styles. In either case, we check to see which context is active and, then set the appropriate x or y value in the slider or matrix controls. The same mechanism is applied to each of the fields in a set of subsequent conditions.

Listing 8 shows code for JColorTest that demonstrates usage. We create a JFrame and watch for the windowClosing event. The code puts a new JColor panel into the center and sets an initial color with the JColor.setColor method. We fetch the color with getColor on exit. Figure 3 shows the JColorTest running. Notice that the JColorMatrix control has the focus and appears slightly lifted from the page.

Figure 3
Figure 3:

Summary
The JColor widget provides developers with a color selection control that's both comprehensive and flexible. Designed to handle a wide variety of needs, it presents itself as an alternative to the JColorPicker control provided with the JFC. More important, it provides a sophisticated design that satisfies advanced imaging software needs as easily as the needs of a novice user.

About the Author
Claude Duguay has been programming since 1980. In 1988 he founded LogiCraft Corporation, and currently leads the development team at Atrieva Corp. You can contact him with questions and comments at [email protected]

	

Listing 1.
 
public interface ColorConstants 
{ 
  public static final int RED = 1; 
  public static final int GREEN = 2; 
  public static final int BLUE = 3; 
  public static final int HUE = 4; 
  public static final int SATURATION = 5; 
  public static final int BRIGHTNESS = 6; 
} 

Listing 2.
 
public class ColorSliderImage extends SyntheticImage 
  implements ColorConstants 
{ 
  protected int style; 

  public ColorSliderImage(int width, int height, int style) 
  { 
    super(width, height); 
    this.style = style; 
  } 

  protected void computeRow(int y, int[] row) 
  { 
    for(int x = 0; x < width; x++) 
    { 
      if (style == RED) 
      { 
        int r = 255 - 
          (int)(((float)y / (float)height) * 255); 
        row[x] = makeColor(r, 0, 0); 
      } 
      if (style == GREEN) 
      { 
        int g = 255 - 
          (int)(((float)y / (float)height) * 255); 
        row[x] = makeColor(0, g, 0); 
      } 
      if (style == BLUE) 
      { 
        int b = 255 - 
          (int)(((float)y / (float)height) * 255); 
        row[x] = makeColor(0, 0, b); 
      } 

      if (style == HUE) 
      { 
        float h = 1f - (float)y / (float)height; 
        row[x] = Color.HSBtoRGB(h, 1f, 1f); 
      } 
      if (style == SATURATION) 
      { 
        float s = 1f - (float)y / (float)height; 
        row[x] = Color.HSBtoRGB(1f, s, 1f); 
      } 
      if (style == BRIGHTNESS) 
      { 
        float b = 1f - (float)y / (float)height; 
        row[x] = Color.HSBtoRGB(1f, 1f, b); 
      } 
    } 
  } 

  private int makeColor(int r, int g, int b) 
  { 
    return (0xff << 24) | (r << 16) | (g << 8) | b; 
  } 
} 
  

Listing 3.
 
public class ColorMatrixImage extends SyntheticImage 
  implements ColorConstants 
{ 
  protected int style; 

  public ColorMatrixImage(int width, int height, int style) 
  { 
    super(width, height); 
    this.style = style; 
  } 

  protected void computeRow(int y, int[] row) 
  { 
    for(int x = 0; x < width; x++) 
    { 
      if (style == RED) 
      { 
        int g = (int)(((float)x / (float)width) * 255); 
        int b = 255 - (int)(((float)y / (float)height) * 255); 
        row[x] = makeColor(0, g, b); 
      } 
      if (style == GREEN) 
      { 
        int r = (int)(((float)x / (float)width) * 255); 
        int b = 255 - (int)(((float)y / (float)height) * 255); 
        row[x] = makeColor(r, 0, b); 
      } 
      if (style == BLUE) 
      { 
        int r = (int)(((float)x / (float)width) * 255); 
        int g = 255 - (int)(((float)y / (float)height) * 255); 
        row[x] = makeColor(r, g, 0); 
      } 

      if (style == HUE) 
      { 
        float s = (float)x / (float)width; 
        float b = 1f - (float)y / (float)height; 
        row[x] = Color.HSBtoRGB(1f, s, b); 
      } 
      if (style == SATURATION) 
      { 
        float h = (float)x / (float)width; 
        float b = 1f - (float)y / (float)height; 
        row[x] = Color.HSBtoRGB(h, 1f, b); 
      } 
      if (style == BRIGHTNESS) 
      { 
        float h = (float)x / (float)width; 
        float s = 1f - (float)y / (float)height; 
        row[x] = Color.HSBtoRGB(h, s, 1f); 
      } 
    } 
  } 

  private int makeColor(int r, int g, int b) 
  { 
    return (0xff << 24) | (r << 16) | (g << 8) | b; 
  } 
} 
  

Listing 4.
 
public abstract class AbstractColorSelector extends JComponent 
  implements MouseListener, KeyListener, 
    FocusListener, ColorConstants 
{ 
  protected Vector listeners = new Vector(); 
  protected ImageIcon image = null; 
  protected Dimension size; 
  protected int style; 

  public void addActionListener(ActionListener listener) 
  { 
    listeners.addElement(listener); 
  } 

  public void removeActionListener(ActionListener listener) 
  { 
    listeners.removeElement(listener); 
  } 

  public void fireActionEvent() 
  { 
    Vector list = (Vector)listeners.clone(); 
    ActionEvent event = new ActionEvent(this, 
      ActionEvent.ACTION_PERFORMED, "Action"); 
    ActionListener listener; 
    for (int i = 0; i < list.size(); i++) 
    { 
      listener = (ActionListener)list.elementAt(i); 
      listener.actionPerformed(event); 
    } 
  } 

  public boolean isFocusTraversable() 
  { 
    return true; 
  } 

  public void mouseClicked(MouseEvent event) {} 
  public void mouseReleased(MouseEvent event) {} 
  public void mouseEntered(MouseEvent event) {} 
  public void mouseExited(MouseEvent event) {} 

  public void keyTyped(KeyEvent event) {} 
  public void keyReleased(KeyEvent event) {} 

  public void focusGained(FocusEvent event) {} 
  public void focusLost(FocusEvent event) {} 
} 
  

Listing 5.
 
public class JColorSlider extends AbstractColorSelector 
{ 
  protected int y = 127 + 5; 
  protected boolean hasFocus = false; 

  public JColorSlider(int style) 
  { 
    setStyle(style); 
    addKeyListener(this); 
    addMouseListener(this); 
    addFocusListener(this); 
  } 

  public void setStyle(int style) 
  { 
    this.style = style; 
    image = null; 
    repaint(); 
  } 

  public Dimension getPreferredSize() 
  { 
    return new Dimension(30, 266); 
  } 

  public int getYValue() 
  { 
    return 255 - (int)(((float)(y - 5) / 
      (float)(getSize().height - 10)) * 256); 
  } 

  public void setYValue(int value) 
  { 
    y = 5 + (int)((getSize().height - 10) * 
      ((float)(255 - value) / 256f)); 
    repaint(); 
  } 

  public void paintComponent(Graphics g) 
  { 
    if (image == null || !size.equals(getSize())) 
    { 
      size = getSize(); 
      int w = size.width - 10; 
      int h = size.height - 10; 
      image = new ImageIcon(createImage( 
        new ColorSliderImage(w, h, style))); 
    } 
    g.drawImage(image.getImage(), 5, 5, this); 
    drawFocus(g); 
    g.setColor(getForeground()); 
    drawLeftArrow(g, y); 
    drawRightArrow(g, y); 
    drawCrosshair(g, getSize().width / 2, y); 
  } 

  protected void drawFocus(Graphics g) 
  { 
    if (hasFocus) 
    { 
      int w = getSize().width - 5; 
      int h = getSize().height - 5; 

      g.setColor(getBackground().darker()); 
      g.drawLine(w, 4, w, h); 
      g.drawLine(4, h, w, h); 
      g.setColor(getBackground().brighter()); 
      g.drawLine(4, 4, w, 4); 
      g.drawLine(4, 4, 4, h); 
    } 
  } 

  protected void drawCrosshair(Graphics g, int x, int y) 
  { 
    g.setColor(Color.black); 
    g.drawLine(x - 3, y, x - 7, y); 
    g.drawLine(x + 3, y, x + 7, y); 

    g.setColor(Color.white); 
    g.drawLine(x - 3, y - 1, x - 7, y - 1); 
    g.drawLine(x - 3, y + 1, x - 7, y + 1); 
    g.drawLine(x + 3, y - 1, x + 7, y - 1); 
    g.drawLine(x + 3, y + 1, x + 7, y + 1); 
  } 

  protected void drawLeftArrow(Graphics g, int y) 
  { 
    int w = 5; 
    int h = 5; 
    int l = 5; 
    Polygon arrow = new Polygon(); 
    arrow.addPoint(l, y); 
    arrow.addPoint(l - w, y - h); 
    arrow.addPoint(l - w, y + h); 
    arrow.addPoint(l, y); 
    g.fillPolygon(arrow); 
  } 

  protected void drawRightArrow(Graphics g, int y) 
  { 
    int w = 5; 
    int h = 5; 
    int r = getSize().width - 5; 
    Polygon arrow = new Polygon(); 
    arrow.addPoint(r, y); 
    arrow.addPoint(r + w, y - h); 
    arrow.addPoint(r + w, y + h); 
    arrow.addPoint(r, y); 
    g.fillPolygon(arrow); 
  } 

  public void mousePressed(MouseEvent event) 
  { 
    requestFocus(); 
    int yy = event.getY(); 
    if (yy < 5 || yy >= getSize().height - 5) return; 
      y = yy; 
    repaint(); 
    fireActionEvent(); 
  } 

  public void keyPressed(KeyEvent event) 
  { 
    int code = event.getKeyCode(); 
    if (code == KeyEvent.VK_UP & y > 5) y--; 
    if (code == KeyEvent.VK_DOWN & 
      y < getSize().height - 6) y++; 
    repaint(); 
    fireActionEvent(); 
  } 

  public void focusGained(FocusEvent event) 
  { 
    hasFocus = true; 
    repaint(); 
  } 

  public void focusLost(FocusEvent event) 
  { 
    hasFocus = false; 
    repaint(); 
  } 
} 

Listing 6.
 
public class JColorMatrix extends JColorSlider 
{ 
  protected int x = 128 + 5; 

  public JColorMatrix(int style) 
  { 
    super(style); 
  } 

  public Dimension getPreferredSize() 
  { 
    return new Dimension(266, 266); 
  } 

  public int getXValue() 
  { 
    int value = (int)(((float)(x - 5) / 
      (float)(getSize().width - 10)) * 256); 
    return value; 
  } 

  public void setXValue(int value) 
  { 
    x = 5 + (int)((getSize().width - 10) * 
      ((float)value / 256f)); 
    repaint(); 
  } 

  public void paintComponent(Graphics g) 
  { 
    if (image == null || !size.equals(getSize())) 
    { 
      size = getSize(); 
      int w = size.width - 10; 
      int h = size.height - 10; 
      image = new ImageIcon(createImage( 
        new ColorMatrixImage(w, h, style))); 
    } 
    g.drawImage(image.getImage(), 5, 5, this); 
    drawFocus(g); 
    g.setColor(getForeground()); 
    drawLeftArrow(g, y); 
    drawRightArrow(g, y); 
    drawTopArrow(g, x); 
    drawBottomArrow(g, x); 
    drawCrosshair(g, x, y); 
  } 

  protected void drawCrosshair(Graphics g, int x, int y) 
  { 
    g.setColor(Color.black); 
    g.drawLine(x, y - 3, x, y - 6); 
    g.drawLine(x, y + 3, x, y + 6); 
    g.drawLine(x - 3, y, x - 6, y); 
    g.drawLine(x + 3, y, x + 6, y); 

    g.setColor(Color.white); 
    g.drawLine(x - 1, y - 3, x - 1, y - 6); 
    g.drawLine(x + 1, y - 3, x + 1, y - 6); 
    g.drawLine(x - 1, y + 3, x - 1, y + 6); 
    g.drawLine(x + 1, y + 3, x + 1, y + 6); 
    g.drawLine(x - 3, y - 1, x - 6, y - 1); 
    g.drawLine(x - 3, y + 1, x - 6, y + 1); 
    g.drawLine(x + 3, y - 1, x + 6, y - 1); 
    g.drawLine(x + 3, y + 1, x + 6, y + 1); 
  } 

  protected void drawTopArrow(Graphics g, int x) 
  { 
    int w = 5; 
    int h = 5; 
    int t = 5; 
    Polygon arrow = new Polygon(); 
    arrow.addPoint(x, t); 
    arrow.addPoint(x - w, t - h); 
    arrow.addPoint(x + w, t - h); 
    arrow.addPoint(x, t); 
    g.fillPolygon(arrow); 
  } 

  public void drawBottomArrow(Graphics g, int y) 
  { 
    int w = 5; 
    int h = 5; 
    int b = getSize().height - 5; 
    Polygon arrow = new Polygon(); 
    arrow.addPoint(x, b); 
    arrow.addPoint(x - w, b + h); 
    arrow.addPoint(x + w, b + h); 
    arrow.addPoint(x, b); 
    g.fillPolygon(arrow); 
  } 

  public void mousePressed(MouseEvent event) 
  { 
    requestFocus(); 
    int xx = event.getX(); 
    int yy = event.getY(); 
    if (yy < 5 || yy >= getSize().height - 5) return; 
    if (xx < 5 || xx >= getSize().width - 5) return; 
    x = xx; 
    y = yy; 
    repaint(); 
    fireActionEvent(); 
  } 

  public void keyPressed(KeyEvent event) 
  { 
    int code = event.getKeyCode(); 
    if (code == KeyEvent.VK_UP & y > 5) y--; 
    if (code == KeyEvent.VK_LEFT & x > 5) x--; 
    if (code == KeyEvent.VK_DOWN & 
      y < getSize().height - 6) y++; 
    if (code == KeyEvent.VK_RIGHT & 
      x < getSize().width - 6) x++; 
    repaint(); 
    fireActionEvent(); 
  } 
} 
  

Listing 7.
 
public void actionPerformed(ActionEvent event) 
{ 
  Object source = event.getSource(); 

  if (source == rRadioRGB) style =  RED; 
  if (source == gRadioRGB) style =  GREEN; 
  if (source == bRadioRGB) style =  BLUE; 
  if (source == hRadioHSB) style =  HUE; 
  if (source == sRadioHSB) style =  SATURATION; 
  if (source == bRadioHSB) style =  BRIGHTNESS; 
  if (source instanceof JRadioButton) 
  { 
    slider.setStyle(style); 
    matrix.setStyle(style); 
  } 

  if (source == rFieldRGB) 
  { 
    if (!(rRadioRGB.isSelected() || 
          gRadioRGB.isSelected() || 
          bRadioRGB.isSelected())) 
    { 
      rRadioRGB.setSelected(true); 
      style = RED; 
      slider.setStyle(style); 
      matrix.setStyle(style); 
    } 
    if (style == RED) 
      slider.setYValue(fieldValue(rFieldRGB)); 
    if (style == GREEN) 
      matrix.setXValue(fieldValue(rFieldRGB)); 
    if (style == BLUE) 
      matrix.setXValue(fieldValue(rFieldRGB)); 
  } 

  // Radio button handlers omited 

  setColorText(); 
  swatch.repaint(); 
} 
  

Listing 8.
 
public class JColorTest extends WindowAdapter 
{ 
  static JColor jcolor; 

  public void windowClosing(WindowEvent event) 
  { 
    System.out.println(jcolor.getColor().toString()); 
    System.exit(0); 
  } 

  public static void main(String[] args) 
  { 
    PLAF.setNativeLookAndFeel(true); 
    jcolor = new JColor(); 
    JFrame frame = new JFrame("JColor Test"); 
    frame.getContentPane().add("Center", jcolor); 
    frame.addWindowListener(new JColorTest()); 
    frame.pack(); 
    frame.show(); 
    jcolor.setColor(Color.green); 
  } 
} 
  

Download Assoicated Source Files (Zip format - 20.7 KB)
 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.