Faking DDE with Private Servers
An alternative to the protocol from hell
Joseph M. Newcomer
Dr. Joseph M. Newcomer received his PhD in the area of compiler optimization from Carnegie Mellon University in 1975. He can be contacted at 610 Kirtland St., Pittsburgh, PA 15208.
Client/server architecture is an elegant solution to a number of application-design problems. In Windows, Microsoft provides a protocol for doing client/server systems called dynamic data exchange (DDE), which is bundled with the 3.1 SDK. DDE is a complex protocol, as evident by the Microsoft Systems Journal article that referred to it as "the protocol from hell." Even the DDEML library doesn't make it easy to use or understand. Part of the complexity comes from the generality: This protocol is designed to allow clients, potentially on multiple machines, to talk to a central server.
OLE, on the other hand, is a very sophisticated protocol built on top of DDE. Like DDE, it provides for having multiple servers (perhaps on multiple machines), but handles the details of initiating the servers. The techniques described here are used for intratask servers. I don't deal with linked or embedded objects, just with distributed control.
However, on one project, I needed something much simpler than the fully general case--I needed a client/server architecture within the application itself. The application consisted of several modeless dialog boxes that had interacting state and had to maintain a consistent state. The number of dialog boxes present would vary. As each dialog box started up, it needed to determine the distributed state that affects its display, then either track the changing state or inform other boxes of the state it had changed.
In short, the application popped up a number of modeless dialog-box windows, each of which provided for database access under a different control scenario. (For the sake of example, I'll consider only a few controls.) Each window had a set of controls labeled << (go backward one record), >> (go forward one record), and First (go to top of database). These controls had to be enabled only when they were valid:
- When the database wasn't open, no control was valid.
- When EOF was hit, >> was to be disabled.
- When BOF was hit, << and First were to be disabled.
- When not at BOF or EOF, all motion buttons should be enabled.
Trying to keep track of who got notified by what means, when the buttons were updated, and so forth, was overly complex. Each new modeless dialog box required notification when the file status changed so it could be updated. It wasn't acceptable to update the windows only when they received the input focus, because the windows were small and could be displayed simultaneously; having one display one form of information and another display different information would be misleading to the user. Since the application was intended to be a single-user, single-application, single-instance server, I didn't worry about database locking (although it's easy to add).
Although Windows is multitasking, I've decided as a matter of programming practice that life is too short for local/global allocation, small/medium model, and other incidental distractions. Therefore, I program exclusively in the large model, because certain C compilers do not allow two instances of the same program to run under Windows. I was able to avoid locking by knowing that only a single instance of the program could be active at any time. (If I'd chosen to use the small or medium models and wanted to avoid locking, I could have used the hPrevInstance parameter to WinMain to determine if an instance was already running, informing the user accordingly.)
Finally, the program was a single-application environment; that is, it was the only program that can or will access the data. Although I use a commercially available database format, I assumed that end users wouldn't actually own the database engine. Still, I used the OF_SHARE_EXCLUSIVE flag to add a single, global, file-lock operation when the file is opened to ensure that no two applications can access the data.
What I ended up building was what I call a "private server," which is implemented as a window that accepts messages I send it and returns values to the sender. Initially, the window is created and made invisible. It accepts messages such as DB_OPEN, DB_GET_
NEXT_RECORD, and DB_CLOSE, which perform obvious operations. It also accepts messages to insert, delete, and modify database records. Field modification is done by sending a message such as DB_WRITE_FIELD, in which wParam is the field index and lParam is an LPSTR to the text to be put into the field. The details don't matter much, and you can apply this technique to any database or other application library. A nice feature of this technique is that the database engine is now separated from the application by this server, allowing you to use it with just about any engine.
The model I am using here is "centralized knowledge/distributed control." The "server" has all the "knowledge" about the state of the database, but has no "control." The many modeless dialog boxes have no knowledge of the state of the database, but are the initiators of actions (and hence have control). The problem arises because these distributed control points, the "clients," in fact need to have knowledge of the database (such as the current record number, what operations are valid, the actual data record, and so forth). The way they obtain this information is that the server distributes its knowledge to the clients when it knows the information has changed. The clients do not need to take responsibility for asking for information; they are told when things have changed. This is analogous to the "polling vs. interrupt" paradigm we have used for years: The peripheral device knows its state, and occasionally needs to inform the processor about it.
An interesting dual is that this is also a system that implements "distributed knowledge/centralized control." Each of the client windows possesses knowledge about its own state, and uses the server as a control mechanism for distributing this knowledge to other interested windows. In addition, each client window that needs knowledge of distributed state (such as the visibility of another client window) uses the server as a control mechanism for making queries about this distributed state.
The Event Registry
The real power comes from the notions of an event registry and of event notification. Any modeless dialog window I pop up knows how to find the database window (dbwind); once it does, it sends a message in its WM_INITDIALOG handler registering itself: SendMessage(dbwind, DB_REGISTER, hDlg, 0L). This causes the database window to add the window handle passed as wParam to its registry. Certain important events will now notify the registered windows by sending them messages. When a modeless dialog box is destroyed, it sends, as part of its WM_DESTROY handler, a DB_UNREGISTER message to ask the registry to remove it. The implementation of the DB_REGISTER and DB_
UNREGISTER handlers is trivial; see Listing Four (page 44).
I could have caused the database window to broadcast to all windows using the HWND_BROADCAST designator for the destination window (0xFFFF). However, this meant that every window in the system would receive these messages. Although the WM_USER identifier is supposed to be the user-defined message base, both existing Windows classes (such as ListBox) and other user-defined classes in applications unknown to me would be using numbers in the WM_
USER and higher range. Therefore, I would have to use RegisterWindowMessage to register all the messages, and UnregisterWindowMessage to remove them. This would unnecessarily complicate what was already turning into a significant effort. Therefore, I decided to force each of the modeless dialog boxes to register their desire instead.
Creating the Server
For the server window, I wanted to use a modeless dialog box but needed to return FAR pointers, 32-bit record values, and other 32-bit values as results. A dialog box normally returns True, False, or a specific type of value such as a brush handle (that is, for WM_
CTLCOLOR messages). Returning anything else in the return statement for any other message normally causes the dialog box to truncate the value to a 16-bit BOOL.
There are three alternatives:
- Don't use the CreateDialog family of API calls and "roll it by hand."
- Use the technique from Petzold for his HEXCALC program (Petzold chapter 10, p. 454ff) and live with 16-bit results.
- Use the undocumented SetDialog
MsgResult function in windowsx.h.
- Use the HEXCALC technique and the undocumented SetDialogMsgResult function in windowsx.h to have a dialog window and a 32-bit result. The SetDialogMsgResult macro uses SetWindowLong to set the DWL_
MSGRESULT word to a 32-bit result.
INITDIALOG processing, I create dbfile, a data structure, and store a pointer to it in this word. Subsequently, I set the local variable db to reference this value each time the procedure is activated. (Before WM_INITDIALOG processing, this pointer is NULL but is not used.)
I use an object-oriented programming technique here: the GDL_USER word is the equivalent to "self," so I only need to pass around the window handle to have access to the "self" pointer; I can also have multiple windows, each with its own private data pointer. By using a pointer to objects in your own heap, you avoid using "window extra" words which consume precious USER heap. For modal dialog boxes, you can even have the data object referenced by this pointer on the stack. An example of the creation and deletion of these objects is in the WM_INITDIALOG and WM_ DESTROY handlers in Listing Two (page 43).
The modeless dialog box associated with the server would normally not be seen by the end user; therefore, in specifying it to the dialog editor I remove the check mark from the Visible box, which removes the WS_VISIBLE property from the resource declaration. Figure 1 is an example of my server window.
The Server-window Procedure
The basic data structures for the server are shown in Listing One (page 43). The registry is a simple, doubly linked list structure. The dbfile structure holds all of the critical state information about the database file: filename (which is shown), an object used by the underlying database engine (which I give the placeholder DATABASE_APPLICATION_
THING dbref), and similar information. I omit most of these because the use of this technique to construct a database-server window isn't as important as the details of the generic "server window" architecture, which could be used for purposes other than a database server.
When a WM_CLOSE message comes in, I request that the window be destroyed. I could also handle WM_QUERYCLOSE if I wanted to interact with the user for confirmation, but this is handled by the top-level window in my app.
Finally, WM_DESTROY sends a DB_
CLOSE message to close the database file, frees up any memory or resources in use by the dbfile object, and finally frees the dbfile object itself, setting the reference in the dialog window to NULL just for completeness.
WM_COMMAND reports the events from the child controls. The only control I've illustrated here is the event from the Hide button, DB_HIDE, which sends a message to the window telling it to hide itself.
The remaining messages are DB_ messages that implement the actual operations of the private server. A DB_ message is sent to the window by one of its clients. The server can either return a 32-bit value as its result or, if it must return longer information (such as a string), it can accept in lParam a pointer to the location of the result.
As an example, the message DB_ GET_RECORD_NUMBER queries the database library for the current record number (via get_recno) and returns the 32-bit record number as the result of the SendMessage. DB_GET_FIELD takes a field number in wParam and a destination in lParam. Because the database has a limit of 256 bytes for a field, this code will handle the maximum field width, setting the value of the field into the control ID_DATA in one of the client dialog windows.
The Event Registry Redux
The real heart of the server is the event registry, a doubly linked list of windows which would like to receive notification events when various "interesting" things happen in the server. For example, the event of hitting end-of-file means that the Next Record button must be disabled or, if the file is closed, all the record-movement buttons must be disabled.
Each window to receive notification registers its intent with the server by sending the DB_REGISTER message. When the window is destroyed, it sends a DB_UNREGISTER message. The handlers for these are trivial; see Listing Three (page 43). The code in each dialog window is shown in Listing Five (page 44).
A notification message can be sent in several ways. Normally, whenever something "interesting" happens in the server, a notification is sent. This is a DBN_
NOTIFICATION message, and it is sent to all registered windows. Each DBN_
NOTIFICATION message reports the state of the database, using the protocol shown in Listing Six (page 44).
Whenever appropriate, any window can send a DB_NOTIFY message to the server, and all registered windows will receive a set of DBN_NOTIFICATION messages that accurately reflect the state of the database. The handler for this is shown in Listing Seven (page 44). An event, particularly DB_NOTIFY, can create more than one DBN_NOTIFICATION message. For example, if the file is positioned in an "internal" record (not the first or last record) and that record is deleted, DB_NOTIFY will send a DB_NOTIFY_MID (in middle of file) and DB_NOTIFY_DELETED. The response of a receiving window is to enable the Next Record, First Record, and Previous Record buttons, and set the text of the Delete button to Undelete. For debugging purposes, I added a Notify button to the server dialog window that forces it to send a DB_NOTIFY message to itself.
The DB_NOTIFY_ME message takes the window handle of the client as its wParam. To handle the initialization of a new modeless dialog box which is a server of the client, DB_NOTIFY_ME notifies a single window. This typically takes place during the WM_INITDIALOG handler for a client.
Whenever a field is changed, a DBN_
FIELD_CHANGE message is sent with HIWORD(lParam) being the index of the changed field. This is used only when an individual field--not an entire record--is changed, and its purpose is to allow the clients to update their displays of that one particular field. If all clients are to be kept up to date on a character-by-character basis with typein, you have to respond to EN_
CHANGE or EN_UPDATE messages in the client that has the window with the input focus.
DB_USER_NOTIFY handles the interaction of window state that is outside the scope of the database and is defined instead by the application. DB_USER_
NOTIFY can take a 16-bit notification code as its wParam, and it sends this to every registered window using the DBN_USER_NOTIFICATION message. For example, in my application one window contains a pushbutton that toggles the visibility of another window. If the target window is closed or minimized, the window(s) containing a Show button must be notified, so the legend can be changed from Hide to Show. However, the target window doesn't have to know which window(s) actually can control it; as part of its response to a minimize request, it sends a DB_USER_
NOTIFY message with the appropriate code to indicate it is closing, and all the windows in the registry are notified that it has closed. They may modify their local controls appropriately; see Listing Nine (page 44).
Finally, there's a protocol for passing state around. Any window whose state may be interesting is expected to define a set of DBN_USER_NOTIFICATION queries which will cause it to report its state (by means of DB_USER_
NOTIFY messages). Thus, a window which wants to know the state of the debug window (wherever it is), knows that if it sends out a DB_USER_NOTIFY request with the code UDB_QUERY_
DEBUG_STATE it will eventually receive a (possibly series of) DNB_USER_NOTIFICATION messages revealing the debug state. These might be defined as telling if the window is hidden or visible, if single stepping is on, or whatever is appropriate. What makes this interesting is that the window making the query only needs to know the query code and the server window handle; it doesn't need to know the window handle of the debug window. The debug window does not need to know who placed the query; it responds with a DB_USER_NOTIFY message to the server, which undertakes to deliver the notification to all registered windows. Once a window has defined the queries it will respond to, it does not need to know which window has made the query. An interesting architectural feature is that you can later reimplement the "debug window" as three windows, and none of the clients need to know that the reimplementation has taken place.
Window Visibility
The message DB_SHOW_STATUS takes SW_SHOW or SW_HIDE as a wParam. This causes the window to be displayed or hidden. (SW_SHOW opens iconic windows.) The message DB_GET_
SHOW_STATUS returns SW_SHOW or SW_HIDE to indicate the current state and is typically used in the main-menu processing to set the check mark properly during WM_INITMENUPOPUP processing, as shown in Listing Ten (page 44). The DB_GET_SHOW_STATUS handler is in Listing Eight (page 44).
The visibility of the server window was a bit tricky. The server window is a child window of the main window. No Windows protocol says, "Make this window the topmost window as long as it has the focus, then when it loses the focus, revert its topmostness to whichever window was topmost before." Without the ability to make this window the topmost, it can be partially obscured by, or even totally hidden behind, some other window which is already the topmost window. Additionally, if it is topmost--but doesn't have the focus--it can obscure other important windows such as a file-open dialog box.
To handle this, I adopted a protocol for my application whereby any window that wishes to be topmost must handle WM_SETFOCUS messages and make itself the topmost. It does not have to worry about determining which window was topmost. This is probably not optimal, but is the best I could do without the additional complication of doing EnumWindows. What I really want is a task-topmost window.
To make the window the topmost window, I use the handler in Listing Eleven (page 44). The SWP_NOACTIVATE flag is important; otherwise, the SetWindowPos will (as an additional side effect) cause focus to return the window, resulting in the window again being forced topmost.
In addition, because focus always returns to the server window, it becomes impossible to gain control of any other window, including the main window, so the menu bar becomes inaccessible! Unfortunately, Windows does not appear to have any way to force a clean "close" on such a runaway process; the only way to regain control is to use Ctrl-Alt-Del, and forcing task termination, which can leave the USER and GDI heaps cluttered with unrecoverable objects and references counts on DLLs artificially high. Eventually, you will have to restart Windows or reboot the machine to get into a clean state.
Field Changes
I've used several dBase III/IV compatible engines, the Paradox Engine, and a number of "proprietary" engines. One common feature is the ability to represent a field by a small integer and name, and map between the two representations. I chose to use the field number in all operations on fields because it is a compact representation (although I use 16 bits, most databases are limited to under 256 unique fields so an 8-bit number is actually sufficient). Fields are handled by the following messages:
- DB_GET_NFIELDS returns the number of fields in the database, numbered 1 through the number returned.
- DB_TYPE_BY_NUMBER returns a character code indicating the field type given the field number.
- DB_WIDTH_BY_NUMBER returns a character code indicating the field width, in characters, given the field number.
- DB_GET_FIELD takes a field number as wParam and a pointer to a buffer as lParam, and copies the contents of the record into the buffer.
- DB_SET_FIELD takes a field number as wParam and a pointer to a buffer as lParam, and copies the text of the buffer into the field. It also sends out a DBN_FIELD_CHANGE message to all registered users.
- DB_NAME_FROM_NUMBER takes a field number as wParam and a pointer to a buffer as lParam, and copies the name of the field into the buffer.
- DB_FIELDNO_BY_NAME takes a pointer to a field name as lParam and returns as its value the field number.
The handling of these messages is illustrated in Listing Two. For example, the db structure is assumed to have an array of field-width values in this sketch. Note that this code does not check that wParam is valid; in the actual code, the database engine provides this service for me. Operations such as CopyField and get_recno are other illustrations of calls to the underlying database engine.
The System Menu
The dialog box is of a fixed size; resizing is not supposed to be permitted. However, there is a system-menu box associated with this window, and it's necessary to disable the SC_SIZE menu option. Also, I didn't want to allow the user to actually close the window, so I had to disable the SC_CLOSE option too. The system-menu handling is shown in the server's pop-up menu handler in Listing Twelve (page 44).
Record Handling
Records are accessed by sequential or index order. DB_SKIP takes a signed lParam value indicating the number of records forward (positive) or backward (negative) to move; for an indexed database, a skip of 1 moves to the next record as determined by the index (next logical record), and for an unindexed database it moves to the next physical record. DB_REWIND and DB_GOTO_
EOF position the record pointer just before the first, or just after the last physical or logical record. An arbitrary record can be found by using DB_SEEK, whose lParam is a pointer to a text key. I plan to add DB_SEEK_LONG and DB_
SEEK_DATE for those (for me) rare cases in which a numeric or date value is used. This application required only a text-key seek.
One problem with the distributed control paradigm is that it's sometimes necessary for a client modeless dialog to do some processing if the active record position is about to be changed; for example, to make sure the values in edit boxes on the screen are properly copied to the record. In addition, it may not be permissible to change the record position (for example, if an edit is underway but a constraint is not met). A typical case might be that an illegal date or illegal number has been typed into an edit control. Consequently, all client windows honor the DBN_QUERY_
CHANGE message, which asks, "Are you willing to let the server move to another record?" If a window doesn't care, or successfully performs the operations it wishes to perform, it returns True; if it doesn't wish the current record to move, it returns False. In the latter case, DB_SEEK, DB_GOTO_, or DB_SKIP return an error code indicating that some other window prevented the operation from taking place.
If the operation is permitted, the record pointer is changed. After the successful completion, a DBN_NOTIFICATION message is sent to all registered clients indicating the position of the file: beginning, middle, or end. This allows the client windows enable or disable their "forward" or "backward" controls. A DBN_POS message is sent out to indicate the current record number.
To add a record, DB_APPEND_
BLANK is sent to the server. This first sends out a DBN_QUERY_CHANGE message to all the clients, since it requires moving from the current record. If successful, it appends a blank record, then sends out a DBN_NRECS message to tell the clients to update the number of records in the file (if they care), and a DBN_NOTIFICATION to indicate the file position.
A record can be deleted by marking it as a "deleted record." Later, an operation such as "pack," will physically remove all deleted records. (Some database engines actually delete the record when the delete operation occurs, or make it impossible to see a record marked as "deleted;" for these engines, the notion of hitting a deleted record is irrelevant.) When a record is selected, a DBN_
NOTIFICATION message is sent out indicating whether the record is deleted or not. In addition, the "delete" and "undelete" operations will send out notifications of the record's status.
File Status
Clients need to be notified about overall file status; for example, the database file may be closed at the time a client connects to the server, and notification about its opening is important. Similarly, if the file is open, notification about its closing is important. Finally, the transition from an empty to a nonempty file or vice versa turned out (in my application) to be important. All of these conditions generate DBN_NOTIFICATION messages.
Summary
Like any serious Windows application, this one required a lot of attention to details. What started out as a trivial exercise to avoid the "more complex" DDE protocol turned out to require more careful design than I had originally thought. It certainly required a great deal more code.
However, it was quite successful; I ended up with an architecture of centralized knowledge and distributed control, and the database server implemented as an active object. Since the implementation, I've added four new view windows to the application.
My next goal is to convert it to a DLL so I can use it with other applications. I'll also probably extend it to support better file and record locking so it can be used for shared database applications.
Figure 1: Sample server window.
[LISTING ONE]
typedef struct dbregistry { struct dbregistry * next; struct dbregistry * prev; HWND target; } dbregistry; typedef struct dbfile { char dbname[_MAX_PATH]; DATABASE_APPLICATION_THING dbref; /* ... other server-specific fields, mostly for debugging, go here... */ LONG count; /* number of messages processed */ /* ... end of server-specific fields */ dbregistry * registry; /* notifications */ } dbfile;
[LISTING TWO]
LONG FAR PASCAL IFS_WndProc(HWND hDBwnd, unsigned message, WPARAM wParam, LPARAM lParam) { dbfile * db = (dbfile *)GetWindowLong(hDBwnd, DBDATA); switch(message) { /* message */ case WM_INITDIALOG: /* ... Child control initialization goes here ... */ db = calloc(1,sizeof(dbfile)); SetWindowLong(hDBwnd, GDL_USER, (LONG) db); break; case WM_CLOSE: DestroyWindow(hDBwnd); break; case WM_DESTROY: SendMessage(hDBwnd, DB_CLOSE, 0, 0L); if(db->buffer != NULL) free(db->buffer); free(db); SetWindowLong(hDBwnd, DBDATA, NULL); break; case WM_COMMAND: switch(wParam) { /* wParam */ case IFS_HIDE: SendMessage(hIFwnd, DB_SHOW_STATUS, SW_HIDE, 0L); break; } /* wParam */ break; case DB_GET_RECORD_NUMBER: return get_recno(db, (LONG *)lParam); case DB_FIELD_WIDTH: return (LONG) db->fieldwidth[wParam]; case DB_GET_FIELD: CopyField((LPSTR)lParam, wParam); return lParam;
[LISTING THREE]
/****** RegisterHandle ************************************************ * Inputs: dbfile * db: server object -- HWND wnd: Window to register * Result: BOOL -- TRUE if success, FALSE if error * Effect: Adds the handle to the registry ***********************************************************************/ static BOOL RegisterHandle(dbfile * db, HWND wnd) { dbregistry * r; for(r = db->registry; r != NULL; r = r->next) { /* scan for membership */ if(r->target == wnd) return TRUE; /* already registered */ } /* scan for membership */ r = calloc(1, sizeof(dbregistry)); if(r == NULL) return FALSE; if(db->registry == NULL) db->registry = r; else { /* link on front */ r->next = db->registry; r->next->prev = r; db->registry = r; } /* link on front */ return TRUE; } /******** UnregisterHandle *********************************************** * Inputs: dbfile * db: server object -- HWND wnd: Window reference * Result: LONG -- SUCCESS if ok (currently it always succeeds) * Effect: Removes the registry entry for the given handle. * Notes: If the handle does not exist, this is acceptable **************************************************************************/ static LONG UnregisterHandle(dbfile * db, HWND wnd) { dbregistry * r; for(r = db->registry; r != NULL; r = r->next) { /* search */ if(r->target == wnd) { /* found it */ if(r->next != NULL) r->next->prev = r->prev; if(r->prev != NULL) r->prev->next = r->next; else db->registry = r->next; free(r); return SUCCESS; } /* found it */ } /* search */ /* Didn't find it. May want to return a code other than SUCCESS code */ return SUCCESS; }
[LISTING FOUR]
case DB_REGISTER: return RegisterHandle(db,wParam); case DB_UNREGISTER: return UnregisterHandle(db,wParam);
[LISTING FIVE]
case WM_INITDIALOG: SendMessage(server, DB_REGISTER, hDlg, 0L); ... break; case WM_DESTROY: SendMessage(server, DB_UNREGISTER, hDlg, 0L); ... break;
[LISTING SIX]
message: DBN_NOTIFICATION or DBN_USER_NOTIFICATION wParam: control id of database server in parent window lParam: LOWORD: window handle of database server window HIWORD: DBN_NOTIFICATION: one of the DB_NOTIFY_ codes, below DBN_USER_NOTIFICATION: wParam of the DB_USER_NOTIFY #define DB_NOTIFY_ERROR 0 /* error occurred */ #define DB_NOTIFY_EMPTY 1 /* file is empty */ #define DB_NOTIFY_BOF 2 /* file is at BOF */ #define DB_NOTIFY_EOF 3 /* file is at EOF */ #define DB_NOTIFY_MID 4 /* file is not empty, not at BOF, not at EOF */ #define DB_NOTIFY_DELETED 5 /* current record is deleted */ #define DB_NOTIFY_OPEN 6 /* file is opened */ #define DB_NOTIFY_CLOSE 7 /* file is closed */ #define DB_NOTIFY_POSITION 8 /* position has changed */ #define DB_NOTIFY_UNDELETED 9 /* current record is undeleted */ #define DB_NOTIFY_NONEMPTY 10 /* file is now nonempty */
[LISTING SEVEN]
/******** NotifyRegistry ************************************************ * Inputs: HWND hDBWnd: Our window -- WORD code: Code to include in message * WORD msg: DBN_NOTIFICATION or DBN_USER_NOTIFICATION * Result: void * Effect: Sends a notification message to each of the registered windows * Notes: message: DBN_NOTIFICATION or DBN_USER_NOTIFICATION * wParam: control id of database server in parent window * lParam: LOWORD: window handle of database server window * HIWORD: DBN_NOTIFICATION: one of the DB_NOTIFY_ codes, below * DBN_USER_NOTIFICATION: wParam of the DB_USER_NOTIFY **************************************************************************/ static void NotifyRegistry(HWND hIFSwnd, WORD msg, WORD code) { dbregistry * r; dbfile * db = (dbfile *)GetWindowLong(hDBwnd, IFSDATA); for(r = db->registry; r != NULL; r = r->next) { /* notify each */ SendMessage(r->target, msg, GetWindowWord(hDBwnd, GWW_ID), MAKELONG(hDBwnd, code)); } /* notify each */ } /* ... */ case DB_NOTIFY: NotifyRegistry
[LISTING EIGHT]
case DB_GET_SHOW_STATUS: if(IsVisible(hDBwnd)) { /* visible */ if(IsIconic(hDBwnd)) return SW_HIDE; else return SW_SHOW; } /* visible */ else { /* invisible */ return SW_HIDE; } /* invisible */
[LISTING NINE]
#define UWM_HIDE_EDITOR (WM_USER+217) #define UWM_SHOW_EDITOR (WM_USER+218) #define UDBN_HIDING_EDITOR 27 #define UDBN_SHOWING_EDITOR 28 /** Editor window: receives message to hide itself, and notifies all clients that it has hidden itself. **/ switch(message) { /* decode message */ /* ... other messages handled here */ case UWM_HIDE_EDITOR: ShowWindow(hDlg, SW_HIDE); SendMessage(server, DB_USER_NOTIFY, UDBN_HIDING_EDITOR, 0L); break; case UWM_SHOW_EDITOR: ShowWindow(hDlg, SW_SHOW); SendMessage(server, DB_USER_NOTIFY, UDBN_SHOWING_EDITOR, 0L); break; } /* decode message */ /** Client window which has editor hide/show button: update button text to indicate what will take place when the button is clicked. **/ switch(message) { /* decode message */ /* ... other messages handled here */ case DBN_USER_NOTIFICATION: switch(HIWORD(lParam)) { /* What notification? */ case UDBN_HIDING_EDITOR: SetDlgItemText(hDlg, ID_EDITOR, "Show"); break; case UDBN_SHOWING_EDITOR: SetDlgItemText(hDlg, ID_EDITOR, "Hide"); break; } /* What notification? */ } /* decode message */
[LISTING TEN]
case WM_INITMENUPOPUP: CheckMenuItem(GetMenu(hWnd), IDM_SHOW_DB, SendMessage(server, DB_GET_SHOW_STATUS, 0, 0L)==SW_SHOW ? MF_CHECKED : MF_UNCHECKED); ... other popup handling return 0;
[LISTING ELEVEN]
case WM_ACTIVATE: switch(wParam) { /* decode type */ case WA_INACTIVE: SetWindowPos(hDBwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); break; default: /* WA_(CLICK)ACTIVE */ SetWindowPos(hDBwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); break; } /* decode type */ SetDlgMsgResult(hDlg, message, 0); return true;
[LISTING TWELVE]
case WM_INITMENUPOPUP: if(HIWORD(lParam)) { /* system menu */ HMENU sys; sys = GetSystemMenu(hDBwnd, false); EnableMenuItem(sys, SC_SIZE, MF_GRAYED); EnableMenuItem(sys, SC_MAXIMIZE, MF_GRAYED); EnableMenuItem(sys, SC_CLOSE, MF_GRAYED); } /* system menu */ End Listings
Copyright © 1993, Dr. Dobb's Journal