Musings about Java and Swing

Qlocktwo with Java Swing

I came across an interesting clock, the Qlocktwo, designed by Biegert & Funk. The clock is an electronic piece of art. Different letters light up at different times to tell the time in words.

Now, there’s no reason to build a computer version of the Qlocktwo. With a computer program, you could display just the necessary words. Or numbers. Or even simulate a mechanical clock.

So, I went ahead and built my own version of the Qlocktwo using Java Swing. Here’s an image of it.

Qlocktwo JFrame

Here’s an image I took later in the morning.

Qlocktwo JFrame

I rearranged the letters in the 3rd row, but otherwise, I copied the letters from the Qlocktwo. I snapped the first screen shot at 8:01 in the morning and the second at 10:37 later in the morning. The four dots on the outside corners represent minutes. The words change every five minutes.

I’ve not seen a Qlocktwo in operation. I’ve seen a few pictures. I had to guess which letters lit to represent different times. I hope I guessed well.

There were some aspects to this project that were interesting. Creating the main JPanel required a couple of tricks. Coloring the JMenuBar required some trial and error.

I used Java 7 and Windows 8.1 to code the Java Swing application.

I used the model / view / controller pattern (MVC) to create this Java Swing clock. When constructing a GUI, the MVC pattern allows you to separate the concerns and focus on one part of the application at a time. This application required 3 model classes, 6 view classes, and a number of controller classes. All but one of the controller classes were coded as anonymous classes or inner classes. I’ll explain why as we go through the class explanations.

We’ll look at the model classes first. The first class we’ll look at is the ColorScheme class.

package com.ggl.qlocktwo.model;

import java.awt.Color;

public class ColorScheme {
	
	private Color background;
	private Color off;
	private Color on;
	
	public ColorScheme() {
		setBlackColorScheme();
	}
	
	public Color getBackground() {
		return background;
	}

	public Color getOff() {
		return off;
	}

	public Color getOn() {
		return on;
	}
	
	public void setBlackColorScheme() {
		this.background = Color.BLACK;
		this.off = Color.DARK_GRAY;
		this.on = Color.WHITE;
	}
	
	public void setPinkColorScheme() {
		this.background = Color.PINK;
		this.off = Color.WHITE;
		this.on = Color.BLACK;
	}
	
	public void setRedColorScheme() {
		this.background = new Color(192, 0, 0);
		this.off = Color.GRAY;
		this.on = Color.WHITE;
	}
	
	public void setBlueColorScheme() {
		this.background = Color.BLUE;
		this.off = Color.DARK_GRAY;
		this.on = Color.WHITE;
	}
	
	public void setGreenColorScheme() {
		this.background = Color.GREEN;
		this.off = Color.GRAY;
		this.on = Color.BLACK;
	}
	
}

Like the physical Qlocktwo, there are several color schemes that are available for the Java Swing Qlocktwo. You choose a color scheme from the JMenuBar, and the rest of the Swing application gets the color information from the ColorScheme instance. You can add additional color schemes if you wish. My favorite is the black scheme, although the pink scheme works surprisingly well.

Next, we’ll look at the QlocktwoCharacter class.

package com.ggl.qlocktwo.model;

public class QlocktwoCharacter {
	
	private boolean on;
	
	private char character;

	public QlocktwoCharacter(char character) {
		this.character = character;
	}

	public boolean isOn() {
		return on;
	}

	public void setOn(boolean on) {
		this.on = on;
	}

	public char getCharacter() {
		return character;
	}

}

Each instance of this class holds one of the Qlocktwo letters. The reason that I didn’t just use a char array is that I need to know when the letter was off and when the letter was on. Rather than trying to coordinate a separate char array and a boolean array, I created the QlocktwoCharacter class, and kept a QlocktwoCharacter array.

Data classes like QlocktwoCharacter are useful for organizing data into structures.

Finally, we’ll look at the QlocktwoModel class.

package com.ggl.qlocktwo.model;

public class QlocktwoModel {
	
	private boolean testClock;
	
	private char[][] characters = 
		{{'I', 'T', 'L', 'I', 'S', 'A', 'S', 'T', 'I', 'M', 'E'},
		 {'A', 'C', 'Q', 'U', 'A', 'R', 'T', 'E', 'R', 'D', 'C'},
		 {'T', 'W', 'E', 'N', 'T', 'Y', 'X', 'F', 'I', 'V', 'E'},
		 {'H', 'A', 'L', 'F', 'B', 'T', 'E', 'N', 'F', 'T', 'O'},
		 {'P', 'A', 'S', 'T', 'E', 'R', 'U', 'N', 'I', 'N', 'E'},
		 {'O', 'N', 'E', 'S', 'I', 'X', 'T', 'H', 'R', 'E', 'E'},
		 {'F', 'O', 'U', 'R', 'F', 'I', 'V', 'E', 'T', 'W', 'O'},
		 {'E', 'I', 'G', 'H', 'T', 'E', 'L', 'E', 'V', 'E', 'N'},
		 {'S', 'E', 'V', 'E', 'N', 'T', 'W', 'E', 'L', 'V', 'E'},
		 {'T', 'E', 'N', 'S', 'E', 'O', 'C', 'L', 'O', 'C', 'K'}};
	
