Musings about Java and Swing

RGB Display Using Java Swing

Recently on Stack Overflow, I saw a question about an RGB Display. Since I’ve coded so many games, I decided to write an RGB display as a Swing application.

Introduction

Here’s a picture of the display with a test image. My window cropping tool does not adjust for the smaller borders of a Windows 10 window. I used Java 7 for the code.

RGB Display

Here’s a picture of the RGB Display with a photograph.

RGB Display

The scroll bars appear when the picture is larger than 400 x 300 pixels. The scroll bars are synchronized, so that when you move to a part of any of the 4 images, the other images adjust to the same place. The synchronization code was new to me.

Large pictures (4000 x 3000 pixels or larger) will take a couple of seconds to render.

Overview

I wrote 6 classes, using the model / view / controller pattern (MVC). I copied two classes from the Oracle tutorial on using a JFileChooser, Utils and ImageFilter.

When I say I use the model / view / controller pattern with Java Swing, I mean:

  1. The view may read values from the model.
  2. The view may not update the model.
  3. The controller will update the model.
  4. The controller will repaint / revalidate the view.

Basically, the model is ignorant of the view and controller. This allows you to change the view and controller from Swing to a web site, or an Android app.

The model / view / controller pattern allows you to focus on one part of the Swing GUI at a time. In general, you’ll create the model first, then the view, and finally the controllers. You will have to go back and add fields to the model. I guarantee that you’ll come up with something you didn’t think of when you created the first cut of the model classes.

Now for the code. The first class is the main RGBDisplay class.

package com.ggl.rgbdisplay;

import javax.swing.SwingUtilities;

import com.ggl.rgbdisplay.model.RGBDisplayModel;
import com.ggl.rgbdisplay.view.RGBDisplayFrame;

public class RGBDisplay implements Runnable {

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

	@Override
	public void run() {
		RGBDisplayModel model = new RGBDisplayModel();
		model.setOriginalImage(RGBDisplayModel.createTestImage());
		new RGBDisplayFrame(model);
	}
}

This short class does 3 things.

  1. Puts the Swing components on the Event Dispatch thread (EDT) by executing the invokeLater method of SwingUtilities.
  2. Instantiates the RGBDisplayModel class.
  3. Instantiates the RGBDisplayFrame class.

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

Model

Let’s look at the model class, RGBDisplayModel.

package com.ggl.rgbdisplay.model;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.image.BufferedImage;

public class RGBDisplayModel {

	private BufferedImage originalImage;
	private BufferedImage redImage;
	private BufferedImage greenImage;
	private BufferedImage blueImage;

	public BufferedImage getOriginalImage() {
		return originalImage;
	}

	public void setOriginalImage(BufferedImage originalImage) {
		this.originalImage = originalImage;
		this.redImage = createColorImage(originalImage, 0xFFFF0000);
		this.greenImage = createColorImage(originalImage, 0xFF00FF00);
		this.blueImage = createColorImage(originalImage, 0xFF0000FF);
	}

	public BufferedImage getRedImage() {
		return redImage;
	}

	public BufferedImage getGreenImage() {
		return greenImage;
	}

	public BufferedImage getBlueImage() {
		return blueImage;
	}

	public static BufferedImage createTestImage() {
		BufferedImage bufferedImage = new BufferedImage(200, 200,
				BufferedImage.TYPE_INT_ARGB);
		Graphics g = bufferedImage.getGraphics();

		for (int y = 0; y < bufferedImage.getHeight(); y += 20) {
			if (y % 40 == 0) {
				g.setColor(Color.WHITE);
			} else {
				g.setColor(Color.BLACK);
			}
			g.fillRect(0, y, bufferedImage.getWidth(), 20);
		}

		g.dispose();
		return bufferedImage;
	}

	private BufferedImage createColorImage(BufferedImage originalImage, int mask) {
		BufferedImage colorImage = new BufferedImage(originalImage.getWidth(),
				originalImage.getHeight(), originalImage.getType());

		for (int x = 0; x < originalImage.getWidth(); x++) {
			for (int y = 0; y < originalImage.getHeight(); y++) {
				int pixel = originalImage.getRGB(x, y) & mask;
				colorImage.setRGB(x, y, pixel);
			}
		}

		return colorImage;
	}

}

