65.9K
CodeProject 正在变化。 阅读更多。
Home

自动化活动窗口资源管理器或 Internet Explorer 窗口

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (17投票s)

2005年10月20日

Ms-PL

7分钟阅读

viewsIcon

1231276

downloadIcon

4728

一篇关于查找活动 IE 或 Explorer 窗口或创建并控制它们的文章。

我从事自动化 Shell 窗口(主要是 Internet Explorer 窗口)已经很久了。有时 `WebBrowser` 控件或 MFC 类 `CHTMLView` 可以满足我的需求,但很多时候我需要挠头,从头开始嵌入 `WebBrowser` 控件,然后尽我所能地模拟 IE 的行为,例如实现 IDocHostUIHandler 以在 `WebBrowser` 控件中启用 自动完成。一个自然的替代方案是,为什么不直接自动化一个 Internet Explorer 窗口呢?

一个新的 Internet Explorer 窗口

完成此操作的最简单方法是调用 `ShellExecute` (Ex),正如 Paul DiLascia 在他的 C++ Q&A 文章 "浏览器检测重访、工具栏信息、COM 和 MFC 中的 IUnknown" 中演示的那样。

/// As I've shown in many programs...
ShellExecute(0, _T("open"), pszMyHTMLFile, 
                           0, 0, SW_SHOWNORMAL);

但是,我无法控制新窗口,而且在程序关闭后,用户会留下一个 IE 窗口。为了清理我的“烂摊子”,我需要先弄清楚哪个窗口是我的,然后在创建新窗口后接管它。

另一种方法是创建和自动化一个 InternetExplorer 对象,并在必要时关闭它。在 Microsoft 知识库 中有一篇题为 "如何自动化 Internet Explorer 以 POST 表单数据" 的文章,它基本上描述了我想要的内容,除了最后的清理。嗯,一个简单的调用 IWebBrowser2::Quit 就可以了。

// create a new IE instance and show it 
//CComQIPtr<IWebBrowser2> m_pWebBrowser2;
m_pWebBrowser2.CoCreateInstance(CLSID_InternetExplorer);
HRESULT hr;
hr = m_pWebBrowser2->put_StatusBar(VARIANT_TRUE);
hr = m_pWebBrowser2->put_ToolBar(VARIANT_TRUE);
hr = m_pWebBrowser2->put_MenuBar(VARIANT_TRUE);
hr = m_pWebBrowser2->put_Visible(VARIANT_TRUE);

if(!::PathIsURL(m_strFileToFind))
    m_strFileToFind=_T("http://blog.joycode.com/jiangsheng");
COleVariant vaURL( ( LPCTSTR) m_strFileToFind);
m_pWebBrowser2->Navigate2(
    &vaURL, COleVariant( (long) 0, VT_I4),
    COleVariant((LPCTSTR)NULL, VT_BSTR),
    COleSafeArray(),
    COleVariant((LPCTSTR)NULL, VT_BSTR)
);
void CAutomationDlg::OnDestroy()
{
    //close the IE window created by this 
    //program before exit
    if(m_pWebBrowser2)
    {
        if(m_bOwnIE)
        {
            m_pWebBrowser2->Quit();
            m_bOwnIE=FALSE;
        }
        UnadvisesinkIE();
        m_pWebBrowser2=(LPUNKNOWN)NULL;
    }
    CDialog::OnDestroy();
}

还有一个问题。如果用户在我能够在 `WM_TIMER` 处理函数中自动化窗口之前几秒钟关闭了新的 IE 窗口怎么办?现在暴露 `IWebBrowser2` 的 IE 对象不存在了。幸运的是,由于微软的努力,程序不会崩溃,但如果我知道它何时关闭,我就可以避免意外的结果,那就更好了。

处理 Internet Explorer 事件

Internet Explorer 对象在终止时会触发 DWebBrowserEvents2::OnQuit 事件。这是释放 `IWebBrowser2` 接口指针的理想时间。由于对象即将消亡,我将停止监视它的事件。

if(m_pWebBrowser2) 
{
    UnadvisesinkIE();
    m_pWebBrowser2=(LPUNKNOWN)NULL;
}

连接到当前的 Internet Explorer 窗口

做无用之事不符合我的性格,但让事情变得完美符合我的性格。虽然我不关心应该连接到哪个窗口,因为 Microsoft 知识库中有一篇题为 "如何连接到正在运行的 Internet Explorer 实例" 的文章,但我认为像 "如何连接到当前的 Internet Explorer 实例" 这样的文章会更有用。

