Skip to content

Data-driven widgets

The widgets in Module 2 dealt with single values — one text field, one checkbox, one slider. Many real applications need to display and manage collections of data: lists of files, hierarchies of items, tables of records. wxRuby3 provides three powerful widgets for this: Wx::ListCtrl, Wx::TreeCtrl, and Wx::Grid.

This lesson builds a working demo for each, using the multi-file structure from lesson 3.2. Each widget gets its own panel class in its own file. The app runs after every addition.

File structure

data_widgets_app/
├── main.rb
└── lib/
    ├── app_frame.rb
    └── panels/
        ├── list_panel.rb
        ├── tree_panel.rb
        └── grid_panel.rb

Start fresh

Create main.rb:

1
2
3
4
5
# main.rb
require 'wx'
require_relative 'lib/app_frame'

Wx::App.run { AppFrame.new.show }

Create lib/app_frame.rb:

 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
# lib/app_frame.rb

class AppFrame < Wx::Frame
  def initialize
    super(nil, title: 'Data Widgets', size: [700, 500])

    build_ui
    bind_events

    layout
    centre
  end

  private

  def build_ui
    @panel    = Wx::Panel.new(self)
    @notebook = Wx::Notebook.new(@panel)

    sizer = Wx::VBoxSizer.new
    sizer.add(@notebook, 1, Wx::EXPAND | Wx::ALL, 8)
    @panel.set_sizer(sizer)
  end

  def bind_events
    evt_close { |event| on_close(event) }
  end

  def on_close(event)
    event.skip
  end
end

Run it with ruby main.rb. An empty window with a notebook — no tabs yet. The skeleton is in place.

Step 1 — ListCtrl

Wx::ListCtrl displays a list of items, optionally with multiple columns. The report style (Wx::LC_REPORT) gives you the familiar file-manager column view.

Create lib/panels/list_panel.rb:

 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# lib/panels/list_panel.rb

class ListPanel < Wx::Panel
  FILES = [
    { name: 'report.pdf',        type: 'PDF',        size: '245 KB', modified: '2026-03-01' },
    { name: 'budget.xlsx',       type: 'Excel',      size: '88 KB',  modified: '2026-02-28' },
    { name: 'notes.txt',         type: 'Text',       size: '4 KB',   modified: '2026-03-05' },
    { name: 'photo.jpg',         type: 'Image',      size: '3.2 MB', modified: '2026-01-15' },
    { name: 'archive.zip',       type: 'ZIP',        size: '12 MB',  modified: '2026-02-10' },
    { name: 'script.rb',         type: 'Ruby',       size: '6 KB',   modified: '2026-03-06' },
    { name: 'presentation.pptx', type: 'PowerPoint', size: '1.4 MB', modified: '2026-02-20' },
  ].freeze

  def initialize(parent)
    super(parent)
    build_ui
  end

  private

  def build_ui
    @sort_asc = true

    @list = Wx::ListCtrl.new(self, style: Wx::LC_REPORT | Wx::LC_SINGLE_SEL)

    @list.insert_column(0, 'Name',     Wx::LIST_FORMAT_LEFT, 200)
    @list.insert_column(1, 'Type',     Wx::LIST_FORMAT_LEFT, 100)
    @list.insert_column(2, 'Size',     Wx::LIST_FORMAT_LEFT, 80)
    @list.insert_column(3, 'Modified', Wx::LIST_FORMAT_LEFT, 120)

    populate(@list, FILES)

    @status = Wx::StaticText.new(self, label: 'Click a row to select it')

    sizer = Wx::VBoxSizer.new
    sizer.add(@list,   1, Wx::EXPAND | Wx::ALL, 8)
    sizer.add(@status, 0, Wx::LEFT | Wx::BOTTOM, 8)
    set_sizer(sizer)

    evt_list_item_selected(@list.id) do |event|
      @status.label = "Selected: #{@list.item_text(event.index)}"
    end

    evt_list_item_activated(@list.id) do |event|
      name = @list.item_text(event.index)
      Wx::message_box("Opening: #{name}", 'Open', Wx::OK | Wx::ICON_INFORMATION)
    end

    evt_list_col_click(@list.id) do |event|
      col = event.column
      key = FILES.first.keys[col]
      @sort_asc = !@sort_asc
      sorted = FILES.sort_by { |f| f[key] }
      sorted.reverse! unless @sort_asc
      populate(@list, sorted)
    end
  end

  def populate(list, files)
    list.delete_all_items
    files.each_with_index do |file, i|
      list.insert_item(i, file[:name])
      list.set_item(i, 1, file[:type])
      list.set_item(i, 2, file[:size])
      list.set_item(i, 3, file[:modified])
    end
  end
