Sun Java Solaris Communities My SDN Account Join SDN
 
Article

Developing the Java 2D Art Applet Using Forte for Java Community Edition

 
 

Articles Index

Art Applet Using Forte for Java Community Edition

This article takes a detailed look at Java 2DTM Cosmic Art Applet, Java 2DTM/Swing. Cosmic Art Applet was built with ForteTM for Java Community Edition. Forte offers a wide range of capabilities that make coding in the Java programming language faster and easier. At the end of the article, you can run the applet, and download the source code. But first, let's take a look at what the Cosmic Art Applet can do.

Image1.gif

The Applet

Java 2D Cosmic Art Applet produces spiraling and unfolding patterns. The Cosmic Art applet uses an algorithm based on ellipses that rotate about their center point and scale according to a positive floating point scaling factor (Scale Fac, on the GUI input). If the scaling factor is 1.0, there is no change in the size of the ellipses. If the scaling factor is less than one, the ellipses decrease in size, and if it's greater than one, the ellipses increase in size. The upper left hand corner of the JPanel display is the 0,0 point, with X values increasing to the right to 530, and Y values increasing downward to 320.

Other input fields control the X and Y placement of the ellipse center and the size (Width and Height) of a rectangle that describes the ellipse. In addition, a starting point in degrees (Start Deg) is specified, and the degree point at which rotation should stop (To Deg). This value can exceed 360 degrees which produces unfolding spiral patterns, such as the one generated by the default GUI input values. Start Deg can be negative or positive, and To Deg can be negative or positive. The degree increment field (Deg Inc), is required to be a positive integer, and describes the number of degrees the ellipse will be rotated each increment in making the trip between the Start Deg and To Deg values.

The Draw button kicks off the Java 2D scale, translate, rotate and draw operations. Draw can be invoked successively to overlay multiple patterns. The Erase button clears the draw panel, which can be used to terminate a Draw operation.

Below the input fields, there is a ComboBox to change pen colors. Try changing colors on-the-fly during a draw operation. It produces some interesting effects.

Before we jump into the development process and coding particulars, we need some background on Forte.

About Forte CE

Sun's Forte for Java, Community Edition is available for download at no charge. It is cross-platform compatible, enabling Java 2 development on Solaris, Linux, and Windows platforms. Forte CE is written entirely in the Java programming language and features extensive support of open standards.

Key features of Forte CE:

  • Text Editor--standard editor with advanced attributes such as syntax coloring for many file types (IDL, HTML, XML and Java), custom indentation, dynamic source code completion, and incremental search.
  • Object Oriented Architecture--a complete set of open APIs allowing access to the core components. Using these APIs, the environment can be extended with plug-in modules developed by Sun or individual software developers (ISVs).
  • Form Editor--a visual GUI development tool that enables developers to quickly build Java Foundation Classes (JFC/Swing), or AWT based user interfaces. It handles JavaBeans components, generates easy to read code, and features advanced support for layout managers like GridBagLayout.
  • Multi-threaded Debugger--a fully integrated debugger offering breakpoints, watches, and presentation of threads as separate tabs in the window for quick access.
  • Object Browser--allows for examination of packages, objects and object members.

Forte for Java, CE is part of a larger Forte family. Other products are scheduled for release later this year, including Forte for Java, Internet Edition and Forte for Java, Enterprise Edition.

  • Internet Edition--for professional developers creating Web-enabled applications.
  • Enterprise Edition--for corporate developers and independent software vendors (ISVs) building and deploying distributed applications on multiple Web and application servers.

Sun is gearing up to release Forte CE to the open source community under the Mozilla Public License model. The Mozilla model has been well-accepted by both open source groups and commercial software vendors.

The modular "plug-in" nature of the Forte CE software makes it the perfect candidate for open source. The goal is to dramatically increase the level of innovation around development tools.

This move marks the first step in a new open tools framework initiative, which will support developers, ISVs and application providers. This community will collaborate to produce a set of complementary and interoperable Java components.

Complete set of Forte CE Open Source FAQs

Getting Started With Forte CE

First, download the software

After you've installed and started up, create a new project, which gives a name to the current IDE state, which is stored for compilation, execution and debugging.

To create a project, choose New Project from the Projects menu.

A window appears for the new project name entry. I chose Ellipses--Second Generation.

The system asks if you would like to start with a new file system or keep the old one--reply New. This choice provides a clean slate on which to construct your applet or application.

You will then be prompted whether to save the current project. If you click Yes, any changes you have made to your current project will be saved before it is closed and a new project is opened.

The file chooser will then appear prompting you to pick a directory to mount. The packages and files in any directories you mount will then be available for inclusion in your project. In our case, we chose Cancel because we will build the applet from the applet template.

Using the Template

Select the Project tab in the Explorer window, then right-click on Project node (in my case Project Ellipses--Second Generation). Clicking on Add New, then clicking on the Classes node, brings up the window shown below.

Image2.gif

I select Swing Forms, then JApplet, then click on the Next button. I type in the name ElpGen2, and click on Finish.

A ElpGen2.java file is automatically created with all the skeleton syntax necessary to support a JApplet. Also, a visual Form Editor window is created to allow Swing and AWT components to be selected, dropped, and easily manipulated. The screen shot below illustrates these elements.

Image3.gif

