Add browser action button in internet explorer BHO
Asked Answered
A

4

39

So. I'm working on a BHO in IE and I want to add a browser action like this:

enter image description here

In internet explorer it would look something like

enter image description here

The only tutorials and docs I've found were on creating toolbar items. None mentioned this option. I know this is possible because crossrider let you do this exact thing. I just don't know how.

I can't find any documentation on how I would implement this in a BHO. Any pointers are very welcome.

I tagged this with C# as a C# solution would probably be simpler but a C++ solution, or any other solution that works is also very welcome.

Amphichroic answered 26/1, 2014 at 13:49 Comment(19)
Take a look at github.com/trigger-corp/browser-extensions/tree/master/ie/… for inspiration.Tribrach
@RobW I was hoping you'd find this question. You're probably one of the few people who know how to do this. If you could find it in your heart (I've searched that codebase and I'm still clueless) to answer this question with code and/or a self contained BHO bootstrap that just adds a button and runs a JavaScript alert when it is clicked I will be eternally grateful and I'm sure it'll help a lot of people. I don't mind offering a hefty bounty if that's a motivator and I can pay you if that's an incentive but I doubt the time would be worth the tax handling and paperwork for you.Amphichroic
@BenjaminGruenbaum: Have you tried to find out about your support options with that browser vendor regarding your issue?Impotent
@Impotent I sent an email to Microsoft's IEBlog team - no reply yet but it has only been two hours so not giving up yet.Amphichroic
Would you mind hosting the 2 images on stackoverflow.com, as my proxy currently blocks imgur? Also, maybe, update your question with infos on the targeted versions (Windows and IE)Inappreciative
@Inappreciative images hosted on Stack Overflow are hosted through imgur. We want IE9+ which all look kind of similar :)Amphichroic
<mylife>LOL. The images that I can see are for example on flick. Sorry for the dummy request. I hate that proxy. Will take a look at home.</mylife>Inappreciative
Working on it. 2 days left may be short.Inappreciative
@Inappreciative it's three actually (as long as you post before it ends). Though if you solve it and don't make it - I promise you another bounty.Amphichroic
I will try to update my answer in the comming days, aiming at a complete implementation guide. Note the "try" word :-)Inappreciative
Great start! Keep me updated on progress!Amphichroic
Since 2/3 of the answers are aimed at C++, I guess that editing the tags to include C++ or remove C# would make sense, not?Tribrach
@RobW It might, I included C# because that's 'easier'. I'd like to include C++ too without discarding C# but I'm really unsure what to discard. I might just discard C#. What do you think?Amphichroic
winapi is implied by bho, so I suggest winapi -> c++.Tribrach
I will upload my full code this week. I don't know where or how. Maybe github, maybe codeplex. Problem is, I don't want to clutter a little more my PC and I am reluctant to install a new bloatware (as "git for windows"). I am pretty happy with svn in the command line, and may not have the time to "learn" the git "cli" in a few days.Inappreciative
I am late, but will deliver. Just added a new part. Github account created.Inappreciative
@Inappreciative awesome work! Keep it up! :)Amphichroic
@Inappreciative any updates on the GH repo?Amphichroic
@BenjaminGruenbaum The code is done. To be developed later: dealing with the Tab Drag & Drop case. I just lost more than one hour trying to set up a way to upload my project to github. That's a real mess, and I am pretty upset, right now. I will try again, maybe, tomorrow.Inappreciative
I
21

EDIT: https://github.com/somanuell/SoBrowserAction


Here is a screen shot of my work in progress.

New button in IE9

The things I did:

1. Escaping from the protected mode

The BHO Registration must update the HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Internet Explorer\Low Rights\ElevationPolicy key. See Understanding and Working in Protected Mode Internet Explorer.

I choose the process way because it's noted as "best practice" and is easier to debug, but the RunDll32Policy may do the trick, too.

Locate the rgs file containing your BHO registry settings. It's the one containing the upadte to the Registry Key 'Browser Helper Object'. Add to that file the following:

HKLM {
  NoRemove SOFTWARE {
    NoRemove Microsoft {
      NoRemove 'Internet Explorer' {
        NoRemove 'Low Rights' {
          NoRemove ElevationPolicy {
            ForceRemove '{AE6E5BFE-B965-41B5-AC70-D7069E555C76}' {
              val AppName = s 'SoBrowserActionInjector.exe'
              val AppPath = s '%MODULEPATH%'
              val Policy = d '3'
            }
          }
        }
      }
    }
  }
}

