Internet Explorer 编程简述(一)WebBrowser还是WebBrowser_V1收藏

来源:互联网 发布:手机美工软件 编辑:程序博客网 时间:2024/06/04 21:16
 

Internet Explorer 编程简述(一)WebBrowser还是WebBrowser_V1收藏

新一篇: Internet Explorer 编程简述(二)在IE中编辑OLE嵌入文档 | 旧一篇: Internet Explorer 编程简述(序)

关键字:WebBrowser, WebBrowser_V1,NewWindow, NewWindow2, NewWindow3, INewWindowManager 

你的机器上总是存在着“两”个WebBrowser,一个叫WebBrowser,另一个叫WebBrowser_V1,其CLASSID如下:

  CLASS_WebBrowser: TGUID = '{8856F961-340A-11D0-A96B-00C04FD705A2}';
  CLASS_WebBrowser_V1: TGUID = '{EAB22AC3-30C1-11CF-A7EB-0000C05BAE0B}';

它们分别对应的接口是IWebBrowser2和IWebBrowser。问题是我们该用哪一个呢?
按照微软的推荐,应该尽量使用前者,因为后者是为兼容Internet Explorer 3.x而保留的(尽管它能够响应来自Internet Explorer 3.x、4.x、5.x、6.x的事件),相应的IWebBrowser和IWebBrowserApp接口也应抛弃。

由于Internet Explorer 3.x年代久远,导致WebBrowser_V1提供的事件少得可怜,但值得一提的是它提供的两个事件OnNewWindow和OnFrameBeforeNavigate有着与OnBeforeNavigate几乎相同的参数:

OnBeforeNavigate(
  BSTR URL, 
  long Flags, 
  BSTR TargetFrameName, 
  VARIANT* PostData, 
  BSTR Headers, 
  BOOL FAR* Cancel)

OnNewWindow(
  BSTR URL, 
  long Flags, 
  BSTR TargetFrameName, 
  VARIANT* PostData, 
  BSTR Headers, 
  BOOL FAR* Processed)

OnFrameBeforeNavigate(
  BSTR URL,
  long Flags, 
  BSTR TargetFrameName, 
  VARIANT* PostData, 
  BSTR Headers, 
  BOOL FAR* Cancel)

所以使用WebBrowser_V1使得我们的浏览器在有新窗口打开时能够轻易捕捉到其URL及相关的数据,如果将Processed设置为TRUE,则可取消新窗口的弹出。同样,处理Frame也比在WebBrowser中来得容易。

但WebBrowser_V1的致命弱点是它不支持高级接口,如 IDocHostUIHandler,即便我们实现了IDocHostUIHandler接口,也不会被WebBrowser_V1调用。所以希望在自己的浏览器中实现XP的界面主题、扩展IE的DOM(Document Object Model)等高级控制的话,就肯定不能选择WebBrowser_V1了。

处理新窗口实在是很麻烦的一件事,不知道微软为什么在新版本的OnNewWindow2事件中去掉了URL这样的参数,而且OnNewWindow2事件不能完全捕捉到所有的新窗口打开。但如果安装了Windows XP SP2的话,好处又回来了。

Windows XP SP2对Internet Explorer 6作了升级,并且提供了一个新的事件OnNewWindow3,它在OnNewWindow2事件之前发生,也包含了让我们能够加以过滤处理的新窗口的URL等参数,再加上INewWindowManager接口,就是实现Windows XP SP2中过滤广告窗口功能的基础。

参考资料
MSDN:185538 HOWTO: Cause Navigation to Occur in Same WebBrowser Window

引用地址Internet Explorer 编程简述(一)WebBrowser还是WebBrowser_V1

 

 

 

 Internet Explorer 编程简述(二)在IE中编辑OLE嵌入文档收藏

新一篇: Internet Explorer 编程简述(三)“整理收藏夹”对话框 | 旧一篇: Internet Explorer 编程简述(一)WebBrowser还是WebBrowser_V1

关键字:OLE嵌入,In-Place Activating,IE, Office, Acrobat

 

 

除了打开Internet上的网页,Internet Explorer还能够浏览本地文件夹及文件。如果浏览的是PDF文档或Office文档,有时候你会发现当调用Navigate("xxx.doc") 的时候,Adobe Reader/Acrobat或Office等Document Servers会在IE中嵌入自己的一个实例以打开相应的文件,当然有时候也会在独立的Acrobat或Office窗口中打开文件。
在Adobe Reader/Acrobat的属性设置窗口中,我们可以找到“Display PDF in browser”的选项,如果勾上,则Navigate("xxx.pdf")将会以嵌入的方式在IE中浏览PDF文件,否则在独立的Adobe Reader/Acrobat窗口中浏览。但在Office的“选项”对话框中我们找不到这样的设置。

 

 


 

问题:如何在自己的浏览器中控制Office这类Ole Servers的打开方式?

答案:修改文件夹选项,或修改注册表。

方法1、如下所示,从控制面板中打开“文件夹”选项,在“文件类型”属性页上找到相应的文件后缀名,如“DOC”,点击“高级”按钮,在弹出的“编辑文件类型”对话框中有“在同一窗口中浏览”的选项,如果勾上,则以嵌入IE的方式打开文档,否则在独立窗口中打开。


Open In Same Window   Open In Same Window

 

方法2、直接修改注册表。

在“HKEY_LOCAL_MACHINE/SOFTWARE/Classes”键值下,保存了各种文件类型的注册信息,以Office文档为例,与文档相关键值如下。

 

文档类型                             键值

Microsoft Excel 7.0 worksheet        Excel.Sheet.5

Microsoft Excel 97 worksheet         Excel.Sheet.8

Microsoft Excel 2000 worksheet       Excel.Sheet.8

Microsoft Word 7.0 document          Word.Document.6

Microsoft Word 97 document           Word.Document.8

Microsoft Word 2000 document         Word.Document.8

Microsoft Project 98 project         MSProject.Project.8

Microsoft PowerPoint 2000 document   PowerPoint.Show.8

 

如果我们要修改Word文档的打开方式,,则在“HKEY_LOCAL_MACHINE/SOFTWARE/Classes/Word.Document.8”下新建一个名为“BrowserFlags”,类型为“REG_DWORD”的子键值,如果设置其值为“8”,则在独立的窗口中打开Word文档,否则在嵌入 IE的Word窗口中打开文档。

注:Microsoft Excel 7.0 worksheet稍有不同,应设置BrowserFlags的值为“9”方可在独立的窗口中打开文档。


Office Document Setting

 

参考资料

MSDN:259970In-Place Activating Document Servers in Internet Explorer

MSDN162059How to configure Internet Explorer to open Office documents in the appropriate Office program instead of in Internet Explorer

引用地址Internet Explorer 编程简述(二)在IE中编辑OLE嵌入文档

 

Internet Explorer 编程简述(三)“整理收藏夹”对话框收藏

新一篇: Internet Explorer 编程简述(四)“添加到收藏夹”对话框 | 旧一篇: Internet Explorer 编程简述(二)在IE中编辑OLE嵌入文档

关于Internet Explorer的收藏夹,比较常见的两个问题就是调用“整理收藏夹”对话框和“添加到收藏夹”对话框。调用的方法有多种,但其中还是有些值得讨论的地方。

 

关键字:添加到收藏夹,整理收藏夹,DoAddToFavDlg, DoOrganizeFavDlg

 

 

1、整理收藏夹

 

调用“整理收藏夹”对话框(如下),基本上来说都用的是同一个方法,即调用“shdocvw.dll”中的“DoOrganizeFavDlg”函数,把父窗口句柄和收藏夹路径作为参数传入即可。

 

 

2、代码

 

代码实例如下所示,值得注意的是对“shdocvw.dll”的处理,为避免重复调用,应该先检查其是否已经在内存中。

 

