由于工作原因,我需要一个浏览器框架,以便可以让我使用脚本进行编程,一来可以简化系统,而来方便调试
由于系统中需要使用多线程,所以要求这个浏览器框架也必须是多线程的。开始的时候使用MFC的MDI模式的窗口,发现MDI模式的窗口虽然可以很容易的作出多页面的浏览器框架,但是一个重要的原因是创建的浏览器窗口不是多线程的,这样将导致其中如果其中一个窗口的js运行被阻塞,那么其他窗口的JS运行也会被阻塞。
我后来分析了3种解决方案
////////////////////////////////////////////////////////////////
方案一:
我们可以确认一个EXE文件是一定是在一个独立的进程内的,如果EXE执行多次,必然这几个exe程序是相对独立的,于是我想了一个相对取巧的方法
首先建立一个 NewExe ,这个程序中有一个WebBrowser控件,然后建立 MainExe ,这个程序里面也有一个WebBrowser控件,当需要打开新网页的时候,用CreateProcess 创建一个 NewExe 的进程,然后将需要打开新网页的句柄传递给这个新进程里面的WebBrowser控件即可,关闭主程序时通过枚举打开的窗口的进程ID,依次关闭打开的新进程。
关键为以下5个函数
函数1,创建新进程
void CMainBrowserDlg::CreateNewBrowser()
{
STARTUPINFO si;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(STARTUPINFO);
si.dwFlags=STARTF_USESHOWWINDOW;
if ( !showNewWindows ) {
si.wShowWindow=SW_SHOWMINIMIZED;
si.wShowWindow = SW_HIDE;
}
CString path;
GetSystemDir(path);
NewWindowsState = 1 ;
openWindowCount++;
path = path + "\\NewBrowser.exe";
if(CreateProcess( path ,
"",
NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi[openWindowCount-1]))
{
NewWindowsState = 2;
// 等待这个进程结束
//WaitForSingleObject(pi.hProcess, INFINITE);
// CloseHandle(pi.hThread);
// CloseHandle(pi.hProcess);
//CWebBrowser2 *pNewDlg;
//pNewDlg = (CWebBrowser2 *)::GetDlgItem((HWND)pi.hThread,102);
//pNewDlg->SetWindowText("111");
//pNewDlg->Navigate("about:blank",NULL,NULL,NULL,NULL);
}
else
{
NewWindowsState = 3;
openWindowCount--;
AfxMessageBox( "无法启动进程!" );
}
}
函数二,获取程序中的WebBrowser控件的接口:
BOOL CALLBACK EnumThreadWndProc(HWND hwnd, LPARAM lParam)
{
if( showNewWindows )
{
// ShowWindow(hwnd,SW_SHOW);
}
else
{
ShowWindow(hwnd,SW_HIDE);
}
//SendMessage(hwnd,WM_CLOSE,NULL,NULL);
//return TRUE;
HWND H1,H2,H3,H4;
H1=H2=H3=H4=NULL;
// "Shell Embedding"
//pWeb = FindWindowEx(hwnd,(HWND)0,"Shell Embedding",NULL);
H1 = hwnd;
if (H1)
{
H2=::FindWindowEx(H1,NULL,"Shell Embedding",NULL);
if (H2)
{
H3=::FindWindowEx(H2,NULL,"Shell DocObject View",NULL);
if (H3)
{
//AfxMessageBox("ok");
H4=::FindWindowEx(H3,NULL,"Internet Explorer_Server",NULL);
if (H4) {
//AfxMessageBox("Server");
pNewDlg[ openWindowCount-1 ] = GetIEFromWnd(H4);
// pNewDlg = ( CWebBrowser2 * )pWeb;
// GetDlgItem(0,pWeb);
//pNewDlg->put_Width( 300);
//pNewDlg->Navigate(abc.AllocSysString(),NULL,NULL,NULL,NULL);
//pNewDlg->put_Visible( VARIANT_FALSE );
//ShowWindow(pWeb,SW_HIDE);
//pNewDlg->ShowWindow( SW_HIDE);
}else
{
//AfxMessageBox("not search");
}
}
else
{
//AfxMessageBox("3");
}
}
else
{
//AfxMessageBox("2");
}
}
else
{
//AfxMessageBox("1");
}
//pNewDlg = (CWebBrowser2 *)GetDlgItem(hwnd,1000-102);
//SetWindowPos(hwnd,HWND_TOPMOST,0,0,0,0,SWP_NOMOVE|SWP_NOSIZE|SWP_SHOWWINDOW);
return TRUE;
}
函数三,将新窗口的句柄传递给自己开的程序的新控件
void CMainBrowserDlg::OnNewWindow2Explorermain(LPDISPATCH FAR* ppDisp, BOOL FAR* Cancel)
{
// TODO: Add your control notification handler code here
//* ppDisp =
CreateNewBrowser();
int count=0;
while( NewWindowsState == 1 && count<50 )
{
// 等新程序的打开
count ++ ;
Sleep(100);
}
if ( NewWindowsState == 3 )
{
*Cancel = true;
}
if ( openWindowCount>0 ) {
if ( pi[openWindowCount-1].dwThreadId !=0 ) {
// 获取 webbrowser 句柄
count = 0;
pNewDlg[openWindowCount-1] = NULL;
while( pNewDlg[openWindowCount-1] == NULL && count<50 )
{
count ++ ;
EnumThreadWindows(pi[openWindowCount-1].dwThreadId,EnumThreadWndProc,0);
Sleep(100);
}
if( pNewDlg[openWindowCount-1] != NULL )
{
READYSTATE state;
pNewDlg[openWindowCount-1]->get_ReadyState(&state);
count =0;
while ( state != READYSTATE_COMPLETE && count<50) {
count++;
Sleep(100);
}
//CString abc;
//abc.Format("count = %d",count);
//AfxMessageBox(abc);
pNewDlg[openWindowCount-1]->get_Application( ppDisp );
}else
{
*Cancel = true;
AfxMessageBox("无法找到窗口对象");
}
}
}
}
函数4,5,关闭主程序时需要关闭自己通过CreateProcess 打开的进程
BOOL CALLBACK EnumThreadWndClose(HWND hwnd, LPARAM lParam)
{
SendMessage(hwnd,WM_CLOSE,NULL,NULL);
return TRUE;
}
void CMainBrowserDlg::CloseAllWindow()
{
for( int i=0;i< openWindowCount ;i++)
{
EnumThreadWindows(pi[i].dwThreadId,EnumThreadWndClose,0);
}
openWindowCount = 0;
memset(pi,0,sizeof(pi));
}
此方法采用的是非正常手法,类似于木马程序,不算正规,而且效率不高,尽量不采用此方法
////////////////////////////////////////////////////////////////
方案二:
通过学习微软的MTMDI程序,创建自己的界面多线程程序,但是不直接将Cview改为Chtmlview,通过在View里面创建窗体,窗体上放置WebBrowser控件来实现,此方法需要使用网上别人封装好的CHtmlCtrl类,主要方法
1.建立一个窗体,在窗体中插入一个 static 控件,用CHtmlCtrl类转换此 static控件,将其改为WebBrowser控件,整个窗体我这里创建为 CDialogIE m_dialogBar 这个对象
2.View创建的时候,顺便创建窗体
if(!m_dialogBar.Create( IDD_DIALOG_IE,this))
{
return -1;
}
m_dialogBar.SetWindowPos(NULL,0,0,100,200,SWP_NOSIZE); // 定义一下大小即可
此方法有点怪胎,个人不喜欢用此方式,而且此方式效率可能较低,代码比较混乱,不想采用此方法
方案三:
此方法是在方法二的基础上使用 继承 Chtmlview 类的方法来实现,基础也是微软的界面多线程程序MTMDI,在此基础上将窗口类CChildView,改为继承CHtmlview而不是CView类。
主要关键难点:
关键点1:
首先由于CView正常的PostNcDestroy实现是使用delete this销毁View。对于Views来说,这是正常的处理方式,因为View是直接在堆里建立的。但是,控件一般是作为另一个窗口对象的成员存在的。这时你就不要delete了,它会被父对象delete掉。所以必须 CChildView 也重载了PostNcDestroy
void CHtmlCtrl::PostNcDestroy()
{
// do nothing
}不过我的方法是创建 CChildView对象的时候不采用系统默认的 CChildView m_view 方式创建一个,改为CChildView *m_wndChildView; 创建时候 m_wndChildView = new CChildView;
BOOL bReturn = m_wndChildView->Create(_T("ChildViewMTChildWnd"),
WS_CHILD | WS_VISIBLE, rect, pParent);
这样
void CHtmlCtrl::PostNcDestroy()
{
CHtmlCtrl::PostNcDestroy();
}就不必重载,或者用默认的函数,让PostNcDestroy中delete this
关键点2:
程序必须是界面多线程,关于界面多线程可以参考网上很多文章和微软的MTMDI例子,其关键的地方就是继承 CWinThread 类,然后 这个类的CreateThread() 来调用,此线程被执行的第一个函数就是 InitInstance 函数,,可以在其中做自己需要的操作我主要的操作如下
m_wndChildView = new CChildView;
BOOL bReturn = m_wndChildView->Create(_T("ChildViewMTChildWnd"),
WS_CHILD | WS_VISIBLE, rect, pParent);
// It is important to set CWinThread::m_pMainWnd to the user interface
// window. This is required so that when the m_pMainWnd is destroyed,
// the CWinThread is also automatically destroyed. For insight into
// how the CWinThread is automatically destroyed when the m_pMainWnd
// window is destroyed, see the implementation of CWnd::OnNcDestroy
// in wincore.cpp of the MFC sources.
if (bReturn)
m_pMainWnd = m_wndChildView;
在线程中创建视图,创建用户界面线程
关键点3:
在View视图中不可以直接调用 MainFrame的创建 新视图的函数,具体原因不清楚,我是通过发送消息给主框架,然后由主框架创建新View来实现
关键点4:
在MTMDI的程序中,如果想一开始就建立一个窗口,请使用PostMessage()传递消息来调用创建窗口的函数,因为在mtmdi窗体中需要自己手工创建第一个窗口,而系统本身会有一个消息队列,如果不按照系统的队列进行操作,会存在诸如菜单栏失效的bug。
关键点5:
在OnNewWindow2函数中,如果想调用另外一个视图的WebBrowser接口,必须使用 列集和散集来传递参数,至于原因可以学习相关COM的知识,具体做法,在创建HTMLView窗口时候,使用CoMarshalInterThreadInterfaceInStream 这个API函数来列集接口。
CHtmlView::Create(lpszIEWindowClass, szTitle, style, rect, pParent,IDC_CHILDVIEW_WND);
pDispatch = (LPDISPATCH)this->GetApplication();
hr = CoMarshalInterThreadInterfaceInStream(
IID_IWebBrowser2, // interface ID to marshal
pDispatch, // ptr to interface to marshal
&pStream) ; // output variable
if (hr != S_OK)
{
AfxMessageBox (_T("Couldn't marshal ptr to thread") );
}
在 OnNewWindow2 中使用 CoGetInterfaceAndReleaseStream 函数来 散集接口
if ( pView != NULL )
{
pView->flagID = this->flagID;
hr = CoGetInterfaceAndReleaseStream(
pStream , // stream containing marshaling info
IID_IWebBrowser2, // interface desired
(void **) &pDispatch) ; // output variable
if (hr != S_OK)
{
AfxMessageBox(_T("Couldn't get interface")) ;
}
*ppDisp = pDispatch; // IID_IWebBrowser2 IID_IDispatch IID_IServiceProvider
}else
{
* Cancel = True;
}
关键点6:
如果想对于WebBrowser 的内容进行控制,在VC7 以上版本中,请重载 CreateControlSite 函数
CCustomControlSite类继承于 COleControlSite ,网上可以找到相关解释CreateControlSite(COleControlContainer * pContainer, COleControlSite ** ppSite, UINT /*nID*/, REFCLSID /*clsid*/)
{
*ppSite = new CCustomControlSite(pContainer,this);;// 创建自己的控制站点实例
(*ppSite)->m_pCtrlCont = pContainer;
return (*ppSite) ? TRUE : FALSE;
}
如果在VC6的版本中,我只知道如果是单线程程序,可以使用 在APP函数中修改 AfxEnableControlContainer() 为如下
CCustomOccManager *theManager = new CCustomOccManager;
AfxEnableControlContainer(theManager); 进行容器的重新控制
多线程的还不知道如何解决
另外需要注意的是 CCustomControlSite 类其实也控制了拖放模式,一开始我发现VC8编译的CHtmlView的程序,开始的时候是支持 拖放的,但是一旦载入一个页面后,就不支持拖放了,即使你设置了SetRegisterAsDropTarget(TRUE); 也是无效,后来发现原来是 COleControlSite 的默认控制在VC7 以上版本中默认值不一样了,只需要重载 COleControlSite 类就可以实现拖放