EternalWindows
DDE / データリンク

DDEにおけるデータリンクとは、サーバー内のデータが変更された際に、クライアントへ通知を行う機能のことを指します。 データリンクにはウォームリンクとホットリンクの2種類が存在し、 前者はデータが変更されたことだけを通知しますが、 後者は変更されたデータそのものを通知します。 データリンクの確立はWM_DDE_ADVISEで可能であり、それはWM_DDE_UNADVISEを送るまで有効です。 クライアントは、サーバーからの通知をWM_DDE_DATAとして受け取ることになります。

今回のプログラムは、Excelの特定のセルが変更された際に通知を受け取ります。 プログラムを起動する前に、sample.xlsxを開いたExcelを起動しておいてください。

#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;
	static BOOL bWarmLink = FALSE;
	
	switch (uMsg) {

	case WM_CREATE: {
		ATOM      atomApp, atomTopic, atomItem;
		HANDLE    hData;
		DDEADVISE *lpAdvise;

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

		atomItem = GlobalAddAtom(TEXT("R2C1"));

		hData = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, sizeof(DDEADVISE));
		lpAdvise = (DDEADVISE *)GlobalLock(hData);
		lpAdvise->cfFormat  = CF_TEXT;
		lpAdvise->fAckReq   = TRUE;
		lpAdvise->fDeferUpd = bWarmLink;
		GlobalUnlock(hData);

		if (!PostDDEMessage(hwndServerDDE, WM_DDE_ADVISE, hwnd, (UINT)hData, atomItem)) {
			GlobalDeleteAtom(atomItem);
			GlobalFree(hData);
		}

		return 0;
	}

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

			UnpackDDElParam(WM_DDE_ACK, lParam, (PUINT)&uData, (PUINT)&atomItem);
			wsprintf(szBuf, TEXT("%x"), uData);
		//	MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);

			GlobalDeleteAtom(atomItem);
			FreeDDElParam(WM_DDE_ACK, lParam);
		}
		return 0;
	}
	
	case WM_DDE_DATA: {
		ATOM    atomItem;
		HANDLE  hData;
		DDEDATA *lpData;
		BOOL    bRelease;
		BOOL    bDeleteAtom = TRUE;

		UnpackDDElParam(WM_DDE_DATA, lParam, (PUINT)&hData, (PUINT)&atomItem);
		if (hData == NULL) {
			MessageBox(hwnd, TEXT("データの変更が通知されました。"), TEXT("OK"), MB_OK);
			GlobalDeleteAtom(atomItem);
			FreeDDElParam(WM_DDE_DATA, lParam);
			return 0;
		}

		lpData = (DDEDATA *)GlobalLock(hData);

		if (lpData->cfFormat == CF_TEXT) {
			MessageBoxA(hwnd, (LPSTR)lpData->Value, "OK", MB_OK);
			if (lpData->fAckReq) {
				bDeleteAtom = FALSE;
				PostDDEMessage(hwndServerDDE, WM_DDE_ACK, hwnd, 0x8000, atomItem);
			}
		}
		else {
			bDeleteAtom = FALSE;
			PostDDEMessage(hwndServerDDE, WM_DDE_ACK, hwnd, 0, atomItem);
		}
		
		bRelease = lpData->fRelease;
		GlobalUnlock(hData);
		if (bRelease)
			GlobalFree(hData);
		if (bDeleteAtom)
			GlobalDeleteAtom(atomItem);

		FreeDDElParam(WM_DDE_DATA, lParam);

		return 0;
	}
	
	case WM_DESTROY:
		if (hwndServerDDE != NULL) {
			ATOM atomItem;
			atomItem = GlobalAddAtom(TEXT("check"));
			if (!PostDDEMessage(hwndServerDDE, WM_DDE_UNADVISE, hwnd, 0, atomItem))
				GlobalDeleteAtom(atomItem);
			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;
}

WM_CREATEではサーバーとの対話を確立すると共に、WM_DDE_ADVISEでデータリンクも確立しています。

atomItem = GlobalAddAtom(TEXT("R2C1"));

hData = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, sizeof(DDEADVISE));
lpAdvise = (DDEADVISE *)GlobalLock(hData);
lpAdvise->cfFormat  = CF_TEXT;
lpAdvise->fAckReq   = TRUE;
lpAdvise->fDeferUpd = bWarmLink;
GlobalUnlock(hData);

if (!PostDDEMessage(hwndServerDDE, WM_DDE_ADVISE, hwnd, (UINT)hData, atomItem)) {
	GlobalDeleteAtom(atomItem);
	GlobalFree(hData);
}

WM_DDE_ADVISEのLPARAMには、DDEADVISE構造体とグローバルアトムが含まれていなければなりません。 グローバルアトムは変化を検出したいアイテムを識別していなければならず、 上記の場合であればR2C1に相当するセルのデータ変更を検出する意味を持ちます。 DDEADVISE構造体はGlobalAllocで確保するようにし、GlobalLockを呼び出してからメンバにアクセスします。 cfFormatはデータを受け取る形式であり、テキスト形式を望む場合はCF_TEXTでよいでしょう。 fAckReqは、WM_DDE_DATAにおけるDDEDATA構造体のfAckReqに格納したい値を指定します。 fDeferUpdは、ウォームリンクの場合にTRUEを指定し、ホットリンクの場合にFALSEを指定します。 bWarmLinkはFALSEで初期化されているため、データリンクの種類はホットリンクということになり、 データ変更の通知の際にはデータそのものが送られることになります。 WM_DDE_ADVISEを送れば応答としてWM_DDE_ACKが返ることになっており、 uDataが0x8000であればリンクは成功し、0の場合はリンクが失敗したことを意味します。