void CMyHtmlView::OnFavOrganizefav()
{
  typedef UINT (CALLBACK* LPFNORGFAV)(HWND, LPTSTR);

  bool bResult = false;

  HMODULE hMod = ::GetModuleHandle( _T("shdocvw.dll") );

  if (hMod == NULL)//如果"shdocvw.dll"尚未载入则载入之
  {
    hMod = ::LoadLibrary( _T("shdocvw.dll") );

    if (hMod == NULL)
    {
      MessageBox( _T("The dynamic link library ShDocVw.DLL cannot be found."),
        _T("Error"), MB_OK | MB_ICONSTOP );
      return;
    }
    LPFNORGFAV lpfnDoOrganizeFavDlg = (LPFNORGFAV)
      ::GetProcAddress( hMod, "DoOrganizeFavDlg" );

    if (lpfnDoOrganizeFavDlg == NULL)
    {
      MessageBox( _T("The entry point DoOrganizeFavDlg cannot be found/n")
        _T("in the dynamic link library ShDocVw.DLL."),
        _T("Error"), MB_OK | MB_ICONSTOP );
      return;
    }

    TCHAR szPath [ MAX_PATH ];
    HRESULT hr;

    hr = ::SHGetSpecialFolderPath( m_hWnd, szPath, CSIDL_FAVORITES, TRUE );
    if (FAILED(hr))
    {
      MessageBox( _T("The path of the Favorites folder cannot be found."),
        _T("Error"), MB_OK | MB_ICONSTOP );
      return;
    }

    bResult = (*lpfnDoOrganizeFavDlg) ( m_hWnd, szPath ) ? true : false;

    ::FreeLibrary( hMod );
  }
  else
//如果"shdocvw.dll"已经在调用者进程的地址空间中则直接使用。
  {
    LPFNORGFAV lpfnDoOrganizeFavDlg = (LPFNORGFAV)
      ::GetProcAddress( hMod, "DoOrganizeFavDlg" );

    if (lpfnDoOrganizeFavDlg == NULL)
    {
      MessageBox( _T("The entry point DoOrganizeFavDlg cannot be found/n")
        _T("in the dynamic link library ShDocVw.DLL."),
        _T("Error"), MB_OK | MB_ICONSTOP );
      return;
    }

    TCHAR szPath [ MAX_PATH ];
    HRESULT hr;

    hr = ::SHGetSpecialFolderPath( m_hWnd, szPath, CSIDL_FAVORITES, TRUE );
    if (FAILED(hr))
    {
      MessageBox( _T("The path of the Favorites folder cannot be found."),
        _T("Error"), MB_OK | MB_ICONSTOP );
      return;
    }

    bResult = (*lpfnDoOrganizeFavDlg) ( m_hWnd, szPath ) ? true : false;
  }

  return;

}


3、讨论

实际上,从“DoOrganizeFavDlg” 函数的原型声明我们可以看到,由于需要一个路径,所以“整理收藏夹”对话框其实不仅可以用来整理收藏夹,还可以整理磁盘上的目录。而且所谓的整理也不过是提供了一个对话框使用户用起来比较方便而已,和直接在资源管理器中整理没有实质性的差别。因此调用“整理收藏夹”对话框的方法从IE4.0开始就没有变过,除了对话框的布局有所改变。

 

typedef UINT (CALLBACK* LPFNORGFAV)(HWND, LPTSTR);

 

 

IE 4.0的“整理收藏夹”对话框

 

 

IE 4.0的“整理收藏夹”对话框(原先的设计)

 

 

“添加到收藏夹”就不同了,“DoAddToFavDlg”函数不再像“DoOrganizeFavDlg”函数一样对所有IE的版本都适用。

 

 

参考资料

MSDN: Adding Internet Explorer Favorites to Your Application

 

引用地址Internet Explorer 编程简述(三)“整理收藏夹”对话框

 

 

Internet Explorer 编程简述(四)“添加到收藏夹”对话框收藏

新一篇: Internet Explorer 编程简述(五)调用IE隐藏的命令 | 旧一篇: Internet Explorer 编程简述(三)“整理收藏夹”对话框

关键字:“添加到收藏夹”对话框, 模态窗口,IShellUIHelper,DoAddToFavDlg, DoOrganizeFavDlg

1、概述

调用“添加到收藏夹”对话框(如下)与调用“整理收藏夹”对话框有不同之处,前者所做的工作比后者要来得复杂。将链接添加到收藏夹除了将链接保存之外,还可能会有脱机访问的设置,从IE 4.0到IE 5.0,处理的方式也发生了一些变化。

 


2、IShellUIHelper接口

微软专门提供了一个接口IShellUIHelper来实现对Windows Shell API一些功能的访问,将链接添加到收藏夹也是其中之一,就是下面的AddFavorite函数。

HRESULT IShellUIHelper::AddFavorite(BSTR URL, VARIANT *Title);

实例代码如下:

void CMyHtmlView::OnAddToFavorites()
{
  IShellUIHelper* pShellUIHelper;
  HRESULT hr = CoCreateInstance(CLSID_ShellUIHelper, NULL,
    CLSCTX_INPROC_SERVER, IID_IShellUIHelper,(LPVOID*)&pShellUIHelper);

  if (SUCCEEDED(hr))
  {
    _variant_t vtTitle(GetTitle().AllocSysString());
    CString strURL = m_webBrowser.GetLocationURL();

    pShellUIHelper->AddFavorite(strURL.AllocSysString(), &vtTitle);
    pShellUIHelper->Release();
  }
}

我们注意到这里的“AddFavorite”函数并没有像 “DoOrganizeFavDlg”那样需要一个父窗口句柄。这也导致与在IE中打开不同,通过IShellUIHelper接口显示出来的“添加到收藏夹”对话框是“非模态”的,有一个独立于我们应用程序的任务栏按钮,这使我们的浏览器显得非常不专业(我是个追求完美的人,这也是我的浏览器迟迟不能发布的原因之一)。
于是我们很自然地想到“shdocvw.dll”中除了“DoOrganizeFavDlg”外,应该还有一个类似的函数,可以传入一个父窗口句柄用以显示模态窗口,也许就像这样:

typedef UINT (CALLBACK* LPFNADDFAV)(HWND, LPTSTR, LPTSTR);

事实上,这样的函数确实存在于“shdocvw.dll”中,那就是“DoAddToFavDlg”。


3、DoAddToFavDlg函数

“DoAddToFavDlg”函数也是“shdocvw.dll”暴露出来的函数之一,其原型如下:

typedef BOOL (CALLBACK* LPFNADDFAV)(HWND, TCHAR*, UINT, TCHAR*, UINT,LPITEMIDLIST);

第一个参数正是我们想要的父窗口句柄,第二和第四个参数分别是初始目录(一般来说就是收藏夹目录)和要添加的链接的名字(比如网页的Title),第三和第五个参数分别是第二和第四两个缓冲区的长度,而最后一个参数则是指向与第二个参数目录相关的item identifier list的指针(PIDL)。但最奇怪的是这里并没有像“AddFavorite”函数一样的链接URL,那链接是怎样添加的呢?答案是“手动创建”。
第二个参数在函数调用返回后会包含用户在“添加到收藏夹”对话框中选择或创建的完整链接路径名(如“X:/XXX/mylink.url”),我们就根据这个路径和网页的URL来创建链接,代码如下(为简化,此处省去检查"shdocvw.dll"是否已在内存中的代码,参见《Internet Explorer 编程简述(三)“整理收藏夹”对话框》):

