2007年02月13日

   我们经常会采用生产者/消费者关系的两个线程来处理一个共享缓冲区的数据。例如一个生产者线程接受用户数据放入一个共享缓冲区里,等待一个消费者线程对数据取出处理。但是如果缓冲区的太小而生产者和消费者两个异步线程的速度不同时,容易出现一个线程等待另一个情况。为了尽可能的缩短共享资源并以相同速度工作的各线程的等待时间,我们可以使用一个“队列”来提供额外的缓冲区。
        创建一个“队列”对象:

import Queue
myqueue = Queue.Queue(maxsize = 10)

Queue.Queue类即是一个队列的同步实现。队列长度可为无限或者有限。可通过Queue的构造函数的可选参数maxsize来设定队列长度。如果maxsize小于1就表示队列长度无限。

        将一个值放入队列中:

myqueue.put(10)

调用队列对象的put()方法在队尾插入一个项目。put()有两个参数,第一个item为必需的,为插入项目的值;第二个block为可选参数,默认为1。如果队列当前为空且block1put()方法就使调用线程暂停,直到空出一个数据单元。如果block0put方法将引发Full异常。

        将一个值从队列中取出:

myqueue.get()

调用队列对象的get()方法从队头删除并返回一个项目。可选参数为block,默认为1。如果队列为空且block1get()就使调用线程暂停,直至有项目可用。如果block为0,队列将引发Empty异常。

        我们用一个例子来展示如何使用Queue:

# queue_example.py
from Queue import Queue
import threading
import random
import time

# Producer thread
class Producer(threading.Thread):
def __init__(self, threadname, queue):
threading.Thread.__init__(self, name = threadname)
self.sharedata = queue
def run(self):
for i in range(20):
print self.getName(),’adding’,i,’to queue’
self.sharedata.put(i)
time.sleep(random.randrange(10)/10.0)
print self.getName(),’Finished’

# Consumer thread
class Consumer(threading.Thread):
def __init__(self, threadname, queue):
threading.Thread.__init__(self, name = threadname)
self.sharedata = queue
def run(self):
for i in range(20):
print self.getName(),’got a value:’,self.sharedata.get()
time.sleep(random.randrange(10)/10.0)
print self.getName(),’Finished’

# Main thread
def main():
queue = Queue()
producer = Producer(‘Producer’, queue)
consumer = Consumer(‘Consumer’, queue)

print ‘Starting threads …’
producer.start()
consumer.start()

producer.join()
consumer.join()

print ‘All threads have terminated.’

if __name__ == ‘__main__’:
main()

        示例代码中实现了两个类:生产者类Producer和消费者类Consumer。前者在一个随机的时间内放入一个值到队列queue中然后显示出来,后者在一定随机的时间内从队列queue中取出一个值并显示出来。

 多个执行线程经常要共享数据,如果仅仅读取共享数据还好,但是如果多个线程要修改共享数据的话就可能出现无法预料的结果。
        假如两个线程对象t1t2都要对数值num=0进行增1运算,那么t1t2都各对num修改10次的话,那么num最终的结果应该为20。但是如果当t1取得num的值时(假如此时num0),系统把t1调度为“sleeping”状态,而此时t2转换为“running”状态,此时t2获得的num的值也为0,然后他把num+1的值1赋给num。系统又把t2转化为“sleeping”状态,t1为“running”状态,由于t1已经得到num值为0,所以他也把num+1的值赋给了num1。本来是2次增1运行,结果却是num只增了1次。类似这样的情况在多线程同时执行的时候是有可能发生的。所以为了防止这类情况的出现就要使用线程同步机制。
        最简单的同步机制就是“锁”。锁对象用threading.RLock类创建:

    mylock = threading.RLock()

        如何使用锁来同步线程呢?线程可以使用锁的acquire() (获得)方法,这样锁就进入“locked”状态。每次只有一个线程可以获得锁。如果当另一个线程试图获得这个锁的时候,就会被系统变为“blocked”状态,直到那个拥有锁的线程调用锁的release() (释放)方法,这样锁就会进入“unlocked”状态。“blocked”状态的线程就会收到一个通知,并有权利获得锁。如果多个线程处于“blocked”状态,所有线程都会先解除“blocked”状态,然后系统选择一个线程来获得锁,其他的线程继续沉默(“blocked”)。

import threading
mylock = threading.RLock()
class mythread(threading.Thread)
    …
    def run(self …):
        …     #此处 不可以 放置修改共享数据的代码
        mylock.acquire()
        …     #此处 可以 放置修改共享数据的代码
        mylock.release()
        …    
