Musings about Java and Swing

Java Swing Marquee

I was walking east on 600 South in Salt Lake City, Utah, when I saw a marquee for a hotel. The letters were purple on black. ┬áThe marquee didn’t do anything fancy. ┬áThe letters just moved from right to left, so you could read the whole message.

Here’s my Java Swing version of that hotel marquee.

Marquee

I used Java 7 on Windows Vista.

The marquee pixels are pink. If the text is shorter than the marquee width, the text is displayed. Otherwise, the text moves from right to left, one column at a time.

You can switch fonts at any time, but you have to resubmit the text to have the new font take effect.

I used the model / view / controller pattern (MVC) to code this Swing application. That way, I could separate the concerns and focus on one aspect of the application at a time. I coded 3 model classes, 3 view classes, and 4 controller classes. There are also anonymous and inner controller classes. Generally, I use anonymous classes when the controller classes are short. I use inner classes when I need lots of fields and / or methods from the view class.

This is how I code an MVC Swing application:

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 case the view to update / repaint.

So, let’s get started looking at the code. The first class we’ll look at is the main class, Marquee.

package com.ggl.marquee;

import javax.swing.SwingUtilities;

import com.ggl.marquee.model.MarqueeModel;
import com.ggl.marquee.view.MarqueeFrame;

public class Marquee implements Runnable {

	@Override
	public void run() {
		new MarqueeFrame(new MarqueeModel());
	}

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

}

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 MarqueeModel class.
3. Instantiates the MarqueeFrame class.

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

Next, let’s look at the model classes. The first model class we will look at is MarqueeModel.

package com.ggl.marquee.model;

import javax.swing.DefaultListModel;

import com.ggl.marquee.runnable.DisplayTextPixelsRunnable;
import com.ggl.marquee.view.MarqueeFrame;

public class MarqueeModel {

	private static final int marqueeWidth = 120;

	private boolean[][] marqueePixels;
	private boolean[][] textPixels;

	private DisplayTextPixelsRunnable dtpRunnable;

	private MarqueeFontFactory fonts;

	private MarqueeFrame frame;

	public MarqueeModel() {
		this.fonts = new MarqueeFontFactory();
		this.marqueePixels = new boolean[marqueeWidth][getMarqueeHeight()];
	}

	public void setFrame(MarqueeFrame frame) {
		this.frame = frame;
	}

	public MarqueeFontFactory getFonts() {
		return fonts;
	}

	public DefaultListModel<MarqueeFont> getDefaultListModel() {
		return fonts.getFontList();
	}

	public MarqueeFont getDefaultFont() {
		return fonts.getDefaultFont();
	}

	public void setDefaultFont(MarqueeFont defaultFont) {
		fonts.setDefaultFont(defaultFont);
	}

	public boolean[][] getMarqueePixels() {
		return marqueePixels;
	}

	public boolean getMarqueePixel(int width, int height) {
		return marqueePixels[width][height];
	}

	public int getMarqueeWidth() {
		return marqueeWidth;
	}

	public int getMarqueeHeight() {
		return fonts.getCharacterHeight();
	}

	public boolean[][] getTextPixels() {
		return textPixels;
	}

	public int getTextPixelWidth() {
		return textPixels.length;
	}

	private void startDtpRunnable() {
		dtpRunnable = new DisplayTextPixelsRunnable(frame, this);
		new Thread(dtpRunnable).start();
	}

	public void stopDtpRunnable() {
		if (dtpRunnable != null) {
			dtpRunnable.stopDisplayTextPixelsRunnable();
			dtpRunnable = null;
		}
	}

	public void setTextPixels(boolean[][] textPixels) {
		this.textPixels = textPixels;
		if (textPixels.length < getMarqueeWidth()) {
			this.marqueePixels = copyCharacterPixels(0, textPixels,
					marqueePixels);
		} else {
			startDtpRunnable();
		}
	}

	public void resetPixels() {
		for (int i = 0; i < getMarqueeWidth(); i++) {
			for (int j = 0; j < getMarqueeHeight(); j++) {
				marqueePixels[i][j] = false;
			}
		}
	}