end

Now add it to app_frame.rb — one require_relative at the top and one add_page in build_ui:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# lib/app_frame.rb

require_relative 'panels/list_panel'

class AppFrame < Wx::Frame
  ...

  def build_ui
    @panel    = Wx::Panel.new(self)
    @notebook = Wx::Notebook.new(@panel)

    @notebook.add_page(ListPanel.new(@notebook), 'Files')

    sizer = Wx::VBoxSizer.new
    sizer.add(@notebook, 1, Wx::EXPAND | Wx::ALL, 8)
    @panel.set_sizer(sizer)
  end

Run it. A Files tab appears with a four-column list. Click a row — the status label updates. Double-click — a dialog appears. Click a column header — the list sorts by that column, toggling ascending and descending.

Key points about ListCtrl:

  • Items are added with insert_item(index, first_column_text), then individual cells set with set_item(index, column, text)
  • event.index gives the row index of the selected item
  • LC_SINGLE_SEL restricts to one selection — remove it for multi-select
  • evt_list_item_selected fires on single click, evt_list_item_activated on double-click

Step 2 — TreeCtrl

Wx::TreeCtrl displays a hierarchy. Each node can have children that expand and collapse. It is the right widget for file system trees, category hierarchies, and any nested structure.

Create lib/panels/tree_panel.rb:

 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# lib/panels/tree_panel.rb

class TreePanel < Wx::Panel
  def initialize(parent)
    super(parent)
    build_ui
  end

  private

  def build_ui
    @tree = Wx::TreeCtrl.new(self, style: Wx::TR_DEFAULT_STYLE | Wx::TR_HAS_BUTTONS)

    populate_tree

    @status = Wx::StaticText.new(self, label: 'Click an item')

    sizer = Wx::VBoxSizer.new
    sizer.add(@tree,   1, Wx::EXPAND | Wx::ALL, 8)
    sizer.add(@status, 0, Wx::LEFT | Wx::BOTTOM, 8)
    set_sizer(sizer)

    evt_tree_sel_changed(@tree.id) do |event|
      @status.label = "Selected: #{@tree.item_text(event.item)}"
    end

    evt_tree_item_activated(@tree.id) do |event|
      item = event.item
      if @tree.item_has_children(item)
        @tree.toggle(item)
      else
        Wx::message_box("Opening: #{@tree.item_text(item)}", 'Open', Wx::OK)
      end
    end
  end

  def populate_tree
    root = @tree.add_root('Project')

    src = @tree.append_item(root, 'src')
    @tree.append_item(src, 'main.rb')
    @tree.append_item(src, 'app_frame.rb')

    lib = @tree.append_item(src, 'lib')
    @tree.append_item(lib, 'document.rb')
    @tree.append_item(lib, 'settings.rb')

    assets = @tree.append_item(root, 'assets')
    icons  = @tree.append_item(assets, 'icons')
    @tree.append_item(icons, 'app.png')
    @tree.append_item(icons, 'toolbar.png')

    @tree.append_item(root, 'Gemfile')
    @tree.append_item(root, 'README.md')

    @tree.expand(root)
    @tree.expand(src)
  end
end

Add it to app_frame.rb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
require_relative 'panels/list_panel'
require_relative 'panels/tree_panel'

...

  def build_ui
    @panel    = Wx::Panel.new(self)
    @notebook = Wx::Notebook.new(@panel)

    @notebook.add_page(ListPanel.new(@notebook), 'Files')
    @notebook.add_page(TreePanel.new(@notebook), 'Project')

    sizer = Wx::VBoxSizer.new
    sizer.add(@notebook, 1, Wx::EXPAND | Wx::ALL, 8)
    @panel.set_sizer(sizer)
  end

Run it. A Project tab appears alongside Files. Click any item to select it. Double-click a folder to expand or collapse it; double-click a file to open it.