#此处 不可以 放置修改共享数据的代码

        我们把修改共享数据的代码称为“临界区”,必须将所有“临界区”都封闭在同一锁对象的acquire()release()方法调用之间。
        锁只能提供最基本的同步级别。有时需要更复杂的线程同步,例如只在发生某些事件时才访问一个临界区(例如当某个数值改变时)。这就要使用“条件变量”。
        条件变量用threading.Condition类创建:

    mycondition = threading.Condition()

        条件变量是如何工作的呢?首先一个线程成功获得一个条件变量后,调用此条件变量的wait()方法会导致这个线程释放这个锁,并进入“blocked”状态,直到另一个线程调用同一个条件变量的notify()方法来唤醒那个进入“blocked”状态的线程。如果调用这个条件变量的notifyAll()方法的话就会唤醒所有的在等待的线程。
        如果程序或者线程永远处于“blocked”状态的话,就会发生死锁。所以如果使用了锁、条件变量等同步机制的话,一定要注意仔细检查,防止死锁情况的发生。对于可能产生异常的临界区要使用异常处理机制中的finally子句来保证释放锁。等待一个条件变量的线程必须用notify()方法显式的唤醒,否则就永远沉默。保证每一个wait()方法调用都有一个相对应的notify()调用,当然也可以调用notifyAll()方法以防万一。

我们在做软件开发的时候很多要用到多线程技术。例如如果做一个下载软件象flashget就要用到、象在线视频工具realplayer也要用到因为要同时下载media stream还要播放。其实例子是很多的。
        线程相对进程来说是“轻量级”的,操作系统用较少的资源创建和管理线程。程序中的线程在相同的内存空间中执行,并共享许多相同的资源。
python中如何创建一个线程对象:
        如果你要创建一个线程对象,很简单,只要你的类继承threading.Thread,然后在__init__里首先调用threading.Thread__init__方法即可:
import threading
class mythread(threading.Thread):
def __init__(self, threadname):
threading.Thread.__init__(self, name = threadname)
….

        这才仅仅是个空线程,我可不是要他拉空车的,他可得给我干点实在活。很简单,重写类的run()方法即可,把你要在线程执行时做的事情都放到里面
import threading
import time
class mythread(threading.Thread):
def __init__(…):
….
def run(self):
for i in range(10):
print self.getName, i
time.sleep(1)

        以上代码我们让这个线程在执行之后每隔1秒输出一次信息到屏幕,10次后结束getName()threading.Thread类的一个方法,用来获得这个线程对象的name。还有一个方法setName()当然就是来设置这个线程对象的name的了。
        如果要创建一个线程,首先就要先创建一个线程对象
mythread1 = mythread(‘mythread 1′)
        一个线程对象被创建后,他就处于“born”(诞生状态),如何让这个线程对象开始运行呢?只要调用线程对象的start()方法即可:
mythread1.start()
现在线程就处于“ready”状态或者也称为“runnable”状态。
        奇怪吗?不是已经start了吗?为什么不称为“running”状态呢?其实是有原因的。因为我们的计算机一般是不具有真正并行处理能力的。我们所谓的多线程只是把时间分成片段,然后隔一个时间段就让一个线程执行一下,然后进入“sleeping ”状态,然后唤醒另一个在“sleeping”的线程,如此循环runnable->sleeping->runnable… ,只是因为计算机执行速度很快,而时间片段间隔很小,我们感受不到,以为是同时进行的。所以说一个线程在start了之后只是处在了可以运行的状态,他什么时候运行还是由系统来进行调度的。
        那一个线程什么时候会“dead”呢?一般来说当线程对象的run方法执行结束或者在执行中抛出异常的话,那么这个线程就会结束了。系统会自动对“dead”状态线程进行清理。
        如果一个线程t1在执行的过程中需要等待另一个线程t2执行结束后才能运行的话那就可以在t1在调用t2join()方法
….
def t1(…):

t2.join()

这样t1在执行到t2.join()语句后就会等待t2结束后才会继续运行。
        但是假如t1是个死循环的话那么等待就没有意义了,那怎么办呢?可以在调用t2join()方法的时候给一个浮点数做超时参数,这样这个线程就不会等到花儿也谢了了。我等你10s,你不回来我还不允许我改嫁啊? :)
def t1(…):

t2.join(10)

        如果一个进程的主线程运行完毕而子线程还在执行的话,那么进程就不会退出,直到所有子线程结束为止,如何让主线程结束的时候其他子线程也乖乖的跟老大撤退呢?那就要把那些不听话的人设置为听话的小弟,使用线程对象的setDaemon()方法,参数为bool型。True的话就代表你要听话,我老大(主线程)扯呼,你也要跟着撤,不能拖后腿。如果是False的话就不用那么听话了,老大允许你们将在外军命有所不受的。需要注意的是setDaemon()方法必须在线程对象没有调用start()方法之前调用,否则没效果。
t1 = mythread(‘t1′)
print t1.getName(),t1.isDaemon()
t1.setDaemon(True)
print t1.getName(),t1.isDaemon()
t1.start()
print ‘main thread exit’

        当执行到 print ‘main thread exit’ 后,主线程就退出了,当然t1这个线程也跟着结束了。但是如果不使用t1线程对象的setDaemon()方法的话,即便主线程结束了,还要等待t1线程自己结束才能退出进程。isDaemon()是用来获得一个线程对象的Daemonflag状态的。
        如何来获得与线程有关的信息呢?
获得当前正在运行的线程的引用:
running = threading.currentThread()
获得当前所有活动对象(即run方法开始但是未终止的任何线程)的一个列表:
threadlist = threading.enumerate()
获得这个列表的长度:
threadcount = threading.activeCount()
查看一个线程对象的状态调用这个线程对象的isAlive()方法,返回1代表处于“runnable”状态且没有“dead”:
threadflag = threading.isAlive()