	public void setAllPixels() {
		for (int i = 0; i < getMarqueeWidth(); i++) {
			for (int j = 0; j < getMarqueeHeight(); j++) {
				marqueePixels[i][j] = true;
			}
		}
	}

	public boolean[][] copyCharacterPixels(int position,
			boolean[][] characterPixels, boolean[][] textPixels) {
		for (int i = 0; i < characterPixels.length; i++) {
			for (int j = 0; j < characterPixels[i].length; j++) {
				textPixels[i + position][j] = characterPixels[i][j];
			}
		}

		return textPixels;
	}

	public void copyTextPixels(int position) {
		for (int i = 0; i < marqueePixels.length; i++) {
			int k = i + position;
			k %= textPixels.length;
			for (int j = 0; j < textPixels[i].length; j++) {
				marqueePixels[i][j] = textPixels[k][j];
			}
		}
	}

}

The MarqueeModel class holds the marquee pixels and the text pixels. Besides the getter and setter methods, there are methods for filling and clearing the marquee pixels, as well as a method for copying the text pixels to the marquee pixels.

Next, let’s look at the class that holds one marquee font, MarqueeFont.

package com.ggl.marquee.model;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.font.FontRenderContext;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;

public class MarqueeFont {

	private static final boolean DEBUG = false;

	private int fontHeight;

	private Font font;

	public MarqueeFont(Font font) {
		this.font = font;

		FontRenderContext frc = new FontRenderContext(null, true, true);
		Rectangle2D r2D = font.getStringBounds("HgH", frc);
		this.fontHeight = (int) Math.round(r2D.getHeight());

		if (DEBUG) {
			System.out.println(font.getFamily() + " " + fontHeight + " pixels");
		}
	}

	public boolean[][] getTextPixels(String s) {
		FontRenderContext frc = new FontRenderContext(null, true, true);

		Rectangle2D r2D = font.getStringBounds(s, frc);
		int rWidth = (int) Math.round(r2D.getWidth());
		int rHeight = (int) Math.round(r2D.getHeight());
		int rX = (int) Math.round(r2D.getX());
		int rY = (int) Math.round(r2D.getY());

		if (DEBUG) {
			System.out.print(s);
			System.out.print(", rWidth = " + rWidth);
			System.out.print(", rHeight = " + rHeight);
			System.out.println(", rX = " + rX + ", rY = " + rY);
		}

		BufferedImage bi = generateCharacterImage(rX, -rY, rWidth, rHeight, s);
		int[][] pixels = convertTo2D(bi);

		if (DEBUG) {
			displayPixels(pixels);
		}

		return createTextPixels(pixels);
	}

	private BufferedImage generateCharacterImage(int x, int y, int width,
			int height, String string) {
		BufferedImage bi = new BufferedImage(width, height,
				BufferedImage.TYPE_INT_RGB);
		Graphics g = bi.getGraphics();
		g.setFont(font);

		g.setColor(Color.WHITE);
		g.fillRect(0, 0, width, height);

		g.setColor(Color.BLACK);
		g.drawString(string, x, y);

		return bi;
	}

	private int[][] convertTo2D(BufferedImage image) {
		final int[] pixels = ((DataBufferInt) image.getRaster().getDataBuffer())
				.getData();
		final int width = image.getWidth();
		final int height = image.getHeight();

		int[][] result = new int[height][width];

		int row = 0;
		int col = 0;
		for (int pixel = 0; pixel < pixels.length; pixel++) {
			result[row][col] = pixels[pixel];
			col++;
			if (col == width) {
				col = 0;
				row++;
			}
		}

		return result;
	}

	private void displayPixels(int[][] pixels) {
		for (int i = 0; i < pixels.length; i++) {
			String s = String.format("%03d", (i + 1));
			System.out.print(s + ". ");
			for (int j = 0; j < pixels[i].length; j++) {
				if (pixels[i][j] == -1) {
					System.out.print("  ");
				} else {
					System.out.print("X ");
				}
			}
			System.out.println("");
		}
	}

