Skip to content

From Rails to wxRuby: the mental model shift

If you have built Rails applications, you already understand MVC, event-driven thinking, and clean separation of concerns. That knowledge transfers. But Rails also builds habits that actively work against you in desktop development — assumptions so deeply embedded that you may not even recognise them as assumptions.

This lesson names those assumptions and replaces them with the mental models that make wxRuby3 code easy to reason about.

The request/response habit

A Rails app lives inside a cycle: a request arrives, the app processes it, a response goes out, and the app forgets everything. Each request is independent. State lives in the database or the session, not in memory. The process itself is stateless between requests.

This model is so natural for web development that it stops feeling like a model — it just feels like how software works.

It is not how desktop software works.

A desktop app starts, and then it runs. It does not process a request and exit. It sits in memory, maintaining its full state, waiting for the user to do something. When the user clicks a button, moves a slider, or types in a text field, the app responds — but it does not reset. Everything it knew before the click is still there after the click.

This is the first and most important shift: from stateless request handlers to a stateful, persistent process.

The event loop

At the heart of every wxRuby3 application is an event loop. You do not write the loop — wxRuby3 runs it for you — but understanding what it does makes everything else clearer.

The event loop looks roughly like this:

while app is running:
    wait for the next event
    dispatch the event to the right handler
    repeat

Events come from everywhere: mouse clicks, keyboard input, window resizes, timer ticks, menu selections, system notifications. The loop picks each one up and routes it to whatever handler you have registered for that event type on that widget.

Your code does not run continuously. It runs in short bursts, each triggered by an event, and then returns control to the loop. This has an important consequence: if your handler takes too long, the UI freezes. The loop cannot process the next event until your handler returns. A slow database query, a large file read, a network request — any of these in a handler will make the app feel broken. This is why Module 3 covers threading: moving slow work off the event loop is not optional polish, it is a basic requirement for a usable app.

How wxRuby3 starts up

Here is the minimal wxRuby3 application:

1
2
3
4
5
6
require 'wx'

Wx::App.run do
  frame = Wx::Frame.new(nil, title: 'Hello')
  frame.show
end

Wx::App.run creates the application object, runs the block you give it (which typically creates the main window), and then starts the event loop. The loop runs until the last window is closed, at which point run returns and the script exits.

The block passed to App.run is your setup code. By the time the event loop starts, all your widgets should be created and ready. Nothing in the block is a handler — it runs once, at startup.

The widget tree

In Rails, your views are templates that produce HTML strings. The HTML describes a structure that the browser renders. The structure is rebuilt from scratch on every request.

In wxRuby3, the widget tree is a live object graph that persists for the lifetime of the app.

Every widget has a parent. The hierarchy looks like this:

Wx::App
  └── Wx::Frame          (the top-level window)
        └── Wx::Panel    (a container for other widgets)
              ├── Wx::StaticText
              ├── Wx::TextCtrl
              └── Wx::Button

When you create a widget, you pass its parent as the first argument:

1
2
3
4
panel  = Wx::Panel.new(frame)
label  = Wx::StaticText.new(panel, label: 'Name:')
input  = Wx::TextCtrl.new(panel, value: '')
button = Wx::Button.new(panel, label: 'Submit')

The parent owns the child. When the parent is destroyed, all its children are destroyed. When the parent is shown or hidden, the children follow. You rarely need to manage widget lifetimes manually — the tree handles it.

Wx::Panel deserves a specific mention. You might wonder why you cannot put widgets directly inside a Wx::Frame. Technically you can, but you should not: panels handle keyboard navigation (tab order between controls) and provide the correct native background colour on all platforms. Always put your controls inside a panel. Always put the panel inside the frame.

Event handlers

Responding to user actions means registering handlers on widgets. In wxRuby3 this is done with evt_* methods, typically called inside the class that owns the widgets:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class MyFrame < Wx::Frame
  def initialize
    super(nil, title: 'Example')

    panel  = Wx::Panel.new(self)
    @input = Wx::TextCtrl.new(panel, value: '')
    button = Wx::Button.new(panel, label: 'Say hello')

    evt_button(button.get_id) { on_button_clicked }
  end

  private

  def on_button_clicked
    name = @input.get_value
    Wx::message_box("Hello, #{name}!")
  end
end

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

evt_button registers a handler for the button’s click event. When the user clicks the button, the event loop picks up the click event, sees that this frame has a handler registered for that button’s ID, and calls the block.

A few things to notice:

  • Event handlers are registered on the parent (the frame), not on the button itself. This is idiomatic wxRuby3 — the parent coordinates the behaviour of its children.
  • The handler is a plain Ruby method. There is nothing special about it beyond the fact that the event loop calls it.
  • Instance variables (@input) are how you share state between the setup code and the handlers. The widget tree is persistent, so @input is still valid and still contains the user’s text when the button handler runs.

Instance variables instead of params

In Rails, data flows into a controller action via params, and out to the view via instance variables that are scoped to a single request. In wxRuby3, instance variables on the frame (or panel) class are your primary mechanism for sharing state between widgets and handlers.

This feels slightly different at first but becomes natural quickly. Your frame class is not a handler that runs and exits — it is an object that lives for the duration of the window. Instance variables accumulate meaning as the user interacts with the app:

1
2
3
4
5
6
7
8
9
class RouteFrame < Wx::Frame
  def initialize
    super(nil, title: 'Route Planner')
    @waypoints = []      # grows as the user clicks the map
    @dirty = false       # tracks unsaved changes
    @current_file = nil  # the last saved/loaded file path
    # ...
  end
end

These are not request-scoped. They are the ongoing state of your application.

Putting it together

The mental model, summarised:

Rails wxRuby3
Stateless between requests Stateful, persistent process
Request triggers a handler Event triggers a handler
View rebuilt from scratch Widget tree lives in memory
Params bring data in Instance variables hold state
Server process Native OS process
HTML/CSS for layout Sizers and native widgets

None of this is more complex than Rails — it is just different. Once the model clicks, wxRuby3 code reads naturally: a class that sets up a widget tree in initialize, registers handlers with evt_* methods, and uses instance variables to maintain state across interactions.


Previous: Why desktop? | Next: Installation and your first window