Search

Wednesday 27 April 2011

Ruby: retrieve OLE object from HWND

I continue investigating the possibility to create custom auto-clicker. This time it's for my current project, so there'll be no blackjack. But anyway, the problems still appear and I have to resolve them. Just like this time when I've got the way to find Win32 windows by their specific attributes. Ruby contains magic gems like:
  • win32-api
  • windows-api
With their help I can easily get an access to Win32 API functions and call them from Ruby. But it works up to the moment when you encounter windows of Internet Explorer_Server class. Their children are HTML elements and they're not accessible directly via API. It's not a problem if we talk about browser. We can use Selenium or Watir to do all we need. But what if we talk about embeded Internet Explorer_Server windows which are placed on non-browser window? One of the ways is to get an access to corresponsing OLE object and hit it's properties and methods. Oh, good idea. But the main question is: how? All we know is just and HWND of this window. There's nothing impossible and this problem has the solution as well. Actually, the problem resolution took a couple of days of surfing the google and numerious experimenting. And finally I succeeded. Core part of this resolution can be found here. I'm talking about the following code (thanks to Park Heesob for this example):
require 'windows/com'
require 'windows/unicode'
require 'windows/error'
require 'windows/national'
require 'windows/window/message'
require 'windows/msvcrt/buffer'

require 'watir/win32ole'

include Windows::COM
include Windows::Unicode
include Windows::National
include Windows::Error
include Windows::Window::Message
include Windows::MSVCRT::Buffer

VariantInit = Win32::API.new('VariantInit', 'P', 'V', 'oleaut32')
OleUninitialize = Win32::API.new('OleUninitialize', 'V', 'V', 'ole32')

DISPID_UNKNOWN = -1
DISPID_VALUE = 0
DISPID_PROPERTYPUT = -3
DISPID_NEWENUM = -4
DISPID_EVALUATE = -5
DISPID_CONSTRUCTOR = -6
DISPID_DESTRUCTOR = -7
DISPID_COLLECT = -8

DISPATCH_METHOD = 0x1
DISPATCH_PROPERTYGET = 0x2
DISPATCH_PROPERTYPUT = 0x4
DISPATCH_PROPERTYPUTREF = 0x8

# Initialize OLE Libraries.
OleInitialize()

SMTO_ABORTIFHUNG = 0x0002
ObjectFromLresult = Win32::API.new('ObjectFromLresult', 'LPIP', 'L', 'oleacc')
IID_NULL = [0x00000000,0x0000,0x0000,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00].pack('LSSC8')