	private int height;
	private int width;
	
	private ColorScheme colorScheme;
	
	private QlocktwoCharacter[][] characterArray;
	
	public QlocktwoModel() {
		this.width = characters[0].length;
		this.height = characters.length;
		this.colorScheme = new ColorScheme();
		this.characterArray = new QlocktwoCharacter[height][width];
		setCharacterArray();
	}

	private void setCharacterArray() {
		for (int i = 0; i < height; i++) {
			for (int j = 0; j < width; j++) {
				if ((i == 9) && (j == 5)) {
					// O with an diacritical apostrophe
					characterArray[i][j] = 
							new QlocktwoCharacter((char) 0x01A0);
				} else {
					characterArray[i][j] = new QlocktwoCharacter(
							characters[i][j]);
				}
			}
		}
	}

	public ColorScheme getColorScheme() {
		return colorScheme;
	}

	public int getHeight() {
		return height;
	}

	public int getWidth() {
		return width;
	}

	public QlocktwoCharacter[][] getCharacterArray() {
		return characterArray;
	}

	public boolean isTestClock() {
		return testClock;
	}

	public void setTestClock(boolean testClock) {
		this.testClock = testClock;
	}

	public void clearClock() {
		for (int i = 0; i < height; i++) {
			for (int j = 0; j < width; j++) {
				characterArray[i][j].setOn(false);
			}
		}
	}

	public void setItIs() {
		setCharacters(0, 0, 1, 3, 4);
	}
	
	public void setPast() {
		setCharacters(4, 0, 1, 2, 3);
	}
	
	public void setTo() {
		setCharacters(3, 9, 10);
	}
	
	public void setZero() {
		setCharacters(9, 5, 6, 7, 8, 9, 10);
	}
	
	public void setFive() {
		setCharacters(2, 7, 8, 9, 10);
	}
	
	public void setTen() {
		setCharacters(3, 5, 6, 7);
	}
	
	public void setFifteen() {
		setCharacters(1, 0, 2, 3, 4, 5, 6, 7, 8);
	}
	
	public void setTwenty() {
		setCharacters(2, 0, 1, 2, 3, 4, 5);
	}
	
	public void setTwentyFive() {
		setCharacters(2, 0, 1, 2, 3, 4, 5, 7, 8, 9, 10);
	}
	
	public void setThirty() {
		setCharacters(3, 0, 1, 2, 3);
	}
	
	public void setOneHour() {
		setCharacters(5, 0, 1, 2);
	}
	
	public void setTwoHour() {
		setCharacters(6, 8, 9, 10);
	}
	
	public void setThreeHour() {
		setCharacters(5, 6, 7, 8, 9, 10);
	}
	
	public void setFourHour() {
		setCharacters(6, 0, 1, 2, 3);
	}
	
	public void setFiveHour() {
		setCharacters(6, 4, 5, 6, 7);
	}
	
	public void setSixHour() {
		setCharacters(5, 3, 4, 5);
	}
	
	public void setSevenHour() {
		setCharacters(8, 0, 1, 2, 3, 4);
	}
	
	public void setEightHour() {
		setCharacters(7, 0, 1, 2, 3, 4);
	}
	
	public void setNineHour() {
		setCharacters(4, 7, 8, 9, 10);
	}
	
	public void setTenHour() {
		setCharacters(9, 0, 1, 2);
	}
	
	public void setElevenHour() {
		setCharacters(7, 5, 6, 7, 8, 9, 10);
	}
	
	public void setTwelveHour() {
		setCharacters(8, 5, 6, 7, 8, 9, 10);
	}

	private void setCharacters(int row, int... column) {
		for (int i = 0; i < column.length; i++) {
			characterArray[row][column[i]].setOn(true);
		}
	}
	
}

This is the main model class for the GUI. I have a char[][] array for the characters, and some other fields that help determine the status of the clock.

I copied the char[][] array into the Qlocktwo[][] array. I changed one of the O characters on the last row to &#x01A0 for &#x01A0 clock.

