Java Programming   Java Programming
 |Sofia Home | Content Gallery |
Home
Syllabus
Schedule
Lessons
Assignments
Resources

Lesson 14.5 Java 2D

Java 2 includes many new advanced graphics capabilities. Collectively, these new capabilities are  called the Java 2D API. The number of new Java 2D graphics capabilities are, to put it bluntly, enormous; we'll just touch the surface in this section. Remember, also, that the Java 2D API is only available in the full Java 2 Platform release; it isn't part of Swing or the JFC, and it doesn't work with Java 1.1.

In this lesson we'll look at three new features available in the Java 2D API:

Enhanced Drawing Methods

Before you can use the Java 2D methods, you need to learn how to create a Graphics2D object by casting a Graphics object. Then you'll be able to make use of the new, enhanced drawing methods available in the 2D API:

  • Lines with different thickness and end-cap styles
  • The new, enhanced Shapes classes
  • The setPaint(), draw(), and fill() methods.

Extended Paint and Fill Methods

The Graphics2D has two extended "fill" classes, GradientPaint and TexturePaint. The GradientPaint class enables you to create a painting "brush" composed of a smooth gradation between two colors. 

The TexturePaint class is even more interesting, enabling you to create a brush from any graphic image. You can use these new fill classes anywhere you previously used a solid-color brush.

Extended Text Support

With the introduction of Java 2, your applications can now make use of all of the fonts installed on your local machine. No longer are you restricted to the same half-dozen logical fonts, available since Java 1.0. Now, if you like, you can pepper your dialogs with Ransom-Note, and decorate your JButtons with Comic Sans.

Of course these three features only touch the surface of what can be done with the Java 2D APIs, but they provide a nice introduction for future exploration.

Java 2D Basics: Graphics2D

Just as the Abstract Window Toolkit (AWT) components of Java 1.1 were largely subsumed by the Java Foundation Classes (JFC, also known as Swing) components in the Java 2 Platform, so also the painting techniques of Java 1.1 have given way to new techniques associated with the Java 2 class Graphics2D.

For compatibility, the old painting techniques remain supported; but the new techniques are so much more powerful and flexible that you'll use them almost exclusively.

The Graphics2D Class

In Java 2, the Graphics object provided with the paint() message is actually an instance of the Graphics2D class, which is a subclass of the abstract class Graphics.

You might wonder how Java can pass a Graphics2D object to a method, paint(), that is expecting to receive a Graphics object. You can't pass a double to a method that is expecting an int, for instance; the Java compiler stops you. That's true. With inheritance, however, things get a little more complicated.

With inheritance, you remember, every subclass object is also a member of its superclass. Thus, every Bear object is both a member of the Bear class and a member of the Mammal class. If you have a method that takes a Mammal reference as an argument, you should be able to pass it any kind of Mammal at all, a Bear, a Horse, or a Pig.

Back to Top

From Graphics to Graphics2D

The situation with the Graphics class and the paint() method is similar. Because the argument to the paint() method is declared as a Graphics object, you can pass it a Graphics2D object, because Graphics2D is a subclass of the Graphics class. 

You can't, however, use the object passed to paint() to call the new Graphics2D methods without first transforming it into a reference to a Graphics2D object. Fortunately, such transformation is simple. Just cast the Graphics object to a Graphics2D object like this:

public void paint(Graphics g)
{
    Graphics2D g2d = (Graphics2D) g;

    // painting statements go here
}

 

Drawing Lines

Now that you've transformed your Graphics object into a powerful Graphics2D object, let's see what it can do. We'll start by drawing styled lines. In Java 1.1, you remember, all lines are one pixel wide. The Java 2 Graphics2D class, on the other hand, has support for more line styles than you could ever use. Let's look at a simple one.

The DashApp

The DashApp program is a simple application that shows how to draw dashed lines. DashApp is built upon the GApp framework used throughout this lesson. The DashApp program simply draws three dashed lines as you can see here.

Image: The DashApp application draws three dashed horizontal lines