This model class holds the original image and creates the red, green, and blue images.

In the createColorImage method, I copy the pixels from the original image to the color image, masking out the colors I don’t want. The 32 bit pixel is divided into 4 8-bit areas. The first area is the saturation. The next 3 areas are the red, green, and blue colors, respectively. These values run from 0 to 255.

View

Now that we’ve defined the model class, let’s look at the view classes. The first view class is the RGBDisplayFrame class.

package com.ggl.rgbdisplay.view;

import java.awt.Dimension;
import java.awt.GridLayout;

import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JScrollPane;

import com.ggl.rgbdisplay.controller.OpenPictureListener;
import com.ggl.rgbdisplay.controller.Synchronizer;
import com.ggl.rgbdisplay.model.RGBDisplayModel;

public class RGBDisplayFrame {

	private DisplayImage redImage;
	private DisplayImage greenImage;
	private DisplayImage blueImage;
	private DisplayImage originalImage;

	private JFrame frame;

	private JScrollPane redScrollPane;
	private JScrollPane greenScrollPane;
	private JScrollPane blueScrollPane;
	private JScrollPane originalScrollPane;

	private RGBDisplayModel model;

	public RGBDisplayFrame(RGBDisplayModel model) {
		this.model = model;
		createPartControl();
	}

	private void createPartControl() {
		frame = new JFrame("RGB Display");
		frame.setIconImage(model.getOriginalImage());
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		frame.setJMenuBar(createMenuBar());
		frame.add(createMainPanel());

		frame.setLocationByPlatform(true);
		frame.pack();
		frame.setVisible(true);
	}

	private JMenuBar createMenuBar() {
		JMenuBar menuBar = new JMenuBar();

		JMenuItem openMenuItem = new JMenuItem("Open Picture");
		openMenuItem.addActionListener(new OpenPictureListener(this, model));
		menuBar.add(openMenuItem);

		return menuBar;
	}

	private JPanel createMainPanel() {
		JPanel mainPanel = new JPanel();
		mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
		mainPanel.setLayout(new GridLayout(0, 2, 10, 10));

		Synchronizer synchronizer = new Synchronizer();

		redImage = new DisplayImage(model.getRedImage());
		redScrollPane = new JScrollPane(redImage);
		redScrollPane.getVerticalScrollBar()
				.addAdjustmentListener(synchronizer);
		redScrollPane.getHorizontalScrollBar().addAdjustmentListener(
				synchronizer);
		mainPanel.add(redScrollPane);

		greenImage = new DisplayImage(model.getGreenImage());
		greenScrollPane = new JScrollPane(greenImage);
		greenScrollPane.getVerticalScrollBar().addAdjustmentListener(
				synchronizer);
		greenScrollPane.getHorizontalScrollBar().addAdjustmentListener(
				synchronizer);
		mainPanel.add(greenScrollPane);

		blueImage = new DisplayImage(model.getBlueImage());
		blueScrollPane = new JScrollPane(blueImage);
		blueScrollPane.getVerticalScrollBar().addAdjustmentListener(
				synchronizer);
		blueScrollPane.getHorizontalScrollBar().addAdjustmentListener(
				synchronizer);
		mainPanel.add(blueScrollPane);

		originalImage = new DisplayImage(model.getOriginalImage());
		originalScrollPane = new JScrollPane(originalImage);
		originalScrollPane.getVerticalScrollBar().addAdjustmentListener(
				synchronizer);
		originalScrollPane.getHorizontalScrollBar().addAdjustmentListener(
				synchronizer);
		mainPanel.add(originalScrollPane);

		return mainPanel;
	}

	public void updateMainPanel() {
		redImage.setImage(model.getRedImage());
		greenImage.setImage(model.getGreenImage());
		blueImage.setImage(model.getBlueImage());
		originalImage.setImage(model.getOriginalImage());

		int maxWidth = 400;
		int maxHeight = 300;
		Dimension d = redImage.getPreferredSize();
		d.width = (d.width < maxWidth) ? d.width : maxWidth;
		d.height = (d.height < maxHeight) ? d.height : maxHeight;

		if ((d.width < maxWidth) && (d.height < maxHeight)) {
			redScrollPane.setPreferredSize(null);
			greenScrollPane.setPreferredSize(null);
			blueScrollPane.setPreferredSize(null);
			originalScrollPane.setPreferredSize(null);
		} else {
			redScrollPane.setPreferredSize(d);
			greenScrollPane.setPreferredSize(d);
			blueScrollPane.setPreferredSize(d);
			originalScrollPane.setPreferredSize(d);
		}

		frame.setVisible(false);
		frame.pack();
		frame.setVisible(true);
	}