2007年02月06日

1、概述

Internet Explorer有实在太多没有公布的东西。上一篇文章《Internet Explorer 编程简述(六)自定义浏览器上下文菜单》提到的获取“编码”菜单的方法就是利用了浏览器的上层窗口“Shell DocObject View”的未公布的命令ID。本文将要介绍的是如何用这个ID把“编码”菜单放到我们自己的菜单中来(如工具条上的“编码”按钮的下拉菜单)。

 

#define SHDVID_GETMIMECSETMENU 27
……
CComPtr spCT;

hr = pcmdTarget->QueryInterface(IID_IOleCommandTarget, (void**)&spCT);
……
// Get the language submenu
hr = spCT->Exec(&CGID_ShellDocView, SHDVID_GETMIMECSETMENU, 0, NULL, &var);

2、原理

上面指向IOleCommandTarget接口的智能指针spCT是从IDocHostUIHandler::ShowContextMenu的参数pcmdTarget得到的,它其实也可以从HTML文档接口得到,这就是实现的关键。

 

3、实现

下面的代码演示了如何将“编码”菜单放置到我们自己的编码菜单上去。

void CMainFrame::OnDropDown( NMHDR* pNotifyStruct, LRESULT* pResult )

{

 const UINT CmdID_GetMimeSubMenu = 27;

 // Command ID for getting the Encoding submenu

 

 NMTOOLBAR* pNMToolBar = ( NMTOOLBAR* )pNotifyStruct;

 CMenu menu;

 CMenu* pPopup = 0;

 CMyHtmlView *pView = NULL;

 m_bIsEncodMenuPopup = false;//标志变量,用以在WM_INITMENUPOPUP消息处理函数中检查“编码”菜单

 switch ( pNMToolBar->iItem )

 {

 ……
 case ID_VIEW_ENCODE://按下“编码”按钮

 {

  m_bIsEncodMenuPopup = true;

  VERIFY( menu.LoadMenu( IDR_ENCODE ) );//IDR_ENCODE是预置的“编码”菜单资源,内含任意一项占位用的菜单

  CMyHtmlView = GetActiveMyHtmlView();//检查当前是否存在活动的浏览器视图窗口

  if ( pView != NULL )

  {

   LPDISPATCH lpDispatch =pView->GetHtmlDocument();//获得文档指针

   if ( lpDispatch != NULL )

   {

    // Get an IDispatch pointer for the IOleCommandTarget interface.

    IOleCommandTarget * pCmdTarget = NULL;

    HRESULT hr = lpDispatch->QueryInterface(IID_IOleCommandTarget, (void**)&pCmdTarget);

    if ( SUCCEEDED( hr ) )

    {

     VARIANT varEncSubMenu;

     ::VariantInit( &varEncSubMenu );

     hr = pCmdTarget->Exec( &::CGID_ShellDocView, CmdID_GetMimeSubMenu, OLECMDEXECOPT_DODEFAULT, NULL, &varEncSubMenu );

     if ( SUCCEEDED( hr ) )

     {

      // 添加“编码”菜单

      MENUITEMINFO miiEncoding;

      ::memset( &miiEncoding, 0, sizeof(MENUITEMINFO) );

 

      miiEncoding.cbSize = sizeof(MENUITEMINFO);

      miiEncoding.fMask = MIIM_SUBMENU;

      miiEncoding.hSubMenu = reinterpret_cast< HMENU > (varEncSubMenu.byref);

      menu.SetMenuItemInfo(0, &miiEncoding, TRUE);//丢掉设计时占位用的菜单,替换为“编码”菜单

      }

    }

   }

  }

  pPopup = menu.GetSubMenu( 0 );

  break;

 }

 ……

 }

 

 if ( pPopup != 0 )

 {

  CRect rc;

  ::SendMessage( pNMToolBar->hdr.hwndFrom, TB_GETRECT, pNMToolBar->iItem, ( LPARAM )&rc );

  rc.top = rc.bottom;

  ::ClientToScreen( pNMToolBar->hdr.hwndFrom, &rc.TopLeft() );

  long lResult = pPopup->TrackPopupMenu( TPM_LEFTALIGN | TPM_LEFTBUTTON | TPM_RETURNCMD, rc.left, rc.top, this );

  m_bIsEncodMenuPopup = false;

  if ( pNMToolBar->iItem == ID_VIEW_ENCODE )

  {

   //其余的事教给浏览器去做,参考《Internet Explorer 编程简述(五)调用IE隐藏的命令(中文版)

    CFindIEWnd FindIEWnd( pView->m_wndBrowser.m_hWnd, "Internet Explorer_Server");

   ::SendMessage( FindIEWnd.m_hWnd, WM_COMMAND, MAKEWPARAM(LOWORD(lResult), 0×0), 0 );

  }

  else

  {

   SendMessage( WM_COMMAND, MAKEWPARAM(LOWORD(lResult), 0×0), 0 );

  }

 }

 *pResult = TBDDRET_DEFAULT;

}

