Chapter 17  Dynamic Data Exchange (DDE)
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ

Dynamic Data Exchange (DDE) is one of three mechanisms of interprocess
communication supported under Windows. The other two are the Windows
clipboard (which I discussed in Chapter 16) and shared memory in dynamic
link libraries (Chapter 19).

DDE is based on the messaging system built into Windows. Two Windows
programs carry on a DDE "conversation" by posting messages to each other.
These two programs are known as the "server" and the "client." A DDE server
is the program that has access to data that may be useful to other Windows
programs. A DDE client is the program that obtains this data from the
server.

A DDE conversation is initiated by the client program. The client broadcasts
a message (called WM_DDE_INITIATE) to all currently running Windows
programs. This message indicates a general category of data the client
needs. A DDE server that has this data can respond to this broadcasted
message. At that point, the conversation begins.

A single Windows program can be both a client to one program and a server to
another, but this requires two different DDE conversations. A server can
deliver data to multiple clients, and a client can obtain data from multiple
servers, but again, this requires multiple DDE conversations. To keep these
conversations unique and separate, each  conversation (on both the client
and server sides) uses a different window. Generally, a program that
supports DDE will create a hidden child window for each conversation it
maintains.

The programs involved in a DDE conversation need not be specifically coded
to work with each other. As I'll discuss in the next section, generally the
writer of a DDE server will publicly document how the data is identified. A
user of a program that can act as a DDE client (such as Microsoft Excel) can
use this information to establish a DDE conversation between the two
programs.

If you write a family of two or more Windows programs that must communicate
with each other but not with other Windows programs, you may consider
defining your own messaging protocol. However, this is not recommended.
While it may work in Windows 3, it is possible that future versions of
Windows will not support any form of message-based interprocess
communication except for DDE.

Because DDE uses the messaging system built into Windows, it fits very
naturally in the environment. But this is not to say that DDE is easy to
implement. The protocol has many options, and programs must be ready to deal
with some rather tricky problems.

BASIC CONCEPTS

When a client asks a server for data, it must be able to identify the type
of data it wants. This is done with three character strings, called the
"application," the data "topic," and the data "item."

Application, Topic, and Item

The idea of the application, topic, and item is best approached with an
example. In this chapter, I'll show you how to write a Windows DDE server
program called DDEPOP. This program contains population data of the United
States from the 1970 census and 1980 census. Based on a linear
extrapolation, the program can calculate the instantaneous ("at this
moment") population of any state or of the United States as a whole. (A
linear extrapolation is not quite accurate, of course, but then again, this
is only a sample program.)

Anybody who writes a DDE server program should document how this data is
identified using three character strings:

  þ   The server application name: In this example, this is simply "DDEPOP."
      Each server has only one application name, the name of the program.

  þ   The topic name: All DDE servers support at least one topic. In the
      case of DDEPOP, only one topic is supported, which is identified by
      the string "US_Population." Conceivably, the DDEPOP program could be
      expanded to include data concerning the square-mile areas of the
      states, in which case the program would support a second topic named
      "US_Area."

  þ   The item name: Within each topic, a DDE server supports one or more
      data items. In DDEPOP, the item identifies the state using the
      standard two-character post-office abbreviation, such as "NY" for New
      York, "CA" for California, and "US" for the total. DDEPOP supports 52
      items--the 50 states, the District of Columbia ("DC"), and the total.

This documentation is sufficient to use the DDEPOP server with another
Windows program that can act as a client, for example, Microsoft Excel. To
use DDEPOP with Microsoft Excel, you can type the following into a
spreadsheet cell:

=DDEPOP|US_Population!US

These three strings indicate the application, topic, and item (in this case,
the total United States population). If DDEPOP.EXE is not already running,
Microsoft Excel will attempt to execute it. (DDEPOP must be in the current
directory or in a directory listed in the PATH environment variable.) If
successful, Excel will initiate a DDE conversation with DDEPOP, obtain the
population data, and display the population as a number in the cell. These
population figures can be formatted, graphed, or used in calculations.

What's most interesting is that the population figures will be periodically
updated in the spreadsheet. This is known as a "hot link" or (in a slight
variation) "warm link." Every 5 seconds, DDEPOP recalculates the population
data and notifies a client when an item has changed. In the case of the
total U.S. population, you'll see the figure increase by 1 about every 15
seconds.


The Types of Conversations

There are three basic types of DDE conversations--cold link, hot link, and
warm link. These conversations use DDE messages defined in the DDE.H header
file. The simplest of the three conversations is known as the cold link.

;NL1.

The Cold LinkA cold link conversation begins when a client broadcasts a
WM_DDE_INITIATE message identifying the application and topic it requires.
(The application and topic may be set to NULL to begin a conversation with
any server application or any data topic.) A server application that
supports the specified topic responds to the client with a WM_DDE_ACK
("acknowledge") message:

APHIC 17-1 ART

The client then requests a particular data item by posting a WM_DDE-
_REQUEST message. If the server can supply this data item, it responds by
posting a WM_DDE_DATA message to the client:

APHIC 17-2 ART

I've also indicated here that the client can acknowledge to the server that
it has received the WM_DDE_DATA message. This is optional (which I've
indicated by putting the WM_DDE_ACK message within parenthesis). The server
indicates whether it wants this acknowledgment in a flag passed with the
WM_DDE_DATA message. A flag passed with the WM_DDE_ACK message indicates a
"positive" acknowledgment.

If the client posts a WM_DDE_REQUEST message to the server, and the server
cannot supply the requested data item, then the server posts a "negative"
WM_DDE_ACK message to the client:

APHIC 17-3 ART

The DDE conversation continues with the client posting WM_DDE- _REQUEST
messages to the server--for the same data item or different data items--and
the server responding with WM_DDE_DATA or WM_DDE_ACK messages. The
conversation is terminated when the client and server post each other
WM_DDE_TERMINATE messages:

APHIC 17-4 ART

Although I've indicated that the client posts the first WM_DDE_TER- MINATE
message, this is not always the case. The server can post the first
WM_DDE_TERMINATE message, and the client must respond to that.

2.

The Hot LinkOne problem with the cold link is that the data the server has
access to may change with the passing of time. (This is the case with
DDEPOP, which calculates an instantaneous population that can change.) In
the cold link, the client does not know when the data changes. The hot link
solves this problem.

Again, the DDE conversation begins with a WM_DDE_INITIATE message and a
WM_DDE_ACK message:

APHIC 17-5 ART

The client indicates the data item it requires by posting a WM_DDE- _ADVISE
message to the server. The server responds by posting a WM_DDE_ACK message
indicating if it has access to this item:

APHIC 17-6 ART

A positive acknowledgment indicates the server can supply the data; a
negative acknowledgment indicates that it cannot.

At this point, the server is obligated to notify the client whenever the
value of the data item changes. This notification uses a WM- _DDE_DATA
message, to which the client (based on a flag set in the  WM_DDE_DATA
message) may or may not respond with a WM- _DDE_ACK message:

APHIC 17-7 ART

When the client no longer wishes to be advised of updates to the data item,
it posts a WM_DDE_UNADVISE message to the server, and the server
acknowledges:

APHIC 17-8 ART

The conversation is terminated with the posting of WM_DDE_TERMINATE
messages:

APHIC 17-9 ART

The cold link and the hot link are not mutually exclusive. During a single
DDE conversation, a client may ask for some data items by using WM-
_DDE_REQUEST (for a cold link) and ask for others by using WM_DDE- _ADVISE
(for a hot link).

3.

The Warm LinkThe warm link combines elements of the cold link and hot link.
The conversation begins as normal:

APHIC 17-10 ART

As with the hot link, the client posts a WM_DDE_ADVISE message to the
server, and the server acknowledges either positively or negatively:

APHIC 17-11 ART

However, a flag passed with the WM_DDE_ADVISE message indicates that the
client wishes only to be informed of changes in data without immediately
receiving the new data item. So the server posts WM- _DDE_DATA messages with
NULL data:

APHIC 17-12 ART

Now the client knows that a particular data item has changed. To obtain this
item, the client uses a WM_DDE_REQUEST message, just as in the cold link:

APHIC 17-13 ART

As in the hot link, a client can stop being advised of changes in data items
by posting a WM_DDE_ADVISE message to the server:

APHIC 17-14 ART