The Form Editor window is located at lower left of the above screen. Let's expand the form window, and prepare to build some of the components required for the applet.

To allow the Form Editor toolbar to be completely displayed, I right click on any free space within the toolbar, which brings up a checkbox list of toolbars. I deselect Edit and Debug, then pull all the toolbars to the left using their handles, depicted by raised vertical grooves. The result looks like the window shown below with the checkbox menu visible.

Image4.gif

Now we're ready to click on the Absolute Layout icon from the Form Editor toolbar. The icons all have ToolTips, so just hover the mouse to check them out.

Absolute Layout is not part of the standard Java layout sets. It is a design aid provided with the IDE, that allows components to be easily resized and moved by dragging. This layout is particularly useful for prototyping. Property settings are not required, and there are no formal limitations within the layout. But it is not recommended for production applications or applets. The fixed location of components in the form does not change, even when the environment changes. This can lead to significant distortions in appearance on different platforms or under changing look-and-feel environments. The solution is to design with Absolute Layout, then when the design is complete, switch to GridBagLayout, which is highly flexible and portable. Let's get some components up, then we'll covert to GridBagLayout.

Using the Component Palette is the easiest way to add new components. The Component Pallet is a toolbar on the Main Window, which holds commonly used visual components that you can add to the Form Editor window. Components are added by clicking on the component in the palette, then clicking on the desired location in the Form Editor window.

To build a display area for 2D graphics, I click on JPanel, then click inside the Form Editor window at the upper left hand corner. The IDE allows me to drag the lower right hand corner of the panel, and click to anchor it. The result looks like the window shown below. Notice the JPanel ToolTip below the palette.

Back to Top

Image5.gif

The results of this operation are immediately reflected in the ElpGen2.java edit window. The code for the JPanel has been generated, and is available for compilation and execution.

Let's take a look at the ComboBox template up on the screen, and some JTextFields, for 2D data entry. The JTextFields are all the same size, so we can copy the first one, and replicate it across the rows and columns. This process allows complex GUI prototypes to be created in minutes.

Once the basic components have been established in the Form Editor window, the Component Inspector is used to fine tune things. First, the text labels need to be changed. After selecting Component Inspector, from the top level View menu, the text field of the properties list is changed by first selecting jButton1, then clicking on the text button of jButton1's properties list. It opens the input field for data entry. I enter "Draw", as the new button annotation, then click once again on the button marked text to end the transaction. More information about this field can be obtained by clicking on the ... area to the right of the data entry field.

Refer to the following screen shot.

Image6.gif

The rest of the text labels are changed in similar fashion by clicking on the component in the Form Editor window, then using the Component Inspector to change the text property.

Changing to GridBagLayout

Once the GUI is finalized, it's time to switch to the GridBagLayout manager. This is done in two mouse clicks. First click the Layout tab in the Component Palette, then move the mouse until the GridBagLayout tool tip appears. Click GridBagLayout, then click the mouse inside some clear space in the Form Editor window. The layout will automatically be changed to GridBagLayout, and code will be generated in the ElpGen2.java edit window to reflect the change to GridBagLayout. This transition, however, may require some remedial nips-and-tucks, due to the grid characteristics of the GridBagLayout manager.

A Few Words About GridBagLayout

GridBagLayout is the most flexible and complex layout manager. Components are placed in a grid of rows and columns, allowing specified components to span multiple rows and/or columns. The Rows are not necessarily the same height, and the columns are not necessarily the same width. Essentially GridBagLayout places components in cells then uses the component's preferred sizes to determine how big the cells should be.

The first step in using the GridBagLayout is determining the appearance of the GUI. If done manually, it is simply a matter is sitting down with pen and paper and determining a row and column scheme that defines the components of your GUI. The upper left hand corner of the GUI is the 0 row and 0 column. With Forte CE, this is taken care of automatically, but the assigned attributes may need to be modified using the component inspector. So here is some background to educate that process.

The following is typical of Forte generated GridBagLayout code.

getContentPane().setLayout(
      new java.awt.GridBagLayout ());
jButton1.setText("Draw");
gridBagConstraints1=
       new java.awt.GridBagConstraints ();
gridBagConstraints1.gridx = 1;
gridBagConstraints1.gridy = 0;
gridBagConstraints1.gridwidth = 9;
gridBagConstraints1.fill=
          java.awt.GridBagConstraints.BOTH;
gridBagConstraints1.insets = 
   new java.awt.Insets (30, 10, 0, 0);
getContentPane(
     ).add(jButton1, gridBagConstraints1);