void CMainFrame::OnInitMenuPopup(CMenu* pPopupMenu, UINT nIndex, BOOL bSysMenu)

{

 CMDIFrameWndEx::OnInitMenuPopup(pPopupMenu, nIndex, bSysMenu);

 if ( m_bIsEncodMenuPopup )

 {

  //默认情况下“编码”的所有菜单项都是Disabled的,在此修改其状态为Enabled

  for ( UINT i=0; i

GetMenuItemCount(); i++ )

  {

   pPopupMenu->EnableMenuItem( pPopupMenu->GetMenuItemID( i ), MF_ENABLED | MF_BYCOMMAND );

  }

 }

}

这样一来,原本只在浏览器上下文菜单中出现的“编码”菜单就出现在了我们自己的工具条按钮下拉菜单上,无需更多的处理,菜单状态的改变,编码的设置等,一切都教给浏览器自己去完成了。

 

1、概述
Internet Explorer提供了非常开发的接口,使开发人员不仅可以把其浏览器核心嵌入应用程序,还可以通过各种接口以实现更深层的控制。本文就将介绍对浏览器进行高级控制的话题之一——自定义上下文菜单。

2、最简单的情况
自定义的IE及WebBrowser的上下文菜单,最简单的方式就是在注册表的HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\MenuExt下添加自定义的键值,步骤如下:
1)添加一个新的键,其名称即为将来显示在上下文菜单中的菜单项名称,如:
 HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\MenuExt\&Google Search
2)将新增的键的默认值设置为一个包含脚本的网页的URL(或文件路径全名),该网页中的脚本将在用户点击上下文菜单中的“Google Search”后被浏览器执行。

3)在新增的键下还可以新建一个二进制值Contexts,用以指定我们新增的菜单项针对特定的网页对象是否出现,其取值可以是如下值的组合(逻辑或)

Context     Value
Default     0×1
Images     0×2
Controls    0×4
Tables     0×8
Text selection 0×10
Anchor     0×20

4)还可以建立一个DWORD类型的Flags项并将其值设置为0×01,这将使得前述脚本在一个模态窗口中执行,就好像是通过window.showModalDialog调用的,但不同的是在脚本中仍然可以访问window对象。
5)实例脚本如下:

通过修改注册表自定义菜单的方法适用于Internet Explorer和WebBrowser,也具有良好的扩展性。但我们如果希望执行的是不仅仅是脚本,二是自己的程序中代码,这种方法就不适用了。

3、使用完全自定义的菜单
1)
IDocHostUIhandler接口提供了一个ShowContextMenu方法,在需要显示上下文菜单之前,MSHTML引擎就会调用实现了IDocHostUIHandler接口的
宿主程序的ShowContextMenu方法。

HRESULTIDocHostUIHandler::ShowContextMenu(
  DWORD dwID,
  POINT *ppt,
  IUnknown *pcmdtReserved,
  IDispatch *pdispReserved
);

dwID参数的意义与Contexts的组合类似;ppt为菜单的弹出点屏幕坐标;pcmdtReserved接口指向IOleCommandTarget接口,可用于检测网页对象的状态和执行命令等操作。pdispReserved在IE5以上版本中指向的是网页对象的IDispatch接口,用以区分不同对象,比如我们可以这样来获得网页对象的指针:

IHTMLElement *pElem;
HRESULT hr;
hr = pdispReserved->QueryInterface(IID_IHTMLElement, (void**)pElem);
if(SUCCEEDED (hr)) {
  BSTR bstr;
  pElem->get_tagName(bstr);
  USES_CONVERSION;
  ATLTRACE("TagName:%s\n", OLE2T(bstr));
  SysFreeString(bstr);
  pElem->Release();
}

如果我们在该方法中返回S_OK,则告诉MSHTML我们将使用自己的菜单(界面),如果是S_FALSE,则弹出默认的菜单。

2)实现
原理清楚之后,实现起来非常简单,和弹出一般的菜单没什么两样,举例如下,显示主框架的“文件菜单”:

HRESULT CMyHtmlView::OnShowContextMenu(DWORD dwID, LPPOINT ppt, IUnknown * pcmdtReserved, IDispatch *)
{
 // 载入主菜单
 HMENU hMenuParent = ::LoadMenu( ::AfxGetInstanceHandle(), MAKEINTRESOURCE(IDR_MAINFRAME) );
 if (hMenuParent)
 {
  HMENU hMenu = ::GetSubMenu( hMenuParent, 0 ); // 取得“文件”子菜单
  if (hMenu)
  {
   // 显示菜单
   TrackPopupMenuEx( hMenu,
    TPM_LEFTALIGN | TPM_TOPALIGN,
    ppt->x,
    ppt->y,
    ::AfxGetMainWnd()->m_hWnd,
    NULL );
  }
  ::DestroyMenu( hMenuParent );
 }
 return S_OK;
}

4、自定义标准上下文菜单
1)
原理
更多的时候我们希望能在浏览器原来菜单的基础上作一些修改,如删掉“查看源文件”,添加自己的菜单项,等等,而不是完全不要原始的菜单,怎么办呢?借助MSDN提供的例子,我们来看看:

