EternalWindows
DDE / DDE対話

プロセス間通信において、通信の両者が自作のプロセスである場合は話が幾分簡単です。 この場合、片方のプロセスはもう片方から送られるデータの意味を理解しているわけですから、 データの処理方法に悩むこともないはずです。 しかし、自分が作成していないプロセスとの通信を考えた場合、 そのプロセスにどのようなデータを送ればよいかは基本的に見当がつきません。 プロセスがドキュメントなどを公開しているのであれば、それに従って通信することもできますが、 こうした固有の通信方法では、他のプロセスとの通信に応用できるような柔軟性に欠けます。 可能ならば、どのようなプロセスでも統一した方法で通信を行いたいですから、 そうした事を可能にする標準的な方法があれば大変便利です。 DDE(Dynamic Data Exchange)が果たす役割は正にこれであり、 通信相手がどのようなデータをサポートするのかを統一した方法で確認できます。

DDE通信を行うためには、DDE特有の概念である、アプリケーション名、トピック名、アイテム名を理解しなければなりません。 これは、実際に通信を行うときのことを想像してみるのが一番よいでしょう。 たとえば、Excelと通信したいのであれば、そのExcelを識別するための名前が必要になりそうですが、 この名前というのがアプリケーション名です。 アプリケーション名を知っていれば、それだけで通信が成立しそうにも思えますが、 Excelは1つ以上のシートが存在しており、どのシートからデータを取得するのかも決めなければなりません。 ここで使用されるのがトピック名になります。 そして、シートが決まったら特定のセルからデータを取得しなければなりませんが、 このセルを識別するのがアイテム名になります。 簡単にいえば、アイテム名は最終的に取得したいデータを識別する名前であり、 トピック名はそのデータを内包しているものの名前ということになります。

DDEはウインドウメッセージをベースとした通信であるため、 クライアントは通信先となるサーバーのウインドウハンドルを取得しなければなりません。 つまり、Excelと通信したいのであればExcelのウインドウハンドルが必要になるわけですが、 ここでアプリケーション名とトピック名が使用されます。

ATOM atomApp, atomTopic;

atomApp = GlobalAddAtom(TEXT("Excel"));
atomTopic = GlobalAddAtom(TEXT("[sample.xlsx]Sheet1"));
SendMessage((HWND)HWND_BROADCAST, WM_DDE_INITIATE, (WPARAM)hwnd, MAKELONG(atomApp, atomTopic));
GlobalDeleteAtom(atomApp);
GlobalDeleteAtom(atomTopic);

SendMessageの第1引数にHWND_BROADCASTを指定した場合は、現在存在するほぼ全てのウインドウに第2引数のメッセージが送信されます。 つまり、Excelが既に起動されているのであれば、Excelにもこのメッセージが届くことになります。 後はこのExcelがクライアントに対してウインドウハンドルを返してくれればよいのですが、 このためには送信するメッセージがWM_DDE_INITIATEでなければなりません。 これにより、サーバーはクライアントがDDE通信を望んでいることを理解できます。 そして、この通信が自分宛てであることをサーバーが理解するために、 サーバーのアプリケーション名とトピック名をLPARAMとして指定します。 アプリケーション名やトピック名の正体は文字列ですが、 2つの文字列をLPARAMに指定することはできないため、 文字列を数値で表すことのできるGlobalAddAtomを使用しています。 SendMessageの第3引数にクライアントのウインドウハンドルを指定しているのは、 サーバーがクライアントにメッセージを返せるようにするためです。 サーバーのWM_DDE_INITIATEの処理は、次のようなイメージになります。

case WM_DDE_INITIATE: {
	ATOM atomApp = GlobalAddAtom(TEXT("Excel"));
	ATOM atomTopic = GlobalAddAtom(TEXT("[sample.xlsx]Sheet1"));

	if (LOWORD(lParam) == atomApp && HIWORD(lParam) == atomTopic) {
		hwndClientDDE = (HWND)wParam;
		SendMessage(hwndClientDDE, WM_DDE_ACK, (WPARAM)hwnd, MAKELONG(atomApp, atomTopic));
	}

	GlobalDeleteAtom(atomTopic);
	GlobalDeleteAtom(atomApp);
	
	return 0;
}

サーバーは、自身のアプリケーション名とトピック名のグローバルアトムを取得し、 それがクライアントから渡されたものと一致するかを調べます。 一致する場合はクライアントとDDE通信を行うということで、 クライアントにWM_DDE_ACKを送信します。 このとき、第3引数にはサーバーのウインドウハンドルを指定します。 WM_DDE_ACKは様々なメッセージへの応答に使用することができますが、 WM_DDE_INITIATEに対する応答の場合はSendMessageを使用します。 これにより、クライアントのSendMessageが制御を返す際には、既にクライアントのWM_DDE_ACKが処理されていることになります。

