在Internet Explorer BHO中添加浏览器操作按钮

Ben*_*aum 38 c# winapi internet-explorer bho browser-action

所以.我在IE中正在开发BHO,我想添加一个像这样的浏览器动作:

在此输入图像描述

在Internet Explorer中它看起来像

在此输入图像描述

我发现的唯一教程和文档是创建工具栏项目.没有提到这个选项.我知道这是可能的,因为crossrider让你做到这一点.我只是不知道如何.

我找不到任何关于如何在BHO中实现这一点的文档.任何指针都非常受欢迎.

我用C#标记这个,因为C#解决方案可能更简单但是C++解决方案,或任何其他有效的解决方案也非常受欢迎.

man*_*ell 20

编辑:https://github.com/somanuell/SoBrowserAction


这是我正在进行的工作的屏幕截图.

IE9中的新按钮

我做的事情:

1.退出保护模式

BHO注册必须更新HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Internet Explorer\Low Rights\ElevationPolicy密钥.请参阅了解和在保护模式下工作Internet Explorer.

我选择过程方式因为它被称为"最佳实践"并且更容易调试,但RunDll32Policy也可以做到这一点.

找到rgs包含BHO注册表设置的文件.它是包含注册表项的upadte的那个'Browser Helper Object'.添加到该文件以下内容:

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'
            }
          }
        }
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

GUID必须是新的,不要使用我的,使用GUID生成器.3策略的值确保代理进程将作为中等完整性进程启动.%MODULEPATH%宏不是预定义的宏.

为什么要使用宏? 如果MSI包含对注册表的更新,则可以避免RGS文件中的新代码.由于处理MSI可能会很痛苦,因此提供"完全自我注册"包通常更容易.但是,如果您不使用宏,则您无法允许用户选择安装目录.使用宏允许使用正确的安装目录动态更新注册表.

如何使宏工作?DECLARE_REGISTRY_RESOURCEID在BHO类的标题中 找到宏并将其注释掉.在该标头中添加以下函数定义:

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);
}
Run Code Online (Sandbox Code Playgroud)

该代码是从ATL实现中借用的DECLARE_REGISTRY_RESOURCEID(在我的情况下,它是VS2010附带的代码,检查您的ATL版本并在必要时更新代码).该IDR_CSOBABHO宏是的资源ID REGISTRY资源添加RGS在你的RC文件.

sm_szModulePath变量必须包含代理进程EXE的安装路径.我选择将它作为我的BHO类的公共静态成员变量.设置它的一种简单方法是在DllMain函数中.当regsvr32加载您的DLL,DllMain被称为和注册表与良好的路径更新.

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);

}
Run Code Online (Sandbox Code Playgroud)

非常感谢MladenJanković.

如何实现经纪人流程?

一个可能的地方是SetSite实施.它将被多次推出,但我们将在这个过程中处理它.稍后我们将看到代理进程可能会受益于接收主机IEFrame的HWND作为参数.这可以通过该IWebBrowser2::get_HWND方法完成.我想这里你已经有了一个IWebBrowser2*成员.

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 );
            }
         }
      }

      [...] 
Run Code Online (Sandbox Code Playgroud)

2.注入IEFrame线程

看起来这可能是最复杂的部分,因为有很多方法可以做到,每一种都有利有弊.

代理进程,即"注入器",可能是一个短暂的进程,有一个简单的参数(HWND或TID),如果尚未由先前的实例处理,则必须处理唯一的IEFrame.

相反,"注入器"可能是一个长期存在的,最终永无止境的过程,它必须不断地观看桌面,在它们出现时处理新的IE框架.命名互斥锁可以保证该过程的单一性.

暂时,我会尝试使用KISS原则(Keep It Simple,Stupid).那就是:短暂的喷射器.我知道这肯定会在BHO中导致特殊处理,因为Tab拖放到桌面的情况下,但我会在稍后看到.

去那条路线涉及一个Dll注入,它在注入器结束时存活,但我会将它委托给Dll本身.

这是注射器过程的代码.它WH_CALLWNDPROCRET为托管IEFrame的线程安装一个钩子,使用SendMessage(用一个特定的注册消息)立即触发Dll注入,然后删除钩子并终止.BHO Dll必须导出一个CallWndRetProc名为的回调HookCallWndProcRet.错误路径被省略.

#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;
}
Run Code Online (Sandbox Code Playgroud)

