Skip to content

User Guide : wxRuby Event Handling

Martin Corino edited this page Nov 17, 2024 · 5 revisions
     About      HowTo      FAQ      Reference documentation

Introduction

Event handling is the core of runtime code execution in event based frameworks like wxWidgets which means it needs to be fully supported by wxRuby. Fortunately it is.
As Ruby is a fully dynamic language though the statically declared event tables typical for wxWidgets application are not.
Instead wxRuby offers a dynamic solution that is just as easy to use and even offers more flexibility in a typical Ruby-way.

Event handlers

Event handlers are pieces of code that are run when an event occurs. There are two parts to setting up an event handler:

  • Writing the code that should be run
  • Connecting that code to the event

The basic way to set up an event handler in wxRuby combines these two steps in a single line of code. This is to call a method named evt_xxx where xxx is the type of event that should be handled, and pass it a block that should be run when the event occurs. For example:

evt_size { puts "I was resized" }

NOTE
Instead of the EVT_XXX event handler declaration macros used in wxWidgets wxRuby provides similarly named event handler connector methods for each of the known event declarations which are inherited by all classes derived from Wx::EvtHandler (which includes all window classes, the Wx::App class and Wx::Timer as well as various other classes).
Naming is (mostly) identical but rubified. So EVT_MENU becomes evt_menu, EVT_IDLE becomes evt_idle, EVT_UPDATE_UI becomes evt_update_ui etc.

Some types of event handlers, those that deal with events arising from a user operating UI controls like buttons, checkboxes and choices, require that the relevant control be identified in the handler:

my_button = Wx::Button.new(self, :label => 'Press me')
evt_button(my_button) { puts "Button was pressed" }

This is discussed in more detail below

