Displaying Information in a Component in Java

In this section, we will show you how to display information inside a frame (Figure 10.3).

You could draw the message string directly onto a frame, but that is not considered good programming practice. In Java, frames are really designed to be containers for components, such as a menu bar and other user interface elements. You normally draw on another component which you add to the frame.

The structure of a JFrame is surprisingly complex. Look at Figure 10.4 which shows the makeup of a JFrame. As you can see, four panes are layered in a JFrame. The root pane, layered pane, and glass pane are of no interest to us; they are required to organize the menu bar and content pane and to imple­ment the look-and-feel. The part that most concerns Swing programmers is

the content pane. Any components that you add to a frame are automatically placed into the content pane:

Component c = . .

frame.add(c); // added to the content pane

In our case, we want to add a single component to the frame onto which we will draw our message. To draw on a component, you define a class that extends JComponent and override the paintComponent method in that class.

The paintComponent method takes one parameter of type Graphics. A Graphics object remembers a collection of settings for drawing images and text, such as the font you set or the current color. All drawing in Java must go through a Graphics object. It has methods that draw patterns, images, and text.

Here’s how to make a component onto which you can draw:

class MyComponent extends JComponent

{

public void paintComponent(Graphics g)

{

code for drawing

}

}

Each time a window needs to be redrawn, no matter what the reason, the event handler notifies the component. This causes the paintComponent methods of all components to be executed.

Never call the paintComponent method yourself. It is called automatically whenever a part of your application needs to be redrawn, and you should not interfere with this automatic process.

What sorts of actions trigger this automatic response? For example, painting occurs when the user increases the size of the window, or minimizes and then restores the window. If the user popped up another window that covered an existing window and then made the overlaid window disappear, the win­dow that was covered is now corrupted and will need to be repainted. (The graphics system does not save the pixels underneath.) And, of course, when the window is displayed for the first time, it needs to process the code that specifies how and where it should draw the initial elements.

As you saw in the code fragment above, the paintComponent method takes a single parameter of type Graphics. Measurement on a Graphics object for screen display is done in pixels. The (0, 0) coordinate denotes the top left corner of the component on whose surface you are drawing.

The Graphics class has various drawing methods, and displaying text is con­sidered a special kind of drawing. Our paintComponent method looks like this:

public class NotHelloWorldComponent extends JComponent

{

public static final int MESSAGE_X = 75;

public static final int MESSAGE_Y = 100;

public void paintComponent(Graphics g)

{

g.drawString(“Not a Hello, World program”, MESSAGE_X, MESSAGE_Y);

}

}

Finally, a component should tell its users how big it would like to be. Override the getPreferredSize method and return an object of the Dimension class with the preferred width and height:

public class NotHelloWorldComponent extends JComponent

{

private static final int DEFAULT_WIDTH = 300;

private static final int DEFAULT_HEIGHT = 200;

public Dimension getPreferredSize()

{

return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT);

}

}

When you fill a frame with one or more components, and you simply want to use their preferred size, call the pack method instead of the setSize method:

class NotHelloWorldFrame extends JFrame

{

public NotHelloWorldFrame()

{

add(new NotHelloWorldComponent());

pack();

}

}

1. Working with 2D Shapes

Starting with Java 1.0, the Graphics class has methods to draw lines, rectangles, ellipses, and so on. But those drawing operations are very limited. We will instead use the shape classes from the Java 2D library.

To use this library, you need to obtain an object of the Graphics2D class. This class is a subclass of the Graphics class. Ever since Java 1.2, methods such as paintComponent automatically receive an object of the Graphics2D class. Simply use a cast, as follows:

public void paintComponent(Graphics g)

{

Graphics2D g2 = (Graphics2D) g;

}

The Java 2D library organizes geometric shapes in an object-oriented fashion. In particular, there are classes to represent lines, rectangles, and ellipses:

Line2D

Rectangle2D

Ellipse2D

These classes all implement the Shape interface. The Java 2D library supports more complex shapes—arcs, quadratic and cubic curves, and general paths—that we do not discuss in this chapter.

To draw a shape, you first create an object of a class that implements the Shape interface and then call the draw method of the Graphics2D class. For example:

Rectangle2D rect = . . .;

g2.draw(rect);

The Java 2D library uses floating-point coordinates, not integers, for pixels. Internal calculations are carried out with single-precision float quantities. Single precision is sufficient—after all, the ultimate purpose of the geometric computations is to set pixels on the screen or printer. As long as any roundoff errors stay within one pixel, the visual outcome is not affected.

However, manipulating float values is sometimes inconvenient for the pro­grammer because Java is adamant about requiring casts when converting double values into float values. For example, consider the following statement:

float f = 1.2; // ERROR–possible loss of precision

This statement does not compile because the constant 1.2 has type double, and the compiler is nervous about loss of precision. The remedy is to add an F suffix to the floating-point constant:

float f = 1.2F; // OK

Now consider this statement:

float f = r.getWidth(); // ERROR

This statement does not compile either, for the same reason. The getWidth method returns a double. This time, the remedy is to provide a cast:

float f = (float) r.getWidth(); // OK