To compile and run DashApp, you'll have to have GApp.java in the same directory as DashApp.java. Remember, too, that you'll have to use Java 2, not Java 1.1 to compile DashApp
[Click here to download DashApp.java
[Click here to download GApp.java]

DashApp Step-By-Step

Before you go further, let's walk through this simple example, so that you'll be better prepared to understand the more complex examples that follow. You may want to open the source code for DashApp.java, as you follow along. 

The application begins by importing the java.awt.geom.* package, along with the AWT and Swing classes, which contains the necessary support for styled lines. 

The body of the DashApp class then has three methods: the paint() method, which draws the lines, a utility method, makeStroke(), which produces the different lines to be drawn, and main(), where the application is launched and displayed. 

Most of the work is done in the paint() method which we'll examine here:

  • The paint() method begins by first casting its Graphics argument to a Graphics2D object, so that it can use the new 2D API:
  • Graphics2D g2d = (Graphics2D) g;
  • It then declares two local variables, a Line2D.Float object named line and a BasicStroke object named stroke:
  • Line2D.Float line;
    BasicStroke stroke;
  • Next, it sets the default painting color used to render the lines by calling the new setPaint() method:
  • g2d.setPaint(Color.red);
  • The four-argument Line2D.Float constructor is used to create a horizontal Line2D object that goes from 10,50 to 300,50, like this:
  • line = new Line2D.Float(10f, 50f, 300f, 50f);
  • After the object is constructed, it is passed to the Graphics2D object's draw() method where it is rendered:
  • g2d.draw(line);

As you can see, this method is pretty straightforward. To really understand how DashApp works, however, you have to know about two classes that are used there: the Line2D class and the BasicStroke class. Let's take a brief look.

Back to Top

The Line2D.Float Class

In Java 1.1, primitive graphics operations were procedural, rather than object-oriented. Instead of having Line or Rectangle objects that "knew" how to draw themselves, you used methods such as drawLine() and drawRect()

Java's 2D drawing model, by contrast, is fully object-oriented, with a rich assortment of objects that can render themselves in a variety of ways. One of the simplest is the Line2D.Float class. Its constructor: 

Line2D.Float(float x1, float y1, 
             float x2, float y2)

creates a line from the point (x1, y1) to the point (x2, y2).

The BasicStroke Class

Although each Line2D object knows where it should be drawn, it has no control over how it should appear; that's the job of the BasicStroke class. The BasicStroke class represents the brush strokes used to render lines on the display The BasicStroke brush is capable of painting with dots or with dashes, or even with patterns. Because the BasicStroke class gives you so much control over the way lines are drawn, its constructor is fairly complex. In the DashApp program, a utility method called makeStroke() is used to construct each BasicStroke object.

If you look inside the makeStroke() method you'll see that it simply calls the BasicStroke constructor, passing it the six arguments it requires:

  • The width argument gives the width of the line. In makeStroke(), all of the lines are 10 pixels wide.
  • The cap argument specifies how strokes are ended. You can end each line in several different ways. In DashApp, BasicStroke.CAP_BUTT is used for all line ends.
  • The join and miterlimit arguments specify how intersecting strokes are joined. In DashApp, there are no joining lines, so these arguments don't really have any effect.
  • The dash argument is a float array that specifies a repeating pattern of stroke segment lengths, alternating between opaque and transparent.
In DashApp, the makeStroke() method uses the same value for all of the arguments except for the dash argument, which it accepts as a parameter. Therefore, each BasicStroke object it produces has the same basic characteristics, but a different pattern of dashes.

After you've constructed a BasicStroke object, you simply tell your Graphics2D object to use the new BasicStroke object whenever it draws lines by using the setStroke() message like this:

stroke = makeStroke(new float[] {30f, 10f});
g2d.setStroke(stroke);

Just like setPaint(), you must remember to call setStroke() each time your paint() method is entered.

Drawing Shapes

In addition to Line2D, the 2D API provides a number of other useful shapes such as Arc2D, CubicCurve2D, Ellipse2D, QuadCurve2D, Rectangle2D, and RoundRectangle2D

To use any of these shapes in your programs, simply find an appropriate constructor from the documentation, create an object using the constructor, and pass the result to Graphics2D.draw() like this:
public void paint(Graphics g)
{
  super.paint(g);

  Graphics2D g2d = (Graphics2D) g;

  Arc2D.Float pie = 
     new Arc2D.Float( 0.0f, 0.0f,      // x, y
         (float) getSize().width - 1,  // width
         (float) getSize().height - 1, // height
         125.0f,                       // start
         270.0f,                       // extent
         Arc2D.PIE);                   // closure

  g2d.draw(pie);
}

This code, when used to replace the paint() method in the DashApp program, draws a PacMan-like shape that fills the entire app, as you can see here.

Image:The ArcApp program--demonstrating how to use the 2D graphics classes--running.

[Click here to download ArcApp.java

Experimenting with different start, extent, and closure arguments is an excellent way to learn how each of these classes work. Another is to look at a sample that shows everything, and picks out what you need. The next program does just that.

Back to Top

The ShapesApp Program

The ShapesApp program draws a little bit of everything. In addition to making use of each of the shapes previously mentioned, it uses the GeneralPath class to draw a shape of its own design. You can click here to download ShapesApp.java, and peruse it at your leisure. 

Here's a screen-shot, so you'll know what to expect.

Image: The ShapesApp program running. Illustrates use of Java 2D Shape classes.

Textures and Gradients

In this section, you'll learn how to use the GradientPaint and TexturePaint classes, which enable you to create sophisticated painting effects. Objects of either class can be passed to the Graphics2D method setPaint() in place of the familiar Color objects used in the DashApp program.

  • The GradientPaint class produces a smooth transition between two different colors located at two different points. You supply the end-points and the colors to display, and Java takes care of providing the interpolated colors required to connect the two points.
  • The TexturePaint class enables you to create a painting "brush" composed of an image. You can use any JPEG or GIF image, and Java takes care of replicating the image to fill the shape you specify. 

The GradientPaint Class

Java's new GradientPaint class lets you paint with a range of colors that smoothly blend from one value to another. To create a GradientPaint object, use the constructor

GradientPaint(float x1, float y1, Color color1,
              float x2, float y2, Color color2)

where the point (x1, y1) has the color specified by color1 and the point (x2, y2) has the color specified by color2. Points between (x1, y1) and (x2, y2) will receive an intermediate color, determined by interpolation.

Back to Top

The GradientApp

The GradientApp program shows how to use the GradientPaint class to paint a simple sunset, which you can see illustrated here.

[Click here to open GradientApp.java]

Image:Running the GradientApp program. Illustrates the use of the GradientPaint class.

The actual application looks much better, at least when it runs on my computer, because the hardware creates a much smoother gradation between the dark purple at the top, and pastel pink at the bottom.

Saving the image as a JPEG--as shown in the first image--makes the gradation appear much rougher; but saving the image using indexed color [i.e. GIF] is even worse, as you can see here.
Image: GradientApp saved as a GIF image

Step-by-Step

Let's take a look at the most interesting lines in the GradientApp program. To get the most from this section, you should click here to download GradientApp.java, and then follow along with the rest of this section.

GradientApp's paint() method begins by calculating the height and width of the panel surface like this:

float height = (float)getSize().height;
float width = (float)getSize().width;

The values returned from getSize() are cast to float to make calling the Graphic2D methods, which require float arguments when you specify coordinates, easier.

After calculating the height and width, a new GradientPaint object is constructed:

GradientPaint gp = new GradientPaint(
    width/2, height/4,            // x1, y1
    new Color(0, 0, 128),         // first color
    width/2, height/4*3,          // x2, y2
    new Color(255, 196, 196));    // second color

The GradientPaint object gp calculates a smooth transition from a dark blue at point x1, y1 (placed near the top of the screen), and a pale pink at point x2, y2 (placed near the bottom). The colors in-between will be blended together.

After it is constructed, the GradientPaint object is installed by using the setPaint() method. The program then draws a filled rectangle that fills the entire panel:

g2d.setPaint(gp);
Rectangle2D.Float rect =
    new Rectangle2D.Float(0f, 0f, width, height);
g2d.fill(rect);

Finally, an Ellipse2D.Float object is constructed to represent the sun, and the program is labeled at the top.

The TexturePaint Class

The 2D API also provides another new class, TexturePaint, that enables you to paint using an image as your brush, rather than simply a color. 

Creating a TexturePaint brush requires a little more work than creating a solid-color brush or GradientPaint brush. Fortunately, most of the repetitive work can be put into a single method that requires only a few arguments.

Here is an overview of the steps required to create a TexturePaint object:

  1. Load an Image from a disk file. This is done using the same methods you learned in Lesson 11.
  2. Create a BufferedImage object to hold your Image in memory before it is transferred to your display.
  3. Render your Image onto your in-memory BufferedImage, using the drawImage() method.
  4. Create a TexturePaint object, using the rendered BufferedImage as an argument.

The TextureApp Program

The TextureApp program creates two different TexturePaint objects, using the names of image files you supply on the command line. 

The first image is used to paint the background, and the second is used to fill an oval in the center of the first. If you don't pass any arguments, it uses the graphics files Rocks.jpg and Water.jpg, which you can download if you want to try the application yourself.

Let's take a look at how the TextureApp program works. We'll start with an overview of the program, then we'll take a closer look at its getTexture() method. You can click here to download TextureApp.java. If you want to compile and run the program on your own computer, you'll have to supply a suitable image file as well. 

Here's a screenshot of TextureApp running with no arguments at all, and another showing TextureApp running with two different JPG files.
Image:Running the TextureApp program with no arguments
Image: Running the TextureApp program using different image files

Most of the action in TextureApp occurs in its paint() method which begins by determining the size of its surface using getSize().height and getSize().width. These are saved in the float variables height and width.

Next, the paint() method constructs a centered Rectangle2D.Float object that occupies three-fifths of the panel's surface, using some simple arithmetic. 

After that, the paint() method calls the getTexture() method to construct a TexturePaint brush, passing the name of a JPEG file as an argument. The TexturePaint object returned is passed to setPaint() and then the rectangle is filled with the texture by calling the Graphics2D method fill():

TexturePaint tp = getTexture(backgrnd, this);
g2d.setPaint(tp);
g2d.fill(rect);

After drawing the rectangle, the paint() method creates another TexturePaint object and uses it to fill an ellipse in the center of the rectangle.

Back to Top

The getTexture() Method

Because you follow these same exact steps each time you construct a TexturePaint object, you can make your programs clearer, as well as smaller, by putting these steps into a method that you can reuse.

To use getTexture(), you pass it the name of the file you want to use as your textured image, along with a reference to the Component where your image will be rendered. The getTexture() method returns a TexturePaint object for you to use when it's done.  

Step 1: Loading Your Image

The first thing that getTexture() does is create an Image object from the file you provide as an argument. You can use any GIF or JPEG file to construct your TexturePaint, but you should try to use relatively small images where the edges blend or mesh smoothly as multiple images are painted side-by side.

The getTexture() method uses the Toolkit.getImage() method, which takes one argument: a String specifying the name of the file containing the image. 

Because images are loaded asynchronously, the getTexture() method constructs a MediaTracker object and uses its waitForID() method to force the image to load immediately.  

Step 2: Creating a BufferedImage

A BufferedImage object is an in-memory object that has an associated Graphics2D object; you can paint directly on a BufferedImage just as you can with a regular Graphics2D object. After you're finished drawing, you render your BufferedImage on the screen by using the drawImage() method, the same way an ordinary image is drawn. The BufferedImage constructed inside the getTexture() method must be the same size as the Image it displays, so getTexture() uses the Image getHeight() and getWidth() methods to retrieve the image dimensions.

Next, getTexture() constructs a BufferedImage object by using its constructor:

BufferedImage bImg = 
   new BufferedImage(imgWidth, imgHeight,
       BufferedImage.TYPE_INT_ARGB);

The third argument specifies the color model used by the BufferedImage. The 2D API provides a variety of color models; the TYPE_INT_ARGB is usually the model of choice.

Step 3: Rendering Your Image

To render the Image onto the BufferedImage, a Graphics2D object is required. Your BufferedImage object will give you a suitable Graphics2D object when you call its createGraphics() method like this:

Graphics2D g2d = bImg.createGraphics();

It is a simple matter to draw the Image by using the drawImage() method in this way:

g2d.drawImage(theImage, new AffineTransform(), c);

As you can see, the Graphics2D version of drawImage() is a little different than the one you are already familiar with. This version takes three arguments:

  1. A reference to the Image you want to render.
  2. An AffineTransform object that can be used to transform the size, location, and orientation of your image. The default constructor of the AffineTransform class--used here--creates an "idenity transfom", which does not change the size, location, or orientation of the image.
  3. A reference to the Component where the BufferedImage will ultimately be displayed.
Step 4:The TexturePaint Object
After your Image has been rendered on your BufferedImage, the last step is to construct a TexturePaint object using the BufferedImage you've just drawn. 

This is accomplished with the TexturePaint constructor like this:

TexturePaint tp = new TexturePaint( 
     bImg, 
     new Rectangle(0, 0, imgWidth, imgHeight),
     TexturePaint.NEAREST_NEIGHBOR);

As you can see, the constructor takes three arguments:

  1. A BufferedImage object that contains your rendered Image.
  2. A Rectangle object that is used to specify the location and size of the rectangle that will be replicated during painting.
  3. An interpolation mode which lets Java fix the position of several critical points along a path and then fill in the points between each pair of points. Two possible types of interpolation are possible: the NEAREST_NEIGHBOR mode, which is fast but imprecise, and the BILINEAR mode, which yields better image quality. The NEAREST_NEIGHBOR mode is adequate for ordinary purposes.

System Fonts

The Toolkit class was used in Java 1.1 to retrieve platform-specific code such as that required to load an image or obtain a list of logical font names. In Java 2, the Toolkit class is joined by the GraphicsEnvironment class, used by Java to give you access to the native fonts, screen, and printer resources on your system.

As with most other parts of the Java2D API, the sheer number of classes and methods is daunting; to make effective use of the GraphicsEnvironment class to use the local fonts on your system. However, you only need to learn two:

getLocalGraphicsEnvironment();
getAllFonts();

Getting your System Fonts

The getLocalGraphicsEnvironment() method is a static method that works exactly like the Toolkit class getDefaultToolkit() method. You call it once to obtain a handle to your system's local resources like this:

GraphicsEnvironment ge =
  GraphicsEnvironment.getLocalGraphicsEnvironment();

You can then use the GraphicsEnvironment object to obtain an array filled with Font objects, one Font for every native font on your system, like this:

Font[] allFonts = ge.getAllFonts();

Unfortunately, you can't use the Font objects returned by getAllFonts() without some further work, because each Font is only one pixel high. Fortunately, that's easily remedied by using the deriveFont() method to create a font in the size you really want.

The FontsApp Program

The FontsApp program is an example that shows you how to get a list of fonts installed on your local machine, and then how to use those fonts in your application. You can click here to download FontsApp.java, and follow along while we look at the highlights.

The FontsApp window consists of two panes:

  • In the left pane you'll see a list of all the fonts available to your Java program.
  • In the right pane you'll find a text area where you can type your information.
When you double-click a font name in the left hand pane, it creates a Font object, using deriveFont(), and then applies the Font to the text area object appearing in the right-hand pane.

In this illustration, the user has just clicked on the Glowworm font and the result is reflected in the right-hand pane.

Image: Running the FontsApp application that allows the user to select and use a native font.
Back to Top

Step-by-Step

There are five important steps inside the FontsApp class. You'll want to look at the source code to make sure you understand each of them:
  1. The FontsApp class first creates a GraphicsEnvironment object named env by using the getLocalGraphicsEnvironment() method.
  2. The GraphicsEnvironment object is then used to retrieve a list of all the fonts in the system, and store them in the array of Font objects named fonts.
  3. Once the Font objects are stored in the fonts array, an array of Strings, called names, is created; and the fonts array is traversed. Each Font object is sent the getFontName() message, and the resulting String is stored in the String array names.
  4. The names array is then added to a JList object, named fontList, on the left half of the application. The right half of the application is a JTextArea in which you can type text.
  5. The FontsApp class adds a MouseListener to fontList, using an anonymous MouseAdapter. Inside the overridden mousePressed() method, the application translates a double-click coordinate into an array index and retrieves the selected Font. A new, derived Font is then created by using the deriveFont() method, like this:
  6. Font f = fonts[idx].deriveFont(24.0f);
The new, derived Font has all of the characteristics of the Font stored in the array, but has a new size--in this case, 24 points.

One note of caution when you run the FontsApp application. Most of the examples you've seen so far--with the exception of those that use the 2D API classes--work equally well with JDK 1.1 and the stand-alone Swing classes or the Swing classes in JDK 1.2. The new Font extensions, however, are not part of Swing, but are extensions to the AWT in Java 1.2. Thus, you can only use these new features if you are writing a JDK 1.2 application.

Something to Talk About

In this section, you learned about the new drawing, filling, and Font methods available in the Java 2D API. As was first mentioned, however, these features barely scratch the surface of those that are available.

You might want to take a few moments to browse through some of the other features available in the 2D API by looking through the Java 2D API Programmer's Guide at: 

http://java.sun.com/products/jdk/1.3/docs/guide/2d/spec/j2d-title.fm.html

Take a look through the table of contents, and read up on one feature that we haven't covered here. Tell us what you chose, and why it interests you.

Please continue to the next section of this lesson.

 

Back to Top

 

Content Developed by Stephen Gilbert, Licensed under a Creative Commons License
Published by the Sofia Open Content Initiative
© 2004 Foothill-De Anza Community College District &The William and Flora Hewlett Foundation