WM_DDE_ACKを受信したクライアントは、サーバーのウインドウハンドルを保存し、 これを通じてサーバーからデータを取得したりデータを設定したりできるようになります。 この時点で、クライアントとサーバーの間にはDDE対話が確立されたことになります。 実を言うと対話の確立にはもう1つ別の方法もあり、 クライアントがWM_DDE_INITIATEのLPARAMに0を指定すれば、サーバーは無条件(一部例外有り)にクライアントとDDE対話を行うようになっています。 つまり、サーバーがクライアントにWM_DDE_ACKを返すということであり、 このメッセージのLPARAMにはアプリケーション名とトピック名を識別する値が含まれています。 この方法を使用すれば、不特定多数のDDEサーバーのアプリケーション名とトピック名を把握できるようになります。

今回のプログラムは、WM_DDE_INITIATEに応答したサーバーのアプリケーション名とトピック名を列挙します。

#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:
		hwndListBox = CreateWindowEx(0, TEXT("LISTBOX"), NULL, WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, hwnd, (HMENU)1, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		SendMessage((HWND)HWND_BROADCAST, WM_DDE_INITIATE, (WPARAM)hwnd, 0);
		return 0;

	case WM_DDE_ACK: {
		TCHAR szApp[256];
		TCHAR szTopic[256];
		TCHAR szBuf[2048];
		HWND  hwndServerDDE;

		GlobalGetAtomName(LOWORD(lParam), szApp, sizeof(szApp) / sizeof(TCHAR));
		GlobalGetAtomName(HIWORD(lParam), szTopic, sizeof(szTopic) / sizeof(TCHAR));
		hwndServerDDE = (HWND)wParam;

		wsprintf(szBuf, TEXT("AppName : %s Topic : %s"), szApp, szTopic);
		SendMessage(hwndListBox, LB_ADDSTRING, 0, (LPARAM)szBuf);

		PostMessage(hwndServerDDE, WM_DDE_TERMINATE, (WPARAM)hwnd, 0);
	
		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);
}

WM_CREATEでは、WM_DDE_INITIATEを現在存在する全てのウインドウに送信しています。 第4引数に0を指定しているため、DDEに対応するサーバーはクライアントとDDE対話を開始することになります。 サーバーがクライアントにWM_DDE_ACKを送れるようにするために、第3引数はクライアントのウインドウハンドルを指定します。 WM_DDE_ACKの処理は次のようになっています。

case WM_DDE_ACK: {
	TCHAR szApp[256];
	TCHAR szTopic[256];
	TCHAR szBuf[2048];
	HWND  hwndServerDDE;

	GlobalGetAtomName(LOWORD(lParam), szApp, sizeof(szApp) / sizeof(TCHAR));
	GlobalGetAtomName(HIWORD(lParam), szTopic, sizeof(szTopic) / sizeof(TCHAR));
	hwndServerDDE = (HWND)wParam;

	wsprintf(szBuf, TEXT("AppName : %s Topic : %s"), szApp, szTopic);
	SendMessage(hwndListBox, LB_ADDSTRING, 0, (LPARAM)szBuf);

	PostMessage(hwndServerDDE, WM_DDE_TERMINATE, (WPARAM)hwnd, 0);

	return 0;
}

WM_DDE_INITIATEへの応答であるWM_DDE_ACKは、LPARAMの下位ワードにアプリケーション名を示す値が格納され、 上位ワードにトピック名を示す値が格納されています。 これらの値の正体はグローバルアトムであるため、GlobalGetAtomNameを呼び出すことによって関連する文字列を取得できます。 wParamにはサーバーのウインドウハンドルが格納されており、 これはこれからサーバーを操作するために必要になるものですから、必ず保存するようにします。 ただ、今回の場合はアプリケーション名とトピック名を取得することが目的ですから、 WM_DDE_TERMINATEでサーバーとの対話を直ぐに終了するようにしています。

DDEに対応するサーバーを多く見つけるために、様々なアプリケーションを事前に起動しておくとよいでしょう。 たとえば、WordやExcelはDDEをサポートしますから、リストボックスに文字列が列挙されるはずです。 ただし、文字列が列挙されなかったからといって、起動しているアプリケーションがDDEに対応していないとは限りません。 たとえば、IEやVisual StudioはLPARAMに0を指定したWM_DDE_INITIATEには応答しませんが、 アプリケーション名とトピック名を正しく指定したWM_DDE_INITIATEには、応答するようになっています。 こうしたサーバーとDDE対話を行いたい場合、アプリケーション名とトピック名をどのように指定すればよいかが気になりますが、 これはレジストリから解決できる場合があります。 .cppファイルにVCExpress 2010が関連付けられているとして、次のレジストリキーを見てみましょう。

