Creating a 16-segment character display component in Swing
Some time ago, Javalobby posted a nice article on how someone created a event talk board that resembled a departure board at the airport or trainstation.
I needed to brush up my Java2D skills, so I started on something easy that was inspired by that article: a 16-segment character display (as described here). It should only use Java2D (no images) and capable of showing A-Z and 0-9.
After an evening of drawing on paper grid, this is the end result:

The code is divided in 3 parts: the single character component, a character group component capable of displaying a String and the alfabet definition.
First I'll show the single character component:
public class SixteenSegmentDisplay extends JComponent {
private java.util.List<Integer> lightedSegments;
public static final int WIDTH = 29;
public static final int HEIGHT = 46;
private final double INACTIVE_COLOR_DARKEN_FRACTION = 0.4;
private static final Image BACKGROUND_IMAGE = createBackgroundImage();
// Highlight Colors
private static final java.awt.Color HIGHLIGHT_COLOR_TOP = new java.awt.Color(0x000000);
private static final java.awt.Color HIGHLIGHT_COLOR_BOTTOM = new java.awt.Color(0x625C52);
private Color activeSegmentColor = new Color(0xFF0000);
private Color inactiveSegmentColor = darken(new Color(0xFF0000), INACTIVE_COLOR_DARKEN_FRACTION);
private Color backgroundColor = Color.black;
public SixteenSegmentDisplay() {
lightedSegments = new ArrayList<Integer>();
}
@Override
public java.awt.Dimension getSize() {
return new Dimension(WIDTH, HEIGHT);
}
@Override
public java.awt.Dimension getPreferredSize() {
return new Dimension(WIDTH, HEIGHT);
}
@Override
public java.awt.Dimension getSize(java.awt.Dimension rv) {
return new Dimension(WIDTH, HEIGHT);
}
public void setSegmentColor(Color segmentColor) {
activeSegmentColor = segmentColor;
inactiveSegmentColor = darken(segmentColor, INACTIVE_COLOR_DARKEN_FRACTION);
}
public void setBackgroundColor(Color backgroundColor) {
this.backgroundColor = backgroundColor;
}
public void setCharacter(char character) {
setLightedSegments(SixteenSegmentAlfabet.MAP.get(character));
}
private void setLightedSegments(SixteenSegmentAlfabet character) {
if (character != null && character.getSegments() != null)
this.lightedSegments = Arrays.asList(character.getSegments());
else
this.lightedSegments = new ArrayList<Integer>(0);
this.repaint();
}
private Color darken(Color c, double fragment) {
Double newRed = fragment * c.getRed();
Double newBlue = fragment * c.getBlue();
Double newGreen = fragment * c.getGreen();
return new Color(newRed.intValue(), newGreen.intValue(), newBlue.intValue());
}
@Override
public void paint(Graphics g) {
Graphics2D graphics = (Graphics2D) g.create();
graphics.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.VALUE_ANTIALIAS_ON);
graphics.setRenderingHint(java.awt.RenderingHints.KEY_ALPHA_INTERPOLATION, java.awt.RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
graphics.setRenderingHint(java.awt.RenderingHints.KEY_COLOR_RENDERING, java.awt.RenderingHints.VALUE_COLOR_RENDER_QUALITY);
paintBackground(graphics);
// left top to top middle
paintSegment(graphics, lightedSegments.contains(1), getPoints("2,2;3,1;14,1;15,2;14,3;3,3;2,2"));
// top middle to right top
paintSegment(graphics, lightedSegments.contains(2), getPoints("15,2;16,1;27,1;28,2;27,3;16,3;15,2"));
// left top to left middle
paintSegment(graphics, lightedSegments.contains(3), getPoints("2,2;3,3;3,22;2,23;1,22;1,3;2,2"));
// top left to middle middle
paintSegment(graphics, lightedSegments.contains(4), getPoints("4,4;5,4;13,19;13,21;12,21;4,6;4,4"));
// top middle to middle middle
paintSegment(graphics, lightedSegments.contains(5), getPoints("15,2;14,3;14,22;15,23;16,22;16,3;15,2"));
// top right to middle middle
paintSegment(graphics, lightedSegments.contains(6), getPoints("25,4;26,4;26,6;18,21;17,21;17,19;25,4"));
// top middle to right middle
paintSegment(graphics, lightedSegments.contains(7), getPoints("28,2;29,3;29,22;28,23;27,22;27,3;28,2"));
// left middle to middle middle
paintSegment(graphics, lightedSegments.contains(8), getPoints("2,23;3,22;14,22;15,23;14,24;3,24;2,23"));
// middle middle to right middle
paintSegment(graphics, lightedSegments.contains(9), getPoints("15,23;16,22;27,22;28,23;27,24;16,24;15,23"));
// left middle to bottom left
paintSegment(graphics, lightedSegments.contains(10), getPoints("2,23;3,24;3,43;2,44;1,43;1,24;2,23"));
// left bottom to middle middle
paintSegment(graphics, lightedSegments.contains(11), getPoints("13,27;13,25;12,25;4,40;4,42;5,42;13,27"));
// bottom middle to middle middle
paintSegment(graphics, lightedSegments.contains(12), getPoints("15,23;16,24;16,43;15,44;14,43;14,24;15,23"));
// bottom right to middle middle
paintSegment(graphics, lightedSegments.contains(13), getPoints("17,25;18,25;26,40;26,42;25,42;17,27;17,25"));
// right middle to bottom right
paintSegment(graphics, lightedSegments.contains(14), getPoints("28,23;29,24;29,43;28,44;27,43;27,24;28,23"));
// bottpm left to bottom middle
paintSegment(graphics, lightedSegments.contains(15), getPoints("2,44;3,43;14,43;15,4;14,45;3,45;2,44"));
// bottom middle to bottom right
paintSegment(graphics, lightedSegments.contains(16), getPoints("15,244;16,43;27,43;28,44;27,45;16,45;15,44"));
}
private void paintBackground(Graphics2D g) {
g.drawImage(BACKGROUND_IMAGE, 0, 0, this);
}
private static final BufferedImage createBackgroundImage() {
GraphicsConfiguration gfxConf = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
final BufferedImage IMAGE = gfxConf.createCompatibleImage(WIDTH, HEIGHT, Transparency.OPAQUE);
Graphics2D g2 = IMAGE.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
g2.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
final java.awt.geom.Point2D START_INNER_BACKGROUND = new java.awt.geom.Point2D.Float(0, 0);
final java.awt.geom.Point2D STOP_INNER_BACKGROUND = new java.awt.geom.Point2D.Float(0, HEIGHT);
final float[] FRACTIONS_INNER_BACKGROUND =
{
0.0f,
1.0f
};
final Color[] COLORS_INNER_BACKGROUND =
{
new Color(0x3D3A31),
new Color(0x232520)
};
final LinearGradientPaint GRADIENT_INNER_BACKGROUND = new LinearGradientPaint(START_INNER_BACKGROUND, STOP_INNER_BACKGROUND, FRACTIONS_INNER_BACKGROUND, COLORS_INNER_BACKGROUND);
g2.setPaint(GRADIENT_INNER_BACKGROUND);
g2.fill(new java.awt.geom.Rectangle2D.Float(0, 0, WIDTH, HEIGHT));
// Highlight
g2.setColor(HIGHLIGHT_COLOR_TOP);
g2.drawLine(0, 0, WIDTH, 0);
g2.setColor(HIGHLIGHT_COLOR_BOTTOM);
g2.drawLine(0, HEIGHT, WIDTH, HEIGHT);
g2.dispose();
return IMAGE;
}
private void paintSegment(Graphics2D g, boolean active, Point... points) {
if (points != null) {
g.setColor(active ? activeSegmentColor : inactiveSegmentColor);
Polygon polygon = new Polygon();
for (Point point : points) {
polygon.addPoint(point.x, point.y);
}
g.fillPolygon(polygon);
}
}
private static Point[] getPoints(String coded) {
if(coded.length() == 0)
{
return null;
}
java.util.List<Point> pointList = new ArrayList<Point>();
String[] points = coded.split(";");
for (String point : points) {
String[] pointCoordinates = point.split(",");
pointList.add(new Point(Integer.parseInt(pointCoordinates[0]), Integer.parseInt(pointCoordinates[1])));
}
return pointList.toArray(new Point[pointList.size()]);
}
}
To use this, you need an alfabet definition that defines which segments should light up for a given character:
public enum SixteenSegmentAlfabet {
A(new char[]{'a', 'A'}, 1, 2, 3, 7, 8, 9, 10, 14),
B(new char[]{'b', 'B'}, 1, 2, 5, 7, 9, 12, 14, 15, 16),
C(new char[]{'c', 'C'}, 1, 2, 3, 10, 15, 16),
D(new char[]{'d', 'D'}, 1, 2, 5, 7, 12, 14, 15, 16),
E(new char[]{'e', 'E'}, 1, 2, 3, 8, 9, 10, 15, 16),
F(new char[]{'f', 'F'}, 1, 2, 3, 8, 10),
G(new char[]{'g', 'G'}, 1, 2, 3, 9, 10, 14, 15, 16),
H(new char[]{'h', 'H'}, 3, 7, 8, 9, 10, 14),
I(new char[]{'i', 'I'}, 1, 2, 5, 12, 15, 16),
J(new char[]{'j', 'J'}, 7, 10, 14, 15, 16),
K(new char[]{'k', 'K'}, 3, 6, 8, 10, 13),
L(new char[]{'l', 'L'}, 3, 10, 15, 16),
M(new char[]{'m', 'M'}, 3, 4, 6, 7, 10, 14),
N(new char[]{'n', 'N'}, 3, 4, 7, 10, 13, 14),
O(new char[]{'o', 'O'}, 1, 2, 3, 7, 10, 14, 15, 16),
P(new char[]{'p', 'P'}, 1, 2, 3, 6, 8, 10),
Q(new char[]{'q', 'Q'}, 1, 2, 3, 7, 10, 13, 14, 15, 16),
R(new char[]{'r', 'R'}, 1, 2, 3, 7, 8, 9, 10, 13),
S(new char[]{'s', 'S'}, 1, 2, 3, 8, 9, 14, 15, 16),
T(new char[]{'t', 'T'}, 1, 2, 5, 12),
U(new char[]{'u', 'U'}, 3, 7, 10, 14, 15, 16),
V(new char[]{'v', 'V'}, 3, 6, 10, 11),
W(new char[]{'w', 'W'}, 3, 7, 10, 11, 13, 14),
X(new char[]{'x', 'X'}, 4, 6, 11, 13),
Y(new char[]{'y', 'Y'}, 4, 6, 12),
Z(new char[]{'z', 'Z'}, 1, 2, 6, 11, 15, 16),
BLANK(new char[]{' '}, null),
ZERO(new char[]{'0'}, 1, 2, 3, 4, 7, 10, 13, 14, 15, 16),
ONE(new char[]{'1'}, 7, 14),
TWO(new char[]{'2'}, 1, 2, 7, 8, 9, 10, 15, 16),
THREE(new char[]{'3'}, 1, 2, 7, 9, 14, 15, 16),
FOUR(new char[]{'4'}, 3, 7, 8, 9, 14),
FIVE(new char[]{'5'}, 1, 2, 3, 8, 13, 15, 16),
SIX(new char[]{'6'}, 1, 2, 3, 8, 9, 10, 14, 15, 16),
SEVEN(new char[]{'7'}, 1, 2, 7, 14),
EIGHT(new char[]{'8'}, 1, 2, 3, 7, 8, 9, 10, 14, 15, 16),
NINE(new char[]{'9'}, 1, 2, 3, 7, 8, 9, 14, 15, 16);
private final Integer[] segments;
private final char[] characters;
public static final Map<Character, SixteenSegmentAlfabet> MAP;
static {
MAP = new HashMap<Character, SixteenSegmentAlfabet>(36);
for (SixteenSegmentAlfabet alfabet : SixteenSegmentAlfabet.values()) {
for (char character : alfabet.characters) {
MAP.put(character, alfabet);
}
}
}
SixteenSegmentAlfabet(char[] characters, Integer... segments) {
this.characters = characters;
this.segments = segments;
}
public Integer[] getSegments() {
return segments;
}
}
To finish, the component capable of showing a String merely groups together a couple of the character components:
public class MultiCharacterDisplay extends JComponent {
private static final int GAP = 5;
private int segmentDisplayCount;
private List<SixteenSegmentDisplay> segmentDisplays;
public MultiCharacterDisplay(int segmentDisplayCount) {
this(segmentDisplayCount, Color.red);
}
public MultiCharacterDisplay(int segmentDisplayCount, Color segmentColor) {
this.segmentDisplayCount = segmentDisplayCount;
segmentDisplays = new ArrayList<SixteenSegmentDisplay>(segmentDisplayCount);
setLayout(new FlowLayout(FlowLayout.LEFT, GAP, 0));
for (int i = 0; i < segmentDisplayCount; i++) {
SixteenSegmentDisplay display = new SixteenSegmentDisplay();
display.setSegmentColor(segmentColor);
segmentDisplays.add(display);
add(display);
}
}
public void setText(String text) {
if (text.length() > segmentDisplayCount) {
text = text.substring(0, segmentDisplayCount);
}
int i = 0;
for (char c : text.toCharArray()) {
segmentDisplays.get(i++).setCharacter(c);
}
}
}
All in all, it was a nice exercise
. If I find some more time, I'll try and build some sort of marquee with it.
Creating a custom binder for Spring RCP
Introduction
For those not familiar with RCP, a quick heads-up. Spring RCP is a Swing application framework, based on the Spring Framework. It utilizes many of Spring's available utilities, the Spring IOC container being the most important one.
Bindings and binders
Spring RCP gives you the possibility to create controls that are bound to certain properties of a given object. These bound controls are called bindings. A binder is a class that creates bindings. A binding factory, if you will.
Standard Spring RCP has some basic binders, but often you will encounter user requirements that require you to build a custom binder. For example, a binder to show a list of objects in a JTable, a binder to show a String in a formatted text field or even a binding to show and select an image. These are not standard in RCP, and you'll need to make those yourself.
The danger of nested exceptions in Java Webstart applications
Everyone knows the following snippet:
try
{
// snip
}
catch(SomeCheckedException ex)
{
throw new RuntimeException(&quot;Hey, some exception occurred&quot;, ex)
}
It's your standard 'I hate checked exceptions, let's transform them into unchecked ones'. Sometimes, you won't even notice it happening. Using Spring and Hibernate, checked exceptions are a rare thing to encounter, since the framework nicely encapsulates them into unchecked ones. But there's a hidden danger lurking within these exceptions. See that second parameter in the exception? It's that one that can cause some problems when dealing with WebStart applications connecting to a remoted back-end somewhere.
Designing Swing applications: Choosing the right look
Developing Swing applications is fun. Developing enterprise Swing application is even more fun. But apart from the functional requirement such an application brings, you also need to take into account the aesthetic part of your application. You're application won't be a real success if your users can bear the sight of it, or the general look of the application is so different from the other applications they use on a daily basis that they use it inefficiently.
Doing GUI testing on a headless Ubuntu server
Recently at Jintec, the company I work for, we leased a server for our Java development.
As good developers, we have a Hudson running on that server, guarding the stability of our code (and our sanity). As I'm currently doing a rich client GUI project, doing GUI tests on my workstation isn't a problem, doing them on the server is, as this server doesn't have a graphical interface.