void CMyHtmlView::OnFavAddtofav()
{
  typedef BOOL (CALLBACK* LPFNADDFAV)(HWND, TCHAR*, UINT, TCHAR*, UINT,LPITEMIDLIST);

  HMODULE hMod = (HMODULE)LoadLibrary("shdocvw.dll");
  if (hMod)
  {
    LPFNADDFAV lpfnDoAddToFavDlg = (LPFNADDFAV)GetProcAddress( hMod, "DoAddToFavDlg");
    if (lpfnDoAddToFavDlg)
    {
      TCHAR szPath[MAX_PATH];
      LPITEMIDLIST pidlFavorites;

      if (SHGetSpecialFolderPath(NULL, szPath, CSIDL_FAVORITES, TRUE) &&
         (SUCCEEDED(SHGetSpecialFolderLocation(NULL, CSIDL_FAVORITES, &pidlFavorites))))
      {
        TCHAR szTitle[MAX_PATH];
        strcpy(szTitle, GetLocationName());

        TCHAR szURL[MAX_PATH];
        strcpy(szURL, GetLocationURL());

        BOOL bOK = lpfnDoAddToFavDlg(m_hWnd, szPath,
             sizeof(szPath)/sizeof(szPath[0]), szTitle,
             sizeof(szTitle)/sizeof(szTitle[0]), pidlFavorites);
        CoTaskMemFree(pidlFavorites);

        if (bOK)
          CreateInternetShortcut( szURL, szPath, "");  //创建Internet快捷方式
      }
    }
    FreeLibrary(hMod);
  }
  return;
}

实现CreateInternetShortcut函数创建Internet快捷方式,可以用读写INI文件的方法,但更好的则是利用IUniformResourceLocator接口。

HRESULT CMyHtmlView::CreateInternetShortcut(LPCSTR pszURL, LPCSTR pszURLfilename,
  LPCSTR szDescription,LPCTSTR szIconFile,int nIndex)
{
  HRESULT hres;

  CoInitialize(NULL);

  IUniformResourceLocator *pHook;

  hres = CoCreateInstance (CLSID_InternetShortcut, NULL, CLSCTX_INPROC_SERVER,
    IID_IUniformResourceLocator, (void **)&pHook);

  if (SUCCEEDED (hres))
  {
    IPersistFile *ppf;
    IShellLink *psl;

  // Query IShellLink for the IPersistFile interface for
  hres = pHook->QueryInterface (IID_IPersistFile, (void **)&ppf);
  hres = pHook->QueryInterface (IID_IShellLink, (void **)&psl);

  if (SUCCEEDED (hres))
  {
    WORD wsz [MAX_PATH]; // buffer for Unicode string

  // Set the path to the shortcut target.
  pHook->SetURL(pszURL,0);

  hres = psl->SetIconLocation(szIconFile,nIndex);

  if (SUCCEEDED (hres))
  {
    // Set the description of the shortcut.
    hres = psl->SetDescription (szDescription);

  if (SUCCEEDED (hres))
  {
    // Ensure that the string consists of ANSI characters.
    MultiByteToWideChar (CP_ACP, 0, pszURLfilename, -1, wsz, MAX_PATH);

  // Save the shortcut via the IPersistFile::Save member function.
  hres = ppf->Save (wsz, TRUE);
  }
  }

  // Release the pointer to IPersistFile.
  ppf->Release ();
  psl->Release ();
  }

  // Release the pointer to IShellLink.
  pHook->Release ();

  }
  return hres;
}

好,上面的方法虽然麻烦一点,但总算解决了“模态窗口”的问题,使得我们的程序不至于让用户鄙视。但是问题又来了,我们发现“允许脱机使用”是Disabled的,那“自定义”也就无从谈起了,尽管90%的人都没有使用过IE提供的脱机浏览。

难道我们的希望要破灭吗?我们一方面想像调用“AddFavorite”函数一样的不必手动创建链接,一方面又要模态显示窗口,就像IE那样,还能自定义脱机浏览。

4、脚本方式

许多网页上都会有一个按钮或链接“添加本页到收藏夹”,实际上通过下面的脚本显示模态的“添加到收藏夹”对话框将网页加入到收藏夹。

window.external.AddFavorite(location.href, document.title);

这里的external对象是WebBrowser内置的 COM自动化对象,以实现对文档对象模型(DOM)的扩展(我们也可以通过IDocHostUIHandler实现自己的扩展).查阅MSDN可以得知 external对象的的方法与IShellUIHelper接口提供的方法是一样的。我们有理由相信,IShellUIHelper提供了对 WebBrowser内置的external对象的访问,如果在适当的地方创建IShellUIHelper接口的实例,也许调用 “AddFavorite”函数显示出来的就是模态对话框了。问题是我们还没有找到这样的地方。

从上面的脚本,我们很自然地又想到另一个方法。如果能够让网页来执行上面的脚本,岂不是问题就解决了?说做就做,如下:

void CMyHtmlView::OnFavAddtofav()
{
  CString strUrl = GetLocationURL();
  CString strTitle = GetLocationName();
  CString strjs = "javascript:window.external.AddFavorite('" + strUrl + "'," + "'" + strTitle + "');";
  ExecScript(strjs);
}

void CMIEView::ExecScript(CString strjs)
{
  CComQIPtr   pHTMLDoc = (IHTMLDocument2*)GetHtmlDocument();
  if ( pHTMLDoc != NULL  )
  {
    CComQIPtr   pHTMLWnd;
    pHTMLDoc->get_parentWindow( &pHTMLWnd );
    if ( pHTMLWnd != NULL  )
    {
      CComBSTR bstrjs = strjs.AllocSysString();
      CComBSTR bstrlan = SysAllocString(L"javascript");
      VARIANT varRet;
      pHTMLWnd->execScript(bstrjs, bstrlan, &varRet);
    }
  }
}

先从CHtmlView获得文档的父窗口window对象的指针,再调用其方法execScript来执行脚本(事实上可以执行任意的脚本)。试验发现,这个方法非常有效,不仅窗口是模态的,而且不需要手动创建链接,更重要的是“允许脱机使用”和“自定义”按钮也可以用了。

5、问题仍旧没有解决

执行脚本的方式看起来有效,可一旦我们的程序实现了 IDocHostUIHandler接口对WebBrowser进行高级控制,就会发现一旦执行的脚本包含有对“external”对象的调用,就会出现 “缺少对象”的脚本错误。原因是当MSHTML解析引擎(并非WebBrowser)检查到宿主实现了IDocHostUIHandler接口,就会调用其GetExternal方法以获得一个用以扩展DOM的自动化接口的引用。

HRESULT IDocHostUIHandler::GetExternal(IDispatch **ppDispatch)

但有时候我们并没有想要扩展DOM,同时我们还希望 WebBrowser使用它自己的DOM扩展。糟糕的是GetExternal方法的文档中说这种情况下必须把ppDispatch设置为NULL,换句话说,WebBrowser连它内置的external对象也不用了,那我们的window.external.AddFavorite就变得无处为家了。

我曾多方尝试将WebBrowser内置的external对象找出来,虽然都没有成功,但是解决问题的方法却被我找到了。

6、完美的方案

WebBrowser 内置的external对象我们虽然找不到,但它肯定存在,我们只要想办法让WebBrowser自己完成对其调用即可。实现非常简单,找到 WebBrowser中包含的“Internet Explorer_Server”窗口的句柄,发一个消息就完成了。下面的代码中假设m_hWndIE就是“Internet Explorer_Server”窗口的句柄。

#define ID_IE_ID_ADDFAV 2261
::SendMessage( m_hWndIE, WM_COMMAND, MAKEWPARAM(LOWORD(ID_IE_ID_ADDFAV), 0x0), 0 );

试一试成果,是不是和在Internet Explorer中选择“添加到收藏夹”的效果一模一样。

至于为什么这样做,后续文章再说。

参考资料
MSDN: Adding Internet Explorer Favorites to Your Application
MSDN: IShellUIHelper Interface
MSDN: external Object
MSDN: IDocHostUIHandler Interface

引用地址Internet Explorer 编程简述(四)“添加到收藏夹”对话框

