通过 IStream 接口从剪贴板获取 Excel Range 对象






4.86/5 (6投票s)
本文演示了如何使用 CF_LINKSOURCE 剪贴板格式从剪贴板获取 Excel Range 对象。
引言
当您将选定的单元格从 Excel 复制到剪贴板时,它会包含多种数据格式。 如果您只需要粘贴数据,那么即使 CF_CSV
也可以满足要求。 但是,如果您想要访问表示复制单元格的 Range
对象该怎么办? 使用 CF_LINKSOURCE
!
背景
在我们公司,我们目前正在开发一个名为 Converter 的项目,该项目将数据从 Excel 导入到我们的应用程序中。 用户必须将 Excel 中的一些单元格范围拖动或复制/粘贴到 Converter 中,并设置一个数据传输方案(从哪个单元格传输到哪个目标数据;第一行数据是否包含字段名称等)。 最后,用户得到一个可以用来传输来自任意数量 Excel 工作簿的数据的方案(当然,工作簿必须具有相同的布局)。 创建这样的方案需要一些关于 Range 布局、单元格名称等的信息,因此您需要 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
。
内部观察
获取 IMoniker
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
的对象。
获取 Range
如果您查看 moniker 的 CLSID,您会发现它是一个复合 moniker。 如果我们生活在一个完美的世界里,moniker.BindToObject()
会给我们 Range
对象。 但在现实生活中,来自 Microsoft Office 的 moniker 只能绑定到文件对象(Excel 中的 Workbook
),因此我们需要拆分复合 moniker 并为 Excel 做一些工作。
private static List<IMoniker> SplitCompositeMoniker(IMoniker compositeMoniker)
{
if (compositeMoniker == null)
throw new ArgumentNullException("compositeMoniker",
"compositeMoniker is null.");
List<IMoniker> monikerList = new List<IMoniker>();
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))
{
monikerList.Add(monikerArray[0]);
}
return monikerList;
}
else
throw new ApplicationException("IMoniker is not composite");
}
现在我们得到了包含文件 moniker 和项目 moniker 的 List
。 要绑定到 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
会产生一个错误(实际上,我们可以使用 IUnknown
的 IID 成功调用 BindToObject
,但返回的对象实际上将是 Workbook 对象,可悲但真实。) 游戏结束了吗? 没那么快。 我们仍然可以使用 IMoniker.GetDisplayName()
从项目 moniker 获取显示名称。 对于 Excel Range
,它将类似于“!blahblahblah!R1C1:R3C3”,其中 blahblahblah 是工作表名称,R1C1:R3C3 标识工作表内的范围。 我编写了一个辅助类来解析 DisplayName
并从 Workbook
对象获取 Range
。 唯一有趣的时刻是
- 用户可以复制整行或整列,分别标识为“Rx:Ry”和“Cx:Cy”。
- 您必须将 R1C1 名称转换为当前的 Excel 引用样式(主要是 A1)才能获取 Range。
辅助工具可以按如下方式使用
ExcelItemMonikerHelper helper = new ExcelItemMonikerHelper(monikers[1], bindctx);
Range range = helper.GetRange(workbook);
所以最后,我们获得了 Range
,就像项目 moniker 的 BindToObject
真正起作用一样 :)