EternalWindows
DDE / トランザクションの開始

DDEによる通信は、クライアントとサーバー間でDDEメッセージを送受信することで成立していますが、 こうしたメッセージを直接扱うことはそれほど簡単ではありません。 たとえば、メッセージをポストする際にはPackDDElParamを呼び出してLPARAMを作成しなければせんし、 アイテムを識別するためにはグローバルアトムを作成する必要もありました。 DDEML(Dynamic Data Exchange Management Library)は、こうした複雑な処理を開発者から隠蔽し、 直観的にDDE通信が行えるための関数セットを提供します。 DDEMLは内部的にはメッセージベースのDDEを使用しているため、 クライアントがDDEMLを使用しているのならば、 サーバーもDDEMLを使用しなければならないというようなことはありません。 サーバーがメッセージベースのDDEで実装されていても、クライアントはDDEMLでサーバーと通信できますし、 その逆も当然可能です。

DDEMLは関数ベースのAPIであり、最初にDdeInitializeを呼び出すことになります。 これは、アプリケーションの種類(クライアント/サーバー)を問わず共通しています。

UINT WINAPI DdeInitialize(
  LPDWORD pidInst,
  PFNCALLBACK pfnCallback,
  DWORD afCmd,
  DWORD ulRes
);

pidInstは、インスタンス識別子を受け取る変数のアドレスを指定します。 この変数は0で初期化されている必要があります。 pfnCallbackは、トランザクションなどに関する通知を受け取るコールバック関数のアドレスを指定します。 afCmdは、定義されている定数を指定します。 ulResは、予約されているため0を指定します。 戻り値は、関数が成功するとDMLERR_NO_ERRORが返ります。

メッセージベースのDDEでは、クライアントがサーバーにWM_DDE_INITIATEを送信し、 サーバーがWM_DDE_ACKを返すことで、クライアントとサーバー間でDDE対話が成立していました。 DDEMLではこの流れがDdeConnectの中に実装されており、これをクライアントは呼び出すことになります。

HCONV WINAPI DdeConnect(
  DWORD idInst,
  HSZ hszService,
  HSZ hszTopic,
  PCONVCONTEXT pCC
);

idInstは、インスタンス識別子を指定します。 hszServiceは、サーバーのサービス名を指定します。 サービス名とは、メッセージベースのDDEにおけるアプリケーション名のことです。 hszTopicは、サーバーのトピック名を指定します。 pCCは、NULLまたはCONVCONTEXT構造体のアドレスを指定します。 この構造体は、XTYP_CONNECTがコールバック関数に通知される際に渡されます。 戻り値は、サーバーとの対話に必要なハンドルが返ります。

メッセージベースのDDEでは、アプリケーション名やトピック名をグローバルアトムで識別していましたが、 DDEMLではこれを文字列ハンドルとして隠蔽しています。 また、DDEMLではアプリケーション名のことをサービス名と呼びます。 文字列ハンドルは、DdeCreateStringHandleで作成することができます。

HSZ WINAPI DdeCreateStringHandle(
  DWORD idInst,
  LPTSTR psz,
  int iCodePage
);

idInstは、インスタンス識別子を指定します。 pszは、ハンドルを作成する文字列を指定します。 iCodePageは、文字列の表現に使うコードページを指定します。 DdeInitializeをANSIとして呼び出した場合はCP_WINANSIを指定しますが、 UNICODEとして呼び出した場合はCP_WINUNICODEを指定します。 CP_WINNEUTRALを指定すれば、適切な値を指定したことになります。 戻り値は、文字列ハンドルが返ります。

メッセージベースのDDEで複雑なのは、目的の動作に関する処理を一括して行えない点でしょう。 たとえば、サーバーからデータを取得する場合はWM_DDE_REQUESTを送り、 その応答としてWM_DDE_DATAを受け取るわけですが、このようにメッセージをまたいでいては、 データの取得に関するコードが分散してしまう点が気になります。 DDEMLでは、目的の動作に関する処理をトランザクションとして一括して行うため、 目的の動作を直観的に行えるようになります。 トランザクションを開始するには、DdeClientTransactionを呼び出します。