发表于 @ 2004年09月12日 22:39:00|评论(17)|编辑

新一篇: Internet Explorer 编程简述(五)调用IE隐藏的命令 | 旧一篇: Internet Explorer 编程简述(三)“整理收藏夹”对话框

评论

#lxwde 发表于2004-09-13 10:08:00  IP: 165.170.128.*
楼主,你这一系列IE开发的文章非常好,继续。
#CathyEagle 发表于2004-09-13 10:21:00  IP: 222.18.4.*
谢谢:)
#埃及小白 发表于2004-09-13 01:41:00  IP: 219.233.101.*
这个办法依次类推可以打开很多的IE对话框。老兄的这个办法实在是好。
#CathyEagle 发表于2004-09-13 01:56:00  IP: 222.18.4.*
mail收到否?按键的问题应该可以解决了。
#埃及小白 发表于2004-09-13 02:12:00  IP: 219.233.101.*
你的上述方法应该说是:WebBrowser.AddToFavorites;
但是如果是那种javaScript:window.external.AddFavorite(...)就不行了。因为要指定网址和标题的。
#CathyEagle 发表于2004-09-13 02:32:00  IP: 222.18.4.*
什么意思?我没有哪个地方提到“上述方法”。
#埃及小白 发表于2004-09-13 02:46:00  IP: 219.233.101.*
::SendMessage( m_hWndIE, WM_COMMAND, MAKEWPARAM(LOWORD(ID_IE_ID_ADDFAV), 0x0), 0 );

就是我说的上述方法,这个办法可以添加当前浏览器控件所浏览的页面到收藏夹。
但如果是点击网页内部的javascript脚本,然后从GetExternal获得了网址和标题,并不可以用上述方法添加到收藏夹。
#CathyEagle 发表于2004-09-13 02:58:00  IP: 222.18.4.*
通过::SendMessage( m_hWndIE, WM_COMMAND, MAKEWPARAM(LOWORD(ID_IE_ID_ADDFAV), 0x0), 0 ); 显示的对话框已经包含了对网址和标题的处理。
#埃及小白 发表于2004-09-14 01:09:00  IP: 219.233.101.*
javaScript:window.external.AddFavorite(...) 的确也可以用这个办法打开。但是发现和在IE中点击该链接打开有区别,但是和IE右键菜单中的“添加到收藏夹”是一样的。(我这边会先告诉你,这个方法不安全,问你是否继续,按继续之后,脱机使用也是灰的)

CE,你可以试试看
#CathyEagle 发表于2004-09-14 02:49:00  IP: 211.138.91.*
当然有区别.。选中链接后调用脚本的话,是将选中的链接添加到收藏夹,在网页上其它地方弹出右键菜单是将该页添加到收藏夹。
我这里没有提示安全问题(当然,如果级别设置得很高的话可能会有),脱机也可用。
#youdu 发表于2004-09-16 09:36:00  IP: 202.104.34.*
好啊!!!但是C++有点陌生~~还不会~呵呵
#lingll 发表于2006-05-09 08:12:00  IP: 222.200.164.*
谢谢,
请问该如何实现方法3呢?
#lingll 发表于2006-05-08 21:20:00  IP: 219.222.194.*
最后的"完美的方法",也有不完美的地方,当然,对于希望完全模仿ie来说,这个是完美的,但如果希望能添加到自定义的收藏夹路经中,例如在应用程序所在目录下的favorite目录,而不是系统指定的目录,这个就不行了,这样说来还是DoAddToFavDlg比较好,但却没了"允许脱机使用"的功能,
不知是否还有更好的解决办法.
#CathyEagle 发表于2006-05-08 21:51:00  IP: 222.67.186.*
所谓“完美”,当然是就模仿IE而言,要完成自定义的功能则需要另寻出路了。就你的要求而言,大致来说,有几个解法:
1、自己实现一个全新的“添加到收藏夹”对话框(如Maxhon的做法)。此法不受任何限制。
2、在调用IE的实现(即本文的“完美”解法)之前临时把收藏夹的位置修改为指定的目录,添加完之后再改回来。此法虽不雅观,但恐怕最为简单直接。
3、Hook对DoAddToFavDlg的调用并在适当的地方修改起始路径(第二个参数)。这就复杂了。
#lingll 发表于2006-05-09 21:08:00  IP: 219.222.194.*
好的,非常感谢
#CathyEagle 发表于2006-05-09 18:12:00  IP: 222.67.186.*
我没有做过具体实现,只是说说原理。至于如何Hook,上网搜一下,有例子,或者找做外挂的朋友问问。
#guti_milan 发表于2006-11-15 11:43:00  IP: 58.214.166.*
真的受益菲浅啊!
perfect!!!!!!!

 

 

 Internet Explorer 编程简述(五)调用IE隐藏的命令收藏

新一篇: 利用WH_CBT Hook将非模态对话框显示为模态对话框 | 旧一篇: Internet Explorer 编程简述(四)“添加到收藏夹”对话框

关键字:Add To Favorite, Import/Export Wizard, Shell DocObject View, Internet Explorer_Server

 

文章请到CodeProject阅读——Invoke Hidden Commands in Your WebBrowser
也可以到CodeGuru阅读——Invoke Hidden Commands In Your WebBrowser
Delphi版本请到swissdelphicenter阅读——Invoke Hidden Commands In Your WebBrowser


中文版稍后附上。

 

 

Internet Explorer 编程简述(十一)实现完美的Inplace Drag & Drop——“超级拖放”收藏

新一篇: IE & Delphi再次复活 | 旧一篇:  Internet Explorer 编程简述(十)响应来自HTML Element的事件通知——几个好用的类

关键字:超级拖放,GetDropTarget,ondragover,IHTMLDataTransfer
 
1、概述
许多多窗口浏览器都提供了一种被称为“超级拖放”(或“超级拖拽”、“随心拖放”等等,不一而足)的功能。作为对IE拖拽行为对扩展,“超级拖放”实现了一些非常实用的功能:
  • 拖放网页链接:通常是在新窗口中打开
  • 拖放选中的文字:保存文字、作为关键字通过搜索引擎搜索网络、作为Url打开等
  • 拖放图片:通常是保存图片到指定文件夹
  • 当然,还有很关键的一点:拖动对象时鼠标指针反馈不同的拖拽效果
在《Internet Explorer 编程简述(十)响应来自HTML Element的事件通知——几个好用的类》中曾提到,尽管许多浏览器都提供了超级拖放的功能,但与IE的缺省实现相比,除了具备鼠标指针拖拽效果外,还没有哪个浏览器的实现能够实现:
  • 文字在页面内与输入框之间的交互拖放(这一点最为重要)
  • 来自外部的文字与网页输入框之间的交互拖放
  • 拖拽时滚动页面(这一点是被忽略了)
 
本文的目的,一是介绍实现超级拖放的两种方法,二是说明如何实现“完美”的拖放——即扩展IE拖拽行为的同时,保留IE默认的拖拽行为。三是给出一个最为直接和简洁的实现,至于拖放不同的对象以实现不同的功能,不在本文讨论的范围,略去。
 
 
2、标准的实现方法
标准方法即通过IDocHostUIHandler的GetDropTarget成员函数来实现,在MSDN这样说到:
IDocHostUIHandler::GetDropTarget Method——Called by MSHTML when it is used as a drop target. This method enables the host to supply an alternative IDropTarget interface.
即在适当的时候,MSHTML引擎会调用IDocHostUIHandler的GetDropTarget方法,为应用程序提供一个机会来替换MSHTML缺省的 DropTarget实现。我们就可以通过这个自定义的DropTarget实现来完成上述的“超级拖放”功能。方法示例如下,其中略去的部分可参考 MFC中CHtmlControlSite和CHtmlView的源代码:
 