I came up with a clever scheme for defining the words I wanted to display. As far as I could tell, all the words were in the rows. There were no words that were in a column. So, I came up with a way to specify the word I wanted by listing the row and the columns that made up a word. You can see that starting with the setItIs method. The setCharacters method took those numbers and used them as array indices to turn on the characters in the QlocktwoCharacter array.

Whew. Now, lets look at the main class, Qlocktwo, which starts the Swing application.

package com.ggl.qlocktwo;

import javax.swing.SwingUtilities;

import com.ggl.qlocktwo.model.QlocktwoModel;
import com.ggl.qlocktwo.view.QlocktwoFrame;

public class Qlocktwo implements Runnable {

	@Override
	public void run() {
		new QlocktwoFrame(new QlocktwoModel());
	}
	
	public static void main (String[] args) {
		SwingUtilities.invokeLater(new Qlocktwo());
	}

}

Short and sweet. This 20 line class does 3 things.

  1. Starts the Swing Application on the Event Dispatch thread (EDT) by calling the SwingUtilities invokeLater method.
  2. Instantiates the Qlocktwo model.
  3. Instantiates the Qlocktwo JFrame.

A variation of this class is how I start all of my Swing applications.

Now, let’s look at the view classes. The first view class is QlocktwoFrame.

package com.ggl.qlocktwo.view;

import java.awt.Image;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.swing.JFrame;

import com.ggl.qlocktwo.controller.ClockRunnable;
import com.ggl.qlocktwo.model.QlocktwoModel;

public class QlocktwoFrame {
	
	private ClockRunnable clockRunnable;

	private JFrame frame;
	
	private QlocktwoMenuBar qlocktwoMenuBar;
	
	private QlocktwoModel model;
	
	private QlocktwoPanel qlocktwoPanel;
	
	public QlocktwoFrame(QlocktwoModel model) {
		this.model = model;
		createPartControl();
	}

	private void createPartControl() {
		frame = new JFrame();
		frame.setIconImage(getFrameImage());
		frame.setTitle("Qlocktwo");
		frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
		frame.addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent event) {
				exitProcedure();
			}
		});
		
		qlocktwoPanel = new QlocktwoPanel(model);
		frame.add(qlocktwoPanel.getPanel());
		
		qlocktwoMenuBar = new QlocktwoMenuBar(this, model);
		frame.setJMenuBar(qlocktwoMenuBar.getMenuBar());
		
		frame.pack();
		frame.setLocationByPlatform(true);
		frame.setVisible(true);
		
		clockRunnable = new ClockRunnable(this, model);
		new Thread(clockRunnable).start();
	}

	public void exitProcedure() {
		clockRunnable.setRunning(false);
		frame.dispose();
		System.exit(0);
	}
	
	public Image getFrameImage() {
		Image image = null;
		try {
			image = ImageIO.read(getClass().getResource(
					"/clock.png"));
		} catch (IOException e) {
			e.printStackTrace();
		}
		return image;
	}

	public JFrame getFrame() {
		return frame;
	}
	
	public void repaintQlocktwoPanel() {
		qlocktwoPanel.repaint();
	}
	
	public void updateCharacterPanel() {
		qlocktwoPanel.updateCharacterPanel();
	}
	
	public void updateDotPanels() {
		qlocktwoPanel.updateDotPanels(clockRunnable.getMinute());
	}
	
}

Notice how we use a JFrame. We don’t extend Swing components unless we want to override a component method. We don’t extend any Java class unless we want to override a class method. I’ll never understand why Swing teachers teach students to extend the Swing components. Using a Swing component keeps your methods separate from the Swing component’s methods.

The createPartControl method is pretty much the same for every Swing application I code. The WindowListener listens for the main window (JFrame) closing, so I can stop the clock runnable. After I create the view, I start the clock runnable. Generally, this is how you code a Swing application. First, you define all of the Swing components and build the view. Then you start any other threads that update the view.

There are 3 convenience methods at the bottom of the class. These methods allow me to update the inner panels from the QlocktwoFrame class. This way, I only have to pass an instance of the QlocktwoFrame class to the controller. The controller doesn’t need to know about the view inner classes.

We use a PNG image as an icon for the JFrame. You can use any JPEG or PNG image as an icon. Here’s the image I used.

Clock icon

The simpler the image, the better. The image will be shrunk to 48 x 48 pixels on Windows 8.1.

I loaded the image using the getFrameImage method. I created an images folder under the project name. I put the images folder on the class path. That way, I can read the image in Eclipse and I can read the image when I pack it in a JAR file.

Finally, this is how I implement the model / view / controller pattern (MVC) in Java Swing.

  1. The view may read values from the model. The view may not change values in the model.
  2. The controller will (probably) change values in the model.
  3. The controller will (probably) cause the view to repaint.