HRESULT CBrowserHost::ShowContextMenu(DWORD dwID, POINT *ppt,IUnknown *pcmdTarget,IDispatch *pdispObject)
{
 #define IDR_BROWSE_CONTEXT_MENU 24641
 #define IDR_FORM_CONTEXT_MENU 24640
 #define SHDVID_GETMIMECSETMENU 27
 #define SHDVID_ADDMENUEXTENSIONS 53

 HRESULT hr;
 HINSTANCE hinstSHDOCLC;
 HWND hwnd;
 HMENU hMenu;

 CComPtr spCT;
 CComPtr spWnd;

 MENUITEMINFO mii = {0};
 CComVariant var, var1, var2;
 hr = pcmdTarget->QueryInterface(IID_IOleCommandTarget, (void**)&spCT);
 hr = pcmdTarget->QueryInterface(IID_IOleWindow, (void**)&spWnd);
 hr = spWnd->GetWindow(&hwnd);//取得浏览器窗口句柄
 hinstSHDOCLC = LoadLibrary(TEXT("SHDOCLC.DLL"));
 if (hinstSHDOCLC == NULL)
 {
  // Error loading module — fail as securely as possible
  return;
 }
 hMenu = LoadMenu(hinstSHDOCLC, MAKEINTRESOURCE(IDR_BROWSE_CONTEXT_MENU));
 hMenu = GetSubMenu(hMenu, dwID);
 // Get the language submenu
 hr = spCT->Exec(&CGID_ShellDocView, SHDVID_GETMIMECSETMENU, 0, NULL, &var);
 mii.cbSize = sizeof(mii);
 mii.fMask = MIIM_SUBMENU;
 mii.hSubMenu = (HMENU) var.byref;
 // Add language submenu to Encoding context item
 SetMenuItemInfo(hMenu, IDM_LANGUAGE, FALSE, &mii);
 // Insert Shortcut Menu Extensions from registry
 V_VT(&var1) = VT_INT_PTR;
 V_BYREF(&var1) = hMenu;
 V_VT(&var2) = VT_I4;
 V_I4(&var2) = dwID;
 hr = spCT->Exec(&CGID_ShellDocView, SHDVID_ADDMENUEXTENSIONS, 0, &var1, &var2);
 // Remove View Source
 DeleteMenu(hMenu, IDM_VIEWSOURCE, MF_BYCOMMAND);//删除“查看源文件”菜单项
 // Show shortcut menu
 int iSelection = ::TrackPopupMenu(hMenu,
  TPM_LEFTALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD,//返回用户选择的菜单命令ID
  ppt->x,
  ppt->y,
  0,
  hwnd,
  (RECT*)NULL);
 // Send selected shortcut menu item command to shell
 LRESULT lr = ::SendMessage(hwnd, WM_COMMAND, iSelection, NULL);//发送到Internet Explorer_Server进行内部处理。
 FreeLibrary(hinstSHDOCLC);
 return S_OK;
}

从上面的例子我们看出,基本的方法就是根据“shdoclc.dll”文件中的菜单资源建立菜单,再通过来自pcmdTarget的IOlcCommandTarget接口获得“编码”菜单以及HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\MenuExt下的定义扩展菜单,然后以TPM_RETURNCMD标志调用TrackPopupMenu或TrackPopupMenuEx弹出菜单,并将返回的菜单命令ID教给浏览器窗口进行处理。这种方法可以调用许多通过浏览器无法直接调用的命令和对话框(参阅:《Internet Explorer 编程简述(五)调用IE隐藏的命令》)。

所以,我们只需要在弹出菜单之前做一些自定义操作即可达到修改默认菜单的目的,如上面代码中就用删除了“查看源文件”菜单项。

2)问题
如果我们不仅仅是删除默认的菜单项或是修改了默认的菜单项,还添加了自己的菜单项,会出现什么情况呢?由于使用了类似于MFC中UpdateUI的机制,遇到不认识的CommandID,浏览器就会将其状态设置为Disabled,所以我们自己添加的菜单是无法被选择的。
可以想到,如果把菜单状态设置为Enabled,并通过TPM_RETURNCMD标志调用TrackPopupMenu或TrackPopupMenuEx,再把返回的命令ID教给合适的窗口(如主框架窗口)去处理不就行了。关键点就在于如何把菜单状态设置为Enabled。

3)实现
解决的办法是截获WM_INITMENUPOPUP消息,在菜单创建以后,尚未显示之前修改菜单项状态,那浏览器就没有办法了。截获WM_INITMENUPOPUP消息则可使用子类化(subclass)的技术,前面通过IOleWindow接口我们得到了浏览器窗口的句柄hwnd,则可以这样做:

HMENU g_hPubMenu = NULL;
WNDPROC g_lpPrevWndProc = NULL;

LRESULT CALLBACK CustomMenuWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
 if (uMsg == WM_INITMENUPOPUP)
 {
  if (wParam == (WPARAM) g_hPubMenu)
  {
   ::EnableMenuItem( 自定义的菜单命令ID, MF_ENABLED | MF_BYCOMMAND );
   ::CheckMenuItem( 自定义的菜单命令ID, MF_BYCOMMAND);
   return 0;
  }
 }
 return CallWindowProc(g_lpPrevWndProc, hwnd, uMsg, wParam, lParam);
}

