Получение Excel.Application IDispatch* в dll, загруженной в Excel

Кто-нибудь знает, как получить указатель Excel.Application IDispatch*, связанный с процессом excel, в который загружен dll?

Ключевым моментом здесь является то, что это процесс excel.exe, и указатель, который мне нужен, должен принадлежать этому процессу. Использование таблицы текущих объектов не будет работать, поскольку Excel регистрирует только свой первый экземпляр.

Я надеюсь, что есть какой-то низкоуровневый обман COM, но я не эксперт в этой области.


person Bathsheba    schedule 15.11.2013    source источник
comment
Вы видели эту статью MSKB?   -  person Roger Rowland    schedule 18.11.2013
comment
Кажется, это использует таблицу текущих объектов.   -  person Eric Brown    schedule 19.11.2013
comment
Действительно, так оно и есть: боюсь, что толку от этого не будет.   -  person Bathsheba    schedule 19.11.2013
comment
Хорошо, я не был уверен при первом чтении (иначе я бы опубликовал ответ) - я заинтригован тем, почему вам нужно это сделать, может быть, есть другой способ ... можете ли вы расширить свой вопрос?   -  person Roger Rowland    schedule 19.11.2013
comment
@ Роджер Роуленд, причина требования довольно сложна: она сводится к некоторому финансовому инженерному коду, который может присоединяться к процессу Excel (а также к другим системам). При подключении к Excel некоторые компоненты интерфейса могут быть привязаны к этому Excel для вывода результатов. По другим причинам я не могу заставить Excel передавать указатель при запуске. Вот почему мне нужно получить указатель приложения. Использование интерфейса xll не будет работать из-за других требований, которые я не должен здесь подробно описывать. Надеюсь, это немного поможет; если немного расплывчато.   -  person Bathsheba    schedule 19.11.2013
comment
У вас есть требования к версии Excel, версии ОС, разрядности и т. д.?   -  person manuell    schedule 19.11.2013
comment
Я был бы доволен чем-либо в Excel12, WindowsXP и более поздних версиях, а также в 32- или 64-разрядных версиях: при необходимости я перенесу решение.   -  person Bathsheba    schedule 19.11.2013


Ответы (4)


Код EDITED II находится под лицензией WTFPL версии 2.

EDITED: добавьте параметр PID, чтобы разрешить фильтрацию, когда в данный момент запущено несколько процессов Excel, в соответствии с предложением комментария от @EricBrown.

Мне удалось получить работающий IDispatch* объект Excel «Приложение» без использования ROT. Хитрость заключается в использовании MSAA. Мой код работает как отдельное консольное приложение, но я думаю, что если код выполняется в процессе Excel через внедрение DLL, он МОЖЕТ работать нормально. Возможно, вам придется быть в специальной теме. Дайте мне знать, если вы хотите, чтобы я перевел эксперимент на уровень внедрения DLL.

Проверено нормально на Window7 64b, со сборками UNICODE (32-битные и 64-битные). Excel версии 2010 64 бита (версия "14")

Я получаю IDispatch через свойство «приложение» из объекта «Рабочий лист». Следствие: должен быть открытый рабочий лист. Чтобы найти хорошее окно MSSA, мне нужно имя класса окна фрейма Excel верхнего уровня. В Excel 2010 это «XLMAIN». Имя класса для рабочих листов - «EXCEL7», и это кажется «стандартом».

Мне не удалось напрямую получить рабочий IDispatch* из главного окна Excel, но я и не очень старался. Это может включать #import с DLL автоматизации из Excel, чтобы QueryInterface IDispatch, который MSAA дает для основного окна (этот IDispatch НЕ предназначен для объекта приложения)

#include <atlbase.h>

#pragma comment( lib, "Oleacc.lib" )