HDDEDATA WINAPI DdeClientTransaction(
  PBYTE pData,
  DWORD cbData,
  HCONV hConv,
  HSZ hszItem,
  UINT wFmt,
  UINT wType,
  DWORD dwTimeout,
  LPDWORD pdwResult
);

pDataは、サーバーに渡したいデータを指定します。 cbDataは、pDataのサイズを指定します。 hConvは、対話に使用するハンドルを指定します。 hszItemは、トランザクションの対象となるデータのアイテム名を指定します。 wFmtは、データの要求や設定におけるフォーマットを指定します。 wTypeは、トランザクションの型を表す定数を指定します。 XTYP_REQUEST(WM_DDE_REQUESTに相当)ならばデータの要求になり、XTYP_POKE(WM_DDE_POKEに相当)ならばデータの設定になります。 dwTimeoutは、同期トランザクションにおいてサーバーからの応答を待つ最大の時間をミリ秒単位で指定します。 この待機している間でも、UI操作は正しく行えるようになっています。 非同期トランザクションを実行する場合は、TIMEOUT_ASYNCを指定します。 pdwResultは、トランザクションの結果を受け取る変数のアドレスを指定します。 戻り値は、トランザクションの中で発生したデータを表すハンドルが返ります。

今回のプログラムは、Excelの特定のセルからデータを取得します。 プログラムを起動する前に、sample.xlsxを開いたExcelを起動しておいてください。