HRESULT CMyHtmlView::OnShowContextMenu(DWORD dwID, LPPOINT ppt,
LPUNKNOWN pcmdtReserved, LPDISPATCH pdispReserved)
{
//浏览器菜单句柄保存在g_hPubMenu中
……
// subclass浏览器窗口
g_lpPrevWndProc = (WNDPROC)::SetWindowLong(hwnd, GWL_WNDPROC, (LONG)CustomMenuWndProc);
//m_SubclassWnd.SubclassWindow( hwnd );//MFC中用此方法更简便

// Show shortcut menu
int iSelection = ::TrackPopupMenu(hSubMenu,
 TPM_LEFTALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD,
 ppt->x,
 ppt->y,
 0,
 hwnd,
 (RECT*)NULL);
// Unsubclass浏览器窗口
::SetWindowLong(hwnd, GWL_WNDPROC, (LONG)g_lpPrevWndProc);
g_lpPrevWndProc = NULL;
//m_SubclassWnd.UnsubclassWindow();

if (iSelection == 自定义的菜单命令ID )
{
 ::SendMessage( ::AfxGetMainWnd()->m_hWnd, WM_COMMAND, MAKEWPARAM(LOWORD(lResult), 0×0), 0 );
}
else
{
 LRESULT lr = ::SendMessage(hwnd, WM_COMMAND, iSelection, NULL);
}
……
}

在MFC中则更为方便,从CWnd继承一个窗口类,假设为CWebBrowserSubclassWnd,为CMyHtmlView添加一个CWebBrowserSubclassWnd类型的成员变量m_SubclassWnd,在子类化和去除子类化时调用m_SubclassWnd.SubclassWindow( hwnd )和m_SubclassWnd.UnsubclassWindow()即可。相应的WM_INITMENUPOPUP消息处理函数如下所示:

void CWebBrowserSubclassWnd::OnInitMenuPopup(CMenu* pPopupMenu, UINT nIndex, BOOL bSysMenu)
{
 CWnd::OnInitMenuPopup(pPopupMenu, nIndex, bSysMenu);

 pPopupMenu->EnableMenuItem( 自定义的菜单命令ID, MF_ENABLED | MF_BYCOMMAND );
 pPopupMenu->CheckMenuItem( 自定义的菜单命令ID, MF_BYCOMMAND);
}

下面的图片显示了将“文字大小”菜单项添加到“编码”菜单项的下面的效果。

5、新的问题
看完上面的代码,我们又自然地想到浏览器编程中的另一个问题,那就是“编码”菜单。我们指定,手动建立一个“编码”菜单是比较麻烦的事,而且很难做到与浏览器上下文菜单上的“编码”菜单一样的效果。何不使用上述的方法让浏览器自己建立“编码”菜单和处理相应的命令呢?

具体实现请看下一篇文章《Internet Explorer 编程简述(七)完美的“编码”菜单》

参考资料
MSDN:Adding Entries to the Standard Context Menu
MSDN:How To Adding to the Standard Context Menus of the WebBrowser Control
MSDN:IDocHostUIHandler::ShowContextMenu Method
BeginThread.com:Custom WebBrowser Context Menus

 1、概述

  除了“整理收藏夹”和“添加到收藏夹”对话框外,还有其它一些对话框是我们希望直接通过WebBrowser调用的,比如“导入/导出”对话框,用一般的方法很难调用。IShellUIHelper尽管提供了ImportExportFavorites方法,但结果只是显示一个选择文件的对话框,且只能导入/导出收藏夹而不能对Cookies操作。

  2、契机

  MSDN中有一篇叫“WebBrowser Customization”的文章,其中介绍了通过IDocHostUIHandler.ShowContextMenu方法自定义WebBrowser上下文菜单的方法。

