Concepts / Drawing Shapes with Graphics2D

Drawing Shapes with Graphics2D

Stroke is an interface, and BasicStroke is the class that implements it and produces the stroke objects you use.

  • Programming

Why Default Lines Look Plain

Suppose you already draw lines and shapes on a Graphics2D object, and every line comes out at the same default thickness with the same plain appearance. You want more control: a heavier line for emphasis, or a line whose ends look different. Java lets you attach a description of the line's appearance to the drawing context. That description is called a stroke. This article explains how to create a stroke, attach it, and reason about what it changes, and then turns to the rectangle, line, oval, and text methods whose coordinates often cause confusion.

Keep a simple question in mind as you read: when a drawing call runs, which settings is it using? The answer depends on the order of your code, and the rest of the article traces that order step by step.

Stroke Interface and the BasicStroke Class

Stroke is an interface. An interface describes what a stroke object must be able to do, but it is not something you instantiate directly. The class that actually produces stroke objects is BasicStroke, and it implements the Stroke interface. So when you want a stroke, your code names BasicStroke in the constructor call, and the resulting object can be used anywhere a Stroke is expected.

Stroke interfacedescribes behaviorBasicStroke classimplements Strokewidththicknesscap styleline endsjoin stylecornersimplemented byholdsholdsholds
Read the diagram as a map of ownership. The Stroke interface at the top promises behavior, BasicStroke is the concrete class that keeps the actual option values, and every option belongs to the BasicStroke object, fixed when that object is built.

Because the interface only describes behavior, the concrete work of storing thickness, end style, and corner style happens inside BasicStroke. Your code therefore constructs a BasicStroke, while the variable that holds it can be typed as the general Stroke interface when that is all the receiving code needs.

Installing a Stroke on Graphics2D

A stroke becomes useful only after it is attached to the Graphics2D object that performs the drawing. The usual sequence has two steps. First you construct a BasicStroke object and keep a reference to it. Then you pass that object to the setStroke method of the Graphics2D object. The code below builds a stroke for solid lines whose thickness is the integer 3.

java
Output
The first line is drawn with the default thickness. The second and third lines are drawn with the thickness-3 stroke.
Notes on the code
  • The BasicStroke object is created once and kept in the variable thick.
  • The line at y=10 is drawn before setStroke, so it keeps the default appearance.
  • The lines at y=30 and y=50 are drawn after setStroke and use the new stroke.
default strokebefore setStrokewidth 3 strokeinstalled on g2later drawLine callsuse width 3g2.setStroke(thick)next draw calls
The call to setStroke changes the state of g2 itself. Every later draw call uses the installed stroke until another stroke is installed, while earlier lines keep the appearance they were drawn with.

What do you think happens?

You have just installed a stroke built with the value 3. Before reading the answer, predict how the next line should look compared with the usual thickness. Which line looks thicker: the one drawn before setStroke or the one drawn after?

  • The line drawn before setStroke, because it was drawn first

  • The line drawn after setStroke, because it uses the width 3 stroke

Reveal answer
Answer:

The line drawn after setStroke is thicker, because it uses the width 3 stroke. The earlier line keeps the default appearance it was rendered with.

The call to setStroke changes the state of g2. Lines drawn before the call keep the old appearance, and lines drawn after the call pick up the new settings.

The inline form is also valid. You can write g2.setStroke(new BasicStroke(3)) and skip the named variable. The named-variable form is easier to reuse across several drawing calls, while the inline form is shorter when the stroke is needed only once.

Fractional Widths and the F Suffix

The width argument is not limited to whole numbers. The thickness is a value of type float, so a fractional thickness is also valid. An integer like 3 is accepted because Java can widen it to the float type. When you need a value such as two and a half, write the float literal with the F suffix, as in 2.5F.

java
Notes on the code
  • The F suffix marks the literal as a float.
  • The stroke installed here affects every later draw call on g2.

The Full BasicStroke Constructor

A thick line raises a question that a thin one hides: what should happen at its two endpoints? Think of drawing with a wide piece of chalk on a board. The end of the mark would naturally be rounded. On a screen, you can also choose a squared-off cut. BasicStroke leaves that decision to you, and the choice is made through the cap style argument. Corners work similarly. Where two thick segments meet, there are several ways to fill the corner, and the join style argument chooses among them.

The constructor used so far takes only a width. A fuller constructor accepts every option at once. Because all options must be supplied when the object is built, this constructor is the one place where you control the complete appearance of a stroke. Once the object exists, none of its options can be altered, so if you need a different style you create a new stroke.