那么,当前的 Internet Explorer 实例是什么?嗯,它是最后一个活动的 IE 窗口。由于 Microsoft Windows 会将活动窗口置于 Z 顺序的顶部,因此它将在所有 IE 窗口中保持在 Z 顺序的顶部。因此,我需要做的就是找出哪个 IE 窗口具有最高的 Z 顺序值。所以,我首先需要弄清楚哪个窗口是 IE 窗口。在对 Spy++ 进行一些调查后,我假设 IE 窗口的窗口类名是 "IEFrame",我编写了一个函数来获取 Shell 窗口的窗口类名。

//shell windows object will list both IE and Explorer windows
//use their window class names to identify them.   
CString CAutomationDlg::GetWindowClassName(IWebBrowser2* pwb)    
{    
    TCHAR szClassName[_MAX_PATH];    
    ZeroMemory( szClassName, _MAX_PATH * sizeof( TCHAR));    
    HWND hwnd=NULL;    
    if (pwb)    
    {    
        LONG_PTR lwnd=NULL;    
        pwb->get_HWND(&lwnd);    
        hwnd=reinterpret_cast<HWND>(lwnd);    
        ::GetClassName( hwnd, szClassName, _MAX_PATH);    
    }    
    return szClassName;    
}

然后,这个问题的其余部分很简单:沿着 Z 轴枚举顶级窗口,找到第一个窗口类名为 "IEFrame" 并且也位于 Shell 窗口列表中的实例。之后,我做了一些棘手的事情来处理 IE DHTML 文档对象模型(或 DOM,它在 IE 窗口触发最后一个 DocumentComplete 事件后可用),以确认窗口已成功连接。

void CAutomationDlg::DocumentComplete(IDispatch *pDisp, 
                                             VARIANT *URL)
{
    //HTML DOM is available AFTER the 
    //DocumentComplete event is fired.   
    //For more information, please visit KB article
    //"How To Determine When a Page Is 
    //Done Loading in WebBrowser Control"
    //http://support.microsoft.com/kb/q180366/
    CComQIPtr<IUnknown,&IID_IUnknown> pWBUK(m_pWebBrowser2);
    CComQIPtr<IUnknown,&IID_IUnknown> pSenderUK( pDisp);
    USES_CONVERSION;
    TRACE( _T( "Page downloading complete:\r\n"));
    CComBSTR bstrName;
    m_pWebBrowser2->get_LocationName(&bstrName);
    CComBSTR bstrURL;
    m_pWebBrowser2->get_LocationURL(&bstrURL);
    TRACE( _T( "Name:[ %s ]\r\nURL: [ %s ]\r\n"),
    OLE2T(bstrName),
    OLE2T(bstrURL));
    if (pWBUK== pSenderUK)
    {
        CComQIPtr<IDispatch> pHTMLDocDisp;
        m_pWebBrowser2->get_Document(&pHTMLDocDisp);
        CComQIPtr<IHTMLDocument2> pHTMLDoc(pHTMLDocDisp);
        CComQIPtr<IHTMLElementCollection> ecAll;
        CComPtr<IDispatch> pTagLineDisp;
        if(pHTMLDoc)
        {
            CComBSTR bstrNewTitle(_T("Sheng Jiang's Automation Test"));
            pHTMLDoc->put_title(bstrNewTitle);
            pHTMLDoc->get_all(&ecAll);
        }
        if(ecAll)
        {
            ecAll->item(COleVariant(_T("tagline")),
                            COleVariant((long)0),&pTagLineDisp);
        }
        CComQIPtr<IHTMLElement> eTagLine(pTagLineDisp);
        if(eTagLine)
        {
          eTagLine->put_innerText(
            CComBSTR(_T(
              "Command what is yours, conquer what is not. --Kane")));
        }
    }
}

现在导航发生在与 IE 相同的窗口中。

附带产品:连接到当前的 Windows Explorer 窗口

在检查 `ShellWindows` 对象的 Shell 窗口列表时,我获得了一个附带产品:似乎 Windows Explorer 窗口也具有通用的窗口类名。因此,相同的机制也适用于 Windows Explorer 窗口,只需将窗口类名从 "IEFrame" 更改为 "ExploreWClass" 即可。由于没有 DHTML DOM 可以操作,我指示 Windows Explorer 窗口浏览现有路径,以表明我已接管此窗口。

//show the folder bar
COleVariant clsIDFolderBar(
    _T("{EFA24E64-B078-11d0-89E4-00C04FC9E26E}"));