The conversation is terminated with the WM_DDE_TERMINATE messages:

APHIC 17-15 ART

These three types of conversations use all the DDE messages except two:
WM_DDE_POKE (in which a client gives a server unsolicited data) and WM_DDE-
_EXECUTE (in which a client sends a command string to a server). These
messages are rarely used, and I won't be covering them in this chapter.

The DDE.H header file also defines four structures:

  þ   DDEACK (used in the WM_DDE_ACK message)

  þ   DDEADVISE (used in the WM_DDE_ADVISE message)

  þ   DDEDATA (used in the WM_DDE_DATA message)

  þ   DDEPOKE (used in the WM_DDE_POKE message)

I'll deal with the first three structures as I discuss the sample programs
in this chapter.


Character Strings and Atoms

I've discussed how a DDE client and server identify data using three
character strings_ the application, topic, and item. But in the actual
messages between the client and server, these character strings do not
appear: "Atoms" are used instead.

Atoms are WORD values that refer to character strings in a case-insensitive
manner. You can use atoms within your own program for working with character
strings, in which case the atom table (the table that references the atom
values with the strings) is stored in your program's default data segment.

You define an atom as follows:

ATOM aAtom ;

You can add a string to the atom table using the function:

aAtom = AddAtom (lpString) ;

If the character string does not already exist in the atom table, this
function adds it and returns a unique value identifying the string. Each
atom has a "reference count," which is the number of times AddAtom has been
called for the same string. The reference count is initially set to 1. If
the character string already exists in the atom table (that is, if this is
the second or subsequent time that AddAtom has been called for the same
string), the function returns the number identifying the character string
and increments the reference count.

The function:

DeleteAtom (aAtom) ;

decrements the reference count. When the count is 0, the atom and character
string are removed from the atom table.

The function:

aAtom = FindAtom (lpString) ;

will return the atom associated with the character string (or 0 if the
string is not in the atom table). This function does not affect the
reference count of the atom.

The function:

nBytes = GetAtomName (aAtom, lpBuffer, nBufferSize) ;

returns the character string for an atom. The last parameter indicates the
size of the buffer pointed to by the second parameter. The function returns
the number of bytes copied to the buffer and does not affect the reference
count.

These four functions (there are several others of lesser importance) allow
you to work with atoms within your own program. However, because the atom
table is stored in your program's default data segment, the atoms are unique
to your program. To use atoms with DDE, you must use another set of four
functions, similar to the functions described above:

aAtom = GlobalAddAtom (lpString) ;
GlobalDeleteAtom (aAtom) ;
aAtom = GlobalFindAtom (lpString) ;
nBytes = GlobalGetAtomName (aAtom, lpBuffer, nBufferSize) ;

The atom table for these atoms is stored in a shared data segment in a
dynamic link library within Windows and hence is common to all Windows
programs. One program  can use GlobalAddAtom to add a string to the atom
table and pass the atom to another program. This other program can use
GlobalGetAtomName to obtain the character string associated with the atom.
This is how Windows programs identify the DDE application, topic, and item.

The rules regarding the use of atoms with DDE are described in the
documentation of the DDE messages in Chapter 15 of the Microsoft Windows
Programmer's Reference. These rules are extremely important: It is not good
if an atom that is still required by one program is deleted from the atom
table by another program. Neither is it good if atoms that are no longer
required are not deleted from the atom table. For this reason, you must be
careful about how your program handles atoms.

Atoms are used for the DDE application, topic, and item strings. The data
structures that are transferred from one Windows program to another must be
allocated using GlobalAlloc with the GMEM_DDESHARE option. This allows the
global memory block to be shared among multiple Windows programs. The DDE
rules that govern which program is responsible for allocating and freeing
these global memory blocks are also quite strict.



A DDE SERVER PROGRAM

We are now ready to begin looking at DDEPOP, the DDE server program that can
supply instantaneous state population data to a DDE client. This program is
shown in Figure 17-1.

 DDEPOP.MAK

#----------------------
# DDEPOP.MAK make file
#----------------------

ddepop.exe : ddepop.obj ddepop.def ddepop.res
     link ddepop, /align:16, NUL, /nod slibcew win87em libw, ddepop
     rc ddepop.res

ddepop.obj : ddepop.c
     cl -c -Gsw -Ow -W2 -Zp ddepop.c

ddepop.res : ddepop.rc ddepop.ico
     rc -r ddepop.rc

 DDEPOP.C

/*--------------------------------------------
   DDEPOP.C -- DDE Server for Population Data
               (c) Charles Petzold, 1990
  --------------------------------------------*/

#include 
#include 
#include 
#include 

struct
     {
     char *szState ;
     long lPop70 ;
     long lPop80 ;
     long lPop ;
     }
     pop [] = {
              "AL",   3444354,   3894025, 0, "AK",    302583,    401851, 0,
              "AZ",   1775399,   2716598, 0, "AR",   1923322,   2286357, 0,
              "CA",  19971069,  23667764, 0, "CO",   2209596,   2889735, 0,
              "CT",   3032217,   3107564, 0, "DE",    548104,    594338, 0,
              "DC",    756668,    638432, 0, "FL",   6791418,   9746961, 0,



              "GA",   4587930,   5462982, 0, "HI",    769913,    964691, 0,
              "ID",    713015,    944127, 0, "IL",  11110285,  11427409, 0,
              "IN",   5195392,   5490212, 0, "IA",   2825368,   2913808, 0,
              "KS",   2249071,   2364236, 0, "KY",   3220711,   3660324, 0,
              "LA",   3644637,   4206116, 0, "ME",    993722,   1125043, 0,
              "MD",   3923897,   4216933, 0, "MA",   5689170,   5737093, 0,
              "MI",   8881826,   9262044, 0, "MN",   3806103,   4075970, 0,
              "MS",   2216994,   2520770, 0, "MO",   4677623,   4916762, 0,
              "MT",    694409,    786690, 0, "NE",   1485333,   1569825, 0,
              "NV",    488738,    800508, 0, "NH",    737681,    920610, 0,
              "NJ",   7171112,   7365011, 0, "NM",   1017055,   1303302, 0,
              "NY",  18241391,  17558165, 0, "NC",   5084411,   5880415, 0,
              "ND",    617792,    652717, 0, "OH",  10657423,  10797603, 0,
              "OK",   2559463,   3025487, 0, "OR",   2091533,   2633156, 0,
              "PA",  11800766,  11864720, 0, "RI",    949723,    947154, 0,
              "SC",   2590713,   3120730, 0, "SD",    666257,    690768, 0,
              "TN",   3926018,   4591023, 0, "TX",  11198655,  14225513, 0,
              "UT",   1059273,   1461037, 0, "VT",    444732,    511456, 0,
              "VA",   4651448,   5346797, 0, "WA",   3413244,   4132353, 0,
              "WV",   1744237,   1950186, 0, "WI",   4417821,   4705642, 0,
              "WY",    332416,    469557, 0, "US", 203302031, 226542580, 0
              } ;

#define NUM_STATES (sizeof (pop) / sizeof (pop [0]))

typedef struct
     {
     unsigned int fAdvise:1 ;
     unsigned int fDeferUpd:1 ;
     unsigned int fAckReq:1 ;
     unsigned int dummy:13 ;
     long         lPopPrev ;
     }
     POPADVISE ;

#define ID_TIMER    1
#define DDE_TIMEOUT 3000

long FAR PASCAL WndProc         (HWND, WORD, WORD, LONG) ;
long FAR PASCAL ServerProc      (HWND, WORD, WORD, LONG) ;
BOOL FAR PASCAL TimerEnumProc   (HWND, LONG) ;
BOOL FAR PASCAL CloseEnumProc   (HWND, LONG) ;
BOOL            PostDataMessage (HWND, HWND, int, BOOL, BOOL, BOOL) ;

