File I/O and preferences
Almost every useful desktop application reads and writes files. It also needs to remember user preferences between sessions — window size and position, recently opened files, UI settings. This lesson covers both, building a simple text editor step by step using the multi-file structure from lesson 3.2.
File structure
file_io_app/
├── main.rb
└── lib/
├── app_frame.rb
├── models/
│ └── document.rb
└── panels/
└── editor_panel.rbStart fresh
Create main.rb:
|
|
Create lib/app_frame.rb with the full menu in place from the start. On macOS, Wx::ID_EXIT is automatically relocated to the application menu — starting with only Exit in the File menu leaves it empty and produces a blank bubble on macOS. Having the full menu present from the beginning avoids this:
|
|
Run it. A window with a complete File menu. New, Open, Save, and Save As are visible but do nothing yet — we add their handlers as we go.
Step 1 — The Document model
The document model holds all state — file path, content, and modification status. The frame and panel know nothing about these details; they ask the model.
Create lib/models/document.rb:
|
|
Add it to app_frame.rb — require it and create an instance in initialize:
|
|
Run it. The title bar shows “Untitled — Editor”.
Step 2 — The editor panel
The editor panel owns the TextCtrl widget and provides a clean interface to it. It notifies the frame when the user types via a callback — but crucially, the notification comes from evt_key_up registered directly on the editor widget, not from evt_text. This is important: setting the editor content programmatically (when loading a file) fires evt_text but does not fire evt_key_up, so there is no false dirty notification.
Create lib/panels/editor_panel.rb:
|
|
Add it to app_frame.rb:
|
|
Run it. Type anything — the title bar gains a * indicating unsaved changes.
Step 3 — File operations and dirty state
Add the file operation handlers. Update bind_events:
|
|
Update on_close and add the handler methods:
|
|
Run it. New, Open, Save, and Save As all work. Close with unsaved changes — the confirm dialog appears.
A few things worth noting:
The @loading flag. Setting the editor content programmatically — when loading a file or clearing for New — fires evt_key_up does not, but to be safe we set @loading = true during any programmatic content change and check it in on_text_changed. This is a standard pattern in wxWidgets applications for suppressing event handling during programmatic updates.
All IO is wrapped in rescue. An unhandled IO error in a desktop app crashes the whole application. Always rescue file operations and report the error to the user.
confirm_discard returns early when unmodified. return true unless @document.modified means file operations proceed without interrupting the user when there are no unsaved changes.
on_save delegates to on_save_as for new documents that have never been saved. This is the standard pattern — Save only needs its own path when one exists.
Step 4 — Persistent preferences with Wx::ConfigBase
Wx::ConfigBase provides platform-appropriate preference storage — the registry on Windows, config files on Linux, NSUserDefaults on macOS. The same code works on all platforms.
Add load_prefs and save_prefs:
|
|
Call load_prefs after layout and centre in initialize, and save_prefs in on_close before event.skip:
|
|
Run it. Resize and reposition the window, close and reopen — it restores exactly where you left it.
Wx::ConfigBase notes:
- Use
readandwrite— the API is simpler than the C++ docs suggest readalways returns a string — call.to_iwhen you expect an integer- Guard restored values:
w > 100 && h > 100prevents applying a corrupted size;x >= 0 && y >= 0prevents positioning off-screen config.flushwrites to disk immediately — without it, a crash may lose saved prefs- Path strings use Unix-style hierarchy:
'/window/width','/editor/font_size'
What to take forward
- The
Documentmodel holds all state — path, content, modified. The frame reads from it and delegates to it. EditorPanelregistersevt_key_updirectly on theTextCtrlwidget — keyboard events go to the focused widget, not its parent panel- Use
@loading = trueduring programmatic content changes to suppress spurious change notifications - Wrap all file IO in
rescue— never let an IO error crash the app Wx::ConfigBase.readreturns strings — always convert with.to_ias needed- Load prefs after
layoutandcentre; save prefs inon_closebeforeevent.skip
Previous: Data-driven widgets | Next: Threading: keeping the UI responsive