gis
gis
管理员
管理员
  • 注册日期2003-07-16
  • 发帖数15951
  • QQ
  • 铜币25345枚
  • 威望15368点
  • 贡献值0点
  • 银元0个
  • GIS帝国居民
  • 帝国沙发管家
  • GIS帝国明星
  • GIS帝国铁杆
阅读:1370回复:1

VC++中怎么VBA使用来操作WORD

楼主#
更多 发布于:2004-12-23 16:38
<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>
喜欢0 评分0
GIS麦田守望者,期待与您交流。
xbdragon
路人甲
路人甲
  • 注册日期2005-05-11
  • 发帖数21
  • QQ
  • 铜币142枚
  • 威望0点
  • 贡献值0点
  • 银元0个
1楼#
发布于:2005-05-11 14:42
<img src="images/post/smile/dvbbs/em01.gif" /><img src="images/post/smile/dvbbs/em01.gif" />
举报 回复(0) 喜欢(0)     评分
游客

返回顶部