Skip to content

HowTo : Catching key events globally

Martin Corino edited this page Nov 21, 2024 · 3 revisions
     About      FAQ      User Guide      Reference documentation

Catching (key) events globally

The regular keyboard events go to the component (window) that currently has focus and do not propagate to the parent. This makes trying to catch key events globally a little tricky. There are several ways to solve this problem (keeping in mind there are probably more than presented here).

Before getting started, some notes about cases where you may be catching the wrong event or where you may not need global key catching at all:

  • Many components will only receive key events if they have the Wx::WANTS_CHARS style flag enabled. If they do you need to catch Wx::EVT_CHAR (see Wx::EvtHandler#evt_char) rather than or in addition to Wx::EVT_KEY_DOWN (see Wx::EvtHandler#evt_key_down).
  • For catching Enter-key presses on text controls, use style flag Wx::TE_PROCESS_ENTER, and catch event Wx::EVT_TEXT_ENTER (see Wx::EvtHandler#evt_text_enter).

Using Wx::EVT_CHAR_HOOK

Catching Wx::EVT_CHAR_HOOK (see Wx::EvtHandler#evt_char_hook) may be useful in certain situations. Note the use of flag Wx::WANTS_CHARS

Example for catching all key events within a frame:

require 'wx'

class MyFrame < Wx::Frame

  def initialize(title, pos, size)
    super(nil, pos: pos, size: size)
    main = Wx::Panel.new(self, Wx::ID_ANY, style: Wx::WANTS_CHARS)
    evt_char_hook :on_key_down
  end

  def on_key_down(evt)
    Wx.message_box("KeyDown: ##{evt.key_code}\n")
    evt.skip
  end
  
end

Wx::App.run do
  frame = MyFrame.new('Hello World', [50,50], [450,340])
  frame.show
end

Using event filters

Override Wx::App#filter_event. This function is called early in event-processing, so you can do things like this (for the F1-key):

require 'wx'

class MyFrame < Wx::Frame

  def initialize(title, pos, size)
    super(nil, pos: pos, size: size)
    main = Wx::Panel.new(self, Wx::ID_ANY)
  end
  
  def on_help_f1(evt)
    Wx.message_box("F1 pressed: ##{evt.key_code}\n")
  end
  
end

class MyApp < Wx::App
  
  def on_init
    @frame = MyFrame.new('Hello World', [50,50], [450,340])
    @frame.show
  end
  
  def filter_event(evt)
    if evt.event_type == Wx::EVT_KEY_DOWN && evt.key_code == Wx::K_F1
      @frame.on_help_f1(evt)
      return 1
    end
    super
  end
  
end

MyApp.run

Instead of overriding Wx::App#filter_event you can also install custom event filters using Wx::EvtHandler.add_filter.

require 'wx'

class MyFrame < Wx::Frame

  def initialize(title, pos, size)
    super(nil, pos: pos, size: size)
    main = Wx::Panel.new(self, Wx::ID_ANY)
  end
  
  def on_help_f1(evt)
    Wx.message_box("F1 pressed: ##{evt.key_code}\n")
  end

end

class MyEventFilter < Wx::EventFilter

  def initialize(frame)
    super()
    @frame = frame
  end
  
  def filter_event(evt)
    if evt.event_type == Wx::EVT_KEY_DOWN && evt.key_code == Wx::K_F1
      @frame.on_help_f1(evt)
      return Wx::EventFilter::Event_Processed
    end
    Wx::EventFilter::Event_Skip
  end
end

Wx::App.run do
  frame = MyFrame.new('Hello World', [50,50], [450,340])
  Wx::EvtHandler.add_filter(MyEventFilter.new(frame))
  frame.show
end

This will work most of the time. It will fail if

  • your application is not in the foreground. If you wish to catch keys even when your app is in the background, you will need to use platform-specific code;
  • your window-manager grabs the key for itself, in which case your app won't see it;
  • the key is pressed while a modal Wx::Dialog is showing.

Using recursive connect

You can recursively connect all components in a frame, therefore you will catch events no matter where the focus is.

require 'wx'

class MyDialog < Wx::Dialog

  def initialize
    super(nil, Wx::ID_ANY, 'My Dialog')

    self.sizer = Wx::VBoxSizer.new { |vszr|
      vszr.add Wx::HBoxSizer.new { |hszr|
        hszr.add(Wx::StaticText.new(self, label: 'Some text:'), Wx::SizerFlags.new.border)
        hszr.add(Wx::TextCtrl.new(self), Wx::SizerFlags.new.border)
      }, Wx::SizerFlags.new
      vszr.add Wx::HBoxSizer.new { |hszr|
        hszr.add(Wx::Button.new(self, Wx::ID_OK, "&Ok"), Wx::SizerFlags.new.border)
        hszr.add(Wx::Button.new(self, Wx::ID_CANCEL, "&Cancel"), Wx::SizerFlags.new.border)
      }, Wx::SizerFlags.new
    }

    set_auto_layout(true)
    self.sizer.set_size_hints(self)
    self.sizer.fit(self)

    connect_key_down(self, self.method(:on_key_down))
  end

  def connect_key_down(win, evh)
    win.evt_key_down(evh)
    win.each_child { |child| connect_key_down(child, evh) }
  end

  def on_key_down(evt)
    if evt.key_code == Wx::K_F1
      Wx.message_box("KeyDown: ##{evt.key_code}\n")
    else
      evt.skip
    end
  end

end

Wx::App.run do
  dlg = MyDialog.new
  dlg.show_modal
  dlg.destroy
  false
end

Using a custom event handler

In this example (inspired by code form the wxWidgets wiki) a custom event handler is used to force certain key events to propagate up.

require 'wx'

class EventPropagator < Wx::EvtHandler
  
  def initialize
    super
    evt_key_down :on_key_down
    evt_key_up :on_key_up
  end
  
  def self.register_for(win)
    win.each_child { |child| child.push_event_handler(self.new) }
  end
  
  def on_key_down(evt)
    if evt.key_code == Wx::K_F1
      evt.resume_propagation(1)
    end
    evt.skip
  end
  
  def on_key_up(evt)
    if evt.key_code == Wx::K_F1
      evt.resume_propagation(1)
    end
    evt.skip
  end
  
end

class MyDialog < Wx::Dialog

  def initialize
    super(nil, Wx::ID_ANY, 'My Dialog')

    self.sizer = Wx::VBoxSizer.new { |vszr|
      vszr.add Wx::HBoxSizer.new { |hszr|
        hszr.add(Wx::StaticText.new(self, label: 'Some text:'), Wx::SizerFlags.new.border)
        hszr.add(Wx::TextCtrl.new(self), Wx::SizerFlags.new.border)
      }, Wx::SizerFlags.new
      vszr.add Wx::HBoxSizer.new { |hszr|
        hszr.add(Wx::Button.new(self, Wx::ID_OK, "&Ok"), Wx::SizerFlags.new.border)
        hszr.add(Wx::Button.new(self, Wx::ID_CANCEL, "&Cancel"), Wx::SizerFlags.new.border)
      }, Wx::SizerFlags.new
    }

    set_auto_layout(true)
    self.sizer.set_size_hints(self)
    self.sizer.fit(self)

    EventPropagator.register_for(self)
    
    evt_key_down :on_key_down
  end

  def on_key_down(evt)
    if evt.key_code == Wx::K_F1
      Wx.message_box("KeyDown: ##{evt.key_code}\n")
    end
  end

end

Wx::App.run do
  dlg = MyDialog.new
  dlg.show_modal
  dlg.destroy
  false
end

Using a system-wide Hot Key

In this example an event handler is connected for the Wx::EVT_HOTKEY event. Currently this only works for WXMSW and WXOSX.

require 'wx'

class MyFrame < Wx::Frame

  def initialize(title, pos, size)
    super(nil, pos: pos, size: size)
    Wx::Panel.new(self, Wx::ID_ANY)
  end
  
end

class MyApp < Wx::App
  
  def on_init
    if Wx.has_feature?(:USE_HOTKEY)
      evt_hotkey Wx::K_F1, :on_help_f1
      
      @frame = MyFrame.new('Hello World', [50,50], [450,340])
      @frame.show
    else
      Wx.message_box('System-wide hotkeys are not supported.')
      false
    end
  end

  def on_help_f1(evt)
    Wx.message_box("F1 pressed: ##{evt.key_code}\n")
  end
  
end

MyApp.run

Using a Menu Hot Key

You can also make a hotkey part of a menu item.

require 'wx'

class MyWindow < Wx::Frame
  def initialize(title)
    super(nil, title: title)
    @panel = Wx::Panel.new(self)

    #----------------
    menuBar = Wx::MenuBar.new

    file_menu = Wx::Menu.new
    mi  = file_menu.append(Wx::ID_NEW,  "&New\tCTRL+N",  'Create New File')
    evt_menu(mi, :new_file)
    mi = file_menu.append(Wx::ID_OPEN, "&Open\tCTRL+O", 'Open File')
    evt_menu(mi, :open_file)
    mi = file_menu.append(Wx::ID_SAVE, "&Save\tCTRL+S", 'Save File')
    evt_menu(mi, :save_file)

    menuBar.append(file_menu, 'File')
    
    help_menu = Wx::Menu.new
    mi = help_menu.append(Wx::ID_HELP, "&Help\tF1", 'Get help')
    evt_menu(mi, :on_help)

    menuBar.append(help_menu, 'Help')
    #----------------

    self.set_menu_bar(menuBar)
    
    centre
  end
  
  def new_file(_)
    Wx.message_box 'New File'
  end

  def open_file(_)
    Wx.message_box 'Open File'
  end

  def save_file(_)
    Wx.message_box 'Save File'
  end
  
  def on_help(_)
    Wx.message_box 'F1 pressed.'
  end
  
end

Wx::App.run do
  window = MyWindow.new("wxRuby MenuBar Guide")
  window.show
end

Using an Accelerator table

Using an accelerator table is also an option.

require 'wx'

class MyFrame < Wx::Frame

  def initialize(title, pos, size)
    super(nil, pos: pos, size: size)
    Wx::Panel.new(self, Wx::ID_ANY)

    set_accelerator_table Wx::AcceleratorTable.new([
                                                    Wx::AcceleratorEntry.new(Wx::ACCEL_NORMAL, Wx::K_ESCAPE, Wx::ID_CLOSE),
                                                    Wx::AcceleratorEntry.new(Wx::ACCEL_NORMAL, Wx::K_F1, Wx::ID_HELP)
                                                  ])
    evt_menu Wx::ID_CLOSE, :on_quit
    evt_menu Wx::ID_HELP, :on_help
  end

  def on_help(_)
    Wx.message_box("F1 pressed.")
  end
  
  def on_quit(_)
    close(true)
  end
  
end

Wx::App.run do
  frame = MyFrame.new('Hello World', [50,50], [450,340])
  frame.show
end

Using regular polling

This may be useful to get a game-like behaviour. You can simply poll for key states using Wx.get_key_state (for instance in a Wx::Timer, or in idle events, etc.).

require 'wx'

class MyFrame < Wx::Frame

  def initialize(title, pos, size)
    super(nil, pos: pos, size: size)
    Wx::Panel.new(self, Wx::ID_ANY)
  end
  
end

class MyApp < Wx::App
  
  def on_init
    # create a global application timer
    @timer = Wx::Timer.new(self, Wx::ID_ANY)
    evt_timer @timer, :on_timer
    @timer.start(30)
    
    @frame = MyFrame.new('Hello World', [50,50], [450,340])
    @frame.show
  end
  
  def on_exit
    @timer.stop
  end

  def on_timer(_)
    @timer.stop
    Wx.message_box("F1 pressed.") if Wx.get_key_state(Wx::K_F1)
    @timer.start
  end
  
end

MyApp.run
Clone this wiki locally