	public JFrame getJFrame() {
		return frame;
	}
}

The createPartControl method is pretty much the same for all of my Java Swing projects. I create a JFrame, and add all of the JPanels. For other projects, the JPanels are different.

Notice that we don’t extend JFrame. We use a JFrame. The only reason you should extend a Swing component, or any Java class, is when you want to override one or more of the methods.

For most Swing applications, you want to create the Swing components first, then modify them as needed. In the createMainPanel method, we have the following hierarchy of Swing components.

JPanel -> JScrollPane -> JPanel

The main JPanel consists of 4 JScrollPanes. Each JScrollPane holds a DrawingPanel (JPanel). We create the JScrollPanes when we create the GUI, even though we won’t need them until we read a picture larger than 400 x 300.

The updateMainPanel method updates the Swing components that we defined in the createMainPanel method. The code to set the preferred size of the JScrollPanes is a little complicated. When we don’t need the JScrollPane scroll bars, we set the preferred size to null and let Swing figure out the component sizes. When we do need the JScrollPane scroll bars, we explicitly set the preferred size. Finally, we pack the JFrame again to let Swing lay out the components and calculate the component sizes.

The final view class is the DisplayImage class.

package com.ggl.rgbdisplay.view;

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.image.BufferedImage;

import javax.swing.JPanel;

public class DisplayImage extends JPanel {

	private static final long serialVersionUID = -1990301575135189666L;

	private BufferedImage image;

	public DisplayImage(BufferedImage image) {
		setImage(image);
	}

	public BufferedImage getImage() {
		return image;
	}

	public void setImage(BufferedImage image) {
		this.image = image;
		this.setPreferredSize(new Dimension(image.getWidth(), image.getHeight()));
	}

	@Override
	protected void paintComponent(Graphics g) {
		super.paintComponent(g);
		g.drawImage(image, 0, 0, getParent());
	}

}

This class is simple. We extend the JPanel class in this case because we want to override the paintComponent method. We override the paintComponent method to draw (paint) on the JPanel. We do not use the Canvas class. Canvas is an AWT component. We do not mix AWT and Swing components unless it’s absolutely necessary. In the paintComponent method, we paint. Period. Full stop. We do nothing else in the paintComponent method. As you’ll see when we talk about the controller classes, we update the model and then and only then we call for a repaint of the drawing panel.

This class allows us to keep the images (model) separate from the JPanel (view).

We set the size of the drawing panel in the setImage method. A JPanel has no size by itself. The children Swing components of a JPanel give the JPanel its size. Because we’re going to draw on the drawing panel, we have to give it a preferred size. We give the drawing panel a new preferred size each time we change the original image.

Controller

Let’s look at the controller classes. The first controller class is the OpenPictureListener class.

package com.ggl.rgbdisplay.controller;

import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.swing.JFileChooser;
import javax.swing.filechooser.FileFilter;

import com.ggl.rgbdisplay.model.RGBDisplayModel;
import com.ggl.rgbdisplay.view.RGBDisplayFrame;

public class OpenPictureListener implements ActionListener {

	private JFileChooser fc;

	private RGBDisplayFrame frame;

	private RGBDisplayModel model;

	public OpenPictureListener(RGBDisplayFrame frame, RGBDisplayModel model) {
		this.frame = frame;
		this.model = model;
		this.fc = new JFileChooser();
		ImageFilter imageFilter = new ImageFilter();
		this.fc.addChoosableFileFilter(imageFilter);
		this.fc.setFileFilter(imageFilter);
		this.fc.setAcceptAllFileFilterUsed(false);
	}