HRESULT GetExcelAppDispatch( CComPtr<IDispatch> & spIDispatchExcelApp, DWORD dwExcelPID ) {

   struct ew {
      struct ep {
         _TCHAR* pszClassName;
         DWORD dwPID;
         HWND hWnd;
      };
      static BOOL CALLBACK ewp( HWND hWnd, LPARAM lParam ) {
         TCHAR szClassName[ 64 ];
         if ( GetClassName( hWnd, szClassName, 64 ) ) {
            ep* pep = reinterpret_cast<ep*>( lParam );
            if ( _tcscmp( szClassName, pep->pszClassName ) == 0 ) {
               if ( pep->dwPID == 0 ) {
                  pep->hWnd = hWnd;
                  return FALSE;
               } else {
                  DWORD dwPID;
                  if ( GetWindowThreadProcessId( hWnd, &dwPID ) ) {
                     if ( dwPID == pep->dwPID ) {
                        pep->hWnd = hWnd;
                        return FALSE;
                     }
                  }
               }
            }
         }
         return TRUE;
      }
   };

   ew::ep ep;

   ep.pszClassName = _TEXT( "XLMAIN" );
   ep.dwPID = dwExcelPID;
   ep.hWnd = NULL;
   EnumWindows( ew::ewp, reinterpret_cast<LPARAM>( &ep ) );
   HWND hWndExcel = ep.hWnd;
   if ( ep.hWnd == NULL ) {
      printf( "Can't Find Main Excel Window with EnumWindows\n" );
      return -1;
   }

   ep.pszClassName = _TEXT( "EXCEL7" );
   ep.dwPID = 0;
   ep.hWnd = NULL;
   EnumChildWindows( hWndExcel, ew::ewp, reinterpret_cast<LPARAM>( &ep ) );
   HWND hWndWorkSheet = ep.hWnd;
   if ( hWndWorkSheet == NULL ) {
      printf( "Can't Find a WorkSheet with EnumChildWindows\n" );
      return -1;
   }

   CComPtr<IDispatch> spIDispatchWorkSheet;
   HRESULT hr = AccessibleObjectFromWindow( hWndWorkSheet, OBJID_NATIVEOM, IID_IDispatch,
                                            reinterpret_cast<void**>( &spIDispatchWorkSheet ) );
   if ( FAILED( hr ) || ( spIDispatchWorkSheet == 0 ) ) {
      printf( "AccessibleObjectFromWindow Failed\n" );
      return hr;
   }
   CComVariant vExcelApp;
   hr = spIDispatchWorkSheet.GetPropertyByName( CComBSTR( "Application" ), &vExcelApp );
   if ( SUCCEEDED( hr ) && ( vExcelApp.vt == VT_DISPATCH ) ) {
      spIDispatchExcelApp = vExcelApp.pdispVal;
      return S_OK;
   }
   return hr;

}
int _tmain(int argc, _TCHAR* argv[])
{

   DWORD dwExcelPID = 0;
   if ( argc > 1 ) dwExcelPID = _ttol( argv[ 1 ] );

   HRESULT hr = CoInitialize( NULL );
   bool bCoUnInitializeTodo = false;
   if ( SUCCEEDED( hr ) ) {
      bCoUnInitializeTodo = true;
      CComPtr<IDispatch> spDispatchExcelApp;
      hr = GetExcelAppDispatch( spDispatchExcelApp, dwExcelPID );
      if ( SUCCEEDED( hr ) && spDispatchExcelApp ) {
         CComVariant vExcelVer;
         hr = spDispatchExcelApp.GetPropertyByName( CComBSTR( "Version" ), &vExcelVer );
         if ( SUCCEEDED( hr ) && ( vExcelVer.vt == VT_BSTR ) ) {
            wprintf( L"Excel Version is %s\n", vExcelVer.bstrVal );
         }
      }
   }
   if ( bCoUnInitializeTodo ) CoUninitialize();
   return 0;
}
person manuell    schedule 19.11.2013
comment
Это очень хорошая реализация, но код для поиска главного окна неверен; вы должны проверить идентификатор процесса главного окна, а не только первого. - person Eric Brown; 19.11.2013
comment
@EricBrown Спасибо. Но я не понимаю, какую проверку я должен был сделать. Вы имеете в виду проверку того, что это процесс «Excel.exe»? - person manuell; 19.11.2013
comment
учитывая, что это должно быть в XLL и найти окно XLMAIN для этого конкретного процесса, вместо использования FindWindow следует использовать EnumWindows и проверить класс == "XLMAIN", а затем использовать GetWindowThreadProcessId для проверки идентификатор процесса. - person Eric Brown; 19.11.2013
comment
У этого есть ноги. Спасибо, что нашли время. +1, принять и щедрость. Я не буду копировать код дословно, поскольку он входит в коммерческое программное обеспечение, но буду использовать вашу технику AccessibleObjectFromWindow. - person Bathsheba; 20.11.2013
comment
@Вирсавия Спасибо! Обновил с лицензией :-) - person manuell; 20.11.2013

