본문 바로가기

Application Programming Interface/Windows API

Win32에서 IDispatch 인터페이스를 활용한 객체 사용 방법

336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

Win32에서 IDispatch 인터페이스를 활용한 객체 사용 방법

본 포스팅에서는 Win32 C++에서 IDispatch Interface를 사용하여 Component Object Model 객체를 생성하고 그 객체의 메서드와 프로퍼티에 접근하는 방법에 대해 알아보겠습니다.

1. IDispatch 인터페이스

IDispatch 인터페이스interface는 COM(Component Object Model) 기반 프로그래밍을 할 때 COM 서버에 등록된 각종 개체들을 연동하는 과정에서 개체들이 제공하는 각종 인스턴스instance, 프로퍼티property, 메서드method 등을 현재 작성중인 코드와 연계시켜주는 역할을 하는 인터페이스입니다.

1-1. COM/OLE의 사용 시작과 종료

IDispatch 인터페이스를 사용하기 위해서 우선, 아래의 함수 중 하나를 호출하여 COM/OLE 기능을 초기화합니다.

HRESULT CoInitialize(IN LPVOID pvReserved);
HRESULT CoInitializeEx(IN LPVOID pvReserved, IN DWORD dwCoInit);
HRESULT OleInitialize(IN LPVOID pvReserved);

 

CoInitialize가 COM 기능을 사용하기 위해 내부 초기화를 수행하는 함수이지만, 현재 새로 개발될 프로그램에 대해서는 멀티스레드를 지원하는 CoInitializeEx 함수를 쓰는 것이 권장됩니다. 이 함수의 dwCoInit 매개변수를 통해 초기화 옵션을 지정할 수 있는데 이 옵션들은 COINIT 열거자에 정의되어 있습니다. 주요 옵션은 다음과 같습니다.

COINIT_MULTITHREADED
COM을 사용하는 본 프로그램이 멀티스레드를 운영하며 작동될 때 사용합니다.
COINIT_APARTMENTTHREADED
COM을 사용하는 본 프로그램이 싱글스레드로 작동될 때 사용합니다.

 

OleInitialize 함수는 내부적으로 CoInitialize 함수를 호출합니다. 다만, 이후 OLE 관련 초기화 작업을 부가적으로 더 시행하므로 둘 중 하나를 호출해도 무방합니다.

pvReserved 매개변수는 나중에 기능 확장을 위해 예약된 자리로 현재 시점에서는 사용되지 않으므로 NULL을 주면 됩니다.

프로그램이 종료될 때는 CoUninitialize 또는 OleUninitialize를 호출합니다. 원형은 다음과 같습니다.

void CoUninitialize(void);
void OleUninitialize(void);

 

1-2. 객체의 생성과 소멸

COM 객체를 생성하는 방법은 다음과 같습니다.

1. 불러올 객체의 CLSID를 알고 있다면 CLSID를 사용하여 직접 불러옵니다.

/* Example */
CLSID clsid; // 불러올 객체의 CLSID
IDispatch * pDispatch; // 불러온 객체가 보관될 곳
CLSIDFromString(OLESTR("{00000000-0000-0000-0000-000000000000}"), &clsid);
CoCreateInstance(clsid, NULL, CLSCTX_LOCAL_SERVER, IID_IDispatch, (LPVOID *)&pDispatch);

 

2. 불러올 객체의 ProgID를 알고 있으면 이 ProgID로 CLSID를 구한 다음 CLSID에 해당하는 객체를 생성하여 불러옵니다.

/* Example */
CLSID clsid; // 불러올 객체의 CLSID
IDispatch * pDispatch; // 불러온 객체가 보관될 곳
CLSIDFromProgID(OLESTR("SomeApplication.SomeClass"), &clsid);
CoCreateInstance(clsid, NULL, CLSCTX_LOCAL_SERVER, IID_IDispatch, (LPVOID *)&pDispatch);

 

3. CLSID와 ProgID를 다 모르는 경우

이 경우는 COM Server에 등록된 CLSID와 ProgID 목록을 가져와서 사용자에게 선택을 맡길 수 있습니다. CLSID와 ProgID는 레지스트리에 기록되어 있는데 이와 관련한 사항은 본 주제와 상이하므로 다른 포스트에서 설명하겠습니다.

 

위의 함수들은 반환형이 모두 HRESULT 형이므로 SUCCEEDED()FAILED() 매크로로 작업 성공여부를 확인할 수 있고, 작업 실패 시 GetLastError 함수로 그 사유를 확인할 수 있습니다.

 

