Concepts / Drawing Lines and Pixels with Graphics

Drawing Lines and Pixels with Graphics

The rectangle outline is drawn one pixel wider and one pixel taller than the width and height passed in.

  • Programming

Why a Border Seems to Miss Its Edge

Suppose you want to draw a border around a component, the rectangular area a program paints on screen. The natural first idea is to ask for a rectangle whose width and height match the component's size, using getWidth() and getHeight(). Both values feel like the right numbers to pass in, and it seems reasonable that a rectangle of width 100 and height 50 would cover exactly 100 by 50 pixels.

Both of those assumptions lead to trouble. The border ends up with its right and bottom edges missing or misplaced. This article traces what the drawing methods of the Graphics class actually paint, one pixel at a time, so you can see where the missing edge goes and how to fix it. Predicting the result before reading on is a good habit here, because the answer is not what the numbers suggest.

Every method in this article belongs to the Graphics class, so each one is called through a Graphics object. The examples refer to that object as g, but the programmer chooses the real name of the graphics context.

Tracing a Rectangle Column by Column

The drawRect method draws the outline of a rectangle. Its upper left corner sits at the point (x, y), and its width and height are the values you pass in. That description is only part of the story. The outline is drawn with the same pen used for lines, and that pen colors pixels one pixel to the right of the geometric line and one pixel below it.

The result is that the outline's real width is the width you passed in plus one, and its real height is the height you passed in plus one. The extra pixel column lies past the right edge, and the extra pixel row lies past the bottom edge. The pixels you see extend one beyond the numbers you typed. This is the key correction to the belief from the opening section.

Read the diagram below as a strip of pixel columns. Each box is one column, numbered from the left. Passing a width of 10 with x equal to 0 paints the outline across columns 0 through 10, which is eleven columns in total. Column 10 is the extra column the pen adds beyond the width you requested.

Column 0left edge of outlineColumn 1paintedColumn 2paintedColumn 3paintedColumn 4paintedColumn 5paintedColumn 6paintedColumn 7paintedColumn 8paintedColumn 9paintedColumn 10extra pixel past width
Which pixel columns does the outline of a rectangle with width 10 actually paint, and why does it reach column 10 instead of stopping at column 9?

What do you think happens?

Inside paint, a component is 100 pixels wide and 50 pixels tall. You call g.drawRect(0, 0, getWidth(), getHeight()). Will the right and bottom edges of the border be visible?

  • Yes, the outline fits exactly inside the component

  • No, the outline reaches one column and one row past the component's last visible pixels

Reveal answer
Answer:

No, the outline reaches one column and one row past the component's last visible pixels

The outline is drawn one pixel wider and one pixel taller than the width and height passed in. With a width of 100, the outline reaches column 100, but the component's visible columns run from 0 to 99. Column 100 lies outside the boundary, so it is ignored, and the same happens to the bottom row.

Parameters of the Four Drawing Methods

The four methods drawString, drawLine, drawRect, and drawOval share one pattern. Each is a method of the Graphics object, and each takes integer coordinates or sizes. The table below summarizes what each argument controls. The entries for drawRect, drawLine, and drawString follow directly from the source's descriptions. The source pack does not spell out the individual arguments of drawOval, so that row is marked accordingly.

MethodHorizontal argumentVertical argumentSize or endpoint argument
drawLine(x1, y1, x2, y2)x1 and x2 locate the two endpoints horizontallyy1 and y2 locate the two endpoints verticallyThe second point (x2, y2) is the endpoint of the line
drawRect(x, y, width, height)x is the left side of the upper left cornery is the top side of the upper left cornerwidth and height set the size; the outline paints one pixel more in each direction
drawString(text, x, y)x marks the left end of the stringy marks the baseline the characters rest onNot a size argument; the font and color come from the graphics context
drawOval(x, y, width, height)Takes an integer horizontal argument (meaning not detailed in the source pack)Takes an integer vertical argument (meaning not detailed in the source pack)Takes integer sizes (meaning not detailed in the source pack)

What each argument controls. The drawOval row reflects only what the source pack states: that the method takes integer coordinates or sizes.

xhorizontal position ofupper left corneryvertical position of upperleft cornerwidthoutline paints one columnmoreheightoutline paints one row more
For drawRect, which argument sets the horizontal position, which sets the vertical position, and which sets the size?

