Download this source file
// A Marquee JavaBean, Which displays a scrolling list of messages.
// Note the property names begin with a capital letter in the get/set
// methods. Marquee is a lightweight component, which means it has no
// Graphics of its own, so the font, foreground, and background color
// must be set each time paint is called.
// Marquee uses double-buffering and other animation techniques to
// present smooth scrolling.
// Each message has the Filler string appended, to seperate adjacent messages.
//
// (C) April 2000 by Wayne Pollock, Tampa Florida USA. All Rights Reserved.
import java.awt.*;
import java.beans.*;
import java.io.*;
import java.util.*;
public class Marquee extends Component implements Serializable, Runnable
{
// Properties:
private boolean scrolling; // Simple boolean property.
private int width; // Bound.
private int height; // Bound.
private String fillerChar; // char to put between consecutive messages.
private int fillerWidth; // How many copies of fillerChar in filler.
private int scrollDelay; // Delay in milliseconds between repaints. Constrained.
private String [] list; // Indexed property. (Dummy just to show how.)
// Non-exposed, non-serialized properties:
private transient Vector messages;
private transient String filler; // String put between consecutive messages.
private transient int leftEdge; // Offset from window's right edge to left edge
// of first displayed message. (Window inset ignored.)
private transient int firstMsgToDraw;
private transient Thread ticker;
private transient PropertyChangeSupport pcs; // Java 2 Component supports this directly.
private transient VetoableChangeSupport vcs;
private transient Image offScreen;
public Marquee ()
{
messages = new Vector();
messages.addElement( "This Space for Rent" );
scrolling = true;
width = 250;
height = 100;
fillerChar = ".";
fillerWidth = 10;
filler = " .......... "; // '\u0095' is a centered bullet.
scrollDelay = 35;
leftEdge = 0;
firstMsgToDraw = 0;
pcs = new PropertyChangeSupport( this );
vcs = new VetoableChangeSupport( this );
Font theFont = new Font( "Serif", Font.BOLD, 36 );
setFont( theFont );
list = new String[3];
list[0] = "First string";
list[1] = "Second string";
list[2] = "Third string";
ticker = new Thread( this );
ticker.start();
}
public void run ()
{ for ( ;; )
{ try
{ Thread.sleep( scrollDelay );
synchronized( this )
{ while ( ! isScrolling() ) wait(); // Suspend thread.
}
}
catch ( InterruptedException ie ) {}
repaint();
}
}
public void update ( Graphics g ) { paint( g ); }
public void paint ( Graphics g )
{
Color bgColor = getBackground(), fgColor = getForeground();
Font theFont = getFont();
FontMetrics fm = g.getFontMetrics( theFont );
if ( messages.size() == 0 )
return;
int winWidth = getSize().width;
int winHeight = getSize().height;
if ( offScreen == null )
offScreen = createImage( winWidth, winHeight );
Graphics og = offScreen.getGraphics();
og.setFont( theFont );
og.setColor( bgColor );
og.fillRect( 0, 0, winWidth, winHeight ); // clear offScreen Image.
og.setColor( fgColor );
++leftEdge;
String msg = (String) messages.elementAt( firstMsgToDraw ) + filler;
int msgWidth = fm.stringWidth( msg );
if ( winWidth - leftEdge + msgWidth < 0 ) // Current msg entirely off-screen.
{
firstMsgToDraw = ( firstMsgToDraw + 1 ) % messages.size();
leftEdge = winWidth; // Left edge of next msg = right edge of old msg,
// which also is the left edge of the window.
msg = (String) messages.elementAt( firstMsgToDraw ) + filler;
msgWidth = fm.stringWidth( msg );
}
// Draw the first message:
int x = winWidth - leftEdge;
int y = ( winHeight + fm.getAscent() ) / 2;
og.drawString( msg, x, y );
// Draw the remaining visible messages:
int msgNum = firstMsgToDraw;
while ( x + msgWidth < winWidth )
{
x += msgWidth;
msgNum = ( msgNum + 1 ) % messages.size();
msg = (String) messages.elementAt( msgNum ) + filler;
msgWidth = fm.stringWidth( msg );
og.drawString( msg, x, y );
}
g.setClip( 0, y - fm.getMaxAscent(), winWidth, fm.getMaxAscent() + fm.getMaxDescent() );
g.drawImage( offScreen, 0, 0, this );
og.dispose();
}
public Dimension getPreferredSize () { return new Dimension( width, height ); }
public Dimension getMinimumSize () { return getPreferredSize(); }
public void invalidate ()
{
super.invalidate();
offScreen = null;
}
//--------------------------------------------------------------------------
// Getters, Setters, and other public methods:
//--------------------------------------------------------------------------
// Beans can have individual property change listeners, but here
// we use a single property change listener (and vetoable change
// listener, for constrained properties) for the whole Bean:
public void addPropertyChangeListener ( PropertyChangeListener listener )
{ pcs.addPropertyChangeListener( listener ); }
public void removePropertyChangeListener ( PropertyChangeListener listener )
{ pcs.removePropertyChangeListener( listener ); }
public void addVetoableChangeListener ( VetoableChangeListener listener )
{ vcs.addVetoableChangeListener( listener ); }
public void removeVetoableChangeListener ( VetoableChangeListener listener )
{ vcs.removeVetoableChangeListener( listener ); }
// Bound properties fire an event after changing a property. In this
// case the container can resize this component. Note non-atomic
// setters (and calls to wait and notify) must be synchronized.
public int getWidth () { return width; }
public synchronized void setWidth ( int w )
{ int oldValue = width;
width = (w < 0) ? 0 : w;
invalidate();
pcs.firePropertyChange( "width", new Integer( oldValue ),
new Integer( width ) );
}
// Another bound property:
public int getHeight () { return height; }
public synchronized void setHeight ( int h )
{ int oldValue = height;
height = (h < 0) ? 0 : h;
invalidate();
pcs.firePropertyChange( "height", new Integer( oldValue ),
new Integer( height ) );
}
// Simple (boolean) property:
public boolean isScrolling () { return scrolling; }
public synchronized void setScrolling ( boolean val )
{ scrolling = val;
if ( scrolling ) // Resume thread.
notify();
}
// Simple property:
public int getFillerWidth () { return fillerWidth; }
public synchronized void setFillerWidth ( int val )
{ if ( val < 0 ) val = 0; // Could have made this constrained,
// but didn't.
if ( val == fillerWidth ) return;
fillerWidth = val;
if ( fillerWidth == 0 )
filler = "";
else // filler is
{ StringBuffer s = new StringBuffer( " " );
for ( int i = 0; i < fillerWidth; ++i )
s.append( fillerChar );
s.append( " " );
filler = s.toString();
}
}
// simple property:
public String getFillerChar () { return fillerChar; }
public synchronized void setFillerChar ( String val )
{ if ( fillerChar == val ) return;
fillerChar = val;
if ( fillerWidth == 0 )
filler = "";
else // filler is
{ StringBuffer s = new StringBuffer( " " );
for ( int i = 0; i < fillerWidth; ++i )
s.append( fillerChar );
s.append( " " );
filler = s.toString();
}
}
// Constrained Property. Any Listener (including this) can veto:
public int getScrollDelay () { return scrollDelay; }
public synchronized void setScrollDelay ( int val )
throws PropertyVetoException
{ if ( val < 0 ) // I will veto this even if noone else does.
{ PropertyChangeEvent pcEvt = new PropertyChangeEvent( this,
"scrollDelay", new Integer( scrollDelay ),
new Integer( val ) );
throw new PropertyVetoException( "scrollDelay must be positive",
pcEvt );
}
vcs.fireVetoableChange( "scrollDelay", scrollDelay, val );
// If we get to here then noone else vetoed the change, so do it:
scrollDelay = val;
}
// Indexed property: (Note: this isn't really needed in the design,
// but I wanted to show how to do it.)
public String [] getList () { return list; }
public void setList ( String [] val ) { list = val; }
public String getListItem ( int index ) { return list[index]; }
public void setListItem ( int index, String val ) { list[index] = val; }
}