STDMETHODIMP CHtmlControlSite::XDocHostUIHandler::GetDropTarget(
LPDROPTARGET pDropTarget, LPDROPTARGET* ppDropTarget)
{
METHOD_PROLOGUE_EX_(CHtmlControlSite, DocHostUIHandler)
*ppDropTarget = g_pDropTarget;//将自定义的实现告知MSHTML引擎
return S_OK;
}
 
其中 g_pDropTarget指向某个全局的IDropTarget接口的实现,我们假定为CIEDropTarget,CIEDropTarget实现了 IDropTarget的几个成员函数DragEnter、DragOver、DragLeave和Drop。在DragEnter中可以决定是否接受一个Drop以及如果接受这个Drop的话该提供怎样的鼠标拖拽反馈,在持续触发的DragOver中同样可以设定鼠标拖拽反馈,从而实现在拖放不同的对象(文字、链接、图像等)时提供不同的拖拽视觉效果,实现相当简单,此处不再赘述。
但上面的实现存在一些问题。首先是选中的文字在页面内与输入框之间交互的拖放没有了。这是自然的,既然我们用自定义的DropTarget替换掉了IE的缺省实现,那这种交互的拖放理应由我们自己实现。难处并非在于不能实现,而是在于实现起来比较麻烦——光是得到鼠标下的HTML Element就够我们烦了;当输入框中有文字的时候,光标还应该随着鼠标的移动而移动——所以这个费力还不一定讨好的功能似乎没有哪个浏览器去做。其次,作为输入框文字拖放的衍生物,拖拽滚动没有了。当鼠标向某个方向拖拽时,网页应该随着将不可见的部分滚动出来,比如某个输入框,让我们有机会将文字拖拽过去。这个Feature的实现并不困难,不过一来是被忽略了(注意到拖拽滚动的人并不多),二来主要Feature都没有实现,这个滚动也意义不大了。
 
3、打入MSHTML内部
既然从GetDropTarget提供外部实现难以得到与输入框的交互式拖放,那就换个角度来考虑问题,让我们打入MSHTML的内部。
着手点是 IHTMLDocumentX接口——操纵IE的DOM的法宝。我们注意到IHTMLDocument2有个ondragstart事件,进而想到应该也有诸如ondragenter、ondragover、ondrop之类的事件(事实上也是有的),如果响应这些事件,处理同输入框的交互式拖放应该就能够解决。因为这些拖放在MSHTML的缺省DropTarget实现中发生,因而当鼠标拖拽到某个输入框上时,肯定会触发一个ondragover事件,而在IHTMLEventObj的辅助下我们能轻松得到相关的HTML Element,其它的操作就容易进行了。再细心一点,我们还发现IHTMLEventObj2接口有个dataTransfer属性——可以得到一个 IHTMLDataTransfer的指针,而IHTMLDataTransfer接口正是浏览器内部用于数据交换的重要手段之一(看看它的属性就知道会很有用了):
IHTMLDataTransfer Members
clearData——Removes one or more data formats from the clipboard through dataTransfer or clipboardData object.
dropEffect——Sets or retrieves the type of drag-and-drop operation and the type of cursor to display.
effectAllowed——Sets or retrieves, on the source element, which data transfer operations are allowed for the object.
getData——Retrieves the data in the specified format from the clipboard through the dataTransfer or clipboardData objects.
setData——Assigns data in a specified format to the dataTransfer or clipboardData object.
 
更进一步,从IHTMLDataTransfer接口还可以访问到IDataObject接口,在进行Ole拖放时,数据就是通过IDataObject接口来传递的。具体用法稍后讨论。
 
4、打入MSHTML内部——思路
提供鼠标反馈效果与实现GetDropTarget的方法类似,有了IHTMLDataTransfer接口,便可在ondragstart及 ondragover事件触发时通过dropEffect属性设置拖拽的效果(可根据需要自行设定,不设置的话使用默认的效果)。再者,“拖”和“放”都在MSHTML的缺省实现中发生,我们从IHTMLEventObj的SrcElement即可得知鼠标所位置的HTML Element是否是输入框。
 
5、打入MSHTML内部——实现
要接收到ondragstart之类的事件,可以采用《Internet Explorer 编程简述(十)响应来自HTML Element的事件通知——几个好用的类》中提到的CHtmlObj类和CHtmlElements类,并在适当的地方连接到Document,示例代码如下所示:
 
HRESULT CHtmlDocument2::OnInvoke(DISPID dispidMember, REFIID riid, LCID lcid, WORD wFlags,
DISPPARAMS * pdispparams, VARIANT * pvarResult,EXCEPINFO * pexcepinfo,
UINT * puArgErr)
{
......
//如果只是要设置鼠标拖拽效果的话,这个事件可以不处理
case DISPID_HTMLELEMENTEVENTS_ONDRAGSTART :
{
OnDragStart();
break;
}
//重点在这里
case DISPID_HTMLELEMENTEVENTS_ONDRAGOVER :
{
OnDragOver();
break;
}
case DISPID_HTMLELEMENTEVENTS_ONDROP :
{
OnDrop();
break;
}
......
}
 
void CHtmlDocument2::OnDragOver(void)
{
SetDragEffect();               //设置鼠标拖拽效果
}
 
void CHtmlDocument2::SetDragEffect(void)
{
CComQIPtr<IHTMLWindow2>  pWindow;
CComQIPtr<IHTMLEventObj>  pEventObj;
CComQIPtr<IHTMLEventObj2>  pEventObj2;
CComQIPtr<IHTMLElement>  pElement;
 
HRESULT hr = m_spHtmlObj->get_parentWindow( &pWindow );
hr = pWindow->get_event( &pEventObj );
 
//ondragover发生时IE的默认行为是“没有鼠标拖拽效果”。
//将IHTMLEventObj的返回值设为false即可取消该事件的默认行为,所以执行完下面这句话,拖拽效果就出现了。
AllowDisplayDragCursor(pEventObj, FALSE);  
 
CComBSTR bstrTagName;
pEventObj->get_srcElement(&pElement);    //获得当前HTML Element
pElement->get_tagName(&bstrTagName);    
if ( IsEditArea(bstrTagName) ) //根据Tag Name判断是否鼠标位于输入框,以便设置焦点使得光标随鼠标移动
{
CComQIPtr<IHTMLElement2>  pElement2;
if ( SUCCEEDED(pElement->QueryInterface(IID_IHTMLElement2, (void **) &pElement2 ))
&& pElement2 )
{
pElement2->focus();
}
//默认情况下,当拖拽文档到输入框时,鼠标会变成拖拽的光标,所以这里使用IE的默认行为。
AllowDisplayDragCursor(pEventObj, TRUE);
}
}
 
BOOL CHtmlDocument2::IsEditArea(CComBSTR bstrTagName)
{
return bstrTagName == "INPUT" || bstrTagName == "TEXTAREA";
}
 
void CHtmlDocument2::AllowDisplayDragCursor(CComQIPtr<IHTMLEventObj> pEventObj, BOOL bAllow)
{
VARIANT v;
v.vt = VT_BOOL;
 
v.boolVal = !bAllow ? VARIANT_FALSE : VARIANT_TRUE;
pEventObj->put_returnValue(v);
}
 