3.幸存注射:"更加困难"

在主IE过程中临时加载Dll足以向工具栏添加新按钮.但是能够监视WM_COMMAND新按钮需要更多:尽管挂钩过程结束,仍然存在永久加载的Dll和挂钩.一个简单的解决方案是再次挂钩线程,传递Dll实例句柄.

由于每个标签打开将导致新的BHO实例化,因此一个新的注入过程,钩子函数必须有办法知道当前线程是否已经被钩住(我不想只为每个标签开口添加一个钩子,那不干净)

线程本地存储是要走的路:

  1. 在分配一个TLS的索引DllMain,对DLL_PROCESS_ATTACH.
  2. 将新存储HHOOK为TLS数据,并使用它来了解线程是否已被挂钩
  3. 必要时取消挂钩,何时 DLL_THREAD_DETACH
  4. 释放TLS索引 DLL_PROCESS_DETACH

这导致以下代码:

// 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 );
}
Run Code Online (Sandbox Code Playgroud)

我们现在有一个几乎完整的钩子功能:

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);
}
Run Code Online (Sandbox Code Playgroud)

AddBrowserActionForIE9稍后将编辑代码.

对于IE9,获取TB非常简单:

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;
}
Run Code Online (Sandbox Code Playgroud)

4.处理工具栏

那部分可能会大大改善:

  1. 我刚创建了一个黑白位图,一切都很好,就是:黑色像素透明.每次我尝试添加一些颜色和/或灰度级时,结果都很糟糕.我完全不熟悉那些"工具栏魔术中的位图"
  2. 位图的大小应取决于工具栏中已有的其他位图的当前大小.我只使用了两个位图(一个"普通",一个"大")
  3. 有可能优化强制IE"重绘"工具栏的新状态的部分,地址栏的宽度较小.它有效,有一个涉及整个IE主窗口的快速"重绘"阶段.

请参阅我对该问题的其他答案,因为我目前无法使用代码格式编辑答案.

  • 该指南非常适合.通过一次调整,它也可以在IE11中实现.将"HKCR\{... GUID HERE ...} \实施类别\ {59fb2056-d625-48d0-a944-1a85b5ab2640} \"添加到注册表中,即使在增强保护模式下也能启用BHO. (2认同)

Der*_*ler 11

经过进一步的审查,我意识到"收藏夹和动作工具栏"只是一个普通的旧的通用控件工具栏(我之前认为它是某种自定义控件).

我还没有能够调整我的代码,看看它在哪里,但方法应该与我下面概述的略有不同.

据我所知,如果您希望工具栏按钮有图像,则必须先将该图像插入工具栏图像列表(TB_GETIMAGELIST以检索列表,TB_ADDBITMAP添加图像).

现在我们可以创建我们的TBBUTTON实例并使用TB_ADDBUTTONSTB_INSERBUTTONS消息将其发送到我们的工具栏.

那应该得到吧台上的按钮.但是如何将它与你的代码挂钩?

WM_COMMAND单击按钮时,工具栏将生成一条消息(可能是低位字中结构的iCommand成员).所以我们只需要使用并等待消息...TBBUTTONwParamSetWindowsHookExWH_CALLWNDPROC

当我开始工作时,实施工作;)


原始答案

正如我们之前在聊天中所讨论的那样,我怀疑是否有一种官方支持的方式来在Internet Explorer UI中的该位置添加其他按钮(或任何UI元素).

但是,仍然存在简单地在Internet Explorer窗口内创建新子窗口的"强力"方式.

到目前为止,我还没有能够创建一个完整的示例,主要是因为我尝试调整3个操作按钮所在的工具栏的大小已经失败.

无论如何,到目前为止我能想到的是:

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;
  }
}
Run Code Online (Sandbox Code Playgroud)

这需要几个Windows API调用,这导致从pinvoke.net复制1600行的野兽,所以我将从这篇文章中省略.

除了我无法让按钮很好地适应工具栏之外,只要我自己设置了自己的窗口消息处理程序,就不再绘制按钮了.

所以显然仍然需要做很多工作才能使这种方法有效,但我认为无论如何我都会分享这个.

想到的另一个想法是忽略整个工具栏,只需将按钮放在旁边即可.也许这更容易处理.

虽然在Web上搜索与Windows API相关的术语,但我也遇到了CodeProject文章Add Your Control On Top Another Application,在这种情况下,它似乎非常相关.