实例解析自定义IE
右键上下文菜单
简介
如果你经常访问某个论坛,会发现有很多问题都是重复提问的,而且之前也有过详细的解答;或许也会发觉,有时很难搜索到特定的某个帖子;又或者用论坛的搜索引擎搜索出来后难以甄别哪个才是你想要的主题,难道就没有个地方可以存放这些有用的帖子呢?为什么不把这些链接就放在论坛的回复窗口中呢?就像下图:
实现自定义的上下文菜单
其实可以利用IE右键上下文菜单来保存及找到曾经看过的网页(或帖子),好了,下面我们来讲怎样实现:
打开注册表编辑器,定位至HKEY_CURRENT_USER/Software/Microsoft/Internet Explorer,在这里,如果没有一个名为MenuExt的键值,就创建一个。在它下面,再创建一个键值,命名为“添加到收藏夹”。
现在,要来定义什么条件下显示这个菜单了,例如,想要它在单击链接时显示、或在主窗体而不是文本框中显示等等,这由一个“Contexts”值来决定——添加一个DWORD键值并命名为“Contexts”,设置值为1,表示它是默认上下文菜单。
在设置好菜单项及何时显示之后,需要指定单击菜单后的动作,这可通过指定脚本的位置来完成,脚本在此可为JavaScript或VBScript。我们设置上下文菜单默认值为一个HTML文件的位置,如C:/tmp/AddToFavorites.html。在C:/tmp下,创建一个名为AddToFavorites.html的新文件,用记事本打开并输入以下代码:
<SCRIPT LANGUAGE="JavaScript">
alert('点击了‘添加到收藏夹’');
</SCRIPT>
这时再打开一个新的IE窗口,右键单击之后应该会看到“添加到收藏夹”菜单项,点击之后会弹出一个消息框“点击了‘添加到收藏夹’”。
同样,在MenuExt下再添加一个键值,如“显示收藏夹”,设置默认值为“C:/tmp/ShowFavorites.html”,添加一个名为“Contexts”的DWORD键值,并设为4,这里的4表示当右键单击控件时(如文本框)显示菜单项。
像前面一样,在C:/tmp下创建一个“ShowFavorites.html”文件,并输入以下代码:
<SCRIPT LANGUAGE="JavaScript">
alert('点击了‘显示收藏夹’');
</SCRIPT>
基本概念已经讲完了,现在要添加必要的脚本动作了,分两步进行:
Ø 当点击“添加到收藏夹”时,是想保存URL及文档标题到文件中。
Ø 第二步刚好相反,也就是说,在文本框中右键单击并选择“显示收藏夹”时,应弹出一个菜单,可供选择其中一项,以便粘贴其中内容到文本框中。
看到这,可能会想到有很多种方法来完成上述目标,甚至完全使用JavaScript都可以,而在这我们是要用C++及COM来完成,也就是在一个ActiveX类中实现这两个功能。
打开Visual Studio IDE(VS2008、VS2005、或VS 6.0),创建一个新项目,选择“ATL COM Appwizard”,输入名字如“Favorites”,其他都以默认设置,点击完成,生成项目:
· 如果使用VS6.0,找到“插入(Insert)”菜单,选择“New ATL Object”,在控件种类(controls category)中选择“Lite Control”,点击下一步,对短名称(shortname),指定“Favorites”,点击OK,生成。
· 如果使用VS2008,找到“项目”菜单,选择“添加类”,选择ATL---->ATL控件,点击添加,对短名称,指定“Favorites”,点击完成,生成。
转到Classview,右键单击IFavorites选择“Add Method”,输入“ShowDefaultContextMenu”作为方法名,而对于参数,则输入:IDispatch* pDispatch, BSTR bstrTitle, BSTR bstrURL,(在VS2008中,这些参数需要一个一个添加),点击OK完成,生成。
同样地,再添加一个方法ShowTextAreaContextMenu,此时参数为IDispatch* pDispatch,点击OK完成,生成。
打开Favorites.cpp并添加下列代码到ShowDefaultContextMenu的实现中。
STDMETHODIMP CFavorites::ShowDefaultContextMenu(
IDispatch *pDispatch, BSTR bstrTitle, BSTR bstrURL)
{
::MessageBoxW(NULL,bstrTitle, bstrURL,MB_OK);
return S_OK;
}
打开AddToFavorites.html并用下列代码替换当中脚本:
<SCRIPT LANGUAGE="JavaScript">
var parentwin = external.menuArguments;
var doc = parentwin.document;
var str = new String(parentwin.event.srcElement.name);
var oFav = new ActiveXObject("Favorites.Favorites");
oFav.ShowDefaultContextMenu(parentwin,doc.title, doc.location);
</SCRIPT>
运行IE,随意打开一个网站,右键单击并选择“添加到收藏夹”,应该会显示一个带有标题及URL的消息框。
好了,第一个目标已经完成。基本上来说,当右键单击时,IE会查看需要为当前上下文菜单添加点什么,这时就会追加上自定义的菜单项。其中某些重要的信息要在menuArguments中传递,如要获取的URL、标题、窗口对象。
来看第二个目标,即显示弹出菜单并传递相关内容到编辑框中。下面的代码就是创建了一个有两个子菜单的弹出菜单,并以TrackPopupMenu API显示出来。
#include <exdisp.h>
STDMETHODIMP CCGFavorites::ShowTextAreaContextMenu(IDispatch *pDispatch)
{
//创建弹出菜单
HMENU hPopupMenu = CreatePopupMenu();
//插入相关菜单项
InsertMenuW(hPopupMenu,0,MF_BYPOSITION,1000,L"First");
InsertMenuW(hPopupMenu,1,MF_BYPOSITION,1001,L"Second");
//获取浏览器窗口的hWnd
CComQIPtr<IServiceProvider> isp = pDispatch;
CComQIPtr<IWebBrowser2> pBrowser2;
isp->QueryService(IID_IWebBrowserApp,IID_IWebBrowser2,
(void**)&pBrowser2);
HWND hWnd;
pBrowser2->get_HWND((long*)&hWnd);
//显示菜单
POINT pt;
GetCursorPos(&pt);
int iSelection = ::TrackPopupMenu(hPopupMenu,
TPM_LEFTALIGN | TPM_LEFTBUTTON | TPM_RETURNCMD,
pt.x,pt.y, 0,hWnd,NULL);
DestroyMenu(hPopupMenu);
return S_OK;
}
此处唯一要注意的是怎样获取浏览器句柄,这也是使用pDispatch的目的所在。接下来,打开ShowFavorites.html文件,并添加以下代码:
<SCRIPT LANGUAGE="JavaScript">
var parentwin = external.menuArguments;
var oFav = new ActiveXObject("Favorites.Favorites");
oFav.ShowTextAreaContextMenu(parentwin);
</SCRIPT>
再打开IE,这次到
www.gmail.com,在用户名输入框中右键单击,这时会有一个“显示收藏夹”的菜单,点击它,咦?怎么没反应?也许是我们调用TrackPopupMenu时,IE也在调用TrackPopupMenu。
怎么解决这个问题呢,因为我们不是窗口的创建者,那只有子类化了,先子类化hwnd,再粘贴所需信息,处理之后显示菜单,然后取消子类化,以下是修改后的代码:
WNDPROC fnOldWndProc;
LRESULT CALLBACK SubclassWndProc(HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{
//判断是否为显示收藏夹列表的自定义消息
if (uMsg == (WM_APP + 1))
{
HMENU hPopupMenu = CreatePopupMenu();
InsertMenuW(hPopupMenu,0,MF_BYPOSITION,1000,L"First");
InsertMenuW(hPopupMenu,1,MF_BYPOSITION,1001,L"Second");
POINT pt;
GetCursorPos(&pt);
int iSelection = ::TrackPopupMenu(hPopupMenu,
TPM_LEFTALIGN | TPM_LEFTBUTTON | TPM_RETURNCMD,
pt.x,pt.y, 0,hwnd,NULL);
DestroyMenu(hPopupMenu);
return 0;
}
return CallWindowProc(fnOldWndProc, hwnd, uMsg,
wParam, lParam);
}
STDMETHODIMP CFavorites::ShowTextAreaContextMenu(IDispatch
*pDispatch)
{
//获取浏览器窗口hWnd
CComQIPtr<IServiceProvider> isp = pDispatch;
CComQIPtr<IWebBrowser2> pBrowser2;
isp->QueryService(IID_IWebBrowserApp,IID_IWebBrowser2,
(void**)&pBrowser2);
HWND hWnd;
pBrowser2->get_HWND((long*)&hWnd);
//在此子类化窗口以便能处理自定义消息来显示菜单
fnOldWndProc = (WNDPROC)::SetWindowLong(hWnd,GWL_WNDPROC,
(DWORD)SubclassWndProc);
::PostMessage(hWnd, (WM_APP + 1), 0,0);
//恢复原先的WndProc
::SetWindowLong(hWnd,GWL_WNDPROC,(DWORD)fnOldWndProc);
return S_OK;
}
生成之后,打开IE来到
www.gmail.com,在用户名输入框上右键单击,点击“显示收藏夹”,这一次,菜单出来了,但闪了一下就不见了。
还是不正确?可能是Post出消息之后没有等到它完成,再添加一个事件,并等PostMessage完成,思路大致是创建一个手工重置的事件,初始设为未引发,并在PostMessage之后等待它,子类化的过程当从TrackPopupMenu返回时将重置事件。下面是修改后的代码:
WNDPROC fnOldWndProc;
LRESULT CALLBACK SubclassWndProc(HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{
if (uMsg == (WM_APP + 1))
{
HMENU hPopupMenu = CreatePopupMenu();
InsertMenuW(hPopupMenu,0,MF_BYPOSITION,1000,L"First");
InsertMenuW(hPopupMenu,1,MF_BYPOSITION,1001,L"Second");
POINT pt;
GetCursorPos(&pt);
int iSelection = ::TrackPopupMenu(hPopupMenu,
TPM_LEFTALIGN | TPM_LEFTBUTTON | TPM_RETURNCMD,
pt.x,pt.y, 0,hwnd,NULL);
//在显示菜单后,发信号给事件
SetEvent((HANDLE)lParam);
DestroyMenu(hPopupMenu);
return 0;
}
return CallWindowProc(fnOldWndProc, hwnd, uMsg,
wParam, lParam);
}
STDMETHODIMP CFavorites::
ShowTextAreaContextMenu(IDispatch *pDispatch)
{
CComQIPtr<IServiceProvider> isp = pDispatch;
CComQIPtr<IWebBrowser2> pBrowser2;
isp->QueryService(IID_IWebBrowserApp,IID_IWebBrowser2,
(void**)&pBrowser2);
HWND hWnd;
pBrowser2->get_HWND((long*)&hWnd);
HANDLE hEvent = CreateEvent(NULL,TRUE,FALSE,NULL);
fnOldWndProc = (WNDPROC)::SetWindowLong(hWnd,GWL_WNDPROC,
(DWORD)SubclassWndProc);
::PostMessage(hWnd, (WM_APP + 1), 0,(LPARAM)hEvent);
//等待事件为有信号状态,此时表示菜单已显示
WaitForSingleObject(hEvent,INFINITE);
CloseHandle(hEvent);
::SetWindowLong(hWnd,GWL_WNDPROC,(DWORD)fnOldWndProc);
return S_OK;
}
生成后,执行前面相同的步骤,这次好多了,菜单显示出来后停留在那等待你选择,点击之后,无反应,当然了,因为还没有编写相关代码嘛。
要进行粘贴操作,可使用IWebBrowser2::ExecWB方法并传递进OLECMDID_PASTE作为命令ID,但我们在子类化过程中要怎样获得这个接口指针呢?其实很简单,这个指针已经在showTextAreaContextMenu方法中了,只需简单地把它作为WPARAM传递给消息就行了,如下所示:
//发送自定义消息来显示菜单
::PostMessage(hWnd, (WM_APP + 1), (WPARAM)pBrowser2.p, (LPARAM)hEvent);
在调用TrackPopupMenu之后,还需不回下列代码:
switch(iSelection)
{
case 1000:
{
CComBSTR oText(L"First one");
CComVariant oVarIn(oText);
CComVariant oVarOut;
//取得IWebBrowser2接口
CComPtr<IWebBrowser2> pSp = (IWebBrowser2*)wParam;
HRESULT hre = pSp->ExecWB(OLECMDID_PASTE,
OLECMDEXECOPT_DODEFAULT,&oVarIn,&oVarOut);
}
break;
case 1001:
{
CComBSTR oText(L"Second one");
CComVariant oVarIn(oText);
CComVariant oVarOut;
//取得IWebBrowser2接口
CComPtr<IWebBrowser2> pSp = (IWebBrowser2*)wParam;
HRESULT hre = pSp->ExecWB(OLECMDID_PASTE,
OLECMDEXECOPT_DODEFAULT,&oVarIn,&oVarOut);
}
break;
}
这里只是从wParam中取出IWebBrowser2接口,并调用ExecWB。
生成之后,在编辑框中重复上述测试步骤,只要不点击任何弹出菜单,一切正常,一旦点击其中某个,就会看到一个异常。究其原因,是因为IWebBrowser2接口在线程边界间传递,而依据COM的原则,这是不允许的,所以如果非要这么做,这依照以下二种方法:一是对接口进行“流(Stream)”化;二是使用全局接口表,全局接口表(GIT)可跨越进程,其是一个进程间的实体对象,如果有多个线程想要创建一个全局接口表,只会返回全局接口表的同一个实例,它就像是一个“装满接口的桶子”,所以如果想在线程间共享接口指针,把它们放在全局接口表中就行了,之后你会得到一个cookie,可把这个cookie传递给其他线程,接着这些线程会把它传递给全局接口表,并得到一个封送(marshal)接口。现在,我们只需创建一个全局接口表,放入IWebBrowser2接口,这次把cookie而不是接口指针传递给子类化过程。
下面是修改后的代码:
WNDPROC FNoLDwNDpROC;
LRESULT CALLBACK SubclassWndProc(HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{
if (uMsg == (WM_APP + 1))
{
HMENU hPopupMenu = CreatePopupMenu();
InsertMenuW(hPopupMenu,0,MF_BYPOSITION,1000,L"First");
InsertMenuW(hPopupMenu,1,MF_BYPOSITION,1001,L"Second");
POINT pt;
GetCursorPos(&pt);
int iSelection = ::TrackPopupMenu(hPopupMenu,
TPM_LEFTALIGN | TPM_LEFTBUTTON | TPM_RETURNCMD,
pt.x,pt.y, 0,hwnd,NULL);
switch(iSelection)
{
case 1000:
{
CComBSTR oText(L"First one");
CComVariant oVarIn(oText);
CComVariant oVarOut;
CComPtr<IWebBrowser2> pSp;
DWORD dwCookie = wParam;
CComQIPtr<IGlobalInterfaceTable, &IID_IGlobalInterfaceTable> spGIT;
CoCreateInstance(CLSID_StdGlobalInterfaceTable,
NULL,CLSCTX_INPROC_SERVER,
IID_IGlobalInterfaceTable,(void **)&spGIT);
spGIT->GetInterfaceFromGlobal(dwCookie, IID_IWebBrowser2,(void**)&pSp);
HRESULT hre = pSp->ExecWB(OLECMDID_PASTE,
OLECMDEXECOPT_DODEFAULT,&oVarIn,&oVarOut);
}
break;
case 1001:
{
CComBSTR oText(L"Second one");
CComVariant oVarIn(oText);
CComVariant oVarOut;
CComPtr<IWebBrowser2> pSp;
DWORD dwCookie = wParam;
CComQIPtr<IGlobalInterfaceTable, &IID_IGlobalInterfaceTable> spGIT;
CoCreateInstance(CLSID_StdGlobalInterfaceTable,
NULL,CLSCTX_INPROC_SERVER,
IID_IGlobalInterfaceTable,(void **)&spGIT);
spGIT->GetInterfaceFromGlobal(dwCookie, IID_IWebBrowser2,(void**)&pSp);
HRESULT hre = pSp->ExecWB(OLECMDID_PASTE,
OLECMDEXECOPT_DODEFAULT,&oVarIn,&oVarOut);
}
break;
}
//显示菜单后,发信号给事件
SetEvent((HANDLE)lParam);
DestroyMenu(hPopupMenu);
return 0;
}
return CallWindowProc(fnOldWndProc, hwnd, uMsg,
wParam, lParam);
}
STDMETHODIMP CFavorites::ShowTextAreaContextMenu(IDispatch
*pDispatch)
{
//创建一个全局接口表对象
//全局接口表用于封送IWebBrowser2接口指针
CComQIPtr<IGlobalInterfaceTable, &IID_IGlobalInterfaceTable> spGIT;
CoCreateInstance(CLSID_StdGlobalInterfaceTable,NULL,
CLSCTX_INPROC_SERVER,IID_IGlobalInterfaceTable, (void **)&spGIT);
//取得浏览器窗口hWnd
CComQIPtr<IServiceProvider> isp = pDispatch;
CComQIPtr<IWebBrowser2> pBrowser2;
isp->QueryService(IID_IWebBrowserApp,IID_IWebBrowser2, (void**)&pBrowser2);
//注册全局接口
DWORD dwCookie = 0;
spGIT->RegisterInterfaceInGlobal(pBrowser2,
IID_IWebBrowser2, &dwCookie);
HWND hWnd;
pBrowser2->get_HWND((long*)&hWnd);
HANDLE hEvent = CreateEvent(NULL,TRUE,FALSE,NULL);
//在此子类化窗口,以便处理自定义消息来显示菜单
fnOldWndProc = (WNDPROC)::SetWindowLong(hWnd,GWL_WNDPROC,
(DWORD)SubclassWndProc);
//发送自定义消息显示菜单
::PostMessage(hWnd, (WM_APP + 1), (WPARAM)dwCookie,
(LPARAM)hEvent);
//等待事件为有信号状态,表明菜单已完成
WaitForSingleObject(hEvent,INFINITE);
CloseHandle(hEvent);
//恢复原有WndProc
::SetWindowLong(hWnd,GWL_WNDPROC,(DWORD)fnOldWndProc);
return S_OK;
}
生成之后重复前述步骤,这一次在选择菜单后就没有异常发生了,然而,IE却失去响应了,这是怎么回事?
这次又是发生什么事了呢?你会发现,如果注释掉ExecWBm,一切正常,而当调用ExecWBm时,它向COM隐含使用的窗口对象发送了大量的窗口消息,然而,因ShowTextAreaContextMenu仍未完成并在等待WaitForSingleObject,致使这些消息没有被处理,形成死锁导致IE失去响应。解决办法是要当仍处于hEvent阻塞时,实现一种窗口消息处理机制,这时我们想到了MsgWaitForMultipleObjects,其刚好可满足上述条件。如下所示,用下列代码替换了WaitForSingleObject:
//开始循环
while (TRUE)
{
//代码块中局部变量
DWORD result ;
MSG msg ;
//在循环中读取所有消息
//并移除我们读取过的每条消息
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
DispatchMessage(&msg);
}
//等待任何发送到此队列中的消息
//或是等待传递进来一个已设为有信号状态的句柄
result = MsgWaitForMultipleObjects(1, &hEvent, FALSE, INFINITE, QS_ALLINPUT);
//result会告知我们事件的类型
if (result == (WAIT_OBJECT_0 + 1))
{
//新消息已到达
//继续回到while循环开始
//以分发它们并重新开始等待
continue;
}
else
{
//我们的事件为有信号状态,是时候退出循环了
break;
}
}
生成后再试一次,这次选择菜单后,文本成功粘贴上去了,我们的主要目标已经完成了,可还有一点改进的地方,如果要使用这个组件,就需要把这个DLL分发出去,而且还有HTML文件及注册表项,如果能打包在一起就方便多了,其实也不难。
Ø 打开项目的资源视图,鼠标右键单击选择“添加/插入新资源”,选择HTML并点击“导入”,找到HTML文件并选择它,同样可添加第二个HTML文件。
Ø 打开resource.h,记下IDR_HTML1及IDR_HTML2的值。
Ø 打开Favorites.rgs文件并将以下内容添加到末尾,把IDR_HTML1及IDR_HTML2替换为刚才记下的值。
HKCU
{
Software
{
Microsoft
{
'Internet Explorer'
{
MenuExt
{
ForceRemove '添加到收藏夹' =
s 'res://%MODULE%/IDR_HTML1'
{
val Contexts = d '1'
}
ForceRemove '显示收藏夹' =
s 'res://%MODULE%/IDR_HTML2'
{
val Contexts = d '4'
}
}
}
}
}
}
删除前面所有手工创建的注册表项,再次打开IE确认上下文菜单不会再显示。
现在,可以生成项目了,运行IE之后,应该会看到上下文菜单项了,这样就把所有的东西都包含在DLL中,往后只需注册这个DLL就可以了,如:RegSvr32 C:/Temp/Favorites.dll。
以后再登录论坛时,只需在浏览器窗口中右键单击,就可以看到一个弹出菜单,选择“添加到收藏夹”可把当前网址添加到收藏夹中;同样,在回复帖子时,在编辑窗口中右键单击,在“显示收藏夹”菜单下就有以前存过的项目,选择其一之后,URL及相关说明就会粘贴到编辑窗口的当前光标处,是不是又快又方便啊。