사용이 끝난 객체는 다음과 같이 소멸시킵니다.

/* Example */
CLSID clsid; // 불러올 객체의 CLSID
IDispatch * pDispatch; // 불러온 객체가 보관될 곳
/* ... 중략 ... */
pDispatch->Release();

2. VARIANT 구조체와 OLESTR 자료형

VARIANT 구조체는 COM 객체와 응용 프로그램간 교환하는 자료의 자료형을 특정할 수 없을 때 사용되는 구조체입니다. 이 구조체의 원형은 다음과 같습니다.

typedef struct tagVARIANT
{
    union
    {
        struct __tagVARIANT
        {
            VARTYPE vt;
            WORD    wReserved1;
            WORD    wReserved2;
            WORD    wReserved3;
            union
            {
                LONGLONG llVal;
                LONG lVal;
                BYTE bVal;
                SHORT iVal;
                FLOAT fltVal;
                DOUBLE dblVal;
                VARIANT_BOOL boolVal;
                _VARIANT_BOOL bool;
                SCODE scode;
                CY cyVal;
                DATE date;
                BSTR bstrVal;
                IUnknown * punkVal;
                IDispatch * pdispVal;
                SAFEARRAY * parray;
                BYTE * pbVal;
                SHORT * piVal;
                LONG * plVal;
                LONGLONG * pllVal;
                FLOAT * pfltVal;
                DOUBLE * pdblVal;
                VARIANT_BOOL * pboolVal;
                _VARIANT_BOOL * pbool;
                SCODE * pscode;
                CY * pcyVal;
                DATE * pdate;
                BSTR * pbstrVal;
                IUnknown ** ppunkVal;
                IDispatch ** ppdispVal;
                SAFEARRAY ** pparray;
                VARIANT * pvarVal;
                PVOID * byref;
                CHAR cVal;
                USHORT uiVal;
                ULONG ulVal;
                ULONGLONG ullVal;
                INT intVal;
                UINT uintVal;
                DECIMAL * pdecVal;
                CHAR * pcVal;
                USHORT * puiVal;
                ULONG * pulVal;
                ULONGLONG * pullVal;
                INT * pintVal;
                UINT * puintVal;
                struct __tagBRECORD
                {
                    PVOID pvRecord;
                    IRecordInfo * pRecInfo;
                } __VARIANT_NAME_4;
            } __VARIANT_NAME_3;
        } __VARIANT_NAME_2;
        DECIMAL             decVal;
    } __VARIANT_NAME_1;
} VARIANT, *LPVARIANT, VARIANTARG, *LPVARIANTARG;

 

이 구조체는 COM에서 지원 가능한 모든 종류의 자료형을 공용체로서 포함하고 있고, 자료의 종류에 대해서는 vt 필드로 식별될 수 있습니다. 이 필드에 사용 가능한 값과 각 값에 따라 접근해야 할 멤버는 다음과 같습니다.

VT_EMPTY
아무 데이터도 포함하지 않습니다. C 언어의 void에 해당합니다.
VT_NULL
NULL 데이터 타입입니다. C 언어의 nullptr에 해당합니다.
VT_I1
BYTE bVal 멤버로 접근해야 하며, 부호 있는 1바이트 정수입니다.
VT_I2
SHORT iVal 멤버로 접근해야 하며, 부호 있는 2바이트 정수입니다.
VT_I4
LONG lVal멤버로 접근해야 하며, 부호 있는 4바이트 정수입니다.
VT_I8
LONGLONG llVal멤버로 접근해야 하며, 부호 있는 8바이트 정수입니다.
VT_UI1
CHAR cVal 멤버로 접근해야 하며, 부호 없는 1바이트 정수입니다.
VT_UI2
USHORT uiVal 멤버로 접근해야 하며, 부호 없는 2바이트 정수입니다.
VT_UI4
ULONG ulVal멤버로 접근해야 하며, 부호 없는 4바이트 정수입니다.
VT_UI8
ULONGLONG ullVal멤버로 접근해야 하며, 부호 없는 8바이트 정수입니다.
VT_R4
FLOAT fltVal 멤버로 접근해야 하며, 4바이트 실수입니다.
VT_R8
DOUBLE dblVal 멤버로 접근해야 하며, 8바이트 실수입니다.
VT_CY
CY cyVal 멤버로 접근해야 하며, 화폐 단위에 의한 값입니다.
VT_DATE
DATE date 멤버로 접근해야 하며 날짜를 나타내는 값입니다.
VT_BSTR
BSTR bstrVal 멤버로 접근해야 하며 COM에서 사용하는 문자열인 BSTR형 문자열입니다.
VT_DISPATCH
IDispatch * pdispVal 멤버로 접근해야 하며 COM 객체를 전달합니다.
VT_ERROR
SCODE scode 멤버로 접근해야 하며, COM 메서드의 실행 결과인 HRESULT와 같습니다.
VT_PTR
VT_SAFEARRAY
SAFEARRAY * parray 멤버로 접근해야 하며, COM에서 사용하는 배열인 SAFEARRAY형 배열입니다.
VT_BOOL
VARIANT_BOOL boolVal 멤버로 접근하며, COM에서 사용하는 불리언 형인 VARIANT_BOOL 논리입니다.
VT_VARIANT | VT_BYREF
VARIANT * pvarVal 멤버로 접근해야 하며, 또 다른 VARIANT 구조체에 대한 포인터입니다. 이 형식을 사용하기 위해서는 반드시 VT_BYREF와 조합해서 씁니다.
VT_UNKNOWN
IUnknown * punkVal 멤버로 접근해야 하며, 임의의 COM 객체를 전달합니다.
VT_DECIMAL
DECIMAL * pdecVal 멤버로 접근해야 하며, 10진수 값입니다.

 