I don’t have one master controller, usually. I let each controller do its thing.

I do usually have one master model. Sometimes, you need more than one GUI model. This isn’t one of those times. The GUI model is usually separate from the application model, when your GUI is part of a much larger Java application.

Next, we’ll look at the QlocktwoPanel class.

package com.ggl.qlocktwo.view;

import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;

import javax.swing.JPanel;

import com.ggl.qlocktwo.model.QlocktwoModel;

public class QlocktwoPanel {
	
	protected static final Insets zeroInsets = 
			new Insets(0, 0, 0, 0);
	
	private CharacterPanel characterPanel;
	
	private DotPanel dotPanelNW;
	private DotPanel dotPanelNE;
	private DotPanel dotPanelSE;
	private DotPanel dotPanelSW;
	
	private DummyPanel dummyPanel1;
	private DummyPanel dummyPanel2;
	private DummyPanel dummyPanel3;
	private DummyPanel dummyPanel4;
	
	private JPanel panel;
	
	private QlocktwoModel model;
	
	public QlocktwoPanel(QlocktwoModel model) {
		this.model = model;
		createPartControl();
	}

	private void createPartControl() {
		characterPanel = new CharacterPanel(model);
		
		dotPanelNW = new DotPanel(model);
		dotPanelNE = new DotPanel(model);
		dotPanelSE = new DotPanel(model);
		dotPanelSW = new DotPanel(model);
		
		dummyPanel1 = new DummyPanel(model);
		dummyPanel2 = new DummyPanel(model);
		dummyPanel3 = new DummyPanel(model);
		dummyPanel4 = new DummyPanel(model);
		
		panel = new JPanel();
		panel.setLayout(new GridBagLayout());
		
		int gridy = 0;
		 
		addComponent(panel, dotPanelNW, 0, gridy, 1, 1,
				zeroInsets, GridBagConstraints.LINE_START,
				GridBagConstraints.BOTH);
		
		addComponent(panel, dummyPanel1, 1, gridy, 1, 1,
				zeroInsets, GridBagConstraints.LINE_START,
				GridBagConstraints.BOTH);
		
		addComponent(panel, dotPanelNE, 2, gridy++, 1, 1,
				zeroInsets, GridBagConstraints.LINE_START,
				GridBagConstraints.BOTH);
		
		addComponent(panel, dummyPanel2, 0, gridy, 1, 1,
				zeroInsets, GridBagConstraints.LINE_START,
				GridBagConstraints.BOTH);
		
		addComponent(panel, characterPanel.getPanel(), 1, gridy, 1, 1,
				zeroInsets, GridBagConstraints.LINE_START,
				GridBagConstraints.BOTH);
		
		addComponent(panel, dummyPanel3, 2, gridy++, 1, 1,
				zeroInsets, GridBagConstraints.LINE_START,
				GridBagConstraints.BOTH);
		
		addComponent(panel, dotPanelSW, 0, gridy, 1, 1,
				zeroInsets, GridBagConstraints.LINE_START,
				GridBagConstraints.BOTH);
		
		addComponent(panel, dummyPanel4, 1, gridy, 1, 1,
				zeroInsets, GridBagConstraints.LINE_START,
				GridBagConstraints.BOTH);
		
		addComponent(panel, dotPanelSE, 2, gridy++, 1, 1,
				zeroInsets, GridBagConstraints.LINE_START,
				GridBagConstraints.BOTH);
	}
	
	private void addComponent(Container container, Component component,
			int gridx, int gridy, int gridwidth, int gridheight, 
			Insets insets, int anchor, int fill) {
		GridBagConstraints gbc = new GridBagConstraints(gridx, gridy,
				gridwidth, gridheight, 1.0D, 1.0D, anchor, fill, 
				insets, 0, 0);
		container.add(component, gbc);
	}

	public JPanel getPanel() {
		return panel;
	}
	
	public void updateCharacterPanel() {
		characterPanel.updatePartControl();
	}
	
	public void repaint() {
		dummyPanel1.repaint();
		dummyPanel2.repaint();
		dummyPanel3.repaint();
		dummyPanel4.repaint();
	}
	
