Java Graphics Tools

This lesson consists of a survey of the graphics tools that we'll be using in the next few lessons. These include facilities in the graphics context, classes and interfaces in the Abstract Window Toolkit (AWT). It is by no means comprehensive, and includes no discussion of user interface components such as push buttons, sliders or drop-down lists. The tools we look at in this lesson will be required in coming lessons. See also Getting Started with Graphics in the Oracle Java Tutorial.

For an introduction to user interface components, see Creating a GUI With Swing in the Oracle Java Tutorials.

GitHub repository: Cartesian Plane Part 1

Previous lesson: Java Graphics Bootstrap

Unit 2: A Survey of Basic Graphics Tools

1. Basic Tools from the Graphics Classes

Let's look at some of the basic drawing tools from the AWT graphics classes. These include drawing simple geometric features such as lines, rectangles and circles. The first few facilities we'll look at draw figures at integer pixel locations; later we'll look at facilities that (importantly!) allow you to specify fractional locations. In specifying locations, recall that the origin of the graphics plane is the upper-left corner of the window, and y coordinates increase as you move down.

 

Graphics.drawLine(int x1, int y1, int x2, int y2)
This method simply draws a line from pixel (x1, y1) to pixel (x2, y2) . By default it draw the line 1 pixel wide. To change the width use a Stroke (see below).

 


 

Graphics.drawRect​(int x, int y, int width, int height)
This method draws the edge of a rectangle of the given width and height, with the upper-left corner at the given x- and y-coordinates.

Graphics.FillRect​(int x, int y, int width, int height)
This method fills a rectangle of the given width and height, with the upper-left corner at the given x- and y-coordinates.

Graphics.drawOval​(int x, int y, int width, int height)
Given a bounding rectangle, draw the edge of an oval inscribed in the rectangle. The x- and y-coordinates of the upper-left corner, and the width and height of the bounding rectangle are provided. To draw a circle, make the bounding rectangle a square.

Graphics.fillOval​(int x, int y, int width, int height)
Given a bounding rectangle, fill the oval inscribed in the rectangle. The x- and y-coordinates of the upper-left corner, and the width and height of the bounding rectangle are provided.
To fill a circle, make the bounding rectangle a square.

Graphics.drawString​(String str, int x, int y)
This method draws the given string at the given x- and y-coordinates. The string is rendered in the font that is currently set in the graphics context; the coordinates specify the location of the baseline of the current font. We'll defer a discussion of drawString() until after we've talked more about fonts, below.

Graphics2D.draw(Shape)
Graphics2D.fill(Shape)
These two methods from the Graphics2D class draw/fill a Shape. Shape is an interface, and Rectangle2D is an example of a class that implements Shape. (I'm going to assume you know what an interface is, and how to use one. For more information see Interfaces
in the Oracle Java Tutorial.)

To round off this discussion, here is a list of drawing methods from the Graphics class that we won't discuss in detail, and won't be using in the Cartesian plane project. For a discussion of these methods, see the documentation for the Graphics class, and Getting Started with Graphics in the Oracle Java Tutorial.

draw3DRect​(int x, int y, int width, int height, boolean raised)
fill3DRect​(int x, int y, int width, int height, boolean raised)
Draw or fill 3D rectangle.

drawArc​(int x, int y, int width, int height, int startAngle, int arcAngle)
fillArc​(int x, int y, int width, int height, int startAngle, int arcAngle)
Draw or fill on ovular arc.

drawImage( ... ) (various overloads)
Draw an image.

drawPolygon​(int[] xPoints, int[] yPoints, int nPoints)
fillPolygon​(int[] xPoints, int[] yPoints, int nPoints)
Draw or fill a polygon.

drawRoundRect​(int x, int y, int width, int height, int arcWidth, int arcHeight)
drawRoundRect​(int x, int y, int width, int height, int arcWidth, int arcHeight)
Draw or fill a rectangle with round corners.

drawPolyline​(int[] xPoints, int[] yPoints, int nPoints)
Draw a segmented line.

2. Graphics Classes

Java has a number of classes for encapsulating common graphical shapes, such as the Rectangle class. An object of this types encapsulates a rectangle using the x- and y-coordinates of its upper-left corner, and its width and height. A Rectangle object stores all of its properties as integers; for the Cartesian Plane project we are more interested in objects that store properties as floating point values. The ones we are interested in are discussed below. Except for Point2D they all implement the Shape interface, and can be drawn and/or filled with the draw(Shape) and fill(Shape) methods of the Graphics2D class. Also, they are all implemented as static nested classes, so using them might look a little unusual.

Digression: Static Nested Classes
A static nested class is a class declared inside another class and is declared static (if you forget the static declaration you get a whole different kind of nested class). Such classes can be public or private. If a static nested class is public, you can use it just like you do any other class; you just have to add the declaring class name to the front. Here's an example:

