EternalWindows
汎用データ転送 / データの取得

Windowsに存在するAPIは、単にWindowsの機能を引き出すために設計されたものばかりではなく、 アプリケーション間の通信を標準化するアーキテクチャのようなものも存在します。 このような標準が存在すれば、アプリケーションは他のアプリケーションの機能を統一的な方法で利用できるようになりますから、 アプリケーション間で独自の規約を設ける必要がなくなります。 たとえば、Windows 2.0で登場したDDE(Dynamic Data Exchange)は、データ交換の標準をウインドウメッセージで実現することに可能にしました。 また、Windows 3.1で登場したOLE(Object Linking and Embedding)は、別のアプリケーションのドキュメントを自身のアプリケーションに表示することを可能にしました。 OLEが登場した当初はOLE1と呼ばれ、このときにはOLEの名前通り、オブジェクトの埋め込みとリンクだけをサポートしていました。

1993年に登場したOLE2は、オブジェクトの埋め込みとリンクだけでなく、 汎用データ転送やOLEドラッグ&ドロップなどの機能もサポートするようになりました。 つまり、データ転送やドラッグ&ドロップなどの方法もOLEによって標準化されたわけです。 また、従来から存在した埋め込みについても、インプレースアクティベーションという新しい形が登場し、 アプリケーションのウインドウ上でドキュメントを編集する事が可能になりました。 OLE2では通信の手段としてCOMが使用されるようになったため、 現在ではOLEにバージョンを付けて呼ぶことは基本的にありません。 COMを使用するようになれば、実行時に新しい機能を問い合わせることができるため、 バージョンというものが存在する意味はなくなります。

アプリケーションがOLEを使用することになるのは、他のアプリケーションの機能やデータを使用したい場合です。 たとえば、他のアプリケーションが汎用データ転送を使用してデータを公開しているのであれば、 アプリケーションは汎用データ転送を使用してデータを取得することができます。 もちろん、そのアプリケーションがDDEを使用してデータを公開しているのであれば、 DDEを通じてデータを取得するのも候補といえるでしょう。 今回、制御することになるExcelはDDEもOLEもサポートしていますが、 OLEの汎用データ転送を使用してデータを取得する例を取り上げます。

OLEの汎用データ転送を理解するためには、 それ以前から存在していた転送メカニズムである、DDEとクリップボードについて考察する必要があります。 これらのAPIが登場した頃には、Windowsに仮想メモリという概念が存在していなかったため、 メモリの確保にはGlobalAllocという関数が使用され、そこに目的のデータが格納されることが主流でした。 こうした、データを格納するためのメモリを確保するというのは一見普通のようにも思えますが、 データを提供する側がデータを取得する媒体を決定することは、本当によいことなのでしょうか。 もしデータを取得する側が、ファイルという媒体でデータを取得したいのであれば、 データを提供する側はそれに応えるべきなのではないでしょうか。 また、クリップボードではデータを要求する際にCF_BITMAPなどのフォーマットを指定しますが、 フォーマットとはこのような1つの定数で表してよいものなのでしょうか。 ビットマップの場合であれば、データをサムネイル形式で要求することもできてよいのではないでしょうか。 汎用データ転送が登場した背景は正にここにあり、 これを使用することでフォーマットやデータの媒体を限りなく汎用的に指定することができます。 汎用データ転送では、フォーマットをFORMATETC構造体で表します。

typedef struct tagFORMATETC {
  CLIPFORMAT     cfFormat;
  DVTARGETDEVICE *ptd;
  DWORD          dwAspect;
  LONG           lindex;
  DWORD          tymed;
} FORMATETC, *LPFORMATETC;