WM_DDE_ADVISEが送られた後でR2C1のセルが変更されたら、WM_DDE_DATAがクライアントに送られることになります。 処理の内容としては、WM_DDE_REQUESTでデータを取得していた際と同じ要領ですが、 bWarmLinkがTRUEの場合はデータを参照しないようにします。 これは、ウォームリンクが変更されたデータまでは通知しないからであり、 UnpackDDElParamで取得できるhDataがNULLになるためです。

クリップボードからの取得

通常、クライアントとのデータリンクをサポートしているサーバーは、 クリップボードにLink形式のデータを設定する機能を持っています。 たとえば、Excelのセルをコピーした場合は、Link形式のデータがクリップボードに設定することになっています。 Link形式のフォーマットは次のようになります。

application\0topic\0item\0\0

applicationの部分にはアプリケーション名が格納され、topicの部分にはトピック名、 itemの部分にはアイテム名が格納されます。 各情報は\0文字で区切られているため、これを頼りに情報を取り出すことになります。 次に例を示します。

#include <windows.h>

HWND g_hwndServerDDE = NULL;

BOOL PasteLink(HWND hwnd);
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)
{
	switch (uMsg) {
	
	case WM_LBUTTONDOWN:
		PasteLink(hwnd);
		return 0;

	case WM_DDE_ACK: {
		if (g_hwndServerDDE == NULL)
			g_hwndServerDDE = (HWND)wParam;
		return 0;
	}
	
	case WM_DDE_DATA: {
		ATOM    atomItem;
		HANDLE  hData;
		DDEDATA *lpData;
		BOOL    bRelease;
		BOOL    bDeleteAtom = TRUE;

		UnpackDDElParam(WM_DDE_DATA, lParam, (PUINT)&hData, (PUINT)&atomItem);
		lpData = (DDEDATA *)GlobalLock(hData);

		if (lpData->cfFormat == CF_TEXT) {
			MessageBoxA(hwnd, (LPSTR)lpData->Value, "OK", MB_OK);
			if (lpData->fAckReq) {
				bDeleteAtom = FALSE;
				PostDDEMessage(g_hwndServerDDE, WM_DDE_ACK, hwnd, 0x8000, atomItem);
			}
		}
		else {
			bDeleteAtom = FALSE;
			PostDDEMessage(g_hwndServerDDE, WM_DDE_ACK, hwnd, 0, atomItem);
		}
		
		bRelease = lpData->fRelease;
		GlobalUnlock(hData);
		if (bRelease)
			GlobalFree(hData);
		if (bDeleteAtom)
			GlobalDeleteAtom(atomItem);

		FreeDDElParam(WM_DDE_DATA, lParam);
		
		PostMessage(g_hwndServerDDE, WM_DDE_TERMINATE, (WPARAM)hwnd, 0);
		g_hwndServerDDE = NULL;
		return 0;
	}

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

BOOL PasteLink(HWND hwnd)
{
	ATOM   atomApp, atomTopic, atomItem;
	UINT   uFormat;
	HANDLE hData;
	LPSTR  lpszData, lpsz;
	CHAR   szApp[256], szTopic[256], szItem[256];
	int    i, nCount;

	OpenClipboard(hwnd);
	uFormat = RegisterClipboardFormat(TEXT("Link"));
	hData = GetClipboardData(uFormat);
	if (hData == NULL) {
		MessageBox(NULL, TEXT("Link形式のデータが存在しません。"), NULL, MB_ICONWARNING);
		CloseClipboard();
		return FALSE;
	}

	lpszData = (LPSTR)GlobalLock(hData);

	nCount = 0;
	lpsz = lpszData;
	for (i = 0;;i++) {
		if (lpsz[i] == '\0') {
			if (nCount == 0)
				lstrcpyA(szApp, lpsz);
			else if (nCount == 1)
				lstrcpyA(szTopic, lpsz);
			else if (nCount == 2) {
				lstrcpyA(szItem, lpsz);
				break;
			}
			else
				;
			nCount++;
			lpsz = &lpsz[i + 1];
		}
	}
	GlobalUnlock(hData);
	CloseClipboard();

	atomApp = GlobalAddAtomA(szApp);
	atomTopic = GlobalAddAtomA(szTopic);
	SendMessage((HWND)HWND_BROADCAST, WM_DDE_INITIATE, (WPARAM)hwnd, MAKELONG(atomApp, atomTopic));
	GlobalDeleteAtom(atomApp);
	GlobalDeleteAtom(atomTopic);

	if (g_hwndServerDDE == NULL)
		return FALSE;

	atomItem = GlobalAddAtomA(szItem);
	if (!PostDDEMessage(g_hwndServerDDE, WM_DDE_REQUEST, hwnd, CF_TEXT, atomItem))
		GlobalDeleteAtom(atomItem);

	return TRUE;
}

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;
}

Link形式のクリップボードフォーマットは、RegisterClipboardFormatにLinkを指定すれば取得できます。 これをGetClipboardDataに指定してNULLが返る場合は、 Link形式のデータが設定されていないことを意味するため、終了するようにしています。 for文ではデータからアプリケーション名、トピック名、アイテム名を取得するようにします。 WM_DDE_INITIATEの後にWM_DDE_REQUESTを送っていますが、 WM_DDE_INITIATEが制御を返す前にWM_DDE_ACKでサーバーのウインドウハンドルを受け取っているため、 これは問題ありません。 WM_DDE_REQUESTを送った場合はWM_DDE_ACKかWM_DDE_DATAが返る可能性がありますが、 アイテム名が不正になることは考えられないため、WM_DDE_ACKが返ることはないでしょう。 WM_DDE_DATAでデータを受け取ったら、WM_DDE_TERMINATEで対話を終了しています。 これは今回のプログラムが、WM_DDE_INITIATEを複数回呼び出す可能性があるためです。



戻る