char   szAppName []     = "DdePop" ;
char   szServerClass [] = "DdePop.Server" ;
HANDLE hInst ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     HWND     hwnd ;
     MSG      msg ;
     WNDCLASS wndclass ;

     if (hPrevInstance)
          return FALSE ;

     hInst = hInstance ;

               // Register window class

     wndclass.style         = 0 ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = LoadIcon (hInstance, szAppName) ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = szAppName ;

     RegisterClass (&wndclass) ;

               // Register window class for DDE Server

     wndclass.style         = 0 ;
     wndclass.lpfnWndProc   = ServerProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 2 * sizeof (WORD) ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = NULL ;
     wndclass.hCursor       = NULL ;
     wndclass.hbrBackground = NULL ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = szServerClass ;

     RegisterClass (&wndclass) ;

     hwnd = CreateWindow (szAppName, "DDE Population Server",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     SendMessage (hwnd, WM_TIMER, 0, 0L) ;   // initialize 'pop' structure
     if (!SetTimer (hwnd, ID_TIMER, 5000, NULL))
          {
          MessageBox (hwnd, "Too many clocks or timers!", szAppName,
                      MB_ICONEXCLAMATION | MB_OK) ;

          return FALSE ;
          }

     ShowWindow (hwnd, SW_SHOWMINNOACTIVE) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }

     KillTimer (hwnd, ID_TIMER) ;

     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static FARPROC lpTimerEnumProc, lpCloseEnumProc ;
     static char    szTopic [] = "US_Population" ;
     ATOM           aApp, aTop ;
     HWND           hwndClient, hwndServer ;
     int            i ;
     long double    ldSecsInDecade, ldSecSince1970 ;
     time_t         lSecSince1970 ;

     switch (message)
          {
          case WM_CREATE :
               lpTimerEnumProc = MakeProcInstance (TimerEnumProc, hInst) ;
               lpCloseEnumProc = MakeProcInstance (CloseEnumProc, hInst) ;
               return 0 ;

          case WM_DDE_INITIATE :

                    // wParam          -- sending window handle
                    // LOWORD (lParam) -- application atom
                    // HIWORD (lParam) -- topic atom

               hwndClient = wParam ;

               aApp = GlobalAddAtom (szAppName) ;
               aTop = GlobalAddAtom (szTopic) ;
                    // Check for matching atoms, create window, and
acknowledge

               if ((LOWORD (lParam) == NULL || LOWORD (lParam) == aApp) &&
                   (HIWORD (lParam) == NULL || HIWORD (lParam) == aTop))
                    {
                    hwndServer = CreateWindow (szServerClass, NULL,
                                               WS_CHILD, 0, 0, 0, 0,
                                               hwnd, NULL, hInst, NULL) ;

                    SetWindowWord (hwndServer, 0, hwndClient) ;
                    SendMessage (wParam, WM_DDE_ACK, hwndServer,
                                 MAKELONG (aApp, aTop)) ;
                    }

                    // Otherwise, delete the atoms just created

               else
                    {
                    GlobalDeleteAtom (aApp) ;
                    GlobalDeleteAtom (aTop) ;
                    }

               return 0 ;

          case WM_TIMER :
          case WM_TIMECHANGE :
               time (&lSecSince1970) ;

                    // Calculate new current populations

               ldSecSince1970 = (long double) lSecSince1970 ;
               ldSecsInDecade = (long double) 3652 * 24 * 60 * 60 ;

               for (i = 0 ; i < NUM_STATES ; i++)
                    {
                    pop[i].lPop = (long)
                         (((ldSecsInDecade - ldSecSince1970) * pop[i].lPop70
+
                            ldSecSince1970 * pop[i].lPop80) / ldSecsInDecade
                                   + .5) ;
                    }

                    // Notify all child windows

               EnumChildWindows (hwnd, lpTimerEnumProc, 0L) ;
               return 0 ;

          case WM_QUERYOPEN :
               return 0 ;
          case WM_CLOSE :

                    // Notify all child windows

               EnumChildWindows (hwnd, lpCloseEnumProc, 0L) ;

               break ;                  // for default processing

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

long FAR PASCAL ServerProc (HWND hwnd, WORD message, WORD wParam, LONG
lParam)
     {
     ATOM          aItem ;
     char          szItem [10], szPopulation [10] ;
     DDEACK        DdeAck ;
     DDEADVISE     Advise ;
     DDEADVISE FAR *lpDdeAdvise ;
     DDEDATA FAR   *lpDdeData ;
     DWORD         dwTime ;
     GLOBALHANDLE  hPopAdvise, hDdeData, hDdeAdvise, hCommands, hDdePoke ;
     int           i ;
     HWND          hwndClient ;
     MSG           msg ;
     POPADVISE FAR *lpPopAdvise ;
     WORD          cfFormat, wStatus ;

     switch (message)
          {
          case WM_CREATE :

                    // Allocate memory for POPADVISE structures

               hPopAdvise = GlobalAlloc (GHND, NUM_STATES * sizeof
(POPADVISE));

               if (hPopAdvise == NULL)
                    DestroyWindow (hwnd) ;
               else
                    SetWindowWord (hwnd, 2, hPopAdvise) ;

               return 0 ;

          case WM_DDE_REQUEST :
                    // wParam          -- sending window handle
                    // LOWORD (lParam) -- data format
                    // HIWORD (lParam) -- item atom

               hwndClient = wParam ;
               cfFormat   = LOWORD (lParam) ;
               aItem      = HIWORD (lParam) ;

                    // Check for matching format and data item

               if (cfFormat == CF_TEXT)
                    {
                    GlobalGetAtomName (aItem, szItem, sizeof (szItem)) ;

                    for (i = 0 ; i < NUM_STATES ; i++)
                         if (strcmp (szItem, pop[i].szState) == 0)
                              break ;

                    if (i < NUM_STATES)
                         {
                         GlobalDeleteAtom (aItem) ;
                         PostDataMessage (hwnd, hwndClient, i,
                                          FALSE, FALSE, TRUE) ;
                         return 0 ;
                         }
                    }

                    // Negative acknowledge if no match

               DdeAck.bAppReturnCode = 0 ;
               DdeAck.reserved       = 0 ;
               DdeAck.fBusy          = FALSE ;
               DdeAck.fAck           = FALSE ;

               wStatus = * (WORD *) & DdeAck ;

               if (!PostMessage (hwndClient, WM_DDE_ACK, hwnd,
                                 MAKELONG (wStatus, aItem)))
                    {
                    GlobalDeleteAtom (aItem) ;
                    }

               return 0 ;

          case WM_DDE_ADVISE :

                    // wParam          -- sending window handle
                    // LOWORD (lParam) -- DDEADVISE memory handle
                    // HIWORD (lParam) -- item atom
               hwndClient = wParam ;
               hDdeAdvise = LOWORD (lParam) ;
               aItem      = HIWORD (lParam) ;

               lpDdeAdvise = (DDEADVISE FAR *) Glo               if
(!PostMessage (hwndClient, WM_DDE_ACK, hwnd,
                                 MAKELONG (wStatus, hCommands)))
                    {
                    GlobalFree (hCommands) ;
                    }
               return 0 ;

          case WM_DDE_POKE :

                    // Post negative acknowledge

               hwndClient = wParam ;
               hDdePoke   = LOWORD (lParam) ;
               aItem      = HIWORD (lParam) ;

               DdeAck.bAppReturnCode = 0 ;
               DdeAck.reserved       = 0 ;
               DdeAck.fBusy          = FALSE ;
               DdeAck.fAck           = FALSE ;

               wStatus = * (WORD *) & DdeAck ;

               if (!PostMessage (hwndClient, WM_DDE_ACK, hwnd,
                                 MAKELONG (wStatus, aItem)))
                    {
                    GlobalFree (hDdePoke) ;
                    GlobalDeleteAtom (aItem) ;

                    }

               return 0 ;

          case WM_DDE_TERMINATE :

                    // Respond with another WM_DDE_TERMINATE message

               hwndClient = wParam ;
               PostMessage (hwndClient, WM_DDE_TERMINATE, hwnd, 0L) ;
               DestroyWindow (hwnd) ;
               return 0 ;

          case WM_TIMER :

                    // Post WM_DDE_DATA messages for changed populations

               hwndClient  = GetWindowWord (hwnd, 0) ;
               hPopAdvise  = GetWindowWord (hwnd, 2) ;
               lpPopAdvise = (POPADVISE FAR *) GlobalLock (hPopAdvise) ;
               for (i = 0 ; i < NUM_STATES ; i++)
                    if (lpPopAdvise[i].fAdvise)
                         if (lpPopAdvise[i].lPopPrev != pop[i].lPop)
                              {
                              if (!PostDataMessage (hwnd, hwndClient, i,
                                                    lpPopAdvise[i].fAckReq,
                                                    FALSE))
                                   break ;

                              lpPopAdvise[i].lPopPrev = pop[i].lPop ;
                              }

               GlobalUnlock (hPopAdvise) ;
               return 0 ;

          case WM_CLOSE :

                    // Post a WM_DDE_TERMINATE message to the client

               hwndClient = GetWindowWord (hwnd, 0) ;
               PostMessage (hwndClient, WM_DDE_TERMINATE, hwnd, 0L) ;

               dwTime = GetCurrentTime () ;

               while (GetCurrentTime () - dwTime < DDE_TIMEOUT)
                    if (PeekMessage (&msg, hwnd, WM_DDE_TERMINATE,
                                     WM_DDE_TERMINATE, PM_REMOVE))
                         break ;

               DestroyWindow (hwnd) ;
               return 0 ;

          case WM_DESTROY :
               hPopAdvise = GetWindowWord (hwnd, 2) ;
               GlobalFree (hPopAdvise) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

BOOL FAR PASCAL TimerEnumProc (HWND hwnd, LONG lParam)
     {
     SendMessage (hwnd, WM_TIMER, 0, 0L) ;

     return TRUE ;
     }
BOOL FAR PASCAL CloseEnumProc (HWND hwnd, LONG lParam)
     {
     SendMessage (hwnd, WM_CLOSE, 0, 0L) ;

     return TRUE ;
     }

BOOL PostDataMessage (HWND hwndServer, HWND hwndClient, int iState,
                      BOOL fDeferUpd, BOOL fAckReq, BOOL fResponse)
     {
     ATOM         aItem ;
     char         szPopulation [10] ;
     DDEACK       DdeAck ;
     DDEDATA FAR  *lpDdeData ;
     DWORD        dwTime ;
     GLOBALHANDLE hDdeData ;
     MSG          msg ;

     aItem = GlobalAddAtom (pop[iState].szState) ;

          // Allocate a DDEDATA structure if not defered update

     if (fDeferUpd)
          {
          hDdeData = NULL ;
          }
     else
          {
          wsprintf (szPopulation, "%ld\r\n", pop[iState].lPop) ;

          hDdeData = GlobalAlloc (GHND | GMEM_DDESHARE,
                                  sizeof (DDEDATA) + strlen (szPopulation))
;

          lpDdeData = (DDEDATA FAR *) GlobalLock (hDdeData) ;

          lpDdeData->fResponse = fResponse ;
          lpDdeData->fRelease  = TRUE ;
          lpDdeData->fAckReq   = fAckReq ;
          lpDdeData->cfFormat  = CF_TEXT ;

          lstrcpy ((LPSTR) lpDdeData->Value, szPopulation) ;

          GlobalUnlock (hDdeData) ;
          }

          // Post the WM_DDE_DATA message

     if (!PostMessage (hwndClient, WM_DDE_DATA, hwndServer,
                       MAKELONG (hDdeData, aItem)))
          {
          if (hDdeData != NULL)
               GlobalFree (hDdeData) ;

          GlobalDeleteAtom (aItem) ;
          return FALSE ;
          }

          // Wait for the acknowledge message if it's requested

     if (fAckReq)
          {
          DdeAck.fAck = FALSE ;

          dwTime = GetCurrentTime () ;

          while (GetCurrentTime () - dwTime < DDE_TIMEOUT)
               {
               if (PeekMessage (&msg, hwndServer, WM_DDE_ACK, WM_DDE_ACK,
                                PM_REMOVE))
                    {
                    DdeAck = * (DDEACK *) & LOWORD (msg.lParam) ;
                    aItem  = HIWORD (msg.lParam) ;
                    GlobalDeleteAtom (aItem) ;
                    break ;
                    }
               }

          if (DdeAck.fAck == FALSE)
               {
               if (hDdeData != NULL)
                    GlobalFree (hDdeData) ;

               return FALSE ;
               }
          }

     return TRUE ;
     }

 DDEPOP.RC

/*---------------------------
   DDEPOP.RC resource script
  ---------------------------*/

DdePop ICON ddepop.ico

 DDEPOP.ICO  -- Please refer to the book.

 DDEPOP.DEF

;-----------------------------------
; DDEPOP.DEF module definition file
;-----------------------------------

NAME           DDEPOP

DESCRIPTION    'DDE Server for Population Data (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc
               ServerProc
               TimerEnumProc
               CloseEnumProc

I described earlier how you can use this server with Microsoft Excel. Later
in this chapter I'll show you a DDE client program (called SHOWPOP) that
also uses DDEPOP as a server.

The DDEPOP Program

You'll notice that the top of the DDEPOP.C listing contains the line:

#include 

This is the header file that includes the DDE messages and data structures.

This is followed in the DDEPOP.C listing by the structure called pop that
contains all the two-character state codes, the 1970 population figures, the
1980 population figures, and a fourth field initialized with zeros that will
contain the current population based on the system date and time.

The program also defines a second structure (called POPADVISE) using a
typedef statement. I'll discuss later how this structure is used.

In WinMain, the program terminates if hPrevInstance is not equal to NULL.
There is no reason for multiple copies of DDEPOP to be running under
Windows. The program registers two window classes. The first has the class
name "DdePop" used for the program's main window. The second has the class
name "DdePop.Server." This second class is used for the child windows that
are created to maintain multiple DDE conversations. Each conversation
requires its own child window based on this window class.

In this second window class, the cbWndExtra field of the WNDCLASS structure
is set to hold two words per window. As you'll see, the first will be used
to store the window handle of the client that the server window is
communicating with. The second will be a handle to a global memory block
that contains NUM_STATE structures of type POPADVISE.

After DDEPOP creates its main window, it explicitly sends the window a
WM_TIMER message. The sole purpose of this message is to allow WndProc an
opportunity to initialize the lPop field of the pop structure with the
current population based on the lPop70 and lPop80 fields and the system date
and time. The program also calls SetTimer to set a 5-second timer to
periodically update the lPop field.

You'll notice that ShowWindow is called with the SW_SHOWMINNOACTIVE
parameter and that WndProc returns 0 from the WM_QUERYOPEN message. This
keeps DDEPOP displayed as an icon (similar to the FREEMEM program from
Chapter 5).


The WM_DDE_INITIATE Message

A DDE conversation is initiated by a client by broadcasting a
WM_DDE_INITIATE message to all top-level windows. (As you'll see when I
discuss the DDE client program later in this chapter, this is accomplished
by calling SendMessage with a 0xFFFF window handle as the first parameter.)

The WM_DDE_INITIATE message is handled by a DDE server in its main window
procedure. As in every DDE message, the wParam parameter is the handle to
the window sending the message. This is the window handle of the client.
WndProc stores this in the variable hwndClient.

For the WM_DDE_INITIATE message, the low word of lParam is the atom
identifying the desired application. This could be NULL if the client wants
a response from any server. The high word of lParam is the atom identifying
the desired topic. Again, this could be NULL if the client wants a response
from a server that can supply any topic.

WndProc processes the WM_DDE_INITIATE message by calling GlobalAddAtom to
add atoms for its application name ("DdePop") and topic name
("US_Population"). It then checks if the atoms supplied in the low word and
high word of lParam are NULL or match these atoms.

If the atoms match, then WndProc creates a hidden child window based on the
"DdePop.Server" window class. This window (whose window procedure is
ServerProc) will handle all subsequent DDE messages in the DDE conversation.
The first of the two words reserved for the window is set to the handle of
the client using SetWindowWord.  WndProc then acknowledges the
WM_DDE_INITIATE message by sending a WM- _DDE_ACK message back to the
client. The wParam parameter is the handle of the just-created server
window, and lParam contains the atoms identifying the server application
name and the topic name. (If the client requested all topics and the server
supports multiple topics, then the server would send multiple WM_DDE_ACK
messages back to the client, one for each topic it supports.)

A program that receives a WM_DDE_ACK message is responsible for deleting all
atoms that accompany the message. WndProc calls GlobalDeleteAtom for the two
atoms it created only if it does not send a WM_DDE_ACK message to the
client.

The WM_DDE_INITIATE message and the WM_DDE_ACK message (in response to
WM_DDE_INITIATE) are the only two DDE messages that are sent using
SendMessage rather than posted using PostMessage. As we'll see later in this
chapter, this means that a client sending a WM_DDE_INITIATE message receives
the WM_DDE_ACK responses before the original SendMessage call has returned.


The ServerProc Window Procedure

With the sending of the WM_DDE_ACK message in response to the
WM_DDE_INITIATE message, the DDE conversation has begun. As I mentioned,
when WndProc sends the WM_DDE_ACK message back to the client, it sets the
wParam parameter to the handle of the child window it creates for the
conversation. This means that all subsequent DDE messages occur between the
client and this child window, whose window procedure is ServerProc.

ServerProc processes its WM_CREATE message by allocating memory required to
hold NUM_STATES structures of type POPADVISE. (I'll discuss how these are
used shortly.) The handle to this global memory block is stored as the
second reserved word using SetWindowWord. This memory block is freed when
ServerProc receives a WM_DESTROY message.


The WM_DDE_REQUEST Message

A client posts a WM_DDE_REQUEST message to a server when it wants data that
is associated with a particular item. This is the type of transaction known
as the cold link. The server responds by posting a WM_DDE_DATA message to
the client with the data or a WM_DDE_ACK message if it cannot satisfy the
request. Let's look at how ServerProc handles the WM_DDE_REQUEST message.

As is usual with DDE messages, the wParam parameter accompanying WM_DDE-
_REQUEST is the handle to the window posting the message, in this case the
client. The low word of the lParam parameter is a requested data format. The
high word of lParam is an atom identifying the requested data item.

The formats of DDE data are the same as clipboard formats, so this low word
of lParam will most commonly be one of the identifiers beginning with the CF
prefix. A  client may send multiple WM_DDE_REQUEST messages to a server for
the same item but with different formats. The server should respond with a
WM_DDE_DATA message for only the formats it supports. Far and away the most
common format for DDE data is  CF_TEXT, and this is the only format that
DDEPOP supports.

So, when processing the WM_DDE_REQUEST message, ServerProc first checks if
the requested format is CF_TEXT. ServerProc then calls the GlobalGetAtomName
function to get the character string associated with the atom passed in the
high word of lParam. If the client knows what it's doing, this will be a
two-character string identifying the state. A for loop goes through the
states and attempts to match this with the szState field of the pop
structure. If there's a match, ServerProc deletes the atom by calling
GlobalDeleteAtom and then calls PostDataMessage (a function towards the end
of DDEPOP that posts the WM_DDE_DATA message and which I'll describe
shortly). ServerProc then returns.

If the requested format is not CF_TEXT, or if there was no match between the
item atom and one of the state names, then ServerProc posts a negative
WM_DDE_ACK message indicating that the data was not available. It does this
by setting the fAck field of a DDEACK structure (defined in DDE.H) to FALSE.
The DDEACK structure is converted to a word, which forms the low word of
lParam. The high word of lParam is the atom for the requested item.
PostMessage posts the WM_DDE_ACK message to the client.

Notice how the atom is handled here. The documentation for WM_DDE_REQUEST
states: "When responding with either a WM_DDE_DATA or WM_DDE_ACK message,
reuse the aItem atom or delete it and create a new one." What this means is
that the state of the global atom table should not be altered by the
server--that is, the reference count for the item atom should not be
incremented or decremented.

There are three cases here:

  þ   If the requested format is CF_TEXT and the atom matches one of the
      state names, then ServerProc calls GlobalDeleteAtom before calling the
      function in DDEPOP.C named PostDataMessage. This PostDataMessage
      function (as we'll see shortly) re-creates the atom when posting a
      WM_DDE_DATA message to the client.

  þ   If the requested format is not CF_TEXT or if the atom does not match
      one of the state names, then ServerProc calls PostMessage to deliver a
      negative WM_DDE_ACK message to the client. The atom is simply reused
      in this message.

  þ   However, if this PostMessage call fails (perhaps indicating that the
      client has been unexpectedly terminated), then ServerProc deletes the
      atom because the client cannot.

We are not yet finished with the WM_DDE_REQUEST message because we have not
yet examined how DDEPOP's PostDataMessage responds with the WM_DDE_DATA
message. That's next.


DDEPOP's PostDataMessage Function

The PostDataMessage function towards the end of DDEPOP.C is responsible for
posting  a WM_DDE_DATA message to a client. This function is set up to also
handle WM- _DDE_ADVISE messages (which I'll discuss shortly), so it's a
little more complex than  if it only had to handle WM_DDE_REQUEST messages.

PostDataMessage has six parameters:

  þ   hwndServer--the window handle of the server

  þ   hwndClient--the window handle of the client

  þ   i--which is the index of the pop array identifying the state for which
      population data is requested

  þ   fDeferUpd--which ServerProc sets to FALSE when responding to
      WM_DDE_REQUEST messages

  þ   fAckReq--which ServerProc also sets to FALSE in this case

  þ   fResponse--which ServerProc sets to TRUE to indicate a response from a
      WM_DDE_REQUEST message

(I'll discuss the fDeferUpd and fAckReq parameters shortly when we get to
the WM_DDE_ADVISE message. For now, just ignore all parts of PostDataMessage
when either of these two parameters is set to TRUE.)

PostDataMessage begins by calling GlobalAddItem to create an atom for the
two-character state name. (You'll recall that ServerProc deleted the atom
before calling PostDataMessage.) It then calls wsprintf to convert the
population for the state (updated by WndProc within the past 5 seconds) to a
character string terminated with a carriage return and line feed.

PostDataMessage then uses GlobalAlloc with the GMEM_DDESHARE option to
allocate a block of memory large enough for a DDEDATA structure (defined in
DDE.H) with the actual data (the character string szPopulation) appended to
the end. In the case of PostDataMessage being used in response to a
WM_DDE_REQUEST message, the fields of the DDEDATA structure are set as
follows:

  þ   The fResponse field of the DDEDATA structure is set to TRUE,
      indicating that the data is in response to a WM_DDE_DATA message.

  þ   The fRelease field is also set to TRUE, indicating that the client
      should free the global memory block just allocated.

  þ   The fAckReq field is set to FALSE, indicating that a WM_DDE_ACK
      message from the client is not required.

  þ   The cfFormat field is set to CF_TEXT, indicating that the data is in a
      text format.

  þ   The szPopulation array is copied into the area of the memory block
      beginning at the Value field of the structure.

PostDataMessage then uses PostMessage to post a WM_DDE_DATA message to the
client. As usual, wParam is the handle of the window sending the message
(the server). The low word of lParam is the handle of the memory block
containing the DDEDATA structure, and the high word of lParam is the atom
identifying the data item (the two-character state name).

If PostMessage is successful, then we're done. The client is responsible for
freeing the memory block and deleting the atom. If PostMessage fails
(perhaps because the client is no longer with us), PostDataMessage frees the
memory block it allocated and deletes the atom.


The WM_DDE_ADVISE Message

You are, I trust, beginning to recognize some of the complexities involved
in DDE. It gets a little more complex with WM_DDE_ADVISE and the hot link.

The WM_DDE_REQUEST message I've just discussed allows the client to obtain
data from the server. But if this data changes (as the instantaneous
population will), then the client has no way to know that. Allowing the
client to know when data has been updated is the purpose of the
WM_DDE_ADVISE message. On receipt of this message, a server is responsible
for notifying the client when the data has changed. (This notification is
accomplished by the server posting WM_DDE_DATA messages to the client.) This
can be tricky because the server must "remember" which items the client has
asked to be advised on. Moreover, the client will ask that this data be
posted in particular ways.

In a WM_DDE_ADVISE message, the low word of lParam is a handle to a global
memory block containing a DDEADVISE structure as defined in DDE.H. The high
word of lParam is the atom identifying the data item.

When processing WM_DDE_ADVISE, ServerProc first checks that the cfFormat
field of the DDEADVISE structure is CF_TEXT. It then obtains the text string
referenced by the atom and checks it against the szState field of the pop
structure.

If there's a match, then ServerProc gets a pointer to the array of POPADVISE
structures that it allocated during the WM_CREATE message. This array has a
POPADVISE structure for each state, and there is a different array for each
window carrying on a DDE conversation. This array is used to store all
information ServerProc will need to update items to the client.

The fields of the POPADVISE structure for the selected state are set as
follows:

  þ   The fAdvise field is set to TRUE. This is simply a flag that indicates
      that the client wants updated information on this state.

  þ   The fDeferUpd ("deferred update") field is set to the value of the
      same field in the DDEADVISE structure. A FALSE value indicates that
      the client wants to establish a warm link rather than a hot link. The
      client will be  advised of a change in data without getting the data
      immediately. (In this case, the server posts a WM_DDE_DATA message
      with a NULL value rather than a handle to the global memory block
      containing a DDEDATA structure. The client will later post a normal
      WM_DDE_REQUEST message to obtain the actual data.) A TRUE value
      indicates that the client wants the data in the WM_DDE_DATA message.

  þ   The fAckReq ("acknowledgment requested") field is set to the value of
      the same field in the DDEADVISE structure. This is a very tricky
      value. A TRUE value instructs the server to post the WM_DDE_DATA with
      the fAckReq field of the DDEDATA structure set to TRUE so that the
      client is required to acknowledge the WM_DDE_DATA message with a
      WM_DDE_ACK message. A TRUE value does not mean that the client is
      requesting a WM_DDE_ACK message from the server; it's requiring that
      the server require a WM_DDE_ACK message from the client when later
      posting the WM_DDE_DATA message.

  þ   The lPopPrev field is set to the current population of the state.
      ServerProc uses this field to determine if the client needs
      notification that the population has changed.

ServerProc is now finished with the DDEADVISE structure and frees the memory
block as the documentation for WM_DDE_ADVISE instructs. ServerProc must now
acknowledge the WM_DDE_ADVISE message by posting a positive WM_DDE_SBACK
message. The fAck field of the DDEACK structure is set to TRUE. If
PostMessage fails, then ServerProc deletes the atom.

If the data format was not CF_TEXT, or if there was no match for the state,
then ServerProc posts a negative WM_DDE_BACK message. In this case, if the
PostMessage call fails, ServerProc both deletes the atom and frees the
DDEADVISE memory block.

In theory, handling of the WM_DDE_ADVISE message is now complete. However,
the client has asked that it be notified whenever a data item changes. Given
that the client doesn't know any value of the data item, it is necessary for
ServerProc to post a WM_DDE_DATA message to the client.

It does this using the PostDataMessage function, but with the third
parameter set to the fDeferUpd field of the POPADVISE structure, the fourth
parameter set to the fAckReq field of the POPADVISE structure, and the last
parameter set to FALSE (indicating a WM_DDE_DATA message posted in response
to WM_DDE_ADVISE rather than WM_DDE_REQUEST).

It's time for another look at PostDataMessage. Toward the beginning of the
function, note that if the fDeferUpd parameter is TRUE, then the function
simply sets hDdeData to NULL rather than allocating memory for it.

If the fAckReq parameter is TRUE, then PostDataMessage waits for a
WM_DDE_ACK message from the client after posting the WM_DDE_DATA message. It
does this by calling  PeekMessage. PostDataMessage deletes the atom in the
WM_DDE_ACK message. If the WM_DDE_ACK message does not arrive within three
seconds--or if the message is a negative acknowledgment--then
PostDataMessage frees the global data block containing the DDEDATA
structure.

If you think that you can skip over part of this work by assuming that a
client will never post a WM_DDE_ADVISE message with the deferred update or
acknowledgment requested fields set to TRUE, guess again. Microsoft Excel
does precisely that, establishing a warm link with acknowledgments to the
WM_DDE_DATA messages.


Updating the Items

After processing a WM_DDE_ADVISE message, a server is required to notify the
client when an item has changed. How this works depends on the server. In
the case of DDEPOP, a timer is used to recalculate the populations every 5
seconds. This occurs while processing the WM_TIMER message in WndProc.

WndProc then calls EnumChildWindows with the TimerEnumProc function (located
after ServerProc in DDEPOP.C). TimerEnumProc sends WM_TIMER messages to all
the child windows, which will all be using the ServerProc window procedure.

ServerProc processes the WM_TIMER message by looping through all the states
and checking if the POPADVISE structure field fAdvise is set to TRUE and the
population has changed. If so, it calls PostDataMessage to post a
WM_DDE_DATA message to the client.


The WM_DDE_UNADVISE Message

The WM_DDE_UNADVISE message instructs a server to stop posting WM_DDE_DATA
messages when a data item has changed. The low word of lParam is either the
data format or 0, indicating all data formats. The high word of lParam is
either the item ATOM or NULL to indicate all items.

DDEPOP handles the WM_DDE_UNADVISE message by setting the appropriate
fAdvise fields of the POPADVISE structure to FALSE, and then acknowledging
with a positive or negative WM_DDE_ACK message.


The WM_DDE_TERMINATE Message

When a client wishes to terminate the conversation, it posts a
WM_DDE_TERMINATE message to the server. The server simply responds with its
own WM_DDE_TERMINATE message back to the client. ServerProc also destroys
the child window on receipt of WM_DDE_TERMINATE because it is no longer
needed, and the conversation that the window has maintained is terminated.

ServerProc also processes WM_DDE_POKE and WM_DDE_EXECUTE messages, but in
both cases simply responds with a negative acknowledgment.

If DDEPOP is closed from its system menu, then it must terminate all DDE
conversations with its clients. So, when WndProc receives a WM_CLOSE
message, it calls  EnumChildWindows with the CloseEnumProc function.
CloseEnumProc sends WM- _CLOSE messages to all the child windows.

ServerProc responds to WM_CLOSE by posting a WM_DDE_TERMINATE message to the
client and then waiting for another WM_DDE_TERMINATE message back from the
client.



A DDE CLIENT PROGRAM

Now that we've examined a DDE server program that you can use with Microsoft
Excel, let's examine a DDE client program that uses DDEPOP as a server. This
program is called SHOWPOP and is shown in Figure 17-2.

 SHOWPOP.MAK

#-----------------------
# SHOWPOP.MAK make file
#-----------------------

showpop.exe : showpop.obj showpop.def
     link showpop, /align:16, NUL, /nod slibcew libw, showpop
     rc showpop.exe

showpop.obj : showpop.c
     cl -c -Gsw -Ow -W2 -Zp showpop.c

 SHOWPOP.C

/*----------------------------------------
   SHOWPOP.C -- DDE Client using DDEPOP
                (c) Charles Petzold, 1990
  ----------------------------------------*/

#include 
#include 
#include 
#include 

struct
     {
     char *szAbb ;
     char *szState ;
     long lPop ;
     }



     pop [] = {
              "AL", "Alabama",             0, "AK", "Alaska",
0,
              "AZ", "Arizona",             0, "AR", "Arkansas",
0,
              "CA", "California",          0, "CO", "Colorado",
0,
              "CT", "Connecticut",         0, "DE", "Delaware",
0,
              "DC", "Dist. of Columbia",   0, "FL", "Florida",
0,
              "GA", "Georgia",             0, "HI", "Hawaii",
0,
              "ID", "Idaho",               0, "IL", "Illinois",
0,
              "IN", "Indiana",             0, "IA", "Iowa",
0,
              "KS", "Kansas",              0, "KY", "Kentucky",
0,
              "LA", "Louisiana",           0, "ME", "Maine",
0,
              "MD", "Maryland",            0, "MA", "Massachusetts",
0,
              "MI", "Michigan",            0, "MN", "Minnesota",
0,
              "MS", "Mississippi",         0, "MO", "Missouri",
0,
              "MT", "Montana",             0, "NE", "Nebraska",
0,
              "NV", "Nevada",              0, "NH", "New Hampshire",
0,
              "NJ", "New Jersey",          0, "NM", "New Mexico",
0,
              "NY", "New York",            0, "NC", "North Carolina",
0,
              "ND", "North Dakota",        0, "OH", "Ohio",
0,
              "OK", "Oklahoma",            0, "OR", "Oregon",
0,
              "PA", "Pennsylvania",        0, "RI", "Rhode Island",
0,
              "SC", "South Carolina",      0, "SD", "South Dakota",
0,
              "TN", "Tennessee",           0, "TX", "Texas",
0,
              "UT", "Utah",                0, "VT", "Vermont",
0,
              "VA", "Virginia",            0, "WA", "Washington",
0,
              "WV", "West Virginia",       0, "WI", "Wisconsin",
0,
              "WY", "Wyoming",             0, "US", "United States Total", 0
              } ;

#define NUM_STATES       (sizeof (pop) / sizeof (pop [0]))
#define WM_USER_INITIATE (WM_USER + 1)
#define DDE_TIMEOUT      3000

long FAR PASCAL WndProc  (HWND, WORD, WORD, LONG) ;

char   szAppName [] = "ShowPop" ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     HWND     hwnd ;
     MSG      msg ;
     WNDCLASS wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (hInstance, szAppName) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "DDE Client - US Population",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     SendMessage (hwnd, WM_USER_INITIATE, 0, 0L) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static BOOL   fDoingInitiate = TRUE ;
     static char   szServerApp [] = "DdePop",
                   szTopic     [] = "US_Population" ;
     static HWND   hwndServer = NULL ;
     static short  cxChar, cyChar ;
     ATOM          aApp, aTop, aItem ;
     char          szBuffer [24], szPopulation [16], szItem [16] ;
     DDEACK        DdeAck ;
     DDEDATA FAR   *lpDdeData ;
     DDEADVISE FAR *lpDdeAdvise ;
     DWORD         dwTime ;
     GLOBALHANDLE  hDdeAdvise, hDdeData ;
     HDC           hdc ;
     MSG           msg ;
     PAINTSTRUCT   ps ;
     short         i, x, y ;
     TEXTMETRIC    tm ;
     WORD          wStatus, cfFormat ;
     switch (message)
          {
          case WM_CREATE :
               hdc = GetDC (hwnd) ;
               GetTextMetrics (hdc, &tm) ;
               cxChar = tm.tmAveCharWidth ;
               cyChar = tm.tmHeight + tm.tmExternalLeading ;
               ReleaseDC (hwnd, hdc) ;
               return 0 ;

          case WM_USER_INITIATE :

                     // Broadcast WM_DDE_INITIATE message

               aApp = GlobalAddAtom (szServerApp) ;
               aTop = GlobalAddAtom (szTopic) ;

               SendMessage (0xFFFF, WM_DDE_INITIATE, hwnd,
                            MAKELONG (aApp, aTop)) ;

                     // If no response, try loading DDEPOP first

               if (hwndServer == NULL)
                    {
                    WinExec (szServerApp, SW_SHOWMINNOACTIVE) ;

                    SendMessage (0xFFFF, WM_DDE_INITIATE, hwnd,
                                 MAKELONG (aApp, aTop)) ;
                    }

                    // Delete the atoms

               GlobalDeleteAtom (aApp) ;
               GlobalDeleteAtom (aTop) ;
               fDoingInitiate = FALSE ;

                    // If still no response, display message box

               if (hwndServer == NULL)
                    {
                    MessageBox (hwnd, "Cannot connect with DDEPOP.EXE!",
                                szAppName, MB_ICONEXCLAMATION | MB_OK) ;

                    return 0 ;
                    }

                    // Post WM_DDE_ADVISE messages
               for (i = 0 ; i < NUM_STATES ; i++)
                    {
                    hDdeAdvise = GlobalAlloc (GHND | GMEM_DDESHARE,
                                              sizeof (DDEADVISE)) ;

                    lpDdeAdvise = (DDEADVISE FAR *) GlobalLock (hDdeAdvise)
;

                    lpDdeAdvise->fAckReq   = TRUE ;
                    lpDdeAdvise->fDeferUpd = FALSE ;
                    lpDdeAdvise->cfFormat  = CF_TEXT ;

                    GlobalUnlock (hDdeAdvise) ;

                    aItem = GlobalAddAtom (pop[i].szAbb) ;

                    if (!PostMessage (hwndServer, WM_DDE_ADVISE, hwnd,
                                      MAKELONG (hDdeAdvise, aItem)))
                         {
                         GlobalFree (hDdeAdvise) ;
                         GlobalDeleteAtom (aItem) ;
                         break ;
                         }

                    DdeAck.fAck = FALSE ;

                    dwTime = GetCurrentTime () ;

                    while (GetCurrentTime () - dwTime < DDE_TIMEOUT)
                         {
                         if (PeekMessage (&msg, hwnd, WM_DDE_ACK,
                                          WM_DDE_ACK, PM_REMOVE))
                              {
                              GlobalDeleteAtom (HIWORD (msg.lParam)) ;

                              DdeAck = * (DDEACK *) & LOWORD (msg.lParam) ;

                              if (DdeAck.fAck == FALSE)
                                   GlobalFree (hDdeAdvise) ;

                              break ;
                              }
                         }

                    if (DdeAck.fAck == FALSE)
                         break ;

                    while (PeekMessage (&msg, hwnd, WM_DDE_FIRST,
                                        WM_DDE_LAST, PM_REMOVE))
                         {
                         DispatchMessage (&msg) ;
                         }
                    }

               if (i < NUM_STATES)
                    {
                    MessageBox (hwnd, "Failure on WM_DDE_ADVISE!",
                                szAppName, MB_ICONEXCLAMATION | MB_OK) ;
                    }
               return 0 ;

          case WM_DDE_ACK :

                    // In response to WM_DDE_INITIATE, save server window

               if (fDoingInitiate)
                    {
                    hwndServer = wParam ;
                    GlobalDeleteAtom (LOWORD (lParam)) ;
                    GlobalDeleteAtom (HIWORD (lParam)) ;
                    }
               return 0 ;

          case WM_DDE_DATA :

                    // wParam          -- sending window handle
                    // LOWORD (lParam) -- DDEDATA memory handle
                    // HIWORD (lParam) -- item atom

               hDdeData  = LOWORD (lParam) ;
               lpDdeData = (DDEDATA FAR *) GlobalLock (hDdeData) ;
               aItem     = HIWORD (lParam) ;

                    // Initialize DdeAck structure

               DdeAck.bAppReturnCode = 0 ;
               DdeAck.reserved       = 0 ;
               DdeAck.fBusy          = FALSE ;
               DdeAck.fAck           = FALSE ;

                    // Check for matching format and data item

               if (lpDdeData->cfFormat == CF_TEXT)
                    {
                    GlobalGetAtomName (aItem, szItem, sizeof (szItem)) ;

                    for (i = 0 ; i < NUM_STATES ; i++)
                         if (strcmp (szItem, pop[i].szAbb) == 0)
                              break ;
                    if (i < NUM_STATES)
                         {
                         lstrcpy (szPopulation, lpDdeData->Value) ;
                         pop[i].lPop = atol (szPopulation) ;
                         InvalidateRect (hwnd, NULL, FALSE) ;

                         DdeAck.fAck = TRUE ;
                         }
                    }

                    // Acknowledge if necessary

               if (lpDdeData->fAckReq == TRUE)
                    {
                    wStatus = * (WORD *) & DdeAck ;

                    if (!PostMessage (wParam, WM_DDE_ACK, hwnd,
                                      MAKELONG (wStatus, aItem)))
                         {
                         GlobalDeleteAtom (aItem) ;
                         GlobalUnlock (hDdeData) ;
                         GlobalFree (hDdeData) ;
                         return 0 ;
                         }
                    }
               else
                    {
                    GlobalDeleteAtom (aItem) ;
                    }

                    // Clean up

               if (lpDdeData->fRelease == TRUE || DdeAck.fAck == FALSE)
                    {
                    GlobalUnlock (hDdeData) ;
                    GlobalFree (hDdeData) ;
                    }
               else
                    {
                    GlobalUnlock (hDdeData) ;
                    }

               return 0 ;

          case WM_PAINT :
               hdc = BeginPaint (hwnd, &ps) ;

               for (i = 0 ; i < NUM_STATES ; i++)
                    {
                    if (i < (NUM_STATES + 1) / 2)
                         {
                         x = cxChar ;
                         y = i * cyChar ;
                         }
                    else
                         {
                         x = 44 * cxChar ;
                         y = (i - (NUM_STATES + 1) / 2) * cyChar ;
                         }

                    TextOut (hdc, x, y, szBuffer,
                             wsprintf (szBuffer, "%-20s",
                                       (LPSTR) pop[i].szState)) ;

                    x += 36 * cxChar ;

                    SetTextAlign (hdc, TA_RIGHT | TA_TOP) ;

                    TextOut (hdc, x, y, szBuffer,
                             wsprintf (szBuffer, "%10ld", pop[i].lPop)) ;

                    SetTextAlign (hdc, TA_LEFT | TA_TOP) ;
                    }

               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_DDE_TERMINATE :

                    // Respond with another WM_DDE_TERMINATE message

               PostMessage (hwndServer, WM_DDE_TERMINATE, hwnd, 0L) ;
               hwndServer = NULL ;
               return 0 ;

          case WM_CLOSE :
               if (hwndServer == NULL)
                    break ;

                    // Post WM_DDE_UNADVISE message

               PostMessage (hwndServer, WM_DDE_UNADVISE, hwnd,
                            MAKELONG (CF_TEXT, NULL)) ;

               dwTime = GetCurrentTime () ;

               while (GetCurrentTime () - dwTime < DDE_TIMEOUT)
                    {
                    if (PeekMessage (&msg, hwnd, WM_DDE_ACK,
                                     WM_DDE_ACK, PM_REMOVE))
                         break ;
                    }

                    // Post WM_DDE_TERMINATE message

               PostMessage (hwndServer, WM_DDE_TERMINATE, hwnd, 0L) ;

               dwTime = GetCurrentTime () ;

               while (GetCurrentTime () - dwTime < DDE_TIMEOUT)
                    {
                    if (PeekMessage (&msg, hwnd, WM_DDE_TERMINATE,
                                     WM_DDE_TERMINATE, PM_REMOVE))
                         break ;
                    }

               break ;             // for default processing

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 SHOWPOP.DEF

;------------------------------------
; SHOWPOP.DEF module definition file
;------------------------------------

NAME           SHOWPOP

DESCRIPTION    'DDE Population Client (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

This program displays the names of the states in its window with the updated
populations obtained from DDEPOP using the WM_DDE_ADVISE facility. You'll
note that SHOWPOP contains a structure called pop just like DDEPOP, but this
version contains the two-letter state abbreviations, the state names, and a
field called lPop (initialized with zeros) that will contain the updated
populations obtained from DDEPOP.

SHOWPOP carries on only one DDE conversation, so it only needs one window
for this conversation, and it uses WndProc for this purpose.

Initiating the DDE Conversation

I've chosen to initiate the DDE conversation by sending WndProc a
user-defined message (which I've called WM_USER_INITIATE) after the
UpdateWindow call in WinMain. Normally a client would initiate the
conversation in response to a menu command.

In response to this user-defined message, WndProc calls GlobalAddAtom to
create atoms for the application name of the server ("DdePop") and the topic
name ("US_Population"). WndProc broadcasts the WM_DDE_INITIATE message by
calling SendMessage with a 0xFFFF window handle.

As we've seen, a server that scores a match with the application and topic
atoms is required to send a WM_DDE_ACK message back to the client. Because
this message is sent using SendMessage rather than posted, the client will
receive the WM_DDE_ACK message before the original SendMessage call with the
WM_DDE_INITIATE message has returned. WndProc handles the WM_DDE_ACK message
by storing the window handle of the server in the variable hwndServer and
deleting the atoms that accompany the message.

If a client broadcasts a WM_DDE_INITIATE message with NULL application or
topic names, then it must be prepared to receive multiple WM_DDE_ACK
messages from each of the servers that can satisfy the request. In this
case, the client must decide which server to use. The others must be posted
WM_DDE_TERMINATE messages to terminate the conversation.

It is possible that hwndServer will still be NULL after the WM_DDE_INITIATE
SendMessage call. This means that DDEPOP is not running under Windows. In
this case, WndProc attempts to execute DDEPOP by calling WinExec. The
WinExec call searches the current directory and the PATH environment
variable to load DDEPOP. WndProc then again broadcasts the WM_DDE_INITIATE
message. If hwndServer is still NULL, then WndProc displays a message box
notifying the user of the problem.

Next, for each of the states listed in the pop structure, WndProc allocates
a DDEADVISE structure by calling GlobalAlloc with the GMEM_DDESHARE flag.
The fAckReq ("acknowledgment requested") flag is set to TRUE (indicating
that the server should post WM_DDE_DATA messages with the fAckReq field in
the DDEDATA field set to NULL). The fDeferUpd flag is set to FALSE
(indicating a hot link rather than a warm link), and the cfFormat field is
set to CF_TEXT. GlobalAddAtom adds an atom for the two-letter state
abbreviation.

This structure and the atom are passed to the server when SHOWPOP posts the
WM_DDE_ADVISE message. If the PostMessage call fails (which might happen if
DDEPOP is suddenly terminated), then SHOWPOP frees the memory block, deletes
the atom, and exits the loop.

Otherwise, SHOWPOP waits for a WM_DDE_ACK message by calling PeekMessage. As
the DDE documentation indicates, the client deletes the atom accompanying
the message, and also frees the global memory block if the client responds
with a negative acknowledgment.

It's quite likely that this WM_DDE_ACK message from the client will be
followed by a WM_DDE_DATA message for the item. For this reason, SHOWPOP
calls PeekMessage and DispatchMessage to extract any DDE messages from the
message queue and dispatch them to WndProc.


The WM_DDE_DATA Message

Following the WM_DDE_ADVISE messages, WndProc will receive WM_DDE_DATA
messages from the server containing updated population data. The low word of
lParam is a memory handle to a global block containing a WM_DDE_DATA
structure, and the high word of lParam is the atom identifying the data
item.

SHOWPOP checks if the cfFormat field of the DDEDATA structure is CF_TEXT.
(Of course, we know that DDEPOP uses CF_TEXT exclusively, but this is just
for the sake of completeness.) It then obtains the text string associated
with the item atom by calling GlobalGetAtomName. This text string is the
two-letter state abbreviation.

Using a for loop, SHOWPOP scans through the states looking for a match. If
it finds one, it copies the population data from the DDEDATA structure into
the szPopulation array, converts it to a long integer using the C function
atol ("ASCII to long"), stores it in the pop structure, and invalidates the
window.

All that remains now is cleaning up. If the client requested an
acknowledgment of the WM_DDE_DATA message, WndProc posts one. If no
acknowledgment was requested (or if the PostMessage call fails), then the
item atom is deleted. If the PostMessage call fails, or if there was no
match on the state (indicating a negative acknowledgment), or if the
fRelease flag in the DDEDATA structure is set to TRUE, then SHOWPOP frees
the memory block.

I originally wrote SHOWPOP so that it posted WM_DDE_ADVISE messages with the
fAckReq field of the DDEADVISE structure set to FALSE. This indicates to the
server that the WM_DDE_DATA messages should be posted with the fAckReq field
of the DDEDATA structure set to FALSE, which in turn indicates to the client
that it should not post WM- _DDE_ACK messages to the server acknowledging
the WM_DDE_DATA messages. This worked fine for normal updates. However, if I
changed the system time in Windows while SHOWPOP was running, then DDEPOP
posted 52 WM_DDE_DATA messages to SHOWPOP without waiting for
acknowledgment. This caused SHOWPOP's message queue to overflow, and it lost
many of the updated populations.

The lesson is clear: If a client wishes to be advised of many data items
that can change all at once, then it must set the fAckReq field of the
DDEADVISE structure to TRUE. This is the only safe approach.


The WM_DDE_TERMINATE Message

Handling a WM_DDE_TERMINATE message posted by the server is simple: SHOWPOP
simply posts another WM_DDE_TERMINATE message back to the client and sets
the hwndServer variable to NULL (indicating the conversation is over).

If SHOWPOP is closed (indicated by a WM_CLOSE message), then the program
first posts a WM_DDE_UNADVISE message to the server to prevent any future
updates. This uses a NULL item atom to indicate all data items. SHOWPOP then
posts a WM_DDE_TERMINATE message to the server and waits for a
WM_DDE_TERMINATE message to return back from the server.



WHEN THINGS GO WRONG

Programming can be comparatively easy when you can assume that nothing can
go wrong. But as you've seen, even when everything in a DDE conversation
proceeds as expected, complexities involving the creation and deletion of
atoms and global memory blocks can be tricky.

DDE is further complicated by the potential for problems, and these problems
are accentuated because there are two programs involved rather than just
one. You've seen that whenever a PostMessage calls (indicating that the
other program has unexpectedly terminated), you must clean up afterwards.

It is almost impossible to reach a point in DDE coding when you are
absolutely certain that you've accounted for all possibilities of
error--particularly when considering that the program your program is
communicating with may have errant behavior. The only advice I can offer
here is simply to do the best you can.