COleVariant FolderBarShow(VARIANT_TRUE,VT_BOOL);
COleVariant dummy;    
if(m_pWebBrowser2)    
    m_pWebBrowser2->ShowBrowserBar(
         &clsIDFolderBar,&FolderBarShow,&dummy);    
//browse to a given folder    
CComQIPtr<IServiceProvider> psp(m_pWebBrowser2);    
CComPtr<IShellBrowser> psb;     
if(psp)    
    psp->QueryService(SID_STopLevelBrowser,
                 IID_IShellBrowser,(LPVOID*)&psb);    
if(psb)    
{    
    USES_CONVERSION;    
    LPITEMIDLIST pidl=NULL;    
    SFGAOF sfgao;    
    SHParseDisplayName (T2OLE(m_strFileToFind),
                            NULL,&pidl,0, &sfgao);    
    if(pidl==NULL)    
        ::SHGetSpecialFolderLocation(m_hWnd,
                              CSIDL_DRIVES,&pidl);    
    m_pidlToNavigate=NULL;    
    if(pidl)    
    {    
        //if the start address is a folder, then browse it.   
        //otherwise browse to its parent folder, 
        //and select it in the folder view.   
        LPCITEMIDLIST pidlChild=NULL;    
        CComPtr<IShellFolder> psf;    
        HRESULT hr = SHBindToParent(pidl, 
                       IID_IShellFolder, 
                       (LPVOID*)&psf, &pidlChild);    
        if (SUCCEEDED(hr)){    
            SFGAOF rgfInOut=SFGAO_FOLDER;    
            hr=psf->GetAttributesOf(1,&pidlChild,&rgfInOut);    
            if (SUCCEEDED(hr)){    
                m_pidlToNavigate=ILClone(pidl);    
                if(rgfInOut&SFGAO_FOLDER){//this is a folder    
                    psb->BrowseObject(pidl,SBSP_SAMEBROWSER);     
                }    
                else    
                {    
                    //this is a file, browse to the parent folder    
                    LPITEMIDLIST pidlParent=ILClone(pidl);    
                    ::ILRemoveLastID(pidlParent);    
                    psb->BrowseObject( pidlParent, SBSP_SAMEBROWSER);    
                    ILFree(pidlParent);    
                }    
            }    
        }    
        //clean up    
        ILFree(pidl);    
    }    
}:

这段代码有点啰嗦,因为我想对文件和文件夹采取不同的操作。如果您调用 IShellBrowser::BrowseObject 并将文件的 `pidl` 传递给该方法,那么 Windows Explorer 会询问您是否要打开该文件,这与在 Windows Explorer 窗口的地址栏中键入文件路径然后按 Enter 键完全相同。我想模拟 "Explorer.exe /select" 的行为,即在文件夹视图中选择文件,因此我在 DocumentComplete 事件处理程序中放置了一些代码。

if(m_pidlToNavigate)
{
    //If the start address is a file, browse to the parent folder
    //and then select it
    CComQIPtr<IServiceProvider> psp(m_pWebBrowser2);
    CComPtr<IShellBrowser> psb;
    CComPtr<IShellView> psv;
    if(psp)
        psp->QueryService(SID_STopLevelBrowser,
                     IID_IShellBrowser,(LPVOID*)&psb);
    if(psb)
        psb->QueryActiveShellView(&psv);
    if(psv)
    {
        LPCITEMIDLIST pidlChild=NULL;
        CComPtr<IShellFolder> psf;
        SFGAOF rgfInOut=SHCIDS_ALLFIELDS;
        HRESULT hr = SHBindToParent(m_pidlToNavigate, 
                         IID_IShellFolder, 
                         (LPVOID*)&psf, &pidlChild);
        if (SUCCEEDED(hr)){
            hr=psf->GetAttributesOf(1,&pidlChild,&rgfInOut);
            if (SUCCEEDED(hr)){
                if((rgfInOut&SFGAO_FOLDER)==0){
                    //a file, select it
                    hr=psv->SelectItem(ILFindLastID(m_pidlToNavigate)
                        ,SVSI_SELECT|SVSI_ENSUREVISIBLE|SVSI_FOCUSED|
                        SVSI_POSITIONITEM);
                }
            }
        }
    }
    //clean up
    ILFree(m_pidlToNavigate);
    m_pidlToNavigate=NULL;
}

一个新的 Windows Explorer 窗口