def get_control_from_hwnd(hnd)
  CoInitialize(0)
  reg_msg = RegisterWindowMessage("WM_HTML_GETOBJECT")
  puts "msg: " + reg_msg.to_s
  
  iid =[0x626FC520,0xA41E,0x11CF,0xA7,0x31,0x00,0xA0,0xC9,0x08,0x26,0x37].pack('LSSC8')
  
  result = 0.chr*4
  SendMessageTimeout(hnd.hex, reg_msg, 0, 0, SMTO_ABORTIFHUNG,1000, result)
  puts "result unpacked: " + result.unpack("L").to_s
  
  result = result.unpack('L')[0]
  idisp = 0.chr * 4
  r = ObjectFromLresult.call(result, iid, 0, idisp)
  if r == 0
    idisp = idisp.unpack('L').first
    puts "idisp: " + idisp.to_s
  end
  
  pDisp = idisp
  ucPtr = multi_to_wide("Script")
  lpVtbl = 0.chr * 4
  table = 0.chr * 28
  memcpy(lpVtbl,pDisp,4)
  memcpy(table,lpVtbl.unpack('L').first,28)
  table = table.unpack('L*')
  getIDsOfNames = Win32::API::Function.new(table[5],'PPPLLP','L')
  dispID = 0.chr * 4
  getIDsOfNames.call(pDisp,IID_NULL,[ucPtr].pack('P'),1,LOCALE_USER_DEFAULT,dispID)
  dispID = dispID.unpack('L').first
  
  dispParams = [0,0,0,0].pack('LLLL')
  
  res = 0.chr * 16
  invoke = Win32::API::Function.new(table[6],'PLPLLPPPP','L')
  hr = invoke.call(pDisp,
  dispID, IID_NULL, LOCALE_SYSTEM_DEFAULT,
  DISPATCH_PROPERTYGET | DISPATCH_METHOD,
  dispParams, res, nil, nil)
  
  if hr != S_OK
    raise StandardError, "IDispatch::Invoke() failed with %08x" % hr
  end
  
  res = res.unpack('SSSSLL')
  
  if res[0] == VT_DISPATCH
    pDisp = res[4]
    
    ucPtr = multi_to_wide("Document")
    lpVtbl = 0.chr * 4
    table = 0.chr * 28
    memcpy(lpVtbl,pDisp,4)
    memcpy(table,lpVtbl.unpack('L').first,28)
    table = table.unpack('L*')
    getIDsOfNames = Win32::API::Function.new(table[5],'PPPLLP','L')
    dispID = 0.chr * 4
    getIDsOfNames.call(pDisp,IID_NULL,[ucPtr].pack('P'),1,LOCALE_USER_DEFAULT,dispID)
    dispID = dispID.unpack('L').first
    
    dispParams = [0,0,0,0].pack('LLLL')
    
    res = 0.chr * 16
    invoke = Win32::API::Function.new(table[6],'PLPLLPPPP','L')
    hr = invoke.call(pDisp,
    dispID, IID_NULL, LOCALE_SYSTEM_DEFAULT,
    DISPATCH_PROPERTYGET | DISPATCH_METHOD,
    dispParams, res, nil, nil)
    if hr != S_OK
      raise StandardError, "IDispatch::Invoke() failed with %08x" % hr
    end
  
    res = res.unpack('SSSSLL')
    
    if res[0] == VT_DISPATCH
      pDisp = res[4]
      
      ucPtr = multi_to_wide("parentWindow")
      lpVtbl = 0.chr * 4
      table = 0.chr * 28
      memcpy(lpVtbl,pDisp,4)
      memcpy(table,lpVtbl.unpack('L').first,28)
      table = table.unpack('L*')
      getIDsOfNames = Win32::API::Function.new(table[5],'PPPLLP','L')
      dispID = 0.chr * 4
      getIDsOfNames.call(pDisp,IID_NULL,[ucPtr].pack('P'),1,LOCALE_USER_DEFAULT,dispID)
      dispID = dispID.unpack('L').first
      
      dispParams = [0,0,0,0].pack('LLLL')
      
      res = 0.chr * 16
      invoke = Win32::API::Function.new(table[6],'PLPLLPPPP','L')
      hr = invoke.call(pDisp,
      dispID, IID_NULL, LOCALE_SYSTEM_DEFAULT,
      DISPATCH_PROPERTYGET | DISPATCH_METHOD,
      dispParams, res, nil, nil)
      
      if hr != S_OK
        raise StandardError, "IDispatch::Invoke() failed with %08x" % hr
      end
      
      res = res.unpack('SSSSLL')
      
      if res[0] == VT_DISPATCH
        puts "#{res}"
        return res[4]
      end
    end
  end
  0
end
Maybe the time will come and I'll dig into this code deeper to get an idea what exactly is going on here but for now it's enough for me to know that this code returns the Dispatch pointer to the OLE object corresponding to the window with specified HWND (passed as parameter). The only thing left here is to connect to OLE object and use it methods and properties. It looks like:
obj = get_control_from_hwnd(handle)
ie = WIN32OLE.connect_unknown( obj )
Where handle variable contains previously found HWND of our OLE object. After that we are free to use OLE object. So, I can write something like:
handle = "00C40180"
obj = get_control_from_hwnd(handle)
ie = WIN32OLE.connect_unknown( obj )
puts ie.window.document.body.innerHTML
Of course, handle should be initialized with the proper HWND value of existing object. In order to get more details about document objects and methods we just have to dig into MSDN here.

That's it and current problem no longer disturbs me, so let's look for other challenges.

No comments:

Post a Comment