2. 객체의 메서드를 호출하는 방법

각 객체에 포함된 메서드, 프로퍼티 등은 CLSID와 별개로 자기 자신을 식별할 수 있는 고유한 번호가 있는데 이를 DISPID라 합니다. 메서드 또는 프로퍼티의 이름을 알고 있는 경우 객체에서 GetIDsOfNames 메서드로 그 대상의 DISPID를 찾습니다. 이 DISPID를 Invoke 메서드에 전달함으로써 호출이 이루어집니다. 메서드에 따라 매개변수를 필요로 하는 메서드의 경우 DISPPARAM 구조체를 사용하여 매개변수를 전달하고, 값을 반환하는 메서드의 경우 VARIANT 구조체를 빌려서 값을 반환합니다. 객체의 메서드를 호출하는 방법은 다음과 같습니다.

1. 객체에서 호출하고자 하는 메서드가 있는지 찾고, 있다면 그 메서드에 부여된 DISPID를 가져온다.

/* Example */
CLSID clsId; // 불러올 객체의 CLSID
DISPID dispId; // 호출할 메서드의 DISPID
IDispatch * pDispatch; // 객체
  
LPOLESTR lpszName = (LPOLESTR)OLESTR("aMethod"); // 실행할 메서드의 이름
pDispatch->GetIDsOfNames(IID_NULL, &lpszName, 1, LOCALE_USER_DEFAULT, &dispId); // 메서드 이름으로부터 DISPID 조회하기

 

메서드 이름에서 알 수 있듯이 GetIDsOfNames 메서드는 한 번의 호출로 여러 메서드의 DISPID를 얻을 수 있습니다. 만일 여러 메서드를 얻고자 할 때는 다음과 같이 합니다.

/* Example */
#define COUNT 3

CLSID clsId; // 불러올 객체의 CLSID
IDispatch * pDispatch; // 객체
  
LPOLESTR rgszName[COUNT] = {
    (LPOLESTR)OLESTR("aMethod"),
    (LPOLESTR)OLESTR("aFake"),
    (LPOLESTR)OLESTR("aTrue")
}; // 실행할 메서드의 이름

DISPID rgDispId[COUNT]; // 호출할 메서드의 DISPID

pDispatch->GetIDsOfNames(IID_NULL, &rgszName, COUNT, LOCALE_USER_DEFAULT, &rgDispId); // 메서드 이름으로부터 DISPID 조회하기

 

2. 가져온 DISPID를 Invoke 메서드에 전달한다.

/* Example */
CLSID clsId; // 불러올 객체의 CLSID
DISPID dispId; // 호출할 메서드의 DISPID
DISPPARAM dispParam = { NULL, NULL, 0, 0 }; // 메서드가 필요로 하는 매개변수
VARIANT variant; // 메서드의 반환 값
IDispatch * pDispatch; // 객체
 