The GUID must be a new one, don't use mine, use a GUID Generator. The 3 value for policy ensures that the broker process will be launched as a medium integrity process. The %MODULEPATH%macro is NOT a predefined one.

Why use a macro? You may avoid that new code in your RGS file, provided that your MSI contains that update to the registry. As dealing with MSI may be painful, it's often easier to provide a "full self registering" package. But if you don't use a macro, you then can't allow the user to choose the installation directory. Using a macro permits to dynamically update the registry with the correct installation directory.

How to make the macro works? Locate the DECLARE_REGISTRY_RESOURCEID macro in the header of your BHO class and comment it out. Add the following function definition in that header:

static HRESULT WINAPI UpdateRegistry( BOOL bRegister ) throw() {
   ATL::_ATL_REGMAP_ENTRY regMapEntries[2];
   memset( &regMapEntries[1], 0, sizeof(ATL::_ATL_REGMAP_ENTRY));
   regMapEntries[0].szKey = L"MODULEPATH";
   regMapEntries[0].szData = sm_szModulePath;
   return ATL::_pAtlModule->UpdateRegistryFromResource(IDR_CSOBABHO, bRegister,
                                                       regMapEntries);
}

That code is borrowed from the ATL implementation for DECLARE_REGISTRY_RESOURCEID (in my case it's the one shipped with VS2010, check your version of ATL and update code if necessary). The IDR_CSOBABHO macro is the resource ID of the REGISTRY resource adding the RGS in your RC file.

The sm_szModulePath variable must contains the installation path of the broker process EXE. I choose to make it a public static member variable of my BHO class. One simple way to set it up is in the DllMain function. When regsvr32 load your Dll, DllMain is called, and the registry is updated with the good path.

extern "C" BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) {

   if ( dwReason == DLL_PROCESS_ATTACH ) {
      DWORD dwCopied = GetModuleFileName( hInstance,
                                          CCSoBABHO::sm_szModulePath,
                                          sizeof( CCSoBABHO::sm_szModulePath ) /
                                                        sizeof( wchar_t ) );
      if ( dwCopied ) {
         wchar_t * pLastAntiSlash = wcsrchr( CCSoBABHO::sm_szModulePath, L'\\' );
         if ( pLastAntiSlash ) *( pLastAntiSlash ) = 0;
      }
   }

   return _AtlModule.DllMain(dwReason, lpReserved);

}

Many thanks to Mladen Janković.

How to lauch the Broker process?

One possible place is in the SetSite implementation. It will be lauched many times, but we will deal with that in the process itself. We will see later that the broker process may benefit from receiving as argument the HWND for the hosting IEFrame. This can be done with the IWebBrowser2::get_HWND method. I suppose here that your already have an IWebBrowser2* member.

