EternalWindows
DDE / サーバーの作成

DDEMLを使用してDDEサーバーを作成することは、メッセージベースのDDEと比べてかなりの利点があります。 メッセージベースの場合は、通信相手となるクライアントのウインドウハンドルを保存しなければならず、 特にクライアントが複数存在する場合は、送られてきたメッセージがどのクライアントのものかを判断する必要があります。 しかし、DDEMLではこうした処理がアプリケーションから隠蔽されているため、 サーバーは自身に接続してきたクライアントを管理する必要はありません。 また、サーバーが返したデータは、常に適切なクライアントへと送られます。

DDEサーバーを作成するというのは、メッセージベースのDDE的に考えるならば、 クライアントからのWM_DDE_INITIATEを処理するということになります。 アプリケーションがこの動作を行うようになるには、DdeNameServiceを呼び出します。

HDDEDATA WINAPI DdeNameService(
  DWORD idInst,
  HSZ hsz1,
  HSZ hsz2,
  UINT afCmd
);

idInstは、インスタンス識別子を指定します。 hsz1は、サーバーのサービス名を指定します。 hsz2は、予約されているためNULLを指定します。 afCmdは、DDE対話を受け入れるようにする場合にDNS_REGISTERを指定し、 受け入れないようにする場合にDNS_UNREGISTERを指定します。

DdeNameServiceを呼び出せば、クライアントからの通知がDdeInitializeに指定したコールバック関数が送られることになります。 たとえば、クライアントがDdeClientTransaction(XTYP_REQUEST指定)や、WM_DDE_REQUESTをポストした場合は、XTYP_REQUESTという通知が送られることになるでしょう。 サーバーがデータの要求をサポートしていない場合は、この通知を検出する必要はありませんが、 サポートする場合は適切なデータをコールバック関数の戻り値として返すことになります。

今回のプログラムは、チェックボックスを表示しており、そのチェックボックスの状態をクライアントが取得したり設定したりできます。

#include <windows.h>

#define ID_CHECKBOX 100

HSZ   g_hszService = NULL;
HSZ   g_hszTopic = NULL;
HSZ   g_hszItem = NULL;
DWORD g_dwInst = 0;
HWND  g_hwndCheck = NULL;

HDDEDATA GetData();
BOOL SetData(char *p);
BOOL IsSupportItem(UINT uFormat, HSZ hsz1, HSZ hsz2);
HDDEDATA CALLBACK DdeCallback(UINT uType, UINT uFmt, HCONV hconv, HSZ hsz1, HSZ hsz2, HDDEDATA hdata, DWORD dwData1, DWORD dwData2);
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_CREATE: {
		ULONG uResult;

		uResult = DdeInitialize(&g_dwInst, DdeCallback, CBF_SKIP_REGISTRATIONS | CBF_SKIP_DISCONNECTS, 0);
		if (uResult != DMLERR_NO_ERROR)
			return -1;
		
		g_hszService = DdeCreateStringHandle(g_dwInst, TEXT("sample"), CP_WINNEUTRAL);
		g_hszTopic = DdeCreateStringHandle(g_dwInst, TEXT("my-topic"), CP_WINNEUTRAL);
		g_hszItem =  DdeCreateStringHandle(g_dwInst, TEXT("check"), CP_WINNEUTRAL);

		DdeNameService(g_dwInst, g_hszService, NULL, DNS_REGISTER);

		g_hwndCheck = CreateWindowEx(0, TEXT("BUTTON"), TEXT("チェック"), WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, 30, 80, 170, 40, hwnd, (HMENU)ID_CHECKBOX, ((LPCREATESTRUCT)lParam)->hInstance, NULL);

		return 0;
	}
	
	case WM_COMMAND: {
		int nId = LOWORD(wParam);
		if (nId == ID_CHECKBOX)
			DdePostAdvise(g_dwInst, g_hszTopic, g_hszItem);
		return 0;
	}

	case WM_DESTROY:
		if (g_dwInst != 0) {
			DdeNameService(g_dwInst, g_hszService, NULL, DNS_UNREGISTER);
			DdeFreeStringHandle(g_dwInst, g_hszItem);
			DdeFreeStringHandle(g_dwInst, g_hszTopic);
			DdeFreeStringHandle(g_dwInst, g_hszService);
			DdeUninitialize(g_dwInst);
		}
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