cfFormatは、取得したいデータのフォーマットを示す値を指定します。 これはクリップボードで扱われるCF_XXXのような形式でも構いませんし、 RegisterClipboardFormatの戻り値でも構いません。 ptdは、取得したデータを扱うデバイスの情報を指定します。 スクリーン上のウインドウで扱う場合はNULLで構いませんが、プリンタの場合は適切な情報を指定することになります。 dwAspectは、データをどのような見た目で取得するかを指定します。 DVASPECT_CONTENTの場合はデータが通常の形で返され、DVASPECT_THUMBNAILの場合はデータがサムネイル型式で返されます。 通常は、DVASPECT_CONTENTを指定することになると思われます。 lindexは、データがページ単位に分割されている場合に、どのページを取得対象にするかを示すインデックスを指定します。 通常は-1を指定し、全てのページを対象にします。 tymedは、データをどのような媒体で受け取るかを示す定数を指定します。

FORMATETC構造体を初期化したアプリケーションは、IDataObject::GetDataを呼び出すことでフォーマットに則ったデータを取得することができます。 IDataObjectは汎用データ転送に使用されるインターフェースであり、データを維持しているオブジェクトを識別しておく必要があります。

HRESULT IDataObject::GetData(
  FORMATETC *pformatetcIn,
  STGMEDIUM *pmedium
);

pformatetcInは、初期化済みのFORMATETC構造体のアドレスを指定します。 pmediumは、データを受け取るためのSTGMEDIUM構造体のアドレスを指定します。

IDataObject::GetDataで返されるデータの媒体が任意であるということは、 どのようなデータでも表すことのできる共用体が必要になります。 STGMEDIUM構造体は、このような共用体を内包しています。

typedef struct tagSTGMEDIUM {
  DWORD    tymed;
  union {
    HBITMAP       hBitmap;
    HMETAFILEPICT hMetaFilePict;
    HENHMETAFILE  hEnhMetaFile;
    HGLOBAL       hGlobal;
    LPOLESTR      lpszFileName;
    IStream       *pstm;
    IStorage      *pstg;
  } ;
  IUnknown *pUnkForRelease;
} STGMEDIUM, *LPSTGMEDIUM;

tymedは、データを受け取る媒体を示す定数が格納されます。 原則としてこれは、FORMATETC構造体のtymedメンバと一致することになるはずです。 tymedがTYMED_GDIである場合は、ビットマップのデータが格納されているということなのでhBitmapからアクセスし、 tymedがTYMED_MFPICTである場合はhMetaFilePictからメタファイルのデータにアクセスします。 このような要領は、他のメンバについても同一です。 pUnkForReleaseは、何らかのオブジェクトのアドレスが返されることがあります。 もし、アドレスが格納されている場合は、オブジェクトの使用後にReleaseを呼び出す必要があります。

STGMEDIUM構造体は、様々な種類のデータを受け取ることができるわけですが、 そうしたデータの開放処理は互いに異なります。 たとえば、TYMED_GDIであれば開放処理はDeleteObjectになり、 TYMED_HGLOBALであればGlobalFreeになります。 幸いにも、こうしたデータ毎の違いはReleaseStgMediumによって吸収されているため、 この関数を呼び出せばどのようなデータでも開放することができます。

void ReleaseStgMedium(
  LPSTGMEDIUM pMedium
);

pMediumは、データを維持しているSTGMEDIUM構造体のアドレスを指定します。 この関数は、pUnkForReleaseのオブジェクトの開放処理も行います。

今回のプログラムは、IDataObjectを使用してExcelのデータを取得します。

#include <windows.h>

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	char        *p;
	HRESULT     hr;
	MULTI_QI    qi;
	FORMATETC   formatetc;
	STGMEDIUM   medium;
	IDataObject *pDataObject;

	OleInitialize(NULL);
	
	qi.pIID = &IID_IDataObject;
	qi.pItf = NULL;

	hr = CoGetInstanceFromFile(NULL, NULL, NULL, CLSCTX_LOCAL_SERVER, STGM_READ, L"C:\\sample.xlsx", 1, &qi);
	if (FAILED(hr)) {
		OleUninitialize();
		return 0;
	}
	
	pDataObject = static_cast<IDataObject *>(qi.pItf);

	formatetc.cfFormat = (CLIPFORMAT)RegisterClipboardFormat(TEXT("Csv"));
	formatetc.ptd      = NULL;
	formatetc.dwAspect = DVASPECT_CONTENT;
	formatetc.lindex   = -1;
	formatetc.tymed    = TYMED_HGLOBAL;

	pDataObject->GetData(&formatetc, &medium);

	p = (char *)GlobalLock(medium.hGlobal);
	MessageBoxA(NULL, (char *)p, "OK", MB_OK);
	GlobalUnlock(medium.hGlobal);
	
	ReleaseStgMedium(&medium);
	pDataObject->Release();
	OleUninitialize();
	
	return 0;
}