让我们将新的成就带回到旧问题。由于我几乎可以通过与连接当前 Windows Explorer 窗口相同的方式连接到当前 Windows Explorer 窗口,那么我是否可以像创建和自动化新 Internet Explorer 窗口那样,创建和自动化一个新的 Windows Explorer 窗口?令我惊讶的是,答案是否定的。没有 Windows Explorer 的类 ID 来创建这样的 COM 对象。虽然我仍然可以创建一个 IE 窗口,导航到文件夹,并显示文件夹浏览器栏,使其看起来像一个 Windows Explorer 窗口,但我无法更改窗口类名 "IEFrame",因此很难将其与显示 HTML 页面和 Active Documents 的其他 IE 窗口区分开来。

好的,如果我无法通过 COM 方式创建它,我仍然可以尝试传统方式。我可以创建一个 `explorer.exe` 进程并查找其主窗口,正如 Paul DiLascia 在他的文章 "获取主窗口,获取 EXE 名称" 中指出的那样,然后发送未公开的消息 WM_GETIShellBrowser 来获取新窗口的 IShellBrowser 接口。

//start the new process
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory( &si, sizeof(si) );
si.cb = sizeof(si);
ZeroMemory( &pi, sizeof(pi) );
// Start the child process. 
if( !CreateProcess( NULL, // No module name (use command line). 
    _T("explorer.exe"), // Command line. 
    NULL,          // Process handle not inheritable. 
    NULL,          // Thread handle not inheritable. 
    FALSE,         // Set handle inheritance to FALSE. 
    0,             // No creation flags. 
    NULL,          // Use parent's environment block. 
    NULL,          // Use parent's starting directory. 
    &si,           // Pointer to STARTUPINFO structure.
    &pi )          // Pointer to PROCESS_INFORMATION structure.
)
//wait a graceful time 
//so the window is created and is ready to answer messages.
::WaitForInputIdle(pi.hProcess,1000);
//m_hExplorerProcess=(DWORD)pi.hProcess;
EnumWindows(EnumWindowsProc,(LPARAM)this);
BOOL CALLBACK CAutomationDlg::EnumWindowsProc(
                                 HWND hwnd,LPARAM lParam)
{
    CAutomationDlg* pdlg=(CAutomationDlg*)lParam;
    DWORD pidwin;
    GetWindowThreadProcessId(hwnd, &pidwin);
    if (pidwin==pdlg->m_hExplorerProcess)
    {
        IShellBrowser* psb=
          (IShellBrowser*)::SendMessage(hwnd,WM_USER+7,0,0);
        CComQIPtr<IWebBrowser2> pwb(psb);
        return FALSE;
    }
    return TRUE;
}

哎呀,这在我的电脑上也没有捕捉到窗口。怎么回事?在我的 Windows Explorer 的文件夹选项页面中,选择了 "在同一个窗口中打开每个文件夹" 选项,因此新的 Windows Explorer 窗口是在现有的 Windows Explorer 进程中创建的。看起来像一条死胡同。

等等,我还有一个对象可用,就是 `ShellWindows` 对象。它可以给我一个 Shell 窗口列表,包括每个 Windows Explorer 窗口以及相应的 `IWebBrowser2` 接口,这是通往其 `IShellBrowser` 接口的门户。现在我需要获取两个 Shell 窗口列表,一个在创建 `explorer.exe` 进程之前,一个在创建之后;然后我必须比较它们以找出新的 Shell 窗口。

m_pShellWindows.CoCreateInstance(CLSID_ShellWindows);
if(m_pShellWindows)
{
    //get the list of running IE windows
    //using the ShellWindows collection
    //For more information, please visit 
    //http://support.microsoft.com/kb/176792
    long lCount=0;
    m_pShellWindows->get_Count(&lCount);
    for(long i=0;i<lCount;i++)
    {
        CComPtr<IDispatch> pdispShellWindow;
        m_pShellWindows->Item(COleVariant(i),
                                 &pdispShellWindow);
        if(pdispShellWindow)
        {
            m_listShellWindows.AddTail(
                new CComQIPtrIDispatch(pdispShellWindow));
        }
    }
}
//enumerate through the new shell window list
long lCount=0;
m_pShellWindows->get_Count(&lCount);
for(long i=0;i<lCount;i++)
{
    //search the new window
    //using the ShellWindows collection
    //For more information, please visit 
    //http://support.microsoft.com/kb/176792
    BOOL bFound=FALSE;
    CComPtr<IDispatch> pdispShellWindow;
    m_pShellWindows->Item(COleVariant(i),
                           &pdispShellWindow);
    //search it in the old shell window list
    POSITION pos=m_listShellWindows.GetHeadPosition();
    while(pos)
    {
        CComQIPtrIDispatch* pDispatch=
                      m_listShellWindows.GetNext(pos);
        if(pDispatch&&pdispShellWindow.p==pDispatch->p)
        {
            bFound=TRUE;break;    
        }
    }
    if(!bFound)//new window found
    {
        //attach to it
        m_pWebBrowser2=pdispShellWindow;
        m_bOwnIE=TRUE;
        //sink for the Quit and DocumentComplete events
        AdviseSinkIE();
        NavigateToSamplePage(FALSE);
    }
}

