阅读:1369回复:1
VC++中怎么VBA使用来操作WORD
<P>怎样从一个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 </P> |
|
|
1楼#
发布于:2005-05-11 14:42
<img src="images/post/smile/dvbbs/em01.gif" /><img src="images/post/smile/dvbbs/em01.gif" />
|
|