// 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; } }