Вы сможете узнать, как это сделать, изучив код в ExcelDNA. Этот проект содержит код, который подключается к Excel из библиотеки расширений. Код, вероятно, будет более сложным, чем вам нужно, но будет реализовывать требуемую ссылку.

person Pekka    schedule 19.11.2013
comment
Хорошая идея, но мне не нужны накладные расходы на среду выполнения .NET Framework. - person Bathsheba; 20.11.2013

Вот как я это делаю: (признайте @manuell). dispatch_wrapper — это класс, вот конструктор для установки m_disp_application:

dispatch_wrapper(void)
{
    DWORD target_process_id = ::GetProcessId(::GetCurrentProcess());

    if (getProcessName() == "excel.exe"){
        HWND hwnd = ::FindWindowEx(0, 0, "XLMAIN", NULL);
        while (hwnd){
            DWORD process_id;
            ::GetWindowThreadProcessId(hwnd, &process_id);
            if (process_id == target_process_id){
                HWND hwnd_desk = ::FindWindowEx(hwnd, 0, "XLDESK", NULL);
                HWND hwnd_7 = ::FindWindowEx(hwnd_desk, 0, "EXCEL7", NULL);
                IDispatch* p = nullptr;
                if (SUCCEEDED(::AccessibleObjectFromWindow(hwnd_7, OBJID_NATIVEOM, IID_IDispatch, (void**)&p))){
                    LPOLESTR name[1] = {L"Application"};
                    DISPID dispid;
                    if (SUCCEEDED(p->GetIDsOfNames(IID_NULL, name, 1U, LOCALE_SYSTEM_DEFAULT, &dispid))){
                        CComVariant v;
                        DISPPARAMS dp;
                        ::memset(&dp, NULL, sizeof(DISPPARAMS));
                        EXCEPINFO ei;
                        ::memset(&ei, NULL, sizeof(EXCEPINFO));
                        if (SUCCEEDED(p->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYGET, &dp, &v, &ei, NULL))){
                            if (v.vt == VT_DISPATCH){
                                m_disp_application = v.pdispVal;
                                m_disp_application->AddRef();
                                return;
                            }
                        }
                    }
                }
            }
            hwnd = ::FindWindowEx(0, hwnd, "XLMAIN", NULL);
        }
    }
    m_disp_application = nullptr;
}

getProcessName() возвращает нижний регистр.

person Bathsheba    schedule 20.11.2013

Поскольку приложения Office регистрируют свои документы в ROT, вы можете присоединяться к экземплярам рядом с первым (который уже находится в ROT) с помощью получение IDispatch для документов в ROT, то вы можете использовать document.Application.hwnd (это VBA, вам нужно translate to IDispatch::GetIDsOfNames и IDispatch::Invoke with DISPATCH_PROPERTYGET), чтобы получить дескрипторы окон всех экземпляров Excel.

Теперь у вас есть сопоставление между IDispatch и дескрипторами Windows всех экземпляров Excel, пришло время найти свой собственный экземпляр Excel. Вы можете вызвать GetWindowThreadProcessId для дескрипторов окна, чтобы получить идентификаторы процессов, затем сравнить с вашим собственным идентификатором процесса, возвращенным GetCurrentProcessId, чтобы увидеть, какое окно Excel принадлежит вашему текущему процессу, и посмотреть в сопоставлении HWND с IDispatch, чтобы найти текущее приложение Excel. Интерфейс IDispatch.

person Sheng Jiang 蒋晟    schedule 19.11.2013
comment
Не хотите объяснить понижение голосов? - person Sheng Jiang 蒋晟; 21.11.2013