GridBagConstraints--Instance Variables
  • gridx, gridy--grid column or row in which upper left of the component will be placed (0 based)
  • gridwidth, gridheight--number of grid columns or rows the component occupies. The default value is 1. Use GridBagConstraints.REMAINDER to specify that the component be the last one in its row (for gridwidth) or column (for gridheight). Use GridBagConstraints.RELATIVE to specify that the component be the next to last one in its row (for gridwidth) or column (for gridheight).
  • weightx, weighty--portion of extra space to allocate horizontally or vertically. Components can become wider or taller when extra space is available. Values 0.0 to 1.0. (Default 0.0)
  • fill--used to determine whether and how to resize the component when display area is larger than the component's requested size. Constant values are NONE (the default), HORIZONTAL, VERTICAL, and BOTH. HORIZONTAL makes the component wide enough to fill display area, but no vertical change. VERTICAL makes component tall enough to fill display area with no horizontal change.
  • ipadx, ipady--specifies the padding, or extra space around an individual component. These values specify how many pixels to add to the minimum size of the component. The width of the component will be at least its minimum width plus ipadx*2 pixels, since the padding applies to both sides of the component. Similarly, the height of the component will be at least its minimum height plus ipady*2 pixels. The default value is zero.
  • insets--specifies the minimum amount of space between the component and the edges of its display area. The value is represented as an insets object. By default, each component has no inset padding. The insets constructor is defined: insets (int top, int left, int bottom, int right). Values are in pixels.
  • anchor--used to determine where to place a component, when the component is smaller than its display area. Valid GridBagConstraints constants are CENTER (the default), NORTH, NORTHEAST, EAST, SOUTHEAST, SOUTH, SOUTHWEST, WEST, and NORTHWEST.

As I mentioned earlier, the GridBagLayout GUI may need some minor adjustment. Forte offers the GridBag customizer to simplify this process. To access the customizer, right click on the GridBagLayout node in the Component Inspector, and choose Customize Layout...from the contextual menu, or choose the Customize button on the property sheet pane.

I didn't use the customizer, because the changes I needed were so easily accomplished using the Component Inspector. The second column of my JTextField data entry fields were wider than the first column after converting from absolute layout to gridbag. So I simply clicked on one of the first column fields, then checked its Grid Width, from the Layout tab. Then I checked the second column Grid Width values. The first column values were 4, and the second column values were 6. I changed all the 6s to 4s, and the problem was solved.

Code Completion

One of the greatest things Forte CE offers (in my opinion) is code completion. If you type a few characters of a variable name, then hit CNTL+SPACE, the system presents possibilities for completing the variable name based on the pool of known variables. If you type an object name and a period, then pause or hit CNTL+SPACE, the system presents you with a list of possible methods and arguments. Double clicking on one of these entries completes the code by inserting the method name. Having all applicable methods at your fingertips is an unbelievable time saver. In the editor, I type the object name graphics. Then I hit CNTL+SPACE. A window like the one shown below appears showing me the possible method calls on this object. Let's take a look at this capability in action.

Back to Top

Image7.gif

The Applet Code--Screening Input Data