#include <windows.h>

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	DWORD    dwInst = 0;
	UINT     uResult;
	HSZ      hszService, hszTopic, hszItem;
	HDDEDATA hData;
	HCONV    hConv;
	CHAR     szData[256];

	uResult = DdeInitialize(&dwInst, NULL, APPCMD_CLIENTONLY, 0);
	if (uResult != DMLERR_NO_ERROR) {
		MessageBox(NULL, TEXT("DDEMLへの登録に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}

	hszService = DdeCreateStringHandle(dwInst, L"Excel", CP_WINNEUTRAL);
	hszTopic = DdeCreateStringHandle(dwInst, L"[sample.xlsx]Sheet1", CP_WINNEUTRAL);
	hConv = DdeConnect(dwInst, hszService, hszTopic, NULL);
	DdeFreeStringHandle(dwInst, hszService);
	DdeFreeStringHandle(dwInst, hszTopic);
	if (hConv == NULL) {
		MessageBox(NULL, TEXT("DDE対話の確立に失敗しました。"), NULL, MB_ICONWARNING);
		DdeUninitialize(dwInst);
		return 0;
	}

	hszItem = DdeCreateStringHandle(dwInst, L"R1C1", 0);
	hData = DdeClientTransaction(NULL, 0, hConv, hszItem, CF_TEXT, XTYP_REQUEST, 1000, NULL);
	DdeGetData(hData, (LPBYTE)szData, sizeof(szData), 0);
	MessageBoxA(NULL, szData, "OK", MB_OK);
	DdeFreeDataHandle(hData);
	DdeFreeStringHandle(dwInst, hszItem);

	DdeDisconnect(hConv);
	DdeUninitialize(dwInst);

	return 0;
}

まず、このプログラムがウインドウを作成していない点に注意してください。 DDEMLを使用していても、内部ではDDEメッセージを使用したやりとりが行われているわけですが、 それはDDEMLが内部で作成した非表示のウインドウが使用されています。 DdeInitializeの第2引数にはコールバック関数のアドレスを指定できますが、NULLでも問題ありません。 第3引数のAPPCMD_CLIENTONLYは、このプログラムがDDEクライアントとして動作することを意味します。 DdeConnectの呼び出しは、次のようになっています。

hszService = DdeCreateStringHandle(dwInst, L"Excel", CP_WINNEUTRAL);
hszTopic = DdeCreateStringHandle(dwInst, L"[sample.xlsx]Sheet1", CP_WINNEUTRAL);
hConv = DdeConnect(dwInst, hszService, hszTopic, NULL);
DdeFreeStringHandle(dwInst, hszService);
DdeFreeStringHandle(dwInst, hszTopic);

DdeConnectを呼び出すには、サーバーのサービス名とトピック名が必要になるため、DdeCreateStringHandleで作成しています。 作成した文字列ハンドルは、不要になったらDdeFreeStringHandleで開放しても問題ありません。

DdeConnectでサーバーとの対話を確立したら、DdeClientTransactionでトランザクションを開始します。

hszItem = DdeCreateStringHandle(dwInst, L"R1C1", CP_WINNEUTRAL);
hData = DdeClientTransaction(NULL, 0, hConv, hszItem, CF_TEXT, XTYP_REQUEST, 1000, NULL);
DdeGetData(hData, (LPBYTE)szData, sizeof(szData), 0);
MessageBoxA(NULL, szData, TEXT("OK"), MB_OK);

今回はデータを要求するトランザクションを開始するため、 データを渡すための引数である第1引数と第2引数はNULLと0になります。 第3引数はDdeConnectで取得したハンドルを取得し、第4引数はデータを識別するアイテムの名前を指定します。 Excelの場合であれば、これはセル名になります。 第5引数はデータのフォーマットであり、テキスト形式を望む場合はCF_TEXTを指定します。 第6引数はデータを要求する場合はXTYP_REQUESTを指定します。 第7引数はサーバーからの応答を待つ時間をミリ秒単位で指定します。 1000を指定しているため、最高1秒間待機できることになります。 第8引数はNULLで問題ありません。 XTYP_REQUESTの場合は、戻り値として返ったハンドルをDdeGetDataに指定することにより、データを取得することができます。

DDEサーバーの列挙

メッセージベースのDDEでは、WM_DDE_INITIATEのLPARAMに0を指定して、複数のサーバーと対話することができましたが、 DDEMLでもこうしたことは可能です。 DdeConnectListを呼び出せば、複数の対話のリストを表すハンドルを取得できるため、 これを呼び出す例を示します。

#include <windows.h>

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{ 
	static HWND hwndListBox = NULL;

	switch (uMsg) {

	case WM_CREATE: {
		DWORD     dwInst = 0;
		HCONV     hConv = NULL;
		CONVINFO  convInfo;
		HCONVLIST hconvList;
		TCHAR     szService[256];
		TCHAR     szTopic[256];
		TCHAR     szBuf[2048];

		hwndListBox = CreateWindowEx(0, TEXT("LISTBOX"), NULL, WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, hwnd, (HMENU)1, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		
		DdeInitialize(&dwInst, 0, APPCMD_CLIENTONLY, 0);

		hconvList = DdeConnectList(dwInst, NULL, NULL, NULL, NULL);
		while ((hConv = DdeQueryNextServer(hconvList, hConv)) != NULL) {
			convInfo.cb = sizeof(CONVINFO);
			DdeQueryConvInfo(hConv, QID_SYNC, (PCONVINFO)&convInfo);
			DdeQueryString(dwInst, convInfo.hszSvcPartner, szService, sizeof(szService) / sizeof(TCHAR), CP_WINNEUTRAL);
			DdeQueryString(dwInst, convInfo.hszTopic, szTopic, sizeof(szTopic) / sizeof(TCHAR), CP_WINNEUTRAL);
			wsprintf(szBuf, TEXT("AppName : %s Topic : %s"), szService, szTopic);
			SendMessage(hwndListBox, LB_ADDSTRING, 0, (LPARAM)szBuf);
		}
		DdeDisconnectList(hconvList);
		
		DdeUninitialize(dwInst);

		return 0;
	}

	case WM_SIZE:
		MoveWindow(hwndListBox, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE);
		return 0;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

DdeConnectListの第2引数と第3引数にNULLを指定すれば、WM_DDE_INITIATEのLPARAMに0を指定したのと同じ効果が得られます。 特定のトピック名を持つサーバーだけを列挙したい場合は、第3引数にそのトピック名を指定すればよいでしょう。 対話リストから個別の対話ハンドルを取得するには、DdeQueryNextServerを呼び出します。 ここで取得した対話ハンドルがどのサーバーとの対話を表しているかは、DdeQueryConvInfoで取得できます。 hszSvcPartnerメンバにはサーバーのサービス名が格納され、hszTopicメンバにトピック名が格納されているため、 これらをリストボックスに表示しています。



戻る