其原理是从“shdoclc.dll”的资源中创建菜单,作一些修改之后用TrackPopupMenu函数(注意在标志中包含TPM_RETURNCMD)将菜单弹出,然后把返回的Command ID发送给“Internet Explorer_Server”窗口进行处理。

  ……
  // 显示菜单
  int iSelection = ::TrackPopupMenu(hMenu,
  TPM_LEFTALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD,
  ppt->x,
  ppt->y,
  0,
  hwnd,
  (RECT*)NULL);
  // 发送Command ID到外壳窗口
  LRESULT lr = ::SendMessage(hwnd, WM_COMMAND, iSelection, NULL);
  ……

  好,如果找到所有上下文菜单的Command ID,不就可以随时调用了?确实是这样的。

  3、实现
  用eXeScope之类应用程序资源探索器打开“shdoclc.dll”便可以在菜单资源下找到上下文菜单的设计,如下图:

  我们要做的,就是将这些ID发送到“Internet Explorer_Server”窗口进行处理。问题是WebBrowser其实是一个OLE容器,我们使用的CHtmlView又是更外层的封装,他们的m_hWnd成员变量并不是IE窗口的句柄,如何找到我们需要的句柄呢?请看下面的图:

  根据图中显示的从属关系,顺藤摸瓜,最内层的窗口“Internet Explorer_Server”的句柄就是我们需要的东西。为了简化问题,我这里使用了来自MSDN Magazine资深专栏撰稿人Paul Dilascia的CFindWnd类,非常好用。

  ////////////////////////////////////////////////////////////////
  // MSDN Magazine — August 2003
  // If this code works, it was written by Paul DiLascia.
  // If not, I don’t know who wrote it.
  // Compiles with Visual Studio .NET on Windows XP. Tab size=3.
  //
  // —
  // This class encapsulates the process of finding a window with a given class name
  // as a descendant of a given window. To use it, instantiate like so:
  //
  // CFindWnd fw(hwndParent,classname);
  //
  // fw.m_hWnd will be the HWND of the desired window, if found.
  //
  class CFindWnd {
  private:
  //////////////////
  // This private function is used with EnumChildWindows to find the child
  // with a given class name. Returns FALSE if found (to stop enumerating).
  //
  static BOOL CALLBACK FindChildClassHwnd(HWND hwndParent, LPARAM lParam) {
  CFindWnd *pfw = (CFindWnd*)lParam;
  HWND hwnd = FindWindowEx(hwndParent, NULL, pfw->m_classname, NULL);
  if (hwnd) {
  pfw->m_hWnd = hwnd; // found: save it
  return FALSE; // stop enumerating
  }
  EnumChildWindows(hwndParent, FindChildClassHwnd, lParam); // recurse
  return TRUE; // keep looking
  }
  public:
  LPCSTR m_classname; // class name to look for
  HWND m_hWnd; // HWND if found
  // ctor does the work–just instantiate and go
  CFindWnd(HWND hwndParent, LPCSTR classname)
  : m_hWnd(NULL), m_classname(classname)
  {
  FindChildClassHwnd(hwndParent, (LPARAM)this);
  }
  };

 

  再写一个函数InvokeIEServerCommand,调用就很方便了,《Internet Explorer 编程简述(四)“添加到收藏夹”对话框》中最后给出的方法就是从这里来的。

  void CMyHtmlView::InvokeIEServerCommand(int nID)
  {
  CFindWnd FindIEWnd( m_wndBrowser.m_hWnd, "Internet Explorer_Server");
  ::SendMessage( FindIEWnd.m_hWnd, WM_COMMAND, MAKEWPARAM(LOWORD(nID), 0×0), 0 );
  }

 void CMyHtmlView::OnFavAddtofav()
  {
  InvokeIEServerCommand(ID_IE_CONTEXTMENU_ADDFAV);//调用“添加到收藏夹”对话框
  }

  4、Command IDs
  对所有的Command ID逐一尝试后我们发现:
  1)不是所有的Command ID都可以用上面的方法调用;
  2)不是所有的Command ID都是由“Internet Explorer_Server”窗口处理;
  3)有一些Command ID是由上一级窗口“Shell DocObject View”处理。
  所以我们还需要写一个函数。

  void CMyHtmlView::InvokeShellDocObjCommand(int nID)
  {
  CFindWnd FindIEWnd( m_wndBrowser.m_hWnd, "Shell DocObject View");
  ::SendMessage( FindIEWnd.m_hWnd, WM_COMMAND, MAKEWPARAM(LOWORD(nID), 0×0), 0 );
  }

 

  调用文章开头提到的“导入/导出”对话框可以这样来做:

  void CDemoView::OnImportExport()
  {
  InvokeShellDocObjCommand(ID_IE_FILE_IMPORTEXPORT);//调用“导入/导出”对话框
  }

  由"Internet Explorer_Server"窗口处理的Command ID:
  #define ID_IE_CONTEXTMENU_ADDFAV 2261
  #define ID_IE_CONTEXTMENU_VIEWSOURCE 2139
  #define ID_IE_CONTEXTMENU_REFRESH 6042

  由"Shell DocObject View"窗口处理的Command ID:
  #define ID_IE_FILE_SAVEAS 258
  #define ID_IE_FILE_PAGESETUP 259
  #define ID_IE_FILE_PRINT 260
  #define ID_IE_FILE_NEWWINDOW 275
  #define ID_IE_FILE_PRINTPREVIEW 277
  #define ID_IE_FILE_NEWMAIL 279
  #define ID_IE_FILE_SENDDESKTOPSHORTCUT 284
  #define ID_IE_HELP_ABOUTIE 336
  #define ID_IE_HELP_HELPINDEX 337
  #define ID_IE_HELP_WEBTUTORIAL 338
  #define ID_IE_HELP_FREESTUFF 341
  #define ID_IE_HELP_PRODUCTUPDATE 342
  #define ID_IE_HELP_FAQ 343
  #define ID_IE_HELP_ONLINESUPPORT 344
  #define ID_IE_HELP_FEEDBACK 345
  #define ID_IE_HELP_BESTPAGE 346
  #define ID_IE_HELP_SEARCHWEB 347
  #define ID_IE_HELP_MSHOME 348
  #define ID_IE_HELP_VISITINTERNET 349
  #define ID_IE_HELP_STARTPAGE 350
  #define ID_IE_FILE_IMPORTEXPORT 374
  #define ID_IE_FILE_ADDTRUST 376
  #define ID_IE_FILE_ADDLOCAL 377
  #define ID_IE_FILE_NEWPUBLISHINFO 387
  #define ID_IE_FILE_NEWCORRESPONDENT 390
  #define ID_IE_FILE_NEWCALL 395
  #define ID_IE_HELP_NETSCAPEUSER 351
  #define ID_IE_HELP_ENHANCEDSECURITY 375

  5、Refresh

  熟悉TEmbeddedWB的读者可能注意到了ID_IE_CONTEXTMENU_REFRESH(6042)这个ID,在TEmbeddedWB中给出了一个当网页刷新时触发的OnRefresh事件,其中的关键代码如下:

  ……
  if Assigned(FOnRefresh) and ((nCmdID = 6041 { F5}) or (nCmdID = 6042 { ContextMenu}) or (nCmdID = 2300)) then
  begin
  FCancel := False;
  FOnRefresh(self, nCmdID, FCancel);
  if FCancel then Result := S_OK;
  end;
  ……

  其中的6402就是我们这里的ID_IE_CONTEXTMENU_REFRESH,2300是内置的刷新命令,那6041呢。见下图,还是“shdoclc.dll”,6041原来是IE“查看”菜单下“刷新”菜单的命令ID。实际开发中我们发现直接调用WebBrowser的Refresh命令有时候会导致一些错误,可以用这里的方法替换一下。

  6、需要注意的问题

  1)用InvokeIEServerCommand(ID_IE_CONTEXTMENU_ADDFAV)调用“添加到收藏夹”对话框时需要注意的是,IE接收到ID_IE_CONTEXTMENU_ADDFAV命令时是对网页中当前被选中的链接来执行“添加到收藏夹”操作的,如果没有选中的链接,才是将当前网页添加到收藏夹。

  2)新建IE窗口。这是浏览器编程中的难题之一,即从当前窗口新建一个Internet Explorer窗口,完全复制当前页的内容(包括“前进”、“后退”的状态),这可以通过InvokeShellDocObjCommand(ID_IE_FILE_NEWWINDOW)来实现。

  3)显示IE的版本信息。调用InvokeShellDocObjCommand(ID_IE_HELP_ABOUTIE),如下:

  4)InvokeShellDocObjCommand(ID_IE_FILE_PRINT)调出的“打印”对话框是非模态的(我们不太清楚Microsoft的设计意图,我认为“打印”对话框应该是模态的),显示模态窗口的方法请参考我的另一篇文章《利用WH_CBT Hook将非模态对话框显示为模态对话框》

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那样,还能自定义脱机浏览。

  3、脚本方式

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

  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<IHTMLDocument2>   pHTMLDoc = (IHTMLDocument2*)GetHtmlDocument();
  if ( pHTMLDoc != NULL  )
  {
  CComQIPtr<IHTMLWindow2>   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来执行脚本(事实上可以执行任意的脚本)。试验发现,这个方法非常有效,不仅窗口是模态的,而且不需要手动创建链接,更重要的是“允许脱机使用”和“自定义”按钮也可以用了。

  4、问题仍旧没有解决

  执行脚本的方式看起来有效,可一旦我们的程序实现了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对象找出来,虽然都没有成功,但是解决问题的方法却被我找到了。

  5、完美的方案

  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), 0×0), 0 );

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

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