STDMETHODIMP CCSoBABHO::SetSite( IUnknown* pUnkSite ) {

   if ( pUnkSite ) {
      HRESULT hr = pUnkSite->QueryInterface( IID_IWebBrowser2, (void**)&m_spIWebBrowser2 );
      if ( SUCCEEDED( hr ) && m_spIWebBrowser2 ) {
         SHANDLE_PTR hWndIEFrame;
         hr = m_spIWebBrowser2->get_HWND( &hWndIEFrame );
         if ( SUCCEEDED( hr ) ) {
            wchar_t szExeName[] = L"SoBrowserActionInjector.exe";
            wchar_t szFullPath[ MAX_PATH ];
            wcscpy_s( szFullPath, sm_szModulePath );
            wcscat_s( szFullPath, L"\\" );
            wcscat_s( szFullPath, szExeName );
            STARTUPINFO si;
            memset( &si, 0, sizeof( si ) );
            si.cb = sizeof( si );
            PROCESS_INFORMATION pi;
            wchar_t szCommandLine[ 64 ];
            swprintf_s( szCommandLine, L"%.48s %d", szExeName, (int)hWndIEFrame );
            BOOL bWin32Success = CreateProcess( szFullPath, szCommandLine, NULL,
                                                NULL, FALSE, 0, NULL, NULL, &si, &pi );
            if ( bWin32Success ) {
               CloseHandle( pi.hThread );
               CloseHandle( pi.hProcess );
            }
         }
      }

      [...] 

2. Injecting the IEFrame threads

It appears that this may be the most complex part, because there are many ways to do it, each one with pros and cons.

The broker process, the "injector", may be a short lived one, with one simple argument (a HWND or a TID), which will have to deal with a unique IEFrame, if not already processed by a previous instance.

Rather, the "injector" may be a long lived, eventually never ending, process which will have to continually watch the Desktop, processing new IEFrames as they appear. Unicity of the process may be guaranteed by a Named Mutex.

For the time being, I will try to go with a KISS principle (Keep It Simple, Stupid). That is: a short lived injector. I know for sure that this will lead to special handling, in the BHO, for the case of a Tab Drag And Drop'ed to the Desktop, but I will see that later.

Going that route involves a Dll injection that survives the end of the injector, but I will delegate this to the Dll itself.

Here is the code for the injector process. It installs a WH_CALLWNDPROCRET hook for the thread hosting the IEFrame, use SendMessage (with a specific registered message) to immediatly trigger the Dll injection, and then removes the hook and terminates. The BHO Dll must export a CallWndRetProc callback named HookCallWndProcRet. Error paths are omitted.

#include <Windows.h>
#include <stdlib.h>

typedef LRESULT (CALLBACK *PHOOKCALLWNDPROCRET)( int nCode, WPARAM wParam, LPARAM lParam );
PHOOKCALLWNDPROCRET g_pHookCallWndProcRet;
HMODULE g_hDll;
UINT g_uiRegisteredMsg;

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE, char * pszCommandLine, int ) {

   HWND hWndIEFrame = (HWND)atoi( pszCommandLine );
   wchar_t szFullPath[ MAX_PATH ];
   DWORD dwCopied = GetModuleFileName( NULL, szFullPath,
                                       sizeof( szFullPath ) / sizeof( wchar_t ) );
   if ( dwCopied ) {
      wchar_t * pLastAntiSlash = wcsrchr( szFullPath, L'\\' );
      if ( pLastAntiSlash ) *( pLastAntiSlash + 1 ) = 0;
      wcscat_s( szFullPath, L"SoBrowserActionBHO.dll" );
      g_hDll = LoadLibrary( szFullPath );
      if ( g_hDll ) {
         g_pHookCallWndProcRet = (PHOOKCALLWNDPROCRET)GetProcAddress( g_hDll,
                                                                      "HookCallWndProcRet" );
         if ( g_pHookCallWndProcRet ) {
            g_uiRegisteredMsg = RegisterWindowMessage( L"SOBA_MSG" );
            if ( g_uiRegisteredMsg ) {
               DWORD dwTID = GetWindowThreadProcessId( hWndIEFrame, NULL );
               if ( dwTID ) {
                  HHOOK hHook = SetWindowsHookEx( WH_CALLWNDPROCRET,
                                                  g_pHookCallWndProcRet,
                                                  g_hDll, dwTID );
                  if ( hHook ) {
                     SendMessage( hWndIEFrame, g_uiRegisteredMsg, 0, 0 );
                     UnhookWindowsHookEx( hHook );
                  }
               }
            }
         }
      }
   }
   if ( g_hDll ) FreeLibrary( g_hDll );
   return 0;
}

3. Surviving Injection: "hook me harder"

The temporary loading of the Dll in the main IE process is sufficient to add a new button to the Toolbar. But being able to monitor the WM_COMMAND for that new button requires more: a permanently loaded Dll and a hook still in place despite the end of the hooking process. A simple solution is to hook the thread again, passing the Dll instance handle.

As each tab opening will lead to a new BHO instantiation, thus a new injector process, the hook function must have a way to know if the current thread is already hooked (I don't want to just add a hook for each tab opening, that's not clean)

Thread Local Storage is the way to go:

  1. Allocate a TLS index in DllMain, for DLL_PROCESS_ATTACH.
  2. Store the new HHOOK as TLS data, and use that to know if the thread is already hooked
  3. Unhook if necessary, when DLL_THREAD_DETACH
  4. Free the TLS index in DLL_PROCESS_DETACH

That leads to the following code:

// DllMain
// -------
   if ( dwReason == DLL_PROCESS_ATTACH ) {
      CCSoBABHO::sm_dwTlsIndex = TlsAlloc();

      [...]

   } else if ( dwReason == DLL_THREAD_DETACH ) {
      CCSoBABHO::UnhookIfHooked();
   } else if ( dwReason == DLL_PROCESS_DETACH ) {
      CCSoBABHO::UnhookIfHooked();
      if ( CCSoBABHO::sm_dwTlsIndex != TLS_OUT_OF_INDEXES )
         TlsFree( CCSoBABHO::sm_dwTlsIndex );
   }

// BHO Class Static functions
// --------------------------
void CCSoBABHO::HookIfNotHooked( void ) {
   if ( sm_dwTlsIndex == TLS_OUT_OF_INDEXES ) return;
   HHOOK hHook = reinterpret_cast<HHOOK>( TlsGetValue( sm_dwTlsIndex ) );
   if ( hHook ) return;
   hHook = SetWindowsHookEx( WH_CALLWNDPROCRET, HookCallWndProcRet,
                             sm_hModule, GetCurrentThreadId() );
   TlsSetValue( sm_dwTlsIndex, hHook );
   return;
}

void CCSoBABHO::UnhookIfHooked( void ) {
   if ( sm_dwTlsIndex == TLS_OUT_OF_INDEXES ) return;
   HHOOK hHook = reinterpret_cast<HHOOK>( TlsGetValue( sm_dwTlsIndex ) );
   if ( UnhookWindowsHookEx( hHook ) ) TlsSetValue( sm_dwTlsIndex, 0 );
}

We now have a nearly complete hook function:

LRESULT CALLBACK CCSoBABHO::HookCallWndProcRet( int nCode, WPARAM wParam,
                                                LPARAM lParam ) {
   if ( nCode == HC_ACTION ) {
      if ( sm_uiRegisteredMsg == 0 )
         sm_uiRegisteredMsg = RegisterWindowMessage( L"SOBA_MSG" );
      if ( sm_uiRegisteredMsg ) {
         PCWPRETSTRUCT pcwprets = reinterpret_cast<PCWPRETSTRUCT>( lParam );
         if ( pcwprets && ( pcwprets->message == sm_uiRegisteredMsg ) ) {
            HookIfNotHooked();
            HWND hWndTB = FindThreadToolBarForIE9( pcwprets->hwnd );
            if ( hWndTB ) {
               AddBrowserActionForIE9( pcwprets->hwnd, hWndTB );
            }
         }
      }
   }
   return CallNextHookEx( 0, nCode, wParam, lParam);
}

The code for AddBrowserActionForIE9 will be edited later.

For IE9, getting the TB is pretty simple:

HWND FindThreadToolBarForIE9( HWND hWndIEFrame ) {
   HWND hWndWorker = FindWindowEx( hWndIEFrame, NULL,
                                   L"WorkerW", NULL );
   if ( hWndWorker ) {
      HWND hWndRebar= FindWindowEx( hWndWorker, NULL,
                                    L"ReBarWindow32", NULL );
      if ( hWndRebar ) {
         HWND hWndBand = FindWindowEx( hWndRebar, NULL,
                                       L"ControlBandClass", NULL );
         if ( hWndBand ) {
            return FindWindowEx( hWndBand, NULL,
                                 L"ToolbarWindow32", NULL );
         }
      }
   }
   return 0;
}

4. Processing the Tool Bar

That part may be largely improved:

  1. I just created a black and white bitmap, and all was fine, that is: the black pixels where transparent. Each time I tried to add some colors and/or grey levels, the results were awful. I am not fluent, at all, with those "bitmap in toolbar magics"
  2. The size of the bitmap should depends on the current size of the other bitmaps already in the toolbar. I just used two bitmaps (one "normal", and one "big")
  3. It may be possible to optimize the part which force IE to "redraw" the new state of the toolbar, with a lesser width for the address bar. It works, there is a quick "redraw" phase involving the whole IE Main Window.

See my other answer to the question, as I am currently unable to edit the answer with code format working.

Inappreciative answered 3/2, 2014 at 23:57 Comment(12)
That guide is pretty spot-on. With one adjustment, it can also be made functional in IE11. Add "HKCR\{...GUID HERE...}\Implemented Categories\{59fb2056-d625-48d0-a944-1a85b5ab2640}\" to the registry to enable the BHO even in Enhanced Protected mode.Tribrach
Hey, please add a self contained example to this answer. Thanks a lot for the effort :) If I were you I'd consider publishing it on GitHub.Amphichroic
@BenjaminGruenbaum Thanks for the bounty. I will update. Never used GitHub so far. Will see later.Inappreciative
@BenjaminGruenbaum Updated for the part one: "escaping protected mode", let me know it that level of details is good enough.Inappreciative
@Inappreciative that level of detail is very nice. It would be also very nice to have a self contained working solution (hosted somewhere, why I suggested GitHub) available to play with and understand. The rationale of this question to have anyone who wants to build BHO with this functionality both be able to and understand how it works. Great work so far by the way!Amphichroic
Thanks. Just let me know if you managed to build a working project. I am willing to fix bugs!Inappreciative
i have few problems with your code on github, it compiles fine, and mostly working (most calls does not return errors, i have added additional checks inside bho dll code), but icon just does not appear (createwindow call working and return not null), i also have checked registry and found what software\microsoft\internet explorer\low rights\elevationpolicy\$GUID\... does not created by regsvr, also added manually does not change anything, i have tested this on few installations of windows 7 with ie10 and ie11, do you have any suggestions what can be wrong ?Troublesome
I will take a look at the end of the week. Stay tuned. Try to add a log function with fopen/fwrite/fclose to AppData\LocalLow\log.txtInappreciative
UPDATE: i have found what HookCallWndProcRect is never called, but SetWindowHookEx in broker process is success, as i understand it may be related to wrong registry data, especially to "software\microsoft\internet explorer\low rights\elevationpolicy\$GUID\"Troublesome
UPDATE2: i have implemented logging to file in both broker and bho itself, broker doing all job, but HookCallWndProcRet does not called inside bho, broker have successfully "GetProcAddress" call and also SetWindowHookEx call, i am bad in winapi programming so just stuck here ..., i can provide patch against your github code for logsTroublesome
@Troublesome I just downloaded the ZIP from github and built it on a fresh new PC. Works fine. Windows Seven + IE 9. Let me know if you find the problem with IE10 IE11.Inappreciative
thx for reply, currently we choose to develop ie toolbar due to lack of time for solving problems with this method, i still not able to get this to work in ie11, but now i have no more time for this, sorry...Troublesome
C
11