To traverse the tree programmatically — useful when you need to collect all items:

1
2
3
4
5
6
7
8
9
def collect_items(item, results = [])
  results << @tree.item_text(item)
  child, cookie = @tree.first_child(item)
  while child.ok?
    collect_items(child, results)
    child, cookie = @tree.next_child(item, cookie)
  end
  results
end

The cookie pattern is wxWidgets’ mechanism for iterating children — first_child returns the first child and a cookie value; next_child uses that cookie to advance to subsequent children.

Step 3 — Grid

Wx::Grid provides a spreadsheet-style table. Use it for tabular data that users can view and edit in place.

Create lib/panels/grid_panel.rb:

 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# lib/panels/grid_panel.rb

class GridPanel < Wx::Panel
  DATA = [
    ['Widget A', '1200', '1450', '1380'],
    ['Widget B', '890',  '920',  '1100'],
    ['Widget C', '2300', '2100', '2450'],
    ['Widget D', '450',  '510',  '490'],
    ['Widget E', '1750', '1800', '1920'],
  ].freeze

  def initialize(parent)
    super(parent)
    build_ui
  end

  private

  def build_ui
    @grid = Wx::Grid.new(self)
    @grid.create_grid(DATA.length, 4)

    @grid.set_col_label_value(0, 'Product')
    @grid.set_col_label_value(1, 'Q1')
    @grid.set_col_label_value(2, 'Q2')
    @grid.set_col_label_value(3, 'Q3')

    @grid.set_col_size(0, 150)
    [1, 2, 3].each { |col| @grid.set_col_size(col, 80) }

    DATA.each_with_index do |row, r|
      row.each_with_index do |val, c|
        @grid.set_cell_value(r, c, val)
      end
    end

    # Right-align numeric columns
    right_attr = Wx::GridCellAttr.new
    right_attr.set_alignment(Wx::ALIGN_RIGHT, Wx::ALIGN_CENTRE)
    [1, 2, 3].each { |col| @grid.set_col_attr(col, right_attr) }

    # Make product column read-only
    readonly_attr = Wx::GridCellAttr.new
    readonly_attr.read_only = true
    @grid.set_col_attr(0, readonly_attr)

    @status = Wx::StaticText.new(self, label: 'Click a cell to edit')

    sizer = Wx::VBoxSizer.new
    sizer.add(@grid,   1, Wx::EXPAND | Wx::ALL, 8)
    sizer.add(@status, 0, Wx::LEFT | Wx::BOTTOM, 8)
    set_sizer(sizer)

    evt_grid_select_cell do |event|
      @status.label = "Cell: row #{event.row}, col #{event.col}"
      event.skip
    end

    evt_grid_cell_changed do |event|
      val = @grid.cell_value(event.row, event.col)
      @status.label = "Changed (#{event.row}, #{event.col}): #{val}"
      event.skip
    end
  end
end

Add it to app_frame.rb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
require_relative 'panels/list_panel'
require_relative 'panels/tree_panel'
require_relative 'panels/grid_panel'

...

  def build_ui
    @panel    = Wx::Panel.new(self)
    @notebook = Wx::Notebook.new(@panel)

    @notebook.add_page(ListPanel.new(@notebook), 'Files')
    @notebook.add_page(TreePanel.new(@notebook), 'Project')
    @notebook.add_page(GridPanel.new(@notebook), 'Sales')

    sizer = Wx::VBoxSizer.new
    sizer.add(@notebook, 1, Wx::EXPAND | Wx::ALL, 8)
    @panel.set_sizer(sizer)
  end

Run it. Three tabs, all working. Click a cell in the Sales tab — the status updates. Click a numeric cell and type to edit it. The product column is read-only.

Always call event.skip in grid event handlers — the grid needs to process the event itself after your handler runs.

Choosing the right widget

Situation Widget
Flat list, possibly with columns Wx::ListCtrl
Nested hierarchy Wx::TreeCtrl
Editable table with rows and columns Wx::Grid
Simple list, no columns needed Wx::ListBox

Wx::ListBox from Module 2 is appropriate for short, simple lists. Use ListCtrl once you need columns, sorting, or large numbers of items.


Previous: Application structure | Next: File I/O and preferences