等等。“在创建 explorer.exe 进程后立即”是什么意思?调用 `CreateProcess` 函数一秒后?或者两秒后?事实上,`ShellWindows` 对象在每个 Shell 窗口创建后会触发一个 `WindowRegistered` 事件,我将比较放在它的事件处理程序中。

//sink DShellWindowsEvents events
LPUNKNOWN pUnkSink = GetIDispatch(FALSE);
m_pShellWindows.CoCreateInstance(CLSID_ShellWindows);
AfxConnectionAdvise((LPUNKNOWN)m_pShellWindows, 
              DIID_DShellWindowsEvents,pUnkSink,
              FALSE,&m_dwCookieShellWindows);
void CAutomationDlg::WindowRegistered(long lCookie) 
{
    //ok, a new shell window is created
    if(m_pShellWindows)
    {
        //enumerate through the new shell window list
        long lCount=0;
        m_pShellWindows->get_Count(&lCount);
        for(long i=0;i<lCount;i++)
        {
            //search the new window
            //using the ShellWindows collection
            //For more information, please visit 
            //http://support.microsoft.com/kb/176792
            BOOL bFound=FALSE;
            CComPtr<IDispatch> pdispShellWindow;
            m_pShellWindows->Item(COleVariant(i),
                                       &pdispShellWindow);
            //search it in the old shell window list
            POSITION pos=m_listShellWindows.GetHeadPosition();
            while(pos)
            {
                CComQIPtrIDispatch* pDispatch=
                              m_listShellWindows.GetNext(pos);
                if(pDispatch&&pdispShellWindow.p==pDispatch->p)
                {
                    bFound=TRUE;break;    
                }
            }
            if(!bFound)//new window 
            {
                //attach to it
                m_pWebBrowser2=pdispShellWindow;
                m_bOwnIE=TRUE;
                //sink for the Quit and DocumentComplete events
                AdviseSinkIE();
                NavigateToSamplePage(FALSE);
            }
        }
        //clean up
        if(m_dwCookieShellWindows!= 0)
        {
            LPUNKNOWN pUnkSink = GetIDispatch(FALSE);
            AfxConnectionUnadvise((LPUNKNOWN)m_pShellWindows, 
                            DIID_DShellWindowsEvents, pUnkSink, 
                            FALSE, m_dwCookieShellWindows);
            m_dwCookieShellWindows= 0;
        }
        POSITION pos=m_listShellWindows.GetHeadPosition();
        while(pos)
        {
            CComQIPtrIDispatch* pDispatch=
                          m_listShellWindows.GetNext(pos);
            delete    pDispatch;
        }
        m_listShellWindows.RemoveAll();
        m_pShellWindows=(LPUNKNOWN)NULL;
    }
}

为什么不使用浏览器帮助对象 (Browser Helper Objects)?

由于新窗口不在我的进程中,COM 调用的进程间封送开销很大。如果您的自动化操作包含过多的 COM 调用,您可能需要将代码放在进程内,例如编写浏览器帮助对象 (BHO)。但是,BHO 将被所有 Windows Explorer 和 Internet Explorer 实例加载,而我不想为了清理我的“烂摊子”而拖慢整个系统。有些人实际上已经使用这项技术来连接到 Internet Explorer 的当前实例

已知问题

如果默认的 `explorer.exe` 进程被终止或未启动,`ShellWindows` 对象仍然可用。在这种情况下,BHO 可以作为替代方案。

结论

这里有一大块令人挠头的代码。此外,还需要一些时间来适应混合的 COM 和 Windows API 函数调用。希望您会觉得这篇文章有用,并且不会被我的代码所迷惑。自动化 Internet Explorer 和 Windows Explorer 窗口可以为您节省大量时间,因为您可以避免模拟系统的默认行为,并且它为最终用户提供了熟悉的外观。

参考

VC6 用户编译说明

源代码中使用的新 Shell API 需要新版本的 Microsoft Platform SDK。它可以在此处下载。其中一些 Shell API 可以替换为此处列出的一些函数。

历史

  • 2005 年 10 月 20 日 - 初始发布。
© . All rights reserved.