After further review, I realized that the "favorites and action toolbar" is just a plain old common controls toolbar (I previously assumed it was some sort of custom control).

I wasn't yet able to adjust my code and see where it takes me, but the approach should be slightly different from what I outlined below.

From what I can tell, if you want your toolbar button to have an image, you must first insert that image into the toolbars image list (TB_GETIMAGELIST to retrieve list, TB_ADDBITMAP to add your image).

Now we can create our TBBUTTON instance and send it to our toolbar with the TB_ADDBUTTONS or TB_INSERBUTTONS message.

That should get the button on the bar. But how to hook it up to your code?

The toolbar will generate a WM_COMMAND message when the button is clicked (probably with the iCommand member of the TBBUTTON structure in the low word of the wParam). So we just need to SetWindowsHookEx with WH_CALLWNDPROC and wait for that message...

Implementation coming up when I get it to work ;)


Original Answer

As we discussed earlier on chat, I have my doubts that there is an officially supported way to add additional buttons (or any UI element for that matter) at that location in the Internet Explorer UI.

However, there is still the "brute force" way of simply creating a new child window inside of the Internet Explorer window.

So far, I haven't been able to create a complete example, mainly because my attempts to resize the toolbar, on which the 3 action buttons sit, have failed.

Anyway, here is what I could come up with so far:

internal class MyButtonFactory
{
  public void Install()
  {

    IntPtr ieFrame = WinApi.FindWindowEx(IntPtr.Zero, IntPtr.Zero, "IEFrame", null);
    IntPtr navigationBar = WinApi.FindWindowEx(ieFrame, IntPtr.Zero, "WorkerW", "Navigation Bar");
    IntPtr reBar = WinApi.FindWindowEx(navigationBar, IntPtr.Zero, "ReBarWindow32", null);
    IntPtr controlBar = WinApi.FindWindowEx(reBar, IntPtr.Zero, "ControlBandClass", null);
    IntPtr toolsBar = WinApi.FindWindowEx(controlBar, IntPtr.Zero, "ToolbarWindow32", "Favorites and Tools Bar");

    IntPtr myButton = WinApi.CreateWindowEx(0, "Button", "MySpecialButtonName",
                                            WinApi.WindowStyles.WS_CHILD | WinApi.WindowStyles.WS_VISIBLE, 0, 0, 16,
                                            16,
                                            toolsBar, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);

    if (IntPtr.Zero == myButton)
    {
      Debug.WriteLine(new Win32Exception(Marshal.GetLastWin32Error()).Message);
    }

    IntPtr buttonWndProc = Marshal.GetFunctionPointerForDelegate(new WinApi.WndProc(WndProc));
    WinApi.SetWindowLongPtr(new HandleRef(this, myButton), -4, buttonWndProc); // -4 = GWLP_WNDPROC
  }

  [AllowReversePInvokeCalls]
  public IntPtr WndProc(IntPtr hWnd, WinApi.WM msg, IntPtr wParam, IntPtr lParam)
  {
    switch (msg)
    {
      case WinApi.WM.LBUTTONUP:
        MessageBox.Show("Hello World");
        break;
      default:
        return WinApi.DefWindowProc(hWnd, msg, wParam, lParam);
    }

    return IntPtr.Zero;
  }
}

This requires a couple of Windows API calls, which resulted in a 1600 lines beast copied together from pinvoke.net, so I will omit that from this post.