	private boolean[][] createTextPixels(int[][] pixels) {
		// The int array pixels is in column, row order.
		// We have to flip the array and produce the output
		// in row, column order.
		if (DEBUG) {
			System.out.println(pixels[0].length + "x" + pixels.length);
		}
		boolean[][] textPixels = new boolean[pixels[0].length][pixels.length];
		for (int i = 0; i < pixels.length; i++) {
			for (int j = 0; j < pixels[i].length; j++) {
				if (pixels[i][j] == -1) {
					textPixels[j][i] = false;
				} else {
					textPixels[j][i] = true;
				}
			}
		}

		return textPixels;
	}

	public Font getFont() {
		return font;
	}

	public int getFontHeight() {
		return fontHeight;
	}

	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		builder.append(font.getFamily());
		builder.append(", ");
		builder.append(getStyleText());
		builder.append(", ");
		builder.append(font.getSize());
		builder.append(" pixels");

		return builder.toString();
	}

	private StringBuilder getStyleText() {
		StringBuilder builder = new StringBuilder();
		int style = font.getStyle();
		if (style == Font.PLAIN) {
			builder.append("normal");
		} else if (style == Font.BOLD) {
			builder.append("bold");
		} else if (style == Font.ITALIC) {
			builder.append("italic");
		} else if (style == (Font.BOLD + Font.ITALIC)) {
			builder.append("bold italic");
		} else {
			builder.append("unknown style");
		}
		return builder;
	}

}

The MarqueeFont class holds the marquee information for a font. The getTextPixels method takes a string, draws the string on a BufferedImage, gets the pixels from the BufferedImage, and creates a 2 dimensional boolean array from the pixels.

Pretty clever, eh. It took me 3 tries and 4 months to come up with this obvious method. You should have seen the kludges that I had before. No, I take that back. Be glad you didn’t see the kludges that I had before. Even professional programmers (especially professional programmers?) don’t always get it right the first time.

The pixels are retrieved from the BufferedImage in column, row order. The createTextPixels method flips the pixels so that the 2 dimensional boolean array is in row, column order. We do this so we can draw the pixels column by column on the marquee.

The methods in this class were complicated to debug. I printed lots of values while testing the class. Putting print statements inside a DEBUG boolean is one way to find and solve problems. It’s easier than running the code through the Java debugger some times.

Next, let’s look at the class that holds all of the marquee fonts, MarqueeFontFactory.

package com.ggl.marquee.model;

import java.awt.Font;

import javax.swing.DefaultListModel;

public class MarqueeFontFactory {

	private DefaultListModel<MarqueeFont> fontList;

	private MarqueeFont defaultFont;

	public MarqueeFontFactory() {
		this.fontList = new DefaultListModel<MarqueeFont>();
		addElements();
	}

	private void addElements() {
		this.defaultFont = new MarqueeFont(new Font("Arial", Font.BOLD, 16));
		fontList.addElement(defaultFont);
		fontList.addElement(new MarqueeFont(new Font("Cambria", Font.BOLD, 16)));
		fontList.addElement(new MarqueeFont(new Font("Courier New", Font.BOLD,
				16)));
		fontList.addElement(new MarqueeFont(new Font("Georgia", Font.BOLD, 16)));
		fontList.addElement(new MarqueeFont(new Font("Lucida Calligraphy",
				Font.BOLD, 16)));
		fontList.addElement(new MarqueeFont(new Font("Times New Roman",
				Font.BOLD, 16)));
		fontList.addElement(new MarqueeFont(new Font("Verdana", Font.BOLD, 16)));
	}

	public DefaultListModel<MarqueeFont> getFontList() {
		return fontList;
	}

	public void setDefaultFont(MarqueeFont defaultFont) {
		this.defaultFont = defaultFont;
	}

	public MarqueeFont getDefaultFont() {
		return defaultFont;
	}

	public int getCharacterHeight() {
		int maxHeight = 0;
		for (int i = 0; i < fontList.getSize(); i++) {
			MarqueeFont font = fontList.get(i);
			int height = font.getFontHeight();
			maxHeight = Math.max(height, maxHeight);
		}
		return maxHeight;
	}
}

The MarqueeFontFactory class is a List that holds the fonts and the font we chose. We get the largest pixel height of all the fonts. This allows us to add and remove fonts from the addElements method without having to change any constants. The largest font of the ones currently in the list is 22 pixels high.