public class Canine1
{
    public static class Poodle
    {
        // ...
        public void setColor( Color color )
        {
            // ...
        }
    }
}
    Canine1.Poodle  bestFriendA = new Canine1.Poodle();
    bestFriendA.setColor( Color.BLUE );

To make things a little more confusing, the static nested classes we'll be using are also subclasses of the enclosing class, so we can get code that looks like this:

public class Canine2
{
    public static class Poodle extends Canine2
    {
        // ...
        public void setColor( Color color )
        {
            // ...
        }
    }
}
    Canine2.Poodle  bestFriendB = new Canine2.Poodle();
        // -- or --
    Canine2 bestFriendC = new Canine2.Poodle();

For more about static nested classes, see Nested Classes in the Oracle Java tutorial.

Point2D.Float, Point2D.Double
These classes encode x- and y-coordinates as either float, or double. Examples:

Point2D.Float   point1  = new Point2D.Float( 29.5f, -3.1f );
Point2D         point2  = new Point2D.Float( 55f, 32.3f );

Line2D.Float, Line2D.Double
These classes encode coordinates of the endpoints of a line as either float, or double.Some convenient constructors are:

Float​(float x1, float y1, float x2, float y2)
Float​(Point2D p1, Point2D p2)

Rectangle2D.Float, Rectangle2D.Double
These classes encode the upper-left corner, width and height of a rectangle as either float, or double. Some convenient constructors are:

Float()
Float​(float x, float y, float w, float h)

Ellipse2D.Float, Ellipse2D.Double
These classes encode an ovular shape as inscribed in a bounding rectangle. The user provides the upper-left corner, width and height of the bounding rectangle as either float, or double. To encapsulate a circle, make the width equal to the height.

Float()
Float​(float x, float y, float w, float h)

