IE add-on development: globally capturing keyboard input

A long time ago, in the third article in my series on IE add-on development, I mentioned a way to globally handle keyboard input in an Internet Explorer add-in using a BHO. In this fourth installment I will talk about this.

The first thing we will need to do in order to handle keys with a BHO (Browser Helper Object) is actually write a BHO. Instead of wasting space here explaining how to do that, I will refer to this MSDN article on BHOs. That article uses ATL, while I do not, but it doesn't matter in this case.

Unfortunately the IE add-on model doesn't really offer any built-in methods for handling key input globally. What we need to do then is to use a keyboard hook, which allows us to intercept keyboard messages before they reach IE. In order to do this we must add an instance variable of type HHOOK to the class that implements our BHO (in the MSDN example this is CHelloWorldBHO, in my Find As You Type add-on it is SearchBrowserHelper; here I will call it ExampleBHO). We also need to add a static method that matches the signature of the KeyboardProc callback (if you use ATL the class definition and constructor will look different, but the rest remains the same; you must still add a HHOOK member and initialize it to NULL):

class ExampleBHO : public IObjectWithSite
{
public:
    ExampleBHO() : _hook(NULL)
    {
        /* Remaining constructor code omitted */
    }
    /* Other members omitted */
private:
    static LRESULT CALLBACK KeyboardProc(int code, WPARAM wParam, LPARAM lParam);
    HHOOK _hook;
public:
    static DWORD TlsIndex; // I will explain this further down.
};

DWORD ExampleBHO::TlsIndex = 0;

We then add the following code to the implementation of IObjectWithSite::SetSite, and we implement the KeyboardProc as well:

STDMETHODIMP SearchBrowserHelper::SetSite(IUnknown *punkSite)
{
    if( _hook != NULL )
    {
        // Remove any existing hooks
        UnhookWindowHookEx(_hook);
        _hook = NULL;
    }
    
    if( punkSite != NULL )
    {
        /* Code to retrieve the IWebBrowser2 interface goes here */
        
        _hook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, NULL, GetCurrentThreadId());
    }
    
    return S_OK;
}

LRESULT CALLBACK ExampleBHO::KeyboardProc(int code, WPARAM wParam, LPARAM lParam)
{
    // Code < 0 should be passed on without doing anything according to the docs.
    if( code >= 0 )
    {
        MessageBox(NULL, TEXT("Keypress detected"), TEXT("ExampleBHO)", MB_OK);
    }    
    return CallNextHookEx(NULL, code, wParam, lParam);
}

Let's look at that code, shall we. A browser helper object gets instantiated for each browser window or tab. Every browser window or tab running in the same process gets its own thread, and the BHO is instantiated on that thread. So we install a keyboard hook that will get the keyboard messages for that thread by passing in the result of GetCurrentThreadId() as the last parameter.

It should be noted that if you decide to cancel key propagation (by not calling CallNextHookEx) this means the keys for which you do this will lose their original function in IE.

One thing you likely want to do in the KeyboardProc however is interact with the BHO object. But since it's a static method, that's not possible (and it has to be static since you can't create a function pointer to an instance method, as you probably know). In the earlier examples with the toolbar we solved this problem by storing a pointer in the toolbar's window data, but a BHO has no window so we can't use that approach here. And since each thread has its own BHO we can't use a global variable either. Fortunately, a solution to this problem exists with Thread Local Storage. That's what the mysterious TlsIndex member I added above was for.

To use this, we must first allocate the storage we want to use, which must be done in the DllMain function:

BOOL APIENTRY DllMain(HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
{
    switch( ul_reason_for_call )
    {
    case DLL_PROCESS_ATTACH:
        ExampleBHO::TlsIndex = TlsAlloc()
        break;
    case DLL_PROCESS_DETACH:
        TlsFree(ExampleBHO::TlsIndex);
        break;
    }
    return TRUE;
}

We can now store a pointer to the BHO. Since we can guarantee our BHO will be created only once for each thread the most logical place to do this is the BHO class's constructor:

ExampleBHO() : _hook(NULL)
{
    /* Remaining constructor code omitted */
    TlsSetValue(TlsIndex, this);
}

Now we can retrieve the pointer in the KeyboardProc:

LRESULT CALLBACK ExampleBHO::KeyboardProc(int code, WPARAM wParam, LPARAM lParam)
{
    // Code < 0 should be passed on without doing anything according to the docs.
    if( code >= 0 )
    {
        ExampleBHO *bho = static_cast<ExampleBHO>(TlsGetValue(TlsIndex));
        
        /* Do something with the BHO */
        
        MessageBox(NULL, TEXT("Keypress detected"), TEXT("ExampleBHO)", MB_OK);
    }    
    return CallNextHookEx(NULL, code, wParam, lParam);
}

There remains however one problem. As I said earlier, in IE7 each tab has its own thread and each BHO will set the hook on that tab's thread. Unfortunately some of the browser's chrome such as the address bar that falls outside the tabs runs on yet another thread. This thread has no browser object associated with it, so it gets no BHO, and thus no hook. Find As You Type 1.1 has this very problem, which is why the CTRL-F shortcut for Find As You Type doesn't work if the address bar has focus.

But there is a way to solve this. It involves getting the top-level window handle, retrieving its thread using GetWindowThreadProcessId, installing a hook for that thread, and using some hocus-pocus to communicate with the currently active tab and to make sure we correctly handle the existence of multiple top-level windows in the same process. That will be the topic of the next article in this series (which hopefully will not take as long as this one). And I'm pleased to say that this solution has been implemented in the soon-to-be-released Find As You Type 1.2.

Categories: Programming
Posted on: 2007-01-24 10:43 UTC.

Comments

big head

2007-02-04 17:38 UTC

it's good article. very useful.
i find it while last two week..
(i'm not native . sorry !!!)

plz next article hurry up !!!!

Roberto

2011-02-26 06:12 UTC

Nice article, is there a way that you could provide us source code.

The thing is, im trying to implement this code using Visual Studio 2010, ATL project, etc.

But, the visual studio 2010 creates a dllmain.cpp containing the dll entry point... and by that im having problems in the part where you allocate in the Tls.

Could you help me?

inconsiderate means

2014-09-21 22:06 UTC

Appreciate the recommendation. Let me try it out.

Add comment

Latest posts

Categories

Archive

Syndication

RSS Subscribe