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 <blank><string><blank>
      {  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 <blank><string><blank>
      {  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;  }

}