The Stroke Interface
A Stroke object is used to control the appearance of a line or edge of a geometrical figure (but not the color; that's controlled by the graphics context). Since it's an interface, if we want to use one we have to find a class that implements it; we will use BasicStroke. The stroke controls the width of a line, the way lines are joined together, how the ends are drawn and whether they are solid or dashed. For a general discussion of strokes, see Styling Line Caps and Line Joins from the Wolfram Demonstration Project. For a discussion of using the BasicStroke class, see Stroking and Filling Graphics Primitives in the Oracle Java Tutorial. The following code was used to create the accompanying figure.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public void paintComponent( Graphics graphics )
{
    // begin boilerplate
    super.paintComponent( graphics );
    currWidth = getWidth();
    currHeight = getHeight();
    gtx = (Graphics2D)graphics.create();
    gtx.setColor( bgColor );
    gtx.fillRect( 0,  0, currWidth, currHeight );
    // end boilerplate
    
    gtx.setColor( Color.black );
    
    ////////////////////////////////
    // Draw a thick, solid line.
    ////////////////////////////////
    Stroke  solidStroke = new BasicStroke( 3.5f );
    gtx.setStroke( solidStroke );
    gtx.drawLine( 20, 30, 120, 30 );
    
    ////////////////////////////////////////////////////////////
    // Draw the edge of a rectangle with a thin, dashed stroke
    ////////////////////////////////////////////////////////////
    
    // Stroke width. This stroke will be 1 pixel wide.
    float   width           = 1.0f;
    // The cap controls the appearance of the end of a line. 
    // BUTT means lines will have square ends.
    int     cap             = BasicStroke.CAP_BUTT;
    // The join determines how lines meet; BEVEL means lines
    // will meet with a straight edge.
    int     join            = BasicStroke.JOIN_BEVEL;
    // The miter limit determines the angle at which lines meet if 
    // they have a mitered join; it is not relevant to this example.
    float   miterLimit      = 1.0f;
    // The "dash array". This simply says that the array will consist
    // of dashes, all of which are 6 pixels long, separated by 5 pixels
    // of space.
    float[] dashes          = { 6.0f, 5.0f };
    // The "dash phase". Specifies the offset into the dash array 
    // to use at the start of a line. 0 means the line will begin
    // with a 6 pixel dash.
    float   dashPhase       = 0;
    Stroke  dashedStroke    =
        new BasicStroke(
            width,
            cap,
            join,
            miterLimit,
            dashes,
            dashPhase
        );
    gtx.setStroke( dashedStroke );
    gtx.drawRect( 20,  40, 100, 75 );

    // begin boilerplate
    gtx.dispose();
    // end boilerplate
}

3. Strings, Text and Fonts

For us, text is a set of characters that can be rendered in a window. The appearance of the text is controlled by a font. In our projects, text will usually come from a string of characters, most often represented in a String object. The font will be controlled by a Font object, which is stored in the graphics context. Text is often rendered using the drawString(String str, int x, int y) method, which draws str at pixel coordinates (x,y) using the current font and the current color. In our Cartesian Plane project, because we need better control over the position of text, we will be using the TextLayout class; see Positioning Text, below. (See also drawBytes and drawChars in the Graphics class documentation.)

Fonts
The history of fonts is a long and fascinating one, going back to the 12th century, well before the invention of the printing press. A discussion of fonts can get complicated, but we'll try to keep it as simple as possible.

Font Types
The two types of fonts that interest us are proportional and monospaced. In a proportional font, characters take up no more space than is strictly necessary; in other words, different character come in different sizes. The text you're reading now was most likely rendered in a proportional font. Here's a demonstration:

"QQQQQ" this is a proportional font;
"iiiii" notice that the characters do not line up in columns.

In a monospaced font, each character takes up the same amount of space. We usually prefer monospaced fonts for writing source code, because we can use them to make code line up in nice, neat rows and columns:

"QQQQQ" This is a monospaced font;
"iiiii" the characters on each line form neat columns.

Font Names
In Java, specific fonts are identified by name; you may recognize "Hevetica," and "Times New Roman," which are proportional fonts commonly available in word processing. An example of a monospaced font is "Courier." These are physical font names, and may or may not be available in a given environment. But Java also has fonts with logical font names, that are guaranteed to always be available. These include "Dialog," which is a proportional font, and "Monospaced" which is monospaced (there are others; for a completed discussion see Physical and Logical Fonts, in the Oracle Java Tutorial).

Font Styles
A font style usually represents a slight variation in the appearance of a font. Font styles include bold and italic.

Font Size
The size of a font determines the relative size of the characters in a font. Here are a couple of examples:

This is a large size in the Helvetica font.
This is a small size in the Helvetica font.

The Font Class
You can  create a font object from scratch, by using the constructor Font​(String name, int style, int size). Here's a fragment of code I used to create the example in the accompanying figure:

 

 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
gtx.setColor( Color.BLACK );
Font fontA = new Font("Helvetica", Font.PLAIN, 24 );
gtx.setFont( fontA );
gtx.drawString( "Helvetica, plain, 24", 20, 30 );

Font fontB = new Font("Helvetica", Font.BOLD, 12 );
gtx.setFont( fontB );
gtx.drawString( "Helvetica, bold, 12", 20, 60 );

Font fontC = new Font("Monospaced", Font.PLAIN, 14 );
gtx.setFont( fontC );
gtx.drawString( "Monospaced, plain, 14", 20, 75 );

Font objects can also be derived from existing font objects, though that's not very important for our project. See, for example, deriveFont​(float size), deriveFont​(int style) and deriveFont​(int style, float size) in the Font class documentation.

Positioning Text
To position text we have to be able to compute various metrics about a string of characters. That gets us into the subject of font metrics which involves a lot of details. The only such detail that interests us for this project is the baseline. For a complete discussion of font metrics, see the Typeface article in Wikipedia.

When you draw a string you enter the x- and y-coordinates of the string's baseline. Picture a word of text being printed and underlined; the underline goes approximately where the baseline is. Some of the text rises above the baseline, and some of it falls below the baseline. We will sometimes need to center the text (either horizontally or vertically), or left- or right-justify it. To perform these tasks we will start by calculating a bounding rectangle. Then, to center the text around a specific x-coordinate xco, for example, we would calculate the x-coordinate of the text to be xco - half the width of the bounding rectangle. Here is a snippet of the code used to draw the accompanying figure. We'll go into details in a future lesson.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private void drawString( String str, int xco, int yco )
{
    gtx.setColor( Color.BLACK );
    gtx.setFont( strFont );
    gtx.setStroke( new BasicStroke( 1 ) );
    
    // Draw the string at (xco, yco)
    FontRenderContext   strFRC = gtx.getFontRenderContext();
    TextLayout  strLayout   = new TextLayout( str, strFont, strFRC );
    Rectangle2D strRect     = strLayout.getBounds();
    strLayout.draw( gtx, xco, yco );
    float       strWidth    = (float)strRect.getWidth();
    
    // Draw the baseline, from xco to a little past
    // the end of the string
    float       baseXco1    = xco;
    float       baseXco2    = baseXco1 + strWidth + 40;
    Line2D      baseline    = 
        new Line2D.Float( baseXco1, yco, baseXco2, yco );
    gtx.draw( baseline );
    
    // Draw the bounding box around the string
    float       rectXco     = xco + (float)strRect.getX();
    float       rectYco     = yco + (float)strRect.getY();
    float       rectWidth   = (float)strRect.getWidth();
    float       rectHeight  = (float)strRect.getHeight();
    Rectangle2D rect        = 
        new Rectangle2D.Float( rectXco, rectYco, rectWidth, rectHeight );        
    Stroke      dashes  =
        new BasicStroke(
            1,
            BasicStroke.CAP_SQUARE,
            BasicStroke.JOIN_ROUND,
            1.0f,
            new float[] { 6 },
            5f
        );
    gtx.setColor( Color.RED );
    gtx.setStroke( dashes );
    gtx.draw( rect );
    // ...
}

Next: The Cartesian Plane Project, Project Layout

No comments:

Post a Comment