/* 매개변수가 없는 메서드의 호출의 예 */
OLECHAR * lpszName = (OLECHAR *)OLESTR("aMethod"); // 실행할 메서드의 이름
VariantInit(&variant); // 반환값을 받기 위한 variant 구조체의 리셋
pDispatch->GetIDsOfNames(IID_NULL, &lpszName, 1, LOCALE_USER_DEFAULT, &dispId); // 메서드 이름으로부터 DISPID 조회하기
pDispatch->Invoke(dispId, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &dispParams, &variant, NULL, NULL);

/* 반환 값이 있다면 VARIANT 구조체로 전달된 값을 읽는다. */
switch (variant.vt)
{
    // ... 생략 ...
}

 

3. 메서드가 매개변수를 필요로 할 경우 DISPPARAM 구조체를 활용한다.

어떤 객체에 다음과 같이 선언된 메서드가 있다고 가정합니다.

int aMethod(int a, [int b], int c); // b 매개변수는 생략가능

 

각 매개변수는 메서드/프로퍼티와는 별개의 또 다른 DISPID가 부여됩니다. 다만, 이 때의 DISPID는 메서드 선언할 때의 매개변수 순서대로 맨 처음 선언된 매개변수의 DISPID = 0, 그 다음 매개변수의 DISPID = 1, ... 순으로 정해졌습니다. DISPPARAM으로 매개변수를 전달할때는 메서드에서 선언한 매개변수의 개수만큼 전달하되, 그 순서는 반드시 역순으로 지정해야 합니다. 그 이유에 대해서는 MSDN(https://msdn.microsoft.com/en-us/library/windows/desktop/ms221653(v=vs.85).aspx에서 그렇게 한다고 정했기 때문입니다.

/* Example */
CLSID clsId; // 불러올 객체의 CLSID
DISPID dispId; // 호출할 메서드의 DISPID
DISPPARAM dispParam = { NULL, NULL, 0, 0 }; // 메서드가 필요로 하는 매개변수
VARIANT variant; // 메서드의 반환 값
IDispatch * pDispatch; // 객체
  
/* 매개변수가 있는 메서드의 호출의 예 */
OLECHAR * lpszName = (OLECHAR *)OLESTR("aMethod"); // 실행할 메서드의 이름

dispParam.rgvarg = new VARIANTARG[3]; // 매개변수를 보관하여 전달할 구조체 배열
dispParam.cArgs = 3; // 매개변수는 총 3개
dispParam.cNamedArgs = 0; // 이 부분은 이어서 다른 예로 설명
dispParam.rgdispidNamedArgs = NULL; // 이 부분은 이어서 다른 예로 설명

/* aMethod(int a[, int b], int c);에 대해 aMethod(11, 22, 33); 으로 호출하고자 할 때 */

VariantInit(&dispParam.rgvarg[0]);
dispParam.rgvarg[0].vt = VT_I4;
dispParam.rgvarg[0].intVal = 33; // c = 33

VariantInit(&dispParam.rgvarg[1]);
dispParam.rgvarg[1].vt = VT_I4;
dispParam.rgvarg[1].intVal = 22; // b = 22

VariantInit(&dispParam.rgvarg[2]);
dispParam.rgvarg[2].vt = VT_I4;
dispParam.rgvarg[2].intVal = 11; // a = 11

VariantInit(&variant); // 반환값을 받기 위한 variant 구조체의 리셋
pDispatch->GetIDsOfNames(IID_NULL, &lpszName, 1, LOCALE_USER_DEFAULT, &dispId); // 메서드 이름으로부터 DISPID 조회하기
pDispatch->Invoke(dispId, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &dispParams, &variant, NULL, NULL);

delete[] dispParam.rgvarg;

/* 반환 값이 있다면 VARIANT 구조체로 전달된 값을 읽는다. */
switch (variant.vt)
{
    // ... 생략 ...
}

 

COM 메서드는 중간의 일부 매개변수를 생략할 수 있습니다. 이 경우 NamedArgs를 사용하여 값을 전달할 매개변수의 DISPID를 하나씩 명시해야 합니다. Invoke 메서드는 DISPPARAM 구조체의 cNamedArgs의 값이 0이면 모든 매개변수가 다 전달되는 것으로 간주하지만, 0이 아닌 값에 대해 이름 지정 매개변수인 것으로 간주합니다. 그리고 rgdispidNamedArgs에서 지정한 매개변수 DISPID에 rgvarg를 대응시킵니다.

/* Example */
CLSID clsId; // 불러올 객체의 CLSID
DISPID dispId; // 호출할 메서드의 DISPID
DISPPARAM dispParam = { NULL, NULL, 0, 0 }; // 메서드가 필요로 하는 매개변수
VARIANT variant; // 메서드의 반환 값
IDispatch * pDispatch; // 객체
  
/* 매개변수가 있는 메서드의 호출의 예 */
OLECHAR * lpszName = (OLECHAR *)OLESTR("aMethod"); // 실행할 메서드의 이름

dispParam.rgvarg = new VARIANTARG[2]; // 매개변수를 보관하여 전달할 구조체 배열
dispParam.cArgs = 2; // 매개변수는 총 2개
dispParam.cNamedArgs = 2; // 이름지정 매개변수
dispParam.rgdispidNamedArgs = new DISPID[2]; // 값을 지정할 매개변수 번호

/* aMethod(int a[, int b], int c);에 대해 aMethod(a: 11, c: 33); 으로 호출하고자 할 때 */

VariantInit(&dispParam.rgvarg[0]);
dispParam.rgvarg[0].vt = VT_I4;
dispParam.rgvarg[0].intVal = 33; // c = 33
dispParam.rgdispidNameArgs[0] = 2;

VariantInit(&dispParam.rgvarg[1]);
dispParam.rgvarg[1].vt = VT_I4;
dispParam.rgvarg[1].intVal = 11; // a = 11
dispParam.rgdispidNameArgs[1] = 0;

VariantInit(&variant); // 반환값을 받기 위한 variant 구조체의 리셋
pDispatch->GetIDsOfNames(IID_NULL, &lpszName, 1, LOCALE_USER_DEFAULT, &dispId); // 메서드 이름으로부터 DISPID 조회하기
pDispatch->Invoke(dispId, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &dispParams, &variant, NULL, NULL);

delete[] dispParam.rgvarg;
delete[] dispParam.rgdispidNameArgs;

/* 반환 값이 있다면 VARIANT 구조체로 전달된 값을 읽는다. */
switch (variant.vt)
{
    // ... 생략 ...
}

 

3. 객체의 프로퍼티에서 값 가져오기/설정하기

객체의 프로퍼티로부터 값을 가져오는 경우 DISPATCH_PROPERTYGET를 사용합니다.

/* Example */
CLSID clsId; // 불러올 객체의 CLSID
DISPID dispId; // 호출할 프로퍼티의 DISPID
DISPPARAM dispParam = { NULL, NULL, 0, 0 }; // 메서드가 필요로 하는 매개변수
VARIANT variant; // 메서드의 반환 값
IDispatch * pDispatch; // 객체

// ... 

OLECHAR * lpszName = (OLECHAR *)OLESTR("aProperty"); // 값을 가져올 프로퍼티의 이름
VariantInit(&variant); // 반환값을 받기 위한 variant 구조체의 리셋
pDispatch->GetIDsOfNames(IID_NULL, &lpszName, 1, LOCALE_USER_DEFAULT, &dispId); // 메서드 이름으로부터 DISPID 조회하기
pDispatch->Invoke(dispId, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYGET, &dispParams, &variant, NULL, NULL);

/* 반환 값이 있다면 VARIANT 구조체로 전달된 값을 읽는다. */
switch (variant.vt)
{
    // ... 생략 ...
}

 

객체의 프로퍼티에 값을 설정하는 경우 DISPATCH_PROPERTYPUT를 사용합니다.

/* Example */
CLSID clsId; // 불러올 객체의 CLSID
DISPID dispId; // 호출할 프로퍼티의 DISPID
DISPPARAM dispParam = { NULL, NULL, 0, 0 }; // 메서드가 필요로 하는 매개변수
VARIANT variant; // 메서드의 반환 값
IDispatch * pDispatch; // 객체

// ...

dispParams.cArgs = 1;
dispParams.cNamedArgs = 1;
dispParams.rgvarg = new VARIANTARG[1];
dispParams.rgdispidNamedArgs = new DISPID[1];
dispParams.rgvarg[0].vt = VT_INT;
dispParams.rgvarg[0].intVal = 1; // 정수 1
dispParams.rgdispidNamedArgs[0] = DISPID_PROPERTYPUT; // 값을 설정할 경우 매개변수의 DISPID는 이와 같이 DISPID_PROPERTYPUT 매크로 상수입니다.
VariantInit(&variant);
hResult = pXlRangeRange->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYPUT, &dispParams, &variant, NULL, NULL);
delete[] dispParams.rgvarg;
delete[] dispParams.rgdispidNamedArgs;
// hResult의 결과에 따른 후속 조치...