Each argument answers one question about the line. The first asks how thick. The next two ask how the line ends and how its corners are joined. The miter limit and the dash arguments then govern the remaining details. Read the constructor from left to right, and you are reading the appearance decisions in the order a designer would make them. The source does not list the named cap and join values individually, so consult the BasicStroke documentation for those constants.

EASY

Draw a rectangle inside a component with a wide stroke, then compare its four corners. Describe how the corners differ from one another and from the straight edges.

Hints
  • Use a stroke wide enough that the corners are easy to see.
  • Look at where two thick segments meet, not just at the middle of each edge.

Why Rectangles Cover One Extra Pixel

Parameters of the Drawing Methods

The four methods in this group are methods of the Graphics object, so each one is called through a Graphics variable. The examples refer to that object as g, but the programmer chooses the actual name. Each method takes integer coordinates or sizes, and the table below summarizes what each parameter controls for the methods the source describes.

MethodParametersWhat each parameter controls
drawLinex1, y1, x2, y2The two endpoints of the line. The line runs from (x1, y1) to (x2, y2).
drawRectx, y, width, heightx and y place the upper left corner. width and height give the size, but the outline is drawn one pixel wider and taller than those values.
drawStringtext, x, yx marks the left end of the string. y marks the baseline, the line on which the characters rest. Descenders such as the tail of y or g extend below it.
drawOvalx, y, width, heightThe source names drawOval in this group but does not describe its arguments in detail. Consult the Graphics documentation for its exact meaning.

Parameters of the drawing methods covered in the source. Descriptions for drawOval are not given in the source pack.

A line is drawn from the point (x1, y1) to the point (x2, y2). The pen that draws it extends one pixel to the right and one pixel down from the point where the pen sits. The source also notes that coordinates refer to the lines between pixels, not to the pixels themselves. Keeping that picture in mind explains why a single point is not invisible.

The drawString method draws text using the current color and font of the graphics context. The x coordinate marks the left end of the string, and the y coordinate marks the baseline. That is why the y value is not the top of the text.

What Happens Outside the Component

All of the drawing commands in this group respect the boundaries of the component. Any drawing done outside those boundaries is ignored. This is the reason the getWidth()-1 adjustment matters: pixels placed past the component's right or bottom edge are simply not shown.

Mistakes Beginners Make with Rectangle Sizes

  • Common mistake

    Assuming a rectangle of width 100 and height 50 covers exactly 100 by 50 pixels

    What it looks like
    g.drawRect(0, 0, 100, 50)
    Why it fails

    The outline is drawn with a pen that colors one pixel to the right and one pixel below the geometric line, so the outline is one pixel wider and one pixel taller than the numbers passed in.

    Do this instead

    Expect the drawn outline to extend one pixel past the values you typed, and plan sizes with that extra pixel in mind.

  • Common mistake

    Drawing a component border with the component's own size

    What it looks like
    g.drawRect(0, 0, getWidth(), getHeight())
    Why it fails

    The extra pixel lands past the last visible column and row, so the right and bottom edges disappear.

    Do this instead

    Subtract one from each size: g.drawRect(0, 0, getWidth()-1, getHeight()-1).

  • Common mistake

    Changing a stroke's options after it is built

    Why it fails

    Once a BasicStroke object exists, none of its options can be altered.

    Do this instead

    Create a new BasicStroke with the style you need and install it with setStroke.

  • Common mistake

    Expecting earlier lines to change when a new stroke is installed

    Why it fails

    A line drawn before setStroke was rendered under the old settings and keeps its old appearance.

Practice: Predicting the Border

MEDIUM

Inside a paint method, a component is 300 pixels wide and 120 pixels tall. You want a visible border that touches all four edges of the component. Write the drawRect call you would use, and explain why the width and height are not 300 and 120.

Hints
  • The visible columns run from 0 to 299.
  • The outline's extra pixel must land on the last visible column and row.
MEDIUM

Create a BasicStroke with a fractional width, install it on g2, then draw one line before and one line after the call. Predict which line is thicker before running the code, and explain the result using the rule about when a stroke applies.

Hints
  • Remember that the float suffix F is needed for values such as 2.5.
  • Think about which drawing calls use the stroke that is currently installed.

Key Takeaways

  • Stroke is an interface, and BasicStroke is the class that implements it and holds the options you choose when you construct the object.
  • A stroke is attached with setStroke on a Graphics2D object, and drawing calls made afterward use it; earlier lines keep their old appearance.
  • The full BasicStroke constructor fixes thickness, cap style, join style, miter limit, and dash settings when the object is built.
  • A rectangle outline is drawn one pixel wider and one pixel taller than the width and height passed in, so use getWidth()-1 and getHeight()-1 to keep the right and bottom edges inside a component.
  • Drawing outside a component's boundaries is ignored, while a negative rectangle width or height draws nothing.