Herman (Mr. DDE) Rodent
Microsoft Developer Network Technology Group
Created: November 30, 1992
Click to open or copy the files in the DDEServ sample application for this technical article.
Click to open or copy the files in the DDERecon sample application for this technical article.
Adding dynamic data exchange (DDE) server support to an application has never been easier. This article describes a code module you can include in your own application that makes adding DDE server support trivial. The code module makes use of the dynamic data exchange management library (DDEML) to implement the DDE protocol and provides the following features:
This article requires considerable familiarity with DDE concepts—it's not a good place to start learning about DDE. Please refer to the Microsoft® Windows™ version 3.1 Software Development Kit (SDK) Programmer's Reference, Volume 1: Overview, Part 2: Extension Libraries for a general introduction to DDE and DDEML. The following articles on the Microsoft Developer Network CD (Technical Articles, Windows Articles, OLE and DDE Articles) provide more detail on various DDE topics:
Creating a dynamic data exchange (DDE) server from scratch or adding DDE server support to an existing application can be a real nightmare. The DDE protocol is quite complex, and correctly implementing it is not a trivial matter. The dynamic data exchange management library (DDEML) has improved matters considerably. DDEML is a support library that implements the DDE protocol and provides an application programming interface (API) to the developer to simplify DDE implementation and make the resulting application conform more exactly to the DDE protocol.
DDEServ, the source code sample included with this article, goes further than the DDEML API set and provides a single code module that fully implements a DDE server. The code module exports a number of functions that will greatly simplify implementation of a DDE server.
This article describes the process of creating a DDE server from scratch using the sample code module and DDEML to provide most of the basic functionality. The server can support an arbitrary number of topics and topic/item pairs. The code maintains a set of simple linked lists and provides functions to add and remove topics and topic/item pairs at any time. The code tracks conversations so that they can be automatically terminated if their topic is removed while a conversation is still active.
The sample code module includes a parser for DDE execute commands that conforms to the syntax used by applications such as Microsoft® Excel and the Windows™ Program Manager. Functions are provided to allow commands to be attached to or removed from a topic at any time.
The functions provided by the sample code are listed below in the "Function Description" section. The structures used to construct the various lists are documented in the "Data Types and Structures" section.
This section gives a step-by-step description of adding DDE server support to an application. The sample code has #ifdef blocks, which follow the steps described here. You can test the steps in the sample code by changing the #define STEP_n statements in the beginning of the DDESERV.C file and then building the server. Here's what the code at the start of the DDESERV.C module looks like if you are preparing to build the sample for Step 1:
#define STEP_1 // Initialization and termination
// #define STEP_2 // Topic/Item pairs
// #define STEP_3 // Additional System topic items
// #define STEP_4 // Execute commands
Before you start modifying your own code, I suggest you take a look at DDEServ, one of the two sample applications that accompanies this technical article, and follow the steps suggested here to familiarize yourself with what is going on at each step. Later, when you make these modifications to your own code, you will be familiar with the behavior you are expecting and have some practice at testing a server.
You should start by building the sample server application without any of the STEP_ statements defined. The application should start but have no DDE service registered. You can verify this by running DDERecon, the other sample application that accompanies this article, and seeing that DDEServ is not listed as a DDE service.
The first step in implementing DDE server support is to add a call to InitializeDDE. An ideal time to do this is right after the main window of the application has been created. The InitializeDDE function calls DDEML to register the application with the library and get the instance identifier from DDEML. The code then goes on to add a standard set of System topic items and finally to register the name of the service the server will provide.
Note The sample code supports only one service per server.
The initialization call looks like this:
InitializeDDE(hInstance,
SZ_SERVICENAME,
&dwDDEInst,
NULL,
0);
When the application receives the WM_DESTROY message, it should call UninitializeDDE, which unregisters the service name, frees all the resources used, and releases DDEML.
The call looks like this:
case WM_DESTROY:
UninitializeDDE();
PostQuitMessage(0);
break;
Having added these two calls to initialize the server and close it down when the application exits, you can now compile the code and test it. You can test its DDE capabilities by running the DDERecon sample application. DDERecon should show the service name in the services list and should show that the minimum System topic support is working. Figure 1 shows what the DDERecon connect dialog box, Create DDE Link, should look like for a Step 1 build of the DDEServ sample application.
Figure 1. The DDERecon connect dialog box shows a Step 1 implementation of DDEServ.
The next step is to add support for each topic/item pair your server supports. This code is added to the existing initialization code. To add a topic/item pair, call the AddDDEItem function. It is not necessary to make a call to AddDDETopic first. If the topic does not exist, AddDDEItem will create it. The sample application supports a topic called Info and an item called Value. The Value item is a 16-bit signed integer quantity that can be interrogated and set from a DDE client application.
The initialization code now includes this call:
AddDDEItem(SZ_INFO,
SZ_VALUE,
MyFormats,
ValueRequest,
ValuePoke);
MyFormats is a null-terminated list of Clipboard formats supported by the server. The DDEServ sample application supports only the CF_TEXT format. ValueRequest and ValuePoke are functions that will be called when a client tries, respectively, to access and alter the item data. Here's the request handler for the Value item:
HDDEDATA ValueRequest(UINT wFmt, HSZ hszTopic, HSZ hszItem)
{
char buf[40];
wsprintf(buf, "%d", giValue);
return MakeCFText(wFmt, buf, hszItem);
}
The value is converted to a string and then to a DDE data item. The MakeCFText function is the skeleton of what might be a more complete piece of code. The sample version will only convert to CF_TEXT format. A more complete server might support other formats.
HDDEDATA MakeCFText(UINT wFmt, LPSTR lpszStr, HSZ hszItem)
{
if (wFmt != CF_TEXT) return NULL;
return DdeCreateDataHandle(dwDDEInst,
lpszStr,
lstrlen(lpszStr)+1,
0,
hszItem,
CF_TEXT,
NULL);
}
Try building the sample server with the STEP_2 statement defined. Run DDERecon again and see that the server now has an Info topic, and the Info topic has a Value item. Note that it also has Formats and TopicItemList items. These were added by the STDDDE module when the Info topic was added. Figure 2 shows the DDERecon connect dialog box at this point.
Figure 2. The DDERecon connect dialog box after the Info topic has been added in Step 2.
One of the optional System topic items in a DDE server is the Help item. You can add items to the System topic in the same way you can add items to any topic. The sample application adds a Help item to the System topic with this call:
AddDDEItem(SZDDESYS_TOPIC,
SZDDESYS_ITEM_HELP,
MyFormats,
SysHelpRequest,
NULL);
The SysHelpRequest function formats a simple string of text and returns it to the client. Try defining STEP_3 in the sample and building it. Use DDERecon to view the System topic items and connect to the Help item. Figure 3 shows the Help item in the System topics list of the DDERecon connect dialog box.
Figure 3. The DDERecon connect dialog box after the Help item is added to the System topic in Step 3.
DDE servers may optionally support a set of execute commands. The syntax of these commands and other details of their implementation is covered in the "DDE Execute Strings" technical article on the Microsoft Developer Network CD. The sample server, DDERecon, supports an Actions topic with a single Text command. A client can connect to the Actions topic and issue execute requests to it. The Text command follows Microsoft Excel syntax :
[Text(20,30,"Hi there Tiger!")]
The first argument is the x ordinate, the second is the y ordinate, and the third is the text itself. The text string is output to the client area of the server main window. The coordinates are in pixels.
The STDDDE.C module provides a command parser for execute function requests. It assumes that the syntax of these requests conforms to the Microsoft Excel standard. The code to add a command looks like this:
AddDDEExecCmd(SZ_CMDTOPIC, "Text", TextFn, 3, 3);
Note that the topic is added automatically if it doesn't already exist. The fourth and fifth arguments describe the minimum and maximum number of arguments, respectively, that the Text command takes. The command processing functions receive their arguments in a list rather like the conventional C argc, argv method. Here's the handler for the Text command:
BOOL FAR TextFn(PDDETOPICINFO pTopic,
LPSTR pszResult,
UINT uiResultSize,
UINT uiNargs,
LPSTR FAR *ppArgs)
{
HDC hDC;
int x, y;
char buf[32];
_fstrncpy(buf, ppArgs[0], sizeof(buf)-1);
x = atoi(buf);
_fstrncpy(buf, ppArgs[1], sizeof(buf)-1);
y = atoi(buf);
hDC = GetDC(ghwndMain);
TextOut(hDC, x, y, ppArgs[2], _fstrlen(ppArgs[2]));
ReleaseDC(ghwndMain, hDC);
return TRUE;
}
The coordinates are converted to integer values by copying their string representations to local memory and using a C run-time function to convert them. TextOut is then used to write the text to the device context (DC) of the application's main window.
This function never fails, but of course there are those that do! If the function detects an error, it can return an error information string by copying it to the buffer pointed to by pszResult. If the client application has requested that return information be saved by using the Result command to name an item in which to save the data, then your return string will be saved in that item for later retrieval by the client. See the "DDE Execute Strings" technical article for more details about how this protocol works. The sample code module supports a reduced version of this protocol.
The preceding section covered the basics of adding DDE server support to an application. This section describes some additional possibilities when using the STDDDE.C sample code module.
A server need not have a fixed set of topics or topic/item pairs. The sample code module (STDDDE.C) provides the RemoveDDEItem and RemoveDDETopic functions to remove an item from a topic or to remove a complete topic. Note that removing all the items from a topic doesn't delete the topic. The STDDDE.C module tracks conversations, so it can disconnect them if their topic is deleted while they are still active.
As an example of where this may be used, consider an application with a multiple document interface (MDI) such as Microsoft Excel, which can open multiple files. For each open file, the application can create a topic so that the file can be manipulated by other DDE requests. When a file is opened, its path is added as a topic, and when the file is closed, that topic is removed. If you have a copy of Microsoft Excel, try opening a number of spreadsheets and running the DDERecon application to view the Microsoft Excel topic list. Figure 4 shows an example of the topics Microsoft Excel creates.
Figure 4. Microsoft Excel creates topics for each file it has open.
Commands, too, may be added to or removed from a topic at any time. They are added by calling AddDDEExecCmd and removed by calling RemoveDDEExecCmd. If you intend to have a dynamically varying command list for your server, you should consider how a client application is going to be able to see what the currently available command set is. There is no standard way of doing this, but you might consider adding another item such as CommandList to the topic that supports the commands. Another way to do this is to consider the set of commands as a protocol supported by the server, give this protocol a name, and include it in the System topic protocols item list. You can publish the command set for your protocol, and client applications can interrogate your server to see if it supports the named protocol.
It is not necessary to have a different handler for every topic/item pair added with the AddDDEItem function. Many topic/item pairs can use the same handler. The handler is passed the HSZ of both the topic and the item, so it can use the FindTopicFromHsz and FindItemFromHsz functions to get pointers to topic or item information structures. These structures (DDETOPICINFO and DDEITEMINFO) can be expanded, according to your needs, to contain whatever information you require. Having received a pointer to the specific information, the handler can perform whatever generic processing is required.
It is possible to have a single handler for an entire topic. The AddDDETopic function has arguments to define generic topic Request and Poke handlers. If a topic/item pair has a generic topic handler and also a specific item handler, the item handler will be used. If neither a generic topic handler nor a specific item handler is supplied, the STDDDE.C module code fails that request.
If a data item changes at the server, it can use the PostDDEAdvise function to notify all clients of the change. PostDDEAdvise takes a pointer to a DDEITEMINFO structure as its argument. You can use the FindItemFromHsz and FindItemFromName functions to return a structure pointer to a specific item. The FindTopicFromHsz and FindTopicFromName functions can locate a pointer to a DDETOPICINFO structure.
If your server supports many Clipboard data formats, the GetCFIdFromName and GetCFFromId functions can be used to translate a Clipboard format ID to and from the string representation of its name. The STDDDE.C module contains standard string names for the defined Windows Clipboard formats.
This section describes the APIs that are global in the STDDDE.C module. The code also contains a number of internal functions that are documented in the code itself. See the "Data Types and Structures" section below for details about the data structures referred to here.
Adds an execute command processor to a topic.
PDDEEXECCMDFNINFO AddDDEExecCmd(pszTopic, pszCmdName, pfnExecCmd,
uiMinArgs, uiMaxArgs)
LPSTR pszTopic
Pointer to a string containing the name of the topic to add the command to.
LPSTR pszCmdName
Pointer to a string containing the name of the command to add.
PDDEEXECCMDFN pfnExecCmd
Pointer to a function to handle the command request.
UINT uiMinArgs
Minimum number of arguments that are valid for this command.
UINT uiMaxArgs
Maximum number of arguments that are valid for this command.
The return value is a pointer to a DDEEXECCMDFNINFO structure if the command is added successfully; otherwise, it is NULL.
If the topic does not exist, it will be created. If the command already exists, the information will be updated.
Adds an item to a topic.
PDDEITEMINFO AddDDEItem(pszTopic, pszItem, pFormatList, pfnRequest, pfnPoke)
LPSTR pszTopic
Pointer to a string containing the name of the topic to add the item to.
LPSTR pszItem
Pointer to a string containing the name of the item to add.
LPWORD pFormatList
Pointer to a list of valid formats.
PDDEREQUESTFN pfnRequest
Pointer to an optional function to handle request requests for this item. If this function is not provided, the default action is to call the topic generic-request-processor function if present.
PDDEPOKEFN pfnPoke
Pointer to an optional function to handle poke requests for this item. If this function is not provided, the default action is to call the topic generic-poke-processor function if present.
The return value is a pointer to a DDEITEMINFO structure if the item is added or NULL if not.
Adds a new topic and default processing for its item list and formats.
PDDETOPICINFO AddDDETopic(pszTopic, pfnExec, pfnRequest, pfnPoke)
LPSTR pszTopic
Pointer to a string containing the name of the topic.
PDDEEXECFN pfnExec
Pointer to an optional execute command processing function. This argument may be NULL, in which case the standard execute command parser will be used to process the request.
PDDEREQUESTFN pfnRequest
Pointer to an optional function to handle request requests for items of this topic. If this function is provided, it will be called for any item not having its own request function processor.
PDDEPOKEFN pfnPoke
Pointer to an optional function to handle poke requests for items of this topic. If this function is provided, it will be called for any item not having its own poke function processor.
The return value is a pointer to a DDETOPICINFO structure if the topic is added or NULL if not.
If the topic already exists, its information will be updated by the call.
Finds a DDE execute command from its string name.
PDDEEXECCMDFNINFO FindExecCmdFromName(pTopic, pszCmd)
PDDETOPICINFO pTopic
Pointer to the DDETOPICINFO structure for the topic being searched.
LPSTR pszCmd
Pointer to the string containing the name of the command to search for.
The return value is a pointer to a DDEEXECCMDFNINFO structure if the command is found; otherwise, it is NULL.
The search is not case sensitive.
Finds an item in the items list of a given topic by searching for its HSZ name.
PDDEITEMINFO FindItemFromHsz(pTopic, hszItem)
PDDETOPICINFO pTopic
Pointer to the DDETOPICINFO structure for the topic being searched.
LPSTR hszItem
HSZ of the item to search for.
The return value is a pointer to a DDEITEMINFO structure if the item is found; otherwise, it is NULL.
The search is not case sensitive.
Finds an item in the items list of a given topic by searching for its string name.
PDDEITEMINFO FindItemFromName(pTopic, pszItem)
PDDETOPICINFO pTopic
Pointer to the DDETOPICINFO structure for the topic being searched.
LPSTR pszItem
Pointer to the string containing the name of the item to search for.
The return value is a pointer to a DDEITEMINFO structure if the item is found; otherwise, it is NULL.
The search is not case sensitive.
Finds a topic in the topics list by searching for its HSZ value.
PDDETOPICINFO FindTopicFromHsz(hszName)
HSZ hszName
HSZ value to search for.
The return value is a pointer to a DDETOPICINFO structure if the topic is found; otherwise, it is NULL.
The search is not case sensitive.
Finds a topic in the topic list by searching for its string name.
PDDETOPICINFO FindTopicFromName(pszName)
LPSTR pszName
Pointer to a string containing the name to search for.
The return value is a pointer to a DDETOPICINFO structure if the topic is found; otherwise, it is NULL.
The search is not case sensitive.
Gets a Clipboard format ID from its name.
WORD GetCFIdFromName(pszName)
LPSTR pszName
Pointer to a string containing the name of the format.
The return value is either a standard format ID or the result of registering the name. The name supplied is always registered if it is not one of the standard ones.
Gets the text name of a Clipboard format from its ID.
LPSTR GetCFNameFromId(wFmt, pBuf, iSize)
WORD wFmt
Format tag to return the name for.
LPSTR pBuf
Pointer to a buffer to receive the name.
int iSize
Size of the buffer.
The return value is a pointer to the return string.
This command supports both standard and registered Clipboard formats.
Initializes all the DDE lists for this server and initializes the DDEML DLL.
BOOL InitializeDDE(hInstance, pszServiceName, pdwDDEInst, pfnCustomCallback)
HANDLE hInstance
Instance handle of the calling application.
LPSTR pszServiceName
Pointer to a string containing the name of the service.
LPDWORD pdwDDEInst
Pointer to a DWORD variable to receive the instance identifier returned by DDEML. This argument is optional. It may be set to NULL.
PFNCALLBACK pfnCustomCallback
Pointer to an optional custom callback function. This parameter may be set to NULL, in which case all the handling will be done by STDDDE.
The return value is TRUE if the initialization succeeds and FALSE if it fails.
This function adds to the System topic a number of items that are supported transparently to the application.
Posts a DDE advise notice to DDEML.
void PostDDEAdvise(pItemInfo)
PDDEITEMINFO pItemInfo
Pointer to a DDEITEMINFO structure describing the item.
There is no return value.
Removes a command from a topic.
BOOL RemoveDDEExecCmd(pszTopic, pszCmdName)
LPSTR pszTopic
Pointer to a string containing the name of the topic to remove the command from.
LPSTR pszCmdName
Pointer to a string containing the name of the command to remove.
The return value is TRUE if the command is removed, FALSE if not.
Removes an item from a topic.
BOOL RemoveDDEItem(pszTopic, pszItem)
LPSTR pszTopic
Pointer to a string containing the name of the topic to remove the item from.
LPSTR pszItem
Pointer to a string containing the name of the item to remove.
The return value is TRUE if the item is removed, FALSE if not.
Removing all the items from a topic does not remove the topic.
Removes a topic from the topics list.
BOOL RemoveDDETopic(pszTopic)
LPSTR pszTopic
Pointer to a string containing the name of the topic.
The return value is TRUE if the topic is removed, FALSE if not.
If there is a conversation active on this topic, it will be disconnected.
Terminates use of the DDEML DLL.
void UninitializeDDE()
There is no return value.
Any resources used by earlier calls are freed.
This section describes the structures used to implement the various linked lists in the STDDDE.C module.
This structure is used to store information about a Clipboard ID and its text name.
typedef struct {
WORD wFmt;
LPSTR pszName;
} CFTAGNAME;
wFmt
Format ID.
pszName
Pointer to the string name.
This structure is used to store information about a Clipboard ID and its text name.
typedef struct {
struct _DDECONVINFO FAR * pNext;
HCONV hConv;
HSZ hszTopicName;
PDDEITEMINFO pResultItem;
} DDECONVINFO;
pNext
Pointer to the next one in the conversation list.
hConv
Handle to the conversation.
hszTopicName
HSZ for the topic of the conversation.
pResultItem
Pointer to a temporary result item used to support the Result topic.
This structure is used to store information about a DDE execute command processor function.
typedef struct {
struct _DDEEXECCMDFNINFO FAR * pNext;
struct _DDETOPICINFO FAR * pTopic;
LPSTR pszCmdName;
PDDEEXECCMDFN pFn;
UINT uiMinArgs;
UINT uiMaxArgs;
} DDEEXECCMDFNINFO;
pNext
Pointer to the next item in the list.
pTopic
Pointer to the topic it belongs to.
pszCmdName
Name of the command.
pFn
Pointer to the function that will be called to process the command.
uiMinArgs
Minimum number of arguments accepted by this command.
uiMaxArgs
Maximum number of arguments accepted by this command.
This structure is used to store information about a DDE item.
typedef struct {
struct _DDEITEMINFO FAR * pNext;
LPSTR pszItemName;
HSZ hszItemName;
struct _DDETOPICINFO FAR * pTopic;
LPWORD pFormatList;
PDDEREQUESTFN pfnRequest;
PDDEPOKEFN pfnPoke;
HDDEDATA hData;
} DDEITEMINFO;
pNext
Pointer to the next item in the list of items.
pszItemName
Pointer to its string name.
hszItemName
DDE string handle for the name.
pTopic
Pointer to the topic it belongs to.
pFormatList
Pointer to a null-terminated list of Clipboard format words.
pfnRequest
Pointer to the item-specific request processor.
pfnPoke
Pointer to the item-specific poke processor.
hData
Data handle for this item. This is only used by STDDDE.C when the item is a special Result item, and may be used as required for other items.
This structure is used to store information about a DDE server that has only one service.
typedef struct {
LPSTR lpszServiceName;
HSZ hszServiceName;
PDDETOPICINFO pTopicList;
DWORD dwDDEInstance;
PFNCALLBACK pfnStdCallback;
PFNCALLBACK pfnCustomCallback;
PDDECONVINFO pConvList;
} DDESERVERINFO;
lpszServiceName
Pointer to the service string name.
hszServiceName
DDE string handle for the name.
pTopicList
Pointer to the topic list.
dwDDEInstance
DDE instance value.
pfnStdCallback
Pointer to standard DDE callback function.
pfnCustomCallback
Pointer to a custom DDE callback function.
pConvList
Pointer to the active conversation list.
This structure is used to store information about a DDE topic.
typedef struct {
struct _DDETOPICINFO FAR * pNext;
LPSTR pszTopicName;
HSZ hszTopicName;
PDDEITEMINFO pItemList;
PDDEEXECFN pfnExec;
PDDEREQUESTFN pfnRequest;
PDDEPOKEFN pfnPoke;
PDDEEXECCMDFNINFO pCmdList;
} DDETOPICINFO;
pNext
Pointer to the next topic in the topic list.
pszTopicName
Pointer to its string name.
hszTopicName
DDE string handle for the name.
pItemList
Pointer to the item list for this topic.
pfnExec
Pointer to the generic execute processor for this topic.
pfnRequest
Pointer to the generic request processor for this topic.
pfnPoke
Pointer to the generic poke processor for this topic.
pCmdList
Pointer to the execute command list for this topic.