Skip to content

Drawing with device contexts

wxRuby3 provides a rich 2D drawing API built around the concept of a device context (DC). A DC is an abstract drawing surface — the same drawing code works whether you are painting to a panel on screen, composing an image in memory, or writing to an SVG file. You work with a DC by setting pens, brushes, and fonts, then calling drawing methods to place shapes and text.

This lesson covers the fundamentals of DC drawing and builds up to a practical bar chart that resizes correctly with the window.

The paint event

All drawing in wxRuby3 happens inside an evt_paint handler. The event fires whenever the panel needs repainting — on first show, after being uncovered by another window, and after resize. You never call the paint handler directly; the event loop calls it when needed.

1
2
3
4
5
@panel = Wx::Panel.new(self)
@panel.set_background_colour(Wx::WHITE)

@panel.evt_paint { on_paint }
@panel.evt_size  { @panel.refresh }

The evt_size handler triggers a repaint when the window is resized. Without it, the drawing would only update on the next natural repaint.

Inside the handler, call paint on the panel to get a DC:

1
2
3
4
5
def on_paint
  @panel.paint do |dc|
    # all drawing happens here
  end
end

Never store the DC or use it outside the paint block — it is only valid for the duration of the block.

Step 1 — Pens, brushes, and shapes

A pen defines the colour and width of lines and outlines. A brush defines the fill colour of shapes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Lines — pen only
dc.set_pen(Wx::Pen.new(Wx::BLACK, 1))
dc.draw_line(10, 10, 200, 10)

dc.set_pen(Wx::Pen.new(Wx::RED, 3))
dc.draw_line(10, 20, 200, 20)

# Filled rectangle
dc.set_pen(Wx::Pen.new(Wx::BLACK, 1))
dc.set_brush(Wx::Brush.new(Wx::Colour.new(70, 130, 180)))
dc.draw_rectangle(10, 35, 80, 40)    # x, y, width, height

# Outline only — transparent brush
dc.set_brush(Wx::TRANSPARENT_BRUSH)
dc.draw_rectangle(110, 35, 80, 40)

# Circle — x, y = centre, r = radius
dc.set_pen(Wx::Pen.new(Wx::Colour.new(220, 20, 60), 2))
dc.set_brush(Wx::Brush.new(Wx::Colour.new(255, 200, 200)))
dc.draw_circle(250, 55, 30)

# Ellipse — x, y = top-left, width, height
dc.set_brush(Wx::Brush.new(Wx::YELLOW))
dc.draw_ellipse(300, 35, 100, 40)

The pen and brush stay set until you change them — you don’t need to reset them before every draw call, only when you want a different style.

Wx::TRANSPARENT_BRUSH is a pre-defined constant for an invisible fill. Use it whenever you want an outline-only shape.

Step 2 — Text and fonts

Set the font with dc.font = and the text colour with dc.set_text_foreground:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Default font
dc.set_text_foreground(Wx::BLACK)
dc.draw_text('Default font', 10, 10)

# Bold
dc.font = Wx::Font.new(12, Wx::FONTFAMILY_DEFAULT,
                       Wx::FONTSTYLE_NORMAL, Wx::FONTWEIGHT_BOLD)
dc.draw_text('Bold 12pt', 10, 30)

# Italic
dc.font = Wx::Font.new(12, Wx::FONTFAMILY_DEFAULT,
                       Wx::FONTSTYLE_ITALIC, Wx::FONTWEIGHT_NORMAL)
dc.draw_text('Italic 12pt', 10, 55)

# Monospace
dc.font = Wx::Font.new(11, Wx::FONTFAMILY_TELETYPE,
                       Wx::FONTSTYLE_NORMAL, Wx::FONTWEIGHT_NORMAL)
dc.draw_text('Monospace 11pt', 10, 80)

Wx::Font.new takes four arguments: point size, family, style, and weight. The font families you will use most are Wx::FONTFAMILY_DEFAULT (system UI font) and Wx::FONTFAMILY_TELETYPE (monospace).

Measuring text

Use dc.get_text_extent(text) to measure a string before drawing it. This returns [width, height] in pixels for the current font:

1
2
3
4
5
6
text = 'Measured string'
tw, th = dc.get_text_extent(text)

# Centre the text in a 200px wide area
x = (200 - tw) / 2
dc.draw_text(text, x, 10)

Measuring text is essential for centring labels above bars in a chart, right-aligning numbers in a table, or fitting text within a fixed-width area.

Draw order

Later draws paint over earlier ones. To draw a bounding box around text without obscuring it:

1
2
3
dc.draw_text(text, x, y)                   # text first
dc.set_brush(Wx::TRANSPARENT_BRUSH)
dc.draw_rectangle(x, y, tw, th)            # outline on top

Step 3 — Coordinates and client size

All coordinates are in pixels from the top-left corner of the panel. @panel.client_size returns [width, height] — use these to make your drawing adapt to the panel’s current size:

1
2
3
4
5
6
7
8
9
def on_paint
  @panel.paint do |dc|
    w, h = @panel.client_size

    # Draw a line from corner to corner
    dc.draw_line(0, 0, w, h)
    dc.draw_line(w, 0, 0, h)
  end
end

Because client_size is read fresh inside the paint handler on every repaint, the drawing automatically adapts to any window size. This is why evt_size { @panel.refresh } produces correctly resizing graphics — the refresh triggers a repaint, and the repaint reads the new size.

Step 4 — Bar chart

A bar chart ties everything together — calculating bar positions from data, drawing filled rectangles, centering text labels, and adapting to the panel size.

The key calculation: given a chart area of height chart_h and a maximum data value max_value, the pixel height of a bar with value v is:

1
2
bar_h = (chart_h * v / max_value).to_i
bar_y = margin_top + chart_h - bar_h   # bars grow upward from the baseline

For centring a label above a bar of width bar_width:

1
2
lw, _lh = dc.get_text_extent(label)
label_x = bar_x + (bar_width - lw) / 2

See drawing_demo.rb for the complete chart implementation including gridlines, axis labels, and value labels above each bar.

Download drawing_demo.rb

What to take forward

  • All drawing happens inside @panel.paint { |dc| } — never outside it
  • evt_size { @panel.refresh } makes the drawing resize correctly
  • Pen = line/outline style; brush = fill style
  • dc.get_text_extent(text) returns [width, height] for the current font — use it to centre and align text
  • Read @panel.client_size inside the paint handler to get the current dimensions
  • Draw order matters — later draws paint over earlier ones; use Wx::TRANSPARENT_BRUSH for outline-only shapes

Previous: Module 3 | Next: Images and bitmaps