The drawLine method draws a line 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 rather than 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. The y coordinate marks the baseline, the horizontal line on which the characters rest. Some characters, such as the tail of a lowercase y or g, extend below the baseline, which is why the y value is not the top of the text.

Fixing the Border with getWidth and getHeight

The source gives the standard fix for drawing a border around a component. Subtract one from the width and height. The drawn outline's extra pixel column and row then land on the last pixel inside the component, rather than past its edge. The source's example is shown below.

java
Notes on the code
  • 0, 0 places the upper left corner at the top left of the component
  • getWidth() - 1 makes the outline's extra column land on the last visible column
  • getHeight() - 1 makes the outline's extra row land on the last visible row
Worked example

Choosing the width for a 100-pixel component

A component is 100 pixels wide. Which width should be passed to drawRect with x equal to 0 so that the right edge is still visible?

  1. 1
    Identify the visible columns

    A component 100 pixels wide has visible columns numbered 0 through 99.

  2. 2
    Apply the one-pixel rule

    drawRect paints one column past the width passed in, so with x equal to 0 the outline reaches column width.

  3. 3
    Solve for the width

    To keep the outline inside columns 0 through 99, the column reached must be 99, so the width passed in is 99.

  4. 4
    Check with getWidth

    Passing getWidth() - 1, which is 100 - 1, gives 99, matching the calculation.

Result

Pass getWidth() - 1, which is 99, so the right edge lands on column 99, the last visible pixel. The height follows the same reasoning with getHeight() - 1.

Tracing What Gets Clipped at the Edge

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. The result is that a shape extending past the edge is partly drawn, with the parts inside the boundary kept and the parts outside discarded.

The before-and-after view shows the difference between the area you asked for and the area you actually see. The request reaches column 150, but only the columns from 0 through 99 belong to the component, so the rest is discarded. Your intended shape is cut short at the boundary, and no error is reported.

Mistakes Beginners Make with Border Sizes

  • Common mistake

    Assuming drawRect with width 100 covers exactly 100 pixels

    What it looks like
    g.drawRect(0, 0, 100, 50) in a 100 by 50 component
    Why it fails

    The outline is drawn one pixel wider and one pixel taller than the values passed in, so it reaches column 100 and row 50.

    Do this instead

    Pass getWidth() - 1 and getHeight() - 1 to keep the right and bottom edges inside the component.

  • Common mistake

    Passing getWidth() and getHeight() directly to draw a border

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

    The extra pixel column and row lie just outside the component, so they are discarded and the right and bottom edges are missing.

    Do this instead

    Subtract one from each size before passing it to drawRect.

  • Common mistake

    Treating the y value of drawString as the top of the text

    What it looks like

    g.drawString("gym", 10, 20) expecting the text to start at row 20

    Why it fails

    The y coordinate marks the baseline, and descenders such as the tail of g extend below it.

    Do this instead

    Think of y as the line the characters rest on, and allow for letters that extend below it.

  • Common mistake

    Expecting a negative width to draw a flipped or partial rectangle, when nothing is drawn at all

Practice: Sizing a Border

EASY

A component is 240 pixels wide and 80 pixels tall. Write the drawRect call with x and y equal to 0 that draws a border whose right and bottom edges are the last visible pixels inside the component. Then explain, using the one-pixel rule, why passing 240 and 80 directly would miss those edges.

Hints
  • The outline reaches one column and one row past the width and height you pass in.
  • The last visible column of a 240-pixel component is column 239.
  • Subtract one from each size before passing it to drawRect.

Key Takeaways

  • A rectangle drawn with a given width and height is painted one pixel wider and one pixel taller than those values, because the pen colors pixels one to the right of and one below the geometric line.
  • drawRect takes x and y for the upper left corner and width and height for its size; drawLine takes two endpoints; drawString takes a left x and a baseline y; the source pack gives only general guidance on drawOval's integer arguments.
  • To draw a border that stays inside a component, pass getWidth() - 1 and getHeight() - 1 so the extra pixel row and column land on the last visible pixels.
  • Drawing outside a component's boundaries is ignored and gets clipped, while a negative width or height causes nothing to be drawn at all.