Skip to content

Core widgets

Widgets are the controls users interact with — buttons they click, fields they type into, checkboxes they toggle. This lesson introduces the controls you will use in almost every application.

You will work with the app.rb skeleton from the previous lesson, adding one widget at a time and running the app after each addition. By the end of the lesson the skeleton will have grown into a working registration form.

The skeleton from lesson 2.1 should still be open. If you need to recreate it, go back and type it in before continuing here.

A note on Ruby-style method names

wxRuby3 is a Ruby binding for a C++ library, and many methods exist in both a C++ style and a Ruby style. Throughout this series we use the Ruby style consistently:

C++ style Ruby style
get_value value
set_value('x') value = 'x'
get_label label
set_label('x') label = 'x'
get_id id
is_checked checked?

When you read community code, wxWidgets C++ documentation, or Python wxPython examples, you will see the get_/set_ style — and it works fine in wxRuby3 too. But in this series we write Ruby, so we use the Ruby style. The mapping above is the same pattern for every widget you will encounter.

The constructor pattern

Every widget follows the same constructor:

1
Wx::SomeWidget.new(parent, keyword_options...)

The first argument is always the parent — the panel or container the widget belongs to. Everything else is a keyword option: label:, value:, style:, size:, and so on. Most have sensible defaults and can be omitted.

A note on positioning in this lesson

Proper layout in wxRuby3 is handled by sizers — that is the subject of the next lesson. For now, we use absolute positioning with pos: [x, y] to keep widgets visible and separated as we add them. This is not how real apps are built, and lesson 2.3 will replace it entirely. Think of pos: here as temporary scaffolding: it keeps things readable while we focus on learning what each widget does.

Step 1 — StaticText

Wx::StaticText displays a non-interactive label. Add the following to build_ui, replacing the existing empty method:

1
2
3
4
5
6
7
def build_ui
  @panel = Wx::Panel.new(self)
  create_status_bar
  set_status_text('Ready')

  @name_label = Wx::StaticText.new(@panel, label: 'Name:', pos: [20, 20])
end

Run the app. The label “Name:” appears at position [20, 20] — 20 pixels from the left and top edges of the panel.

To change a label’s text at runtime:

1
@name_label.label = 'Full name:'

Step 2 — TextCtrl

Wx::TextCtrl is the text input field. Update build_ui to add a name field next to the label:

1
2
@name_label = Wx::StaticText.new(@panel, label: 'Name:', pos: [20, 22])
@name_field = Wx::TextCtrl.new(@panel, value: '', pos: [80, 18], size: [200, -1])

Run it. The label and field sit on the same row, the field accepting input immediately. The size: [200, -1] sets the width to 200 pixels; -1 tells wxRuby3 to use the default height for the platform. Read the value in a method with:

1
2
3
@name_field.value            # returns the current string
@name_field.value = 'John'   # sets the value programmatically
@name_field.clear            # empties the field

Add a second row for email below the first:

1
2
3
4
@name_label  = Wx::StaticText.new(@panel, label: 'Name:',  pos: [20,  22])
@name_field  = Wx::TextCtrl.new(@panel,  value: '',        pos: [80,  18], size: [200, -1])
@email_label = Wx::StaticText.new(@panel, label: 'Email:', pos: [20,  52])
@email_field = Wx::TextCtrl.new(@panel,  value: '',        pos: [80,  48], size: [200, -1])

Run it. Two clean rows, label and field side by side.

TextCtrl has several useful style variants. You will use these in later lessons:

1
2
3
4
5
6
7
8
# Multi-line text area
Wx::TextCtrl.new(@panel, value: '', style: Wx::TE_MULTILINE)

# Password field — masks input
Wx::TextCtrl.new(@panel, value: '', style: Wx::TE_PASSWORD)

# Read-only field
Wx::TextCtrl.new(@panel, value: 'Cannot edit this', style: Wx::TE_READONLY)

Step 3 — CheckBox

Wx::CheckBox is a labelled boolean toggle. Add one below the email field:

1
2
@subscribe_cb = Wx::CheckBox.new(@panel, label: 'Subscribe to newsletter', pos: [80, 80])
@subscribe_cb.value = true    # checked by default

Run it. The checkbox appears, checked. Read its state with:

1
@subscribe_cb.value    # true or false

Now wire up a handler in bind_events so something happens when the user toggles it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def bind_events
  evt_close { |event| on_close(event) }

  evt_checkbox(@subscribe_cb.id) { |event| on_subscribe_changed(event) }
end

def on_subscribe_changed(event)
  if event.checked?
    set_status_text('Subscribed')
  else
    set_status_text('Not subscribed')
  end
end

