ORIGINAL DRAFT

Digital displays can enhance certain types of applications, especially those that try to use real-world equipment as a metaphor. One of our objectives this month is not only to effectively present a view that looks digital, but to take advantages of images as a model for rendering a digital view. To accomplish this, we’ll keep a clear separation between the model and rendering elements of our implementation and we’ll provide a model that extends the BufferedImage class so that any image can be rendered by the JDigital component.

Figure 1: A functional Digital Clock 
based on JDigital, uses the LEDRenderer with the ROUND option.

Figure 1: A functional Digital Clock based on JDigital, uses the LEDRenderer with the ROUND option.

While this component is relatively simple, it exemplifies good separation of responsibility and loose coupling through interfaces. We’ll use both model and renderer interfaces to encourage customizability and extensibility. Figure 2 shows the class relationship for the JDigital component.

Figure 2: Class relationships between 
JDigital and it's model and renderer interfaces, and their concrete implementations.

Figure 2: Class relationships between JDigital and it's model and renderer interfaces, and their concrete implementations.

We’ll provide multiple model implementations, including a simple DigitalImage, PatternImage and ClockImage. Not shows are a couple of test classes. JDigitalClockTest shows how a clock can be implemented with the JDigital class (see Figure 1). The JDigitalTest class simply shows the digits available in the PatternImage class.

There’s only one implementation of the renderer but the LEDRenderer supports a couple of variants. Of course, you can write your own by implementing the DigitalRenderer interface, which looks like this:

public interface DigitalRenderer
{
  public JComponent
    getDigitalRendererComponent(
      JComponent parent, int color);
}

The color argument defines the pixel color to be renderer. We also provide access to the parent component so that more sophisticated rendering is possible. This is an example of supporting the unexpected. We have no way of predicting what information might be useful to a specialized renderer that might be dreamed up in the future, so we make sure that the calling component can be passed through the interface, thereby enabling a programmer to retrieve other values without having to change the DigitalRenderer interface.

The DigitalModel interface includes a few more methods:

public interface DigitalModel
{
  public int getWidth();
  public int getHeight();
  public int getRGB(int x, int y);
  public void setRGB(int x, int y, int rgb);
  public void addChangeListener(ChangeListener listener);
  public void removeChangeListener(ChangeListener listener);
}

If you spend a lot of time working with images, you’ll notice the similarity between the first four methods of our interface and those provided by BufferedImage. I’ve kept the method signatures the same to make things more convenient. Though the names might have been different, the functionality would have been the same in any case. In addition to width, height and pixel accessors, we enforce the ability to add and remove ChangeListener instances, so that our view can always respond dynamically to any changes made to the model.

We’ll take a closer look at the PatternImage class, which implements our DigitalModel interface by extending the DigitalImage class. But first, DigitalImage deserves a little explanation, though it’s simple enough that we don’t need to take a closer look at the code.

DigitalImage extends BufferedImage and implements the DigitalModel interface. Primarily, this means supporting the BufferedImage constructor and adding ChangeListener support. We override the setRGB method, call the superclass setRGB method to keep the standard behavior, then call fireChangeListener to make sure change listeners are notified.

DigtalImage also provides a constructor that accepts any Image class. This is a variant that’s surprisingly useful. The constructor basically creates an image of the same dimensions and draws the passed-in image into the graphics context.

Let’s get back to the PatternImage class in Listing 1. The class is designed to generate images from text patterns. You might wonder why anybody would do this, given that you could use byte or RGB values just as easily. The reason is that String values are easy to edit and store as hard-coded in-line value. This is not always a good approach and image files from disk are usually a better idea. Often enough, though, you just want to support a simple icon or, like this, a few simple digit images.

PatternImage declares a number of static values, including default foreground and background colors (green and black), String patterns for digits from 0 to 9, as well as an indexable array of PatternImages for the ten digits. The rest of the code is a set of constructor variants. The first two merely enable superclass variants, while the next three are specific to handling patterns. The first lets you pass in a number (between 0 and 9) while the other two expect a width, height and pattern. The last one supports the definition of explicit foreground and background colors.

Figure 3: JDigital component displaying 
numbers from 0 to 9 using the LEDRenderer with the SQUARE option.

Figure 3: JDigital component displaying numbers from 0 to 9 using the LEDRenderer with the SQUARE option.

Figure 3 shows the patterns rendered by the LEDRenderer. You can see the source code in Listing 2. We support two types of rendering and use static values for ROUND and SQUARE. The static declaration for the DARK color is there to create the black pixel effect you see in the background. We can’t see a gradient from black to black, so we need to use a dark, yet visible color instead. By default, we provide a constructor that uses the SQUARE type. The other constructor merely stores the type that was passed-in in an instance variable.

Because we’re extending a Swing JPanel class, we can implement the DigitalRender interface by storing the Color value with the setForeground method and returning the current instance. The paintComponent method does most of the work. It uses a Graphics3D object so that we can set rendering a hint to use antialiasing. That makes the output smoother, especially when rendering a ROUND pixel.