关于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的“整理收藏夹”对话框

 

 

 

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

 

 

参考资料

MSDN: Adding Internet Explorer Favorites to Your Application

 

 

 

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

 

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

  除了打开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的方式打开文档,否则在独立窗口中打开。

  方法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”方可在独立的窗口中打开文档。

  参考资料:

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

  MSDN:162059:How to configure Internet Explorer to open Office documents in the appropriate Office program instead of in Internet Explorer

 

一直对Microsoft Internet Explorer编程非常感兴趣,曾花了不少时间琢磨,也与众多网友讨论过问题,2000年将心得写成一篇《TWebBrowser编程简述》,发表在自己的个人主页“阿甘的家”上,得到了不少网友的回应,也被许多网站转载。此后相当长的时间内不断回答网友的提问,收获良多。

  其间正是多窗口浏览器全面开花的日子,无奈手头事情太多,我的作品Multiple iExplorer也一直未能问世,至今遗憾。后来常与GoSurf的作者交流学习,替他解决了不少问题,也从他那里学到许多。如今GoSurf有了比较固定的用户群,有我一份功劳,算是一种安慰吧,他也一度在GoSurf官方主页上将我列为核心技术支持,但我后来因为工作和学习的关系很少再和他联系,实在惭愧。

  回头再看当时的文章,错误实在不少,认识也比较浅薄,有些问题更是一直没有得到解决,所以我觉得有必要在前文的基础上,花点时间将我积累的关于Internet Explorer编程的问题比较完整地写出来,希望对自己有个交代,对大家有一些帮助。

  是为序。

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

  你的机器上总是存在着“两”个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中过滤广告窗口功能的基础。