1. Technology
You can opt-out at any time. Please refer to our privacy policy for contact information.

Sending Win32 Keyboard Messages with via FFI

By

Reaching out to Windows

OK, now we know how to use FFI to interface with the Win32 API. This seems straightforward, but there is not Win32 API call to do this in one neat little package. Instead, we'll have to iterate over all visible windows and try to find the one we want. But to do this, we need to use a callback.

Callbacks are common constructs in C programs. A callback is a function that is called from another function, much like passing a block to a method in Ruby. The challenge here is that the C function has no idea how to call a Ruby method or proc object, so the FFI library has to act as a bridge. Further, callbacks in C have a specific type (a function pointer) that includes the function signature (its number and type of arguments), so we'll need to tell FFI the signature of the callback to expect.

First, the Win32 API function we'll be using. There are a few functions that can do this, but we'll be using EnumDesktopWindows. It takes a handle to a "desktop," we'll use nil for that since we want windows from the current desktop. The next parameter is the callback, before we attach the function we'll have to figure out the signature of the callback function. And finally a parameter that will be passed to the callback function, we won't be using it.

So, this callback must be of type EnumWindowsProc. In other words, it must be a function pointer that takes an HWND (a :pointer) and LPARAM (a :long). The callback method defines such a callback type to be used in parameter lists of further calls to attach_function, its parameters are very similar to attach_function as well. The first parameter is the name we're giving the type of callback, the second is a list of parameter types (the function signature) and the last is the return type of the callback function. Note that we're defining the signature of a callback function here, not an actual callback function, we just need to give a name to the type of callback function we'll be using later.

Next we'll attach our EnumDesktopWindows function. This is the same as the previous example with MessageBoxA, except we need to use our user-defined callback type. Also note I've started putting the function attachments on multiple lines to make them a bit easier to read, because these lines can get quite long.


  callback :enum_callback, [:pointer, :long], :bool
  attach_function :enum_desktop_windows,
	:EnumDesktopWindows,
	[ :pointer, :enum_callback, :long ],
	:bool

Great, we're almost all set. We need to attach one more function, GetWindowText that gets the title of a window given an HWND, so let's do that real quick and then we can get down to enumerating windows.


  attach_function :get_window_text,
	:GetWindowTextA,
	[ :pointer, :pointer, :int ],
	:int

The actual enumeration is not difficult now that we have all the pieces in place. We'll need to allocate some memory using FFI using the FFI::MemoryPointer.new method, we'll create a proc object with our callback and we'll feed all our information to yet another block that will be passed down from higher level code. We'll put all this into an enumerate_all_windows method that lives right along side our API functions.


  def self.enumerate_all_windows(&block)
	title = FFI::MemoryPointer.new :char, 512
	
	callback = Proc.new do|wnd,param|
  	title.clear
  	Win.get_window_text(wnd, title, title.size)  	
  	block.call(wnd,param,title.get_string(0)) if block
  	
  	true
	end
	
	Win.enum_desktop_windows(nil, callback, 0)
  end

To use this, we'll wrap it in yet another method. Instead of enumerating over all windows and letting the end user sort it all out, we'll just find windows with a certain name and store them in an array. This method make an empty array, compiles the string to a regular expression if needed, iterates over the window and adds the handle and title to the array if the name matches. We'll use this later to finally start sending the windows some messages.


  def self.find_windows(name)
	windows = []
	name = Regexp.compile(name) unless name.is_a?(Regexp)
	
	enumerate_all_windows do|wnd,param,title|
  	windows << [wnd,title] if title =~ name
	end
	
	return windows
  end

Then to use this, a simple call like this.


spotify_windows = Win.find_windows('Spotify')
puts spotify_windows.inspect

# C:\Users\uzimo_000\Documents\webkeys>jruby findwindow.rb
# [[#, "Spotify - Beck \x96 Lord Only Knows"]]

Finally, we're almost there. We can enumerate over all windows and find the one we want, complete with HWND parameter we'll need later. The Win32 API call we actually need to send the keypresses is called SendMessage (and again, we'll need the SendMessageA variant). Normally on C you'll have some constants like WM_KEYDOWN (0x100) and WM_KEYUP (0x101) but as we don't have access to these constants for now we'll just use magic numbers. All we need to do is send WM_KEYDOWN with the correct VK_ code (we want to send the space key to Spotify to pause or unpause, so look up VK_SPACE, it's 0x20) and… that's it. This part is really pretty easy.


  attach_function :send_message,
	:SendMessageA,
	[ :pointer, :uint, :uint, :long ],
	:int

spotify = Win.find_windows('Spotify').first
Win.send_message(spotify[0], 0x100, 0x20, 0)
Win.send_message(spotify[0], 0x101, 0x20, 0)

Phew, that was a long way for a seemingly simple thing. For completeness, here is the complete code.


require 'java'
require 'ffi'
module Win
  extend FFI::Library
 
  ffi_lib 'user32'
  ffi_convention :stdcall
 
  attach_function :message_box, :MessageBoxA, [:pointer, :string, :string, :uint], :int
  #attach_function :find_window, :FindWindow, [:string, :string], :pointer
 
  callback :enum_callback, [:pointer, :long], :bool
  attach_function :enum_desktop_windows,
	:EnumDesktopWindows,
	[ :pointer, :enum_callback, :long ],
	:bool
  attach_function :get_window_text,
	:GetWindowTextA,
	[ :pointer, :pointer, :int ],
	:int
	
  attach_function :send_message,
	:SendMessageA,
	[ :pointer, :uint, :uint, :long ],
	:int
 
  def self.enumerate_all_windows(&block)
	title = FFI::MemoryPointer.new :char, 512
	
	callback = Proc.new do|wnd,param|
  	title.clear
  	Win.get_window_text(wnd, title, title.size)  	
  	block.call(wnd,param,title.get_string(0)) if block
  	
  	true
	end
	
	Win.enum_desktop_windows(nil, callback, 0)
  end
 
  def self.find_windows(name)
	windows = []
	name = Regexp.compile(name) unless name.is_a?(Regexp)
	
	enumerate_all_windows do|wnd,param,title|
  	windows << [wnd,title] if title =~ name
	end
	
	return windows
  end
end
spotify = Win.find_windows('Spotify').first
Win.send_message(spotify[0], 0x100, 0x20, 0)
Win.send_message(spotify[0], 0x101, 0x20, 0)
  1. About.com
  2. Technology
  3. Ruby
  4. JRuby
  5. Automation of Windows using FFI
  6. Reaching out to Windows

©2014 About.com. All rights reserved.