前些天接到一个需求,通过程序要获取word文档的摘要信息。大家应该都知道这些信息其实就是在word文档上右键属性里面有一个摘要页,摘要里的信息都是word文档的内部信息而不是简单的windows系统通用文件信息。不过接到这个需求之初我也是认为可以通过通用文件操作解决问题,然后查了些文件属性和shell方面的东西,结果是碰了一鼻子灰。
通用的路子没有走通那就换个方向,既然是word文档自己的信息看看word的com接口有没有提供获取这些信息的方法。方向对了果然事半功倍,经过一番查找我找到了我需要的函数GetBuiltInDocumentProperties,顾名思义这个函数就是用来获取word文档的内建信息的,这部分信息也就是摘要的内容。有了明确的目标事情就好办了,上MSDN看看有没有相关的使用说明,果然找到一篇《如何使用自动化检索内置文档属性》。有兴趣的朋友可以看看这篇文章,我按着文章中的介绍实现了我的需求。但是在寻找解决方案的整个过程中我发现网上关于这个问题的介绍比较少,而且MSDN的这篇文章本身也存在一定的问题,因此我觉得应该把我小小的研究成果拿出来和大家分享一下,也希望在这方面有更多经验的朋友可以帮助完善关于这个问题的解决方案。
在word文档的属性中有这样一个属性BuiltInDocumentProperties,根据Properties这个复数形式可以很容易的判断BuiltInDocumentProperties是一个属性集合,它包含了所有word文档的内置信息。类似的属性集在excel文档属性中也可以找到,关于word的BuiltInDocumentProperties属性集的详细介绍可以参阅MSDN中关于DocumentBase.BuiltInDocumentProperties 属性的介绍。通过MSDN的这篇文章我们并不能知道属性集中都包括那些属性,但是我们可以掌握一个信息,那就是属性是可以通过下标访问的,而这个下标被定义成了枚举类型WdBuiltInProperty,在MSDN中不难找到关于WdBuiltInProperty Enumeration的详细介绍,我们可以看到BuiltInDocumentProperties中包含30个属性,而且MSDN明确指出如果Microsoft Office Word没有为某个内置文档属性定义值,则在读取该文档属性的Value属性时将引发异常。而且MSDN中也说明,如果想知道文档有什么属性可以先行通过某种方式查看文档属性。了解到这我们应该可以读懂微软的示例代码了,而且我们也应该知道实际编程时应该注意那些问题。
下面我们分析一下《如何使用自动化检索内置文档属性》一文中微软提供的代码示例。这段代码的开头是通过com接口调用word的标准做法,这里我就不多说了。有一点我提一下,示例代码中Open函数用了16个参数,对应的应该是2003。我具体实现时用的导出文件是通过VC6.0基于word 2000制作的,所以Open函数是12个参数。而更早的版本这里只需10参数,不过我没有用过。从总体上看,微软的例子是打开一个word文档,然后将这个文档的全部内置信息添加到文档的开头部分。所以代码中有大量和word文档操作相关的代码。忽略那些我们不关心的语句,lpDisp = objDoc.GetBuiltInDocumentProperties(); 这句代码真正完成了获取内置属性组的功能。然后通过COleDispatchDriver变量获取属性集的整体信息,第一步是获取属性的数量,调试一下很容易看出这个值和我们之前掌握到的信息是一致的,是30。接下来是一个循环,还是通过COleDispatchDriver变量分别获取每一个属性的名字和值然后根据属性值的不同类型有针对性的将这些值连接成字符串。
应该说这段代码整个的思路很清晰,而且也很规范。代码中用了异常处理来保证程序的健壮性,当然我们前面也了解到了如果某个属性值没有定义则读取时将引发异常,所以合理的处理异常才能完整的遍历每一个可访问的属性。针对这一问题微软这段代码是通过goto语句实现的,我在实际编译过程中这里没有通过,说是不能跳入try语句中。而且有经验的程序员也都知道,在实际编程中应该尽量避免使用goto语句,因为它会使代码难于维护。对于这个例子使用goto语句倒是无可厚非,但是如果我们要在实际项目中应用这段代码那就要慎重考虑了,这也就是我说微软提供的解决方案存在一定问题的主要原因。
基于以上考虑,我在这段示例代码的基础上制作了一个小例子。下图是一个执行效果:
图中我们可以看到这个例子非常简单,通过浏览按钮定位一个word文档,然后通过获取摘要按钮就可以读到此文档的摘要信息。我简单说一下我做的改动,首先很明显的是我没有把信息写回到word中,这样就去掉了源代码中关于word的操作,也使主题功能代码更紧凑。同时使用ListCtrl显示摘要信息更贴近文件属性中摘要页的形式,感觉更直观一些。然后我是通过在循环体内部处理异常的方式实现对所有属性的遍历,这样就避免了goto语句的使用,使代码更易维护。下面给出获取摘要按钮的全部代码,有兴趣的朋友也可以下载完整示例
_Application objWordApp;
_Document objDoc;
LPDISPATCH lpDisp;
// Common OLE variants that are easy to use for calling arguments.
COleVariant covTrue((short)TRUE),
covFalse((short)FALSE),
covOptional((long)DISP_E_PARAMNOTFOUND, VT_ERROR);
if(!objWordApp.CreateDispatch(_T("Word.Application")) )
{
AfxMessageBox(_T("无法启动Word,可能并未安装!"));
return;
}
Documents docs(objWordApp.GetDocuments());
lpDisp = docs.Open(COleVariant(m_strDocFile, VT_BSTR), covFalse, covTrue, covFalse, covOptional, covOptional, covFalse, covOptional, covOptional, covOptional, covOptional, covFalse);
objDoc.AttachDispatch(lpDisp);
COleDispatchDriver oleDispInfo, oleDispItem;
DISPID dispID, dispID2;
wchar_t* wcPtr; // Temporary name holder for
VARIANT vtResult; // Holds results from
VARIANT vtResult2; // Holds result for 'Type'.
BYTE* parmStr; // Holds parameter descriptions
lpDisp = objDoc.GetBuiltInDocumentProperties();
oleDispInfo.AttachDispatch(lpDisp); // LPDISPATCH returned from
// GetBuiltInDocumentProperties.
VARIANT i; // integer;
VARIANT count; // integer;
CString strName, strValue;
wcPtr = L"Count"; // Collections have a Count
try
{
oleDispInfo.m_lpDispatch->GetIDsOfNames(IID_NULL, (LPOLESTR*)&wcPtr, 1, LOCALE_USER_DEFAULT, &dispID);
oleDispInfo.InvokeHelper(dispID, DISPATCH_METHOD|DISPATCH_PROPERTYGET, VT_VARIANT, (void*)&vtResult, NULL);
}
catch(...)
{
AfxMessageBox(_T("获取属性集大小失败"));
return;
}
count = vtResult; // Require a separate variable for loop limiter.
// For i = 1 to count,
// get the Item, Name & Value members of the collection.
i.vt = VT_I4;
for(i.lVal=1; i.lVal<=count.lVal; i.lVal++)
{
wcPtr = L"Item"; // Collections have a Count
try
{
oleDispInfo.m_lpDispatch->GetIDsOfNames(IID_NULL, (LPOLESTR*)&wcPtr, 1, LOCALE_USER_DEFAULT, &dispID);
parmStr = (BYTE*)(VTS_VARIANT);
oleDispInfo.InvokeHelper(dispID, DISPATCH_METHOD|DISPATCH_PROPERTYGET, VT_VARIANT, (void*)&vtResult, parmStr, &COleVariant(i));
// Get the Name member for the Item.
oleDispItem.AttachDispatch(vtResult.pdispVal);
wcPtr = L"Name"; // Collection has a Name member
oleDispItem.m_lpDispatch->GetIDsOfNames(IID_NULL, (LPOLESTR*)&wcPtr, 1, LOCALE_USER_DEFAULT, &dispID);
oleDispItem.InvokeHelper(dispID, DISPATCH_METHOD|DISPATCH_PROPERTYGET, VT_VARIANT, (void*)&vtResult, NULL);
wcPtr = L"Value"; // Collection has a Value member.
oleDispItem.m_lpDispatch->GetIDsOfNames(IID_NULL, (LPOLESTR*)&wcPtr, 1, LOCALE_USER_DEFAULT, &dispID2);
oleDispItem.InvokeHelper(dispID2, DISPATCH_METHOD|DISPATCH_PROPERTYGET, VT_VARIANT, (void *)&vtResult2, NULL);
oleDispItem.ReleaseDispatch();
}
catch(...)
{
continue;
}
switch(vtResult2.vt) // Type of property
{
case VT_BSTR:strValue = vtResult2.bstrVal;break;
case VT_DATE:
{
COleDateTime oleDateTime(vtResult2.date);
strValue.Format(_T("%d-%d-%d %d:%02d"), oleDateTime.GetYear(), oleDateTime.GetMonth(), oleDateTime.GetDay(), oleDateTime.GetHour(), oleDateTime.GetMinute());
}
break;
case VT_I4:strValue.Format(_T("%ld"), vtResult2.lVal);break;
default:break;
}
int nItem = m_listInfo.GetItemCount();
m_listInfo.InsertItem(nItem, vtResult.bstrVal);
m_listInfo.SetItemText(nItem, 1, strValue);
}
// Release IDispatch pointers on local objects.
if(vtResult.vt == VT_DISPATCH)
vtResult.pdispVal->Release();
if(count.vt == VT_DISPATCH)
count.pdispVal->Release();
oleDispInfo.ReleaseDispatch();
objWordApp.Quit(covFalse, covFalse, covFalse);
}
最后再补充一点,微软的文档中也提到了(最后的注释部分),如果只是提取一个属性信息那没有必要全部遍历,按照相应的下标直接操作即可。另外寻找解决方案的初期我还找到一篇《如何读取直接使用 VC++的复合文档属性》,这篇文章介绍的方法可以脱离word实现文件信息获取,不过这篇文章的方法相对复杂,我当时急于实现需求所以没有深入研究。这篇文章的代码我倒是编译了,没发现什么问题可以直接使用,有兴趣的朋友可以自行研究一下。如果有什么心得也希望能分享出来,让我们共同提高。