发信人: wenbobo(事了拂衣去) 
整理人: wenbobo(2002-12-06 23:33:10), 站内信件
 | 
 
 
翻译得好辛苦!但是因为从来没有用过MFC(虽然VC用了很久)了,所以看不明白也不要对我吼! 
 
 Microsoft Word的自动化和事件 
 
 来源:codeguru 
 原文件张贴于:1999年2月20日 
 本文原作者:Christian Staffe([email protected]) 
 翻译:wenbobo([email protected]) 
 
 
 在这文章中,我示范几个在MFC中有关Word automation的主题: 
 
 怎样从一个MFC客户程序通过#import汇编指示来使用automation. 
 
 怎样使用由#import所产生的包装功能简单地添加一个新文档,并使Word把它显示给 
 用户. 
 
 怎样理解和使用由封装的类所产生的异议. 
 
 怎样使用相关技术在你的程序的客户区上建立一个接收器界面,以捕获Word所产 
 生的应用程序和用文档事件. 
 
 怎样把你的接收器界面连接到Word中的相关技术. 
 
 怎样找回Word文档中的内置文件属性. 
 
 
 这里提出的几乎所有的题目都能很容易的扩展到任何应用程序,使自动化界面和它的 
 类库显露出来. 
 
 完整例子是一个ZIP压缩格式的VC编写的基于对话的MFC程序.如果你运行它,将出现 
 一个有二个按纽的对话框:"Run Word"和"Cancel".如果你按下"Run Word",Word的 
 一个实例将被启动并且创建一个新文件.你可以通过直接退出Word和发送新文件命令 
 来进行实验.注意到,这时事件操作句柄显示的消息框出现在屏幕上,Word不会有任 
 何响应(事件是同步产生的),所以在你能继续进行下去之前,你不得不回答消息框。 
 当收到关闭文件的事件时,文档的页数被算出并在信息框上显示出来(在退出Word之 
 前,只需要按一下Ctrl Enter就可以在Word中插入新的页,你可以看到页数的的增 
 加).当你退出Word的时候,事件将被堵塞,程序终止 .如果你在Word已经被开始之后 
 按下"Cancel"按纽,程序将在自己结束之前关闭Word. 
 
 自动化信息能在下列的微软知识库文章中找出: 
 Q181845 : Create a Sink Interface in MFC-Based COM client  
 Q183599 : Catch Microsoft Word97 Application Events Using VC++  
 Q152087 : Connpts.exe Implements Connection Poitns in MFC Apps  
 Q179494 : Use Automation to Retrieve Built-in Document Properties  
 Q183369 : Use Automation to Run a Word 97 Macro with Arguments  
 
 这些例子相当老了,不很全面(至少对于我来说), 不是面向对象的并且没有使用新 
 的#import指示,但是比MFC的ColeDispatchDriver类要好多了.我希望我能成功的 
 把这些例子讲清楚.但是,我不会解释所有的COM 材料和相关的技术,已经有很多这 
 方面的文章了. 
 
 现在,废话少说,该来一点代码了 (好,好,再过几行,我保证)! 
 
 首先你需要把Word97安装在你的机器上,找到类库.对于Word97,它已把安装在 
 C:\Program Files\MicrosoftOffice\Office(查找msword8.olb).此外,你需要 
 mso97.dll和vbeext1.olb(可以在C:\Program Files\CommonFiles\Microsoft Shared\VBA 
 中找到).如果你问为什么除了Word97类库外还需要这两个额外的二进制 
 文件,看看#import指示符所产生的msword8.tlh文件(后面有更多这方面的内容), 
 你将看见一个注释信息,告诉你这些是msword8.olb所需要的交叉类型库。现在你已 
 经有了所有这些文件了,这里是封装的类的代码. 
   
 
 #pragma warning (disable:4146) 
 #import "mso97.dll" 
 #pragma warning (default:4146) 
 #import "vbeext1.olb" 
 #import "msword8.olb" rename("ExitWindows", "WordExitWindows") 
 
 你需要用pragma编译指示来避免由office97所产生的警告信息.编译器将为每一个 
 #import产生两个文件(扩展名为tlh和tli).感谢自动完成申明代码的功能,使用 
 Visual Studio 6.0, 你实际上并不需要看这些封装代码。我们可以这么认为,这 
 些类只是围绕在Word提供的界面周围的聪明的指针. 
 
 既然你已经有了这些封装好的类,你就可以把Word作为一自动化服务器来启动,用下 
 面的代码类添加一个新文件并让Word显示出来: 
 
 Word::_ApplicationPtr m_pWord; 
 Word::_DocumentPtr m_pDoc; 
 
 try 
 { 
 HRESULT hr = m_pWord.CreateInstance(__uuidof(Word::Application)); 
 ASSERT(SUCCEEDED(hr)); 
 
 m_pDoc = m_pWord->Documents->Add(); 
 m_pWord->Visible = VARIANT_TRUE; 
 } 
 catch (_com_error& ComError) 
 { 
 DumpComError(ComError); 
 } 
 
 void DumpComError(const _com_error& e) 
 { 
 CString ComErrorMessage;  
 ComErrorMessage.Format("COM Error: 0x%08lX. %s",e.Error(), e.ErrorMessage()); 
 AfxMessageBox(ComErrorMessage); 
 } 
 
 
 很简单,不是吗? 你首先申明两个聪明的指针:一个是_Application接口,另一个是 
 _Document接口. #import指示已在最后添加了Ptr来指出这是指针. __uuidof允许 
 你找回Word.Application对象的CLSID,该对象是对Word对象模块的“入口点”(不 
 要和_Application混淆,它是coclass Application的接口).通过可视化的属性( 
 感谢__declspec(property) 指示符,使这种类似于VB的属性机制建立起来),你 
 可以很容易的新建文档并用Word显示出来。 
 
 
 你可能已注意到try/catch块了.你调用的封装函数把COM的错误从 HRESULT 转换为 
 _com_error类型的异常(当然你也可以调用封装所提供的函数来避免这个) 。你可 
 以显示出这个错误值和用DumpComError函数得到的“体贴用户的”错误信息。 
 
 好了,完成了头三个题目.现在,让我们潜心钻研更有趣的部分:联接点和事件. 事件 
 既可以在应用程序层也可以在文档层(参看tli文件或者用OLE/COM对象浏览器观察 
 ApplicationEvents和DocumentEvents的对外接口 )由Word产生。在  
 ApplicationEvents 接口中有3个方法(Startup, Quit 和 DocumentChange), 
 在 DocumentEvents 接口中也有3个方法(New, Open 和 Close)。 在你能够用 
 MFC程序捕获事件之前,你得先增加一个用以接收事件和连接到Word的接收器接口。 
 
 首先,添加一个从CCmdTarget(安装自动化的检查框)继承来的新的类CWordEventSink。 
 我们将把这个类作为应用程序事件和文档事件的接收器。这里是头文件(不包含与 
 讨论无关的代码)。 
 
   
 const IID IID_IWordAppEventSink = __uuidof(Word::ApplicationEvents);  
 const IID IID_IWordDocEventSink = __uuidof(Word::DocumentEvents);  
   
 class CWordEventSink : public CCmdTarget  
 {  
 public:  
 CWordEventSink();  
 virtual ~CWordEventSink();  
 protected:  
   
 // Generated OLE dispatch map functions  
 //{{AFX_DISPATCH(CWordEventSink)  
 afx_msg void OnAppStartup();  
 afx_msg void OnAppQuit();  
 afx_msg void OnAppDocumentChange();  
 afx_msg void OnDocNew();  
 afx_msg void OnDocOpen();  
 afx_msg void OnDocClose();  
 //}}AFX_DISPATCH  
 };   
 
 
 的确也不太复杂. 仅仅看到哪些要被由CCmdTarget类发送的,在头文件中定义好的 
 消息所调用的方法。这里是一部分源文件: 
 
   
 BEGIN_DISPATCH_MAP(CWordEventSink, CCmdTarget)  
 //{{AFX_DISPATCH_MAP(CWordEventSink)  
 DISP_FUNCTION(CWordEventSink, "Startup",OnAppStartup,VT_EMPTY, VTS_NONE)  
 DISP_FUNCTION(CWordEventSink, "Quit",OnAppQuit,VT_EMPTY, VTS_NONE)  
 DISP_FUNCTION(CWordEventSink, "DocumentChange", OnAppDocChange,VT_EMPTY, VTS_NONE)  
 DISP_FUNCTION(CWordEventSink, "New",OnDocNew,VT_EMPTY, VTS_NONE)  
 DISP_FUNCTION(CWordEventSink, "Open",OnDocOpen,VT_EMPTY, VTS_NONE)  
 DISP_FUNCTION(CWordEventSink, "Close",OnDocClose,VT_EMPTY, VTS_NONE)  
 //}}AFX_DISPATCH_MAP  
 END_DISPATCH_MAP()  
   
 BEGIN_INTERFACE_MAP(CWordEventSink, CCmdTarget)  
 INTERFACE_PART(CWordEventSink, IID_IWordAppEventSink, Dispatch)  
 INTERFACE_PART(CWordEventSink, IID_IWordDocEventSink, Dispatch)  
 END_INTERFACE_MAP()  
   
 void CWordEventSink::OnAppQuit()   
 {  
 AfxMessageBox("AppQuit event received");  
 }  
 
 
 消息映射的前3项是关于ApplicationEvents接口的,下3项是有关DocumentEvents 
 接口的。注意这里有一个class Wizard的小窍门:DISP_FUNCTION连续地使用从1开 
 始的dispids,刚好和Word产生的事件相匹配(你可以从tlb中得到验证,例如 
 Startup 的dispids是1,New的dispid为4)。如果没有匹配,你就应该这样定义: 
 
   
 DISP_FUNCTION_ID(CWordEventSink, "Quit", 0x02, OnAppQuit, VT_EMPTY, VTS_NONE)  
 
 很不幸,看来class wizard不支持这种写法. 令人震惊! 
 
 你仅仅需要一个CWordEventSink类的实例,但是你还是不会收到事件,因为你仍然需 
 要把你的接收器类和Word联接起来(Word怎么会知道你想接收这些事件呢)。这一般使 
 用AfxConnectionAdvise和AfxConnectionUnadvise函数完成,但是我打算介绍另外 
 一种面向对象的方法来做。和一个接收器连接和断开的基本功能封装在一个 
 CConnectionAdvisor类中,这里是头文件: 
 
 class CConnectionAdvisor    
 {  
 public:  
 CConnectionAdvisor(REFIID iid);  
 BOOL Advise(IUnknown* pSink, IUnknown* pSource);  
 BOOL Unadvise();  
 virtual ~CConnectionAdvisor();  
   
 private:  
 CConnectionAdvisor();  
 CConnectionAdvisor(const CConnectionAdvisor& ConnectionAdvisor);  
 REFIID m_iid;  
 IConnectionPoint* m_pConnectionPoint;  
 DWORD m_AdviseCookie;  
 };  
 
 
 构造函数参考了你需要连接的接口(在这个例子里是IID_IWordAppEventSink或者 
 IID_IWordDocEventSink)。当你需要把你的接收器连接到源(这里就是Word)的 
 给定的接口时你要调用Advise。Advise的实现代码非常象AfxConnectionAdvise 
 但是我们保留了一个指向IConnectionPoint接口的指针以便于Unadvise更加容易的 
 实现。如果你忘记了断开,析构函数将进行处理,这是实现部分: 
 
 
 CConnectionAdvisor::CConnectionAdvisor(REFIID iid) : m_iid(iid)  
 {  
 m_pConnectionPoint = NULL;  
 m_AdviseCookie = 0;  
 }  
   
 CConnectionAdvisor::~CConnectionAdvisor()  
 {  
 Unadvise();  
 }  
   
 BOOL CConnectionAdvisor::Advise(IUnknown* pSink, IUnknown* pSource)  
 {   
 // Advise already done   
 if (m_pConnectionPoint != NULL)  
 {  
 return FALSE;  
 }  
   
 BOOL Result = FALSE;  
   
 IConnectionPointContainer* pConnectionPointContainer;  
   
 if (FAILED(pSource->QueryInterface(  
 IID_IConnectionPointContainer,  
 (void**)&pConnectionPointContainer)))  
 {  
 return FALSE;  
 }  
   
 if (SUCCEEDED(pConnectionPointContainer->FindConnectionPoint(m_iid, &m_pConnectionPoint)))  
 {  
 if (SUCCEEDED(m_pConnectionPoint->Advise(pSink, &m_AdviseCookie)))  
 {  
 Result = TRUE;  
 }  
 else  
 {  
 m_pConnectionPoint->Release();  
 m_pConnectionPoint = NULL;  
 m_AdviseCookie = 0;  
 }  
 }  
 pConnectionPointContainer->Release();  
 return Result;  
 }  
   
 BOOL CConnectionAdvisor::Unadvise()  
 {   
 if (m_pConnectionPoint != NULL)  
 {  
 HRESULT hr = m_pConnectionPoint->Unadvise(m_AdviseCookie);  
 // If the server is gone, ignore the error  
 // ASSERT(SUCCEEDED(hr));  
 m_pConnectionPoint->Release();  
 m_pConnectionPoint = NULL;  
 m_AdviseCookie = 0;  
 }  
 return TRUE;  
 }  
 
 
 几乎完美了!当然,CWordEventSink有一个CConnectionAdvisor的实例是很自然的. 
 当我设计接收器以处理两个对外的接口时,在我的CWordEventSink类里面插入两个 
 CConnectionAdvisor对象,就象这样: 
 
   
 class CWordEventSink : public CCmdTarget  
 {  
 // Some code already presented is deleted  
   
 public:  
 BOOL Advise(IUnknown* pSource, REFIID iid);  
 BOOL Unadvise(REFIID iid);  
   
 private:  
 CConnectionAdvisor m_AppEventsAdvisor;  
 CConnectionAdvisor m_DocEventsAdvisor;  
 };  
 
 
 这里有两个新的函数Advise和Unadvise以及新的CwordEventSink构造函数: 
 
   
 CWordEventSink::CWordEventSink() :  
 m_AppEventsAdvisor(IID_IWordAppEventSink),   
 m_DocEventsAdvisor(IID_IWordDocEventSink)  
 {  
 EnableAutomation();  
 }  
   
 BOOL CWordEventSink::Advise(IUnknown* pSource, REFIID iid)  
 {  
 // This GetInterface does not AddRef  
 IUnknown* pUnknownSink = GetInterface(&IID_IUnknown);  
 if (pUnknownSink == NULL)  
 {  
 return FALSE;  
 }  
   
 if (iid == IID_IWordAppEventSink)  
 {  
 return m_AppEventsAdvisor.Advise(pUnknownSink, pSource);  
 }  
 else if (iid == IID_IWordDocEventSink)  
 {  
 return m_DocEventsAdvisor.Advise(pUnknownSink, pSource);  
 }  
 else   
 {  
 return FALSE;  
 }  
 }  
   
 BOOL CWordEventSink::Unadvise(REFIID iid)  
 {  
 if (iid == IID_IWordAppEventSink)  
 {  
 return m_AppEventsAdvisor.Unadvise();  
 }  
 else if (iid == IID_IWordDocEventSink)  
 {  
 return m_DocEventsAdvisor.Unadvise();  
 }  
 else   
 {  
 return FALSE;  
 }  
 }  
 
 
 什么时候你需要advise或者unadvise时,你必须指定你需要连接的源和接口.Advise 
 方法看起来有点象QueryInterface的实现,因为它必需把特定的接口映射到类里面的 
 某个CConnectionAdvisor。注意这可以用一些类似于MFC的maps和macros来完成。 
 
 现在,是看看连接你的事件的代码的时候了.这段代码应该加在在你创建了你的Word 
 实例并且新建了文档(见前面的叙述)之后。 
 
 CWordEventSink  m_WordEventSink  
   
 BOOL Res = m_WordEventSink.Advise(m_pWord, IID_IWordAppEventSink);  
 ASSERT(Res == TRUE);  
   
 Res = m_WordEventSink.Advise(m_pDoc, IID_IWordDocEventSink);  
 ASSERT(Res == TRUE);  
 
 然而,某些事件有些有趣的事情:例如你永远都收不到Startup事件,因为在你有机会 
 把你的接收器连接到ApplicationEvents接口之前Word就已经产生这个事件了。看来 
 在你可以把接收器连接到DocumentEvents接口之前,当你需要一个文档接口时同样的 
 事情也发生在文档事件上。这时候New和Open事件已经产生了,所以只有DocumentChange, 
 Quit和Close事件可以捕获到。 
 
 现在为最后一件事准备好:找回在Word中内置的文件属性.这是很有趣的因为封装类将 
 返回给你一个IDispatch指针,指向一个VB对象,你得在这个对象里面找到属性。作为 
 一个例子,我将提供找到文档中页的数目的方法.异常处理就不再重复了。 
 
   
 DWORD PageCount;  
 IDispatchPtr pDispatch(m_pWord->ActiveDocument->BuiltInDocumentProperties);  
 ASSERT(pDispatch != NULL);  
   
 // this pDispatch will be released by the smart pointer, so use FALSE    
 COleDispatchDriver DocProperties(pDispatch, FALSE);  
 _variant_t Property((long)Word::wdPropertyPages);  
 _variant_t Result;  
   
 // The Item method is the default member for the collection object  
 DocProperties.InvokeHelper(DISPID_VALUE,   
    DISPATCH_METHOD | DISPATCH_PROPERTYGET,   
    VT_VARIANT,  
    (void*)&Result,  
    (BYTE*)VTS_VARIANT,  
    &Property);  
 // pDispatch will be extracted from variant Result  
 COleDispatchDriver DocProperty(Result);  
 // The Value property is the default member for the Item object  
 DocProperty.GetProperty(DISPID_VALUE, VT_I4, &PageCount);  
 // The page count is now in PageCount    
 
 
 首先你调用BuiltInDocumentProperties方法得到一个IDispatch指针,指向文件属性 
 对象.你不会从#import里得到更多的帮助,但是你可以使用COleDispatchDriver类完 
 成这个任务.你需要得到的实际上就是对象里的“Item(wdPropertyPage).Value”。 
 首先用你得到的IDispatch指针建立第一个COleDispatchDriver.对象有一个成员叫Item, 
 它时collection对象的缺省成员,因此你可以在InvokeHelper调用中使用DISPID_VALUE。 
 你还必须给出你需要获得的属性作为参数,这样,你会得到一个包含一个你需要的 
 IDispatch的新的变量。用IDispatch创建一个新的COleDispatchDriver,调用它的 
 GetProperty方法就得到了页面数目。因为Value是缺省的成员,你可以再用一次 
 DISPID_VALUE。得意的事情是COleDispatchDriver, _variant_t或者IDispatchPtr 
 作了大量的自动类型转换工作并且会释放所有的东西。 
   
 Happy Automation。 
 
 
 下载(32千字节): 
 http://codeguru.earthweb.com/atl/wordauto.zip
 
 
 
 
 【 在 xu_ying 的大作中提到:】
 :如题...
 :......
  
 
  ---- ◢█◣◢█◣
 ◤◥◢█◤◥
 ◣◢█◤◣◢
 ◥█◤◥█◤   | 
 
 
 |