HDDEDATA CALLBACK DdeCallback(UINT uType, UINT uFmt, HCONV hconv, HSZ hsz1, HSZ hsz2, HDDEDATA hdata, DWORD dwData1, DWORD dwData2)
{
	switch (uType) {

	case XTYP_CONNECT:
		if (hsz1 != g_hszTopic)
			return (HDDEDATA)FALSE;
		return (HDDEDATA)TRUE;
	
	case XTYP_REQUEST:
		if (!IsSupportItem(uFmt, hsz1, hsz2))
			return NULL;
		return GetData();

	case XTYP_POKE: {
		char *p;
		BOOL bResult;
		
		if (!IsSupportItem(uFmt, hsz1, hsz2)) {
			DdeFreeDataHandle(hdata);
			return DDE_FNOTPROCESSED;
		}
		
		p = (char *)DdeAccessData(hdata, NULL);
		bResult = SetData(p);
		DdeUnaccessData(hdata);
		DdeFreeDataHandle(hdata);
		if (!bResult)
			return DDE_FNOTPROCESSED;

		return (HDDEDATA)DDE_FACK;
	}
	
	case XTYP_ADVSTART:
		if (!IsSupportItem(uFmt, hsz1, hsz2))
			return (HDDEDATA)FALSE;
		return (HDDEDATA)TRUE;
	
	case XTYP_ADVREQ:
		if (!IsSupportItem(uFmt, hsz1, hsz2))
			return NULL;
		return GetData();

	default:
		break;

	}
	
	return (HDDEDATA)FALSE;
}

HDDEDATA GetData()
{
	BOOL bCheck;
	CHAR szData[256];
	
	bCheck = SendMessage(g_hwndCheck, BM_GETCHECK, 0, 0) == BST_CHECKED;
	if (bCheck)
		lstrcpyA(szData, "true");
	else
		lstrcpyA(szData, "false");
	
	return (HDDEDATA)DdeCreateDataHandle(g_dwInst, (LPBYTE)szData, lstrlenA(szData) + 1, 0, g_hszItem, CF_TEXT, 0);
}

BOOL SetData(char *p)
{
	BOOL bCheck;

	if (lstrcmpA(p, "true") == 0)
		bCheck = TRUE;
	else if (lstrcmpA(p, "false") == 0)
		bCheck = FALSE;
	else
		return FALSE;

	SendMessage(g_hwndCheck, BM_SETCHECK, bCheck ? BST_CHECKED : BST_UNCHECKED, 0);

	return TRUE;
}

BOOL IsSupportItem(UINT uFormat, HSZ hsz1, HSZ hsz2)
{
	return hsz1 == g_hszTopic && hsz2 == g_hszItem && uFormat == CF_TEXT;
}

WM_CREATEでは、DdeInitializeとDdeNameServiceを呼び出しています。 DdeInitializeの第3引数は0でも問題ありませんが、 不要な通知を受け取りたくない場合は、その通知を示す定数を指定することができます。 たとえば、DdeNameServiceは既定でコールバック関数にXTYP_REGISTERを通知しますが、 CBF_SKIP_REGISTRATIONSを指定すればこれを受け取らないようにできます。 また、クライアントがDdeDisconnectを呼び出した場合はXTYP_DISCONNECTが通知されますが、 CBF_SKIP_DISCONNECTSを指定すればこれを受け取らないようにできます。 サーバーには、サービス名やトピック名、アイテム名なども必要になりますが、 これらの文字列ハンドルもWM_CREATEで作成しておきます。 コールバック関数に送られる通知を1つずつ見ていきます。

case XTYP_CONNECT:
	if (hsz1 != g_hszTopic)
		return (HDDEDATA)FALSE;

	return (HDDEDATA)TRUE;

XTYP_CONNECTは、クライアントがDdeConnectを呼び出した場合や、WM_DDE_INITIATEを送信した場合に送られます。 hsz1にはクライアントが指定したトピック名が格納されており、これがサーバーがサポートする名前と一致するかを調べます。 一致しない場合はDDE対話ができないことを示すFALSEを返し、一致する場合は対話可能を示すTRUEを返します。

case XTYP_REQUEST:
	if (!IsSupportItem(uFmt, hsz1, hsz2))
		return NULL;
	return GetData();

XTYP_REQUESTは、DdeClientTransaction(XTYP_REQUEST指定)を呼び出した場合や、WM_DDE_REQUESTをポストした場合に呼ばれます。 hsz1にはクライアントが指定したトピック名が格納されており、hsz2にはデータを識別するアイテム名が格納されています。 IsSupportItemは、これらとフォーマットが適切であるかを調べる関数であり、 適切である場合はDdeCreateDataHandleで作成したハンドルを返すようにします。 GetDataの実装は次のようになっています。