Run it. Toggle the checkbox and watch the status bar update. This is the first time we are handling a widget event — notice the pattern: evt_checkbox in bind_events takes the widget’s id, and the block receives an event object. event.checked? is the Ruby-style equivalent of the C++ event.IsChecked() you will see in wxWidgets documentation.

Step 4 — RadioButton

Radio buttons present a set of mutually exclusive options. wxRuby3 determines group membership by creation order: all buttons created after a Wx::RB_GROUP button, and before the next Wx::RB_GROUP button, belong to the same group. This means you can have multiple independent groups on the same panel — the Wx::RB_GROUP flag acts as the boundary marker between them.

Add two groups — one for size, one for colour. Store each group as an array of buttons:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@size_buttons = [
  Wx::RadioButton.new(@panel, label: 'Small',  pos: [80,  112], style: Wx::RB_GROUP),
  Wx::RadioButton.new(@panel, label: 'Medium', pos: [148, 112]),
  Wx::RadioButton.new(@panel, label: 'Large',  pos: [220, 112]),
]
@size_buttons[1].value = true    # Medium selected by default

@colour_buttons = [
  Wx::RadioButton.new(@panel, label: 'Red',   pos: [80,  140], style: Wx::RB_GROUP),
  Wx::RadioButton.new(@panel, label: 'Green', pos: [130, 140]),
  Wx::RadioButton.new(@panel, label: 'Blue',  pos: [190, 140]),
]
@colour_buttons[0].value = true    # Red selected by default

Run it. The two groups operate completely independently — selecting “Large” has no effect on the colour selection, and vice versa. wxRuby3 knows which group each button belongs to because of the Wx::RB_GROUP flag on the first button of each group.

Now wire up handlers for both groups. Storing buttons in arrays pays off here — one registration loop per group, and the handler uses find(&:value) to locate the selected button and reads its label directly, so you never have to keep button names synchronised with a separate list of strings:

1
2
@size_buttons.each   { |rb| evt_radiobutton(rb.id) { on_group_changed } }
@colour_buttons.each { |rb| evt_radiobutton(rb.id) { on_group_changed } }
1
2
3
4
5
def on_group_changed
  size   = @size_buttons.find(&:value).label
  colour = @colour_buttons.find(&:value).label
  set_status_text("Size: #{size}  Colour: #{colour}")
end

Run it. The status bar shows both selections updating as you click either group. find(&:value) returns the first button whose value is true — the selected one — and .label gives you its text without any if/elsif chain.

Step 5 — Choice

Wx::Choice is a drop-down list that shows only the selected item. Add a country selector:

1
2
3
countries = ['Australia', 'Canada', 'New Zealand', 'United Kingdom', 'United States']
@country  = Wx::Choice.new(@panel, choices: countries, pos: [80, 144], size: [200, -1])
@country.selection = 0    # select the first item by default

Run it. The drop-down shows “Australia” and expands when clicked. Read the selection:

1
2
@country.selection           # integer index of selected item
@country.string_selection    # the selected string directly

Add and remove items at runtime if needed:

1
2
3
@country.append('Ireland')
@country.delete(0)     # remove item at index 0
@country.clear         # remove all items

Wire up a handler:

1
2
3
4
5
evt_choice(@country.id) { on_country_changed }

def on_country_changed
  set_status_text("Country: #{@country.string_selection}")
end

Run it. The status bar shows the selected country as you change it.

Wx::ComboBox is a related control that adds a text field, allowing the user to type a value not in the list. Use Wx::Choice when the options are fixed, Wx::ComboBox when the user might provide their own value.

Step 6 — Slider

Wx::Slider lets the user select a numeric value within a range by dragging a handle. Add an age slider with a label that updates as it moves:

1
2
3
@age_slider      = Wx::Slider.new(@panel, value: 25, min_value: 18, max_value: 99,
                                   pos: [80, 180], size: [180, -1])
@age_value_label = Wx::StaticText.new(@panel, label: '25', pos: [270, 183])

Wire up the slider event in bind_events:

1
2
3
evt_slider(@age_slider.id) do |event|
  @age_value_label.label = event.int.to_s
end

Run it. Drag the slider and the label updates in real time. event.int returns the slider’s current integer value — this is the Ruby equivalent of the C++ event.GetInt().

Step 7 — Button and submit

Add a submit button and wire it to a handler that reads all the values and shows them in a message box:

1
2
@submit_btn = Wx::Button.new(@panel, label: 'Submit', pos: [80, 220])
@submit_btn.set_default    # activated by pressing Enter
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
evt_button(@submit_btn.id) { on_submit }