You’ll notice that we get the foreground color and adjust it to DARK if it’s black. We then brighten the color and darken it so that we can create a suitable GradientPaint object. The gradient is effectively the same in both our cases (ROUND or SQUARE) except that the ROUND type insets the outline a little for effect. If we’re drawing a ROUND pixel, we use a drawOval call. If we’re dealing with a SQUARE pixel, we use the drawRect method. The ROUND type adds a little visual candy by drawing a pair or reflection pixels at the upper left, except when we’re dealing with a DARK pixel.

Listing 3 shows the code for JDigital, the main component class which ties all this together. Our design supports a number of useful constructors, all variants on providing a model, renderer and optional outline. The outline is an Insets object that specifies the number of blank pixels to draw around the model being rendered. This is especially useful for spacing. We avoid using the standard Insets or Border accessors for this so that normal borders can still be used.

We implement useful accessors for the model, renderer and outline values. One of the things we need to watch for, however, is the default size of the component. If we don’t handle this properly, some layout managers will not respond well. As such, I’ve implemented a method called updatePreferredSize that performs a calculation using a Dimension object called cellSize. Accessors for the cell size are also provided and updatePreferredSize is called whenever one of the key values change. In each case, the setPreferredSize method is called with appropriate values. You’ll notice that these values also account for any border insets.

The paintComponent method calls the renderer with a pixel value for each RGB value in the model. After calculating positional values that account for the border, model dimensions and any outline, we loop through the pixel matrix and call the renderer component. We then use a CellRendererPane to draw each pixel. CellRendererPane is part of the Swing infrastructure. If you’ve never seen it before, take a glance at the JavaDoc API documentation.

JDigital is a flexible component that effectively enlarges pixels to look like Light Emitting Diodes (LEDs). The code is not optimized to handle large images, however, and you should keep this in mind. It is capable of rendering small images in real-time without any flicker. Writing a flat renderer would speed this up dramatically but then, if you’re not looking for a specific effect, you may as well just scale the image directly.

For digital clocks, alphanumeric displays and other instrumentation, JDigital is a flexible solution. You can write renderers that control the look of each pixel or develop more sophisticated models based on other matrices. Amplitude indicators in the form of vertical bars that move in real time, are easy to develop, for example, perhaps including renderers that vary in color by amplitude. JDigital is ultimately flexible enough to handle all kinds of instrumentation rendering. I hope it serves you well.

Listing 1

import java.awt.*;
import java.awt.image.*;

public class PatternImage
  extends DigitalImage
{
  public static final int ON = Color.green.getRGB();
  public static final int OFF = Color.black.getRGB();

  public static final String[] PATTERN =
  {
    "01110100011000110001100011000101110", // 0
    "00100111000010000100001000010011111", // 1
    "01110100010000100010001000100011111", // 2
    "01110100010000101110000011000101110", // 3
    "00010001100101011111000100001000010", // 4
    "11111100001000011110000011000101110", // 5
    "01110100011000011110100011000101110", // 6
    "11111000010001000100001000010000100", // 7
    "01110100011000101110100011000101110", // 8
    "01110100011000101111000010000100001"  // 9
  };
  
  public static final PatternImage[] DIGIT =
  {
    new PatternImage(0),
    new PatternImage(1),
    new PatternImage(2),
    new PatternImage(3),
    new PatternImage(4),
    new PatternImage(5),
    new PatternImage(6),
    new PatternImage(7),
    new PatternImage(8),
    new PatternImage(9)
  };
  
  public PatternImage(Image image)
  {
    super(image);
  }
  
  public PatternImage(int w, int h, int type)
  {
    super(w, h, type);
  }
  
  public PatternImage(int digit)
  {
    this(5, 7, PATTERN[digit]);
  }
  
  public PatternImage(
    int w, int h, String pattern)
  {
    this(w, h, pattern, ON, OFF);
  }
  
  public PatternImage(
    int w, int h, String pattern,
    int foreground, int background)
  {
    super(w, h, TYPE_INT_RGB);
    
    for (int y = 0; y < h; y++)
    {
      for (int x = 0; x < w; x++)
      {
        char bit =  pattern.charAt(y * w + x);
        setRGB(x, y, bit == '1' ?
          foreground : background);
      }
    }
  }
}

Listing 2

import java.awt.*;
import javax.swing.*;