These suffixes and casts are a bit of a pain, so the designers of the 2D library decided to supply two versions of each shape class: one with float coordinates for frugal programmers, and one with double coordinates for the lazy ones. (In this book, we fall into the second camp and use double coordinates whenever we can.)

The library designers chose a curious mechanism for packaging these choices. Consider the Rectangle2D class. This is an abstract class with two concrete subclasses, which are also static inner classes:

Rectangle2D.Float

Rectangle2D.Double

It is best to ignore the fact that the two concrete classes are static inner classes—that is just a gimmick to avoid names such as FloatRectangle2D and DoubleRectangle2D.

When you construct a Rectangle2D.Float object, you supply the coordinates as float numbers. For a Rectangle2D.Double object, you supply them as double numbers.

var floatRect = new Rectangle2D.Float(10.0F, 25.0F, 22.5F, 20.0F);

var doubleRect = new Rectangle2D.Double(10.0, 25.0, 22.5, 20.0);

The construction parameters denote the top left corner, width, and height of the rectangle.

The Rectangle2D methods use double parameters and return values. For example, the getWidth method returns a double value, even if the width is stored as a float in a Rectangle2D.Float object.

What we just discussed for the Rectangle2D classes holds for the other shape classes as well. Furthermore, there is a Point2D class with subclasses Point2D.Float and Point2D.Double. Here is how to make a point object:

var p = new Point2D.Double(10, 20);

The classes Rectangle2D and Ellipse2D both inherit from the common superclass RectangularShape. Admittedly, ellipses are not rectangular, but they have a bounding rectangle (see Figure 10.6).

The RectangularShape class defines over 20 methods that are common to these shapes, among them such useful methods as getWidth, getHeight, getCenterX, and getCenterY (but, sadly, at the time of this writing, not a getCenter method that would return the center as a Point2D object).

Finally, a couple of legacy classes from Java 1.0 have been fitted into the shape class hierarchy. The Rectangle and Point classes, which store a rectangle and a point with integer coordinates, extend the Rectangle2D and Point2D classes.

Figure 10.7 shows the relationships between the shape classes. However, the Double and Float subclasses are omitted. Legacy classes are marked with a gray fill.

Rectangle2D and EHipse2D objects are simple to construct. You need to specify

  • The x and y coordinates of the top left corner; and
  • The width and height.

For ellipses, these refer to the bounding rectangle. For example,

var e = new Ellipse2D.Double(150, 200, 100, 50);

constructs an ellipse that is bounded by a rectangle with the top left corner at (150, 200), width of 100, and height of 50.

When constructing an ellipse, you usually know the center, width, and height, but not the corner points of the bounding rectangle (which don’t even lie on the ellipse). The setFrameFromCenter method uses the center point, but it still re­quires one of the four corner points. Thus, you will usually end up constructing an ellipse as follows:

var ellipse

= new Ellipse2D.Double(centerX – width / 2, centerY – height / 2, width, height);

To construct a line, you supply the start and end points, either as Point2D objects or as pairs of numbers:

var line = new Line2D.Double(start, end);

or

var line = new Line2D.Double(startX, startY, endX, endY);

The program in Listing 10.3 draws a rectangle, the ellipse that is enclosed in the rectangle, a diagonal of the rectangle, and a circle that has the same center as the rectangle. Figure 10.8 shows the result.

2. Using Color

The setPaint method of the Graphics2D class lets you select a color that is used for all subsequent drawing operations on the graphics context. For example:

g2.setPaint(Color.RED);

g2.drawString(“Warning!”, 100, 100);

You can fill the interiors of closed shapes (such as rectangles or ellipses) with a color. Simply call fill instead of draw:

Rectangle2D rect = . . .;

g2.setPaint(Color.RED);

g2.fill(rect); // fills rect with red

To draw in multiple colors, select a color, draw or fill, then select another color, and draw or fill again.

Define colors with the Color class. The java.awt.Color class offers predefined constants for the following 13 standard colors:

BLACK, BLUE, CYAN, DARK_GRAY, GRAY, GREEN, LIGHT_GRAY,

MAGENTA, ORANGE, PINK, RED, WHITE, YELLOW

You can specify a custom color by creating a Color object by its red, green, and blue components, each a value between 0 and 255:

g2.setPaint(new Color(0, 128, 128)); // a dull blue-green

g2.drawString(“Welcome!”, 75, 125);

To set the background color, use the setBackground method of the Component class, an ancestor of JComponent.

var component = new MyComponent();

component.setBackground(Color.PINK);

There is also a setForeground method. It specifies the default color that is used for drawing on the component.

3. Using Fonts

The “Not a Hello World” program at the beginning of this chapter displayed a string in the default font. Sometimes, you will want to show your text in a different font. You can specify a font by its font face name. A font face name is composed of a font family name, such as “Helvetica”, and an optional suffix such as “Bold”. For example, the font faces “Helvetica” and “Helvetica Bold” are both considered to be part of the family named “Helvetica.”