def on_submit
   size   = @size_buttons.find(&:value).label
   colour = @colour_buttons.find(&:value).label

    summary = [
        "Name:      #{@name_field.value}",
        "Email:     #{@email_field.value}",
        "Subscribe: #{@subscribe_cb.value ? 'Yes' : 'No'}",
        "Size:      #{size}",
        "Colour:    #{colour}",
        "Country:   #{@country.string_selection}",
        "Age:       #{@age_slider.value}",
    ].join("\n")

  Wx::message_box(summary, 'Form values', Wx::OK | Wx::ICON_INFORMATION)
end

Run it. Fill in the form and click Submit — a native message dialog shows everything you entered.

The finished lesson app

Your complete app.rb at the end of this lesson:

  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
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
require 'wx'

class AppFrame < Wx::Frame
  def initialize
    super(nil, title: 'Widget Demo', size: [500, 520])

    build_ui
    bind_events

    centre
  end

  private

  def build_ui
    @panel = Wx::Panel.new(self)
    create_status_bar
    set_status_text('Ready')

    @name_label  = Wx::StaticText.new(@panel, label: 'Name:',  pos: [20,  22])
    @name_field  = Wx::TextCtrl.new(@panel,   value: '',       pos: [80,  18], size: [200, -1])

    @email_label = Wx::StaticText.new(@panel, label: 'Email:', pos: [20,  52])
    @email_field = Wx::TextCtrl.new(@panel,   value: '',       pos: [80,  48], size: [200, -1])

    @subscribe_cb = Wx::CheckBox.new(@panel, label: 'Subscribe to newsletter', pos: [80, 80])
    @subscribe_cb.value = true

    @size_buttons = [
      Wx::RadioButton.new(@panel, label: 'Small',  pos: [80,  112], style: Wx::RB_GROUP),
      Wx::RadioButton.new(@panel, label: 'Medium', pos: [148, 112]),
      Wx::RadioButton.new(@panel, label: 'Large',  pos: [220, 112]),
    ]
    @size_buttons[1].value = true

    @colour_buttons = [
      Wx::RadioButton.new(@panel, label: 'Red',   pos: [80,  140], style: Wx::RB_GROUP),
      Wx::RadioButton.new(@panel, label: 'Green', pos: [130, 140]),
      Wx::RadioButton.new(@panel, label: 'Blue',  pos: [190, 140]),
    ]
    @colour_buttons[0].value = true

    countries = ['Australia', 'Canada', 'New Zealand', 'United Kingdom', 'United States']
    @country  = Wx::Choice.new(@panel, choices: countries, pos: [80, 172], size: [200, -1])
    @country.selection = 0

    @age_slider      = Wx::Slider.new(@panel, value: 25, min_value: 18, max_value: 99,
                                       pos: [80, 208], size: [180, -1])
    @age_value_label = Wx::StaticText.new(@panel, label: '25', pos: [270, 211])

    @submit_btn = Wx::Button.new(@panel, label: 'Submit', pos: [80, 250])
    @submit_btn.set_default
  end

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

    evt_checkbox(@subscribe_cb.id) { |event| on_subscribe_changed(event) }

    @size_buttons.each   { |rb| evt_radiobutton(rb.id) { on_group_changed } }
    @colour_buttons.each { |rb| evt_radiobutton(rb.id) { on_group_changed } }

    evt_choice(@country.id) { on_country_changed }

    evt_slider(@age_slider.id) do |event|
      @age_value_label.label = event.int.to_s
    end

    evt_button(@submit_btn.id) { on_submit }
  end

  def on_close(event)
    event.skip
  end

  def on_subscribe_changed(event)
    set_status_text(event.checked? ? 'Subscribed' : 'Not subscribed')
  end

  def on_group_changed
    size   = @size_buttons.find(&:value).label
    colour = @colour_buttons.find(&:value).label
    set_status_text("Size: #{size}  Colour: #{colour}")
  end

  def on_country_changed
    set_status_text("Country: #{@country.string_selection}")
  end

  def on_submit
    size   = @size_buttons.find(&:value).label
    colour = @colour_buttons.find(&:value).label

    summary = [
      "Name:      #{@name_field.value}",
      "Email:     #{@email_field.value}",
      "Subscribe: #{@subscribe_cb.value ? 'Yes' : 'No'}",
      "Size:      #{size}",
      "Colour:    #{colour}",
      "Country:   #{@country.string_selection}",
      "Age:       #{@age_slider.value}",
    ].join("\n")

    Wx::message_box(summary, 'Form values', Wx::OK | Wx::ICON_INFORMATION)
  end
end

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

The widgets are laid out with absolute pos: values — workable, but inflexible. The form does not resize gracefully and the positioning is manual arithmetic. Sizers replace all of that in the next lesson, and you will apply them to this exact app to see the difference.


Previous: Frames and panels | Next: Layout with sizers