	public void updateDotPanels(int minute) {
		int dot = minute % 5;
		
		if (dot == 0) {
			dotPanelNW.setOn(false);
			dotPanelNE.setOn(false);
			dotPanelSE.setOn(false);
			dotPanelSW.setOn(false);
		} else if (dot == 1) {
			dotPanelNW.setOn(true);
			dotPanelNE.setOn(false);
			dotPanelSE.setOn(false);
			dotPanelSW.setOn(false);
		} else if (dot == 2) {
			dotPanelNW.setOn(true);
			dotPanelNE.setOn(true);
			dotPanelSE.setOn(false);
			dotPanelSW.setOn(false);
		} else if (dot == 3) {
			dotPanelNW.setOn(true);
			dotPanelNE.setOn(true);
			dotPanelSE.setOn(true);
			dotPanelSW.setOn(false);
		} else if (dot == 4) {
			dotPanelNW.setOn(true);
			dotPanelNE.setOn(true);
			dotPanelSE.setOn(true);
			dotPanelSW.setOn(true);
		} 
		
		dotPanelNW.repaint();
		dotPanelNE.repaint();
		dotPanelSE.repaint();
		dotPanelSW.repaint();
	}

}

This JPanel is made up of 9 subordinate JPanels, arranged in a 3 x 3 grid. The JPanels that make up the grid are different sizes.

I used the GridBagLayout to lay out the 9 subordinate JPanels. There are 4 dot panels, one in each corner. There are 4 dummy panels to fill out the main JPanel. There’s a character panel in the middle that displays the clock letters. I’ll describe these subordinate JPanels later in more detail.

I wrote the addComponent method to make sure each Swing component gets it’s own GridBagConstraints. Even though you can share one GridBagConstraints between Swing components, I don’t like to. I don’t want to remember the defaults and which elements to change for each Swing component. It’s much easier and less error-prone to create a complete GridBagConstraints for each Swing component.

I specified a fill of GridBagConstraints.BOTH for all of the subordinate JPanels so they would stretch when the JFrame is maximized.

Next, we’ll look at the DotPanel class.

package com.ggl.qlocktwo.view;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;

import javax.swing.JPanel;

import com.ggl.qlocktwo.model.QlocktwoModel;

public class DotPanel extends JPanel {

	private static final long serialVersionUID = -6203388399246019293L;
	
	private boolean on;
	
	private QlocktwoModel model;
	
	public DotPanel(QlocktwoModel model) {
		this.model = model;
	}

	public boolean isOn() {
		return on;
	}

	public void setOn(boolean on) {
		this.on = on;
	}

	@Override
	public Dimension getPreferredSize() {
		return new Dimension(64, 64);
	}

	@Override
	protected void paintComponent(Graphics g) {
		super.paintComponent(g);
		g.setColor(model.getColorScheme().getBackground());
		g.fillRect(0, 0, getWidth(), getHeight());
		Color color = null;
		if (isOn()) {
			color = model.getColorScheme().getOn();
		} else {
			color = model.getColorScheme().getOff();
		}
		paintCircle(g, color, 6, getWidth() / 2, getHeight() / 2);
	}
	
	private void paintCircle(Graphics g, Color color, 
			int radius, int x, int y) {
		g.setColor(color);
		g.fillOval(x - radius, y - radius, radius + radius, 
				radius + radius);
	}
}

We extend a JPanel because we want to override the getPreferredSize and paintComponent methods.

Basically, we paint a circle in the middle of the JPanel. We choose the color for the circle depending on whether it’s off or on. See how we get the colors from the ColorScheme instance.

The paintCircle method is a convenience method that allows us to define the circle with a radius and center point. Feel free to copy this method. I did, sometime long ago.

Next, we’ll look at the DummyPanel class.

package com.ggl.qlocktwo.view;

import java.awt.Graphics;

import javax.swing.JPanel;

import com.ggl.qlocktwo.model.QlocktwoModel;

public class DummyPanel extends JPanel {

	private static final long serialVersionUID = -2025698607367461610L;
	
	private QlocktwoModel model;
	
	public DummyPanel(QlocktwoModel model) {
		this.model = model;
	}

	@Override
	protected void paintComponent(Graphics g) {
		super.paintComponent(g);
		g.setColor(model.getColorScheme().getBackground());
		g.fillRect(0, 0, getWidth(), getHeight());
	}

}

All this class does is paint the dummy panel with the current background color. This fills in the space around the dot panels and the character panel.

Next, we’ll look at the most interesting subordinate JPanel class, the CharacterPanel class.

package com.ggl.qlocktwo.view;

import java.awt.Color;
import java.awt.Font;
import java.awt.GridLayout;

import javax.swing.BorderFactory;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.border.Border;

import com.ggl.qlocktwo.model.QlocktwoCharacter;
import com.ggl.qlocktwo.model.QlocktwoModel;

public class CharacterPanel {

	private JLabel[][] label;
	
	private JPanel panel;
	
	private QlocktwoModel model;

	public CharacterPanel(QlocktwoModel model) {
		this.model = model;
		createPartControl();
	}
	