HDDEDATA GetData()
{
	BOOL bCheck;
	CHAR szData[256];
	
	bCheck = SendMessage(g_hwndCheck, BM_GETCHECK, 0, 0) == BST_CHECKED;
	if (bCheck)
		lstrcpyA(szData, "true");
	else
		lstrcpyA(szData, "false");
	
	return (HDDEDATA)DdeCreateDataHandle(g_dwInst, (LPBYTE)szData, lstrlenA(szData) + 1, 0, g_hszItem, CF_TEXT, 0);
}

チェックボックスの現在の状態を確認し、チェックが付いている場合はtrue、チェックが付いていない場合はfalseを返すようにします。 DdeCreateDataHandleの第2引数には作成するデータを指定し、第3引数にはデータのサイズを指定します。 第5引数はデータを識別するアイテム名であり、第6引数はデータのフォーマットを指定します。 今回のデータはテキスト形式であるため、CF_TEXTを指定しています。

case XTYP_POKE: {
	char *p;
	BOOL bResult;
	
	if (!IsSupportItem(uFmt, hsz1, hsz2)) {
		DdeFreeDataHandle(hdata);
		return DDE_FNOTPROCESSED;
	}
	
	p = (char *)DdeAccessData(hdata, NULL);
	bResult = SetData(p);
	DdeUnaccessData(hdata);
	DdeFreeDataHandle(hdata);
	if (!bResult)
		return DDE_FNOTPROCESSED;

	return (HDDEDATA)DDE_FACK;
}

XTYP_POKEは、DdeClientTransaction(XTYP_POKE指定)を呼び出した場合や、WM_DDE_POKEをポストした場合に呼ばれます。 hsz1とhsz2は、XTYP_REQUESTと同じようにトピック名とアイテム名が格納されています。 これらをサポートしていない場合は、渡されたデータをDdeFreeDataHandleで開放し、 DDE_FNOTPROCESSEDを返すようにします。 サポートする場合はDdeAccessDataでデータを取得し、hsz2で識別されるアイテムにデータを設定してDDE_FACKを返します。 SetDataの内部は次のようになっています。

BOOL SetData(char *p)
{
	BOOL bCheck;

	if (lstrcmpA(p, "true") == 0)
		bCheck = TRUE;
	else if (lstrcmpA(p, "false") == 0)
		bCheck = FALSE;
	else
		return FALSE;

	SendMessage(g_hwndCheck, BM_SETCHECK, bCheck ? BST_CHECKED : BST_UNCHECKED, 0);

	return TRUE;
}

BM_SETCHECKは、チェックボックスの状態を変更する際に使用できます。 文字列がtrueである場合はチェックをするということでBST_CHECKEDを指定し、 falseの場合はチェックを外すBST_UNCHECKEDを指定します。

case XTYP_ADVSTART:
	if (!IsSupportItem(uFmt, hsz1, hsz2))
		return (HDDEDATA)FALSE;
	return (HDDEDATA)TRUE;

XTYP_ADVSTARTは、DdeClientTransaction(XTYP_ADVSTART指定)を呼び出した場合や、WM_DDE_ADVISEをポストした場合に呼ばれます。 hsz1とhsz2は、XTYP_POKEと同じようにトピック名とアイテム名が格納されています。 ここでTRUEを返すことは、hsz2のアイテムのデータが変更された際に、 クライアントへ通知することを意味します。 今回の場合のアイテムというのはチェックボックスであり、 ユーザーがクリックした場合は次のコードが実行されます。

case WM_COMMAND: {
	int nId = LOWORD(wParam);
	if (nId == ID_CHECKBOX)
		DdePostAdvise(g_dwInst, g_hszTopic, g_hszItem);
	return 0;
}

チェックボックスの状態が変化した場合はクライアントに通知を行いため、 このような際にはDdePostAdviseを呼び出すようにします。 そうすると、コールバック関数にXTYP_ADVREQが通知されます。

case XTYP_ADVREQ:
	if (!IsSupportItem(uFmt, hsz1, hsz2))
		return NULL;
	return GetData();

hsz1とhsz2については、XTYP_ADVSTARTと同様です。 戻り値は通知したいデータになるため、GetDataの戻り値をそのまま指定できます。

クライアントの作成

今回のサーバーに対応するクライアントの例を示します。 マウスの左ボタンが押された場合はチェックの状態を取得し、 マウスの右ボタンが押された場合はチェックを付けるようにします。 サーバー上でチェックの状態が変化した場合はそれを検出します。

#include <windows.h>

