Application structure
Every app in this series so far has lived in a single file. That works for tutorials, but real applications need more structure. As an app grows, a single file becomes hard to navigate, hard to test, and hard to reason about.
This lesson covers how to split a wxRuby3 app across multiple files, how to keep each class focused on one responsibility, and how to manage shared state without creating tangled dependencies.
The single-file problem
A typical single-file app grows like this: you add a feature, the frame class gets a new method. Then another. Then the frame is handling file I/O, business logic, UI layout, event handling, and data management all at once. Finding anything becomes difficult and changing one thing breaks another.
The solution is the same as in any Ruby application — separate concerns into separate classes, put those classes in separate files, and use require_relative to pull them together.
A standard file structure
For a wxRuby3 app of moderate complexity:
my_app/
├── main.rb # Entry point only — Wx::App.run lives here
├── Gemfile
├── lib/
│ ├── app_frame.rb # Main frame class
│ ├── panels/
│ │ ├── input_panel.rb
│ │ └── output_panel.rb
│ ├── dialogs/
│ │ └── preferences_dialog.rb
│ └── models/
│ ├── document.rb
│ └── settings.rb
└── assets/
├── icons/
└── html/main.rb does nothing but require and run:
|
|
Everything else lives under lib/. Each file contains one class.
Step 1 — A two-panel app
Create the following file structure:
hello_app/
├── main.rb
└── lib/
├── app_frame.rb
└── panels/
├── input_panel.rb
└── output_panel.rbThe input panel has a text field and a button. The output panel shows a greeting. We will connect them so that clicking the button in the input panel updates the output panel.
Start with main.rb:
|
|
Create lib/panels/input_panel.rb:
|
|
Create lib/panels/output_panel.rb:
|
|
Create lib/app_frame.rb:
|
|
Run it with ruby main.rb. Type a name and click Greet — the output panel updates.
Notice what each class knows and does not know:
InputPanelknows about its text field and button. It does not knowOutputPanelexists.OutputPanelknows about its greeting display. It does not knowInputPanelexists.AppFrameknows both panels exist and connects them. It does not know the internal details of either.main.rbknows only thatAppFrameexists.
Each class has one job. This is the structure every Module 6 app follows.
Step 2 — Communication between panels
The hello app uses a callback to connect the panels. The frame passes a named method to InputPanel as a keyword argument:
|
|
InputPanel stores it and calls it when the button is clicked:
|
|
The frame’s on_greet method receives the name and tells the output panel to update:
|
|
The input and output panels remain completely unaware of each other. All coordination happens in the frame.
Why not use a block? You might expect to write
InputPanel.new(splitter) { |name| ... }— but wxRuby3 intercepts blocks passed to widget constructors for its own initialisation, yielding the panel object itself to your block. Always pass callbacks as explicit keyword arguments to avoid this.
There are two other patterns worth knowing:
Direct method call — the panel holds a reference to the frame and calls a method on it:
|
|
This works but creates a tight coupling — the panel needs to know the frame’s API. The keyword argument approach avoids this.
Custom events — the panel posts a custom event that the frame handles. This is the most decoupled approach and is covered in lesson 3.5. For straightforward inter-panel communication, keyword argument callbacks are simpler and clearer.
Step 3 — Model classes
Business logic and data belong in model classes, not in UI classes. A frame that also manages its own data is doing two jobs — split them.
The pattern is straightforward: create a model class that holds the data, and a frame that displays it. The model knows nothing about widgets. The frame knows nothing about where the data came from.
Create lib/models/document.rb:
|
|
Create lib/app_frame.rb:
|
|
And main.rb:
|
|
Run it. The frame displays the document’s title, author, and content — but it does not know or care how Document stores that data. If Document later loads from a file or a database, the frame code does not change.
The key point: Document has no require 'wx', no widgets, no UI knowledge whatsoever. It is plain Ruby. The frame reads from it and displays what it finds. That separation is the pattern.
A more complete example
The city browser app shows these same patterns applied to a more realistic scenario — a sidebar list of cities connected to a detail panel, with Add and Remove functionality. Download it, run it, and read through the source to see how the structure scales.
What to take forward
The structural principles used in every Module 6 app:
main.rbcontains onlyrequireandWx::App.run- Each class lives in its own file under
lib/ - Panels are separate classes inheriting from
Wx::Panel - Data and business logic live in model classes with no knowledge of widgets
- Inter-panel communication uses keyword argument callbacks
- Never pass callbacks as blocks to widget constructors — wxRuby3 intercepts them
Previous: Event handling in depth | Next: Data-driven widgets