	private void createPartControl() {
		int height = model.getHeight();
		int width = model.getWidth();
		
		panel = new JPanel();
		panel.setBackground(model.getColorScheme().getBackground());
		panel.setLayout(new GridLayout(height, width));
		
		Font font = new Font("SansSerif", Font.BOLD, 36);
		
		label = new JLabel[height][width];
		
		QlocktwoCharacter[][] characterArray = 
				model.getCharacterArray();
		
		Border border = BorderFactory.createLineBorder(
				model.getColorScheme().getBackground(), 4);
		
		for (int i = 0; i < height; i++) {
			for (int j = 0; j < width; j++) {
				String s = Character.toString(
						characterArray[i][j].getCharacter());
				label[i][j] = new JLabel(s);
				label[i][j].setBorder(border);
				label[i][j].setFont(font);
				label[i][j].setForeground(
						model.getColorScheme().getOff());
				label[i][j].setHorizontalAlignment(JLabel.CENTER);
				panel.add(label[i][j]);
			}
		}
	}
	
	public void updatePartControl() {
		panel.setBackground(model.getColorScheme().getBackground());
		
		Border border = BorderFactory.createLineBorder(
				model.getColorScheme().getBackground(), 4);
		
		int height = model.getHeight();
		int width = model.getWidth();
		
		QlocktwoCharacter[][] characterArray = 
				model.getCharacterArray();
		
		for (int i = 0; i < height; i++) {
			for (int j = 0; j < width; j++) {
				Color color = null;
				if (characterArray[i][j].isOn()) {
					color = model.getColorScheme().getOn();
				} else {
					color = model.getColorScheme().getOff();
				}
				label[i][j].setBorder(border);
				label[i][j].setForeground(color);
			}
		}
	}

	public JPanel getPanel() {
		return panel;
	}
	
}

This class creates a JLabel[][] array to hold the clock letters. One clock letter for each JLabel. We keep the instances of JLabel because we want to be able to turn the letters on and off by painting the letters a different color. Finally, we need to repaint the JLabel border when the color scheme changes.

I use a basic sans serif font that comes with Java. This font should be available on most, if not all, systems that have Java. 36 point worked out to a JFrame of 617 × 740 pixels, which should fit on most desktop monitors.

Notice how we separate the definition of the JLabels from the updating of the JLabels. Again, you first define the Swing components, then you use the Swing components. Don’t try to define and use the Swing components at the same time. You’re only asking for heartache. Separate your concerns as much as possible.

Finally, we’ll look at the JMenuBar class, the QlocktwoMenuBar class.

package com.ggl.qlocktwo.view;

import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;

import javax.swing.ButtonGroup;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import com.ggl.qlocktwo.model.QlocktwoModel;

public class QlocktwoMenuBar {
	
	private JMenu colorsMenu;
	private JMenu fileMenu;
	private JMenu optionsMenu;
	
	private JMenuBar menuBar;
	
	private JMenuItem blackColorMenuItem;
	private JMenuItem blueColorMenuItem;
	private JMenuItem exitMenuItem;
	private JMenuItem greenColorMenuItem;
	private JMenuItem pinkColorMenuItem;
	private JMenuItem redColorMenuItem;
	private JMenuItem testingMenuItem;
	
	private QlocktwoFrame frame;
	
	private QlocktwoModel model;

	public QlocktwoMenuBar(QlocktwoFrame frame, QlocktwoModel model) {
		this.frame = frame;
		this.model = model;
		createPartControl();
		updatePartControl();
	}
	
