在当前的项目开发中,团队正在致力于一个名为Converter的项目,该项目的核心功能是将Excel中的数据导入到应用程序中。用户需要从Excel中选择或复制/粘贴特定的单元格范围到Converter,然后设置一个数据传输方案。这个方案定义了数据从Excel的哪个单元格传输到应用程序的哪个目标位置,以及是否包含字段名等信息。最终,用户将得到一个可以用于从任意数量的Excel工作簿中传输数据的方案(当然,这些工作簿必须具有相同的布局)。创建这样的方案需要了解单元格范围布局、单元格名称等信息,因此需要Range对象来获取这些信息。本文将介绍如何通过使用CF_LINKSOURCE剪贴板格式的IStream接口来实现这一点。
主要算法非常简单,类似于示例代码中的GetRange方法:
public static Range GetRange(IDataObject dataObject)
{
IStream iStream = IStreamFromDataObject(dataObject);
IMoniker compositeMoniker = IMonikerFromIStream(iStream);
return RangeFromCompositeMoniker(compositeMoniker);
}
不要与IDataObject混淆,这里指的是System.Runtime.InteropServices.ComTypes.IDataObject,而不是System.Windows.Forms.IDataObject。
IStreamFromDataObject很简单,这里将跳过。要从IStream中获取IMoniker,需要调用ole32.dll中的P/Invoked函数OleLoadFromStream。
[DllImport("ole32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern HRESULT OleLoadFromStream(
IStream pStm,
[In] ref Guid riid,
[MarshalAs(UnmanagedType.IUnknown)] out object ppvObj);
HRESULT是一个结构体,它来自于pinvoke.net。将上一步中的流传递给OleLoadFromStream,然后...出错了!经过一段时间的调试,发现需要将流重置!好的,让这样做。
iStream.Seek(0, 0, IntPtr.Zero);
然后,从OleLoadFromStream获取对象,并且成功地将其转换为IMoniker。
如果查看moniker的CLSID,将看到它是一个复合moniker。如果生活在一个完美的世界里,moniker.BindToObject()将给Range对象。但在现实生活中,来自Microsoft Office的moniker只能绑定到文件对象(在Excel的情况下是Workbook),所以需要分割复合moniker并做一些工作。
private static List SplitCompositeMoniker(IMoniker compositeMoniker)
{
if (compositeMoniker == null)
throw new ArgumentNullException("compositeMoniker", "compositeMoniker is null.");
List monikerList = new List();
IEnumMoniker enumMoniker;
compositeMoniker.Enum(true, out enumMoniker);
if (enumMoniker != null)
{
IMoniker[] monikerArray = new IMoniker[1];
IntPtr fetched = new IntPtr();
HRESULT res;
while ((res = enumMoniker.Next(1, monikerArray, fetched)) == 0)
{
monikerList.Add(monikerArray[0]);
}
return monikerList;
}
else
throw new ApplicationException("IMoniker is not composite");
}
现在得到了一个包含文件moniker和项目moniker的列表。要绑定到Workbook,只需要调用IMoniker.BindToObject:
IBindCtx bindctx;
if (!ole32.CreateBindCtx(0, out bindctx) || bindctx == null)
throw new ApplicationException("Can't create bindctx");
object obj;
Guid workbookGuid = Marshal.GenerateGuidForType(typeof(Workbook));
monikers[0].BindToObject(bindctx, null, ref workbookGuid, out obj);
Workbook workbook = obj as Workbook;
但是调用项目moniker的BindToObject会出错(实际上,可以成功调用BindToObject与IUnknown的IID,但返回的对象实际上是Workbook对象,这是令人悲伤但真实的事实)。游戏结束了吗?不是那么快。仍然可以从项目moniker的IMoniker.GetDisplayName()获取显示名称。对于Excel Range,它将是类似于"!blahblahblah!R1C1:R3C3"的东西,其中blahblahblah是工作表名称,R1C1:R3C3标识工作表内的区域。写了一个辅助类来解析DisplayName并从Workbook对象中获取Range。唯一有趣的事情是:
ExcelItemMonikerHelper helper = new ExcelItemMonikerHelper(monikers[1], bindctx);
Range range = helper.GetRange(workbook);