Drawing Shapes with Graphics2D
Stroke is an interface, and BasicStroke is the class that implements it and produces the stroke objects you use.
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.
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.
The first line is drawn with the default thickness. The second and third lines are drawn with the thickness-3 stroke.- 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
setStrokeand use the new stroke.
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?
Reveal 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.
- 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.
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.
| Method | Parameters | What each parameter controls |
|---|---|---|
| drawLine | x1, y1, x2, y2 | The two endpoints of the line. The line runs from (x1, y1) to (x2, y2). |
| drawRect | x, y, width, height | x 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. |
| drawString | text, x, y | x 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. |
| drawOval | x, y, width, height | The 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 likeg.drawRect(0, 0, 100, 50)Why it failsThe 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 insteadExpect 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 likeg.drawRect(0, 0, getWidth(), getHeight())Why it failsThe extra pixel lands past the last visible column and row, so the right and bottom edges disappear.
Do this insteadSubtract 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 failsOnce a BasicStroke object exists, none of its options can be altered.
Do this insteadCreate 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 failsA line drawn before
setStrokewas rendered under the old settings and keeps its old appearance.
Practice: Predicting the Border
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.
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
setStrokeon 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 andgetHeight()-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.