	private void createPartControl() {
		menuBar = new JMenuBar();
		
		fileMenu = new JMenu("File");
		fileMenu.setMnemonic(KeyEvent.VK_F);
		menuBar.add(fileMenu);
		
		exitMenuItem = new JMenuItem("Exit");
		exitMenuItem.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent event) {
				frame.exitProcedure();
			}	
		});
		exitMenuItem.setMnemonic(KeyEvent.VK_X);
		fileMenu.add(exitMenuItem);
		
		colorsMenu = new JMenu("Colors");
		colorsMenu.setMnemonic(KeyEvent.VK_C);
		menuBar.add(colorsMenu);
		
		ButtonGroup group = new ButtonGroup();
		ColorActionListener listener = new ColorActionListener();
		
		blackColorMenuItem = new JRadioButtonMenuItem(
				"Black background");
		blackColorMenuItem.addActionListener(listener);
		blackColorMenuItem.setActionCommand("black");
		blackColorMenuItem.setMnemonic(KeyEvent.VK_B);
		blackColorMenuItem.setSelected(true);
		group.add(blackColorMenuItem);
		colorsMenu.add(blackColorMenuItem);
		
		blueColorMenuItem = new JRadioButtonMenuItem(
				"Blue background");
		blueColorMenuItem.addActionListener(listener);
		blueColorMenuItem.setActionCommand("blue");
		blueColorMenuItem.setMnemonic(KeyEvent.VK_U);
		blueColorMenuItem.setSelected(true);
		group.add(blueColorMenuItem);
		colorsMenu.add(blueColorMenuItem);
		
		greenColorMenuItem = new JRadioButtonMenuItem(
				"Green background");
		greenColorMenuItem.addActionListener(listener);
		greenColorMenuItem.setActionCommand("green");
		greenColorMenuItem.setMnemonic(KeyEvent.VK_G);
		greenColorMenuItem.setSelected(true);
		group.add(greenColorMenuItem);
		colorsMenu.add(greenColorMenuItem);
		
		pinkColorMenuItem = new JRadioButtonMenuItem(
				"Pink background");
		pinkColorMenuItem.addActionListener(listener);
		pinkColorMenuItem.setActionCommand("pink");
		pinkColorMenuItem.setMnemonic(KeyEvent.VK_K);
		group.add(pinkColorMenuItem);
		colorsMenu.add(pinkColorMenuItem);
		
		redColorMenuItem = new JRadioButtonMenuItem(
				"Red background");
		redColorMenuItem.addActionListener(listener);
		redColorMenuItem.setActionCommand("red");
		redColorMenuItem.setMnemonic(KeyEvent.VK_R);
		group.add(redColorMenuItem);
		colorsMenu.add(redColorMenuItem);
		
		optionsMenu = new JMenu("Options");
		optionsMenu.setMnemonic(KeyEvent.VK_O);
		menuBar.add(optionsMenu);
		
		testingMenuItem = new JCheckBoxMenuItem(
				"Test Clock");
		testingMenuItem.addChangeListener(new ChangeListener() {
			@Override
			public void stateChanged(ChangeEvent event) {
				JCheckBoxMenuItem item = 
						(JCheckBoxMenuItem) event.getSource();
				if (item.isSelected()) {
					model.setTestClock(true);
				} else {
					model.setTestClock(false);
				}
			}	
		});
		testingMenuItem.setMnemonic(KeyEvent.VK_T);
		optionsMenu.add(testingMenuItem);
	}
	
	private void updatePartControl() {
		Color background = model.getColorScheme().getBackground();
		Color on = model.getColorScheme().getOn();
		
		menuBar.setBackground(background);
		
		fileMenu.setForeground(on);
		
		exitMenuItem.setBackground(background);
		exitMenuItem.setForeground(on);
		
		colorsMenu.setForeground(on);
		
		blackColorMenuItem.setBackground(background);
		blackColorMenuItem.setForeground(on);
		
		blueColorMenuItem.setBackground(background);
		blueColorMenuItem.setForeground(on);
		
		greenColorMenuItem.setBackground(background);
		greenColorMenuItem.setForeground(on);
		
		pinkColorMenuItem.setBackground(background);
		pinkColorMenuItem.setForeground(on);
		
		redColorMenuItem.setBackground(background);
		redColorMenuItem.setForeground(on);
		
		optionsMenu.setForeground(on);
		
		testingMenuItem.setBackground(background);
		testingMenuItem.setForeground(on);
	}

	public JMenuBar getMenuBar() {
		return menuBar;
	}
	
	public class ColorActionListener implements ActionListener {

		@Override
		public void actionPerformed(ActionEvent event) {
			String command = event.getActionCommand();
			Object o = event.getSource();
			JRadioButtonMenuItem item = (JRadioButtonMenuItem) o;
			if (item.isSelected()) {
				if (command.equals("black")) {
					model.getColorScheme().setBlackColorScheme();
				} else if (command.equals("blue")) {
					model.getColorScheme().setBlueColorScheme();
				} else if (command.equals("green")) {
					model.getColorScheme().setGreenColorScheme();
				} else if (command.equals("pink")) {
					model.getColorScheme().setPinkColorScheme();
				} else if (command.equals("red")) {
					model.getColorScheme().setRedColorScheme();
				} 
				frame.updateCharacterPanel();
				frame.updateDotPanels();
				updatePartControl();
			}
		}
	}
	
}

The createPartControl method builds the JMenuBar, the JMenus, and the JMenuItems in a standard way. I give all the JMenuBar components a mnemonic key to facilitate using the keyboard instead of the mouse. The ALT key is assumed.

I use anonymous controller classes because they are small. It wasn’t worth the effort to make them separate controller classes. I created the ColorActionListener as an inner class because one of the methods it performs is to repaint the JMenuBar itself. You can make the ColorActionListener its own class if you want.

Finally, we get to the controller class that runs the clock, the ClockRunnable class.

package com.ggl.qlocktwo.controller;

import java.util.Calendar;