The default font is also the current font. You can switch fonts at any time, but you have to resubmit the text to have the new font take effect.

Now that we’ve seen all of the model classes, let’s look at the view classes. The first class that we’ll look at is the MarqueeFrame class.

package com.ggl.marquee.view;

import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JPanel;

import com.ggl.marquee.model.MarqueeModel;
import com.ggl.marquee.runnable.DisplayAllPixelsRunnable;

public class MarqueeFrame {

	private ControlPanel controlPanel;

	private JFrame frame;

	private MarqueeModel model;

	private MarqueePanel marqueePanel;

	public MarqueeFrame(MarqueeModel model) {
		this.model = model;
		model.setFrame(this);
		createPartControl();
	}

	private void createPartControl() {
		frame = new JFrame();
		// frame.setIconImage(getFrameImage());
		frame.setTitle("Marquee");
		frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
		frame.addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent event) {
				exitProcedure();
			}
		});

		JPanel mainPanel = new JPanel();
		mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.PAGE_AXIS));

		marqueePanel = new MarqueePanel(model);
		mainPanel.add(marqueePanel);
		controlPanel = new ControlPanel(this, model);
		mainPanel.add(controlPanel.getPanel());

		frame.add(mainPanel);
		frame.pack();
		frame.setLocationByPlatform(true);
		frame.getRootPane().setDefaultButton(controlPanel.getSubmitButton());
		frame.setVisible(true);

		new Thread(new DisplayAllPixelsRunnable(this, model)).start();
	}

	private void exitProcedure() {
		frame.dispose();
		System.exit(0);
	}

	public void repaintMarqueePanel() {
		marqueePanel.repaint();
	}

}

In the MarqueFrame class, you’ll notice that we use a JFrame. We do not extend a JFrame. The only time you extend a Swing class (or any other Java class) is when you want to override one of the class methods.

We set the instance of the MarqueeFrame in the MarqueeModel. We do this so we can pass the MarqueeFrame instance to the DisplayTextPixelsRunnable class, so the DisplayTextPixelsRunnable class can repaint the marquee. We create the MarqueeModel before we create the MarqueeFrame, so we have to pass the MarqueeFrame instance to the MarqueeModel with a set method.

The createPartControl method is pretty much boilerplate from project to project. The only difference is the JPanels that make up the project. In this project, we have a marquee panel and a control panel for the font selection, text field, and submit button. We put both panels in a main JPanel, and put the main JPanel in the JFrame. You will save yourself a lot of heartache if you put your panels in a main panel.

The DisplayAllPixelsRunnable class displays all of the pixels for 3 seconds. When you turn a real marquee on, all of the pixels display so that the operator can replace any burnt out bulbs. We simulate that for a bit of realism, and to show how multiple Runnables can be used in a Swing GUI.

The repaintMarqueePanel is a convenience method. That way, the controller doesn’t have to know about the view details. The controller calls the convenience method, which repaints the marquee panel.

Next, let’s take a look at the MarqueePanel class.

package com.ggl.marquee.view;

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

import javax.swing.JPanel;

import com.ggl.marquee.model.MarqueeModel;

public class MarqueePanel extends JPanel {

	private static final long serialVersionUID = -1677343084333836763L;

	private static final int pixelWidth = 4;
	private static final int gapWidth = 2;
	private static final int totalWidth = pixelWidth + gapWidth;
	private static final int yStart = gapWidth + totalWidth + totalWidth;

	private MarqueeModel model;

	public MarqueePanel(MarqueeModel model) {
		this.model = model;

		int width = model.getMarqueeWidth() * totalWidth + gapWidth;
		int height = model.getMarqueeHeight() * totalWidth + yStart + yStart;
		setPreferredSize(new Dimension(width, height));
	}