Besides the fact that I wasn't able to get the button to fit nicely into the toolbar, as soon as I set my own window message handler up, the button is no longer drawn.

So there is obviously still a lot of work required to make this approach work, but I thought I'd share this so far anyway.

Another idea that came to mind was to ignore the whole toolbar and just place the button next to it. Maybe that is easier to handle.

While wildly searching the web for Windows API related terms, I also came across the CodeProject article Add Your Control On Top Another Application, which seems like it could be very relevant in this context.

Calyptra answered 27/1, 2014 at 22:42 Comment(1)
I created a GitHub repo for this approach. Sadly, at this time all it does is crash IE :P github.com/oliversalzburg/ie-buttonCalyptra
I
2

Continuation from my other answer.

Code for the AddBrowserActionForIE9 function.

void AddBrowserActionForIE9( HWND hWndIEFrame, HWND hWndToolBar ) {

   // do nothing if already done
   LRESULT lr = SendMessage( hWndToolBar, TB_BUTTONCOUNT, 0, 0 );
   UINT ButtonCount = (UINT)lr;
   for ( WPARAM index = 0; index < ButtonCount; ++index ) {
      TBBUTTON tbb;
      LRESULT lr = SendMessage( hWndToolBar, TB_GETBUTTON, index, reinterpret_cast<LPARAM>( &tbb ) );
      if ( lr == TRUE ) {
         if ( tbb.idCommand == 4242 ) return;
      }
   }
   HIMAGELIST hImgList = (HIMAGELIST)SendMessage( hWndToolBar, TB_GETIMAGELIST, 0, 0 );
   HIMAGELIST hImgListHot = (HIMAGELIST)SendMessage( hWndToolBar, TB_GETHOTIMAGELIST, 0, 0 );
   HIMAGELIST hImgListPressed = (HIMAGELIST)SendMessage( hWndToolBar, TB_GETPRESSEDIMAGELIST, 0, 0 );
   // load little or big bitmap
   int cx, cy;
   BOOL bRetVal = ImageList_GetIconSize( hImgList, &cx, &cy );

   HBITMAP hBitMap = LoadBitmap( CCSoBABHO::sm_hModule,
                                 MAKEINTRESOURCE( cx <= 17 ? IDB_BITMAP_SO_LITTLE : IDB_BITMAP_SO_BIG ) );
   int iImage = -1;
   if ( hImgList ) {
      iImage = ImageList_Add( hImgList, hBitMap, NULL );
   }
   if ( hImgListHot ) {
      ImageList_Add( hImgListHot, hBitMap, NULL );
   }
   if ( hImgListPressed ) {
      ImageList_Add( hImgListPressed, hBitMap, NULL );
   }
   TBBUTTON tbb;
   memset( &tbb, 0, sizeof( TBBUTTON ) );
   tbb.idCommand = 4242;
   tbb.iBitmap = iImage;
   tbb.fsState = TBSTATE_ENABLED;
   tbb.fsStyle = BTNS_BUTTON;
   lr = SendMessage( hWndToolBar, TB_INSERTBUTTON, 0, reinterpret_cast<LPARAM>( &tbb ) );
   if ( lr == TRUE ) {
      // force TB container to expand
      HWND hWndBand = GetParent( hWndToolBar );
      RECT rectBand;
      GetWindowRect( hWndBand, &rectBand );
      HWND hWndReBar = GetParent( hWndBand );
      POINT ptNew = { rectBand.left - cx, rectBand.top };
      ScreenToClient( hWndReBar, &ptNew );
      MoveWindow( hWndBand, ptNew.x, ptNew.y, rectBand.right - rectBand.left + cx,
                  rectBand.bottom - rectBand.top, FALSE );
      // force IE to resize address bar
      RECT rect;
      GetWindowRect( hWndIEFrame, &rect );
      SetWindowPos( hWndIEFrame, NULL, rect.left, rect.top, rect.right - rect.left + 1,
                    rect.bottom - rect.top, SWP_NOZORDER );
      SetWindowPos( hWndIEFrame, NULL, rect.left, rect.top, rect.right - rect.left,
                    rect.bottom - rect.top, SWP_NOZORDER );
   }
   if ( hBitMap ) DeleteObject( hBitMap );
   return;
}

5. Routing the Click

The simplest way to listen to the click is just to catch WM_COMMAND messages in the hook and check the command Id in wParam. Real production code may be more complete (verify that the WM_COMMAND is indeed coming from the Toolbar).

if ( pcwprets && ( pcwprets->message == WM_COMMAND ) ) {
   if ( LOWORD( pcwprets->wParam ) == 4242 ) {
      NotifyActiveBhoIE9( pcwprets->hwnd );
   }
}

