Pull to refresh

Получение через IMoniker объекта Range, скопированного из Excel в буфер обмена

Reading time5 min
Views2K
Год назад в нашей компании возникла задача написать на C# приложение для импорта данных из Excel, в том числе с помощью буфера обмена и drag'n'drop. Excel при копировании в буфер кладет туда данные в нескольких форматах. Часть из них стандартные типа CF_TEXT, CF_CSV и т.п. Однако, если нужно иметь дело с объединенными ячейками и прочими радостями, то может понадобиться получить доступ непосредственно к объекту Range, который был перетащен или скопипастен. Для этого нужно воспользоваться форматом CF_LINKSOURCE и лежащим в нем интерфейсом IMoniker. О том, как это сделать, читайте под хабракатом.

В теории, все просто как в сказке про Кащея: 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.

Тут и сказочке конец. А кто слушал — молодец и таким же образом сможет добраться до объектов любого приложения, которое кладет моникер в буфер обмена.
Демо-проект лежит здесь. Не смотря на то, что ссылка ведет на кодпроджект, статью я не скопипастил, как можно подумать =) Просто, зимой кармы на опубликование статьи здесь еще не хватало, и чтобы не забыть материал, я опубликовал его там.

Спасибо за внимание!
Tags:
Hubs:
Total votes 30: ↑22 and ↓8+14
Comments0

Articles