Event handler setup is typically something done during the initialization of an event handler object (like a window) but this is not required. As all event handlers are assigned dynamically in wxRuby you can setup (some) event handlers at a later moment. You could also disconnect earlier activated handlers at any time (see Wx::EvtHandler#disconnect).

In case of some frame class MyForm including a menu a wxWidgets static event handling table like:

wxBEGIN_EVENT_TABLE(MyForm, wxFrame)
    EVT_IDLE(MyForm::OnIdle)
    EVT_MOVE(MyForm::OnMove)
    EVT_SIZE(MyForm::OnResize)

    EVT_MENU( wxID_ABOUT, MyForm::OnAbout )
    EVT_MENU( wxID_EXIT, MyForm::OnCloseClick )
wxEND_EVENT_TABLE()

could translate to event handler initializations in wxRuby like this:

class MyForm < Wx::Frame
  
  def initialize(title)
    super(nil, title: title)
    
    # initialize frame elements
    # ...
    
    # setup event handlers
    evt_idle do |evt|
      # do something
      evt.skip
    end
    evt_move :on_move
    
    evt_size method(:on_size)
    
    evt_menu(Wx::ID_ABOUT, Proc.new { on_about })
    evt_menu(Wx::ID_EXIT) { close(false) }
  end
  
  def on_idle(evt)
    #...
  end
  
  def on_move(evt)
    #...
  end

  def on_resize(evt)
    #...
  end

  def on_about
    #...
  end
  
end

As you can see there are multiple options for specifying the actual handler. Any event handler definition method will accept either a Symbol (or String) specifying a method of the receiver (the event handler instance), a Proc object (or lambda) or a Method object.

Event handler methods are not required to declare the single event object argument. The event handler connector method will take care of checking and handling method arity.

Working with event objects

Whenever an event occurs, an Wx::Event object is created containing additional information about the event. For example, for a mouse event, this might be where on the screen it took place; for a key event, what key was pressed; for an activation event, whether the frame window became active or inactive.

To access this additional information within an event handler, simply set up the block to accept a single parameter, the event object.

evt_size { | event | puts "Width is now #{event.size.width}" }

The event object will be of the appropriate class for the type of event being handled - for example, a Wx::SizeEvent for an evt_size handler, or a Wx::TreeEvent from a TreeCtrl.

Note that events are short-lived objects and are destroyed once all the relevant handlers have been called. Therefore you shouldn't attempt to store an event object passed into a handler in a variable referenced outside the handler block.

Using methods to handle events

As an application develops, more event handlers are used, and each contains more code. It then becomes easier to organise this code into methods, for example:

def initialize
  # ..
  evt_size { | event | on_size(event) }
  evt_button(my_button) { | event | on_button_pressed(event) }
end

def on_size(event)
  # ...
end

def on_button_pressed(event)
  # ...
end

For convenience, wxRuby permits the block here to be replaced simply with the name of the method that should handle the event.

evt_size :on_size
evt_button my_button, :on_button_pressed

Obviously, this can give considerably clearer and shorter code. The method may still choose whether or not to receive the event object, for example like this:

def initialize
  # ..
  evt_size :on_size
  evt_button(my_button, :on_button_pressed)
end

def on_size(event)
  # ...
end

# this handler method does not use/receive the event object 
def on_button_pressed
  # ...
end

The event handler connector method will take care of checking and handling method arity.

Events with IDs

As mentioned above, some event handlers, for example, evt_button require that the control which generates the event be identified. This is one aspect of a distinction between two broad groups of events in wxRuby. The first is CommandEvents, which are generated by user interaction with controls. The second includes all other events.

Command events

CommandEvents are generated by user interface controls such as buttons, text boxes, radio buttons, lists, menus and so on. These controls enable particular types of interaction, such as typing in a text box, selecting a radio item, or choosing an item from a drop-down list or menu. Such actions can be thought of as "commands", hence the name. It's often desirable to manage the event handlers for a related group of such controls within a container window, such as a Wx::Frame or Panel.

To facilitate this, CommandEvents 'bubble' up to parent windows, so parent windows can set up handlers to listen to events generated by child controls. This also means that you have to explicitly tell WxRuby the source of events you want to handle events from. In C++ this is done by referring to the integer id of the control whose events are being listened to, and this is permitted in wxRuby too:

MY_BUTTON_ID = 1001
my_button = Wx::Button.new(panel, MY_BUTTON_ID, 'Press me')
evt_button(MY_BUTTON_ID) { 'my_button was pressed' }

However, this use of explicit constants is rather cumbersome and unnecessary in a dynamic language like Ruby. Therefore, it is typically easier to allow wxRuby to assign ids to new windows, and then simply pass the control itself as the parameter to the event handler:

my_button = Wx::Button.new(panel, :label => 'Press me')
evt_button(my_button) { 'my_button was pressed' }

If there are many similar controls within a Frame or Dialog, it can be easier to set up a global event handler, then work out what specific action should be taken within the event handling code itself, perhaps using the Wx::Event#event_object method to identify the control. To set up a catch-all event handler for CommandEvent types, use the special Wx::ID_ANY identifier:

evt_button(Wx::ID_ANY) { 'some button was pressed' }

Other events

Other events in wxRuby include sizing, activating, closing and moving windows, changing layouts with splitters and sashes, and moving through multi-pane organisers like Wizards and Notebooks. Importantly, this also includes generic mouse movement and keyboard presses that are not associated with any particular command action. All these kind of events are only visible to the window itself. They are not visible to parent windows. The practical effect of this is that the evt_xxx method must be set up for the window (event handler) that generates the event, and no id parameter is needed.

evt_size { "I was resized" }

The fact that non-CommandEvents are only sent to the window that generated them is not, in practice, a serious restriction, given wxRuby's dynamic nature. It is entirely permitted to call the evt_xxx method on the widget whose events should be listened for, but keep the handling code in another instance like this:

class MyFrame < Wx::Frame
  def initialize
    # ...
    my_panel = Wx::Panel.new(self)
    my_panel.evt_size { on_panel_sized } # note we cannot simply specify the method name here as the 
                                         # method does not belong to the receiver
  end

  def on_panel_sized
    # ...
  end

  # ...
end

Just note that in this case it is not possible to use the shortcut method-name notation shown above; the following will not work:

my_panel.evt_size :on_panel_sized

Non-gui events

As well as events that are triggered by UI actions, wxRuby is also able to use event handling to carry out timed or regular actions. The Wx::Timer class can be used to trigger one-off or repeating TimerEvents events which can be handled as with other events. Note that use of this approach is preferred to ruby's own 'timeout' library as a way to handle timed events in wxRuby. This is because Ruby's interpreter threads do not co-operate easily with event-driven toolkits like wxRuby.

There is also an evt_idle handler which specifies actions to be done when the application is otherwise doing nothing. See Wx::IdleEvent for more information.

Vetoing and skipping events

Sometimes a given event should not be permitted. For example, a user has tried to close a dialog, but there is an invalid value in a text box, so the application should prevent the dialog being closed until the value is corrected. The action can be blocked by calling the Wx::Event#veto method:

if not ready
event.veto
end

Sometimes you want the opposite - to ensure that the event's original aim is completed, or passed on upwards to a parent window for handling. In this case, the Wx::Event#skip method should be called.

event.skip

This is important, for example, in evt_close handlers. If Wx::Event#skip is not called, the Frame or Dialog will not be closed at all.

Disabling event handlers

Although it is not frequently needed, it is possible to disable any event handler dynamically within program code. This is achieved by calling the Wx::EvtHandler#disconnect method. An example of this can be found in the "event" sample included with wxRuby.

Custom Events

Custom event definitions are fully supported in wxRuby including the definition of new event types.

New event classes can be registered with Wx::EvtHandler.register_class which returns the new event type for the event class like this:

# A custom type of event associated with a target control. Note that for
# user-defined controls, the associated event should inherit from
# Wx::CommandEvent rather than Wx::Event.
class ProgressUpdateEvent < Wx::CommandEvent
  # Create a new unique constant identifier, associate this class
  # with events of that identifier and create an event handler definition method 'evt_update_progress'
  # for setting up this handler.
  EVT_UPDATE_PROGRESS = Wx::EvtHandler.register_class(self, nil, 'evt_update_progress', 0)

  def initialize(value, gauge)
    # The constant id is the arg to super
    super(EVT_UPDATE_PROGRESS)
    # simply use instance variables to store custom event associated data
    @value = value
    @gauge = gauge
  end

  attr_reader :value, :gauge
end

Check the reference documentation here for more information.

Event processing

In wxRuby overruling the normal chain of event handling has been limited to being able to override the default Wx::EvtHandler#try_before and Wx::EvtHandler#try_after methods. These are the advised interception points for events when you really need to do this.
Overriding Wx::EvtHandler#process_event is not considered to be efficient (or desired) for wxRuby applications and has therefor been blocked.

Event insertion

Use of Wx::EvtHandler#process_event or Wx::EvtHandler#queue_event and Wx::EvtHandler#add_pending_event in wxRuby to trigger event processing of user generated (possibly custom) events is fully supported.

As with wxWidgets Wx::EvtHandler#process_event will trigger immediate processing of the given event, not returning before this has finished.
Wx::EvtHandler#queue_event and Wx::EvtHandler#add_pending_event on the other hand will post (append) the given event to the event queue and return immediately after that is done. The event will than be processed after any other events in the queue. Unlike in wxWidgets in wxRuby there is no practical difference between queue_event and add_pending_event.

Asynchronous execution

In addition to Wx::EvtHandler#queue_event and Wx::EvtHandler#add_pending_event to trigger asynchronous processing wxRuby also supports Wx::EvtHandler#call_after.

This method provides the means to trigger asynchronous execution of arbitrary code and because it has been rubified is easy and powerful to use. Like with event handler definition this method accepts a Symbol or String (identifying a method of the receiver), a Proc object (or lambda), a Method object or a block. Unlike an event handler method no event object will be passed but rather any arguments passed to the call_after method in addition to the 'handler'.

Given an event handler object call_after could be used like:

# sync call to method of event handler (no args)
evt_handler.call_after :async_method

# async call of lambda (single arg)
evt_handler.call_after(->(txt) { Wx.log_info(txt) }, "Hello")

# async call of block
evt_handler.call_after('Call nr. %d', 1) { |fmt, num| Wx.log_info(fmt, num) }

Event life cycles!

Like in C++ the wxRuby Event objects passed to the event handlers are (in general) references to temporary objects which are only safe to access within the execution scope of the event handler that received the reference. If you need (really?) to store a reference to such an object do so to a cloned version (see Wx::Event#clone) and not to the original object otherwise you will run into 'Object already deleted' exceptions.

Only (user defined) events instantiated in Ruby code (or cloned Event objects) will be subject to Ruby's normal life cycle rules (GC). This means that when you instantiate a (user defined) event and pass it to Wx::EvtHandler#process_event it would be possible to directly store the reference to such an Event object passed to it's event handler. You have to know for sure though (see below). So, in case of doubt (or to be safe) use Wx::Event#clone.

Another 'feature' to be aware of is the fact that when passing a(n) (user instantiated) event object to Wx::EvtHandler#queue_event the event object will be 'disowned', i.e. the Ruby instance will not control the lifecycle of it's C++ counterpart anymore. Instead the C++ side takes ownership and will delete the Event object when asynchronously handled (also unlinking the Ruby instance at that point). Therefor the Ruby event object should be considered discarded from the moment the Wx::EvtHandler#queue_event returns (the internal tracking of wxRuby3 will keep the Ruby instance alive until it has been handled).

The Wx::EvtHandler#add_pending_event method also queues events to be asynchronously handled but does so by queuing a (shallow) copy of the Ruby event object passed. Therefor in this case the event object originally passed to Wx::EvtHandler#add_pending_event remains valid after this method returns. Altering the state of this object will not affect the queued copy (see the documentation of Wx::EvtHandler#add_pending_event regarding potential issues regarding Ruby member objects and overriding #initialize_clone to implement deep copying to overcome such issues).

Clone this wiki locally