The NotifyActiveBhoIE9 function will:

a) Find the IEFrame in the current thread
b) Find the current activated tab for the found IEFrame
c) Find the thread hosting the tab

Each BHO instance will have an invisible window created with the Thread Identifier in it's Window Text. A simple FindWindow call will give us that window and the BHO will be notified with a message.

Creating the private window:

// New Members in CCSoBABHO
static wchar_t * sm_pszPrivateClassName
static void RegisterPrivateClass( void );
static void UnregisterPrivateClass( void );
HWND m_hWndPrivate;
static LRESULT CALLBACK wpPrivate( HWND hWnd, UINT uiMsg,
                                   WPARAM wParam, LPARAM lParam );
static wchar_t * MakeWindowText( wchar_t * pszBuffer, size_t cbBuffer,
                                 DWORD dwTID );
bool CreatePrivateWindow( void );
bool DestroyPrivateWindow( void ) {
   if ( m_hWndPrivate ) DestroyWindow( m_hWndPrivate );
};

// implementation
wchar_t * CCSoBABHO::sm_pszPrivateClassName = L"SoBrowserActionClassName";

void CCSoBABHO::RegisterPrivateClass( void ) {
   WNDCLASS wndclass;
   memset( &wndclass, 0, sizeof( wndclass ) );
   wndclass.hInstance = sm_hInstance;
   wndclass.lpszClassName = sm_pszPrivateClassName;
   wndclass.lpfnWndProc = wpPrivate;
   RegisterClass( &wndclass );
   return;
}

void CCSoBABHO::UnregisterPrivateClass( void ) {
   UnregisterClass( sm_pszPrivateClassName, sm_hInstance );
   return;
}

wchar_t * CCSoBABHO::MakeWindowText( wchar_t * pszBuffer, size_t cbBuffer,
                                     DWORD dwTID ) {
   swprintf( pszBuffer, cbBuffer / sizeof( wchar_t ),
             L"TID_%.04I32x", dwTID );
   return pszBuffer;
}

bool CCSoBABHO::CreatePrivateWindow( void ) {
   wchar_t szWindowText[ 64 ];
   m_hWndPrivate = CreateWindow( sm_pszPrivateClassName,
                                 MakeWindowText( szWindowText,
                                                 sizeof( szWindowText ),
                                                 GetCurrentThreadId() ),
                                 0, 0, 0,0 ,0 ,NULL, 0, sm_hInstance, this );
   return m_hWndPrivate ? true : false;
}

Call sites:
RegisterPrivateClass called in DllMain, when PROCESS_ATTACH
UnregisterPrivateClass called in DllMain, when PROCESS_DETACH
CreatePrivateWindow called in SetSite, when pUnkSite != NULL
DestroyPrivateWindow called in SetSite, when pUnkSite == NULL

The NotifyActiveBhoIE9 implementation:

void CCSoBABHO::NotifyActiveBhoIE9( HWND hWndFromIEMainProcess ) {
   // Up to Main Frame
   HWND hWndChild = hWndFromIEMainProcess;
   while ( HWND hWndParent = GetParent( hWndChild ) ) {
      hWndChild = hWndParent;
   }
   HWND hwndIEFrame = hWndChild;

   // down to first "visible" FrameTab"
   struct ew {
      static BOOL CALLBACK ewp( HWND hWnd, LPARAM lParam ) {
      if ( ( GetWindowLongPtr( hWnd, GWL_STYLE ) & WS_VISIBLE ) == 0 ) return TRUE;
         wchar_t szClassName[ 32 ];
         if ( GetClassName( hWnd, szClassName, _countof( szClassName ) ) ) {
            if ( wcscmp( szClassName, L"Frame Tab" ) == 0 ) {
               *reinterpret_cast<HWND*>( lParam ) = hWnd;
               return FALSE;
            }
         }
         return TRUE;
      }
   };

   HWND hWndFirstVisibleTab = 0;
   EnumChildWindows( hwndIEFrame, ew::ewp,
                     reinterpret_cast<LPARAM>( &hWndFirstVisibleTab ) );
   if ( hWndFirstVisibleTab == 0 ) return;

   // down to first child, (in another process) 
   HWND hWndThreaded = GetWindow( hWndFirstVisibleTab, GW_CHILD );
   if ( hWndThreaded == 0 ) return;
   DWORD dwTID = GetWindowThreadProcessId( hWndThreaded, NULL );
   wchar_t szWindowText[ 64 ];
   HWND hWndPrivate = FindWindow( sm_pszPrivateClassName,
                                  MakeWindowText( szWindowText,
                                                  sizeof( szWindowText ), dwTID ) );
   if ( hWndPrivate ) SendMessage( hWndPrivate, WM_USER, 0, 0 );
}