COMを使用するアプリケーションはCoInitialize(Ex)を呼び出すことが常ですが、 OLEを使用するアプリケーションはOleInitializeを呼び出すことになります。 この関数は内部でCoInitializeExを呼び出すと共に、OLE関連の初期化処理も行います。 CoGetInstanceFromFileは、指定したファイルに関連するオブジェクトを返す関数です。 今回の場合はExcelのファイルを指定しているため、返されるのはExcelのオブジェクトです。 このオブジェクトをIDataObjectで識別したい場合は、MULTI_QI構造体のpIIDにIDataObjectのIIDを指定します。 これにより関数の成功時に、pItfにオブジェクトのアドレスが格納されます。

オブジェクトをIDataObjectで識別したら、GetDataを呼び出してデータを取得することになります。

formatetc.cfFormat = RegisterClipboardFormat(TEXT("Csv"));
formatetc.ptd      = NULL;
formatetc.dwAspect = DVASPECT_CONTENT;
formatetc.lindex   = -1;
formatetc.tymed    = TYMED_HGLOBAL;

pDataObject->GetData(&formatetc, &medium);

FORMATETC構造体のcfFormatは、取得したいフォーマットの形式を指定します。 CF_TEXTを指定すればテキスト形式で取得できますが、Excelの場合はCsv形式で取得したほうが分かりやすいでしょう。 Csv形式の値は、RegisterClipboardFormatにCsvという文字列を指定することで取得できます。 ptdからlindexについては、上記のように指定することになるでしょう。 tymedにTYMED_HGLOBALを指定していることから、データはグローバルメモリの形式で返ることになります。

データを取得したら、そのデータを表示することになります。 データはグローバルメモリで要求したため、STGMEDIUM構造体のtymedにはTYMED_HGLOBALが格納され、 hGlobalからグローバルメモリへアクセスすることができます。 グローバルメモリを扱う場合は、最初にGlobalLockのデータをロックし、 データを使い終えたらGlobalUnlockでアンロックすることになります。 データが不要になった場合は、ReleaseStgMediumでデータを開放します。

CoGetInstanceFromFileの内部動作

CoGetInstanceFromFileは、指定したファイルに関連するオブジェクトのアドレスを返すことができますが、 これはどのような仕組みで行われているのでしょうか。 予想としては、次に示す3つの処理がポイントになると思われます。

CLSID        clsid;
IPersistFile *pPersistFile;

GetClassFile(szFilePath, &clsid);

CoCreateInstance(clsid, NULL, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&pPersistFile));

pPersistFile->Load(szFilePath, STGM_READ);
pPersistFile->QueryInterface(IID_PPV_ARGS(&pDataObject));
pPersistFile->Release();

ExcelやWordのファイルには、関連するオブジェクトのCLSIDが格納されており、これはGetClassFileで取得することができます。 このCLSIDを基にCoCreateInstanceを呼び出せばオブジェクトを作成することができますが、 このときにCLSCTX_LOCAL_SERVERを指定する点が重要です。 理由は、Excelがアウトプロセスサーバーであるからです。 アウトプロセスサーバーということは、CoCreateInstanceの成功時にはプロセスが起動されることになりますが、 プロセスのウインドウ自体は表示されません。 オブジェクトをIPersistFileで表しているのは、オブジェクトに対してファイルをロードさせる必要があるからです。 これは、IPersistFile::Loadを呼び出すことが可能です。 以上の作業が終了すればオブジェクトをIDataObjectで表し、データを取得することができます。



戻る