public class LEDRenderer extends JPanel
  implements DigitalRenderer
{
  protected static final Color DARK =
    new Color(32, 32, 32);
  
  public static final int SQUARE = 1;
  public static final int ROUND = 2;
  
  protected int type;
  
  public LEDRenderer()
  {
    this(SQUARE);
  }
  
  public LEDRenderer(int type)
  {
    this.type = type;
  }
  
  public JComponent getDigitalRendererComponent(
    JComponent parent, int color)
  {
    setForeground(new Color(color));
    return this;
  }
  
  public void paintComponent(Graphics gc)
  {
    int w = getSize().width - 1;
    int h = getSize().height - 1;
    Graphics2D g = (Graphics2D)gc;
    
    g.setRenderingHint(
      RenderingHints.KEY_ANTIALIASING,
      RenderingHints.VALUE_ANTIALIAS_ON);
    
    Color color = getForeground();
    if (color.equals(Color.black))
    {
      color = DARK;
    }
    Color shade = color.darker();
    Color light = color.brighter();
    
    if (type == ROUND)
    {
      g.setPaint(new GradientPaint(
        1, 1, light, w - 2, h - 2, shade));
      g.fillOval(0, 0, w, h);
      if (color != DARK)
      {
        g.setColor(Color.white);
        g.drawLine(w / 4 + 1, h / 4, w / 4 + 1, h / 4);
        g.drawLine(w / 4, h / 4 + 1, w / 4, h / 4 + 1);
      }
    }
    else
    {
      g.setPaint(new GradientPaint(
        0, 0, light, w, h, shade));
      g.fillRect(0, 0, w, h);
    }
  }
}

Listing 3

import java.awt.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.border.*;

public class JDigital extends JPanel
  implements ChangeListener
{
  public static Insets DEFAULT_OUTLINE =
    new Insets(1, 1, 1, 1);
  public static Dimension DEFAULT_CELLSIZE =
    new Dimension(8, 8);
  
  protected CellRendererPane cellRendererPane =
    new CellRendererPane();
  protected DigitalRenderer renderer;
  protected DigitalModel model;
  protected Dimension cellSize;
  protected Insets outline;

  public JDigital()
  {
    this(PatternImage.DIGIT[0],
      new LEDRenderer(LEDRenderer.SQUARE),
      DEFAULT_OUTLINE);
  }
  
  public JDigital(DigitalModel model)
  {
    this(model,
      new LEDRenderer(LEDRenderer.SQUARE),
      DEFAULT_OUTLINE);
  }
  
  public JDigital(DigitalModel model,
    DigitalRenderer renderer)
  {
    this(model, renderer, DEFAULT_OUTLINE);
  }
  
  public JDigital(DigitalModel model,
    DigitalRenderer renderer, Insets outline)
  {
    setModel(model);
    setRenderer(renderer);
    setOutline(outline);
    setBackground(Color.black);
    setForeground(Color.green);
    setCellSize(DEFAULT_CELLSIZE);
    setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
  }
  
  public DigitalModel getModel()
  {
    return model;
  }
  
  public void setModel(DigitalModel model)
  {
    if (model != null)
    {
      model.removeChangeListener(this);
    }
    model.addChangeListener(this);
    this.model = model;
    updatePreferredSize();
  }
  
  public DigitalRenderer getRenderer()
  {
    return renderer;
  }
  
  public void setRenderer(DigitalRenderer renderer)
  {
    this.renderer = renderer;
  }
  
  public Insets getOutline()
  {
    return outline;
  }
  
  public void setOutline(Insets outline)
  {
    this.outline = outline;
  }
  
  public void setBorder(Border border)
  {
    super.setBorder(border);
    updatePreferredSize();
  }
  
  public Dimension getCellSize()
  {
    return cellSize;
  }
  
  public void setCellSize(Dimension cellSize)
  {
    this.cellSize = cellSize;
    updatePreferredSize();
  }
  
  protected void updatePreferredSize()
  {
    Insets insets = getInsets();
    if (model != null && outline != null)
    {
      int horz = outline.left + outline.right;
      int vert = outline.top + outline.bottom;
      int w = (horz + model.getWidth()) *
         cellSize.width + 
        (insets.left + insets.right);
      int h = (vert + model.getHeight()) *
         cellSize.height +
        (insets.top + insets.bottom);
      setPreferredSize(new Dimension(w, h));
    }
  }

  public void paintComponent(Graphics gc)
  {
    int w = getSize().width;
    int h = getSize().height;
    Graphics2D g = (Graphics2D)gc;
    g.setColor(getBackground());
    g.fillRect(0, 0, w, h);
    Insets insets = getInsets();
    w -= (insets.left + insets.right);
    h -= (insets.top + insets.bottom);
    int horz = outline.left + outline.right;
    int vert = outline.top + outline.bottom;
    int width = model.getWidth() + horz;
    int height = model.getHeight() + vert;
    int ww = w / width;
    int hh = h / height;
    for (int x = 0; x < width; x++)
    {
      for (int y = 0; y < height; y++)
      {
        int xx = x * ww + insets.left;
        int yy = y * hh + insets.top;
        
        int color = 0;
        if (x >= outline.left && x < width - outline.right &&
            y >= outline.top && y < height - outline.bottom)
        {
          color = model.getRGB(
            x - outline.left, y - outline.top);
        }
        JComponent rendererComponent =
          renderer.getDigitalRendererComponent(this, color);
        cellRendererPane.paintComponent(
          g, rendererComponent, this, xx, yy, ww, hh);
      }
    }
  }

  public void stateChanged(ChangeEvent event)
  {
    repaint();
  }
}