The invisible window is connected to the BHO with a classic one: storing a this pointer in Windows Words.

LRESULT CALLBACK CCSoBABHO::wpPrivate( HWND hWnd, UINT uMsg,
                                       WPARAM wParam, LPARAM lParam ) {
   switch( uMsg ) {
      case WM_CREATE: {
         CREATESTRUCT * pCS = reinterpret_cast<CREATESTRUCT*>( lParam );
         SetWindowLongPtr( hWnd, GWLP_USERDATA,
                           reinterpret_cast<LONG_PTR>( pCS->lpCreateParams ) );
         return 0;
      }
      case WM_USER: {
         CCSoBABHO * pThis =
            reinterpret_cast<CCSoBABHO*>( GetWindowLongPtr( hWnd, GWLP_USERDATA ) );
         if ( pThis ) pThis->OnActionClick( wParam, lParam );
         break;
      }
      default: return DefWindowProc( hWnd, uMsg, wParam, lParam );
   }
   return 0;
}

6. Processing the "TAB DRAG & DROP" case

When you "Drag and Drop" a tab to the Desktop, IE9 creates a new IEFrame Main window, in a new thread in the source iexplore.exe process, hosting the tab.

To detect that, a simple solution is to listen to the DISPID_WINDOWSTATECHANGED event: use the IWebBrowser2::get_HWND method to retrieve the current IE main window. If that window is not the same as the previously save one, then the tab has been reparented. Then, just launch the broker process: if the new parent frame has not yet the button, it will be added.

case DISPID_WINDOWSTATECHANGED: {
   LONG lFlags = pDispParams->rgvarg[ 1 ].lVal;
   LONG lValidFlagsMask = pDispParams->rgvarg[ 0 ].lVal;
   LONG lEnabledUserVisible = OLECMDIDF_WINDOWSTATE_USERVISIBLE |
                              OLECMDIDF_WINDOWSTATE_ENABLED;
   if ( ( lValidFlagsMask & lEnabledUserVisible ) == lEnabledUserVisible ) {
      SHANDLE_PTR hWndIEFrame = 0;
      HRESULT hr = m_spIWebBrowser2->get_HWND( &hWndIEFrame );
      if ( SUCCEEDED( hr ) && hWndIEFrame ) {
         if ( reinterpret_cast<HWND>( hWndIEFrame ) != m_hWndIEFrame ) {
            m_hWndIEFrame = reinterpret_cast<HWND>( hWndIEFrame );
            LaunchMediumProcess();
         }
      }
   }
   break;
}

The github project has been updated.

Inappreciative answered 18/2, 2014 at 15:27 Comment(0)
C
1

Dll injection is the answer, buddy.

Here you go.

Edit:

Sure. It seems you don't have to do DLL injection, BHO's got access from the inside of the IE process. So then it's a lot easier.

Basicly, you need to find the window first. So by modifying the function to suit your needs, it will look like this:

BOOL FindFavoritesAndToolsBar(HWND mainWnd, HWND* addressBarWnd, HWND* cmdTargetWnd)
{
  mainWnd = ::FindWindowEx( mainWnd, NULL, TEXT( "WorkerW" ), NULL );
  mainWnd = ::FindWindowEx( mainWnd, NULL, TEXT( "ReBarWindow32" ), NULL );

  *cmdTargetWnd = ::FindWindowEx
  mainWnd, NULL, TEXT( "ControlBandClass" ), NULL );

  if( *cmdTargetWnd  )
      *addressBarWnd = ::FindWindowEx(
      *cmdTargetWnd, NULL, TEXT( "ToolbarWindow32" ), L"Favorites and Tools Bar" );

  return cmdTargetWnd != NULL;
}

I used Spy++ to find it.

The rest of the logic is the same as the article I linked. You subclass to intercept the message loop, and add your own event handlers for your own button.

Another approach is to just create a button as a popup window, set IE window as the parent, find the position of the "Favorities and tools bar", and position the button adjacent to it. Even easier, but less elegant of course.

Edit 2: Sorry, I just saw I echoed some of Oliver's answer. However, if you do what I wrote above inside the BHO, the button will behave as any of IE's own buttons and you have full control over it.

Cornhusk answered 3/2, 2014 at 18:23 Comment(1)
Thanks for trying to help. Please consider adding details.Amphichroic

© 2022 - 2024 — McMap. All rights reserved.