import javax.swing.SwingUtilities;

import com.ggl.qlocktwo.model.QlocktwoModel;
import com.ggl.qlocktwo.view.QlocktwoFrame;

public class ClockRunnable implements Runnable {
	
	private boolean running;
	
	private long sleepInterval;
	
	private Calendar now;
	
	private QlocktwoFrame frame;
	
	private QlocktwoModel model;

	public ClockRunnable(QlocktwoFrame frame, QlocktwoModel model) {
		this.frame = frame;
		this.model = model;
	}

	@Override
	public void run() {
		running = true;
		while (running) {
			setSleepInterval();
			now = getTime();
			setModelTime(now);
			updateGUI();
			sleep();
		}
	}
	
	private void setSleepInterval() {
		if (model.isTestClock()) {
			this.sleepInterval = 100L;
		} else {
			this.sleepInterval = 10000L;
		}
	}
	
	private Calendar getTime() {
		if (model.isTestClock()) {
			return generateFastCalendar();
		} else {
			return Calendar.getInstance();
		}
	}

	private Calendar generateFastCalendar() {
		Calendar now = Calendar.getInstance();
		int seconds = now.get(Calendar.SECOND);
		int minutes = now.get(Calendar.MINUTE);
		minutes = minutes % 24;
		now.set(Calendar.HOUR_OF_DAY, minutes);
		now.set(Calendar.MINUTE, seconds);
		return now;
	}
	
	private void setModelTime(Calendar now) {
		int hour = now.get(Calendar.HOUR);
		int minute = now.get(Calendar.MINUTE) / 5 * 5;
		
		if (minute > 30) hour = ++hour % 12;
		
		model.clearClock();
		model.setItIs();
		
		switch (minute) {
			case 0: 	model.setZero();
						break;
			case 5:		model.setPast();
						model.setFive();
						break;
			case 10:	model.setPast();
						model.setTen();
						break;
			case 15:	model.setPast();
						model.setFifteen();
						break;
			case 20:	model.setPast();
						model.setTwenty();
						break;
			case 25:	model.setPast();
						model.setTwentyFive();
						break;
			case 30:	model.setPast();
						model.setThirty();
						break;
			case 35:	model.setTo();
						model.setTwentyFive();
						break;
			case 40:	model.setTo();
						model.setTwenty();
						break;
			case 45:	model.setTo();
						model.setFifteen();
						break;
			case 50:	model.setTo();
						model.setTen();
						break;
			case 55:	model.setTo();
						model.setFive();
						break;
		}
		
		switch (hour) {
			case 0:		model.setTwelveHour();
						break;
			case 1:		model.setOneHour();
						break;
			case 2:		model.setTwoHour();
						break;
			case 3:		model.setThreeHour();
						break;
			case 4:		model.setFourHour();
						break;
			case 5:		model.setFiveHour();
						break;
			case 6:		model.setSixHour();
						break;
			case 7:		model.setSevenHour();
						break;
			case 8:		model.setEightHour();
						break;
			case 9:		model.setNineHour();
						break;
			case 10:	model.setTenHour();
						break;
			case 11:	model.setElevenHour();
						break;
		}
	}
	
	private void updateGUI() {
		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				frame.updateCharacterPanel();
				frame.updateDotPanels();
			}	
		});
	}

	private void sleep() {
		try {
			Thread.sleep(sleepInterval);
		} catch (InterruptedException e) {
		}
	}

	public void setRunning(boolean running) {
		this.running = running;
	}

	public int getHour() {
		return now.get(Calendar.HOUR);
	}

	public int getMinute() {
		return now.get(Calendar.MINUTE);
	}

}

If you look back to the QlocktwoFrame class, I run this class in a separate thread. The ClockRunnable class has the same change model, update GUI, and sleep loop you would find in any animation class.

The test mode runs in minutes and seconds instead of hours and minutes. This allows me and you to cycle the clock through a 12 hour period in 12 minutes. I used this mode to test the clock. You can use this mode to show the clock off to your friends.

We sleep for a period of time, depending on whether we’re running in test mode or normal mode. I wake up the thread 6 times a second or 6 times a minute, depending on the mode. This keeps the clock more closely synchronized to the computer time. Even at 6 times a second, the thread doesn’t use much CPU time.

In the setModelTime method, there are two long (too long?) switch statements. I couldn’t think of a better way to turn the clock words on.

Since we’re running this class in a separate thread, we update the GUI by putting the methods in the SwingUtilities invokeLater method. This ensures that the GUI update happens on the Event Dispatch thread (EDT).

Thank you for reading this entire article. I hope you learned something about how to put a Swing application together.

Post a Comment

Your email is kept private. Required fields are marked *