Год назад в нашей компании возникла задача написать на C# приложение для импорта данных из Excel, в том числе с помощью буфера обмена и drag'n'drop. Excel при копировании в буфер кладет туда данные в нескольких форматах. Часть из них стандартные типа CF_TEXT, CF_CSV и т.п. Однако, если нужно иметь дело с объединенными ячейками и прочими радостями, то может понадобиться получить доступ непосредственно к объекту Range, который был перетащен или скопипастен. Для этого нужно воспользоваться форматом CF_LINKSOURCE и лежащим в нем интерфейсом IMoniker. О том, как это сделать, читайте под хабракатом.
В теории, все просто как в сказке про Кащея: Range лежит в IMoniker, IMoniker в IStream, IStream в IDataObject, IDataObject в Clipboard. Именно так и будет выглядеть метод, вызываемый из клиентского кода:
Нюанс здесь только один: нам нужен не System.Windows.Forms.IDataObject, возвращаемый Clipboard.GetDataObject(), а System.Runtime.InteropServices.ComTypes.IDataObject. Благо, решается это простым приведением.
Получить IStream из IDataObject тоже несложно. Обратите внимание, что среди пары десятков форматов, помещенных в буфер обмена Excel'ем, нам нужен CF_LINKSOURCE.
Так, IStream получили. Теперь нужно вытащить из него IMoniker. Здесь обнаружился первый нюанс: IStream необходимо перемотать в начало. Иначе OleLoadFromStream вернет STG_E_READFAULT. Кстати, OleLoadFromStream надо импортировать из ole32.dll. Нам в этом поможет pinvoke.net. Единственное, в нашем коде заменим вовзращаемый ей результат с int на HRESULT, описанный там же.
Есть IMoniker! Дальше в теории всё должно было бы быть очень просто. Вызываем из ole32.dll функцию BindMoniker, в которую передаем IMoniker и Guid класса Range, и на выходе получаем Range. На самом деле однако, вместо этого мы получим ошибку MK_E_NOOBJECT. Дело здесь вот в чем. Моникеры бывают нескольких типов: File Moniker, Item Moniker, Composite Moniker и проч. В нашем случае из буфера обмена мы получаем моникер типа Composite. Который объединяет в себе два других — File и Item. Первый указывает на Workbook, а второй — на Range внутри Workbook. Формировать составные моникеры Excel, как мы видим, умеет. А вот разбирать — нет. Что ж, придется ему помочь.
С помощью SplitCompositeMoniker мы разбиваем Composite moniker на File moniker и Item moniker, после чего у файлового моникера просто вызываем BindToObject и получаем объект Workbook. А дальше делаем работу за Excel и получаем по Item moniker объект Range. Для этого мы написали хелпер. Код в статье я приводить не буду, вы можете посмотреть его в демо-проекте. По сути он парсит свойство DisplayName моникера, вытаскивает оттуда имя листа и границ выделенной области и получает по ним нужный Range из Workbook стандартными методами Microsoft.Office.Interop.Excel.
Тут и сказочке конец. А кто слушал — молодец и таким же образом сможет добраться до объектов любого приложения, которое кладет моникер в буфер обмена.
Демо-проект лежит здесь. Не смотря на то, что ссылка ведет на кодпроджект, статью я не скопипастил, как можно подумать =) Просто, зимой кармы на опубликование статьи здесь еще не хватало, и чтобы не забыть материал, я опубликовал его там.
Спасибо за внимание!
В теории, все просто как в сказке про Кащея: Range лежит в IMoniker, IMoniker в IStream, IStream в IDataObject, IDataObject в Clipboard. Именно так и будет выглядеть метод, вызываемый из клиентского кода:
public static Range GetRange()
{
IDataObject dataObject = System.Windows.Forms.Clipboard.GetDataObject() as IDataObject;
IStream iStream = IStreamFromDataObject(dataObject);
IMoniker compositeMoniker = IMonikerFromIStream(iStream);
return RangeFromCompositeMoniker(compositeMoniker);
}
Нюанс здесь только один: нам нужен не System.Windows.Forms.IDataObject, возвращаемый Clipboard.GetDataObject(), а System.Runtime.InteropServices.ComTypes.IDataObject. Благо, решается это простым приведением.
Получить IStream из IDataObject тоже несложно. Обратите внимание, что среди пары десятков форматов, помещенных в буфер обмена Excel'ем, нам нужен CF_LINKSOURCE.
private const string CF_LINKSOURCE_ID = "Link Source";
private static IStream IStreamFromDataObject(IDataObject dataObject)
{
STGMEDIUM medium;
FORMATETC formatEtc = new FORMATETC();
formatEtc.cfFormat = (short)System.Windows.Forms.DataFormats.GetFormat(CF_LINKSOURCE_ID).Id;
formatEtc.dwAspect = DVASPECT.DVASPECT_CONTENT;
formatEtc.lindex = -1;
formatEtc.ptd = new IntPtr(0);
formatEtc.tymed = TYMED.TYMED_ISTREAM;
dataObject.GetData(ref formatEtc, out medium);
return Marshal.GetObjectForIUnknown(medium.unionmember) as IStream;
}
Так, IStream получили. Теперь нужно вытащить из него IMoniker. Здесь обнаружился первый нюанс: IStream необходимо перемотать в начало. Иначе OleLoadFromStream вернет STG_E_READFAULT. Кстати, OleLoadFromStream надо импортировать из ole32.dll. Нам в этом поможет pinvoke.net. Единственное, в нашем коде заменим вовзращаемый ей результат с int на HRESULT, описанный там же.
private static IMoniker IMonikerFromIStream(IStream iStream)
{
iStream.Seek(0, 0, IntPtr.Zero);
Guid guid = Marshal.GenerateGuidForType(typeof(stdole.IUnknown));
object obj;
if (ole32.OleLoadFromStream(iStream, ref guid, out obj))
return obj as IMoniker;
else
return null;
}
Есть IMoniker! Дальше в теории всё должно было бы быть очень просто. Вызываем из ole32.dll функцию BindMoniker, в которую передаем IMoniker и Guid класса Range, и на выходе получаем Range. На самом деле однако, вместо этого мы получим ошибку MK_E_NOOBJECT. Дело здесь вот в чем. Моникеры бывают нескольких типов: File Moniker, Item Moniker, Composite Moniker и проч. В нашем случае из буфера обмена мы получаем моникер типа Composite. Который объединяет в себе два других — File и Item. Первый указывает на Workbook, а второй — на Range внутри Workbook. Формировать составные моникеры Excel, как мы видим, умеет. А вот разбирать — нет. Что ж, придется ему помочь.
private static Range RangeFromCompositeMoniker(IMoniker compositeMoniker)
{
List<IMoniker> monikers = SplitCompositeMoniker(compositeMoniker);
if (monikers.Count != 2)
throw new ApplicationException("Invalid moniker");
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;
ExcelItemMonikerHelper helper = new ExcelItemMonikerHelper(monikers[1], bindctx);
return helper.GetRange(workbook);
}
private static List<IMoniker> SplitCompositeMoniker(IMoniker compositeMoniker)
{
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");
}
С помощью SplitCompositeMoniker мы разбиваем Composite moniker на File moniker и Item moniker, после чего у файлового моникера просто вызываем BindToObject и получаем объект Workbook. А дальше делаем работу за Excel и получаем по Item moniker объект Range. Для этого мы написали хелпер. Код в статье я приводить не буду, вы можете посмотреть его в демо-проекте. По сути он парсит свойство DisplayName моникера, вытаскивает оттуда имя листа и границ выделенной области и получает по ним нужный Range из Workbook стандартными методами Microsoft.Office.Interop.Excel.
Тут и сказочке конец. А кто слушал — молодец и таким же образом сможет добраться до объектов любого приложения, которое кладет моникер в буфер обмена.
Демо-проект лежит здесь. Не смотря на то, что ссылка ведет на кодпроджект, статью я не скопипастил, как можно подумать =) Просто, зимой кармы на опубликование статьи здесь еще не хватало, и чтобы не забыть материал, я опубликовал его там.
Спасибо за внимание!