void CHtmlDocument2::OnDrop(void)
{
CComQIPtr<IHTMLWindow2>  pWindow;
CComQIPtr<IHTMLEventObj>  pEventObj;
CComQIPtr<IHTMLEventObj2>  pEventObj2;
CComQIPtr<IHTMLElement>  pElement;
CComQIPtr<IHTMLDataTransfer>   pdt; //此处演示如何使用IHTMLDataTransfer
 
HRESULT hr = m_spHtmlObj->get_parentWindow( &pWindow );
hr = pWindow->get_event( &pEventObj );
hr = pEventObj->QueryInterface(IID_IHTMLEventObj2, (void **) &pEventObj2 );
hr = pEventObj2->get_dataTransfer(&pdt);
 
CComBSTR bstrFormat = "URL"; //首先尝试获取URL
VARIANT Data;
hr = pdt->getData(bstrFormat, &Data);
if ( Data.vt != VT_NULL )
{     //获取成功,拖放的对象是Url
DoOpenUrl(CString(Data.bstrVal));
}
else
{     //否则尝试获取选中的文本
bstrFormat = "Text";
hr = pdt->getData(bstrFormat, &Data);
if ( Data.vt != VT_NULL )
{     //获取成功,拖放的内容是文本
CComBSTR bstrTagName;
pEventObj->get_srcElement(&pElement);
pElement->get_tagName(&bstrTagName);
if ( IsEditArea(bstrTagName) )
{
//Drop target是输入框,不做任何操作,由IE进行默认处理
return;
}
else
{     //否则我们自己处理文本,或保存,或检测是否链接后打开,等等
DoProcessText(CString(Data.bstrVal));
//Process the text
}
}
else
{     //既不是链接,也不是文本,可认为是来自外部(如Windows Shell)的文件拖放
DoOnDropFiles(pdt);
}
}
}
 
//演示如何从IHTMLDataTransfer得到IDataObject

void CHtmlDocument2::DoOnDropFiles(CComQIPtr<IHTMLDataTransfer> pDataTransfer)

{
CComQIPtr<IServiceProvider>  psp;
CComQIPtr<IDataObject>  pdo;
if ( FAILED(pDataTransfer->QueryInterface(IID_IServiceProvider, (void **) &psp)) )
{
return;
}
if ( FAILED(psp->QueryService(IID_IDataObject, IID_IDataObject, (void **) &pdo)) )
{
return;
}
 
COleDataObject DataObject;
DataObject.Attach(pdo);
......
}
 
6、再次回到标准方法
上述通过 Event Sink响应网页拖拽的方法已经能够很好地工作,可说“趋于完美”了,但仍有两个“小”问题:第一,必须与document建立连接才能工作,而建立连接的时机不容易掌握(MSDN中推荐的位置是DocumentComplete,但在NavigateComplete中也可,或者是检测到 WebBrowser的readystate变为READYSTATE_INTERACTIVE时进行连接)。第二,实现方法还是略显复杂。
有没有更简单的方法呢?我决定再次对GetDropTarget进行“调研”。所谓“踏破铁鞋无觅处,得来全不费功夫”,晃了一眼GetDropTarget方法的声明后,灵机一动,我忽然想到了办法。事实证明,这是完美的解决办法。
 
让我们再来看看GetDropTarget的声明,其中第一个参数指向MSHTML提供的缺省DropTarget实现,而第二个参数用以返回应用程序的自定义 DropTarget实现,如果在GetDropTarget中返回S_OK,MSHTML将以应用程序提供的自定义DropTarget替换缺省的 DropTarget实现。

HRESULT GetDropTarget( IDropTarget *pDropTarget, IDropTarget **ppDropTarget);

参数说明

pDropTarget

[in] Pointer to an IDropTarget interface for the current drop target object supplied by MSHTML.

ppDropTarget

[out] Address of a pointer variable that receives an IDropTarget interface pointer for the alternative drop target object supplied by the host.

想到了吗?解决问题的关键就在于第一个参数pDropTarget。相信很多浏览器在处理的时候都忽略掉了第一个参数而只是将自己的实现通过第二个参数告知MSHTML,因而丢失了IE缺省的行为。既然如此,将缺省的IDropTarget接口的指针保存下来,在适当的时候调用,不就能够保留IE的原始拖放行为了吗?

 
7、完美实现
完整的代码就不再给出,我们只列出关键的部分作为示例。假设我们用来实现IDropTarget接口的类叫做CBrowserDropTarget:
//构造函数,传入参数即是从GetDropTarget得到的那个pDropTarget,它是MSHTML的缺省实现
CBrowserDropTarget::CBrowserDropTarget(IDropTarget *pOrginalDropTarget)
m_bDragTextToInputBox(FALSE)
//这个布尔变量用来判断是否正在向InputBox拖拽文字
m_pOrginalDropTarget(pOrginalDropTarget)
//m_pOrginalDropTarget用来保存MSHTML的缺省实现
{
}
 
STDMETHODIMP CBrowserDropTarget::DragEnter(/* [unique][in] */IDataObject __RPC_FAR *pDataObj,
/* [in] */ DWORD grfKeyState,
/* [in] */ POINTL pt,
/* [out][in] */ DWORD __RPC_FAR *pdwEffect)
{
//调用缺省的行为
return m_pOrginalDropTarget->DragEnter(pDataObj, grfKeyState, pt, pdwEffect);
}
 
STDMETHODIMP CBrowserDropTarget::DragOver(/* [in] */ DWORD grfKeyState,
/* [in] */ POINTL pt,
/* [out][in] */ DWORD __RPC_FAR *pdwEffect)
{
//在网页内拖拽文字时这个值是DROPEFFECT_COPY(拖拽的文字不属于输入框中)
//或DROPEFFECT_COPY | DROPEFFECT_MOVE(拖拽的文字是输入框中的文字)
DWORD dwTempEffect = *pdwEffect;
 
//接下来调用IE的缺省行为
HRESULT hr = m_pOrginalDropTarget->DragOver(grfKeyState, pt, pdwEffect);
 
//判断是否是往输入框拖拽文字
m_bDragTextToInputBox = IsDragTextToInputBox(dwOldEffect, *pdwEffect);
if ( !m_bDragTextToInputBox )
{
//不是往输入框拖拽文字,则使用原始的拖拽效果。否则和IE的缺省效果一样——也就是没有效果
*pdwEffect = dwTempEffect;
}
return S_OK;
}
 
//根据调用缺省行为前后的Effect值判断是否是往输入框拖拽文字
BOOL CBrowserDropTarget::IsDragTextToInputBox(DWORD dwOldEffect, DWORD dwNewEffect)
{
//如果是把非输入框中文字往输入框拖动,则dwOldEffect与dwNewEffect相等,都是DROPEFFECT_COPY
BOOL bTextSelectionToInputBox = ( dwOldEffect == DROPEFFECT_COPY )
&& ( dwOldEffect == dwNewEffect );
 
//如果是把文字从一个输入框拖到另一个输入框,则dwOldEffect为DROPEFFECT_COPY | DROPEFFECT_MOVE,
//而dwNewEffect的值可能为DROPEFFECT_MOVE(默认情况),也可能为DROPEFFECT_COPY(按下Ctrl键时)
BOOL bInputBoxToInputBox = ( dwOldEffect == (DROPEFFECT_COPY | DROPEFFECT_MOVE) )
&& ( dwNewEffect == DROPEFFECT_MOVE || dwNewEffect == DROPEFFECT_COPY );
 
//来自Microsoft Word的拖拽特殊一些,dwOldEffect是所有效果的组合值
BOOL bMSWordToInputBox =
( dwOldEffect == (DROPEFFECT_COPY | DROPEFFECT_MOVE | DROPEFFECT_LINK) )
&& ( dwNewEffect == DROPEFFECT_MOVE || dwNewEffect == DROPEFFECT_COPY );
 
//来自Edit Plus的拖拽过也特殊一些,dwOldEffect是个负数(怀疑是Edit Plus的拖拽实现有问题)
BOOL bEditPlusToInputBox = ( dwOldEffect < 0 )
&& ( dwNewEffect == DROPEFFECT_MOVE || dwNewEffect == DROPEFFECT_COPY );
 
//也许还有些例外,可再添加
......
return bTextSelectionToInputBox || bInputBoxToInputBox || bMSWordToInputBox || bEditPlusToInputBox;
}
 