	@Override
	protected void paintComponent(Graphics g) {
		super.paintComponent(g);

		g.setColor(Color.BLACK);
		g.fillRect(0, 0, getWidth(), getHeight());

		int x = gapWidth;
		int y = yStart;

		for (int i = 0; i < model.getMarqueeWidth(); i++) {
			for (int j = 0; j < model.getMarqueeHeight(); j++) {
				if (model.getMarqueePixel(i, j)) {
					g.setColor(Color.PINK);
				} else {
					g.setColor(Color.BLACK);
				}

				g.fillRect(x, y, pixelWidth, pixelWidth);
				y += totalWidth;
			}
			y = yStart;
			x += totalWidth;
		}
	}

}

The MarqueePanel class extends JPanel because we want to override the paintComponent method. We set the width and height of the panel in the constructor, based on values passed from the model.

In the paintComponent method, we draw the marquee based on values passed from the model. These values will change each time we redraw the panel.

Next, we’ll look at the ControlPanel class.

package com.ggl.marquee.view;

import java.awt.BorderLayout;

import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;

import com.ggl.marquee.controller.CreateMarqueeActionListener;
import com.ggl.marquee.controller.FontSelectionListener;
import com.ggl.marquee.model.MarqueeFont;
import com.ggl.marquee.model.MarqueeModel;

public class ControlPanel {

	private JButton submitButton;

	private JPanel panel;

	private MarqueeFrame frame;

	private MarqueeModel model;

	public ControlPanel(MarqueeFrame frame, MarqueeModel model) {
		this.frame = frame;
		this.model = model;
		createPartControl();
	}

	private void createPartControl() {
		panel = new JPanel();
		panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));

		JPanel fontPanel = new JPanel();
		fontPanel.setLayout(new BorderLayout());

		JLabel fontLabel = new JLabel("Fonts");
		fontPanel.add(fontLabel, BorderLayout.NORTH);

		JList<MarqueeFont> fontList = new JList<MarqueeFont>(
				model.getDefaultListModel());
		fontList.setSelectedValue(model.getDefaultFont(), true);
		fontList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
		fontList.setVisibleRowCount(3);

		ListSelectionModel listSelectionModel = fontList.getSelectionModel();
		listSelectionModel.addListSelectionListener(new FontSelectionListener(
				model));

		JScrollPane fontScrollPane = new JScrollPane(fontList);

		fontPanel.add(fontScrollPane, BorderLayout.CENTER);

		panel.add(fontPanel);

		JPanel fieldPanel = new JPanel();

		JLabel fieldLabel = new JLabel("Marquee Text: ");
		fieldPanel.add(fieldLabel);

		JTextField field = new JTextField(30);
		fieldPanel.add(field);

		panel.add(fieldPanel);

		JPanel buttonPanel = new JPanel();

		submitButton = new JButton("Submit");
		submitButton.addActionListener(new CreateMarqueeActionListener(frame,
				model, field));
		buttonPanel.add(submitButton);

		panel.add(buttonPanel);
	}

	public JPanel getPanel() {
		return panel;
	}

	public JButton getSubmitButton() {
		return submitButton;
	}

}

The ControlPanel class is a pretty straightforward Swing class. We put each line of components in their own JPanel, and put the JPanels in a main JPanel. Nesting JPanels in this way allows you to build complicated GUIs.

The JList is probably the most interesting component. We pre-select the default font, so the user doesn’t have to select a font if she’s happy with the default font. We limit the selection to a single font, and set the visible row count to 3. This sizes the JList in proportion to the marquee panel.

Now that we’ve seen all of the view classes, let’s look at the controller classes. The first class that we’ll look at is the CreateMarqueeActionListener class.

package com.ggl.marquee.controller;

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

import javax.swing.JTextField;

import com.ggl.marquee.model.MarqueeModel;
import com.ggl.marquee.view.MarqueeFrame;

public class CreateMarqueeActionListener implements ActionListener {

	private JTextField field;

	private MarqueeFrame frame;

	private MarqueeModel model;

	public CreateMarqueeActionListener(MarqueeFrame frame, MarqueeModel model,
			JTextField field) {
		this.frame = frame;
		this.model = model;
		this.field = field;
	}

	@Override
	public void actionPerformed(ActionEvent event) {
		model.stopDtpRunnable();
		model.resetPixels();

		String s = field.getText().trim();
		if (s.equals("")) {
			frame.repaintMarqueePanel();
			return;
		}

		s = " " + s + "    ";
		model.setTextPixels(model.getDefaultFont().getTextPixels(s));
		frame.repaintMarqueePanel();
	}

}