HDDEDATA CALLBACK DdeCallback(UINT uType, UINT uFmt, HCONV hconv, HSZ hsz1, HSZ hsz2, HDDEDATA hdata, DWORD dwData1, DWORD dwData2);
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 DWORD dwInst = 0;
	static HCONV hConv = NULL;

	switch (uMsg) {

	case WM_CREATE: {
		HSZ   hszService, hszTopic, hszItem;
		ULONG uResult;
	
		uResult = DdeInitialize(&dwInst, DdeCallback, APPCMD_CLIENTONLY, 0);
		if (uResult != DMLERR_NO_ERROR)
			return -1;

		hszService = DdeCreateStringHandle(dwInst, TEXT("sample"), CP_WINNEUTRAL);
		hszTopic = DdeCreateStringHandle(dwInst, TEXT("my-topic"), CP_WINNEUTRAL);
		hConv = DdeConnect(dwInst, hszService, hszTopic, NULL);
		DdeFreeStringHandle(dwInst, hszService);
		DdeFreeStringHandle(dwInst, hszTopic);
		if (hConv == NULL) {
			MessageBox(NULL, TEXT("DDE対話の確立に失敗しました。"), NULL, MB_ICONWARNING);
			return -1;
		}

		hszItem = DdeCreateStringHandle(dwInst, TEXT("check"), CP_WINNEUTRAL);
		DdeClientTransaction(NULL, 0, hConv, hszItem, CF_TEXT, XTYP_ADVSTART | XTYPF_ACKREQ, 1000, NULL);
		DdeFreeStringHandle(dwInst, hszItem);

		return 0;
	}
	
	case WM_LBUTTONDOWN: {
		HSZ      hszItem;
		HDDEDATA hData;
		char     szData[256];

		hszItem = DdeCreateStringHandle(dwInst, TEXT("check"), CP_WINNEUTRAL);
		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);
		DdeFreeStringHandle(dwInst, hszItem);
		
		return 0;
	}
	
	case WM_RBUTTONDOWN: {
		HSZ  hszItem;
		char szData[] = "true";

		hszItem = DdeCreateStringHandle(dwInst, TEXT("check"), CP_WINNEUTRAL);
		DdeClientTransaction((LPBYTE)szData, sizeof(szData), hConv, hszItem, CF_TEXT, XTYP_POKE, 1000, NULL);
		DdeFreeStringHandle(dwInst, hszItem);
		
		return 0;
	}

	case WM_DESTROY:
		if (dwInst != 0) {
			if (hConv != NULL) {
				HSZ hszItem;

				hszItem = DdeCreateStringHandle(dwInst, TEXT("check"), CP_WINNEUTRAL);
				DdeClientTransaction(NULL, 0, hConv, hszItem, CF_TEXT, XTYP_ADVSTOP, 1000, NULL);
				DdeFreeStringHandle(dwInst, hszItem);
				
				DdeDisconnect(hConv);
			}
			DdeUninitialize(dwInst);
		}
		
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

HDDEDATA CALLBACK DdeCallback(UINT uType, UINT uFmt, HCONV hconv, HSZ hsz1, HSZ hsz2, HDDEDATA hdata, DWORD dwData1, DWORD dwData2)
{
	switch (uType) {

	case XTYP_ADVDATA: {
		if (hdata != NULL) {
			char szData[256];
			DdeGetData(hdata, (LPBYTE)szData, sizeof(szData), 0);
			MessageBoxA(NULL, szData, "OK", MB_OK);
		}
		else
			MessageBox(NULL, TEXT("データの変更が通知されました。"), TEXT("OK"), MB_OK);
		return (HDDEDATA)TRUE;
	}

	default:
		break;

	}
	
	return (HDDEDATA)FALSE;
}

WM_CREATEではDdeConnectを呼び出して、サーバーとDDE対話を確立しようとしています。 このときに指定するサービス名とトピック名は、今回のサーバーで定義していたものと同一になります。 DdeClientTransactionにXTYP_ADVSTARTを指定しているのは、サーバーとのデータリンクを確立するためです。 XTYPF_NODATAを指定すればデータが変更されたことだけが通知されるウォームリンクになり、 XTYPF_ACKREQを指定すれば変更されたデータそのものが通知されるホットリンクになります。 通知の際には、コールバック関数のuTypeがXTYP_ADVDATAになり、 ホットリンクの場合はDdeGetDataで変更後のデータを取得できます。 マウスの左ボタンが押された場合は、DdeClientTransactionにXTYP_REQUESTを指定しています。 この場合は戻り値としてデータのハンドルが返るため、それをDdeGetDataに指定します。 マウスの右ボタンが押された場合は、DdeClientTransactionにXTYP_POKEを指定しています。 この場合は、第1引数に渡したいデータを指定し、第2引数にデータのサイズを指定します。 trueを指定していることから、サーバーのチェックボックスにチェックが付くことになります。



戻る