	@Override
	public void actionPerformed(ActionEvent event) {
		int returnVal = fc.showOpenDialog(frame.getJFrame());
		if (returnVal == JFileChooser.APPROVE_OPTION) {
			File file = fc.getSelectedFile();
			try {
				createOriginalImage(file);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	private void createOriginalImage(File file) throws IOException {
		BufferedImage image = ImageIO.read(file);
		BufferedImage originalImage = new BufferedImage(image.getWidth(),
				image.getHeight(), BufferedImage.TYPE_INT_ARGB);

		Graphics g = originalImage.getGraphics();
		g.drawImage(image, 0, 0, null);
		g.dispose();

		model.setOriginalImage(originalImage);
		frame.updateMainPanel();
	}

	public class ImageFilter extends FileFilter {

		@Override
		public boolean accept(File f) {
			if (f.isDirectory()) {
				return true;
			}

			String extension = Utils.getExtension(f);
			if (extension != null) {
				if (extension.equals(Utils.jpeg) || extension.equals(Utils.jpg)
						|| extension.equals(Utils.png)) {
					return true;
				} else {
					return false;
				}
			}

			return false;
		}

		@Override
		public String getDescription() {
			return "Image files";
		}
	}

}

This class uses a JFileChooser to allow us to select which picture you wish to display. By putting the JFileChooser in the constructor, the JFileChooser “remembers” which file was opened last. This helps the user go through a series of pictures without any additional coding on our part.

In the createOriginalImage method, we copy the image we read into another image with a defined format. We do this for two reasons.

  1. Some png images do not display correctly. By copying the image, we hope that the copied image will display correctly.
  2. We copy the image into a known format, so that the model masking process works correctly.

The ImageFilter class was copied from an Oracle Swing tutorial. I made it an inline class because the OpenPictureListener class is the only class that uses the ImageFilter class. I could have made ImageFilter package private. I made it public.

The next controller class is the Synchronizer class. This is the class that synchronizes the 4 JScrollPane scroll bars.

package com.ggl.rgbdisplay.controller;

import java.awt.Component;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;

import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;

public class Synchronizer implements AdjustmentListener {

	@Override
	public void adjustmentValueChanged(AdjustmentEvent event) {
		JScrollBar adjustedScrollBar = (JScrollBar) event.getSource();
		int value = adjustedScrollBar.getValue();

		JScrollPane adjustedScrollPane = (JScrollPane) adjustedScrollBar
				.getParent();
		JScrollBar horizontalScrollBar = adjustedScrollPane
				.getHorizontalScrollBar();

		JPanel panel = (JPanel) adjustedScrollPane.getParent();

		Component[] components = panel.getComponents();

		for (Component component : components) {
			if (component instanceof JScrollPane) {
				JScrollPane scrollPane = (JScrollPane) component;
				if (!scrollPane.equals(adjustedScrollPane)) {
					if (adjustedScrollBar.equals(horizontalScrollBar)) {
						JScrollBar bar = scrollPane.getHorizontalScrollBar();
						bar.setValue(value);
					} else {
						JScrollBar bar = scrollPane.getVerticalScrollBar();
						bar.setValue(value);
					}
				}
			}
		}
	}

}

The class only has one method, the adjustmentValueChanged method.

We work our way up to the main JPanel, then process all of the JScrollPanes in the JPanel. We skip any components that aren’t a JScrollPane. We don’t process the JScrollPane that the user moved. We process the other 3 JScrollPanes.

There are no constants in the adjustmentValueChanged method. This method would work with any number of JScrollPanes.

The final controller class is the Utils class.

package com.ggl.rgbdisplay.controller;

import java.io.File;

public class Utils {

	public final static String jpeg = "jpeg";
	public final static String jpg = "jpg";
	public final static String gif = "gif";
	public final static String tiff = "tiff";
	public final static String tif = "tif";
	public final static String png = "png";

	/*
	 * Get the extension of a file.
	 */
	public static String getExtension(File f) {
		String ext = null;
		String s = f.getName();
		int i = s.lastIndexOf('.');

		if (i > 0 && i < s.length() - 1) {
			ext = s.substring(i + 1).toLowerCase();
		}

		return ext;
	}
}

I copied this class from the Oracle JFileChooser tutorial. This class returns the extension of a file.

I hope this article was interesting. I’d never manipulated JScrollPanes so much, nor tried to synchronize JScrollPane scroll bars before.

Post a Comment

Your email is kept private. Required fields are marked *