All of the methods dealing with the marquee are in the model classes, so all we have to do in the actionPerformed method is call the methods. We add a blank before the text and a few blanks after the text so the marquee display looks better.

Next, we’ll look at the FontSelectionListener class.

package com.ggl.marquee.controller;

import javax.swing.DefaultListSelectionModel;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

import com.ggl.marquee.model.MarqueeFont;
import com.ggl.marquee.model.MarqueeModel;

public class FontSelectionListener implements ListSelectionListener {

	private MarqueeModel model;

	public FontSelectionListener(MarqueeModel model) {
		this.model = model;
	}

	@Override
	public void valueChanged(ListSelectionEvent event) {
		DefaultListSelectionModel selectionModel = (DefaultListSelectionModel) event
				.getSource();
		if (!event.getValueIsAdjusting()) {
			int index = selectionModel.getMinSelectionIndex();
			if (index >= 0) {
				MarqueeFont font = model.getDefaultListModel().get(index);
				model.setDefaultFont(font);
			}
		}
	}

}

The actionPerformed method sets the selected font to the default font. The action listener for the submit button will take the text and draw it in the newly selected font.

The last two classes are Runnable classes. We run these classes in separate threads, so we can keep the GUI responsive. First, we’ll look at the DisplayAllPixelsRunnable class.

package com.ggl.marquee.runnable;

import javax.swing.SwingUtilities;

import com.ggl.marquee.model.MarqueeModel;
import com.ggl.marquee.view.MarqueeFrame;

public class DisplayAllPixelsRunnable implements Runnable {

	private MarqueeFrame frame;

	private MarqueeModel model;

	public DisplayAllPixelsRunnable(MarqueeFrame frame, MarqueeModel model) {
		this.frame = frame;
		this.model = model;
	}

	@Override
	public void run() {
		model.setAllPixels();
		repaint();

		try {
			Thread.sleep(3000L);
		} catch (InterruptedException e) {

		}

		model.resetPixels();
		repaint();
	}

	private void repaint() {
		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				frame.repaintMarqueePanel();
			}
		});
	}

}

This class displays all of the marquee pixels for 3 seconds. You can type the marquee text in the JTextField while the pixels are displayed if you’re fast enough.

The marquee panel is repainted inside a call to the SwingUtilities invokeLater method. This ensures that the repaint happens on the Event Dispatch thread.

Finally, we’ll look at the DisplayTextPixelsRunnable class.

package com.ggl.marquee.runnable;

import javax.swing.SwingUtilities;

import com.ggl.marquee.model.MarqueeModel;
import com.ggl.marquee.view.MarqueeFrame;

public class DisplayTextPixelsRunnable implements Runnable {

	private static int textPixelPosition;

	private boolean running;

	private MarqueeFrame frame;

	private MarqueeModel model;

	public DisplayTextPixelsRunnable(MarqueeFrame frame, MarqueeModel model) {
		this.frame = frame;
		this.model = model;
		textPixelPosition = 0;
	}

	@Override
	public void run() {
		this.running = true;
		while (running) {
			model.copyTextPixels(textPixelPosition);
			repaint();
			sleep();
			textPixelPosition++;
			textPixelPosition %= model.getTextPixelWidth();
		}
	}

	private void repaint() {
		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				frame.repaintMarqueePanel();
			}
		});
	}

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

		}
	}

	public void stopDisplayTextPixelsRunnable() {
		this.running = false;
	}

}

This class keeps track of the text pixel position and moves the text one pixel to the left every 50 milliseconds. This moves the marquee at a nice pace.

The marquee panel is repainted inside a call to the SwingUtilities invokeLater method. This ensures that the repaint happens on the Event Dispatch thread.

This class is invoked when the text width is greater than the marquee width. Otherwise, the text is displayed in the marquee without any movement.

Thanks for reading this article. I hope it helped you learn more about Java Swing. I especially like how I converted text to pixels for the marquee, even if it took me 3 tries and 4 months to make it elegant.

Post a Comment

Your email is kept private. Required fields are marked *