STDMETHODIMP CBrowserDropTarget::DragLeave()
{
//调用缺省的行为
return m_pOrginalDropTarget->DragLeave();
}
 
STDMETHODIMP CBrowserDropTarget::Drop(/* [unique][in] */ IDataObject __RPC_FAR *pDataObj,
/* [in] */ DWORD grfKeyState,
/* [in] */ POINTL pt,
/* [out][in] */ DWORD __RPC_FAR *pdwEffect)
{
if ( m_bDragTextToInputBox )
{
//是文字拖放,调用IE的缺省行为
return m_pOrginalDropTarget->Drop(pDataObj, grfKeyState, pt, pdwEffect);
}
 
//否则是拖放链接、图片、文件等,按常规的IDataObject处理方式
......
return S_OK;
}
 
至此,我们就得到了一个完美的“超级拖放”的基本框架,它在扩展的同时保留了IE的默认行为:
  1. 文字在页面内与输入框之间能够交互拖放。
  2. 来自外部的文字与网页输入框之间也能交互拖放
  3. 拖拽时能够自动滚动页面
 
其余的功能,如向不同的方向拖拽以完成不同的工作,左键右键拖放执行不同的功能,按住Alt保存文字等等,可根据需要自行实现,不再讨论。
8、修正
今天和Stanley Xu聊了几个钟头,受益匪浅。根据Stanley的提议,毋须再作是否往输入框拖拽文字的判断,因为我们需要的只是在IE的缺省行为没有鼠标拖拽效果的时候让它有拖拽效果,因此只需要简单地判断调用IE缺省行为后的Effect值是否为0即可,如下:
//判断是否是往输入框拖拽文字
m_bDragTextToInputBox = *pdwEffect != 0;
简单而直接,当然更重要的是:可用。
 
9、参考资料
 
 

Internet Explorer 编程简述(十二)正确地设置和转移焦点收藏

新一篇: FAQ: 如何动态创建并访问网页元素 | 旧一篇: IE & Delphi再次复活

 
关键字:焦点,Focus,加速键,Accelerator,OLEIVERB_UIACTIVATE,IHTMLWindow2,IHTMLDocument4
 
1、概述
对于 99%有UI的Windows应用程序来说,键盘操作都是不可或缺而又容易被人们遗忘的一环。如果对Windows组件作一次逐个的测试,我们会发现 Microsoft提供的任何一个Windows组件都通过键盘实现完全的控制(“计算器”比较特殊,它是一个按钮很多且每个按钮都不能获得焦点的程序,但在帮助文档中我们仍然可以找到为每个按钮设置的快捷键),这对于一个专业的Windows应用程序或软件来说非常重要。换句话说,就算没有鼠标用户也不应该束手无策,用户应该可以通过键盘操作完成其希望的功能。焦点的转移无疑是键盘操作的一个重要方面,在浏览器编程中尤其如此。
 
2、焦点的基本概念
一般说来,在Windows中用户通过键盘转移焦点(Focus)有两个方法:第一,对于输入框附近有标签提示的情况,按住Alt+某个预设的字母(Accelerator,加速键)将焦点快速转移到输入框。如下图所示,按下“Alt+D”,焦点应转移到地址输入框;按下“Alt+G”,焦点应转移到搜索框(本文对此不做讨论)。第二,按住Tab键,焦点转移到由应用程序控制的下一个可获得焦点的窗口;按下Shift+Tab,焦点转移到上一个可获得焦点的窗口。如下图所示,如果地址输入框是当前获得焦点的窗口,则按下Tab时,焦点应转移到搜索框,再按下Shift+Tab,焦点应回到地址输入框。
 

 
焦点的设置和转移对于用户体验(Experience)来说是细微体贴而又重要的设计,但不幸的是不少Windows应用程序都或多或少犯了一些错误:
  1. 完全没有加速键。
这在国产信息系统中尤为常见。设计较差的信息系统常常会出现一个窗口拥有数十个输入框的情况,如果为每个编辑框都提供一个加速键的话,问题就出来了。字母键只有26个,就算把数字键也用上,也难免不能满足要求,所以很多信息系统干脆就不要加速键。
  1. 摆设用的加速键。
一些应用软件甚至不懂得加速键的意义,只知道依样画葫地在输入框的旁边用标签说明加速键,但仅此而已,用户根本无法通过Alt+加速键转移焦点到输入框。
  1. 错误地(或不能)转移焦点
对于基于对话框的应用程序来说,常犯的错误是用户按下Tab键时,焦点出乎用户意料地在输入框之间乱窜。而在上图这样的例子中,常犯的错误则是不能通过Tab转移焦点,或者按Tab能转移焦点但按Shift+Tab不能朝反方向转移焦点。
  1. 对嵌入的ActiveX控件缺乏处理
对于嵌入的ActiveX控件,尤其是WebBrowser控件来说,焦点的处理就更为麻烦了(这本是基于WebBrowser的浏览器编程的难题之一)。常见的浏览器要么不处理常规窗口与WebBrowser控件之间的焦点传递(Maxthon、Gosurf只支持在输入框之间传递焦点);要么处理不完整,焦点一旦从某个输入框转移到WebBrowser控件就再也回不来(如GreenBrowser);更有的根本就不处理任何焦点的传递(如世界之窗浏览器)。
 
按照本系列文章的惯例,本文讨论的目的将是提供一个完整(未必完美)的解决方案——:一,焦点在嵌入ReBar的各个输入框之间传递;二,焦点在普通Windows窗口(输入框)与WebBrowser控件之间传递。
 
3、设定目标
下图说明了我们希望实现的正常的焦点转移行为:
  • 从工具条上的任何一个输入框出发,按Tab将焦点转移到下一个输入框,按Shift+Tab将焦点转移到上一个输入框
  • 如果焦点所在输入框是工具条上的最后一个输入框,按Tab将焦点转移到WebBrowser控件当前的活动Html Element(上一次获得焦点的Element)
  • 如果焦点所在输入框是工具条上的第一个输入框,按Shift+Tab将焦点转移到WebBrowser控件当前活动Html Element
  • 对于上面两种情况,若WebBrowser控件没有当前活动的可获得焦点Html Element,则焦点应从输入框转移到WebBrowser控件的第一个或最后一个可获得焦点的Html Element
  • 如果焦点当前位于WebBrowser控件中,按Tab将焦点转移到下一个Html Element,按Shift+Tab将焦点转移到上一个Html Element
  • 如果焦点当前位于WebBrowser控件中,且当前的活动Html Element是最后一个可获得焦点的Html Element,按Tab将焦点转移到工具条的第一个输入框
  • 如果焦点当前位于WebBrowser控件中,且当前的活动Html Element是第一个可获得焦点的Html Element,按Shif+Tab将焦点转移到工具条的最后输入框
 
以下图为例,“Google大全”为WebBrowser当前获得焦点的Html Element,举例如下:
  • 例1:假设当前焦点位于地址输入框,按下Tab键不松开,焦点转移的顺序应是:“地址栏”,“搜索栏”,“Google大全”……“将Google设为首页”,“地址栏”,“搜索栏”,“个性化主页”,“搜索记录”……
  • 例2:假设当前焦点位于地址输入框,且WebBrowser控件没有活动的获得焦点的Html Element,按下Tab键不松开,焦点转移的顺序应是:“地址栏”,“搜索栏”,“个性化主页”,“搜索记录”……“将Google设为首页”,“地址栏”,……
  • 例3:假设当前焦点位于“搜索记录”,按下Shift+Tab键不松开,焦点转移的顺序应是:“搜索记录”,“个性化主页”,“搜索栏”,“地址栏”,“将Google设为首页”……“搜索记录”……
 

 