Now let's get into the code. When the Draw button is clicked, the jButton1ActionPerformed method is fired. The first long block of code is related to verifying that no invalid data slips through that could crash the applet or produce unexpected behavior. Here is an example that logic.

 private void 
 jButton1ActionPerformed 
    (java.awt.event.ActionEvent evt) {
    // Draw Button Code
    // Input Field Data Validation
    inputError = false;
    try {
      xCent = 
         Integer.parseInt(jTextField1.getText());
      if ( xCent  < -1000 | xCent > 1000 ) {
        /* x outside range */
        jTextField1.setText("Too Big");
        inputError = true;
      }
    }
    catch ( NumberFormatException nfe ) {
      inputError = true;
      jTextField1.setText("Integer");
    }

The inputError Boolean is initially false. If it becomes true through the course of screening the input values, no drawing takes place, and the user must make input value changes based on the instructive feedback appearing in the input fields.

In the example above, the jTextField1.getText() method call extracts text from the input field. The result is used as an argument to the method Integer.parseInt. This method converts the text to an integer. If this is successful, the value is passed to the integer variable xCent. Notice that the code is in a try block, so if the parseInt method call fails, the catch block picks up the NumberFormatException. The inputError Boolean is set true and the text "Integer" is entered into the input field by the execution of the method jTextField1.setText("Integer");.

If the parseInt method call is successful, the data is further screened by the logic below.

if ( xCent  < -1000 | xCent > 1000 ) {

If the input value satisfies this logic test, then there is a problem. The data is out of range. The inputError variable is set true, and an informative message is entered into the input field, in the manner just described.

The rest of the input data screening code is very similar to this example.

The 2D Code

Note: this code has been extracted from surrounding logic.

   g2d.setRenderingHint(
      RenderingHints.KEY_ANTIALIASING,
      RenderingHints.VALUE_ANTIALIAS_ON);
   g2d.setColor( colorObj[colorInt] );
   afn = new AffineTransform();
   /* calculate upper left corner */
   /* of rectange defining the ellipse */
   x = xCent - w / 2;
   y = yCent - h / 2;
   elp = new Ellipse2D.Double( x, y, w, h );
   /* calculate translation constants to keep */
   /* ellipses centered                       */
        xTransCon = 
          ( xCent*scaleFact - xCent )/scaleFact;
   yTransCon = 
          ( yCent*scaleFact - yCent )/scaleFact;
   /* scaleFactacc is used to turn off graphics*/
   /* if too far outside viewable area         */
   scaleFactacc = scaleFact ;
   /* prepare first ellipse in degreeStart pos */
   afn.rotate( -Math.toRadians(degreeStart), 
      xCent , yCent );
   elpTrans = afn.createTransformedShape( elp );
   g2d.draw( elpTrans );

The first method fired against the Graphics2D object g2d is setRenderingHint, which establishes, through the two constants, that anti-aliasing will be turned on. This smoothes out the ellipses, getting rid of stair-stepping effects. It is fairly expensive, in terms of processing, but worth it.

The next method is setColor, which establishes the drawing color based on selections made in the color ComboBox. The setColor argument, (colorObj[colorInt]), is an array of Color.black type predefined Color instances. The integer colorInt is defined in the jComboBox1ActionPerformed method using the syntax colorInt = jComboBox1.getSelectedIndex();. This syntax is executed when the user makes a ComboBox selection.

Now we come to the powerful AffineTransform. This is the engine of creation for the ellipse effects. It allows the linear mapping from 2D coordinates to other 2D coordinates in a manner that preserves the straightness and parallelness of the lines. Affine transforms can be constructed using sequences of translations, scales, flips, rotations and shears. In our case, we will only utilize rotation, scaling, and translation.

The AffineTransform object afn is established by the line:

afn = new AffineTransform();.

The user inputs an X and Y value for the ellipse center. These values are stored as xCent and yCent. But ellipses are defined, in this setting, by the rectangle that contains the ellipse. The upper left hand corner of that rectangle, and its width and height complete the description. So the calculation:

    x = xCent - w / 2;
    y = yCent - h / 2;


establishes the upper left hand corner, by converting the user's ellipse center input values. So now we have the upper left hand corner of the rectangle, and the width and height. That's all we need, let's define an Ellipse2D object:

   elp = new Ellipse2D.Double( x, y, w, h );


Remember that the 0,0 point is the upper left-hand corner of the JPanel.

The code below defines translation constants that will be used later to reposition the user space after each scaling transformation, so that the ellipses remain centered.

    xTransCon = 
      ( xCent*scaleFact - xCent )/scaleFact;
    yTransCon = 
      ( yCent*scaleFact - yCent )/scaleFact;


The following syntax completes the transformation in this section:

    afn.rotate( -Math.toRadians(degreeStart ), 
       xCent , yCent );
    elpTrans = 
            afn.createTransformedShape( elp );
    g2d.draw( elpTrans );

Here, a rotation is fired against the AffineTransform object afn. Rotations are handled in radians, and my inputs are all in degrees, so a conversion takes place. Also, the rotation is done around the ellipse center point. A negative rotation value results in the image rotating in a positive direction, since the direction refers to rotation of the frame of reference, and not the object. In other words, the user space rotates, not the object.

A Shape object is created by the following syntax:

   elpTrans = 
          afn.createTransformedShape( elp );

This is done to preserve the stroke width during later scaling operations. Otherwise, as the ellipse is scaled up or down, the drawing would take on huge bold strokes, or tiny strokes if the scale factor was less than one.

Then the Graphics2D object's draw method is fired with elpTrans as its argument. This draws the ellipse in a buffer for later output.

g2d.draw( elpTrans );

Scaling and Translating

The previous code illustrated drawing the first ellipse, which is not scaled. It is rotated into initial position, then drawn. The following code represents the more general case, where the ellipses are rotated, translated and scaled.

Note: this code has been extracted from surrounding logic, which will be explored later.

    afn.scale( scaleFact, scaleFact );
    afn.translate( -xTransCon , -yTransCon );
    afn.rotate(  
       Math.toRadians(
            degreeIncrement)*direction, 
                           xCent, yCent );   
    elpTrans = 
        afn.createTransformedShape( elp );
    g2d.draw( elpTrans );
    if ( scaleFactacc > 300.0 ||  
       scaleFactacc < .001 ) {
       drawing = false;
       stopAnimation();
    }


Composing transforms refers to stacking transforms to be applied. The last transform fired against the AffineTransformobject is the first to be applied--a LIFO stack. In general, the order of these transformations is important to the outcome. Usually, the effect of applying transform T1 and then transform T2, is not the same as applying T2 first, then T1.

In our case the key operations that are sensitive to this ordering are translate, then scale. The translate transform adjusts the user space so that when the scaling takes place, the ellipse remains centered. Since rotation is done about the ellipse center point (xCent, yCent), the rotation can be done before or after this translate/scale pair.

The translation constant (defined earlier) is presented below again. This value, as an argument to the translate method, moves the user space so that the ellipse will appear centered after each scaling operation.

  xTransCon = 
    ( xCent*scaleFact - xCent )/scaleFact;
  yTransCon = 
    ( yCent*scaleFact - yCent )/scaleFact;
  


As before, a shape object is created by the following syntax:

elpTrans = 
    afn.createTransformedShape( elp );

And the ellipse is drawn into a buffer by firing the draw method.
g2d.draw( elpTrans );

The following code turns off the drawing functionality if the scaling calculations produce results that are wildly outside the viewable area. The scaleFactacc variable is an accumulator indicating passes through the scaling operation.

if ( scaleFactacc > 300.0 ||  
         scaleFactacc < .001 ) {
         drawing = false;
         stopAnimation();
}


The Need for a Timer

Every program that performs animation by painting at regular intervals needs an animation loop. Generally, this loop should be in its own thread. It should never be in the paintComponent method, since that would take over the event-dispatching thread, which is in charge of painting and event handling. (The paintComponent method is discussed later.)

Notes on Timers

Generally, a timer supports executing a task periodically, or just once at some future time. Timers are valuable tools because they simplify the job of scheduling activities.

Swing Timer

The javax.swing.Timer class allows for the scheduling of an arbitrary number of periodic or delayed actions using one thread. This Timer class is used by Swing components for things like blinking the text cursor and for timing tool-tip appearances and disappearances. The Swing timer implementation fires an action event whenever the specified interval or delay time passes.

The important difference between using the Swing timer class and creating your own thread, is that the Swing timer class uses just one thread for all timers. It deals with scheduling actions and putting its thread to sleep internally.

Utility Timer and TimerTask Classes

Timers are not the exclusive domain of GUI applications. In version 1.3 of the Java platform, support for timers was added to the java.util package. Like the Swing Timer class, the main java.util class is also called Timer. The java.util.Timer is generally referred to as the "utility Timer class," to differentiate from the Swing Timer class. Instead of scheduling Actions, the utility Timer class schedules instances of a class called TimerTask. The utility timer facility has a different division of labor from than javax.swing.Timer. For example, control of the utility timer is invoked using methods of TimerTask rather than Timer.

Both timer facilities have the same basic support for delayed and periodic execution. But the java.util.Timer provides more flexibility over scheduling timers. For example, the java.util.Timer allows the specification of running at a fixed rate, or repeatedly after a fixed delay. The latter scheme (the java.util.Timer model) means that a timer's frequency can drift because of extra delays introduced by garbage collection or long-running timer tasks. This possible drift is acceptable for animations and many other applications. But it is not appropriate for driving a clock in situations where multiple timers must be kept in lockstep. The most important difference between javax.swing.Timer and java.util.Timer is that the latter does not run its tasks on the event-dispatching thread.

Choosing a Timer Class

As we have seen, the javax.swing.Timer and java.util.Timer facilities provide roughly the same functionality. Generally speaking, the java.util.Timer is recommended for self contained applications, usually with no GUI related functionality. The javax.swing.Timer class is recommended for building new Swing components, and for applications requiring a relatively small number of timers (small is defined here as approximately 12 or less).

The new utility timer classes provide control over how many timer threads are created. Each java.util.Timer object creates one thread. If an application requires a large number of timers, creating several java.util.Timer objects is recommended. Each of these objects can be used to schedule related TimerTasks.

The javax.swing.Timer class uses a single private thread to schedule timers. A typical GUI component or application uses at most a handful of timers to control various animation or other effects. The single thread offered by the javax.swing.Timer is more than sufficient.

Implementing javax.swing.Timer

For this applet, I've chosen the javax.swing.Timer. It is instantiated and set-up with the following code in the applet's init() method, which is fired when the applet is loaded or reloaded.

public void init() {
   /* default color is black */
   colorInt = 0;
   drawing = false;
   timer = new Timer( 30, this );
   timer.setInitialDelay( 0 );
   timer.setCoalesce( true );
}

Timer definition:
private Timer timer;

The Timer is instantiated with a first argument designed to fire an action every 30 milliseconds. The second argument, this, refers to the current object (the applet), which implements ActionListener. The Timer fires the actionPerformed method.

Firing the setInitialDelay method sets the initial delay value. In this case, the value is set to 0, because we want the timer to be available immediately.

The setCoalesce method determines whether or not the Timer coalesces multiple pending ActionEvent firings. A busy application may not be able to keep up with the message generation for a Timer, causing multiple actionPerformed() message sends to be queued. When processed, the application sends these messages one after the other, causing the Timer listeners to receive a sequence of actionPerformed() messages with no delay between them. Coalescing avoids this situation by reducing multiple pending messages to a single message send. Timers coalesce their message sends by default. The javax.swing.Timer environment is now set up and ready to go, so let's see how it is used in the applet.

The code below illustrates the use of the timer object and the actionPerformed method.

public void 
   actionPerformed
     (final java.awt.event.ActionEvent p1) {
   jPanel1.updateBuffer();
   jPanel1.repaint();
  }

  public void start() {

    startAnimation();
  }

  public void stop() {

    stopAnimation();
  }

  public synchronized void startAnimation() {

    if ( !drawing ) {
      /* do nothing, animation not enabled */
    }
    else if ( !timer.isRunning() ) {
      timer.start();
    }

  }

  public synchronized void stopAnimation() {
    if( timer.isRunning() ) {
      timer.stop();
    }
  }

The start method is fired when the applet is loaded, or the user revisits a page that contains the applet. During the applet loading phase, the Timer is not started, because the Boolean variable drawing is not true. If the applet window is minimized during a drawing operation, the stop method is called, which calls stopAnimation. Since the Timer is running, its stop method is called. This shuts down the drawing operation. When the applet window is maximized, the applet's start method is fired, which calls startAnimation. This time, since the drawing Boolean is true, the Timer is restarted by firing its start method. When the line timer.start() is executed, the drawing operation resumes.

The synchronized keyword is used on both the startAnimation and stopAnimation methods to ensure that tests done on variables within them are performed in the sequence they were called. No thread related internal processing details can affect the intended outcome of the code. Synchronizing these methods for use with the javax.swing.Timer is good practice.

The Timer drives the actionPerformed method, which in turn executes the following code.

Back to Top

    jPanel1.updateBuffer();
    jPanel1.repaint(); 
Calling the repaint() method is the proper way to trigger application or applet driven painting. The paint() method is called when it's time to render. Swing factors the paint() call into three separate methods, which are called in the following order:
   protected void paintComponent(Graphics g)
   protected void paintBorder(Graphics g)
   protected void paintChildren(Graphics g)  
Swing programs should override paintComponent() instead of overriding paint().

Now let's take a look at the named inner class which is instantiated to generate the jPanel1 object used in the syntax:

   jPanel1.repaint();

Here's the inner class which extends JPanel.
private class DrElp extends JPanel {

    public void DrElp() {
    }

    public void updateBuffer() {

      if ( firstPass ) {

        if ( buffer == null ) {
           Dimension size = getSize();
           insets = getInsets();
           size.width = size.width - 
              insets.left - insets.right;
           size.height = size.height - 
              insets.top - insets.bottom;
           buffer = (BufferedImage)createImage( 
              size.width, size.height );
           g2d = buffer.createGraphics();
           g2d.setColor( getBackground() );
           g2d.fillRect( insets.left, 
              insets.top, size.width, 
                              size.height );
        }//End  if ( buffer == null)

        /* set anti-alias ON, 
            for smooth lines */
        g2d.setRenderingHint(
           RenderingHints.KEY_ANTIALIASING,
           RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setColor( colorObj[colorInt] );
        afn = new AffineTransform();
        /* calculate upper left corner of */ 
        /* rectange defining the ellipse */
        x = xCent - w / 2;
        y = yCent - h / 2;
        elp = new Ellipse2D.Double( x, y, w, h );
        /* calculate translation constants to */
        /* keep ellipses centered */
        xTransCon = 
          (xCent*scaleFact - xCent)/scaleFact;
        yTransCon = 
          (yCent*scaleFact - yCent)/scaleFact;
        scaleFactacc = scaleFact;
        afn.rotate( -Math.toRadians(degreeStart), 
           xCent , yCent );
        elpTrans = 
            afn.createTransformedShape( elp );
        g2d.draw( elpTrans );
        /* shape rotates in positive 
                           direction */
        direction = -1; 
        if ( degreeRotateTo < 0 ) {
           /* shape rotates in negative 
                             direction */
           direction =  1;  
        }
        theta = degreeStart;
        firstPass = false;
        drawing = true;
      }//End  if (firstPass)

      /*  LOOP--ROTATE and EXPAND */
      if (drawing) {
        if ( degreeRotateTo > 0 ) {
           theta += degreeIncrement;
        }
        else if ( degreeRotateTo < 0 ) {
           theta -= degreeIncrement;
        }
        else if ( degreeRotateTo == 0 && 
           degreeStart != 0 ) {
           theta += degreeIncrement;
        }
        if ( ( degreeRotateTo > 0 ) && 
           ( theta > degreeRotateTo ) ) {
           drawing = false;
           stopAnimation();
        }
        else if ( ( degreeRotateTo < 0 ) && 
           ( theta < degreeRotateTo ) ) {
           drawing = false;
           stopAnimation();
        }
        else if ( ( degreeRotateTo == 0 ) && 
           ( (theta > 360 ) ||
           (theta == 0) ) ) {
           drawing = false;
           stopAnimation();
        }
        else {
          /* Affine Transforms    */
          afn.scale( scaleFact, scaleFact );
          afn.translate
                   ( -xTransCon , -yTransCon );
          afn.rotate(  
             Math.toRadians(
                    degreeIncrement)*direction, 
             xCent, yCent );
          elpTrans = 
            afn.createTransformedShape( elp );
          g2d.draw( elpTrans );
          scaleFactacc *= scaleFact;
          /* if wildly outside viewable area */
          /*  turn off the loop */
          if ( scaleFactacc > 300.0 ||  
             scaleFactacc < .001 ) {
             drawing = false;
             stopAnimation();
          }
        }
      }//End  if ( drawing )
    }//End  updateBuffer()

    public void paintComponent( Graphics g ) {
       if ( buffer != null ) {
          Graphics2D graphics = (Graphics2D)g;
          graphics.drawImage( buffer, 
             insets.left, insets.top, this );
      }
      else {
         super.paintComponent( g );
      }
    }//End  paintComponent()
  }//End  inner class DrElp

Most of the code above has been covered earlier in the article (albeit extracted from its surrounding logic). The if and else structures control the direction of ellipse rotation, and the start/stop points for the transform operations.

The most important thing to discover about the DrElp inner class is the structure of the methods updateBuffer and paintComponent.

Remember that our actionPerformed method, which is driven by javax.swing.Timer, has these two lines of code inside:

    jPanel1.updateBuffer();
    jPanel1.repaint(); 

Also remember that jPanel1 is an instantiation of the inner class DrElp. So the first method call, JPanel1.updateBuffer() does some extensive initialization on its first time through, based on the binary switch firstPass. After that, when the binary switch drawing is true, the code is focused on preparing a buffer for later output. That output takes place when the line jPanel1.repaint() is executed. The paintComponent() method is called automatically by the repaint() method.

Let's look at updateBuffer() and paintComponent() in greater depth.

Just inside updateBuffer(), we find this block of code.

if ( firstPass ) {

   if ( buffer == null ) {
      Dimension size = getSize();
      insets = getInsets();
      size.width = size.width - 
         insets.left - insets.right;
      size.height = size.height - 
         insets.top - insets.bottom;
      buffer = (BufferedImage)createImage( 
         size.width, size.height );
      g2d = buffer.createGraphics();
      g2d.setColor( getBackground() );
      g2d.fillRect( insets.left, 
                insets.top, size.width, 
                          size.height );
   }//End  if ( buffer == null)

If the firstPass Boolean is true, and the buffer variable is null, as it is upon applet startup, or after the JPanel has been erased, then the block above is executed to set up the drawing attributes and the buffer. First the size of the JPanel is interrogated with a call to getSize(). Then the Insets object insets is created with a call to getInsets. As we learned in the GridBagLayout section, an Insets object has four public members--top, bottom, left, and right. In the general case, the members represent the number of pixels from each edge of the component, to the drawing area of the component. A calculation is then made, using this data, to construct a width and height reflecting the area within the component that is usable for drawing operations. These calculations have been done for the general case, where component borders or title bars may actually obscure the drawing area. In the case of our JPanel component, the getInsets method returns all 0s, because the JPanel has no border, title bar, etc.

Next, the createImage method of the Component class is called to create an off-screen drawable image which will be used for double buffering. This double buffering scheme is required to eliminate conflict between Swing's intrinsic rendering functionality and the operation of drawing the ellipses. The createImage method returns an Image object that is cast to a (BufferedImage) object.

Then the buffer object is used to create a Graphics2D object for drawing into the buffer. The line below illustrates this.

g2d = buffer.createGraphics();

After that, we set the background color, which interrogates the background color of the parent applet, then uses this color to fill the JPanel rectangle drawing area. The fillRect method specifies the area to be filled, starting with an integer x and y value. Remember that our coordinate scheme specifies the upper left corner as the 0,0 point. So the method call here begins the fill at the inset values (in this case 0s), to clear the component boarder (if a border exists), then specifies the rest of the rectangle using the JPanel's width and height. The call looks like this:
   g2d.fillRect( insets.left, 
      insets.top, size.width, size.height );

That completes the basic setup, now let's see how the paintComponent method uses the buffer. The method is defined like this:
public void paintComponent( Graphics g ) {
   if ( buffer != null ) {
     Graphics2D graphics = (Graphics2D)g;
     graphics.drawImage( buffer, insets.left,
        insets.top, this );
   }
   else {
     super.paintComponent( g );
   }
 }//End  paintComponent()

As mentioned earlier, execution of jPanel1.repaint() from within the actionPerformed() method, triggers calls to the paintComponent method. When paintComponent( Graphics g ) is invoked by Swing, the Graphics object parameter g is pre-configured with the appropriate state for drawing the JPanel. Programmers who override paintComponent must use this Graphics object (or one derived from it), to render output. In this case, the Graphics object is cast to a Graphics2D object.
Graphics2D graphics = (Graphics2D)g;

Then the resulting Graphics2D object is used to render the output from the buffer. The syntax below handles the generation of this output.
   graphics.drawImage( buffer, insets.left,
      insets.top, this );
The drawImage method draws the image specified by the first argument. The image is drawn with it's top left corner at point specified by the second and third arguments. In this case insets.left and insets.top are both 0. Again, the use of insets makes sure that the component border does not obscure output. The process that draws the image can notify the specified observer (the third argument), in this case, the current object this.

If the buffer is null, then the call to super.paintComponent(g) repaints the background.

And that about wraps it up. As an epilogue, let's take a look at using the Forte for Java CE debugger, which can be very helpful in determining exactly what is going on, when things seem confusing.

Back to Top

Using The Forte CE Debugger

The debugger can be used to present "snapshots" of the system state during execution. By placing breakpoints at key positions throughout the source code, the debugger can halt and display details of the current environment at that point in the source. You can effectively step through code, monitoring execution as it occurs. The debugger can also be connected to an already-running process.

Debugger Window

The Debugger Window is a three-tabbed display with tabs for Breakpoints, Threads, and Watches. In the right half of the window, is the property sheet pane which displays the properties and their current values for the node selected in the left pane.

Breakpoints

To add a breakpoint, right click on a line of code in the Editor Window and select Add/Remove Breakpoint from the pop-up menu, or choose Add/Remove Breakpoint from the Debug menu or toolbar in the Main Window. The current line is highlighted in blue to indicate that a breakpoint has been set.

Breakpoints can also be set explicitly by selecting New Breakpoint from the Debug menu, which brings up the Add Breakpoint dialogue box. In this mode, the type of breakpoint (either exception, method, or line) can be chosen from the combo box, then settings can be entered specifying exception class name, class name and method name, or class name and line number.

The screen shot below illustrates a breakpoint I added by right-clicking the mouse on the desired line, then clicking the pop-up Add/Remove Breakpoint option, which functions as a toggle. The blue line indicates the breakpoint. The cyan line indicates where the debugger is currently stopped. In this case, one line after the breakpoint, as a result of a Trace Over command which will be explained later.

Image8.gif

Setting Watches

Watches can be set from the Debug menu on the Main Window, or from the contextual menu of a variable you have selected in the Editor. In the Editor, double-click a variable to select it, then right-click to bring up the menu of context choices, including Add Watch.

Standard watches, as described above, refer to the value of a variable of that name currently in scope. It is possible to create a fixed watch, which always refers to the variable in the context in which the watch was created. In other words, if another variable in the currently executing scope has the same name, the fixed watch will refer back to the previous value. In this applet, there are no variables of the same name in different scopes.

The Debugging Session

Now let's execute a debug session. Click on Start Debugging from the Debug menu to begin. In my example, a breakpoint is set at the statement:

scaleFact = 
  Float.parseFloat(jTextField7.getText());.  

After the debugger presents the applet's Swing GUI, I click the applet's Draw button to begin the applet's processing. The debugger runs to the set breakpoint then halts. The debug window shows variables that have been selected for watches. One of the most useful attributes of this debugger is the ability to hover the mouse over variables and interrogate their values using tool tip style pop-up boxes. With this feature, the current state of the applet is easily revealed. Selecting the Continue option from the Debug menu jumps to the next breakpoint. Or, successive Java statements can be executed by selecting Trace Over from the debug menu, or by pressing the F8 key. A useful technique for stepping through the program is to just hit F8 over-and-over, while watching the cyan execution bar march down through the program listing in the edit window.

Here is a summary of the basic debugger commands.

  • Trace Into--steps into the method at which the debugger is currently stopped if there is a method call on that line, and breaks at the start of the called method, enabling you to observe execution incrementally. If there is no method call on the current line, it behaves like Trace Over.
  • Go To Cursor--executes the current statement and all ensuing statements until the insertion point line is reached.
  • Trace Over--executes the current statement without breaking and stops at the next statement.
  • Trace Out--halts execution after the current method finishes and control passes to the caller.
  • Continue--resumes execution, which continues until debugger reaches the next breakpoint or the end of the application.
  • Finish Debugging--ends the current debugging session.

The screen shot below illustrates four watches in the Debugger Window, and a pop-up showing a data value in the Edit window.

Image9.gif

Threads

The Threads tab displays all thread groups in the current debugging process. These thread groups are expandable hierarchies; each group containing other thread groups or single threads, which in turn contain CallStack and Locals nodes.

When a thread is suspended:

The CallStack node can be expanded to show the current hierarchy of method calls made during execution. The Locals node displays local variables and their current values in the context of the current thread. You can expand these nodes to see the object sub-structure. If the process you are debugging has more than one thread, all threads and thread groups appear in the Threads tab showing a thread name and current status (such as "running", "at breakpoint", "cond. waiting" and "suspended"). Suspended threads and threads at breakpoint display all "current" system information.

The Debugger Window displays the following properties for each running thread:

Name - Thread name (according to the thread class).
State - Status of the thread, such as Running, Cond. waiting, etc.
Class - Name of the class in which the thread is suspended.
Method - Name of the method in which the thread is suspended.
Line Number - Current line in the thread.
Stack Depth - Number of methods in the call stack.
Suspended - If True, the thread is suspended.

The screen capture below illustrates expanded thread information derived from my applet.

Image10.gif

Note: There is an extensive user's manual available at Help | Documentation from the Forte CE Main Window.

Preparing to Run the Applet

The Java Plug-in is required to run this applet. The plug-in directs applets to run using the Java 2 Runtime Environment, Standard Edition (JRE) instead of the web browser's default virtual machine. Sun's JRE provides a Java Compatible environment for the widely adopted web browsers, and ensures that applets have access to the latest Java 2 technologies.

The Plug-in Page

The HTML Converter

To compile the applet code and initiate it locally, rather than downloading from the server, requires the HTML Converter. The HTML converter modifies HTML syntax to access the Java 2 JRE Plug-in. The converter can operate on a single file, or an entire directory of HTML files, including all sub-directories, if desired. The original HTML files are automatically saved in an alternate directory. The following screen capture shows the HTML Converter's GUI:

converter

HTML Converter Page

Running the Applet

If you've done a fast-forward to this point, you'll need the Java 2 Plug-in, see commentary above. Run the applet

Code Listing

Click here to view ElpGen2.java or right-click and select Save Link As to download the code.

Conclusion

Forte for Java Community Edition made the development of this applet much easier and more fun. The Form Editor makes initial GUI design a ten minute exercise. And the code completion function presents an object's available methods at a glance. The debugging facilities also expedite the development process. Once you've used an IDE, it's almost impossible to go back to writing code the "old fashioned" way.

Reference Texts

The Java Tutorial, Third Edition, Mary Campione, Kathy Walrath and Alison Huml, Addison-Wesley, 2000

Java 2D API Graphics, Vincent J. Hardy, Sun Microsystems Press/Prentice Hall, 2000

Java How to Program, 3rd Edition, Deitel & Deitel, Prentice Hall, 1999

Java 2 Platform, Laura Lemay and Rogers Cadenhead, Sams, 1999

Reference URLs

Download Forte for Java Community Edition

Using Timers in Swing Applications

How to Use Timers

Painting in AWT and Swing

The Plug-in Page

HTML Converter Page

Coffecup Logo

Acknowledgements

Special thanks to Sun's Kathy Walrath for the BufferedImage scheme and performance tweaks.

About the Author

Michael Meloan, a frequent contributor to the Java Developer Connection, began his professional career writing IBM mainframe and DEC PDP-11 assembly languages. He went on to code in PL/I, APL, C and Java. In addition, his fiction has appeared in WIRED, BUZZ, Chic, L.A. Weekly, and on National Public Radio.

Back to Top

coffeecup