ユーザーがエクスプローラー上で.cppファイルを開こうとした場合、 エクスプローラーは上記キーのCommandからexeファイルのフルパスを取得し、 VCExpress 2010を起動しようとします。 このVCExpress 2010が起動された状態で、また何からの.cppファイルを開いた場合、 再びCommandを参照されてVCExpress 2010が起動されそうに思えますが、そのようにはならないはずです。 これは既にアプリケーションが起動されている場合は、そのアプリケーションがファイルを開くようにする機能が働くからであり、 ここで参照されるのがddeexec以下のApplicationとTopicです。 これらにはそれぞれアプリケーション名とトピック名が格納されていますから、 これをWM_DDE_INITIATEのLPARAMに指定することによって、VCExpress 2010とDDE対話を行うことができるわけです。 ちなみに、UAC環境下においてVCExpress 2010が管理者として起動されている場合は、 .cppファイルを開いても制限されたVCExpress 2010が新しく起動されることになります。 これは、制限されたプロセスと管理者プロセスのメッセージ通信が、UIPIという機能によって遮断されてしまうからです。 このことから分かるように、基本的にDDEによる対話はセキュリティコンテキスト(権限)が同じプロセス同士で行うべきといえます。

コマンドの実行例

ddeexecキー以下を参照してアプリケーション名とトピック名を取得できても、 そのアプリケーションをどのように操作したらよいかという疑問が残ります。 実は多くのDDEサーバーはコマンドの実行というものをサポートしており、 このコマンドの内容はddeexecキーの既定のエントリとして格納されています。 VCExpress 2010の場合はこれがOpen("%1")となっているため、 %1の部分をファイルパスに置き換えて実行することにより、 ファイルをオープンさせることができます。 次に例を示します。

#include <windows.h>

BOOL PostDDEMessage(HWND hwndServerDDE, UINT uMsg, HWND hwndClientDDE, UINT uLow, UINT uHigh);
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 hwndServerDDE = NULL;
	
	switch (uMsg) {

	case WM_CREATE: {
		ATOM atomApp, atomTopic;

		atomApp = GlobalAddAtom(TEXT("VCExpress.10.0"));
		atomTopic = GlobalAddAtom(TEXT("system"));
		SendMessage((HWND)HWND_BROADCAST, WM_DDE_INITIATE, (WPARAM)hwnd, MAKELONG(atomApp, atomTopic));
		GlobalDeleteAtom(atomApp);
		GlobalDeleteAtom(atomTopic);

		if (hwndServerDDE == NULL) {
			MessageBox(NULL, TEXT("DDE対話の確立に失敗しました。"), NULL, MB_ICONWARNING);
			return -1;
		}

		return 0;
	}

	case WM_LBUTTONDOWN: {
		LPVOID  lp;
		HANDLE  hData;
		CHAR    szData[] = "open(\"C:\\sample.cpp\")";
		DWORD   dwSize = lstrlenA(szData) + 1;

		hData = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, dwSize);
		lp = GlobalLock(hData);
		CopyMemory(lp, szData, dwSize);
		GlobalUnlock(hData);

		if (!PostDDEMessage(hwndServerDDE, WM_DDE_EXECUTE, hwnd, 0, (UINT)hData)) {
			GlobalFree(hData);
		}

		return 0;
	}
	
	case WM_DDE_ACK: {
		if (hwndServerDDE == NULL)
			hwndServerDDE = (HWND)wParam;
		else {
			ULONG uData;
			TCHAR szBuf[256];

			UnpackDDElParam(WM_DDE_ACK, lParam, (PUINT)&uData, NULL);

			wsprintf(szBuf, TEXT("%x"), uData);
		//	MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);

			FreeDDElParam(WM_DDE_ACK, lParam);
		}
		return 0;
	}

	case WM_DESTROY:
		if (hwndServerDDE != NULL)
			PostMessage(hwndServerDDE, WM_DDE_TERMINATE, (WPARAM)hwnd, 0);
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

BOOL PostDDEMessage(HWND hwndServerDDE, UINT uMsg, HWND hwndClientDDE, UINT uLow, UINT uHigh)
{
	LPARAM lParam = PackDDElParam(uMsg, uLow, uHigh);
	BOOL   bResult;

	bResult = PostMessage(hwndServerDDE, uMsg, (WPARAM)hwndClientDDE, lParam);
	if (!bResult)
		FreeDDElParam(uMsg, lParam);

	return bResult;
}

VCExpress 2010とのDDE対話を想定しているため、アプリケーション名にVCExpress.10.0に指定し、トピック名にsystemを指定しています。 これらはddeexecキー以下に格納されている値を基にしています。 マウスの左ボタンが押された場合は、サーバーにWM_DDE_EXECUTEを送信することになりますが、 これがコマンドの実行を示すメッセージです。 コードの全体的な内容については、次節以降で説明します。



戻る