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의 결과에 따른 후속 조치...
'Application Programming Interface > Windows API' 카테고리의 다른 글
Win32 C++에서 Microsoft Excel 파일 다루는 방법 (0) | 2018.02.25 |
---|---|
Windows API로 콘솔(터미널) 입/출력하기 (0) | 2018.02.20 |
RAW형 데이터의 프린터 출력을 위한 Windows API 호출 과정 (1) | 2017.07.17 |
Windows NT 4.0 DDK 문서 - IMEAPPS.DOC [Part 2] (0) | 2014.10.02 |
Windows NT 4.0 DDK 문서 - IMEAPPS.DOC [Part 1] (2) | 2014.10.02 |