wxRuby for the Lazy

WxRuby is probably the best overall GUI library for Ruby currently available. It is cross-platform, provides native look-and-feel and is stable enough for production use. All other GUI libraries, despite their various merits, fall short in at least one of the areas. However, WxRuby does have one major downfall. It is pretty much a straight port of the C API. Writing WxRuby code is largely the same as writing actual WxWidgets C code. It’s far from the “Ruby Way”.

So how did I mange to get fairly nice Ruby code despite a binding that is essentially a straight port of the underlying C API? I built it from the bottom-up using a lazy coding technique. And I mean “bottom-up” literally –the following code might actually be easier to read if you start from the bottom and work your way up to the top. The trick is to break down one’s interface into individual widgets and create an instance method for each using the ||= memoization trick.

You can see from the following code I was able to apply this “trick” to everything but toolbar buttons (aka ‘tools’). This is because the toolbar itself is needed to create them. So I simply defined attributes for each tool, but actually created the tool buttons in the toolbar’s method. Have a look.

  class View < ::Wx::Frame
    def initialize
      super(nil, -1, "My Application",
        :style => Wx::DEFAULT_FRAME_STYLE | Wx::TAB_TRAVERSAL,
        :size => [800,600]
      )
      setup_controls
      setup_events
    end
 
    # kickstart widget creation from the bottom up
    def setup_controls
      search_toolbar
      settings_toolbar
      search_list
    end
 
    def frame_panel
      @frame_panel ||= (
        panel = Wx::Panel.new(self)
        panel
      )
    end
 
    def frame_sizer
      @frame_sizer ||= (
        sizer = Wx::VBoxSizer.new
        frame_panel.sizer = sizer
        sizer
      )
    end
 
    def notebook
      @notebook ||= (
        notebook = Wx::Notebook.new(frame_panel)
        frame_sizer.add(notebook, 1, Wx::GROW)
        notebook
      )
    end
 
    def search_panel
      @search_panel ||= (
        panel = Wx::Panel.new(notebook)
        notebook.add_page(panel, 'Search')
        panel
      )
    end
 
    def settings_panel
      @settings_panel ||= (
        panel = Wx::Panel.new(notebook)
        notebook.add_page(panel, 'Settings')
        panel
      )
    end
 
    def search_sizer
      @search_sizer ||= ( 
        sizer = Wx::VBoxSizer.new
        search_panel.set_sizer(sizer)
        sizer
      )
    end
 
    def settings_sizer
      @settings_sizer ||= (
        sizer = Wx::VBoxSizer.new
        settings_panel.set_sizer(sizer)
        sizer
      )
    end
 
    def search_toolbar
      @search_toolbar ||= (
        toolbar = Wx::ToolBar.new(search_panel)
        @search_start_tool   = toolbar.add_tool(-1, 'Start'   ,
            Wx::Bitmap.new(DIR + '/images/search.gif', Wx::BITMAP_TYPE_GIF), 'Start')
        @search_stop_tool    = toolbar.add_tool(-1, 'Stop'    ,
            Wx::Bitmap.new(DIR + '/images/stop.gif'  , Wx::BITMAP_TYPE_GIF), 'Stop')
        @search_insert_tool  = toolbar.add_tool(-1, 'Insert'  ,
            Wx::Bitmap.new(DIR + '/images/insert.gif', Wx::BITMAP_TYPE_GIF), 'Insert')
        @search_import_tool  = toolbar.add_tool(-1, 'Import'  ,
            Wx::Bitmap.new(DIR + '/images/import.gif', Wx::BITMAP_TYPE_GIF), 'Import')
        @search_delete_tool  = toolbar.add_tool(-1, 'Delete'  ,
            Wx::Bitmap.new(DIR + '/images/delete.gif', Wx::BITMAP_TYPE_GIF), 'Delete')
        search_sizer.add(toolbar, 0, Wx::GROW)
        toolbar
      )
    end
 
    def search_start_tool    ; @search_start_tool   ; end
    def search_stop_tool     ; @search_stop_tool    ; end
    def search_insert_tool   ; @search_insert_tool  ; end
    def search_import_tool   ; @search_import_tool  ; end
    def search_delete_tool   ; @search_delete_tool  ; end
 
    def settings_toolbar
      @settings_toolbar ||= (
        toolbar = Wx::ToolBar.new(settings_panel)
        @settings_save_tool     = toolbar.add_tool(-1, 'Save'    ,
            Wx::Bitmap.new(DIR + '/images/save.gif'    , Wx::BITMAP_TYPE_GIF), 'Save')
        @settings_undo_tool     = toolbar.add_tool(-1, 'Undo'    ,
            Wx::Bitmap.new(DIR + '/images/undo.gif'    , Wx::BITMAP_TYPE_GIF), 'Undo')
        @settings_restore_tool  = toolbar.add_tool(-1, 'Restore' ,
            Wx::Bitmap.new(DIR + '/images/restore.gif' , Wx::BITMAP_TYPE_GIF), 'Restore')
        settings_sizer.add(toolbar, 0, Wx::GROW)
        toolbar
      )
    end
 
    def settings_save_tool    ; @settings_save_tool    ; end
    def settings_undo_tool    ; @settings_undo_tool    ; end
    def settings_restore_tool ; @settings_restore_tool ; end
 
    def search_list
      @search_list ||= (
        list = Wx::ListBox.new(search_panel, :choices=>ctlr.sites.map{|s| s.href})
        search_sizer.add(list, 1, Wx::GROW)
        list
      )
    end
 
    def setup_events
      evt_menu( search_start_tool     ) { ctlr.search_start     }
      evt_menu( search_stop_tool      ) { ctlr.search_stop      }
      evt_menu( search_insert_tool    ) { ctlr.search_insert    }
      evt_menu( search_import_tool    ) { ctlr.search_import    }
      evt_menu( search_delete_tool    ) { ctlr.search_delete    }
 
      evt_menu( settings_save_tool    ) { ctlr.settings_save    }
      evt_menu( settings_undo_tool    ) { ctlr.settings_undo    }
      evt_menu( settings_restore_tool ) { ctlr.settings_restore }
    end
  end
 
  class Controller < ::Wx::App
    attr :service
    attr :view
 
    def on_init
      @service = Service.new   # backend service
      @service.connect         # connect to database
 
      @view = View.new(self)
      @view.show(true)
    end
 
    def sites ; service.sites ; end
 
    def search_start     ; service.search_start     ; end
    def search_stop      ; service.search_stop      ; end
    def search_insert    ; service.search_insert    ; end
    def search_import    ; service.search_import    ; end
    def search_delete    ; service.search_delete    ; end
 
    def settings_save    ; service.settings_save    ; end
    def settings_undo    ; service.settings_undo    ; end
    def settings_restore ; service.settings_restore ; end
  end
 
  Controller.new.main_loop

The thing to notice, if you haven’t caught it yet, is how calling #search_toolbar leads to calling #search_sizer which in turn leads to calling #search_panel, and so forth all the way to the top #frame_panel. This code is a striped down version of actual code I am using. I hope it helps others create wxRuby application more easily. As I said in my previous post, I found in mind-numbingly difficult to create WxRuby interfaces until I worked out this approach. WxRuby is still a difficult API to master, but this technique makes the effort more manageable, and therefore more likely to succeed.

For another example of building structures lazily, have a look at my solution for Ruby Quiz 10 – Crosswords.