4、工具条输入框之间的焦点转移
为实现统一的处理,我们从CDialogBar派生一个CDialogBarEx类,由该类处理Tab/Shift Tab按键,而输入框(如EditBox,ComboBox等)则放在CDialogBarEx的派生类(如CUrlAddressBar、 CSearchBar等)中,这样输入框就可以专注于其它的功能。示例代码如下:
 
BOOL CDialogBarEx::PreTranslateMessage(MSG* pMsg)
{
if ( ( pMsg->message==WM_KEYDOWN ) )
{
if ( (pMsg->wParam == VK_TAB) )
{
//由MainFrame处理如何转移焦点,按下Shift表示焦点应转移到上一个窗口
g_pMainFrame->SetFocusToNextControl( GetKeyState(VK_SHIFT) >= 0 );
return TRUE;
}
}
......
return CDialogBar::PreTranslateMessage(pMsg);
}
 
void CMainFrame::SetFocusToNextControl(bool bNext)
{
//m_wndReBar是一个CReBarEx,可从CReBar派生
if ( !m_wndReBar.SetFocusToNextControl(bNext) )
{
//如果CReBarEx在其子窗口中找不到下(上)一个可以设置焦点的窗口,则把焦点转移到WebBrowser
CChildFrame *pChildFrame = (CChildFrame *)MDIGetActive();
if ( pChildFrame && pChildFrame->GetActiveView() )
{
pChildFrame->GetActiveView()->SetFocus();
}
}
}
 
bool CReBarEx::SetFocusToNextControl(bool bNext)
{
return bNext ? FocusNextControl() : FocusPrevControl();
}
 
bool CReBarEx::FocusNextControl()
{
REBARBANDINFO rbbi;
rbbi.cbSize = sizeof( rbbi );
rbbi.fMask = RBBIM_CHILD;
 
//先找到当前获得焦点的Band
UINT nBand;
for ( nBand = 0; nBand < m_rbCtrl.GetBandCount(); nBand++ )
{
VERIFY( m_rbCtrl.GetBandInfo(nBand, &rbbi) );
if ( ::IsChild(rbbi.hwndChild, ::GetFocus()) )
{
break;
}
}
 
//如果运行到这里,必定能够找到当前获得焦点的Band
ASSERT(nBand < m_rbCtrl.GetBandCount());
 
for ( nBand = nBand + 1; nBand < m_rbCtrl.GetBandCount(); nBand++ )
{
VERIFY( m_rbCtrl.GetBandInfo(nBand, &rbbi) );
::SetFocus(rbbi.hwndChild);
if ( ::IsChild(rbbi.hwndChild, ::GetFocus()) )
{
//成功找到并设置焦点到下一个窗口
return true;
}
}
//当前获得焦点的窗口已经是ReBarEx中最后一个可获得焦点的窗口
return false;
}
 
bool CReBarEx::FocusPrevControl()
{
//实现与FocusNextControl类似,此处略去
}
 
void CReBarEx::OnSetFocus(CWnd* pOldWnd)
{
//如果此时Shift为按下的状态,表示焦点可能是从WebBrowser的第一个活动Html Element转过来,
//则将焦点转移到最后一个输入框,否则转移到第一个输入框
//SetFocusToLastControl与SetFocusToFirstControl的实现相当简单,略去
return GetKeyState(VK_SHIFT) < 0 ? SetFocusToLastControl() : SetFocusToFirstControl();
}
 
5、焦点从WebBrowser转移到工具条输入框
处理浏览器的按键也曾是嵌入WebBrowser控件的编程难题之一,Delphi对WebBrowser的封装对按键的支持就存在很大问题。在《Programming Internet Explorer》中曾提到的方法是处理MainFrame的PreTranslateMessage,并在其中从WebBrowser的 Document查询得到IOleInPlaceActiveObject接口,将按键交给IOleInPlaceActiveObject的 TranslateAccelerator成员区处理。查询MSDN我们可以知道,IOleInPlaceActiveObject:: TranslateAccelerator被调用时,MSHTML引擎会调用IDocHostUIHandler接口的 TranslateAccelerator方法,从而给开发人员一个接口来处理按键。所以对于实现了IDocHostUIHandler接口的应用程序来说,按键处理就非常简单了。
 
//在此处理将焦点从WebBrowser中转移到ReBar上的输入框
HRESULT CMyView::OnTranslateAccelerator(LPMSG lpMsg,const GUID* pguidCmdGroup, DWORD nCmdID)
{
if (lpMsg && lpMsg->message == WM_KEYDOWN && lpMsg->wParam == VK_TAB)
{
LPDISPATCH lpDispatch = GetHtmlDocument();
CComQIPtr<IHTMLDocument2> pHTMLDoc = lpDispatch;
if ( pHTMLDoc )
{
CComQIPtr<IHTMLElement> pElement;
if ( SUCCEEDED(pHTMLDoc->get_activeElement(&pElement)) && !pElement )
{
//没有任何活动的Html Element,把焦点转移到ReBar
g_pMainFrame->m_wndReBar.SetFocus();
//通知MSHTML不要再继续处理按键
return S_OK;
}
}
}
return S_FALSE;
}
 
6、使WebBrowser获得焦点
使浏览器获得焦点也颇为讲究。我的一篇老文章《TWebBrowser编程简述中》写到有好几种方法可以使WebBrowser获得焦点:IOleObject::DoVerb(OLEIVERB_UIACTIVATE...)、 IHTMLWindow2::focus()、IHTMLDocument4::focus()。而实际上这几种方法是有区别的(内部实现我们并不清楚,也不关心)。
  • IOleObject:: DoVerb能够将焦点设置到WebBrowser上一次失去焦点时获得焦点的Html Element上。缺点在于如果WebBrowser上次失去焦点时没有任何Html Element获得焦点,则DoVerb并不能保证焦点会转移到WebBrowser中。
  • IHTMLWindow2::focus不管三七二十一,将焦点转移到WebBrowser的开头Html Element。这显然不是我们想要的。
  • 测试的结果,IHTMLDocument4::focus似乎能够满足要求:能够记住WebBrowser上次失去焦点时获得焦点的Html Element;在WebBrowser上次失去焦点时没有任何Html Element获得焦点的情况下,能够焦点转移到开头的Html Element。但事实上并不理想,假如按住Tab键不松开,反复调用IHTMLDocument4::focus多次之后,我们会发现焦点再也到不到 WebBrowser中了。
 
有没有完美解决的办法呢?答案当然是Positive的,如下:
void CMyView::OnSetFocus(CWnd* pOldWnd)
{
LPDISPATCH lpDisp = GetHtmlDocument();
CComQIPtr<IHTMLDocument2, &IID_IHTMLDocument2> pHTMLDoc(lpDisp);
if ( pHTMLDoc )
{
CComQIPtr<IHTMLElement> pElement;
if ( SUCCEEDED(pHTMLDoc->get_activeElement(&pElement)) && !pElement )
{
//没有任何活动元素,把焦点转移到WebBrowser的开头
CComQIPtr<IHTMLWindow2> pHTMLWnd;
if( SUCCEEDED(pHTMLDoc->get_parentWindow( &pHTMLWnd )) && pHTMLWnd )
{
pHTMLWnd->focus();
return;
}
}
}
 
//有活动的元素(上一次的焦点),直接将焦点转移过去
//CWnd::SetFocus()会调用IOleObject::DoVerb()正确地设置焦点
m_wndBrowser.SetFocus();
}
 
7、总结
至此,我们就算完整地实现了焦点在普通窗口和浏览器之间的传递,任何时候,按住Tab键不松开,焦点将会在所有可获得焦点的窗口之间循环传递;同样,按住 Shift-Tab不松开,焦点会以反方向传递。而不会出现用户无法将焦点转移到浏览器窗口的情况,或者焦点无法从浏览器窗口转移到输入框的情况。当然,还有比较重要也比较抽象的一点,增强了用户体验,呵呵。
 
8、参考资料
《Programming Internet Explorer》