Skip to content

Images and bitmaps

wxRuby3 has two distinct classes for image data: Wx::Image and Wx::Bitmap. Understanding the difference between them is essential before trying to display or manipulate images.

Wx::Image is a platform-independent image container. It holds raw pixel data, supports loading and saving in various formats, and provides transformation operations like scaling, rotation, and colour manipulation. It is the right class for image processing.

Wx::Bitmap is a platform-specific image optimised for display. It is what the DC drawing methods expect. You cannot scale or transform a bitmap directly — convert it to an image first, transform, then convert back.

The workflow is: load as Image → transform → convert to Bitmap → draw.

Loading images

1
2
img = Wx::Image.new('path/to/photo.jpg')
img = Wx::Image.new('path/to/icon.png')

Wx::Image.new detects the format from the file extension. Supported formats include JPEG, PNG, GIF, BMP, TIFF, and WebP.

Always check that loading succeeded before using the image:

1
2
3
4
5
6
img = Wx::Image.new
if img.load_file('photo.jpg')
  # image loaded successfully
else
  # handle error
end

Displaying images

Never use Wx::StaticBitmap for resizable image display. Its client_size reflects the bitmap dimensions rather than the available space, which means resizing the window has no effect. Use a plain Wx::Panel with evt_paint instead:

1
2
3
4
5
6
7
8
9
@panel = Wx::Panel.new(self)
@bitmap = Wx::Bitmap.new(Wx::Image.new('photo.jpg'))

@panel.evt_paint do
  @panel.paint do |dc|
    dc.draw_bitmap(@bitmap, 0, 0, false)
  end
end
@panel.evt_size { @panel.refresh }

The third argument to draw_bitmap is use_mask — pass false for opaque images, true if the bitmap has a transparency mask.

Scaling images

Scale via Wx::Image#scale, then convert back to a bitmap:

1
2
3
img    = Wx::Image.new('photo.jpg')
scaled = img.scale(400, 300, Wx::IMAGE_QUALITY_HIGH)
bitmap = Wx::Bitmap.new(scaled)

Wx::IMAGE_QUALITY_HIGH uses bicubic interpolation — slower but produces much better results than the default nearest-neighbour. Always use it for display scaling.

Fit to a bounding box

To scale an image to fit within a box while preserving aspect ratio, take the minimum of the two scale factors:

1
2
3
4
5
6
7
box_w, box_h = 400, 300

scale = [box_w.to_f / img.width, box_h.to_f / img.height].min
fit_w = (img.width  * scale).to_i
fit_h = (img.height * scale).to_i

fitted = Wx::Bitmap.new(img.scale(fit_w, fit_h, Wx::IMAGE_QUALITY_HIGH))

This is the pattern used in every image thumbnail, photo browser, and gallery app.

Off-screen drawing with MemoryDC

Wx::MemoryDC.draw_on creates a DC that targets a bitmap rather than the screen. All the drawing methods from lesson 4.1 work identically. Use it to compose images programmatically — thumbnails, contact sheets, charts that need to be saved to file.

1
2
3
4
5
6
7
8
9
bmp = Wx::Bitmap.new(400, 300)
Wx::MemoryDC.draw_on(bmp) do |dc|
  dc.set_background(Wx::Brush.new(Wx::WHITE))
  dc.clear
  dc.set_pen(Wx::Pen.new(Wx::BLUE, 2))
  dc.draw_circle(200, 150, 80)
  # ... more drawing ...
end
# bmp is now complete and ready to display or save

The bitmap is finalised when the block exits. The DC is only valid inside the block — the same constraint as the paint DC.

Saving images to file

Convert a bitmap to an image and call save_file:

1
2
bitmap.convert_to_image
      .save_file('output.png', Wx::BITMAP_TYPE_PNG)

Supported output formats include Wx::BITMAP_TYPE_PNG, Wx::BITMAP_TYPE_JPEG, and Wx::BITMAP_TYPE_BMP. PNG is recommended for graphics and screenshots; JPEG for photographs.

This works for both bitmaps loaded from files and bitmaps composed with MemoryDC.draw_on — they are identical from the DC’s perspective.

Resizable image display pattern

The complete pattern for a panel that displays an image and scales it to fit when resized:

 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
class ImagePanel < Wx::Panel
  def initialize(parent)
    super(parent)
    @bitmap = nil
    set_background_colour(Wx::BLACK)
    evt_paint { on_paint }
    evt_size  { refresh }
  end

  def load(path)
    img     = Wx::Image.new(path)
    @bitmap = Wx::Bitmap.new(img)
    refresh
  end

  private

  def on_paint
    paint do |dc|
      return unless @bitmap
      cw, ch = client_size
      img    = @bitmap.convert_to_image
      scale  = [cw.to_f / img.width, ch.to_f / img.height].min
      fit_w  = (img.width  * scale).to_i
      fit_h  = (img.height * scale).to_i
      scaled = Wx::Bitmap.new(img.scale(fit_w, fit_h, Wx::IMAGE_QUALITY_HIGH))
      x = (cw - fit_w) / 2
      y = (ch - fit_h) / 2
      dc.draw_bitmap(scaled, x, y, false)
    end
  end
end

This pattern is used in the Gallery Tiler and GPS Track Viewer in Module 6.

Performance note: Scaling inside the paint handler works well for occasional repaints, but scaling a large image on every resize event can make the UI feel sluggish. For better performance, cache the scaled bitmap and only rescale when the panel size changes meaningfully.

See image_demo.rb for the complete working demonstration including contact sheet composition and file saving.

Download image_demo.rb

What to take forward

  • Wx::Image — for loading, saving, and transforming. Wx::Bitmap — for display.
  • Scale via img.scale(w, h, Wx::IMAGE_QUALITY_HIGH), then Wx::Bitmap.new(scaled)
  • Use evt_paint + draw_bitmap for resizable image display — never Wx::StaticBitmap
  • Wx::MemoryDC.draw_on(bmp) { |dc| } for off-screen composition
  • Save with bitmap.convert_to_image.save_file(path, Wx::BITMAP_TYPE_PNG)
  • Fit to a box: scale = [box_w / img.width, box_h / img.height].min

Previous: Drawing with device contexts | Next: Rich text