To find out which fonts are available on a particular computer, call the getAvailableFontFamilyNames method of the GraphicsEnvironment class. The method returns an array of strings containing the names of all available fonts. To obtain an instance of the GraphicsEnvironment class that describes the graphics environment of the user’s system, use the static getLocalGraphicsEnvironment method. The following program prints the names of all fonts on your system:

import java.awt.*;

public class ListFonts

{

public static void main(String[] args)

{

String[] fontNames = GraphicsEnvironment

.getLocalGraphicsEnvironment()

.getAvailableFontFamilyNames();

for (String fontName : fontNames)

System.out.println(fontName);

}

}

The AWT defines five logical font names:

SansSerif

Serif

Monospaced

Dialog

DialogInput

These names are always mapped to some fonts that actually exist on the client machine. For example, on a Windows system, SansSerif is mapped to Arial.

In addition, the Oracle JDK always includes three font families named “Lucida Sans,” “Lucida Bright,” and “Lucida Sans Typewriter.”

To draw characters in a font, you must first create an object of the class Font. Specify the font face name, the font style, and the point size. Here is an example of how you construct a Font object:

var sansbold14 = new Font(“SansSerif”, Font.BOLD, 14);

The third argument is the point size. Points are commonly used in typography to indicate the size of a font. There are 72 points per inch.

You can use a logical font name in place of the font face name in the Font constructor. Specify the style (plain, bold, italic, or bold italic) by setting the second Font constructor argument to one of the following values:

Font.PLAIN

Font.BOLD

Font.ITALIC

Font.BOLD + Font.ITALIC

The font is plain with a font size of 1 point. Use the deriveFont method to get a font of the desired size:

Font f = f1.deriveFont(14.0F);

Here’s the code that displays the string “Hello, World!” in the standard sans serif font on your system, using 14-point bold type:

var sansbold14 = new Font(“SansSerif”, Font.BOLD, 14);

g2.setFont(sansbold14);

var message = “Hello, World!”;

g2.drawString(message, 75, 100);

Next, let’s center the string in its component instead of drawing it at an arbitrary position. We need to know the width and height of the string in pixels.

These dimensions depend on three factors:

  • The font used (in our case, sans serif, bold, 14 point);
  • The string (in our case, “Hello, World!”); and
  • The device on which the font is drawn (in our case, the user’s screen).

To obtain an object that represents the font characteristics of the screen device, call the getFontRenderContext method of the Graphics2D class. It returns an object of the FontRenderContext class. Simply pass that object to the getStringBounds method of the Font class:

FontRenderContext context = g2.getFontRenderContext();

Rectangle2D bounds = sansbold14.getStringBounds(message, context);

The getStringBounds method returns a rectangle that encloses the string.

To interpret the dimensions of that rectangle, you should know some basic typesetting terms (see Figure 10.9). The baseline is the imaginary line where, for example, the bottom of a character like ‘e’ rests. The ascent is the distance from the baseline to the top of an ascender, which is the upper part of a letter like ‘b’ or ‘k’, or an uppercase character. The descent is the distance from the baseline to a descender, which is the lower portion of a letter like ‘p’ or ‘g’.

Leading is the space between the descent of one line and the ascent of the next line. (The term has its origin from the strips of lead that typesetters used to separate lines.) The height of a font is the distance between successive baselines, which is the same as descent + leading + ascent.

The width of the rectangle that the getStringBounds method returns is the hori­zontal extent of the string. The height of the rectangle is the sum of ascent, descent, and leading. The rectangle has its origin at the baseline of the string. The top y coordinate of the rectangle is negative. Thus, you can obtain string width, height, and ascent as follows:

double stringWidth = bounds.getWidth();

double stringHeight = bounds.getHeight();

double ascent = -bounds.getY();

If you need to know the descent or leading, use the getLineMetrics method of the Font class. That method returns an object of the LineMetrics class, which has methods to obtain the descent and leading:

LineMetrics metrics = f.getLineMetrics(message,

context); float descent = metrics.getDescent();

float leading = metrics.getLeading();

To show that the positioning is accurate, the sample program in Listing 10.4 centers the string in the frame and draws the baseline and the bounding rectangle. Figure 10.10 shows the screen display.

4. Displaying Images

You can use the Imagelcon class to read an image from a file:

Image image = new ImageIcon(fitename).getImage();

Now the variable image contains a reference to an object that encapsulates the image data. Display the image with the drawImage method of the Graphics class.

public void paintComponent(Graphics g)

{

g.drawImage(image, x, y, null);

}

We can take this a little bit further and tile the window with the graphics image. The result looks like the screen shown   in Figure 10.11.  We do the tiling in the paintComponent method. We first draw one copy of    the   image in    the top left corner and then use the copyArea call to copy it into the entire window:

for (int i = 0; i * imageWidth <= getWidth(); i++)

for (int j = 0; j * imageHeight <= getHeight(); j++)

if (i + j > 0)

g.copyArea(0, 0, imageWidth, imageHeight, i * imageWidth, j * imageHeight);

Source: Horstmann Cay S. (2019), Core Java. Volume I – Fundamentals, Pearson; 11th edition.

Leave a Reply

Your email address will not be published. Required fields are marked *