Microsoft Systems Journal Volume 5


────────────────────────────────────────────────────────────────────────────

Volume 5 - Number 1

──────────────────────────────────────────────────────────────────────────── Accessing Presentation Manager Facilities from Within OS/2 Kernel Applications Richard Hale Shaw You've just ported an important DOS1 application to the OS/2 operating system. The program includes a module that offers full-screen editing with cut and paste capabilities. Unfortunately, you don't know how to let the cut-and-paste module access the Clipboard facility in OS/2 Presentation Manager (hereafter "PM"). Fortunately, even though the Clipboard is available only to PM programs, you can give OS/2 kernel programs access to the Clipboard and Dynamic Data Exchange (DDE) facilities using the PMServer program in this article. This article presents both PMServer and a sample program, PMAccess, which demonstrates how to access PMServer from a kernel program. First, I will discuss briefly the PM Clipboard services and what they offer. I will follow with a discussion of how PMServer uses the Clipboard. Next, I will present PMServer, the way it works, and how OS/2 kernel programs can use it. And finally, I willl discuss PMAccess. In a future article, I will present a version of PMServer that includes DDE support for kernel programs, and I will provide a version of PMAccess that takes advantage of this. For further information on how PM programs work (specifically the use of windows, messages, message queues, and window procedures), refer to the sidebar "A Presentation Manager Primer." The sidebar contains the essential issues and concepts that I will assume you have a basic understanding of, particularly in the discussion of PMServer. PM Clipboard The PM Clipboard is a facility that offers general cut, copy, and paste options for moving data to and from PM programs. These activities are not automatic; because the user usually initiates a cut, copy, or paste option, an application should not be able to initiate Clipboard operations without explicit instructions from the user. For additional information about automatic data transfer between applications, see "A Complete Guide to OS/2 Interprocess Communications and Device Monitors," MSJ (Vol. 4, No. 5). The classic way to use a clipboard is to select some text from a document in a word-processing program, cut or copy it into the clipboard, and then paste the text somewhere else. You can also cut, copy, and paste graphic images to transfer them between applications. The clipboard is only a temporary storage location: a user can alter its contents at any time, but once the computer is turned off, the contents of the clipboard are lost. Data Formats Although the user may move either text or graphic images into and out of the PM Clipboard, three standard formats are used for transferring Clipboard data: text, PM bitmaps, and PM metafiles. Text is an ASCII block that includes tabs and spaces. Each line is delimited by a linefeed, and the entire block is terminated with a NULL. PM bitmaps and PM metafiles are exclusive Presentation Manager formats. Because each data format is a different representation of the same Clipboard data (that is, the data most recently placed in the Clipboard), an application can choose the format most appropriate for its needs. Alternatively, an application can define new data formats for special purposes. Using the Clipboard The PM API offers several functions for accessing and manipulating the Clipboard: ■ WinOpenClipbrd opens the Clipboard ■ WinEmptyClipbrd removes any stored data from the Clipboard ■ WinSetClipbrdData places data into the Clipboard for a particular data format ■ WinCloseClipbrd closes the Clipboard ■ WinQueryClipbrdData retrieves data from the Clipboard in a specified data format ■ WinQueryClipbrdFmtInfo determines whether the data in the Clipboard is available in a particular format ■ WinEnumClipbrdFmts enumerates the available Clipboard formats An application program can become the Clipboard viewer, an application that maintains a window which displays the current contents of the Clipboard; or an application program can become the Clipboard owner, an application that supplies data to the Clipboard when it's to be drawn in the viewer or when it's being supplied to the Clipboard on a delayed basis. An application can delay data delivery to the Clipboard if it supports one or more formats that are time-consuming to render. Therefore, several additional Clipboard API calls are available, used primarily by applications that must become the Clipboard viewer or owner. These uses of the Clipboard are beyond the scope of PMServer, however, so I've limited their discussion here. Because more than one application must be able to access the Clipboard, it's important that the Clipboard's data be shareable. When an application passes a PM bitmap or a PM metafile to the Clipboard or receives one from the Clipboard, the Clipboard functions automatically make it shareable. Text, however, must be placed in a shared memory segment. The application does this by calling DosAllocSeg (with the SEG_GIVEABLE flag) to create the segment and then copying the text into the segment. Text is transferred between the Clipboard and the application via a shared memory selector; bitmaps and metafiles are transferred via a handle. Once a shared memory selector or a handle has been passed to the Clipboard, the selector or handle is invalid, and the application should not use it again. Furthermore, once a selector or a handle to Clipboard data has been returned from the Clipboard, the application must use it before closing the Clipboard--the selector or handle will no longer be valid once the Clipboard is closed. Thus, the application must copy the data to a local segment before closing the Clipboard if it is to use the data after the Clipboard is closed. Cutting and Copying To cut or copy data to the Clipboard, an application must first call WinOpenClipbrd. This function does not return until the Clipboard is available, so if another application has already opened the Clipboard, this function will wait until the Clipboard is available before it returns. However, the application can still receive and respond to messages in its window message queue. Once an application has opened the Clipboard, it should clear it with a call to WinEmptyClipbrd. It should then place the data into the Clipboard via a call to WinSetClipbrdData for each data format that the application supports (all clipboard formats coexist until the Clipboard is emptied again). Again, the application should not use a selector or a handle to data that has been passed to the Clipboard. Finally, the application should call WinCloseClipbrd to close the Clipboard and release it for use by other applications. Pasting Before an application can paste data from the Clipboard, it must retrieve the data as it does with the cutting and copying operations. To retrieve data from the Clipboard, an application first calls WinOpenClipbrd to open the Clipboard. Then it should call WinQueryClipbrdData, which will, for a specified format, return a selector or a handle to the data. If the data is available in the format specified, WinQueryClipbrdData returns a 32-bit integer, which is a handle for a bitmap or a metafile; if the data is text, the low 16 bits contain a selector to a shared memory segment. If the function returns a NULL, the specified format is not available, but the application can continue to call WinQueryClipbrdData for any data format that it supports, until either the data is returned or it runs out of supported formats. Finally, the application should either use the data immediately or copy it and then call WinCloseClipbrd. PMSERVER To PM and the Clipboard, PMServer (see Figure 1) looks like another PM application, but it's actually a liaison between kernel applications and PM. PMServer is, for the most part, an ordinary PM program; indeed, it accommodates only a few messages. It has a couple of frame flags set, and it's not visible. The only indication to the user that PMServer is running is its name on the Task Manager's task list, which is where you should terminate PMServer when you're ready. A unique aspect of PM necessitated the creation of PMServer as a separate program rather than as an object file or a DLL that can be linked to a kernel application. Clipboard interface functions are only available to a PM program with a PM message queue and a client window. Once a program opens a PM message queue, all its input and output must be performed by Presentation Manager, which means that a kernel application's attempts to use the Vio, Kbd, and Mou subsystems are ignored once the PM message queue is open (you can, of course, use AVio calls, but these still depend on PM programming). Even if you place the Clipboard-handling code in another program, you cannot run that program in the same session: once the program opens the window message queue, the PM screen group handles all I/O for that session. Therefore, the only convenient way to offer Clipboard services to a kernel application is to place the Clipboard-handling code in a separate process that runs in a separate session--a PM program. The next step in designing PMServer was to build an interface that allowed kernel applications to communicate with the PM program. Because it's crucial that PM applications keep their window message processing as efficient as possible, including the kernel program interface in PMServer's main thread or its window procedure was a bad idea. The logical alternative was to put this code in a separate thread of execution. PMServer thus creates a separate thread to receive and manage requests from kernel applications. To understand PMServer, you need to be familiar with PM programming terminology and a message queue system for kernel applications. The PMServer approach to programming OS/2 kernel applications uses OS/2 queues to pass messages between threads. It allows you to write event-driven, message-based applications in which a thread receives messages about events and reacts to them accordingly. (If this sounds like PM, it should--the idea is to imitate the approach to programming used in PM.) This system, contained in MSGQ.C (see Figure 2), is used extensively in PMAccess. It also provides a convenient means for a client application to communicate with PMServer and vice versa. In this manner, a kernel application can pass messages and data to PMServer by opening its queue and writing to it. If the application provides a queue of its own, PMServer can pass messages and data back to it in kind. Because the functions pass data by allocating a shareable segment, you can use the message queue functions to pass data and messages across applications the way they are used by PMServer and its clients. In addition, the message queue functions can pass data between threads; a keyboard thread can thus pass a variety of keyboard information to the main thread of an application. Before exploring PMServer's use of the Clipboard, let's enter the program from the side door: the queue used by kernel programs to make requests, and the thread that manages that queue. Queue Manager Thread PMServer's second thread is essentially a queue manager. Any kernel application can open the PMServer queue and pass messages to it. These messages are simple unsigned words (contained in PMSERVER.H) prefixed with PMS_ in the source code. They are processed by PMServer's second thread, which maintains a table of all the client processes using PMServer at any one time. A client process should therefore register itself (using the PMS_INIT message) with PMServer before attempting to use any of the services offered, and it should deregister itself (using the PMS_TERMINATE message) when it will no longer use PMServer or when it is about to terminate. In addition, a client program can create its own queue for receiving messages and Clipboard data from PMServer and can pass the name of the queue to PMServer at registration time. If it does, PMServer opens the queue and stores the queue handle in the table. Although a kernel program should go through this registration process, it doesn't always have to; it can bypass registration and the need to offer a return queue when it is only placing data in the Clipboard. In that case, PMServer only needs the data to be placed in the Clipboard, which the kernel application can pass with a PMS_COPY message (see Figure 3). A kernel application that only uses PMS_COPY can access PMServer without registering with it or providing a return queue. Copy Operation PMServer's second thread processes messages from kernel applications that are received in its OS/2 queue. Depending on the nature of these messages, the second thread notifies PMServer's window procedure, which sets up and calls the Clipboard interface functions. Therefore, if the second thread finds a PMS_COPY message in the OS/2 queue, it calls WinPostMsg to place a WM_COPY message (and the data to be placed in the Clipboard) in the window message queue of PMServer's window procedure. Note that WinPostMsg merely places the message in the queue and returns right away; the alternative, WinSendMsg, calls the window procedure directly and doesn't return until the window procedure has processed the message, which could be a long while if its message queue is full. Because a long wait could tie up both the window procedure and the second thread at the same time, this alternative wasn't chosen. Instead, the second thread posts the message to the window procedure with WinPostMsg and returns to process the next message received from a kernel application. Once PMServer's window procedure receives a WM_COPY message, it prepares the data to place it in the Clipboard. It creates the segment with a call to DosAllocSeg (with the SEG_GIVEABLE flag) and copies the data into the segment. It then opens and empties the Clipboard. When it calls WinSetClipbrdData to place the data in the Clipboard, it tells the Clipboard that the data is text by using the CF_TEXT and CFI_SELECTOR flags. (PMServer assumes that any data transferred between kernel applications is text. If your kernel application uses another data format, modify PMServer and include the data format information in the message passed from the kernel application.) Finally, the window procedure closes the Clipboard. Paste Operation Although processing a PMS_COPY message from a kernel application is relatively straightforward, providing it with paste services is not. The problem is the difference in the way a PM application and a kernel application notify users that paste services are available. In a typical PM scenario, a user selects the Edit pull-down menu from the PM application's menu bar. This action generates a message to display the menu, which is received by the application's window procedure. The window procedure can call WinQueryClipbrdFmtInfo before displaying the menu and determine whether data for a particular format is available in the Clipboard. It can then enable or disable the Paste entry in the Edit pull-down menu before it is displayed. Unfortunately, the lack of uniformity among character-based interfaces necessitated a multistep approach to the paste operation. First, a kernel application should post a PMS_CLPBRD_QUERY message to PMServer, which generates a WM_PASTE_MSG message in PMServer's window message queue. Upon receiving this message, the window procedure calls WinQueryClipbrdFmtInfo and posts a reply message (either PMS_CLPBRD or PMS_CLPBRD_EMPTY) back to the kernel application via its return queue. Therefore, the kernel application can use PMS_CLPBRD_QUERY to determine whether Clipboard data is available for pasting. Indeed, a kernel application can periodically send this message to PMServer and be kept abreast of the status of the Clipboard, a technique described in the discussion of PMAccess below. If an application receives PMS_CLPBRD_EMPTY, it should notify the user that no data is available in the Clipboard. If, however, the application receives PMS_CLPBRD after posting a PMS_CLPBRD_QUERY, it should immediately reply with PMS_PASTE, which will generate a WM_PASTE message in PMServer's window message queue. Upon receiving this message, PMServer's window procedure will open the Clipboard, retrieve the data, and pass the data back to the kernel application with a PMS_CLPBRD_DATA message, indicating that it has received data for pasting. The window procedure then closes the Clipboard. As you can see, PMServer deals directly with the Clipboard: kernel applications can use it to access the Clipboard's cut and paste operations, without having to be PM applications themselves. PMAccess PMAccess is a program demonstrating the use of PMServer's Clipboard facilities in an OS/2 kernel application. The program lets you use the mouse to select text or program functions. Program functions are available via screen buttons and accelerator hot keys (Esc for the Esc button, Alt-O for Copy, Alt-U for Cut, Alt-P for Paste, and Alt-C for Clear). When you run PMAccess (be sure to start PMServer first), it displays the five buttons at the bottom of the window (see Figure 4). You can type text into the input screen above these buttons and use the mouse to select the text. To copy or cut a block of text to the Clipboard, simply select some text and press the appropriate screen button with the mouse (see Figure 5). The Paste button will remain nonfunctional until some data is available in the Clipboard for pasting. Once Clipboard data is available, the Paste button will light up. Then you can press this button and PMAccess will paste the data at the current cursor location (see Figure 6). PMAccess: A Look under the Hood PMAccess (see Figure 7) relies heavily on several modules. MSGQ.C contains the message queue functions that are the backbone of PMAccess's event-driven, message-based architecture. BUTTON.C (see Figure 8) contains the functions for generating and managing the screen buttons used in the interface. The code for the keyboard and mouse threads is contained in KBD.C (see Figure 9) and MOU.C (see Figure 10). PMAccess's keyboard thread is relatively simple. It opens the message queue to PMAccess, sets up the keyboard, and notifies PMAccess that it's ready by clearing a semaphore. It then waits for a key and returns the key information as part of a MSG_CHAR message to PMAccess's main thread. The mouse thread is slightly more complex: it also opens a queue to PMAccess, initializes the mouse, and notifies PMAccess by clearing a semaphore. But as it receives information on each mouse event, it looks first for mouse clicks on one of the screen buttons. Then it passes along only those mouse clicks that fall in the input screen, filtering out those that fall below (and did not activate one of the screen buttons). In addition to the keyboard and mouse threads, PMAccess creates a third thread, which periodically checks with PMServer about the availability of Clipboard data (see Figure 11). This request thread posts a PMS_CLPBRD_QUERY message to PMServer approximately every 30 seconds, causing PMServer to reply with either PMS_CLPBRD (data is available) or PMS_CLPBRD_EMPTY (the Clipboard is empty). When PMAccess receives one of these messages, it highlights the Paste button (indicating that pasting is allowed) or sets it to normal (pasting not allowed) so that the user will know whether he or she can use the Paste facility. The PMAccess program (found in PMACCESS.C) begins by obtaining its own process ID (for use with some of the PMServer functions) and by creating its main message queue. It then opens the message queue of PMServer and starts the keyboard thread, the mouse thread and the request thread. After initializing and displaying the buttons and the screen, PMAccess obtains a pointer to its video buffer, which it will use to access selected blocks of text. It then registers itself with PMServer via a call to PMS_Init. The balance of PMAccess's code is a switch statement that contains responses to the messages it expects to receive. These include responses for processing screen button presses (MSG_ESCAPE, MSG_COPY, and so on), as well as responses to mouse events (MSG_B1DOWN, MSG_MOUSEMOVED). When these messages are received, PMAccess can access the appropriate keyboard or mouse data via the MOUMSG or KBDMSG macros, which, along with the message definitions, can be found in MSGS.H. To communicate with PMServer, PMAccess needs to use only the messages in PMSERVER.H, which includes the name of PMServer's message queue. The code for handling cut and copy requests from the user (see the case for MSG_COPY and MSG_CUT) is virtually identical: the difference lies in blanking out the text block for cut and resetting the attributes of the highlighted area to normal video for copy. PMServer offers features for OS/2 kernel applications that have been previously limited to PM applications. You can make your own OS/2 kernel programs work with PM Server by adapting PMAccess or by simply cribbing some code from it. In a future article, I'll present expanded versions of PMServer and PMAccess that provide access to Presentation Manager's DDE facilities. Figure 1 PMSERVER.MAK #-------------------- # pmserver make file #-------------------- COPT=/Lp /W3 /Zpiel /G2sw /I$(INCLUDE) /Od /Alfw errexit.obj: errexit.c errexit.h cl /c $(COPT) errexit.c msgq.obj: msgq.c msgq.h cl $(COPT) /c msgq.c pmserver.obj : pmserver.c pmserver.h cl /c $(COPT) pmserver.c pmserver.exe : pmserver.obj pmserver.def errexit.obj msgq.obj link pmserver errexit msgq, /align:16 /co, NUL,os2 llibcmt, pmserver PMSERVER.H /* pmserver.h common header file for PMServer and client programs */ #define PMSERVERQUE "\\QUEUES\\PMSERVER.QUE" // messages sent by client #define PMS_INIT 100 // client initializing #define PMS_COPY 101 // copy data to clipboard #define PMS_PASTE 103 // get data from clipboard #define PMS_TERMINATE 104 // client is terminating #define PMS_CLPBRD_QUERY 105 // is anything in clipboard? // messages sent by server #define PMS_CLPBRD 120 // clipboard data available #define PMS_NO_INIT 121 // can't initialize client #define PMS_INIT_ACK 122 // server acknowledges init #define PMS_MSG_UNKNOWN 123 // server can't identify msg #define PMS_CLPBRD_EMPTY 124 // clipboard data not avail #define PMS_CLPBRD_DATA 125 // here's your clipboard data typedef struct _clientdata { PID pid; BYTE qname[21]; } CLIENTDATA; #define CLIENTDATAMSG(ptr) ((CLIENTDATA *)ptr) #define MAXAPPNAME 50 #define MAXTOPICNAME 50 PMSERVER.C /* PM program that supplies PM services to VIO apps */ #define INCL_WIN #define INCL_VIO #define INCL_AVIO #define INCL_DOS #include<os2.h> #include<mt\stdlib.h> #include<mt\process.h> #include<mt\string.h> #include"pmserver.h" #include"msgq.h" #include"errexit.h" #define WM_COPY (WM_USER + 0) #define WM_PASTE (WM_USER + 1) #define WM_PASTE_MSG (WM_USER + 2) typedef struct _client { PID clientpid; HQUEUE clientqhandle; } CLIENT; #define MAXCLIENTS 25 CLIENT clients[MAXCLIENTS]; HAB hab; HWND ThreadWindowHdl; #define QMGRTHREADSTACK 500 BYTE QMgrThreadStack[QMGRTHREADSTACK]; HQUEUE qhandle; int main(void); MRESULT EXPENTRY ClientWndProc(HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2); void QMgrThread(void); USHORT QMgrFindClient(PID pid,HQUEUE *qhandle,USHORT *position); int main(void) { static CHAR szClientClass[] = "PMServer"; static ULONG flFrameFlags = FCF_TASKLIST; HMQ hmq; HWND hwndFrame, hwndClient; QMSG qmsg; MsgQCreate(&qhandle,PMSERVERQUE); // create input queue hab = WinInitialize(0); // initialize window // start queue manager thread if(_beginthread(QMgrThread,QMgrThreadStack, sizeof(QMgrThreadStack),NULL) = = -1) error_exit(0,"_beginthread(QMgrThread)"); hmq = WinCreateMsgQueue(hab, 0); // create window message queue // register window class WinRegisterClass(hab, szClientClass, ClientWndProc, 0L, 0); // create window hwndFrame = WinCreateStdWindow(HWND_DESKTOP, 0L, &flFrameFlags, szClientClass, NULL, 0L, NULL, 0L, &hwndClient); while(WinGetMsg(hab, &qmsg, NULL, 0, 0)) // process messages WinDispatchMsg(hab, &qmsg); WinDestroyWindow(hwndFrame); // destroy window WinDestroyMsgQueue(hmq); // destroy message queue WinTerminate(hab); // terminate window return 0; } MRESULT EXPENTRY ClientWndProc(HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2) { SEL ClipTextSel; PVOID temp1,temp2; USHORT len; PCH temp3; USHORT fmtInfo; switch(msg) { // ************* Create processing ********** case WM_CREATE: ThreadWindowHdl = hwnd; return 0; // **************Clipboard handling ********* case WM_COPY: // place client data in Clpbrd DosAllocSeg(SHORT1FROMMP(mp2),&ClipTextSel,SEG_GIVEABLE); temp1 = MAKEP(ClipTextSel,0); temp2 = PVOIDFROMMP(mp1); memmove(temp1,temp2,SHORT1FROMMP(mp2)); temp1 = temp2 = NULL; WinOpenClipbrd(hab); // open the clipboard WinEmptyClipbrd(hab); // empty it // put the new data in it WinSetClipbrdData(hab, (ULONG)ClipTextSel, CF_TEXT, CFI_SELECTOR); WinCloseClipbrd(hab); // close the clipboard return 0; case WM_PASTE: // get client data from Clpbrd WinOpenClipbrd(hab); // open the clipboard ClipTextSel = (SEL)WinQueryClipbrdData(hab, CF_TEXT); if(ClipTextSel) // if data in clipboard { temp3 = temp1 = MAKEP(ClipTextSel,0); for( len = 0; temp3[len]; len++); // get data length len++; // include the NULL // send data to client MsgQSend(SHORT1FROMMP(mp1),temp1,len,PMS_CLPBRD_DATA); } else // nothing in clipboard MsgQSend(SHORT1FROMMP(mp1),NULL,0,PMS_CLPBRD_EMPTY); WinCloseClipbrd(hab); return 0; case WM_PASTE_MSG: // client query about Clpbrd // tell client whether avail MsgQSend(SHORT1FROMMP(mp1),NULL,0, (WinQueryClipbrdFmtInfo(hab,CF_TEXT,&fmtInfo) ? PMS_CLPBRD : PMS_CLPBRD_EMPTY)); return 0; } return WinDefWindowProc(hwnd, msg, mp1, mp2); } void QMgrThread(void) { PVOID msgdata = NULL; USHORT msg,i,msgsize,position; HQUEUE temphandle; PBYTE tempname; PID temppid; memset(clients,0,sizeof(clients)); while(TRUE) { temppid = 0; MsgQGet(qhandle, &msgdata, &msgsize, &msg); switch(msg) { case PMS_COPY: break; case PMS_INIT: case PMS_TERMINATE: case PMS_CLPBRD_QUERY: case PMS_PASTE: default: temppid = (msgdata ? CLIENTDATAMSG(msgdata)->pid : 0); break; } switch(msg) { case PMS_INIT: // client is initializing to use the server if(msgdata) // if valid message packet { tempname = CLIENTDATAMSG(msgdata)->qname; if(*tempname) // client has return queue MsgQOpen(&temphandle,tempname); // find open slot for( i = 0; i < MAXCLIENTS && clients[i].clientpid; i++); if(i = = MAXCLIENTS) // no slots found { if(temphandle) MsgQSend(temphandle,NULL,0,PMS_NO_INIT); } else // got open slot { clients[i].clientpid = CLIENTDATAMSG(msgdata)->pid; if(temphandle) { clients[i].clientqhandle = temphandle; MsgQSend(temphandle,NULL,0,PMS_INIT_ACK); } } } tempname = NULL; break; case PMS_COPY: // client has data for the Clipboard WinPostMsg(ThreadWindowHdl,WM_COPY,MPFROMP(msgdata), MPFROMSHORT(msgsize)); msgdata = NULL; break; case PMS_CLPBRD_QUERY: // client wants to know if something's in the clipboard if(QMgrFindClient(temppid,&temphandle,&position)) WinPostMsg(ThreadWindowHdl,WM_PASTE_MSG, MPFROMSHORT(temphandle),NULL); break; case PMS_PASTE: // client is requesting a copy of the Clipboard data if(QMgrFindClient(temppid,&temphandle,&position)) WinPostMsg(ThreadWindowHdl,WM_PASTE, MPFROMSHORT(temphandle),NULL); break; case PMS_TERMINATE: // client is terminating or is no longer using PM Services if(QMgrFindClient(temppid,&temphandle,&position)) { // if not last slot, shift down 1 if(position != MAXCLIENTS-1) memmove(&clients[position],&clients[position+1], (sizeof(CLIENT)*(MAXCLIENTS-position-1))); // clear last slot memset(&clients[MAXCLIENTS-1],0,sizeof(CLIENT)); } break; default: // unknown message rec'd, notify client if it has a queue if(QMgrFindClient(temppid,&temphandle,&position)) MsgQSend(temphandle,NULL,0,PMS_MSG_UNKNOWN); break; } if(msgdata) DosFreeSeg(SELECTOROF(msgdata)); msgdata = NULL; } } // finds PMServer client and qhandle in table USHORT QMgrFindClient(PID pid,HQUEUE *qhandle,USHORT *position) { USHORT i; for( i = 0; i < MAXCLIENTS; i++) if(clients[i].clientpid = = pid) { *qhandle = clients[i].clientqhandle; *position = i; return TRUE; } return FALSE; } Figure 2 MSGQ.H /* msgq.h Message Queue prototypes */ void MsgQCreate(HQUEUE *qhandle, char *qname); USHORT MsgQOpen(HQUEUE *qhandle, char *qname); void MsgQSend(HQUEUE qhandle, PVOID event, USHORT size, USHORT msg); void MsgQClose(HQUEUE qhandle); void MsgQGet(HQUEUE qhandle, PVOID *event, USHORT *size, USHORT *msg); MSGQ.C /* msgq.c message queue routines */ #define INCL_DOS #include<os2.h> #include<string.h> #include<malloc.h> #include"errexit.h" #include"msgq.h" typedef struct _qtable // message queue table structure { HQUEUE qhandle; PID qowner; } QTABLE; #define MAXQUEUES 15 QTABLE qtable[MAXQUEUES]; // message queue table qtableindex = 0; // creates a new message queue void MsgQCreate(HQUEUE *qhandle, char *qname) { USHORT retval; if(retval = DosCreateQueue(qhandle,QUE_FIFO,qname)) error_exit(retval,"DosCreateQueue"); } // opens an existing message queue USHORT MsgQOpen(HQUEUE *qhandle, char *qname) { PID qowner; USHORT retval; if(retval = DosOpenQueue(&qowner,qhandle,qname)) // open queue { *qhandle = 0; return retval; } qtable[qtableindex].qhandle = *qhandle; // put handle and PID qtable[qtableindex].qowner = qowner; // into table qtableindex++; return 0; } // sends a message void MsgQSend(HQUEUE qhandle, PVOID event, USHORT size, USHORT msg) { USHORT retval,i; SEL sel,newsel; PVOID qptr; if(event != NULL) // if data with the message { for(i = 0; i < qtableindex; i++) // find the queue if(qtable[i].qhandle = = qhandle) break; // create a segment for data if(retval = DosAllocSeg(size,&sel,(SEG_GIVEABLE))) error_exit(retval,"DosAllocSeg"); DosGiveSeg(sel,qtable[i].qowner,&newsel); // make it giveable qptr = MAKEP(newsel,0); memmove(qptr,event,size); // put the data in it } else qptr = NULL; // write message+data to q if(retval = DosWriteQueue(qhandle,msg,size,(PBYTE)qptr,0)) error_exit(retval,"DosWriteQueue"); if((qptr != NULL) && (sel != newsel)) // free seg if not ours DosFreeSeg(sel); } // get a message from a queue void MsgQGet(HQUEUE qhandle, PVOID *event, USHORT *size, USHORT *msg) { QUEUERESULT qresult; BYTE priority; USHORT retval; if(retval = DosReadQueue(qhandle,&qresult,size, (PVOID FAR *)event,0x0000,DCWW_WAIT,&priority,0L)) error_exit(retval,"DosReadQueue"); *msg = qresult.usEventCode; } // close queue and remove from table void MsgQClose(HQUEUE qhandle) { USHORT i; DosCloseQueue(qhandle); for(i = 0; i < qtableindex; i++) if(qtable[i].qhandle = = qhandle) break; if(i != qtableindex) memmove(&qtable[i],&qtable[i+1], (sizeof(QTABLE)*(qtableindex -(i+1)))); qtableindex--; } Figure 7 PMACCESS.MAK # # make file for pmaccess.c # #COPT=/Lp /W3 /Zp /Zl /G2s /Ox /I$(INCLUDE) /Alfw COPT=/Lp /W3 /Zpiel /G2s /I$(INCLUDE) /Alfw /Od kbd.obj: kbd.c kbd.h msgs.h msgq.h kbddefs.h button.h cl $(COPT) /c kbd.c mou.obj: mou.c mou.h msgs.h msgq.h moudefs.h button.h errexit.h cl $(COPT) /c mou.c msgq.obj: msgq.c msgq.h cl $(COPT) /c msgq.c button.obj: button.c button.h cl $(COPT) /c button.c errexit.obj: errexit.c errexit.h cl /c $(COPT) errexit.c pmaccess.obj: pmaccess.c pmaccess.h moudefs.h kbddefs.h msgq.h msgs.h mou.h kbd.h cl /c $(COPT) pmaccess.c pmaccess.exe: pmaccess.c pmaccess.mak msgq.obj button.obj errexit.obj \ mou.obj kbd.obj cl $(COPT) pmaccess msgq button errexit kbd mou /link /co /noi llibcmt markexe windowcompat pmaccess.exe PMACCESS.H /* pmaccess.h macros and typedefs for dipop.c */ #define BTOGGLE 0x0001 #define BPRESS 0x0002 #define INPUT 0X0003 typedef struct _button { char *text; // text to be displayed USHORT startrow; // upper row USHORT startcol; // left column USHORT endrow; // lower row USHORT endcol; // right column BYTE attribute; // color attribute USHORT type; // type of object USHORT left_button_val; // mouse left button event USHORT right_button_val; // mouse right button event USHORT accelerator; // keyboard event USHORT state; // button state: on or off } BUTTON; #define MOUMSG(ptr) ((MOUEVENTINFO *)ptr) #define KBDMSG(ptr) ((KBDKEYINFO *)ptr) PMACCESS.C /* pmaccess.c application that uses PMSERVER for accessing PM services This application is a VIO app that offers the following services: 1. It allows the user to position the cursor anywhere in the input screen 2. It allows the user to type text into any part of the input screen 3. It allows the user to mark any text in the input screen 4. It allows the user to copy, cut, or paste any part of the input screen. */ #define INCL_SUB #define INCL_DOSERRORS #define INCL_DOS #include<os2.h> #include<mt\stdio.h> #include<mt\string.h> #include<mt\stdlib.h> #include<mt\process.h> #include<mt\dos.h> #include"mou.h" #include"kbd.h" #include"pmserver.h" #include"moudefs.h" #include"kbddefs.h" #include"msgq.h" #include"button.h" #include"errexit.h" #include"msgs.h" #define MAINMSGQUEUE "\\QUEUES\\PMACCESS.QUE" #define VIOHDL 0 #define KEYTHREADSTACK 500 // keyboard thread stack #define REQUESTTHREADSTACK 500 // request thread stack #define MOUTHREADSTACK 800 // mouse thread stack #define BORDERCOL 79 #define BORDERROW 21 #define STARTROW 0 #define ENDROW 20 #define BUTTON_ON 0x70 #define BUTTON_OFF 0x0f #define NORMAL BUTTON_OFF #define HIGHLIGHT BUTTON_ON #define beep() DosBeep(440,200) #define Lastchar(str) (str[strlen(str)-1]) #define MAXREQUESTCOUNT 10 #define REQUESTTHREADSLEEPTIME 3000L /********************* Buttons ****************************/ #define BUTTONLIST 6 BUTTON buttonlist[BUTTONLIST] = { /* text title row,col,row,col,attr, type left val, right val, accelerator */ {" Copy ","", 22, 0, 0, 0, BUTTON_OFF, BPRESS, MSG_COPY, MSG_COPY, ALT_O, 0}, {" Cut ","", 22, 9, 0, 0, BUTTON_OFF, BPRESS, MSG_CUT, MSG_CUT, ALT_U, 0}, {" Paste ","", 22,17, 0, 0, BUTTON_OFF, BPRESS, MSG_PASTE, MSG_PASTE, ALT_P, 0}, {" Clear ","", 22,63, 0, 0, BUTTON_OFF, BPRESS, MSG_CLR, MSG_CLR, ALT_C, 0}, {" Esc ","", 22,73, 0, 0, BUTTON_OFF, BPRESS, MSG_ESCAPE, MSG_ESCAPE, ESC, 0}, {NULL, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} }; typedef struct _screen_label { PCHAR text; SHORT row; SHORT column; } SCREEN_LABEL; #define SCREEN_LABELS 2 SCREEN_LABEL screen_labels[SCREEN_LABELS] = { {"Clipboard Functions", BORDERROW, 3}, {"General Functions", BORDERROW, 63} }; PCHAR mainmsgqueue = MAINMSGQUEUE; BYTE MouThreadStack[MOUTHREADSTACK]; BYTE KbdThreadStack[KEYTHREADSTACK]; BYTE RequestThreadStack[REQUESTTHREADSTACK]; long MouSem = 0L, KbdSem = 0L; unsigned BlankCell = 0x0f20; USHORT dirrow = BORDERROW; USHORT DDEstartrow,DDEstartcol,DDEendrow,DDEendcol; USHORT rowBlockBeg,colBlockBeg,startBlock,ScreenRows,ScreenCols,blo cksize; USHORT startrow,startcol,endrow,endcol; BYTE highlight_att = HIGHLIGHT,normal_att = NORMAL; PBYTE blockptr, bufptr; HQUEUE pmshdl; BYTE tempbuffer[8192]; USHORT clipboard_data = TRUE; PIDINFO pidinfo; /********************* Function Prototypes ********************/ void main(void); void RequestThread(void); void InitScreen(void); void SetBlock(USHORT row, USHORT col); void ResetScreen(void); void BlankBlock(void); void ResetBlock(void); void ButtonPress(USHORT *eventcode); USHORT prepare_CLPBRDblock(void); void PMS_Init(HQUEUE serverhandle,char *qname); void PMS_Terminate(HQUEUE serverhandle); USHORT readtextblock(PCHAR buffer, USHORT srow, USHORT scol, USHORT erow, USHORT ecol); void main(void) { USHORT pause = 0, msg, capture, valid_block, msgsize, retval, i; USHORT post_on = FALSE,row,col; HQUEUE qhandle; PVOID *msgdata; BUTTON *b; PBYTE p; DosGetPID(&pidinfo); // get process id MsgQCreate(&qhandle,MAINMSGQUEUE); if(retval = MsgQOpen(&pmshdl,PMSERVERQUE)) error_exit(retval,"MsgQOpen - PM Server probably hasn't started..."); DosSemSet(&MouSem); DosSemSet(&KbdSem); // start other threads if(_beginthread(MouThread,MouThreadStack, sizeof(MouThreadStack),NULL) = = -1) error_exit(0,"_beginthread(Mouse)"); if(_beginthread(KbdThread,KbdThreadStack, sizeof(KbdThreadStack),NULL) = = -1) error_exit(0,"_beginthread(Keyboard)"); if(_beginthread(RequestThread,RequestThreadStack, sizeof(RequestThreadStack),NULL) = = -1) error_exit(0,"_beginthread(Request)"); DosSemWait(&MouSem,SEM_INDEFINITE_WAIT); // let threads get going DosSemWait(&KbdSem,SEM_INDEFINITE_WAIT); InitButtons(); InitScreen(); DisplayButtons(); VioGetBuf((PULONG)&bufptr, &msg, VIOHDL); PMS_Init(pmshdl,MAINMSGQUEUE); while(TRUE) { MsgQGet(qhandle, &msgdata, &msgsize, &msg); if(ButtonEvent(msg)) ButtonPress(&msg); switch(msg) { case MSG_ESCAPE: case ESC: MsgQClose(qhandle); // close the message queue // clear the screen VioScrollUp(0,0,-1,-1,-1,(char *)&BlankCell,VIOHDL); PMS_Terminate(pmshdl); DosExit(EXIT_PROCESS,0); // get out of dodge break; case MSG_CLR: // user clearing screen VioScrollUp(0,0,BORDERROW-1,ScreenCols,0xffff, (char *)&BlankCell,VIOHDL); InitScreen(); DisplayButtons(); MsgQSend(pmshdl,&pidinfo.pid,sizeof(PID),PMS_CLPBRD_QUERY); VioSetCurPos(0,0,VIOHDL); break; // ************* Mouse Message handling case MSG_B1DOWN: // clear any existing block and set up a new one valid_block = FALSE; ResetScreen(); capture = TRUE; rowBlockBeg = MOUMSG(msgdata)->row; colBlockBeg = MOUMSG(msgdata)->col; startBlock = (rowBlockBeg * ScreenCols) + colBlockBeg; SetBlock(rowBlockBeg,colBlockBeg); break; case MSG_MOUSEMOVED: if(capture) SetBlock(MOUMSG(msgdata)->row, MOUMSG(msgdata)->col); break; case MSG_B1UP: if(capture) { capture = FALSE; valid_block = TRUE; SetBlock(MOUMSG(msgdata)->row, MOUMSG(msgdata)->col); } break; // ************* Keyboard message handling case MSG_CHAR: VioWrtTTY(&(KBDMSG(msgdata)->chChar),1,VIOHDL); switch(KBDMSG(msgdata)->chChar) { case '\b': VioWrtTTY(" \b",2,VIOHDL); break; case '\r': VioGetCurPos(&row,&col,VIOHDL); if(row = = ScreenRows) VioScrollUp(0,0,BORDERROW-1,ScreenCols,1, (char *)&BlankCell,VIOHDL); else VioWrtTTY("\n",1,VIOHDL); break; } break; case MSG_UPARROW: case MSG_DOWNARROW: case MSG_LEFTARROW: case MSG_RIGHTARROW: case MSG_HOME: case MSG_END: VioGetCurPos(&row,&col,VIOHDL); // get current cursor // position switch(msg) { case MSG_UPARROW: if(row > 0) row--; break; case MSG_DOWNARROW: if(row < ScreenRows) row++; break; case MSG_LEFTARROW: if(col > 0) col--; break; case MSG_RIGHTARROW: if(col < ScreenCols) col++; break; case MSG_HOME: col = 0; break; case MSG_END: col = ScreenCols-1; break; } VioSetCurPos(row,col,VIOHDL); break; // *******************Clipboard handling***************** case MSG_COPY: // user copy to clipboard case MSG_CUT: // user cut to clipboard if(!valid_block) // if no block selected { beep(); // warn user break; // forget it } i = prepare_CLPBRDblock(); // pass data to server MsgQSend(pmshdl,tempbuffer,i,PMS_COPY); if(msg == MSG_CUT) // reset screen BlankBlock(); else ResetBlock(); valid_block = FALSE; break; case MSG_PASTE: // user wants to paste MsgQSend(pmshdl,&pidinfo.pid,sizeof(PID),PMS_PASTE); break; // **************** PMServer message handling *********** case PMS_CLPBRD: // clpbrd data is available clipboard_data = TRUE; findbutton(" Paste ", &b); ButtonPaint(b,BUTTON_ON); break; case PMS_CLPBRD_EMPTY: // clpbrd is empty clipboard_data = FALSE; findbutton(" Paste ", &b); ButtonPaint(b,BUTTON_OFF); break; case PMS_CLPBRD_DATA: // clipboard data rec'd VioGetCurPos(&row,&col,VIOHDL); // get current cursor // position for(i = 0, p = (PBYTE)msgdata; *p; p++) if(*p == 13) // if a linefeed i++; // # of lines in text if(row+i > ScreenRows) { int j = (row+i)-ScreenRows; VioScrollUp(0,0,BORDERROW-1,ScreenCols,j, (char *)&BlankCell,VIOHDL); row -= j; VioSetCurPos(row,col,VIOHDL); } VioWrtTTY((PCH)msgdata,msgsize-1,VIOHDL); MsgQSend(pmshdl,&pidinfo.pid,sizeof(PID), PMS_CLPBRD_QUERY); break; case PMS_NO_INIT: // server can't initialize error_exit(msg,"PMS_Init - unable to access PM Server"); break; case PMS_INIT_ACK: // server initialized clipboard_data = FALSE; // then fall thru default: break; } if(msgdata != NULL) DosFreeSeg(SELECTOROF(msgdata)); } } // terminates PMServer connection void PMS_Terminate(HQUEUE serverhandle) { // terminate connection MsgQSend(serverhandle,&pidinfo.pid,sizeof(PID),PMS_TERMINATE); MsgQClose(serverhandle); // close queue } // initializes PMServer connection void PMS_Init(HQUEUE serverhandle,char *qname) { CLIENTDATA clientdata; strcpy(clientdata.qname,qname); // set up qname clientdata.pid = pidinfo.pid; // set up PID // initialize connection MsgQSend(serverhandle,&clientdata,sizeof(clientdata),PMS _INIT); } // prepare data for clipboard USHORT prepare_CLPBRDblock(void) { return readtextblock(tempbuffer,startrow,startcol,endrow,endcol); } // read text from screen USHORT readtextblock(PCHAR buffer,USHORT srow, USHORT scol, USHORT erow, USHORT ecol) { USHORT i,row,len,col; for( row = srow, col = scol, i = 0; row <= erow; row++) { len = ScreenCols; if(row != srow) col = 0; if(row = = erow) len = ecol+1; VioReadCharStr(&buffer[i],&len,row,col,VIOHDL); i += len-1; while(buffer[i] = = ' ') i--; buffer[++i] = '\r'; buffer[++i] = '\n'; i++; } buffer[i] = '\0'; return i+1; // return length of block } // set screen text block void SetBlock(USHORT row, USHORT col) { USHORT endBlock = (row * ScreenCols) + col; USHORT presize,postsize,start; if(startBlock <= endBlock) // block end at/after start { // normal from top to startBlock-1, // highlight from startBlock to endBlock // normal from endBlock+1 to bottom presize = startBlock; startrow = rowBlockBeg; startcol = colBlockBeg; blocksize = endBlock-startBlock+1; endBlock++; endrow = (endBlock/ScreenCols); endcol = (endBlock%ScreenCols); postsize = (ScreenRows*ScreenCols)-endBlock+1; } else // block end before start { // normal from top to endBlock-1 // highlight from endBlock to startBlock // normal from startBlock+1 to bottom presize = endBlock; startrow = row; startcol = col; blocksize = startBlock-endBlock+1; start = startBlock+1; endrow = (start/ScreenCols); endcol = (start%ScreenCols); postsize = (ScreenRows*ScreenCols)-start+1; } blockptr = bufptr+presize; // set to beginning of block // now write pre-block and post-block normal; highlight block VioWrtNAttr(&normal_att,presize,0,0,VIOHDL); VioWrtNAttr(&highlight_att,blocksize,startrow,startcol,V IOHDL); VioWrtNAttr(&normal_att,postsize,endrow,endcol,VIOHDL); } // reset screen work area void ResetScreen(void) { USHORT i; for( i = STARTROW; i <= ENDROW; i++) VioWrtNAttr(&normal_att,BORDERCOL,STARTROW,0,VIOHDL); } // blank screen text block void BlankBlock(void) { VioWrtNCell((PBYTE)&BlankCell,blocksize, startrow,startcol,VIOHDL); } // reset screen to normal void ResetBlock(void) { VioWrtNAttr(&normal_att,(ScreenCols*ScreenRows),0,0,VIOHDL); } // initialize screen void InitScreen(void) { SHORT i; VIOMODEINFO modeinfo; VioGetMode(&modeinfo,VIOHDL); / get screen mode, rows, cols ScreenCols = modeinfo.col; ScreenRows = modeinfo.row; if(ScreenRows > BORDERROW-1) ScreenRows = BORDERROW-1; VioScrollUp(0,0,-1,-1,-1,(char *)&BlankCell,VIOHDL); // clear screen VioSetCurPos(0,0,VIOHDL); // set cursor VioWrtNChar("-",ScreenCols,BORDERROW,0,VIOHDL); // draw border line for( i = 0; i < SCREEN_LABELS; i++) // write labels VioWrtCharStr(screen_labels[i].text, strlen(screen_labels[i].text), screen_labels[i].row,screen_labels[i].column,VIOHDL); } // manipulate buttons for specific event code and button type void ButtonPress(USHORT *eventcode) { BUTTON *b = &buttonlist[0]; MOUSECODEOFF(*eventcode); // turn off mouse bit for( ; b->text; b++) // find the button if((b->left_button_val = = *eventcode) || (b->right_button_val = = *eventcode) || (b->accelerator = = *eventcode)) { switch(b->type) { case BPRESS: // if a press button ButtonPaint(b,BUTTON_ON); // turn it on DosSleep(100L); // wait ButtonPaint(b,BUTTON_OFF); // turn it off break; case BTOGGLE: // if a toggle button b->state = !b->state; // toggle it // and toggle the color ButtonPaint(b,(b->state ? (BYTE)BUTTON_ON : b->attribute)); break; default: break; } return; } } /******* end of main thread code **************************/ /******* start of request thread code *********************/ // periodically wakes up and queries PMServer about Clipboard void RequestThread(void) { USHORT count = 0; while(TRUE) { if(!clipboard_data) // if flag invalidated // ask if pasting possible MsgQSend(pmshdl,&pidinfo.pid,sizeof(PID),PMS_CLPBRD_QUERY); DosSleep(REQUESTTHREADSLEEPTIME); // sleep a while count++; if(count >= MAXREQUESTCOUNT) // if count exceeded limit { clipboard_data = FALSE; // invalidate flag count = 0; // reset count } } } /******* end of request thread code ***********************/ Figure 8 BUTTON.H /* button.h macros and typedefs for button.c */ #define BTOGGLE 0x0001 #define BPRESS 0x0002 #define INPUT 0X0003 typedef struct _button { char *text; // text to be displayed char *title; // button title (optional) USHORT startrow; // upper row USHORT startcol; // left column USHORT endrow; // lower row USHORT endcol; // right column BYTE attribute; // color attribute USHORT type; // type of object USHORT left_button_val; // mouse left button event USHORT right_button_val; // mouse right button event USHORT accelerator; // keyboard event USHORT state; // button state: on or off } BUTTON; void InitButtons(void); void ButtonInit(BUTTON *b); void ResetButtons(void); void findbutton(char *text,BUTTON **bptr); void DisplayButtons(void); void ButtonPaint(BUTTON *b, BYTE attribute); void ButtonDisplay(BUTTON *b); BUTTON.C /* button.c text-button functions */ #define INCL_SUB #include<os2.h> #include<string.h> #include"button.h" #define VIOHDL 0 extern BUTTON buttonlist[]; void InitButtons(void) { BUTTON *b = buttonlist; for( ; b->text; b++) ButtonInit(b); } void ButtonInit(BUTTON *b) { b->endrow = (b->startrow+2); // startrow+#of ptrs-1 // startcol+strlen of text-1 b->endcol = (b->startcol+strlen(b->text)+1); } void ResetButtons(void) { BUTTON *b = buttonlist; for( ; b->text; b++) { b->state = 0; ButtonPaint(b,b->attribute); } } void findbutton(char *text,BUTTON **bptr) { BUTTON *b = buttonlist; for( ; b->text; b++) if(!strncmp(b->text,text,strlen(text))) { *bptr = b; return; } } void DisplayButtons(void) { BUTTON *b = buttonlist; for( ; b->text; b++) ButtonDisplay(b); } void ButtonDisplay(BUTTON *b) { BYTE cell[2]; USHORT row = b->startrow; USHORT endcol = b->endcol; USHORT startcol = b->startcol; char *text = b->title; USHORT len = endcol - startcol - 1; cell[0] = '='; cell[1] = b->attribute; // write the 1st corner char VioWrtCharStrAtt("",1,row,startcol,&cell[1],VIOHDL); // write the top line VioWrtNCell(cell,len,row,startcol+1,VIOHDL); // write the 2nd corner char VioWrtCharStrAtt("",1,row,endcol,&cell[1],VIOHDL); if(*text) // if title, write it VioWrtCharStrAtt(text,strlen(text),row,startcol+2, &cell[1],VIOHDL); // write the left border VioWrtCharStrAtt("",1,++row,startcol,&cell[1],VIOHDL); text = b->text; // reset pointer // write the message VioWrtCharStrAtt(text,strlen(text),row, startcol+1,&cell[ 1],VIOHDL); // write the right border VioWrtCharStrAtt("",1,row,endcol,&cell[1],VIOHDL); // write the 3rd corner VioWrtCharStrAtt("",1,++row,startcol,&cell[1],VIOHDL); // write the bottom line VioWrtNCell(cell,len,row,startcol+1,VIOHDL); // write the 4th corner VioWrtCharStrAtt("",1,row,endcol,&cell[1],VIOHDL); } void ButtonPaint(BUTTON *b, BYTE attribute) { USHORT row = b->startrow; USHORT col = b->startcol; USHORT endrow = b->endrow; USHORT num = b->endcol-col+1; for( ; row <= endrow; row++) VioWrtNAttr(&attribute,num,row,col,VIOHDL); } Figure 9 KBD.H void KbdThread(void); KBD.C Source Code /* keyboard thread code for PMAccess */ #define INCL_SUB #define INCL_DOS #include<os2.h> #include"kbd.h" #include"kbddefs.h" #include"msgq.h" #include"button.h" #include"msgs.h" extern long KbdSem; extern PCHAR mainmsgqueue; extern BUTTON buttonlist[]; USHORT AcceleratorPressed(unsigned char key); void KbdThread(void) { KBDINFO kbdinfo; KBDKEYINFO KbdKeyInfo; HKBD KbdHandle = 0; HQUEUE qhandle; USHORT event; MsgQOpen(&qhandle,mainmsgqueue); KbdFlushBuffer(KbdHandle); // flush keyboard buffer KbdGetStatus(&kbdinfo,KbdHandle); // get keyboard status kbdinfo.fsMask &= ~COOKED; // turn off COOKED bit kbdinfo.fsMask |= RAW; // turn on RAW bit KbdSetStatus(&kbdinfo,KbdHandle); // set the keyboard status DosSemClear(&KbdSem); // notify main thread while(TRUE) { KbdCharIn(&KbdKeyInfo,IO_WAIT,KbdHandle); // get a character if(KbdKeyInfo.chChar) // if Ascii code MsgQSend(qhandle,&KbdKeyInfo,sizeof(KbdKeyInfo),MSG_CHAR); else if(event = AcceleratorPressed(KbdKeyInfo.chScan)) // Accelerator? MsgQSend(qhandle,NULL,0,event); // if so, pass it on else MsgQSend(qhandle,&KbdKeyInfo,sizeof(KbdKeyInfo), SCANCODE(KbdKeyInfo.chScan)); DosSleep(32L); } } USHORT AcceleratorPressed(unsigned char key) { BUTTON *b = &buttonlist[0]; for( ; b->text; b++) if(key = = (unsigned char)b->accelerator) return MOUSECODE(b->left_button_val); return 0; } Figure 10 MOU.H void MouThread(void); MOU.C /* mou.c mouse thread code for PMAccess */ #define INCL_DOS #define INCL_SUB #include<os2.h> #include"mou.h" #include"moudefs.h" #include"msgq.h" #include"button.h" #include"msgs.h" #include"errexit.h" extern long MouSem; extern PCHAR mainmsgqueue; extern BUTTON buttonlist[]; extern USHORT dirrow; typedef struct _moubuttons { USHORT mask; USHORT movedmask; USHORT down; USHORT upmsg; USHORT downmsg; } MOUBUTTONS; MOUBUTTONS buttons[3] = { { MOU_B1, MOUSE_MOTION_WITH_BN1_DOWN, FALSE, MSG_B1UP, MSG_B1DOWN }, { MOU_B2, MOUSE_MOTION_WITH_BN2_DOWN, FALSE, MSG_B2UP, MSG_B2DOWN }, { MOU_B3, MOUSE_MOTION_WITH_BN3_DOWN, FALSE, MSG_B3UP, MSG_B3DOWN } }; USHORT ButtonPressed(MOUEVENTINFO *ev); void MouThread(void) { MOUEVENTINFO MouEvent; USHORT WaitOption = MOU_WAIT; // set to block on input HQUEUE qhandle; USHORT buttondown = FALSE, numbuttons,i; USHORT retval, mouse_moved,event; HMOU MouHandle; MsgQOpen(&qhandle,mainmsgqueue); if((retval = MouOpen((PSZ)NULL,(PHMOU)&MouHandle))) error_exit(retval,"MouOpen"); MouDrawPtr(MouHandle); // display mouse pointer MouFlushQue(MouHandle); // flush mouse queue MouGetNumButtons(&numbuttons,MouHandle); // get button count DosSemClear(&MouSem); // notify main thread while(TRUE) { // read the queue MouReadEventQue(&MouEvent,&WaitOption,MouHandle); MouEventDropLowBit(MouEvent); // turn off the low bit // notify if screen button pressed if(!buttondown && IsMouButtonPressed(MouEvent)) // if mouse button pressed if(event = ButtonPressed(&MouEvent)) // while on screen button { MsgQSend(qhandle,NULL,0,event); buttondown = TRUE; DosSleep(32L); continue; } buttondown = FALSE; if(MouEvent.row >= dirrow) // non-PM-like line: continue; // protecting mouse buttons for( i = 0, mouse_moved = FALSE; i < numbuttons; i++) // if the button is down now if(MouButtonPressed(MouEvent,buttons[i].mask)) { if(!buttons[i].down) // if button was previously up { MsgQSend(qhandle,&MouEvent,sizeof(MouEvent), buttons[i].downmsg); buttons[i].down = TRUE; } // if the button was previously down but the mouse moved else if(MouButtonPressed(MouEvent,buttons[i].movedmask)) mouse_moved = TRUE; } else // if button is not down now { if(buttons[i].down) // if button previously down { buttons[i].down = FALSE; } } // notify of all mouse movement if button is down (PM gets all // movement) if(mouse_moved) MsgQSend(qhandle,&MouEvent,sizeof(MouEvent),MSG_MOUSEMOVED); DosSleep(32L); } MouClose(MouHandle); } USHORT ButtonPressed(MOUEVENTINFO *ev) { register USHORT row = ev->row, col = ev->col; BUTTON *b = &buttonlist[0]; for( ; b->text; b++) { if((row >= b->startrow) && (row <= b->endrow) // if on button && (col >= b->startcol) && (col <= b->endcol)) if(MouB1Pressed(ev->fs)) return MOUSECODE(b->left_button_val); else if(MouB2Pressed(ev->fs)) return MOUSECODE(b->right_button_val); else return 0; } return 0; } A Presentation Manager Primer In the Presentation Manager programming environment, the program architecture is largely the reverse of that found under DOS and most OS/2 kernel programs. DOS programs are essentially active: a program must actively seek out input, and format its own screen output. If you are programming for DOS, you must design the program so that it can efficiently obtain and process input and manage output. This structure places most of the overhead of managing I/O on the program, so that you have to write code that juggles the program's attention between different I/O modules, in an effort to see that an incoming event is not lost or a user kept waiting too long. The same is largely the case with OS/2 kernel programming, with the exception that you can use multiple threads to manage diverse sources of input. Under PM, a program is mostly reactive. Instead of actively seeking input, a program remains in a fundamentally idle state. When PM receives input such as a keypress or a mouse click, it routes the input to the appropriate application in the form of messages. The application, after receiving a message, makes the appropriate response to that event and returns to its idle state. If the application needs to display text or graphics in a window, it calls PM to do so. Thus, PM applications are said to be message-based and event-driven. Objects and Windows PM's design was greatly influenced by the success of other object-oriented programming environments. A PM window is an object, the building-block upon which PM applications are built. A window is that part of a PM application that actually receives, processes, and responds to messages sent to it by PM or other windows. A PM window can have its own set of routines, and can have data associated with it like a mini-program. The messages that a window receives determine the operations that it performs on its data. A window can interact with users, but only through PM. If a user clicks on a control (such as a scroll bar) that belongs to a window, PM sends the window a message telling it what happened. The window responds to the user by calling PM functions, which is the equivalent of sending a message back to PM. PM offers many built-in objects. Some are visible, such as controls (scroll-bars, list boxes, mouse pointers, and so on); while others, such as anchor blocks, bitmaps, message queues, and metafiles, may not be. You can also create your own customized windows to complete an application. The PM programmer's primary task is to define the appropriate windows, their attributes, and their responses to specific messages. PM windows can be classed and subclassed. You can create more than one of a particular type or class of window, in which each inherits the characteristics of the original window but addresses a different aspect of an application's needs (a subclass). Each window is unique whatever its class; it is identified by a 32-bit value or window handle. "Window" does not necessarily refer to one of the overlapping frames of information that appear on the PM screen. Although a window may create a screen window to display its output or to interact with the user, the creation of a window does not imply that its actions are visible onscreen. An application composed of one or more windows may have none that are visible. PMServer, for example, creates a window that does not produce any visible screen activity other than PMServer's entry in the Task Manager window, and even that can be eliminated. PM Messages PM messages tell a PM object what to do. You rarely get to see how an object works unless you write it yourself. Each object is a black box--you must look at the documentation to find out what messages it responds to and the type of response. Each message contains arguments that give the object more information to act on the message. The messages are stored in structures of type QMSG, which you can find in PMWIN.H together with the predefined PM messages and macros for accessing and interpreting them. PM can generate about 100 general-purpose messages and about 150 special-purpose messages, which service controls such as dialog box windows and scroll bars. You can also create messages to use in your own applications to communicate between windows. Messages always contain an identifying value; they may also contain the handle of the window to which the message is addressed. The message can include two message parameters, each a 32-bit value that can contain combinations of 16- and 8-bit values. Macros are supplied in PMWIN.H for extracting or setting these values. The time the message originated and the x and y coordinates of the mouse pointer at that moment are also included in the message. Message Queues PM has two types of message queues--the system message queue and the application message queue. When an input event occurs, PM converts the information about the event into a message and places the message in its system message queue. This message queue has the capacity for holding information about 60 keystrokes or mouse clicks. A PM component, called the input router, routes each message to the application that had input focus at the time the message was generated and places the message in that application's message queue. Other types of messages, such as window create and destroy messages and Clipboard messages, are placed directly into the message queue of the target application; they don't need to go through the system message queue. Every PM application has a message queue, an attribute that fundamentally distinguishes it from an OS/2 kernel program. Once an application creates a PM message queue, subsequent input must come to the application through PM. Therefore, the Kbd, Mou, and Vio subsystems are no longer available to the application--with the minor exception of Advanced Vio (AVio) calls, which are managed by PM. After creating a PM message queue, every program running in the application's screen group will have to use PM for subsequent I/O--a consequence that has profound implications for kernel applications. For instance, if you run (via DosExecPgm) a PM program from an OS/2 kernel application that is running in a screen group other than PM's, the kernel application will not be able to interface with the Kbd, Vio, or Mou subsystems until the PM application destroys its message queue just before termination. The same is also true for every other program in the application's screen group. As mentioned earlier, windows wait for messages to be sent to them by PM. When a window receives a message, the message is processed by a window procedure, a function that is registered with PM right after an application creates its message queue. Every window has a window procedure associated with it that is written to expect the same arguments (comprising several of a message's components); PM calls the function with the appropriate arguments for each message received by the application. The application itself doesn't call the window procedure directly. Instead, PM calls the window procedure when the window receives a message. PM's built-in objects have their own window procedures, but programmers can customize windows by supplying a window procedure, thus controlling what the window actually does. The backbone of a window procedure is a C switch statement, with a case for every message specifically handled by the window. This block of code is called the message handler. There's no need to have a case in the message handler for every possible message. One for each message you specifically want the window to process is sufficient. The unprocessed messages are passed on to a default window procedure provided by PM. Each successfully processed message should result in a return value of FALSE; unsuccessful ones should return TRUE. Message processing should also be efficient. I recommend placing any code that requires more than 1/10th of a second to execute in a separate thread. There are two ways to pass messages from one window to another. A window can send a message to another window directly by calling WinSendMsg. In this case, the sender must wait until the target window has processed the message (that is, while PM calls the target's window procedure to process the message and return). Alternatively, a window can post a message to the other window, which places the message in the target window's messge queue and returns immediately. Using the second method, the window sourcing the message does not wait while the target window processes it. PMServer's second thread uses this method; it calls WinPostMsg to post messages to PMServer's window. PMServer's PM Code Setting up and initializing a PM application with a message queue and a window requires only 10-20 lines of code. The remainder of the application is largely contained in the window procedure. This setup code can be kept as a template and pasted into a source file when you're writing a new program. As you can see, PM programming focuses on choosing which controls and attributes an application's windows should have, what messages they should respond to, and what the responses will be. Look at PMServer's window startup code. Note that virtually every identifier, macro, or window function discussed here can be found in PMWIN.H. You might also note that because PMServer does not have any visible windows, many of the parameters to these functions are either 0 or NULL. PMServer's PM-specific code begins in the main function with the call to WinInitialize. A thread should call this function before it calls any other PM functions. The function returns an anchor block handle that is used in many of the subsequent PM calls. This handle refers specifically to this application. Next, PMServer calls WinCreateMsgQueue, which creates the application message queue and returns a handle to it. This function's second parameter specifies the maximum queue size or 0 to accept the default queue size (currently 10). The next call is to WinRegisterClass. You must register every window's class with PM before creating one or more instances of the window. Here, this function creates a class of windows called "PMServer," designated by the string passed as the second parameter. The third parameter is the address of the window procedure, ClientWndProc. PMServer does not use the remaining parameters. The fourth parameter specifies the default window style for any instance of this window class, and the last parameter specifies the number of bytes of reserved storage that can be allocated for each window in the class. At this point, PMServer has initialized itself and created a message queue, but it hasn't yet created a window. The call to WinCreateStdWindow actually creates a window and returns a handle to it. This function creates a window that is a child of the PM desktop (the ancestor of all PM windows); thus, the HWND_DESKTOP identifier represents the handle of the desktop window. The second parameter specifies the frame window styles used to create the window. PMServer does not use any of these, so 0L is passed. However, PMServer does use one of the options that control how the frame window is created; the next parameter, FCF_TASKLIST, adds the window to the Task Manager's switch list. The window class name in the next parameter tells WinCreateStdWindow that the window will be an instance of the previously registered PMServer class. The window class name is followed by the window's title bar text, which is NULL because PMServer has no title bar. WinCreateStdWindow has four additional parameters, but PMServer uses only one of them. The parameter that follows the title bar text specifies the styles for client windows. Although none of these are used, the omission of WS_VISIBLE ensures that PMServer will not be visible. The next two parameters are a handle to a resource module (that contains definitions of other PM objects such as menus) if one is provided, followed by the frame window identifier in the resource file. The last parameter is the variable that receives the handle of the newly created client window. At this point PMServer is ready to receive and process messages. The while loop containing WinGetMsg and WinDispatchMsg will retrieve a message from PMServer's message queue and dispatch it to the newly created window. The program will remain in this loop as long as PM does not place a WM_QUIT message in the window's message queue. The WinGetMsg function requires the anchor block handle, the address of a QMSG structure in which to place the newly retrieved message, and three additional parameters (not used by PMServer) that allow you to filter the messages as they are received. The WinDispatchMsg function, on the other hand, calls PM to dispatch the message to the window procedure associated with the message queue. WinDispatchMsg will return when the window procedure has finished processing the message. Once the program breaks out of the loop, only a few steps remain before the program is terminated. WinDestroyWindow obviously destroys the window whose handle is passed to it, as well as any child windows that belong to it. WinDestroyMsgQueue terminates the message queue, and WinTerminate will terminate the thread's use of PM. As you can see, PMServer's PM setup and initialization code is relatively short: the bulk of the program resides in the window procedure. The version of PMServer in a future article will include DDE support to the window procedure, with few changes to the PM code in the main function. Simplifying Complex Windows Development Through the Use of a Client-Server Architecture Scott Kerber The Microsoft Windows program is a fertile environment in which to develop applications that can communicate with one another in a prescribed manner. Windows1 supports numerous methods to encapsulate software functions including libraries, dynamic-link libraries (DLLs), and client-server applications. The communication mechanisms available in Windows, such as message passing, user-definable message types, global data sharing, and multitasking, make the implementation of client-server architectures particularly convenient. This article explores the Windows facilities for developing server-based applications as implemented in a program called WinTrieve. A server is any application offering a service that can be reached from any other application running on the system. Servers accept requests, perform their service, and return the results to the application that requested the service. A client is an application that sends a request to a server and waits for a response. Often communication between client and server is synchronous; in other words, the client must wait for the results of a request before continuing to process. Typically, the client-server relationship is many-to-one: several clients are making concurrent requests to a single server. The benefits of the client-server model include reduced client program size, the ability to take advantage of Windows multitasking, and the ability to move the server more easily into a networked environment. These benefits provide a basis for the design and implementation of server-specific protocols. The ideas here are for servers that either do not require the complexity or whose model of operation is inappropriate for utilizing an asynchronous protocol such as the Dynamic Data Exchange (DDE). Although both synchronous and asynchronous communications are supported in Windows, this article concerns synchronous communications between client and server. Examples (illustrations and group samples) of these concepts use WinTrieve, which was developed by The Whitewater Group. WinTrieve consists of the WinTrieve server, a C language Application Programming Interface (API), and a set of Actor language classes for accessing the WinTrieve server. (Actor is an object-oriented language and development environment for Windows developed by The Whitewater Group.) The WinTrieve server is an Indexed Sequential Access Method (ISAM) file manager that runs as a separate application under Windows. It supports multiple concurrent access to ISAM files, including file and record level locking and journaling. WinTrieve is used to illustrate techniques for writing server applications and for designing protocols under Windows. Design Goals of WinTrieve A major design goal of WinTrieve was to minimize the amount of memory required from the client application's address space. Many hardware configurations support Windows' ability to remove applications temporarily from memory and thus map more applications into the limited 1Mb space available. If many applications are running concurrently, Windows must continually swap program segments, which often results in severe performance degradation. (For an explanation of Windows memory management, see "EMS Support Improves Microsoft Windows 2.0 Application Performance," MSJ, Vol. 3, No. 1.) Minimizing the required memory is a general concern for anyone developing large applications targeted for a user environment that must support several simultaneously running applications. Another design goal was to make the low-level interface language-independent. WinTrieve is targeted for C and Actor language developers. C is supported through an Application Program Library (APL) and Actor through a set of classes known as a class library. Under tight memory constraints WinTrieve had to be carefully segmented to allow competing applications as much memory as possible. Other long-term goals for WinTrieve include support for networks (it could possibly be used as a centralized network server), record change notification, and record lock and unlock notification. These design goals were best met by the server approach, which minimizes client address space requirements, supports language-independent interfaces, has minimal protocol overhead, and takes maximum advantage of Windows memory management. The Client-Server Model Most people familiar with the client-server model equate it with networked environments, but the client-server model can be used in any environment that supports interprocess communications, such as Windows. Generally, the lifespans of servers and clients are different. A server starts execution before client-server interaction begins and continues to accept requests and send responses without ever terminating. A client, on the other hand, usually terminates after making a finite number of requests to the server. To make requests, a client must connect to the server. A connection identifies a communications path for both client and server. Figure 1 shows several client applications accessing a single server. To connect, the client must be able to identify the server of interest. Methods for doing this under Windows are discussed later. The life of a connection is known as a session, which consists of all requests and responses from the time that a connection is accepted by the server to the time that the client terminates the connection. A session is governed by an agreed-upon method of communications known as a protocol, which defines the communication conventions used by client and server. In many cases, servers must be more complex than outlined here. A server must be able to protect itself against malformed requests or against requests that will abort the server. In preemptive environments, a server may also have to handle multiple concurrent requests. In nonpreemptive Windows, a server tends to behave more like a DLL with a single entry point. Alternate Approaches The most common alternate methods to the client-server approach of providing shared services under Windows are object libraries, terminate-and-stay-resident (TSR) programs, and DLLs. Object libraries have been available on most computing systems for some time. In DOS2, object libraries are created with the LIB utility, and filenames have the LIB extension. When a program is linked, all program object modules and object code in libraries that are referenced by the program are combined into one executable file. Object library code is physically copied into the program's executable file when it is linked. This creates much redundancy of code. Each running program has its own copy of common library modules. This wastes memory and reduces the address space available to the application code. Using object libraries involves other considerations. For example, WinTrieve maintains tables that contain locking information about open files and records. Using object libraries requires that each executable file has access to a central lock table. The only true way to share memory among applications in Windows is to allocate memory from the nonbanked area of the global heap. Unfortunately, nonbanked memory is quite limited and used for many purposes by Windows, so allocating large chunks of unbanked memory is unwise. Using the server approach allows the lock table to be allocated within the server's own address space. Access to the lock table is limited to the server itself. Terminate-and-stay-resident (TSR) programs are similar to the server approach. A TSR can be written in the form of a library. Only one copy executes at a time, eliminating the code redundancy problem inherent in linkable libraries. An application communicates with a TSR through a software interrupt. This type of TSR is known as a passive TSR because it responds only when explicitly invoked by another program. An active TSR is generally invoked by a hardware interrupt or keystroke. A TSR is started from DOS before Windows is run, which causes a problem. DOS loads the TSR into low memory (see Figure 2); Windows is loaded on top of the TSR, starting from the lowest available memory. Depending on the size of the TSR, the amount of memory available for Windows to run other applications could be significantly reduced. For example, WinTrieve requires approximately 120Kb total memory to run. If it were a TSR, 120Kb less memory would be available for Windows. Windows has no options with a TSR, because it can neither place the TSR in expanded memory (EMS), nor move or swap segments to take advantage of available memory. Dynamic-link libraries are the third alternative and also the most viable under Windows. Windows itself consists of several DLLs. DLLs, put simply, are shared libraries. A DLL that has been referenced by an application is linked (external references to the DLL are resolved) to that application at run time. This process is known as dynamic linking. DLLs have many excellent features. For example, only one instance of a DLL need reside in memory for all of the applications that reference it. This solves the code redundancy problem inherent in object libraries. DLLs can be segmented and code segments can be discarded when Windows needs memory for other purposes. A DLL can have its own single data segment that is used for global and static variable allocation and the local heap. For example, if WinTrieve were a DLL, it could allocate memory for the lock table from its local heap and then reference it with impunity. A DLL uses the stack of the calling program. Ultimately a DLL that is referenced by a program must be mapped into the program's address space. Therefore, the DLL may limit the overall size of the application. Figure 3 shows a sample memory map of an application and WinTrieve if written as a DLL. Also, a DLL's data segment is allocated from nonbanked memory, which is a limited resource; an application's data segment is allocated from the banked portion of Windows memory. Banked memory is often a more plentiful resource than nonbanked memory. In addition to supporting bank-switching memory, Windows supports discardable segments. If segments can be discarded, an application's total size can be much larger than available memory. Windows decides which segments to discard based on a least recently used (LRU) algorithm. The application's developer can determine how Windows manages segments. An application's module definition file (DEF) specifies program segment attributes, such as whether a segment is discardable, movable, fixed, preloaded, or loaded on call. Code segments are usually marked discardable. Why then should the size of a DLL cause any concern? As an application runs, it normally settles on a core of data and code that it uses most often. This core is known as an application's working set. Occasionally the application will go outside its working set, which might require code segments to be discarded and others loaded into memory. If this happens seldom or incrementally, the user should not notice. But if memory is insufficient to contain an application's working set and Windows must continually discard code segments to load in others, the result will be severe performance degradation, a phenomenon known as thrashing. For example, Actor requires a small code space but a very large data space. Windows has few options in this case. Either the application won't run because it and the minimal space required for the DLL exceed available memory bounds, or performance becomes intolerable because segment swapping occurs at an inordinate rate. Empirical studies have shown that WinTrieve has a working set size of approximately 100Kb, including its data segment and several code segments. If only a smaller amount of memory is available, Windows will have to continually swap commonly used code segments. For example, a routine operation in an ISAM application is to read sequentially all records in a file and calculate a total value from a field in each record. If for each read operation performed Windows must discard and load several segments, performance will be greatly reduced. Using the server approach, Windows can place WinTrieve in a separate memory space and bank it into 1Mb physical address space when a client sends a request to it. Figure 4 shows a sample memory map of this situation. Now compare Figure 4 and Figure 3. The overhead of doing the context switch and banking WinTrieve into memory becomes much less than the overhead of thrashing segments. Also, the possibility that a large application will not be able to run at all is lessened because only a small amount of interface code need reside with the client. Designing a Server Under Windows Figure 1 depicts a typical client-server scenario. Three client applications labeled App 1, App 2, and App 3 are currently connected to the server. The arrows between the applications and the server represent a connection, a two-way communications path. Each end of the connection is identified by a unique address. As will be seen later, a window handle is used as an address. As stated earlier, this discussion is limited to synchronous communications between client and server. Synchronous communications are characterized by the client blocking on a request while waiting for a response from the server. After receiving the response, the client is unblocked and continues. Synchronous communications are common. Often a client cannot continue in its thread of execution until it receives data from the server. This is similar to making a function call, except that control is passed from the client to the server. Synchronous communications greatly simplifies the protocol between client and server. Windows fully supports the client-server model (see Figure 5). Windows memory management allows programs a fairly large virtual address space (discardable segments) and permits several large applications to run simultaneously (bank switching). Specialized protocols can be defined through Windows support of user-definable message types. Request and response data are passed between client and server via memory blocks allocated from the Windows global heap. Messages under Windows are usually sent to other windows. A receiving window may reside anywhere in the system and is identified by its window handle (hWnd). An hWnd is analogous to an address as described above. Messages are sent with the SendMessage function or with the PostMessage function. Figure 6 illustrates the differences between the two. A call to SendMessage results in the receiver window procedure (WndProc) being called directly. Control is transferred to the receiver WndProc, where the message is processed. Upon return of the message, control is transferred back to the point in the sender immediately after the SendMessage call. SendMessage is akin to a function call except that the called function may reside in another application. SendMessage is most useful for providing synchronous communications between windows. PostMessage, on the other hand, does not call the receiver WndProc directly. Instead, the call to PostMessage results in the message being put into the receiver window's message queue. The sending application never gives up control to another and continues executing after the PostMessage call. The receiver window will obtain the message through normal processing of its get-translate-dispatch message loop. PostMessage provides a mechanism for asynchronous communications between windows. Normally when a client sends a request to a server, it must first put its request into a packet. A packet is usually a buffer that the client allocates. The client fills the buffer with the data required to make the request. The data must be in a format previously agreed upon between client and server. When ready, the client executes a network send primitive (typically a function call) to send the request packet to the server. When the server receives the packet, it is typically copied into the server address space by the network receive message primitive. Windows does not support this type of message passing directly, but it can be simulated. A message in Windows consists of three parts: a message value, a word parameter, and a long parameter. A packet can be simulated by specifying a global memory handle in either the word parameter or the long parameter when the message is sent. Applications can share global memory in two ways. Allocation rules must be followed if sharing of global memory is to work with all memory configurations that Windows supports. Global memory is allocated by using the Windows GlobalAlloc function. In the first method, global memory is allocated from the nonbanked area of Windows global heap. Nonbanked global memory is allocated by specifying the GMEM_NON_BANKED flag in the GlobalAlloc function call. Any application can read from and write into these memory blocks. Because they are in nonbanked memory, memory blocks of this type are always mapped into every application's address space. The number and size of nonbanked memory blocks should be kept small; nonbanked memory is limited in size, and Windows uses it for several other purposes. By using nonbanked memory, applications can communicate with the lowest overhead. Both client and server can allocate and use a single block of nonbanked memory for request and response data. WinTrieve uses nonbanked memory to communicate request and response data with client applications. A sample WinTrieve client-server memory map is shown in Figure 7. The second way that global memory can be allocated is with the GMEM_SHARE flag, which is the same as the GMEM_DDESHARE flag. Memory blocks of this type can be safely passed among running applications. Use of GMEM_SHARE memory blocks is restricted. Applications that allocate this type of memory are free to read from it and write to it. Other applications must treat it as a read-only memory block. In large frame EMS configurations, Windows allocates the global memory from the application's own EMS address space. When another application locks the memory block, Windows detects that it was allocated with GMEM_SHARE and that the application attempting to lock the memory block is not its allocator. In this case, Windows copies the memory block from the owner's address space into the locking application's address space. Figure 8 shows a sample memory map (large frame EMS) for the client-server that communicates using GMEM_SHARE global memory blocks. The figure shows the server mapped into memory to service a client request. The request data packet, allocated by the client, contains the appropriate request data. The global memory handle of the request data packet was passed by the client to the server when the request message was sent. To send the request message, the client calls the Windows function SendMessage. When the server calls GlobalLock to lock the request data packet, Windows notes that the request data packet was allocated with the GMEM_SHARE flag and copies it into the server's address space. The server can now read the request data packet. When the server calls the GlobalUnlock function to unlock the data packet, the copy is freed from the server's address space. To return data to the client, the server could allocate a response data packet using the GMEM_SHARE flag. It could then return the handle of the response data packet to the client in either the high or low word of the long value returned by SendMessage. The unused word of the return value could be used to store a protocol return code. After receiving the response data packet, the client would copy it into its own address space. By agreement, the client would be responsible for releasing the response data packet memory. Typically, this would be stated as part of a formal protocol specification. Because GMEM_SHARE global memory allocations are only copies, they must be treated as read-only by nonallocator applications. Note that in non-EMS or small frame EMS configurations, Windows always allocates global memory from the nonbanked portion of the global heap. Sample Client-Server Session Figure 9 shows a sample client-server session. Assume that the client is Books Browser, an application that allows the user to view, change, or modify a bookstore's inventory control system. The inventory control system consists of a database of WinTrieve ISAM files. Books Browser accesses the database through the WinTrieve server. When Books Browser is started, it must first try to connect to WinTrieve, as the arrow labeled Initiate Connection in Figure 9 shows. WinTrieve, upon receiving the connection request, must decide whether to accept the connection or reject it. If WinTrieve accepts the connection, further requests from Books Browser are allowed. If it rejects the connection request, for example, because of an error condition, then its client connect table is full and further communications are not allowed. WinTrieve accepts the connection and indicates this to Books Browser by returning a connection acknowledgment. Assume that with Books Browser the user can sequentially scan a Books ISAM file one record at a time. The Books file contains records whose fields consist of a book's ISBN number, its title, its author, its publisher, and the number of copies in stock. The user views a record in the file by selecting Next in the menu bar. Upon selection of "next record," Books Browser sends a request message to WinTrieve. If the request is accepted and processed, WinTrieve returns a request acknowledgment. The user subsequently makes several more requests, as shown in Figure 9. Finally the user decides to close Books Browser. Before exiting, Books Browser terminates the connection by sending a terminate connection message to WinTrieve. WinTrieve then returns a terminate acknowledgment. The WinTrieve Protocol The synchronous protocol implemented for WinTrieve is not WinTrieve-dependent. It should be relatively straightforward to implement for other server applications. Code samples, although taken from WinTrieve, have been rewritten to minimize or completely remove WinTrieve-specific details. The WinTrieve protocol supports three message types (see Figure 10 for the message types and their descriptions). The three messages correspond to the messages described above in the sample session with one exception: there is no corresponding acknowledgment message. The WinTrieve protocol handles acknowledgments as return values, not as messages. Because the WinTrieve protocol is synchronous, clients send messages using the Windows function SendMessage. The long value returned by SendMessage represents the message acknowledgment. Return values are actually return codes: one represents success and others represent errors. Figure 11 lists the valid return codes. The return code ISNOSERVER has special significance. SendMessage returns zero if the specified receiver window handle is invalid (no window with that handle is currently running). The receiving WndProc must not return a zero value; if it did so, the sender would be unable to differentiate between a SendMessage error and a protocol error. As defined by the WinTrieve protocol, all return codes are nonzero. Figure 12 summarizes the SendMessage argument values for each WinTrieve protocol message type. Making the Connection When the client wants to connect, it must first obtain the hWnd of the server. The server needs a mechanism by which it can make itself known to potential clients. This can be accomplished in at least two ways. The method used by the WinTrieve protocol specifies that the server's communications window have a well-known class and window name. The class name is defined when the window class is registered, and the window name is defined when the window is created. For WinTrieve the window name is the string ISAM SERVER. When a client application wants to connect to WinTrieve, it calls the WinTrieve C API function isconnect, a version of which is shown in Figure 13. Except for connection initiation and termination functions, the WinTrieve C API completely insulates the developer of WinTrieve applications from the WinTrieve server protocol. The WinTrieve C API is a small object library that is linked with the client application. The WinTrieve protocol message values are obtained by calling the Windows function RegisterWindowMessage (see Figure 13). The Windows function FindWindow is then called to obtain the handle of the server's communications window. FindWindow generally returns the handle of the window, identified by its class and window name. Note that using FindWindow necessarily restricts the number of running servers of the same type to one. WinTrieve enforces this restriction by immediately exiting if an instance of itself is already running. After obtaining the handle to the server's communications window, isconnect sends an initiate connection message to the server. The wParam (word parameter) argument of the SendMessage call is set to the handle of a client application's window. The handle is passed as the sole argument to isconnect and is normally the handle of the client's main window. The only restriction on the client window handle is that it remain valid throughout the life of the connection. If the function completes successfully, the global variable hWndServer is set to the handle of the server's communications window, and the global variable hWndClient is set to the hWnd argument. These globals will be used subsequently in communications with the server. Making a Request When a client is connected, it can make requests to the server. In order to make a request, the client must first build a request block, which will typically contain an opcode that specifies the request type and any other necessary request data. Figure 14 shows the C structure declaration for a WinTrieve protocol request block. The structure is generalized because the fields of the structure support all the possible opcodes. For any single opcode only a few fields may actually be used. "Request block" is something of a misnomer because the server also uses the same request block to return response data. Remember that the WinTrieve protocol specifies that the request block be allocated from the nonbanked portion of the global heap. A client need allocate this block of memory only once and use it throughout the lifetime of a session, thus minimizing the amount of nonbanked memory allocated for each client. For example, Figure 15 shows the fields and their description for a WinTrieve open ISAM file request. Code for a sample WinTrieve C API request function is in Figure 16. This function reads a record in an ISAM file. All of the WinTrieve C API request functions follow the same basic techniques as isread to make a request. The requisite fields for the request are filled as specified by the protocol definition. After unlocking the request block, the function sends a request message to the server. The SendMessage return value is checked to determine if any errors occurred at the protocol level. Response data is copied into the client's local address space. Note how isread introduces a second level of error handling that deals with errors that occurred while processing the request. Request-processing errors are returned in status fields of the request block (see Figure 14). The first level, protocol level, deals with errors related to processing the protocol. Two layer error handling tends to simplify protocol and make it more generic for other server applications. Ultimately the WinTrieve C API functions combine both levels of error handling into a single layer for the user. After making several requests, and before closing, the client application must end the session with the server. It does this by sending a terminate connection message to the server. Figure 17 shows sample code for the WinTrieve C API function that terminates the connection. Server Perspective Servers can be built in layers as shown in Figure 18. The first layer is the protocol manager, which mainly consists of the server's communications window, WndProc, and associated routines. This layer is fairly generic and can be applied easily to other server types. The next layer, the dispatch manager, is more server-specific, but its overall form is that of a large switch statement keyed off the request opcode. Subsequent layers are very server-specific. When a server is started, it must first create the window for client applications to send messages. This window has been referred to as the server's communications window. The only requirement when creating the server's communications window is that its class and name conform to the protocol specification. Figure 19 shows sample code for creating the WinTrieve communications window. The code is generic enough that other server applications can use it by specifying a different class and window name. Figure 20 shows sample code for the WinTrieve WndProc routine. Note that the code is simplified to be generic and to improve clarity. Loading the Server A seeming disadvantage of the server approach is that the server must be explicitly executed. This places an unnecessary burden on the end user. Windows supports a method by which applications can be started transparently with no user interaction. This is done by specifying the executable file name in the load line of the [windows] section of the WIN.INI file. Windows reads this file when it starts. Any program listed in the load line is started (as an icon) after Windows initially begins. As with WinTrieve, many servers will only run in an iconic state. The only indication that the user will have that the server is running is the server icon displayed in the lower-left portion of the screen. Language Interface When building servers such as those described in this article, you should strive to provide an API for the target application language also. The API should hide the details of the protocol and give the appearance that the user is calling a library and not a server. WinTrieve, for example, provides two APIs. One is for C (see Figure 21 for a summary of WinTrieve C API functions), and one is for Actor (Actor APIs take the form of class libraries). Two C API functions, isconnect and isdisconnect, were introduced to support the server approach. All other functions remained the same. Conclusion In many situations, the client-server approach is the only way by which large client applications can run. All other methods either directly or indirectly reduce the client's address space. A disadvantage of the server approach is that a certain amount of overhead is incurred for context switch and data passing. Figure 22 summarizes the advantages and disadvantages of the client-server approach under Windows. Windows support for memory management, user-defined protocols, shared memory, and message passing allows for the efficient implementation of the client-server model that is largely application-independent. Figure 10 Message Type Description INITIATE Message sent by client to server to initiate a connection SEND Message sent by client to server to request services TERMINATE Message sent by client to server to terminate a connection Figure 11 Error Constant Value INITIATE SEND TERMINATE Description ISNOSERVER 250 3 3 3 Invalid server handle (server not running) ISMAXCONN 251 3 Maximum number of connections would be exceeded ISNOTCONN 252 3 3 Client not connected ISCONN 253 3 Client is connected ISNULLPTR 254 3 GlobalLock of query block returned NULL pointer ISOK 255 3 3 3 Message sent successfully Figure 12 Message SendMessage Arguments hWnd msg wParam lParam INITIATE hWndServer WM_INIT_ISAM hWndClient 0L SEND hWndServer WM_SEND_ISAM hWndClient High word contains handle of client request block TERMINATE hWndServer WM_TERM_ISAM hWndClient 0L Figure 13 /* WinTrieve C API, error codes are returned in global variable, iserrno. If an API function returns -1 it indicates an error. */ int iserrno; /* These values need to be available to other WinTrieve C API functions. For the example code given, assume these variables are globally defined. This does not necessarily reflect the actual implementation. */ HWND hWndServer; /* server window handle */ HWND hWndClient; /* client window handle */ WORD wmInitISAM; /* WM_INITIATE_ISAM message value */ WORD wmSendISAM; /* WM_SEND_ISAM message value */ WORD wmTermISAM; /* WM_TERMINATE_ISAM message value */ /* Make connection to server. Argument hWnd is handle to a client window. Should ensure that hWnd remains valid throughout the life of the connection. */ int isconnect( HWND hWnd ) { static char *szServerName = "ISAM SERVER"; static char *szInit = "WM_INITIATE_ISAM"; static char *szSend = "WM_SEND_ISAM"; static char *szTerm = "WM_TERMINATE_ISAM"; static int iFirstTime = 1; long lRetVal; /* Obtain WinTrieve protocol message values. */ if (iFirstTime) { /* Only do this once. */ wmInitISAM = RegisterWindowMessage((LPSTR)szInit); wmSendISAM = RegisterWindowMessage((LPSTR)szSend); wmTermISAM = RegisterWindowMessage((LPSTR)szTerm); iFirstTime = 0; } /* Get the handle of the server's communications window. */ hWndServer = FindWindow((LPSTR)szServerName, (LPSTR)szServerName); if (hWndServer = = 0L) { /* Error, server not running. */ iserrno = ISNOSERVER; return -1; /* Error, server not running. */ } lRetVal = SendMessage(hWndServer, WM_INITIATE_ISAM, hWnd, 0L); if (lRetVal != ISOK) { /* Error, two possible conditions, server must * have died or maximum connections reached. */ if (lRetVal = = 0L) /* Assign error code to iserrno = ISNOSERVER; / * global variable. */ else iserrno = retVal; return -1; /* Indicates error. */ } /* Connection successful. */ hWndClient = hWnd; return 0; } Figure 14 /* An abbreviated list of opcode constants. */ #define OPADDINDEX /* add an index */ #define OPBGNTRAN /* begin a transaction */ #define OPBUILD /* create an ISAM file */ #define OPCLOSE /* close an ISAM file */ #define OPCOMMIT /* commit a transaction */ #define OPDELETE /* delete a record */ #define OPOPEN /* open an ISAM file */ #define OPREAD /* read a record */ #define OPREWRITE /* update a record */ #define OPWRITE /* write a record */ struct isrequest { int iOpcode; int iIsfd; int iMode; int iKeynum; int iReclen; long lRecnum; long lUniqid; GLOBALHANDLE hBuffer; GLOBALHANDLE hFileName1; GLOBALHANDLE hFileName2; GLOBALHANDLE hRecord; char isstat1; char isstat2; int iserrno; int iserrio; int isretval; }; Figure 15 Query Block Fields Description Client iOpcode OPOPEN Request iMode Access mode in effect while file is open. Arithmetic sum of a read/write Data mode and lock mode values. hFilename1 Handle to a GMEM_NON_BANKED global memory that contains a null-terminated character string identifying the ISAM file to be opened. iReclen File record length Server isretval Return code; -1 indicates error such as file not found. Otherwise, file Response descriptor (isfd) identifies open file; used in subsequent file operations. Data iserrno ISAM error code iserrio System error code isstat1 Status 1 code isstat2 Status 2 code Figure 16 /* Handle to isrequest struct, previously allocated from the nonbanked portion of the global heap. An appropriate place to allocate the request block is in isconnect. */ GLOBALHANDLE hRequestBlock; /* Table maintained by the C API that holds the record length of all open ISAM files. Indexed by isfd of open file. The table record length is filled in the table when a file is opened or created. */ int iRecLenTbl[MAX_FILE_DESCRIPTORS]; /* Function to read a record from an ISAM file. */ int isread( int isfd, /* fd of previously opened ISAM file */ char *record, /* buf to copy read record into */ int mode) /* which record, next, prev, equal, etc */ { struct isquery far *lpRB; LPSTR lpRec; int iRecLen; int iRetVal; lpRB = (struct isquery far *)GlobalLock(hRequestBlock); if (lpRB = = NULL) { /* Error, not able to lock query block. */ iserrno = ISNULLPTR; return -1; } lpRB->iOpcode = OPREAD; lpRB->iIsfd = isfd; lpRB->iMode = mode; /* WinTrieve allows the user to search for a record based on index key fields. It is expected the values of interest are filled in the record buffer to be passed to the server. For the sake of simplicity assume that the global memory block specified by hRecord is large enough to hold the record. */ lpRec = GlobalLock(lpRB->hRecord); if (lpRec = = NULL) { /* Error, not able to lock the record block. */ GlobalUnLock(hRequestBlock); iserrno = ISNULLPTR; return -1; } /* Copy the record. */ iRecLen = iRecLenTbl[isfd]; lmemcpy(lpRec, (LPSTR)record, iRecLen); /* All fields filled, now unlock and make the request. */ GlobalUnlock(lpRB->hRecord); GlobalUnlock(hRequestBlock); iRetVal = SendMessage(hWndServer, wmSendISAM, hWndClient, MAKELONG(hRequestBlock, 0)); if (iRetVal != ISOK) { /* Protocol error. */ if (iRetVal = = 0L) iserrno = ISNOSERVER; else iserrno = iRetVal; return -1; } /* Lock the request block to get the response values. */ lpRB = (struct isquery far *)GlobalLock(hRequestBlock); if (lpRB = = NULL) { /* Error, not able to lock query block. */ iserrno = ISNULLPTR; return -1; } /* This introduces the second level of error handling. * These are errors related to the processing of the * request which are differentiated from the processing * of the protocol. */ iRetVal = lpRB->iRetVal; if (iRetVal = = -1) { iserrno = lpRB->iserrno; return iRetVal; } lpRec = GlobalLock(lpRB->hRecord); if (lpRec = = NULL) { /* Error, not able to lock the record block. */ GlobalUnLock(hRequestBlock); iserrno = ISNULLPTR; return -1; } /* Copy the record that was read. Assume lmemcpy is * long pointer version of C Runtime memcpy. */ lmemcpy((void far *)record, lpRec, iRecLen); GlobalUnlock(lpRB->hRecord); GlobalUnlock(hRequestBlock); return iRetVal; } Figure 17 /* Function to terminate connection with server. */ int isdisconnect() { int iRetVal; iRetVal = SendMessage(hWndServer, wmTermISAM, hWndClient, 0L); if (iRetVal != ISOK) { /* Error terminating connection. */ if (iRetVal = = 0L) iserrno = ISNOSERVER; else iserrno = iRetVal; return -1; } /* Successfully terminated connection with server. */ hWndServer = NULL; return 0; } Figure 19 /* Create the server's communications window. If successful, returns window handle. If error, returns NULL. */ HWND CreateCommWindow(HANDLE hInstance, HANDLE hPrevInstance ) { /* Server's class and window name as defined by WinTrieve protocol specification. */ static char *szCommName = "ISAM SERVER"; WNDCLASS wndclass; HWND hWnd; /* Only one instance of the server is allowed * to run at any one time. Return error. */ if (hPrevInstance) return NULL; /* Register the server window class. Nothing special here. */ wndclass.style = CS_HREDRAW | CS_VREDRAW; wndclass.lpfnWndProc = WndProc; wndclass.cbClsExtra = 0; wndclass.cbWndExtra = 0; wndclass.hInstance = hInstance; wndclass.hIcon = NULL; wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); wndclass.hbrBackground = GetStockObject(WHITE_BRUSH); wndclass.lpszMenuName = NULL; wndclass.lpszClassName = szCommName; if (!RegisterClass(&wndclass)) return NULL; /* Create server communications window. */ hWnd = CreateWindow(szCommName, szCommName, WS_OVERLAPPED|WS_SYSMENU, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL); return hWnd; }; Figure 20 /* Table of currently connected clients. The clientTbl is typically accessed through the three routines getClient, putClient, delClient, and reapClient. */ static HWND clientTbl[MAX_CLIENTS]; static int putClient(HWND); static int getClient(HWND); static int delClient(HWND); static int reapClient(HWND); /* Server communications window WndProc. All client requests come through here. */ long FAR PASCAL WndProc ( HWND hWnd, unsigned iMessage, WORD wParam, LONG lParam ) { static char *szInit = "WM_INITIATE_ISAM"; static char *szSend = "WM_SEND_ISAM"; static char *szTerm = "WM_TERMINATE_ISAM"; static WORD wmInitISAM; static WORD wmSendISAM; static WORD wmTermISAM; GLOBALHANDLE hRequestBlock; HWND hWndClient; int iRetVal; switch (iMessage) { case WM_CREATE: /* Register WinTrieve protocol messages. */ wmInitISAM = RegisterWindowMessage(szInit); wmSendISAM = RegisterWindowMessage(szSend); wmTermISAM = RegisterWindowMessage(szTerm); break; case WM_CLOSE: /* Detect if any clients are still connected. */ if (reapClients()) { /* There are still connected clients. What * is done here is server dependent. WinTrieve * displays a message box and returns (does * not terminate). */ } else /* No connected clients, ok to terminate. */ return DefWindowProc(hWnd, iMessage, wParam, lParam); break; case WM_DESTROY: /* Can do some final cleanup here before the server terminates. */ break; case WM_QUERYOPEN: /* The server is always an icon. */ break; default: if (iMessage = = wmSendISAM) { /* Get handle to query block. */ hRequestBlock = HIWORD(lParam); /* Check that client is connected. */ hWndClient = wParam; if (getClient(hWndClient) < 0) return ISNOTCONN; /* Process the request, this is server dependent. * If protocol error, for example, cannot lock * query block returns an error code, otherwise * returns ISOK. */ iRetVal = ProcessRequest(hWndClient, hRequestBlock); return iRetVal; } else if (iMessage = = wmInitISAM) { /* A server must be robust. Client handles * that have become invalid are checked for here. */ reapClients(); /* Initiate a connection. */ hWndClient = wParam; /* Check that client isn't already connected. */ if (getClient(hWndClient) >= 0) return ISCONN; /* Check for maximum connections. */ if (putClient(hWndClient) < 0) return ISMAXCONN; /* Client connected. */ return ISOK; } else if (iMessage = = wmTermISAM) { /* Terminate a connection. */ hWndClient = wParam; /* Check that client is connected. */ if (delClient(hWndClient) < 0) return ISNOTCONN; return ISOK; } else /* Default message processing. */ return DefWindowProc(hWnd, iMessage, wParam, lParam); break; } return 0L; } /* Put new client into client table. If successful, returns index of table where the handle is stored. If the table is full, returns -1. */ static int putClient( HWND hWnd ) { register int i; for (i = 0; i < MAXHANDLES; i++) { if (clientTbl[i] = = NULL) { clientTbl[i] = hWnd; return i; } } return -1; /* client table full */ } /* Delete client from client table. If successful, returns index position of table where the handle was deleted from. If handle not found, returns -1. */ static int delClient(hWnd) HWND hWnd; { register int i; for (i = 0; i < MAXHANDLES; i++) { if (clientTbl[i] = = hWnd) { clientTbl[i] = NULL; return i; } } return -1; /* handle not found */ } /* Returns index position in client window handles table for the specified client handle. If client handle not found, returns -1. */ static int getClient(hWnd) HWND hWnd; { register int i; for (i = 0; i < MAXHANDLES; i++) { if (clientTbl[i] = = hWnd) return i; } return -1; } /* Check the client table for any invalid client handles. For example clients that died suddenly or clients that just plain forgot to terminate the connection properly. Returns 0 if no more clients connected. */ static int reapClients() { int i; int n; HWND hWndClient; /* Run through client table looking for connected clients. */ for (i = n = 0; i < MAX_CLIENTS; i++) { if (clientTbl[i] != NULL) { hWndClient = clientTbl[i]; /* Check if still valid handle by calling * Windows function IsWindow. */ if (IsWindow(hWndClient) = = NULL) { /* Client died. The function cleanUp * is a server specific routine that * removes the state of a client * if it is no longer valid. */ cleanUp(hWndClient); clientTbl[i] = NULL; continue; } n++; /* increment connected clients count */ } } return n; } Figure 21 Function Description isaddindex Add an index to an ISAM file isbgntran Begin a transaction isbuild Create an ISAM file isclose Close an ISAM file iscommit Commit a transaction isconnect Initiate session with the server isdelcurr Delete the current record isdelete Delete record specified by primary key isdelindex Delete and index isdelrec Delete record specified by record number isdisconnect Terminate session with the server iserase Delete an entire ISAM file isindexinfo Obtain information about an ISAM file isjrnlmsg Add message to journal file isjrnloff Turn off journaling islock Lock an ISAM file isopen Open an ISAM file isread Read a record in an ISAM file isrelease Unlocks all manually locked records in an ISAM file isrename Rename an ISAM file isrewcurr Rewrite the current record isrewrec Rewrite the record identified by record number isrewrite Rewrites a record identified by its primary key isrollback Roll back the current transaction issetjrnl Begin journaling issetuniqid Set the ISAM file's unique ID value isstart Select an index and locate a record isuniqueid Get next unique ID value isunlock Unlock an ISAM file iswrcurr Write a new record and make it current iswrite Write a new record into an ISAM file Figure 22 Advantages ■ Permits larger applications by not residing in client address space with EMS memory configurations ■ Data segments may reside in banked memory with EMS memory configurations ■ Not limited to a single data segment Disadvantages ■ Server memory is not freed if no longer used ■ Overhead of context switch when server is called ■ Overhead of copying request and response data between client and server ■ Windows task overhead Enhancing the Presentation Manager User Interface with Formatted Edit Fields Marc Adler Some applications, such as those designed for the financial sector, require a series of formatted data entry screens. Although the data is frequently in a specific format, such as a Social Security number or a date, the Microsoft Windows and OS/2 Presentation Manager (hereafter "PM") environments do not support editing and validating user-defined formats as a standard feature. This article presents a formatted edit class for PM. This edit class offers great control over user input by providing format validation on a character-by-character basis. It also uses picture masks to define data entry fields. Code The code for the formatted edit class is surrounded by a small shell that constitutes a test program for the new class. The test program (see Figure 1) displays a dialog box consisting of several formatted edit fields and then accepts input from the user. In the startup code (in the main function) PM is initialized by a call to WinInitialize; a call to WinCreateMsgQueue creates a message queue. These two functions must be present at the start of every PM program. The next step is to find the address of the default window procedure for the normal edit class. The window procedure of the edit class does most of the hard work (it takes care of most of the editing commands, for example); it simply intercepts some of the messages directed to the new type of edit controls. You must therefore get a pointer to the default edit procedure so that at times you can let it insert the window procedure into the edit buffer and display the new contents of the buffer. To get the address of the default edit procedure, look into the edit class information structure, the CLASSINFO structure. The CLASSINFO structure contains the style bits associated with that class, the number of bytes reserved per window for a data storage area, and the address of the class window procedure. You use the PM function WinQueryClassInfo to get a copy of this structure. The code to do this is: CLASSINFO clsInfo; WinQueryClassInfo(hAB, WC_ENTRYFIELD, (PCLASSINFO) &clsInfo); pfnOldEditWndProc = clsInfo.pfnWindowProc; You must register the new class with PM in order to store the three elements mentioned above into a CLASSINFO structure, which PM maintains. To do so, use the following call: WinRegisterClass(hAB, "Formatted", FmtWndProc, CS_SIZEREDRAW,clsInfo.cbWindowData + sizeof(PVOID)); By specifying "sizeof(PVOID)" in the last argument, you reserve space for a pointer to the format information structure that will be attached to each formatted edit control. The same steps are repeated for the static text class to intercept the static class. In this way you can intercept requests to draw the text on the screen and instead use your own drawing routines. Finally, you create the main window and go into the standard message polling loop that accompanies every PM program. The WinSubclassWindow function is not used to get the address of the default window procedures for the various classes, because you must provide the handle to the window that you want to subclass as your first argument. You must first know the IDs of the edit controls to get their window handles. Thus, a table in your program must contain the ID of each edit control in each dialog box, and you must have the code to subclass each edit control in each dialog box procedure. An example would be: for (i = 0; idTable[i]; i++) if ((hEdit = WinWindowFromID(hDlg, idTable[i]))) pfnOldProc = WinSubclassWindow(hEdit, MyWindowProc); This is too much overhead in my opinion. That's why WinSubclassWindow was not used in this application. Structure As mentioned above, every window can have a private data storage area. The amount of memory allocated for this area is determined by the cbWindowData field of the CLASSINFO structure. For each formatted edit control, an extra four bytes of storage is reserved to hold a far pointer to a format information block. The format information block has the following information: typedef struct format { ULONG fFormatFlags; /* formatting style bits */ NPFN pfnValidChar; /* function to validate each character*/ PSZ szPicture; /* picture string */ } FORMAT, FAR *PFORMAT; fFormatFlags contains style bits for the control (see Figure 2). pfnValidChar is a pointer to a function that will be called in order to validate each character that is typed. pfnValidChar will not be used if a picture clause is associated with the control. szPicture can contain an optional picture clause that is to be used for formatting. The formatting characters that can be used within a picture clause are listed in Figure 3. The PICINFO structure associates validation functions for each of the formatting characters within a picture clause. For instance, if you type a character in a column that has the picture character 9 attached to it, the ValidDigit function will be called to ensure that the typed character is a digit. If a character in a picture clause is not in the PICINFO structure, it is assumed to be a protected character. Characters cannot be entered in a column that has a protected picture character attached to it. For example, the picture clause for a phone number would be: (999) 999-9999 In this picture, the parentheses and the dash are protected characters. The user will not be allowed to place the cursor in a protected column in the code that implements the formatted edit class. A control does not need a picture clause in order for character validation to take place. It can use the styles FLD_NUMERIC, FLD_SIGNEDNUMERIC, FLD_ALPHA, FLD_ALPHANUMERIC, and FLD_LOGICAL to impose a uniform character type throughout the entire edit control. For instance, a control that has the FLD_ALPHA style can accept only the letters A-Z and a-z as input. How is a formatted edit control defined as a member of a dialog box? You simply specify the word Formatted as the control class in the resource file definition. For example, the following defines a formatted edit control in the application: CONTROL "", ID_NUMERIC, 65, 49, 96, 8, "Formatted", ES_LEFT | ES_MARGIN | WS_TABSTOP | WS_VISIBLE The edit control's text field can be used to define a picture clause: CONTROL "(999) 999-9999", ID_NUMERIC, 65, 49, 96, 8, "Formatted",ES_LEFT | ES_MARGIN | WS_TABSTOP | WS_VISIBLE You can also use the CTLDATA clause to define other style bits. For instance, to make the control above a required field (that is, a field with the FLD_REQUIRED style), you can do the following: #define FLD_REQUIRED 32 CONTROL "", ID_NUMERIC, 65, 49, 96, 8, "Formatted", ES_LEFT | ES_MARGIN | WS_TABSTOP | WS_VISIBLE CTLDATA FLD_REQUIRED In the code, FLD_REQUIRED is defined as 20h or 32. The values specified in the CTLDATA clause are passed to the window procedure by the WM_CREATE message in the first parameter. They are found in the CREATESTRUCT structure also, which is pointed to by the second parameter. One drawback in using CTLDATA is that the current version of the resource compiler, RC.EXE, does not allow the use of the bitwise OR operator in the CTLDATA clause. Therefore, the following clause is not allowed. CTLDATA FLD_NUMERIC | FLD_REQUIRED To avoid this, determine the correct value yourself and insert it directly into the CTLDATA clause so that you have: CTLDATA 1056 Format Window Procedure All of the messages that PM sends to an edit control will be routed not to the default edit control window procedure, but to your own window procedure for the formatted edit class, FormatWndProcedure. Because you want to provide complete editing capabilities in your formatted controls, use the existing default window procedure for edit controls to do most of the hard work associated with editing. Simply use FormatWndProcedure to provide pre-processing and post-processing to the normal editing actions. When an edit control is created, a WM_CREATE message is sent to the class window procedure. A pointer to a block of information, called the CREATESTRUCT, is passed in the mp2 parameter of the WM_CREATE message. You can examine the pszText member of this structure to see if a picture clause was specified as the control's text. If so, save a copy of the picture and set pszText to NULL, which tells PM that the edit control is initially blank. Next, allocate a format info block and put its address in the edit control's storage area for each of its windows. Call the default edit window procedure to have the default creation processing take place, and then provide some added value to the control. Examine the mp1 parameter, which points to any data specified by the CTLDATA clause. If you find some data, use the bitwise OR operator to flip these style bits with any style bits that had been set by the picture clause. If there are protected characters in a picture clause, you must ensure that the user cannot place the cursor over a protected column. But what happens if the user clicks the mouse over a protected column? The default behavior for an edit control is to set the cursor to the column that the mouse was clicked on. To prevent that, intercept the WM_SETFOCUS message. Because you are only interested in whether you are gaining the input focus, not losing it, examine the mp2 parameter. If mp2 is FALSE, you are losing focus, so you should let the default edit window procedure handle it. By sending the edit control an EM_QUERYSEL message, you can determine in which column you are. If you are over a protected column, you send the edit control a simulated RIGHT-ARROW keystroke until the cursor moves into a nonprotected column. The WM_DESTROY message must also be intercepted. This message is sent to a control when the control is being destroyed and gives the application the opportunity to release any resources allocated to that control. In this case, you want to release the memory that was allocated to the format information block. Failure to do so might result in a dangerous fragmented memory situation if many formatted edit controls are used. Before we get to the interesting part, let's look at what you should do with the WM_PAINT message. You want to draw a shadow around the border of the edit control in order to create a three-dimensional effect. You would also like to use a monospaced font for the edit text. Because a different font is being used, be sure that the cursor is in the correct column. The default edit window procedure thinks you are using the system-proportional font and therefore maps the logical cursor position into a physical column, using the width tables associated with the system font. Because you are using a different font and therefore different character widths, you must map correctly the logical cursor position into a physical column. FormatWndProcedure is sent a WM_PAINT message whenever the edit control needs to be drawn. First, you use the GpiMove function to draw the border around the window. Next use GpiBox to draw shadows under and to the right of the borders. Use GpiCharStringAt to draw the text in the presentation space associated with the monospaced font instead of in the default presentation space. Finally, you calculate the correct physical column in which to place the cursor. The WinCreateCursor call provides the ability to set the cursor to a specific coordinate within a window. WM_CHAR Message Most of the work implementing the formatted edit control is in dealing with keystrokes. If you type a printable character, you must determine the proper validation routine to call if a picture clause is associated with the edit control. If you use protected columns, after each character is typed you must advance the cursor to the next unprotected column, rather than advancing it by a single column. The same is true for the cursor keys. The left, right, home, and end keys must account for protected columns. To do this, send VK_RIGHT or VK_LEFT keystrokes into the edit control until you move into an unprotected column. This is easily accomplished in PM by sending the edit control a WM_CHAR message with the mp1 and mp2 parameters set to the proper values. The code is shown below. while (cnt--) { /* Create a simulated keystroke */ mp1 = MPFROMSH2CH(KC_VIRTUALKEY, 1, 0); mp2 = MPFROM2SHORT(0, VK_RIGHT); pfnOldEditWndProc(hWnd, WM_CHAR, mp1, mp2); } A more complicated issue arises when you are trying to insert and delete characters. If a picture clause has mixed character types, inserting or deleting a character could throw off the entire field. I'll take the safe way out by disallowing the insert, delete, or backspace characters to be passed on to the formatted edit control. If an edit control has a picture clause, it will always remain in overstrike mode. Overstrike mode is implemented by selecting the character to the right of the one you inserted and then deleting that character. The code to do this is: /* Implement overstrike mode by deleting the next character */ pfnOldEditWndProc(hWnd, EM_SETSEL, MPFROM2SHORT(iPos, iPos+1), 0L); pfnOldEditWndProc(hWnd, EM_CLEAR, 0L, 0L); /* Let the edit win procedure handle the insertion of the character */ pfnOldEditWndProc(hWnd, WM_CHAR, mp1, mp2); Different Fonts When developers received the official release of PM, the default system font had been changed from a monospaced font to a proportionally spaced one. Many applications that used list boxes to display columns of data had to be redesigned. But what if you simply wanted Courier or a proportionally spaced font different from the system font? A presentation space (PS) is a data structure that maintains information about an application's device-independent drawing environment, including the current logical font the application uses when it writes text to the output device. In this application, you set the current logical font to the monospaced Courier font whenever dialog boxes, which contain formatted edit fields, are used. For further information, see "The Graphics Programming Interface: A Guide to OS/2 Presentation Spaces," MSJ (Vol. 3, No. 3). Because the screen is the main concern for now, work with a cached micro-PS, not the other types of presentation spaces (normal and standard micro). Before a dialog box is shown, the WM_INITDLG message is sent to the dialog box's window procedure; usually applications initialize the data inside the dialog box's controls at this time. The application will substitute the Courier font for the current font during this period. The WinGetPS function is used to retrieve the cached presentation space. This PS can be used to perform subsequent drawing. SetMonospacedFont is called to locate the desired font and to load it into the PS. At the end of the application, WinReleasePS must be called in order to give the PS back to the cache. At the start of SetMonospacedFont, the following code loads a copy of the Courier font: GpiLoadFonts(hAB, (PSZ) "C:\\os2\\dll\\courier.fon"); The fonts are in a dynamic-link library; each process that needs to access the fonts must use GpiLoadFonts. However, the fonts are loaded into memory only once; if any other process loads them, a reference count is simply incremented. To find the particular font that suits your needs after the fonts are loaded, load the font metric information for each font in that family. First, however, you must find out how many fonts of the specified family have been loaded. To do this, you use the following: nRequestFonts = 0L; nFonts = GpiQueryFonts(hPS, QF_PUBLIC | QF_PRIVATE, (PSZ) "Courier", &nRequestFonts, 0L, NULL); Because nRequestFonts is set to 0, no font metric information is returned, but the number of fonts available in that family is returned. Next, you allocate a buffer to hold the font metric information. The font metrics are retrieved by calling: GpiQueryFonts(hPS, QF_PUBLIC | QF_PRIVATE, (PSZ) "Courier", &nFonts, (LONG)sizeof(FONTMETRICS), pMetrics); It is necessary to go through all of the font metrics to find a font that fits your specifications. GpiCreateLogFont is used to try to create a logical font that matches your needs. If the value returned is 2, then you have a match. Each created font is referenced by a local numeric identifier. rcMatch = GpiCreateLogFont(hPS, (PSTR8) szFont, (LONG) ++IDCourier, (PFATTRS) &FontAttrs); if (rcMatch = = 2) { GpiSetCharSet(hPS, IDCourier); } GpiCharSet will set the current font in the presentation space to the created logical font referenced by the identifier IDCourier. Once the logical font is associated with the presentation space, any text that is drawn on the screen is drawn in that font. Enhancements Many things can be done to improve the formatted edit class presented here. Not all formatting styles are implemented in this code because of space considerations. I have not considered what happens when a user deletes or copies a marked area that contains protected characters. Evidently, the protected characters must remain intact, but should you allow a user to do this? I have also not implemented insert mode; the edit controls are always in overstrike mode because of the problem mentioned above. The code could be modified to allow insertion and deletion if the result would not invalidate the data. I have also skimped on some of the other messages that edit controls should handle. The WinSetWindowText function issues the WM_SETWINDOWPARAMS message, for instance. For this message, you should detect if the user is trying to set the edit control's text, and check the validity of the new text before setting it. The WinQueryWindowText function issues the WM_QUERYWINDOWPARAMS message. You may or may not want to insert the protected characters of the picture into the string that is sent back to the user for this message. You can also think about implementing the multiple-line edit class that PM needs. Compare the power of the edit class in the Microsoft Windows environment with that of PM, and you will find that much work needs to be done. Figure 4 lists the messages that can be sent to an edit control in both Windows and PM. As you can see, PM has a long way to go. Figure 1 FMT OBJS = fmt.obj fmt.exe: $(OBJS) fmt.def fmt.res link /co $(OBJS), /align:16, nul, os2, fmt.def rc fmt.res fmt.obj: fmt.c cmd /c mcc fmt fmt.res: fmt.rc rc -r fmt.rc FMT.RC /*****************************************************************/ /* resource file for the formatted edit control test program */ /*****************************************************************/ #include <os2.h> #include "fmt.h" #define FLD_HASPICTURE 1 #define FLD_AUTONEXT 2 #define FLD_NOECHO 4 #define FLD_PROTECT 8 #define FLD_IGNORE 16 #define FLD_REQUIRED 32 #define FLD_TOUPPER 64 #define FLD_TOLOWER 128 #define FLD_CENTER 256 #define FLD_RJUST 512 #define FLD_NUMERIC 1024 #define FLD_SIGNEDNUMERIC 2048 #define FLD_ALPHA 4096 #define FLD_ALPHANUMERIC 8192 #define FLD_LOGICAL 16384 MENU DLG_FORMAT { SUBMENU "~File", 1 { MENUITEM "~Test...", ID_DLG MENUITEM "E~xit", ID_EXIT } } DLGTEMPLATE DLG_FORMAT LOADONCALL MOVEABLE DISCARDABLE BEGIN DIALOG "FORMATTING", DLG_FORMAT, 92, 90, 213, 75, FS_NOBYTEALIGN | FS_DLGBORDER | WS_VISIBLE | WS_CLIPSIBLINGS | WS_SAVEBITS, FCF_TITLEBAR BEGIN CONTROL "NUMERIC", 256, 11, 49, 50, 8, "MonoStatic", SS_TEXT | DT_LEFT | DT_TOP | WS_GROUP | WS_VISIBLE CONTROL "ALPHA", 257, 12, 29, 29, 8, "MonoStatic", SS_TEXT | DT_LEFT | DT_TOP | WS_GROUP | WS_VISIBLE CONTROL "ALNUM", 258, 10, 11, 37, 8, "MonoStatic", SS_TEXT | DT_LEFT | DT_TOP | WS_GROUP | WS_VISIBLE CONTROL "(999) 999-9999", ID_NUMERIC, 65, 49, 96, 8, "Formatted", ES_LEFT | ES_MARGIN | WS_TABSTOP | WS_VISIBLE CTLDATA 0 CONTROL "AAAAAAAA", ID_ALPHA, 64, 29, 67, 8, "Formatted", ES_LEFT | ES_MARGIN | WS_TABSTOP | WS_VISIBLE CTLDATA FLD_TOUPPER CONTROL "", ID_ALNUM, 64, 10, 71, 8, "Formatted", ES_LEFT | ES_MARGIN | WS_TABSTOP | WS_VISIBLE CTLDATA FLD_ALPHANUMERIC /* | FLD_REQUIRED */ CONTROL "OK", ID_OK, 159, 9, 38, 12, WC_BUTTON, BS_PUSHBUTTON | BS_DEFAULT | WS_TABSTOP | WS_VISIBLE END END FMT.H #define DLG_FORMAT 300 #define ID_OK 262 #define ID_ALNUM 261 #define ID_ALPHA 260 #define ID_NUMERIC 259 #define ID_DLG 500 #define ID_EXIT 599 FMT.DEF NAME fmt WINDOWAPI DESCRIPTION 'Formatting Test' PROTMODE HEAPSIZE 1024 STACKSIZE 8192 EXPORTS ClientWndProc FmtDlgProc FmtWndProc MonoStaticWndProc FMT.C /*****************************************************************/ /* Formatted edit routines for Presentation Manager. */ /*****************************************************************/ #define INCL_WIN #define INCL_DOS #define INCL_GPI #include <os2.h> #include <stdio.h> #include <ctype.h> #include <malloc.h> #include <stdarg.h> #include "fmt.h" #define SimpleMessage(hWnd, msg) WinMessageBox(hWnd, hWnd, \ (PSZ) msg, (PSZ) "Message", 0, MB_OK) #define LCID_COURIER 1L FONTMETRICS FMCourier; FATTRS FontAttrs; LONG IDCourier = 0; HPS hMyPS; HWND hWndFrame, hWndClient; HAB hAB; PFNWP pfnOldEditWndProc = (PFN) NULL; PFNWP pfnOldStaticWndProc = (PFN) NULL; MRESULT EXPENTRY ClientWndProc(HWND, USHORT, MPARAM, MPARAM); MRESULT EXPENTRY FmtDlgProc(HWND, USHORT, MPARAM, MPARAM); MRESULT EXPENTRY FmtWndProc(HWND, USHORT, MPARAM, MPARAM); MRESULT EXPENTRY MonoStaticWndProc(HWND, USHORT, MPARAM, MPARAM); void pascal EditSetCursor(HWND hWnd); extern PSZ lstrcpy(PSZ s, PSZ t); int pascal ValidDigit(); int pascal ValidDigitSignSpace(); int pascal ValidAlpha(); int pascal ValidAlphaNum(); int pascal ValidLogical(); int pascal ValidAny(); int pascal ConvertToUpper(); int pascal ConvertToLower(); typedef struct format { ULONG fFormatFlags; #define FLD_HASPICTURE 0x0001L #define FLD_AUTONEXT 0x0002L /* Go to next field when all filled */ #define FLD_NOECHO 0x0004L /* Don't echo chars (for passwds) */ #define FLD_PROTECT 0x0008L /* No data in this field */ #define FLD_IGNORE 0x0010L /* Cursor skips over this field */ #define FLD_REQUIRED 0x0020L /* User MUST enter data in this field */ #define FLD_TOUPPER 0x0040L /* Convert characters to uppercase */ #define FLD_TOLOWER 0x0080L /* Convert characters to lowercase */ #define FLD_CENTER 0x0100L /* Center the data in the field */ #define FLD_RJUST 0x0200L /* Right-justify the data in the field */ #define FLD_NUMERIC 0x0400L #define FLD_SIGNEDNUMERIC 0x0800L #define FLD_ALPHA 0x1000L #define FLD_ALPHANUMERIC 0x2000L #define FLD_LOGICAL 0x4000L #define FLD_MIXEDPICTURE 0x10000L NPFN pfnValidChar; /* Function to validate each char */ PSZ szPicture; /* Picture string */ } FORMAT, FAR *PFORMAT; struct mask_to_func { ULONG mask; NPFN pfnFunc; } MaskToFunc[] = { FLD_NUMERIC, ValidDigit, FLD_SIGNEDNUMERIC, ValidDigitSignSpace, FLD_ALPHA, ValidAlpha, FLD_ALPHANUMERIC, ValidAlphaNum, FLD_LOGICAL, ValidLogical, }; typedef struct picinfo { NPFN pfnPicFunc; /* Validation function corresponding to mask */ BYTE chPic; /* The mask character */ } PICINFO; extern PICINFO *CharToPicInfo(int c); PICINFO PicInfo[] = { ValidDigit, '9', ValidDigitSignSpace, '#', ValidAlpha, 'A', ValidLogical, 'L', ValidAlphaNum, 'N', ValidAny, 'X', ConvertToUpper, '!', NULL, '\0' }; /*******************************************************************/ /* main() */ /*******************************************************************/ int main(void) { HMQ hMQ; QMSG qMsg; ULONG flCreateFlags = FCF_TITLEBAR | FCF_SYSMENU | FCF_SIZEBORDER | FCF_MINMAX | FCF_TASKLIST | FCF_SHELLPOSITION | FCF_MENU; PSZ szClassName = "ClientClass"; CLASSINFO clsInfo; /* Initialize the window and create the message queue */ hAB = WinInitialize(0); hMQ = WinCreateMsgQueue(hAB, 0); /* Get the address of the default window proc for edit controls. We will call this proc to do most of the processing for the new formatted edit controls. */ if (!WinQueryClassInfo(hAB, WC_ENTRYFIELD, (PCLASSINFO) &clsInfo)) { DEBUG("Could not get class info for WC_ENTRYFIELD"); goto bye; } pfnOldEditWndProc = clsInfo.pfnWindowProc; /* Register the main window class */ WinRegisterClass(hAB, szClassName, ClientWndProc, CS_SIZEREDRAW, sizeof(PVOID)); if (!WinRegisterClass(hAB, "Formatted", FmtWndProc, CS_SIZEREDRAW, clsInfo.cbWindowData + sizeof(PVOID))) { DEBUG("Could not register class Formatted"); goto bye; } /* We have our own static class too! */ if (!WinQueryClassInfo(hAB, WC_STATIC, (PCLASSINFO) &clsInfo)) { DEBUG("Could not get class info for WC_STATIC"); goto bye; } pfnOldStaticWndProc = clsInfo.pfnWindowProc; if (!WinRegisterClass(hAB, "MonoStatic", MonoStaticWndProc, CS_SIZEREDRAW, clsInfo.cbWindowData + sizeof(PVOID))) { DEBUG("Could not register class MonoStatic"); goto bye; } /* Create the main window */ hWndFrame = WinCreateStdWindow(HWND_DESKTOP, /* Parent */ WS_VISIBLE /* Window styles */ &flCreateFlags, /* Frame styles */ szClassName, /* Window title */ CS_SIZEREDRAW, /* Client style */ NULL, /* Resource ID */ DLG_FORMAT, /* Frame window id */ &hWndClient); /* Client handle */ /* Message Processing Loop. */ while (WinGetMsg(hAB, &qMsg, NULL, 0, 0)) { WinDispatchMsg(hAB, &qMsg); } /* End of the program. Destroy the window and message queue. */ WinReleasePS(hMyPS); WinDestroyWindow(hWndFrame); bye: WinDestroyMsgQueue(hMQ); WinTerminate(hAB); return 0; } /********************************************************************/ /* ClientWndProc() */ /* Main window procedure for this app. All messages to the client */ /* window get sent here. */ /********************************************************************/ MRESULT EXPENTRY ClientWndProc(HWND hWnd, USHORT msg, MPARAM mp1, MPARAM mp2) { HPS hPS; switch (msg) { case WM_PAINT : /* Erase the window */ hPS = WinBeginPaint(hWnd, NULL, NULL); GpiErase(hPS); WinEndPaint(hPS); return MRFROMSHORT(TRUE); case WM_COMMAND : switch (COMMANDMSG(&msg)->cmd) { case ID_DLG : WinDlgBox(HWND_DESKTOP, hWnd, FmtDlgProc, NULL, DLG_FORMAT, NULL); break; case ID_EXIT : WinPostMsg(hWnd, WM_CLOSE, 0L, 0L); break; } return MRFROMSHORT(FALSE); } /* end switch */ return WinDefWindowProc(hWnd, msg, mp1, mp2); } /********************************************************************/ /* FmtDlgProc() */ /* Driver for the sample dialog box. */ /********************************************************************/ MRESULT EXPENTRY FmtDlgProc(HWND hDlg, USHORT msg, MPARAM mp1, MPARAM mp2) { HWND hEdit; struct fldfmtinfo *pFld; PFNWP pfn; PICINFO *pi; int i; switch (msg) { case WM_INITDLG : /* Get the monospaced font (Courier) */ hMyPS = WinGetPS(hDlg); SetMonospacedFont(hMyPS); /* for (i = 256; i <= 258; i++) if ((hEdit = WinWindowFromID(hDlg, i))) { pfn = WinSubclassWindow(hEdit, MonoStaticWndProc); if (!pfnOldStaticWndProc) pfnOldStaticWndProc = pfn; } */ return MRFROMSHORT(FALSE); case WM_COMMAND : switch (COMMANDMSG(&msg)->cmd) { case ID_OK : WinDismissDlg(hDlg, TRUE); return MRFROMSHORT(TRUE); default : break; } return MRFROMSHORT(TRUE); } return WinDefDlgProc(hDlg, msg, mp1, mp2); } /********************************************************************/ /* FmtWndProc() */ /* Window Proc for the new formatted edit control class. We will */ /* process some of the messages, but mostly, we rely on the */ /* standard edit control window proc to handle the hard stuff. */ /********************************************************************/ MRESULT EXPENTRY FmtWndProc(HWND hWnd, USHORT msg, MPARAM mp1, MPARAM mp2) { PSZ szFmtMask; PSZ szPicture = NULL; char buf[128], *s; char chFmt; MRESULT mr; PICINFO *pi; HPS hPS; int rc, iPos, picch, piclen; int incr; int cnt; PFORMAT pFmt; struct fldfmtinfo *pFld; switch (msg) { /* WM_CREATE We create a format information structure and associate it with this control. */ case WM_CREATE: { PCREATESTRUCT pCr; /* Get the control ID from the create-structure */ pCr = (PCREATESTRUCT) PVOIDFROMMP(mp2); /* See if we have a picture clause in the edit control's title. If so, save the picture and set the title to NULL so that the edit control will be initially empty. */ if (pCr->pszText && pCr->pszText[0]) { szPicture = pCr->pszText; pCr->pszText = NULL; } /* Let the normal edit proc do its thing. */ pfnOldEditWndProc(hWnd, msg, mp1, mp2); /* Search the list of formatted controls for this one */ /* Allocate a format structure and have the window point to it */ pFmt = (PFORMAT) malloc(sizeof(FORMAT)); WinSetWindowPtr(hWnd, 0, (PVOID) pFmt); /* There is an address passed in mp1 - this means a char mask. */ if (szPicture) { /* Set a bit to signify that we have a character mask, and copy the mask into some private storage area. */ pFmt->fFormatFlags = FLD_HASPICTURE; pFmt->szPicture = lstrcpy((PSZ) malloc(lstrlen(szPicture)+1), szPicture); /* We want to display the edit control's "protected" characters, but we don't want to display the mask characters. We copy the mask into a character array, translating the mask chars into blanks. If we have protected characters in the mask, then set a bit to signify this. */ lstrcpy((PSZ) buf, szPicture); for (s = buf; *s; s++) if (CharToPicInfo(*s)) *s = ' '; else pFmt->fFormatFlags |= FLD_MIXEDPICTURE; WinSetWindowText(hWnd, (PSZ) buf); } if (mp1) { /* We got a bitmask instead of a character mask. We search the mask table for this mask, and set the corresponding character validation function. */ struct mask_to_func *mf; pFmt->fFormatFlags |= (long) (* (PUSHORT) mp1); for (mf = MaskToFunc; mf < MaskToFunc + sizeof(MaskToFunc)/sizeof(MaskToFunc[0]); mf++) if (pFmt->fFormatFlags & mf->mask) { pFmt->pfnValidChar = mf->pfnFunc; break; } } /* Set the cursor position to the first character */ pfnOldEditWndProc(hWnd, EM_SETSEL, 0L, 0L); return MRFROMSHORT(FALSE); } /* WM_CHAR */ case WM_CHAR: if ((pFmt = (PFORMAT) WinQueryWindowPtr(hWnd, 0)) == NULL) break; if (CHARMSG(&msg)->fs & KC_CHAR) { BYTE c = (BYTE) CHARMSG(&msg)->chr; if (c = = '\t' || c = = '\n' || c = = 27) return MRFROMSHORT(FALSE); if (pFmt->szPicture) { iPos = SHORT1FROMMR(pfnOldEditWndProc(hWnd, EM_QUERYSEL, 0L, 0L)); piclen = strlen(pFmt->szPicture); /* Don't let the user type beyond the last picture char */ if (iPos >= piclen) return MRFROMSHORT(TRUE); /* Validate the character */ pi = CharToPicInfo(picch = pFmt->szPicture[iPos]); if (picch = = '!') c = ConvertToUpper(c); if (pi && pi->pfnPicFunc && (*pi->pfnPicFunc)(c) = = FALSE) return MRFROMSHORT(FormatError()); /* Implement overstrike mode by deleting the next character */ pfnOldEditWndProc(hWnd, EM_SETSEL, MPFROM2SHORT(iPos, iPos+1), 0L); pfnOldEditWndProc(hWnd, EM_CLEAR, 0L, 0L); /* Let the edit win proc handle the insertion of the character */ pfnOldEditWndProc(hWnd, WM_CHAR, mp1, mp2); /* Find out what position we are in */ iPos = SHORT1FROMMR(pfnOldEditWndProc(hWnd, EM_QUERYSEL, 0L, 0L)); advance: /* If the char typed was last one in the picture, go back to the left. */ if (iPos >= piclen) { c = VK_LEFT; mp1 = MPFROMSH2CH(KC_VIRTUALKEY, 1, 0); mp2 = MPFROM2SHORT(0, VK_LEFT); goto rightleft; } /* Figure out how many "protected" columns to skip */ cnt = 0; while (iPos < piclen && (picch = pFmt->szPicture[iPos]) && CharToPicInfo(picch) = = NULL) iPos++, cnt++; /* Skip 'em if there is a valid column to go to */ if (picch) while (cnt--) { mp1 = MPFROMSH2CH(KC_VIRTUALKEY, 1, 0); mp2 = MPFROM2SHORT(0, VK_RIGHT); pfnOldEditWndProc(hWnd, WM_CHAR, mp1, mp2); } return MRFROMSHORT(TRUE); } /* NO MASK - we just have a validation function */ else { if (pFmt->pfnValidChar && (*pFmt->pfnValidChar) (c) = = FALSE) return MRFROMSHORT(FormatError()); if (pFmt->fFormatFlags & FLD_TOUPPER) c = toupper(c); else if (pFmt->fFormatFlags & FLD_TOLOWER) c = tolower(c); } } /* end if KC_CHAR */ else if (CHARMSG(&msg)->fs & KC_VIRTUALKEY) { BYTE c = (BYTE) CHARMSG(&msg)->vkey; if (c = = VK_TAB || c = = VK_BACKTAB || c = = VK_NEWLINE || c = = VK_ESC) return MRFROMSHORT(FALSE); if (pFmt->szPicture) { iPos = SHORT1FROMMR(pfnOldEditWndProc(hWnd, EM_QUERYSEL, 0L, 0L)); piclen = strlen(pFmt->szPicture); /* Don't let the user type beyond the last picture char */ if (iPos >= piclen) return MRFROMSHORT(TRUE); switch (c) { case VK_LEFT: case VK_RIGHT: { rightleft: incr = (c = = VK_LEFT) ? -1 : 1; do { /* BUG - we don't know what rc is */ rc = SHORT1FROMMR(pfnOldEditWndProc(hWnd,WM_CHAR,mp1,mp2)); } while (rc && !CharToPicInfo(pFmt->szPicture[iPos += incr])); EditSetCursor(hWnd); return MRFROMSHORT(TRUE); } case VK_HOME: /* Pass the HOME key onto the edit control & set position to 0 */ pfnOldEditWndProc(hWnd, WM_CHAR, mp1, mp2); iPos = 0; goto advance; case VK_END: /* Pass the END key onto the edit control, then get the position */ pfnOldEditWndProc(hWnd, WM_CHAR, mp1, mp2); iPos = SHORT1FROMMR(pfnOldEditWndProc(hWnd, EM_QUERYSEL, 0L, 0L)); /* Move left until we hit a maskable character */ while (iPos >= piclen || (picch = pFmt->szPicture[iPos]) && !CharToPicInfo(picch)) { mp1 = MPFROMSH2CH(KC_VIRTUALKEY, 1, 0); mp2 = MPFROM2SHORT(0, VK_LEFT); if (!pfnOldEditWndProc(hWnd, WM_CHAR, mp1, mp2)) break; iPos--; } EditSetCursor(hWnd); return MRFROMSHORT(TRUE); case VK_DELETE: case VK_INSERT: case VK_BACKSPACE: /* If we have protected chars in this field, don't let the user delete or toggle insert mode. */ if (pFmt->fFormatFlags & FLD_MIXEDPICTURE) return MRFROMSHORT(TRUE); break; default: break; return MRFROMSHORT(FALSE); } /* end switch (c) */ } /* if picture */ } /* end if KC_VIRTUALKEY */ return MRFROMSHORT(TRUE); /* WM_SETFOCUS We intercept this to insure that the cursor is not placed over a protected mask character. */ case WM_SETFOCUS : if (SHORT1FROMMP(mp2) == FALSE) /* losing focus? */ break; /* Get the window's format structure */ if ((pFmt = (PFORMAT) WinQueryWindowPtr(hWnd, 0)) == NULL || !pFmt->szPicture) break; /* Let the normal edit proc do its thing for setting focus. */ rc = pfnOldEditWndProc(hWnd, msg, mp1, mp2); /* Get current position. Move past all protected mask chars */ iPos = SHORT1FROMMR(pfnOldEditWndProc(hWnd, EM_QUERYSEL, 0L, 0L)); while ((picch = pFmt->szPicture[iPos]) && CharToPicInfo(picch) == NULL) { /* We move past a mask character by simulating the user pressing the RIGHT arrow key. */ mp1 = MPFROMSH2CH(KC_VIRTUALKEY, 1, 0); mp2 = MPFROM2SHORT(0, VK_RIGHT); if (!pfnOldEditWndProc(hWnd, WM_CHAR, mp1, mp2)) break; iPos++; } return MRFROMSHORT(rc); /* WM_PAINT We do the drawing and shadowing ourselves. */ case WM_PAINT : { USHORT iLen; HPS hPS; char szText[128]; SWP swp; POINTL ptL; CURSORINFO cursorInfo; /* Erase the edit control */ hPS = WinBeginPaint(hWnd, NULL, NULL); GpiErase(hPS); WinEndPaint(hPS); /* Get the text and the coordinates of the control. */ iLen = WinQueryWindowText(hWnd, sizeof(szText), (PCH) szText); WinQueryWindowPos(hWnd, (PSWP) &swp); /* Draw the border around the control. */ ptL.x = swp.x; ptL.y = swp.y; GpiMove(hMyPS, (PPOINTL) &ptL); ptL.x += swp.cx; GpiLine(hMyPS, (PPOINTL) &ptL); ptL.y += swp.cy; GpiLine(hMyPS, (PPOINTL) &ptL); ptL.x -= swp.cx; GpiLine(hMyPS, (PPOINTL) &ptL); ptL.y -= swp.cy; GpiLine(hMyPS, (PPOINTL) &ptL); /* Draw the shadow */ ptL.x = swp.x + 4; ptL.y = swp.y; GpiMove(hMyPS, (PPOINTL) &ptL); ptL.x += swp.cx; ptL.y -= 4; GpiBox(hMyPS, DRO_FILL, (PPOINTL) &ptL, 0L, 0L); GpiMove(hMyPS, (PPOINTL) &ptL); ptL.x = swp.x + swp.cx; ptL.y += swp.cy; GpiBox(hMyPS, DRO_FILL, (PPOINTL) &ptL, 0L, 0L); /* Draw the edit text in a monospaced font. */ ptL.x = swp.x; ptL.y = swp.y + (swp.cy - FMCourier.lMaxBaselineExt) / 2 + FMCourier.lInternalLeading; GpiCharStringAt(hMyPS, (PPOINTL) &ptL, (LONG) iLen, (PCH) szText); EditSetCursor(hWnd); /* Return FALSE to signify that we processed the message ourselves. */ return MRFROMSHORT(FALSE); } case WM_DESTROY : if ((pFmt = (PFORMAT) WinQueryWindowPtr(hWnd, 0))) free(pFmt); break; } /* switch */ /* We got a message that we weren't interested in. Let the normal edit procedure process the message. */ return pfnOldEditWndProc(hWnd, msg, mp1, mp2); } /*********************************************************************//* EditSetCursor() */ /* Attempts to position the editing cursor correctly within a window */ /********************************************************************/ void pascal EditSetCursor(HWND hWnd) { CURSORINFO cursorInfo; SHORT iPos; /* Find out the 0-based logical position of the cursor within the edit field */ iPos = SHORT1FROMMR(pfnOldEditWndProc(hWnd, EM_QUERYSEL, 0L, 0L)); /* Get a copy of the cursor information */ WinQueryCursorInfo(HWND_DESKTOP, (PCURSORINFO) &cursorInfo); /* The cursor is placed to the right of the current character. */ WinCreateCursor(hWnd, (SHORT) (iPos+1) * FMCourier.lAveCharWidth - FMCourier.lMaxCharInc, cursorInfo.y, 0, 0, CURSOR_SETPOS, (PRECTL) NULL); } /********************************************************************/ /* CharToPicInfo() Returns a pointer to the PICINFO */ /* structure associated with character c. */ /********************************************************************/ PICINFO *CharToPicInfo(c) { PICINFO *p; c = toupper(c); for (p = PicInfo; p->chPic && p->chPic != c; p++) ; return p->chPic ? p : NULL; } /*******************************************************************/ /* VALIDATION FUNCTIONS */ /*******************************************************************/ int pascal ValidDigit(c) { return isdigit(c); } int pascal ValidDigitSignSpace(c) { return isdigit(c) || isspace(c) || c = = '+' || c = = '-'; } int pascal ValidAlpha(c) { return isalpha(c); } int pascal ValidAlphaNum(c) { return isalnum(c); } int pascal ValidLogical(c) { return strchr("TtFfYyNn", c) != NULL; } int pascal ValidAny(c) { return TRUE; } int pascal ConvertToUpper(c) { return toupper(c); } /********************************************************************/ /* MonoStaticWndProc() */ /* Used to "front-end" the normal behavior of a static text control */ /* so that we could print it in a mono-spaced font. */ /********************************************************************/ MRESULT EXPENTRY MonoStaticWndProc(HWND hWnd,USHORT msg, MPARAM mp1,MPARAM mp2) { HPS hPS; POINTL ptL; SHORT iLen; SWP swp; char szText[128]; switch (msg) { case WM_PAINT : /* Call Begin/EndPaint in order to satisfy PM. */ hPS = WinBeginPaint(hWnd, NULL, NULL); WinEndPaint(hPS); /* Get the text of the static control and its position */ iLen = WinQueryWindowText(hWnd, sizeof(szText), (PCH) szText); WinQueryWindowPos(hWnd, (PSWP) &swp); /* Write the string using our own presentation space */ ptL.x = swp.x; ptL.y = swp.y; GpiCharStringAt(hMyPS, (PPOINTL) &ptL, (LONG) iLen, (PCH) szText); /* Return FALSE if we processed the message ourselves */ return MRFROMSHORT(FALSE); } return pfnOldStaticWndProc(hWnd, msg, mp1, mp2); } /********************************************************************/ /* SetMonospacedFont() */ /* Sets the font of the current presentation space to a mono-spaced */ /* font. Returns TRUE if we found the font, FALSE if not. */ /********************************************************************/ SetMonospacedFont(hPS) HPS hPS; { FONTMETRICS *pMetrics; LONG nFonts, nRequestFonts; int i; LONG rcMatch; ERRORID errID; PSZ szFont = "Courier"; static BOOL bFirstTime = TRUE; /* Load in the Courier font */ if (bFirstTime) { GpiLoadFonts(hAB, (PSZ) "C:\\os2\\dll\\courier.fon"); bFirstTime = FALSE; } /* Make a dummy call to GpiQueryFonts() to find out how many courier fonts are available. */ nRequestFonts = 0L; nFonts = GpiQueryFonts(hPS, QF_PUBLIC | QF_PRIVATE, (PSZ) szFont, &nRequestFonts, 0L, NULL); if (nFonts = = 0) return FALSE; /* Allocate temp space to hold the font-metrics info */ if ((pMetrics = malloc((unsigned int) nFonts * sizeof(FONTMETRICS))) = = NULL) return FALSE; /* Get the font-metric info for all Courier fonts */ GpiQueryFonts(hPS, QF_PUBLIC | QF_PRIVATE, (PSZ) szFont, &nFonts, (LONG) sizeof(FONTMETRICS), pMetrics); for (i = 0; i < nFonts; i++) { /* Set up the FontAttrs structure in preparation for GpiCreateLogFont() */ FontAttrs.usRecordLength = sizeof(FontAttrs); FontAttrs.fsSelection = pMetrics[i].fsSelection; FontAttrs.lMatch = pMetrics[i].lMatch; strcpy(FontAttrs.szFacename, szFont); FontAttrs.idRegistry = pMetrics[i].idRegistry; FontAttrs.usCodePage = 850; FontAttrs.lMaxBaselineExt = pMetrics[i].lMaxBaselineExt; FontAttrs.lAveCharWidth = pMetrics[i].lAveCharWidth; FontAttrs.fsType = FATTR_TYPE_FIXED; FontAttrs.fsFontUse = 0; /* Given the above attributes, try to find the best match in a Courier font */ rcMatch = GpiCreateLogFont(hPS, (PSTR8) szFont, (LONG) ++IDCourier, (PFATTRS) &FontAttrs); /* We got a match if rcMatch is 2 */ if (rcMatch == 2) { GpiSetCharSet(hPS, IDCourier); GpiQueryFontMetrics(hPS, (long) sizeof(FONTMETRICS), &FMCourier); /* If the matched font is 16x9, then let's use it. */ if (FMCourier.lAveCharWidth = = 9L && FMCourier.lMaxBaselineExt >= 16L) break; } } free(pMetrics); return (i < nFonts); } /********************************************************************/ /* FormatError() */ /* Sounds a little beep when the user presses a bad key. */ /********************************************************************/ FormatError() { WinAlarm(HWND_DESKTOP, WA_WARNING); return TRUE; } /********************************************************************/ /* lstrlen(), lstrcpy() */ /* Miscellaneous far-pointer routines */ /********************************************************************/ int lstrlen(PSZ s) { int len = 0; while (*s++) len++; return len; } PSZ lstrcpy(PSZ s, PSZ t) { PSZ orig_s = s; while (*s++ = *t++) ; return orig_s; } /********************************************************************/ /* DEBUG() */ /* Simple diagnostic routine which displays a message box. */ /********************************************************************/ char szDebug[82]; DEBUG(char *fmt, ...) { va_list arg; va_start(arg, fmt); vsprintf(szDebug, fmt, arg); /* format the message */ va_end(arg); SimpleMessage(hWndClient, szDebug); /* put it in a message box */ } Figure 2 #define FLD_HASPICTURE 0x0001L #define FLD_AUTONEXT 0x0002L /* Go to next field when all filled */ #define FLD_NOECHO 0x0004L /* Don't echo the chars (for passwds) */ #define FLD_PROTECT 0x0008L /* Can't enter data into this field */ #define FLD_IGNORE 0x0010L /* The cursor skips over this field */ #define FLD_REQUIRED 0x0020L /* User MUST enter data in this field */ #define FLD_TOUPPER 0x0040L /* Convert characters to uppercase */ #define FLD_TOLOWER 0x0080L /* Convert characters to lowercase */ #define FLD_CENTER 0x0100L /* Center the data in the field */ #define FLD_RJUST 0x0200L /* Right-justify the data in the field */ #define FLD_NUMERIC 0x0400L #define FLD_SIGNEDNUMERIC 0x0800L #define FLD_ALPHA 0x1000L #define FLD_ALPHANUMERIC 0x2000L #define FLD_LOGICAL 0x4000L #define FLD_MIXEDPICTURE 0x10000L Figure 3 A Alphabetic (a-z, A-Z) 9 Decimal digit # Digit, sign, or blank N Alphanumeric L Logical (T,t,F,f,Y,y,N,n) X Any character ! Convert char to upper case Figure 4 EM_QUERYFIRSTCHAR (PM) Return the offset to the first character visible at the left edge of the control window. EM_SETFIRSTCHAR (PM) Scrolls the text so that the character at the specified offset is the first character visible at the left edge of the control window. Returns TRUE if successful, or FALSE if it is not. EM_QUERYCHANGED (PM) Returns TRUE if the text has changed since the EM_GETMODIFY (Win) last EM_QUERYCHANGED message. EM_QUERYSEL (PM) Returns a long word that contains the offsets for EM_GETSEL (Win) the first and last characters of the current selection in the control window. EM_SETSEL (PM) Sets the current selection to the specified EM_SETSEL (Win) character offsets. EM_SETTEXTLIMIT (PM) Allocates memory from the control heap for the specified maximum number EM_LIMITTEXT (Win) of characters, returning TRUE if it is successful, FALSE if it is not. The following messages exist only in Windows: EM_CANUNDO Queries an edit control to see if it can respond to the EM_UNDO message. EM_CLEAR Clears characters in the selected area of the edit control. EM_FMTLINES Adds or removes end-of-line characters from wordwrapped lines. EM_GETHANDLE Retrieves the local handle of the edit buffer. EM_GETLINE Copies a line from the edit control. EM_GETLINECOUNT Retrieves the number of lines of text in the control. EM_GETRECT Retrieves the formatting rectangle of an edit control. EM_LINEFROMCHAR Returns the number of the line which contains the ith character. EM_LINEINDEX Returns the index of a line. EM_LINELENGTH Returns the number of characters in a line. EM_LINESCROLL Scrolls the control by a certain number of lines. EM_REPLACESEL Replaces the currently selected text with new text. EM_SCROLL Does scrollbar-like scrolling of an edit control. EM_SETHANDLE Establishes the text buffer which will be used by an edit control. EM_SETMODIFY Sets the "data modified" flag of an edit control. EM_SETRECT Sets the formatting rectangle of an edit control. EM_SETRECTNP Sets the formatting rectangle of an edit control without repainting. EM_SETWORDBREAK Supplies a new wordbreak function to the edit control. EM_UNDO Undoes the last edit. Techniques for Tuning and Optimization of PM and Windows Applications Kevin P. Welch Something of a renaissance in the art of program tuning and optimization has occurred during the last few years. The sophistication of recently introduced environments has caused developers to rethink many techniques and procedures typically used to refine applications. The Microsoft Windows and OS/2 Presentation Manager environments, for example, are two of the most challenging environments ever created in which to develop software for personal computers. Part of the challenge of developing in these environments is to meet or exceed user expectations. In the past, pleasing users was considerably easier because their expectations were lower, interfaces were less complicated, and programs were generally smaller. Today, users expect highly refined, full-featured applications that perform flawlessly at maximum speed in a variety of situations. Developers are meeting those expectations with improved optimization and tuning techniques. This article describes techniques I have used with measured success in structuring and reshaping Windows1 and OS/2 Presentation Manager (hereafter "PM") applications. Although many of these techniques are not new, using them together is a more powerful optimizing and tuning approach. Note, however, that these techniques are designed to complement, not replace, good software development. Because the basic issue is management, applying these techniques is an art, not a highly quantified science. Furthermore, these techniques are most applicable to large, moderately well-structured applications. They are of little benefit to small or very poorly designed applications. A Good Application Before you can optimize a program, you must have a clear understanding of the characteristics of a "good" Windows or PM application: sound software design principles, effective memory management, and well-managed code. Using sound software design principles is probably the most determining element in developing a successful application. Almost without exception, well-written Windows and PM applications are built from the bottom up, using layered construction techniques that incorporate large numbers of highly modular, self-contained objects that have few interdependencies. Furthermore, these techniques are usually supplemented with comprehensive product specifications, implementation plans, and internal software documentation standards. Next to good design, effective memory management is probably the most significant development issue facing Windows and PM programmers. Although most applications require at least a certain amount of memory, some require prodigious quantities of it--further increasing the need for good memory management techniques. Generally, well-written applications are built on the assumption that memory is an extremely precious and limited system resource that must be shared among competing processes. As a result, they typically implement their own virtual memory management scheme and use system resources sparingly. Whenever possible, these applications also tend to use a small number of larger, movable global memory blocks instead of a large number of small ones. They always clean up after themselves, and ensure that all unwanted memory fragments and resources are purged from the system. Finally, they attempt to minimize the amount of memory restructuring required when the user performs particular operations. Good code management is another extremely significant characteristic of outstanding Windows and PM applications. Almost all large applications are well-organized; they group collections of related functions into segments, each less than 4Kb in size. These applications also tend to use the C run-time library sparingly. Before each library function is used, the associated memory, object, and support code ramifications are considered. Finally, the Windows or PM API is used as efficiently as possible by avoiding redundant operations and needless message processing. Often lower-level calls are substituted for higher-level, more generalized ones. Methodology We now explore three techniques: review, analysis, and optimization. Where appropriate, specific examples of the principles involved are included. Many of these techniques are general and tend to be useful only when applied to large applications, and they assume a solid understanding of Windows and PM programming. Review Sometimes serious development errors can be identified by a skilled observer after a few minutes reviewing the application's source code. Although some problems result from an insufficient or improper understanding of the environment on the part of the designers, many can be traced to a poor overall structure--something that plagues programs in any operating environment. Because the typical Windows or PM application contains a large percentage of code related to the user interface, special attention should be given to the hierarchy and ownership of windows and the message passing scheme used to coordinate their activities. Many times applications inadvertently use message-passing schemes that result in reentrancy, which can easily create problems much later in the development cycle. Commercial tools do exist that can ease the code review process. Perhaps the simplest are those utilities that generate cross-reference listings for the entire application. The reports generated by such programs make it considerably easier to trace the use of specific functions or variables. Because the central problem in optimizing and tuning Windows and PM applications is complexity, software metrics are useful. Software metrics enable you to understand the general characteristics of a piece of software in quantitative terms. They are perhaps the best source-code based mechanism for analyzing large programs. Software metrics clearly identify areas of complexity and enable you to allocate review, analysis, and testing resources effectively. The most commonly used software metric expresses the size of a function or an application in lines of noncommented source code. Although this metric is sufficient in certain situations, it is often simplistic when used with Windows or PM applications. In the late 1970s and early 1980s computer scientists started searching for more precise methods for quantifying software complexity. Their goal was to develop a set of metrics that would quantitatively describe the complexity of a piece of source code. The result was the development of several useful new metrics that can be helpful to a person when trying to understand and refine program operation. Today 15 metrics, some statistical in nature, can be used to describe a piece of source code (see Figure 1). Although a complete discussion of these metrics is beyond the scope of this article, a few are particularly interesting and can be of great help when reviewing a large amount of source code. The program volume (V) metric can be used as an alternative to lines of source code (LOC) when describing program size. It is based on the assumption that if a program has n unique operators and operands then it would take approximately log2(n) statements to represent them uniquely. If there are N total usages of these operators and operands, then the number of statements necessary to represent the program would be: V = N * log2(n) Because this metric is closely tied to the number of mental lookups required to read and understand a particular program fully, it can be statistically related to the number of errors that might be present in the work. Independent research by psychologists and several empirical studies have demonstrated, with some degree of confidence, that the resulting statistic can be a useful starting point when allocating testing resources to a specific project. The software science effort (E) metric reflects the number of mental discriminations that a programmer must perform to write a particular program. Studies show that, in addition to being a good metric for describing the complexity of a piece of source code, this metric can also be used to estimate the amount of time needed to write the program. Although the resulting estimates need to be carefully calibrated, using the characteristics of the environment and the programmer in question, they can be quite useful. The cyclomatic complexity (VG1) metric is a popular metric that attempts to quantify the flow of control in a program, based on the understanding that the more decision points in an application, the more complex it will be. Apart from being a fairly good estimate of complexity, this metric can be calculated easily from the source code by counting decision-making statements. During the spring of 1988, I used software metrics to assist in analyzing and optimizing Scrapbook+ (a product developed by David West and myself). Although we were intimately familiar with the product's structure, we felt that software metrics would give insight into the global workings of our program. We used an analysis tool called PC-METRIC to process each of the related C source code files and generate the base statistics. Once the data was collected, we created a huge series of linked Microsoft Excel spreadsheets that enabled us to interpret and summarize graphically the resulting data by function, module, and application. Figure 2 shows the basic metrics that we collected from the analysis of each source code module in Scrapbook+. Based on this master spreadsheet, we created a series of charts that show many of the significant values. Figure 3 displays the number of lines (noncommented) of source code, by module, for the entire application. As you might expect, this graph correlates somewhat to the software volume shown in Figure 4. The analysis becomes a little more interesting, however, when we compare Figure 3 with Figures 5 and 6. The extended cyclomatic complexity graph, Figure 5, is more evenly distributed, taking into account the many short but complex modules in the application. The software science effort graph, Figure 6, clearly demonstrates that the bulk of the development effort (and thus anticipated errors) went into creating SCRAP01.C, SCRAP22.C, and SCRAP25.C. These graphics helped us a great deal, especially considering that SCRAP01.C contained the main window message processing function, SCRAP22.C the initialization and cleanup related functions, and SCRAP25.C the database and data formatting functions. Using the metrics we had generated and our internal programming error rate, we also statistically calculated the number of errors we should expect when beta testing the product (see Figure 7). This value, which turned out to be surprisingly accurate, was of great help when deciding to end testing and make the product commercially available. Analysis After you have thoroughly reviewed the source code, you can begin more specific tests to determine areas for improvement. One test commonly overlooked is running the program in the same or similar conditions as those of your potential customers. The working environment of many Windows and PM programmers is often dramatically different from that of the average software user. While analyzing your application, use a variety of hardware, display adapters, and printers. Deliberately run the application while using other programs and in low memory or out-of-disk-space situations. Throughout this process, record all your observations, even to the extent of timing various critical operations. These timings are especially valuable as they quantify the changes you make, both good and bad. In most cases you will find that tuning involves trade-offs, sometimes improving performance in one area and degrading it in others. Whenever possible, use additional software tools to assist you in this evaluation. For example, in Windows you can use Spy to examine the window hierarchy used by an application and watch the various messages exchanged between windows. You should also run the program with Shaker to see how it performs when segments are being moved rapidly. You can use HeapWalk to investigate how code and data segments associated with the application are loaded and discarded. Ideally, almost all segments should be slightly less than 4Kb in size, be marked movable and discardable, and have a lock count of zero. By comparing the MAP generated when the program was linked with the information displayed by HeapWalk, you can get a good idea of the "working set" of code required to perform a particular operation. In addition, look for segments whose lock count never returns to zero after use and for GDI objects that are never discarded. Resource X-Ray by Eikon Systems, Inc (see Figure 8) is a tool that lets you open an application and examine each of its associated resources. You can look at each cursor, icon, menu, dialog box, font, string table, and accelerator. Many times you can find improperly defined or unused resources that would be difficult to identify any other way. To look closer at a particular resource, you can copy it to the clipboard and paste it into one of the resource editors provided with the Windows Software Development Kit. After you complete an external analysis of the application, you can proceed to a more detailed internal analysis. By now you should know where the application could be improved. Although you can accomplish internal analysis in a number of ways, the goal is to understand clearly how the application uses the API provided by the environment and its flow of execution. The best way to understand how the system API is used is to create your own debugging versions of critical system calls. These versions, in turn, can output debugging information before calling the function provided by the system. This is an excellent way to examine each parameter and its frequency of use--information that is especially useful when functions with considerable overhead are used. To profile the sequence of function calls used inside the application you can use the Microsoft CodeView debugger or insert your own debugging calls at the beginning and end of each block of code. You can make these debugging calls with a debugging utility such as the one described in the article "Debug Microsoft Windows Programs More Effectively with a Simple Utility," MSJ (Vol. 3, No. 3). Using these debugging calls, you can create a directed graph (see Figure 9) that summarizes the flow of control through the application for a specific operation or command. Each node in the graph should represent a specific function and each arrow should represent a call from one function to another. If multiple calls are made to the same function, you should indicate how many times the call was made during the profile you are creating, and note on the graph the current segment assignment and size in bytes of each function. Repeat this process in a variety of situations, until you have a series of directed graphs that summarize the behavior of the application in each circumstance. Although this data gathering can be tedious, it is extremely valuable when optimizing the program. Figure 9 illustrates two such directed graphs, each profiling the flow of control resulting from a particular operation in a simple application. Note especially the reentrancy involved in each operation and the current segment assignment of each function. Optimization After reviewing your source code and analyzing its operation, where and how your application can be improved should be apparent. Optimization is probably the most critical, least understood, and most time-consuming step. In some situations be prepared to stop, accept the fact that your program can no longer be improved, and start rewriting it immediately. Although this is a rather drastic form of optimization, rewrites almost always turn out much better than the previous version and often require only a fraction of the original time to develop. Generally, your application will be more than acceptable in certain situations and less than acceptable in others. You must decide somewhat arbitrarily what is most important for your users and begin to adjust accordingly. Approach optimization on two levels; on a line-by-line basis throughout the source code, and on a functional or structural level taking into account the entire application. You are probably familiar with optimizing individual statements and functions. Usually you verify that each function is based on a good algorithm (not too simple yet not unnecessarily complex) and that it is well implemented and thoroughly tested. In unacceptable cases, you may need to rewrite one or two functions, perhaps using Assembler. To a certain extent, you can use your compiler and linker to improve further the performance of your application. Although compiler optimization techniques, such as loop optimization and pointer aliasing, can result in dramatic improvements, they are not a substitute for good design and implementation. In addition, before using such compilation options, verify that your code is valid under lower levels of optimization. Only then should you activate additional optimization options. Finally, always carefully test the optimized code, being prepared to disable optimization for particular modules when problems arise. Optimizing on a structural level assumes the presence of a collection of well-written functions, to be arranged into carefully related segments called working sets. By carefully restructuring your application into carefully chosen working sets, you can minimize the number of intersegment function calls, thus reducing the total amount of memory required by the application. To do this restructuring, you need the information collected in the review and analysis steps. Because the ideal Windows or PM application uses a minimal amount of memory and contains many small, interrelated code segments, each slightly less than 4Kb in size, you should avoid large data segments that are fixed for extended lengths of time or several code segments that are repetitively loaded and discarded. Unfortunately, determining an optimal arrangement for a large number of such code and data segments is difficult if not impossible. Using the directed graphs you created in the analysis phase, you can create a mathematically blended model that summarizes the flow of control throughout your application. In this blending, you may choose to ignore certain aspects of the program and focus on areas that are of more concern to your users. Figure 10 demonstrates the blending of the two operational profiles described in Figure 9 into one directed graph. In this illustration, the two profiles were considered equally important, and no mathematical averaging was performed. If, for example, the first operational profile in Figure 9 was considered twice as important as the second profile, we would double each of the function call values in the first profile before combining them. Additionally, we could ignore relatively unimportant sections of the profile. Once the blended operational profile has been created, rearrange each of the functions into segments such that the number of intersegment calls is minimized. This rearrangement should be limited in size to less than 4Kb. The resulting segment assignment in Figure 10 eliminates those previously more than 4Kb and reduces the total number of intersegment references from 41 to 13. Although your application will, in all probability, be considerably more complex than this example, the same principles apply. You could also use relatively conventional nonlinear optimization techniques, perhaps using the duality of the original problem. In most cases, however, you will probably want to do the work by hand. This enables you to minimize the amount of restructuring required and group related functions into the same segment whenever appropriate. When you have determined a collection of working sets that minimize intersegment references, you can start rearranging your source code. While doing this, be sure to include the code segment that contains the run time library functions. You may need to replace some of these functions with your own or perhaps purchase the run-time library source code and recompile it into several segments. While we were developing Scrapbook+, we were made aware of this issue when we discovered that the entire Microsoft C floating-point library was linked into our product. Throughout development we had carefully avoided using any floating point arithmetic and were mystified at the inclusion of this extra support code. After a lengthy search, we discovered a single assignment statement (out of approximately 50,000 lines) defining the product version as 1.00. Needless to say, we took this statement out and eliminated the need for the entire floating-point library--dramatically reducing the size of our _TEXT segment. After restructuring and recompiling your application, be sure to test for regression errors. Once you have verified that the program has survived this rearrangement, you can perform various timings to assess the benefits of the changes you made. You will probably find that certain areas need additional optimization or possibly even rewriting. Conclusion Optimizing a Windows or PM application can involve considerable work. Ideally, all our applications would be perfectly designed and use the environment optimally. Unfortunately, in the real world it is difficult, if not impossible, to achieve this. Consequently, the job of application tuning and optimization will probably be with us for some time to come. In the future it is likely that many new and powerful tools will become available to assist you with this process; but for now, I hope the ideas in this article will help. Figure 1 n1 # of unique operators n2 # of unique operands N1 total # of operators N2 total # of operands n program vocabulary (n1+n2) N "length" of program (N1+N2) N^ predicted length of program P/R software purity ration V program volume E software science effort VG1 cyclomatic complexity VG2 extended cyclomatic complexity LOC lines of source code <;> executable semicolons SP span of reference for variables A Survey of Windows and Presentation Manager Prototyping Tools Marc Adler Prototyping tools and application generators are programs that can not only relieve some of the tedium of writing code but that can also automatically generate a substantial amount of it. This article examines several prototyping tools and application generators for the Microsoft Windows environment and OS/2 Presentation Manager (hereafter "PM"): CASE:W and CASE:PM from CASEWORKS, Winpro/2 and Winpro/PM from Xian Corporation, and WAPE from Intersoft. The discussion focuses on the ways in which the programs interact with the user, how each program generates code, and the quality of the generated code. Prototyping tools have been most often associated with database programs. Users draw data entry forms on the screen, and then the prototyping tool generates all the code to create the form, display it on the screen, have it interact with the user, validate the data, and return the data to the database. With more ambitious application generators, you can put together a powerful application with pull-down menus and linked forms, among other features. Probably the most important use of application generators is to assist an inexperienced programmer or a nontechnical user in assembling working code. These are people who want to put together an application as quickly as possible without having to learn a language. Another use for prototyping tools is to aid programmers unfamiliar with Windows1 and PM. Many programmers are finding it difficult to adjust to a message-passing model from a sequential style of programming. Prototyping tools can help them become more accustomed to object-oriented programming. CASE:W and CASE:PM CASE:W comprises an inference engine and a knowledge base of Windows programming techniques. It breaks the program design phase into four stages: Program Configuration, Main Window, Menus, and Client Area. You can access each from the menu bar item called Design. When you begin to write your application, you need to select the Program Configuration option to choose your compiler options (including memory model, optimization, stack size, heap size, CodeView debugger support, and so on) and the appropriate libraries with which to link. You also define the options you want to associate with your code and data segments (for example, MOVEABLE, DISCARDABLE, and so on), and you determine the routines that your application will import and export. CASE:W uses this information when it generates your make file and the application's DEF file. The second stage will most likely be the design of the application's main window. First, the main window's caption is defined. In CASE:W you can click on an object if you want to modify it. In this case, with the mouse cursor over the main window's title bar, you would click the left button; CASE:W brings up the dialog box, which prompts you for the new title. You can choose the icon, cursor, and font that the application will use. For the icon and cursor, CASE:W presents you with the same kind of file-chooser dialog box found in so many Windows applications. You can select the appropriate ICO or CUR resource. The font dialog box gives you a choice of fonts. To finish designing the main window, you can select its foreground and background colors and its initial size and location. Unfortunately, CASE:W has no way to define the frequently used accelerator and string table resources, and the generated code does not include statements to load them. The next stage of design is the layout of the menu system. CASE:W includes a simple menu editor for building most of your menu system; unfortunately, it lacks enough features to make it worthwhile to use. For instance, you cannot delete a pull-down menu item once it has been defined, and you cannot associate style attributes with a pull-down menu item (such as CHECKED, GRAYED, or SEPARATOR). Using the menu editor, you can append, insert, delete, and move menu bar items. When you define a menu item, you can tell CASE:W to generate code that invokes a modal or modeless dialog box when that item is selected, or you can tell CASE:W that some user-defined code will be executed when the item is selected. To generate code that invokes a modal or modeless dialog box, you choose a dialog box from a list of those currently available in your application. If you haven't defined the dialog box you want yet, CASE:W will let you bring up the Microsoft dialog box editor, DIALOG.EXE. After you select a dialog box, CASE:W generates a skeleton dialog box procedure for it. CASE:W also generates the call to invoke that dialog box when its corresponding menu item is selected. To execute user-defined code when that item is selected, CASE:W creates a placemarker where you can insert your own code. Between regenerations of the application code, CASE:W can distinguish between your code and the code that it generates itself, as long as you place your code after these placemarkers. The final design stage is to define the code that is associated with any dialog boxes not linked to the menu system. CASE:W generates the dialog box procedure skeletons for the dialog boxes that you select; however, you must write the code to invoke the dialog box yourself. Now that the difficult part of designing your application is finished, you can tell CASE:W to generate the code. CASE:W will generate the appropriate make file, the DEF definition file, the C source code, and the RC file. After the code is generated, if you are running the Windows/386 program, you can invoke MAKE from within CASE:W to compile and link your application. If you are running the Windows/286 program, you must exit Windows to compile the program. You can then run your application to see how it works. To add some more code or do a little tweaking, CASE:W lets you bring up an editor (or Microsoft's Notepad) on the generated C file. After you finish your editing, you can regenerate the code, recompile, and test-run your application again. CASE:W (see Figure 1) lets you access whatever Windows development tools you need during your application building session. By default, CASE:W gives you access to the Microsoft dialog editor, the icon editor, and the font editor. You can define other tools you want to invoke. CASE:W uses an ASCII file called REGEN.C that contains the skeleton of the C file to be generated. This skeleton contains #include definitions, function prototypes, a WinMain, a WndProc for the main window, and dialog box procedure skeletons. The WinMain function handles the tasks that a WinMain program normally handles, such as the class registration, creation of color brushes, displaying the main window, and the message loop. WndProc primarily handles the WM_COMMAND messages that are sent to a window procedure by Windows whenever a menu item is selected. CASE:W generates a large switch statement to handle the code for each menu item in the application. If a menu item has a dialog box associated with it, the call to invoke that dialog box is generated. WndProc also sets the main window's color by processing the WM_CREATE message and calling SetTextColor, CreatePen, and SelectObject. For every dialog box in the application, CASE:W generates a dialog box procedure called xxxMsgProc, where xxx is the name of the dialog box. Code is generated to dismiss the dialog box whenever an OK or CANCEL push button is pressed. CASE:W claims to read the DLG file that the dialog editor creates and to generate case statements based on the control IDs of each control window. However, when I tried this on a simple file-open dialog box that contained check boxes, list boxes, and edit fields, no code was generated for those controls. The only code that was generated was the case statement to process the OK and CANCEL buttons. For this operation, Winpro/2 (see below) really excels--it generates intelligent code for most controls in a dialog box. CASE:W can certainly assist the beginning Windows programmer to create an application, but the experienced programmer can also use it to generate a working skeleton of an application and save a few hours. CASE:PM (see Figure 2) functions in Presentation Manager the same way CASE:W does in Windows. Once you are familiar with CASE:W, using CASE:PM is easy--the only difference is the code generated. WINPRO/2 Winpro/2 is another Windows code generator. Winpro/2 and CASE:W produce the same kind of code for a Windows application, but unlike CASE:W, Winpro/2 is not actually a Windows program. It is a translator--that is, Winpro/2 takes a resource (RC) file and turns out a full Windows program from it. To invoke Winpro/2, just use the name of your RC file. For example, if I had an RC file called SAMPLE.RC, I would invoke Winpro/2 thus: wpr2 sample The hard part is that you still have to generate the RC file using the Windows dialog editor and a lot of hand coding. Here, CASE:W excels by providing easy access to the various Windows tools together with its own menu editor. Using Winpro/2(see Figure 3), you only need to write an RC file; no header (H) files are necessary, because Winpro/2 generates them. Winpro/2 imposes certain naming conventions and restrictions on your RC file. You must define a string table with the identifier IDS_NAME in it. Winpro/2 uses this identifier to generate the names of the resulting C files and the functions within the files. For instance, if I had the lines STRINGTABLE BEGIN IDS_NAME "Sample" END in my RC file, Winpro/2 would use the word "Sample" as the prefix for the names of the functions it generates. The first four characters are also used as the first four characters of the names of the generated source files. If you have the entry IDS_TITLE in your string table, Winpro/2 will use the definition as the caption of the main window. IDS_TITLE "My first application" The next step in creating the RC file is the definition of your menu system. Winpro/2 allows a single MENU statement within the RC file, which is sufficient for most applications. You must identify the MENU with the same name as the one found in the IDS_NAME string. Also, all of the identifiers that are assigned to the MENUITEM items must be symbolic, not numeric, such as MENUITEM "&Paste," ID_PASTE. When Winpro/2 generates the code, it takes these symbolic identifiers and automatically generates #define directives for them. If you define a menu item with the identifier IDEXIT, Winpro/2 generates code to exit your application when the user selects that menu item. For each MENUITEM, a case statement is generated that calls a separate function corresponding to that item. In the generated function, a call will be made to invoke a dialog box if the menu item has the same symbolic identifier as the dialog box. If the menu item does not invoke a dialog box, code to display a simple message box will be generated. The next step is to define the icon and the accelerator table that your application will use. In the generated WinMain, Winpro/2 creates the code to load the icon and the accelerator table. The final step in creating your RC file is the definition of the dialog boxes. You use the dialog editor to create them and then read the generated DLG file into your RC file. You then edit each dialog definition to assign the appropriate symbolic identifiers. If one of your MENUITEMs invokes a dialog box, the dialog box should be given the same symbolic identifier as that MENUITEM. For example, here is part of a resource file that defines a file-open dialog box invoked by a menu item: Example2 MENU BEGIN POPUP "&File" BEGIN MENUITEM "&Open...", midFILEOPEN ■ ■ ■ END ■ ■ ■ END midFILEOPEN DIALOG 8, 20, 148, 102 STYLE WS_DLGFRAME | WS_POPUP BEGIN .... dialog box controls defined here END Like CASE:W, Winpro/2 can generate code for unlinked dialog boxes (those not invoked from a menu item). Winpro/2 will generate the dialog box procedure, but the programmer must code the call that invokes it. Generating code for dialog box procedures is where Winpro/2 really shines. The dialog box procedure name is generated by placing the dialog box's symbolic ID before the DlgWndProc string. If my dialog box has the identifier ID_PRINT, a dialog box function called ID_PRINTDlgWndProc is generated. Acceptable code is generated for the following types of controls: ■ check boxes ■ auto check boxes ■ 3-state boxes ■ auto 3-state boxes ■ radio buttons ■ edit fields Winpro/2 recognizes the special identifiers IDOK and IDCANCEL as IDs of buttons that are used to dismiss a dialog box. If these are found, Winpro/2 generates the code to transfer the data from the dialog box controls to variables within your program (in the case of IDOK) and also generates code to dismiss the dialog box. For each dialog box control, Winpro/2 generates a global variable that will be used to hold the data associated with it. For instance, if you define an edit field in a dialog box, Winpro/2 declares a global character array that will be used to transfer the data to and from that edit field. For check boxes, Winpro/2 will declare an integer variable. For radio buttons, an integer variable will be declared that holds the identifier of the selected radio button within the radio group. Winpro/2 generates code that responds to the WM_INITDLG message sent to your dialog box procedure immediately before the dialog box is displayed. The generated code will transfer all of the data between the generated global variables and the dialog box controls. For radio buttons, code is generated to check the appropriate button. Winpro/2 generates reasonable code to respond to WM_COMMAND messages. These messages are sent to a dialog box procedure by Windows when certain events happen, such as the selection of a radio button or a check box, the press of a push button, or a change in an edit field. In the case of a check box, code is generated to toggle the state of the check box between on and off. For radio buttons, code is generated to highlight the selected button and unhighlight the other buttons within the radio group. For edit controls, no real code is generated. An IF statement that tests for an EN_KILLFOCUS message (received by the dialog box procedure when an edit control loses the input focus) is, however, generated with a comment to insert any user-defined data validation code at that point. As mentioned above, Winpro/2 will generate code to transfer the contents of the dialog box controls back to the global variables if it detects that you have defined a push button with the identifier IDOK. Winpro/2 generates several files from the RC file, including a make file, a linker response file, and a DEF file that contains EXPORTs for all of the dialog box procedures. Two include files are generated, one containing the automatically generated #define directive for the RC file and the other containing the generated global variables that correspond to the dialog box controls. Five C files are generated: a DISCARDABLE initialization file, a file that declares the global variables, a DISCARDABLE/LOADONCALL file that contains the menu processing code, a DISCARDABLE file that contains the dialog box procedures, and a file containing the WinMain function. The file xxxxMAIN.C (where xxxx is the first four letters of your application name) contains WinMain, a routine to paint your main window with the default colors, a large switch statement to handle all of the WM_COMMANDs that come from selecting a menu item, a routine that is called when your application terminates (which asks you if you really want to terminate it), and the main window's WinProc. The WinProc has code to handle WM_ERASEBKGND, and WM_CLOSE followed by the WM_QUERYENDSESSION, WM_ENDSESSION, and WM_DESTROY messages. The file xxxxINIT.C contains routines to load in the string table, register the main window class, initialize the default brushes, and create and display the main window. The file xxxxMENU.C contains one function for each menu item defined. If a menu item does not call a dialog box, code is generated to display a simple message box that contains the function's name. If a menu item does call a dialog box, code to invoke the dialog box is generated. The heart of the program is the generated xxxxDLG.C file, which contains all of the dialog box processing and is where you will change the most code. Unfortunately, unlike CASE:W, Winpro/2 does not seem to maintain any kind of regeneration points within the generated code. Therefore, if you modify the generated C files, do not expect your code to be retained if you tell Winpro/2 to regenerate your C files. If you write a substantial amount of code and decide later that your application needs several more dialog boxes, you will have to generate the dialog procedures yourself. Winpro/PM is a Xian product for PM that is similar to Winpro/2 (see Figure 4). The processing in Winpro/PM is similar to that in Winpro/2, except the generated C files contain PM code. The code that Winpro/2 generates is good and complete. WAPE WAPE (Windows Application Programming Environment) is from Intersoft Corporation. It has a small API that combines several Windows API functions. WAPE has a help system and performs four services: maintaining projects (a "project" is simply a group of files), editing source files, generating code, and supporting other Windows application development tools (such as the dialog editor, font editor, and so on). When you start a WAPE session, you can use the admin service to create or open a project. The admin service also lets you delete files from projects and copy files among projects. After you select a file from a project, you can work on it with the WAPE editor. The editor is fairly primitive, lacking features such as regular expressions, brace matching, and macros; it has basically the same capabilities as the Windows multiline edit class (cut and paste, simple undo, and cursor movement). The WAPE editor can search and replace and lets you jump to the beginning of a file, the end of a file, and to a particular line number. WAPE can import existing code templates into your file. Intersoft has templates for the three functions every Windows programmer must code eventually--the WinMain procedure, a generic window procedure, and a generic dialog procedure. You can modify these imported templates to suit your purposes. The most interesting aspect of WAPE is the code generation process, which is actually an online help system. Intersoft has documented each function of the Windows API, categorized it, and organized access to it from the main menu. When you ask for help for a certain function, you choose the various arguments for that function from a dialog box. If the argument must be chosen from among several predefined values, you can choose from a list of the possible arguments. You can also get help on any argument. After you choose the arguments to the function, the code for that function call is inserted into your source file. This feature basically eliminates the need for the Windows reference manual. You can get this assistance for the WAPE API as well as for the Windows API. The WAPE API consists of 32 functions that will perform some of the most repetitive Windows tasks. For instance, the ZSetListBoxStrings function fills a list box with each string from a passed array of strings. Although this is not a terribly important function, it does its part in helping to cut down coding time. WAPE gives you access to the existing Windows tools that let you build resources. You can choose to invoke the Windows dialog editor, the font editor, or the icon editor. WAPE also provides a menu editor to help you create your pull-down menu system--one of the more tedious tasks in Windows programming. You can assign the various menu attributes to each menu item (such as GRAYED, CHECKED, and INACTIVE). When you save the menu, an MNU text file is generated that you can later include in your RC file, and a RES resource file is generated. WAPE (see Figure 5) is neither a prototyping tool nor an applications generator. Its strengths are its comprehensive help system, its menu editor, and its set of API functions. In my opinion, this tool will appeal mainly to beginning Windows developers. Summary Windows and Presentation Manager code generators are new and powerful tools that can substantially increase developers' productivity. Each of those discussed here has advantages and disadvantages, and the one you choose will depend on the project at hand. As the tools themselves are made more comprehensive, they will be applicable to an even wider range of program development. Drawing the Checkerboard and Pieces Using GPI Charles Petzold In the last issue of MSJ, I presented an informal specification for a game of checkers that I am writing for OS/2 Presentation Manager. In this and the next few issues, I'll discuss the code I write for CHECKERS as I build the program. In each issue, I plan to present a complete working program. The goal for this article is a program that simply draws the checkerboard and pieces, as shown in Figure 1. The user can adjust the size of the CHECKERS window using the normal sizing border. The board and pieces must adjust themselves to the new window size while retaining the correct aspect ratio. When the program is minimized, a miniature version of the CHECKERS board appears as an icon at the bottom of the screen. Drawing an image that can adjust itself to the size of the window while retaining the correct aspect ratio is one of the challenges for the first version of the program. CHECKERS also includes a menu with several options. You use these options to show a standard "About" dialog box, to change the colors of the board and the pieces, and to change the orientation of the board. By default, the board is oriented so that the black pieces appear closest to the user. Did I say black pieces? Do you see any black pieces in Figure 1? This may be a little confusing at first. It is common in checkers to refer to black squares and white squares and to black pieces and white pieces. In reality, standard tournament boards are green and buff, and the pieces are red and white. The board is always oriented with a black square (which is really green) at the left corner of the side of the board closest to each player. Pieces can be placed only on black squares. The player with the black pieces (which are really red) always moves first. The CHECKERS Files Seven files are necessary for creating this first version of CHECKERS (see Figures 2 and 3). These files are fairly standard components of a Presentation Manager (hereafter "PM") program. CHECKERS is a make file used to construct CHECKERS.EXE. If you have the Microsoft C Version 5.1 compiler and the OS/2 1.1 Programmer's Toolkit installed, you can create CHECKERS.EXE by entering: MAKE CHECKERS The main function of CHECKERS.C is the entry point to the program, the window procedure for the program's client window, and the dialog procedure for the "About" dialog box. CHECKERS.RC is the resource script. This file contains a menu template and two dialog box templates, one for the "About" box and the other to change colors. Currently, the CHECKERS.H header file contains mostly definitions of identifiers used both in CHECKERS.C and CHECKERS.RC, but it will eventually contain other information (such as structure definitions) that will be used throughout the program. CHECKERS.DEF is the module definition file. This file contains information that LINK uses to construct the CHECKERS.EXE executable. CKRDRAW.C (Figure 3) is the source code file for all the drawing functions in CHECKERS. I expect CKRDRAW.C to be expanded in the next article. The CKRDRAW.H header file contains declarations of functions in CKRDRAW.C that are called by CHECKERS.C. All other functions in CKRDRAW.C are used only within the CKRDRAW.C module. The CHECKERS.C Module CHECKERS.C is fairly simple and straightforward. The main function creates a message queue, registers a window class for the program's client window, and creates a standard window. It then enters the normal message loop. On exiting the message queue, main destroys the window and the message queue and terminates. ClientWndProc, the window procedure for the client window, processes only five messages: WM_CREATE (to perform program initialization), WM_SIZE (which occurs when the window changes size), WM_PAINT (to handle the window painting logic), WM_COMMAND (to handle messages from the menu indicating user selections), and WM_DESTROY (to clean up before termination). For all these messages, ClientWndProc calls one or more functions beginning with the prefix Ckd (which stands for "checkers draw"). These functions are in the CKRDRAW.C module; the function templates are in CKRDRAW.H. As I add more source code modules to CHECKERS, I will consistently preface the function name with an abbreviation of the name of the file in which the function is defined. Doing so makes it easier to find functions in multimodule programs. The CKRDRAW.C Module The CKRDRAW.C file isolates all the drawing functions in one file. The file contains several global variables required in these functions, but these variables are defined as static and hence are private to the module. Only one variable is common to both CHECKERS.C and CKRDRAW.C: hab, the thread's anchor block handle. This variable is obtained in main from the WinInitialize call and is required in several GPI functions used in CKRDRAW. CKRDRAW.C also contains a number of functions defined as static. These functions are also private to the CKRDRAW.C module and not visible to other modules. Only the nonstatic functions can be called from CHECKERS.C. Invisible variables and functions in CKRDRAW.C make it easier to treat the module as a black box. In a black box, a few functions provide an interface to all the module's operations. Board and Piece Dimensions Near the top of CKRDRAW.C are several #define statements that define constants representing the various dimensions of the checkerboard and the pieces in an arbitrary coordinate system. Figure 4 shows how these constants define the size of the various components of the checkerboard, including the margins around the four sides. The first function in CKRDRAW.C, CkdQueryBoardDimensions, calculates the size of the checkerboard (with the margins) in the arbitrary coordinate system. The goal is to fit this checkerboard inside the client window and to preserve the correct aspect ratio regardless of the size of the window. Figure 5 shows the three constants that define the size of the playing pieces, in both normal and kinged versions. These constants will be used to create bitmaps to display the pieces on the board. The Presentation Page It will be most convenient for the CHECKERS program to draw the checkerboard on the window using the arbitrary coordinates. Somehow the coordinates must be translated to the pixels of the program's window. This is the first problem I'll attack. I will use the GPI presentation page and page viewport for this job. The presentation page and page viewport together define the GPI device transform. A presentation page is a rectangular surface on which a Presentation Manager program draws. The size and units of the presentation page are specified in GpiCreatePS. In CKRDRAW.C, the presentation page size is set to the dimensions of the checkerboard. The dimensions of a presentation page are defined by a unit of measurement called page units. For example, you can specify that your presentation page is in units of inches or millimeters. If you do, images are drawn using coordinates in units of inches or millimeters; GPI maps these coordinates to the pixels of the window. The PU_ARBITRARY page units chosen for CHECKERS are a little different. PU_ARBITRARY indicates that the horizontal and vertical coordinates of the presentation page are to be equal in physical size, even if the video display has unequal horizontal and vertical resolution. The first step is to create the presentation space in which the board and pieces will be drawn. The ClientWndProc function in CHECKERS.C calls CkdCreatePS while processing the WM_CREATE message. Next, CkdCreatePS calls CkdQueryBoardDimensions to obtain the size of the checkerboard in the arbitrary coordinates. This size (4500 units wide by 2900 units high) is stored in a structure of type SIZEL. The structure is passed directly to the GpiCreatePS function to specify the size of the presentation page. The page units are specified in the last parameter to GpiCreatePS with the use of the PU_ARBITRARY constant. The Page Viewport When a program calls GpiCreatePS to create a presentation space for a window, GPI uses the page size, the page units, and the resolution of the video display to define a page viewport. The page viewport is a rectangle in units of pixels. It is equal in physical size to the presentation page in page units. (For example, if you specify that your presentation page is two inches by two inches, and the video display has a resolution of 72 dots per inch, the page viewport is 144 pixels by 144 pixels.) The page viewport rectangle is relative to the lower-left corner of the window associated with the presentation space. When a program draws on a presentation page in page units, GPI maps the points to the pixels of the page viewport. The four corners of the presentation page map to the four corners of the page viewport. The PU_ARBITRARY page units force GPI to define a page viewport that can fit in the screen with equal horizontal and vertical resolution in the presentation page. For example, CHECKERS defines a presentation page that is 4500 units wide by 2900 units high--the size of the checkerboard. When you run CHECKERS on an EGA monitor (which has a screen size of 640 by 350 pixels), the page viewport rectangle is set to 640 by 309 pixels. On a VGA monitor, with a screen size of 640 by 480 pixels, the page viewport is 640 by 412 pixels. If I wanted to run CHECKERS in full-screen mode only, I would now be finished. But because I want to run CHECKERS in a window that is smaller than the full screen, the page viewport rectangle must be adjusted based on the size of the window. When ClientWndProc receives a WM_SIZE message, it calls the CkdResizePS function. This function uses the pixel size of the window (from WinQueryWindowRect) and the original page viewport rectangle (obtained from a call to GpiQueryPageViewport in CkdCreatePS) to calculate a scaling factor that preserves the aspect ratio. This scaling factor is used to adjust the page viewport rectangle, which is then centered in the client window. Thus, the program can draw the checkerboard on the presentation page in the arbitrary coordinates. GPI maps these page coordinates to the pixels of the window (see Figure 6). Each time the size of the window changes, a new page viewport is calculated. Drawing the Board The checkerboard and playing pieces are drawn on the screen when ClientWndProc receives a WM_PAINT message. The window procedure calls three functions in CKRDRAW.C: CkdDrawWindowBackground, CkdDrawWholeBoard, and CkdDrawAllPieces. The CkdDrawWindowBackground function is simple: it calls WinQueryWindowRect and WinFillRect to color the whole window in the background color. Because WinFillRect is not a GPI function, it draws the window in units of pixels regardless of the presentation page and page viewport. The CkdDrawWholeBoard function calls CkdDrawAllBoardSquares. CkdDrawAllBoardSquares then calls CkdDrawBoardSquare 64 times to draw the 64 squares that make up the checkerboard. The values of x and y in Figure 7 are a simple coordinate system used to identify each square. CkdDrawBoardSquare draws one square (actually a trapezoid, but I'll call it a square nevertheless). It sets a border color to black and an area color to either the color used for the black square or the color used for the white square. The function then calls GpiBeginArea to begin an area bracket. The coordinates of the four corners of the square (in the arbitrary presentation page units) are obtained from CkdQuerySquareCoords. CkdQuerySquareCoords calls CkdQuerySquareOrigin four times to obtain these coordinates. CkdDrawBoardSquare then calls GpiMove and GpiPolyLine in the area bracket. The GpiEndArea function ends the area bracket, causing the outline and interior of the square to be drawn. (You'll notice that CkdDrawBoardSquare returns the value that was returned from the GpiEndArea function. CkdDrawAllBoardSquares checks if this value is equal to GPI_HITS. This has no meaning now, but I'll make use of it in the next installment of the program to determine the square that the user clicks with the mouse.) CkdDrawWholeBoard then finishes by drawing the edge in the front of the board, also using GpiBeginArea and GpiEndArea to define an area bracket. Creating the Bitmaps So far I have covered all the code necessary for drawing the checkerboard. Drawing the playing pieces is a little more complex, because the program must eventually allow a user to pick up a piece with the mouse and move it to another square. This will require that CHECKERS be able to draw and erase a piece quickly as it is moved across the window. To achieve satisfactory performance, the playing pieces must be stored as bitmaps. The bitmaps are created when ClientWndProc receives a WM_SIZE message and calls CkdCreatePieces. (In subsequent WM_SIZE messages, any previous bitmaps that exist are destroyed first by a call to CkdDestroyPieces. The bitmaps are also destroyed during the WM_DESTROY message.) Recreating the bitmaps during a WM_SIZE message is necessary because bitmaps are always in a specific pixel size that depends on the size of the window. CkdCreatePieces creates six bitmaps. The handles to four of these bitmaps are stored in the global array ahbmPiece. (The ahbm prefix on this variable name stands for "array of handles to bitmaps.") These four bitmaps are the black and white pieces in both normal and kinged versions. The handles to the other two bitmaps are stored in the array ahbmMask. I will show shortly how these mask bitmaps allow me to use bitmaps to draw nonrectangular images on the screen. The CkdCreatePieces function creates a presentation space associated with the memory device context. This presentation space is given the same presentation page size and page viewport as the presentation space associated with the window. Thus, we can draw the piece on the bitmap using the arbitrary coordinates. However, the bitmap must be created in a specific pixel size. The size of the bitmap in page coordinates is stored in the sizlPiece variable; this is converted to device coordinates by calling GpiConvert. The CkdCreatePieces function then sends up a BITMAPINFOHEADER structure and calls GpiCreateBitmap. The GpiSetBitmap function sets the bitmap to be the drawing surface of the presentation space associated with the memory device context. A call to CkdRenderPiece draws the piece into this presentation space and hence on the bitmap. First, CkdRenderPiece colors the background of the bitmap by calls to GpiMove and GpiBox. It then uses two of the GPI ellipse functions (GpiPointArc and GpiFullArc) to draw the top ellipse and bottom partial ellipse that make up the piece. The two ellipses are connected with vertical lines. The four bitmaps for the pieces and the two bitmaps for the masks are all drawn with calls to CkdRenderPiece, but the colors used in drawing the bitmaps are different. The piece bitmap is drawn with a black background. (I use the GPI identifier CLR_FALSE for this. CLR_FALSE really means that all color bits are set to 0, which is equivalent to black on the video display.) The foreground is the piece itself, using the current colors of the black and white pieces. The mask bitmap is drawn with a CLR_TRUE background (which means all color bits are set to 1 and the color is white) and a CLR_FALSE foreground where the piece would appear. The mask resembles a white surface, with a hole where the piece would appear. I'll describe shortly how the mask and piece bitmaps are used to draw a piece on the board. The BOARD Structure Let's return to CHECKERS.C for a moment. Near the top of ClientWndProc is a structure variable named brd, defined as type BOARD. This BOARD structure is defined in CHECKERS.H. It has three ULONG (32-bit unsigned long integer) fields: ulBlack, ulWhite, and ulKing. The BOARD structure defines the state of the board at any point in time. In the last issue, I discussed the standard checkers notation in which all the black squares of the board (the squares on which pieces may be placed) are numbered from 1 to 32. These numbers can also refer to the bit positions of a 32-bit integer. To make the notation more convenient, subtract 1 from the numbers used for standard checkers notation so that we have bit positions from 0 through 31, where bit position 0 is the least significant bit of the 32-bit integer. The bits correspond to the black squares of the board (see Figure 8). A 1 bit in the ulBlack field of the BOARD structure means that a black piece is on that square. A 1 bit in the ulWhite field means that a white piece is on that square. A 1 bit in the ulKing field means that the corresponding black or white piece has been kinged. The BOARD structure is initialized thus for the beginning of a game: black pieces in positions 0 through 11, white pieces in positions 20 through 31, and no kings. During the WM_PAINT message, ClientWndProc draws the pieces on the board by calling CkdDrawAllPieces, passing a pointer to the BOARD structure. CkdDrawAllPieces simply loops through all the possible board squares and calls CkdDrawOnePiece. CkdDrawOnePiece calls CkdConvertCoordsToIndex, which converts the coordinates shown in Figure 7 to the bit positions shown in Figure 8. The CkdQuerySquarePieceOrigin determines the page coordinates of the lower-left corner of the piece. This is converted to device coordinates (pixels relative to the window) by a call to GpiConvert. CkdDrawOnePiece then uses the index returned from CkdConvertCoordsToIndex with the fields of the BOARD structure to test whether a square contains a black or a white piece and if that piece is kinged. If it is, the function calls CkdShowPiece to display the piece on the board. Drawing the Pieces The CkdShowPiece function is responsible for drawing a single piece on the board. The POINTL structure passed to the function contains the position of the lower-left corner of the piece measured in device coordinates. You'll recall that the mask bitmaps were created with a white background composed of 1 bits and a black foreground composed of 0 bits. The mask bitmap is selected into the memory presentation space and displayed on the screen using the GpiBitBlt function with a raster operation of ROP_SRCAND. The raster operation of ROP_SRCAND means that the screen and the bitmap are combined with a bitwise AND operation. The background of 1 bits leaves the screen intact. The foreground of 0 bits creates a black hole that is the size of the piece. The piece bitmap is then selected into the memory presentation space and displayed on the screen using the GpiBitBlt function with a raster operation of ROP_SRCPAINT. This raster operation is a bitwise OR operation between the screen and the bitmap. The black background leaves the screen intact. The foreground (the piece itself) is, in effect, drawn over the black hole left by the mask bitmap. This process is shown in Figure 9 (A through E). It is similar to the way that the Presentation Manager draws nonrectangular icons and mouse pointers on the screen. Colors and Orientation So far I've been discussing how the program draws the checkerboard and the pieces. This version of CHECKERS also has a menu with Game and Color options. The five options in the Color menu let you change any of the five colors that the program uses for coloring the window background and drawing the board and pieces. This menu's dialog box is shown in Figure 10. You can also select Standard Colors from the Color menu, which causes all the colors to revert to the default tournament standards. The window procedure for the dialog box is named ColorDlgProc and is in CKRDRAW.C. The dialog box template is defined in CHECKERS.RC. The same dialog box and dialog box procedure is used for all five color selections. First, CHECKERS.C passes a pointer to a menu command ID number as the last parameter to WinDlgBox. This pointer is available in the dialog box procedure during the WM_INITDLG message. The dialog procedure determines which menu option has been selected and sets the text of the window appropriately. The dialog procedure also saves a pointer to the appropriate global color variable in pclr. If the user presses the OK button, the selected color is saved in this variable. When the dialog box is terminated, ClientWndProc invalidates the window so that it can be repainted. But if the colors of the black or white pieces have been changed, ClientWndProc calls CkdDestroyPieces and CkdCreatePieces first to recreate the bitmaps using the new colors. The first time you run the program, CHECKERS uses WinWriteProfileData to store the color preferences in the OS2.INI file. The colors are saved during the CkdDestroyPS function, which is called from ClientWndProc during the WM_DESTROY message. The next time you run the program, the colors saved in OS2.INI are retrieved during the CkdCreatePS function using calls to WinQueryProfileData. Normally, the program displays the black pieces on the bottom of the board, the side that appears closest to the user. You can switch the board around by unchecking the Black on Bottom option in the Game menu. The current orientation of the board is stored in the sBottom variable in ClientWndProc. This variable is passed as a parameter to CkdDrawAllPieces and eventually affects the way coordinates are converted to bit position indexes in the CkdConvertCoordsToIndex function. Testing the Code This version of CHECKERS is fairly easy to test. You can resize the window and note that the checkerboard and pieces are redrawn to the new size. You can also change colors from the menu and dialog boxes. You may want to alter the fields of the BOARD structure defined in ClientWndProc and then recompile the program to see how the pieces are arranged on the board with the new values. When doing so, be sure the ulBlack and ulWhite fields do not have any common bits set. The bits set in the ulKing field should duplicate bits set in either the ulBlack or ulWhite fields. The Next Installment In the next issue, I'll enlarge CHECKERS.C, add a few more functions to CKRDRAW.C, and create another module or two. The goal will be to add keyboard and mouse interfaces to CHECKERS and to add logic that tests valid checker moves. With the version of CHECKERS in the next issue, you'll be able to play a game of checkers by yourself, alternating moving the black and white pieces. Figure 2 CHECKERS CC = cl -c -G2sw -W3 checkers.obj : checkers.c checkers.h ckrdraw.h $(CC) checkers.c ckrdraw.obj : ckrdraw.c checkers.h ckrdraw.h $(CC) ckrdraw.c checkers.res : checkers.rc checkers.h rc -r checkers checkers.exe : checkers.obj ckrdraw.obj checkers.def link checkers ckrdraw, /align:16, NUL, os2, checkers rc checkers.res checkers.exe checkers.exe : checkers.res rc checkers.res CHECKERS.H #define BLACK 0 #define WHITE 1 #define NORM 0 #define KING 1 typedef struct { ULONG ulBlack ; ULONG ulWhite ; ULONG ulKing ; } BOARD ; #define ID_RESOURCE 1 #define IDM_BOTTOM 1 #define IDM_ABOUT 2 #define IDM_COLOR_BACKGROUND 11 #define IDM_COLOR_BLACK_SQUARE 12 #define IDM_COLOR_WHITE_SQUARE 13 #define IDM_COLOR_BLACK_PIECE 14 #define IDM_COLOR_WHITE_PIECE 15 #define IDM_COLOR_STANDARD 16 #define IDD_ABOUT_DLG 1 #define IDD_COLOR_DLG 2 #define IDD_HEADING 10 #define IDD_COLOR 20 CHECKERS.DEF NAME CHECKERS WINDOWAPI DESCRIPTION 'Checkers Version 0.10 (c) 1990, Charles Petzold' PROTMODE HEAPSIZE 1024 STACKSIZE 8192 EXPORTS ClientWndProc AboutDlgProc ColorDlgProc CHECKERS.RC #include <os2.h> #include "checkers.h" MENU ID_RESOURCE { SUBMENU "~Game", -1 { MENUITEM "~Black on Bottom", IDM_BOTTOM,, MIA_CHECKED MENUITEM SEPARATOR MENUITEM "~About Checkers...", IDM_ABOUT } SUBMENU "~Colors", -1 { MENUITEM "Wi~ndow Background...", IDM_COLOR_BACKGROUND MENUITEM "~Black Square...", IDM_COLOR_BLACK_SQUARE MENUITEM "~White Square...", IDM_COLOR_WHITE_SQUARE MENUITEM "B~lack Piece...", IDM_COLOR_BLACK_PIECE MENUITEM "W~hite Piece...", IDM_COLOR_WHITE_PIECE MENUITEM SEPARATOR MENUITEM "~Standard colors", IDM_COLOR_STANDARD } } #define GRP WS_GROUP DLGTEMPLATE IDD_ABOUT_DLG { DIALOG "", 0, 32, 32, 200, 100,, FCF_DLGBORDER { CTEXT "Checkers Version 0.10" -1, 10, 76, 180, 8 CTEXT "(Draws the Board and Pieces)" -1, 10 62, 180, 8 CTEXT "Microsoft Systems Journal,1/90" -1, 10, 48, 180, 8 CTEXT "(c) 1990, Charles Petzold" -1, 10, 34, 180, 8 DEFPUSHBUTTON "OK" DID_OK, 80, 8, 40, 16, GRP } } DLGTEMPLATE IDD_COLOR_DLG { DIALOG "", 0, 32, 32, 180, 180,, FCF_DLGBORDER { CTEXT "", IDD_HEADING, 10, 166, 160, 8 GROUPBOX "Color" -1, 16, 32, 148, 130 RADIOBUTTON "Black" IDD_COLOR + CLR_BLACK, 20, 136, 64, 12, GRP RADIOBUTTON "Blue" IDD_COLOR + CLR_BLUE, 20, 122, 64, 12 RADIOBUTTON "Green" IDD_COLOR + CLR_GREEN, 20, 108, 64, 12 RADIOBUTTON "Cyan" IDD_COLOR + CLR_CYAN, 20, 94, 64, 12 RADIOBUTTON "Red" IDD_COLOR + CLR_RED, 20, 80, 64, 12 RADIOBUTTON "Pink" IDD_COLOR + CLR_PINK, 20, 66, 64, 12 RADIOBUTTON "Yellow" IDD_COLOR + CLR_YELLOW, 20, 52, 64, 12 RADIOBUTTON "Pale Gray" IDD_COLOR + CLR_PALEGRAY, 20, 38, 64, 12 RADIOBUTTON "Dark Gray" IDD_COLOR + CLR_DARKGRAY, 94, 136 64, 12 RADIOBUTTON "Dark Blue" IDD_COLOR + CLR_DARKBLUE, 94, 122 64, 12 RADIOBUTTON "Dark Green" IDD_COLOR + CLR_DARKGREEN, 94, 108, 64, 12 RADIOBUTTON "Dark Cyan" IDD_COLOR + CLR_DARKCYAN, 94, 94, 64, 12 RADIOBUTTON "Dark Red" IDD_COLOR + CLR_DARKRED, 94, 80, 64, 12 RADIOBUTTON "Dark Pink" IDD_COLOR + CLR_DARKPINK, 94, 66, 64, 12 RADIOBUTTON "Brown" IDD_COLOR + CLR_BROWN, 94, 52, 64, 12 RADIOBUTTON "White" IDD_COLOR + CLR_WHITE, 94, 38, 64, 12 DEFPUSHBUTTON "OK" DID_OK, 16, 8, 52, 16, GRP PUSHBUTTON "Cancel" DID_CANCEL, 112, 8, 52, 16 } } CHECKERS.C /*------------------------------------------------------------------ CHECKERS.C source code file, version 0.10 (draws board and pieces) (c) 1990, Charles Petzold -----------------------------------------------------------------*/ #define INCL_WIN #include <os2.h> #include "checkers.h" #include "ckrdraw.h" MRESULT EXPENTRY ClientWndProc (HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2) ; MRESULT EXPENTRY AboutDlgProc (HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2) ; HAB hab ; int main (void) { static CHAR szClientClass[] = "Checkers" ; static ULONG flFrameFlags = FCF_TITLEBAR | FCF_SYSMENU | FCF_SIZEBORDER | FCF_MINMAX | FCF_SHELLPOSITION | FCF_TASKLIST | FCF_MENU ; HMQ hmq ; HWND hwndFrame, hwndClient ; QMSG qmsg ; hab = WinInitialize (0) ; hmq = WinCreateMsgQueue (hab, 0) ; WinRegisterClass (hab, szClientClass, ClientWndProc, CS_SIZEREDRAW, 0) ; hwndFrame = WinCreateStdWindow (HWND_DESKTOP, WS_VISIBLE, &flFrameFlags, szClientClass, NULL, 0L, NULL, ID_RESOURCE, &hwndClient) ; while (WinGetMsg (hab, &qmsg, NULL, 0, 0)) WinDispatchMsg (hab, &qmsg) ; WinDestroyWindow (hwndFrame) ; WinDestroyMsgQueue (hmq) ; WinTerminate (hab) ; return 0 ; } MRESULT EXPENTRY ClientWndProc (HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2) { static BOARD brd = { 0x00000FFF, 0xFFF00000, 0x00000000 } ; static HPS hps ; static HWND hwndMenu ; static SHORT sBottom = BLACK ; switch (msg) { case WM_CREATE: hwndMenu = WinWindowFromID ( WinQueryWindow (hwnd, QW_PARENT, FALSE), FID_MENU) ; hps = CkdCreatePS (hwnd) ; return 0 ; case WM_SIZE: CkdResizePS (hps, hwnd) ; CkdDestroyPieces () ; CkdCreatePieces (hps) ; return 0 ; case WM_PAINT: WinBeginPaint (hwnd, hps, NULL) ; CkdDrawWindowBackground (hps, hwnd) ; CkdDrawWholeBoard (hps) ; CkdDrawAllPieces (hps, &brd, sBottom) ; WinEndPaint (hps) ; return 0 ; case WM_COMMAND: switch (COMMANDMSG (&msg)->cmd) { case IDM_BOTTOM: WinSendMsg (hwndMenu, MM_SETITEMATTR, MPFROM2SHORT (IDM_BOTTOM, TRUE), MPFROM2SHORT (MIA_CHECKED, sBottom ? MIA_CHECKED : 0)) ; sBottom ^= 1 ; WinInvalidateRect (hwnd, NULL, FALSE) ; return 0 ; case IDM_ABOUT: WinDlgBox (HWND_DESKTOP, hwnd, AboutDlgProc, NULL, IDD_ABOUT_DLG, NULL) ; return 0 ; case IDM_COLOR_BACKGROUND: case IDM_COLOR_BLACK_SQUARE: case IDM_COLOR_WHITE_SQUARE: case IDM_COLOR_BLACK_PIECE: case IDM_COLOR_WHITE_PIECE: if (!WinDlgBox (HWND_DESKTOP, hwnd, ColorDlgProc, NULL, IDD_COLOR_DLG, &(COMMANDMSG (&msg)->cmd))) return 0 ; if (COMMANDMSG (&msg)->cmd == IDM_COLOR_BLACK_PIECE || COMMANDMSG (&msg)->cmd == IDM_COLOR_WHITE_PIECE) { CkdDestroyPieces () ; CkdCreatePieces (hps) ; } WinInvalidateRect (hwnd, NULL, FALSE) ; return 0 ; case IDM_COLOR_STANDARD: CkdSetStandardColors () ; CkdDestroyPieces () ; CkdCreatePieces (hps) ; WinInvalidateRect (hwnd, NULL, FALSE) ; return 0 ; } break ; case WM_DESTROY: CkdDestroyPieces () ; CkdDestroyPS (hps) ; return 0 ; } return WinDefWindowProc (hwnd, msg, mp1, mp2) ; } MRESULT EXPENTRY AboutDlgProc (HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2) { switch (msg) { case WM_COMMAND: switch (COMMANDMSG(&msg)->cmd) { case DID_OK: case DID_CANCEL: WinDismissDlg (hwnd, TRUE) ; return 0 ; } } return WinDefDlgProc (hwnd, msg, mp1, mp2) ; } Figure 3 CKRDRAW.H MRESULT EXPENTRY ColorDlgProc (HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2) ; HPS CkdCreatePS (HWND hwnd) ; VOID CkdResizePS (HPS hps, HWND hwnd) ; BOOL CkdDestroyPS (HPS hps) ; VOID CkdSetStandardColors (VOID) ; VOID CkdCreatePieces (HPS hps) ; VOID CkdDestroyPieces (VOID) ; VOID CkdDrawWindowBackground (HPS hps, HWND hwnd) ; VOID CkdDrawWholeBoard (HPS hps) ; VOID CkdDrawAllPieces (HPS hps, BOARD *pbrd, SHORT sBottom) ; CKRDRAW.C #define INCL_WIN #define INCL_GPI #include <os2.h> #include <stdlib.h> #include "checkers.h" #include "ckrdraw.h" /*---------------------------------------- Defines for board and piece dimensions ----------------------------------------*/ #define BRD_HORZFRONT 500 #define BRD_HORZBACK 350 #define BRD_VERT 300 #define BRD_EDGE 75 #define BRD_HORZMARGIN 250 #define BRD_FRONTMARGIN 250 #define BRD_BACKMARGIN 250 #define PIECE_XAXIS (BRD_HORZBACK - 50) #define PIECE_YAXIS (BRD_VERT - 50) #define PIECE_HEIGHT 50 /*------------------------------------- Global variables external to module -------------------------------------*/ extern HAB hab ; /*------------------------------------- Global variables internal to module -------------------------------------*/ // Background, board, and piece colors static LONG clrBackground = CLR_CYAN ; static LONG clrBlackSquare = CLR_DARKGREEN ; static LONG clrWhiteSquare = CLR_PALEGRAY ; static LONG clrBlackPiece = CLR_RED ; static LONG clrWhitePiece = CLR_WHITE ; // Text strings for saving colors to OS2.INI static CHAR szApplication [] = "Checkers" ; static CHAR szClrBackground [] = "Background Color" ; static CHAR szClrBlackSquare [] = "Black Square Color" ; static CHAR szClrWhiteSquare [] = "White Square Color" ; static CHAR szClrBlackPiece [] = "Black Piece Color" ; static CHAR szClrWhitePiece [] = "White Piece Color" ; // Original viewport for adjusting board to window size static RECTL rclOrigViewport ; // Bitmaps for drawing pieces static HDC hdcMemory ; static HPS hpsMemory ; static HBITMAP ahbmPiece[2][2], ahbmMask[2] ; static SIZEL sizlPiece[2] ; /*-------------------------------------------------------------- CkdQueryBoardDimensions: Obtains size of board with margins ------------------------------------------------------------ */ static VOID CkdQueryBoardDimensions (SIZEL *psizlPage) { psizlPage->cx = 8 * BRD_HORZFRONT + 2 * BRD_HORZMARGIN ; psizlPage->cy = 8 * BRD_VERT + BRD_FRONTMARGIN + BRD_BACKMARGIN ; } /*----------------------------------------------------------- CkdQuerySquareOrigin: Obtains lower left corner of square ----------------------------------------------------------*/ static VOID CkdQuerySquareOrigin (SHORT x, SHORT y, POINTL *pptl) { pptl->x = BRD_HORZMARGIN + y * (BRD_HORZFRONT - BRD_HORZBACK) / 2 + x * (y * BRD_HORZBACK + (8 - y) * BRD_HORZFRONT) / 8 ; pptl->y = BRD_FRONTMARGIN + y * BRD_VERT ; } /*----------------------------------------------------- CkdQuerySquareCoords: Obtains coordinates of square -----------------------------------------------------*/ static VOID CkdQuerySquareCoords (SHORT x, SHORT y, POINTL aptl[]) { CkdQuerySquareOrigin (x, y, aptl + 0) ; CkdQuerySquareOrigin (x + 1, y, aptl + 1) ; CkdQuerySquareOrigin (x + 1, y + 1, aptl + 2) ; CkdQuerySquareOrigin (x, y + 1, aptl + 3) ; } /*----------------------------------------------- CkdDrawBoardSquare: Draws one square of board -----------------------------------------------*/ static LONG CkdDrawBoardSquare (HPS hps, SHORT x, SHORT y) { AREABUNDLE abnd ; LINEBUNDLE lbnd ; LONG lReturn ; POINTL aptl[4] ; GpiSavePS (hps) ; lbnd.lColor = CLR_BLACK ; GpiSetAttrs (hps, PRIM_LINE, LBB_COLOR, 0L, &lbnd) ; abnd.lColor = (x + y) & 1 ? clrWhiteSquare : clrBlackSquare ; GpiSetAttrs (hps, PRIM_AREA, LBB_COLOR, 0L, &abnd) ; GpiBeginArea (hps, BA_ALTERNATE | BA_BOUNDARY) ; CkdQuerySquareCoords (x, y, aptl) ; GpiMove (hps, aptl + 3) ; GpiPolyLine (hps, 4L, aptl) ; lReturn = GpiEndArea (hps) ; GpiRestorePS (hps, -1L) ; return lReturn ; } /*---------------------------------------------------- CkdDrawAllBoardSquares: Draws all squares of board ----------------------------------------------------*/ static LONG CkdDrawAllBoardSquares (HPS hps) { SHORT x, y ; for (y = 0 ; y < 8 ; y++) for (x = 0 ; x < 8 ; x++) if (CkdDrawBoardSquare (hps, x, y) == GPI_HITS) return MAKELONG (x, y) ; return MAKELONG (-1, -1) ; } /*---------------------------------------------------- CkdRenderPiece: Draws piece on bitmap in memory PS ----------------------------------------------------*/ static VOID CkdRenderPiece (HPS hpsMemory, LONG lColorBack, LONG lColor, LONG lColorLine, SHORT sKing) { ARCPARAMS arcp ; AREABUNDLE abnd ; LINEBUNDLE lbnd ; POINTL ptl, aptlArc[2] ; SHORT s ; GpiSavePS (hpsMemory) ; // Draw background of bitmap GpiSetColor (hpsMemory, lColorBack) ; ptl.x = 0 ; ptl.y = 0 ; GpiMove (hpsMemory, &ptl) ; ptl.x = PIECE_XAXIS ; ptl.y = PIECE_YAXIS + (sKing + 1) * PIECE_HEIGHT ; GpiBox (hpsMemory, DRO_FILL, &ptl, 0L, 0L) ; // Set colors for areas and outlines abnd.lColor = lColor ; GpiSetAttrs (hpsMemory, PRIM_AREA, ABB_COLOR, 0L, &abnd) ; lbnd.lColor = lColorLine ; GpiSetAttrs (hpsMemory, PRIM_LINE, LBB_COLOR, 0L, &lbnd) ; // Set arc parameters arcp.lP = PIECE_XAXIS / 2 ; arcp.lQ = PIECE_YAXIS / 2 ; arcp.lR = 0 ; arcp.lS = 0 ; GpiSetArcParams (hpsMemory, &arcp) ; // draw the piece for (s = 0 ; s <= sKing ; s++) { GpiBeginArea (hpsMemory, BA_ALTERNATE | BA_BOUNDARY) ; ptl.x = 0 ; ptl.y = PIECE_YAXIS / 2 + (s + 1) * PIECE_HEIGHT ; GpiMove (hpsMemory, &ptl) ; ptl.y -= PIECE_HEIGHT ; GpiLine (hpsMemory, &ptl) ; aptlArc[0].x = PIECE_XAXIS / 2 ; aptlArc[0].y = s * PIECE_HEIGHT ; aptlArc[1].x = PIECE_XAXIS ; aptlArc[1].y = PIECE_YAXIS / 2 + s * PIECE_HEIGHT ; GpiPointArc (hpsMemory, aptlArc) ; ptl.x = PIECE_XAXIS ; ptl.y = PIECE_YAXIS / 2 + (s + 1) * PIECE_HEIGHT ; GpiLine (hpsMemory, &ptl) ; GpiEndArea (hpsMemory) ; } ptl.x = PIECE_XAXIS / 2 ; ptl.y = PIECE_YAXIS / 2 + (sKing ? 2 : 1) * PIECE_HEIGHT ; GpiMove (hpsMemory, &ptl) ; GpiFullArc (hpsMemory, DRO_OUTLINEFILL, MAKEFIXED (1,0)) ; GpiRestorePS (hpsMemory, -1L) ; } /*------------------------------------------------------- CkdQuerySquareCenter: Obtains center of board square ------------------------------------------------------*/ static VOID CkdQuerySquareCenter (SHORT x, SHORT y, POINTL *pptlCenter) { POINTL aptl[4] ; CkdQuerySquareCoords (x, y, aptl) ; pptlCenter->x = (aptl[0].x + aptl[1].x + aptl[2].x + aptl[3].x) / 4 ; pptlCenter->y = (aptl[1].y + aptl[2].y) / 2 ; } /*-------------------------------------------------------------- CkdPieceOriginFromCenter: Converts center of square to piece origin -------------------------------------------------------------*/ static VOID CkdPieceOriginFromCenter (POINTL *pptl) { pptl->x -= PIECE_XAXIS / 2 ; pptl->y -= PIECE_YAXIS / 2 ; } /*-------------------------------------------------------------- CkdQuerySquarePieceOrigin: Obtains origin of piece on a square -------------------------------------------------------------*/ static VOID CkdQuerySquarePieceOrigin (SHORT x, SHORT y, POINTL *pptlOrigin) { CkdQuerySquareCenter (x, y, pptlOrigin) ; CkdPieceOriginFromCenter (pptlOrigin) ; } /*-------------------------------------------------------------- CkdConvertCoordsToIndex: Obtains index (0-31) from square coordinates -------------------------------------------------------------*/ static SHORT CkdConvertCoordsToIndex (SHORT x, SHORT y, SHORT sBottom) { if (x < 0 || x > 7 || y < 0 || y > 7) return -1 ; if ((x - (y & 1)) & 1) return -1 ; if (sBottom == WHITE) { x = 7 - x ; y = 7 - y ; } return 3 ^ (4 * y + (x - (y & 1)) / 2) ; } /*------------------------------------------------------------- CkdShowPiece: Draws a piece on the screen at specific point ------------------------------------------------------------*/ static VOID CkdShowPiece (HPS hps, POINTL *pptlOrg, SHORT sColor, SHORT sKing) { POINTL aptl[3] ; // Write out mask with bitwise AND aptl[0] = *pptlOrg ; aptl[1].x = pptlOrg->x + sizlPiece[sKing].cx ; aptl[1].y = pptlOrg->y + sizlPiece[sKing].cy ; aptl[2].x = 0 ; aptl[2].y = 0 ; GpiSetBitmap (hpsMemory, ahbmMask[sKing]) ; GpiBitBlt (hps, hpsMemory, 3L, aptl, ROP_SRCAND, BBO_IGNORE) ; // Write out piece with bitwise OR aptl[0] = *pptlOrg ; aptl[1].x = pptlOrg->x + sizlPiece[sKing].cx ; aptl[1].y = pptlOrg->y + sizlPiece[sKing].cy ; aptl[2].x = 0 ; aptl[2].y = 0 ; GpiSetBitmap (hpsMemory, ahbmPiece[sColor][sKing]) ; GpiBitBlt (hps, hpsMemory, 3L, aptl, ROP_SRCPAINT, BBO_IGNORE) ; GpiSetBitmap (hpsMemory, NULL) ; } /*--------------------------------------------------------------- CkdDrawOnePiece: Draws a piece on the board at specific coordinates -------------------------------------------------------------*/ static VOID CkdDrawOnePiece (HPS hps, SHORT x, SHORT y, BOARD *pbrd, SHORT sBottom) { POINTL ptlOrigin ; SHORT i, sKing ; i = CkdConvertCoordsToIndex (x, y, sBottom) ; if (i == -1) return ; CkdQuerySquarePieceOrigin (x, y, &ptlOrigin) ; GpiConvert (hps, CVTC_PAGE, CVTC_DEVICE, 1L, &ptlOrigin) ; sKing = pbrd->ulKing & 1L << i ? 1 : 0 ; if (pbrd->ulBlack & 1L << i) CkdShowPiece (hps, &ptlOrigin, BLACK, sKing) ; if (pbrd->ulWhite & 1L << i) CkdShowPiece (hps, &ptlOrigin, WHITE, sKing) ; } /*---------------------------------------------------- ColorDlgProc: Dialog procedure for changing colors ----------------------------------------------------*/ MRESULT EXPENTRY ColorDlgProc (HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2) { static LONG *pclr ; static SHORT sColor ; CHAR *pchHeading ; switch (msg) { case WM_INITDLG: switch (* (PSHORT) PVOIDFROMMP (mp2)) { case IDM_COLOR_BACKGROUND: pchHeading = "Window Background Color" ; pclr = &clrBackground ; break ; case IDM_COLOR_BLACK_SQUARE: pchHeading = "Black Square Color" ; pclr = &clrBlackSquare ; break ; case IDM_COLOR_WHITE_SQUARE: pchHeading = "White Square Color" ; pclr = &clrWhiteSquare ; break ; case IDM_COLOR_BLACK_PIECE: pchHeading = "Black Piece Color" ; pclr = &clrBlackPiece ; break ; case IDM_COLOR_WHITE_PIECE: pchHeading = "White Piece Color" ; pclr = &clrWhitePiece ; break ; } WinSetDlgItemText (hwnd, IDD_HEADING, pchHeading) ; sColor = (SHORT) *pclr ; WinSendDlgItemMsg (hwnd, IDD_COLOR + sColor, BM_SETCHECK, MPFROM2SHORT (TRUE, 0), NULL) ; WinSetFocus (HWND_DESKTOP, WinWindowFromID (hwnd, IDD_COLOR + sColor)) ; return 1 ; case WM_CONTROL: WinSendDlgItemMsg (hwnd, IDD_COLOR + sColor, BM_SETCHECK, MPFROM2SHORT (FALSE, 0), NULL) ; sColor = SHORT1FROMMP (mp1) - IDD_COLOR ; WinSendDlgItemMsg (hwnd, IDD_COLOR + sColor, BM_SETCHECK, MPFROM2SHORT (TRUE, 0), NULL) ; return 0 ; case WM_COMMAND: switch (COMMANDMSG(&msg)->cmd) { case DID_OK: *pclr = (LONG) sColor ; WinDismissDlg (hwnd, TRUE) ; return 0 ; case DID_CANCEL: WinDismissDlg (hwnd, FALSE) ; return 0 ; } break ; } return WinDefDlgProc (hwnd, msg, mp1, mp2) ; } /*------------------------------------------------- CkdCreatePS: Create PS for checker board window -------------------------------------------------*/ HPS CkdCreatePS (HWND hwnd) { HDC hdc ; HPS hps ; SIZEL sizlPage ; USHORT sDataSize ; CkdQueryBoardDimensions (&sizlPage) ; hdc = WinOpenWindowDC (hwnd) ; hps = GpiCreatePS (hab, hdc, &sizlPage, PU_ARBITRARY | GPIF_DEFAULT | GPIT_MICRO | GPIA_ASSOC) ; GpiQueryPageViewport (hps, &rclOrigViewport) ; // Get colors from OS2.INI sDataSize = sizeof (LONG) ; WinQueryProfileData (hab, szApplication, szClrBackground, &clrBackground, &sDataSize) ; sDataSize = sizeof (LONG) ; WinQueryProfileData (hab, szApplication, szClrBlackSquare, &clrBlackSquare, &sDataSize) ; sDataSize = sizeof (LONG) ; WinQueryProfileData (hab, szApplication, szClrWhiteSquare, &clrWhiteSquare, &sDataSize) ; sDataSize = sizeof (LONG) ; WinQueryProfileData (hab, szApplication, szClrBlackPiece, &clrBlackPiece, &sDataSize) ; sDataSize = sizeof (LONG) ; WinQueryProfileData (hab, szApplication, szClrWhitePiece, &clrWhitePiece, &sDataSize) ; return hps ; } /*--------------------------------------------------------- CkdResizePS: Change page viewport for new window size -------------------------------------------------------*/ VOID CkdResizePS (HPS hps, HWND hwnd) { LONG lScale ; RECTL rclWindow, rclViewport ; WinQueryWindowRect (hwnd, &rclWindow) ; // Calculate scaling factor lScale = min (65536L * rclWindow.xRight / rclOrigViewport.xRight, 65536L * rclWindow.yTop / rclOrigViewport.yTop) ; // Adjust page viewport of memory PS rclViewport.xLeft = 0 ; rclViewport.yBottom = 0 ; rclViewport.xRight = lScale * rclOrigViewport.xRight / 65536L ; rclViewport.yTop = lScale * rclOrigViewport.yTop / 65536L ; rclViewport.xLeft = (rclWindow.xRight - rclViewport.xRight) / 2 ; rclViewport.yBottom = (rclWindow.yTop - rclViewport.yTop) / 2 ; rclViewport.xRight += rclViewport.xLeft ; rclViewport.yTop += rclViewport.yBottom ; GpiSetPageViewport (hps, &rclViewport) ; } /*--------------------------------------------------- CkdDestroyPS: Destroy PS for checker board window ---------------------------------------------------*/ BOOL CkdDestroyPS (HPS hps) { // Save colors in OS2.INI WinWriteProfileData (hab, szApplication, szClrBackground, &clrBackground, sizeof (LONG)) ; WinWriteProfileData (hab, szApplication, szClrBlackSquare, &clrBlackSquare, sizeof (LONG)) ; WinWriteProfileData (hab, szApplication, szClrWhiteSquare, &clrWhiteSquare, sizeof (LONG)) ; WinWriteProfileData (hab, szApplication, szClrBlackPiece, &clrBlackPiece, sizeof (LONG)) ; WinWriteProfileData (hab, szApplication, szClrWhitePiece, &clrWhitePiece, sizeof (LONG)) ; return GpiDestroyPS (hps) ; } /*------------------------------------------------------------ CkdSetStandardColors: Sets colors to tournament standards ----------------------------------------------------------*/ VOID CkdSetStandardColors (VOID) { clrBackground = CLR_CYAN ; clrBlackSquare = CLR_DARKGREEN ; clrWhiteSquare = CLR_PALEGRAY ; clrBlackPiece = CLR_RED ; clrWhitePiece = CLR_WHITE ; } /*-------------------------------------------------------------- CkdCreatePieces: Creates bitmaps to use for drawing pieces ------------------------------------------------------------ */ VOID CkdCreatePieces (HPS hps) { BITMAPINFOHEADER bmp ; LONG alBitmapFormat[2] ; RECTL rclViewport ; SHORT sColor, sKing ; SIZEL sizlPage ; // Create memory DC's and PS's CkdQueryBoardDimensions (&sizlPage) ; hdcMemory = DevOpenDC (hab, OD_MEMORY, "*", 0L, NULL, NULL) ; hpsMemory = GpiCreatePS (hab, hdcMemory, &sizlPage, PU_ARBITRARY | GPIF_DEFAULT | GPIT_MICRO | GPIA_ASSOC) ; // Set page viewport for hpsMemory GpiQueryPageViewport (hps, &rclViewport) ; rclViewport.xRight -= rclViewport.xLeft ; rclViewport.yTop -= rclViewport.yBottom ; rclViewport.xLeft = 0 ; rclViewport.yBottom = 0 ; GpiSetPageViewport (hpsMemory, &rclViewport) ; // Get bitmap format of video display GpiQueryDeviceBitmapFormats (hps, 2L, alBitmapFormat) ; // Loop through possible color and size combinations for (sKing = 0 ; sKing < 2 ; sKing++) { // Determine pixel dimensions of bitmaps sizlPiece[sKing].cx = PIECE_XAXIS ; sizlPiece[sKing].cy = PIECE_YAXIS + (sKing + 1) * PIECE_HEIGHT ; GpiConvert (hpsMemory, CVTC_PAGE, CVTC_DEVICE, 1L, (PPOINTL) &sizlPiece[sKing]) ; sizlPiece[sKing].cx ++ ; sizlPiece[sKing].cy ++ ; // Set up BITMAPINFOHEADER structure bmp.cbFix = sizeof bmp ; bmp.cx = (SHORT) sizlPiece[sKing].cx ; bmp.cy = (SHORT) sizlPiece[sKing].cy ; bmp.cPlanes = (SHORT) alBitmapFormat[0] ; bmp.cBitCount = (SHORT) alBitmapFormat[1] ; // Create ahbmPiece bitmaps for (sColor = BLACK ; sColor <= WHITE ; sColor++) { ahbmPiece[sColor][sKing] = GpiCreateBitmap (hps, &bmp, 0L, 0L, NULL) ; GpiSetBitmap (hpsMemory, ahbmPiece[sColor][sKing]) ; CkdRenderPiece (hpsMemory, CLR_FALSE, sColor ? clrWhitePiece : clrBlackPiece, CLR_BLACK, sKing) ; } // Create ahbmMask bitmaps ahbmMask[sKing] = GpiCreateBitmap (hps, &bmp, 0L, 0L, NULL) ; GpiSetBitmap (hpsMemory, ahbmMask[sKing]) ; CkdRenderPiece (hpsMemory, CLR_TRUE, CLR_FALSE, CLR_FALSE, sKing) ; } GpiSetBitmap (hpsMemory, NULL) ; } /*--------------------------------------------------- CkdDestroyPieces: Destroy bitmaps used for pieces ---------------------------------------------------*/ VOID CkdDestroyPieces (VOID) { SHORT sColor, sKing ; for (sKing = 0 ; sKing < 2 ; sKing++) { for (sColor = BLACK ; sColor <= WHITE ; sColor++) if (ahbmPiece[sColor][sKing] != NULL) GpiDeleteBitmap (ahbmPiece[sColor][sKing]) ; if (ahbmMask[sKing] != NULL) GpiDeleteBitmap (ahbmMask[sKing]) ; } GpiDestroyPS (hpsMemory) ; DevCloseDC (hdcMemory) ; } /*--------------------------------------------------------------- CkdDrawWindowBackground: Fills entire window with background color -------------------------------------------------------------*/ VOID CkdDrawWindowBackground (HPS hps, HWND hwnd) { RECTL rcl ; WinQueryWindowRect (hwnd, &rcl) ; WinFillRect (hps, &rcl, clrBackground) ; } /*------------------------------------------------------------ CkdDrawWholeBoard: Draws the board squares and front edge -----------------------------------------------------------*/ VOID CkdDrawWholeBoard (HPS hps) { AREABUNDLE abnd ; LINEBUNDLE lbnd ; SHORT x ; POINTL aptl[4] ; CkdDrawAllBoardSquares (hps) ; GpiSavePS (hps) ; lbnd.lColor = CLR_BLACK ; GpiSetAttrs (hps, PRIM_LINE, LBB_COLOR, 0L, &lbnd) ; for (x = 0 ; x < 8 ; x++) { CkdQuerySquareCoords (x, 0, aptl) ; aptl[2].x = aptl[1].x ; aptl[2].y = aptl[1].y - BRD_EDGE ; aptl[3].x = aptl[0].x ; aptl[3].y = aptl[0].y - BRD_EDGE ; abnd.lColor = x & 1 ? clrWhiteSquare : clrBlackSquare ; GpiSetAttrs (hps, PRIM_AREA, LBB_COLOR, 0L, &abnd) ; GpiBeginArea (hps, BA_ALTERNATE | BA_BOUNDARY) ; GpiMove (hps, aptl + 3) ; GpiPolyLine (hps, 4L, aptl) ; GpiEndArea (hps) ; } GpiRestorePS (hps, -1L) ; } /*----------------------------------------------------- CkdDrawAllPieces: Draws all the pieces on the board -----------------------------------------------------*/ VOID CkdDrawAllPieces (HPS hps, BOARD *pbrd, SHORT sBottom) { SHORT x, y ; for (y = 0 ; y < 8 ; y++) for (x = 0 ; x < 8 ; x++) CkdDrawOnePiece (hps, x, y, pbrd, sBottom) ; } Interfacing OS/2 Compiled BASIC to Presentation Manager Lars Opstad and Arthur Hanson Microsoft OS/2 Presentation Manager (hereafter "PM") offers a message-based windowing system and high-resolution graphics. Until now, however, only C programmers have been able to take advantage of the graphic and windowing routines in the Microsoft OS/2 Presentation Manager Toolkit. This article can serve as the basis for adapting the PM Toolkit to BASIC and possibly to other languages. It assumes that you are familiar with the fundamental concepts of OS/2 programming. Adapting the PM Toolkit for use with BASIC involves several issues. Because of the limitations of BASIC, several interface functions require another language. These functions are presented here in both C and Assembler source code. The conversion of the C header files, found in the PM Toolkit, to BASIC INCLUDE files is demonstrated. Finally, a sample BASIC program using PM is presented. You will need, in addition to an OS/2 BASIC compiler, the OS/2 Presentation Manager Toolkit and one of the following: the BASIC OS/2 Presentation Manager Toolkit Supplement (an application note is available free of charge from the BASIC language support group of Microsoft Product Support at (206) 454-2030), the Microsoft C Compiler, or Microsoft Macro Assembler. Although the application note is not required, it will save many hours of work. Interface Functions An initial part of a PM program is registering a window class. To do this, you need the address of the window's message-handling function. Because you cannot get or manipulate the address of a function from within BASIC, some C and/or Assembler routines are required (see Figures 1 and 2). Both C and Assembler can return the address of a function. The sample routine, RegBas, is used with a BASIC function of a predefined name (ClientWndProc&). RegBas itself is used as the function pointer parameter when making PM calls. Because BASIC and C receive parameters differently in functions, an intermediate non-BASIC function must be used with RegBas. BASIC receives all parameters by reference; C receives them by value. Because PM follows the C convention, a BASIC function cannot be called by PM directly. Whenever a BASIC function must be registered with PM, an alternate non-BASIC routine must be registered instead. This translator routine accepts parameters using the C calling convention and then calls the BASIC routine using the BASIC calling convention. RegBas actually registers the translator routine BasClientWndProc, which in turn calls the BASIC routine ClientWndProc . PM functions deal with far addresses as a single element. BASIC, however, deals with segments and offsets, not with a combined far address. Again a non-BASIC routine can be used to combine or separate segments and offsets to allow BASIC to interface with PM. The function MakeLong takes two integer values and combines them to form a long. This is generally used to convert the values returned by VARSEG and VARPTR into a single far address. For variable-length strings, MakeLong is used with VARSEG and SADD to make the correct address. BreakLong, the inverse of MakeLong, breaks apart a long into two integer values. The first parameter is the long variable to be broken down. The hi-word is returned in the second parameter and the lo-word is returned in the third parameter. One use of BreakLong is to break a far address into a segment and an offset, which can then be used with DEF SEG and PEEK or POKE. Include File Conversion Process After the interface functions are created, the C header files need to be converted. The PM Toolkit contains the header files used to define the PM functions for the C compiler; these need to be converted into BASIC INCLUDE files for use with the BASIC compiler. The application note contains all the header files in BASIC format. The C header files in the Toolkit use the Hungarian naming convention, in which the name of a type describes its use. For example, the pointer and handle types are identified by the first character of each type: p for pointers and h for handles. This naming convention allows types to be identified easily for conversion to BASIC or to any other language. In the first part of the conversion, the fundamental types are converted from C types to BASIC types. All C types, except structures, can be converted to three types in BASIC: STRING * 1, INTEGER, and LONG. The STRING * 1 type can only be used in TYPE ... END TYPEs converted from C structures containing CHAR, UCHAR, or BYTE elements. Because BASIC can neither pass single-byte parameters by value (using the BYVAL keyword) nor return single-byte FUNCTION values, all functions in either of these categories should be commented out. The INTEGER type is the most straightforward. The C types that correspond to INTEGERs are INT, SHORT, USHORT, and BOOL. The LONG is basically a catchall. Besides the obvious C types of LONG and ULONG, all pointers and handles are converted to LONGs. For consistency, the LONG type was chosen for the pointers instead of the segment and offset convention. To pass pointers to data items, you need to use the MakeLong function (from Figure 1 or 2) with the VARSEG and VARPTR functions built into BASIC. (VARSEG and SADD should be used with MakeLong when referencing variable-length strings.) In addition to these standard types, many miscellaneous types (MPARAM, for example) fall into the LONG category. After the fundamental types are converted, the functions and structures must be converted (see Figure 3). Because BASIC's compiler workspace is limited, some converted header files must be split into smaller INCLUDE files. The smaller header files such as PMSHL.H (9Kb) do not need further division, but PMWIN.H (78Kb) and PMGPI.H (55Kb) each need to be divided. PM Overview PM uses a message-based windowed system. This means that all user input comes to the program in the form of messages (see Figure 4). Messages usually come from the user in the form of a mouse click, a key press, or a menu selection. The message is then passed to the PM Message Dispatcher to determine which program should receive the message; the PM Message Dispatcher then places the message in that program's message queue. PM calls the registered message-handling function to process the messages in the program's message queue. In the sample program, the translator function BasClientWndProc is called, which in turn calls the actual BASIC message-handling function, ClientWndProc. The program continually receives messages from and dispatches messages to the appropriate window procedures. These messages are passed to the window's procedure (discussed below) where they are processed or passed on to PM's default window procedure. Every PM program has a special structure that is used to provide a smooth interface to the PM messaging system. In a PM-BASIC program, the interface routines are needed to conform to the special structure (see Figure 5). The first component of the PM-BASIC program--or of any OS/2 program--is a definition file (see Figure 6). The definition file specifies the name of the program, the type of the program, heapsize, stacksize, and global procedures. The structure of a definition file is the same for a program in any language. The easiest way to show the relationship of the interface functions, converted header files, and BASIC code is to examine a sample PM-BASIC program (see Figure 7). The first part of the source code contains constants and function declarations converted from the PM Toolkit's C header files. The first section of the actual code is the initialization section. During initialization, an anchor block is set up (WinInitialize), a message queue is created (WinCreateMsgQueue), a window class is registered (WinRegisterClass), and a standard window is created (WinCreateStdWindow). After WinInitialize creates an anchor block that links the program to the system, a message queue can be set up by calling WinCreateMsgQueue. The message queue stores messages returned by the system. Every window is based on a window "class." A window class determines which window procedure is used to process messages. WinRegisterClass is important because it registers a new window class with PM and defines the function to be called to process messages for that class of window. RegBas is used to return the address of the BASIC message processing procedure. WinCreateStdWindow next creates a window of the newly defined class. The window's appearance is controlled by flFrameFlags. The window will have a standard title bar, a system menu, minimize and maximize icons, and a border for resizing. It will also have a menu and will appear on the system task list. This is accomplished by ORing constants togther to set bits in flFrameFlags. The second part of the PM program is the message processing loop. This loop consists of two function calls: WinGetMsg and WinDispatchMsg. WinGetMsg will succeed (that is, return a nonzero) until a WMQUIT message is received. A WMQUIT is sent whenever you close a standard window. Because PM programs are message-based, it is imperative to keep this loop tight. PM requires that messages be processed in a limited time period and will give an error message when this limit is reached. The code that actually processes messages is in the function ClientWndProc. Because the naming convention of the interface routines is arbitrary, the name ClientWndProc& must be used for the window function. The preferred structure of a window procedure is a SELECT CASE statement (equivalent to a C switch statement) with CASEs for the messages you want to handle explicitly. For messages you don't want to process, transfer control to the default window procedure, WinDefWindowProc. The return value of ClientWndProc is initially set to zero, on the assumption that the messages are processed correctly. Because only one CASE returns a value other then zero (caused if the message must be passed to PM's default message-handling procedure), assuming a zero return value saves code space. In this program three messages are processed. WMSIZE is received whenever the user resizes the program window. Processing this message ensures that the displayed picture is always proportional to the window size. WMPAINT is used to display the picture. It is received whenever the contents of the window need to be generated or redisplayed. Any program containing a WMPAINT case must use the functions WinBeginPaint and WinEndPaint. In most programs in which the window contents change, a WinInvalidateRect function should be used before a WinBeginPaint function. The WinInvalidateRect function tells PM what part of the current window needs to be repainted. For example, if you move another window over part of the current window and then remove it, that part of the current window would need to be repainted. Specifying a zero as the second parameter in the call to WinInvalidateRect invalidates the entire window. WinBegin/EndPaint is the routine that handles repainting the windows. GpiErase erases the invalidated region by filling in the entire region with the background color. In the sample program, the actual drawing is accomplished with four other GPI functions. First, the points for the drawing are chosen at random. After the points have been assigned, a GpiMove is executed to the last generated random point. This is used to set the starting location. GpiBeginArea and GpiEndArea are used to mark a block of graphic statements, which will be filled using a specified method. In this program, the filling is specified as alternating fill (BAALTERNATE) with no boundary (BANOBOUNDARY). The only actual graphics statement is GpiPolyFillet, which draws a pattern specified by the array of points. This statement creates a checkered pattern of randomly shaped areas. Finally, WMCOMMAND is received whenever the user selects a menu item. The user can select from the menu how many random points the drawing should contain. It is necessary to use WinSendMsg with WMPAINT to cause the window to be redrawn with the newly specified number of points. If the received message is not one of the above three, control is passed to PM's default message-handling procedure (WinDefWindowProc). The last part of a PM program is the finalize section, which is very similar to the initialization section, but the order is reversed. First you destroy the window handle (WinDestroyWindow) and the message queue (WinDestroyMsgQueue) and then release the anchor block (WinTerminate). The third file necessary for this sample is a resource script file (see Figure 8). Resources are used with the Resource Compiler (RC.EXE) from the PM Toolkit to create menus, accelerators, and dialog boxes, to name a few. The sample program's resource script file is simple because it deals only with menus. The menu defined has one topic with two choices, Points and Exit, and a menu separator between them. The Points option has a submenu that lets the user choose the number of random points. Note that the tilde (~) precedes the key to press to select the menu item. Limitations Although this article describes how to program PM in BASIC, several aspects of the BASIC language limit which PM functions can be used. The most significant limitation is that the BASIC compiler has a smaller workspace than that of the C compiler. This limits the number of PM functions that may be declared in a single BASIC module. You will encounter this limit in developing most PM applications with BASIC. When you include too many files, the compiler error "Out of Memory" will occur. The solution is to break the program into smaller modules, which can be linked together. Another limitation is that BASIC has no single byte type. Several PM functions either return a CHAR or require a CHAR as a parameter; these functions cannot be called from BASIC. Finally, the BASIC run time is not reentrant. Therefore, neither dynamic-link libraries (DLLs) nor multithreaded programs are possible with BASIC. Without threads, problems can arise when a program has a lengthy processing loop. Generally, a C program spawns a thread to deal with a lengthy process; because this is not possible in BASIC, alternative methods must be used. One such method is to start a timer that will cause a part of the loop to be executed every time a WMTIMER message is received. Adapting BASIC for use with the PM environment is not simple. The guidelines presented here will simplify this task and allow access to the extensive capabilities of PM. With slight modifications, these techniques can retrofit other OS/2 compilers. Figure 1 ; Program Name: RegBas.ASM ; ; Functions : ; BreakLong ; MakeLong ; BasClientWndProc ; RegBas ; ; Description : This supplies interface routines for BASIC programs. ; RegBas is used to register window classes from BASIC. ; Also provided are the utility routines: BreakLong and ; MakeLong. DOSSEG .286 ; OS/2 runs on 286/386 machines .model medium, PASCAL ; Medium memory model when interfacing with BASIC ; PASCAL and BASIC use same calling conventions .data hwndb1 DW ? ; set up local data for BasClientWndProc hwndb2 DW ? msgb DW ? mp1b1 DW ? mp1b2 DW ? mp2b1 DW ? mp2b2 DW ? .code EXTRN ClientWndProc:PROC ; BASIC function to call PUBLIC BasClientWndProc, BreakLong, MakeLong, RegBas ; Breaklong(Long, hi_word, lo_word) ; BreakLong PROC FAR USES bx, long1:WORD, long2:WORD, hiword:WORD, loword:WORD mov bx, long1 ; Get the hi-byte of the long mov ax, bx mov bx, hiword ; Return it to hiword mov [bx], ax mov bx, long2 ; Get the lo-byte mov ax, bx mov bx, loword ; Return it to loword mov [bx], ax ret BreakLong ENDP ; MakeLong&(hi_word, low_word) ; MakeLong PROC FAR USES bx, hiword:WORD, loword:WORD mov bx, hiword mov ax, bx mov dx, ax ; Return hi-word in dx mov bx, loword ; Lo-word in ax for function mov ax, bx ; Returning a long ret MakeLong ENDP BasClientWndProc PROC hwnd1:word, hwnd2:word, msg:word, mp11:word, mp12:word, mp21:word, mp22:word push ds mov ax, @data ; Get our data segment mov ds, ax mov ax, hwnd2 ; Transfer the values passed mov hwndb1, ax ; from PM to local variables mov ax, hwnd1 ; for the call to BASIC mov hwndb2, ax mov ax, msg mov msgb, ax mov ax, mp12 mov mp1b1, ax mov ax, mp11 mov mp1b2, ax mov ax, mp22 mov mp2b1, ax mov ax, mp21 mov mp2b2, ax mov ax, OFFSET hwndb1 ; Set up for call to BASIC push ax ; BASIC expects values to mov ax, OFFSET msgb ; be passed by reference. push ax mov ax, OFFSET mp1b1 push ax mov ax, OFFSET mp2b1 push ax call ClientWndProc ; Call BASIC routine - note pop ds ; return values are already ret ; in dx, ax so we don't have ; to do anything. BasClientWndProc ENDP RegBas PROC mov dx, SEG BasClientWndProc ; Return address of mov ax, OFFSET BasClientWndProc ; BASIC routine. ret RegBas ENDP Figure 2 #define INCL_WIN #include <os2.h> extern MRESULT EXPENTRY ClientWndProc ( long near *, int near *, long near *, long near *); MRESULT EXPENTRY BasClientWndProc(HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2) { static long near hwndb; static int near msgb; static long near mp1b; static long near mp2b; hwndb = (long)hwnd; msgb = (int)msg; mp1b = (long)mp1; mp2b = (long) mp2; return ClientWndProc(&hwndb, &msgb, &mp1b, &mp2b); } /* BasClientWndProc */ long pascal far RegBas(void) { return (long) BasClientWndProc; } /* regbas */ long pascal Makelong (long passedlong) { return passedlong; } /* MakeLong */ void pascal BreakLong(int hiword, int loword, int *phiword, int *ploword) { *phiword = hiword; *ploword = loword; } /* BreakLong */ Figure 3 After the types are set, the conversion of the constants, structures, and functions begins. The steps for this are as follows: 1 Remove macros and "#ifdef"s since BASIC does not support these: substitute inline code 2 For comments, do the following: ■ Change the open comments from "/* " to apostrophe and remove the closing "*/ " ■ Place an apostrophe on each line of multi-line comment ■ Code cannot follow a comment in BASIC C BASIC /* This is a ' This is a multi-line comment */ ' multi-line comment 3 For constants, do the following: ■ Change "#define" to "CONST" ■ Remove underscores from constant names ■ Change "0x" to "&H" for hexadecimal values ■ Change trailing "L"s to "&"s to indicate longs where necessary ■ Place equal signs ■ Note: Byte constants are not possible C BASIC #define A_LONG 0x01L CONST ALONG = &H01& #define A_BYTE 27 'byte constant C name: A_BYTE, value:27 4 For structures, do the following: ■ Change "typedef struct <type name> {" to "TYPE <type name>" ■ Remove underscores from type names ■ Change C format <type> <elem> to BASIC format <elem> AS <type> ■ For any array types, change to <elem>0 .. <elem>n-1 ■ Change "}" (to mark end of structure) to "END TYPE" C BASIC typedef struct A_STRUCT { TYPE ASTRUCT int elem1 elem1 AS INTEGER pvoid elem2 elem2 AS LONG char elem3[2] elem30 AS STRING * 1 } elem31 AS STRING * 1 END TYPE 5 For functions, do the following: ■ Place "DECLARE FUNCTION" on each function line ■ Remove type from before function name and place appropriate suffix (& or %) ■ Change param format from <type> <par> to "BYVAL <par> AS <type>". If no <par> name is given, use C type name or slight variation) ■ Place underscore at end of each continued line C BASIC BOOL AFunc(int p1, DECLARE FUNCTION AFunc%(BYVAL p1 AS INTEGER,_ HMODULE, BYVAL HMODULE AS LONG,_ LONG) BYVAL aLONG AS LONG) Figure 6 NAME PMBASIC WINDOWAPI DESCRIPTION 'PM Demo Program written in BASIC' PROTMODE HEAPSIZE 1024 STACKSIZE 8192 EXPORTS BasClientWndProc Figure 7 '+------------------------------------------------------------------- '| Program Name: PMBasic.BAS '| '| Description: This program gives a simple demo of a Presentation '| Manager program written with BASIC Compiler Version '| 6.00 and the BASIC OS/2 Presentation Manager Toolkit '| Supplement. '| This program draws a figure with GpiBegin/EndArea and '| GpiPolyFillet. It allows the user to choose--with '| a menu defined in the resource script file, '| PMBasic.RC--the number of random points used in the '| area drawn. '+------------------------------------------------------------------- '***** Type definitions TYPE POINTL x AS LONG y AS LONG END TYPE TYPE QMSG hwnd AS LONG msg AS INTEGER mp1 AS LONG mp2 AS LONG time AS LONG ptl AS LONG END TYPE '***** CONSTant definitions CONST FCFTITLEBAR = &H00000001 : CONST FCFSYSMENU = &H00000002 CONST FCFMENU = &H00000004 : CONST FCFSIZEBORDER = &H00000008 CONST FCFMINMAX = &H00000030 : CONST FCFSHELLPOSITION = &H00000400 CONST FCFTASKLIST = &H00000800 : CONST CSSIZEREDRAW = &H00000004 CONST HWNDDESKTOP = &H00000001 : CONST WSVISIBLE = &H80000000 CONST WMSIZE = &H0007 : CONST WMPAINT = &H0023_ : CONST WMCOMMAND = &H0020 CONST BANOBOUNDARY = 0 : CONST BAALTERNATE = 0 '***** FUNCTION declarations DECLARE FUNCTION WinInitialize& (BYVAL ushort AS INTEGER) DECLARE FUNCTION WinCreateMsgQueue&(BYVAL hab AS LONG,_ BYVAL cmsg AS INTEGER) DECLARE FUNCTION WinRegisterClass%(BYVAL hab AS LONG,_ BYVAL pszCN AS LONG, BYVAL pfnWP AS LONG,_ BYVAL flSty AS LONG, BYVAL cbWD AS INTEGER) DECLARE FUNCTION MakeLong&(BYVAL hiword AS INTEGER,_ BYVAL loword AS INTEGER) DECLARE FUNCTION RegBas& DECLARE FUNCTION WinCreateStdWindow&(BYVAL hwndP AS LONG,_ BYVAL flS AS LONG, BYVAL pflCF AS LONG,_ BYVAL pszC AS LONG, BYVAL pszT AS LONG,_ BYVAL styC AS LONG, BYVAL hmod AS INTEGER,_ BYVAL idRes AS INTEGER, BYVAL phwnd AS LONG) DECLARE FUNCTION WinGetMsg%(BYVAL hab AS LONG, BYVAL pqmsg AS LONG,_ BYVAL hwndF AS LONG, BYVAL msgFF AS INTEGER,_ BYVAL msgFL AS INTEGER) DECLARE FUNCTION WinDispatchMsg&(BYVAL hab AS LONG,_ BYVAL pqmsg AS LONG) DECLARE FUNCTION WinDestroyWindow%(BYVAL hwnd AS LONG) DECLARE FUNCTION WinDestroyMsgQueue%(BYVAL hmq AS LONG) DECLARE FUNCTION WinTerminate%(BYVAL hab AS LONG) DECLARE SUB BreakLong(BYVAL along AS LONG, hiword AS INTEGER,_ loword AS INTEGER) DECLARE FUNCTION WinInvalidateRect%(BYVAL hwnd AS LONG,_ BYVAL pwrc AS LONG,_ BYVAL fIC AS INTEGER) DECLARE FUNCTION WinBeginPaint&(BYVAL hwnd AS LONG,_ BYVAL hps AS LONG, BYVAL prcl AS LONG) DECLARE FUNCTION GpiErase%(BYVAL HPS AS LONG) DECLARE FUNCTION GpiBeginArea%(BYVAL HPS AS LONG, BYVAL ULONG AS LONG) DECLARE FUNCTION GpiMove%(BYVAL HPS AS LONG, BYVAL PPTL AS LONG) DECLARE FUNCTION GpiPolyFillet&(BYVAL HPS AS LONG,_ BYVAL ALONG AS LONG, BYVAL PPTL AS LONG) DECLARE FUNCTION GpiEndArea&(BYVAL HPS AS LONG) DECLARE FUNCTION WinEndPaint%(BYVAL hps AS LONG) DECLARE FUNCTION WinSendMsg&(BYVAL hwnd AS LONG,_ BYVAL msg AS INTEGER,_ BYVAL mp1 AS LONG,_ BYVAL mp2 AS LONG) DECLARE FUNCTION WinDefWindowProc&(BYVAL hwnd AS LONG,_ BYVAL msg AS INTEGER,_ BYVAL mp1 AS LONG,_ BYVAL mp2 AS LONG) '********* Initialization section *********** DIM aqmsg AS QMSG flFrameFlags& = FCFTITLEBAR OR FCFSYSMENU OR FCFSIZEBORDER OR_ FCFMENU OR FCFMINMAX OR FCFTASKLIST OR_ FCFSHELLPOSITION Class$ = "ClassName" + CHR$(0) hab& = WinInitialize(0) hmq& = WinCreateMsgQueue(hab&, 0) bool% = WinRegisterClass(hab&,_ MakeLong(VARSEG(Class$), SADD(Class$)),_ RegBas, CSSIZEREDRAW, 0) hwndFrame& = WinCreateStdWindow(HWNDDESKTOP, WSVISIBLE,_ MakeLong(VARSEG(flFrameFlags&), VARPTR(flFrameFlags&)),_ MakeLong(VARSEG(Class$), SADD(Class$)), 0, 0, 0, 1,_ MakeLong(VARSEG(hwndClient&), VARPTR(hwndClient&))) '************* Message loop *************** WHILE WinGetMsg(hab&, MakeLong(VARSEG(aqmsg), VARPTR(aqmsg)), 0, 0, 0) bool% = WinDispatchMsg(hab&, MakeLong(VARSEG(aqmsg), VARPTR(aqmsg))) WEND '*********** Finalize section *************** bool% = WinDestroyWindow(hwndFrame&) bool% = WinDestroyMsgQueue(hmq&) bool% = WinTerminate(hab&) END '*********** Window procedure *************** FUNCTION ClientWndProc&(hwnd&, msg%, mp1&, mp2&) STATIC ClientWndProc& = 0 SELECT CASE msg% CASE WMSIZE 'Store size to make area proportional to window CALL BreakLong(mp2&, cyClient%, cxClient%) CASE WMPAINT 'Paint window with PolyFillet with (pts%) rand pts ' Invalidate to paint whole window bool% = WinInvalidateRect(hwnd&, 0, 0) hps& = WinBeginPaint(hwnd&, 0, 0) 'Begin painting bool% = GpiErase(hps&) 'Erase window '***Set up array of random points. Number of points set w/ menu. IF pts% = 0 THEN pts% = 50 REDIM aptl(pts%) AS POINTL FOR I% = 0 to pts% aptl(I%).x = cxClient% * RND : aptl(I%).y = cyClient% * RND NEXT I% '*** Start at last pt and draw PolyFillet through all pts ' alternating fill bool% = GpiMove(hps&, MakeLong(VARSEG(aptl(pts%)),_ VARPTR(aptl(pts%)))) bool% = GpiBeginArea (hps&, BAALTERNATE OR BANOBOUNDARY) bool% = GpiPolyFillet(hps&, pts% + 1,_ MakeLong(VARSEG(aptl(0)), VARPTR(aptl(0)))) bool% = GpiEndArea(hps&) bool% = WinEndPaint(hps&) CASE WMCOMMAND 'Menu item sets number of pts to use in drawing. CALL BreakLong(mp1&, hiword%, pts%) bool% = WinSendMsg(hwnd&, WMPAINT, 0, 0) 'Send WMPAINT to draw CASE ELSE 'Pass control to system for other messages ClientWndProc& = WinDefWindowProc(hwnd&, msg%, mp1&, mp2&) END SELECT END FUNCTION Figure 8 /*-------------------------------------------------------------------- | Program Name: PMBasic.RC | | Description: This is the resource script file for PMBasic.BAS. | A menu containing nested submenus is created to allow the user to | select the number of points to be used in the figure in | PMBasic.BAS. | | NOTE: Because the "Exit" item has the MIS_SYSCOMMAND, there is no | command message to be processed in PMBasic.BAS. This item will | automatically close the window. +--------------------------------------------------------------------/ #include <os2.h> MENU 1 { SUBMENU "~PM Basic Demo", 1 { SUBMENU "~Points", 2 { MENUITEM "~10", 10 MENUITEM "~20", 20 MENUITEM "~30", 30 MENUITEM "~40", 40 MENUITEM "~50", 50 MENUITEM "~60", 60 MENUITEM "~70", 70 MENUITEM "~80", 80 MENUITEM "~90", 90 } MENUITEM SEPARATOR MENUITEM "E~xit", SC_CLOSE, MIS_SYSCOMMAND } } DOS Commands Inside Your Code: Process Control and Signal Handling Kris Jamsa As the complexity of your applications increases, you might need to access DOS1 commands, such as PRINT, BACKUP, or RESTORE, from within your programs. You might even want to let the user exit your application temporarily to issue commands at the DOS prompt. When the user wants to return to your application, he or she can use the DOS EXIT command. In this article we examine several ways to access DOS commands from within your program. In addition, we look at several exceptions, such as Ctrl-Break, that can occur during the execution of your program and how you can install functions that handle them. Invoking DOS Commands Inside Your Programs The easiest method to invoke DOS commands from within your program is to use the C run-time library function "system." The following program (DOSDIR.C) uses the system function to execute the DOS DIR command: #include <process.h> /* needed for system */ main() { system("DIR"); } The DOS DIR command is an internal DOS command. Had the program invoked an external DOS command instead, such as DISKCOPY or BACKUP, DOS would first search the location specified in the function argument, then the current directory, and following that, the locations identified in the PATH= environment entry. The system function is helpful in other ways. Most users like the convenience of working with familiar applications. By using system, your programs can satisfy this preference by executing the end user's word processor, spreadsheet, or other application. Likewise, system provides a way for users to exit an application temporarily to look up files or to perform other DOS commands. The following program (SYS.C) uses system to exit the application temporarily so that the user can issue DOS commands. When the user finishes executing commands at the DOS prompt, he or she can type EXIT and press Enter to return to the program. #include <process.h> /* needed for system */ #include <stdio.h> /* needed for getchar */ main() { printf("Type EXIT to continue"); system("COMMAND"); printf("Back in application - press Enter\n"); getchar(); } To give you greater control, the run-time library also provides two series of routines that access DOS commands: exec functions and spawn functions. The exec functions invoke a DOS command without returning control to the program when the command completes. The C run-time library provides eight forms of the exec function--execl, execle, execlp, execlpe, execv, execve, execvp, and execvpe. In general, the differences among them are the ways the calling program passes command line arguments and environment entries to the command and the directories DOS searches to locate a command. The letters in the function name suffix have the meanings described in Figure 1. To demonstrate the way various exec functions work, let's create a program you can invoke with them. Enter and compile the following program (SHOW.C), which displays its command line arguments and environment entries: main(int argc, char *argv[], char *env[]) { while (*argv) /* display command line arguments */ puts(*argv++); while (*env) /* display environment entries */ puts(*env++); } The following program (RUNSHOW.C) uses the execl function to execute SHOW.EXE. The output of SHOW reveals that it receives the arguments specified in RUNSHOW and a copy of the RUNSHOW environment entries. #include <process.h> /* needed for execl */ #include <stdio.h> /* needed for NULL */ main() { execl("SHOW.EXE", "SHOW", "A", "B", "C", NULL); printf("This line will never be executed\n"); } Let's look at the arguments for execl. The first argument is a character string that identifies the program to execute. If the program does not reside in the current directory, specify a complete pathname. Remember, execl does not support the PATH environment variable. The remaining string arguments represent entries on the DOS command line, argv[0] through argv[3]. The NULL pointer terminates the argument list. Note that the printf statement never executes: when you use an exec function, your program does not resume control. You might modify RUNSHOW.C slightly by replacing execl with execlp. The new version would support the PATH environment entry. The following program (RUNSHOW2.C) uses the C run-time library function execle to execute SHOW.EXE. RUNSHOW2.C defines a new environment and passes it to SHOW.EXE: #include <process.h> /* needed for execle */ #include <stdio.h> /* needed for NULL */ main() { char static *new_env[4] = { "BOOK=C SSS", "CHAPTER=35", "COMPILER=MSC" }; execle("SHOW.EXE", "SHOW", "A", "B", "C", NULL, new_env); } When you run this program, your screen shows SHOW.EXE A B C BOOK=C SSS CHAPTER=35 COMPILER=MSC Each of the preceding exec functions passes the command line entries as a variable number of arguments. The next set of exec functions passes the command line as an array of pointers to character strings. The following program (RUNSHOW3.C) defines an array of pointers to character strings that serve as the command line arguments for SHOW.EXE. The program uses the execv function to invoke SHOW. #include <process.h> /* needed for execv */ main() { static char *args[4] = { "SHOW", "A", "B" }; execv("SHOW.EXE", args); } Try substituting execvp for execv in RUNSHOW3.C. Then experiment with different locations of SHOW.EXE. As long as the new location is identified in your PATH= entry, execvp is able to run SHOW. The following program (RUNSHOW4.C) defines an array of pointers to the command line and an array of pointers to environment entries. The program calls execve, which uses these arrays to execute SHOW.EXE. #include <process.h> /* needed for execve */ main() { static char *args[4] = { "SHOW", "A", "B" }; static char *new_env[4] = { "BOOK=C SSS", "CHAPTER=35", "COMPILER=MSC" }; execve("SHOW.EXE", args, new_env); } The exec functions are useful when you do not want control to return to your program. But if your program needs to regain control when the command is complete, the program must use one of the spawn functions. The C run-time library provides eight varieties of spawn functions, just as it does for exec. Let's examine the two primary spawn routines, spawnlpe and spawnvpe. The various suffix letters have the same meanings for spawn functions as they do for the exec set of functions. Figure 2 summarizes the spawn functions. The spawn functions are convenient in that they let an application execute a second program and then--if the mode flag is set to P_WAIT--regain control when the program is complete. Consequently, most applications use the P_WAIT flag when they call spawn functions. If an application does not need to regain control after it executes a command, it can use an exec function. The following program (RUNSHOW5.C) uses spawnlpe to execute SHOW.EXE. The program creates an array of pointers to environment entries and passes them to spawnlpe. When SHOW.EXE is complete, DOS returns control to RUNSHOW5, which displays a message to verify the return of execution. #include <process.h> /* needed for spawnlpe */ #include <stdio.h> /* needed for NULL */ main() { static char *new_env[4] = { "BOOK=C SSS", "CHAPTER=35", "COMPILER=MSC" }; spawnlpe(P_WAIT, "SHOW.EXE", "A", "B", NULL, new_env); printf("Back from spawn\n"); } Programs can return an exit status when they end. If you spawn a program, the spawn function returns the exit status of the program. The following program (RUNSHOW6.C) uses spawnvpe to execute SHOW.EXE. When DOS returns control to RUNSHOW6, the program displays SHOW's exit status. #include <process.h> /* needed for spawnvpe */ main() { static char *new_env[4] = { "BOOK=C SSS", "CHAPTER=35", "COMPILER=MSC" }; static char *args[4] = { "SHOW", "A", "B" }; int result; result = spawnvpe(P_WAIT, "SHOW.EXE", args, new_env); printf("Back from spawn - Exit value is %d\n", result); } Signal Handling As operating systems offer a richer set of services, we will find that the operating system treats more and more events in the system as signals. A signal can be viewed as an alarm the operating system rings when a specific event occurs. Each unique signal has its own alarm. Under DOS, the three signals your programs can receive are SIGINT, which occurs when a user presses Ctrl-Break; SIGFPE, which occurs when a floating point error (such as division by zero) is detected; and SIGABRT, which occurs when a program aborts with an exit status of 3. The C run-time library function signal, defined in the include file signal.h, lets you install a function that executes when a specific signal occurs. The signal.h file should not be used under OS/2. The following program (CBREAK.C) defines the function ctrl_break and installs it to handle the SIGINT signal, which occurs when the user presses Ctrl-Break. Our handler ignores the signal until the user has pressed Ctrl-Break five times. On the fifth occurrence of Ctrl-Break, the program ends. #include <signal.h> /* needed for signal */ #include <stdio.h> /* needed for getchar */ #include <process.h> /* needed for exit */ main() { int ctrl_break(); signal(SIGINT, ctrl_break); printf("Press Ctrl-Break 5 times to terminate program\n"); getchar(); } ctrl_break() { static int count = 1; signal(SIGINT, SIG_IGN); if (count++ = = 5) { printf("Program terminated by Ctrl-Break\n"); exit(); } signal(SIGINT, ctrl_break); } The function ctrl_break is the signal handler. It directs DOS to ignore Ctrl-Break signals that occur while the handler is executing. The routine then increments a count of the number of Ctrl-Break signals that have occurred. Last, the function reinstalls itself as the Ctrl-Break handler. Our ctrl_break function calls signal with SIG_IGN, which directs DOS to ignore incoming signals of the same type while the handler executes. After the handler performs its processing, it uses signal to reinstall itself as the Ctrl-Break handler. As you begin to write signal handlers, you will find that testing a handler is, in some cases, quite difficult. To assist you, the C run-time library provides the raise function. The following program (RAISE.C) defines the function abort_handler and installs it as the SIGABRT handler. Next the program raises the signal so that you can test your handler. #include <signal.h> /* needed for signal, raise, and constants */ #include <stdio.h> /* needed for fcloseall */ main() { int abort_handler(); signal(SIGABRT, abort_handler); raise(SIGABRT); } abort_handler() { signal(SIGABRT, SIG_IGN); printf("In abort handler -- closing all open files\n"); fcloseall(); } By using raise in this manner, your program can test your handler and verify that it responds to SIGABRT. You might then replace the call to raise with a call to the run-time library function abort and so verify that DOS raises the SIGABRT signal when the program aborts. Signal processing is still in its infancy. With the advent of OS/2, you will see its use grow, as signals become an important tool for interprocess communication. Figure 1 Meaning of Letters in Function Name Suffixes Letter Meaning l Passes a variable number of command line arguments, the last being NULL v Passes an array of pointers to command line arguments p Supports the PATH environment variable e Passes an array of pointers to environment entries Figure 2 The spawn Functions Behave as Their Suffixes Specify Function Name PATH= Support Environment Entries Command Line Entries spawnl No Copies current NULL-terminated list spawnle No Array argument NULL-terminated list spawnlp Yes Copies current NULL-terminated list spawnlpe Yes Array argument NULL-terminated list spawnv No Copies current Array argument spawnve No Array argument Array argument spawnvp Yes Copies current Array argument spawnvpe Yes Array argument Array argument ────────────────────────────────────────────────────────────────────────────

Volume 5 - Number 2

──────────────────────────────────────────────────────────────────────────── Using the OS/2 National Language Support Services to Write International Programs Asmus Freytag and Michael Leu The OS/2 operating system is designed to be an international operating system. Localized editions of OS/21 Version 1.2 now exist for Japan, Korea, and most of Europe. Application writers can use the National Language Support (NLS) system services provided by OS/2 to write programs that support many national languages automatically (see Figure 1). Programs such as word processors that require more detailed linguistic knowledge can bypass the default functionality of OS/2 to exploit its more advanced NLS features. The process of modifying a program for use in a different country is called localization. Ideally, applications have only one set of source code for all languages, instead of one version for each market. Localization decreases source code maintenance costs and makes it much easier to release the product simultaneously in multiple markets. This article discusses OS/2 NLS functions and localization principles and demonstrates them using two sample programs. Localizing a piece of software involves much more than merely translating text messages. Methods of processing and formatting data as well as handling input may differ from country to country. In addition, an application may have to deal with specialized, country-specific hardware. Code Pages and Country Codes First, you must understand how OS/2 uses code pages to represent text data. Data is merely a stream of bytes. A code page is a table that maps byte values (or code points) to a set of characters or glyphs (picture characters). Code pages are uniquely numbered (for example, 437 is the U.S. code page), so if you specify a code page number with a stream of bytes, you have ensured that your data can always be interpreted correctly. Another factor in determining how data will be processed is the country code. A country code is a three digit number based on those used by the international phone system to designate countries (001 represents the U. S., 081 represents Japan, and so on). In OS/2, country codes specify the country rules that should be used in operation (for example, how to format number, date, and time strings); it also restricts the list of possible valid code pages. Unlike the code page, the system country code cannot be changed at run time. However, different values can be specified to OS/2 function calls that take the country code as an argument. Code pages 850 and 863 are shown in Figure 2. As you can see from the code page 850 and 863 tables, the same code point may reference different characters in different code pages. However, while code pages differ in their representation of non-English characters and symbols, most map the bytes in the 20H to 7EH range to the printable ASCII characters. This allows ASCII strings to be preserved when the code page is changed. As expected, many system functions depend on the characters or glyphs of the passed arguments, not on their binary value. When the application calls a function, the code page to be used for interpreting the binary values must be established. Specifying the Code Page and Country Code In the CONFIG.SYS file, there are two statements that initialize the code page and country code for all OS/2 Vio sessions. The COUNTRY configuration command specifies the country code and the location of the file COUNTRY.SYS, which contains country-specific information. The CODEPAGE configuration command allows you to specify up to two code pages (restricted by the country code), which are prepared, and which can be selected by the Change Codepage command. The DosGetCp call can be used by applications to obtain the prepared code pages. If no code pages are specified, DosGetCp returns zero for the code page value, and the system uses the character set supported in the hardware (the ROM code page). For an application that needs to know its code page, use the code in Figure 3. Usually this code is unnecessary since most Dos APIs will accept zero as a code page argument and will then use the process code page. Each OS/2 process has an associated process code page. The process code page is used by Dos API functions, such as the file system calls DosOpen, DosFindFirst, and DosFindNext to interpret string parameters. This code page is initially the primary code page defined in CONFIG.SYS. Furthermore, every OS/2 subsystem, such as Vio for screen output and Kbd for keyboard handling, has its own code page. Given this, the code page is inherited as follows: DosSetCp will set the process and all subsystem code pages or DosSetProcCp can be called to set the process code page but not the subsystem code pages. The default code page settings from CONFIG.SYS can be overridden by calling subsystem-specific calls, such as VioSetCp and KbdSetCp. However, note that subsystems are shared among all processes of a session and there is no notification of a code page switch to the other processes. In OS/2 Presentation Manager (hereafter "PM"), code pages are associated with message queues and presentation spaces (PSs). To change the message queue code page, call WinSetCp. This changes the keyboard translation tables for that message queue (WM_CHAR messages will contain characters in the specified code page); and it also defines the default code page of all new PSs associated with the message queue. To override this inheritance mechanism explicitly, the PS code page can be switched by calling GpiSetCp. This will cause all output to the PS to be interpreted according to the newly specified code page. Existing PSs are not affected by a switch of the queue code page; however, WinBeginPaint implicitly creates a new PS each time it is called. Thus, after a WinSetCp call, WinBeginPaint will create PSs with the new queue code page. Queue code pages may only be set to those code pages available to non-PM programs as indicated by DosGetCp. (The only exception to this is the output-only desktop publishing code page 1004.) If DosGetCp returns 0, the queue code page will default to 850 (the multilingual code page) if the country setting is European or American. In Asian OS/2, the queue code page will default to the primary Asian code page. The set of code pages available for PSs is a superset of the set of available queue code pages. WinQueryCpList provides a list of all available code pages for the GpiSetCp call. This allows the use of special desktop publishing code pages as well as EBCDIC code pages for output purposes. Note that even though WinSetCp and GpiSetCp have been used to set the queue and PS code pages, the file system code page will still be dependent on the process code page (which may be set using DosSetCp or DosSetProcCp). Country settings can be set in the Control Panel. These options are stored in the system profile and can be referenced with the appropriate Prf calls. If these calls fail, it is recommended that DosGetCtryInfo be used. Message Substitution Once you understand code pages, you can tackle localization concerns. The first step in localizing applications is the translation of the relevant application strings. To make this translation step easy, the messages should be separable from the source code. Furthermore, the strings should be flexible, allowing simple word replacement. OS/2 provides several means by which messages and program strings can be isolated from the code. One method available to all OS/2 programs is to keep strings in message files, using the DosGetMessage API to retrieve the messages. Not only does this isolate the message text from the program source, but it also has the advantage that DosGetMessage can take a variable number of arguments that can be inserted into the text of the message. This allows program strings to be generated regardless of the word order of the sentence in the language (see Figure 4). Most PM strings, such as those used in dialog boxes, menus, and other system controls, are already stored in resource templates. Other program informational strings (such as error messages) should be placed into string tables and then loaded at run time with the WinLoadString instruction. PM is capable of substituting strings using the WinSubstituteStrings instruction. Besides the mechanics of replacing strings, there are other considerations. Arrays that store messages should allocate up to 30 percent more storage for messages, because the strings may grow when translated into other languages. Dialog boxes should always be the right size to accommodate their string contents. Also, since the byte values of frame characters are code page-dependent, three MSG_APPL system messages have been defined in OSO001.MSG to allow the user to access the appropriate frame characters. These messages should be retrieved and used whenever frame characters are to be drawn in Vio or DOS mode (see Figure 5). This facility is not as useful in PM, because frame characters may not line up with proportional fonts, and because frames can be drawn with GPI calls. Country-Specific Formatting Numbers, dates, and other information are formatted differently in different countries. For example, January 1, 1990 is formatted 1 January 1990 in England. OS/2 allows these settings to be customized in the Control Panel. These options are then written to the OS2.INI system profile, with the AppName PM_National and with the keys shown in Figure 6. Note that these symbols are code page-dependent. For example, if a code page is specified that does not contain the Yen () symbol, the Yen marker cannot be displayed. The strings in Figure 6 can be retrieved from the system profile by making the appropriate Prf calls (see Figure 7). If these profile queries fail, the application writer can still fall back on the DosGetCtryInfo call, which will retrieve these values from the COUNTRY.SYS file. It is a good idea to use the Prf calls, because the formatting information can be specified by the user at run time using the Control Panel. The information retrieved by DosGetCtryInfo is more of a default, as it statically comes from the file COUNTRY.SYS. Data Processing When data is processed in different code pages, special care must be taken. In code page 850 (multilingual), there are accented characters, which have code point values greater than 7FH. Because of this, be sure to declare characters unsigned. (The -J switch can be specified to the Microsoft C compiler Version 5.1 to make all characters default to unsigned.) Another difficulty that arises is that many standard U.S. C run-time functions, such as isalpha and strcmpi, will not work correctly when given strings that are not in code page 437, the U.S. code page. (The C run-time functions strlen and strcpy will work with all code pages, because they look only for the NULL terminator.) To avoid these problems, use OS/2 system services to perform code page-dependent operations such as case conversions (use DosCaseMap, WinUpper, or WinUpperChar) and string comparisons (DosGetCollate or WinCompareStrings). Two more useful instructions are WinCpTranslateChar and WinCpTranslateString, which allow conversion between code pages. By using these calls, files that are stored in one code page can be used by an application running in a different code page. String comparison is tricky because the order in which characters are sorted depends on the language of the text. For example, some European languages sort accented characters between the unaccented characters; some, like the Scandinavian languages, sort them at the end. To make your program sort correctly in every country, you can use DosGetCollate. DosGetCollate returns a table of sorting weights for each character. Instead of sorting by the byte value of the original characters, replace them by their sort weights and compare these against each other. The sorting table returned by DosGetCollate gives the same weights for uppercase and lowercase characters. WinCompareStrings uses this collating table to sort strings correctly. Another consideration is how accented characters can be entered. If the keyboard does not have keys representing the accented characters, deadkeys may be used. Deadkeys are keys that produce an accent mark used in combination with another character. Deadkeys do not advance the cursor. Deadkey handling is made much easier by internal PM handling; the WM_CHAR message has three special KC_ flags that tell the state of deadkey processing. The KC_DEADKEY flag means that the key pressed is a deadkey; the character KC_COMPOSITE means that the deadkey's accent mark has been fused with the current character to form a new character; and the KC_INVALIDCOMP flag denotes an invalid composition, and returns the last character selected. If KC_INVALIDCOMP is specified, and the current character is not a space, the application should beep the speaker and display the new character code. For an example of proper WM_CHAR message handling, refer to the TyperChar subroutine discussed below. Device Handling This concern is not localization-specific, but a reminder that OS/2 is an operating system that runs on many platforms. The application writer should therefore be sure to use device-independent units as much as possible. For example, an image of size 53-by-53 pixels is much smaller on a high-resolution screen than on a low-resolution screen. Also, direct manipulation of hardware devices (such as writing characters directly to the physical video buffer) should be avoided at all costs. Double Byte Character Set European editions of OS/2 are essentially equivalent to the U.S. version. The Japanese and Korean versions of OS/2, however, require support for character sets that contain more than 256 characters. Support for the large character sets is available in Japanese OS/2 Version 1.1 and in the U.S. and European OS/2 with PM Version 1.2. U.S. and European PM can use code page 932, but since Japanese display drivers and ROM fonts are not included, the special characters cannot be displayed. Up to this point, we have only discussed code pages that have 256 entries (one for each possible byte value). But Japanese has more than 5000 characters! Japanese is written in a mix of Kanji (ideograph) characters and Kana (phonetic) signs. There are two kinds of Kana signs: Hiragana, the Japanese alphabet of sounds, and Katakana, which is used to write foreign words phonetically. Kana-Kanji conversion is a convenient method for typing Kanji characters; Japanese keyboards are often labeled with Katakana to allow the typing of Japanese words phonetically (see Figure 8). There are 118 Katakana and Hiragana characters and about 3500 Kanjis in general use today. To handle all these characters, a double byte character set (DBCS) was introduced. (In this article, DBCS refers to a character set that contains both single and double byte characters. Purists would call this type of character set a mixed byte character set--MBCS. They would use "DBCS" for a set that is strictly double byte.) In a double byte character set, certain ranges of code points in the code page are designated as leading bytes. Leading bytes have no character value themselves; instead, they indicate that they together with the following byte represent a single character. This second byte is called a trailing byte or trail byte. You must make certain that the two bytes of a double byte character are always treated as a unit; if they become separated, the character and/or the byte stream of data will be processed incorrectly. This article restricts itself to DBCS code page 932, the primary code page used in Japan today. Figure 9 shows the first 256 bytes, which are divided into several ranges. There are two main single byte ranges, in which one byte corresponds to one character. In the range from 00H to 7FH, the byte values are treated as ASCII (as in the European code pages; the differences are that the byte value 5CH is used for the Yen marker instead of the backslash, and that the character assignments in the control area from 01H to 1FH represent different graphic characters). The second single byte range is from A0H to DFH, and contains codes for the Japanese Katakana syllabary. These codes correspond to keys on some standard Japanese keyboards, such as the Architecture Extended (AX) (see Figure 8). In code page 932, the ranges from 81H to 9FH and from E0H to FCH have been reserved as lead bytes. (Each DBCS code page has a different set of lead byte ranges. To determine these ranges, call DosGetDBCSEv; if no ranges are found, you are in a SBCS code page.) In essence, each lead byte "points" to a subsequent block of 256 characters. This scheme provides for the representation of the characters in the first two Japanese Industrial Standard (JIS) character set levels (see Figure 10) and leaves room for future additions. A disadvantage is that trail bytes may have any value except NULL, so applications must be careful when performing string processing. Data Processing There are guidelines that will prevent DBCS data processing errors. For instance, problems will arise any time a character stream is scanned linearly for a particular byte value. This problem arises because the only byte guaranteed never to be a trail byte is NULL (00H) and many applications will scan a string for a special delimiter (such as backslash, space, or tab). (Some delimiters such as the period and tab are not valid trailing bytes in code page 932, but this may change in the future.) For example, 5CH is a valid trail byte in code page 932 and it is also used in OS/2 as a path delimiter. Therefore, it is necessary to scan a string by characters, noting double byte characters as they occur. DBCS string truncation must be performed carefully. Never allow a string to end in a lead byte or begin with a trailing byte. If a string somehow starts with a trailing byte, it should be either replaced with a single byte padding character (such as a space), or the string pointer should be advanced by a byte. When a string ends with a leading byte, the string can be either shortened by a byte, or again, the offending byte can be replaced by a single byte padding character. Note that backspace and delete processing should always remove double byte characters completely. While this sounds easy enough, from within a string it is not easy to determine what the DBCS character boundaries are. Figure 11 demonstrates how DBCS strings can become corrupted. A sure way to identify DBCS characters is to scan from the beginning of the string, which can be done by calling WinNextChar and WinPrevChar instead of blindly incrementing string pointers. Also, as in other code pages, WinCompareStrings can be used to compare strings properly. DBCS output should be performed a full line at a time. By using system services to do this, DBCS bisection problems can be dealt with by OS/2. For example, the WinDrawText call performs correct word breaking, whatever the language, based on the country code and code page. Another guideline is always to scan strings forward. You should replace backward searches by forward searches and replace pointer decrements by safe macros or function calls. Using a forward search from the beginning of the string is the easiest and safest way to ensure that characters are scanned correctly. Do not increment or decrement string pointers. Although the ++ operator in C is a convenient way to scan an SBCS string, it does not take double byte characters into consideration. Instead of incrementing string pointers, skip entire DBCS characters by using safe functions such as WinPrevChar and WinNextChar, or define your own macros if speed is critical. Be very careful when matching special characters. Many special characters are in the trail byte range (for example, backslash and tilde). Avoid problems by skipping entire double byte characters when moving through the string. You should pass pointers to lead bytes or single byte characters, because all system functions (except VioWrtTTY) assume that pointers point to a lead byte or a single byte character. Application functions should also be written this way, as a rule. Most importantly, always treat double byte characters as a unit. Never allow partial selection of double byte characters and never allow the cursor to rest on the second byte of a double byte character. Character Input and Output To input DBCS characters in Vio sessions, the same API calls previously discussed can be used. An application might use the KbdCharIn call to read characters into a buffer. Since KbdCharIn returns characters a byte at a time, the application writer must keep track of double byte handling, buffering leading bytes as necessary, to make sure that partial DBCS strings are not displayed. Trail bytes should never be left in the typeahead buffer: either read the second byte, or discard the leading byte and flush the input queue. Again, the delete and backspace keys should always delete or back up entire DBCS characters. PM input handling is easier, because double byte characters are returned as a unit in mp2 by the WM_CHAR message. Single byte characters are also returned in mp2, with the second byte to be ignored. This reinforces the idea that double byte characters should be handled as a unit. As for DBCS output, in Vio sessions all Vio text output calls may be used (for example, VioCharStringAtt). However, two rules should be followed. First, display updates must never result in the display of partial double byte characters. To prevent this, never pass a string starting with a trail byte, and never pass a string containing a lead byte with no trail byte. (This rule does not hold for VioWrtTTY, which will buffer lead bytes that it encounters at the end of strings. However, this feature may require the application to keep track of when a lead byte is buffered.) The second rule is that if character attributes such as reverse video are specified, use the worldwide Logical Video Buffer (LVB) format (see Figure 12). DBCS character handling is easier in PM than in full screen mode. As long as complete DBCS strings are output, they will be clipped properly to the window boundary. Another useful OS/2 facility is the DT_WORDBREAK flag that can be specified with the WinDrawText call. In European languages, word breaking occurs when spaces are encountered; however, in Japanese, word breaks may occur between double byte Kanji characters because each character may represent a concept or word. WinDrawText will use the current PS code page to determine the word-breaking rule to use. (The hook HK_FINDWORD can be used to define word-breaking rules.) Given the preceding, you should avoid detailed text handling by calling system APIs with a full line of text or more at a time. In this manner, word-breaking logic can be handled for you, as well as correct handling of proportional fonts; the system will also handle the special cases where DBCS strings can become corrupted. Asian Input Methods A PM facility introduced in Japanese OS/2 Version 1.1 is the ability to specify alternative mnemonics. As you may know, a mnemonic is an underlined character in menu items or on buttons in dialog boxes. Typing that character on your keyboard selects the menu item or button. The mnemonic keystroke is defined as the character following the tilde in the menu string provided in the menu template, "~Item", for example. But because there are two types of keyboards in Japan (labeled in Katakana or Hiragana and the Latin alphabet) the simple U.S. scheme had to be extended. There are two kinds of touch typists in Japan, those who are accustomed to the standard QWERTY layout and those who are used to one of the native Japanese layouts. (The operator can toggle between English and Japanese keytops with a special shift key.) To solve this problem, mnemonics can be specified with either the tilde or with the sequences \036 and \037. For example, consider a string "Item (\036x\037X)", where "x" is the U.S. keyboard mnemonic, and "X" is the Katakana mnemonic. Then if the U.S. keyboard mnemonic is active, the menu item would appear as "Item (x)"; otherwise you'd see "Item (X)". The user can select which of the two mnemonics is to be displayed by setting the system value SV_ALTMNEMONIC from the Control Panel. SV_ALTMNEMONIC is TRUE if the non-Latin mnemonic is active. The action of the mnemonic keystrokes is such that when one of the sets of mnemonics is selected, the system responds to that key regardless of the shift state of the keyboard. For an illustration of how mnemonics appear, see Figure 13. The most visible difference in the Japanese input scheme is that the bottom of most screens (Vio screens and PM windows) contains a keyboard status area. This status area should always be present if characters can be entered. It is used to reflect the state of the keyboard and to allow Kana-Kanji (KK) conversions. The possible keyboard states are whether the keyboard is in English, Katakana, Hiragana, or Romanji (Roman character) mode; whether the shift key is depressed; and whether or not KK conversion is enabled. As you might imagine, with more than 3500 Kanjis in use, it would be very difficult to have a key cap for each possible Kanji character. Touch typing would be rather tedious! To solve this problem, OS/2 allows the user to enter Kana-Kanji conversion mode. In this mode, all typed input is placed in a conversion window. The user inputs a Kana string (usually in Hiragana), which is sent to a dictionary when the user presses the VK_DBE_CONV key. The dictionary then returns a Kanji character with that Kana pronunciation. If the Kanji character is not acceptable, the VK_DBE_CONV key can be used to select an alternative Kanji string. If this Kanji character is acceptable, it is selected with the VK_ENTER key, upon which the conversion area disappears and the characters are relayed to the application. The visual effect of this conversion is shown in Figure 14. In the Vio screen, pop-up windows (conversion areas) are placed on the screen at the current cursor location. In PM, the system sends the WM_QUERYCONVERTPOS message to the window, asking where to place the conversion window. The window can either process this message by specifying the coordinates to be used, or it can pass the message to its parent. The standard frame window procedure will place the conversion window on its status line if FCF_DBE_APPSTAT has been specified (which is why this flag must be supplied on frame windows when input is occurring). In Vio screens, the bottom of the screen may have several lines reserved for the status line and input conversion area. Applications can determine the size of this area (in lines) by calling function number 7 of VioGetState. Scrolling and cursor positioning are restricted automatically to exclude the status area; therefore, to clear the screen without overwriting the status area, VioScrollUp should be called with FFFFH as the number of lines to scroll. For PM applications from OS/2 Version 1.2 (OS/2 Version 1.1 in Japan) onwards, the flag FCF_DBE_APPSTAT should be added to any frame window that has children that process input (such as dialog boxes that contain entry fields or list boxes). This flag currently has no effect in the U.S. and Europe, but creates the status line if the queue code page is a DBCS code page. This can be seen on the Japanese window in Figure 1. Note that other than the fact that the client area is smaller by the height of the status area, the application behavior is no different. Other Considerations The standard U.S. and European fonts (Times Roman, Helvetica, and Courier) do not contain DBCS glyphs. Therefore, if a PM program loads these fonts, Japanese characters cannot be displayed, even when running under code page 932. As a result, the font name string of font resources should be contained in a resource file, just like the other language-dependent strings. Another implication is that the code page of the font must be specified on creation; if code page 0 is passed to GpiCreateLogFont, the font is created as a U.S. and European font. (Technically, a Universal Glyph List, UGL, font is created. However, the UGL in OS/2 Version 1.2 contains only U.S. and European characters, so UGL fonts are not sufficient for Japan.) The layout of the LVB in DBCS countries is often quite different from that in the United States. There are many proprietary LVB formats; changing the attribute bytes of a Vio screen may yield unpredictable results on different hardware. The best approach is to use the worldwide LVB format (see Figure 12), which is a superset of the 3-byte attribute format in PM AVIO PSs. You should not directly manipulate the Physical Video Buffer (PVB) in text mode. Doing this not only exposes your program to vastly different hardware standards, but it may also collide with the Asian input method conversion software. Printers must be switched explicitly to the code page of your print job, which is done by calling DosDevIOCtl with the PRT_ACTIVEFONT command. In addition, some printer drivers may support other IOCtl commands in other countries (for example, to select Shift-JIS to JIS character encoding translation and the like). A few miscellaneous concerns: always use DosGetDBCSEv to find lead byte ranges. Never hardcode ranges for lead bytes. Add FCF_DBE_APPSTAT to all frame windows within which input can take place. Process the WM_QUERYCONVERTPOS message if your application deals with general input streams. Sample Vio Program MAKEBOX (see Figure 15) is a sample program that simply pops up a message box. The first step in building MAKEBOX is constructing the message panel. In DBCS countries, DosGetMessage retrieves system messages from different files, depending on the process code page. This allows error messages to be displayed in English when the process is not in a DBCS code page. However, the process code page may or may not be the code page of the video subsystem. Since DosGetMessage uses the process code page, you must explicitly synchronize the process code page with the current video code page for the duration of the message retrieval. A worldwide program is likely to be run on machines, especially in Japan, where the familiar frame drawing characters are not all available or have different character codes, so be sure to use the MSG_APPL messages defined in Figure 5. Traditionally, another area of hardware incompatibility is the layout of character attributes. However, OS/2 Version 1.2 and Japanese OS/2 Version 1.1 introduced the worldwide LVB format with a standard layout for the character attributes. This LVB format is fully supported by windowed Vio and full screen sessions, as far as the hardware is capable. Unsupported attributes are ignored. For example, in Japan, a common limitation is that either the foreground color or the background color must be black. The solution is to virtualize the character colors of your application and let the user customize them in the installation process, with suitably restricted defaults. MAKEBOX has a color definition table, although in this sample its values are filled with constants. Now that you know how to construct the message panel, the next consideration is how to save and restore the original screen contents. This presents a small challenge in the case of DBCS code pages. You will see shortly why it is so necessary to plan ahead to write a truly worldwide program; designing support for DBCS code pages from the start is much easier than trying to graft it on after the fact. The main problem with saving a rectangular section of a screen containing Japanese text is that double byte characters often occupy two cells on the display. Often, several of them straddle the boundary of the rectangle that will be covered. When the message box is drawn, it will obscure part of these characters. Since most display hardware is not capable of interpreting partial characters, Vio automatically replaces the leftover half with a space character. Therefore, the area to be saved should be wider by 1 column than the sides of the message box, so that the padded characters will be entirely restored. However, the saved rectangle may now contain other half characters; Vio will not let us write cell strings that start or end with incomplete DBCS characters. There are several solutions to this: the best choice depends largely on the architecture of your program. MAKEBOX relies entirely on the Vio API, so that its message box can be used independent of the LVB format. This is especially important since many OEMs in Japan provide LVB formats that are not compatible. If you have a compelling reason to manipulate the LVB directly, you should make your program more portable by using the worldwide LVB format. Avoiding direct LVB manipulation makes it possible to use this sample code in a transparent Vio pop up. MAKEBOX.C saves an area of the screen, draws a message panel, and then restores the original screen. To save the area underneath the message box, use VioReadCellStr on each partial line, extending one column to the left and right around the width of the message box. (A Vio cell consists of characters and attributes. The Vio cell calls always handle complete characters, whether single or double byte. They will never return half of a double byte character; they will pad the screen with spaces if necessary.) This may cause you to start by reading the trail byte of a DBCS character or end by reading a lead byte cell. These orphaned halves of double byte characters that start or end cell strings must be taken care of later when you restore the rectangle. Vio functions are used to draw the message box. Vio will replace any orphaned halves of DBCS characters on the screen with spaces. When the underlying rectangle is restored, the original DBCS characters must be restored as well, but you must not partially overwrite any DBCS characters already on the screen, lest Vio blank out additional cells. The function VioCheckCharType is available to inspect any screen location and determine whether it is a single cell (that is, alphabetic character) or a leading or trailing half of a DBCS character. This function is called for each line's boundary cells; if you encounter orphaned double byte characters, call VioWrtCellStr with the entire saved cell string for that line. Thus, you can correctly restore the screen contents. Sample PM Program As you might suspect, PM controls all handle deadkeys and double byte characters properly. Therefore, the easiest way to write programs that handle NLS concerns properly is to use PM controls wherever possible. TYPER (see Figure 16) is a sample PM program. It allows the user to type in the client area and has very primitive wordwrapping capability (see Figure 17). The easiest way to write this application is to create a multiline edit control (WC_MLE) in the client area and have the client window procedure route WM_SIZE messages to it. However, to demonstrate proper NLS handling, it has been modified to run in all OS/2 environments. In the main routine of a simple PM application, the program will call WinInitialize, WinCreateMsgQueue, create the main application window, code a message loop, then clean up. In the main routine of TYPER.C, an InitLeadByteTable call has been added to initialize the global array vfIsLeadByte. After the call, this array will contain Boolean values that tell whether value i is a DBCS lead byte in the current code page. The other modifications in the main routine have been to load strings such as the program title from the resource file (using WinLoadString) and to add the FCF_DBE_APPSTAT flag to the frame window. Note that none of the modifications made above have any effect when running the U.S. version of OS/2; the lead byte table would contain FALSE for all entries, the strings can always be loaded from resources, and FCF_DBE_APPSTAT has no effect in non-DBCS code pages. WM_CHAR Processing The TyperChar subroutine is contained within TYPER.C (see Figure 16). This call performs WM_CHAR processing, which involves adding characters to a fixed-length line. The new concerns are deadkey combinations and making sure that double byte characters are not mangled. Deadkeys are handled as follows (assuming that deadkey and composite characters are always single byte): ■ Do not increment the "cursor" (vcchLine). ■ If the next message is a composite character, overwrite the deadkey (it will do this automatically since you write at cursor location vcchLine). ■ If the next message is an invalid composite and the character is a single byte space, leave the deadkey character as is. Increment the character pointer. ■ If the next message is an invalid composite and the character is not a space, wipe out the deadkey. There are two major concerns in handling DBCS characters. If there is only one byte free at the end of the line, you must "wrap" the double byte characters to the next line. Also, remember that if you backspace or delete double byte characters, you must delete both bytes. One nice OS/2 feature is that WM_CHAR will give entire double byte characters in a DBCS code page. This makes it easy to check if you've been given a double byte character. If you have, the application will just insert both bytes into the string. When deleting the character, since you cannot determine whether the preceding byte is a trail byte or a single byte character, pass the beginning of the string and the current offset to WinPrevChar and let it compute how far you should retreat the character pointer. WM_QUERYCONVERTPOS Processing The WM_QUERYCONVERTPOS function is sent by OS/2 to ask where the Kana-Kanji conversion window is to be placed. If this message is not processed by the application, the conversion will occur on the status line. If you choose to process the message, set the fields of the mp1 parameter, which points to a PRECTL structure. The yBottom and xLeft values you specify will be used to place the conversion window if QCP_CONVERT is returned. If QCP_NOCONVERT is returned, KK conversion is not allowed. In TYPER (see Figure 16), the conversion window is placed at the end of the bottom text line. It determines the length of the text by calling WinDrawText with the DT_QUERYEXTENT flag and uses WinQueryWindowRect to determine the bottom of the client area of the window. Resource Files The resource file is essentially equivalent to the message file, in that it stores all the strings that the application will display (such as prompts, menu strings, and titles). For these resources, the default code page that the resource compiler uses is code page 850 (the multilingual code page). If this default is unacceptable (for example, if you are writing double byte menu items in code page 932), the resource code pages can be specified by using the CODEPAGE resource directive or by specifying the -cp flag to RC.EXE. (This flag is -k in some versions of rc.exe.) In this sample application, the strings should be translated in order to be read in the language concerned. The resource file is also the place that alternate mnemonics may be specified on the menu items, if you are writing applications for Asian OS/2. Summary As you can see, PM programs handle NLS concerns better than Vio-based programs. The WM_PAINT routine does not usually need to be modified since clipping does not cause DBCS characters to be bisected. Wordwrapping behavior already works properly because WinDrawText was used (with DT_WORDBREAK). The code modifications that had to be made to TYPER included initializing the lead byte table, putting strings into the resource file, adding FCF_DBE_APPSTAT to the frame window, fixing the WM_CHAR processing, and adding processing for the WM_QUERYCONVERTPOS message. Nontrivial work, but the result is a program that will run correctly with all versions of OS/2, where the only additional localization has been restricted to the resource file. Figure 3 cpOld = WinQueryProcessCP(); if (!idCodePage) idCodePage = cpOld; else { /* * The user specified a code page, make sure it is in the * list of valid code pages, as reported by the base. * If the base fails to give us any code pages, we will only * allow code page 850 to pass */ if (DosGetCp(sizeof(cp), &cp[0], &cb) || cp[0] = = 0) { if (idCodePage != 850) { return FALSE; } } else { /* see if in the list */ for (cb /= 2, i=0; i < cb; i++) { if (idCodePage = = cp[i]) break; /* found it */ } if (i = = cb) { return FALSE; /* did not find it, so return error */ } } } /* Save the new code page in a process private cache, if this process has only one message queue. */ if (cmqProcess= =1) { uQueueCP = idCodePage; fIsDBCS = fIsDBCSCp(idCodePage); } else { /* if cmqProcess>1, uQueueCP is invalid */ fIsDBCS |= fIsDBCSCp(idCodePage); } fPossibleDBCS |= fIsDBCS; return TRUE; Figure 5 In the message file OSO001.MSG, three special messages reveal the characters to be used for drawing frames. These are most useful in the nongraphic environment of Vio screens. Following are the message IDs that can be used with DosGetMessage to obtain the frame characters and the order of the characters in the message. Sample text shows the message contents for code page 850. Characters represented: single top left corner single top right corner single bottom left corner single bottom right corner single left vertical single right vertical single top horizontal single bottom horizontal single left T single right T single top T single bottom T single cross Characters represented: double top left corner double top right corner double bottom left corner double bottom right corner double left vertical double right vertical double top horizontal double bottom horizontal double left T double right T double top T double bottom T double cross Characters represented: up arrow down arrow left arrow right arrow Figure 6 Key Meaning iCountry Country code iDate Date format iCurrency Whether the currency symbol precedes the amount iDigits Number of digits following decimal point iTime Whether time is 12 or 24 hour format iLzero Whether leading zeros should be displayed s1159 NLS string describing AM s2359 NLS string describing PM sCurrency Currency symbol sThousand Thousands separator sDecimal Decimal marker sDate Date separator sTime Time separator sList List separator Figure 7 DosCaseMap Casemaps the characters in a string DosGetCollate Retrieves a collating sequence table DosGetCp Retrieves a list of code pages for the process DosGetCtryInfo Retrieves country-dependent formatting information DosGetDBCSEv Retrieves the DBCS environment vector DosGetMessage Retrieves a message DosSetCp Sets the code page for the current process DosSetProcCp Sets the code page for a process GpiQueryCp Retrieves the code page identifier GpiSetCp Sets the graphics code page identifier KbdGetCp Retrieves the current code page identifier KbdOpen Opens a logical keyboard KbdSetCp Sets a code page identifier for a logical keyboard KbdSetCustXt Installs a custom translation table VioCheckCharType Tells if a byte is a single, lead, or trail byte VioGetCp Retrieves a video code page identifier VioSetCp Sets a video code page VioSetFont Sets a video font WinCompareStrings Compares two strings WinCpTranslateChar Translates a character between code pages WinCpTranslateString Translates a string between code pages WinNextChar Moves to the next character in a string WinPrevChar Moves to the previous character in a string WinQueryCp Retrieves the identifier of the queue code page WinSubstituteStrings Performs a string substitution PrfQueryProfile Retrieves profile filenames PrfQueryProfileData Retrieves information from the profile file PrfQueryProfileInt Retrieves an integer from the profile file PrfQueryProfileSize Retrieves a key name's size from the program file PrfQueryProfileString Retrieves a string from the profile file Special Features: HK_FINDWORD FCF_DBE_APPSTAT MSG_APPL WM_QUERYCONVERTPOS Figure 10 ASCII Acronym for American Standard Code for Information Interchange. This standard defines characters to be associated with 7-bit values. bisection Refers to the separation of a leading byte from its trailing byte. code page (CP) A table that describes the glyphs that correspond to various byte (or double byte) values. code point An index into a code page. For example, code point 20H in code page 850 is the space character. collation Sorting. One of the things obtainable with DosGetCtryInfo is the collating sequence, which is a table that tells the sorted order of code points for a given country code. country code A structure that stores a country identifier and a code page. Normally if this structure is passed with two zeroes, the current process country and code page are used. DBCS An acronym for double byte character set. Used as an adjective to describe code pages. DBCS-enabled A product is called DBCS-enabled if it handles all the nuances of DBCS string handling properly. deadkey A keystroke that is not processed but is consumed. This is one way to enter accented characters at the keyboard; first hit the deadkey (accent mark), then a character which can combine with it. In WM_CHAR processing, deadkey handling is easier because KC_DEADKEY, KC_INVALIDCOMP, and KC_COMPOSITE flags are set. double byte character Refers to characters that require two bytes for definition. In Japanese, these correspond mainly to the Kanji characters. FCF_DBE_APPSTAT A new frame control flag that specifies that a status bar is to be added to PM frame windows. This flag creates three child windows: the keyboard status area, the application status line, and an area for Kana-Kanji conversion. For Japan, the keyboard status area keeps information about whether the keyboard is in Hiragana, Katakana, or English alphanumeric mode; whether the shift key is depressed; and whether Kana-Kanji conversion is active. glyph A symbolic figure that has a well-defined meaning (such as an exclamation mark). "a" and "A" would be considered two different glyphs, although they have nearly the same meaning. Hiragana Japanese phonetic syllabary. Mixed with Kanji characters to write all of or parts of Japanese words phonetically. ideograph Used to describe pictoral characters and words, such as in Chinese characters or Japanese Kanji. This contrasts with Latin-based words, which are composed of alphabetic characters. JIS Japan Industrial Standard. Here used to refer to the various standardized sets of Japanese Kanji characters. JIS level I contains about 2,000 of the most common characters, ordered by reading. Level II contains the same characters, ordered by how the characters are written. A third level has just been defined, which increases the character set. The JIS standard is based on a 7-bit format, requiring special escape sequences to enter or leave DBCS mode. Kana-Kanji conversion The process of converting Hiragana (the Japanese alphabet of sounds) to Kanji (ideographs). This provides a convenient way for Japanese users to enter Kanji, namely, by how the Kanji are pronounced. Kanji The Japanese characters that are ideographs (as opposed to being part of a syllabary). Katakana Japanese phonetic syllabary. Japanese keyboards are often labeled with Katakana to allow typing Japanese words phonetically. leading byte (lead byte) The first byte of a double byte character. NLS National Language Support. PM is NLS-enabled, which means that PM will deal with various international concerns correctly. padding character Character used to fill out a string. In this context, the character is used to replace invalid lead and trail bytes. prepared code page A code page specified on the code page line of the CONFIG.SYS file. This code page can be used by DOS, Kbd, and Vio calls; up to two code pages can be prepared at any given time. Note that PM is not restricted to the prepared code pages, but the file system is (because it uses DOS calls). Romanji The Japanese name for Latin characters. Shift-JIS The name commonly used for the DBCS encoding scheme for the JIS standard. Shift-JIS implements JIS in 8-bit format, allowing direct (unescaped) access to the 128 ASCII characters for compatibility. syllabary A scheme in which symbols represent all possible sounds of word syllables. This is similar to an alphabet, except the sound units represented are slightly larger. trailing byte (trail byte) The second byte of a double byte character. Figure 11 This figure demonstrates what a Japanese user in code page 932 might see if DBCS strings are incorrectly handled. Suppose a DBCS string is processed, in which "S" denotes a single byte character and "Dd" denotes a double byte character. In the second row byte values are displayed, and in the third row we see what would appear on a Japanese OS/2 screen. If the third byte of the string is deleted, the damage is almost unnoticeable. In this case, the trailing byte happens to be a single byte character, so the string does not become too badly corrupted. However, a yen mark (backslash) is now part of the string, and may cause the string to be handled improperly (especially if it is a filename). If the fourth byte is deleted instead, the damage is more evident. The resulting string looks nothing like the original text (although most bytes are the same!) Clearly if double byte characters are to be deleted, they should be completely deleted. Below, the fourth character has been properly deleted. Figure 12 We strongly recommend that you use the worldwide LVB format for any application that needs to handle attribute bytes. This LVB format is supported by OS/2 Version 1.2 worldwide and by Japanese OS/2 from Version 1.1. The format ID is 70H and the attribute count is three (three attribute bytes per character cell). The layout is as follows: where C represents character code points A represents attributes The character can be a single byte character or one of the two bytes of a double byte character. The attributes for the screen are defined as follows: 1st byte CGA attribute (background and foreground color) 2nd byte bit 7 underscore 6 reverse video 5 blinking (ignored by AVio) 4 background transparency (transparent = 1, opaque = 0) 2-3 grid attributes 3 Left Vertical 2 Top Horizontal 0-1 reserved (must be zero) (Font ID for AVio) 3rd byte bit 7 trailing byte (1 iff trailing byte) 1-6 reserved (must be zero) 0 double byte character (1 iff part of double byte character) The attributes in the second byte are ignored where hardware cannot support them, but are implemented fully in the DBCS AVio. The third byte is under system control, and must be present when reading or writing cells or cell strings, but application changes are ignored. Figure 14 In the windows below, the keyboard status area is below the English words. The Hiragana characters in the first window indicate that the keyboard is in English single byte alphanumeric mode. In the second window, the user has enabled Kana-Kanji conversion. The user types the Hiragana string shown inside the black conversion box. Then the typist hits the convert key. The Hiragana is converted to Kanji. Since these Kanji characters are acceptable, the user hits enter and the characters are sent to the application. Figure 15 MAKEBOX #======================= MAKEBOX ================================ # # make file for VIO DBCS SAMPLE # # Assumes: MAKEBOX.C # MAKEBOX.H # SLIBCE.LIB # SLICEP.LIB # DOSCALLS.LIB # plus OS/2 1.2 include files # # Generates:MAKEBOX.LNK # MAKEBOX.OBJ # MAKEBOX.EXE # MAKEBOX.MAP # MAKEBOX.SYM # #================================================================ # # Default compilation macros # CC = cl -W2 -c -AS -G2sw -Od -Zpe ASM = masm MKMSG = mkmsgf #================================================================ # # Default inference rules # #================================================================ .c.obj: $(CC) $*.c .asm.obj: $(ASM) $*.asm; .txt.msg: $(MKMSG) $*.txt $*.msg #============================================================== # # A list of all of the object files # #============================================================== ALL.OBJ = makebox.obj #============================================================== # # Dependencies # #============================================================== makebox.lnk: makebox echo $(ALL.OBJ) >> makebox.lnk echo makebox.exe >> makebox.lnk echo makebox.map/map >> makebox.lnk echo /nod:slibce slibcep doscalls >> makebox.lnk echo NUL.DEF >> makebox.lnk makebox.obj: makebox.c makebox.msg: makebox.txt makebox.exe: $(ALL.OBJ) makebox.lnk link @makebox.lnk mapsym makebox MAKEBOX.H /********************************* MAKEBOX.H ******************\ * * HEADER FILE for VIO DBCS SAMPLE program * \**************************************************************/ #define CBMAXDBCS 10 // Max size of DosGetDBCSEv buffer #define VIOHANDLE 0 // default handle for full screen VIO extern int main(SHORT argc,char * *argv); extern VOID MakeBox( USHORT yTop,USHORT xLeft, USHORT yBottom, USHORT xRight ); extern VOID SaveArea(USHORT yTop,USHORT xLeft, USHORT yBottom, USHORT xRight, BOOL fSave); extern VOID ParseFrameMsg(PBYTE pchMsg, USHORT cbMsg); /******************** for OS2 1.2 US ******************** * These defines are needed to run under 1.2 US *********************************************************/ typedef struct t_VIOSCROLL { /* vioscroll */ USHORT cb; USHORT type; USHORT cnscrl; } VIOSCROLL, FAR * PVIOSCROLL; #define VS_GETSCROLL 6 #ifndef MSG_APPL_SINGLEFRAMECHAR #define MSG_APPL_SINGLEFRAMECHAR 130 #define MSG_APPL_DOUBLEFRAMECHAR 131 #define MSG_APPL_ARROWCHAR 132 #endif #ifndef VCC_SBCSCHAR #define VCC_SBCSCHAR 0 // Cell contains SBCS character #define VCC_DBCSFULLCHAR 1 // Cell contains Full DBCS character #define VCC_DBCS1STHALF 2 // Cell contains leading byte of DBCS #define VCC_DBCS2NDHALF 3 // Cell contains trailing byte of DBCS USHORT APIENTRY VioCheckCharType (PUSHORT pType, USHORT usRow, USHORT usColumn, HVIO hvio); #endif MAKEBOX.LNK makebox.obj makebox.exe makebox.map/map /nod:slibce slibcep doscalls NUL.DEF MAKEBOX.TXT MKB MKB0000I: Command Line Arguments to %1: %2 %3 %4 MAKEBOX.C /********************************* MAKEBOX ************************\ * * VIO DBCS SAMPLE program * * This program puts up panel containing a message into which * the first 3 command line arguments have been substituted. * * This program will run on any OS/2 1.2 version. * However, most of the DBCS machinery in it lies unused if run * in a non-DBCS code page or on non-DBCS hardware. * * It is therefore typical of an 'international application' which * also contains that bit of extra code to allow it to run * everywhere. * * This program works so it can be run as a second process in a * screen group, i.e. it adapts to the VIO mode, VIO code page etc * that were set up by the first process. Alternatively this code * would work well in a DLL. * \******************************************************************/ #define INCL_VIO #define INCL_DOSNLS #define INCL_DOSMISC #define INCL_DOSPROCESS #include <os2.h> #include <ctype.h> #include <stdio.h> #include "makebox.h" BYTE fbLead[256]; // lead byte table, initially all 0 // (i.e. FALSE or not a lead byte) #define fIsDbcsLead( x ) (fbLead[(x)]) #define CBMAXMSG 200 // upper bound for message text /** MAIN: ** ** Construct a message panel and display ** the arguments in a message text **/ main( argc, argv ) SHORT argc; CHAR ** argv; { USHORT ib, ich, iLo, iHi; USHORT usCodePage; // code-page identifier USHORT usReserved=0; // must be zero USHORT cbBuf; // length of buffer COUNTRYCODE ctryc; // structure for country code CHAR abDbcsRange[CBMAXDBCS]; // buffer for DBCS info CHAR achMsgBuf[CBMAXMSG]; // return buffer for Text USHORT cbMsg; // # of message bytes returned PCHAR apchVTable[10]; // pointer to table of pointers // pointers to strings SHORT iTmp; USHORT xLeft, // Message box coordinates xRight, yTop, yBottom; VIOSCROLL vioscroll; // VioGetState scroll info block VIOMODEINFO viomi; // Video mode information // First, determine where to locate the message box. // Get the current Vio mode to find the number of lines on the // screen. viomi.cb = sizeof(VIOMODEINFO); VioGetMode(&viomi, 0); // note: bottom screen line is used for keybd status in DCBS // countries so we use VioGetState to return the size of this // (nonscrollable) area vioscroll.cb = sizeof(vioscroll); vioscroll.type = VS_GETSCROLL; VioGetState((PVIOSCROLL) &vioscroll, VIOHANDLE); // Set Message box coordinates to stay inside screen area xLeft = 10, xRight = viomi.col - 9, yTop = 5, yBottom = viomi.row - vioscroll.cnscrl - 5; // Get Current VIO Code Page VioGetCp(0, // must be zero &usCodePage, // code-page identifier VIOHANDLE); // video handle // Get Lead Byte range (if any) for current VIO code page // (for SBCS code page abDbcsRange is all 0) ctryc.country = 0; ctryc.codepage = usCodePage; DosGetDBCSEv(cbBuf, (PCOUNTRYCODE) &ctryc, (PCHAR) abDbcsRange); // Construct lead byte table for quick parsing of DBCS strings for (ib=0; ib < cbBuf && abDbcsRange[ib]; ib++){ if( ib % 1 ){ for( ich = iLo; ich <= abDbcsRange[ib]; ich++ ) fbLead[ich] = 1; } else iLo = abDbcsRange[ib]; } // Synch Process and VIO code page, so DosGetMessage retrieves // characters that VIO can display. DosSetProcCp(usCodePage, usReserved); // Save screen area to be obscured by message box. SaveArea(yTop, xLeft, yBottom, xRight, TRUE); // Retrieve frame characters DosGetMessage( (PCHAR FAR *) NULL, // pointer to table of ptrs to strings 0, // number of pointers in table (PBYTE) achMsgBuf, // buffer receiving message CBMAXMSG, // size of message buffer MSG_APPL_SINGLEFRAMECHAR,// message number to retrieve "oso001.msg", // name of file containing message (PUSHORT) &cbMsg); // number of bytes in returned message // Parse the frame characters message and draw a box ParseFrameMsg( achMsgBuf, cbMsg); MakeBox(yTop, xLeft, yBottom, xRight); // Retrieve message text; merge w/ first few arguments from argv argc = (argc > 4 ? 4 : argc ); for ( iTmp = 0; iTmp < argc; iTmp++) { apchVTable[iTmp] = (PCHAR) argv[iTmp]; } DosGetMessage( apchVTable, // pointer to table of pointers to strings argc, // number of pointers in table (PCHAR) achMsgBuf, // buffer receiving message CBMAXMSG, // size of message buffer 0, // message number to retrieve "makebox.msg", // name of file containing message (PUSHORT) &cbMsg); // number of bytes in returned message // Set Cursor to beginning of message text VioSetCurPos(yTop + 2, // cursor row xLeft + 2, // cursor column VIOHANDLE); // video handle // Output message text VioWrtTTY(achMsgBuf, cbMsg, VIOHANDLE); /** ** User interaction here ■ ■ ■ **/ DosSleep( 2000L ); // Restore screen from saved area SaveArea(yTop, xLeft, yBottom, xRight, FALSE); return 0; } // The boxes defined here have black on red borders and a white // interior. The attributes are ULONG so they work correctly // with the world-wide LVB format's 3 byte attributes. // USHORT ausBoxChar[3][3]; // Skeleton for a box frame static ULONG aulAttr[][3] = // Corresponding char attribute vals { {0x40, 0x40, 0x40}, {0x40, 0x70, 0x40}, {0x40, 0x40, 0x40} }; /** PARSEFRAMEMSG: ** ** Collect the frame characters from the message and place them ** into a skeleton 3x3 array for the box. **/ VOID ParseFrameMsg( pchMsg, cbMsg ) PCH pchMsg; USHORT cbMsg; { sscanf( (char *)pchMsg, "%x %x %x %x %x %x %x %x", &ausBoxChar[0][0], &ausBoxChar[0][2], &ausBoxChar[2][0], &ausBoxChar[2][2], &ausBoxChar[1][0], &ausBoxChar[1][2], &ausBoxChar[2][1], &ausBoxChar[0][1] ); ausBoxChar[1][1] = 0x20; } /** MAKEBOX: ** ** Expand Box skeleton to draw box of the desired size. Uses VIO ** calls exclusively in order to be portable to different H/W. **/ VOID MakeBox(yTop, xLeft, yBottom, xRight) USHORT xLeft, // Box coordinates xRight, yTop, // not inclusive yBottom; { static int iTimes[] = {1, 1, 1}; int i1, i2, xTemp; USHORT usChar; iTimes[1] = xRight - xLeft; xTemp = xLeft; // The nested loops below repeatedly draw the center row and // center column of the box skeleton, thereby expanding the // box to the desired size. for (i1 = 0; i1 < 3; i1++) do { for (i2 = 0; i2 < 3; i2++) { // Write Frame Character usChar = ausBoxChar[i1][i2]; VioWrtNChar((PCH)&usChar, iTimes[i2],yTop, xLeft, VIOHANDLE); // Write corresponding attribute // (need to write twice for DBCS frame chars) VioWrtNAttr((PBYTE)&aulAttr[i1][i2], iTimes[i2], yTop, xLeft, VIOHANDLE); if( fIsDbcsLead( usChar & 0xFF)){ VioWrtNAttr((PBYTE)&aulAttr[i1][i2], iTimes[i2], yTop, xLeft, VIOHANDLE); } xLeft + = iTimes[i2]; } xLeft = xTemp; yTop++; } while ((i1 = = 1) && (yTop < yBottom)); } /* end MakeBox */ /** SAVEAREA: ** ** This function saves and restores the area of the screen that ** will be obscured by our message panel. Care is taken that any ** DBCS chars which might be straddling the edges of the rectangle ** are saved and restored correctly. ** ** This problem may exist if the rectangle to be saved doesn't ** extend to the screen edge. It is handled by saving an extra ** cell on each side. The saved buffer will contain the full DBCS ** character for any DBCS characters that were straddling the ** initial rectangle. ** ** Before restoring we need to know which of the extra cells ** contain such halves of DBCS characters necessary for completion. ** VioCheckCharType is used to check the cell in the video buffer. **/ VOID SaveArea(yTop, xLeft, yBottom, xRight, fSave) USHORT xLeft, // area coordinates xRight, yTop, // not inclusive yBottom; BOOL fSave; { static SEL selArea; // Selector for saved segment static PBYTE pbArea; // Pointer to saved screen area PBYTE pbLine; // Pointer to line being drawn SHORT cbmax, cbCur, xCur, cbCell, cbArea; VIOMODEINFO viomi; // Video mode information USHORT usType; // Cell type //Get the current Vio mode to find out whether we have 1 or 3 //attribute bytes per cell. Set the size of the saved area //accordingly; viomi.cb = sizeof(VIOMODEINFO); VioGetMode(&viomi, 0); cbCell = viomi.attrib+1; // Calculate maximum number of bytes per line to be saved cbmax = (xRight - xLeft + 3) * cbCell; // Allocate buffer to hold saved area if( fSave ){ cbArea = (yBottom - yTop + 1) * cbmax; DosAllocSeg(cbArea, //bytes to allocate &selArea, //address of selector SEG_NONSHARED); //sharing flag } pbArea = MAKEP(selArea, 0); //convert selector to pointer do { // Number of bytes per line to be saved and // starting offset for this line cbCur = cbmax; pbLine = pbArea + (yBottom - yTop) * cbmax; // Extend area to be saved by 1 col Left & Right // (unless we are already at the edge of the screen) if(xLeft ) xCur = xLeft-1; else xCur=xLeft, cbCur-= cbCell; if ( xRight = = viomi.col ) cbCur-= cbCell; if( fSave ){ // Save one line VioReadCellStr(pbLine,(PUSHORT) &cbCur,yTop,xCur,VIOHANDLE); } else { // Inspect the byte left of the obscured area: // If it's a trail byte, our saved string // begins with this trail byte which is illegal. // Advance our string pointer and start column by one. if(xLeft > xCur){ VioCheckCharType( (PUSHORT) &usType, yTop, xCur, VIOHANDLE); if( usType = = VCC_DBCS2NDHALF ) { // skip trail byte on write xCur = xLeft; cbCur -= cbCell; pbLine + = cbCell; } } // As above, we cannot end a write on a lead byte. if ( xRight < viomi.col ){ VioCheckCharType( (PUSHORT) &usType, yTop, xRight+1, VIOHANDLE); if( usType = = VCC_DBCS1STHALF ) // don't end write on lead byte cbCur-= cbCell; } // Restore one line VioWrtCellStr(pbLine, cbCur, yTop, xCur, VIOHANDLE); } } while (++yTop <= yBottom); // Free memory after restore if( !fSave ){ DosFreeSeg( selArea ); } } /* end SaveArea */ Figure 16 TYPER CC = cl -c -AS -G2sw -Od -W3 -Zpei LF = /align:16 /codeview /map /NOD .c.obj: $(CC) $*.c .rc.res: rc -r $*.rc typer.obj: typer.c typer.h typer.res: typer.rc typer.h typer.exe: typer.obj typer.res typer.def typer.lnk link $(LF) typer, , , os2 slibcep, typer.def rc typer.res mapsym typer TYPER.RC #include <os2.h> #include "typer.h" STRINGTABLE { IDS_NAME, "Typer" IDS_TITLE, " - Simple Typewriter" } MENU IDR_TYPER { SUBMENU "~File", IDM_FILE { MENUITEM "A~bout...", IDM_ABOUT, MIS_TEXT MENUITEM SEPARATOR MENUITEM "E~xit", SC_CLOSE, MIS_SYSCOMMAND } } DLGTEMPLATE IDD_ABOUT { DIALOG "", 0, 10, 60, 160, 75, WS_SAVEBITS | FS_DLGBORDER { CTEXT "OS/2 Presentation Manager" -1, 10, 60, 140, 10 CTEXT "Typing Sample" -1, 30, 50, 100, 10 CTEXT "Version 1.0" -1, 10, 30, 140, 10 CTEXT "Created by Microsoft Corp., 1990" -1, 10, 20, 140, 10 DEFPUSHBUTTON "~Ok" DID_OK, 64, 4, 32, 14, WS_GROUP } } TYPER.DEF NAME Typer WINDOWAPI DESCRIPTION 'Microsoft OS/2 PM Simple Virtual Typewriter' STUB 'OS2STUB.EXE' CODE MOVEABLE DATA MOVEABLE MULTIPLE HEAPSIZE 8192 STACKSIZE 8192 TYPER.LNK typer typer/align:16 typer/map os2 slibcp/NOD typer.def TYPER.H // Resource ids // #define IDD_ABOUT 1 // About... dialog id #define IDM_FILE 1 // File menu id #define IDM_ABOUT 0x10 // About... menu item id #define IDR_TYPER 1 // Main resource ID #define IDS_NAME 1 // String ID for window class name #define IDS_TITLE 2 // String ID for title bar text // Useful constants // #define MAX_LINES 35 // Must be larger than window height in chars. #define MAX_LINE_LENGTH 255 // Should be larger than // window width in chars. #define MAX_STRING 21 // Max length of strings to be loaded #define TABLE_SIZE 12 // Current max is 5 lead byte pairs // "Exported" procedure declarations // MRESULT EXPENTRY AboutDlgProc(HWND, USHORT, MPARAM, MPARAM); MRESULT EXPENTRY TyperWndProc(HWND, USHORT, MPARAM, MPARAM); TYPER.C /* TYPER.C -- based on the sample of the same name. This program correctly handles DBCS and also demonstrates word wrapping. It uses a circular buffer to store lines. Limitations: This program does not recompute word breaks on WM_SIZE messages. */ #define INCL_DOSNLS #define INCL_PM #define INCL_NLS #include <os2.h> #include <string.h> #include "typer.h" // Variable declarations // UCHAR vaszBuffer[MAX_LINES][MAX_LINE_LENGTH + 1]; // Circular queue of lines USHORT vcchLine = 0; // Number of characters in current line SHORT vcpelyChar = 0; // Character height BOOL vfIsLeadByte[256]; // Is Byte i a DBCS lead byte? HAB vhab; // Handle to the Anchor Block HWND vhwndTyper; // Handle to the Client Area HWND vhwndTyperFrame; // Handle to the Frame Window USHORT vusCurrent = 0; // Current vaszBuffer line referenced // Useful macros // #define LOADSTRING(id, sz) \ WinLoadString(vhab, NULL, id, sizeof(sz), (PCH) sz) // Internal declarations // VOID main(VOID); // Main routine VOID InitLeadByteTable(VOID); // Initialize lead byte table VOID TyperChar(HWND, MPARAM, MPARAM);// WM_CHAR processing subrtn VOID TyperCreate(HWND); // WM_CREATE processing subrtn VOID TyperPaint(HWND, HPS, PRECTL); // WM_PAINT processing subrtn VOID main(VOID) { HMQ hmqTyper; QMSG qmsg; UCHAR szClassName[MAX_STRING]; UCHAR szWindowTitle[MAX_STRING]; ULONG ctlData; // Standard initialization for a PM application // vhab = WinInitialize(NULL); hmqTyper = WinCreateMsgQueue(vhab, 0); // Register the Typer window class -- if not successful, exit. // LOADSTRING(IDS_NAME, szClassName); if (!WinRegisterClass(vhab, (PCH) szClassName, (PFNWP)TyperWndProc, CS_SIZEREDRAW, 0)) DosExit(EXIT_PROCESS, 0); // Get the title of the application, and create the main window. // Notice that FCF_DBE_APPSTAT is included because the client area // will allow Kanji characters to be input. // LOADSTRING(IDS_TITLE, szWindowTitle); ctlData = FCF_STANDARD & ~(FCF_ICON | FCF_ACCELTABLE) | FCF_DBE_APPSTAT; vhwndTyperFrame = WinCreateStdWindow(HWND_DESKTOP, WS_VISIBLE, &ctlData, (PCH)szClassName, (PCH)szWindowTitle, 0L, NULL, IDR_TYPER, (PHWND)&vhwndTyper); // Initialize the lead byte table // InitLeadByteTable(); // Poll messages from event queue // while (WinGetMsg(vhab, &qmsg, NULL, 0, 0)) WinDispatchMsg(vhab, &qmsg); // Standard cleanup code for PM applications // WinDestroyWindow(vhwndTyperFrame); WinDestroyMsgQueue(hmqTyper); WinTerminate(vhab); DosExit(EXIT_PROCESS, 1); } MRESULT EXPENTRY TyperWndProc(HWND hwnd, USHORT usMsg, MPARAM mp1, MPARAM mp2) { /* Note: This scrolling behavior may not be extensible if the chars are to be output from top to bottom, right to left, as can be seen in traditional Chinese and Japanese scripts. */ HPS hps; RECTL rclPaint; RECTL rclWindow; switch (usMsg) { case WM_CREATE: TyperCreate(hwnd); break; case WM_CLOSE: WinPostMsg(hwnd, WM_QUIT, 0L, 0L); break; case WM_ERASEBACKGROUND: // Erase the background if this message is received // return TRUE; break; case WM_CHAR: if (!(SHORT1FROMMP(mp1) & KC_KEYUP)) { // Process the downstrokes entered. // TyperChar(hwnd, mp1, mp2); // Invalidate the updated line. // WinQueryWindowRect(hwnd, &rclWindow); rclWindow.yTop = vcpelyChar - 1; WinInvalidateRect(hwnd, &rclWindow, TRUE); WinPostMsg(hwnd, WM_PAINT, 0L, 0L); } break; case WM_QUERYCONVERTPOS: WinQueryWindowRect(hwnd, &rclWindow); hps = WinGetPS(hwnd); WinDrawText(hps, 0xFFFF, vaszBuffer[vusCurrent], &rclWindow, 0L, 0L, DT_LEFT | DT_BOTTOM | DT_QUERYEXTENT); WinReleasePS(hps); ((PRECTL) mp1)->xLeft = rclWindow.xRight; ((PRECTL) mp1)->yBottom = rclWindow.yBottom; return QCP_CONVERT; break; case WM_PAINT: // Repaint the invalid region // Side effect: May scroll up one line as needed. // hps = WinBeginPaint(hwnd, NULL, &rclPaint); TyperPaint(hwnd, hps, &rclPaint); WinEndPaint(hps); break; case WM_COMMAND: switch (COMMANDMSG(&usMsg)->cmd) { // Trap the About... menu item, and put up the dialog box // case IDM_ABOUT: WinDlgBox(HWND_DESKTOP, hwnd, AboutDlgProc, NULL, IDD_ABOUT, NULL); break; default: break; } break; default: return WinDefWindowProc(hwnd, usMsg, mp1, mp2); break; } return 0L; } MRESULT EXPENTRY AboutDlgProc(HWND hwndDlg,USHORT usMsg,MPARAM mp1, MPARAM mp2) { /* About... dialog procedure */ switch(usMsg) { case WM_COMMAND: switch(COMMANDMSG(&usMsg)->cmd) { case DID_OK: WinDismissDlg(hwndDlg, TRUE); default: break; } default: return WinDefDlgProc(hwndDlg, usMsg, mp1, mp2); } return FALSE; } VOID TyperChar(HWND hwnd, MPARAM mp1, MPARAM mp2) { /* This routine does simple input processing. Assumptions: There are no DBCS deadkeys, or invalid composites. This also implies that a deadkey followed by a DBCS space results in a DBCS space (without WinAlarm() being called). */ BOOL fDone; PUCHAR pszLast; UCHAR ch; USHORT fs; USHORT vkey; ch = (UCHAR) CHAR1FROMMP(mp2); fs = (USHORT) SHORT1FROMMP(mp1); vkey = (USHORT) SHORT2FROMMP(mp2); fDone = FALSE; if (fs & KC_VIRTUALKEY) { // Receiving a backspace... // if ((fDone = (vkey = = VK_BACKSPACE || vkey = = VK_DELETE)) && vcchLine > 0) { pszLast = WinPrevChar(vhab, 0, 0, &vaszBuffer[vusCurrent][0], &vaszBuffer[vusCurrent][vcchLine]); // If DBCS, zero out the trailing byte. // if (vfIsLeadByte[*pszLast]) { vaszBuffer[vusCurrent][--vcchLine] = '\0'; } // Zero out one character (SBCS, or DBCS lead byte) // vaszBuffer[vusCurrent][--vcchLine] = '\0'; // Receiving a carriage return (CR)... // } else if (vkey == VK_NEWLINE || vkey = = VK_ENTER) { fDone = TRUE; // Save the line in the vaszBuffer // vusCurrent = (vusCurrent + 1) % MAX_LINES; // Initialize the next text line // vcchLine = 0; vaszBuffer[vusCurrent][0] = '\0'; // Scroll window upwards, for efficient updating. // WinScrollWindow(hwnd,0,vcpelyChar,NULL,NULL,NULL,NULL, SW_INVALIDATERGN); } } // If we haven't encountered VK_BACKSPACE, VK_DELETE, VK_NEWLINE, or // VK_ENTER, process KC_CHAR values. We do this because some valid // characters are also valid vkeys (like Space). // if (!fDone) { // If DBCS character fits, add it. // if (fs & KC_CHAR) { if (vfIsLeadByte[ch] && (vcchLine + 2 < MAX_LINE_LENGTH)) { // Add DBCS to line array // vaszBuffer[vusCurrent][vcchLine++] = ch; vaszBuffer[vusCurrent][vcchLine++] = CHAR2FROMMP(mp2); vaszBuffer[vusCurrent][vcchLine] = '\0'; // If SBCS character fits, add it. // } else if (vcchLine + 1 < MAX_LINE_LENGTH) { if (fs & KC_INVALIDCOMP) { // If we have a space, advance over the current character. // if (ch = = ' ') vcchLine++; // Otherwise, complain audibly. // else WinAlarm(HWND_DESKTOP, WA_WARNING); } else { // Add character to line array // vaszBuffer[vusCurrent][vcchLine++] = ch; } vaszBuffer[vusCurrent][vcchLine] = '\0'; // If it's a deadkey, reposition so that we'll overwrite // the deadkey on KC_INVALIDCOMP or KC_COMPOSITE. // if (fs & KC_DEADKEY) vcchLine--; } } } } VOID TyperCreate(HWND hwnd) { HPS hps; FONTMETRICS fmTyper; USHORT i; // Initialize text buffer // for (i = 0; i < MAX_LINES; i++) vaszBuffer[i][0] = '\0'; // Get the character height // hps = WinGetPS(hwnd); GpiQueryFontMetrics(hps, (LONG) sizeof(FONTMETRICS), &fmTyper); vcpelyChar = (SHORT) fmTyper.lMaxBaselineExt + 1; WinReleasePS(hps); } VOID TyperPaint(HWND hwnd, HPS hps, PRECTL prclUpdate) { USHORT usUpdateTop; USHORT usUpdateBottom; RECTL rclArea; RECTL rclWindow; UCHAR *pszTmp; USHORT cchDrawn; USHORT cchTmp; USHORT usWhich; USHORT usUpdate; SHORT sNew = -1; // Compute the lines to be updated. // NOTE: This assumes that screen coordinates start with y = 0. // usUpdateTop = (USHORT) (prclUpdate->yTop / vcpelyChar); usUpdateBottom = (USHORT) (prclUpdate->yBottom / vcpelyChar); // The following code is functionally equivalent to // usWhich = (vusCurrent - usUpdateBottom) % MAX_LINES; // sNew = (vusCurrent - usUpdateBottom); usWhich = ((sNew >= 0) ? sNew % MAX_LINES : MAX_LINES - (-sNew % MAX_LINES)); // Initialize the width of the text line // WinQueryWindowRect(hwnd, &rclWindow); rclArea.xLeft = rclWindow.xLeft; rclArea.xRight = rclWindow.xRight; // For all the relevant lines... // for (usUpdate = usUpdateBottom; usUpdate <= usUpdateTop; usUpdate++) { // Initialize the height of the text line // rclArea.yBottom = vcpelyChar * usUpdate; rclArea.yTop = rclArea.yBottom + (vcpelyChar - 1); // Load the line to be printed // pszTmp = vaszBuffer[usWhich]; cchTmp = strlen(pszTmp); // Always draw in a word wrapped fashion // if (cchTmp) { cchDrawn = WinDrawText(hps, cchTmp, pszTmp, &rclArea, CLR_BLACK, CLR_BACKGROUND, DT_ERASERECT | DT_LEFT | DT_TOP | DT_WORDBREAK); if (usWhich = = vusCurrent) { while (cchDrawn && cchDrawn < cchTmp) { // Fix up code, line's longer than expected! // We must perform additional word wrapping. // // Add a new line to the line buffer (this // code assumes that we must be on the last // line to wrap). // sNew = (vusCurrent + 1) % MAX_LINES; vaszBuffer[sNew][0] = '\0'; strcpy(vaszBuffer[sNew], vaszBuffer[vusCurrent] + cchDrawn); // Adjust old line to reflect character deletion // vaszBuffer[vusCurrent][cchDrawn] = '\0'; // Set up new parameters for TyperChar() // vcchLine -= cchDrawn; vusCurrent = sNew; // Scroll the window, then fill in the invalid region // WinScrollWindow(hwnd, 0, vcpelyChar, NULL, NULL, NULL, &rclArea, 0); pszTmp = vaszBuffer[vusCurrent]; cchTmp -= cchDrawn; cchDrawn = WinDrawText(hps, cchTmp, pszTmp, &rclArea, CLR_BLACK, CLR_BACKGROUND, DT_ERASERECT | DT_LEFT | DT_TOP | DT_WORDBREAK); // We increment the update pointer because there's one // fewer line to update. usUpdate++; } } } else { // Clear the rectangle // WinFillRect(hps, &rclArea, CLR_BACKGROUND); } // Point to the next line to be drawn // This is functionally equivalent to // usWhich = (usWhich - 1) % MAX_LINES; // usWhich = (usWhich ? (usWhich - 1) : MAX_LINES - 1); } } VOID InitLeadByteTable(VOID) { /* This routine initializes the array which tells if index "i" is a valid leading byte in the current codepage. */ COUNTRYCODE ctryc; // Used with DosGetDBCSEv() call UCHAR vachDBCSEv[TABLE_SIZE]; // Lead Byte range table SHORT i, j; // Temporary variables // Initialize the array // for (i = 0; i <= 0xFF; i++) vfIsLeadByte[i] = FALSE; // Get the valid lead byte ranges // Use (country, codepage) = (0, 0) for current process settings // ctryc.country = ctryc.codepage = 0; DosGetDBCSEv(TABLE_SIZE, &ctryc, vachDBCSEv); // Fill in the array, "blacking out" all the returned ranges // while (vachDBCSEv[j] && vachDBCSEv[j + 1]) { for (i = vachDBCSEv[j]; i <= vachDBCSEv[j + 1]; i++) vfIsLeadByte[i] = TRUE; j + = 2; } } Common Questions and Answers Why aren't code pages sorted in glyph order? The data would be misrepresented upon code page switch. It was decided that it would be better to force the 00H to 7FH range to be mainly ASCII, and have the code pages differ in the 80H to FEH range. What is the code point for the DBCS space character? Any DBCS code page has two code points for space characters. In code page 932, they are 20H, the single byte space, and 8140H, the double byte space. The double byte space covers the area of two single byte spaces on a Vio screen. How does DBCS affect filenames? On a FAT file system a file can have an 8-byte filename and a 3-byte extension. In DBCS, the only rule is that the filename and extension must be legal DBCS strings (they do not start with a trailing byte or end with a leading byte), so the filename can contain up to four double byte characters, with up to one double byte character in the extension. For file systems with long name support, the 254 character limit translates into a 254 byte limit; again, with the provision that the filename is a legal DBCS string. It is recommended, however, that filenames be in ASCII range from 20H to 7FH if it is critical to interchange files worldwide. Are the C run-time library calls DBCS-enabled? No, they aren't, at least not the U.S. editions. The functions strlen and strcpy will work with NULL-terminated strings, because NULL isn't a valid trailing byte, but most of the other string functions might not (such as scanf). The Japanese run-time library calls will work properly with code page 932, but not with other DBCS code pages. What is the maximum number of disjoint lead byte ranges in a code page? Currently, the maximum number of ranges returned by the system is five. How should sorting be handled in DBCS code pages? The DosGetCollate function will return a table of sorted single byte and lead byte characters, so double byte characters should be sorted according to the value of their lead bytes. If two double byte characters have the same lead byte, sort them by their trail bytes (the double byte ranges are in sorted order). For code page 932, the collating sequence is English, then Kana, then double byte characters. Building an Extensible Clipboard Viewer Through the Use of Dynamic-Link Libraries Kevin P. Welch VIEWER is an application for the Microsoft Windows environment that demonstrates advanced clipboard handling techniques. Similar in many ways to CLIPBRD, VIEWER (see Figure 1) functions as an extensible viewer for the display and manipulation of clipboard data. Like CLIPBRD, VIEWER allows you to view the current clipboard contents. Unlike CLIPBRD, VIEWER allows you to choose from among all available data formats (see Figure 2), and extend its viewing capabilities through the use of dynamically linked display libraries. With this capability you can interactively add or remove libraries from the viewer, and support the display and manipulation of private or predefined clipboard formats. In addition, these libraries can provide other information associated with the selected format, such as full name, owner, object dimensions, and size. When you first bring up the VIEWER in Windows it will be incapable of displaying clipboard data. You will, however, be able to tell which formats the clipboard itself has available at any given moment by using the status window and the Display pull-down menu. To display clipboard data you must install a display library. BITMAP.DLL and OWNER.DLL, two such libraries, are provided with the VIEWER. If you copy both libraries to your Windows directory you can install them into the VIEWER. You should install the BITMAP.DLL library for both the Bitmap and Display Bitmap formats and the OWNER.DLL library for Owner Display. Although most programs work with many more than these three formats, you will be able to see some of the possibilities the VIEWER creates. These display libraries are installed into the VIEWER using the Add Format... command under the Options menu. When selected, the dialog box shown in Figure 3 is displayed. It enables you to enter the name of the dynamic library you wish to install and the corresponding clipboard format for which it will be responsible. When you enter a clipboard format, be careful to type the exact characters that make up the name, including their case. If you wish, you can reference one of the predefined clipboard formats by entering a name exactly from the table in Figure 4. VIEWER will consider the entry of a name not on this table as user-defined and will automatically register this name as a new clipboard format. The library field should contain the full name and path of the dynamically linked display library that will support the clipboard format. In its current state, the program is incapable of detecting invalid libraries, so be sure to specify the correct library. When you press the Add button the system will automatically load the dynamic library and update WIN.INI to reference the new library. VIEWER's client area will also immediately reflect the changes. If the newly supported format is selected, the data will be displayed in the client area. Note that you can use the same library to support multiple clipboard formats. This allows you, for example, to use the same generalized text handling library for the support of the CF_TEXT, CF_DIF, CF_SYLK, and perhaps even PostScript formats. You can also replace an existing display library by installing a new one using the same clipboard format name. VIEWER will inform you that the format is currently supported and ask if you wish to replace the installed library. As is the case when installing a new display library, when you replace an existing one the window's client area will automatically reflect any changes made. You can remove a display library from the application at any time. This is done with the Remove Format command under the Options menu. When selected, the dialog box shown in Figure 5 will be displayed. It lists all supported clipboard formats and their associated dynamic display libraries. Both the Add and Remove Format commands manipulate a section of WIN.INI (the Windows initialization and configuration file), saving the current list of supported clipboard formats and their associated display libraries. The format of the VIEWER entry in WIN.INI is as follows: [Clipboard] <Format>=<Dynamic Library Name> If you wish, you can manually create this section in WIN.INI using a text editor and enter the formats and libraries. When VIEWER is first run it will automatically load all the clipboard display libraries listed here. For example (assuming you had the dynamic display libraries present), the following entry in WIN.INI would define support for the CF_TEXT, CF_BITMAP, and CF_METAFILEPICT clipboard formats: [Clipboard] Text=C:\WINDOWS\DLL\TEXT.DLL Bitmap=C:\WINDOWS\DLL\BITMAP.DLL Picture=C:\WINDOWS\DLL\PICTURE.DLL VIEWER Components VIEWER is built from the series of source files shown in Figures 6 through 10. The VIEWER make file is like that of any Windows application. It references all the viewer components and lists an ordered sequence of instructions that assists construction of the application. Note that VIEWER uses the medium programming model (far code and near data references), placing each source code module in its own discardable segment. This segmentation is important since it breaks down the application into a number of smaller modules, each around 4Kb in size. VIEWER.DEF defines the operating characteristics of VIEWER, including the specification for the default attributes of each code segment. Note that all of the segments are marked as discardable. This allows Windows1 to remove all the application code from memory if necessary, retaining only its own internal data structures that manage the program. Although the DEF file doesn't show it, the segment _TEXT is also present, assuming MOVEABLE and DISCARDABLE as default code attributes. This segment is always present when an application uses the C run-time library and references the Windows API. If you look at the MAP file created by the linker when you build VIEWER, you will notice that _TEXT is one of the largest segments. Although not a problem in this case, with a larger application _TEXT can grow well beyond the optimal 4Kb Windows code segment size. In this situation you would be well-advised to obtain the source code to the C run-time library and recompile it into a number of carefully selected segments. For additional information on program optimization, see "Techniques for Tuning and Optimization of PM and Windows Applications," MSJ (Vol.5, No.1). The VIEWER.H include file is referenced by the application resource file and each of the source code modules. It provides definitions of general interest, as well as function prototypes and a number of useful programming macros. Note the private message definitions in this file. Structurally speaking, VIEWER consists of a main, top-level window that manages three child windows (the status, client, and size box windows), and several dialog boxes. The dialog boxes, status, and client windows communicate with the top-level window using these private messages. The WM_UPDATE message is sent by the top-level window when a change occurs to the clipboard or when the user selects a different format for display. The WM_ADDFMT message is sent by the Add Format dialog box to the top-level window whenever a new display library is to be installed. Although the data structure that maintains the display libraries is publicly defined, it is managed by the top-level window only. The WM_GETFMT message is sent by the Remove Format dialog box to the top-level window to retrieve the list of supported clipboard formats and their associated display libraries. This message, in effect, circumvents the Windows limitation that prevents the transfer of data to a dialog box at the time it is created. Analogous to WM_ADDFMT, the WM_REMFMT message is sent by the Remove Format dialog box to the top-level window when the user selects a particular display library for removal. The top-level window is then responsible for updating the display library data structure and for notifying each child window of the change in application capabilities. Following the private messages in VIEWER.H are a series of definitions that specify display library function addresses. These ordinal values are used throughout the application to reference particular routines in the dynamically linked display libraries. VIEWER must explicitly create procedure addresses for each function in the display library since the module handle is unknown at load time. The last item of interest in VIEWER.H is the library module data structure. Currently, VIEWER is arbitrarily limited to 16 display libraries. Each of these libraries is listed in a fixed length array that contains the library module handle (necessary when dynamically creating function addresses), and the associated clipboard format number for which it is responsible. VIEWER.RC defines the VIEWER menu and the various associated dialog boxes. Throughout the dialog box templates you will notice the use of the CONTROL statement in place of such identifiers as LISTBOX, BUTTON, and so on. This is because the dialog boxes used in the RC file were created by the Windows dialog box editor. The dialog box editor distributed with Version 2.1 of the Windows Software Development Kit uses only the CONTROL identifier when specifying the size, position, and style of a control in a DLG file. Other items to notice are the size and position values used for the dialog box controls. VIEWER uses only even numbered values. Since they are specified in RC units, Windows transforms them into screen units according to the following formula when the control is created: screenX = rcX * <Avg Character Width> / 4 screenY = rcY * <Avg Character Height> / 8 This scaling is dependent on the characteristics of the default system font; therefore, you can expect it to change from device to device. If the average character width is a multiple of four, or if the character height is a multiple of eight, then the RC coordinate system is identical to the display coordinate system. On some systems this is, unfortunately, not the case. Well-written applications anticipate these scaling changes and attempt to use multiples of four horizontally and multiples of eight vertically. Although this isn't always possible, it does eliminate positioning errors when your application is run with unusual system fonts. At the very least, positioning errors can be minimized if you use multiples of two. VIEWER1.C The VIEWER1.C module (see Figure 7) contains the application's WinMain, the message processing loop, and the function responsible for handling all the messages relating to the top-level window. At the beginning of this module you will notice the definition of identifiers for each of the VIEWER child windows. Although they are not really used in the program, we could enumerate all the child windows and recognize them by their identifiers. Following the identifiers is a list of properties maintained by the top-level window. In addition to providing a handle to the next member of the clipboard viewer chain (CBCHAIN) and the currently selected clipboard format (CBFORMAT), this property list also maintains the data structure used in the management of the dynamically linked display libraries (CBLIBRARY). WinMain is listed after the property list definitions. This function is perhaps in a different form than what you are used to. It registers the VIEWER window class, creates it in an appropriate size and location on the screen, then retrieves and dispatches all the application messages until the program is ended. Note how the initial width and height of the window are calculated. You could use the default values, but for users with high-resolution displays these values result in unusually large windows. Because of this the maximum window size is limited by using the current system metrics. Following WinMain is the heart of the application, ViewerWndFn. This function handles all the messages related to the top-level application window, including those generated when menu items are selected. This function usually acts like a large switch statement, intercepting those messages of interest and passing the remainder on to the system for default handling. Various messages encountered by the ViewerWndFn are described as follows. The first message processed by ViewerWndFn is WM_CREATE. When this message is received the function immediately defines the current and previous application instance handles. If you notice carefully, during the CreateWindow call in WinMain, the handle to the previous application instance is passed to the ViewerWndFn using one of the creation parameters. This value is retrieved inside the function and used to block the registration of child window classes when previous instances exist. After defining the default window property lists, the function creates the initial dynamic display library structure, loading the libraries listed in WIN.INI. Following this the associated child window classes are defined, registered, and created. When all are successfully created the internal property lists are updated and the child windows are displayed. The WM_GETMINMAXINFO message is used by the system to retrieve the minimum and maximum window dimensions. The ViewerWndFn responds to this message by setting the minimum window size to an arbitrary multiple of appropriate system metrics. The WM_INITMENU message is received whenever the user selects the top-level application menu. Rather than saving the current menu state, the ViewerWndFn resets the entire menu each time it is selected, enabling or disabling options based on the current state of the application. Although this takes a little extra code, it simplifies the program and eliminates additional data items. The WM_COMMAND message is received whenever one of the menu items is selected. By design, the application contains a variable number of menu items under the Display pull-down menu. The menu is used to list each of the formats currently on the clipboard. The identifiers attached to these menu items are equal to the clipboard format number plus IDM_FORMATS. This implies that the user has selected one of the clipboard formats when a value greater than IDM_FORMATS is received. The first menu item handled under WM_COMMAND is IDM_STATUS. When Status is selected the status window display is toggled, making it either hidden or visible, followed by a forced resizing of all top-level child windows. Resizing is accomplished when the SendMessage function call sends a message to itself using the new window dimensions as parameters. The use of such reentry mechanisms is hotly debated in many Windows programming circles. In one sense they can be thought of as function calls, executing a specific and known set of code. In another sense, they are more complicated than a simple function call, sometimes requiring considerable additional stack space and linkage to other code resources. Like most issues of this kind, you will probably be the ultimate judge of its efficacy. At the very least you should be aware of the issues involved and clearly understand the ensuing flow of control that might result. Following IDM_STATUS are a number of other menu items that bring up assorted dialog boxes, erase the current clipboard contents, or select new clipboard formats. Selecting a new clipboard format is perhaps of greatest interest. In this process the clipboard is opened, the current format defined, the data retrieved, and a search commenced for a display library capable of displaying the format. Note how the CBGETDATA property is set to TRUE before GetClipboardData is called. This acts as a semaphore, blocking any and all WM_DRAWCLIPBOARD messages received during the GetClipboardData call. This situation occurs when a request is made for data that is provided as part of a delayed rendering scheme. Without this semaphore, a number of undesirable activities might be performed when you least expect them. As soon as the selected data item is retrieved and the responsible display library determined, each of the associated child windows is notified using a WM_UPDATE command. The child windows can then respond to this new information, updating their respective client areas. The WM_ADDFMT message is sent to the top-level window by the Add Format dialog box whenever the user adds a new display library to the system. In this case wParam is equal to the new clipboard format and lParam is equal to a long pointer to the display library name. While handling this message, ViewerWndFn checks to see if the clipboard format is already supported by a display library. If it is, you will be asked if you wish to replace the existing library with the new one. If the format is unsupported, the new library will be appended to the end of the library data structure and the display will be updated. In both cases WIN.INI is updated to indicate the installation of the new library. The WM_GETFMT message is sent to the top-level window by the Remove Format dialog box to retrieve the current display library list. The resulting concatenated series of strings can then be used when removing one of the display libraries. The WM_REMFMT message is sent to the top-level window by the Remove Format dialog box when a display library is selected for removal. When received, the library is released, the library data structure updated, WIN.INI modified, and the entire application notified of the change in display capabilities. This notification process is especially visible if you remove a library while viewing a rendering of the clipboard using that particular format: the VIEWER client area goes blank. The WM_SIZE command is received when the top-level window is first created, or whenever the window size changes. This message causes the size and location of each top-level child window to be recalculated using the parameters provided with the message. The end result is an updated display that reflects each of the adjusted window sizes. Note that the WM_PAINT message normally follows the WM_SIZE message. The WM_DRAWCLIPBOARD message is usually received whenever the clipboard contents change. This message is first passed down the clipboard viewer chain. Following this, the old clipboard format names are removed from the application menu and the new ones are enumerated. The order in which each new format is encountered is identical to that used by the copying application. When the clipboard contents have been enumerated, the data for the first supported format is retrieved. To prevent unwanted WM_DRAWCLIPBOARD messages, it is again surrounded by a semaphore. Finally, the clipboard is closed, the menu updated to reflect the selected format, and all the child windows informed of the change. The WM_CHANGECBCHAIN message is received whenever a change is made to the clipboard viewer chain. If the window being removed is the next in the chain, then the link is updated to reference the following window. In all other cases the message is sent down the chain after a check to see if a valid handle exists for the next window. This prevents the sending of messages to an invalid or NULL window handle. The WM_DESTROY message is one of the last received by the ViewerWndFn. When received, all display libraries are released, the application unlinked from the clipboard viewer chain, and all associated window properties released. VIEWER2.C The VIEWER2.C module (see Figure 8) contains the window message handling functions for the status and client windows. The status window displays the name and size of the selected clipboard format and the current clipboard owner. The client window, immediately below the status window, is responsible for the actual display of the data object. The StatusWndFn processes all the status window related messages. Although most of the actions performed by this function are reasonably straightforward, there are a few clipboard-related operations that warrant further explanation. The status window function, like any other window function, remains dormant until a message is received. Only WM_UPDATE and WM_PAINT result in any significant action within the status window. The WM_UPDATE message informs the status window that the clipboard contents have changed. The parameters provided with this message include the new clipboard format and an associated data handle. These values are subsequently saved for future reference using window property lists. Although the status window saves a handle to the current clipboard data, note that, in general, this is not good programming practice. The use of this technique requires that the status window be immediately notified whenever the clipboard contents change. Failure to do so could result in the use of an invalid data handle. The WM_PAINT message is received whenever a portion of the status window needs repainting. Like many other Windows applications, the StatusWndFn routine simplifies this request and repaints the entire window. The actual update process involves a number of clipboard-related activities. The first of these is the retrieval of the current clipboard owner name. This is done by retrieving the window handle of the clipboard owner, retrieving the module handle from the class data structure defining the window, and finally retrieving the module file name. The following code fragment from the WM_PAINT case illustrates this sequence: GetModuleFileName( GetClassWord( GetClipboardOwner(), GCW_HMODULE ), szFmtSize, sizeof(szFmtSize) ); After retrieving the module file name the code determines the size and other characteristics of the current clipboard data object. For most formats this is relatively easy since it involves a simple call to GlobalSize using the current data handle as a parameter. In the case of bitmaps and metafiles this approach is insufficient. Both of these clipboard formats involve the use of a special data structure that contains another handle referencing the actual bits or encoded GDI function calls. For the CF_BITMAP and CF_DSPBITMAP formats this process involves retrieving a BITMAP data structure using the GDI GetObject function. This data structure (defined in WINDOWS.H) contains the height, width, color format, and bit values of a logical bitmap, as follows: typedef struct tagBITMAP { short bmType; short bmWidth; short bmHeight; short bmWidthBytes; BYTE bmPlanes; BYTE bmBitsPixel; LPSTR bmBits; } BITMAP; The size of the bitmap is calculated by multiplying, in bytes, the bitmap height by the bitmap width by the number of color planes. Although only one type of logical bitmap is in use at present, this may not be the case in the future. For this reason you should check the bitmap type before directly manipulating the bitmap bits. To calculate the metafile size for the CF_METAFILEPICT and CF_DSPMETAFILEPICT formats, a different process is used. Unlike the bitmap case, the metafile data handle references a global memory object in the following format: typedef struct tagMETAFILEPICT { int mm; int xExt; int yExt; HANDLE hMF; } METAFILEPICT; The metafile size, in addition to other supplemental information, is calculated by locking the handle and summing the respective global size of the METAFILEPICT data structure (defined in WINDOWS.H) and actual metafile bits referenced by the hMF handle. The ClientWndFn processes all the client window related messages. Internally, this function maintains three distinct window properties--the current clipboard format, a handle to the display library module responsible for displaying the format, and an internal data structure for exclusive use by the display library when managing the display of the clipboard data. The actual format of the display data structure is determined by the active display library. The client window serves only as an agent to store and retrieve the information. In most cases the display library will use this data structure to save a handle to the currently selected clipboard data in addition to other relevant information. Like the status window, the client window processes only a few selected messages, passing the remainder on to the DefWindowProc. Of the processed messages, all but WM_CREATE involve some interaction with a dynamically linked display library. As it does in the status window, the WM_UPDATE message informs the client window that the clipboard contents have changed. The ClientWndFn function first responds to this message by asking the currently active display library to restore the window to its original state, then by asking the new display library to reinitialize the window. Since the display libraries were manually loaded by the top-level VIEWER window, the ClientWndFn must explicitly link to the library functions using predefined ordinal values. Both the old and new display libraries must be present in memory when the WM_UPDATE command is received. This is because the old library is responsible for restoring the window to its original condition, and the new library for reinitializing the window. The end result is that the VIEWER window cannot release a library until the client window has finished updating itself. Although VIEWER is responsible for loading and unloading the library, the display library determines how the selected format is displayed and what changes to the client window are necessary to display the information. For example, if you developed a library for the display of CF_OWNERDISPLAY data, the library would be responsible for dispatching the WM_PAINTCLIPBOARD, WM_SIZECLIPBOARD, and all other owner display related messages to the current clipboard owner. VIEWER3.C The VIEWER3.C module simply contains the code that manages the various dialog boxes used by the VIEWER application (see Figure 9). The AddFmtDlgFn manages the Add Format dialog box. This function waits until the user enters both the clipboard format name and the library responsible for handling its display. When you press the Add button these names are retrieved and passed to the parent window using a WM_ADDFMT message. The parent window (in this case, VIEWER) responds to this message by loading the library and inserting it into its internal library data structure. The RemFmtDlgFn is responsible for managing the Remove Format dialog box. When the dialog box is created, this function retrieves a list of the currently installed display libraries from the VIEWER window. This list of libraries and their associated clipboard formats is displayed in a list box. After selecting a library, you can remove it from the system by clicking on the Remove button. This causes a WM_REMFMT message to be sent to the VIEWER window, which updates the status and client windows and releases the display library. VIEWER4.C The fourth and last module, VIEWER4.C (see Figure 10), contains the code for a number of utility functions used throughout the VIEWER application. Two of these functions are of particular interest. The GetClipboardFmtName function is an extension of the Windows API function, GetClipboardFormatName. However, GetClipboardFmtName can return the name of any clipboard format, including CF_OWNERDISPLAY and the formats predefined in WINDOWS.H, unlike its Windows equivalent. In the case of predefined clipboard formats, the function returns the commonly used name of the format. Although these names may differ slightly from program to program, most applications use these values consistently when translating a clipboard format number to a corresponding text string. For CF_OWNERDISPLAY data, the function sends a WM_ASKCBFORMATNAME message to the current clipboard owner, asking it to provide the full name of the format. If the clipboard is without an owner, the text string "Owner Display" is returned by default. For those people who are unfamiliar with the CF_OWNERDISPLAY clipboard format, it is one of the most interesting, yet underutilized clipboard formats supported by Windows. When data is supplied in this format, an application can manage the display or the information in the clipboard viewer window. For example, Windows Write uses this technique to display formatted text copied to the clipboard. In the case of formats not predefined in WINDOWS.H, GetClipboardFmtName assumes that they have been manually registered. Their formal name is retrieved using the GetClipboardFormatName function. The GetClipboardFmtNumber function is similar to GetClipboardFmtName in that it returns the internal clipboard format number associated with the text string provided. In the case of predefined formats it returns the value defined in WINDOWS.H. For all others, it manually registers the name provided using RegisterClipboardFormat and returns the result. Creating Display Libraries Two display libraries, with complete source code, are provided with the VIEWER application. (Full source code can be downloaded from any of the MSJ bulletin boards listed on the inside back cover--Ed.) As mentioned previously, the BITMAP.DLL (see Figure 11) library supports the display of color and monochrome bitmaps and the OWNER.DLL library the display of CF_OWNERDISPLAY data. Although these two libraries take very different approaches while supporting their respective data formats, they use an identical set of entry points, function parameters, and return values. In addition, since these display modules are DLLs, they are subject to the standard SS != DS programming constraint and require the use of an assembly language entry point. The sidebar describes each of the entry points and parameter lists that must be supported when creating your own display libraries for the VIEWER. When creating a display library, the module definition file must explicitly export each of the functions using the ordinal values shown in Figure 12. This article has covered a number of important clipboard-related topics and demonstrated how an extensible clipboard viewer can be created that separates the management and display of information into two distinct tasks. Although the concepts presented here are relatively simple, the definition of dynamic-link libraries that support the display and manipulation of specific data types can have broad application to commercial Windows products. Figure 2 Clipboard Formats Format Description DLL to Use CF_TEXT Conventional ASCII text CF_BITMAP Windows GDI bitmap BITMAP.DLL CF_METAFILEPICT Windows metafile picture CF_SYLK Microsoft Symbolic Link format (spreadsheet data) CF_DIF Software Arts' Data Interchange Format CF_TIFF Tagged Image File Format CF_OEMTEXT OEM text CF_OWNERDISPLAY Owner display format OWNER.DLL CF_DSPTEXT Display text CF_DSPBITMAP Display bitmap BITMAP.DLL CF_DSPMETAFILEPICT Display metafile picture CF_PRIVATEFIRST First privately owned data object CF_PRIVATELAST Last privately owner data object CF_GDIOBJFIRST First GDI data object CF_GDIOBJLAST Last GDI data object BIFF Binary Interchange File Format CSV Comma Separated Variable Printer_Bitmap Printer bitmap Printer_Picture Printer metafile picture Rich Text Rich text format Postscript PostScript text Figure 4 Name Constant DLL to Use Text CF_TEXTCF Bitmap CF_BITMAP BITMAP.DLL Picture CF_METAFILEPICT SYLK CF_SYLK DIF CF_DIF TIFF CF_TIFF OEM TEXT CF_OEMTEXT Owner Display CF_OWNERDISPLAY OWNER.DLL Display Text CF_DSPTEXT Display Bitmap CF_DSPBITMAP BITMAP.DLL Display Picture CF_DSPMETAFILEPICT Figure 6 VIEWER STDFLAGS=-c -u -AM -FPa -Gsw -Os -Zep -W2 viewer.res: viewer.rc viewer.h viewer.ico rc -r viewer.rc viewer1.obj: viewer1.c viewer.h cl $(STDFLAGS) -NT _VIEWER viewer1.c viewer2.obj: viewer2.c viewer.h cl $(STDFLAGS) -NT _DISPLAY viewer2.c viewer3.obj: viewer3.c viewer.h cl $(STDFLAGS) -NT _DIALOG viewer3.c viewer4.obj: viewer4.c viewer.h cl $(STDFLAGS) -NT _UTILITY viewer4.c viewer.exe: viewer1.obj viewer2.obj viewer3.obj \ viewer4.obj viewer.res viewer.def link4 viewer1+viewer2+viewer3+viewer4 /AL:16 /NOE,\ viewer,,mlibw,viewer.def rc viewer.res VIEWER.DEF NAME VIEWER DESCRIPTION 'Extensible Clipboard Viewer' STUB 'WINSTUB.EXE' DATA MOVEABLE MULTIPLE CODE MOVEABLE DISCARDABLE SEGMENTS _VIEWER MOVEABLE DISCARDABLE LOADONCALL _DISPLAY MOVEABLE DISCARDABLE LOADONCALL _DIALOG MOVEABLE DISCARDABLE LOADONCALL _UTILITY MOVEABLE DISCARDABLE LOADONCALL HEAPSIZE 2048 STACKSIZE 2048 EXPORTS AboutDlgFn @1 StatusWndFn @2 ClientWndFn @3 ViewerWndFn @4 AddFormatDlgFn @5 RemFormatDlgFn @6 VIEWER.H /* * WINDOWS CLIPBOARD VIEWER - GENERAL HEADER FILE * * LANGUAGE : Microsoft C 5.1 * ENVIRONMENT : Microsoft Windows 2.1 SDK * STATUS : operational * * 1.01-Kevin P. Welch-add param to GetClipboardFmtName. * */ /* main menu definitions */ #define IDM_STATUS 0x0100 #define IDM_ADDFMT 0x0101 #define IDM_REMFMT 0x0102 #define IDM_EXIT 0x0103 #define IDM_ABOUT 0x0104 #define IDM_ERASE 0x0105 #define IDM_FORMATS 0x0106 /* private message definitions */ #define WM_UPDATE (WM_USER+1) #define WM_ADDFMT (WM_USER+2) #define WM_GETFMT (WM_USER+3) #define WM_REMFMT (WM_USER+4) /* dialog box definitions */ #define IDADD 0x0100 #define IDFORMAT 0x0101 #define IDLIBRARY 0x0102 #define IDREMOVE 0x0100 #define IDLIBLIST 0x0101 /* library function addresses */ #define LIB_INIT MAKEINTRESOURCE(1) #define LIB_CREATE MAKEINTRESOURCE(2) #define LIB_SIZE MAKEINTRESOURCE(3) #define LIB_HSCROLL MAKEINTRESOURCE(4) #define LIB_VSCROLL MAKEINTRESOURCE(5) #define LIB_PAINT MAKEINTRESOURCE(6) #define LIB_DESTROY MAKEINTRESOURCE(7) /* general programming extensions */ #define ID(x) GetWindowWord(x,GWW_ID) #define PARENT(x) GetWindowWord(x,GWW_HWNDPARENT) #define INSTANCE(x) GetWindowWord(x,GWW_HINSTANCE) #define WARNING(x,y) MessageBox(x,y,"Clipboard Viewer",\ MB_OK|MB_ICONEXCLAMATION) /* library module data structure */ #define MAX_MODULE 16 typedef struct { WORD hModule; WORD wFormat; } MODULE; typedef struct { WORD wModules; MODULE Module[MAX_MODULE]; } LIBRARY; typedef LIBRARY FAR * LPLIBRARY; /* viewer function definitions (viewer1.c) */ LONG FAR PASCAL ViewerWndFn( HWND, WORD, WORD, LONG ); /* client and status function definitions (viewer2.c) */ LONG FAR PASCAL ClientWndFn( HWND, WORD, WORD, LONG ); LONG FAR PASCAL StatusWndFn( HWND, WORD, WORD, LONG ); /* dialog function definitions (viewer3.c) */ BOOL FAR PASCAL Dialog( HWND, LPSTR, FARPROC ); BOOL FAR PASCAL AboutDlgFn( HWND, WORD, WORD, LONG ); BOOL FAR PASCAL RemFormatDlgFn( HWND, WORD, WORD, LONG ); BOOL FAR PASCAL AddFormatDlgFn( HWND, WORD, WORD, LONG ); /* utility function definitions (viewer4.c) */ BOOL FAR PASCAL CenterPopup( HWND, HWND ); WORD FAR PASCAL GetClipboardFmtNumber( LPSTR ); HANDLE FAR PASCAL GetClipboardModule( WORD, HANDLE ); WORD FAR PASCAL GetClipboardFmtName( WORD, LPSTR, WORD, BOOL ); /* undocumented internal function definitions */ int FAR PASCAL lstrlen( LPSTR ); int FAR PASCAL lstrcmp( LPSTR, LPSTR ); LPSTR FAR PASCAL lstrcpy( LPSTR, LPSTR ); LPSTR FAR PASCAL lstrcat( LPSTR, LPSTR ); VIEWER.RC /* * WINDOWS CLIPBOARD VIEWER - RESOURCE FILE * * LANGUAGE : Microsoft C5.1 * ENVIRONMENT : Microsoft Windows 2.1 SDK * STATUS : operational * * 1.01 - Kevin P. Welch - make listboxes unsorted. * */ #include <style.h> #include <debug.h> #include "viewer.h" ViewerIcon ICON Viewer.ico ViewerMenu MENU BEGIN POPUP "&Options" BEGIN MENUITEM "&Status", IDM_STATUS MENUITEM SEPARATOR MENUITEM "&Add Format...", IDM_ADDFMT MENUITEM "&Remove Format...", IDM_REMFMT MENUITEM SEPARATOR MENUITEM "E&xit", IDM_EXIT MENUITEM "A&bout...", IDM_ABOUT END POPUP "&Display" BEGIN MENUITEM "&Erase...", IDM_ERASE,GRAYED MENUITEM SEPARATOR MENUITEM "&1. (Empty)", IDM_FORMATS,GRAYED END END ViewerAbout DIALOG LOADONCALL MOVEABLE DISCARDABLE 32, 32, 200, 92 CAPTION "About Clipboard Viewer..." STYLE WS_BORDER | WS_CAPTION | WS_DLGFRAME | WS_POPUP | WS_VISIBLE BEGIN CONTROL "&Ok", IDOK, "button", BS_DEFPUSHBUTTON | WS_TABSTOP | WS_GROUP | WS_CHILD, 164, 4, 32, 14 CONTROL "Extensible Clipboard Viewer", -1, "static", SS_LEFT | WS_GROUP | WS_CHILD, 4, 6, 144, 8 CONTROL "Microsoft Systems Journal", -1, "static", SS_LEFT | WS_CHILD, 4, 14, 144, 8 CONTROL "March 1990", -1, "static", SS_LEFT | WS_CHILD, 4, 22, 144, 8 CONTROL "Designed && Developed by:", -1, "static", SS_LEFT | WS_CHILD, 4, 38, 144, 8 CONTROL "Kevin P. Welch", -1, "static", SS_LEFT | WS_CHILD, 4, 54, 144, 8 CONTROL "Eikon Systems, Inc.", -1, "static", SS_LEFT | WS_CHILD, 4, 62, 144, 8 CONTROL "989 East Hillsdale Blvd, Suite 260", -1, "static", SS_LEFT | WS_CHILD, 4, 70, 144, 8 CONTROL "Foster City, California 94404", -1, "static", SS_LEFT | WS_CHILD, 4, 78, 136, 8 CONTROL "", -1, "static", SS_BLACKFRAME | WS_CHILD, 160, 0, 1, 92 CONTROL "ViewerIcon", -1, "static", SS_ICON | WS_CHILD, 180, 56, 16, 32 END ViewerAdd DIALOG LOADONCALL MOVEABLE DISCARDABLE 32, 32, 172, 86 CAPTION "Add Format to Viewer..." STYLE WS_BORDER | WS_CAPTION | WS_DLGFRAME | WS_POPUP BEGIN CONTROL "", IDFORMAT, "edit", ES_LEFT | ES_AUTOHSCROLL | WS_BORDER | WS_TABSTOP | WS_CHILD, 38, 4, 90, 12 CONTROL "", IDLIBRARY, "edit", ES_LEFT | ES_AUTOHSCROLL | WS_BORDER | WS_TABSTOP | WS_CHILD, 38, 18, 90, 12 CONTROL "&Add", IDADD, "button", BS_DEFPUSHBUTTON | WS_GROUP | WS_TABSTOP | WS_CHILD, 136, 4, 32, 14 CONTROL "&Cancel", IDCANCEL, "button", BS_PUSHBUTTON | WS_CHILD, 136, 22, 32, 14 CONTROL "&Format :", -1, "static", SS_LEFT | WS_GROUP | WS_CHILD, 4, 6, 32, 8 CONTROL "&Library:", -1, "static", SS_LEFT | WS_CHILD, 4, 20, 32, 8 CONTROL "To add a format to the viewer", -1, "static", SS_LEFT | WS_CHILD, 4, 40, 124, 8 CONTROL "you must enter the name of the", -1, "static", SS_LEFT | WS_CHILD, 4, 48, 120, 8 CONTROL "clipboard format, followed by", -1, "static", SS_LEFT | WS_CHILD, 4, 56, 120, 8 CONTROL "the name of the dynamic library", -1, "static", SS_LEFT | WS_CHILD, 4, 64, 126, 8 CONTROL "that will support the format.", -1, "static", SS_LEFT | WS_CHILD, 4, 72, 120, 8 CONTROL "Text", -1, "static", SS_BLACKFRAME | WS_CHILD, 132, 0, 1, 86 END ViewerRemove DIALOG LOADONCALL MOVEABLE DISCARDABLE 32, 32, 216, 84 CAPTION "Remove Format from Viewer..." STYLE WS_BORDER | WS_CAPTION | WS_DLGFRAME | WS_POPUP BEGIN CONTROL "", IDLIBLIST, "listbox", LBS_NOTIFY | LBS_STANDARD | WS_BORDER | WS_VSCROLL | WS_TABSTOP | WS_CHILD, 4, 4, 168, 74 CONTROL "&Remove", IDREMOVE, "button", BS_DEFPUSHBUTTON | WS_GROUP | WS_TABSTOP | WS_CHILD, 180, 4, 32, 14 CONTROL "&Cancel", IDCANCEL, "button", BS_PUSHBUTTON | WS_CHILD, 180, 22, 32, 14 CONTROL "Text", -1, "static", SS_BLACKFRAME | WS_CHILD, 176, 0, 1, 85 END Figure 7 /* * WINDOWS CLIPBOARD VIEWER - SOURCE CODE * * LANGUAGE : Microsoft C 5.1 * MODEL : medium * ENVIRONMENT : Microsoft Windows 2.1 SDK * STATUS : operational * * 1.01-Kevin P. Welch- add param to GetClipboardFmtName. * */ #define NOCOMM #include <windows.h> #include <process.h> #include <memory.h> #include <string.h> #include <stdio.h> #include "viewer.h" /* viewer window child id numbers */ #define ID_STATUS 1 #define ID_CLIENT 2 #define ID_SIZEBOX 3 /* viewer window properties */ #define CBCHAIN MAKEINTRESOURCE(1) #define CBFORMAT MAKEINTRESOURCE(2) #define CBGETDATA MAKEINTRESOURCE(3) #define CBLIBRARY MAKEINTRESOURCE(4) #define HWNDSTATUS MAKEINTRESOURCE(5) #define HWNDCLIENT MAKEINTRESOURCE(6) #define HWNDSIZEBOX MAKEINTRESOURCE(7) /* * WinMain( hInstance, hPrevInstance, lpszCmd, wCmdShow ) : VOID * * hCrntInst current instance handle * hPrevInst previous instance handle * lpszCmdLine current command line * wInitShowCmd initial show window command * * This function is responsible for registering and creating * the clipboard viewer window. Once the window has been created, * it is also responsible for retrieving and dispatching all the * messages related to the window. * */ VOID PASCAL WinMain( HANDLE hCrntInst, HANDLE hPrevInst, LPSTR lpszCmdLine, WORD wInitShowCmd ) { /* local variables */ MSG msgViewer; WORD wWndWidth; WORD wWndHeight; HWND hWndViewer; WNDCLASS classViewer; /* warning level 3 compatibility */ lpszCmdLine; /* define viewer class */ memset( &classViewer, 0, sizeof(WNDCLASS) ); classViewer.lpszClassName =(LPSTR)"Viewer"; classViewer.hCursor = LoadCursor( NULL, IDC_ARROW ); classViewer.lpszMenuName = (LPSTR)"ViewerMenu"; classViewer.style = CS_HREDRAW | CS_VREDRAW; classViewer.lpfnWndProc = ViewerWndFn; classViewer.hInstance = hCrntInst; classViewer.hIcon = LoadIcon( hCrntInst, "ViewerIcon" ); /* register class if no previous instance */ if ( (hPrevInst) || (RegisterClass(&classViewer)) ) { /* calculate initial width & height */ wWndWidth = ( GetSystemMetrics(SM_CXSCREEN) > 40 * GetSystemMetrics(SM_CXVSCROLL) ) ? 40 * GetSystemMetrics(SM_CXVSCROLL ) : 2 * GetSystemMetrics(SM_CXSCREEN) / 3; wWndHeight = ( GetSystemMetrics(SM_CYSCREEN) > 30 * GetSystemMetrics(SM_CYHSCROLL) ) ? 30 * GetSystemMetrics(SM_CYHSCROLL ) : 2 * GetSystemMetrics(SM_CYSCREEN) / 3; /* create viewer window */ hWndViewer = CreateWindow( "Viewer", "Clipboard Viewer", WS_TILEDWINDOW|WS_CLIPCHILDREN, CW_USEDEFAULT, CW_USEDEFAULT, wWndWidth, wWndHeight, (HWND)NULL, (HMENU)NULL, hCrntInst, (LPSTR)(DWORD)hPrevInst ); /* continue if successful */ if ( hWndViewer ) { /* display window */ ShowWindow( hWndViewer, wInitShowCmd ); /* process all related messages */ while ( GetMessage( &msgViewer, NULL, 0, 0 ) ) { TranslateMessage( &msgViewer ); DispatchMessage( &msgViewer ); } /* normal exit */ exit( msgViewer.wParam ); } else WARNING( NULL, "Unable to create Clipboard Viewer!" ); } else WARNING( NULL, "Unable to register Clipboard Viewer!" ); /* abnormal exit */ exit( TRUE ); } /* * ViewerWndFn( hWnd, wMsg, wParam, lParam ) : LONG * * hWnd window handle * wMsg message number * wParam additional message information * lParam additional message information * * This window function processes all the messages related to * the clipboard viewer window. When created this window * registers and creates the associated status and client windows. * */ LONG FAR PASCAL ViewerWndFn( hWnd, wMsg, wParam, lParam ) HWND hWnd; WORD wMsg; WORD wParam; LONG lParam; { LONG lResult; /* initialization */ lResult = FALSE; /* process each message */ switch( wMsg ) { case WM_CREATE : /* create window */ { char * pKey; HANDLE hFmtLib; HANDLE hLibrary; HANDLE hCrntInst; HANDLE hPrevInst; LPLIBRARY lpLibrary; HWND hWndStatus; HWND hWndClient; HWND hWndSizebox; WNDCLASS classStatus; WNDCLASS classClient; char szLib[128]; char szList[128]; /* define instance handles */ hCrntInst = INSTANCE(hWnd); hPrevInst = (HANDLE)((LPCREATESTRUCT)lParam)->lpCreateParams; /* define initial property lists */ SetProp( hWnd, CBCHAIN, NULL ); SetProp( hWnd, CBFORMAT, NULL ); SetProp( hWnd, CBGETDATA, NULL ); SetProp( hWnd, HWNDSTATUS, NULL ); SetProp( hWnd, HWNDCLIENT, NULL ); SetProp( hWnd, HWNDSIZEBOX, NULL ); /* define library data structure */ hLibrary = GlobalAlloc( GHND, (DWORD)sizeof(LIBRARY) ); if ( hLibrary ) { lpLibrary = (LPLIBRARY)GlobalLock( hLibrary ); if ( lpLibrary ) { /* initialization */ lpLibrary->wModules = 0; /* retrieve list of supported formats */ if ( GetProfileString("Clipboard",NULL,"", szList,sizeof(szList)) ) { pKey = &szList[0]; while ( *pKey ) { GetProfileString("Clipboard",pKey,"", szLib, sizeof(szLib)); if ( szLib[0] > ' ' ) { hFmtLib = LoadLibrary( szLib ); if ( hFmtLib >= 32 ) { lpLibrary->Module[lpLibrary->wModules].hModule = hFmtLib; lpLibrary->Module[lpLibrary->wModules++].wFormat = GetClipboardFmtNumber(pKey); } } pKey += strlen(pKey) + 1; } } /* unlock library & save handle */ GlobalUnlock( hLibrary ); SetProp( hWnd, CBLIBRARY, hLibrary ); /* define status class */ memset( &classStatus, 0, sizeof(WNDCLASS) ); classStatus.lpszClassName =(LPSTR)"ViewerStatus"; classStatus.hCursor = LoadCursor( NULL, IDC_ARROW ); classStatus.style = CS_HREDRAW | CS_VREDRAW; classStatus.lpfnWndProc = StatusWndFn; classStatus.hInstance = hCrntInst; classStatus.hbrBackground =(HBRUSH)(COLOR_WINDOW + 1); /* define client class */ memset( &classClient, 0, sizeof(WNDCLASS) ); classClient.lpszClassName =(LPSTR)"ViewerClient"; classClient.hCursor = LoadCursor( NULL, IDC_ARROW ); classClient.style = CS_HREDRAW | CS_VREDRAW; classClient.lpfnWndProc = ClientWndFn; classClient.hInstance = hCrntInst; classClient.hbrBackground =(HBRUSH)(COLOR_APPWORKSPACE+1); /* register classes if no previous instance */ if ( (hPrevInst) || (RegisterClass(&classStatus) && RegisterClass(&classClient)) ) { /* create child windows */ hWndSizebox = CreateWindow( "Scrollbar", "", WS_CHILD|SBS_SIZEBOX, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, hWnd, (HMENU)ID_SIZEBOX, hCrntInst, (LPSTR)(DWORD)hPrevInst ); hWndClient = CreateWindow( "ViewerClient", "", WS_CHILD|WS_BORDER| WS_HSCROLL|WS_VSCROLL| WS_CLIPSIBLINGS, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, hWnd, (HMENU)ID_CLIENT, hCrntInst, (LPSTR)(DWORD)hPrevInst ); hWndStatus = CreateWindow( "ViewerStatus", "", WS_CHILD|WS_BORDER| WS_CLIPSIBLINGS, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, hWnd, (HMENU)ID_STATUS, hCrntInst, (LPSTR)(DWORD)hPrevInst ); /* continue if successful */ if ( hWndStatus && hWndClient && hWndSizebox ) { /* update property lists */ SetProp( hWnd, HWNDSTATUS, hWndStatus ); SetProp( hWnd, HWNDCLIENT, hWndClient ); SetProp( hWnd, HWNDSIZEBOX, hWndSizebox ); SetProp( hWnd, CBCHAIN, SetClipboardViewer(hWnd) ); /* make child windows visible */ ShowWindow( hWndStatus, SW_SHOWNA ); ShowWindow( hWndClient, SW_SHOWNA ); ShowWindow( hWndSizebox, SW_SHOWNA ); } else { WARNING( hWnd, "Unable to create child windows!" ); PostQuitMessage( 4 ); } } else { WARNING( hWnd, "Unable to register child windows!" ); PostQuitMessage( 3 ); } } else { WARNING( hWnd, "Unable to access instance data!" ); PostQuitMessage( 2 ); } } else { WARNING( hWnd, "Insufficient memory!" ); PostQuitMessage( 1 ); } } break; case WM_GETMINMAXINFO : /* retrieve window size constraints */ /* set minimum tracking size */ ((LPPOINT)lParam)[3].x = 22 * GetSystemMetrics(SM_CXVSCROLL); ((LPPOINT)lParam)[3].y = 10 * GetSystemMetrics(SM_CYCAPTION); break; case WM_INITMENU : /* initialize menu */ { LPLIBRARY lpLibrary; /* check status window visibility */ CheckMenuItem( wParam, IDM_STATUS, IsWindowVisible(GetProp(hWnd,HWNDSTATUS)) ? MF_CHECKED : MF_UNCHECKED ); /* enable-disable format library menu options */ lpLibrary = (LPLIBRARY)GlobalLock( GetProp(hWnd,CBLIBRARY) ); if ( lpLibrary ) { EnableMenuItem(wParam,IDM_ADDFMT,(lpLibrary->wModules < MAX_MODULE) ? MF_ENABLED : MF_GRAYED ); EnableMenuItem(wParam,IDM_REMFMT,(lpLibrary->wModules) ? MF_ENABLED : MF_GRAYED ); GlobalUnlock( GetProp(hWnd,CBLIBRARY) ); } else WARNING( hWnd, "Unable to access instance data!" ); /* enable-disable erase menu option */ EnableMenuItem( wParam, IDM_ERASE, (GetProp(hWnd,CBFORMAT) > IDM_FORMATS) ? MF_ENABLED : MF_GRAYED ); } break; case WM_COMMAND : /* menu command */ /* process sub-message */ switch( wParam ) { case IDM_STATUS : /* toggle status bar visibility */ { RECT rcClient; /* change status bar visibility */ ShowWindow( GetProp(hWnd,HWNDSTATUS), IsWindowVisible( GetProp(hWnd,HWNDSTATUS)) ? SW_HIDE : SW_SHOWNA ); /* force resizing of child windows */ GetClientRect( hWnd, &rcClient ); SendMessage( hWnd, WM_SIZE, 0, MAKELONG(rcClient.right,rcClient.bottom) ); } break; case IDM_EXIT : /* exit application */ PostQuitMessage( 0 ); break; case IDM_ABOUT : /* about viewer */ Dialog( hWnd, "ViewerAbout", AboutDlgFn ); break; case IDM_ADDFMT : /* add a new clipboard format */ Dialog( hWnd, "ViewerAdd", AddFormatDlgFn ); break; case IDM_REMFMT : /* remove a clipboard format */ Dialog( hWnd, "ViewerRemove", RemFormatDlgFn ); break; case IDM_ERASE : /* erase clipboard contents */ /* verify option */ if ( MessageBox(hWnd,"Erase clipboard contents?", "Clipboard Viewer", MB_ICONQUESTION|MB_YESNO) = = IDYES ) { if ( OpenClipboard(hWnd) ) { EmptyClipboard(); CloseClipboard(); } else WARNING( hWnd, "Unable to open clipboard!" ); } break; default : /* one of the selected formats */ /* open clipboard */ if ( OpenClipboard(hWnd) ) { WORD wCrntFmt; HANDLE hCrntData; HANDLE hCrntModule; /* attempt to retrieve clipboard data - semaphore call */ wCrntFmt = wParam - IDM_FORMATS; if ( wCrntFmt ) { SetProp( hWnd, CBGETDATA, TRUE ); hCrntData = GetClipboardData( wCrntFmt ); hCrntModule = GetClipboardModule( wCrntFmt, GetProp(hWnd,CBLIBRARY) ); SetProp( hWnd, CBGETDATA, NULL ); if ( hCrntData = = NULL ) WARNING( hWnd, "Unable to retrieve clipboard data!" ); } else { hCrntData = NULL; hCrntModule = NULL; } /* close clipboard */ CloseClipboard(); /* update checked menu item - even if data inaccessible */ CheckMenuItem( GetMenu(hWnd), GetProp(hWnd,CBFORMAT), MF_UNCHECKED ); SetProp( hWnd, CBFORMAT, wParam ); CheckMenuItem( GetMenu(hWnd), wParam, MF_CHECKED ); /* notify child windows - note that clipboard is now closed */ SendMessage( GetProp(hWnd,HWNDSTATUS), WM_UPDATE, wCrntFmt, MAKELONG(hCrntData,hCrntModule) ); SendMessage( GetProp(hWnd,HWNDCLIENT), WM_UPDATE, wCrntFmt, MAKELONG(hCrntData,hCrntModule) ); } else WARNING( hWnd, "Unable to open clipboard!" ); break; } break; case WM_ADDFMT : /* add a new clipboard library */ { WORD wEntry; HANDLE hFmtLib; HANDLE hOldLib; LPLIBRARY lpLibrary; char szFmt[64]; char szLib[64]; char szMsg[80]; /* copy name of library */ lstrcpy( szLib, (LPSTR)lParam ); /* lock down instance data */ lpLibrary = (LPLIBRARY)GlobalLock( GetProp(hWnd,CBLIBRARY) ); if ( lpLibrary ) { /* define clipboard format name */ GetClipboardFmtName( wParam, szFmt, sizeof(szFmt), FALSE ); /* check to see if format already listed */ for (wEntry=0; (wEntry<lpLibrary->wModules)&& (wParam!=lpLibrary->Module[wEntry].wFormat); wEntry++ ); if ( wEntry < lpLibrary->wModules ) { /* entry already present - ask if replace? */ GetModuleFileName( lpLibrary->Module[wEntry].hModule, szLib, sizeof(szLib) ); sprintf( szMsg, "Replace %s?", szLib ); if ( MessageBox(hWnd,szMsg,"Clipboard Viewer", MB_ICONQUESTION|MB_YESNO) = = IDYES ) { hFmtLib = LoadLibrary( (LPSTR)lParam ); if ( hFmtLib >= 32 ) { lResult = TRUE; hOldLib = lpLibrary->Module[wEntry].hModule; lpLibrary->Module[wEntry].hModule = hFmtLib; SendMessage( hWnd, WM_COMMAND, GetProp(hWnd,CBFORMAT), 0L ); WriteProfileString( "Clipboard", szFmt, szLib ); FreeLibrary( hOldLib ); } } } else { /* check if space available internally */ if ( lpLibrary->wModules < MAX_MODULE ) { hFmtLib = LoadLibrary( (LPSTR)lParam ); if ( hFmtLib >= 32 ) { lResult = TRUE; lpLibrary->Module[lpLibrary->wModules].hModule=hFmtLib; lpLibrary->Module[lpLibrary->wModules++].wFormat = wParam; SendMessage( hWnd, WM_COMMAND, GetProp(hWnd,CBFORMAT), 0L ); WriteProfileString("Clipboard", szFmt, (LPSTR)lParam ); } } else WARNING( hWnd, "Insufficient memory to add library!" ); } /* unlock library data */ GlobalUnlock( GetProp(hWnd,CBLIBRARY) ); } else WARNING( hWnd, "Unable to access instance data!" ); } break; case WM_GETFMT : /* retrieve clipboard format list */ { LPSTR lpStr; WORD wEntry; LPLIBRARY lpLibrary; char szFmt[64]; char szLib[80]; char szEntry[128]; /* lock down instance data */ lpLibrary = (LPLIBRARY)GlobalLock( GetProp(hWnd,CBLIBRARY) ); if ( lpLibrary ) { lpStr = (LPSTR)lParam; lResult = lpLibrary->wModules; for (wEntry=0; wEntry<lpLibrary->wModules; wEntry++) { GetClipboardFmtName( lpLibrary->Module[wEntry].wFormat, szFmt, sizeof(szFmt), FALSE ); GetModuleFileName( lpLibrary->Module[wEntry].hModule, szLib, sizeof(szLib) ); sprintf( szEntry, "%s - %s", szFmt, szLib ); lstrcpy( lpStr, szEntry ); lpStr + = strlen(szEntry) + 1; } lstrcpy( lpStr, "" ); GlobalUnlock( GetProp(hWnd,CBLIBRARY) ); } else WARNING( hWnd, "Unable to access instance data!" ); } break; case WM_REMFMT : /* remove a listed clipboard library */ { HANDLE hOldLib; LPLIBRARY lpLibrary; char szFmt[64]; /* lock down instance data & remove library module */ lpLibrary = (LPLIBRARY)GlobalLock( GetProp(hWnd,CBLIBRARY) ); if ( lpLibrary ) { if ( wParam < lpLibrary->wModules ) { lResult = TRUE; hOldLib = lpLibrary->Module[wParam].hModule; GetClipboardFmtName( lpLibrary->Module[wParam].wFormat, szFmt, sizeof(szFmt), FALSE ); while ( ++wParam < lpLibrary->wModules ) { lpLibrary->Module[wParam-1].wFormat = lpLibrary->Module[wParam].wFormat; lpLibrary->Module[wParam-1].hModule = lpLibrary->Module[wParam].hModule; } lpLibrary->wModules--; SendMessage( hWnd, WM_COMMAND, GetProp(hWnd,CBFORMAT), 0L ); WriteProfileString( "Clipboard", szFmt, "" ); FreeLibrary( hOldLib ); } else WARNING( hWnd, "Attempt to remove invalid library!" ); GlobalUnlock( GetProp(hWnd,CBLIBRARY) ); } else WARNING( hWnd, "Unable to access instance data!" ); } break; case WM_SIZE : /* window being resized */ /* reposition status window (even if invisible) */ MoveWindow( GetProp(hWnd,HWNDSTATUS), -1, -1, LOWORD(lParam)+2, GetSystemMetrics(SM_CYMENU)+2, TRUE ); /* reposition client window (always visible) */ MoveWindow( GetProp(hWnd,HWNDCLIENT), -1, (IsWindowVisible(GetProp(hWnd,HWNDSTATUS))) ? GetSystemMetrics(SM_CYMENU) : -1, LOWORD(lParam)+2, (IsWindowVisible(GetProp(hWnd,HWNDSTATUS))) ? HIWORD(lParam)-GetSystemMetrics(SM_CYMENU)+1 : HIWORD(lParam)+2, TRUE ); /* reposition sizebox window (always visible) */ MoveWindow( GetProp(hWnd,HWNDSIZEBOX), LOWORD(lParam)-GetSystemMetrics(SM_CXVSCROLL)+1, HIWORD(lParam)-GetSystemMetrics(SM_CYHSCROLL)+1, GetSystemMetrics(SM_CXVSCROLL), GetSystemMetrics(SM_CYHSCROLL), TRUE ); break; case WM_DRAWCLIPBOARD : /* clipboard contents changing */ { HMENU hSubMenu; WORD wCrntFmt; WORD wCrntEntry; HANDLE hCrntData; HANDLE hCrntModule; char szFmtName[48]; char szMenuName[48]; /* filter out spurious WM_DRAWCLIPBOARD messages */ if ( GetProp(hWnd,CBGETDATA) = = NULL ) { /* pass the message down the chain first */ if ( GetProp(hWnd,CBCHAIN) ) SendMessage( GetProp(hWnd,CBCHAIN), wMsg, wParam, lParam ); /* retrieve handle to view sub-menu */ hSubMenu = GetSubMenu( GetMenu(hWnd), 1 ); /* remove old clipboard formats */ SetProp( hWnd, CBFORMAT, IDM_FORMATS ); for ( wCrntEntry = GetMenuItemCount(hSubMenu)-1; wCrntEntry>1; wCrntEntry-- ) ChangeMenu( hSubMenu, wCrntEntry, (wCrntEntry>2) ? NULL : "&1. (Empty)", (wCrntEntry>2) ? NULL : IDM_FORMATS, (wCrntEntry>2) ? MF_DELETE|MF_BYPOSITION : MF_CHANGE|MF_GRAYED|MF_BYPOSITION ); /* open clipboard */ if ( OpenClipboard(hWnd) ) { /* enumerate available clipboard formats */ wCrntEntry = 0; hCrntModule = NULL; wCrntFmt = EnumClipboardFormats( NULL ); while ( wCrntFmt ) { /* define new menu entry */ GetClipboardFmtName( wCrntFmt, szFmtName, sizeof(szFmtName), TRUE ); sprintf( szMenuName, "&%u. %s", wCrntEntry+1, (szFmtName[0]) ? szFmtName : "Undefined" ); /* update view menu */ ChangeMenu( hSubMenu, (wCrntEntry) ? NULL : IDM_FORMATS, szMenuName, IDM_FORMATS + wCrntFmt, (wCrntEntry) ? MF_APPEND : MF_CHANGE ); /* define selected format */ if ( hCrntModule = = NULL ) { SetProp( hWnd, CBFORMAT, IDM_FORMATS+wCrntFmt ); hCrntModule=GetClipboardModule(wCrntFmt, GetProp(hWnd,CBLIBRARY)); } /* retrieve next available format */ wCrntEntry++; wCrntFmt = EnumClipboardFormats( wCrntFmt ); } /* attempt to retrieve data handle - semaphore call */ wCrntFmt = GetProp(hWnd,CBFORMAT) - IDM_FORMATS; if ( wCrntFmt ) { SetProp( hWnd, CBGETDATA, TRUE ); hCrntData = GetClipboardData( wCrntFmt ); SetProp( hWnd, CBGETDATA, NULL ); } else { hCrntData = NULL; hCrntModule = NULL; } /* close clipboard */ CloseClipboard(); /* mark selected format */ CheckMenuItem(hSubMenu, GetProp(hWnd,CBFORMAT), MF_CHECKED); /* notify child windows - clipboard now closed */ SendMessage( GetProp(hWnd,HWNDSTATUS), WM_UPDATE, wCrntFmt, MAKELONG(hCrntData,hCrntModule) ); SendMessage( GetProp(hWnd,HWNDCLIENT), WM_UPDATE, wCrntFmt, MAKELONG(hCrntData,hCrntModule) ); } else WARNING( hWnd, "Unable to open clipboard!" ); } } break; case WM_CHANGECBCHAIN : /* clipboard viewer chain being changed */ /* re-link viewer chain */ if ( wParam = = GetProp(hWnd,CBCHAIN) ) SetProp( hWnd, CBCHAIN, LOWORD(lParam) ); else if ( GetProp(hWnd,CBCHAIN) ) SendMessage( GetProp(hWnd,CBCHAIN), wMsg, wParam, lParam ); break; case WM_DESTROY : /* destroy window */ { WORD wEntry; LPLIBRARY lpLibrary; /* retrieve & lock module library */ lpLibrary = (LPLIBRARY)GlobalLock( GetProp(hWnd,CBLIBRARY) ); if ( lpLibrary ) { /* free each listed library */ for ( wEntry=0; wEntry<lpLibrary->wModules; wEntry++ ) FreeLibrary( lpLibrary->Module[wEntry].hModule ); /* unlock data structure */ GlobalUnlock( GetProp(hWnd,CBLIBRARY) ); } /* free allocated memory & unlink from clipboard chain */ GlobalFree( RemoveProp(hWnd,CBLIBRARY) ); ChangeClipboardChain( hWnd, RemoveProp(hWnd,CBCHAIN) ); /* remove remaining properties */ RemoveProp( hWnd, CBFORMAT ); RemoveProp( hWnd, CBGETDATA ); RemoveProp( hWnd, HWNDSTATUS ); RemoveProp( hWnd, HWNDCLIENT ); RemoveProp( hWnd, HWNDSIZEBOX ); /* end it all */ PostQuitMessage( 0 ); } break; default : /* send to default */ lResult = DefWindowProc( hWnd, wMsg, wParam, lParam ); break; } /* return normal result */ return( lResult ); } Figure 8 /* * WINDOWS CLIPBOARD VIEWER - DIALOG SOURCE CODE * * LANGUAGE : Microsoft C 5.1 * MODEL : medium * ENVIRONMENT : Microsoft Windows 2.1 SDK * STATUS : operational * */ #define NOCOMM #include <windows.h> #include <string.h> #include "viewer.h" /* * Dialog( hParentWnd, lpszTemplate, lpfnDlgProc ) : BOOL * * hParentWnd handle to parent window * lpszTemplate dialog box template * lpfnDlgProc dialog window function * * This utility function displays the specified dialog box, using the * template provided. It automatically makes a new instance of the * dialog box function. Note that this function will NOT work * correctly if an invalid or NULL parent window handle is provided. * */ BOOL FAR PASCAL Dialog( hParentWnd, lpszTemplate, lpfnDlgProc ) HWND hParentWnd; LPSTR lpszTemplate; FARPROC lpfnDlgProc; { /* local variables */ BOOL bResult; FARPROC lpProc; /* display palette dialog box */ lpProc = MakeProcInstance( lpfnDlgProc, INSTANCE(hParentWnd) ); bResult = DialogBox( INSTANCE(hParentWnd), lpszTemplate, hParentWnd, lpProc ); FreeProcInstance( lpProc ); /* return result */ return( bResult ); } /* * AddFormatDlgFn( hDlg, wMsg, wParam, lParam ) : BOOL ; * * hDlg handle to dialog box * wMsg message or event * wParam word portion of message * lParam long portion of message * * This function is responsible for adding a new dynamic library * to the list of supported formats. While doing so it checks to * make sure that the specified format is not referenced twice. * After adding a new format, this function also updates WIN.INI to * reference the dynamic library. * */ BOOL FAR PASCAL AddFormatDlgFn( HWND hDlg, WORD wMsg, WORD wParam, LONG lParam ) { BOOL bResult; /* initialization */ bResult = TRUE; /* process message */ switch( wMsg ) { case WM_INITDIALOG : CenterPopup( hDlg, GetParent(hDlg) ); EnableWindow( GetDlgItem(hDlg,IDADD), FALSE ); break; case WM_COMMAND : /* process sub-message */ switch( wParam ) { case IDCANCEL : EndDialog( hDlg, FALSE ); break; case IDADD : { char szFmt[32]; char szLib[64]; /* retrieve format & library names */ GetDlgItemText( hDlg, IDFORMAT, szFmt, sizeof(szFmt) ); GetDlgItemText( hDlg, IDLIBRARY, szLib, sizeof(szLib) ); /* upshift library name */ strupr( szLib ); /* end dialog & add library to list */ EndDialog( hDlg, TRUE ); SendMessage( GetParent(hDlg), WM_ADDFMT, GetClipboardFmtNumber(szFmt), (LONG)(LPSTR)szLib ); } break; case IDFORMAT : case IDLIBRARY : /* enable or disable add button */ if ( HIWORD(lParam) = = EN_CHANGE ) EnableWindow(GetDlgItem(hDlg,IDADD), ( SendMessage(GetDlgItem(hDlg,IDFORMAT), WM_GETTEXTLENGTH,0,0L) && SendMessage(GetDlgItem(hDlg,IDLIBRARY), WM_GETTEXTLENGTH,0,0L) ) ? TRUE : FALSE ); break; default : bResult = FALSE; break; } break; default : bResult = FALSE; break; } /* return final result */ return( bResult ); } /* * RemFormatDlgFn( hDlg, wMsg, wParam, lParam ) : BOOL ; * * hDlg handle to dialog box * wMsg message or event * wParam word portion of message * lParam long portion of message * * This function is responsible for removing the display dynamic * library support for a particular clipboard format. While doing * so is automatically updates WIN.INI and the parent window * instance variables. * */ BOOL FAR PASCAL RemFormatDlgFn( HWND hDlg, WORD wMsg, WORD wParam, LONG lParam ) { BOOL bResult; /* initialization */ bResult = TRUE; /* process message */ switch( wMsg ) { case WM_INITDIALOG : { WORD wEntry; WORD wModules; HWND hWndLibList; char * pszEntry; char szList[512]; /* center window */ CenterPopup( hDlg, GetParent(hDlg) ); EnableWindow( GetDlgItem(hDlg,IDREMOVE), FALSE ); /* initialize dialog box */ wModules = (WORD)SendMessage( GetParent(hDlg), WM_GETFMT, 0, (LONG)(LPSTR)szList ); if ( wModules > 0 ) { /* initialize listbox */ hWndLibList = GetDlgItem( hDlg, IDLIBLIST ); SendMessage(hWndLibList,WM_SETREDRAW, (WORD)FALSE, (LONG)0 ); SendMessage(hWndLibList,LB_RESETCONTENT, (WORD)0, (LONG)0 ); /* retrieve and display each listed library module */ pszEntry = &szList[0]; for ( wEntry=0; wEntry<wModules; wEntry++ ) { SendMessage( hWndLibList, LB_ADDSTRING, (WORD)0, (LONG)(LPSTR)pszEntry ); pszEntry + = strlen(pszEntry) + 1; } /* display listbox */ SendMessage(hWndLibList,WM_SETREDRAW,(WORD)TRUE,(LONG)0); InvalidateRect( hWndLibList, (LPRECT)NULL, TRUE ); } } break; case WM_COMMAND : /* process sub-message */ switch( wParam ) { case IDLIBLIST : /* enable remove button if library selected */ if (HIWORD(lParam) = = LBN_SELCHANGE) EnableWindow( GetDlgItem(hDlg,IDREMOVE), TRUE ); break; case IDREMOVE : { WORD wEntry; /* retrieve selected library index & notify parent */ wEntry = (WORD)SendMessage( GetDlgItem(hDlg,IDLIBLIST), LB_GETCURSEL, 0, 0L ); if ( SendMessage(GetParent(hDlg),WM_REMFMT,wEntry,0L) ) EndDialog( hDlg, TRUE ); } break; case IDCANCEL : EndDialog( hDlg, FALSE ); break; default : bResult = FALSE; break; } break; default : bResult = FALSE; break; } /* return final result */ return( bResult ); } /* * AboutDlgFn( hDlg, wMsg, wParam, lParam ) : BOOL ; * * hDlg handle to dialog box * wMsg message or event * wParam word portion of message * lParam long portion of message * * This function is responsible for processing all the messages * that relate to the Viewer about dialog box. About the only * acts this function performs is to center the dialog box and * wait for the Ok button to be pressed. * */ BOOL FAR PASCAL AboutDlgFn( HWND hDlg, WORD wMsg, WORD wParam, LONG lParam ) { BOOL bResult; /* warning level 3 compatibility */ lParam; /* initialization */ bResult = TRUE; /* process message */ switch( wMsg ) { case WM_INITDIALOG : CenterPopup( hDlg, GetParent(hDlg) ); break; case WM_COMMAND : /* process sub-message */ switch( wParam ) { case IDOK : EndDialog( hDlg, TRUE ); break; case IDCANCEL : EndDialog( hDlg, FALSE ); break; default : bResult = FALSE; break; } break; default : bResult = FALSE; break; } /* return final result */ return( bResult ); } Figure 9 /* * WINDOWS CLIPBOARD VIEWER - STATUS & CLIENT WINDOW SOURCE CODE * * LANGUAGE : Microsoft C 5.1 * MODEL : medium * ENVIRONMENT : Microsoft Windows 2.1 SDK * STATUS : operational * * Note that both the client and status windows save the current * clipboard format and data handle. In general this is NOT good * programming practice. In this situation we assume that we will * always be notifed when ANY change occurs to the clipboard. * * 1.01 - Kevin P. Welch - add param to GetClipboardFmtName. * */ #define NOCOMM #include <windows.h> #include <string.h> #include <stdio.h> #include "viewer.h" /* client window properties */ #define CLIENT_FORMAT MAKEINTRESOURCE(1) #define CLIENT_MODULE MAKEINTRESOURCE(2) #define CLIENT_DISPINFO MAKEINTRESOURCE(3) /* status window properties */ #define STATUS_DATA MAKEINTRESOURCE(1) #define STATUS_FORMAT MAKEINTRESOURCE(2) #define STATUS_DISPFONT MAKEINTRESOURCE(3) /* * StatusWndFn( hWnd, wMsg, wParam, lParam ) : LONG * * hWnd window handle * wMsg message number * wParam additional message information * lParam additional message information * * This window function processes all the messages related to * the status child window of the clipboard viewer. * */ LONG FAR PASCAL StatusWndFn( hWnd, wMsg, wParam, lParam ) HWND hWnd; WORD wMsg; WORD wParam; LONG lParam; { LONG lResult; /* initialization */ lResult = FALSE; /* process each message */ switch( wMsg ) { case WM_CREATE : /* window being created */ { HDC hDC; LOGFONT LogFont; TEXTMETRIC TextMetric; /* retrieve system font metrics */ hDC = GetDC( hWnd ); GetTextMetrics( hDC, &TextMetric ); ReleaseDC( hWnd, hDC ); /* define display font */ LogFont.lfHeight = TextMetric.tmHeight; LogFont.lfWidth = TextMetric.tmAveCharWidth; LogFont.lfEscapement = 0; LogFont.lfOrientation = 0; LogFont.lfWeight = FW_MEDIUM; LogFont.lfItalic = FALSE; LogFont.lfUnderline = FALSE; LogFont.lfStrikeOut = FALSE; LogFont.lfCharSet = ANSI_CHARSET; LogFont.lfOutPrecision = OUT_STROKE_PRECIS; LogFont.lfClipPrecision = CLIP_STROKE_PRECIS; LogFont.lfQuality = PROOF_QUALITY; LogFont.lfPitchAndFamily = DEFAULT_PITCH|FF_MODERN; strcpy( LogFont.lfFaceName, "Helv" ); /* define property lists */ SetProp( hWnd, STATUS_DATA, NULL ); SetProp( hWnd, STATUS_FORMAT, NULL ); SetProp( hWnd, STATUS_DISPFONT, CreateFontIndirect(&LogFont) ); } break; case WM_UPDATE : /* update command from parent */ /* update property lists */ SetProp( hWnd, STATUS_DATA, LOWORD(lParam) ); SetProp( hWnd, STATUS_FORMAT, wParam ); /* force update of window */ InvalidateRect( hWnd, NULL, TRUE ); break; case WM_PAINT : /* paint window */ { PAINTSTRUCT Ps; WORD wCrntFmt; HFONT hOldFont; RECT rcClient; HANDLE hCrntData; TEXTMETRIC TextMetric; char szFmtSize[32]; char szFmtName[80]; char szFmtOwner[80]; /* retrieve property list data */ hCrntData = GetProp( hWnd, STATUS_DATA ); wCrntFmt = GetProp( hWnd, STATUS_FORMAT ); /* define clipboard format name */ GetClipboardFmtName(wCrntFmt,szFmtName,sizeof(szFmtName),TRUE); /* define clipboard format owner */ if ( wCrntFmt && GetClipboardOwner() ) { GetModuleFileName( GetClassWord( GetClipboardOwner(), GCW_HMODULE ), szFmtSize, sizeof(szFmtSize) ); sprintf( szFmtOwner, "%s from %s", szFmtName, strrchr(szFmtSize,'\\')+1 ); strcpy( szFmtName, szFmtOwner ); } /* define clipboard format size */ switch( wCrntFmt ) { case NULL : /* empty */ strcpy( szFmtSize, "0 bytes" ); break; case CF_BITMAP : /* standard GDI bitmap */ case CF_DSPBITMAP : /* display bitmap */ { BITMAP bmStruct; /* retrieve bitmap object */ if (GetObject(hCrntData,sizeof(BITMAP),(LPSTR)&bmStruct)= = sizeof(BITMAP) ) { sprintf( szFmtSize, " - %u x %u%s", bmStruct.bmWidth, bmStruct.bmHeight, ((bmStruct.bmPlanes= =1)&&(bmStruct.bmBitsPixel= =1)) ? " - mono" : "" ); strcat( szFmtName, szFmtSize ); sprintf( szFmtSize, "%ld bytes", sizeof(BITMAP) + ((LONG)bmStruct.bmWidthBytes * bmStruct.bmPlanes * bmStruct.bmHeight) ); } else strcpy( szFmtSize, "(size unknown)" ); } break; case CF_METAFILEPICT : /* standard GDI metafile */ case CF_DSPMETAFILEPICT : /* display metafile */ { LPMETAFILEPICT lpmfStruct; char szMapMode[32]; /* retrieve bitmap object */ lpmfStruct = (LPMETAFILEPICT)GlobalLock( hCrntData ); if ( lpmfStruct ) { switch( lpmfStruct->mm ) { case MM_TEXT : sprintf( szFmtSize, " - %u x %u - text", lpmfStruct->xExt, lpmfStruct->yExt ); break; case MM_LOMETRIC : sprintf( szFmtSize, " - %u x %u - low metric", lpmfStruct->xExt, lpmfStruct->yExt ); break; case MM_HIMETRIC : sprintf( szFmtSize, " - %u x %u - high metric", lpmfStruct->xExt, lpmfStruct->yExt ); break; case MM_LOENGLISH : sprintf( szFmtSize, " - %u x %u - low english", lpmfStruct->xExt, lpmfStruct->yExt ); break; case MM_HIENGLISH : sprintf( szFmtSize, " - %u x %u - high english", lpmfStruct->xExt, lpmfStruct->yExt ); break; case MM_TWIPS : sprintf( szFmtSize, " - %u x %u - twips", lpmfStruct->xExt, lpmfStruct->yExt ); break; case MM_ISOTROPIC : sprintf( szFmtSize, " - isotropic" ); break; case MM_ANISOTROPIC : sprintf( szFmtSize, " - anisotropic" ); break; default : strcpy( szMapMode, "" ); break; } strcat( szFmtName, szFmtSize ); sprintf( szFmtSize, "%ld bytes", GlobalSize(hCrntData)+GlobalSize(lpmfStruct->hMF) ); UnlockData( hCrntData ); } else strcpy( szFmtSize, "(size unknown)" ); } break; case CF_TEXT : /* standard text */ case CF_SYLK : /* standard SYLK text */ case CF_DIF : /* standard DIF text */ case CF_TIFF : /* standard binary TIFF data */ case CF_OEMTEXT : /* standard OEM text */ case CF_OWNERDISPLAY : /* owner display */ case CF_DSPTEXT : /* display text */ default : /* something else */ sprintf( szFmtSize, "%ld bytes", GlobalSize(hCrntData) ); break; } /* make sure entire window is updated */ InvalidateRect( hWnd, NULL, TRUE ); /* start painting */ BeginPaint( hWnd, &Ps ); GetTextMetrics( Ps.hdc, &TextMetric ); hOldFont = SelectObject( Ps.hdc, GetProp(hWnd, STATUS_DISPFONT) ); /* retrieve & adjust client rectangle for drawing */ GetClientRect( hWnd, &rcClient ); rcClient.left + = TextMetric.tmAveCharWidth; rcClient.right -= TextMetric.tmAveCharWidth; /* update clipboard format name & size */ DrawText( Ps.hdc, szFmtName, -1, &rcClient, DT_LEFT|DT_VCENTER|DT_SINGLELINE ); DrawText( Ps.hdc, szFmtSize, -1, &rcClient, DT_RIGHT|DT_VCENTER|DT_SINGLELINE ); /* end painting */ SelectObject( Ps.hdc, hOldFont ); EndPaint( hWnd, &Ps ); } break; case WM_CLOSE : /* window being closed */ /* delete display font */ DeleteObject( GetProp(hWnd,STATUS_DISPFONT) ); /* remove property lists */ RemoveProp( hWnd, STATUS_DATA ); RemoveProp( hWnd, STATUS_FORMAT ); RemoveProp( hWnd, STATUS_DISPFONT ); /* end it all */ DestroyWindow( hWnd ); break; default : /* send to default */ lResult = DefWindowProc( hWnd, wMsg, wParam, lParam ); break; } /* return normal result */ return( lResult ); } /* * ClientWndFn( hWnd, wMsg, wParam, lParam ) : LONG * * hWnd window handle * wMsg message number * wParam additional message information * lParam additional message information * * This window function processes all the messages related to * the client area of the clipboard viewer. * */ LONG FAR PASCAL ClientWndFn( hWnd, wMsg, wParam, lParam ) HWND hWnd; WORD wMsg; WORD wParam; LONG lParam; { FARPROC lpFn; LONG lResult; /* initialization */ lResult = FALSE; /* process each message */ switch( wMsg ) { case WM_CREATE : /* window being created */ /* define property lists */ SetProp( hWnd, CLIENT_FORMAT, NULL ); SetProp( hWnd, CLIENT_MODULE, NULL ); SetProp( hWnd, CLIENT_DISPINFO, NULL ); break; case WM_UPDATE : /* update command from parent */ /* check if clipboard data present */ if (GetProp(hWnd,CLIENT_FORMAT) && GetProp(hWnd,CLIENT_MODULE)) { lpFn=GetProcAddress(GetProp(hWnd,CLIENT_MODULE),LIB_DESTROY ); (*lpFn)( hWnd, GetProp(hWnd,CLIENT_DISPINFO) ); } /* update property lists */ SetProp( hWnd, CLIENT_FORMAT, wParam ); SetProp( hWnd, CLIENT_MODULE, HIWORD(lParam) ); if ( wParam && HIWORD(lParam) ) { lpFn = GetProcAddress( GetProp(hWnd,CLIENT_MODULE), LIB_CREATE); SetProp( hWnd, CLIENT_DISPINFO, (*lpFn)(hWnd,LOWORD(lParam)) ); } else { SetProp( hWnd, CLIENT_DISPINFO, NULL ); SetScrollPos( hWnd, SB_HORZ, 0, TRUE ); SetScrollPos( hWnd, SB_VERT, 0, TRUE ); } /* force update of window */ InvalidateRect( hWnd, NULL, TRUE ); break; case WM_SIZE : /* window being sized */ case WM_HSCROLL : /* horizontal scroll request */ case WM_VSCROLL : /* vertical scroll request */ case WM_PAINT : /* window being painted */ /* determine if a responsible library function available */ if (GetProp(hWnd,CLIENT_FORMAT) && GetProp(hWnd,CLIENT_MODULE)) { LPSTR lpszLibrary; /* determine which library is responsible */ switch( wMsg ) { case WM_SIZE : lpszLibrary = LIB_SIZE; break; case WM_HSCROLL : lpszLibrary = LIB_HSCROLL; break; case WM_VSCROLL : lpszLibrary = LIB_VSCROLL; break; case WM_PAINT : lpszLibrary = LIB_PAINT; break; } /* call display library function */ lpFn = GetProcAddress(GetProp(hWnd,CLIENT_MODULE),lpszLibrary); (*lpFn)( hWnd, GetProp(hWnd,CLIENT_DISPINFO), wParam, lParam ); } else lResult = DefWindowProc( hWnd, wMsg, wParam, lParam ); break; case WM_CLOSE : /* window being closed */ /* pass to responsible destroy library function */ if (GetProp(hWnd,CLIENT_FORMAT) && GetProp(hWnd,CLIENT_MODULE)) { lpFn = GetProcAddress(GetProp(hWnd,CLIENT_MODULE),LIB_DESTROY ); (*lpFn)( hWnd, GetProp(hWnd,CLIENT_DISPINFO) ); } /* remove properties */ RemoveProp( hWnd, CLIENT_FORMAT ); RemoveProp( hWnd, CLIENT_MODULE ); RemoveProp( hWnd, CLIENT_DISPINFO ); /* end it all */ DestroyWindow( hWnd ); break; default : /* send to default */ lResult = DefWindowProc( hWnd, wMsg, wParam, lParam ); break; } /* return normal result */ return( lResult ); } Figure 10 /* * WINDOWS CLIPBOARD VIEWER - UTILITY SOURCE CODE * * LANGUAGE : Microsoft C 5.1 * MODEL : medium * ENVIRONMENT : Microsoft Windows 2.1 SDK * STATUS : operational * * 1.01- Kevin P. Welch - add param to GetClipboardFmtName. * */ #define NOCOMM #include <windows.h> #include "viewer.h" /* * CenterPopup( hWnd, hParentWnd ) : BOOL * * hWnd window handle * hParentWnd parent window handle * * This routine centers the popup window in the screen or display * using the window handles provided. The window is centered over * the parent if the parent window is valid. Special provision * is made for the case when the popup would be centered outside * the screen - in this case it is positioned at the appropriate * border. * */ BOOL FAR PASCAL CenterPopup( HWND hWnd, HWND hParentWnd ) { /* local variables */ int xPopup; /* popup x position */ int yPopup; /* popup y position */ int cxPopup; /* width of popup window */ int cyPopup; /* height of popup window */ int cxScreen; /* width of main display */ int cyScreen; /* height of main display */ int cxParent; /* width of parent window */ int cyParent; /* height of parent window */ RECT rcWindow; /* temporary window rect */ /* retrieve main display dimensions */ cxScreen = GetSystemMetrics( SM_CXSCREEN ); cyScreen = GetSystemMetrics( SM_CYSCREEN ); /* retrieve popup rectangle */ GetWindowRect( hWnd, (LPRECT)&rcWindow ); /* calculate popup extents */ cxPopup = rcWindow.right - rcWindow.left; cyPopup = rcWindow.bottom - rcWindow.top; /* calculate bounding rectangle */ if ( hParentWnd ) { /* retrieve parent rectangle */ GetWindowRect( hParentWnd, (LPRECT)&rcWindow ); /* calculate parent extents */ cxParent = rcWindow.right - rcWindow.left; cyParent = rcWindow.bottom - rcWindow.top; /* center within parent window */ xPopup = rcWindow.left + ((cxParent - cxPopup)/2); yPopup = rcWindow.top + ((cyParent - cyPopup)/2); /* adjust popup x-location for screen size */ if ( xPopup+cxPopup > cxScreen ) xPopup = cxScreen - cxPopup; /* adjust popup y-location for screen size */ if ( yPopup+cyPopup > cyScreen ) yPopup = cyScreen - cyPopup; } else { /* center within entire screen */ xPopup = (cxScreen - cxPopup) / 2; yPopup = (cyScreen - cyPopup) / 2; } /* move window to new location & display */ MoveWindow( hWnd, ( xPopup > 0 ) ? xPopup : 0, ( yPopup > 0 ) ? yPopup : 0, cxPopup, cyPopup, TRUE ); /* normal return */ return( TRUE ); } /* * GetClipboardFmtName( wFmt, lpszFmt, wMax, bInquire ) : WORD; * * wFmt clipboard format number * lpszFmt name of clipboard format * wMax maximum name size * bInquire inquire full name if owner-display * * This utility function is identical to GetClipboardFormatName, but * is capable of defining the name of ANY clipboard format, including * predefined and owner-display ones. Value returned is the number * of bytes copied to the clipboard format name. If this value is * zero then the clipboard format number specified is undefined. * * Note that if the bInquire flag is TRUE, this function will attempt * to ask the clipboard owner for the full name of the owner display * data. This may result in misleading information if this function * is called with an owner display format number when the real owner * is not currently present! * */ WORD FAR PASCAL GetClipboardFmtName( WORD wFmt, LPSTR lpszFmt, WORD wMax, BOOL bInquire ) { HANDLE hTemp; LPSTR lpszTemp; /* initialization */ lpszFmt[0] = 0; /* define format name */ switch( wFmt ) { case NULL : /* empty */ lstrcpy( lpszFmt, "(Clipboard Empty)" ); break; case CF_TEXT : /* standard text */ lstrcpy( lpszFmt, "Text" ); break; case CF_BITMAP : /* standard GDI bitmap */ lstrcpy( lpszFmt, "Bitmap" ); break; case CF_METAFILEPICT : /* standard GDI metafile */ lstrcpy( lpszFmt, "Picture" ); break; case CF_SYLK : /* standard SYLK text */ lstrcpy( lpszFmt, "SYLK" ); break; case CF_DIF : /* standard DIF text */ lstrcpy( lpszFmt, "DIF" ); break; case CF_TIFF : /* standard binary TIFF data */ lstrcpy( lpszFmt, "TIFF" ); break; case CF_OEMTEXT : /* standard OEM text */ lstrcpy( lpszFmt, "OEM Text" ); break; case CF_OWNERDISPLAY : /* owner display */ lstrcpy( lpszFmt, "Owner Display" ); if ( bInquire && GetClipboardOwner() ) { hTemp = GlobalAlloc( GHND, (DWORD)64 ); if ( hTemp ) { lpszTemp = GlobalLock( hTemp ); if ( lpszTemp ) { SendMessage( GetClipboardOwner(), WM_ASKCBFORMATNAME, 63, (LONG)lpszTemp ); lstrcpy( lpszFmt, lpszTemp ); GlobalUnlock( hTemp ); } GlobalFree( hTemp ); } } break; case CF_DSPTEXT : /* display text */ lstrcpy( lpszFmt, "Display Text" ); break; case CF_DSPBITMAP : /* display bitmap */ lstrcpy( lpszFmt, "Display Bitmap" ); break; case CF_DSPMETAFILEPICT : /* display picture */ lstrcpy( lpszFmt, "Display Picture" ); break; default : /* something else */ if ( GetClipboardFormatName(wFmt,lpszFmt,wMax) = = 0 ) lstrcpy( lpszFmt, "(Undefined)" ); break; } /* return size of string */ return( (WORD)lstrlen(lpszFmt) ); } /* * GetClipboardFmtNumber( lpszFmt ) : WORD; * * lpszFmt name of clipboard format * * This function retrieves (and if necessary, defines) the internal * clipboard format number for the specified string. Before checking * registered clipboard formats, this function checks to see if one * of the predefined formats is referenced. * */ WORD FAR PASCAL GetClipboardFmtNumber( LPSTR lpszFmt ) { WORD wFmt; /* check predefined formats */ if ( lstrcmp(lpszFmt,"Text") = = 0 ) wFmt = CF_TEXT; else if ( lstrcmp(lpszFmt,"Bitmap") = = 0 ) wFmt = CF_BITMAP; else if ( lstrcmp(lpszFmt,"Picture") = = 0 ) wFmt = CF_METAFILEPICT; else if ( lstrcmp(lpszFmt,"SYLK") = = 0 ) wFmt = CF_SYLK; else if ( lstrcmp(lpszFmt,"DIF") = = 0 ) wFmt = CF_DIF; else if ( lstrcmp(lpszFmt,"TIFF") = = 0 ) wFmt = CF_TIFF; else if ( lstrcmp(lpszFmt,"OEM Text") = = 0 ) wFmt = CF_OEMTEXT; else if ( lstrcmp(lpszFmt,"Owner Display") = = 0 ) wFmt = CF_OWNERDISPLAY; else if ( lstrcmp(lpszFmt,"Display Text") = = 0 ) wFmt = CF_DSPTEXT; else if ( lstrcmp(lpszFmt,"Display Bitmap") = = 0 ) wFmt = CF_DSPBITMAP; else if ( lstrcmp(lpszFmt,"Display Picture") = = 0 ) wFmt = CF_DSPMETAFILEPICT; else wFmt = RegisterClipboardFormat( lpszFmt ); /* return format */ return( wFmt ); } /* * GetClipboardModule( wCrntFmt, hLibrary ) : HANDLE; * * wCrntFmt current clipboard foramt * hLibrary handle to clipboard support library * * This function searches the clipboard module library for one which * can support the format in question. If none is present a value of * NULL is returned. This should be interpreted that the current * format is not supported by the system. * */ HANDLE FAR PASCAL GetClipboardModule( WORD wCrntFmt, HANDLE hLibrary ) { WORD wEntry; HANDLE hModule; LPLIBRARY lpLibrary; /* initialization */ hModule = NULL; lpLibrary = (LPLIBRARY)GlobalLock( hLibrary ); if ( lpLibrary ) { /* search library for module */ for ( wEntry=0;(wEntry<lpLibrary->wModules)&&(hModule= =NULL); wEntry++ ) if ( lpLibrary->Module[wEntry].wFormat = = wCrntFmt ) hModule = lpLibrary->Module[wEntry].hModule; /* unlock library */ GlobalUnlock( hLibrary ); } /* return module handle */ return( hModule ); } Figure 11 BITMAP DLLFLAGS=-c -u -Asnw -FPa -Gsw -Os -W2 -Zep bitmap1.obj: bitmap1.asm masm bitmap1; bitmap2.obj: bitmap2.c cl $(DLLFLAGS) bitmap2.c bitmap.dll: bitmap1.obj bitmap2.obj bitmap.def link4 bitmap1+bitmap2 /AL:16,bitmap.dll,,swinlibc+slibw,bitmap.def BITMAP.DEF LIBRARY BITMAP STUB 'WINSTUB.EXE' DESCRIPTION 'Bitmap Format Display Library ' CODE MOVEABLE DISCARDABLE SHARED DATA MOVEABLE SINGLE EXPORTS BitmapInit @1 BitmapCreate @2 BitmapSize @3 BitmapHScroll @4 BitmapVScroll @5 BitmapPaint @6 BitmapDestroy @7 BITMAP1.ASM ; ; WINDOWS CLIPBOARD VIEWER - BITMAP FORMAT SOURCE CODE ; ; LANGUAGE : Microsoft Macro Assembler 5.1 ; MODEL : small ; ENVIRONMENT : Microsoft Windows 2.1 SDK ; STATUS : operational ; ; Extrn BitmapInit:Near _TEXT SEGMENT BYTE PUBLIC 'CODE' ASSUME CS:_TEXT PUBLIC LibInit LibInit PROC FAR Push DI ; hInstance Push DS ; Data Segment Push CX ; Heap Size Push ES Push SI ; Command Line Call BitmapInit Ret LibInit ENDP _TEXT ENDS End LibInit BITMAP2.C /* * WINDOWS CLIPBOARD VIEWER - BITMAP FORMAT SOURCE CODE * * LANGUAGE : Microsoft C5.1 * MODEL : small * ENVIRONMENT : Microsoft Windows 2.1 SDK * STATUS : operational * */ #define NOCOMM #include <windows.h> #define HORZ_BORDER 2 #define VERT_BORDER 2 #define HORZ_STEPSIZE 8 #define VERT_STEPSIZE 8 typedef struct { HANDLE hBmp; HANDLE hData; POINT ptOrg; RECT rcWnd; RECT rcBmp; HDC hScrDC; HDC hMemDC; } DISPINFO; typedef DISPINFO FAR * LPDISPINFO; BOOL PASCAL BitmapInit( HANDLE, WORD, WORD, LPSTR ); HANDLE FAR PASCAL BitmapCreate( HWND, HANDLE ); HANDLE FAR PASCAL BitmapSize( HWND, HANDLE, WORD, LONG ); HANDLE FAR PASCAL BitmapHScroll( HWND, HANDLE, WORD, LONG ); HANDLE FAR PASCAL BitmapVScroll( HWND, HANDLE, WORD, LONG ); HANDLE FAR PASCAL BitmapPaint( HWND, HANDLE, WORD, LONG ); HANDLE FAR PASCAL BitmapDestroy( HWND, HANDLE ); /* BOOL * BitmapInit( hInstance, wDataSegment, wHeapSize, lpszCmdLine ) * * hInstance library instance handle * wDataSegment library data segment * wHeapSize default heap size * lpszCmdLine command line arguments * * This function performs all the initialization necessary to use * the bitmap viewer display dynamic library. It is assumed that no * local heap is used, hence no call to LocalInit. A non-zero value * is returned if the initialization was sucessful. * */ BOOL PASCAL BitmapInit(hInstance,wDataSegment,wHeapSize,lpszCmdLine) HANDLE hInstance; WORD wDataSegment; WORD wHeapSize; LPSTR lpszCmdLine; { /* warning level 3 compatibility */ hInstance; wDataSegment; wHeapSize; lpszCmdLine; /* sucessful return */ return( TRUE ); } /* * BitmapCreate( hWnd, hClipData ) : HANDLE; * * hWnd handle to display window * hClipData handle to current clipboard data * * This function performs all the initialization necessary in order * to view a bitmap clipboard format. A handle to a display infor- * mation data block (the internal format of which is only known * inside this module) which the owner is responsible for saving. * */ HANDLE FAR PASCAL BitmapCreate( HWND hWnd, HANDLE hClipData ) { BITMAP Bitmap; HANDLE hDispInfo; WORD wWndWidth; WORD wWndHeight; WORD wScrollWidth; WORD wScrollHeight; LPDISPINFO lpDispInfo; /* reset scroll bars */ SetScrollPos( hWnd, SB_HORZ, 0, TRUE ); SetScrollPos( hWnd, SB_VERT, 0, TRUE ); /* attempt to allocate data */ hDispInfo = GlobalAlloc( GHND, (DWORD)sizeof(DISPINFO) ); if ( hDispInfo ) { /* lock it down */ lpDispInfo = (LPDISPINFO)GlobalLock( hDispInfo ); if ( lpDispInfo ) { /* define bitmap data */ lpDispInfo->hData = hClipData; /* define bitmap dimensions */ GetObject( hClipData, sizeof(BITMAP), (LPSTR)&Bitmap ); lpDispInfo->rcBmp.top = 0; lpDispInfo->rcBmp.left = 0; lpDispInfo->rcBmp.right = Bitmap.bmWidth; lpDispInfo->rcBmp.bottom = Bitmap.bmHeight; /* define window origin in bitmap coordinates */ lpDispInfo->ptOrg.x = -HORZ_BORDER; lpDispInfo->ptOrg.y = -VERT_BORDER; /* define window dimensions */ GetClientRect( hWnd, &lpDispInfo->rcWnd ); lpDispInfo->rcWnd.top = VERT_BORDER; lpDispInfo->rcWnd.left = HORZ_BORDER; lpDispInfo->rcWnd.right -= HORZ_BORDER; lpDispInfo->rcWnd.bottom -= VERT_BORDER; OffsetRect( &lpDispInfo->rcWnd, -HORZ_BORDER, -VERT_BORDER ); IntersectRect( &lpDispInfo->rcWnd, &lpDispInfo->rcWnd, &lpDispInfo->rcBmp ); OffsetRect( &lpDispInfo->rcWnd, HORZ_BORDER, VERT_BORDER ); wWndWidth = lpDispInfo->rcWnd.right - lpDispInfo->rcWnd.left; wWndHeight = lpDispInfo->rcWnd.bottom - lpDispInfo->rcWnd.top; /* define scrollbar ranges */ wScrollWidth = ( Bitmap.bmWidth > wWndWidth ) ? Bitmap.bmWidth - wWndWidth : 1; wScrollHeight = ( Bitmap.bmHeight > wWndHeight ) ? Bitmap.bmHeight - wWndHeight : 1; SetScrollRange( hWnd, SB_HORZ, 0, wScrollWidth, FALSE ); SetScrollRange( hWnd, SB_VERT, 0, wScrollHeight, FALSE ); /* unlock data */ GlobalUnlock( hDispInfo ); } else { GlobalFree( hDispInfo ); hDispInfo = NULL; } } /* return display info handle */ return( hDispInfo ); } /* * BitmapSize( hWnd, hDispInfo, wParam, lParam ) : HANDLE; * * hWnd handle to display window * hDispInfo handle to display information block * wParam word parameter of WM_SIZE message * lParam long parameter of WM_SIZE message * * This function resets the display information data block whenever * the size of the display region is changed. If successful, a * handle to the new display information data block is returned. * Failure to call this function whenever the size of the display * region is changed will cause unusual display results. * */ HANDLE FAR PASCAL BitmapSize( HWND hWnd, HANDLE hDispInfo, WORD wParam, LONG lParam ) { WORD wWndWidth; WORD wBmpWidth; WORD wWndHeight; WORD wBmpHeight; WORD wScrollWidth; WORD wScrollHeight; LPDISPINFO lpDispInfo; /* warning level 3 compatibility */ wParam; lParam; /* reset scroll bars */ SetScrollPos( hWnd, SB_HORZ, 0, TRUE ); SetScrollPos( hWnd, SB_VERT, 0, TRUE ); /* lock it down display information block */ lpDispInfo = (LPDISPINFO)GlobalLock( hDispInfo ); if ( lpDispInfo ) { /* define window origin in bitmap coordinates */ lpDispInfo->ptOrg.x = -HORZ_BORDER; lpDispInfo->ptOrg.y = -VERT_BORDER; /* define bitmap dimensions */ wBmpWidth = lpDispInfo->rcBmp.right - lpDispInfo->rcBmp.left; wBmpHeight = lpDispInfo->rcBmp.bottom - lpDispInfo->rcBmp.top; /* define window dimensions */ GetClientRect( hWnd, &lpDispInfo->rcWnd ); lpDispInfo->rcWnd.top = VERT_BORDER; lpDispInfo->rcWnd.left = HORZ_BORDER; lpDispInfo->rcWnd.right -= HORZ_BORDER; lpDispInfo->rcWnd.bottom -= VERT_BORDER; OffsetRect( &lpDispInfo->rcWnd, -HORZ_BORDER, -VERT_BORDER ); IntersectRect( &lpDispInfo->rcWnd, &lpDispInfo->rcWnd, &lpDispInfo->rcBmp ); OffsetRect( &lpDispInfo->rcWnd, HORZ_BORDER, VERT_BORDER ); wWndWidth = lpDispInfo->rcWnd.right - lpDispInfo->rcWnd.left; wWndHeight = lpDispInfo->rcWnd.bottom - lpDispInfo->rcWnd.top; /* define scrollbar ranges */ wScrollWidth=(wBmpWidth>wWndWidth) ? wBmpWidth-wWndWidth : 1; wScrollHeight=(wBmpHeight>wWndHeight) ? wBmpHeight-wWndHeight : 1; SetScrollRange( hWnd, SB_HORZ, 0, wScrollWidth, FALSE ); SetScrollRange( hWnd, SB_VERT, 0, wScrollHeight, FALSE ); /* unlock data */ GlobalUnlock( hDispInfo ); } else hDispInfo = NULL; /* return final result */ return( hDispInfo ); } /* * BitmapHScroll( hWnd, hDispInfo, wParam, lParam ) : HANDLE; * * hWnd handle to display window * hDispInfo handle to display information block * wParam current scroll code * lParam current scroll parameter * * This function is responsible for handling all the horizontal * scroll messages received when viewing a bitmap clipboard format. * If necessary, changes to the display information block can be * made. As currently implemented, no action is taken. The value * returned is the handle to the updated display information block. */ HANDLE FAR PASCAL BitmapHScroll( HWND hWnd, HANDLE hDispInfo, WORD wParam, LONG lParam ) { WORD wWndWidth; WORD wBmpWidth; WORD wOldScrollPos; WORD wNewScrollPos; WORD wOldScrollRange; LPDISPINFO lpDispInfo; /* access display information block */ lpDispInfo = (LPDISPINFO)GlobalLock( hDispInfo ); if ( lpDispInfo ) { /* initialization */ wWndWidth = lpDispInfo->rcWnd.right - lpDispInfo->rcWnd.left; wBmpWidth = lpDispInfo->rcBmp.right - lpDispInfo->rcBmp.left; wOldScrollPos = lpDispInfo->ptOrg.x + HORZ_BORDER; wOldScrollRange=(wBmpWidth > wWndWidth) ? wBmpWidth-wWndWidth : 0; /* define display contexts (if necessary) */ if ( lpDispInfo->hScrDC == NULL ) { lpDispInfo->hScrDC = GetDC( hWnd ); lpDispInfo->hMemDC = CreateCompatibleDC( lpDispInfo->hScrDC ); lpDispInfo->hBmp = SelectObject( lpDispInfo->hMemDC, lpDispInfo->hData ); } /* process scroll message */ switch( wParam ) { case SB_LINEUP : /* move left one line */ wNewScrollPos = (wOldScrollPos > HORZ_STEPSIZE) ? wOldScrollPos-HORZ_STEPSIZE : 0; break; case SB_LINEDOWN : /* move right one line */ wNewScrollPos = (wOldScrollPos+HORZ_STEPSIZE<=wOldScrollRange) ? wOldScrollPos+HORZ_STEPSIZE : wOldScrollRange; break; case SB_PAGEUP : /* move left one page */ wNewScrollPos = (wOldScrollPos > wWndWidth) ? wOldScrollPos-wWndWidth : 0; break; case SB_PAGEDOWN : /* move right one page */ wNewScrollPos = (wOldScrollPos+wWndWidth <= wOldScrollRange) ? wOldScrollPos+wWndWidth : wOldScrollRange; break; case SB_THUMBPOSITION : /* move to an absolute position */ case SB_THUMBTRACK : /* track the current thumb position */ wNewScrollPos = (wOldScrollRange > 1) ? LOWORD(lParam) : 0; break; case SB_TOP : /* move to the first line */ wNewScrollPos = 0; break; case SB_BOTTOM : /* move to the last line */ wNewScrollPos = wOldScrollRange; break; case SB_ENDSCROLL : /* end scrolling */ wNewScrollPos = wOldScrollPos; break; } /* perform scroll and update (if necessary) */ if ( wNewScrollPos != wOldScrollPos ) { /* update window */ BitBlt( lpDispInfo->hScrDC, HORZ_BORDER, VERT_BORDER, wWndWidth, lpDispInfo->rcWnd.bottom - lpDispInfo->rcWnd.top, lpDispInfo->hMemDC, wNewScrollPos, lpDispInfo->ptOrg.y + VERT_BORDER, SRCCOPY ); /* update origin & horizontal scrollbar */ lpDispInfo->ptOrg.x = wNewScrollPos - HORZ_BORDER; SetScrollPos( hWnd, SB_HORZ, wNewScrollPos, TRUE ); } /* release display context (if necessary) */ if ( (wParam = = SB_ENDSCROLL)||(wParam = = SB_THUMBPOSITION) ) { SelectObject( lpDispInfo->hMemDC, lpDispInfo->hBmp ); DeleteDC( lpDispInfo->hMemDC ); ReleaseDC( hWnd, lpDispInfo->hScrDC ); lpDispInfo->hScrDC = NULL; } /* unlock data */ GlobalUnlock( hDispInfo ); } else hDispInfo = NULL; /* return result */ return( hDispInfo ); } /* * BitmapVScroll( hWnd, hDispInfo, wParam, lParam ) : HANDLE; * * hWnd handle to display window * hDispInfo handle to display information block * wParam current scroll code * lParam current scroll parameter * * This function is responsible for handling all the vertical scroll * messages received when viewing a bitmap clipboard format. If * necessary, changes to the display information block can be made. * As currently implemented, no action is taken. The value returned * is the handle to the updated display information block. */ HANDLE FAR PASCAL BitmapVScroll( HWND hWnd, HANDLE hDispInfo, WORD wParam, LONG lParam ) { WORD wWndHeight; WORD wBmpHeight; WORD wOldScrollPos; WORD wNewScrollPos; WORD wOldScrollRange; LPDISPINFO lpDispInfo; /* access display information block */ lpDispInfo = (LPDISPINFO)GlobalLock( hDispInfo ); if ( lpDispInfo ) { /* initialization */ wWndHeight = lpDispInfo->rcWnd.bottom - lpDispInfo->rcWnd.top; wBmpHeight = lpDispInfo->rcBmp.bottom - lpDispInfo->rcBmp.top; wOldScrollPos = lpDispInfo->ptOrg.y + VERT_BORDER; wOldScrollRange = (wBmpHeight > wWndHeight) ? wBmpHeight-wWndHeight : 0; /* define display contexts (if necessary) */ if ( lpDispInfo->hScrDC = = NULL ) { lpDispInfo->hScrDC = GetDC( hWnd ); lpDispInfo->hMemDC = CreateCompatibleDC( lpDispInfo->hScrDC ); lpDispInfo->hBmp = SelectObject( lpDispInfo->hMemDC, lpDispInfo->hData ); } /* process scroll message */ switch( wParam ) { case SB_LINEUP : /* move up one line */ wNewScrollPos = (wOldScrollPos > VERT_STEPSIZE) ? wOldScrollPos-VERT_STEPSIZE : 0; break; case SB_LINEDOWN : /* move down one line */ wNewScrollPos=(wOldScrollPos+VERT_STEPSIZE<=wOldScrollRange) ? wOldScrollPos+VERT_STEPSIZE : wOldScrollRange; break; case SB_PAGEUP : /* move up one page */ wNewScrollPos=(wOldScrollPos > wWndHeight) ? wOldScrollPos-wWndHeight : 0; break; case SB_PAGEDOWN : /* move down one page */ wNewScrollPos=(wOldScrollPos+wWndHeight<=wOldScrollRange) ? wOldScrollPos+wWndHeight : wOldScrollRange; break; case SB_THUMBPOSITION : /* move to an absolute position */ case SB_THUMBTRACK : /* track the current thumb position */ wNewScrollPos = (wOldScrollRange > 1) ? LOWORD(lParam) : 0; break; case SB_TOP : /* move to the first line */ wNewScrollPos = 0; break; case SB_BOTTOM : /* move to the last line */ wNewScrollPos = wOldScrollRange; break; case SB_ENDSCROLL : /* end scrolling */ wNewScrollPos = wOldScrollPos; break; } /* perform scroll and update (if necessary) */ if ( wNewScrollPos != wOldScrollPos ) { /* update window */ BitBlt( lpDispInfo->hScrDC, HORZ_BORDER, VERT_BORDER, lpDispInfo->rcWnd.right - lpDispInfo->rcWnd.left, wWndHeight, lpDispInfo->hMemDC, lpDispInfo->ptOrg.x + HORZ_BORDER, wNewScrollPos, SRCCOPY ); /* update origin & horizontal scrollbar */ lpDispInfo->ptOrg.y = wNewScrollPos - VERT_BORDER; SetScrollPos( hWnd, SB_VERT, wNewScrollPos, TRUE ); } /* release display context (if necessary) */ if ( (wParam = = SB_ENDSCROLL)||(wParam = = SB_THUMBPOSITION) ) { SelectObject( lpDispInfo->hMemDC, lpDispInfo->hBmp ); DeleteDC( lpDispInfo->hMemDC ); ReleaseDC( hWnd, lpDispInfo->hScrDC ); lpDispInfo->hScrDC = NULL; } /* unlock data */ GlobalUnlock( hDispInfo ); } else hDispInfo = NULL; /* return result */ return( hDispInfo ); } /* * BitmapPaint( hWnd, hDispInfo, wParam, lParam ) : HANDLE; * hWnd handle to display window * hDispInfo handle to display information block * wParam WM_PAINT word parameter * lParam WM_PAINT long parameter * * This function is responsible for handling all the paint related * aspects for the bitmap clipboard format. This function calculates * the required update portion of the window and BitBlts the bitmap * into the region. The handle returned by this function is to the * update display information block. */ HANDLE FAR PASCAL BitmapPaint( HWND hWnd, HANDLE hDispInfo, WORD wParam, LONG lParam ) { PAINTSTRUCT Ps; HDC hMemDC; HANDLE hOldData; LPDISPINFO lpDispInfo; /* warning level 3 compatibility */ wParam; lParam; /* access display information block */ lpDispInfo = (LPDISPINFO)GlobalLock( hDispInfo ); if ( lpDispInfo ) { /* start paint operation */ BeginPaint( hWnd, &Ps ); /* define update region */ IntersectRect(&Ps.rcPaint,&Ps.rcPaint,&lpDispInfo->rcWnd ); OffsetRect(&Ps.rcPaint,lpDispInfo->ptOrg.x,lpDispInfo->ptOrg.y ); IntersectRect(&Ps.rcPaint,&Ps.rcPaint,&lpDispInfo->rcBmp ); OffsetRect(&Ps.rcPaint,-lpDispInfo->ptOrg.x,-lpDispInfo->ptOrg.y); /* perform BitBlt operation */ hMemDC = CreateCompatibleDC( Ps.hdc ); if ( hMemDC ) { hOldData = SelectObject( hMemDC, lpDispInfo->hData ); BitBlt( Ps.hdc, Ps.rcPaint.left, Ps.rcPaint.top, Ps.rcPaint.right - Ps.rcPaint.left, Ps.rcPaint.bottom - Ps.rcPaint.top, hMemDC, Ps.rcPaint.left + lpDispInfo->ptOrg.x, Ps.rcPaint.top + lpDispInfo->ptOrg.y, SRCCOPY ); SelectObject( hMemDC, hOldData ); DeleteDC( hMemDC ); } /* unlock data & end paint operation */ GlobalUnlock( hDispInfo ); EndPaint( hWnd, &Ps ); } else hDispInfo = NULL; /* return final result */ return( hDispInfo ); } /* * BitmapDestroy( hWnd, hDispInfo ) : HANDLE; * * hWnd handle to display window * hDispInfo handle to display information block * * This function is to be called whenever the display region is being * destroyed. It is responsible for restoring the system to it's * original state and for releasing any memory or resources defined. * The value returned is the handle to the OLD display information * block. This handle should NEVER be used after this function is * called. If an error occurs a value of NULL is returned. */ HANDLE FAR PASCAL BitmapDestroy( HWND hWnd, HANDLE hDispInfo ) { /* warning level 3 compatibility */ hWnd; /* free allocated memory block & return old handle */ GlobalFree( hDispInfo ); return( hDispInfo ); } Figure 12 LibInit @1 LibCreate @2 LibSize @3 LibHScroll @4 LibVScroll @5 LibPaint @6 LibDestroy @7 Sidebar BOOL FAR PASCAL LibInit( hInstance, wDataSeg, wHeapSize, lpszCmdLine ) The LibInit function is responsible for all the initialization necessary to use the dynamic library. This function is normally called by your assembly language entry point to the library. If necessary, this function should initialize the local heap by calling LocalInit. In some cases it may also be necessary to register a clipboard format in this function. Parameter Type Description hInstance HANDLE library instance handle wDataSeg WORD library data segment wHeapSize WORD default library heap size lpszCmdLine LPSTR initial command line arguments The return value determines library initialization status. A value of TRUE indicates successful initialization. By returning FALSE you indicate to your assembly language entry point that the initialization has failed. Normally this should cause the library load operation to fail. HANDLE FAR PASCAL LibCreate( hWnd, hClipData ) The LibCreate function is called whenever a WM_UPDATE message is sent to the client window. It performs all the initialization necessary to display the data provided. Since each display library is manually associated with a clipboard format by the user, this function has to assume that the data is in a usable format. As part of its initialization, this function should allocate a block of global memory and use it to store whatever information it deems necessary to display the provided data. Parameter Type Description hWnd HWND handle to display window hClipData HANDLE handle to new clipboard data The return value identifies the block of global memory allocated by the LibCreate function. This handle is saved by the client window and passed back whenever one of the other functions is called. HANDLE FAR PASCAL LibSize( hWnd, hDispInfo, wParam, lParam ) The LibSize function is called whenever a WM_SIZE message is received by the display window. The revised width and height are extracted from message parameters and passed on to this function. It should respond by updating its internal display data structures to account for this change. Parameter Type Description hWnd HWND handle to display window hDispInfo HANDLE handle to display information data block wParam WORD word parameter of WM_SIZE message lParam LONG long parameter of WM_SIZE message The return value identifies the updated block of global memory allocated by the LibCreate function. The return of an invalid or NULL handle indicates that the function call has failed. HANDLE FAR PASCAL LibHScroll( hWnd, hDispInfo, wParam, lParam ) The LibHScroll function is called whenever a WM_HSCROLL message is received by the display window. The actual scroll parameters received by the display window are passed through to this function unmodified. LibHScroll should respond by interpreting the parameters provided and by performing the horizontal scroll operation. Parameter Type Description hWnd HWND handle to display window hDispInfo HANDLE handle to display information data block wParam WORD word parameter of WM_HSCROLL message lParam LONG long parameter of WM_HSCROLL message The return value identifies the updated block of global memory allocated by the LibCreate function. The return of an invalid or NULL handle indicates that the function call has failed. HANDLE FAR PASCAL LibVScroll( hWnd, hDispInfo, wParam, lParam ) The LibVScroll function is called whenever a WM_VSCROLL message is received by the display window. Like the LibHScroll function, the actual scroll parameters received by the display window are passed through to this function unmodified. LibVScroll should respond by interpreting the parameters provided and perform the vertical scroll operation. Parameter Type Description hWnd HWND handle to display window hDispInfo HANDLE handle to display information data block wParam WORD word parameter of WM_VSCROLL message lParam LONG long parameter of WM_VSCROLL message The return value identifies the updated block of global memory allocated by the LibCreate function. The return of an invalid or NULL handle indicates that the function call has failed. HANDLE FAR PASCAL LibPaint( hWnd, hDispInfo, wParam, lParam ) The LibPaint function is called whenever a WM_PAINT message is received by the display window. This function should respond by performing a BeginPaint operation, updating the window contents, and calling EndPaint. Failure to do so will leave a portion of the display unvalidated, resulting in an endless sequence of WM_PAINT messages. Parameter Type Description hWnd HWND handle to display window hDispInfo HANDLE handle to display information data block wParam WORD word parameter of WM_PAINT message lParam LONG long parameter of WM_PAINT message The return value identifies the updated block of global memory allocated by the LibCreate function. The return of an invalid or NULL handle indicates that the function call has failed. HANDLE FAR PASCAL LibDestroy( hWnd, hDispInfo ) The LibDestroy is called whenever a WM_UPDATE message is received by the display window or the display window is destroyed. This function restores the display window to its original state and releases any allocated resources. Parameter Type Description hWnd HWND handle to display window hDispInfo HANDLE handle to display information data block The return value identifies the updated block of global memory allocated by the LibCreate function. The return of an invalid or NULL handle indicates that the function failed. Note that the handle returned should only be used to determine the success or failure of the function call and should never be used to access the memory block. Microsoft C Version 6.0 Provides an Integrated Development Environment Noel J. Bergman The Microsoft C Version 6.0 Professional Development System contains the new C compiler and improved development tools. While maintaining the command line interface, it adds a complete integrated environment, the new Programmer's WorkBench (hereinafter "PWB"). The PWB (see Figure 1) is similar to that of the Microsoft QuickC compiler but is more flexible and advanced, offering a built-in project make facility and a new source-level code browser. The latest version of Microsoft QuickC1 is also included in the Professional Development System for doing rapid compilations. Another type of pointer, a based pointer, is introduced. Other enhancements include new run-time library routines; OS/2 systems support; ILINK, an incremental linker; broader on-line support; and a new version of the Microsoft CodeView debugger. Setting up the development system is straightforward. A new version of the standard Microsoft languages SETUP program looks very much like the tool used to install C Version 5.1 and asks many of the same questions. There are a number of command line switches for SETUP. The /L switch directs SETUP to build libraries only. This is useful for installing additional memory models or floating point support libraries after your initial installation. Installing all memory and floating point models for DOS2, the Microsoft Windows environment, and OS/23--including special support for building dynamic-link libraries (DLLs) and multithreaded programs--takes up almost 10 megabytes for the libraries alone. Therefore, the ability to go back and install a new library set when you need it will undoubtedly prove very useful to people with limited disk storage. The /COPY switch, another useful switch, decompresses files--the C 6.0 package is so large that Microsoft compressed all the files. This switch not only decompresses the requested file, it knows exactly which disk needs to be inserted to find the file. This means no more searching through all of the subdirectories on a half dozen floppy disks. The /HELP switch lists and explains all of the available options. After the files have been copied and the libraries installed, the remaining steps for installation are similarly straightforward. New versions of HIMEM.SYS, SMARTDRV.SYS, and RAMDRIVE.SYS are included in the package. These drivers are designed to work together, using HIMEM.SYS to provide all of the memory management facilities for the others. Programs that use extended memory such as the CodeView debugger also work with these drivers. However, this HIMEM.SYS is incompatible with Version 2.11 of the Windows/386 environment. New Environment Variables In order for PWB to work properly, two environment variables must be set. INIT, the first variable, should point to the directory containing TOOLS.INI, which is where PWB will place its status file, CURRENT.STS. If you do not set INIT, PWB will litter your directories with CURRENT.STS files using the current directory rather than the INIT directory. The other environment variable, HELPFILES, tells PWB and the new QuickHelp where to look for the on-line reference files. TOOLS.INI is the file used by all of the tools (NMAKE, PWB, PWB extensions, CodeView4, and so on) to store configuration information. It is structured similarly to a Windows5 WIN.INI file, with each tool setting up one or more labeled sections to contain settings. The settings are then used as defaults for later executions of each program. You should merge the provided TOOLS.PRE file into any existing TOOLS.INI file you have or rename TOOLS.PRE to TOOLS.INI if there isn't an existing TOOLS.INI file. When you change the settings of an editor (for example, going into 50 line mode in PWB), saved changes will be recorded in TOOLS.INI. PWB settings are temporary unless you save them. Only the changes are recorded in TOOLS.INI, not the entire list of possible settings. CURRENT.STS is the current status file for PWB. The settings for the project you were working on last are recorded in CURRENT.STS and remain in effect until you issue a Set Program List. At that time any new settings specified in the project's make file or STS file will take effect. For this reason, you must make certain that INIT= is set up properly. If it is not, PWB won't be able to retrieve the old settings, unless there happens to be a CURRENT.STS in the current directory. I have a useful little trick that I use to keep both Version 5.10 and Version 6.0 of C on my system at the same time. First, I moved all the programs, libraries, and include files to subdirectories called \C510 and \C600. I used SUBST to create a pseudodisk for the C compiler package. SUBST M: C:\C510 "creates" the \BIN, \PBIN, \RBIN, \LIB, and \INCLUDE directories on the M drive. SUBST M: C:\C600 does the same thing for the C 6.00 package. The PATH, INCLUDE and LIB environment variables all use references to M:\<dir>. This allows me to switch between them very easily. Unfortunately, this trick doesn't work with OS/2, because there is no OS/2 protected-mode version of the SUBST command. The Programmer's WorkBench If you can imagine the Microsoft Editor enhanced both internally and with extensions to resemble the Microsoft QuickC Version 2.0 environment, you have a rough idea of what PWB is like to work with. In addition to the editor, PWB provides access to the compiler (see Figure 2), linker, make facility, debugger, on-line reference, and source browser. In PWB, the Microsoft Editor, M, has been enhanced through extensions to provide facilities for source code browsing, creating and modifying make files, building programs, and accessing on-line reference material from within the editor. One useful feature is Bookmarks (both temporary and stored in files). Bookmarks allows you to move quickly to specific named points in source files. The Search and Replace facility supports regular expressions, in both UNIX and M syntax. This facility extends to text already loaded into the editor and to files on disk as well. Another useful feature is a search for files on disk. Most of you undoubtedly have standalone programs for this purpose, but this feature lets you find files without leaving the environment. Finally, you can extend PWB with both extensions written in C and macros. Most C extensions written for the Microsoft Editor will move easily into PWB. To prove the power of C extensions, the Browser, MS C Advisor, and other PWB tools are all implemented as extensions. I have also seen a large set of public domain extensions to make the Microsoft Editor look a lot like the popular editor BRIEF (from Solution Systems). PWB also lets you customize the Microsoft Editor. Most of you undoubtedly already use an editor you are happy with--if you are going to switch over to the Microsoft Editor, you want to do so with a minimum of pain and effort. To get help for settings, press F1, which will bring up the context-sensitive Microsoft C Advisor. However, for some of the changes, the tools for getting help are not overly practical. When you want to change the color, you can get a list of hex values for different colors, but there isn't a color chart or a set-up dialog box. One major problem that I have with the Microsoft Editor is the lack of generalized support for keybinding. You can bind any function or macro to almost any key, but only to a single key. WordStar, EMACS, and many other editors use multiple keystrokes to access some features. Epsilon, a popular editor based on EMACS from Lugaru Software, has more than 60 commands bound to pairs of keys. Using the EMACS model for keybinding, Epsilon can be tailored to mimic any editor's keyboard interface. One way to get around this problem is to write an extension. This extension would be attached to the first key of a multikey sequence, and would be responsible for taking the next key or keys and dispatching the correct command. WS.C in the PWB on-line reference is an example of this kind of extension; it performs some of the WordStar commands. All in all, though, the PWB editor is quite serviceable. The feature set and extensibility are good, and it is fully integrated with the rest of the environment. If you use an editor that accesses all commands with single keystrokes, you should be able to make the PWB editor react the way you want it to, even if not all of the features of your editor are present. Project Management Facilities Project management is one area where PWB shines, although for some developers the built-in support for NMAKE may be useless. The key to the project make facility is the Program List (see Figure 3). A Program List is a list of files (C, ASM, OBJ, LIB, and so on), upon which the project target (a single EXE or DLL) is dependent. For each ASM and C file, the necessary rules will be added to build the OBJ, which in turn will be used to build the project target. Please note that you should add only LIB files that are part of your own project and likely to change; these will appear as dependencies for the project target. Other libraries, such as GRAPHICS.LIB, should be added as additional libraries under the LINK OPTIONS menu. Once you have added the source files the project target is dependent on, use the Set Dependencies command. This scans all the source files and adds any necessary header files to their dependencies lists. Another use for this feature is to update any PWB-compatible make files that have hardcoded pathnames for the header files. Set Dependencies looks for the headers along the INCLUDE path and updates that path in the make file. Another serviceable feature in PWB's set of project management tools is the Build Options dialog box (see Figure 4A). This is used to maintain a list of predefined build configurations. The defined configurations serve as templates to build new make files. Among the list of stock configurations are EXEs for DOS, Windows, OS/2, and OS/2 Presentation Manager (hereafter "PM"); DLLs for OS/2 and PM that incorporate C run-time library routines; DLLs and EXEs that use C run-time library DLLs; and special C run-time library DLLs (see Figure 4B). The Save Current Configuration command saves new named configurations for later use. Although its name implies otherwise, the command is used to create a newly defined configuration for future projects, not to save the current settings for use in the current project. Each configuration has two smaller configurations within it. The first configuration has the options for building a target for debugging; the second configuration has the options for building a target for release. These two configurations allow you to make a complete switch from one set of compiler and linker options to another very easily and quickly. It is very important to know that PWB stores project information both in a make file and in an STS file. There are a number of interactions between these files. If you ever want to make changes to the make file, you should delete the STS file or your changes may be lost when PWB loads the STS file. Editing Make Files You can edit a PWB-generated make file (although not while it is the selected Program List), but there are some limits. PWB only understands a limited subset of NMAKE capabilities as make files. For example, you can't make a LIB target from a bunch of OBJs and use the LIB to build the EXE. You can build the LIB another way and use it as a dependency for the EXE. Also, you cannot have multiple EXE or DLL targets in the make file. What you can have is a single EXE or DLL target that is made from many source, object, and library files. The restrictions that cause PWB to reject a make file as a non-PWB file are sometimes unclear. Certainly, multiple targets in a make file will cause PWB to reject the file, but other types of activity can cause a rejection, too. When PWB rejects a make file, it asks you if you want to use it as a foreign make file. Foreign make files are make files too complex for PWB to work with. To designate a make file as foreign, precede its name with the @ symbol when you use Set Program List. Otherwise PWB will tell you if it is problematic. Once PWB knows that a make file is foreign, it disables most of the menu items that control building. One item that remains available with a foreign make file is NMAKE OPTIONS. This item has great utility when working around some of the limits of foreign make files. Remember that PWB make files understand how to build both debugging and release versions of a project; this is controlled by conditional directives in the make file. If you follow similar conventions in your own make files, you can use NMAKE OPTIONS to control their execution. You could set some macros that control debug and release, localization, and other version-related information, for example. Source Browser One of the best features provided by PWB is a new Source Level Browser extension that allows you, for example, to browse code for large projects; to look at the varied relationships between routines, variables, and symbols in many files; to find where something is used and/or defined; and so on (see Figure 5). The Browser consists of a set of dialog boxes. One dialog box goes to definitions, one goes to references, one creates a list of references, one creates a call tree for a function or file, one generates an outline (sort of the reverse of the list of references), and one lets you browse relationships between various entities in the project. The GOTO commands, the cross-reference commands, and the call tree generator are the easiest Browse options to use. The View Relationships dialog is not as straightforward, but it is the most powerful. In essence, View Relationships is a sophisticated hub from which to explore your program. All the power of the other options, and more, is available using View Relationships, although the operations are not always formatted the same way. It is also possible to see the history of the relationships you have viewed for the file. To provide this information, the Browser maintains a database about your project, called a Browser Symbol Cache (BSC file). If you make the project with PWB and select Generate Browser Information in the Browser Options dialog box, PWB ensures that the BSC file is kept up to date. If you write your own make files or work from the command line, you have to keep the BSC file up to date yourself. A new compiler option (/Fr) writes browser information for each file to an SBR file. PWBRMAKE updates the BSC file from the SBR files, truncating the SBR file in the process. The project name, which is also the name of the program list, is a magic name for certain PWB extensions. One example is the Browser, which looks for a BSC file with the same name as the program list (a MAK file). This information will be helpful when you make your own foreign make files. Compilers The Professional Development System comes with both the C optimizing compiler and the QuickC compiler. The optimizing compiler generates the fastest, most optimized code. The quick compiler generates code much more quickly and also supports incremental compiling. Incremental compiling means that only routines that have been changed since the last compilation are recompiled. Typically, the quick compiler is used to build debugging versions of a program and the optimizing compiler is used to build release versions of a program. Use the /qc switch for CL to select the QuickC compiler. The predefined build configurations throw that switch when making debugging versions. Both compilers have been enhanced with new features in Version 6.0. There are now six memory models--the old small, medium, compact, large, and huge models and a new tiny (/AT) model. The tiny model generates COM files, putting both code and data in the same 64Kb segment. ANSI conformity has been further addressed in the compiler revisions. All of the extended Microsoft keywords are now prefixed with underscore (for example, _near), although the old versions are still supported. Programmers should start making their own code conform to the ANSI standards as well, adding the underscore as needed to the keywords. The long-awaited semantic support for the "volatile" keyword has been implemented. This keyword is important when writing multithreaded OS/2 programs or Windows code, when working with hardware devices, and at any time when the compiler needs to be told that a change may have been made to memory without the compiler being aware of it. There are some new features to make programs run faster. The optimizing compiler supports global register optimization (/Oe) and global optimizations and common subexpression (/Og). Both compilers support a new register-based parameter-passing scheme known as fastcall (/Or or _fastcall) and in-line assembler code (_asm). If you use one of the other function modifiers (such as _cdecl, _pascal, and _export), _fastcall will be disabled for that function. This means that headers for prebuilt calls (for example, third-party object libraries, OS/2, and Windows) must specify the correct modifiers, rather than make assumptions about the compiler options. That was already the case if you wanted to use /Gc in earlier compiler versions. Based Pointers A new kind of pointer has been added to Microsoft C. Based pointers combine attributes of near and far pointers, giving you the addressability of a far pointer and the size and speed of a near pointer. A based pointer is an offset off of ES. Once the segment or selector has been set up, all references are as fast and compact as they would be using near pointers. There are new keywords to support based pointers. The _based keyword defines a pointer or object as based. It is a lot like the _near and _far modifiers, but it takes an argument in parentheses that specifies the base. For example, you can use a pointer as the base for other pointers: char *p; char _based(p) *bp1; char _based(p) *bp2; bp1 = (char *) 0; // bp1 = p+0 bp2 = (char *) 2 // bp1 = p+2 The base can also be indicated in one of several other ways. You can specify a named segment as a base using _segname; for example, char _based (_segname("COPYR")) copyright[] = \ "(C) 1990 Microsoft" A new data type, _segment, is likely to be the most common way to specify bases. You can declare variables to be of type _segment or cast addresses to type segment. When _segment is used to cast a near address, the result is the current value in DS. If the address is a far address, the result is the segment for that far address. This is the case whether the address is the contents of a pointer or taken using &. The difference between _based(p) and _based((_segment)p) is that the former is based upon the address in the pointer including its offset, and the latter is based upon the segment address for the pointer (DS for near pointers, the segment and selector for far pointers). Based pointers can also be based on void or on _self. Pointers based on void do not have an implicit base and can be combined with a segment using the new :> operator: _segment segvar; int _based(void) *bp; int screen[screen_size], i; segvar = (color) ? 0xB800 : 0xB000; for (i=0,bp=0 ; i<screen_size ; i++,bp++) screen[i] = *(segvar:>bp); In a sense, _self is similar, but instead of not having a base, it says that the base is relative. _self based pointers are declared as _based((_segment)_self). Uses for _self could be in tree structures or linked lists. You can create a segment to hold the entire tree or linked list. Each of the pointers within the tree or linked list would be _self based, which tells that compiler that they all use the same ES. Based pointers have many advantages over near and far pointers. Near pointers are like based pointers that are fixed on DS, so based pointers have more addressability. Far pointers are much slower than near pointers, so based pointers have a tremendous advantage in speed over them. Also, based pointers are relative to their base, so you can move segments around in memory, save it off to disk and reload it, or any number of other operations without invalidating the based pointer. This will make it easy to implement persistent structures in C. If you allocate segments, and use based pointers within them (for example, _self), you can save the entire segment and restore it later, while preserving the integrity of your pointers. Run-time Library Enhancements Microsoft C Version 6.0 adds a lot of new run-time library (RTL) enhancements. These enhancements have been made to support system features such as based pointers and to make programming for OS/2 easier. The standard documentation for the C RTL is much shorter than it has been in the past. Each function is listed with its name, prototype, parameter descriptions, a one or two sentence description of the routine, and a compatibility table (ANSI, DOS, OS/2, UNIX, and XENIX systems). Detailed documentation and examples are present in the on-line references. Figure 6 contains the new C RTL routines for based pointers. Most of these functions also have analogues in the default (memory-model-dependent) near and far heaps. OS/2 support is one of the major enhancements in the C 6.0 package. The libraries are better organized to deal with all of the details involved in building DLLs and multithreaded applications, and the headers have been combined using conditional compilation directives. This makes the whole process of working with DOS, OS/2, DLLs, and so on, more straightforward and streamlined. CL has been enhanced with new options to turn on the necessary compiler directives for the target environment. The new OS/2 support is in the form of a few new libraries and CL options. OS/2 applications that do not need a reentrant C library can continue to use the standard xLIBCyP libraries. Standalone, multithreaded applications use the /MT switch, which causes the LLIBCMT library to be used for the program. The new version of this library is enhanced in C 6.0 to support as many threads as OS/2 will allow, not the former arbitrary limit of 32. For this reason, you can now use _beginthread for all threads, without having to worry about a limit. Among the new libraries for OS/2 is GRTEXTP.LIB, which provides a text-oriented subset of the GRAPHICS.LIB functionality for OS/2 protected mode. If you are building a DLL that uses the C run-time library, you use the /ML switch. This causes the LLIBCDLL library to be used. Using /ML sets /FPa because DLLs must not use the floating point coprocessor (except in a special case noted below). To build a standalone DLL, link your source code with the following files: OS2.LIB (OS/2 import library); LLIBCDLL.LIB (a multithreaded library for standalone DLLs); DLLINIT.OBJ (an optional module for providing user-defined DLL initialization); and DLLTERM.OBJ (the termination counterpart to DLLINIT.OBJ). Standalone programs and standalone DLLs do not share C run-time code or structures with each other. Another type of DLL, a private C run-time DLL, provides C run-time support to a closely related set of programs and DLLs and contains the working set of C run-time library functions necessary for the package. It is also the only way to allow DLLs to use the math coprocessor. To build a private C run-time library, link the following files: OS2.LIB (OS/2 import library); CDLLOBJS.LIB (dynamic-link C run-time library); CRTLIB.OBJ (start-up code for a C RT DLL); USER.DEF (a definition file listing the necessary C run-time functions). To find the functions you need, look at CDLLOBJS.DEF, which lists all of the available functions. Simply remove the unnecessary routines (to save space) when you have time. After you have built the private DLL, use IMPLIB to build an import library and LIB to add CDLLSUPP.LIB to your import library. This step is important, since CDLLSUPP contains C run-time routines that cannot be dynamically linked. To use your new DLL from another DLL in your application suite, use CL's /MD option, and link the object files and DEF file for your DLL with the following files: OS2.LIB (OS/2 import library); MYIMPORT.LIB (the import library for the private DLL); CRTDLL.OBJ (the start-up code for a DLL using a C run-time DLL); and CRTDLL_I.OBJ (an optional file that replaces CRTDLL.OBJ and supports user-defined DLL initialization). There is no termination module because you can properly handle termination using atexit or DosExitList. The program that uses the private C run-time DLL and any other associated DLLs is built by linking the object code and DEF file with: OS2.LIB (OS/2 import library); MYCIMPORT.LIB (import library for the private C run-time DLL); MYDLLIMPORT.LIB (an import library for another DLL); and CRTEXE.OBJ (start-up code for a program using a C run-time DLL). All the programs and DLLs that share a common private C run-time library DLL will be sharing C run-time library structures such as the file handle table. It is not a good idea to build a single C run-time DLL and use it for all of your programs. Each set of programs and DLLs that make up a single application should share their own private C run-time DLL. Intelligent Linking With all the support in the compiler for getting compilations done quickly, it would be a shame if you had to wait forever for a link to finish. Fortunately, the package includes ILINK, the incremental linker. After you have done a full link with LINK's /INC option, you can do most links with ILINK. ILINK basically adds the new or changed routines, although if it has to, ILINK will call LINK to do another full link. When you are ready to build a release version, you do another full link without the /INC option. This gives you the best combination of speed, convenience and compactness for your programs. The best part, of course, is the minimized link time. NMAKE As previously stated, PWB is somewhat limited in the complexity of projects that it can build. The new NMAKE tool, which is used by PWB, has no such limitations. NMAKE (for New MAKE) is a superset of the XENIX make program written by Microsoft, and a superset of the UNIX-like make programs familiar to many developers who gave up on Microsoft's earlier make program. If you are a user of one of those make systems, you should be pleased with NMAKE. NMAKE, like other UNIX-style make tools, disregards the ordering in the make file. NMAKE is given the name of the target to build, rather than building all targets in order. That normal parameter to NMAKE is the name of the target to be built, not the name of the make file. Unless you specify a new name for the make file, using the /F option, NMAKE assumes that the name is MAKEFILE. If you are using Microsoft's old make program, you will need to make a few changes. The easiest way to convert from MAKE to NMAKE is to add a pseudotarget, ALL, as the first target in the make file. ALL should be dependent upon all of the high-level targets in your make file. This does not mean each of the OBJs, unless those are final outputs from the make file, but just the EXEs, DLLs, and so on, that are the desired final result. You can also put other pseudotargets into a make file. For example, you could have one called SRCLIB that is dependent upon all of your source files and cause a source archive to be updated. Another common pseudotarget is CLEAN, which can be used to delete OBJ, BAK, and other nonrequired files from the project directory. There are some other nice features in NMAKE that you may want to take advantage of in your own make files. For example, as alluded to earlier, NMAKE supports directives in a make file. Such directives include !IF, !IFDEF, !IFNDEF, !ELSE, and !ENDIF; these are used by PWB to implement the conditional make files that build both debug and release versions of programs. Working directly with the full power of NMAKE, you can build even more options into your make files. When you invoke NMAKE, you add command line options of the form "macro = value." The NMAKE OPTIONS menu in PWB allows you to add command line options from within PWB. Another nice feature is that you can specify search paths to NMAKE. Search paths can be used in two places. First, you can use paths in inference rules. The syntax for inference rules is: {frompath}.fromext{topath}.toext commands The paths are optional. You can use paths in dependency lists also. For example: prog.obj : {\private\src; \group\src}prog.c tells NMAKE to look first in your private source library for possibly modified code for prog.c and to look in the group library if it doesn't find prog.c in yours. A macro, SRCPATH, could be defined and used throughout the make file. NMAKE supports string substitutions within macros, too. For example, suppose you have a macro SRCFILES that lists all of the source files necessary for a program. $(SRCFILES:.c=.obj) replaces all occurrences of .c with .obj in that macro, resulting in a list of all the OBJ files. Another option lets you process a list of files. The "!" command modifier tells NMAKE to execute the command for each file in the dependency list if the command uses either $? (out-of-date dependencies) or $** (all dependencies). On-line References Microsoft QuickHelp improves in its latest version in the C development system, although the improvement is based more on the use of QuickHelp throughout the product than on a change in the technology. Each of the tools (CL, PWB, LINK, ILINK, BIND, LIB, and so on) supports the command line option /HELP and reacts to it by invoking QuickHelp on its topic. This makes it very easy to get detailed hypertext-like help on all of the tools. Help is also available through the QuickHelp program and through the Microsoft C Advisor built into PWB. The development system comes with help files for each of the tools, the utilities, the C run-time library, the C language, PWB, and writing PWB extensions (see Figure 7). A table of contents and an index have been added. When you enter QuickHelp, you can ask it to get the table of contents for the available help files. As distributed, the package will show you a table of contents for PWB and for the resource compiler, RC. PWB's table of contents contains entries for the tools, the languages, the various APIs (OS/2, PWB extensions, network, and so on), and other useful categories. Not all of these files may be available on your system. For example, you may not have the help file for BASIC Version 7.0 or the network API. Also, you may want to add new help entries of your own to the table of contents. HELPMAKE is the tool used to build help files. It can also decode help files if they have not been locked. Microsoft's languages group does not lock their help files. You can use HELPMAKE to decode the PWB help file, find the table of contents, modify it as desired, and then use HELPMAKE again to rebuild the help file. Several groups have adopted commenting conventions for their file and function headers that can be scanned by AWK scripts and turned into source files for HELPMAKE. They can then call up custom help on each of their own functions and function categories as easily as on OS/2 programming and the OS/2 API. CodeView Version 3.0 The CodeView debugger has been enhanced in the Professional Development System. CodeView can now make use of some extended memory to cut down on the memory crunch often experienced by users (see Figure 8). The windowing package used by CodeView is now similar to that of PWB and Microsoft QuickC, Microsoft QuickBASIC, and so on. This windowing package permits you to open multiple windows of the same kind. There are now menu items to supplement the OS/2 thread debugging commands. And, as with all of the other tools in the package, CodeView Version 3.0 interfaces with a QuickHelp database, making it easier than ever to get help while debugging. I still do not find the CodeView user interface as easy to use and as powerful as that in the OS/2 Presentation Manager version of the MultiScope debugger by Logitech, but it is clearly moving in the right direction. The ability to write extensions such as those for PWB should be added to CodeView. In addition, the CodeView format has changed again. This means that those of you who use the MultiScope debugger for OS/2 will have to get a new version to work with the changed format. Up and Running from Version 5.1 Moving up to Version 6.0 from Version 5.1 is relatively painless. I have already noted a trick that you can use to keep them happily coresident while you make the change, and how to convert MAKE files to NMAKE files with little effort. One utility that you will not find on the disks is the venerable EXEMOD. A new version of EXEHDR contains all of the functionality of EXEMOD, as well as its own. It is now the only tool for working with the headers of executable files. There are a couple of other things that you may want to keep in mind. The C compiler generates code to call internal library routines for certain operations. These are called helper functions and are used for such things as shifting a long integer. The names of the helper routines have changed in C 6.0. Old third-party libraries that use the old names are all right since the new library supports them; other run-time libraries, such as those for Windows Version 2.11 that provide the helper functions, don't provide the names expected by C 6.0. The -Gh switch tells the C compiler to generate the old helper names, rather than the new ones. Finally, in the process of upgrading to ANSI support, Microsoft made a few changes in the language semantics. One example is the conversion of unsigned short now going to a signed long instead of an unsigned long. Summary All in all, the Microsoft C Version 6.0 Professional Development System is a major step forward for developers. Though more refinements are necessary, for example, supporting more complex make files within the PWB environment without having to make them foreign make files, it shows the direction that Microsoft is going to take their development tools over the next few years. Figure 6 RTL Enhancements Function Description _bfreeseg Free a based heap _bheapseg Create a based heap _bcalloc Calloc within a based heap _bexpand Expand block (not moved) _bfree Free allocate block _bheapadd Expand size of heap _bheapchk Consistency check heap _bheapmin Release unused heap space _bheapset Check and fill free blocks _bheapwalk Walk the heap _bmalloc Malloc in the based heap _brealloc Realloc in the based heap TRACER: A Debugging Tool for OS/2 Presentation Manager Development Daniel Hildebrand The introduction of a new machine or operating system compels developers to determine how well the associated development environment meets their debugging needs. Developers often need a variety of debugging methodologies not satisfied by out-of-the-box tools. Though all environments include standard debuggers that allow developers to monitor machine registers and trace through programs at a microscopic level, tools that permit logical debugging at a higher level may also be needed. In addition to standard debugging, facilities within the OS/2 development environment provide the ability to monitor messages critical to some programs. The toolkit falls short, however, of providing a way to perform the simplest form of debugging: the trace display of formatted program output to some auxiliary device. This article examines some existing debugging tools and their effectiveness and then presents a simple program called TRACER that is useful for tracing and debugging OS/2 Presentation Manager (hereafter "PM") applications. For the purposes of this article, I would like to distinguish between types of debugging processes. The first, "exception" debugging, is what is done when a fatal error is encountered. Based on a diagnosis of the problem, a post-mortem debugging mode is entered to recreate the scenario and isolate the bug. The second, "development" debugging, is used during the evolution of code to monitor progress and prevent potential bugs. Commonly, a series of trace statements is placed within the code to monitor the values of selected variables, ensure that array boundaries are respected, verify that return codes are correct, check program states, and so on. These trace mechanisms usually redirect formatted output to devices such as files, printers, or auxiliary screens. Usually bugs are caught before they happen and when a bug does occur it is fairly simple to identify by examining the memory and the state of the program via the output of the trace mechanism. This simple form of development tracing solves many of the problems that developers encounter. Debugging Environment Versions 1.1 or higher of the OS/2 Software Development Kit come with a debugging version of the device driver PMDD.SYS. It can be used by adding the line DEVICE=\OS2\DEBUG\PMDD.SYS /M to the CONFIG.SYS file. The /M switch tells the system to output debugging information to a secondary monitor. When this special driver is used, debugging is activated for applications running within the PM screen group. Useful information is displayed for PM applications, including messages telling the developer that window handles have been created, memory has been allocated or freed, OS2.INI is being queried or written to, and so on. Although this information lets developers see the underlying actions being carried out by the system, the facility does not allow developers to send their own messages to the secondary monitor. The Spy program is familiar to Windows and OS/2 programmers. Spy is very helpful for resolving certain kinds of bugs and for learning about message-based architecture. For example, by observing message traffic in normal circumstances, you can often determine which messages are missing or required in exceptional circumstances. Spy cannot, however, find certain problems. A pointer problem, for example, will usually not be discovered using Spy. Sometimes, merely loading the Spy program into memory will change the target location that the bug has been affecting and lessen your chances of finding or even duplicating the bug. The PM version of Spy displays process and thread information and is superior to the Windows version, but is not designed for development debugging. There are some very good facilities for doing interrupt-level debugging in OS/21. The Microsoft CodeView debugger and Multiscope (a debugging tool available from Logitech Corporation) are very useful for trapping memory corruption bugs. Both can step through a program while dynamically revealing variable values, dumping the contents of structures, evaluating loop counts, and so on. However, the larger executable generated by compiling an application for use with the CodeView or Multiscope debuggers often moves the bug around in memory, making it sometimes hard to find and difficult to fix. These tools are good for what they do, but again, they do not provide efficient development debugging. Tracing in Windows and PM Development debugging in the Windows2 environment is quite a bit more complicated than in DOS3. Printf functions sprinkled among a program's regular output are no longer useful, since Windows provides a purely graphical user interface (GUI) and manages the video output fully. Substituting the Windows API function TextOut, which displays text strings within a window's client area, doesn't work well for debugging purposes either. With a secondary monochrome monitor attached to your system, you can redirect formatted output to that monitor from within your program. This is done by placing the file OX.SYS onto your hard disk, booting your machine with the statement device=\ox.sys in your CONFIG.SYS file, and then using fprintf functions in your code to "print" to the auxiliary screen. The device driver OX.SYS, a small program available from Microsoft, redirects stdaux output to the monochrome video address B0000H. This is a very useful technique employed by many Windows developers. In OS/2, however, the protected mode rules of the 80286 are adhered to; the average PM application may no longer directly manipulate hardware. Although the debugging version of PM can itself output strings directly to a secondary monitor, I know of no facility that will enable you to do the same thing from within your PM application. Of course, whether you're developing in Windows or OS/2, if you don't have a secondary monitor you can't write to it. One alternative is to come up with a program that allows clients to display messages from within their programs in a window and redirect those messages to files if we desire. This method is also ideal for studying run-time problems encountered on end-user systems in which it is impossible to install development software. Under Windows, writing this program is simple. Writing a trace window that can support multiple applications is no problem because of the nonpreemptive multitasking of Windows. A window can be created with a client area whose sole purpose is to display text messages. Applications then simply send a user-defined Windows message to the trace window, with a far pointer (LPSTR) to the text string in the lParam of the message. The window need only use TextOut to display the text string, and the job is complete. Developing this trace window for PM, on the other hand, is more complex. TRACER The preemptive multitasking of OS/2 means that a PM tracing window must contain code to explicitly support multiple applications running simultaneously This is especially true if serial interprocess communication (IPC) and shared memory between segments are used in the application. TRACER is a PM window that displays messages from client PM applications (see Figure 1). IPC, named shared memory, and system semaphores allow TRACER to be used by multiple concurrent applications. The Options menu choice allows you to toggle the screen display and/or the file display on and off. It also allows you to truncate the file and/or empty the window before proceeding. TRACER.C (see Figure 2) contains the main section of the application, including the window procedure that manages the TRACER handshaking messages and display of debug strings. TRACERP.C contains code to manage the TRACER output window; TRACERD.C contains code to write output strings to a file. Only one instance of TRACER may run at a time. This is important because the program uses system resources (a semaphore and named shared memory) and must not attempt to access more than one copy of those resources at a time. Also, since TRACER shares those resources with clients, TRACER may not be closed until all clients have released the resources by closing down first. TRACER attempts to return those resources to the system; it will wreak havoc with the system if it tries to return them while the usage counts for those resources are nonzero. TRACER provides MessageBox and auditory responses if an attempt is made to start more than one copy of it or to shut it down while clients are still running. It does not matter whether TRACER is brought up before or after the client application: the code performs handshaking in both instances. TRACER is straightforward to use from within a client program. The following discussion provides a five-step process to use TRACER. I have included a sample application using TRACER, called CLIENT.C (see Figure 3), that can be referenced as you go through these steps. The file TRACER.H (see Figure 2), which is included by this client, contains a macro to be used at each step. First, include the file TRACER.H in your code. Step two involves the macro TRACERVARIABLES, which contains declarations for the variables that the client will need. These variables include the name of the selector to the global named shared memory, a far pointer to that memory, a handle to a system semaphore, the TRACER window handle, and a variable called bTracerConnected, which is used to determine whether the client is connected to TRACER. TRACER allocates 80 bytes of memory to share with clients. Clients use the memory to transfer the text string for display to TRACER. You need to add the line TRACERVARIABLES above your main. At the bottom of TRACER.H is a macro called EXTERNTRACERVARS that should be used in any secondary code segments (that is, other source files)in which you want to use TRACER. EXTERNTRACERVARS declares the TRACERVARIABLES as extern. Somewhere between the client's WM_CREATE and the subsequent break statement, add the macro TRACERHELLO. TRACERHELLO, the third step, is used to sign on, so to speak, to TRACER. The macro sends a message called TRACER_REQUEST_HANDLE to every message queue application in the system. TRACERHELLO passes its own window handle in mParam1 of the message so that when TRACER receives the message, it can return the TRACER window handle to the client (see Figure 3). When TRACER receives the message, it places its window handle into the mParam1 of a TRACER_RECEIVE_HANDLE message and posts it back to the client. To review: the client passes its window handle to TRACER by broadcasting a message to the system. If TRACER is running, it returns its own window handle to the client in the TRACER_RECEIVE_HANDLE message. Step four occurs when both applications have each other's window handle, the client must then retrieve permission from the system to access the named shared memory segment allocated by TRACER. To serialize requests for services, the client must also retrieve a handle to a system semaphore that will be used by TRACER to block requests by clients until it is their turn to use TRACER. To illustrate the importance of blocking requests, imagine two clients, A and B, that want to display a message string in TRACER and request the service at almost the same time. The OS/2 scheduler has one purpose--to pass control to threads in the system based on their readiness to run and then to retake control and pass it to the next thread that is ready to run. Consider the mess that will occur if client A's message is half-written to the screen when the scheduler takes control and gives it to client B, which then has its message written where the second half of client A's message should have been. Using a system semaphore blocks threads that may concurrently request a TRACER service. To implement the semaphore, add the macro TRACERATTACH as a separate case in the client window procedure. TRACERATTACH receives the shared segment selector and the handle to the semaphore from the system. You are now ready for step five, which is to use the TRACER macro freely. This macro has one parameter in it, the text string you want TRACER to print in its window. When the TRACER macro is called, the client waits for the system semaphore to be cleared; when it is, the client sets the semaphore for its own use. At this point the text message is prepared and a WinSendMessage (that is, a function call to the TRACER window procedure) is made requesting action using the TRACER_REQUEST_ACTION message. Note the TRACER call in the client window procedure for WM_BUTTON1DOWN. This demonstrates how a client might use sprintf to formulate its own output string for output to TRACER. Here, the client formats a buffer to include the message name and the current time (actually the number of milliseconds elapsed since the system was started) and sends the formatting string to TRACER. Try pressing the first mouse button on the client's window. The client may construct any number of arbitrarily complex strings for display within TRACER. TRACER evaluates whether the user wishes messages to be displayed in the window. If it does, it simply sends the appropriate list box control messages to the window and the job is done. Likewise, if the user wants the message to be echoed to the debugging trace file, TRACER writes the message to the file as well. The TRACER message then breaks, and the client clears the semaphore and continues processing, thereby freeing TRACER for its next request for action. The bTracerConnected variable protects a client from trying to send output to the application when TRACER is not running, thereby preventing the client from trying to set a system semaphore that is not available or to access a data segment outside of its process. To initiate contact with unconnected clients, the WM_CREATE message of TRACER sends its own TRACER_RECEIVE_HANDLE to the system. Connected clients ignore the message. A small problem with TRACER is that a certain number of messages must be exchanged between the client and TRACER before the TRACER macro will work. Because of this, you may not see trace messages that occur very early in the life of your PM program (those within the response to the WM_CREATE message, for example). Conclusion Although TRACER is designed to assist the developer during the construction of PM programs, it is also an excellent tool to debug problems in production environments. TRACER can be used to record the events leading to fatal problems. A post-mortem analysis of the TRACER file may lead to a resolution of the problem. While this article's purpose is to provide a simple and useful debugging tool for PM developers, the TRACER facility is also presented in order to review the use of some interesting and important OS/2 mechanisms, such as system semaphores and named shared memory. Figure 2 TRACER #------------------------------------------------------------------- # # Standard command line definitions # #------------------------------------------------------------------- cp=cl -c -W3 -Alfw -G2sw -Os -Zpei #------------------------------------------------------------------- # # Default inference rules # #------------------------------------------------------------------- .c.obj: $(cp) $*.c .asm.obj: masm $*.asm; .rc.res: rc -r $*.rc #------------------------------------------------------------------- # # Dependencies # #------------------------------------------------------------------- tracer.obj: tracer.c tracer.h tracerp.obj: tracerp.c tracer.h tracerd.obj: tracerd.c tracer.h tracer.res: tracer.rc tracer.ico tracer.h tracer.exe: tracer.obj tracerp.obj tracerd.obj tracer.res tracer.lnk tracer.def link @tracer.lnk rc tracer.res TRACER.H //---------------------------------------------- //-TRACER.H------------------------------------- //---------------------------------------------- //---------------------------------------------- //-To use TRACER, follow these 5 steps.--------- //---------------------------------------------- //----1. Include this header in your program.--- // Includes necessary macros. //---------------------------------------------- //----2. Add macro TRACERVARIABLES above main(). // Defines necessary variables / defines.- //---------------------------------------------- //----3. Add macro TRACERHELLO in your WM_CREATE // Put between WM_CREATE and break; // Initiates handshaking with debugger. //---------------------------------------------- //----4. Add macro TRACERATTACH in your WndProc. // Put as a separate case in your WndProc. // Completes handshaking with debugger. //---------------------------------------------- //----!!! When placing the TRACERATTACH in your- //--------window procedure, don't forget the---- //--------break; statement after it. !!!-------- //---------------------------------------------- //----5. Use macro TRACER("string") freely.----- // This is the debug statement. //---------------------------------------------- //----Note:------------------------------------- //----You may need to include EXTERNTRACERVARS-- //----in secondary modules if you want to call-- //----TRACER from those modules.---------------- //---------------------------------------------- //---------------------------------------------- //-Note:---------------------------------------- //---------------------------------------------- //----In the TRACERHELLO macro, there is a param //----called hWnd. You need to pass your window //----handle to me so that I can send mine back- //----to you. You may need to change "hWnd" to- //----whatever your wndproc parameter name is--- //----for window handle ( e.g. MyhWnd ).-------- //---------------------------------------------- #define TRACER_SEGMENT "\\SHAREMEM\\TRACER.SEG" #define TRACER_SEMAPHORE "\\SEM\\TRACER.SEM" #define TRACERICON 1 #define ID_HELPBUTTON 2 #define ID_TRACERLB 3 #define IDMOPTIONS 4 #define IDMABOUT 5 #define ID_OK 6 #define ID_MENU 7 #define IDMCLEAR 8 #define IDMLOG 9 #define ID_CANCEL 10 #define ID_LOGFILEEDIT 11 #define ID_LOGFILESCROLL 12 #define ID_SCREENSCROLL 13 #define ID_REFRESH 14 // Step 1 - INCLUDE THIS FILE IN YOUR APP CODE AS FOLLOWS //#include <tracer.h> #define TRACER_REQUEST_HANDLE WM_USER + 501 #define TRACER_RECEIVE_HANDLE WM_USER + 502 #define TRACER_REQUEST_ACTION WM_USER + 503 #define TRACER_GOODBYE WM_USER + 504 #define TRACER_MYCLOSE WM_USER + 505 // Step 2 - INCLUDE THE TRACERVARIABLES STATEMENT IN YOUR // DECLARATION SECTION #define TRACERVARIABLES \ unsigned short sTracerSelector; \ HSYSSEM hTracerSysSem; \ BOOL bTracerConnected; \ char far * szSelector_string; \ HWND hTracerWnd; // Step 3 - INCLUDE THE TRACERHELLO STATEMENT IN YOUR // WM_CREATE MESSAGE #define TRACERHELLO \ WinBroadcastMsg( hWnd, TRACER_REQUEST_HANDLE, \ MPFROMHWND( hWnd ), 0L, \ BMSG_FRAMEONLY | BMSG_POSTQUEUE ); // Step 4 - INCLUDE THE TRACERATTACH STATEMENT IN YOUR // MESSAGE WNDPROC #define TRACERATTACH \ case TRACER_RECEIVE_HANDLE: \ if ( bTracerConnected ) \ break; \ hTracerWnd = HWNDFROMMP( mp1 ); \ bTracerConnected = 1; \ if ( DosGetShrSeg( TRACER_SEGMENT, &sTracerSelector ) ) \ bTracerConnected = 0; \ if ( DosOpenSem( &hTracerSysSem, TRACER_SEMAPHORE ) ) \ bTracerConnected = 0; // Step 5 - THEN USE THE TRACER("HELLO, WORLD") MACRO FREELY #define TRACER(s) \ if ( bTracerConnected ) \ { \ DosSemRequest( hTracerSysSem, -1L ); \ szSelector_string = \ ( char far * )( ( unsigned long )sTracerSelector << 16 ); \ strncpy( ( char far * )szSelector_string, ( char far * )s, 80 ); \ szSelector_string[80] = '\0'; \ WinSendMsg( hTracerWnd, TRACER_REQUEST_ACTION, 0L, 0L ); \ DosSemClear( hTracerSysSem ); \ } #define EXTERNTRACERVARS \ extern unsigned short sTracerSelector; \ extern HSYSSEM hTracerSysSem; \ extern BOOL bTracerConnected; \ extern char far * szSelector_string; \ extern HWND hTracerWnd; TRACER.DEF NAME TRACER WINDOWAPI DESCRIPTION 'OS/2 PM Tracer Utility' STUB 'OS2STUB.EXE' CODE MOVEABLE DATA MOVEABLE MULTIPLE HEAPSIZE 1024 STACKSIZE 4096 EXPORTS TracerWndProc @1 HelpHook @2 TracerAboutDlg @3 TracerLogFileDlg @4 TRACER.LNK tracer.obj + tracerp.obj + tracerd.obj tracer/align:16 tracer/map os2 llibcmt doscalls/NOD tracer.def TRACER.RC #include <os2.h> #include "tracer.h" POINTER TRACERICON tracer.ico MENU TRACERICON PRELOAD BEGIN SUBMENU "~Options", IDMOPTIONS BEGIN MENUITEM "~Clear Message Area\t^C", IDMCLEAR, MIS_TEXT MENUITEM "~Log Messages to File...\t^L", IDMLOG, MIS_TEXT MENUITEM "~About...\t^A", IDMABOUT, MIS_TEXT END MENUITEM "~F1=Help", ID_HELPBUTTON, MIS_HELP | MIS_BUTTONSEPARATOR END ACCELTABLE TRACERICON BEGIN "^A", IDMABOUT "^L", IDMLOG "^C", IDMCLEAR END DLGTEMPLATE IDMABOUT LOADONCALL MOVEABLE DISCARDABLE BEGIN DIALOG "", 5, 10, 23, 190, 52, FS_NOBYTEALIGN | FS_DLGBORDER | WS_CLIPSIBLINGS | WS_SAVEBITS BEGIN CONTROL "OS/2 Debugging Utility", -1, 17, 35, 156, 14, WC_STATIC, SS_TEXT | DT_CENTER | DT_TOP | WS_GROUP | WS_VISIBLE CONTROL "Version 1.00", -1, 57, 17, 74, 8, WC_STATIC, SS_TEXT | DT_CENTER | DT_TOP | WS_GROUP | WS_VISIBLE CONTROL "OK", 6, 5, 2, 38, 12, WC_BUTTON, BS_PUSHBUTTON | BS_DEFAULT | WS_TABSTOP | WS_VISIBLE CONTROL "Daniel Hildebrand", 256, 55, 29, 82, 8, WC_STATIC, SS_TEXT | DT_LEFT | DT_TOP | WS_GROUP | WS_VISIBLE END END DLGTEMPLATE 9 LOADONCALL MOVEABLE DISCARDABLE BEGIN DIALOG "", 9, 10, 17, 209, 74, FS_NOBYTEALIGN | FS_DLGBORDER | WS_CLIPSIBLINGS | WS_SAVEBITS BEGIN CONTROL "Ok", 6, 7, 4, 38, 12, WC_BUTTON, BS_PUSHBUTTON | BS_DEFAULT | WS_TABSTOP | WS_VISIBLE CONTROL "Cancel", 10, 51, 4, 38, 12, WC_BUTTON, BS_PUSHBUTTON | BS_DEFAULT | WS_TABSTOP | WS_VISIBLE CONTROL "Log File: ", -1, 6, 57, 50, 10, WC_STATIC, SS_TEXT | DT_CENTER | DT_TOP | WS_GROUP | WS_VISIBLE CONTROL "\\tracer.fle", 11, 63, 59, 100, 8, WC_ENTRYFIELD, ES_LEFT | ES_AUTOSCROLL | ES_MARGIN | WS_TABSTOP | WS_VISIBLE CONTROL "Screen Scroll On/Off", 13, 106, 24, 103, 9, WC_BUTTON, BS_AUTOCHECKBOX | WS_TABSTOP | WS_VISIBLE CONTROL "Log File On/Off", 12, 10, 24, 91, 9, WC_BUTTON, BS_AUTOCHECKBOX | WS_TABSTOP | WS_VISIBLE CONTROL "Help", 2, 97, 4, 38, 12, WC_BUTTON, BS_PUSHBUTTON | BS_DEFAULT | BS_HELP | WS_TABSTOP | WS_VISIBLE CONTROL "Truncate Log File before proceeding", 14, 10, 41, 171, 10, WC_BUTTON, BS_AUTOCHECKBOX | WS_TABSTOP | WS_VISIBLE END END TRACER.C //---------------------------------------------- //-TRACER ( OS/2 PM Debugging Trace Facility )-- //---------------------------------------------- //-(c) 1990 Daniel Hildebrand------------------- //---------------------------------------------- #define INCL_PM #define INCL_WIN #define INCL_DOS #define INCL_WINLISTBOXES #define INCL_WINHOOKS #define INCL_ERRORS #include <os2.h> #include <process.h> #include <stdlib.h> #include <string.h> #include "tracer.h" //---------------------------------------------- //-Module declarations-------------------------- //---------------------------------------------- int main ( void ); MRESULT EXPENTRY TracerWndProc ( HWND hWnd, USHORT msg, MPARAM mp1, MPARAM mp2 ); VOID TracerCommand ( HWND hWnd, SHORT id, SHORT source, BOOL mouse ); static VOID ClearSelector ( char far * selector_string ); BOOL EXPENTRY HelpHook ( HAB hab, USHORT usMode, USHORT idTopic, USHORT idSubTopic, PRECTL prcPosition ); static VOID HelpMessage ( void ); //---------------------------------------------- //-External references-------------------------- //---------------------------------------------- extern void FAR PASCAL TracerPaint ( HWND hWnd, USHORT msg, MPARAM mp1, MPARAM mp2 ); extern void FAR PASCAL ClearLB ( HWND hLB ); extern VOID LogToFile ( PSZ selector_string ); extern MRESULT EXPENTRY TracerAboutDlg ( HWND hWndDlg, USHORT message, MPARAM mp1, MPARAM mp2 ); extern MRESULT EXPENTRY TracerLogFileDlg ( HWND hWndDlg, USHORT message, MPARAM mp1, MPARAM mp2 ); //---------------------------------------------- //-Global variables----------------------------- //---------------------------------------------- HAB hAB; /* anchor block */ HMQ hmqTracer; /* handle queue */ HSYSSEM hSysSem; /* sys semaphore */ HFILE pLogFile; /* handle to file */ HWND hParentWnd; /* client hWnd */ HWND hParentFrm; /* frame hWnd */ HWND hLB; /* handle ListBox */ BOOL bUserRequestsScroll = TRUE; /* Scroll ON ? */ BOOL bUserRequestsNewLog = TRUE; /* Truncate Log? */ BOOL bUserRequestsLog = FALSE; /* LogFile ON ? */ BOOL bLoadFail; /* TRACER Load OK?*/ char szMessage[] = " PM TRACER "; /* Log File */ char szLogFile[] = "\\tracer.fle"; /* Window Class */ char szParentClass[] = "PClass"; char far *selector_string; /* pointer to named shared mem */ int iNumberItems; /* # error strings*/ USHORT selector; /* selector to named shared mem*/ USHORT pusAction; /* file IO */ USHORT pusBytesWritten; /* file IO */ //---------------------------------------------- //-main()--------------------------------------- //---------------------------------------------- int main( ) { QMSG qmsg; ULONG ctldata; SWP swpCurrent; int aiGen; // initialize this process hAB = WinInitialize( NULL ); // create a PM message queue hmqTracer = WinCreateMsgQueue( hAB, 0 ); // register window class if ( !WinRegisterClass( hAB, (PCH)szParentClass, (PFNWP)TracerWndProc, CS_SIZEREDRAW, 0 ) ) return( 0 ); // file control flags ctldata = FCF_TITLEBAR | FCF_SYSMENU | FCF_BORDER | FCF_MINBUTTON | FCF_MENU | FCF_ICON | FCF_SHELLPOSITION | FCF_TASKLIST | FCF_ACCELTABLE; // create standard window hParentFrm = WinCreateStdWindow( HWND_DESKTOP, WS_SYNCPAINT, &ctldata, (PCH)szParentClass, NULL, 0L, (HMODULE)NULL, TRACERICON, (HWND FAR *)&hParentWnd ); // if semaphore was obtained // if named shared memory segment was obtained // if tracer file was opened // ... if ( ! bLoadFail ) { // auditory feedback to user that app coming up OK. for ( aiGen = 0; aiGen < 12; aiGen += 2 ) DosBeep( ((aiGen + 1) * 100), 1 ); WinSetWindowText( hParentFrm, "OS\\2 TRACER Version 1.0"); WinSetWindowPos ( hParentFrm, HWND_TOP, 20, 10, 500, 125, SWP_SIZE | SWP_MOVE | SWP_SHOW ); } // enter message loop while( WinGetMsg( hAB, (PQMSG)&qmsg, (HWND)NULL, 0, 0 ) ) WinDispatchMsg( hAB, (PQMSG)&qmsg ); // destroy TRACER window WinDestroyWindow( hParentFrm ); // destroy message queue WinDestroyMsgQueue( hmqTracer ); // bye bye WinTerminate( hAB ); } //---------------------------------------------- //-TracerWndProc()------------------------------ //---------------------------------------------- //---------------------------------------------- MRESULT EXPENTRY TracerWndProc( HWND hWnd, USHORT msg, MPARAM mp1, MPARAM mp2 ) { RECTL rRect; int aiGen; switch (msg) { case WM_CREATE: // if semaphore already exists ... if ( DosCreateSem( CSEM_PUBLIC, &hSysSem, TRACER_SEMAPHORE ) ) // ... and we cannot open it, then abort if ( DosOpenSem( &hSysSem, TRACER_SEMAPHORE ) ) { DosBeep(300,300); WinMessageBox( HWND_DESKTOP, hWnd, (PCH)"Failed to Load!", (PCH)"OS/2 TRACER Version 1.0", NULL, MB_OK| MB_ICONEXCLAMATION ); bLoadFail = 1; WinPostMsg( hWnd, WM_QUIT, 0L, 0L ); break; } // allocate an 81 byte data segment if ( DosAllocShrSeg( 81, TRACER_SEGMENT, &selector ) ) { DosBeep(300,300); WinMessageBox( HWND_DESKTOP, hWnd, (PCH)"Failed to Load!", (PCH)"OS/2 TRACER Version 1.0", NULL, MB_OK| MB_ICONEXCLAMATION ); bLoadFail = 1; WinPostMsg( hWnd, WM_QUIT, 0L, 0L ); break; } // open TRACER file DosOpen( (PSZ)szLogFile, &pLogFile, &pusAction, 100L, 0, 0x11, 0x41, 0L ); // obtain a pointer to the named segment selector_string = (char far *)((unsigned long)selector << 16); // clear the segment ClearSelector( selector_string ); // F1 help hook WinSetHook( hAB, hmqTracer, HK_HELP, (PFN)HelpHook, NULL ); // Send to anyone who was brought up BEFORE the TRACER WinBroadcastMsg( hWnd, TRACER_RECEIVE_HANDLE, MPFROMHWND( hWnd ), 0L, BMSG_FRAMEONLY | BMSG_POSTQUEUE ); return( WinDefWindowProc( hWnd, msg, mp1, mp2 ) ); break; case WM_COMMAND: // handle menu commands TracerCommand( hWnd, SHORT1FROMMP(mp1), SHORT1FROMMP(mp2), SHORT2FROMMP(mp2)); break; case TRACER_REQUEST_HANDLE: // a client has broadcast a request for handshake WinPostMsg( HWNDFROMMP(mp1), TRACER_RECEIVE_HANDLE, MPFROMHWND(hWnd), 0L ); break; case TRACER_REQUEST_ACTION: // client has issued a TRACER() request // is screen toggled ON ? if ( bUserRequestsScroll ) { selector_string[80] = '\0'; WinSendMsg( hLB, LM_INSERTITEM, (MPARAM)-1L, (MPARAM)(PCH)selector_string ); WinSendMsg( hLB, LM_SELECTITEM, (MPARAM)(iNumberItems - 1), (MPARAM)FALSE ); WinSendMsg( hLB, LM_SELECTITEM, (MPARAM)iNumberItems++, (MPARAM)TRUE ); WinSendMsg( hLB, LM_SETTOPINDEX, (MPARAM)(iNumberItems - 1), (MPARAM)TRUE ); } // is file toggled ON ? if ( bUserRequestsLog ) { selector_string[80] = '\0'; LogToFile( selector_string ); } ClearSelector( selector_string ); break; case WM_CLOSE: // return semaphore to system DosCloseSem( hSysSem ); // is semaphore available to be obtained ? if ( DosCreateSem( CSEM_PUBLIC, &hSysSem, TRACER_SEMAPHORE ) ) { // that's no good. a client must still have it opened ! DosBeep(75,200); WinMessageBox( HWND_DESKTOP, hWnd, (PCH) "Will not shut down while client(s) are still attached!", (PCH)"OS/2 TRACER Version 1.0", NULL, MB_OK| MB_ICONEXCLAMATION ); DosOpenSem( &hSysSem, TRACER_SEMAPHORE ); break; } else DosCloseSem( hSysSem ); /* one for create */ // continue close down WinPostMsg( hWnd, TRACER_MYCLOSE, 0L, 0L ); break; case TRACER_MYCLOSE: // free the named shared data segment if ( DosFreeSeg( selector ) ) DosBeep( 100, 450 ); // release the F1 help hook WinReleaseHook ( hAB, hmqTracer, HK_HELP, (PFN)HelpHook, NULL ); // flush the buffer to the TRACER file / close it DosBufReset( pLogFile ); DosClose( pLogFile ); // auditory feedback to user that app coming down OK. for ( aiGen = 12; aiGen > 0; aiGen -= 2 ) DosBeep( ((aiGen - 1) * 100), 1 ); WinPostMsg( hWnd, WM_QUIT, 0L, 0L ); break; case WM_PAINT: // paint routine for TRACER window TracerPaint( hWnd, msg, mp1, mp2 ); break; case WM_ERASEBACKGROUND: return( TRUE ); break; default: return( WinDefWindowProc( hWnd, msg, mp1, mp2 ) ); break; } return(0L); } //---------------------------------------------- //--HelpHook------------------------------------ //---------------------------------------------- //---------------------------------------------- BOOL EXPENTRY HelpHook( HAB hab, USHORT usMode, USHORT idTopic, USHORT idSubTopic, PRECTL prcPosition ) { WinMessageBox( HWND_DESKTOP, hParentWnd, (PCH) "TRACER.H has instructions on how to use TRACER in your app.", (PCH)"OS/2 TRACER HelpHook", NULL, MB_OK| MB_CUANOTIFICATION ); #ifdef HelpHook switch ( usMode ) { case HLPM_MENU: // idtopic is submenu identifier // idsubtopic is item identifier // prcposition is boundary of item switch ( idTopic ) { case ID_HELPBUTTON: HelpMessage(); } break; case HLPM_FRAME: // idtopic is frame identifier // idsubtopic is focus window identifier // prcposition is boundary of focus window switch ( idTopic ) { case ID_HELPBUTTON: HelpMessage(); } break; case HLPM_WINDOW: // idtopic is parent of focus window // idsubtopic is focus window identifier // prcposition is boundary of focus window switch ( idTopic ) { case ID_HELPBUTTON: HelpMessage(); } break; } #endif return TRUE; } //---------------------------------------------- //--TracerCommand------------------------------- //---------------------------------------------- //---------------------------------------------- VOID TracerCommand( HWND hWnd, SHORT id, SHORT source, BOOL mouse ) { switch( id ) { case IDMABOUT: WinDlgBox( HWND_DESKTOP, hWnd, (PFNWP)TracerAboutDlg, NULL, IDMABOUT, NULL ); break; case IDMLOG: WinDlgBox( HWND_DESKTOP, hWnd, (PFNWP)TracerLogFileDlg, NULL, IDMLOG, NULL ); break; case IDMCLEAR: ClearLB( hLB ); break; } } //---------------------------------------------- //-HelpMessage()-------------------------------- //---------------------------------------------- //---------------------------------------------- static VOID HelpMessage ( void ) { WinMessageBox( HWND_DESKTOP, hParentWnd, (PCH) "TRACER.H has instructions on how to use TRACER in your app.", (PCH)"OS/2 TRACER HelpHook", NULL, MB_OK| MB_CUANOTIFICATION ); return; } //---------------------------------------------- //-ClearSelector()------------------------------ //---------------------------------------------- //---------------------------------------------- static VOID ClearSelector ( PSZ selector_string ) { int aiX; for ( aiX = 0; aiX < 80; aiX++ ) selector_string[aiX] = ' '; return; } TRACERD.C //----------------------------------------------- //-TRACERD.C Source code for TRACER file routines //----------------------------------------------- #define INCL_PM #define INCL_WIN #define INCL_DOS #define INCL_WINLISTBOXES #define INCL_WINBUTTONS #define INCL_WINHOOKS #define INCL_ERRORS #include <os2.h> #include "tracer.h" //---------------------------------------------- //-External references-------------------------- //---------------------------------------------- extern char szLogFile[]; extern BOOL bUserRequestsScroll; extern BOOL bUserRequestsLog; extern BOOL bUserRequestsNewLog; extern HFILE pLogFile; extern USHORT pusAction; extern USHORT pusBytesWritten; //---------------------------------------------- //--LogToFile()--------------------------------- //---------------------------------------------- VOID LogToFile ( PSZ selector_string ) { // write text string to log file DosWrite( pLogFile, (char far *)selector_string, (USHORT)80, &pusBytesWritten ); DosWrite( pLogFile, (char far *)"\r\n", (USHORT)2, &pusBytesWritten ); return; } //---------------------------------------------- //--TracerAboutDlg()---------------------------- //---------------------------------------------- MRESULT EXPENTRY TracerAboutDlg( HWND hWndDlg, USHORT message, MPARAM mp1, MPARAM mp2 ) { switch( message ) { case WM_COMMAND: /* the user has pressed a button */ switch( SHORT1FROMMP( mp1 ) ) { case ID_OK: WinDismissDlg( hWndDlg, TRUE ); break; default: return( FALSE ); } break; default: return( WinDefDlgProc( hWndDlg, message, mp1, mp2 ) ); } return( FALSE ); } //---------------------------------------------- //--TracerLogFileDlg()-------------------------- //---------------------------------------------- MRESULT EXPENTRY TracerLogFileDlg( HWND hWndDlg, USHORT message, MPARAM mp1, MPARAM mp2 ) { switch( message ) { case WM_INITDLG: // options controls init WinSendDlgItemMsg( hWndDlg, ID_LOGFILESCROLL, BM_SETCHECK, MPFROM2SHORT( (bUserRequestsLog) ? 1 : 0, 0), 0L ); WinSendDlgItemMsg( hWndDlg, ID_SCREENSCROLL, BM_SETCHECK, MPFROM2SHORT( (bUserRequestsScroll) ? 1 : 0, 0), 0L ); case WM_COMMAND: switch( SHORT1FROMMP( mp1 ) ) { case ID_CANCEL: WinDismissDlg( hWndDlg, TRUE ); break; case ID_OK: // if user wishes to truncate log file, do it if ( bUserRequestsNewLog = (SHORT)WinSendDlgItemMsg( hWndDlg, ID_SCREENSCROLL, BM_QUERYCHECK, 0L, 0L ) ) DosNewSize( pLogFile, 0L ); // does user wish to see messages on screen ? bUserRequestsScroll = (SHORT)WinSendDlgItemMsg( hWndDlg, ID_SCREENSCROLL, BM_QUERYCHECK, 0L, 0L ); // does user wish to log messages to file ? bUserRequestsLog = (SHORT)WinSendDlgItemMsg( hWndDlg, ID_LOGFILESCROLL, BM_QUERYCHECK, 0L, 0L ); WinDismissDlg( hWndDlg, TRUE ); break; default: return( FALSE ); } break; default: return( WinDefDlgProc( hWndDlg, message, mp1, mp2 ) ); } return( FALSE ); } TRACERP.C //----------------------------------------------- //-TRACERP.C Source code for TRACER paint routine //----------------------------------------------- #define INCL_PM #define INCL_WIN #define INCL_DOS #include <os2.h> #include <string.h> #include "tracer.h" //---------------------------------------------- //-Module declarations-------------------------- //---------------------------------------------- void FAR PASCAL ClearLB( HWND hLB ); static int iFirstTimeOnly; //---------------------------------------------- //-External references-------------------------- //---------------------------------------------- extern HWND hLB; extern HWND hParentWnd; extern int iNumberItems; //---------------------------------------------- //-TRACERPaint()-------------------------------- //---------------------------------------------- void FAR PASCAL TRACERPaint( HWND hWnd, USHORT msg, MPARAM mp1, MPARAM mp2 ) { HPS hPS; RECTL rRect; // create listbox on first paint only if ( ! iFirstTimeOnly ) { hLB = WinCreateWindow( hParentWnd, WC_LISTBOX, "", WS_VISIBLE | WS_SYNCPAINT, 4, 4, 5, 5, hParentWnd, HWND_TOP, ID_TRACERLB, NULL, 0 ); WinQueryWindowRect( hWnd, &rRect ); WinSetWindowPos ( hLB, HWND_TOP, 6, 4, (SHORT)(rRect.xRight - rRect.xLeft - 14), (SHORT)(rRect.yTop - rRect.yBottom - 4), SWP_SIZE | SWP_MOVE | SWP_SHOW ); ClearLB( hLB ); iFirstTimeOnly = 1; } // quick and dirty hPS = WinBeginPaint( hWnd, (HPS)NULL, (PWRECT)NULL ); GpiErase( hPS ); WinEndPaint( hPS ); } //---------------------------------------------- //-ClearLB()------------------------------------ //---------------------------------------------- void FAR PASCAL ClearLB( HWND hLB ) { iNumberItems = 0; WinSendMsg( hLB, LM_DELETEALL, (MPARAM)-1L, (MPARAM)0L ); WinSendMsg( hLB, LM_INSERTITEM, (MPARAM)-1L, (MPARAM)(PCH)"Begin..." ); WinSendMsg( hLB, LM_SELECTITEM, (MPARAM)iNumberItems++, (MPARAM)TRUE ); } Figure 3 CLIENT #------------------------------------------------------------------- # # Standard command line definitions # #------------------------------------------------------------------- cp=cl -c -W3 -Alfw -G2sw -Os -Zpei #------------------------------------------------------------------- # # Default inference rules # #------------------------------------------------------------------- .c.obj: $(cp) $*.c .asm.obj: masm $*.asm; .rc.res: rc -r $*.rc #------------------------------------------------------------------- # # Dependencies # #------------------------------------------------------------------- client.obj: client.c client.h client.res: client.rc client.ico client.h client.exe: client.obj client.res client.lnk client.def link @client.lnk rc client.res CLIENT.C //------------------------ //-CLIENT.C------ //------------------------ #define INCL_PM #define INCL_WIN #define INCL_DOS #include <os2.h> #include <stdio.h> #include <string.h> #include "client.h" // TRACER step 1 - include tracer.h #include "tracer.h" // TRACER step 2 - declare TRACER VARIABLES TRACERVARIABLES //---------------------------------------------- //-main()-------------------------------- //---------------------------------------------- int cdecl main( ) { QMSG qmsg; ULONG ctldata; hAB = WinInitialize( NULL ); hmqClient = WinCreateMsgQueue( hAB, 0 ); if ( !WinRegisterClass( hAB, (PCH)szPClass, (PFNWP)ClientWndProc, CS_SIZEREDRAW, 0) ) return( 0 ); ctldata = FCF_TITLEBAR | FCF_SYSMENU | FCF_SIZEBORDER | FCF_MINMAX | FCF_ICON | FCF_SHELLPOSITION | FCF_TASKLIST ; hPanelFrm = WinCreateStdWindow( HWND_DESKTOP, WS_DISABLED, &ctldata, (PCH)szPClass, NULL, 0L, (HMODULE)NULL, CLIENTICON, (HWND FAR *)&hPanelWnd ); WinEnableWindow( hPanelFrm, TRUE ); WinShowWindow( hPanelFrm, TRUE ); while( WinGetMsg( hAB, (PQMSG)&qmsg, (HWND)NULL, 0, 0 ) ) WinDispatchMsg( hAB, (PQMSG)&qmsg ); WinDestroyWindow( hPanelFrm ); WinDestroyMsgQueue( hmqClient ); WinTerminate( hAB ); } //---------------------------------------------- //-ClientWndProc()-------------- //---------------------------------------------- MRESULT EXPENTRY ClientWndProc( HWND hWnd, USHORT msg, MPARAM mp1, MPARAM mp2 ) { switch (msg) { case WM_CREATE: // TRACER step 3 - attach to TRACER TRACERHELLO return( WinDefWindowProc( hWnd, msg, mp1, mp2 ) ); break; // TRACER step 4 - obtain system resources - Don't forget break; TRACERATTACH break; case WM_CLOSE: // TRACER step 5 TRACER("Posting QUIT"); WinPostMsg( hWnd, WM_QUIT, 0L, 0L ); break; case WM_SETFOCUS: // TRACER step 5 TRACER("Setting Focus"); return( WinDefWindowProc( hWnd, msg, mp1, mp2 ) ); break; case WM_ENABLE: return( WinDefWindowProc( hWnd, msg, mp1, mp2 ) ); break; case WM_BUTTON1DOWN: sprintf( szTraceBuf, "WM_BUTTON1DOWN hit at %ld", WinGetCurrentTime( hAB ) ); TRACER( szTraceBuf ); break; case WM_PAINT: ClientPaint( hWnd, msg, mp1, mp2 ); break; case WM_ERASEBACKGROUND: return( TRUE ); break; default: return( WinDefWindowProc( hWnd, msg, mp1, mp2 ) ); break; } return(0L); } //---------------------------------------------- //-ClientPaint()------------------ //---------------------------------------------- void FAR PASCAL ClientPaint( HWND hWnd, USHORT msg, MPARAM mp1, MPARAM mp2 ) { int aiCharWidth; HPS hPS; FONTMETRICS fm; RECTL rRect; CHARBUNDLE cb; POINTL pt; hPS = WinBeginPaint( hWnd, (HPS)NULL, (PWRECT)NULL ); // TRACER step 5 TRACER("ClientPaint()"); GpiErase( hPS ); GpiQueryFontMetrics( hPS, (LONG)sizeof fm , &fm); aiCharWidth = (SHORT)fm.lAveCharWidth; WinQueryWindowRect( hWnd, &rRect ); pt.x = (rRect.xRight / 2) - ((strlen(szMessage) / 2) * aiCharWidth); pt.y = (rRect.yTop / 2); cb.lColor = CLR_BLACK; GpiSetAttrs( hPS, PRIM_CHAR, CBB_COLOR, 0L, (PBUNDLE)&cb ); GpiCharStringAt( hPS, &pt, (LONG)strlen( szMessage ), szMessage ); WinEndPaint( hPS ); } CLIENT.DEF NAME CLIENT WINDOWAPI DESCRIPTION 'Client to TRACER' STUB 'OS2STUB.EXE' CODE MOVEABLE DATA MOVEABLE MULTIPLE HEAPSIZE 1024 STACKSIZE 4096 EXPORTS ClientWndProc @1 CLIENT.H #define CLIENTICON 1 extern void FAR PASCAL ClientPaint( HWND hWnd, USHORT msg, MPARAM mp1, MPARAM mp2 ); MRESULT EXPENTRY ClientWndProc( HWND, USHORT, MPARAM, MPARAM ); int cdecl main( void ); char szMessage[] = " Client App "; char szPClass[] = "PClass"; HAB hAB; HMQ hmqClient; HWND hPanelWnd; HWND hPanelFrm; char szTraceBuf[81]; CLIENT.LNK client.obj client/align:16 client/map os2 llibcmt doscalls/NOD client.def CLIENT.RC #include "client.h" POINTER CLIENTICON client.ico CHECKERS for Presentation Manager Part III: Moving the Pieces Charles Petzold When asked to write a checkers program for the OS/2 Presentation Manager (hereafter "PM"), I started researching bulletin boards to see what other noncommercial checkers games were available. I found one that was several years old and had been programmed in BASIC. Although it displayed a checkerboard on the screen, the user interface was awkward and primitive. You had to type a number to indicate the piece you wanted to move, and another number for the destination square. I wanted my checkers program for PM to be just like a real game. My program's players can therefore move a piece using the mouse as a surrogate for their fingers. This makes the program difficult to write, but then nobody ever claimed that programming for an interactive, graphical environment was easy. In the last issue, I described how the checkerboard and pieces are depicted on the program's window. The next step is to add an interface to move the pieces using the mouse or the keyboard. Complete validation of moves and jumps, including logic to remove jumped pieces and logic to king those pieces reaching the opposite side of the board, has also been added. In fact, this version of CHECKERS lets you play an entire game, albeit only by yourself, by alternating the black and white pieces. (All the source code and the CHECKERS.EXE executable file may be downloaded from any MSJ bulletin board--Ed.) Mouse Pointers A CHECKERS game in progress is shown in Figure 1. On the right side of the board, a customized mouse pointer in the shape of a hand indicates which player's turn it is to move. A hand with fingers pointing up means that it's black's turn. After a black move, the pointer changes to a hand with fingers pointing down, indicating a white move. Originally I thought that the mouse pointer should resemble a checkers piece when the user selects a piece to move. But the size of a PM mouse pointer is fixed, based on the resolution of the video display--only by coincidence would this pointer be the same size as the pieces shown on the board. Instead, I decided to store the playing pieces as bitmaps. When a piece is selected with the mouse, the program hides the normal mouse pointer and moves the piece bitmap around the window based on the position of the mouse. This complicates things considerably (in effect, I have to duplicate mouse pointer drawing logic), but it looks much nicer (see Figure 2). Experienced users of the Windows and PM environments who have seen CHECKERS are unhappy about the way the mouse buttons move a piece on the board. They expect to drag a piece from one square to another while the mouse button is depressed. Releasing the mouse button would set the piece down on the new square. But that's not the way this version of CHECKERS works. You must click (press and release) the mouse button to pick up the piece, and click again to set it down. That may seem like an insignificant difference, but it's different from dragging operations in other programs. Dragging wasn't used because it causes problems with multiple jumps. According to the rules of checkers, if a player jumps a piece and it is possible to jump another piece, the move must continue. Using dragging, the player would release the mouse button to indicate the destination square of the first jump, but for the second jump, the player would still have control of the piece without any button depressed. To avoid this problem, CHECKERS requires you to press and release the mouse button to pick up a piece, and press and release the button again to set it down. The program indicates that there are more moves to be made by not changing the mouse pointer back to one of the hands; it's as if the program will not let you put down the piece until you've finished the move. A Little Restructuring In the last installment, I had two C source code files: CHECKERS.C contained the main function, the client window procedure, and a dialog box procedure for the "About" box. CKRDRAW.C contained all the drawing functions, including a dialog box for changing the colors. I've added three files and two additional window procedures to the new version. Their relationship is shown in Figure 3. The three window procedures communicate with each other through user-defined messages found in CHECKERS.H. JudgeWndProc in CKRJUDGE.C calls functions in CKRMOVES.C to determine valid moves and jumps. BoardWndProc in CKRBOARD.C handles user input and calls functions in CKRDRAW.C in order to draw the board. Although there are a number of global variables in CKRDRAW.C, only hab, the infamous anchor block handle, is global to more than one source code file. The anchor block handle is obtained by a call to WinInitialize in main and used as a parameter to several other PM functions in other modules. CHECKERS.C and the associated files for compilation are shown in Figure 4. The ClientWndProc window procedure processes menu commands and creates JudgeWndProc and BoardWndProc. A new menu command, New Game, causes everything to be reset and the board to be redrawn for starting a new game. In future versions of the program, ClientWndProc will display a dialog box when you select this option. This dialog box will let you select the type of game you want to play, such as a game against the program itself or against another player across a network. ClientWndProc will then instruct JudgeWndProc to begin the game. CKRJUDGE.C is shown in Figure 5. JudgeWndProc is the window procedure for an object window (that is, a child of HWND_OBJECT). Object windows do not appear on the screen and do not process user input, but they can send and receive messages just like normal windows. JudgeWndProc controls the game by maintaining a description of the current board layout in a structure of type BOARD (defined in CHECKERS.H), by telling a player when to make a move, and by determining whether a move is valid. In future versions of the program, the Judge will maintain a log of the game and communicate with modules that implement other parts of the CHECKERS program, particularly the checkers-playing strategy and the network communications. CKRBOARD.C is shown in Figure 6. BoardWndProc is the window procedure for a child of the client window. This window is set to the same size as the client window and completely covers it. BoardWndProc communicates with the user by drawing the board and processing keyboard and mouse input. In a future version of the program, BoardWndProc will also move a piece, on command from the Judge, to make a move chosen by the checkers-playing strategy. Hit-Testing With GPI When the user clicks the mouse button on the CHECKERS window, the program must determine which board square (if any) the mouse pointer is positioned over. This process is called hit-testing, and is one of the many techniques you must learn to program for a graphical interface such as PM. For more information on hit-testing, see chapter 9 of Programming the OS/2 Presentation Manager (Microsoft Press, 1989). It's easy to hit-test on a square grid like the checkerboard. But I had to make things hard for myself. To give the board a three-dimensional effect, I chose to draw the checkerboard as a grid of trapezoids, not squares. Although determining whether a point falls within a particular trapezoid is not particularly difficult, I decided to let the graphics programming interface (GPI) do the hit-testing for me. The built-in hit-testing facility in GPI is not well known, probably because there is no single call that does this function. Indeed, the actual hit-testing is performed by calling the same functions you call to draw graphics on the window. When you click on the board, BoardWndProc calls the CkdQueryHitCoords function in CKRDRAW.C (see Figure 7). This function calls GpiSetPickApertureSize and GpiSetPickAperturePosition to begin hit-testing. The pick aperture position is the point you want to test (in this case, the mouse pointer position). In some cases, you may want the hit-test to have some tolerance and not require the user to hit the graphic directly. This tolerance is the pick aperture size. Calling GpiSetDrawControl with the DCTL_DISPLAY and DCTL_OFF parameters sets the graphics display off. This prevents any subsequently called drawing functions from drawing on the window. This is an unusual feature in a graphics programming language, but a useful one for the next step. To turn on hit-testing, GpiSetDrawControl is called with the DCTL_CORRELATE and DCTL_ON parameters. Any graphics drawing functions subsequently called return a special value (GPI_HITS, defined in PMGPI.H as 2L) if any part of the graphic is within the pick aperture size of the pick aperture position. After these preliminaries, CkrQueryHitCoords calls CkdDrawAllBoardSquares (see Figure 8). This function calls CkdDrawBoardSquare 64 times to draw the checkerboard squares. CkdDrawBoardSquare uses an area bracket (GpiBeginArea and GpiEndArea) to draw each square. The function returns the return value of GpiEndArea, which can be GPI_HITS if the pick aperture position is within the area being drawn. When CkdDrawAllBoardSquares detects a return value of GPI_HITS, it combines the x and y coordinates (each of which can range from 0 to 7) in a LONG and returns it from the function. Although GPI's built-in hit-testing simplifies programming a great deal, there's one problem with it in the CHECKERS program--it's just too slow. Try clicking on a white square near the top of the board. It takes a while before CHECKERS responds with an error beep. GPI is obviously structured to do a generalized hit-test based on a scan-line conversion of an enclosed area. A very specific trapezoid hit-test should be much faster. I'll leave the program as it is for now; it does demonstrate the useful GPI hit-testing facility. Perhaps I'll sneak in a more efficient routine in a later version. This version of CHECKERS also has a keyboard interface. You can move the mouse pointer using the cursor arrow keys; the Home, End, Page Up, and Page Down keys move the mouse pointer to the four corners of the checkerboard. Pressing the space bar simulates a mouse click. The keyboard interface was fairly easy to implement because I designed it to emulate the mouse interface. During the WM_CHAR message in BoardWndProc (contained in CKRBOARD.C), the space bar is converted into a WM_BUTTON1UP message. The cursor keys are processed by a call to WinSetPointerPos, which results in a WM_MOUSEMOVE message being sent to the window procedure. Move Validation When BoardWndProc gets a mouse click, it must determine if the move the user is attempting to make is valid. This validation has several stages. Any invalid move is signaled by an error beep. First, if CkdDrawAllBoardSquares returns -1, the mouse pointer is not positioned over one of the squares of the board. That's the easy one. Second, BoardWndProc passes the x and y coordinates to the CkdConvertCoordsToIndex function in CKRDRAW.C to convert them into an index ranging from 0 through 31 (see Figure 9). If CkdConvertCoordsToIndex returns -1, then the coordinates indicate an illegal white square--no piece can ever be located on this square. The third step occurs when BoardWndProc sends a WM_QUERY_JUDGE_PICKUP_PIECE message to JudgeWndProc. JudgeWndProc returns TRUE or FALSE indicating whether or not the piece can be picked up for a move or a jump. JudgeWndProc uses several functions in CKRMOVES.C (see Figure 10) to determine valid moves. These look more like the blackboard scribbles of a demented Boolean algebraist than normal C functions, though! A group of identifiers near the top of CKRMOVES.C make the Boolean expressions in the various functions somewhat readable, even if incomprehensible at first. (To review the method used to maintain the state of the board, see "Representing the Board"--Ed.) The CkmQueryAllMoveablePieces function uses the valnojump (valid no jump) structure in CKRMOVES.C and the board layout to determine all pieces that can move without jumping. CkmQueryAllJumpablePieces similarly determines all pieces that can make jumps using the valjumps (valid jumps) structure. This structure has two increments to indicate the piece being jumped and the destination of the piece doing the jumping. The piece being jumped must be the opposite color and the destination must be empty. JudgeWndProc calls both CkmQueryAllMoveablePieces and CkmQueryAllJumpablePieces. If the user is selecting a piece that cannot make a jump, and a jump is available, JudgeWndProc returns FALSE. (Remember, CHECKERS enforces the rule that jumps must be taken.) Likewise, BoardWndProc sends messages to JudgeWndProc to determine if a destination for a piece is valid, and to determine whether additional jumps are available. One enhancement I am considering is for BoardWndProc and JudgeWndProc to send messages to ClientWndProc when the user attempts to make an illegal move. ClientWndProc would display a message on the window such as "You can't move that piece. You have a jump available." Right now, all you get is a beep. This is sometimes confusing. When I hear a beep, I usually have to scan the board again to find the jump I've conveniently missed. Moving the Pieces I discussed in the last issue how CKRDRAW.C creates several bitmaps (stored in the arrays ahbmPiece and ahbmMask) to draw the pieces on the checkerboard. The ahbmMask bitmaps have a white background and black foreground. There is one bitmap for a kinged piece and one for an unkinged piece. These are drawn by first using GpiBitBlt with a raster operation of ROP_SRCAND, which performs a bitwise AND operation between the board and the bitmap, leaving a black hole where the piece would be. The ahbmPiece bitmaps have an image of the piece with a black background. Four of these bitmaps are required: one black kinged, one black unkinged, one white kinged, and one white unkinged. GpiBitBlt draws the piece with a raster operation of ROP_SRCPAINT (a bitwise OR operation). In this version, CKRDRAW.C creates two more sets of bitmaps, ahbmSave and ahbmMove. The ahbmSave bitmaps (for kinged and unkinged pieces) are the same size as the ahbmPiece and ahbmMask bitmaps. The ahbmMove bitmaps are twice the width and height of the other bitmaps. These bitmaps are used for moving a piece across the window when the user picks it up. Interactively moving a bitmap across the screen involves some fairly standard techniques. If the piece begins its existence at point A, there are two steps involved. 1. Save the screen area at point A in ahbmSave. 2. Draw the piece on the screen at point A using ahbmMask and ahbmPiece. Now the user moves the mouse from point A to point B, meaning that the piece must be erased from point A and drawn at point B. This is accomplished by the following three steps: 1. Restore the area at point A from ahbmSave. This effectively erases the piece. 2. Save the area at point B in ahbmSave. 3. Draw the piece at point B using ahbmMask and ahbmPiece. This continues until the user deposits the piece on a square. The three functions in CKRDRAW.C that perform these operations are shown in Figure 11. Although this works all right, I was unsatisfied. Moving a piece with the mouse resulted in an annoying flicker caused by the delay between erasing the piece from one point and drawing at another point. I hate when something like this happens--the only thing worse than buggy code is functional code that performs disappointingly. I wanted to see smooth, flickerless piece movement and I wasn't getting it. Finally, I decided to make the extra effort of writing the CkdDragMove function (see Figure 12). If points A and B are far enough away so there is no overlap of the old piece at A and the new piece at B, this function performs the three steps listed above. Otherwise, it does a more complex operation: 1. Copy the screen area encompassing points A and B to the ahbmMove bitmap. (Remember that this bitmap is twice the width and height of the pieces.) 2. Restore the area corresponding to point A on the ahbmMove bitmap from the ahbmSave bitmap. This erases the image of the piece from ahbmMove. 3. Save the area corresponding to point B on the ahbmMove bitmap to the ahbmSave bitmap. 4. Draw the piece at point B on the ahbmMove bitmap. 5. Copy the ahbmMove bitmap to the screen. The results (after getting the function to work properly) were impressive. The piece moves smoothly and without flicker because the updating of the image is done offscreen. Nothing is drawn on the window until step 5, when the new image replaces the old one. This is the part of the program I am proudest of, the part that most users will take for granted. Few people are going to say "Wow! No flicker!" But I'm glad I made the change. It makes a big difference. The Next Installment CHECKERS has made some considerable progress, but playing a game by yourself gets boring quickly. In the next installment, CHECKERS will include game-playing logic so you will be able to play a game against the program. I will also include a specification for a dynamic-link library interface, so you will be able to write your own checkers-playing logic. Figure 4 CHECKERS #---------------------------------- # CHECKERS make file, Version 0.30 #---------------------------------- CC = cl -c -G2sw -W3 checkers.obj : checkers.c checkers.h $(CC) checkers.c ckrjudge.obj : ckrjudge.c checkers.h ckrmoves.h $(CC) ckrjudge.c ckrboard.obj : ckrboard.c checkers.h ckrdraw.h $(CC) ckrboard.c ckrmoves.obj : ckrmoves.c checkers.h ckrmoves.h $(CC) ckrmoves.c ckrdraw.obj : ckrdraw.c checkers.h ckrdraw.h $(CC) ckrdraw.c checkers.res : checkers.rc checkers.h ckruhand.ptr ckrdhand.ptr rc -r checkers checkers.exe : checkers.obj ckrjudge.obj ckrboard.obj \ ckrmoves.obj ckrdraw.obj checkers.def link @checkers.lnk rc checkers.res checkers.exe checkers.exe : checkers.res rc checkers.res CHECKERS.RC /*------------------------------------------- CHECKERS.RC resource script, Version 0.30 -------------------------------------------*/ #include <os2.h> #include "checkers.h" POINTER IDP_UPHAND ckruhand.ptr POINTER IDP_DNHAND ckrdhand.ptr MENU ID_RESOURCE { SUBMENU "~Game", -1 { MENUITEM "~New Game", IDM_NEWGAME MENUITEM SEPARATOR MENUITEM "~Black on Bottom", IDM_BOTTOM,, MIA_CHECKED MENUITEM SEPARATOR MENUITEM "~About Checkers...", IDM_ABOUT } SUBMENU "~Colors", -1 { MENUITEM "Wi~ndow Background...", IDM_COLOR_BACKGROUND MENUITEM "~Black Square...", IDM_COLOR_BLACK_SQUARE MENUITEM "~White Square...", IDM_COLOR_WHITE_SQUARE MENUITEM "B~lack Piece...", IDM_COLOR_BLACK_PIECE MENUITEM "W~hite Piece...", IDM_COLOR_WHITE_PIECE MENUITEM SEPARATOR MENUITEM "~Standard colors", IDM_COLOR_STANDARD } } #define GRP WS_GROUP DLGTEMPLATE IDD_ABOUT_DLG { DIALOG "", 0, 32, 32, 200, 100,, FCF_DLGBORDER { CTEXT "Checkers Version 0.30" -1, 10, 76, 180, 8 CTEXT "(A Game With Yourself)" -1, 10 62, 180, 8 CTEXT "Microsoft Systems Journal, 3/90" -1, 10, 48, 180, 8 CTEXT "(c) 1990, Charles Petzold" -1, 10, 34, 180, 8 DEFPUSHBUTTON "OK" DID_OK, 80, 8, 40, 16, GRP } } DLGTEMPLATE IDD_COLOR_DLG { DIALOG "", 0, 32, 32, 180, 180,, FCF_DLGBORDER { CTEXT "", IDD_HEADING, 10, 166, 160, 8 GROUPBOX "Color" -1, 16, 32, 148, 130 RADIOBUTTON "Black" IDD_COLOR + CLR_BLACK, 20, 136, 64, 12, GRP RADIOBUTTON "Blue" IDD_COLOR + CLR_BLUE, 20, 122, 64, 12 RADIOBUTTON "Red" IDD_COLOR + CLR_RED, 20, 108, 64, 12 RADIOBUTTON "Pink" IDD_COLOR + CLR_PINK, 20, 94, 64, 12 RADIOBUTTON "Green" IDD_COLOR + CLR_GREEN, 20, 80, 64, 12 RADIOBUTTON "Cyan" IDD_COLOR + CLR_CYAN, 20, 66, 64, 12 RADIOBUTTON "Yellow" IDD_COLOR + CLR_YELLOW, 20, 52, 64, 12 RADIOBUTTON "Pale Gray" IDD_COLOR + CLR_PALEGRAY, 94, 38, 64, 12 RADIOBUTTON "Dark Gray" IDD_COLOR + CLR_DARKGRAY, 94, 136, 64, 12 RADIOBUTTON "Dark Blue" IDD_COLOR + CLR_DARKBLUE, 94, 122, 64, 12 RADIOBUTTON "Dark Red" IDD_COLOR + CLR_DARKRED, 94, 108, 64, 12 RADIOBUTTON "Dark Pink" IDD_COLOR + CLR_DARKPINK, 94, 94, 64, 12 RADIOBUTTON "Dark Green" IDD_COLOR + CLR_DARKGREEN, 94, 80, 64, 12 RADIOBUTTON "Dark Cyan" IDD_COLOR + CLR_DARKCYAN, 94, 66, 64, 12 RADIOBUTTON "Brown" IDD_COLOR + CLR_BROWN, 94, 52, 64, 12 RADIOBUTTON "White" IDD_COLOR + CLR_WHITE, 20, 38, 64, 12 DEFPUSHBUTTON "OK" DID_OK, 16, 8, 52, 16, GRP PUSHBUTTON "Cancel" DID_CANCEL, 112, 8, 52, 16 } } CHECKERS.DEF ;--------------------------------------------------- ; CHECKERS.DEF module definition file, Version 0.30 ;--------------------------------------------------- NAME CHECKERS WINDOWAPI DESCRIPTION 'Checkers Version 0.30 (c) 1990, Charles Petzold' PROTMODE HEAPSIZE 1024 STACKSIZE 8192 EXPORTS ClientWndProc JudgeWndProc BoardWndProc AboutDlgProc ColorDlgProc CHECKERS.LNK checkers ckrjudge ckrboard ckrmoves ckrdraw /align:16 NUL os2 checkers ---------------------------------------- CHECKERS.LNK link script, Version 0.30 ---------------------------------------- CHECKERS.H /*-------------------------------------- CHECKERS.H header file, Version 0.30 --------------------------------------*/ // Constants and structures #define BLACK 0 #define WHITE 1 #define NORM 0 #define KING 1 typedef struct { ULONG ulBlack ; ULONG ulWhite ; ULONG ulKing ; } BOARD ; typedef BOARD FAR *PBOARD ; // Menu ID #define ID_RESOURCE 1 // Pointer ID's #define IDP_UPHAND 2 #define IDP_DNHAND 3 // Menu ID's #define IDM_NEWGAME 1 #define IDM_BOTTOM 2 #define IDM_ABOUT 3 #define IDM_COLOR_BACKGROUND 11 #define IDM_COLOR_BLACK_SQUARE 12 #define IDM_COLOR_WHITE_SQUARE 13 #define IDM_COLOR_BLACK_PIECE 14 #define IDM_COLOR_WHITE_PIECE 15 #define IDM_COLOR_STANDARD 16 // Dialog Box ID's #define IDD_ABOUT_DLG 1 #define IDD_COLOR_DLG 2 #define IDD_HEADING 10 #define IDD_COLOR 20 // User-Defined Messages // Messages from ClientWndProc to JudgeWndProc #define WM_TELL_JUDGE_BOARD_HANDLE (WM_USER + 1) #define WM_TELL_JUDGE_NEW_GAME (WM_USER + 2) // Messages from ClientWndProc to BoardWndProc #define WM_TELL_BOARD_JUDGE_HANDLE (WM_USER + 10) #define WM_TELL_BOARD_NEW_ORIENTATION (WM_USER + 11) #define WM_TELL_BOARD_COLOR_DIALOG (WM_USER + 12) #define WM_TELL_BOARD_STANDARD_COLORS (WM_USER + 13) // Messages from Judge to Board #define WM_JUDGE_SAYS_RESET_BOARD (WM_USER + 20) #define WM_JUDGE_SAYS_MOVE_BLACK (WM_USER + 21) #define WM_JUDGE_SAYS_MOVE_WHITE (WM_USER + 22) #define WM_JUDGE_SAYS_REMOVE_PIECE (WM_USER + 23) // Messages from Board to Judge #define WM_QUERY_JUDGE_CURRENT_BOARD (WM_USER + 30) #define WM_QUERY_JUDGE_PICKUP_PIECE (WM_USER + 31) #define WM_QUERY_JUDGE_IF_KING (WM_USER + 32) #define WM_QUERY_JUDGE_PUTDOWN_PIECE (WM_USER + 33) #define WM_QUERY_JUDGE_CONTINUE_MOVE (WM_USER + 34) #define WM_TELL_JUDGE_BOARD_MOVE_ENDED (WM_USER + 35) CKRMOVES.H /*------------------------------------- CKRMOVES.H header file, Version 0.30 -------------------------------------*/ ULONG CkmQueryAllMoveablePieces (BOARD *pbrd, SHORT sColor) ; ULONG CkmQueryAllJumpablePieces (BOARD *pbrd, SHORT sColor) ; ULONG CkmQueryMoveDestinations (BOARD *pbrd, SHORT sColor, ULONG ulPiece) ; ULONG CkmQueryJumpDestinations (BOARD *pbrd, SHORT sColor, ULONG ulPiece) ; SHORT CkmQueryJumpedPiece (SHORT sBeg, SHORT sEnd) ; CKRDRAW.H /*------------------------------------ CKRDRAW.H header file, Version 0.30 ------------------------------------*/ MRESULT EXPENTRY ColorDlgProc (HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2) ; HPS CkdCreatePS (HWND hwnd) ; VOID CkdResizePS (HPS hps, HWND hwnd) ; BOOL CkdDestroyPS (HPS hps) ; VOID CkdSetStandardColors (VOID) ; VOID CkdCreatePieces (HPS hps) ; VOID CkdDestroyPieces (VOID) ; VOID CkdDrawWindowBackground (HPS hps, HWND hwnd) ; VOID CkdDrawWholeBoard (HPS hps) ; VOID CkdDrawAllPieces (HPS hps, BOARD *pbrd, SHORT sBottom) ; VOID CkdErasePiece (HPS hps, SHORT x, SHORT y) ; VOID CkdQueryHitCoords (HPS hps, POINTL ptlMouse, SHORT *px, SHORT *py) ; SHORT CkdConvertCoordsToIndex (SHORT x, SHORT y, SHORT sBottom) ; VOID CkdConvertIndexToCoords (SHORT i, SHORT *px, SHORT *py, SHORT sBottom) ; VOID CkdDragSave (HPS hps, POINTL *pptlMouse, SHORT sKing) ; VOID CkdDragRestore (HPS hps, POINTL *pptlMouse, SHORT sKing) ; VOID CkdDragShow (HPS hps, POINTL *pptlMouse, SHORT sColor, SHORT sKing) ; VOID CkdDragMove (HPS hps, POINTL *pptlFrom, POINTL *pptlTo, SHORT sColor, SHORT sKing) ; VOID CkdDragDeposit (HPS hps, SHORT x, SHORT y, SHORT sColor, SHORT sKing) ; VOID CkdQueryNearestXYFromPoint (HPS hps, POINTL *pptlMouse, SHORT *px, SHORT *py) ; VOID CkdQuerySlightOffsetFromXY (HPS hps, SHORT x, SHORT y, POINTL *pptl) ; CHECKERS.C /*-------------------------------------------------------------------- CHECKERS.C -- OS/2 Presentation Manager Checkers Program, Version 0.30 (c) 1990, Charles Petzold -------------------------------------------------------------------*/ #define INCL_WIN #include <os2.h> #include "checkers.h" MRESULT EXPENTRY ClientWndProc (HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2); MRESULT EXPENTRY JudgeWndProc (HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2); MRESULT EXPENTRY BoardWndProc (HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2); MRESULT EXPENTRY AboutDlgProc (HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2); HAB hab ; int main (void) { static CHAR szClientClass[] = "Checkers" ; static ULONG flFrameFlags = FCF_TITLEBAR | FCF_SYSMENU | FCF_SIZEBORDER | FCF_MINMAX | FCF_SHELLPOSITION | FCF_TASKLIST | FCF_MENU ; HMQ hmq ; HWND hwndFrame, hwndClient ; QMSG qmsg ; hab = WinInitialize (0) ; hmq = WinCreateMsgQueue (hab, 0) ; WinRegisterClass (hab, szClientClass, ClientWndProc, CS_SIZEREDRAW, 0) ; hwndFrame = WinCreateStdWindow (HWND_DESKTOP, WS_VISIBLE, &flFrameFlags, szClientClass, NULL, 0L, NULL, ID_RESOURCE, &hwndClient) ; while (WinGetMsg (hab, &qmsg, NULL, 0, 0)) WinDispatchMsg (hab, &qmsg) ; WinDestroyWindow (hwndFrame) ; WinDestroyMsgQueue (hmq) ; WinTerminate (hab) ; return 0 ; } MRESULT EXPENTRY ClientWndProc (HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2) { static CHAR szJudgeClass[] = "Checkers.Judge", szBoardClass[] = "Checkers.Board" ; static HWND hwndJudge, hwndBoard, hwndMenu ; static SHORT sBottom = BLACK ; switch (msg) { case WM_CREATE: // Register Judge class & create window WinRegisterClass (hab, szJudgeClass, JudgeWndProc, CS_SIZEREDRAW, 0) ; hwndJudge = WinCreateWindow (HWND_OBJECT, szJudgeClass, NULL, 0L, 0, 0, 0, 0, hwnd, HWND_BOTTOM, 1, NULL, NULL) ; // Register Board class & create window WinRegisterClass (hab, szBoardClass, BoardWndProc, CS_SIZEREDRAW, 0) ; hwndBoard = WinCreateWindow (hwnd, szBoardClass, NULL, WS_VISIBLE, 0, 0, 0, 0, hwnd, HWND_BOTTOM, 2, NULL, NULL) ; // Inform windows of each other's handles WinSendMsg (hwndJudge, WM_TELL_JUDGE_BOARD_HANDLE, MPFROMHWND (hwndBoard), NULL) ; WinSendMsg (hwndBoard, WM_TELL_BOARD_JUDGE_HANDLE, MPFROMHWND (hwndJudge), NULL) ; // Begin a new game WinSendMsg (hwndJudge, WM_TELL_JUDGE_NEW_GAME, NULL, NULL) ; // Obtain handle of menu window hwndMenu = WinWindowFromID ( WinQueryWindow (hwnd, QW_PARENT, FALSE), FID_MENU) ; return 0 ; case WM_SIZE: // Resize Board window WinSetWindowPos (hwndBoard, NULL, 0, 0, SHORT1FROMMP (mp2), SHORT2FROMMP (mp2), SWP_MOVE | SWP_SIZE) ; return 0 ; case WM_CHAR: // Send keystrokes to Board window return WinSendMsg (hwndBoard, WM_CHAR, mp1, mp2) ; case WM_COMMAND: // Process menu commands switch (COMMANDMSG (&msg)->cmd) { case IDM_NEWGAME: WinSendMsg (hwndJudge, WM_TELL_JUDGE_NEW_GAME, NULL, NULL) ; return 0 ; case IDM_BOTTOM: WinSendMsg (hwndMenu, MM_SETITEMATTR, MPFROM2SHORT (IDM_BOTTOM, TRUE), MPFROM2SHORT (MIA_CHECKED, sBottom ? MIA_CHECKED : 0)) ; sBottom ^= 1 ; WinSendMsg (hwndBoard, WM_TELL_BOARD_NEW_ORIENTATION, MPFROMSHORT (sBottom), NULL) ; return 0 ; case IDM_ABOUT: WinDlgBox (HWND_DESKTOP, hwnd, AboutDlgProc, NULL, IDD_ABOUT_DLG, NULL) ; return 0 ; case IDM_COLOR_BACKGROUND: case IDM_COLOR_BLACK_SQUARE: case IDM_COLOR_WHITE_SQUARE: WinSendMsg (hwndBoard, WM_TELL_BOARD_COLOR_DIALOG, MPFROMP (&COMMANDMSG (&msg)->cmd), MPFROMSHORT (FALSE)) ; return 0 ; case IDM_COLOR_BLACK_PIECE: case IDM_COLOR_WHITE_PIECE: WinSendMsg (hwndBoard, WM_TELL_BOARD_COLOR_DIALOG, MPFROMP (&COMMANDMSG (&msg)->cmd), MPFROMSHORT (TRUE)) ; return 0 ; return 0 ; case IDM_COLOR_STANDARD: WinSendMsg (hwndBoard, WM_TELL_BOARD_STANDARD_COLORS, NULL, NULL) ; return 0 ; } break ; } return WinDefWindowProc (hwnd, msg, mp1, mp2) ; } MRESULT EXPENTRY AboutDlgProc (HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2) { switch (msg) { case WM_COMMAND: switch (COMMANDMSG(&msg)->cmd) { case DID_OK: case DID_CANCEL: WinDismissDlg (hwnd, TRUE) ; return 0 ; } } return WinDefDlgProc (hwnd, msg, mp1, mp2) ; } Figure 5 /*--------------------------------------------------------------- CKRJUDGE.C -- JudgeWndProc for controlling game, Version 0.30 (c) 1990, Charles Petzold ---------------------------------------------------------------*/ #include <os2.h> #include "checkers.h" #include "ckrmoves.h" MRESULT EXPENTRY JudgeWndProc (HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2) { static BOOL fKing, fNewKing, fMustJump ; static BOARD brd = { 0x00000FFF, 0xFFF00000, 0x00000000 }, brdLast ; static HWND hwndBoard ; static SHORT sColor = BLACK, iBegin, iEnd, iJump ; ULONG ulBit, ulMove, ulJump ; switch (msg) { case WM_TELL_JUDGE_BOARD_HANDLE: hwndBoard = HWNDFROMMP (mp1) ; return 0 ; case WM_TELL_JUDGE_NEW_GAME: brd.ulBlack = 0x00000FFF ; brd.ulWhite = 0xFFF00000 ; brd.ulKing = 0x00000000 ; sColor = BLACK ; WinSendMsg (hwndBoard, WM_JUDGE_SAYS_RESET_BOARD, NULL, NULL) ; WinSendMsg (hwndBoard, WM_JUDGE_SAYS_MOVE_BLACK, NULL, NULL) ; return 0 ; case WM_QUERY_JUDGE_CURRENT_BOARD: * (PBOARD) PVOIDFROMMP(mp1) = brd ; return 0 ; case WM_QUERY_JUDGE_PICKUP_PIECE: iBegin = SHORT1FROMMP (mp1) ; ulBit = 1L << iBegin ; fKing = brd.ulKing & ulBit ? TRUE : FALSE ; fMustJump = FALSE ; fNewKing = FALSE ; ulMove = CkmQueryAllMoveablePieces (&brd, sColor) ; ulJump = CkmQueryAllJumpablePieces (&brd, sColor) ; if (ulJump != 0) // ie, some possible jumps { if (!(ulBit & ulJump)) return FALSE ; // invalid piece for jumping fMustJump = TRUE ; } else // no possible jumps { if (!(ulBit & ulMove)) // invalid piece for moving return FALSE ; } // save board and adjust it brdLast = brd ; if (sColor = = BLACK) brd.ulBlack &= ~ulBit ; else brd.ulWhite &= ~ulBit ; brd.ulKing &= ~ulBit ; return TRUE ; case WM_QUERY_JUDGE_IF_KING: return fKing ; case WM_QUERY_JUDGE_PUTDOWN_PIECE: ulBit = 1L << iBegin ; ulMove = CkmQueryMoveDestinations (&brdLast, sColor, ulBit) ; ulJump = CkmQueryJumpDestinations (&brdLast, sColor, ulBit) ; iEnd = SHORT1FROMMP (mp1) ; ulBit = 1L << iEnd ; if (fMustJump) { if (!(ulBit & ulJump)) /* not a valid jump destination */ return FALSE ; iJump = CkmQueryJumpedPiece (iBegin, iEnd) ; } else { if (!(ulBit & ulMove)) /* not a valid move destination */ return FALSE ; iJump = -1 ; } // adjust board brdLast = brd ; if (sColor = = BLACK) { brd.ulBlack |= ulBit ; if (iJump != -1) brd.ulWhite &= ~(1L << iJump) ; if (iEnd >= 28 || fKing = = TRUE) { brd.ulKing |= ulBit ; if (!fKing) fNewKing = TRUE ; } } else // (sColor = = WHITE) { brd.ulWhite |= ulBit ; if (iJump != -1) brd.ulBlack &= ~(1L << iJump) ; if (iEnd <= 3 || fKing = = TRUE) { brd.ulKing |= ulBit ; if (!fKing) fNewKing = TRUE ; } } if (fNewKing) fKing = TRUE ; // inform board of jumped piece if (iJump != -1) WinSendMsg (hwndBoard, WM_JUDGE_SAYS_REMOVE_PIECE, MPFROMSHORT (iJump), NULL) ; return TRUE ; case WM_QUERY_JUDGE_CONTINUE_MOVE: if (fNewKing) return FALSE ; if (!fMustJump) return FALSE ; iBegin = SHORT1FROMMP (mp1) ; ulBit = 1L << iBegin ; ulJump = CkmQueryAllJumpablePieces (&brd, sColor) ; if (ulBit & ulJump) { fMustJump = TRUE ; brdLast = brd ; if (sColor = = BLACK) brd.ulBlack &= ~ulBit ; else brd.ulWhite &= ~ulBit ; brd.ulKing &= ~ulBit ; return TRUE ; } return FALSE ; case WM_TELL_JUDGE_BOARD_MOVE_ENDED: if (sColor = = BLACK) { sColor = WHITE ; WinSendMsg (hwndBoard, WM_JUDGE_SAYS_MOVE_WHITE, NULL, NULL) ; } else { sColor = BLACK ; WinSendMsg (hwndBoard, WM_JUDGE_SAYS_MOVE_BLACK, NULL, NULL) ; } return 0 ; } return WinDefWindowProc (hwnd, msg, mp1, mp2) ; } Figure 6 /*--------------------------------------------------------------- CKRBOARD.C -- BoardWndProc for user interaction, Version 0.30 (c) 1990, Charles Petzold ---------------------------------------------------------------*/ #define INCL_WIN #include <os2.h> #include <stdlib.h> #include "checkers.h" #include "ckrdraw.h" MRESULT EXPENTRY BoardWndProc (HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2) { static BOOL fMovingPiece ; static HPS hps ; static HPOINTER hptrUpHand, hptrDnHand, hptrArrow ; static HWND hwndJudge ; static POINTL ptlLast ; static SHORT sBottom = BLACK, sColor = -1, sKing = 0 ; BOARD brd ; POINTL ptlMouse ; SHORT x, y, i ; switch (msg) { case WM_CREATE: hps = CkdCreatePS (hwnd) ; hptrUpHand = WinLoadPointer (HWND_DESKTOP, NULL, IDP_UPHAND) ; hptrDnHand = WinLoadPointer (HWND_DESKTOP, NULL, IDP_DNHAND) ; hptrArrow = WinQuerySysPointer (HWND_DESKTOP, SPTR_ARROW, FALSE) ; return 0 ; case WM_SIZE: CkdResizePS (hps, hwnd) ; CkdDestroyPieces () ; CkdCreatePieces (hps) ; return 0 ; case WM_TELL_BOARD_JUDGE_HANDLE: hwndJudge = HWNDFROMMP (mp1) ; return 0 ; case WM_TELL_BOARD_NEW_ORIENTATION: sBottom = SHORT1FROMMP (mp1) ; WinInvalidateRect (hwnd, NULL, FALSE) ; return 0 ; case WM_TELL_BOARD_COLOR_DIALOG: if (!WinDlgBox (HWND_DESKTOP, hwnd, ColorDlgProc, NULL, IDD_COLOR_DLG, mp1)) return 0 ; if (SHORT1FROMMP (mp2)) { CkdDestroyPieces () ; CkdCreatePieces (hps) ; } WinInvalidateRect (hwnd, NULL, FALSE) ; return 0 ; case WM_TELL_BOARD_STANDARD_COLORS: CkdSetStandardColors () ; CkdDestroyPieces () ; CkdCreatePieces (hps) ; WinInvalidateRect (hwnd, NULL, FALSE) ; return 0 ; case WM_BUTTON1UP: if (sColor = = -1) return 0 ; WinSetActiveWindow (HWND_DESKTOP, hwnd) ; // get mouse coords and index ptlMouse.x = MOUSEMSG(&msg)->x ; ptlMouse.y = MOUSEMSG(&msg)->y ; CkdQueryHitCoords (hps, ptlMouse, &x, &y) ; i = CkdConvertCoordsToIndex (x, y, sBottom) ; if (i = = -1) // didn't hit black square { WinAlarm (HWND_DESKTOP, WA_ERROR) ; return 0 ; } if (!fMovingPiece) // ie, picking up piece { if (!WinSendMsg (hwndJudge, WM_QUERY_JUDGE_PICKUP_PIECE, MPFROMSHORT (i), NULL)) { WinAlarm (HWND_DESKTOP, WA_ERROR) ; return 0 ; } sKing = (SHORT) WinSendMsg (hwndJudge, WM_QUERY_JUDGE_IF_KING, NULL, NULL) ; // Remove the mouse pointer WinSetPointer (HWND_DESKTOP, NULL) ; // Erase, save area, and show piece at mouse CkdErasePiece (hps, x, y) ; CkdDragSave (hps, &ptlMouse, sKing) ; CkdDragShow (hps, &ptlMouse, sColor, sKing) ; // Prepare for WM_MOUSEMOVE fMovingPiece = TRUE ; ptlLast = ptlMouse ; } else // ie, attempt to set down piece { if (!WinSendMsg (hwndJudge, WM_QUERY_JUDGE_PUTDOWN_PIECE, MPFROMSHORT (i), NULL)) { WinAlarm (HWND_DESKTOP, WA_ERROR) ; return 0 ; } // restore area CkdDragRestore (hps, &ptlMouse, sKing) ; sKing = (SHORT) WinSendMsg (hwndJudge, WM_QUERY_JUDGE_IF _KING NULL, NULL) ; // set down the piece on the square CkdDragDeposit (hps, x, y, sColor, sKing) ; // check for continued jumps if (WinSendMsg (hwndJudge, WM_QUERY_JUDGE_CONTINUE_MOVE, MPFROMSHORT (i), NULL)) { CkdErasePiece (hps, x, y) ; CkdDragSave (hps, &ptlLast, sKing) ; } else // the move is over { fMovingPiece = FALSE ; sColor = -1 ; WinSetPointer (HWND_DESKTOP, hptrArrow) ; WinSendMsg (hwndJudge, WM_TELL_JUDGE_BOARD_MOVE_ENDED, NULL, NULL) ; } } return 0 ; case WM_MOUSEMOVE: ptlMouse.x = MOUSEMSG(&msg)->x ; ptlMouse.y = MOUSEMSG(&msg)->y ; // set the mouse pointer and move the piece if (fMovingPiece) { WinSetPointer (HWND_DESKTOP, NULL) ; CkdDragMove (hps, &ptlLast, &ptlMouse, sColor, sKing) ; ptlLast = ptlMouse ; } else if (sColor = = -1) WinSetPointer (HWND_DESKTOP, hptrArrow) ; else WinSetPointer (HWND_DESKTOP, sBottom ^ sColor ? hptrDnHand : hptrUpHand) ; return 0 ; case WM_SETFOCUS: // set the mouse pointer if (WinQuerySysValue(HWND_DESKTOP,SV_MOUSEPRESENT) = =0) WinShowPointer (HWND_DESKTOP, SHORT1FROMMP(mp2) ? TRUE : FALSE); if (fMovingPiece) WinSetPointer (HWND_DESKTOP, NULL) ; else if (sColor = = -1) WinSetPointer (HWND_DESKTOP, hptrArrow) ; else WinSetPointer (HWND_DESKTOP, sBottom ^ sColor ? hptrDnHand : hptrUpHand) ; return 0 ; case WM_CHAR: if (CHARMSG(&msg)->fs & KC_KEYUP) return 0 ; if (!(CHARMSG(&msg)->fs & KC_VIRTUALKEY)) return 0 ; // convert pointer position to x, y coords WinQueryPointerPos (HWND_DESKTOP, &ptlMouse) ; WinMapWindowPoints (HWND_DESKTOP, hwnd, &ptlMouse, 1) ; CkdQueryNearestXYFromPoint (hps, &ptlMouse, &x, &y) ; // move the coordinates switch (CHARMSG(&msg)->vkey) { case VK_HOME: x = 0 ; y = 7 ; break ; case VK_END: x = 0 ; y = 0 ; break ; case VK_PAGEUP: x = 7 ; y = 7 ; break ; case VK_PAGEDOWN: x = 7 ; y = 0 ; break ; case VK_UP: y = min (y + 1, 7) ; break ; case VK_DOWN: y = max (y - 1, 0) ; break ; case VK_RIGHT: x = min (x + 1, 7) ; break ; case VK_LEFT: x = max (x - 1, 0) ; break ; case VK_SPACE: break ; default: return 0 ; } // process keystrokes like mouse messages CkdQuerySlightOffsetFromXY (hps, x, y, &ptlMouse) ; switch (CHARMSG(&msg)->vkey) { case VK_SPACE: WinSendMsg (hwnd, WM_BUTTON1UP, MPFROM2SHORT ((SHORT) ptlMouse.x, (SHORT)ptlMouse.y), NULL) ; break ; default: WinMapWindowPoints (hwnd, HWND_DESKTOP, &ptlMouse, 1) ; WinSetPointerPos (HWND_DESKTOP, (SHORT) ptlMouse.x, (SHORT) ptlMouse.y) ; break ; } return 0 ; case WM_JUDGE_SAYS_REMOVE_PIECE: i = SHORT1FROMMP (mp1) ; CkdConvertIndexToCoords (i, &x, &y, sBottom) ; CkdErasePiece (hps, x, y) ; return 0 ; case WM_JUDGE_SAYS_RESET_BOARD: fMovingPiece = FALSE ; sColor = -1 ; WinSetPointer (HWND_DESKTOP, hptrArrow) ; WinInvalidateRect (hwnd, NULL, FALSE) ; return 0 ; case WM_JUDGE_SAYS_MOVE_BLACK: sColor = BLACK ; WinSetPointer (HWND_DESKTOP, sBottom = = BLACK ? hptrUpHand : hptrDnHand) ; return 0 ; case WM_JUDGE_SAYS_MOVE_WHITE: sColor = WHITE ; WinSetPointer (HWND_DESKTOP, sBottom = = BLACK ? hptrDnHand : hptrUpHand) ; return 0 ; case WM_PAINT: WinBeginPaint (hwnd, hps, NULL) ; WinSendMsg (hwndJudge, WM_QUERY_JUDGE_CURRENT_BOARD, MPFROMP (&brd), NULL) ; CkdDrawWindowBackground (hps, hwnd) ; CkdDrawWholeBoard (hps) ; CkdDrawAllPieces (hps, &brd, sBottom) ; if (fMovingPiece) { WinQueryPointerPos (HWND_DESKTOP, &ptlMouse) ; WinMapWindowPoints (HWND_DESKTOP,hwnd,&ptlMouse, 1) ; CkdDragSave (hps, &ptlMouse, sKing) ; CkdDragShow (hps, &ptlMouse, sColor, sKing) ; ptlLast = ptlMouse ; } WinEndPaint (hps) ; return 0 ; case WM_DESTROY: CkdDestroyPieces () ; CkdDestroyPS (hps) ; return 0 ; } return WinDefWindowProc (hwnd, msg, mp1, mp2) ; } Figure 7 /*--------------------------------------------------------------- CkdQueryHitCoords: Obtains coords from mouse pointer position -----------------------------------------------------------------*/ VOID CkdQueryHitCoords (HPS hps,POINTL ptlMouse, SHORT *px,SHORT *py) { LONG lCoords ; SIZEL sizlAperture ; sizlAperture.cx = 1 ; sizlAperture.cy = 1 ; GpiSetPickApertureSize (hps, PICKAP_REC, &sizlAperture) ; GpiConvert (hps, CVTC_DEVICE, CVTC_PAGE, 1L, &ptlMouse) ; GpiSetPickAperturePosition (hps, &ptlMouse) ; GpiSetDrawControl (hps, DCTL_DISPLAY, DCTL_OFF) ; GpiSetDrawControl (hps, DCTL_CORRELATE, DCTL_ON) ; lCoords = CkdDrawAllBoardSquares (hps) ; GpiSetDrawControl (hps, DCTL_DISPLAY, DCTL_ON) ; GpiSetDrawControl (hps, DCTL_CORRELATE, DCTL_OFF) ; *px = LOUSHORT (lCoords) ; *py = HIUSHORT (lCoords) ; } Figure 8 /*---------------------------------------------------- CkdDrawAllBoardSquares: Draws all squares of board ----------------------------------------------------*/ static LONG CkdDrawAllBoardSquares (HPS hps) { SHORT x, y ; for (y = 0 ; y < 8 ; y++) for (x = 0 ; x < 8 ; x++) if (CkdDrawBoardSquare (hps, x, y) = = GPI_HITS) return MAKELONG (x, y) ; return MAKELONG (-1, -1) ; } Figure 9 /*----------------------------------------------------------------- CkdConvertCoordsToIndex: Obtains index (0-31) from square coordinates ------------------------------------------------------------------*/ SHORT CkdConvertCoordsToIndex (SHORT x, SHORT y, SHORT sBottom) { if (x < 0 || x > 7 || y < 0 || y > 7) return -1 ; if ((x - (y & 1)) & 1) return -1 ; if (sBottom = = WHITE) { x = 7 - x ; y = 7 - y ; } return 3 ^ (4 * y + (x - (y & 1)) / 2) ; } Figure 10 /*-------------------------------------------------------------------- CKRMOVES.C -- Ckm routines for determining valid moves, Version 0.30 (c) 1990, Charles Petzold ------------------------------------------------------------------*/ #include <os2.h> #include "checkers.h" #include "ckrmoves.h" // Some handy constants #define B pbrd->ulBlack #define W pbrd->ulWhite #define K pbrd->ulKing #define E (~B & ~W) // empty squares #define MP valnojmp[0][i].ulGrid // valid no-jump moves (positive) #define MN valnojmp[1][i].ulGrid // valid no-jump moves (negative) #define IP valnojmp[0][i].incr // valid no-jump increments (pos) #define IN valnojmp[1][i].incr // valid no-jump increments (neg) #define JP valjumps[0][i].ulGrid // valid jumps (positive) #define JN valjumps[1][i].ulGrid // valid jumps (negative) #define IP1 valjumps[0][i].incr1 // valid jumps increment 1 (pos) #define IN1 valjumps[1][i].incr1 // valid jumps increment 1 (neg) #define IP2 valjumps[0][i].incr2 // valid jumps increment 2 (pos) #define IN2 valjumps[1][i].incr2 // valid jumps increment 2 (neg) static struct // Valid No-Jump Moves { SHORT incr ; ULONG ulGrid ; } valnojmp[2][3] = { 3, 0x00E0E0E0, 4, 0x0FFFFFFF, 5, 0x07070707, 3, 0x07070700, 4, 0xFFFFFFF0, 5, 0xE0E0E0E0 } ; static struct // Valid Jump Moves { SHORT incr1, incr2 ; ULONG ulGrid ; } valjumps[2][4] = { 3, 4, 0x00E0E0E0, 4, 3, 0x000E0E0E, 4, 5, 0x00707070, 5, 4, 0x00070707, 3, 4, 0x07070700, 4, 3, 0x70707000, 4, 5, 0x0E0E0E00, 5, 4, 0xE0E0E000 } ; /*-------------------------------------------------------------------- Obtain 32-bit ULONG of all pieces that can be moved without jumping ------------------------------------------------------------------*/ ULONG CkmQueryAllMoveablePieces (BOARD *pbrd, SHORT sColor) { SHORT i ; ULONG S = 0 ; // stands for "source" for (i = 0 ; i < 3 ; i++) { if (sColor = = BLACK) { S |= (((B & MP) << IP) & E) >> IP ; S |= (((B & K & MN) >> IN) & E) << IN ; } else { S |= (((W & MN) >> IN) & E) << IN ; S |= (((W & K & MP) << IP) & E) >> IP ; } } return S ; } /*------------------------------------------------------- Obtain 32-bit ULONG of all pieces that can make jumps -------------------------------------------------------*/ ULONG CkmQueryAllJumpablePieces (BOARD *pbrd, SHORT sColor) { SHORT i ; ULONG S = 0 ; for (i = 0 ; i < 4 ; i++) { if (sColor = = BLACK) { S |= ((((B & JP) << IP1 ) & W) >> IP1 ) & ((((B & JP) << (IP1 + IP2)) & E) >> (IP1 + IP2)) ; S |= ((((B & K & JN) >> IN1 ) & W) << IN1 ) & ((((B & K & JN) >> (IN1 + IN2)) & E) << (IN1 + IN2)) ; } else { S |= ((((W & JN) >> IN1 ) & B) << IN1 ) & ((((W & JN) >> (IN1 + IN2)) & E) << (IN1 + IN2)) ; S |= ((((W & K & JP) << IP1 ) & B) >> IP1 ) & ((((W & K & JP) << (IP1 + IP2)) & E) >> (IP1 + IP2)) ; } } return S ; } /*----------------------------------------------------------------- Obtain all destinations of a particular move-without-jump piece ----------------------------------------------------------------*/ ULONG CkmQueryMoveDestinations (BOARD *pbrd, SHORT sColor, ULONG ulPiece) { SHORT i ; ULONG P = ulPiece, D = 0 ; for (i = 0 ; i < 3 ; i++) { if (sColor = = BLACK) { D |= ((P & B & MP) << IP) & E ; D |= ((P & B & K & MN) >> IN) & E ; } else { D |= ((P & W & MN) >> IN) & E ; D |= ((P & W & K & MP) << IP) & E ; } } return D ; } /*------------------------------------------------------ Obtain all destinations of a particular jumping piece ------------------------------------------------------*/ ULONG CkmQueryJumpDestinations (BOARD *pbrd, SHORT sColor, ULONG ulPiece) { SHORT i ; ULONG P = ulPiece, D = 0 ; for (i = 0 ; i < 4 ; i++) { if (sColor = = BLACK) { D |= ((((P & B & JP) << IP1 ) & W) << IP2) & ((((P & B & JP) << (IP1 + IP2)) & E) ) ; D |= ((((P & B & K & JN) >> IN1 ) & W) >> IN2) & ((((P & B & K & JN) >> (IN1 + IN2)) & E) ) ; } else { D |= ((((P & W & JN) >> IN1 ) & B) >> IN2) & ((((P & W & JN) >> (IN1 + IN2)) & E) ) ; D |= ((((P & W & K & JP) << IP1 ) & B) << IP2) & ((((P & W & K & JP) << (IP1 + IP2)) & E) ) ; } } return D ; } /*---------------------------------------------------------- Obtain index of a jumped piece based on jumper's indices ----------------------------------------------------------*/ SHORT CkmQueryJumpedPiece (SHORT sBeg, SHORT sEnd) { return (sBeg + sEnd) / 2 + (sBeg & 4 ? 0 : 1) ; } Figure 11 /*-------------------------------------------------------------------- CkdConvertIndexToCoords: Obtains square coordinates from index (0-31) -------------------------------------------------------------------*/ VOID CkdConvertIndexToCoords (SHORT i, SHORT *px, SHORT *py, SHORT sBottom) { if (i <= 0 || i >= 32) { *px = -1 ; *py = -1 ; } *py = i / 4 ; *px = 2 * ((i ^ 3) % 4) + (*py & 1) ; if (sBottom = = WHITE) { *px = 7 - *px ; *py = 7 - *py ; } } /*---------------------------------------------------- CkdDragSave: Saves screen area when dragging piece ----------------------------------------------------*/ VOID CkdDragSave (HPS hps, POINTL *pptlMouse, SHORT sKing) { POINTL ptlOrigin, aptl[3] ; CkdPieceOriginFromCenterDevice (&ptlOrigin, pptlMouse) ; aptl[0].x = 0 ; aptl[0].y = 0 ; aptl[1].x = sizlPiece[sKing].cx ; aptl[1].y = sizlPiece[sKing].cy ; aptl[2] = ptlOrigin ; GpiSetBitmap (hpsMemory, ahbmSave[sKing]) ; GpiBitBlt (hpsMemory, hps, 3L,aptl,ROP_SRCCOPY,BBO_IGNORE) ; GpiSetBitmap (hpsMemory, NULL) ; } /*---------------------------------------------------------- CkdDragRestore: Restores screen area when dragging piece ----------------------------------------------------------*/ VOID CkdDragRestore (HPS hps, POINTL *pptlMouse, SHORT sKing) { POINTL ptlOrigin, aptl[3] ; CkdPieceOriginFromCenterDevice (&ptlOrigin, pptlMouse) ; aptl[0] = ptlOrigin ; aptl[1].x = ptlOrigin.x + sizlPiece[sKing].cx ; aptl[1].y = ptlOrigin.y + sizlPiece[sKing].cy ; aptl[2].x = 0 ; aptl[2].y = 0 ; GpiSetBitmap (hpsMemory, ahbmSave[sKing]) ; GpiBitBlt (hps, hpsMemory, 3L,aptl,ROP_SRCCOPY,BBO_IGNORE) ; GpiSetBitmap (hpsMemory, NULL) ; } /*------------------------------------------------------------- CkdDragShow: Shows piece in new position when being dragged -------------------------------------------------------------*/ VOID CkdDragShow (HPS hps, POINTL *pptlMouse, SHORT sColor, SHORT sKing) { POINTL ptlOrigin, aptl[3] ; CkdPieceOriginFromCenterDevice (&ptlOrigin, pptlMouse) ; // Write out mask with bitwise AND aptl[0] = ptlOrigin ; aptl[1].x = ptlOrigin.x + sizlPiece[sKing].cx ; aptl[1].y = ptlOrigin.y + sizlPiece[sKing].cy ; aptl[2].x = 0 ; aptl[2].y = 0 ; GpiSetBitmap (hpsMemory, ahbmMask[sKing]) ; GpiBitBlt (hps, hpsMemory, 3L, aptl, ROP_SRCAND, BBO_IGNORE) ; // Write out piece with bitwise OR aptl[0] = ptlOrigin ; aptl[1].x = ptlOrigin.x + sizlPiece[sKing].cx ; aptl[1].y = ptlOrigin.y + sizlPiece[sKing].cy ; aptl[2].x = 0 ; aptl[2].y = 0 ; GpiSetBitmap (hpsMemory, ahbmPiece[sColor][sKing]) ; GpiBitBlt (hps, hpsMemory, 3L, aptl, ROP_SRCPAINT, BBO_IGNORE) ; GpiSetBitmap (hpsMemory, NULL) ; } Figure 12 /*------------------------------------------------------------- CkdDragMove: Moves piece from one part of screen to another -------------------------------------------------------------*/ VOID CkdDragMove (HPS hps, POINTL *pptlFrom, POINTL *pptlTo, SHORT sColor, SHORT sKing) { POINTL ptlCenter, ptlOrigin, aptl[3], ptlFrom, ptlTo ; if ((labs (pptlFrom->x - pptlTo->x) > sizlPiece[sKing].cx) || (labs (pptlFrom->y - pptlTo->y) > sizlPiece[sKing].cy)) { CkdDragRestore (hps, pptlFrom, sKing) ; CkdDragSave (hps, pptlTo, sKing) ; CkdDragShow (hps, pptlTo, sColor, sKing) ; return ; } ptlCenter.x = min (pptlFrom->x, pptlTo->x) ; ptlCenter.y = min (pptlFrom->y, pptlTo->y) ; CkdPieceOriginFromCenterDevice (&ptlOrigin, &ptlCenter) ; // Copy screen into ahbmMove bitmap in hpsMemory2 aptl[0].x = 0 ; aptl[0].y = 0 ; aptl[1].x = sizlMove[sKing].cx ; aptl[1].y = sizlMove[sKing].cy ; aptl[2] = ptlOrigin ; GpiSetBitmap (hpsMemory2, ahbmMove[sKing]) ; GpiBitBlt (hpsMemory2, hps, 3L, aptl, ROP_SRCCOPY, BBO_IGNORE) ; // Do restore, save, & show to hpsMemory2 ptlFrom.x = pptlFrom->x - ptlOrigin.x ; ptlFrom.y = pptlFrom->y - ptlOrigin.y ; ptlTo.x = pptlTo->x - ptlOrigin.x ; ptlTo.y = pptlTo->y - ptlOrigin.y ; CkdDragRestore (hpsMemory2, &ptlFrom, sKing) ; CkdDragSave (hpsMemory2, &ptlTo, sKing) ; CkdDragShow (hpsMemory2, &ptlTo, sColor, sKing) ; // Copy ahbmMove bitmap in hpsMemory2 to screen aptl[0] = ptlOrigin ; aptl[1].x = ptlOrigin.x + sizlMove[sKing].cx ; aptl[1].y = ptlOrigin.y + sizlMove[sKing].cy ; aptl[2].x = 0 ; aptl[2].y = 0 ; GpiBitBlt (hps, hpsMemory2, 3L, aptl, ROP_SRCCOPY, BBO_IGNORE) ; GpiSetBitmap (hpsMemory2, NULL) ; } Sidebar As I discussed in "Checkers Part I" MSJ (Vol. 4, No. 6), I am using techniques discussed in Christopher S. Strachey's paper "Logical Nonmathematical Programs" originally published in Proceedings of the Association for Computing Machinery Conference, Toronto (1952, pp. 46-49) and reprinted in Computer Games I, edited by David N.L. Levy (Springer-Verlag, 1988). At any time, the board is represented by three 32-bit integers, which we can refer to as B (black), W (white), and K (king). The 32 bits correspond to the 32 black squares. A 1 bit means that the square is occupied or (in the case of K) that the piece on the square is kinged. Any bit set in K must have a corresponding bit set in B or W. These three integers are stored in the CHECKERS program in the BOARD structure. The game begins with the integers set as follows (in hexadecimal): B = 00000FFF W = FFF00000 K = 00000000 The empty squares can be represented by: E = ~B & ~W All squares occupied by black kings can be determined using: B & K You can also use this representation for the squares from which pieces can be moved. For example, there are nine squares from which a black piece can move to a position three higher in number: M = 00E0E0E0 (The above value was printed incorrectly in MSJ Vol. 4, No. 6--Ed.) The current black pieces that may be able to move to squares that are 3 higher in number are: B & M The destinations of these pieces are: (B & M) << 3 The move is only valid if the destination is empty: ((B & M) << 3) & E So, the complete set of black pieces that can move to a position 3 higher in number is: (((B & M) << 3) & E) >> 3 There can also be moves to positions 4 and 5 higher in number. For white pieces (or black kings), moves can be to positions 3, 4, or 5 lower in number. ────────────────────────────────────────────────────────────────────────────

Volume 5 - Number 3

──────────────────────────────────────────────────────────────────────────── OS/2 Version 2.0: Exploiting the 32-bit Architecture of 80386- and 80486-based Systems Ray Duncan The long-awaited 32-bit 80386-specific version of the OS/21 operating system, OS/2 Version 2.0, represents two major milestones in the evolution of personal computer systems software. It is the last stage in the transition from the 16-bit real-mode world of the Intel(R) 8086 to the 32-bit protected-mode world of the 80386 and 80486. And it is the first significant step in the operating system's evolution towards a truly portable system that can support high-performance, graphical applications on multiple dissimilar CPU architectures. This article will survey the features of OS/2 Version 2.0 that are of most interest to veterans of protected-mode programming under OS/2 Versions 1.0 through 1.2. It is true that OS/2 Version 2.0 has the same commitment to backwards compatibility as its predecessors, and will run DOS2 and OS/2 Version 1.x applications unchanged. But the full benefits of OS/2 2.0, and the potential for painless migration to other hardware platforms, can only be enjoyed by programs that are converted and recompiled for OS/2 2.0's 32-bit Application Program Interface (API). 32-bit Application Memory Model The most far-reaching architectural improvement in Version 2.0 is its support of a nonsegmented, linear address space (sometimes referred to as the flat memory model or 0:32 addressing) for 32-bit application programs. This feature is extremely important because it is essentially identical to the memory model used on many minicomputers and Motorola 680x0-based or RISC-based workstations. The disappearance of segmentation means that it will be much easier to port sophisticated applications and programming tools from these other architectures, while the performance penalty for using high-level languages instead of assembly language will be significantly reduced. To understand the so-called flat model fully, we first must review the differences between 80286 and 80386 protected-mode address generation. (For our purposes here, we will assume that the 80486 behaves exactly like the combination of an 80386 and an 80387.) On the 80286 in protected mode, a far pointer is composed of a 16-bit selector and a 16-bit offset (see Figure 1). The upper 13 bits of the selector are an index to an entry in a descriptor table--a system data structure that is maintained by the operating system and interpreted by the hardware. Each descriptor in a descriptor table is composed of many fields that completely describe the corresponding memory segment: a 24-bit base address, a 16-bit length, a segment type (read-only, read-write, executable), the privilege level required to access the segment, a present bit that indicates whether the segment's data is physically resident in RAM, and so on. In short, a selector is analogous to a file handle, in that it symbolizes data but its value has no direct correspondence to that data's location. Whenever an instruction references memory, a segment register is selected either implicitly (by the instruction's operand) or explicitly (by a segment register override). The CPU looks up the descriptor that corresponds to the selector in the segment register and uses the information in the descriptor to validate the memory access. If it finds no reason to disallow the access, it forms a physical memory address by combining the base address from the descriptor with a 16-bit offset from the instruction and/or one or more registers. On the 80386 in 32-bit protected mode, the relatively simple scheme used by the 80286 is altered in several ways (see Figure 2). First, a far pointer is defined as a 16-bit selector and 32-bit offset. Second, the format of descriptor table entries is extended to support 32-bit segment base addresses, corresponding to a physical address space of 4Gb, and segment sizes as large as 4Gb. Third, an additional (optional) layer of address translation called paging is added. The 32-bit result of the hardware's interpretation of a selector and offset is referred to as a linear address, which can then be remapped in 4Kb chunks or pages into a 32-bit physical address. The 80386's paging unit operates completely independently of the segmentation mechanism; it relies on its own data structures--page tables and a page table directory--which, like descriptor tables, are maintained by the operating system and interpreted by the hardware. The paging hardware even provides its own memory protection mechanisms: bit fields in the page table entries determine whether each page is writable or read-only, and whether the page is accessible to an application or only to the operating system. The page table entries also contain present, accessed, and dirty bits that aid the implementation of an efficient virtual memory manager. OS/2 2.0 exploits the 80386 paging hardware for nearly all of its memory management, allowing it to make segmentation essentially invisible to 32-bit application programs. Both the operating system and the programs that run under it live in a single huge 4Gb segment. The operating system and its private data structures and buffers are mapped into the segment at the high end; the application program's code and data are mapped into the segment at the low end (see Figure 3). Of course, protected mode does mandate that all segment registers contain valid selectors, but only two selectors are required: an executable/readable selector in CS and a data read/write selector in DS, ES, FS, GS, and SS. The descriptors for both selectors are called aliases because they map to the same linear memory addresses. The application program's portion of the segment is further divided into three areas. The lower end contains the program's static code, static data, and dynamically allocated private memory such as thread stacks and heaps. The upper end contains dynamic-link library code and data--including all operating system entry points--as well as any dynamically allocated shared memory. Free memory (or more properly, free address space) lies between, and as the program allocates additional memory, the private and shared areas grow towards one another until the linear address space, or more typically the physical RAM plus disk swapping space, is exhausted. In OS/2 2.0, the total application address space is limited to 512Mb, and a minimum address space of 64Mb is enforced for both the private and shared areas. The single-segment scheme just described has many beneficial consequences. For a 32-bit application, all memory offsets, jumps, and calls are near; it never needs to load or even be aware of the segment registers and the selectors that they contain (hence the term 0:32 addressing). Since segment register loads are relatively expensive, and the messy selector arithmetic that is necessary to support data structures larger than 64Kb under OS/2 Version 1.x is even more expensive, the performance of very large programs (or of programs that manipulate very large data structures) should be dramatically enhanced under OS/2 Version 2.0. Even more importantly, the elimination of both far pointers and the 80286's 64Kb segment limit vastly simplifies the implementation and use of high-level languages. There is no longer any place for the plethora of memory models--tiny, small, compact, medium, large, and huge--that was prevalent in OS/2 Version 1.x programming, each with its own idiosyncratic benefits, drawbacks, and run-time library. Instead, all 32-bit compilers for OS/2 2.0 can be shipped with a single run-time library that serves all purposes and programming situations. The compiler vendor does not even need to provide separate libraries for software and hardware (numeric coprocessor) floating point, because OS/2 Version 2.0 has a floating point emulator built in. Memory Protection and Virtual Memory Management Although such topics are not usually the concern of the applications programmer, it is also interesting to contrast the ways OS/2 Versions 1.x and 2.0 carry out the fundamental memory management chores of a protected-mode, multitasking, virtual memory operating system: protection of operating system code and data from applications, segregation of one application's code and data from another, sharing of memory between applications, and memory overcommit (that is, the use by applications of a logical address space larger than the physical address space). OS/2 Version 1.x, being an 80286 operating system, must rely solely on the selector/descriptor table mechanism for memory management. The 80286 allows two descriptor tables to be active at any given time: a Global Descriptor Table (GDT), whose base address is loaded into the Global Descriptor Table Register (GDTR), and a Local Descriptor Table (LDT), which resides in a segment with a GDT descriptor and whose GDT selector is loaded into the Local Descriptor Table Register (LDTR). Fortunately, the existence of these two descriptor tables is sufficient to allow OS/2 1.x to carry out all its memory management chores. OS/2 Version 1.x uses the GDT for the segments containing its own code and data and for any segments that must be accessible to all processes (such as the global information segment). It prevents applications from manipulating these segments either by making the segments read-only or setting the privilege level required for access to the segments higher than the privilege level given to any application. OS/2 1.x segregates one application from another by putting the descriptors for each program's code and data in a separate LDT, then enabling the appropriate LDT as part of a context switch. As a result, the memory that does not belong to an application is simply not "visible" to that application. Memory sharing is accomplished by building descriptors in each application's LDT that refer to the same physical segment. Virtual memory in OS/2 Version 1.x is also implemented at the segment level; if the virtual memory manager (VMM) must roll a segment's contents out to the swap file on disk, it simply clears the present bit in the segment's descriptor. The next time the segment is referenced, the hardware generates a "segment not present" exception, which is fielded by the operating system and passed to the VMM. The VMM then allocates some physical memory to hold the segment (possibly swapping or discarding one or more other segments to obtain the memory), reads the segment back in from disk, updates the descriptor with the new physical base address, sets the "present" bit in the descriptor, and executes an interrupt return to restart the instruction that caused the exception in the first place. The main use of the selector/descriptor mechanism when running a 32-bit application in OS/2 2.0 is to protect the operating system's code and data. The system's memory manager creates kernel mode (ring 0) and user mode (ring 3) CS and DS selectors whose descriptors differ only in their segment length ("limit") fields; the user mode descriptors map the bottom 512Mb of the linear address space, while the kernel mode descriptors map the entire 4Gb. The application can invoke any operating system service with near calls while running on the user mode selectors, because all API entry points lie within the shared memory area below the 512Mb boundary. If a particular API function requires access to code and data in the operating system's private area above 512Mb, it will pass through a call gate (invisible to the application) that allows a switch to the kernel mode selectors. Maintenance of a separate "addressing" context for each 32-bit application, on the other hand, is carried out by OS/2 2.0 solely through the paging hardware. There is no need for a per-process LDT at all. Instead, the operating system edits the page tables and page table directory during a context switch to make the physical memory owned by the previously executing process invisible and the memory owned or shared by the new process visible and addressable. In the case of shared memory areas, the page table entries for each application that has access to the memory are merely aliases to the same physical addresses. Since both the operating system and the application coexist in a single huge segment, segment motion and swapping is no longer a practical basis for virtual memory management in OS/2 2.0. Instead, physical memory is always allocated and swapped in units of 4Kb pages; the VMM relies entirely on the present, accessed, and dirty bits in the page table entries. This scheme, called demand paged virtual memory, generally improves throughput. Small working sets of pages tend to stay in memory for all active applications, data never needs to be copied from one location to another to coalesce free memory (since disjoint physical pages can be mapped at will onto contiguous linear addresses), and swap file management is simplified by the constant page size. The 32-Bit API The 32-bit API of OS/2 2.0 is not a simple translation of the 16-bit API found in OS/2 1.2 (and also exported by OS/2 2.0 to run 16-bit protected-mode applications). In fact, there are significant differences between the 16-bit and 32-bit APIs at several levels. The impact of these differences on the conversion of an existing application for 32-bit mode depends on the language in which the program was written and the techniques employed by the program to interact with the user. First, let's look at the most fundamental aspects of the two APIs. Most of the 16-bit API's characteristics follow from the original design objective that programs written in high-level languages should be able to invoke operating system services directly, without the need for intermediary library functions written in assembly language. The 16-bit API functions are invoked by far calls to individual named entry points, whose addresses are resolved by the system loader using the technique known as dynamic linking. Parameters are passed to the 16-bit API functions on the stack, using the far _pascal calling convention (a practice OS/2 Version 1.x inherited from real-mode Microsoft(R) Windows). This means that when an API function call is written in C, the arguments are pushed left to right, and the called routine clears the stack. A 16-bit status code or other result (often a handle) is returned in AX; additional results are returned in variables (typically structures) in the caller's address space. Addresses are always passed or returned in the form of far pointers, consisting of a 16-bit selector and 16-bit offset. Several of the changes found in the 32-bit API at this level are perfectly predictable. The addresses of operating system entry points are still fixed up at load time by dynamic linking, but the API entry points are reached by near calls rather than far. Parameters are still passed on the stack, but the far pointers used in the 16-bit API are superseded by 32-bit offsets, and most single-precision parameters such as handles, byte counts, and error codes are simply extended to 32 bits. A more surprising discovery is that the far _pascal calling convention has been abandoned. Instead, the 32-bit API uses a new calling convention called _syscall, where parameters are pushed right to left and the caller clears the stack. The _syscall convention is identical to the _cdecl convention used in the C run-time library, except that the compiler does not prefix an underscore to function names. In any event, calling conventions and parameter passing methods are properly the business of the compiler and linker. The low-level differences between the 16-bit and 32-bit APIs that I have just described are largely hidden from the C language programmer who uses the header files found in the Microsoft OS/2 Programmer's Toolkit (although users of other languages are going to run into a fair number of problems in the near term). Let's move on, therefore, to the more abstract differences between the 16-bit and 32-bit APIs that are not handled transparently by the Programmer's Toolkit header files. The functions in Version 1.x's API fall naturally into two groups: kernel functions with names in the form of Dosxxx, Vioxxx, Kbdxxx, and Mouxxx , and Presentation Manager (hereafter "PM") functions with names in the form of Winxxx, GPIxxx, and so on. The kernel functions embrace character- or byte-oriented file and device I/O, timers and maintenance of the time and date, interprocess communication, memory management, and basic multitasking services; while the PM functions are concerned with management of windows, menus, dialog boxes, graphical drawing operations, and all the other vital elements of a graphical user interface. The natural division of API functions into these two groups persists in the 32-bit API, but the two groups have met with rather different fates regarding upward compatibility. The PM functions have survived virtually unscathed, which is something of a testament to the quality of their original design--not only have they lived up to their charter of device independence, they have proven to be reasonably independent of the host CPU's architecture as well. In fact, 16-bit application programs that were compiled in the large model and called only the PM functions and the standard C run-time library functions need little or no editing before they can be recompiled for 32-bit protected mode under OS/2 Version 2.0. In the 32-bit kernel API, on the other hand, changes are everywhere: some functions have been renamed but take the same arguments and have the same actions, a number of new functions have appeared, and many of the old functions have disappeared. Taken as a whole, these modifications to the kernel API have a single overriding objective: to bring the kernel API as close as possible to the state of device and architectural independence already achieved in the PM API. But the immediate consequence is somewhat startling: character-oriented kernel applications, which are considerably less complex than graphically-oriented PM applications, require paradoxically far more effort to port from 16-bit protected mode to 32-bit protected mode. Figures 4 and 5 provide an exhaustive cross-reference between the 16-bit and the 32-bit kernel API functions. You may find it helpful to refer to these tables as you read the remainder of this article and when converting your own programs to 32-bit protected mode. Changes to the 32-bit Kernel API The renaming of kernel functions has been carried out on a grand scale in the 32-bit API. In general, these renamings have taken place to normalize the kernel API function names by putting them into a more symmetric, predictable form. For example, the 16-bit API contains many functions that obtain information from the operating system, and the names of these functions have three distinctly different templates: DosQxxx, DosQueryxxx, and DosGetxxx. In the 32-bit API, all of the functions that obtain information but do not allocate some system resource (with a couple of inexplicable exceptions) have names in the form of DosQueryxxx. Where necessary, names have also been transformed into a verb-object format symmetric with the PM API; DosCWait has become DosWaitChild, DosBufReset has been changed to DosResetBuffer, and so on. Actually, renaming is even more pervasive than it appears at first glance, because the entry points for all of the 32-bit API functions have true names in the form of Dos32xxx for the purposes of dynamic linking. This allows the 16-bit and 32-bit entry points for functions with otherwise identical names to be distinguished, so that a 16-bit protected-mode program can selectively call 32-bit entry points and a 32-bit protected-mode program can call 16-bit entry points. Under normal circumstances, fortunately, the programmer can ignore the Dos32xxx forms because they are all aliased to the form Dosxxx in the header files with #define statements. The 16-bit kernel functions that have vanished altogether in the 32-bit API are principally those with implicit or explicit machine dependencies. This includes the entire battery of Vioxxx, Kbdxxx, and Mouxxx functions; all the DosMonxxx functions; and miscellaneous other Dosxxx functions such as DosSetVec, which is the API equivalent of poking an 80x86 interrupt vector; DosCLIAccess, DosPortAccess, DosCallBack, and DosR2StackRealloc, all of which are intimately related to the 80x86 architecture's "ring" scheme of privilege levels, and DosGetInfoSeg, which returns selectors for two read-only information segments. Of all the mutations in the 32-bit kernel API, I suspect that the disappearance of the Vioxxx, Kbdxxx, and Mouxxx functions is going to cause the most controversy and dismay. Many programmers of the old school (including myself) who learned their trade in DOS or even CP/M have been using the existence of these functions as an excuse to avoid learning how to write event-driven graphical applications. Although the 16-bit entry points for the Vioxxx, Kbdxxx, and Mouxxx functions still exist in OS/2 Version 2.0 and in fact can be called by 32-bit applications, it is clearly the intention of the system's designers that all 32-bit programs will either be true PM applications or will use DosRead and DosWrite with the standard input and standard output handles. (At press time, a limited 32-bit Vio interface for Version 2.0 was under discussion--Ed.) The completely new functions in the 32-bit kernel API fall into four main areas: memory management, thread management, interprocess communication, and exception handling. There are also significant file system interface issues for programmers who have not yet adapted their products for the OS/2 1.2 kernel API and installable file systems. In the following sections, we'll discuss the new functions in the context of a more general survey of the 32-bit kernel API by functional group. 32-bit File and Device API Functions The 32-bit file and device API (see Figure 6) is essentially parallel to the 16-bit file and device API found in OS/2 Version 1.2, allowing for some renamings and the disappearance of the 16-bit functions DosQFileMode, DosSetFileMode, DosReadAsync, and DosWriteAsync. Those few programmers who have already modified their applications to handle gracefully the long filenames and extended attributes that first appeared in OS/2 1.2 will find virtually no work to do in this area when converting their programs to 32-bit mode. Those who did not will be faced with considerable work. Although there is not room in this article to discuss file issues in depth, the key issues are listed as follows. The maximum length of a pathname may differ from one version of the system to another, so a program should obtain this information at run time using DosQuerySysInfo and allocate its filename and pathname buffers dynamically. Filename parsing routines and file open dialogs of OS/2 1.0 or 1.1 vintage may not work properly in OS/2 1.2 and 2.0 because of the latter's support for long, mixed-case filenames with embedded blanks and multiple dot (.) delimiters. To make matters even more complex, a single OS/2 system may well have file systems of several different types on-line simultaneously, each with a different set of restrictions on filename lengths and formats. With respect to extended attributes, a program must be careful not to damage existing extended attributes on any file that it touches and to replicate these attributes when it backs up or copies a file. Furthermore, a program is responsible for associating certain standard extended attributes (such as .TYPE) with all files that it creates or "owns," and must ensure that the values of these extended attributes remain consistent with the file's actual name and contents. The key to manipulation of extended attributes is mastery of the EAOP, FEAList, and GEAList structures defined in the Microsoft PTK header files. The 32-bit functions DosOpen (equivalent to the 16-bit function DosOpen2), DosQueryFileInfo, DosQueryPathInfo, DosSetFileInfo, and DosSetPathInfo accept a pointer to the EAOP structure, which in turn contains pointers to FEAList and GEAList structures. A more detailed discussion of installable file systems, long filenames, and extended attributes can be found in the Power Programming columns in PC Magazine (Vol. 9, Nos. 6 through 9). 32-bit Disk and Directory API Functions The functions in this group (see Figure 7) hold few surprises. The 32-bit disk and directory API functions correspond almost exactly to the corresponding OS/2 Version 1.2 functions. Note that the 32-bit functions DosCreateDir and DosFindFirst are equivalent to the 16-bit functions DosMkDir2 and DosFindFirst2, in that they are "extended-attribute-aware" and can accept a pointer to an EAOP structure as one of their parameters. The modifications made to this group were all related to installable file systems, long filenames, or extended attributes. 32-bit Memory Management API Functions Of all the groups of API functions, the 32-bit memory management API (see Figure 8) merits the closest study, because it holds the key to understanding the most crucial differences between OS/2 Versions 1.x and 2.0. In OS/2 1.x, the currency of every memory transaction was the selector, and the fundamental unit of memory management was the segment. In OS/2 2.0, segments and selectors are almost always irrelevant to 32-bit applications; memory is managed in terms of objects, the size of an object is always a multiple of 4Kb (the 80386 page size), and the handle for a memory object is the 32-bit offset of its base. One of the most important new concepts in OS/2 2.0 memory management is the differentiation between allocation and commitment. Allocation reserves a range of addresses in the linear address space, but does not in itself make those addresses valid and accessible. Commitment reserves physical memory and/or swap file space for part or all of a memory object on a page-by-page basis, causing the addresses within those pages to become valid. A private memory object is allocated by a call to the 32-bit function DosAllocMem, which can be thought of as analogous to the 16-bit function DosAllocSeg. The pages within the object can all be committed at allocation time, or they can be selectively committed later by a call to DosSetMem. Access characteristics (read-only, read-write, executable, or guard) can also be specified at allocation time or on a page-by-page basis with DosSetMem. Later, a program can retrieve commitment and access type information for a range of pages within an object with DosQueryMem. A shared memory object is allocated with the 32-bit function DosAllocSharedMem (similar to the 16-bit function DosAllocShrSeg). As with DosAllocMem, the pages within a shared object can be committed and given access rights at the time of allocation, or later on a page-by-page basis with DosSet Mem. The shared object can either have a global name in the form \SHAREMEM\objectname.extension or it can be allocated as an anonymous object by setting the function call parameter for the object's name to a null pointer. A named shared memory object can be accessed by another process with the DosGetNamedSharedMem API function (analogous to the 16-bit function DosGetShrSeg), which establishes addressability for the object and returns the 32-bit offset of its base. When it is allocated, an anonymous shared object must be declared giveable or gettable so that later it can be made addressable for another process with the functions DosGetSharedMem and DosGiveSharedMem (equivalent in their behavior to the 16-bit functions DosGetSeg or DosGiveSeg). Both private and shared memory objects are deallocated as a unit by calling DosFreeMem, which is analogous to the 16-bit function DosFreeSeg. After an object is freed, the linear addresses within the pages previously occupied by that object are no longer valid, the characteristics and access rights of those pages (guard, read-only, and so on) are no longer relevant, and any reference to those addresses causes a GP Exception. The 16-bit function DosAllocHuge has no counterpart in the 32-bit API because it is not needed; the memory objects created by DosAllocMem and DosAllocSharedMem can (theoretically, at least) be any size up to the application's entire linear address space. Likewise, the 16-bit functions DosGetHugeShift, DosSizeSeg, DosLockSeg, DosUnlockSeg, DosR2StackRealloc, and DosMemAvail are not found in the 32-bit API; they have also been made irrelevant by the transition from segment-based to page-based memory management. The 16-bit functions DosReallocSeg and DosReallocHuge have no successors in the 32-bit API either, but the reasons here are less obvious. Since the handle for a memory object is a 32-bit offset, which is susceptible to arithmetic manipulations, the system cannot move memory objects around with impunity as it could the physical memory assigned to segments. Consequently, when you allocate a memory object, you should make it large enough for its worst-case requirements, then call DosSetMem to commit increasingly large portions of the object only as you need them. Alternatively, when data outgrows its memory object, you can allocate and commit a new larger object, copy the data from the old object to the new, then release the original memory object. The behavior of the DosSubxxx functions is essentially unchanged in the 32-bit API, with the addition of a new DosSubUnset function; programmers may actually find these functions useful now that they can employ them for the management of local heaps and address those heaps with near pointers. 32-bit Multitasking API Functions The 32-bit multitasking API (see Figure 9) can be considered as consisting of three subgroups of functions: thread control, process control, and session control. The process and session functions are nominally the same as the 16-bit API, allowing for renamings and the usual exchanges of far pointers for 32-bit offsets. The DosStartSession function has some minor enhancements that allow protected-mode applications to launch real-mode (MS-DOS(R)) programs in a virtual DOS machine (VDM) and specify certain characteristics of the VDM such as the keyboard polling rate and amount of emulated EMS memory. The thread-related 32-bit functions, on the other hand, have been significantly improved. Stack space for new threads is allocated automatically by DosCreateThread and reclaimed automatically when the thread terminates. The entry point specified in the DosCreateThread call is a 32-bit offset rather than a far pointer, the new thread can be passed a doubleword argument on its stack, and the thread can be started in either an active or suspended state. There is also a new function, DosWaitThread, that allows one thread to wait for any other thread to die; it is analogous to the action of DosWaitChild (formerly DosCWait) for processes. System level, per-process, and per-thread information is no longer obtained from global and local information segments, which no longer exist for pure 32-bit apps. The values of system constants are found by calling DosQuerySysInfo, while per-process and per-thread information is obtained from the 32-bit function DosGetThreadInfo. DosGetThreadInfo returns two structures containing (among other things) the process ID, parent process's ID, module handle, command line pointer, environment pointer, current thread ID, current thread priority, thread stack size and base, and a pointer to the thread's exception handler chain. Aside from the differences that are evident in the thread-oriented API functions, the operating system also has significant internal architectural improvements in this area. OS/2 2.0 can support as many as 4096 threads and 4096 processes, as opposed to a maximum of 511 threads and 255 processes for OS/2 1.2, and there is no additional per-process limit on the number of threads as there was in OS/2 Version 1.x. The Version 2.0 scheduler and dispatcher has been extensively overhauled so that the process of picking and running the proper thread from the list of all eligible threads is much more efficient. 32-bit Interprocess Communication API Functions In the important area of interprocess communication (IPC), there's good news, there's great news, and there's also a little bad news. The good news is that the changes that were made to the queue and pipe functions when they were ported to the 32-bit API are trivial; code using these functions will need, at most, some minor mechanical editing (see Figure 10). (The shared memory functions have already been discussed.) The great news is that the 16-bit semaphore and signal functions, which weren't very well thought out anyway, have been completely replaced with new and much better 32-bit functions. The bad news is that every line of code that uses semaphores and signals will need to be rethought and rewritten. The 16-bit semaphore API has proven somewhat troublesome because it supports three subtly different types of semaphores--system semaphores, RAM semaphores, and Fast-Safe RAM semaphores. Furthermore, the first two types can be used in two incompatible ways: for signaling an event, in which case the semaphore is either set or cleared, or for mutual exclusion, after which the semaphore is considered to be either owned or not owned. Overlap and ambiguity in the 16-bit API functions adds to the confusion. For example, DosSemRequest acquires ownership of a semaphore, while DosSemSet sets a semaphore, but DosSemClear is used either to clear a semaphore used for signaling, or to release ownership of a semaphore used for mutual exclusion. The RAM semaphores can also be combined into lists, which require the use of still other special-purpose API functions, but only when the semaphores are used for signaling! Another chronic problem in the 16-bit semaphore API has been the status of system semaphores as a critical and often application-limiting resource. System semaphores are valuable because they support counting or nested request calls, they are not swappable, they can be accessed by device drivers in either real mode or protected mode, and the operating system assists with clean up if a process dies owning a system semaphore. But these characteristics also ensure that relatively few system semaphores can be available in OS/2 1.x. System semaphores must be located below the 640Kb boundary, and thus take away memory from the DOS compatibility environment. In addition, OS/2 itself must use a large proportion of the available system semaphores. The 32-bit semaphore API solves these historical problems by tossing out the old semaphore classes and functions completely. It defines three new types of semaphores--mutual exclusion (mutex) semaphores, event (signaling) semaphores, and multiple-wait (muxwait) semaphores--and three new, disjoint sets of API functions to manipulate them. There is no longer any distinction between RAM semaphores and system semaphores, although it remains possible to create an anonymous semaphore that another process can't access; all semaphores are controlled by the system, are shareable, and can be cleaned up. There is also no longer any chance of two processes or threads coming into conflict in the way they access a semaphore; if the wrong API call is used with a particular semaphore, an error code will be returned but no other damage will be done. Signals as we knew them in OS/2 1.x have essentially disappeared, which I for one consider to be a blessing. Due to their UNIX(R) heritage, signals always seemed grafted in for the benefit of the C run-time library. Architecturally, signals were alien to OS/2 1.x, and the meaning of some of the signals (such as the Ctrl-C signal SIGINTR) became increasingly unclear in the PM environment. In Version 2.0, signal handling has been merged with hardware fault handling (such as divide by zero) and placed under the control of four new 32-bit functions: DosRaiseException, DosSetExceptionHandler, DosUnsetExceptionHandler, and DosUnwindException. The new exception mechanisms are powerful and general, and will simplify application code and improve application performance. Four special aspects of the new, unified exception handling are worth mentioning. First, exception handlers can be chained and nested, and an exception handler in the chain can decide whether to let later handlers in the chain execute or not. Second, OS/2 2.0 allows applications to register exception handlers for general protection (GP) faults, which is a boon to the authors of interpreters, incremental compilers, and products such as editors that support macro languages. Third, Version 2.0 does not allow applications to trap "coprocessor not found" faults; it provides transparent per-thread floating point emulation instead. This allows all application programs to be compiled and distributed with in-line numeric coprocessor instructions and eliminates the need for each high-level language to have its own floating point emulation library. Fourth, OS/2 2.0 exception handlers can be written entirely in a high-level language. 32-bit Time and Date API Functions Allowing for renamings and the replacement of 16-bit parameters with 32-bits, the time and date functions of OS/2 2.0 are equivalent to those in OS/2 1.x (see Figure 11). There are no substantive conversion issues in this group of functions. 32-bit Dynamic Linking API Functions The 32-bit dynamic-linking functions, like the 32-bit time and date functions, are an almost exact superset of their 16-bit predecessors (see Figure 12). But note that the 32-bit DosGetResource and DosQueryProcAddr return 32-bit offsets rather than selectors and offsets, and that the 32-bit DosGetResource is more nearly parallel to the 16-bit DosGetResource2 of OS/2 Version 1.2 rather than the 16-bit DosGetResource of Versions 1.0 and 1.1. 32-bit Internationalization Support API Functions The functions in the 32-bit National Language Support API help the software developer adapt an application to the character sets and keyboard layouts, and the currency, date, and time formats used in Europe and Asia. The 32-bit internationalization functions (see Figure 13) are parallel to those found in the 16-bit API, with the exception of the 16-bit DosPFSxxx functions which were not carried forward. Miscellaneous 32-bit API Functions Finally, the 32-bit functions in this group (see Figure 14) behave as expected; the group is mainly remarkable for the 16-bit API functions that have no 32-bit equivalents. The 16-bit functions DosCallBack, DosCLIAccess, and DosPortAccess are obsolete because Version 2.0 uses paged memory management instead of segmented memory management. The function DosGetMachineMode is absent because all 32-bit applications run in protected mode by definition. The 16-bit functions DosGetVersion, DosGetEnv, and DosGetInfoSeg are superseded by the 32-bit functions DosGetThreadInfo and DosQuerySysInfo. Summary Compared to its 16-bit predecessors, OS/2 Version 2.0 is based on several new architectural and philosophical tenets: use of demand-paged virtual memory instead of segment motion and swapping, a flat memory model for 32-bit applications, and movement of the kernel API toward device and CPU-architecture independence. Pure Presentation Manager applications are only slightly affected by these changes in direction, while applications that make extensive use of the OS/2 kernel API to take full advantage of the memory management, multitasking, and interprocess communication facilities in OS/2 may require extensive source code conversion. This article was based on a prerelease version of OS/2 2.0. The retail version of the system may differ from the system described here--Ed. Figure 6 Function Description DosClose Close file or device handle DosCopy Copy file(s) with extended attributes DosDelete Delete a file DosDupHandle Duplicate file or device handle DosEditName Build new pathname using edit string DosEnumAttribute Obtain extended attribute names for file DosFileIO Combined seek, lock, read or write, and unlock operation DosMove Rename file and/or move to another directory DosOpen Open, replace, or create a file, or open a device DosQueryFHState Return access and sharing attributes for handle DosQueryFileInfo Return file size, file attributes, and date/time stamps for handle DosQueryHType Return handle type (file, device, or pipe) DosQueryPathInfo Return extended attributes, fully qualified name, or file system identifier for pathname DosRead Read data from file, pipe, or device DosResetBuffer Flush file buffers, update directory DosSetFHState Set sharing and access characteristics for handle DosSetFileInfo Set file attributes or time/date stamps for handle DosSetFileLocks Lock or unlock file region DosSetFilePtr Set file pointer position for next read or write DosSetFileSize Extend or truncate file DosSetMaxFH Set maximum number of handles for process DosSetPathInfo Set extended attributes for pathname DosWrite Write data to file, pipe, or device Figure 7 Function Description DosCreateDir Create new directory DosDeleteDir Remove directory DosFindClose Close search context DosFindFirst Initialize search for file or directory DosFindNext Continue search for file or directory DosFSAttach Associate logical volume with file system DosFSCtl File system-specific commands and information DosPhysicalDisk Return information about disk partitions DosQueryCurrentDir Return name of current directory for drive DosQueryCurrentDisk Return identifier for current drive DosQueryFSAttach Return identify of file system for specified volume DosQueryFSInfo Return file system information or volume label DosQueryVerify Return state of read-after-write verify flag DosSearchPath Searches list of directories for file DosSetCurrentDir Select current directory for drive DosSetDefaultDisk Select current drive DosSetFSInfo Set volume label for drive DosSetVerify Set state of read-after-write verify flag DosShutDown Notify file system to prepare for system power-down or reset Figure 8 Function Description DosAliasMem Create address alias for object DosAllocMem Allocate private memory object DosAllocSharedMem Allocate shared memory object DosFreeMem Release memory object DosGetNamedSharedMem Get base address of named shared memory object DosGetSharedMem Make shared memory object addressable by the current process DosGiveSharedMem Make shared memory object addressable by another process DosQueryMem Get characteristics of memory object DosSetMem Set characteristics of memory object DosSubAlloc Allocate memory from local heap DosSubFree Free memory in local heap DosSubSet Creates or resets size of local heap DosSubUnset Destroy local heap Figure 9 Function Description DosCreateThread Create new thread of execution in current process and allocate stack DosEnterCritSec Suspend context switching for other threads in current process DosExit Terminate current thread DosExitCritSec Restore context switching for other threads in same process DosGetThreadInfo Return thread and process information DosResumeThread Reactivate thread in same process DosSetPriority Set execution priority of thread or of other process DosSuspendThread Suspend execution of other thread in same process DosWaitThread Wait for termination of other thread DosDebug Debugging interface for controlled execution of other process DosDynamicTrace Log execution information for process DosExecPgm Create child process DosExit Terminate current process DosExitList Register routines to be executed at process termination DosKillProcess Unilaterally terminate another process DosWaitChild Wait for child process to terminate DosSelectSession Switch session into foreground DosSetSession Set session characteristics DosStartSession Create new session and start process within that session DosStopSession Terminate session Figure 10 Function Description DosAddMuxWaitSem Add semaphore to multiple-wait list DosCloseEventSem Close event (signaling) semaphore DosCloseMutexSem Close mutual-exclusion semaphore DosCloseMuxWaitSem Close multiple-wait semaphore DosCreateEventSem Create event (signaling) semaphore DosCreateMutexSem Create mutual-exclusion semaphore DosCreateMuxWaitSem Create multiple-wait semaphore list DosDeleteMuxWaitSem Remove semaphore from multiple-wait list DosOpenEventSem Return handle for event (signaling) semaphore DosOpenMutexSem Return handle for mutual-exclusion semaphore DosOpenMuxWaitSem Return handle for multiple-wait semaphore list DosPostEventSem Set event (signaling) semaphore DosQueryEventSem Return state of event (signaling) semaphore DosQueryMutexSem Return state of mutual-exclusion semaphore DosQueryMuxWaitSem Return state of multiple-wait semaphore list DosReleaseMutexSem Release ownership of mutual-exclusion semaphore DosRequestMutexSem Wait for ownership of mutual-exclusion semaphore DosResetEventSem Clear event (signaling) semaphore DosWaitEventSem Wait for event (signaling) semaphore to be cleared DosWaitMuxWaitSem Wait for one of a list of semaphores to be cleared DosCallNPipe Open, write, read, and close named pipe DosConnectNPipe Wait for client to open pipe DosCreateNPipe Create named pipe DosCreatePipe Create anonymous pipe DosDisConnectNPipe Unilaterally close named pipe DosPeekNPipe Inspect data in named pipe without removing it from pipe DosQueryNPHState Return modes for named pipe handle DosQueryNPipeInfo Return characteristics of named pipe DosQueryNPipeSemState Return information for pipe associated with semaphore DosRawReadNPipe Read raw data from named pipe DosRawWriteNPipe Write raw data to named pipe DosSetNPHState Set characteristics of named pipe DosSetNPipeSem Associate semaphore with named pipe DosTransactNPipe Write then read named pipe DosWaitNPipe Wait for availability of named pipe DosCloseQueue Close queue (also destroy if queue creator) DosCreateQueue Create named queue DosOpenQueue Obtain handle for named queue DosPeekQueue Inspect queue message without removing it from queue DosPurgeQueue Discard all messages in queue DosQueryQueue Return number of messages waiting in queue DosReadQueue Read and remove message from queue DosWriteQueue Write message into queue DosRaiseException Create exception condition for current thread or another process DosSetExceptionHandler Register handler for specified exception DosUnsetExceptionHandler Restore default processing for specified exception DosUnwindException Remove handlers from list for current exception Figure 11 Function Description DosAsyncTimer Start asynchronous one-shot timer DosGetDateTime Return current date, time, and day of the week DosSetDateTime Set current date and time DosSleep Suspend current thread for specified interval DosStartTimer Start asynchronous repeating timer DosStopTimer Stop asynchronous repeating timer Figure 12 Function Description DosFreeModule Release handle for dynamic-link library DosFreeResource Release read-only program resource DosGetResource Return offset of read-only program resource DosLoadModule Load dynamic-link library if not already loaded DosQueryModuleHandle Return handle for dynamic-link library DosQueryModuleName Return pathname for dynamic-link library DosQueryProcAddr Return entry point for function in dynamic-link library Figure 13 Function Description DosMapCase Translate ASCII string in place DosQueryCollate Return collating sequence table DosQueryCp Return current code page DosQueryCtryInfo Return internationalization information DosQueryDBCSEnv Return table of double byte character set codes DosSetCp Set code page for current session DosSetProcessCp Set code page for current process Figure 14 Function Description DosBeep Generate tone DosDevConfig Return system configuration information DosDevIOCtl Device-specific commands and information DosErrClass Return information about error code DosError Disable or enable system's critical error handler DosGetMessage Retrieve message text from disk file DosInsertMessage Insert variable text into body of message DosPutMessage Send message to file, pipe, or device DosQueryAppType Return application type (PM-aware, PM- compatible, etc.) DosQuerySysInfo Return system information such as version, maximum pathname length, and page size DosScanEnv Search environment for variable and return its value Adding Hypertext-based Help to Your Application Using Microsoft Help Technology Marc Adler If you were to look at the thousand most popular software programs, you would probably find a thousand help systems. Until now, there has been no attempt to standardize the implementation of these systems. Some products store the help information in an external ASCII file, some compress the help text and store it in an external file, and some embed the help text within the program code. As for user access of the help text, some programs allow users to modify the help system, while other programs do not. To improve on-line help systems, Microsoft will be making its help technology available by license to the software development community. This help technology is embedded in products such as the Microsoft(R) C Version 6.0 Programmer's WorkBench, the Microsoft QuickC(R) Compiler, and Microsoft QuickBasic. The help system provides a help compiler together with a help access library, which allows you to create a hypertext-based on-line help system with very little overhead. If you are programming OS/2 systems, all you have to do is provide the screen management; if the program is to run under DOS1, you must write a few memory management routines. Creating a Help File A help file is comprised of a series of topics and text describing each topic. The help text can contain embedded formatting codes and hyperlinks to other topics contained in the same help file or other files. A help file can be formatted in QuickHelp format (an ASCII file with embedded codes), Rich Text Format (RTF), or in minimally formatted ASCII. Once a help file is written, it is "compiled" into a special compressed format that can be read by either Microsoft QuickHelp or by an application program that uses the Microsoft help library (see Figure 1). The format you use depends on your application; however, be aware that you need an RTF-aware word processor if you choose Rich Text Format. Because the QuickHelp format is the default format used by the help compiler, this article concentrates on the QuickHelp format. The first step in creating on-line help is to organize the help information into topics. For example, if you are trying to write help for a text editor, possible topics might be "Deleting a Line," "Inserting a Line," and "Opening a Window." These topics are called context strings. Context strings must be able to be displayed in a single line on the screen, even if composed of more than one word. A context string is defined by using the .context command, one of several dot commands recognized by the help compiler. For example, the line .context open defines a context string containing the word "open." Following this line, you insert the help text that describes the "open" command. Multiple context strings can refer to the same topic text. For example, the contexts open, close, read, and write might refer to a single topic that describes file operations in C. You can associate multiple context strings with the same help text like this: .context open.context read.context write < help text for file operations goes here > The word open is an implicit cross reference. When the user clicks on "open" anywhere in the help text, the associated topic text will be displayed, because "open" has been defined as a context string in the help file. It is an implicit reference because you do not have to write a special help code to link each instance of the word "open" to another topic. Microsoft help technology defines several standard context strings that an application can use to adhere to the look and feel of the Microsoft products using this help engine (see Figure 2). For example, if you are referencing a third-party QuickHelp file from within your application and you want to bring up an index of the available topics, you could ask the help engine for a topic called "h.index." Hyperlinks Using this technique, you can embed special formatting characters within the topic text to emphasize certain words or sentences, and you can also create cross-references to other topics. Hyperlinks, or explicit cross-references, provide an intuitive way of browsing through help information. A hyperlink is a pair of strings that represent a connection between two topics. The first string in the pair is embedded in the topic text and is displayed on the screen with the rest of the help text (it is usually displayed in a different color). The second string of the pair is a context string and is not displayed on the screen as part of the topic text. If the user selects the first string, the topic text associated with the second string is displayed. A hyperlink is an explicit cross reference because you must actually code these links when writing your help file. An example of a hyperlink is shown in Figure 3. The word mouse is tied to the context string mouse_def although mouse_def is not displayed as part of the help text. When the user selects the word mouse within the text associated with "Selecting Lines," the help system will search for the mouse_def topic. If the topic is located, the topic text associated with mouse_def will be displayed. The help facility supports the concept of a distributed help database by allowing a hyperlink to reference a topic in another file (even one across a network), or to reference a single file (such as a C language include file) as a topic. The latter feature lends itself to new kinds of applications in source code browsing. The HELPMAKE compiler recognizes several special formatting flags that can be used to emphasize parts of your help text or to create hyperlinks (see Figure 4). A hyperlink is defined by: hyperlink\vcontext\v For example, in the topic text The mouse\vmouse_def\v is an integrated part of WB. the word mouse has a hyperlink to the context mouse_def. By default, the hyperlink is considered to be the word that precedes the invisible context string. To use several words as a hyperlink, an anchor must be used to tell HELPMAKE where the start of the hyperlink is. For example, in the following sentence the phrase "Programmer's WorkBench" is hyperlinked to the context string wb. \aProgrammer's WorkBench\vwb\v is an integrated tool. If you click the mouse within the phrase, QuickHelp will show the help text associated with context wb. You can reference a topic in an external file by using the form filename!context-string For instance, mouse\vdevice.hlp!mouse_def\v would search the help file device.hlp for the topic mouse_def, while mouse\v$HELPFILES:device.hlp!mouse_def\v would search the path referred to by the HELP environment variable for the file device.hlp, and once located, would search device.hlp for the context string. Note that the environment variable HELPFILES is the standard variable that all upcoming Microsoft language applications will use to locate their help files. As you saw above, you can use dot commands to control the way QuickHelp displays the help information (see Figure 5). The most common dot command is .context, which defines context strings for topics. Other commands tell QuickHelp to start and end a paste section, to display a list of topics, to display a pop-up window, and to display a list of strings for the References menu. HELPMAKE The HELPMAKE help compiler is a utility that is distributed with new Microsoft language products such as C Version 6.0. It will take a help file written in RTF, QuickHelp format, or minimally formatted ASCII and transform it into a file that can be read or manipulated by QuickHelp or an application using the Microsoft help library. You must use HELPMAKE if you are going to use the help library from within your application or if you want QuickHelp to be able to read your help files (except if you use minimally formatted ASCII files--they can be read directly, without compression, by the help engine). One of HELPMAKE's tasks is to compress the massive amount of text usually taken up by ASCII help files. Compression saves a great deal of disk space and improves the speed of the help engine. HELPMAKE can use one or more of the following compression methods: run-length compression, keyword compression, extended keyword compression, and Huffman compression. Another task of HELPMAKE is to create all the hotlinks between context strings and topic text (see Figure 6). All context strings and cross-reference strings are given a unique 4-byte context number. For instance, the word "Copy" might be given the context number of 17 and the word "Paste" might be given the context number 20. Each of these context numbers must then be mapped into the topic numbers they are associated with. For instance, if "Copy" and "Paste" both refer to the same help text, the context numbers 17 and 20 must map into the same topic number. Finally, each topic number maps into a value that represents a seek position in the help file where the compressed topic text is found. The following C statement will produce a seek position from a context number. filePos = fposTopics[ ContextMap [ ContextNumber ]]; Knowing each topic's seek position allows rapid random access to any topic no matter which topic you are currently viewing. The format of a compiled help file is shown in Figure 7. The first data structure is a file header that contains items such as the signature, the number of topics, and the number of contexts. Following that is an index of the seek positions of the compressed text for each of the topics. Next is a list of the context strings used in the help database. Each of these strings maps to a unique context number. Then comes an array of numbers that maps the context numbers to topic numbers. Next are the two tables used in the decompression algorithms, the keyword table and the Huffman decoding tree. The Huffman tree is used to map a sequence of bits into an 8-bit ASCII character. At the end is the actual compressed topic text. HELPMAKE can decode any help file into QuickHelp format, unless the help text developer compiled it with the locking option. Once it's in QuickHelp format, the help text can be edited with most word processors or text editors and recompiled into a binary help file. By decompiling a help file, you can integrate your own help information into an existing help database, such as the ones provided by Microsoft C Version 6.0. This allows more integration of third-party development tools into the Programmer's WorkBench environment. Help Library The Microsoft help library provides an API for the developer who wants to use the text generated by HELPMAKE within an application. The API provides routines to control multiple help file management, context lookup and topic retrieval, browsing, and help text formatting. In addition, there are some underlying routines to perform text retrieval with text decompression. This section shows you how to implement a simple help system in OS/22 using several of the major functions. The first step the application programmer must take is to open a help file. As many as 50 help files may be open simultaneously. The function to do this is: ncInitContext = HelpOpen(char far *szHelpFileName); If the specified help file was opened, the initial context number is returned. The initial context number is important for you to keep in mind, as it will be used as an anchor point for the various text retrieval routines. Once the file is open, you can search for a context string and the help topic text associated with it. In order to do this, you must ask the help "engine" to map the context string to a context number. The routine to perform this mapping is : ncContext = HelpNc(char far *szContext, nc ncInitialContext); The first argument is the context string to search for, and the second argument specifies the context at which the search begins. Most of the time, the value is the anchor point that is returned by HelpOpen. After you have the context number, you need to allocate a buffer large enough to hold the decompressed topic text, retrieve the compressed topic text, and decompress it into something that is understandable by your application. The sequence of calls to perform the topic retrieval is: nCompressedBytes = HelpNcCb(ncContext); pCompressBuffer = (PB) MyAllocate(nCompressedBytes); nDecompressedBytes = HelpLook(ncContext, (PB) pCompressBuffer); pTextBuffer = (PB) MyAllocate(nDecompressedBytes + sizeof(topichdr)); HelpDecomp((PB) pCompressBuffer, (PB) pTextBuffer, ncContext); MyFree(pCompressBuffer); You must provide the help engine a buffer to place the compressed topic text in and an additional buffer for the uncompressed text (plus a small header that contains information about the uncompressed text). HelpNcCb tells you the number of bytes that the compressed topic text takes. Using this value, you can allocate a buffer of the right size to hold the compressed text and call HelpLook to retrieve the compressed text. HelpLook returns the number of bytes that the uncompressed text will occupy. This value is used to allocate a second buffer to hold the uncompressed text plus the topic header. Finally, HelpDecomp will decompress the topic text and transfer it into the specified buffer. Now that you have the topic text, you will probably want to display it on the screen in a cohesive manner. Because the help engine is simply a text retrieval tool, it does not have any information about the application that contains it. Therefore, screen display and screen management routines are up to you. However, the help engine does contain routines to return the uncompressed help text to your application, one line at a time. You can retrieve the color attribute information for each line also. HelpGetLine retrieves the help text only; HelpGetCells retrieves the help text and the physical color attributes each character should be displayed with: nBytesTransferred = HelpGetLine( ushort iLineNumber, ushort cbMaxBytes, char far *szDestination, PB pTextBuffer); nBytesTransferred = HelpGetCells( ushort iLineNumber, ushort cbMaxBytes, char far *szDestination, PB pTextBuffer, unsigned char far *szAttributes); HelpGetCells will map the logical text attributes (underlined, bold, and so on) into screen colors. The help engine also provides a routine to retrieve only the attribute information associated with a line (HelpGetLineAttr) and a routine to find out the number of lines in the decompressed topic text (HelpcLines). HelpcLines is useful in creating a window that is the exact size of the topic text. The help text is now on the screen, so most of your job is finished. The final step is to allow the user to browse through the help database. Any simple help system interface will allow the user to move sequentially forward and backward through the help database by topic. The Microsoft help library assists you in implementing this by providing some useful functions. HelpNcNext takes a context number as an argument and returns the context number of the next help topic that physically follows the passed context in the help database. Similarly, HelpNcPrev returns the context number of the help topic that is before the passed context. ncNextContext = HelpNcNext(ncCurrentContext); ncPrevContext = HelpNcPrev(ncCurrentContext); HelpNcRecord(ncCurrentContext); ncPrevContext = HelpNcBack; The help engine also maintains a back-trace list that can keep track of the topics the user has asked for help on. The HelpNcRecord function places a context number on this stack and the HelpNcBack returns the context number which is on the top of the stack. Hyperlinks Revisited The final step in putting your help system together is allowing the user to traverse the hyperlinks. In our sample help system, we would like the user to invoke a hyperlink by selecting a hyperlinked string with the mouse or by tabbing through the strings with the left and right arrow keys and pressing the ENTER key. The help engine provides two functions to help you navigate through hyperlinked information. Both functions use a hotspot structure to map a point in the help text to a cross-reference string. The hotspot structure looks like this: typedef struct { /* the line number with the xref */ ushort line; /* the starting column of xref */ ushort col; /* the ending column of xref */ ushort ecol; /* pointer to the xref string */ uchar far *pXref; } hotspot; The HelpXRef function takes a pointer to the help text and a hotspot structure as arguments and returns a context string. Before calling HelpXRef, you must fill the hotspot structure with the line number and starting column. If the row and column position corresponds to a hyperlinked string, the cross reference for that string is returned: hotspot hs; ■ ■ ■ hs.line = CurrTopic.cursor.iCurrLine; hs.col = CurrTopic.cursor.iCurrColumn; if ((HelpXRef(CurrTopic.pbBuffer, (hotspot far *) &hs)) != NULL) { /* We have a cross-referenced string */ uchar far *szXref = hs.pXref; /* Invoke help system on cross-ref'ed string */ MyDisplayHelpTopic(szXref); } else MessageBox("No help text", "for that topic", NULL, "Error!", MB_OK); To assist the user in navigating through the hotlinks associated with a screen of help text, the help engine provides the HelpHlNext function that, given a hotspot structure, returns the position of the next or previous hotlink. rc = HelpHlNext(int clead, PB pbTopic, hotspot far *pHotSpot); The first argument controls whether the topic text is searched forward or backward for the next or previous hotlink or whether it searches forward or backward for a hotlink beginning with a certain character. Finishing Up When you are finished using a help file, close it by calling HelpClose. HelpClose(ncContext); The help file that is associated with the context string is closed and all memory allocated to process that help file is released. The functions discussed here are all you need to implement a sophisticated help system for your application--if you are programming under OS/2. If you are developing a DOS application, you must provide some call-back functions. These call-back functions are involved with opening and closing files, reading data from a file, allocating and deallocating memory, and locking and unlocking memory. The help library imposes this burden upon the programmer because of the limited resources available under DOS. If the help engine were to use the standard malloc or _dos_allocmem functions to obtain memory for itself, it would probably find itself running out of memory if it were embedded into a large program, or possibly conflicting with the program's own memory management scheme. For example, many large programs manage their memory using a virtual memory management scheme. By forcing the application to provide call-back functions to control memory allocation, the help engine never intrudes on the application's own memory management scheme. These call backs are : /* allocate nBytes of memory */ handle = HelpAlloc(nBytes) /* release the memory pointed to by handle */ HelpDealloc(handle) /* map handle to a far address */ char far *HelpLock(handle) /* unlocks the memory pointed to by handle */ HelpUnlock(handle) /* open a file on the path */ handle = OpenFileOnPath(szFilename, fWrite) /* close a file */ CloseFile(handle) /* read nbytes from a file into buf */ ReadHelpFile(handle, fpos, buf, nbytes) The final consideration in deciding to use the help engine is how much extra code it costs. Depending on the memory model and environment used, the extra code varies between 4Kb and 6Kb bytes. In addition, the application generally requires about 4Kb of code to interface the engine to its own environment. The help engine takes about 100 bytes of static data and 115 bytes for each open help file. A friendly user interface to HELPMAKE called HELPCOMP.EXE is available on any bulletin board containing MSJ files (see the inside back cover for a list). The HELPCOMP utility displays dialog boxes that let you choose among the various HELPMAKE options for encoding or decoding a help file. It also allows you to save your options in a state file that resembles the state files used by the Programmer's WorkBench. HELPCOMP is the basis for an integrated environment that lets you construct and test help files. Microsoft's help technology is defining a new standard for help systems. The HELPMAKE help compiler together with the help library allows software developers to embed sophisticated on-line help systems into their products with little overhead. Figure 2 Context String Associated Topic h.default The default help topic, typically displayed when the user requests help at the "top level" in the application. The topic is generally devoted to information on using help. h.contents The help topic displayed when the user requests a table of contents for a help database. h.index The help topic displayed when the user requests an index for a help database. Figure 3 In the help file, you have defined .context Selecting Lines You can use a \bmouse\b\vmouse_def\v to select lines. .context mouse_def A mouse is an input device. Displayed on the screen: You can use a mouse to select lines. After clicking on the word mouse the text changes to: A mouse is an input device. Figure 4 Formatting flags are used to highlight parts of the help file and to mark links in the help text. Each formatting flag consists of a backslash character (\) followed by a single character. To insert a backslash character without having it interpreted as a formatting flag, use two backslash characters (\\). The following flags are used to change the highlighting of text: Flag Action \b Toggles boldface on or off \i Toggles italics \u Toggles underlining \p Turns off all attributes Two formatting flags are used to define explicit links: Flag Action \a Anchors text-defining hotspots \v Toggles invisibility around link Figure 5 Dot Command Action .comment <string> Comments your source file. The entire line is ignored. Comments are useful for documenting the purpose of a link. .context <string> Defines a context string for a topic. .end Ends a paste section. .freeze <n> "Freezes" <n> lines at the top of the topic. Frozen lines do not scroll. This code can be used to prevent a screen title or row of "buttons" from scrolling out of view when the user scrolls the text in the help topic. .length <n> Sizes the help window to <n> vertical lines. The line with this code should follow the last context definition for the topic. .list Indicates that the current topic contains a list of topics. QuickHelp will display a list box of these topics and it allows you to choose the topic to view. .paste <pastename> Begins a paste section. The <pastename> appears in the QuickHelp Paste menu. .popup Tells the help system to display the current topic as a pop-up window instead of a scrollable list box. .ref <string(s)> Displays the list of <string> topics in the References menu. If you are listing more than one topic, separate each with a comma. .topic <text> Defines <text> as the name of the topic. The application may use this name to title the help window when displaying the topic. The line with this code should follow the last context definition for the topic. An In-depth Exploration of the PC Keyboard and its Interrupt Service Routines Michael J. Mefford From the time you strike a key on your keyboard to the time an application receives the keystroke and displays a character on the screen, a whole sequence of events takes place. This article begins with an overview of the interrupts that direct keyboard events. It then discusses the hardware and programming particulars concerning the keyboard and its interrupts. (Some understanding of hardware terminology will be presumed.) Next, it presents some programming examples that illustrate the inner workings of the keyboard. Finally the article presents several utilities to enhance the functionality of your keyboard. In most applications, as soon as you press a key a character instantly appears on the screen. This response suggests that there is a simple direct connection between the keyboard and the monitor. Actually, a myriad of components are involved and a flurry of activity takes place with each keystroke. The first step in understanding keystroke processing is knowing how the PC's interrupt-handling system works. An interrupt, as its name implies, causes the CPU to stop what it is doing and do something else. The 80x86 family of CPUs provides two fundamentally different types, software and hardware interrupts. A hardware interrupt occurs whenever a hardware device needs service. A program has little control over when the interruption occurs short of totally disabling it, which is rarely done. In fact, most programs are not even aware of the brief interruptions caused by hardware. A software interrupt is an elective interrupt initiated from within a program itself. A software interrupt can be thought of as a program choosing to interrupt itself in order to execute a subroutine or a short specialty program outside of its own code. Raising a software interrupt is similar to calling a function, except that you don't have to write the function yourself. One of the primary purposes of the operating system (DOS1 or OS/22) is to provide a standard set of software interrupt service routines so that an application doesn't have to write its own set of instructions for tasks common to all programs. When you press a key on your PC's keyboard, the keyboard's circuitry informs the CPU that it has a keystroke that should be processed immediately. This is because the keyboard can remember only about four consecutive keystrokes. If the keyboard can't pass on these keystrokes, it will lose them and "raise a flag." To avoid this, the CPU saves its place in its current task (for example, recalculating a spreadsheet) and runs a special program for processing keystrokes. This program is called INT 9H, the keyboard hardware interrupt. Because keystroke processing is such a basic system function, INT 9H is built into ROM as part of the BIOS. INT 9H Interrupt The main function of the INT 9H BIOS program is to get a keystroke from the keyboard via I/O Port A, interpret it, and store it in a keyboard buffer. The keyboard buffer is in low memory in a reserved part of RAM called the BIOS data area. The keystroke has to be interpreted, because all an interrupt handler program gets from the keyboard is a number called a scan code that represents the physical key that was pressed or released. For example, when you press the "A" key, the number 1EH is sent. A lookup table tells INT 9H that this is the A key. INT 9H also looks at the states of other keys, such as the Caps Lock and right and left Shift keys, to determine whether the keystroke should be stored as a lowercase or uppercase A. Both the scan code and its ASCII interpretation are then stored in the keyboard buffer. Once the keystroke is stored in the buffer, the INT 9H program terminates. The CPU then picks up exactly where it left off when it was interrupted. The system is restored so well that the interrupted program has no idea that it was momentarily frozen. If INT 9H works so transparently, how does an application know that a keystroke is waiting for it in the buffer? INT 16H, the keyboard software interrupt, tells an application if there are any keystrokes available in the BIOS data area's keyboard buffer. INT 16H Interrupt Hardware interrupt INT 9H has only one duty: to interpret and buffer keystrokes. Software interrupt INT 16H has more flexibility, offering three subservices numbered 0-2. An application designates which subservice it desires by passing the appropriate function number to INT 16H in the CPU's AH register. The first service (function 0) returns keystrokes from the keyboard buffer to the application. The problem with function 0 is that the buffer will be empty more often than not, and INT 16H will not return empty. Rather, function 0 sits in a loop wasting valuable CPU time waiting until a keystroke appears in the buffer. This takes us back to INT 9H. Whenever you press a key, an INT 9H is generated that interrupts the INT 16H function 0 buffer watch and transparently makes a deposit in the buffer while INT 16H is suspended. When INT 9H finishes, INT 16H suddenly finds the keystroke and returns to the application. Function 1 of INT 16H is more efficient. It lets an application inquire if there are any keystrokes in the buffer, immediately returning with a simple "yes" or "no." If the buffer is empty the application quickly regains control, which is a more efficient process. All an application has to do is periodically use function 1. If it returns with a "yes," the application can immediately retrieve the character via function 0. The last function of INT 16H, subservice 2, returns the current shift status information for the keys Caps Lock, Insert, Num Lock, Scroll Lock, Alt, Ctrl, and left and right Shift. We'll return to this function later. The difference between INT 9H and INT 16H is, therefore, that INT 9H is a hardware interrupt provided by the BIOS and used exclusively by the keyboard for storing keystrokes, whereas INT 16H is a software interrupt provided by the ROM BIOS and used by application programs for retrieving stored keystrokes. Interrupts in Greater Detail Since the interrupt concept is the key to understanding how the keyboard processes function, let's take a closer look at how interrupts actually work. The Intel 80x86 family of CPUs perform the same sequence of operations for the software interrupt (INT) instruction as for a hardware-generated interrupt. Both give temporary control to a program known as an interrupt handler. The INT instruction has a complementary instruction, IRET, used by the interrupt handler to return control to the interrupted program. To see how this is done, let's first look at the INT instruction. When an INT instruction occurs, whether it is initiated externally by the hardware or internally by a program, the CPU preserves the current state of the FLAGS register by pushing it on the stack. It then clears the interrupt flag (IF) and trap flag (TP) to 0. Clearing the IF has the same effect as a Clear Interrrupts (CLI) instruction--it disables interrupts and ensures that the CPU is not interrupted until the INT procedure performs its critical task. An example of a time-critical task is retrieving a character from a communications port and buffering it. An interrupt handler should enable interrupts as soon as it is safe to do so, using a Set Interrupts (STI) instruction so other pending time-critical interrupts can occur. An STI can be omitted, however, if the handler's code is very short. Next, the CPU preserves the current Code Segment (CS) register by pushing it on the stack. The CS is then loaded with the segment address of the handler in the interrupt vector table. The interrupt vector table of addresses is kept at the bottom of memory. The table is indexed by multiplying the INT number by 4 and then adding 2. The 2 is added to comply with the backward storage of numbers used by Intel, namely, offset then segment. The current Program Counter (PC) or Instruction Pointer (IP) is preserved by pushing it on the stack. The IP is loaded with the value of the offset address of the handler found in the interrupt vector table. The table is indexed by multiplying the INT number by 4. This process gives control to the procedure pointed to by the new CS:IP pair, the interrupt program. As mentioned, one of the first things the interrupt handler usually does is turn interrupts back on with an STI. Any registers that may be modified by the handler are then preserved by pushing them on the stack. The interrupt goes about its business; when it is done it reinstates the registers by popping the values back off the stack. If the handler is a hardware interrupt, it informs the Programmable Interrupt Controller (PIC) that the interrupt is about to end by sending it an End of Interrupt (EOI). The last thing the handler does is issue an IRET instruction. An IRET gives control back to the interrupted process by doing the opposite of an INT instruction. Specifically, the top two bytes (one word) are popped into the program counter. This is the offset of the next instruction to be executed. The next word is popped into the code segment register, which is the segment of the next instruction to be executed. The last word is popped into the flag's register. This process gives control back to the program at the same place it was interrupted. Since the state of the machine (especially the registers) is restored to what it was, the interrupted program isn't aware that it was suspended and continues without missing a beat. The interrupt vector lookup table technique provides a very clean way to make modifications to the interrupt programs without changing the way the CPU functions. All that has to be done is to change the interrupt's entry point address in the table. The most common change in interrupt service programs happens when you upgrade an operating system. DOS Version 3.x, for example, has different interrupt vectors for system services than DOS 2.x. Similarly, the BIOS interrupt services of an IBM(R) PC/AT(R) and clone PC will be different, but the CPU finds the correct address in both cases. INT 9H in Depth Recall that all keyboard events can be divided into two interrupts--the hardware INT 9H responsible for buffering keystrokes from the keyboard and the software INT 16H that retrieves those keystrokes from the buffer and delivers them to your application. Let's consider the hardware INT 9H in depth (see Figure 1). Almost all PC keyboards contain the 8048 chip. The 8048 is a self-contained computer consisting of an 8-bit microprocessor, about 64 bytes of RAM, a ROM with the keyboard scan code assignments, and a BIOS. When you press a key (a key make) or release a key (a key break), the 8048 looks in its ROM for the scan code assigned to that key. The scan code is not the same as the ASCII value of the key; the "A" key, for example, has a scan code of 1EH. INT 9H is responsible for decoding the scan code 1EH into an ASCII code of 41H for an uppercase A and a 61H for a lowercase a. A key break is distinguished from a key make of the same key by setting the scan code's high bit. After the correct scan code is assembled, the 8048 prepares to send the 1-byte code serially to the LS322 support chip within the PC's system unit. (The chips inside your computer might not have the same numbers mentioned here--while different classes of machines and clone manufacturers may use different chips, the net effect is the same.) The scan code is sent as long as the 8048 is given a green light by the S74 chip in the system unit, acknowledging that the last code that was sent has been read and deciphered by INT 9H. If the code can't be sent right away, it is buffered into a RAM buffer in the keyboard's 8048, which I'll call the scan code buffer. (This buffer should not be confused with the 16-character circular buffer in the system unit's BIOS area, where INT 9H stores characters.) If the 8048's scan code buffer is full, an overrun occurs and the scan code data is lost. In that case, the 8048 informs the system unit of the problem by sending a special scan code of FFH, and the INT 9H handler notifies the user with a beep. An overrun, however, is not very likely. Even if you're an explosive typist, the chances are next to nil that you can cause an overrun short of pressing several keys at once with the palm of your hand. The real purpose of the overrun is to detect the event of something like a book dropping on the keyboard. The beep the BIOS issues, by the way, is the same tone emitted by a stuffed BIOS data keyboard buffer. In contrast, a full keyboard buffer can easily happen if an application is slow in processing keystrokes. Returning to our example: after the keyboard serially transmits the scan code for A, 1EH, it is received by the LS322 chip, an 8-bit shift register in the system unit (see step 1 and the following steps in Figure 1). The scan code is sent serially. The data is prefaced with a "start" bit, for a total of 9 bits sent. The receiving shift register has an initial value of 0. (It was cleared at the end of the last key processing.) As each bit is received, including the start bit, room is made for it by shifting the previous 8 bits of the shift register left by one. When the final data bit is received, the shifting and storage will have reassembled the scan code back to its original 1 byte unit. The shift register received a total of 9 bits, but it can hold only 8. Therefore, the shifting of the last data bit will have pushed that first start bit off the end. The start bit is not lost, however. It is used to set a flip-flop in the S74 support chip high 2, which triggers the Interrupt Request (IRQ1) line of INT 9H to go high . This flip-flop simultaneously sends a low signal to the 8048 in the keyboard, a "red light" signal back on the same data line that sent the scan code. This suspends any further data transfer by the keyboard and puts the 8048 into the buffer mode mentioned earlier. An IRQ is a request from an external device, such as a keyboard, for the CPU's attention. The 8259A PIC is the mediator of IRQs and decides, on a priority basis, what will get serviced first in case of multiple requests for service. The PIC has eight IRQ lines. (The PIC, however, is not limited to eight IRQs. The IRQs can be chained together, with one PIC as the master and the chained PICs as slaves. The AT and PS/2 take advantage of a slave PIC to add additional IRQ lines.) When the PIC receives the IRQ it sets a bit in the Interrupt Request Register (IRR) of the PIC representing that IRQ. The IRR is an 8-bit register, one bit for each IRQ, used to keep track of which IRQs need servicing. The priority resolver of the PIC uses this register to pick the IRQ of highest priority to be serviced. As an example, assume the keyboard IRQ1 is the only one needing service. The PIC sets its INT line high 4, which passes on the keyboard's service request by setting the CPU's Interrupt Request (INTR) line high. The CPU will grant the request as long as interrupts have previously been enabled with a STI instruction. Interrupts enabled is the normal state of the processor; they are disabled with a CLI only when a program needs to do a critical task like changing the Stack Pointer or reading a communications port. An interrupt itself, which is about to happen here, will also disable interrupts. Assume the CPU will accept an interrupt. The CPU finishes its current instruction and then acknowledges the PIC's interrupt request indirectly by sending a three bit signal to the 8288 Bus Controller (BC, as shown in 5). The BC makes sure data gets to its destination. The 3-bit CPU code can control seven types of requests to the BC, including the signal that the CPU intends to receive or send data to memory or a port. That is, it distinguishes between IN or OUT instructions intended for a port and a MOV instruction targeted to or from memory. In the case of an interrupt request, the 3-bit signal sent by the CPU is the code for an acknowledge signal ("green light") to be routed by the 8288 BC back to the 8259A through the 1-bit Interrupt Acknowledge (INTA) line 6. The first INTA sent to the 8259A is used for timing purposes, similar to the start bit of serial communications. It takes another 3-bit acknowledge from the CPU to the BC, which sends another INTA to 8259A to begin the next set of events. The 8259A next sends the interrupt vector number, INT 9H in the case of IRQ1, to the CPU -. It also sets the IRQ's In-Service Register (ISR), another 8-bit register, so the PIC can keep track of which interrupt is being serviced. How did the 8259A know that an IRQ1 was to map to a 9? The 8259A knows to send the CPU a 9 for an IRQ of 1 because it is programmed with several parameters as part of the boot process. One of those parameters is the top 5 bits of a byte representing the interrupt number to be passed to the CPU. The bottom 3 bits are programmed to the number of the IRQ when the IRQ occurs. With these 3 bottom bits you can represent neatly all of the 8 IRQs--IRQ0 through IRQ7. The boot process programs the PIC's top 5 bits to 00001B. Thus, an IRQ of 1 tacks on a bottom 3 bits of 001B, making the total byte 00001001B or 9H. The 8 IRQs map into INT 8H through INT FH (15 decimal). Now the CPU uses the interrupt number as an index to the vector table containing the address of the interrupt program, as I previously discussed. Since the vector table is at the bottom of memory, the INT 0's vector can be found at the first memory address (0000:0000). Each address or vector takes 4 bytes: 2 for the segment and 2 for the offset. To find the vector associated with the interrupt, the CPU multiplies the interrupt number by 4 and uses the result as the index into the vector table. This address is loaded into the CS:IP of the CPU, effectively giving control to INT 9H. Finally, the interrupt 9H is in progress. The first thing the INT 9H BIOS program has to do is retrieve the scan code. Remember, the scan code is in the shift register, LS322. INT 9H doesn't have direct access to this chip and the scan code. But the LS322 has sent the scan code, on 8 parallel lines (instead of serially), to Port B (address at 60H) of another chip called the 8255-A Programmable Peripheral Interface (PPI). Besides handling keyboard functions, the 3-port PPI reads the PC's system board switches and controls its speaker. For INT 9H, it serves as the source of the scan code 8. The INT 9H program reads the scan code from Port A, interprets it into an ASCII character, and stores the character with the scan code in the keyboard buffer. Note that INT 9H is not the only program that can read Port A. Since reading this port, unlike many ports, does not destroy the scan code, any program can safely read it. Now that INT 9H has read the scan code, it is safe to open the gate. This is done by momentarily setting bit 7 of Port B of the PPI high (see 9 and Figure 2 for the assembly language instructions.) Bit 7 is connected to the "clear" line of both the LS322 and S74. The LS322 clear line zeros out the shift register so it will be ready to receive the next scan code. The S74 clear line resets the IRQ1 flip-flop, which in turn informs the keyboard's 8048 that it may now send a new scan code. Once the scan code and its ASCII equivalent have been stored in the keyboard buffer and the keyboard is reenabled, the last chore of INT 9H is to tell the PIC that it has finished its task and is about to return control to the program it interrupted 10. This is done by sending an EOI encoded as 20H to port 20H of the PIC. The 20H for both the code and the port is entirely coincidental. For more on this, see the sidebar "How an Interrupt Handler Resets the Programmable Interrupt Controller." The EOI signal resets the ISR and IRR bit associated with IRQ1, reenabling that request line. The PIC can now attend to waiting hardware interrupts of lower priority, including another IRQ1. Now that INT 9H has finished its job, it passes control back to the interrupted program by issuing an IRET instruction. For more information on the 8259A, see the Peripheral Handbook (Intel, 1990), available from Intel Literature at (800) 548-4725. Keyboard Scan Codes As I've mentioned, INT 9H interprets the scan code that arrives at Port A of the 8255-A PPI. In fact, INT 9H is called into action every time there is any keyboard activity. This includes key presses, key releases, and typematic responses issued when you hold down a key. The keyboard simply sends a scan code for the pressed key to the PC and INT 9H. INT 9H determines how it should be interpreted. The BIOS INT 9H deciphers the scan codes sent by the keyboard into meaningful ASCII characters that can be used by an application program. One thing to keep in mind is that INT 9H is hardware-specific, so the interrupt program for the 83-key keyboard is different than the 101-key extended keyboard version. The latter obviously needs additional code to cope with the additional keys. To understand scan codes, refer to the PORT-A.ASM program (see Figure 3), which displays in real time the scan codes sent by the keyboard as you press and release keys. (The keyboard delivers scan codes to Port A, address 60H, of the PPI.) PORT-A intercepts the scan codes before INT 9H gets to them by "hooking" INT 9H. Hooking an interrupt means replacing an interrupt handler's address in the interrupt vector table with your address. The original address is saved so that once PORT-A, or any hooked interrupt program, is done, it can pass control to the former interrupt handler. Passing control in the case of an INT 9H handler gives the BIOS INT 9H keyboard program a chance at the scan code. (Although hooking an interrupt is how TSRs get control of the PC when you press a hot key, PORT-A.ASM is not a TSR.) When you press Esc, PORT-A will put the vector table back to the way it was, replacing the saved address of the previous owner and returning you to the DOS prompt. I'll leave the rest of the programming particulars of PORT-A to the comments found in the assembly listing. You can assemble the source code listed, but the easiest way to obtain PORT-A.ASM as well as the other programs mentioned in this article is from one of the bulletin boards listed on the inside back cover. When you run PORT-A, whenever you press a key, the scan code for that key make will appear on the screen. When you release the same key break, another scan code will appear. For the keys in the 83-key keyboard, the key break will be the key make plus 80H (1000 0000B), which sets the high bit of the scan code. This is also true for most of the same keys of the 101-key keyboard, but for the new keys--dedicated cursor keys and some key combinations--a series of scan codes is sent, some of which have the high bit set for a key make. Before the 101-key keyboard, programmers could easily distinguish a key make from a key break by checking the high bit of the scan code. That approach does not work with the 101-key keyboard. Programmers may find PORT-A a valuable tool to determine what scan codes an INT 9H interceptor program can expect when the user presses specific keys. Running PORT-A is self-explanatory; the meaning of its scan code output may be obscure at first but will become clear once INT 9H's operation is understood. I will therefore examine INT 9H's decoding logic step by step. The scan code INT 9H receives from reading Port A is, for the most part, a number representing the relative position of the key on the keyboard. Thus, the scan code 01H is for the Esc key, 02H for the !/1 key, 03H for the @/2 key, and so on, left to right on the keyboard. (Unlike most computer numbering schemes, there is no key associated with a 0 scan code. INT 9H uses 0 as a pseudo-scan code to mean Ctrl-Break.) When you get to the Backspace key, the sequential numbering resumes with the Tab key of the QWERTY row. After the typewriter-style keys, the numbering system continues with the function keys, followed by the keypad. Some keys, such as the Esc and function keys, are in different places on different keyboards, so for them the numbering system does not make sense. To remain compatible, the scan codes remain the same but remember, the scan code has nothing to do with the ASCII value of the key. INT 9H has a lookup table to decode scan codes. A series of decision trees are followed as INT 9H looks for a match for the scan code in the table. Once the scan code is deciphered, INT 9H stores the ASCII character interpretation along with the scan code in the keyboard buffer and returns control to the interrupted program. Before the translation process begins, however, INT 9H checks to see if the scan code is FFH. As I mentioned, FFH is the keyboard's code signaling that an overrun has occurred: INT 9H warns you of this with a beep. Assuming no overrun, INT 9H checks the translation table in a logical order for a matching scan code. The first keys checked are the function-altering shift keys: right and left Shift, Ctrl, and Alt; and the four toggle keys: Insert, Caps Lock, Num Lock, and Scroll Lock. If the scan code is for any of the four shift state keys, it is noted by setting a bit in the KD_FLAG info byte found at 40:17H of the BIOS data area (see Figure 4). When a key break scan code of a shift key is detected, the appropriate bit is reset back to 0. This method enables INT 9H to know if any of the shift keys are depressed. Shift Keys The best way to illustrate the logic of the Shift keys is to watch what happens before and after INT 9H decodes the scan code. First let's observe the after, or processed, scan code by watching what happens when you press a few keys at the DOS prompt. At the DOS prompt, press and hold down the left Shift key. Nothing seems to happen. While you are still holding the left Shift, press and hold the "A" key. A capital A will appear as expected (assuming Caps Lock is not on) and will quickly be followed by a stream of capital A's from the typematic response of the keyboard. In the middle of the capital A output, release the left Shift but continue to hold down the A key. The capital A will change to a lowercase "a" and the typematic response will continue. With the aid of PORT-A, you can see what is actually happening with the scan codes. Press Esc to cancel the meaningless AAAAAaaaaaa command and run PORT-A.COM. While PORT-A is running, repeat what you did at the DOS prompt, starting by holding down the left Shift. A scan code of 2AH will appear and quickly start to repeat. (The keyboard starts a typematic response after about a half-second delay. This is true for all keys, including the Shift keys.) Still keeping the left Shift depressed, press the A key. The stream of 2AH shift codes will be replaced by 1EH--the scan code for the A key. (A typematic response stops when the keyboard detects that another key has been depressed; if the second key is held, it becomes the new typematic key.) Next, release the left Shift key but not the A key. One AAH scan code will appear in the stream of the A key's 1EH scan codes, then the typematic 1EH's will continue. Note that, unlike what happened at the DOS prompt when the uppercase A changed to an lowercase a, the same scan code (1EH) for the A key is displayed after the left Shift is released. Release the A key; the output will end with a 9EH, the key break (1EH + 80H = 9EH). As you saw, INT 9H interprets the scan codes into an uppercase, lowercase response. What actually happens is that when you hold down the left Shift, INT 9H sees the 2A key make, turns on (using the bitwise OR operator) the left shift bit (bit 1) of the KD_FLAG, then exits. The same logic takes place with each subsequent typematic left Shift response. The continuous turning on of the same bit causes no additional change in the key depressed state. When you change the input by pressing the A key, INT 9H finds the 1EH scan code that matches a lowercase a. At the same time, INT 9H checks the state of the two Shift keys by looking at the KD_FLAG; it finds that the left Shift key is still depressed, so the "a" is capitalized. Before storing the uppercase A in the keyboard buffer, INT 9H checks the Caps Lock bit (7) of KD_FLAG. If Caps Lock is off, INT 9H stores the uppercase A as is and exits. If it's locked, it changes the A back to lowercase, effectively canceling the Caps Lock state. Thus, when you release the left Shift key and INT 9H sees the scan code AAH, it knows that the left Shift has just been released (AAH = 2AH + 80H, a left Shift key break scan code). INT 9H turns off the KD_FLAG left shift bit. With the next typematic 1EH scan code "a," INT 9H finds that the left Shift is no longer depressed (neither is the right Shift nor is Caps Lock locked), so the lowercase "a" matched with the 1EH is stored as is. This same function-altering logic works with the numeric keypad. If Num Lock is on, pressing either the left or right Shift key before pressing a number key on the keypad will undo the status, thereby changing the keys back to cursor keys. This flexibility is great for 83-key keyboard users who normally leave the keypad in Num Lock. (When it's necessary to use the keypad for cursor control, instead of toggling the Num Lock key, you can hold down the left Shift key and massage the cursor keys as usual. Just the opposite can be done if you use the keypad mainly for cursor control and wish to enter numbers temporarily.) Of course with the 101-key keyboard, there are both dedicated cursor control keys and a keypad. The machines with these keyboards generally boot with Num Lock on, so the keypad is ready for numeric entry. For some of us, however, habits die hard and you still reach for the keypad for cursor control. Since the extended keyboard boots in Num Lock, invariably the first cursor control attempt results instead in a string of numbers across the screen. For the 83-key keyboard and Num Lock advocate, the same frustration occurs in reverse. You can, however, toggle these keys from a batch file, such as AUTOEXEC.BAT (see Figure 5). Toggle Keys The logic for the four toggle keys (Insert, Caps Lock, Num Lock, and Scroll Lock) works somewhat differently than the Shift keys. For the Caps Lock, when INT 9H sees the scan code for one of the toggle keys it prepares to set (using the bitwise OR operator) the appropriate bit in KD_FLAG1. (Since all the KD_FLAG bits are already assigned, a second byte is used to hold the state of these keys; see Figure 6.) Before it is set, however, INT 9H checks to see if the bit is already set. If it is, INT 9H knows that this key make is a typematic response and exits without taking action. You can see from PORT-A that if you press and hold Caps Lock, it reacts like all the other keys, sending multiple scan codes from the typematic response. Without the additional logic for toggle keys, the typematic would cause uncontrolled and undesirable toggling. If the bit is not already set, it is set now, indicating the key is depressed. Also, the bit in KD_FLAG is flipped to indicate a change in status. This is done by XORing the bit (bit 6 in the case of Caps Lock) in the KD_FLAG, having the effect of toggling the bit. To see the KD_FLAGs on your machine, read the sidebar "Examining the Keyboard Shift Status and Shift State Bytes." Alt Input INT 9H gives special consideration to the Alt and keypad combinations. If Alt is pressed (INT 9H can tell from bit 3 of KD_FLAG), and if the scan code for one of the number keys of the keypad is detected, INT 9H adds the number to a byte at 40:19 of the BIOS data area--ALT_INPUT. The previous ALT_INPUT is multiplied by 10 (to move the decimal point to the right one position) before the new number is added. When the Alt key is released, this number will be interpreted as a decimal ASCII equivalent and placed in the keyboard buffer. As an example, from the DOS prompt, press and hold down the Alt key. Next, press and release the 6; then press and release the 5 on the keypad. Finally, release the Alt key; an "A," the ASCII of 65, will appear. When Alt is released, INT 9H resets ALT_INPUT back to 0. If ALT_INPUT is 0 when you release Alt, no input is assumed, which explains why you can't enter a null (ASCII 0) with this method. This alternate input method only works with the keypad, not with the number keys on the top row. The status of the keypad's Num Lock does not matter. Using the Alt input method is handy for entering the ASCII control characters below the space and the high bit characters, especially box and line drawing characters. Hold State If a match is not found for any of the Shift or toggle keys, INT 9H next checks bit 3 of KD_FLAG1 to see if Pause is active. If it is set, meaning the machine is frozen, it is reset and INT 9H exits. Bit 3 is set by pressing Ctrl-Num Lock on the 83-key keyboard or the dedicated Pause key on the 101-key keyboard. To freeze the machine, INT 9H goes into a loop after the pause bit is set and waits until the bit is reset. Resetting occurs when you press any key except any of the shift keys. The placement of the shift keys' tests before the pause release explains why none of the shift keys will end the pause. For example, enter the DOS DIR command, then quickly press Ctrl-Num Lock on an 83-key keyboard or Pause on a 101-key keyboard to freeze the output. (Be sure to ask for a large directory.) Now press any of the shift keys. The shift state will change, but DOS will not continue the DIR listing. Entering Ctrl-S (^S) will also pause a DIR output. This, however, is not an INT 9H implementation. Ctrl-S is processed and placed in the keyboard buffer by INT 9H like any other character and it's DOS that interprets it as a pause command. Ctrl-Alt-Del The Ctrl-Alt-Del key combination also has special meaning. If found, a value of 1234H is placed in the RESET_FLAG found at memory location 40:72H of the BIOS data area and a direct jump is made to the initialization procedure that gets control when you turn the power on. The boot program skips the power-on self-test if the RESET_FLAG has the value of 1234H, resulting in a warm boot. If you look at the KD_FLAG you'll notice there is a bit reserved to indicate the state of Ctrl and Alt but none for Del. Nor is there a Del key state bit in KD_FLAG1. This means INT 9H has no way of remembering if the Del key is depressed. Therefore to get a warm boot, you have to press and hold Ctrl and Alt before pressing Del. It does not matter in which order Ctrl and Alt are pressed, as long as Del is pressed last. Ctrl-Break If Ctrl-Break is detected, INT 9H does several things. First it clears the keyboard buffer by setting the head to the tail. INT 9H then sets the high bit of the BIOS_BREAK bit at address 40:71H of the BIOS data area to 1. Any program can later examine this bit to detect a Ctrl-Break press. Then it calls the Ctrl-Break interrupt, INT 1BH. Normally INT 1BH points to DOS, but any program can replace that address and thereby handle Ctrl-Break. If DOS is called by INT 1BH, an internal flag is set. If a program later uses certain DOS function calls and Ctrl-Break has not been turned off (the Ctrl-Break in DOS, not in INT 9H's), either by you with the DOS BREAK OFF command or by the program itself, the program is terminated. DOS displays ^C and you are returned to the DOS prompt. The last thing INT 9H does with Ctrl-Break is place a zero character and scan code in the keyboard buffer. Entering Ctrl-C (^C) is also interpreted as Ctrl-Break. This command, however, like the ^S interpretation, is not an INT 9H implementation. Ctrl-C is processed by INT 9H like any other character; it's DOS that interprets it as a break command. Print Screen If Shift-PrtScrn on the 83-key keyboard or the dedicated Print Screen key on the 101-key keyboard is detected, INT 9H simply calls INT 5H to send the screen dump to the printer. INT 9H doesn't actually do the printing. Alphanumeric If INT 9H does not find any of the special scan codes, it knows the scan code is for an alphanumeric ASCII character. For the alpha characters, this is when INT 9H checks the Caps Lock status. INT 9H then stores both the interpreted ASCII character and the scan code in the keyboard buffer. This gives an application the opportunity to make decisions based on the ASCII character or on the particular key pressed. Before anything can be stored in the keyboard buffer, however, there must be room. If there is not enough room, INT 9H beeps the speaker and exits without storing the character. Keyboard Buffer So far, you have followed a keystroke's scan code from the keyboard to a chip inside the system unit, where it is temporarily stored. We've seen how the ROM BIOS INT 9H program retrieves the scan code from the chip, converts it into an ASCII character code, and stores both the scan code and the character code in the keyboard buffer. Next, we'll examine this keyboard buffer using a program called KEYVIEW.ASM. The keyboard buffer is a classic example of a circular buffer, although it isn't actually circular and it doesn't reside in the keyboard. Its implementation gives it its name. The keyboard buffer consists of 32 consecutive, linear (not circular) byte addresses very close to the beginning of the CPU's RAM. This buffer memory is reserved by the ROM BIOS programs INT 9H and INT 16H as a communication area to store and pass keystroke information. INT 9H gets keystrokes from the keyboard and stores them in the buffer; INT 16H retrieves the keystrokes from the buffer and returns them to an application. Address 400H (102410) starts the second kilobyte of memory and is the area used by the BIOS programs for data storage of, among other things, the keyboard buffer. The buffer's memory location starts at address 41EH (105410) and ends at, but does not include, address 43EH (108610). The buffer's 32 bytes are grouped by twos, one byte for storing the INT 9H-interpreted ASCII character code followed by one byte for the scan code. The buffer start and ending word offset addresses can be thought of as an indexed distance from the start of the 400H second kilobyte of memory. These offset addresses, 001EH and 003EH, are stored in another part of the BIOS data area in two consecutive words at addresses 40:80H and 40:82H, respectively. Figure 7 shows the memory locations for the keyboard buffer and its pointers. The pointers enable INT 9H to find the keyboard buffer; they can be changed to alter the location and size of the buffer. Two additional pointers, the head and tail of the buffer, are used to keep track of what's in the buffer. These pointers are also located in the BIOS data area; they are at the consecutive offset addresses 001AH and 001CH, respectively. The KEYVIEW.ASM program (see Figure 8) displays a graphic image of the keyboard buffer and lets you see its operation as INT 9H and INT 16H store and retrieve keystrokes. The keyboard buffer is displayed as two rectangular bands, one atop the other (see Figure 9). The bands are divided into eight boxes, for a total of the 16 2-byte memory locations of the keyboard buffer. The buffer's sequential addressing moves left to right starting with the buffer start (offset 1EH) in the leftmost box of the top band and ending with the buffer in the rightmost box of the band on the second row. The bottom of each box contains the buffer's current ASCII character code to the left next to its scan code pair to the right. The ASCII char/scan code order is the same as it appears in memory. Above each char/scan code pair is the display representation of the ASCII character. It is included for reference and does not exist as part of the keyboard buffer. Programmers may find KEYVIEW a valuable tool to determine the ASCII and scan codes a program using INT 16H can expect when a user presses specific keys--especially with the extended keyboard. When you run KEYVIEW, one of the first things you will notice in the buffer is the individual letters of the KEYVIEW command, including the carriage return (an ASCII 0DH displayed as a quarter note) just as you entered them at the DOS prompt. If you have a 101-key keyboard, the carriage return will be followed by an FF FF char/scan combination, placed there by KEYVIEW during initialization to detect an enhanced keyboard. (This is the method recommended in the BIOS Technical Reference.) See the KEYVIEW assembly listing for more information. The head and tail will both be pointing to the storage location right after the carriage return (or the FF FF if you have a 101-key keyboard.) The tail points to the location at which the next keystroke will be stored, and the head points to the logical start or first stored character. When the head equals the tail, both INT 9H and INT 16H know the buffer is empty. DOS emptied the buffer when you entered the command to run KEYVIEW. By empty, I don't mean the buffer's memory is empty or null. The KEYVIEW execution command was not touched when it was retrieved from the buffer. As you will see in a moment, all that changes is the head pointer. Make sure Caps Lock is off; then press the "a" key to store a character. The ASCII code of 61H, along with the scan code of 1EH, will be stored at the tail's location. The stored keystroke will be highlighted so you can see the buffered keystroke easily. In addition, the tail will move up to the next available storage location. The head stays put, pointing to "a," the logical start of the buffer. Continue to press the "a" key until all 16 buffer slots are filled and you will discover two things. When the tail reaches the end of the buffer (the far right of the bottom band), it goes to the beginning of the buffer in the top left of the top band. This wrapping or circular pattern accounts for the keyboard buffer being called a circular buffer. You will also discover that the last box will not accept a keystroke. Instead, you will hear the same beep that is emitted when the buffer is full. This happens because the tail has circumnavigated the buffer, thereby catching up with the logical head of the buffer. If INT 9H filled this last slot, the tail would have to be moved up one, but that would place the tail at the same position as the head. Remember, when the head equals the tail it means the buffer is empty, so this last slot can never be filled. Thus, the 16-word keyboard buffer can only hold 15 characters. Now retrieve the keystrokes by pressing F1; that instructs KEYVIEW to ask INT 16H for a buffered keystroke. The character at the head will return and be displayed at the bottom of the screen. INT 16H does not physically remove or erase the character from the buffer. It just reads the memory and notes that the character has been "removed" by moving the head pointer up one. KEYVIEW also removes the highlight from the retrieved location. Continue to press F1 until all the "a's" are retrieved and the head catches up with the tail. If you press F1 requesting a read from an empty buffer, KEYVIEW will beep. This circular buffer technique very neatly implements the FIFO logic necessary to keep keystrokes in the same serial order they were struck. With KEYVIEW you can see the subtle difference a program can detect between different keys that elicit the same ASCII character. For example, press the minus key on the top row between the 0 and = keys and the minus key on the key pad. KEYVIEW will display the minus ASCII code 2DH for both keypresses, but it will display different scan codes--0CH and 4AH, respectively. These key-specific scan codes enable a program to know exactly which key you pressed. You can observe similar differences between the entire top row of number keys and the keypad numbers. (When pressing the keypad numbers be sure Num Lock is on.) The same difference can also be seen by pressing the two asterisk keys. Pressing F3 at the DOS prompt displays the last command entered. If you press F3 while running KEYVIEW, all that INT 9H stores in the buffer is 00H 3DH. F3 has no special meaning to INT 9H and is implemented as a "repeat last command" only by DOS, which explains why it won't work with in other programs. If you have an 101-key keyboard, the KEYVIEW menu will have an F2 toggle to activate/deactivate the extended INT 16H function calls. When this function is Active, KEYVIEW takes advantage of the extended INT 16H function calls for the extended keyboard support. The extended functions enable programs to distinguish between things like the dedicated and keypad cursor keys. One of the more interesting things you can observe with KEYVIEW is what INT 9H does with the buffer when you press Ctrl-Break. First, buffer some keystrokes, then press Ctrl-Break. INT 9H first clears the buffer by making the head equal to the tail. (Again, the actual memory is untouched. Only the pointers are changed.) Depending on the BIOS version, the buffer is cleared by moving the tail to the current head position or the head and tail are both moved to the start of the buffer. Then a pseudo-char/scan code of 00H 00H is stored and the tail is moved up. As I've mentioned, Ctrl-Break is an INT 9H command and Ctrl-C is a DOS command. Although they are thought to be interchangeable, you can see with KEYVIEW that INT 9H, instead of clearing the buffer, stores a Ctrl-C keypress as if it was any other character. Ctrl-C is a low ASCII control character of 3H (displayed as a heart) and has a scan code of 2EH. The same sometimes-misunderstood relationship can be seen with the INT 9H-interpreted Ctrl-Num Lock pause command on the 83-key keyboard (the same as the dedicated Pause key on the 101-key keyboard) and the DOS Ctrl-S pause command. Press either Ctrl-Num Lock or Pause, whichever applies to your keyboard; nothing is stored in the buffer as INT 9H places the machine in a pause state. Press any other key to unfreeze the machine. That key also is not stored; it merely sets things back in motion. Next, press Ctrl-S; an ASCII character 13H, a double exclamation point, is stored along with the scan code of 1FH. The machine is not frozen; only if DOS gets hold of the Ctrl-S, for example, when it's displaying a DIR listing, is Ctrl-S treated as a pause command. Special Scan Codes Although I said earlier that INT 9H stores the scan code that comes from the keyboard along with the ASCII character interpretation, this isn't entirely accurate. For some key combinations, INT 9H makes up its own scan code. For example, as you saw with PORT-A, a keyboard may send the same scan code for a key no matter what the state of the Shift keys. Thus when the !/1 key is pressed, the same scan code is sent regardless of the status of the Caps Lock or the Shift keys. If you pressed the Alt key and held it and then pressed the !/1 key, PORT-A (see Figure 3) would display the same 02H scan code for the !/1 key as when the Alt key was not depressed. With KEYVIEW, a !/1 keypress is stored as a 31H (the ASCII code for 1) along with the original 02H scan code as expected. But with an Alt !/1 keypress, INT 9H stores 00H ASCII code with a scan code of 78H. The 78H (12010) is larger than the total number of keys on a keyboard. INT 9H has converted this key combination into a special char/scan code so an application can quickly detect additional special key combinations without having to sift through the shift status byte. Both the ASCII code and the scan code have been assigned by INT 9H and do not reflect the scan code that actually came from the keyboard. The returned 00H ASCII code lets an application program know that the accompanying scan code is for a special key combination. For DOS and other high-level language keyboard function calls, the 00H code cues the program to repeat the call to get the special scan code. A partial list of other key combinations that store special scan codes are Alt, Ctrl or Shift function keys, Num Lock keypad keys, and a Shift Tab. The 101-Key Keyboard The advent of the 101-key keyboard introduced two new function keys, F11 and F12. In an effort to make these keys available for new applications, as well as to provide downward compatibility for applications written before these keys existed, the 101-key keyboard INT 16H ROM BIOS routine uses some clever logic. Programmers are familiar with the three INT 16H calls, functions 0-2, providing the Keyboard Read, Keystroke Status, and Shift Status services. The 101-key keyboard BIOS support added three new calls known as extended function calls. These new calls, 10H, 11H, and 12H, have the same function as the old 00H, 01H, 02H calls except they return specific information about the new 101-key keyboard keys, including F11 and F12. The new BIOS applies a special formula for Keyboard Read and Keystroke Status--the calls that pre-101-key keyboard applications used and most new programs still use. The old AH = 00H (Keyboard Read) and AH = 01H (Keystroke Status) INT 16H function calls return scan and character codes by converting like codes to compatible codes and extracting the scan code/character code combination until a compatible combination is found. If you use the PORT-A program, you will notice that some keys, like the dedicated cursor keys, return with a whole series of scan codes. These additional codes enable INT 9H to tell the difference between, for example, a left arrow that comes from the dedicated cursors and one that comes from the keypad. If you press these two different left arrow keys while running KEYVIEW, you will see that the same scan code is stored for both (4BH), but the dedicated left cursor has an ASCII code of E0H while the keypad has one of 00H. (The 00H null ASCII code is a signal that a special key combination like a cursor arrow has been entered. If you experiment with KEYVIEW, you will see that the E0H code is used for all the dedicated keys in a similar manner.) The extended INT 16H functions pass these special scan codes to your program so it can also detect the difference between a keypad arrow and a dedicated arrow. In the BIOS translation logic, the first rule means that an old INT 16H function call will convert a dedicated left arrow to a keypad left arrow so that an old program will not know the difference. Remember, applications before the 101-key keyboard don't know the new keys exist. You can see the translation easily with the aid of KEYVIEW. For example, buffer a couple of dedicated left arrow keypresses. What you will see stored is two E0H 4BH char/scan pairs. (With the PORT-A, program you can also see that the keyboard sends an extra E0H along with the 4BH scan code.) Now retrieve one left arrow by pressing F1 while the F2 extended support is Active. (This will only appear as a menu option if you have an extended keyboard. KEYVIEW can tell if you have a 101-key keyboard from the assembly 101-key keyboard detection routine mentioned above.) KEYVIEW will retrieve one of the left arrows using the AH = 10H extended keyboard read call and display the returned ASCII value of E0H, which looks like an odd lowercase a. That is, the extended keyboard read call returns the E0H scan code as is. Next press F2 to toggle the extended function support off or Inactive. Press F1; KEYVIEW will retrieve the second left arrow using the old INT 16H, AH = 00H function call. You will see a null that looks like a space character blank. INT 16H has converted the E0H to a 00H ASCII code, removing the distinction between the two types of arrows. Now to see what happens with the F11 and F12 keys, clear the KEYVIEW buffer, if it isn't already, by pressing F1 and making sure the F2 extended keyboard function support is toggled Active. Buffer five keystrokes by pressing the A key, F10, F11, F12, and B in that order. As expected, the tail moves as these five keystrokes are buffered. Retrieve all five keystrokes by pressing F1. Next press F2 to deactivate the extended functions; enter the same five keystrokes again--A, F10, F11, F12, B. Retrieve the keystrokes one at a time by pressing F1. The A comes back as an A and the F10 as a blank (an ASCII null scan code). But after you retrieved the F10, the head skipped over the F11 and F12, effectively removing them from the buffer, and stopped at the B. Press F1 and the B is retrieved as normal. The explanation for this seemingly bizarre behavior is that when you deactivated the extended support, KEYVIEW used the old INT 16H function calls to retrieve the characters. The old functions do not know about F11 and F12, so keystrokes were extracted and ignored by INT 16H until it found a code it did understand--the compatible B. (KEYVIEW is programmed to continually call INT 16H, function 01H or 11H, whichever is active, to find out if a keystroke is available. Calling the old 01H Status function is KEYVIEW clears the F11 and F12 keystrokes from the buffer.) While the extended keyboard support is Inactive, press F11 or F12. The keystrokes are stored, the tail moves up, and the head follows quickly. Since the head equals the tail, the buffer looks empty to an application. Any application that uses the old INT 16H BIOS calls--and most do, including newly written applications--will, unwittingly, never see an F11 or F12. Now you might think all this supports the argument that a program should go through the operating system instead of directly to the BIOS for keystrokes. I haven't talked at all about the DOS keyboard function call support and I'm not going to start now, other than to say that DOS ends up calling the BIOS INT 16H to get the keystroke information anyway, so why bother going through the operating system. (Don't get me wrong. DOS keyboard functions do have their place. For example, DOS INT 21H, function 0AH, provides an easy way to implement buffered keyboard input. This and all the other DOS functions are amply described in the DOS Technical Reference.) There's a point I want to make regarding DOS. No version of DOS, not even DOS Version 3.3 or DOS 4.0, is aware of the F11 and F12 keys. DOS uses the old INT 16H calls. This can be demonstrated by entering the DOS PAUSE command at the DOS prompt; type PAUSE and press enter. Nothing will appear to happen. DOS is waiting for you to press a key before returning the DOS prompt. Press both the F11 and F12 keys; nothing happens. Press F10 (or any other key), and things will be put back in motion. Now try this. At the DOS prompt, enter a DOS DIR command on a large directory and freeze the output by pressing the dedicated Pause key. Pressing F11 or F12 in this case will put the directory listing back in action. The Pause key function is a BIOS INT 9H implementation and, as you would expect, INT 9H knows about the INT 16H F11 and F12 keys. The conclusion is that going through the operating system does not always ensure machine-independent compatibility. DOS does not support these extended keys. You have to go to the BIOS directly and use the extended calls for F11 and F12 support to detect the difference between the dedicated and keypad cursor keys. As you may know, direct BIOS calls cannot be done in OS/2 programming. However, OS/2 keyboard calls have been updated and are aware of F11 and F12. Expanding the Keyboard Buffer As we've seen, the main problem with the keyboard buffer is its limited 15-keystroke capacity. In some situations, it's desirable to type commands into the buffer faster than an application can process them. If the 15-keystroke capacity is reached, the buffer will fill, the speaker will beep, and you will be stopped from making any additional keyboard entries. A filled keyboard buffer is rare in applications that spend most of their time processing keystrokes, such as word processors. But data crunching applications, such as spreadsheet recalculations or source code compilation, often get behind. Some applications manage their own buffers to circumvent the miniature INT 9H keyboard buffer. These programs will store keystrokes as they occur and process them when they get time. These are the exception, however, not the rule. I will present two programming solutions to expand the keyboard buffer, preventing a full buffer and that annoying beep. To understand the logical solution to expanding the buffer, it is helpful to consider all the pros and cons of the existing keyboard buffer. As I mentioned, a negative feature is that the buffer is only 16 words long and can actually hold only 15 characters. There are, however, pointers to the start and end of the buffer. When you change the pointers, you change the location and size of the buffer. To explore the buffer further, see the sidebar "The Keyboard Buffer and its Pointers." Another negative is that the pointers are only 16-bit offset addresses. The segment base value used by the INT 9H and INT 16H programs is always 0040H of the BIOS data area. This can't be changed. That means the keyboard buffer must be located within 64Kb of memory segment 0040H. The good news is there is some memory within this boundary that seems to be used only during the boot process and is available to expand the keyboard buffer. The bad news is this same memory is not managed or allocated by DOS, which means other similar programs may use this memory and destroy the new buffer, or vice versa. The simplest way to expand the keyboard buffer is to take advantage of this unused low memory. The apparent unused memory vacuum is the 256 bytes starting at address 0040:0200H (0040:0200H can also be expressed as 0060:0000H; see Figure 10, the PC memory map). With the nonmemory management caveat just mentioned in mind, I'm going to give you a short program named KBDBUFF.COM that moves the keyboard buffer to this memory location. KBDBUFF's moving of the buffer is a simple matter of changing the BIOS data keyboard buffer start and end offset pointers to point to 0040:0200H and 0040:0300H, respectively. The buffer head and tail pointers also have to be changed to point inside the new buffer. This is accomplished by setting both pointers to the start of the buffer, which also initializes the buffer as empty. To create KBDBUFF.COM, create a file named KBDBUFF.SCR containing the Debug commands shown in Figure 11. Then redirect the commands to Debug by entering: Debug < KBDBUFF.SCR KBDBUFF will be created. Since there are so few instructions, you can start up Debug and type them in directly. (The script file method is preferred because it's easier to correct errors.) Be sure to include the blank line after the INT 20H instruction. Once KBDBUFF.COM is created, place the KBDBUFF command at the beginning of your AUTOEXEC.BAT. Thereafter, every time you boot, the keyboard buffer will be moved and expanded to the absolute memory location of 420H with a new maximum capacity of 256/2, or 128 keystrokes. KBDBUFF is a cheap and easy remedy to the buffer problem, but it may not work in all environments if another application is also using the same small hole in low memory. For this situation, I've written a better program called KBBUFFER.CTL (see Figure 12), which is available on bulletin boards, including those of MSJ. This program gets its memory allocation for the keyboard buffer through DOS and is, therefore, more reliable. The cost of being well behaved makes for a longer program; however, the additional use of memory is negligible. KBBUFFER.CTL is a device driver that will expand the keyboard buffer. Add to your CONFIG.SYS file: DEVICE = [path]KBBUFFER.CTL [buffer size] The optional buffer size parameter is a decimal number from 16 to 200 indicating the desired keystroke capacity of the new keyboard buffer. The default size of the buffer is 80 keystrokes and should suffice in most situations. KBBUFFER.CTL is a fake device driver; that is, it doesn't drive anything. All KBBUFFER does is take advantage of the loading order of device drivers to get within range of the 64Kb offset requirement of the new buffer. Before I go on, a little background on how DOS uses memory is necessary. When a PC boots, (a complicated process of which I will present an abbreviated rendition) DOS allocates memory in the following order. First, the two hidden system files, IBMBIO.COM and IBMDOS.COM, are loaded (in that order) in two contiguous memory areas starting at memory location 0070:0000H just above the BIOS data area. (These programs might be tagged IO.SYS and MSDOS.SYS with your version of DOS, but they are essentially the same programs.) IBMBIO.COM contains the system resident device drivers CON, PRN, AUX and is the interface between DOS and the ROM BIOS. IBMDOS.COM is the DOS kernel that contains all the DOS 21H function call services. Part of the system files' initialization process is to allocate a work area right after the resident system files for things like file control blocks and disk buffers. Default size values are used for these work areas if specific requests are not found in the CONFIG.SYS BUFFERS, FILES, and FCB commands. It's during this CONFIG.SYS examination that DOS loads and initializes any device drivers found in the CONFIG.SYS file. Finally, COMMAND.COM is loaded and given control. COMMAND.COM processes the AUTOEXEC.BAT file and displays the command prompt. COMMAND.COM exists in two parts, a resident portion that is located right after the device drivers and the transient portion located at the top of RAM. The resident portion is the interface between you and the DOS kernel, including those sometimes nebulous error messages. The transient portion processes batch files and the internal commands like DIR and TYPE and any service that is not needed when a program is running. The transient portion is, therefore, expendable and its memory is made available to any program when it is loaded. (Loading and executing programs is also a task of the resident portion.) When the application exits back to DOS, resident COMMAND.COM does a checksum on the transient portion to see if it was overwritten by the application and needs to be reloaded. (If you have a floppy system, this is when and why you are prompted for the boot disk in drive A. That enables COMMAND.COM to reload its transient portion.) Collectively, the system files, tables, and COMMAND.COM is what's referred to as DOS. Getting back to the keyboard buffer problem, at first glance it might appear that a simple TSR that would reserve a small amount of memory and change the pointers like KBDBUFF would do the trick. TSRs occupy memory right after COMMAND.COM; for most versions of DOS, the size of COMMAND.COM puts any TSR, even if it's the first one loaded, out of reach of the 64Kb offset requirement of the new keyboard buffer. Since device drivers are loaded before COMMAND.COM and the system files take much less than 64Kb, a device driver will always be within reach of the BIOS data segment 64Kb maximum. This is the key to the success of KBBUFFER.CTL. (Note that since device drivers are loaded in the same order as they are listed in the CONFIG.SYS file, KBBUFFER.CTL should appear first to keep it within range.) Device Drivers When a device driver is loaded, it is briefly given control to do any necessary initialization. This is when a "real device" would do things like inquire what hardware is available, set it up, and hook any pertinent interrupt vectors. When KBBUFFER.CTL is given the opportunity to initialize, it changes the BIOS data keyboard buffer pointers to point to itself. At the end of its initialization, a device driver passes control back to DOS and tells DOS how much memory it needs to function. KBBUFFER.CTL's initialization code, like a TSR's, will not be needed again so it is returned to the system memory pool. KBBUFFER.CTL only needs enough memory to serve as the new keyboard buffer. This is calculated from the argument (if any) found on KBBUFFER.CTL's command line. The only memory displaced by KBBUFFER.CTL, therefore, is the few hundred bytes for the new keyboard buffer. A device driver has a different format than executable COM and EXE files. Instead of program code, the file starts with a device header with codes that tell DOS, among other things, what type of a device driver it is and what DOS services the device driver will render. KBBUFFER.CTL seems to be a character device (like ANSI.SYS as opposed to a block device like a disk driver) and tells DOS that it can't handle any DOS services. The latter guarantees that KBBUFFER.CTL will not be called on again by DOS after the initialization and, since it hasn't hooked any vectors, KBBUFFER.CTL will remain undisturbed. The exception is that INT 9H and INT 16H will use the now protected memory for the new keyboard buffer. One last note: KEYVIEW demands the keyboard buffer be in the default 16 word BIOS data area in order to operate. This is because the screen isn't large enough for KEYVIEW to display a larger keyboard. KEYVIEW will, therefore, refuse to run when either KBDBUFF.COM or KBBUFFER.CTL have been used. If you've ever wondered how keystrokes are processed, you should certainly have some idea by now. The programming examples presented should help clarify the operation of your PC's keyboard, and the utilities will make your keyboard more functional. Examining the Keyboard Shift Status and Shift State Bytes (Be sure to remove any keyboard TSRs you may be using, such as NDOSEDIT, from your AUTOEXEC.BAT and reboot before trying this exercise--Ed.) To see the two KD_FLAGs change, start Debug from the DOS prompt and at the minus sign prompt enter D 40:17 L1 (dump segment 40H, the BIOS data segment, offset 17H, length one byte). Debug will display the KD_FLAG status byte (see Figure 4). It will be easiest to follow along in this Debug session if this value is 0. If it is not 0, it means you have one of the shift keys locked. If necessary, unlock the shift keys (Insert, Caps Lock, Num Lock and/or Scroll Lock) and verify by entering the above command again. You should see 0. Throughout this Debug session you can press F3 to display the last command. Press F3 to display the D 40:17 L1 command, but don't press Enter yet. First press the Insert key to toggle Insert on; now press Enter. You will see 80H, indicating Insert is active. Give Insert another tap to toggle it off; then toggle each of the other locking shift keys on (Caps Lock, Num Lock and Scroll Lock) one at a time. Press F3 and Enter after you shift each one on to see its effect; then toggle it off. Check that the value you get is the same as the top four values in Figure 4. Next, toggle on both the Caps Lock and Num Lock shift keys. Press F3 and Enter. This time you will see the combined value of the individual bits, namely, 60H (40H + 20H). Note that you cannot observe the Ctrl and Alt shift states of KB_FLAG because Ctrl-Enter and Alt-Enter do not produce carriage returns. Now do the same sort of thing with the BIOS data area key state KB_FLAG1 at address 40:18. Enter: D 40:18 L1 You will see a 0 this time regardless of the locking keys status. KB_FLAG1 is used by the BIOS to keep track of multiple shift keys depressed at the same time. To see it, repeat the above command with F3 (don't press Enter); then press and hold down the Insert key while you press Enter. You should see 80H, indicating that the state of the Insert key is pressed. Follow the same procedure to experiment with the other shift keys. Enter Q to quit when you wish to return to DOS. The Keyboard Buffer and its Pointers All of the keyboard buffer related information is in the bottom of RAM and can be easily viewed with the aid of Debug. Run Debug and at the minus sign prompt enter the command: D 40:80 L4 That command tells Debug to dump the 4 bytes at segment address 40 with an offset of 80. At 40:80 the 2-byte offset address of the start of the keyboard buffer is stored and at 40:82 the offset of the end of the buffer is stored. The result of the above instruction is: 0040:0080 1E 00 3E 00 ..>. Since the Intel(R) storage technique is backward, the 2-byte offset value stored at address 40:80 is reconstructed to 001E and the 40:82 offset value to 003E. Only offset values are used because the BIOS assumes a segment value of 40 for the keyboard buffer. The start of the keyboard buffer, therefore, can be found in the consecutive bytes starting at address 0040:001E and ending, but not including, the byte at 0040:003E. You can confirm the 16 entry buffer size by subtracting the following two numbers. 3EH minus 1EH is 20H, or 32 decimal. Since there are two bytes (scan code plus ASCII code) for every keystroke, 32/2 equals a 16 keystroke buffer. To see the buffer, enter D 40:1E L20 which means dump segment 40, offset 1EH (the buffer start) for a length of 20 hex bytes (16). Note that the prefacing zeros (0040:001E) are not needed in the command. The output will be similar to: 0040:0010 34 05 4. 0040:0020 30 0B 3A 27 31 02 45 12-20 39 4C 26 32 03 30 0B 0.:'1.E. 9L&2.0. 0040:0030 0D 1C 20 39 4C 26 34 05-0D 1C 44 20 20 39 .. 9L&4...D 9 The display to the far right is Debug's attempt to show everything that is alphanumeric in the memory dump in ASCII. That includes the scan codes that appear as every other odd memory address to the left of its ASCII equivalent. If you look closely, you can pick out the dump command (D 40:1E L20) you just entered. The 9s are the ASCII interpretation of the scan code for space bar (39H). Their accompanying 20H is the command's actual delimiting spaces. In the above dump the command starts with the D toward the end of the third line, continues with the 4 at the end of the first line, the 0 at the start of the third line, and so on. Since the keyboard buffer is a circular buffer and the current head and tail keep moving, the order in which the commands appear on your machine will differ from what you see here. The current head and tail of the buffer is stored in consecutive words (2 bytes) at location 40:1A and 40:1C. To view these words enter: D 40:1A L4 My output was 0040:0010 28 00 28 00 (.(. Again the storage is backward--28 00 is 0028. The current head and tail are, therefore, both at 0040:0028. The output you get will differ, but the values will equal each other, indicating that the head equals the tail and the buffer is currently empty. Enter the above commands again, and you will see the values change as the keystrokes scamper around the circular buffer. Enter Q to return to DOS. Figure 2 IN AL,20H ;Get the current state of Port B. OR AL,80H ;Set the high bit. JMP $ + 2 ;Jump delay for fast machines. OUT 20H,AL ;Reset the keyboard. AND AL,NOT 80H ;Turn the high bit back off. JMP $ + 2 ;Delay. OUT 20H,AL ;Set Port B to its original state. Figure 3 TITLE PORT-A.ASM PAGE 60,132 ;---------------------------------------------------; ; A visual display of the keyboard scan code port. ; ; Michael J. Mefford ; ;---------------------------------------------------; _TEXT SEGMENT PUBLIC 'CODE' ASSUME CS:_TEXT ASSUME DS:_TEXT ORG 100H START: JMP MAIN ; DATA AREA ; --------- DB CR,SPACE,SPACE,SPACE,CR,LF COPYRIGHT DB "PORT-A 1.0 (c) 1990 " PROGRAMMER DB "Michael J. Mefford",CR,LF,LF,"$" DB CTRL_Z CR EQU 13 LF EQU 10 CTRL_Z EQU 26 SPACE EQU 32 BOX EQU 254 ESC_SCAN EQU 1 PORT_A EQU 60H BIOS_INT_9 DW ?,? MENU LABEL BYTE DB "Press and release any key to see " DB "make and break scan code",CR,LF DB "Press Esc to Exit",CR,LF,LF,"$" ; CODE AREA ; --------- MAIN PROC NEAR CALL CLS ;Clear the screen. MOV DX,OFFSET COPYRIGHT ;Display Copyright and menu. MOV AH,9 INT 21H MOV DX,OFFSET MENU INT 21H MOV AX,3509H ;Get keyboard interrupt. INT 21H MOV BIOS_INT_9[0],BX ;Save old interrupt. MOV BIOS_INT_9[2],ES MOV DX,OFFSET PORT_A_INT_9 ;Install new interrupt. MOV AX,2509H INT 21H ;-----------------------------------; ; Loop here until Esc is detected. ; ;-----------------------------------; GET_KEY: XOR AH,AH ;Go and wait for a keystroke. INT 16H CMP AH,ESC_SCAN ;If it's Esc, exit. JNZ GET_KEY ;Else, continue. EXIT: MOV DX,BIOS_INT_9[0] ;Restore old INT 9. MOV DS,BIOS_INT_9[2] MOV AX,2509H INT 21H CALL CLS ;Clear the screen. MOV AX,4C00H ;Exit with error level zero. INT 21H MAIN ENDP ;***************; ;* SUBROUTINES *; ;***************; ;--------------------------------------------------------; ; This "hooked" INT 9 procedure will be called whenever ; ; a key is pressed which gives us the opportunity to get ; ; a look at the scan code and display it. ; ;--------------------------------------------------------; PORT_A_INT_9 PROC NEAR ASSUME DS:NOTHING PUSH AX ;Preserve registers. PUSH BX PUSH CX IN AL,PORT_A ;Get the scan code. CALL HEX_OUTPUT ;And display it. OLD_INT_9: POP CX ;Restore registers. POP BX POP AX JMP DWORD PTR BIOS_INT_9 ;Jump to the BIOS INT 9 routine. PORT_A_INT_9 ENDP ;--------------------------; HEX_OUTPUT PROC NEAR MOV BX,AX ;Store number in BX. MOV CX,204H ;4 positions/word; 4bits/char. ROTATE_HEX: ROL BL,CL ;Move highest bits to lowest. MOV AL,BL ;Store number in AL. AND AL,1111B ;Mask off all but four lowest. ADD AL,"0" ;Convert to ASCII. CMP AL,"9" ;Is it alpha? JLE PRINT_HEX ;If no, print it. ADD AL,7 ;Else, adjust. PRINT_HEX: MOV AH,0EH ;Print via BIOS. INT 10H DEC CH ;Done all four positions? JNZ ROTATE_HEX ;If no, get next. MOV CX,2 ;Display two spaces DELIMIT: MOV AL,SPACE ; between scan codes MOV AH,0EH ; as delimiters. INT 10H LOOP DELIMIT RET HEX_OUTPUT ENDP ;--------------------------; CLS PROC NEAR MOV AH,0FH ;Get current video mode. INT 10H CMP AL,7 ;Is is mono? JZ CLEAR_SCREEN ;If yes, clear screen. MOV AL,3 ;Else, make sure in a text mode. CLEAR_SCREEN: XOR AH,AH ;Clear screen by setting mode. INT 10H MOV AX,500H ;Make sure page zero. INT 10H RET CLS ENDP _TEXT ENDS END START Figure 5 Programs to Toggle Shift Status Either create the following script file and feed it to debug by entering Debug < TOGGLE.SCR or enter the instructions directly while you are in Debug. The resulting short files, INSERT.COM, CAPSLOCK.COM, NUMLOCK.COM, and SCROLOCK.COM, can be used in batch files to toggle the appropriate shift states. For some extended keyboards, these toggle programs will not change the keyboard LED indicators. The resulting shift state, however, will be correct. RCX C A MOV AX,0040 MOV DS,AX XOR Byte Ptr [0017],80 INT 20 N INSERT.COM W E 109 40 N CAPSLOCK.COM W E 109 20 N NUMLOCK.COM W E 109 10 N SCROLOCK.COM W Q Figure 7 Address Size Name Function 0040:001A 1 word BUFFER_HEAD Points to logical start of buffer 0040:001C 1 word BUFFER_TAIL Points to logical end of buffer 0040:001E 16 words KB_BUFFER ASCII character/scan code storage 0040:003E Buffer end (exclusive of buffer) 0040:0080 1 word BUFFER_START Points to physical start of buffer (001EH) 0040:0082 1 word BUFFER_END Points to physical end of buffer (003EH) Figure 8 KEYVIEW.ASM TITLE KEYVIEW.ASM PAGE 60,132 ;-----------------------------------------------; ; KeyView - Visual display of keyboard buffer. ; ; Michael J. Mefford ; ;-----------------------------------------------; BIOS_DATA SEGMENT AT 40H ORG 1AH BUFFER_HEAD DW ? BUFFER_TAIL DW ? ORG 71H BIOS_BREAK DB ? ORG 80H KBD_BUFF_START DB ? BIOS_DATA ENDS _TEXT SEGMENT PUBLIC 'CODE' ASSUME CS:_TEXT ASSUME DS:_TEXT ORG 100H START: JMP MAIN ; DATA AREA ; --------- DB CR,SPACE,SPACE,SPACE,CR,LF COPYRIGHT DB "KEYVIEW 1.0 (c) 1990 " DB "Michael J. Mefford",CR,LF,LF,"$" DB CTRL_Z CR EQU 13 LF EQU 10 CTRL_Z EQU 26 SPACE EQU 32 BOX EQU 254 BUFFER_START EQU 1EH BUFFER_END EQU 3EH UP_ARROW EQU 24 DN_ARROW EQU 25 TRUE EQU 1 FALSE EQU 0 BOX_ROW EQU 8 BOX_COL EQU 7 BOX_TWO EQU 6 CHAR_START EQU 1400H ESC_SCAN_CODE EQU 1 F1_SCAN_CODE EQU 3BH F2_SCAN_CODE EQU 3CH PORT_A EQU 60H PORT_B EQU 61H COMMAND_PORT EQU 20H EOI EQU 20H ;-------------------------; BIOS_INT_9 DW ?,? NORMAL EQU 07H INVERSE_BLUE EQU 17H INVERSE DB 70H LAST_POS DW CHAR_START LAST_HEAD DW BUFFER_START LAST_TAIL DW BUFFER_START F1_FLAG DB FALSE ESC_FLAG DB FALSE EXTENDED_CALL EQU 10H KEY_SUPPORT DB 0 SUPPORT_FLAG DB 0 HEAD DB "Head",DN_ARROW TAIL DB "Tail",UP_ARROW ERASE DB 5 DUP (SPACE) INVALID_MSG DB "Keyboard Buffer not supported",CR,LF,"$" MENU LABEL BYTE DB "Press any key to add to keyboard buffer",CR,LF DB "Press F1 to retrieve a character from buffer",CR,LF,"$" INT_16_MSG DB "Press F2 to toggle extended keyboard " DB "support; Support is now: ",CR,LF,"$" INT_16_LEN EQU $ - INT_16_MSG - 3 ESC_MSG DB "Press Esc to Exit$" INACTIVE_MSG DB "INACTIVE " INACTIVE_LEN EQU $ - INACTIVE_MSG - 2 BUFFER_WINDOW LABEL BYTE DB 201,7 DUP(7 DUP(205),203),7 DUP(205),187 DB 2 DUP(186,7 DUP(7 DUP(32),186),7 DUP(32),186) DB 200,7 DUP(7 DUP(205),202),7 DUP(205),188 ; CODE AREA ; --------- MAIN PROC NEAR ;--------------------------------------; ; Exit with non-support message if ; ; original keyboard buffer not active. ; ;--------------------------------------; MOV AX,SEG BIOS_DATA ;Point to BIOS data. MOV ES,AX CMP ES:KBD_BUFF_START,BUFFER_START ;Original buffer active? JZ GOOD_BUFFER ;If yes, continue. MOV DX,OFFSET INVALID_MSG ;Else, display invalid message. CALL PRINT_STRING MOV AX,4C01H ;Exit with error level 1. JMP TERMINATE GOOD_BUFFER: CLD ;All string moves forward. CALL VIDEO ;Check the video equip. and CLS. MOV DX,OFFSET COPYRIGHT ;Display copyright. CALL PRINT_STRING MOV DX,OFFSET MENU CALL PRINT_STRING ;----------------------------------------------------------; ; BIOS Tech Ref recommended 101-keyboard detection method. ; ;----------------------------------------------------------; MOV DL,2 ;Make two attempts to write MOV CX,0FFFFH ; char/scan code of FFFFh to KBD_WRITE: MOV AH,05H ; buffer via extended keyboard INT 16H ; write funtion. OR AL,AL ;Was it successful? JZ RETRIEVE ;If yes, extended supported. MOV AH,10H ;Else, maybe buffer full. Make INT 16H ; room by retrieving char. DEC DL ;Is this the second time through? JNZ KBD_WRITE ;If no, try to write again. JMP SHORT MENU_END ;Else, done here; no support. RETRIEVE: MOV CX,15 ;Try 15 times to retrieve SEARCH_KBD: MOV AH,11H ; the FFFFh scan code. INT 16H ;Buffer empty? JZ MENU_END ;If yes, no support. MOV AH,10H ;Else, extended Kbd read. INT 16H CMP AX,0FFFFH ;Did we find the FFFFh? JZ EXTENDED_KBD ;If yes, extended Kbd support. LOOP SEARCH_KBD ;Else, search all 15 possible. JMP SHORT MENU_END ;If feel through, no support. ;-------------------------------------------------------------; ; If 101-keyboard exists, display additional support message. ; ;-------------------------------------------------------------; EXTENDED_KBD: MOV SUPPORT_FLAG,1 ;Flag that supported. MOV DX,OFFSET INT_16_MSG ;Display support message. CALL PRINT_STRING CALL EXTENDED ;Display "ACTIVE" message. MOV DX,600H ;Move cursor to next line. CALL SET_CURSOR ;-----------------------------------------------------; ; Display Esc message regardless of keyboard support. ; ;-----------------------------------------------------; MENU_END: MOV DX,OFFSET ESC_MSG ;Display Esc message. CALL PRINT_STRING ;----------------------------------------------------------; ; Display my visual interpretation of the keyboard buffer. ; ;----------------------------------------------------------; MOV DX,BOX_ROW SHL 8 + BOX_COL ;Start pos. of first box. MOV BL,INVERSE ;Display in inverse color. MOV CX,2 ;Two boxes to display. NEXT_BOX: PUSH CX ;Save counter. MOV CX,4 ;Four rows per box. MOV SI,OFFSET BUFFER_WINDOW ;Point to buffer box. NEXT_ROW: PUSH CX ;Save counter. MOV CX,65 ;65 columns per box. NEXT_BYTE: LODSB ;Get a byte CALL WRITE_CHAR ; and display it. LOOP NEXT_BYTE ;Repeat for all 65 columns. MOV DL,BOX_COL ;Box column start INC DH ;Next row. POP CX ;Retrieve counter. LOOP NEXT_ROW ;Repeat for all four rows. MOV DX,(BOX_ROW + BOX_TWO) SHL 8 + BOX_COL ;2nd box start. POP CX ;Retrieve counter. LOOP NEXT_BOX ;Do both boxes. CALL INITIALIZE ;Fill the visual buffer. ;-------------------------; ; Hook our INT 9 handler. ; ;-------------------------; MOV AX,3509H ;Get keyboard interrupt. INT 21H MOV BIOS_INT_9[0],BX ;Save old interrupt. MOV BIOS_INT_9[2],ES MOV DX,OFFSET KEYVIEW_INT_9 ;Install new interrupt. MOV AX,2509H INT 21H MOV AX,SEG BIOS_DATA ;Point to BIOS data area. MOV ES,AX ;--------------------------------------------------------------; ; Ready for action. Hide the cursor off screen so it's not ; ; distracting. Retrieve characters if F1 pressed. Toggle ; ; extended support on/off if F2 pressed. Clear visual buffer ; ; if Ctrl Break detected. Exit if Esc pressed. ; ;--------------------------------------------------------------; GET_KEY: MOV DX,1900H ;Hide the cursor off screen CALL SET_CURSOR ; on row 25. CK_F1: CMP F1_FLAG,TRUE ;Was F1 pressed? JZ CK_AVAILABLE ;If yes, retrieve a character. MOV AH,1 ;This will extract extended OR AH,KEY_SUPPORT ; codes if extended support INT 16H ; is not active. CMP SUPPORT_FLAG,2 ;F2 pressed and extended JZ DO_F2 ; support? If yes, toggle. CMP ESC_FLAG,TRUE ;Was Esc pressed? JZ EXIT ;If yes, exit. TEST ES:BIOS_BREAK,10000000B ;Was Ctrl Break pressed? JNZ CTRL_BREAK ;If yes, clear visual buffer. CLI ;No interrupts. MOV DI,ES:BUFFER_TAIL ;Retrieve buffer tail and head. MOV SI,ES:BUFFER_HEAD STI ;Interrupts OK now. MOV BP,LAST_TAIL ;Retrieve last tail. CMP DI,BP ;Has the tail moved? JZ CK_HEAD ;If no, check head. MOV LAST_TAIL,DI ;Else, store new tail. MOV SI,OFFSET TAIL ;Indicate tail moved JMP SHORT DO_BUFFER ; and update visual buffer. CK_HEAD: MOV BP,LAST_HEAD ;Retrieve last head. MOV DI,SI ;Assume head moved. CMP DI,BP ;Has head moved? JZ CK_F1 ;If no, nothing to do; Check F1. MOV SI,BP ;Else, next head position. INC SI INC SI CMP SI,BUFFER_END ;If moved past end of buffer JNZ STORE_HEAD ; circle to beginning of buffer. MOV SI,BUFFER_START STORE_HEAD: MOV LAST_HEAD,SI ;Store new head. MOV SI,OFFSET HEAD ;Indicate head has moved DO_BUFFER: CALL UPDATE_BUFFER ; and update visual buffer. JMP SHORT GET_KEY ;Next key. CK_AVAILABLE: MOV F1_FLAG,FALSE ;Reset F1 flag. MOV AH,1 ;Get keystroke status OR AH,KEY_SUPPORT ; add in type keyboard support. INT 16H JNZ DO_F1 ;If key available, get it. MOV AX,0E07H ;Else, beep speaker. INT 10H JMP SHORT CK_F1 ;Done here. DO_F1: CALL DISPLAY_CHAR ;Retrieve character from buffer JMP GET_KEY ; and display; next key. DO_F2: CALL EXTENDED ;Toggle extended support. MOV SUPPORT_FLAG,1 ;Reset support flag. JMP GET_KEY ;Next key. CTRL_BREAK: AND ES:BIOS_BREAK,NOT 80H ;Reset Ctrl Break bit. CALL INITIALIZE ;Clear the visual buffer. JMP GET_KEY ;Next key. ;---------------------------------------------------------; ; Clear the buffer by setting head = tail. Clear screen. ; ; Restore INT 9. ; ;---------------------------------------------------------; EXIT: CLI ;No interrupts. MOV ES:BUFFER_HEAD,BUFFER_START ;Set head = buffer start. MOV ES:BUFFER_TAIL,BUFFER_START ;Set tail = buffer start. STI ;Interrupts OK now. CALL VIDEO ;Clear screen. MOV DX,BIOS_INT_9[0] ;Restore old INT 9. MOV DS,BIOS_INT_9[2] MOV AX,2509H INT 21H MOV AX,4C00H ;Error level zero. TERMINATE: INT 21H MAIN ENDP ;***************; ;* SUBROUTINES *; ;***************; ;-----------------------------------------------------; ; This subroutine will toggle the extended keyboard ; ; support on/off and display ACTIVE/INACTIVE message. ; ;-----------------------------------------------------; EXTENDED PROC NEAR MOV DX,4 SHL 8 + INT_16_LEN ;Row 4; column at end of msg. MOV CX,INACTIVE_LEN ;Length of message. MOV BL,NORMAL ;Normal attribute. MOV SI,OFFSET INACTIVE_MSG ;Assume INACTIVE msg. XOR KEY_SUPPORT,EXTENDED_CALL ;Toggle support. JZ DISPLAY_EXT ;If zero, guessed right. INC SI ;Else, bump pointer past "IN". INC SI DISPLAY_EXT: LODSB ;Display the message. CALL WRITE_CHAR LOOP DISPLAY_EXT RET EXTENDED ENDP ;------------------------------------------------------; ; All sixteen positions of visual buffer are updated ; ; when program starts and when Ctrl Break is detected. ; ;------------------------------------------------------; INITIALIZE PROC NEAR MOV BP,LAST_TAIL ;Retrieve last tail. MOV DI,ES:BUFFER_HEAD ;Retrieve current head. PUSH DI ;Preserve last tail. MOV LAST_TAIL,DI ;Make last tail = head. MOV SI,OFFSET TAIL ;Point to tail msg. CALL UPDATE_BUFFER ;Update the visual buffer. MOV BP,LAST_HEAD ;Retrieve last head. MOV AX,BP ;Save in AX NEXT_INIT: PUSH AX ;Preserve AX. MOV DI,BP ;Move head up one. INC DI INC DI CMP DI,BUFFER_END ;If head = buffer end JNZ DO_INIT ; then head = buffer start. MOV DI,BUFFER_START DO_INIT: MOV SI,OFFSET HEAD ;Point to head msg. CALL UPDATE_BUFFER ;Update visual buffer. MOV BP,DI POP AX ;Retrieve last head. CMP BP,AX ;Did we do all 16 positions? JNZ NEXT_INIT ;If not, continue until done. POP DI ;Retrieve last tail. MOV LAST_HEAD,DI ;Last head = last tail. MOV SI,OFFSET HEAD ;Point to head msg. CALL UPDATE_BUFFER ;Update visual buffer. RET INITIALIZE ENDP ;----------------------------; ; This is the INT 9 handler. ; ;----------------------------; KEYVIEW_INT_9 PROC NEAR ASSUME DS:NOTHING STI ;Interrupts OK. PUSH AX ;AX will be destroyed; preserve. IN AL,PORT_A ;Get the scan code. CMP AL,F1_SCAN_CODE ;Is it F1 make? JZ RESET_MAKE ;If yes, flag and reset KBD. CMP AL,F1_SCAN_CODE OR 80H ;Else, is it F1 break? JZ RESET_BREAK ;If yes, ignore. CMP AL,ESC_SCAN_CODE ;Else, is it Esc? JZ ESC_EXIT ;If yes, flag Esc. CMP SUPPORT_FLAG,1 ;Extended keyboard support? JNZ OLD_INT_9 ;If no, done here. CMP AL,F2_SCAN_CODE OR 80H ;Else, is it F2 break? JZ RESET_BREAK ;If yes, ignore. CMP AL,F2_SCAN_CODE ;Else, is it F2 make? JNZ OLD_INT_9 ;If no, done here. MOV SUPPORT_FLAG,2 ;Else, flag pressed. JMP SHORT RESET_BREAK ;Reset the KBD. ESC_EXIT: MOV ESC_FLAG,TRUE ;If Esc pressed, flag and exit. OLD_INT_9: POP AX ;Restore AX. JMP DWORD PTR BIOS_INT_9 ;Go to BIOS INT 9 routine. ;------------------------------------------------------; ; If F1 or F2 pressed, don't let BIOS store character. ; ; Instead reset KBD and throw away the keystroke. ; ;------------------------------------------------------; RESET_MAKE: MOV F1_FLAG,TRUE ;Flag F1 pressed. RESET_BREAK: IN AL,PORT_B ;Retrieve Port B. OR AL,80H ;Turn bit 7 on to reset JMP $ + 2 ;I/O delay. OUT PORT_B,AL ;Reset KBD. AND AL,NOT 80H ;Turn bit 7 back off. JMP $ + 2 ;I/O delay. OUT PORT_B,AL ;Restore port. CLI ;Interrupts off. MOV AL,EOI ;Send End Of Interrupt OUT COMMAND_PORT,AL ; to 8259A PIC. POP AX ;Restore AX. IRET ;Interrupt return. ASSUME DS:_TEXT KEYVIEW_INT_9 ENDP ;-----------------------------------------------; ; When F1 is pressed, a character is retrieved ; ; from the buffer and displayed. ; ;-----------------------------------------------; DISPLAY_CHAR PROC NEAR MOV AX,LAST_POS ;Get last char display column. CMP AL,78 ;Was it column 78? JBE RESTORE_POS ;If below or equal, OK. MOV CX,CHAR_START ;Else, clear the line. MOV DX,CHAR_START + 78 MOV BH,NORMAL MOV AX,600H INT 10H MOV AX,CHAR_START ;Start at beginning of line. RESTORE_POS: MOV DX,AX ;Cursor position in DX. INC AX ;Bump to new cursor position MOV LAST_POS,AX ; and save for next time. MOV AH,0 ;Retrieve character; include OR AH,KEY_SUPPORT ; appropriate keyboard support. INT 16H MOV BL,INVERSE ;Display in inverse video. CALL WRITE_CHAR RET DISPLAY_CHAR ENDP ;------------------------------------; ; INPUT ; ; BP = Last position. ; ; DI = New position. ; ; SI = OFFSET HEAD or OFFSET TAIL. ; ;------------------------------------; UPDATE_BUFFER PROC NEAR MOV DX,BP ;Last position in DX. CALL CURSOR_POS ;Calculate cursor position. PUSH DX ;Preserve cursor position. ADD DH,2 ;Move two columns right. MOV AX,ES:[BP] ;Retrieve last scan/char code. MOV BL,INVERSE ;Display in inverse CMP SI,OFFSET TAIL ; if tail moved. JZ DO_CHAR MOV BL,NORMAL ;Else, head moved;display normal. DO_CHAR: PUSH DX ;Save cursor position. CALL SPACES ;Center char in top of box with CALL WRITE_CHAR ; three spaces on either side. CALL SPACES POP DX ;Retrieve cursor position. INC DH ;Next row. XCHG AL,AH ;Swap scan code and ASCII char. PUSH AX ;Preserve. MOV AL,SPACE ;Display a space. CALL WRITE_CHAR POP BP ;Retrieve char/scan code. CALL HEX_OUTPUT ;Display in hex. POP DX ;Retrieve cursor position. INC DL ;Next column. MOV BL,NORMAL ;Normal attribute. CMP SI,OFFSET HEAD ;Erase either head or tail msg. JZ DO_ERASE ADD DH,5 ;Tail at bottom of box. DO_ERASE: PUSH SI ;Save msg pointer. MOV SI,OFFSET ERASE ;Erase old msg. CALL DISPLAY_POINT MOV DX,DI ;Move to new position. CALL CURSOR_POS INC DL ;Move right one column. POP SI CMP SI,OFFSET HEAD ;Display either head or tail msg. JZ DO_POINTER ADD DH,5 DO_POINTER: CALL DISPLAY_POINT RET UPDATE_BUFFER ENDP ;--------------------------; SPACES PROC NEAR PUSH AX ;Preserve AX. MOV CX,3 ;Display three spaces. MOV AL,SPACE NEXT_SPACE: CALL WRITE_CHAR LOOP NEXT_SPACE POP AX RET SPACES ENDP ;-----------------------------; ; INPUT ; ; BP = Character/scan code. ; ;-----------------------------; HEX_OUTPUT PROC NEAR MOV CX,2 ;Two codes to display. NEXT_NIBBLE: PUSH CX ;Preserve counter. MOV CX,204H ;4 positions/word; 4bits/char. ROTATE_HEX: ROL BP,CL ;Move highest bits to lowest. MOV AX,BP ;Char/scan code in AX. AND AL,1111B ;Mask off all but four lowest. ADD AL,"0" ;Convert to ASCII. CMP AL,"9" ;Is it alpha? JLE PRINT_HEX ;If no, print it. ADD AL,7 ;Else, adjust. PRINT_HEX: CALL WRITE_CHAR ;And write them. DEC CH ;Done all four positions? JNZ ROTATE_HEX ;If no, get next. MOV AL,SPACE ;Delimit with a space. CALL WRITE_CHAR POP CX ;Do both char and scan code. LOOP NEXT_NIBBLE RET HEX_OUTPUT ENDP ;------------------------------; ; INPUT ; ; SI points to pointer text. ; ;------------------------------; DISPLAY_POINT PROC NEAR MOV CX,5 ;Five characters to pointer. NEXT_POINT: LODSB CALL WRITE_CHAR ;Write them. LOOP NEXT_POINT RET DISPLAY_POINT ENDP ;--------------------------; CURSOR_POS PROC NEAR SUB DX,BUFFER_START ;Difference = offset. MOV DH,BOX_ROW - 1 ;Point to starting row of box. CMP DL,16 ;If offset less than 16 OK. JB GET_COLUMN SUB DL,16 ;Else adjust offset. ADD DH,BOX_TWO ;Point to second box. GET_COLUMN: SHL DL,1 ;Multiply by 4 to get cursor SHL DL,1 ; column. ADD DL,BOX_COL + 1 ;Add offset of box start. RET CURSOR_POS ENDP ;-------------------------; ; INPUT ; ; DX = cursor position. ; ;-------------------------; SET_CURSOR: PUSH AX ;Preserve AX. XOR BH,BH ;Page zero. MOV AH,2 INT 10H ;Set cursor position. POP AX ;Retrieve AX. RET ;-----------------------------; ; INPUT ; ; DX = new cursor position. ; ; AL = Character to write. ; ;-----------------------------; WRITE_CHAR PROC NEAR PUSH AX ;Preserve AX and CX. PUSH CX CALL SET_CURSOR ;Set cursor position. MOV CX,1 ;One character to write. MOV AH,9 INT 10H INC DL POP CX ;Restore registers. POP AX RET WRITE_CHAR ENDP ;--------------------------; VIDEO PROC NEAR MOV AH,0FH ;Retrieve current video mode. INT 10H CMP AL,7 ;Is it mono? JZ CLEAR_SCREEN ;If yes, clear screen. CMP AL,2 ;Is it black and white CGA? JZ CLEAR_SCREEN ;If yes, clear screen. MOV INVERSE,INVERSE_BLUE ;Else, use color attributes. MOV AL,3 ; and video mode CO80. CLEAR_SCREEN: XOR AH,AH ;Set video mode. INT 10H MOV AX,500H ;Set page zero. INT 10H RET VIDEO ENDP ;--------------------------; PRINT_STRING: MOV AH,9 ;Print string via DOS. INT 21H RET _TEXT ENDS END START Figure 11 KBDBUFF.COM N KBDBUFF.COM ;Name: KBDBUFF.COM A ;Assemble MOV AX,0040 ;Move 40H into the data segment MOV DS,AX CLI ;No interrupts MOV Word Ptr [001A],0200 ;Buffer head = offset 200 MOV Word Ptr [001C],0200 ;Buffer tail = offset 200 MOV Word Ptr [0080],0200 ;Buffer start = offset 200 MOV Word Ptr [0082],0300 ;Buffer end = offset 300 STI ;Interrupts back on INT 20 ;Terminate ;Quit assembly RCX ;Register CX is file length 21 ;File length = 21H bytes W ;Write the file Q ;Quit Debug Figure 12 KBBUFFER.ASM ;---------------------------------------------------------------; ; KBBUFFER.CTL * Michael J. Mefford ; ; Is loaded as a device driver just to get within offset ; ; range of the BIOS data area and the keyboard buffer so ; ; it can replace the default 15 key buffer with a larger one. ; ;---------------------------------------------------------------; BIOS_DATA SEGMENT AT 40H ORG 1AH BUFFER_HEAD DW ? BUFFER_TAIL DW ? ORG 80H BUFFER_START DW ? BUFFER_END DW ? BIOS_DATA ENDS _TEXT SEGMENT PUBLIC 'CODE' ASSUME CS:_TEXT,DS:_TEXT,ES:_TEXT,SS:_TEXT ORG 0H ;COPYRIGHT DB "KBBUFFER.CTL 1.0 (c) 1990 ",CR,LF ;PROGRAMMER DB "Michael J. Mefford",CR,LF,CTRL_Z ;************* DEVICE_HEADER *************; POINTER DD -1 ATTRIBUTE DW 1000000000000000B DEVICE_STRAG DW STRATEGY DEVICE_INT DW INTERRUPT DEVICE_NAME DB "BUFFERCTL" CR EQU 13 LF EQU 10 CTRL_Z EQU 26 SPACE EQU 32 BOX EQU 254 ;-------------------------; REQUEST_HEADER STRUC HEADER_LENGTH DB ? UNIT_CODE DB ? COMMAND_CODE DB ? STATUS DW ? RESERVED DQ ? REQUEST_HEADER ENDS DONE EQU 0000000100000000B ;Status codes. UNKNOWN EQU 1000000000000011B ;-------------------------; INIT STRUC HEADER DB (TYPE REQUEST_HEADER) DUP(?) UNITS DB ? ENDING_OFFSET DW ? ENDING_SEGMENT DW ? ARGUMENTS_OFF DW ? ARGUMENTS_SEG DW ? INIT ENDS REQUEST_OFFSET DW ? REQUEST_SEG DW ? ; CODE AREA ; --------- ;---------------------------------------------; ; The only task of the strategy routine is to ; ; save the pointer to the request header. ; ;---------------------------------------------; STRATEGY PROC FAR MOV CS:REQUEST_OFFSET,BX ;Request header address is MOV CS:REQUEST_SEG,ES ; passed in ES:BX. RET STRATEGY ENDP ;----------------------------------------; ; The interrupt procedure will be called ; ; immediately after the strategy. ; ;----------------------------------------; INTERRUPT PROC FAR PUSH AX ;Responsible for all registers. PUSH BX PUSH CX PUSH DX PUSH DS PUSHF MOV DS,CS:REQUEST_SEG ;Retrieve request header pointer. MOV BX,CS:REQUEST_OFFSET OR STATUS[BX],DONE ;Tell DOS we are done. CMP COMMAND_CODE[BX],0 ;Is it INIT command? JZ MAKE_STACK ;If yes, do our stuff. OR STATUS[BX],UNKNOWN ;Else, exit with confused JMP SHORT UNKNOWN_EXIT ; message to DOS. MAKE_STACK: MOV CX,SS ;Save DOS stack. MOV DX,SP MOV AX,CS CLI MOV SS,AX ;Make new stack. MOV SP,0FFFEH STI PUSH CX ;Save old stack pointers on new. PUSH DX PUSH ES ;Save rest of registers. PUSH SI PUSH BP CALL INITIALIZE ;Go do our stuff. POP BP ;Restore registers. POP SI POP ES POP DX ;Restore old DOS stack. POP CX CLI MOV SS,CX MOV SP,DX STI UNKNOWN_EXIT: POPF ;Restore rest of registers. POP DS POP DX POP CX POP BX POP AX RET ;Far return back to DOS. INTERRUPT ENDP KBBUFFER_CTL_END LABEL WORD ;************* END OF RESIDENT PORTION *************; BUFFER_DEFAULT EQU 80 BUFFER_MIN EQU 16 BUFFER_MAX EQU 200 HEADING LABEL BYTE DB "KBBUFFER.CTL 1.0 (c) 1990 " DB "Michael J. Mefford",CR,LF,LF,"$" Creating Windows List Boxes that Support Virtualized Scrolling Robert A. Wood One of the standard resources the Microsoft(R) Windows environment provides to programmers is the list box. The standard list box works best in instances that involve a relatively small number of short strings: the "file/open..." dialog box provided by most Windows applications is a good example. But what if an application needs to support a more complex string selection facility? For example, a user might have to view and select long records from a large database application with hundreds of thousands of records. One of the limitations of a standard list box is the memory required to store the list box strings. These list box strings must be stored in a single segment of memory with a maximum size of 64Kb. As the number and size of the strings increase, it takes much longer to load the list box. It takes even longer to fill the list box when you have it sort the strings, since the sort is performed every time you add a string. Another limitation is the width of a standard list box's display string. List boxes display no more than 76 characters in a single line. One solution is to create a virtual list box (VLB) facility that can provide arbitrary amounts of vertical and horizontal scrolling. Linked into your application, the VLB described here manages a short list of strings in memory. When the user scrolls horizontally or vertically, the VLB updates the contents of the list box with additional strings obtained from your application. We will first describe the callback function your application must supply, then the VLB subclass itself, followed by a sample program. Callback Function Because the VLB manages only the strings currently visible within the list box, it relies on a specialized string retrieval callback function provided by your application when the VLB is initialized. The callback function responds to requests from the VLB for list box strings and related information. The specific source of the strings is irrelevant to the VLB. The callback function receives four types of calls from the VLB: initialization, virtual limits, string retrieval, and horizontal scrolling. The "messages" for each type are listed in Figure 1. When responding to string retrieval requests (messages) from the VLB, the callback function must assign a unique long integer string ID to each retrieved string. This ID can be a database record number, a file offset, a numeric key value, or any other numeric value. The VLB subsequently uses this string ID to identify each string and reference it when communicating with the callback function. The VLB passes a far pointer to a long integer string ID to the callback function to be used as a basis for a string retrieval. The new string's ID must be provided by the callback function via the pointer. String Management The VLB stores the string IDs for the visible strings in memory. An application can request the total number of selected strings with the message VLB_GETSELCOUNT, and request the selected string IDs with the message VLB_GETSELID. For a single selection list box, this message returns the selected string ID. For a multiselection list box, it returns a handle to an array of the selected string IDs. The message VLB_GETSELSTR returns a pointer to the entire selected string. When the message is for a multiselection list box, the wParam value of the VLB_GETSELSTR message specifies the selected string to be retrieved: to get the first string, wParam is zero. When the VLB needs another string for the list box, it sends one of the string retrieval messages to the callback function. The message may include a string ID, and it always includes a pointer to a string buffer to be loaded with the retrieved display string. Creating a VLB You create a VLB object like any other standard control: with the Dialog Editor or by editing the *.rc file. The VLB is created as a subclass of the standard list box or edit control class. If you use the list box class, a horizontal scroll bar cannot be displayed although horizontal scrolling is still available via the keyboard. To display the horizontal scroll bar, you must specify the edit class with both scroll bars, although the edit class has a problem displaying the border of the VLB. To circumvent this, include a static class frame behind the edit control; this makes the frame appear to border the VLB edit control. The static control must have the same coordinates as the edit control. The VLB Procedure A VLB (see Figure 2) must be initialized by the application using it with the InitVLB function. This function will allocate memory and initialize a structure of information for the VLB. If the VLB is used in a dialog box, InitVLB would be called while processing the WM_INITDIALOG message. The InitVLB function requires an instance handle, parent handle, list box ID, and the name of the callback function your application provides. First, the list box ID is saved for sending messages to the callback function and for sending notifications to the parent. The control's style is checked for multiple selection. The character width and height text metrics are retrieved. The client area size is saved; it is then used with the character width and height metrics to calculate the number of characters that will fit in a display string and the number of strings that will fit in the window. The VLB will keep track of which string has the focus (designated by a dotted frame). The position of the focus string, the position of the first displayed string, and the total number of selected strings are initialized to zero. The VLB callback function is also saved. Next, the VLB allocates a buffer to hold the displayed strings. The size of the buffer is calculated by multiplying the number of characters per display string (plus one for the null byte) by the number of display strings. The VLB then allocates memory for an array of longs for the string ID of each display string. The string IDs are initialized to -1. The handles to these memory blocks are stored in the VLB information structure (see Figure 3). For a single selection list box, the selected string ID is saved in a long integer. It is initialized to -1. For a multiple selection list box, another allocation is done. It is an array of longs for the selected string IDs. The size of the array starts at the total number of display strings. A maximum number of selected strings is saved and initialized to the total number of display strings. This value is used to determine if more memory needs to be allocated for the array of selected string IDs when multiple strings are being selected. The array is then initialized to -1. The handle to the allocated memory block is stored in the VLB information structure. Next, MakeProcInstance is performed on the VLB window procedure, VLBProc. The VLB is subclassed with the function SetWindowLong. The handle to the allocated VLB information structure is added to the control property list using the function SetProp. Create messages are then sent to the VLB procedure and to the callback function. The callback function is sent a VCB_LENGTH message to query the number of virtual strings. The vertical scroll bar range is set from zero to the number of virtual strings less the number of display strings less one. This allows the thumb to be at the bottom of the scroll bar for the last page of display strings. If the window includes a horizontal scroll bar, the callback function is next sent a VCB_WIDTH message to obtain the maximum number of characters in the display string. The horizontal scroll bar range is set from zero to the virtual string width minus one. Finally, InitVLB loads the list box. The VLB procedure is sent a VLB_RELOAD message with the wParam equal to RELOAD_STRINGPOS and lParam equal to zero. This tells the VLB to reload its list box starting with string position zero. The initial request to load the list box causes the VLB to send a VCB_VTHUMB message with a string ID equal to zero to the callback function. The callback function will put its first display string in the display string buffer and will assign the new string ID to the far pointer to a long parameter. The callback function will receive VCB_NEXT messages for the remaining display strings. The string ID will be equal to the previously retrieved string ID. When the VLB receives new display strings, it adjusts its internal buffer of display strings and its array of displayed string IDs. After all the necessary strings are retrieved and the adjustments are made to the internal information, the VLB processes the paint message. The VLB painting process is done in three steps. First, each display string is written to the client area with the TextOut function. Second, the selected string or strings are inverted. Third, the focus string is framed with a dotted border if it is visible. The VLB sends a WM_CTLCOLOR message to the parent of the list box before painting. The wParam specifies the handle to the device context, hCtl, and lParam is equal to MAKELONG( hCtl, CTLCOLOR_LISTBOX) The display string IDs are compared to the selected string ID for a single selection list box and to the array of selected string IDs for a multiple selection list box. The VLB processes the WM_GETDLGCODE message and returns DLGC_WANTARROWS. When the VLB receives the WM_SETFOCUS message, it puts a dotted border around the focus string if it is visible and sends a VCB_SETFOCUS message to the callback function. When the VLB receives the WM_KILLFOCUS message, it removes the dotted border and sends a VCB_KILLFOCUS message to the callback function. These messages can be processed by the callback function to open and close the source file. The WM_VSCROLL messages cause the VLB to send the appropriate messages to the callback function to get the new display strings. If the wParam of WM_VSCROLL equals SB_THUMBPOSITION, the list box is completely reloaded based on the new position. If wParam equals SB_LINEDOWN, the VLB moves its internal buffer of display strings back one string and requests a new last display string from the callback function. Similarly, for SB_LINEUP, the VLB moves its internal buffer of display strings forward one string and requests a new first display string from the callback function. If wParam equals SB_PAGEUP, the VLB moves its first display string to the last display string position and sends VCB_PREV messages to the callback function for the previous display strings. If wParam equals SB_PAGEDOWN, the VLB moves its last display string to the first display string position and sends VCB_NEXT messages to the callback function for the next set of display strings. When the VLB display string buffer is updated the array of string IDs is also updated. The WM_HSCROLL messages cause the VLB to send VCB_. . . messages to the callback function to set the new starting column of the display strings. If wParam equals SB_THUMBPOSITION, the new starting column is the thumb position. If wParam equals SB_LINEUP, the starting column should be moved one character to the left. If wParam equals SB_LINEDOWN, the starting column should be moved one character to the right. If wParam equals SB_PAGEUP or SB_PAGEDOWN, the starting column should be moved left or right one data column, respectively. The callback function stores an array of tab stops for the beginning of each data column. After processing a WM_HSCROLL message, the currently displayed strings are reloaded based on their new starting column. The arrow keys and other direction keys cause the VLB to scroll its contents. These keys also change the selected string. The Control key and left or right arrow keys simulate the horizontal page left or right. The left mouse button click selects the string above the mouse cursor. The left mouse button double click causes a VLBN_DBLCLK notification to be sent to the parent. When the VLB receives the WM_DESTROY message it frees all memory allocated for the VLB information structure and removes the handle to the VLB information structure from the control's property list. All messages that the VLB procedure does not handle are passed on to the DefWindowProc. DEMO Program DEMO (see Figure 4) displays a VLB created from a multiline edit control with a static frame control as a border. The VLB has both horizontal and vertical scroll bars and is a single-selection list box. The callback function shown here, VLBfile, retrieves strings from a sample file we've included named VLB.TXT (see Figure 5). The VLB.TXT file contains a listing of a subdirectory with five columns of data. The callback function has an array of five tab stops for the VCB_PAGERIGHT and VCB_PAGELEFT messages. It also stores a static int called Column that sets the starting column of the display string. Column is manipulated by the VCB_. . . horizontal scrolling messages. The unique string ID is the physical record number. The strings are retrieved by first performing an lseek to the record offset. The offset is specified by multiplying the string ID by the record length. DEMO processes the VLBN_SELCHANGE notification by requesting the selected string from the VLB and setting the string in the static text control above the list box. Enhancements Although the VLB is completely functional, possible enhancements might include support for proportional fonts and moving the implementation into a dynamic-link library. The VLB could also be modified to allow resizing of the list box. The ScrollWindow function might be used to speed up painting of the list box. Finally, in a commercial product, the VLB should support mouse dragging for multiple selections and the use of the shift key to deselect strings. Figure 1 VLB Procedure Messages to the VLB Callback Function Message Description LONG FAR * LPSTR Return Value 1. Initialization VCB_CREATE VLB has just been created N/A N/A N/A VCB_DESTROY VLB is going to be destroyed N/A N/A N/A VCB_SETFOCUS VLB has just received focus N/A N/A N/A VCB_KILLFOCUS VLB is going to lose focus N/A N/A N/A 2. Virtual limits VCB_LENGTH Request total virtual strings N/A N/A Total strings VCB_WIDTH Request virtual string width N/A N/A String width 3. String retrieval VCB_FULLSTRING Get full string by string ID String ID String buffer TRUE/FALSE VCB_STRING Get string by string ID String ID String buffer TRUE/FALSE VCB_VTHUMB Get string by thumb position Thumb position String buffer TRUE/FALSE VCB_SEARCH Get string by search string String ID String buffer TRUE/FALSE VCB_FIRST Get first display string String ID String buffer TRUE/FALSE VCB_LAST Get last display string String ID String buffer TRUE/FALSE VCB_NEXT Get next display string String ID String buffer TRUE/FALSE VCB_PREV Get previous display string String ID String buffer TRUE/FALSE 4. Horizontal scrolling VCB_HTHUMB New start of display string Thumb position N/A N/A VCB_LEFT New start of display string N/A N/A N/A VCB_RIGHT New start of display string N/A N/A N/A VCB_PAGELEFT New start of display string N/A N/A N/A VCB_PAGERIGHT New start of display string N/A N/A N/A Application Messages to the VLB Procedure Message wParam lParam Return Value VLB_GETCOUNT N/A N/A Total virtual strings VLB_GETSELCOUNT N/A N/A Total selected strings VLB_GETSTRLEN N/A N/A Virtual string length VLB_GETSELSTR Single selection N/A N/A Pointer to selected string Multiple selection String number N/A Pointer to selected string VLB_GETSELID Single selection N/A N/A Selected string ID Multiple selection N/A N/A Handle to array of selected string IDs VLB_SETCURSEL N/A String ID N/A VLB_SETSEL TRUE/FALSE String ID N/A VLB_RELOAD RELOAD_STRINGS N/A N/A RELOAD_STRINGPOS String position N/A RELOAD_STRINGID String ID N/A VLB Procedure Notifications to the Dialog Procedure Message wParam lParam Return Value VLBN_DBLCLK N/A N/A N/A VLBN_ERRSPACE N/A N/A N/A VLBN_SELCHANGE N/A N/A N/A Figure 2 Virtual List Box Source Code VLB.H /* ** VLB.H Virtual List Box header file ** ** Author: Robert A. Wood ** */ #define VLB_CALLBACK lpVLB->CallBack #define VLBSTRLEN 255 // maximum virtual string length typedef LONG (FAR PASCAL * VLBPROC)( WORD, WORD, LONG FAR *, LPSTR ); // VCB Virtual CallBack function Messages //******************************************************************* #define VCB_CREATE 1 // list box has just been created #define VCB_DESTROY 2 // list box is to be destroyed #define VCB_SETFOCUS 3 // list box has received focus #define VCB_KILLFOCUS 4 // list box is to lose focus #define VCB_LENGTH 5 // request for total strings #define VCB_WIDTH 6 // request for vitual width #define VCB_FULLSTRING 7 // get the full string specified #define VCB_STRING 8 // get string specify by StringId #define VCB_VTHUMB 9 // get string specify by thumb #define VCB_SEARCH 10 // get string for search criteria #define VCB_FIRST 11 // get the first string #define VCB_LAST 12 // get the last string #define VCB_NEXT 13 // get the next string #define VCB_PREV 14 // get the previous string #define VCB_HTHUMB 15 // move str start specify thumb #define VCB_LEFT 16 // move str start left one char #define VCB_RIGHT 17 // move str start right one char #define VCB_PAGELEFT 18 // move str start left one col #define VCB_PAGERIGHT 19 // move str start right one col // VLB Messages Sent to the VLB Procedure //******************************************************************* #define VLB_GETCOUNT WM_USER+1 // get total virtual strings #define VLB_GETSELCOUNT WM_USER+2 // get total selected strings #define VLB_GETSTRLEN WM_USER+3 // get total string length #define VLB_GETSELSTR WM_USER+4 // get the select String #define VLB_GETSELID WM_USER+5 // get the select StringId(s) #define VLB_SETCURSEL WM_USER+6 // selects the specify StringId #define VLB_SETSEL WM_USER+7 // sets selection of a StringId #define VLB_RELOAD WM_USER+8 // reload curr. display strings // VLB_RELOAD wParam options //******************************************************************* #define RELOAD_STRINGS 0 #define RELOAD_STRINGPOS 1 #define RELOAD_STRINGID 2 // Notifications Sent to the VLB Parent by the VLB Procedure //******************************************************************* #define VLBN_DBLCLK LBN_DBLCLK // double clicked on a string #define VLBN_ERRSPACE LBN_ERRSPACE // can't allocate memory #define VLBN_SELCHANGE LBN_SELCHANGE // selected string changed // VLB Control Information //******************************************************************* typedef struct { WORD ListBoxId; // list box control id BOOL MultiSelection; // Single or Multi selection RECT ClientRect; // client area rectangle WORD CharWidth; // character width WORD CharHeight; // character height BYTE DisplayStrings; // max number of displayed strings BYTE DisplayChars; // max characters in displayed strings LONG FocusString; // string position of focus frame LONG TotalStrings; // number of virtual strings LONG TotalWidth; // number of virtual chars per string LONG FirstDisplayString; // number of first displayed string LONG TotalSelectedStrings; // number of selected strings LONG MaxSelectedStrings; // maximum number of selected strings VLBPROC CallBack; // VLB Callback function HANDLE hDisplayBuffer; // handle to buffer of display strings HANDLE hStringIds; // handle - array of display StringIds HANDLE hSelectedStringIds; // handle - array of select StringIds LONG SelectedStringId; // single selection selected StringId int ScrollWindow; // scroll lines for ScrollWindow() } VLB, FAR *LPVLB; // 62 bytes // Function called by application to initialize VLB //******************************************************************* BOOL FAR PASCAL InitVLB( HANDLE, HWND, WORD, VLBPROC ); // Internal functions //******************************************************************* LONG FAR PASCAL VLBProc( HWND hCtl, unsigned message, WORD wParam , LONG lParam ); BOOL FAR PASCAL LoadVLB( HWND hCtl, WORD wParam, LONG lParam ); BOOL FAR PASCAL ScrollVLB( HWND hCtl, WORD wParam, int Scroll ); VOID FAR PASCAL SetSelectedString( HWND hCtl, WORD wParam, LPVLB); VOID FAR PASCAL SetFocusString( WORD wParam, LPVLB lpVLB ); VOID InvertSelectedStrings( HDC hCtl, LPVLB lpVLB, int StringPos ); VOID FrameFocusString( HWND hCtl, LPVLB lpVLB, BOOL draw ); VOID FAR PASCAL PaintVLB( HWND hCtl, LPVLB lpVLB ); VOID FAR PASCAL GetVLBColors( HWND hCtl, HANDLE hDC ); //*** END OF VLB.H ************************************************** VLB.C /* ** vlb.c Virtual List Box Source Code file ** ** Author: Robert A. Wood ** Executive Micro Systems ** 1716 Azurite Trail ** Plano, TX 75075 ** ** Microsoft C version 5.1 / medium memory model ** Microsoft Windows SDK version 2.1 ** Runtime: Windows 286 version 2.1 ** */ #include <windows.h> #undef min #undef max #include <stdlib.h> #include <stdio.h> #include "vlb.h" #include "lmem.h" static char StringBuffer[ VLBSTRLEN + 1 ]; static char szVLBPropName[] = "VLB"; static FARPROC lpfnVLBProc = NULL; LPSTR FAR PASCAL lstrcpy (LPSTR, LPSTR) ; WORD FAR PASCAL Rpad( LPSTR str, WORD length ); /* ** Virtual List Box initialization function ********************************************************************/ BOOL FAR PASCAL InitVLB( HANDLE hInstance, HWND hDlg, WORD ListBoxId, VLBPROC VLBCallBack ) { int x; HANDLE hCtl; HANDLE hVLB; LPVLB lpVLB; HDC hDC; TEXTMETRIC tm; // need info about character sizes LONG FAR * lpStringIds; // check that a call back function address was passed in if( ! VLBCallBack ) { MessageBeep( 0 ); MessageBox( hDlg, "Invalid VLBCallBack", NULL, MB_ICONHAND | MB_OK ); return( FALSE ); } // check for existence of List boxId in specified dialog box if( ( hCtl = GetDlgItem( hDlg, ListBoxId ) ) = = NULL ) { MessageBeep( 0 ); MessageBox( hDlg, "Invalid List Box Id", NULL, MB_ICONHAND | MB_OK ); return( FALSE ); } // allocate VLB control information structure if( hVLB = GlobalAlloc( GHND, (LONG)sizeof( VLB ) ) ) lpVLB = (LPVLB)GlobalLock( hVLB ); else { SendMessage( hDlg, WM_COMMAND, ListBoxId, MAKELONG( hCtl, VLBN_ERRSPACE ) ); return( FALSE ); } hDC = GetDC( hCtl ); GetTextMetrics( hDC, &tm ); ReleaseDC( hCtl, hDC ); lpVLB->ListBoxId = ListBoxId; lpVLB->MultiSelection = (BOOL) (GetWindowLong( hCtl, GWL_STYLE ) & LBS_MULTIPLESEL); GetClientRect( hCtl, &lpVLB->ClientRect ); lpVLB->CharWidth = tm.tmAveCharWidth; lpVLB->CharHeight = tm.tmHeight + tm.tmExternalLeading; lpVLB->DisplayStrings = (BYTE) lpVLB->ClientRect.bottom / lpVLB->CharHeight; lpVLB->DisplayChars = (BYTE) lpVLB->ClientRect.right / lpVLB->CharWidth; lpVLB->FocusString = 0; lpVLB->FirstDisplayString = 0; lpVLB->TotalSelectedStrings = 0; lpVLB->MaxSelectedStrings = lpVLB->DisplayStrings; VLB_CALLBACK = VLBCallBack; lpVLB->SelectedStringId = -1; // allocate buffer for displayed strings if( !(lpVLB->hDisplayBuffer = GlobalAlloc( GHND, (LONG) (lpVLB->DisplayStrings * (lpVLB->DisplayChars + 1) ) ) ) ) { GlobalUnlock( hVLB ); GlobalFree( hVLB ); SendMessage( hDlg, WM_COMMAND, ListBoxId, MAKELONG( hCtl, VLBN_ERRSPACE ) ); return( FALSE ); } // allocate array of longs for the string Ids of displayed strings if( !(lpVLB->hStringIds = GlobalAlloc( GHND, (LONG) (lpVLB->DisplayStrings * sizeof( LONG ) ) ) ) ) { GlobalUnlock( lpVLB->hDisplayBuffer ); GlobalFree( lpVLB->hDisplayBuffer ); GlobalUnlock( hVLB ); GlobalFree( hVLB ); SendMessage( hDlg, WM_COMMAND, ListBoxId, MAKELONG( hCtl, VLBN_ERRSPACE ) ); return( FALSE ); } // initialize string Ids to -1 lpStringIds = (LONG FAR *) GlobalLock( lpVLB->hStringIds ); for( x = 0; x < lpVLB->DisplayStrings; x++, lpStringIds[x] = -1L ) ; GlobalUnlock( lpVLB->hStringIds ); // setup for a multiselection list box if( lpVLB->MultiSelection ) { LONG FAR * lpSelectedStringIds; // allocate array of longs for the selected string Id's if( !(lpVLB->hSelectedStringIds = GlobalAlloc( GHND, (LONG) (lpVLB->MaxSelectedStrings * sizeof( LONG ) ) ) ) ) { GlobalUnlock( lpVLB->hStringIds ); GlobalFree( lpVLB->hStringIds ); GlobalUnlock( lpVLB->hDisplayBuffer ); GlobalFree( lpVLB->hDisplayBuffer ); GlobalUnlock( hVLB ); GlobalFree( hVLB ); SendMessage( hDlg, WM_COMMAND, ListBoxId, MAKELONG( hCtl, VLBN_ERRSPACE ) ); return( FALSE ); } // initialize selected string Ids to -1 lpSelectedStringIds = (LONG FAR *) GlobalLock( lpVLB->hSelectedStringIds ); for( x = 0; x < lpVLB->DisplayStrings; x++, lpSelectedStringIds[x] = -1L ) ; GlobalUnlock( lpVLB->hSelectedStringIds ); } if( !lpfnVLBProc ) lpfnVLBProc = MakeProcInstance( (FARPROC) VLBProc, hInstance ); // subclass VLB SetWindowLong( hCtl, GWL_WNDPROC, (LONG) lpfnVLBProc ); // store handle to VLB structure in the control's property list SetProp( hCtl, szVLBPropName, hVLB ); // send create messages to control and to the VLB callback func SendMessage( hCtl, WM_CREATE, 0, 0L ); VLB_CALLBACK( ListBoxId, VCB_CREATE, 0, 0L ); // get the vitual list box length and set the scroll range lpVLB->TotalStrings = VLB_CALLBACK( ListBoxId, VCB_LENGTH, 0, 0L); SetScrollRange( hCtl, SB_VERT, 0, (int) lpVLB->TotalStrings - lpVLB->DisplayStrings - 1, TRUE ); // check for horizontally scrolling if( GetWindowLong( hCtl, GWL_STYLE ) & WS_HSCROLL ) { // get the vitual list box width and set the scroll range lpVLB->TotalWidth = VLB_CALLBACK( ListBoxId, VCB_WIDTH, 0, 0L); SetScrollRange( hCtl, SB_HORZ, 0, (int) lpVLB->TotalWidth - 1, TRUE ); } // load list box VLB_CALLBACK( ListBoxId, VCB_SETFOCUS, 0, 0L ); SendMessage( hCtl, VLB_RELOAD, RELOAD_STRINGPOS, 0L ); VLB_CALLBACK( ListBoxId, VCB_KILLFOCUS, 0, 0L ); // I'm out of here GlobalUnlock( hVLB ); return( TRUE ); } /* ** Virtual List Box Procedure ********************************************************************/ LONG FAR PASCAL VLBProc( HWND hCtl, unsigned message, WORD wParam, LONG lParam ) { HANDLE hVLB; LPVLB lpVLB; LONG lThumb; switch ( message ) { case WM_CHAR: break; case WM_SETCURSOR: // override setting cursor break; case WM_GETDLGCODE: // don't let dialog manager have return( DLGC_WANTARROWS ); // his way with the arrow keys case WM_CREATE: // sent just after being subclassed return( 1L ); case VLB_RELOAD: // reload the list box LoadVLB( hCtl, wParam, lParam ); return( 1L ); case WM_PAINT: // this is where it all takes shape hVLB = GetProp( hCtl, szVLBPropName ); lpVLB = (LPVLB) GlobalLock( hVLB ); PaintVLB( hCtl, lpVLB ); GlobalUnlock( hVLB ); break; case WM_SETFOCUS: HideCaret(hCtl); hVLB = GetProp( hCtl, szVLBPropName ); lpVLB = (LPVLB) GlobalLock( hVLB ); VLB_CALLBACK( lpVLB->ListBoxId, VCB_SETFOCUS, 0, 0L ); FrameFocusString( hCtl, lpVLB, TRUE ); GlobalUnlock( hVLB ); break; case WM_KILLFOCUS: hVLB = GetProp( hCtl, szVLBPropName ); lpVLB = (LPVLB) GlobalLock( hVLB ); FrameFocusString( hCtl, lpVLB, FALSE ); InvertSelectedStrings( hCtl, lpVLB, (int)( lpVLB->FocusString - lpVLB->FirstDisplayString ) ); VLB_CALLBACK( lpVLB->ListBoxId, VCB_KILLFOCUS, 0, 0L ); GlobalUnlock( hVLB ); break; case WM_VSCROLL: switch( wParam ) { case SB_THUMBPOSITION: LoadVLB( hCtl, RELOAD_STRINGPOS, (LONG)LOWORD( lParam ) ); break; case SB_LINEDOWN: case SB_LINEUP: case SB_PAGEDOWN: case SB_PAGEUP: ScrollVLB( hCtl, wParam, NULL ); break; } break; case WM_HSCROLL: hVLB = GetProp( hCtl, szVLBPropName ); lpVLB = (LPVLB) GlobalLock( hVLB ); lThumb = -1; switch( wParam ) { case SB_THUMBPOSITION: lThumb = (LONG)LOWORD( lParam ); lThumb = VLB_CALLBACK( lpVLB->ListBoxId, VCB_HTHUMB, &lThumb, 0L ); LoadVLB( hCtl, RELOAD_STRINGS, 0L ); break; case SB_LINEDOWN: lThumb = VLB_CALLBACK( lpVLB->ListBoxId, VCB_RIGHT, 0, 0L ); LoadVLB( hCtl, RELOAD_STRINGS, 0L ); break; case SB_LINEUP: lThumb = VLB_CALLBACK( lpVLB->ListBoxId, VCB_LEFT, 0, 0L ); LoadVLB( hCtl, RELOAD_STRINGS, 0L ); break; case SB_PAGEDOWN: lThumb = VLB_CALLBACK( lpVLB->ListBoxId,VCB_PAGERIGHT,0,0L); LoadVLB( hCtl, RELOAD_STRINGS, 0L ); break; case SB_PAGEUP: lThumb = VLB_CALLBACK( lpVLB->ListBoxId, VCB_PAGELEFT,0,0L); LoadVLB( hCtl, RELOAD_STRINGS, 0L ); break; } if( lThumb >= 0 ) { lThumb = (LONG)min( (int)lpVLB->TotalWidth - 1, max( 0, (int)lThumb ) ); SetScrollPos( hCtl, SB_HORZ, (int)lThumb, TRUE ); } GlobalUnlock( hVLB ); break; case WM_MOUSEMOVE: break; case WM_LBUTTONDBLCLK: SendMessage( GetParent( hCtl ), WM_COMMAND, GetWindowWord( hCtl, GWW_ID ), MAKELONG( hCtl, VLBN_DBLCLK ) ); break; case WM_LBUTTONDOWN: { LONG lFocusString; hVLB = GetProp( hCtl, szVLBPropName ); lpVLB = (LPVLB) GlobalLock( hVLB ); lpVLB->FocusString = lpVLB->FirstDisplayString + ( HIWORD( lParam ) / lpVLB->CharHeight ); SetSelectedString( hCtl, NULL, lpVLB ); InvalidateRect( hCtl, NULL, TRUE ); GlobalUnlock( hVLB ); break; } case WM_KEYDOWN: { BOOL CtrlKey = HIBYTE( GetKeyState( VK_CONTROL ) ); hVLB = GetProp( hCtl, szVLBPropName ); lpVLB = (LPVLB) GlobalLock( hVLB ); switch( wParam ) { case ' ': { SetSelectedString( hCtl, wParam, lpVLB ); InvalidateRect( hCtl, NULL, TRUE ); break; } case VK_DOWN: if( lpVLB->FocusString = = lpVLB->FirstDisplayString + lpVLB->DisplayStrings - 1 ) ScrollVLB( hCtl, SB_LINEDOWN, NULL ); SetFocusString( wParam, lpVLB ); if( ! CtrlKey ) SetSelectedString( hCtl, wParam, lpVLB ); InvalidateRect( hCtl, NULL, TRUE ); PaintVLB( hCtl, lpVLB ); break; case VK_UP: if( lpVLB->FocusString = = lpVLB->FirstDisplayString ) ScrollVLB( hCtl, SB_LINEUP, NULL ); SetFocusString( wParam, lpVLB ); if( ! CtrlKey ) SetSelectedString( hCtl, wParam, lpVLB ); InvalidateRect( hCtl, NULL, TRUE ); PaintVLB( hCtl, lpVLB ); break; case VK_NEXT: if( lpVLB->FocusString = = lpVLB->FirstDisplayString + lpVLB->DisplayStrings - 1 ) ScrollVLB( hCtl, SB_PAGEDOWN, NULL ); SetFocusString( wParam, lpVLB ); if( ! CtrlKey ) SetSelectedString( hCtl, wParam, lpVLB ); InvalidateRect( hCtl, NULL, TRUE ); PaintVLB( hCtl, lpVLB ); break; case VK_PRIOR: if( lpVLB->FocusString = = lpVLB->FirstDisplayString ) ScrollVLB( hCtl, SB_PAGEUP, NULL ); SetFocusString( wParam, lpVLB ); if( ! CtrlKey ) SetSelectedString( hCtl, wParam, lpVLB ); InvalidateRect( hCtl, NULL, TRUE ); PaintVLB( hCtl, lpVLB ); break; case VK_HOME: LoadVLB( hCtl, RELOAD_STRINGPOS, 0L ); SetFocusString( wParam, lpVLB ); if( ! CtrlKey ) SetSelectedString( hCtl, wParam, lpVLB ); InvalidateRect( hCtl, NULL, TRUE ); break; case VK_END: LoadVLB( hCtl, RELOAD_STRINGPOS, lpVLB->TotalStrings - 1 ); SetFocusString( wParam, lpVLB ); if( ! CtrlKey ) SetSelectedString( hCtl, wParam, lpVLB ); InvalidateRect( hCtl, NULL, TRUE ); break; case VK_RIGHT: case VK_LEFT: { int message; if( wParam = = VK_RIGHT ) { if( CtrlKey ) message = VCB_PAGERIGHT; else message = VCB_RIGHT; } else { if( CtrlKey ) message = VCB_PAGELEFT; else message = VCB_LEFT; } lThumb = VLB_CALLBACK( lpVLB->ListBoxId, message, 0, 0L ); lThumb = (LONG)min( (int)lpVLB->TotalWidth - 1, max( 0, (int)lThumb ) ); SetScrollPos( hCtl, SB_HORZ, (int)lThumb, TRUE ); LoadVLB( hCtl, RELOAD_STRINGS, 0L ); InvalidateRect( hCtl, NULL, TRUE ); break; } } GlobalUnlock( hVLB ); break; } case VLB_SETCURSEL: // WM_USER+6 select specified StringId case VLB_SETSEL: // WM_USER+7 sets selection of StringId break; case VLB_GETCOUNT: // WM_USER+1 get total virtual strings case VLB_GETSELCOUNT: // WM_USER+2 get total selected strings case VLB_GETSTRLEN: // WM_USER+3 get total string length case VLB_GETSELSTR: // WM_USER+4 get the selected String case VLB_GETSELID: // WM_USER+5 get selected StringId(s) { LONG lret = 0; hVLB = GetProp( hCtl, szVLBPropName ); lpVLB = (LPVLB) GlobalLock( hVLB ); if( message = = VLB_GETCOUNT ) lret = lpVLB->TotalStrings; else if( message = = VLB_GETSELCOUNT ) lret = lpVLB->TotalSelectedStrings; else if( message = = VLB_GETSTRLEN ) lret = lpVLB->CharWidth; else if( message = = VLB_GETSELSTR ) { if( lpVLB->TotalSelectedStrings ) { if( lpVLB->MultiSelection ) { LONG FAR *lpSelectedStringIds = (LONG FAR *)GlobalLock( lpVLB->hSelectedStringIds); VLB_CALLBACK( lpVLB->ListBoxId, VCB_FULLSTRING, lpSelectedStringIds + wParam, StringBuffer ); GlobalUnlock( lpVLB->hSelectedStringIds ); } else VLB_CALLBACK( lpVLB->ListBoxId, VCB_FULLSTRING, &lpVLB->SelectedStringId, StringBuffer ); } else *StringBuffer = 0; lret = (LONG)(LPSTR)StringBuffer; } else if( message = = VLB_GETSELID ) { if( lpVLB->MultiSelection ) lret = lpVLB->hSelectedStringIds; else lret = lpVLB->SelectedStringId; } GlobalUnlock( hVLB ); return( lret ); } case WM_DESTROY: hVLB = GetProp( hCtl, szVLBPropName ); lpVLB = (LPVLB) GlobalLock( hVLB ); VLB_CALLBACK( lpVLB->ListBoxId, VCB_DESTROY, 0, 0L ); GlobalFree( lpVLB->hDisplayBuffer ); GlobalFree( lpVLB->hStringIds ); if( lpVLB->MultiSelection ) GlobalFree( lpVLB->hSelectedStringIds ); GlobalUnlock( hVLB ); GlobalFree( hVLB ); RemoveProp( hCtl, szVLBPropName ); break; default: return( DefWindowProc( hCtl, message, wParam, lParam ) ); } return( 0L ); } //******************************************************************* VOID FrameFocusString( HWND hCtl, LPVLB lpVLB, BOOL draw ) { RECT Rect; HDC hDC; // is the focus string visible if( lpVLB->FocusString >= lpVLB->FirstDisplayString && lpVLB->FocusString < lpVLB->FirstDisplayString + lpVLB->DisplayStrings ) { hDC = GetDC( hCtl ); Rect.left = lpVLB->ClientRect.left + 1; Rect.right = lpVLB->ClientRect.right; Rect.top = (WORD) ( (lpVLB->FocusString - lpVLB->FirstDisplayString) * lpVLB->CharHeight) + 1; Rect.bottom = Rect.top + lpVLB->CharHeight; if( draw ) FrameRect( hDC, &Rect, GetStockObject( GRAY_BRUSH ) ); else // redraw string without frame { LPSTR lpString = (LPSTR)GlobalLock( lpVLB->hDisplayBuffer ); WORD nLine = (WORD) ( lpVLB->FocusString - lpVLB->FirstDisplayString ); // move display buffer pointer to focus string lpString += nLine * ( lpVLB->DisplayChars + 1 ); TextOut ( hDC, 1, ( nLine * lpVLB->CharHeight ) + 1, lpString, lpVLB->DisplayChars ); GlobalUnlock( lpVLB->hDisplayBuffer ); } ReleaseDC( hCtl, hDC ); } } //******************************************************************* VOID InvertSelectedStrings( HDC hCtl, LPVLB lpVLB, int StringPos ) { int x; LONG y; RECT Rect; int FirstString, LastString; LONG FAR *lpStringIds; HDC hDC; hDC = GetDC( hCtl ); lpStringIds = (LONG FAR *)GlobalLock( lpVLB->hStringIds ); Rect.left = lpVLB->ClientRect.left + 1; Rect.right = lpVLB->ClientRect.right; if( StringPos < 0 ) // process all displayed strings { FirstString = 0; LastString = (int)lpVLB->DisplayStrings - 1; } else FirstString = LastString = StringPos; if( lpVLB->MultiSelection ) { LONG FAR *lpSelectedStringIds = (LONG FAR *)GlobalLock( lpVLB->hSelectedStringIds ); for( x = FirstString; x <= LastString; x++ ) { for( y = 0; y < lpVLB->TotalSelectedStrings; y++ ) { if( lpStringIds[x] == lpSelectedStringIds[ y ] ) { Rect.top = ( x * lpVLB->CharHeight ) + 1; Rect.bottom = Rect.top + lpVLB->CharHeight; InvertRect( hDC, &Rect ); break; } } } GlobalUnlock( lpVLB->hSelectedStringIds ); } else // single selection { for( x = FirstString; x <= LastString; x++ ) { if( lpStringIds[x] = = lpVLB->SelectedStringId ) { Rect.top = ( x * lpVLB->CharHeight ) + 1; Rect.bottom = Rect.top + lpVLB->CharHeight; InvertRect( hDC, &Rect ); break; } } } ReleaseDC( hCtl, hDC ); GlobalUnlock( lpVLB->hStringIds ); } //******************************************************************* WORD FAR PASCAL Rpad( LPSTR str, WORD length ) { LPSTR cp = str; // pointer to string int x; // current string position for( x = 0; *cp; x++, cp++ ) // skip to end of string ; for( ; x < length; x++, *cp++ = ' ' ) // pad string with spaces ; *cp = 0; // NULL terminate string return( length ); // return new string length } //******************************************************************* BOOL FAR PASCAL LoadVLB( HWND hCtl, WORD wParam, LONG lParam ) { int x; HANDLE hVLB = GetProp( hCtl, szVLBPropName ); LPVLB lpVLB = (LPVLB)GlobalLock( hVLB ); LPSTR lpString = (LPSTR)GlobalLock( lpVLB->hDisplayBuffer ); LONG FAR *lpStringIds = (LONG FAR *)GlobalLock(lpVLB->hStringIds); LONG l; // initialize the DisplayBuffer and StringIds lmemset( lpString, 0, lpVLB->DisplayStrings * (lpVLB->DisplayChars + 1) ); for( x = 0; x < lpVLB->DisplayStrings; x++, lpStringIds[x] = -1L ) ; if( wParam = = RELOAD_STRINGS ) VLB_CALLBACK( lpVLB->ListBoxId, VCB_STRING, &lpStringIds[0], StringBuffer ); else if( wParam = = RELOAD_STRINGPOS ) { lpStringIds[0] = min( lParam, lpVLB->TotalStrings - lpVLB->DisplayStrings ); lpVLB->FirstDisplayString = lpStringIds[0]; VLB_CALLBACK( lpVLB->ListBoxId, VCB_VTHUMB, &lpStringIds[0], StringBuffer ); } else if( wParam = = RELOAD_STRINGID ) { lpStringIds[0] = lParam; VLB_CALLBACK( lpVLB->ListBoxId, VCB_STRING, &lpStringIds[0], StringBuffer ); } // load first DisplayString StringBuffer[ lpVLB->DisplayChars ] = 0; // Null @ Display width lstrcpy( lpString, StringBuffer ); // advance to next string Rpad( lpString, lpVLB->DisplayChars ); // pad with spaces for( x = 1; x < lpVLB->DisplayStrings; x++ ) { l = lpStringIds[x - 1]; if( VLB_CALLBACK( lpVLB->ListBoxId, VCB_NEXT, &l, StringBuffer ) ) { StringBuffer[ lpVLB->DisplayChars ] = 0; // Null Terminate lpString += lpVLB->DisplayChars + 1; // go to next str lstrcpy( lpString, StringBuffer ); // copy buf to str Rpad( lpString, lpVLB->DisplayChars ); // pad with spaces lpStringIds[x] = l; } else break; } InvalidateRect( hCtl, NULL, TRUE ); // Force WM_PAINT message // set new scroll bar thumb postion SetScrollPos( hCtl, SB_VERT, (int)lpVLB->FirstDisplayString,TRUE); GlobalUnlock( lpVLB->hDisplayBuffer ); GlobalUnlock( lpVLB->hStringIds ); GlobalUnlock( hVLB ); return( TRUE ); } //******************************************************************* BOOL FAR PASCAL ScrollVLB( HWND hCtl, WORD wParam, int Scroll ) { int x, scroll, ret = TRUE; HANDLE hVLB = GetProp( hCtl, szVLBPropName ); LPVLB lpVLB = (LPVLB)GlobalLock( hVLB ); LPSTR lpString = (LPSTR)GlobalLock( lpVLB->hDisplayBuffer ); LONG FAR *lpStringIds = (LONG FAR *)GlobalLock(lpVLB->hStringIds); LONG l; LPSTR lpGoodStrings; int nGoodStrings; // Check if scroll request is possible if( wParam = = SB_LINEDOWN ) { // at the end of the total strings if( lpVLB->FirstDisplayString + lpVLB->DisplayStrings > lpVLB->TotalStrings - 1 ) ret = FALSE; else scroll = 1; lpVLB->ScrollWindow = 1; } else if( wParam = = SB_LINEUP ) { if( lpVLB->FirstDisplayString = = 0 ) ret = FALSE; else scroll = 1; lpVLB->ScrollWindow = -1; } else if( wParam = = SB_PAGEDOWN ) { if( lpVLB->FirstDisplayString + lpVLB->DisplayStrings > lpVLB->TotalStrings - 1 ) ret = FALSE; else scroll = min( (int)lpVLB->DisplayStrings - 1, (int)(lpVLB->TotalStrings - ( lpVLB->FirstDisplayString + lpVLB->DisplayStrings ) ) ); lpVLB->ScrollWindow = scroll; } else if( wParam = = SB_PAGEUP ) { if( lpVLB->FirstDisplayString = = 0 ) ret = FALSE; else scroll = min( (int)lpVLB->DisplayStrings - 1, (int)lpVLB->FirstDisplayString ); lpVLB->ScrollWindow = scroll * -1; } else // scroll number of the Scroll 3rd paramater { lpVLB->ScrollWindow = Scroll; if( Scroll < 0 ) { Scroll = abs( Scroll ); // absolute scroll amount wParam = SB_LINEUP; // scroll up } else if( Scroll > 0 ) wParam = SB_LINEDOWN; // scroll down else // if( Scroll = = 0 ) // no scroll amt specified ret = FALSE; if( Scroll >= lpVLB->DisplayStrings ) // scroll less than # ret = FALSE; // of displayed strings else scroll = Scroll; } if( ret = = FALSE ) { lpVLB->ScrollWindow = FALSE; GlobalUnlock( lpVLB->hDisplayBuffer ); GlobalUnlock( lpVLB->hStringIds ); GlobalUnlock( hVLB ); return( FALSE ); } // pointer to strings that will still be displayed lpGoodStrings = lpString + ( (lpVLB->DisplayChars + 1) * scroll ); // number of strings that will still be displayed nGoodStrings = ( lpVLB->DisplayStrings - scroll ); // adjust strings that will still be displayed and get new strings if( wParam = = SB_LINEUP || wParam = = SB_PAGEUP ) { lpVLB->FirstDisplayString -= scroll; // push good strings & StringIds down in their buffers lmemmove( lpGoodStrings, lpString, nGoodStrings * (lpVLB->DisplayChars + 1) ); lmemmove( lpStringIds + scroll, lpStringIds, nGoodStrings * sizeof( LONG ) ); // move Display string pointer to last string to get lpString + = ( scroll - 1 ) * (lpVLB->DisplayChars + 1); // get the previous scroll number of strings for( x = 0; x < scroll; x++ ) { // get the StringId of the last string read l = lpStringIds[ scroll - x ]; if( VLB_CALLBACK( lpVLB->ListBoxId, VCB_PREV, &l, StringBuffer ) ) { StringBuffer[ lpVLB->DisplayChars ] = 0;// Null Terminate lstrcpy( lpString, StringBuffer ); // copy buf to str Rpad( lpString, lpVLB->DisplayChars ); // pad with spaces lpString -= lpVLB->DisplayChars + 1; // go to next str lpStringIds[ ( scroll - x ) - 1 ] = l; // save StringId } else break; } } else // if( wParam = = SB_LINEDOWN || wParam = = SB_PAGEDOWN ) { lpVLB->FirstDisplayString + = scroll; // move good strings & StringIds to the beginning of their bufs lmemmove( lpString, lpGoodStrings, nGoodStrings * (lpVLB->DisplayChars + 1) ); lmemmove( lpStringIds, lpStringIds + scroll, nGoodStrings * sizeof( LONG ) ); // move Display string pointer to first string to get lpString += nGoodStrings * (lpVLB->DisplayChars + 1); // get the next scroll number of strings for( x = 0; x < scroll; x++ ) { // get the StringId of the last string read l = lpStringIds[ ( nGoodStrings + x ) - 1 ]; if( VLB_CALLBACK( lpVLB->ListBoxId, VCB_NEXT, &l, StringBuffer ) ) { StringBuffer[ lpVLB->DisplayChars ] = 0;// Null Terminate lstrcpy( lpString, StringBuffer ); // copy buf to str Rpad( lpString, lpVLB->DisplayChars ); // pad with spaces lpString += lpVLB->DisplayChars + 1; // go to next str lpStringIds[ nGoodStrings + x ] = l; // save StringId } else break; } } InvalidateRect( hCtl, NULL, TRUE ); // Force WM_PAINT message // set new scroll bar thumb postion SetScrollPos( hCtl, SB_VERT, (int)lpVLB->FirstDisplayString,TRUE); GlobalUnlock( lpVLB->hDisplayBuffer ); GlobalUnlock( lpVLB->hStringIds ); GlobalUnlock( hVLB ); return( TRUE ); } //******************************************************************* VOID FAR PASCAL SetSelectedString( HWND hCtl, WORD wParam, LPVLB lpVLB ) { LONG FAR *lpStringIds; LONG FAR *lpSelectedStringId; lpStringIds = (LONG FAR *)GlobalLock( lpVLB->hStringIds ); if( lpVLB->MultiSelection ) { LONG FAR *lpSelectedStringIds; if( ! HIBYTE( GetKeyState( VK_SHIFT ) ) ) lpVLB->TotalSelectedStrings = 0; else { if(lpVLB->TotalSelectedStrings = = lpVLB->MaxSelectedStrings) { lpVLB->MaxSelectedStrings + = lpVLB->DisplayStrings; if( ! ( lpVLB->hSelectedStringIds = GlobalReAlloc( lpVLB->hSelectedStringIds, lpVLB->MaxSelectedStrings * sizeof( LONG ), GHND ) ) ) { lpVLB->MaxSelectedStrings -= lpVLB->DisplayStrings; SendMessage( GetParent( hCtl ), WM_COMMAND, lpVLB->ListBoxId, MAKELONG( hCtl, VLBN_ERRSPACE ) ); return; } } } lpSelectedStringIds = (LONG FAR *) GlobalLock( lpVLB->hSelectedStringIds ); lpSelectedStringId = lpSelectedStringIds + lpVLB->TotalSelectedStrings++; } else { lpVLB->TotalSelectedStrings = 1; lpSelectedStringId = &lpVLB->SelectedStringId; } switch( wParam ) { case NULL: // mouse left button click case ' ': // space bar select focus string case VK_DOWN: case VK_UP: *lpSelectedStringId = lpStringIds[lpVLB->FocusString - lpVLB->FirstDisplayString]; break; case VK_NEXT: case VK_END: *lpSelectedStringId = lpStringIds[lpVLB->DisplayStrings-1]; break; case VK_PRIOR: case VK_HOME: *lpSelectedStringId = lpStringIds[0]; break; } SendMessage( GetParent( hCtl ), WM_COMMAND, lpVLB->ListBoxId, MAKELONG( hCtl, VLBN_SELCHANGE ) ); if( lpVLB->MultiSelection ) GlobalUnlock( lpVLB->hSelectedStringIds ); GlobalUnlock( lpVLB->hStringIds ); } //******************************************************************* VOID FAR PASCAL SetFocusString( WORD wParam, LPVLB lpVLB ) { switch( wParam ) { case VK_DOWN: case VK_UP: if( lpVLB->FocusString < lpVLB->FirstDisplayString ) lpVLB->FocusString = lpVLB->FirstDisplayString; else if( lpVLB->FocusString > lpVLB->FirstDisplayString +lpVLB->DisplayStrings - 1 ) lpVLB->FocusString = lpVLB->FirstDisplayString + lpVLB->DisplayStrings - 1; else { if( wParam = = VK_DOWN ) { if( lpVLB->FocusString < lpVLB->TotalStrings - 1 ) lpVLB->FocusString++; } else if( lpVLB->FocusString ) lpVLB->FocusString--; } break; case VK_NEXT: lpVLB->FocusString = lpVLB->FirstDisplayString + lpVLB->DisplayStrings - 1; break; case VK_PRIOR: lpVLB->FocusString = lpVLB->FirstDisplayString; break; case VK_HOME: lpVLB->FocusString = 0; break; case VK_END: lpVLB->FocusString = lpVLB->TotalStrings - 1; break; } } //******************************************************************* VOID FAR PASCAL PaintVLB( HWND hCtl, LPVLB lpVLB ) { int x, y, first, last; HDC hDC; PAINTSTRUCT ps; LPSTR lpString; RECT Rect; LONG FAR * lpStringIds; // let's get the display string buffer and each string's Id lpString = (LPSTR)GlobalLock( lpVLB->hDisplayBuffer ); lpStringIds = (LONG FAR *)GlobalLock( lpVLB->hStringIds ); hDC = BeginPaint( hCtl, &ps ); GetVLBColors( hCtl, hDC ); first = 0; last = lpVLB->DisplayStrings - 1; for( x = first, y = 1; x <= last; x++, y + = lpVLB->CharHeight ) { if( lpStringIds[x] >= 0 ) { TextOut ( hDC, 1, y, lpString, lpVLB->DisplayChars ); lpString += lpVLB->DisplayChars + 1; } else { lmemset( lpString, ' ', lpVLB->DisplayChars ); TextOut ( hDC, 1, y, lpString, lpVLB->DisplayChars ); } } EndPaint( hCtl, &ps ); if( lpVLB->TotalSelectedStrings ) InvertSelectedStrings( hCtl, lpVLB, -1 ); if( hCtl == GetFocus() ) FrameFocusString( hCtl, lpVLB, TRUE ); lpVLB->ScrollWindow = FALSE; GlobalUnlock( lpVLB->hDisplayBuffer ); GlobalUnlock( lpVLB->hStringIds ); } //******************************************************************* VOID FAR PASCAL GetVLBColors( HWND hCtl, HANDLE hDC ) { HANDLE hBrush; SetBkColor( hDC, GetSysColor( COLOR_WINDOW ) ); SetTextColor( hDC, GetSysColor( COLOR_WINDOWTEXT ) ); if( hBrush = (HANDLE)SendMessage( GetParent( hCtl ), WM_CTLCOLOR, hDC, MAKELONG( hCtl, CTLCOLOR_LISTBOX ) ) ) SelectObject( hDC, hBrush ); } //*** END OF VLB.C ************************************************** Figure 3 VLB Information Structure typedef struct { WORD ListboxId; // list box control ID BOOL MultiSelection; // single or multi selection RECT ClientRect; // client area rectangle WORD CharWidth; // character width WORD CharHeight; // character height BYTE DisplayStrings; // maximum number of displayed strings BYTE DisplayChars; // maximum chars in displayed strings LONG FocusString; // string position of focus frame LONG TotalStrings; // number of virtual strings LONG TotalWidth; // number of virtual chars per string LONG FirstDisplayString; // number of first displayed string LONG TotalSelectedStrings; // number of selected strings LONG MaxSelectedStrings; // maximum number of selected strings VLBPROC CallBack; // VLB Callback function HANDLE hDisplayBuffer; // handle to buffer of display strings HANDLE hStringIds; // handle to array - display StringIDs HANDLE hSelectedStringIds; // handle to array of select StringIDs LONG SelectedStringId; // single selection selected StringID int ScrollWindow; // scroll lines for ScrollWindow() } VLB, FAR *LPVLB; // 62 bytes Figure 4 DEMO Source Code and Build Files DEMO # # demo make file # COM = cl -c -AM -Gsw -Oas -Zlpe -W2 $*.c CVCOM = cl -c -AM -Gsw -Od -Zilpe -W2 $*.c LNK = link4 demo vlb /align:16,,, lmem mlibw mlibcew, demo.def CVLNK = link4 demo vlb /align:16,,, lmem mlibw mlibcew, demo.def /CO ASM = MASM $*.ASM; LIB = lib lmem -+ $*; .c.obj: $(COM) .asm.obj: $(ASM) $(LIB) demo.res: demo.dlg demo.rc demo.h rc -r demo.rc demo.obj: demo.c demo.h vlb.h vlb.obj: vlb.c vlb.h lmemmove.obj: lmemmove.asm setup.h lmemset.obj: lmemset.asm setup.h demo.exe: demo.obj demo.res vlb.obj $(LNK) rc demo.res DEMO. RC #include <windows.h> #include "demo.h" demo MENU BEGIN MENUITEM "Dialog...", IDM_DIALOG END rcinclude demo.dlg DEMO.DEF NAME demo STUB 'WINSTUB.EXE' CODE MOVEABLE DATA MOVEABLE MULTIPLE HEAPSIZE 1024 STACKSIZE 4096 EXPORTS VLBPROC @1 DLGPROC @2 MAINWNDPROC @3 DEMO.H /* ** demo.h header file for demo of Virtual List Box ** ** Author: Robert A. Wood ** Executive Micro Systems ** 1716 Azurite Trail ** Plano, TX 75075 ** */ #define IDD_LISTBOX 100 #define IDD_TEXT 101 #define IDM_DIALOG 100 BOOL MainInit( HANDLE ); LONG FAR PASCAL MainWndProc( HWND, unsigned, WORD, LONG ); BOOL FAR PASCAL DlgProc( HWND, unsigned, WORD, LONG ); LONG FAR PASCAL VLBfile( WORD, unsigned, LONG FAR *, LPSTR ); DEMO.DLG DEMO DIALOG LOADONCALL MOVEABLE DISCARDABLE 27, 28, 107, 110 CAPTION " Virtual List Box " STYLE WS_BORDER | WS_CAPTION | WS_DLGFRAME | WS_POPUP BEGIN CONTROL "", 102, "static", SS_BLACKFRAME | WS_CHILD, 10, 20, 84, 57 CONTROL "", 100, "edit", WS_BORDER | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP | WS_CHILD, 10, 20, 84, 57 CONTROL "OK", 1, "button", BS_DEFPUSHBUTTON | WS_TABSTOP | WS_CHILD, 11, 90, 32, 14 CONTROL "Cancel", 2, "button", BS_PUSHBUTTON | WS_TABSTOP | WS_CHILD, 56, 90, 32, 14 CONTROL "Selected String", 101, "static", SS_LEFT | WS_CHILD, 10, 6, 80, 8 END DEMO.C /* ** demo.c demonstration of VLB (virtual list box) ** ** Author: Robert A. Wood ** ** Microsoft C version 5.1 / medium memory model ** Microsoft Windows SDK version 2.1 ** Runtime: Windows 286 version 2.1 ** */ #include <windows.h> #include <stdio.h> #include <string.h> #include <io.h> #include <fcntl.h> #include "demo.h" #include "vlb.h" #include "lmem.h" HWND hWnd; HANDLE hInst; char szAppName[] = "Demo"; WORD FAR PASCAL WinMain( HANDLE hInstance, HANDLE hPrevInstance, LPSTR lpCmdLine, WORD nCmdShow ) { MSG msg; if( ! hPrevInstance ) MainInit( hInstance ); hWnd = CreateWindow( szAppName, // window class szAppName, // window name WS_OVERLAPPEDWINDOW, // window style 0, // x position 0, // y position CW_USEDEFAULT, // width 0, // height NULL, // parent handle NULL, // menu or child ID hInstance, // instance NULL); // additional info hInst = hInstance; ShowWindow( hWnd, nCmdShow ); UpdateWindow( hWnd ); while( GetMessage( &msg, NULL, 0, 0 ) ) { TranslateMessage( &msg ); DispatchMessage( &msg ); } return( msg.wParam ); } /* ** Initializes window data and registers window class ********************************************************************/ BOOL MainInit( HANDLE hInstance ) { WNDCLASS Class; Class.hCursor = LoadCursor(NULL,IDC_ARROW); Class.hIcon = NULL; Class.cbClsExtra = 0; Class.cbWndExtra = 0; Class.lpszMenuName = szAppName; Class.lpszClassName = szAppName; Class.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);; Class.hInstance = hInstance; Class.style = CS_VREDRAW | CS_HREDRAW; Class.lpfnWndProc = MainWndProc; Class.style = NULL; return( RegisterClass( &Class ) ); } /* ** Main Window Procedure ********************************************************************/ LONG FAR PASCAL MainWndProc( HWND hWnd, unsigned message, WORD wParam, LONG lParam ) { FARPROC lpfnDlgProc; switch( message ) { case WM_COMMAND: if( wParam = = IDM_DIALOG ) { lpfnDlgProc = MakeProcInstance( DlgProc, hInst ); DialogBox( hInst, "DEMO", hWnd, lpfnDlgProc ); FreeProcInstance(lpfnDlgProc); } break; case WM_DESTROY: PostQuitMessage( FALSE ); break; default: return( DefWindowProc( hWnd, message, wParam, lParam ) ); } return( FALSE ); } /* ** Dialog Procedure ********************************************************************/ BOOL FAR PASCAL DlgProc( HWND hDlg, unsigned message, WORD wParam, LONG lParam ) { switch ( message ) { case WM_INITDIALOG: InitVLB( hInst, hDlg, IDD_LISTBOX, VLBfile ); break; case WM_COMMAND: if ( wParam = = IDOK ) EndDialog( hDlg, TRUE ); if ( wParam = = IDCANCEL ) EndDialog( hDlg, FALSE ); if( wParam = = IDD_LISTBOX ) { switch( HIWORD( lParam ) ) { case VLBN_DBLCLK: case VLBN_SELCHANGE: SendMessage( GetDlgItem( hDlg, IDD_TEXT ), WM_SETTEXT, 0, SendMessage( LOWORD( lParam ), VLB_GETSELSTR, 0, 0L ) ); break; } } break; default: return( FALSE ); } return( TRUE ); } /* ** demo VLB CallBack function for an ASCII file ********************************************************************/ LONG FAR PASCAL VLBfile( WORD wListBoxId, // control id unsigned message, // message LONG FAR *lplStringID, // string ID LPSTR lpszString ) // string { static int hFile = 0; // file handle static LONG TotalStrings = 0; // total records in file static Tabs[] = { 0, 9, 15, 23, 33 }; // tabs for pageleft & right static int TotalTabs = 5; // total tabs static int Column = 0; // 1st col of display string static int StrLength = 40; // total string length static int RecLength = 42; // total record len (CR/LF) static char filename[] = "vlb.txt"; // VLB filename char buf[60]; // temp buffer to read to switch( message ) { case VCB_LENGTH: return( TotalStrings ); case VCB_WIDTH: return( Tabs[ TotalTabs-1 ] ); case VCB_CREATE: { long length; if( ( hFile = open( filename, O_RDONLY ) ) = = NULL ) return( FALSE ); length = lseek( hFile, 0L, SEEK_END ); lseek( hFile, 0L, SEEK_SET ); TotalStrings = length / RecLength; Column = 0; // 1st column of displayed string return( TRUE ); } case VCB_SETFOCUS: case VCB_KILLFOCUS: return( TRUE ); case VCB_DESTROY: close( hFile ); return( TRUE ); case VCB_NEXT: if( *lplStringID >= TotalStrings - 1 ) return( FALSE ); (*lplStringID)++; break; case VCB_PREV: if( *lplStringID <= 0 ) return( FALSE ); (*lplStringID)--; break; case VCB_FIRST: *lplStringID = 0; break; case VCB_LAST: *lplStringID = TotalStrings-1; break; case VCB_VTHUMB: case VCB_FULLSTRING: case VCB_STRING: *lplStringID = (LONG)min( (int)(TotalStrings-1), max( 0, (int)*lplStringID ) ); break; case VCB_LEFT: // move string start one char left return( Column = max( 0, Column - 1 ) ); case VCB_RIGHT: // move string start one char right return( Column = min( Tabs[ TotalTabs-1 ], Column + 1 ) ); case VCB_PAGELEFT: // move string start one column left { int x; if( Column <= 0 ) return( Column = Tabs[0] ); for( x = 0; Column > Tabs[x]; x++ ) ; Column = Tabs[x-1]; return( Column = max( 0, Column ) ); } case VCB_PAGERIGHT: // move string start one column right { int x; if( Column >= Tabs[ TotalTabs - 1 ] ) return( Column = Tabs[ TotalTabs - 1 ] ); for( x = 0; Column >= Tabs[x]; x++ ) ; Column = Tabs[x]; return( Column = min( Tabs[ TotalTabs - 1 ], Column ) ); } case VCB_HTHUMB: // move string start specified by thumb return( Column = min( StrLength-1, max( 0, (int)*lplStringID ) ) ); default: return( FALSE ); } if( message = = VCB_NEXT || message = = VCB_PREV || message = = VCB_VTHUMB || message = = VCB_STRING || message = = VCB_FIRST || message = = VCB_LAST || message = = VCB_FULLSTRING ) { if( hFile < 0 ) // file open failed return( FALSE ); lseek( hFile, *lplStringID * RecLength, SEEK_SET ); read( hFile, buf, RecLength ); if( message = = VCB_FULLSTRING ) { lmemmove( lpszString, buf, StrLength ); *( lpszString + StrLength ) = 0; } else { lmemmove( lpszString, buf + Column, StrLength - Column ); *( lpszString + StrLength - Column ) = 0; } } return( TRUE ); } SETUP.H ; ; setup.h header file for C utility functions ; _DATA SEGMENT WORD PUBLIC 'DATA' _DATA ENDS CONST SEGMENT WORD PUBLIC 'CONST' CONST ENDS _BSS SEGMENT WORD PUBLIC 'BSS' _BSS ENDS DGROUP GROUP CONST, _BSS, _DATA ASSUME DS: DGROUP, SS: DGROUP ENTRY MACRO X push ds pop ax nop inc bp push bp mov bp, sp push ds mov ds, ax ASSUME DS:NOTHING sub sp, X push es push si push di ENDM EXIT MACRO X ; X should always be even - ; can't pass a single byte. ; X is used for PASCAL type stack clean-up. pop di pop si pop es sub bp, 2 mov sp, bp pop ds ASSUME DS:DGROUP pop bp dec bp ret X ENDM LMEM.H /* ** lmem.h header file for far memory functions */ VOID FAR PASCAL lmemmove( VOID FAR * lpDest, VOID FAR * lpSrc, WORD wCount ); VOID FAR PASCAL lmemset( VOID FAR * lpDest, int Char, WORD wCount ); LMEMMOVE.ASM ; ; lmemmove.asm far version of memmove ; INCLUDE SETUP.H CLIB SEGMENT WORD PUBLIC 'CODE' CLIB ENDS CLIB SEGMENT ASSUME CS: CLIB PUBLIC lmemmove lmemmove PROC FAR Destination EQU DWORD PTR [bp] + 12 Source EQU DWORD PTR [bp] + 8 Count EQU WORD PTR [bp] + 6 ENTRY 0 lds si, Source ; DS:SI = Source les di, Destination ; ES:DI = Destination mov ax, di ; Dest. in AX for return mov cx, Count ; cx = number bytes to move jcxz done ; if cx = 0, nothing to copy ; ; Check for overlapping buffers: ; If segments are different, assume no overlap ; Do normal (Upwards) Copy ; Else If (Dest. <= Source) Or (Dest.>= Source + Count) Then ; Do normal (Upwards) Copy ; Else ; Do Downwards Copy to avoid propogation ; mov ax, es ; compare the segments cmp ax, WORD PTR (Source+2) jne CopyUp cmp di, si ; Source <= Destination ? jbe CopyUp mov ax, si add ax, cx cmp di, ax ; Dest. >= (Source + Count) ? jae CopyUp ; ; Copy Down to avoid propagation in overlapping buffers ; mov ax, di ; AX = return value (offset) add si, cx add di, cx dec si ; DS:SI = Source + Count - 1 dec di ; ES:DI = Dest. + Count - 1 std ; Set Direction Flag = Down rep movsb cld ; Set Direction Flag = Up jmp short done CopyUp: mov ax, di ; AX = return value (offset) ; ; There are 4 types of word alignment of "Source" and "Destination": ; 1. Source and Destination are both even (best case) ; 2. Source is even and Destination is odd ; 3. Source is odd and Destination is even ; 4. Source and Destination are both odd (worst case) ; ; Case #4 is much faster if a single byte is copied before the ; REP MOVSW instruction. Cases #2 and #3 are effectively unaffected ; by such an operation. To maximum the speed of this operation, ; only DST is checked for alignment. For cases #2 and #4, the first ; byte will be copied before the REP MOVSW. ; test al, 1 ; fast check for Dest. odd address jz move movsb ; move a byte to improve alignment dec cx ; ; Now the bulk of the copy is done using REP MOVSW. This is much ; faster than a REP MOVSB if the Source and Dest. addresses are both ; word aligned and the processor has a 16-bit bus. Depending on ; the initial alignment and the size of the region moved, there ; may be an extra byte left over to be moved. This is handled ; by the REP MOVSB, which moves either 0 or 1 bytes. ; move: shr cx, 1 ; Shift CX for Count of words rep movsw ; CF set if 1 byte left over adc cx, cx ; CX = 1 or 0, - Carry Flag rep movsb ; possible final byte ; ; Return the "Destination" address in AX/DX:AX ; done: EXIT 10 lmemmove ENDP CLIB ENDS END LMEMSET.ASM ; ; lmemset far version of memset ; INCLUDE SETUP.H CLIB SEGMENT WORD PUBLIC 'CODE' CLIB ENDS CLIB SEGMENT ASSUME CS: CLIB PUBLIC lmemset lmemset PROC FAR Destination EQU DWORD PTR [bp] + 10 FillChar EQU BYTE PTR [bp] + 8 Count EQU WORD PTR [bp] + 6 ENTRY 0 les di, Destination ; ES:DI = Destination mov bx, di ; save a copy of DST mov cx, Count jcxz toend ; if no work to do mov al, FillChar ; the byte FillChar to store mov ah, al ; store it as a word test di, 1 ; is Destination address odd? jz dowords ; yes: proceed stosb ; store byte for word align dec cx dowords: shr cx, 1 rep stosw ; store word at a time adc cx, cx rep stosb ; store final ("odd") byte toend: mov di, dx ; Restore DI xchg ax, bx ; AX = Destination mov dx, es ; segment part of addr EXIT 8 lmemset ENDP CLIB ENDS END Figure 5 VLB.TXT 1059A HEX 5707 1-12-89 3:42p ABC TXT 42 7-01-88 12:00a ATRM1111 FNT 5966 7-26-88 12:00a B PIF 369 6-08-89 11:03a CALC EXE 28000 7-01-88 12:00a CALENDAR EXE 38896 7-01-88 12:00a CARDFILE EXE 39264 7-01-88 12:00a CLIPBRD EXE 10800 7-01-88 12:00a CLOCK EXE 8960 7-01-88 12:00a COMMAND PIF 369 6-08-89 11:42a CONTROL EXE 58064 7-01-88 12:00a COURC FON 13040 9-07-88 12:00a COURD FON 21328 9-07-88 12:00a COURE FON 23808 9-07-88 12:00a CTRN PIF 369 9-19-89 1:04p CVTPAINT EXE 5712 7-01-88 12:00a DIALOG EXE 56864 7-26-88 12:00a DOTHIS TXT 493 7-01-88 12:00a EPSON9 DRV 43776 9-07-88 12:00a FONTEDIT EXE 35536 7-26-88 12:00a FREEMEM EXE 4223 2-01-89 9:40a FSLPT2 PCL 4950 8-25-89 11:42a FSOUTPUT PCL 7258 8-25-89 11:37a GETPOS EXE 3328 11-17-88 11:09p HEAPWALK EXE 30624 7-26-88 12:00a HELVC FON 38960 9-07-88 12:00a HELVD FON 58144 9-07-88 12:00a HELVE FON 64784 9-07-88 12:00a HOOK EXE 3077 7-26-88 12:00a HPPCL DRV 212096 9-07-88 12:00a HWG00 TXT 8023 4-13-89 10:04a ICONEDIT EXE 37184 7-26-88 12:00a ICONMENU EXE 18272 8-29-89 11:26a KLU ICO 1038 1-31-89 2:44p KLU PIF 369 1-18-89 12:25p MAPMEM PIF 369 2-14-89 8:59a MEMSET EXE 36099 9-07-88 12:00a MENU HEX 1232 1-05-89 10:04a MODERN FON 7584 9-07-88 12:00a MSDOS ZAP 1 11-08-88 9:16a NOTEPAD EXE 19072 7-01-88 12:00a PAINT EXE 93280 7-01-88 12:00a PC3270 PIF 369 2-14-89 8:54a PIFEDIT EXE 30288 7-01-88 12:00a PRACTICE WRI 2944 7-01-88 12:00a QD PIF 369 6-08-89 11:08a QUIT EXE 4368 8-27-89 6:50a READCVW TXT 15566 7-26-88 12:00a README TXT 16383 7-01-88 12:00a README WRI 65280 7-26-88 12:00a READMEE9 TXT 1228 9-07-88 12:00a REVERSI EXE 15552 7-01-88 12:00a ROMAN FON 11120 9-07-88 12:00a SCRIPT FON 10304 9-07-88 12:00a SETPOS EXE 3216 11-17-88 11:10p SETUP EXE 68779 9-07-88 12:00a SHAKER EXE 8864 7-26-88 12:00a SNAP EXE 8048 7-26-88 12:00a SPIT RC 559 1-05-89 12:19p SPOOLER EXE 14336 7-01-88 12:00a SPY EXE 20160 7-26-88 12:00a STATE RST 199 12-19-89 12:59p TERMINAL EXE 48640 7-01-88 12:00a TMSRC FON 37824 9-07-88 12:00a TMSRD FON 57184 9-07-88 12:00a TMSRE FON 58304 9-07-88 12:00a TT PIF 369 12-21-88 7:58a TTY DRV 6224 9-07-88 12:00a WFINDER EXE 13072 6-22-89 5:36p WIN CLR 3798 3-01-89 4:51p WIN COM 5489 11-08-88 9:14a WIN INI 4501 3-05-90 10:52a WIN OLD 2496 11-07-88 2:06p WIN SAV 3786 3-01-89 11:38a WIN200 BIN 231520 11-08-88 9:14a WIN200 OVL 259984 11-08-88 9:14a WIN87EM EXE 11459 2-22-89 3:37p WIN87EM OLD 11331 7-26-88 12:00a WINOLDAP GRB 3574 11-08-88 9:16a WINOLDAP MOD 60464 7-01-88 12:00a WINSTUB EXE 570 7-26-88 12:00a WRITE EXE 198368 7-01-88 12:00a Using Object-Oriented Methodologies in Windows Applications Kevin P. Welch Even a casual observer of the software development community would have to agree that object-oriented programming has been receiving a great deal of attention lately from both programmers and the press. Object-oriented techniques can simplify programming for modern complex systems and enable us to create and utilize reusable software objects. This article shows you how to use certain object-oriented methodologies when developing applications for the Microsoft(R) Windows graphical environment. Fundamental to most object-oriented programming is the concept of a software object or class. Three principles describe how objects relate to each other: encapsulation, inheritance, and dynamic binding. Window Classes As you probably know, almost everything of visual importance in Windows1 is created using a window class. These classes, which are either defined by Windows or formally registered with the system by your program, serve as the framework of applications. You can associate methods with each class by supporting a set of system and private messages or defining a library of class-specific functions. (A method is a set of actions or functions, associated with a particular class, that performs tasks.) Before an application can create and use a window, it must define and register a class or template. Classes are used to define systemwide software objects. When a window is created, the system uses this class information and the CreateWindow parameters to define a new window structure. This structure contains additional information, specific to each window instance, that is used and managed by the system until the window is destroyed. The elements of a window class largely define the default behavior of those windows created from the base template. Your application can create a new window class by initializing the following WNDCLASS data structure and passing it as a parameter to the RegisterClass function: typedef struct tagWNDCLASS { WORD style; LONG (FAR PASCAL * lpfnWndProc)(); int cbClsExtra; int cbWndExtra; HANDLE hInstance; HICON hIcon; HCURSOR hCursor; HBRUSH hbrBackground; LPSTR lpszMenuName; LPSTR lpszClassName; } WNDCLASS; The values you define when initializing this structure enable you to specify how the system handles the class; the function responsible for processing all messages relating to the class; the amount of extra information associated with each class and window instance; the module supporting the class; the class icon, cursor, background color and menu; and the class name. The class name field is particularly important because it uniquely identifies the window. However, class names can easily conflict--in Windows Version 2.1 all window class names are publicly defined. One solution is to prefix each window class name with the application name or module instance handle. This should make the class name unique. In some situations (especially when defining control classes), you may want to define a window class to be used by several applications. To do this, choose a class name that does not conflict with one of the predefined Windows classes: edit, static button, list box, and scrollbar. Encapsulation Encapsulation is a technique in which data is automatically associated with each instance of an object or the class that defines that object. In traditional object-oriented programming environments such as Smalltalk or Actor(R), encapsulation is supported by the system so that little additional development effort is required. Encapsulation facilitates the creation of software objects that can be easily reused. For example, an edit field in a dialog box is a reusable software object defined by the system that uses encapsulation to separate data between various instances. Although they are less transparent, three Windows techniques provide some degree of encapsulation. The first technique is the use of extra class words. When you register a new window class using the RegisterClass function, Windows creates a new internal class data structure based on the information in the WNDCLASS data structure. By specifying a value for the cbClsExtra field you can instruct Windows to allocate space for a small amount of additional information (say 8 to 16 bytes) at the end of the class data structure. The information stored in this area is shared among all instances of the window class. When the class has been registered, the template will remain active until the responsible application termi-nates. When this occurs, Windows will automatically unregister all associated class definitions. Unfortunately, this is only true for applications: for window classes registered inside dynamic-link libraries, the class will remain permanently defined until the Windows session is ended. Additionally, multiple instances of the same application should be careful not to reregister the same class, and different applications should prefix their class names with their module name to avoid name conflicts. Once a window class has been registered, the only way to set and retrieve information relating to the class data structure, including encapsulated data, is to use the functions in Figure 1. Unfortunately, because each function requires a valid window handle, it is not possible to query the system about a class before an instance has been created. These functions require that you specify an offset (defined in WINDOWS.H) to access specific elements of the class data structure (see Figure 2). Note that each of the offsets is negative in value. This is because the internal pointer used by Windows to reference the class data structure points to the beginning of the extra class data. This enables you to define a set of positive offsets to use when referencing this data. Also note that if you change one of the values referenced by the predefined negative offsets, the results may never appear in those windows based on the class. For example, if you change the icon for a particular class, the changes will not become apparent until one of the windows is minimized. All other existing minimized windows based on that class will continue to display the old icon until a display update occurs. For example, if you had allocated space for 4 extra bytes at the end of the class data structure when it was registered, you could access the information like this: /* class data definitions */ #define CLASS_WORD1 0 #define CLASS_WORD2 2 /* code to set class data */ SetClassWord( hWnd, CLASS_WORD1, X); SetClassWord( hWnd, CLASS_WORD2, Y); /* code to retrieve class data */ X = GetClassWord( hWnd, CLASS_WORD1 ); Y = GetClassWord( hWnd, CLASS_WORD2 ); Any changes you make to a class data structure will at some time affect all the windows based on that class. Because of this you cannot use the GCW_HCURSOR offset to indicate a special edit or selection mode in your window when multiple instances are present. If you do use this technique, the same cursor will appear in all instances of the window class, regardless of their current editing or selection mode. There are many uses for extra class data, ranging from the storage of simple variables to handles to GDI objects and global data blocks. One that I have found particularly effective is the storage of font handles that are shared between different instances of the same window class. There is one important limitation, however: because these fonts are not managed by the system, the host application must release any allocated resources (in this case, fonts) when the last instance of the program terminates. The second encapsulation technique is using extra window words. Whenever you create a new window with a CreateWindow function call, you are in effect defining a new structure that is associated with that particular window instance. Although not described in WINDOWS.H, this structure contains additional information relating to the particular window, beyond that maintained by the class structure: typedef struct tagWNDSTRUCT { DWORD dwStyle; WORD wId; HANDLE hszText; HWND hWndParent; HANDLE hInstance; LONG (FAR PASCAL * lpfnWndProc)(); } WNDSTRUCT; By specifying a value for the cbWndExtra field (when registering the window class) you can instruct Windows to allocate space for a small amount of additional information (8 to 16 bytes) at the end of each window data structure. Like the additional space you can allocate with the class structure, you can use this area to store data. The difference is that extra window words are associated only with a specific window instance and are not shared by the entire class. Once a window has been created, the only way to set and retrieve information relating to the window structure is to use the set of functions in Figure 3. Like their class structure counterparts, these functions require that you provide a valid window handle and specify an offset (defined in WINDOWS.H) to access specific elements of the window data structure (see Figure 4). Again, note that each of the preceding offsets is negative in value, like the class offsets mentioned previously. Using the same mechanism used for class offsets, you can define your own set of positive offsets for use when referencing your window-specific data. The third and final technique that supports data encapsulation is the use of property lists. Although somewhat underdocumented in the Windows Software Development Kit, property lists represent one of the more powerful mechanisms provided by Windows for data encapsulation. Property lists facilitate the association of a named block of data with a particular window handle. To support this, Windows defines the set of functions found in Figure 5. You can use property lists to store any double-byte numeric value, including handles to global and local data blocks. For example, if you wished to use property lists to store, access, and remove a simple numeric value, you would use the following series of function calls: /* define a property */ SetProp( hWnd, "Index", wIndex ); /* access a property */ wIndex = GetProp( hWnd, "Index" ); /* remove a property */ RemoveProp( hWnd, "Index" ); Also, because property lists are maintained by the system, any application can access a particular window's property list. But be careful--only simple numeric data values or handles to global or system objects are shareable. Local memory handles cannot be shared. Another issue is that a window's property list, like other window-related data, belongs to Windows and is allocated from the user heap (the local heap of the user library). Although there is no specified limit to the number of entries in a property list, the actual maximum is dependent on the amount of space available in the user heap. Because of this uncertainty, you should define as few properties as possible. One often-overlooked issue is the removal of properties. When a window is destroyed, the window must remove the property and any data associated with each property, because Windows does not automatically remove them. Inheritance Generally, inheritance is characterized as a mechanism whereby one software object can assume, extend, or replace all or part of the characteristics and function of another object. In a traditional object-oriented programming environment, inheritance is supported by the enforcement of a strict set of rules that specify how objects are named (classes), what they do (methods), and how they are related (class hierarchy). Windows significantly differs from most object-oriented environments in that mechanisms by which named objects may be related are not strictly enforced, that these mechanisms usually apply only to visual objects, and that the programmer is responsible for using these mechanisms consistently. When you define a window class, you are in effect formally defining a static class hierarchy based on a preexisting set of window classes. A particular window can be a derivative of some other window class or combine several classes into a more integrated one. This hierarchy should not be confused with the parent-child relationship that specifies the operational interaction between windows. Although this area is perhaps the most weakly developed of the object-oriented mechanisms supported by Windows, two techniques exist that roughly approximate the general concept of inheritance. The first and most common of these techniques is subclassing. Subclassing involves replacing the message processing function responsible for a particular window with a new one. Using this technique you can intercept and process particular messages, passing others to the original window function. The following code fragment demonstrates how subclassing can be activated and deactivated with a new window message processing function in an application: /* activate subclassing */ lpWndfn = MakeProcInstance((FARPROC)NewWndFn, hInstance); lpWndOldfn = SetWindowLong( hWnd, GWL_WNDPROC, lpWndfn ); /* the new window function now is responsible for processing messages until removed - it can pass these messages if necessary to the old window function for default handling */ /* deactivate subclassing */ SetWindowLong( hWnd, GWL_WNDPROC, lpWndOldfn ); FreeProcInstance(lpWndfn); Subclassing is most appropriate when you want to restrict the functionality of a single window instance. For example, if you wanted to define a specialized edit control that accepted only numeric input, you could create a standard edit control and intercept all non-numeric characters. The result would be a particular instance of the predefined edit window class with slightly altered functionality. Although subclassing is a powerful technique, it has some disadvantages. The first disadvantage is that you must subclass each window instance individually, possibly making your application unnecessarily complicated. Second, unless you have access to the source code for the base window class, it can be extremely difficult to use subclassing to extend the existing functionality. If you attempt to do this, your new window message processing function often tends to replicate much of the code supporting the base class. Third, it is possible to subclass a window at the class level by replacing the default window function in the class data structure. Since the change will affect only newly created window instances based on the class, you should avoid doing this. The second and less used subclassing technique is something I call "superclassing." Superclassing involves the definition of a new window class that utilizes one or more preexisting or application-defined classes. Returning to the numeric edit control example, you could use superclassing to define a new window class (called, say, EditNumeric) that utilizes a single child window of class edit. Whenever an instance of the EditNumeric class is created, it creates a standard edit control in its place and automatically subclasses it to provide the desired functionality. The EditNumeric class could be used whenever a numeric data entry field was required, ignoring the way it was actually implemented. In more complicated situations, you can use superclassing to define extremely complex window classes that are based on several predefined or preexisting classes. You will probably need to create your own set of predefined messages that enable other windows to interact efficiently with this new object. Although the concept of superclassing is simple, with some effort you can create an entire application using layers of reusable objects with only a C compiler and the Windows Software Development Kit. Also, if the window classes are completely message-based with carefully encapsulated data, they could be easily extracted and integrated into other programs. Dynamic Binding Dynamic binding, another important characteristic of most object-oriented environments, defines how various software objects are integrated and related at run time. In most traditional object-oriented environments, all objects (not just windows) are managed by some sort of internal object management facility. With this facility a programmer can easily add new objects to the environment and replace or remove existing ones. The changes made are immediately put into effect and apply to all subsequent operations. Although not so well-defined, Windows provides several mechanisms that support this characteristic. The most important is dynamic-link libraries (DLLs). DLLs provide a way to place software objects in reusable modules. These modules (with their associated window classes) can be explicitly or implicitly referenced or imported into other applications. When different functionality is desired, the DLL can be easily replaced, with the change in effect instantly. With a little work, it should be relatively simple to incorporate these object-oriented techniques into applications you are presently developing. Great productivity gains can be achieved if you consistently apply these techniques--despite their shortcomings and dissimilarities with a true object-oriented programming environment. Best of all, these methodologies can be used immediately with your existing set of development tools. My next article will demonstrate these object-oriented Windows programming techniques in a sample program. Figure 1 Function Description GetClassWord Retrieve words from the class data structure GetClassLong Retrieve double-words from the class data structure SetClassWord Replace words in the class data structure SetClassLong Replace double-words in the class data structure Figure 2 Class Elements Definition Offset Extra Data (application defined) 0 Name (undefined) - 4 Menu Name GCL_MENUNAME - 8 Background Brush GCW_HBRBACKGROUND - 10 Cursor GCW_HCURSOR - 12 Icon GCW_HICON - 14 Module Handle GCW_HMODULE - 16 Extra Window Bytes GCW_CBWNDEXTRA - 18 Extra Class Bytes GCW_CBCLSEXTRA - 20 Window Function GCL_WNDPROC - 24 Style Flags GCW_STYLE - 26 Figure 3 Function Description GetWindowWord Retrieve words from the window data structure GetWindowLong Retrieve double-words from the window data structure SetWindowWord Replace words in the window data structure SetWindowLong Replace double-words in the window data structure Figure 4 Window Elements Definition Offset Extra Data (application-defined) 0 Function GWL_WNDPROC - 4 Instance GWW_HINSTANCE - 6 Parent GWW_HWNDPARENT - 8 Text GWW_HWNDTEXT - 10 ID GWW_ID - 12 Style GWL_STYLE - 16 Figure 5 SetProp Define a named handle or word GetProp Retrieve a named handle or word EnumProps Enumerate all window properties RemoveProp Remove a named handle or word Fundamental Recommendations on C Programming Style Greg Comeau Programming savvy is hard to develop. Transfixed in front of your editor, it is easy to hack at one specific problem and lose sight of the task at large. Having periodic sessions away from your editor can help you concentrate instead on the overall design of your code and your programming style. Attention to fundamentals up front can prevent subsequent programming and maintenance problems. This article discusses a number of recommendations on C programming style. I'm not going to discuss issues like "you must indent only four spaces" or "the opening brace of an if statement must go on the same line as the if." While these issues are important, they're really just matters of taste--one way is often as good as another. It's more important to develop a consistent, intelligent style of programming, in your own code or when programming with a team of developers. Macros Most C programmers already follow the first simple guideline, which is that macro names should always be written in uppercase. This draws attention to them in code, and should be done for both constants and functionlike macros. There is usually no good reason to break this rule. You might notice that compiler files such as stdio.h sometimes have getchar and other macros written in lowercase: consider stdio.h an exception. Because getchar is sometimes a real function and not a macro, compilers that allow these names to be either usually have a switch to control their use. If you must explicitly select the run-time library function, use the ugly notation (getchar)() Second, make sure a macro maps into what you expect it to. This is also done with the proper use of parentheses within the macro. For instance, #define abadmacro 1 + 1 a = b * abadmacro; maps into a = b * 1 + 1; which is b + 1, not b * 2, as you probably wanted. You should have coded it like this: #define agoodmacro (1 + 1) Some compilers provide command-line options that allow you to examine the output of the preprocessor. Given macros such as the following, do not end them with semicolons unless you are positive of their usage. #define AMACRO(arg) ...arg++... #define ANOTHERMACRO(arg) (arg + arg) You should be aware of side effects in macro invocations. For example, in the above code, the incrementing of arg within AMACRO might not be readily apparent. Calling ANOTHERMACRO with an argument of i++ will probably not produce the result you wanted nor increment i the number of times you wanted it to be incremented. Remember, functionlike macros are not real functions. Using #define directives liberally can make your program more readable and easier to change. Too often, I see a piece of code like the following: int array[20]; int i; for (i = 0; i < 20; i++) array[i] = 0; This has potential problems because of its haphazard use of constants. There are a couple of ways to fix this. Using a #define directive allows you to change the array size as well as the loop termination safely. #define SIZEOFARRAY 20 int array[SIZEOFARRAY]; int i; for (i = 0; i < SIZEOFARRAY; i++) array[i] = 0; You can use the invaluable sizeof operator to determine the size of fundamental types, structs, and arrays. The following macro calculates the number of elements in an array: #define HBOUND(array) \ (sizeof(array) / sizeof(array[0])) (A macro such as this cannot be used, however, if you've declared an array as extern and haven't specified the array's dimension.) Using the HBOUND macro, the loop will automatically handle arrays of varying length: for (i = 0; i < HBOUND(array); i++) array[i] = 0; It's worthwhile to investigate the useful offsetof macro from stddef.h, which determines the "absolute" offset of a structure member from the beginning of a given structure tag. This macro lets you avoid having to calculate and hand-code structure member offsets. Structure member offset values are not predictable: they can even change on the same machine with the same compiler if you vary structure packing requirements via a command-line switch or using a #pragma compiler directive. Macros, while useful, can also be problematic because the C preprocessor "doesn't know" C. Strange things can easily occur. Also, transferring constant or expression use to a macro is not always helpful. For instance given: #define RED 0 #define ORANGE 1 #define GREEN 2 ■ ■ ■ #define MAUVE 147 How can I easily add YELLOW between RED and ORANGE without renumbering everything? The solution is to use an enumeration instead of a series of #defines: enum colors {RED, YELLOW, ORANGE, ..., MAUVE}; Since enum is a C keyword, code such as the following is useful: enum colors mycolors; mycolors = RED; Unfortunately, C will also allow the following code without complaint: mycolors = 999; I think this is a quality of implementation issue: I would like to see at least a compile-time warning for such situations. The ability to associate a constant value with a name that the C compiler knows about still outweighs this disadvantage. Because enum is now part of the C language, more compilers are likely to issue proper debugging records. Declarations If your compiler accepts function prototypes, use them. They are there to serve as documentation and to ensure various type checking and efficiency concerns. The UNIX C compiler is perhaps one of the last compilers with a large user base to add function prototypes: UNIX System V Release 4 (SVR4) is now an ANSI C-compliant compiler with prototype support. To port C code to compilers that still don't have function prototypes, utilities such as sed are available commercially and in the public domain. There are a few things to be aware of about prototypes. First, it is generally unwise to mix prototypes with "old style" (K&R) code. Use either one or the other throughout all the source files of a project. For example, when using function prototypes, the following combination is not recommended: int foo(int arg1, int arg2); /* Fcn prototype */ ■ ■ ■ int foo(arg1, arg2) /* Old-style fcn definition */ int arg1; int arg2; { ■ ■ ■ } The function definition should instead be: int foo(int arg1, int arg2) /* New-style definition */ { ■ ■ ■ } The former combination is especially a problem when the prototype and the definition are in two different source files. If you mix styles, you'll eventually confuse the compiler, yourself, or other developers. Second, including identifier names in prototype declarations is helpful from a documentation standpoint. The identifier names serve as comments to the function's arguments. I suggest that you prefix each identifier appearing within prototype declarations with an underscore (note that I'm not referring to the function definition here), because you may have inadvertently used a #define to create an identifier with the same name. If the defined name contains a token representation such as [ ] or ( ), the C compiler will be happy but you'll get a bogus-looking warning or error message where the function is called in your code. Maybe worse, the replacement could be an acceptable type so the compiler wouldn't warn you at all. Third, if a declaration refers to an external function or data, add the extern keyword to it. There are often relaxed meanings of how a C definition looks. This implies flexibility in the way declarations and definitions are allowed and handled both by various compilers and linkers. Maximum portability is assured by adding extern to declarations. This leads us to the difference between declarations and definitions. A definition is a declaration that allocates storage for the object or function. A declaration only serves as a reference to that storage. Furthermore, an identifier with external or internal linkage should be initialized in only one place--its definition. Doing otherwise is quite confusing: an error will occur if the compiler finds two or more definitions of an identifier, each initialized with different values. Fortunately most linkers will warn of this situation when resolving references. Placing more than one declaration on a line is a potentially serious problem. For example, the following code is a declaration of a single char * and a single char, not of two char *. char * p, c; You can avoid many declaration problems by typedef'ing as many functions and data objects as possible. Remember that typedef is a part of C, so that the points previously raised during the enum versus #define discussion generally apply here also. Typedef statements make declarations easier to read, write, and understand: they also help with the abstraction of data elements. You should use systemwide and projectwide typedefs as appropriate. Also don't waste time inventing or ignoring typedefs that already exist. At the very least you should scan or grep your compiler's include files as well as your project's include files to become intimate with the typedefs and other information used within them. You might want to consider using the static keyword more often to "hide" names from the linker. This not only avoids name space pollution of your compiler system's symbol table, it makes identifiers invisible to other source files. It does prohibit reentrant routines since statics are often used as state variables; keep this in mind if you are encapsulating the functionality of a group of routines. Be sure that you end all typedefs, structures, and unions with semicolons. There is nothing worse than: File c.h: struct { ■ ■ ■ } File c.c: #include <c.h> ■ ■ ■ Since the struct in c.h is missing a semicolon, this error will not show up until used within c.c, most likely one or two lines after the c.h is included. This can confuse the compiler and the programmer since the compile-time error eventually emitted usually has little to do with the actual problem. I know programmers who have spent hours tracking this problem down. As for declarations, use void * as the proper generic pointer--void * was invented for this purpose. Do not use a pointer to char (char *). Use void * when comparing or swapping memory that you don't necessarily need to know the type of, as in generic functions, for example. Also use void * when using incompatible types, as generic arguments to functions, or as return types for functions. Standard routines such as malloc, memcmp, or qsort can serve as models. Functions Functions are relatively straightforward: you should generally "adhere to their policies." For instance, if a function returns a value, you should usually utilize that value for something. You can scoff at this practice, but there's plenty of broken production code out there because of lax attitudes with regard to this. As an example, nonzero error return codes are often the result of an unexpected run-time condition. Not scrupulously dealing with these possible errors at development time may mean that your user will have to at run time. Although C does not require you to code the return type of a function or the types of all arguments, not doing so is bad style. Also, if a function doesn't return a value, then say so in your code. Make sure that the function has a void return type and that it does not issue a return statement. For example, you should write: #include <stdio.h> void func(void) { printf("I'm a void func!\n"); } instead of func() { printf("I'm a void func!\n"); } Similarly if you have a function that takes no arguments, be sure to say so. Writing the following void foo(void) ensures that your compiler will bark at foo(param1) Be sure that any functions that return values do not mistakenly reach the end of the function or issue a return statement without actually returning an expression. Although some C compilers warn of these situations, it is a bad idea to depend upon the compiler for this guidance. It's better to code it correctly in the first place. It is sometimes a good idea to pass structures to functions by reference (&some_structure, for example) rather than by value. Remember that structures, like any other argument of a function, get placed on the stack; passing them by value can result in large stack usage, inefficient programs, and stack overflows. The same is true of functions that return structures. Statements Program statements should be used appropriately. If a loop needs a break or continue statement, write one. Understand the difference between while and do . . .while loops and use them both: don't rely on one over the other merely because of force of habit. Understand the full power of for, but beware of prefix versus postfix incrementing: was the value of the final loop iteration really what you thought or was it one short? Use switch statements with reasonable case labels instead of complex if statements wherever possible. All C programmers should understand side-effects. Take the time to understand why statements such as the following do not have predictable outcomes even though they may appear to function properly with your compiler. a[i++] = i++; func(i++, i++, i++); Many things are just not guaranteed by C and you should be aware of them. Include Files As already mentioned, create and use projectwide, systemwide, and operating system or compiler files since they provide helpful resources. Also, don't use "in-line" declarations such as extern int func(int); in a source file. Use the header file containing the declaration of func instead. The only exception is if the function declaration is strictly a forward reference for another function (such as a function defined later in that same source file) that doesn't need a header for its functions. Don't assume anything about functions. For example, many programs call malloc without regard for its argument type or return type: it is assumed to return an integer. This assumption can wreak havoc on machines on which pointers and ints are mutually exclusive. Even on machines where they are, this can be upset simply, say by changing the memory model. It is also not a good idea to define or initialize identifiers in a header file. Header files exist only to serve as declaration references, so do nothing but declare in them. This leaves no room for initialization, so create your own init function of some sort that contains respective definitions as well as appropriate initializations or initial values to those identifiers or variables via assignments. Another good idea is to "envelope" header files. Doing this avoids redeclaration problems and eliminates the need to synchronize header files. For example, the following code guarantees trouble: File a.h: #include <stdio.h> ■ ■ ■ File b.h: #include <stdio.h> ■ ■ ■ File c.c: #include "a.h" #include "b.h" To avoid this, wrap every header file you write. For instance, your C compiler should have stdio.h in a form such as this: #ifndef STDIO_H #define STDIO_H ...#defines, decls, etc for stdio.h.... #endif This way, if stdio.h or any other header file is processed a second time, the #ifndef declaration will fail because the control variable, STDIO_H, would have been defined the first time stdio.h was included. And if you were thinking that perhaps a.h and b.h should not both be using stdio.h, stop! There is nothing wrong with such a construction. You should also know the relationships between header files. Instead of including endless lists of them in your code, determine which .H files can be included into other include files. This makes them simpler and neater to use. General Make certain that you use the = sign correctly. Too often a programmer will slip and leave out the second = of a Boolean expression. Some compilers warn about this. When a constant is used on one side of a Boolean test, a very handy way to pinpoint mistakes automatically is to put the constant on the left hand side. If if (0 = = a) is erroneously entered as: if (0=a) the compiler will bark. Using C idioms is still considered acceptable practice: i++; i =+ 3; while ((c = getchar()) != EOF)... Such constructs were originally used in order to generate better code, but these days compilers are so good that the use of such idioms is often unnecessary. Nevertheless, these idioms are popular constructs and soon become quite natural. Avoid union type puns. Remember, a union is for the reuse of storage and not for the redefinition of that storage. If you really feel the need to, consider using a type cast instead. Use the const and volatile type qualifiers. Yes, these are relatively new features in C but they are valuable. It should be considered that some variables should not be modified and others should only be changed at set points. Why not clarify aspects of your code with these keywords? Nesting comments or miscoding a / * , * / pair is common. If you find yourself doing that, use the following instead: #if 0 ... code possibly including comments ... #endif This is invaluable for debugging. Changing the 0 to 1 and vice versa is quick and harmless; adding a #else can also help. The overuse of this method can make code look ugly, however. Finally, stay away from nonportable constructs if you can. For further information, see: "C Scope and Linkage: The Keys to Understanding Identifier Accessibility," MSJ (Vol. 3, No. 6); "A Guide to Understanding Even the Most Complex C Declarations," MSJ (Vol. 3, No. 5); "Organizing Data in Your C Program with Structures, Unions, and Typedefs," MSJ (Vol. 4, No. 2); "Advanced Techniques for Using Structures and Unions in Your C Code," MSJ (Vol. 4, No. 3). ANSI C/C++ The X3J11 draft ANSI C proposal is now an official standard. For a copy of this standard call CBEMA (Computer and Business Equipment Manufacturers Association) at 202-737-8888. The price is $65. The main features of ANSI C include function prototypes, a massive clean-up of the C preprocessor, a finite language specification, and a comprehensive list of the gray areas of C. The standard can help you answer many of your C questions. Also, an X3J16 committee has formed and is responsible for producing ANSI C++ and influencing ISO C++. They have already met twice. For information about the X3J16 committee, call the author at 718-945-0009. Examining Object-Oriented Techniques Using QuickPascal(TM) Kris Jamsa Pascal is not inherently an object-oriented language. It is possible, however, to enjoy some of the benefits of object-oriented programming (OOP) by developing your software with certain object-oriented techniques. This article demonstrates these techniques with a set of object-oriented sorting routines using the Microsoft(R) QuickPascal compiler. By creating the objects as shown here, your programs can use one procedure called SortArray to sort arrays of type Integer, arrays of type Real, or even arrays of type String. The Sorting Algorithm Before you examine the object-oriented sorting routines, you need to have a general understanding of the sorting algorithm used in the programs in this article. To start, the sorting routines will use ascending order. By the end of the article, you will have examined routines that can sort in either direction. Suppose, for example, that an array contains the values shown in Figure 1. To sort the array, compare each value in the array to the first element. If another array element is smaller than the value in the first element, you exchange the values and then compare the next value with the new first element. If you compare each value with the value of the first element and exchange values as necessary, the array values are moved as shown in Figure 2. Because the first element is now the smallest in the array and is in the correct position, you can repeat the process of comparing and exchanging for the second element (see Figure 3). Repeating these steps for the third, fourth, and fifth elements produces the results shown in Figure 4. The sample program (see Figure 5) creates a procedure named SortIntegers that performs the preceding sorting algorithm. The program uses the procedure to sort an array of 10 integer values. After you compile and run this program, your screen displays the following output: 0 129 174 308 350 449 556 619 619 648 The sorting procedure sorts the values correctly; however, the program works only for arrays defined as type IntArray that contain only values of type Integer. If the array size changes, you have to edit the program and recompile. Also, if your program needs to sort an array of type Real, you must write a second procedure. The program shown in Figure 6 creates procedures that sort arrays of type Integer, of type Real, and of type String. When you compile and run this program, your screen displays the following sorted values: 0 82.930 AAA 129 128.538 AAAA 174 292.222 BBB 308 464.632 XXXX 350 575.029 ZZZ 449 599.333 556 609.789 619 623.415 619 736.158 648 905.790 For each array type that it sorted, the program needed to create a different procedure. The procedure code in each case was identical. The only difference was the array type that each procedure supported. As you will learn in the next section, you can use QuickPascal's object-oriented capabilities to write a single sorting procedure that supports all array types. Creating an Object-Oriented Sort One of the goals of object-oriented programming is to combine routines and associated data into encapsulated objects. As a result, an object in this example has as data fields the following attributes of an array: ■ the starting address of the first element in memory ■ the number of elements ■ the size of an element in bytes Using these attributes, you can define a class--in this case named SortArray--as follows: TYPE SortArray = OBJECT { generic sort-array class } fStart: Pointer; { pointer to first element } fNumElements: Integer; { number of array elements } fElementSize: Integer; { element size in bytes } PROCEDURE SortArray.SortValues; { performs sort } FUNCTION SortArray.Compare(a, b: Pointer):Boolean; { compares 2 values } END; The class's first field, fStart, is a pointer to the array's first element. By using pointers, the object is not limited to using an array of only a fixed number of elements. Instead, the array can be any size. The fNumElements field contains the number of elements in the array. Likewise, the fElementSize field contains the number of bytes that each array element occupies. Using the array's starting address and the element size, the sorting procedure can determine the location of each element in memory. Figure 7 shows how the procedure can determine the location of the third element in an array of type Integer and in an array of type Real. The definition of the SortArray class shows that it contains two methods: SortArray.SortValues and SortArray.Compare. SortArray.SortValues, the first method, is the method your programs use to sort each array. The second method, SortArray.Compare, compares two values. As shown below, the program defines three subclasses (one for each of the QuickPascal data types Integer, Real, and String) that override this method: IntArray = OBJECT(SortArray){integer sort-array subclass} FUNCTION IntArray.Compare(a, b: Pointer): Boolean; OVERRIDE; END; RealArray = OBJECT(SortArray) {real sort-array subclass} FUNCTION RealArray.Compare(a, b: Pointer): Boolean; OVERRIDE; END; StringArray = OBJECT(SortArray) {str sort-array subclass} FUNCTION StringArray.Compare(a, b: Pointer): Boolean; OVERRIDE; END; Each subclass uses a function to compare two values. The function receives pointers to each of the values that will be compared. If the first value specified is greater than or equal to the second value, the function returns the value TRUE. If the first value is less than the second value, the function returns the value FALSE. The following function implements the IntArray. FUNCTION IntArray.Compare(a, b: Pointer): Boolean; VAR aPtr, bPtr: ^Integer; BEGIN { Convert generic pointers to integer pointers } aPtr := a; bPtr := b; Compare := aPtr^ >= bPtr^; END; The function receives two untyped pointers. To correctly compare values of type Integer, the function assigns the pointers to typed integer pointers and then dereferences them. Each of the methods that compares values uses this technique. The method first assigns the untyped pointer to a pointer of the correct type and then dereferences the pointer during the comparison. Each class inherits and uses the SortArray.SortValues method, which sorts the array's contents (see Figure 8). Getting Started with OOP The method begins by allocating space that is used for the temporary storage location when two values in the array are exchanged. Because the method might sort values of type Integer, values of type Real, or values of type String, the size of the temporary variable will differ. As a result, the method uses the fElementSize data field to allocate a memory location only large enough to store one element of the array being sorted. The method uses two FOR loops to compare the array elements. Within the nested FOR loop, the program first creates pointers to the two elements to be compared. The program calculates the location of each element in the manner described earlier in the discussion of the fStart and fElementSize data fields. The program calls these pointers aPtr and bPtr. Next the method passes the pointers to the comparison function that is specific to each subclass. If the values need to be exchanged, the procedure uses the QuickPascal Move procedure to do so. Move copies the specified number of bytes from one block of memory to another. Because the array types that the method sorts can differ, the Move procedure lets the method exchange the correct number of bytes for each type. Last, after the array is sorted, the procedure releases the memory it allocated for the temporary buffer. The sample program in Figure 9 uses the object-oriented sort method to sort an array of type Integer, an array of type Real, and an array of type String. After you compile and run the program, your screen displays the output shown below: 0 82.930 AAA 129 128.538 AAAA 174 292.222 BBB 308 464.632 JJJ 350 575.029 ZZZ 449 599.333 556 609.789 619 623.415 619 736.158 648 905.790 The program first calls the New procedure to allocate space for each object and then assigns values to three arrays that are of different types. Next the program assigns the data field values to each object and calls the SortValues method for each object. Last the program displays the sorted contents of each array and uses the Dispose procedure to release the memory allocated for each object. By using object-oriented techniques, this program was able to replace several sorting procedures with only one method. Sorting in Ascending or Descending Order The preceding program used object-oriented techniques to create a method that sorts an array of any type. Unfortunately, the program sorts values only in ascending order. To provide ascending and descending sorting capabilities, the class definition below adds a new field called fAscending to the SortArray class: TYPE SortArray = OBJECT { generic sort-array class } fStart: Pointer; { pointer to first element } fNumElements: Integer; { number of array elements } fElementSize: Integer; { element size in bytes } fAscending: Boolean; { TRUE if lower to higher } PROCEDURE SortArray.SortValues; { performs sort } FUNCTION SortArray.Compare(a,b:Pointer): Boolean; { compares 2 vals } END; When the fAscending field contains the value TRUE, the program sorts the values in ascending order. When the fAscending field contains the value FALSE, the program sorts the values in descending order. The program in Figure 10 uses objects of the above class to sort arrays in either order. Before the program sorts the array, it assigns either the value TRUE or the value FALSE to the ascending data field to select the desired order. Notice also the changes in the SortArray.SortValues method that were necessary to enable sorting in both orders. Improving the Sort Algorithm Many computer scientists devote time and effort to enhancing sorting algorithms for top performance. Although the simple sorting algorithm used in the preceding programs generates the desired results, the algorithm takes a long time to complete if the array is large. A better sorting algorithm is the Shell sort, which is named after its inventor, Donald Shell. To improve sorting performance, the Shell sort eliminates much of an array's disorder early in the sort by exchanging elements that are spaced farther apart in the array. As an example, we use a modified Shell sort. This sort uses the same concept as does the Shell sort, but it is changed slightly for the sake of readability. The modified Shell sort establishes the size of the gap between the elements to be compared. To begin, the gap is set to the number of elements in the array, divided by 2. Given an eight-element array, the initial gap value is 4. The modified Shell sort examines array elements in which the index to the second element is the index of the first element plus 4. If the value of the first element is larger than the value of the second, the elements are exchanged. The sort examines the elements whose indexes differ by 4 until no more exchanges occur. Next the sort divides the size of the gap by 2, which in this case creates a gap of 2. The modified Shell sort then repeatedly tests for elements whose indexes differ by 2 until no exchanges occur and then divides the gap by 2 again, yielding a gap value of 1. The sort repeatedly examines elements whose indexes differ by 1 until the array is sorted. The program in Figure 11 uses the modified Shell sort described above to sort arrays faster, either in ascending or in descending order. The only program changes required to implement the Shell sort algorithm were within the SortArray.SortValues method. Object-oriented programming techniques can enhance your code. By encapsulating data and code into objects, a single method can process multiple data types. And while the ideas presented here are straightforward, they have a broad range of application. Figure 5 PROGRAM SortInts; { Filename: SORTINTS.PAS } { Creates the SortIntegers procedure and uses it to sort an array } { of 10 integers } TYPE IntArray = ARRAY [1..100] OF Integer; PROCEDURE SortIntegers(VAR values: IntArray; numElements: Integer); VAR i, j: Integer; { indexes into the array } temp: Integer; { temporary buffer for exchange } BEGIN FOR i := 1 TO numElements - 1 DO FOR j := i + 1 TO numElements DO BEGIN IF (values[i] > values[j]) THEN BEGIN temp := values[i]; values[i] := values[j]; values[j] := temp; END; END; END; VAR intValues: IntArray; i: Integer; BEGIN FOR i := 1 TO 10 DO intValues[i] := Random(1000); { fill the array } SortIntegers(intValues, 10); { Display the sorted array } FOR i := 1 TO 10 DO Writeln(intValues[i]); END. Figure 6 PROGRAM SortVals; { Filename: SORTVALS.PAS } { Creates the SortIntegers, SortReals, and SortStrings procedures } { and uses them to sort three arrays of different types } TYPE IntArray = ARRAY [1..100] OF Integer; RealArray = ARRAY [1..100] OF Real; StringArray = ARRAY [1..100] OF String; { Sorts an array of type IntArray } PROCEDURE SortIntegers(VAR values: IntArray; numElements: Integer); VAR index, index2: Integer; { indexes into the array } temp: Integer; { temporary buffer for exchange } BEGIN FOR index := 1 TO numElements - 1 DO FOR index2 := index + 1 TO numElements DO BEGIN IF (values[index] > values[index2]) THEN BEGIN temp := values[index]; values[index] := values[index2]; values[index2] := temp; END; END; END; { Sorts an array of type RealArray } PROCEDURE SortReals(VAR values: RealArray; numElements: Integer); VAR index, index2: Integer; { indexes into the array } temp: Real; { temporary buffer for exchange } BEGIN FOR index := 1 TO numElements - 1 DO FOR index2 := index + 1 TO numElements DO BEGIN IF (values[index] > values[index2]) THEN BEGIN temp := values[index]; values[index] := values[index2]; values[index2] := temp; END; END; END; { Sorts an array of type StringArray } PROCEDURE SortStrings(VAR values: StringArray; numElements: Integer); VAR index, index2: Integer; { indexes into the array } temp: String; { temporary buffer for exchange } BEGIN FOR index := 1 TO numElements - 1 DO FOR index2 := index + 1 TO numElements DO BEGIN IF (values[index] > values[index2]) THEN BEGIN temp := values[index]; values[index] := values[index2]; values[index2] := temp; END; END; END; VAR intValues: IntArray; realValues: RealArray; strValues: StringArray; index: Integer; BEGIN FOR index := 1 TO 10 DO intValues[index] := Random(1000); FOR index := 1 TO 10 DO realValues[index] := Random * 1000; strValues[1] := 'AAA'; strValues[2] := 'ZZZ'; strValues[3] := 'AAAA'; strValues[4] := 'BBB'; strValues[5] := 'XXXX'; SortIntegers(intValues, 10); SortReals(realValues, 10); SortStrings(strValues, 5); { Display the sorted arrays } FOR index := 1 TO 10 DO BEGIN Write(intValues[index]:4); Write(realValues[index]:15:3); IF (index <= 5) THEN Writeln(strValues[index]:10) ELSE Writeln; END; END. Figure 8 SortArray.SortValues Method PROCEDURE SortArray.SortValues; VAR i, j: Integer; { indexes into the array } aPtr, bPtr, temp: Pointer; BEGIN { Allocate memory for temporary swap buffer } GetMem(temp, Self.fElementSize); FOR i := 1 TO Self.fNumElements - 1 DO FOR j := i + 1 TO Self.fNumElements DO BEGIN { Create pointers to the current two elements } aPtr := Ptr(Seg(Self.fStart^), Ofs(Self.fStart^) + (i - 1) * Self.fElementSize); bPtr := Ptr(Seg(Self.fStart^), Ofs(Self.fStart^) + (j - 1) * Self.fElementSize); IF (Self.Compare(aPtr, bPtr)) THEN BEGIN Move(bPtr^, temp^, Self.fElementSize); Move(aPtr^, bPtr^, Self.fElementSize); Move(temp^, aPtr^, Self.fElementSize); END; END; { Release memory allocated for temporary buffer } FreeMem(temp, Self.fElementSize); END; Figure 9 SORT.PAS {$M+} { test for object memory allocation } PROGRAM Sort; { Filename: SORT.PAS } { Creates classes, methods, and objects to sort an array of } { integers, an array of reals, and an array of character strings } TYPE SortArray = OBJECT { generic sort-array class } fStart: Pointer; { pointer to first element } fNumElements: Integer; { number of array elements } fElementSize: Integer; { element size in bytes } PROCEDURE SortArray.SortValues; { performs sort } FUNCTION SortArray.Compare(a, b: Pointer): Boolean;{comp 2 vals} END; IntArray = OBJECT(SortArray) { integer sort-array subclass } FUNCTION IntArray.Compare(a, b: Pointer): Boolean; OVERRIDE; END; RealArray = OBJECT(SortArray) { real sort-array subclass } FUNCTION RealArray.Compare(a, b: Pointer): Boolean; OVERRIDE; END; StringArray = OBJECT(SortArray) { string sort-array subclass } FUNCTION StringArray.Compare(a, b: Pointer): Boolean; OVERRIDE; END; FUNCTION SortArray.Compare(a, b: Pointer): Boolean; BEGIN { No statements--always overridden by a subclass } END; { Compares two integer values using pointers. Returns TRUE if 1st } { value is > or = to the second; otherwise, returns FALSE. } FUNCTION IntArray.Compare(a, b: Pointer): Boolean; VAR aPtr, bPtr: ^Integer; BEGIN { Convert generic pointers to integer pointers } aPtr := a; bPtr := b; Compare := aPtr^ >= bPtr^; END; { Compares two real values using pointers. Returns TRUE if 1st } { value is greater than or equal to 2nd; otherwise, returns FALSE. } FUNCTION RealArray.Compare(a, b: Pointer): Boolean; VAR aPtr, bPtr: ^Real; BEGIN { Convert generic pointers to real pointers } aPtr := a; bPtr := b; Compare := aPtr^ >= bPtr^; END; { Compares two string values using pointers. Returns TRUE if 1st } { value is greater than or equal to 2nd; otherwise, returns FALSE. } FUNCTION StringArray.Compare(a, b: Pointer): Boolean; VAR aPtr, bPtr: ^String; BEGIN { Convert generic pointers to string pointers } aPtr := a; bPtr := b; Compare := aPtr^ >= bPtr^; END; { Sorts the array } PROCEDURE SortArray.SortValues; VAR i, j: Integer; { indexes into the array } aPtr, bPtr, temp: Pointer; BEGIN { Allocate memory for temporary swap buffer } GetMem(temp, Self.fElementSize); FOR i := 1 TO Self.fNumElements - 1 DO FOR j := i + 1 TO Self.fNumElements DO BEGIN { Create pointers to the current two elements } aPtr := Ptr(Seg(Self.fStart^), Ofs(Self.fStart^) + (i - 1) * Self.fElementSize); bPtr := Ptr(Seg(Self.fStart^), Ofs(Self.fStart^) + (j - 1) * Self.fElementSize); IF (Self.Compare(aPtr, bPtr)) THEN BEGIN Move(bPtr^, temp^, Self.fElementSize); Move(aPtr^, bPtr^, Self.fElementSize); Move(temp^, aPtr^, Self.fElementSize); END; END; { Release memory allocated for temporary buffer } FreeMem(temp, Self.fElementSize); END; VAR { Declare objects and other variables } realValues: RealArray; intValues: IntArray; strValues: StringArray; array1 : ARRAY [1..10] OF Integer; array2 : ARRAY [1..10] OF Real; array3 : ARRAY [1..5] OF String; i: Integer; BEGIN { Allocate memory for each object } New(intValues); New(realValues); New(strValues); FOR i := 1 TO 10 DO array1[i] := Random(1000); FOR i := 1 TO 10 DO array2[i] := Random * 1000; array3[1] := 'AAA'; array3[2] := 'ZZZ'; array3[3] := 'BBB'; array3[4] := 'AAAA'; array3[5] := 'JJJ'; { Give values to the data fields of the objects } intValues.fStart := @array1[1]; intValues.fNumElements := 10; intValues.fElementSize := SizeOf(array1[1]); intValues.SortValues; { sort the array } realValues.fStart := @array2[1]; realValues.fNumElements := 10; realValues.fElementSize := SizeOf(array2[1]); realValues.SortValues; { sort the array } strValues.fStart := @array3[1]; strValues.fNumElements := 5; strValues.fElementSize := 256; strValues.SortValues; { sort the array } { Display the sorted arrays } FOR i := 1 TO 10 DO BEGIN Write(array1[i]:4); Write(array2[i]:15:3); IF (i <= 5) THEN Writeln(array3[i]:10) ELSE Writeln; END; { Release the memory allocated for the objects } Dispose(intValues); Dispose(realValues); Dispose(strValues); END. Figure 10 ORDER.PAS {$M+} { test for object memory allocation } PROGRAM OrderSort; { Filename: ORDER.PAS } { Creates classes, methods, and objects to sort (in either } { ascending or descending order) an array of integers, an array } { of reals, and an array of character strings } TYPE SortArray = OBJECT { generic sort-array class } fStart: Pointer; { pointer to first element } fNumElements: Integer; { number of array elements } fElementSize: Integer; { element size in bytes } fAscending: Boolean; { TRUE if lower to higher } PROCEDURE SortArray.SortValues; { performs sort } FUNCTION SortArray.Compare(a, b: Pointer): Boolean;{comp 2 vals} END; IntArray = OBJECT(SortArray) { integer sort-array subclass } FUNCTION IntArray.Compare(a, b: Pointer): Boolean; OVERRIDE; END; RealArray = OBJECT(SortArray) { real sort-array subclass } FUNCTION RealArray.Compare(a, b: Pointer): Boolean; OVERRIDE; END; StringArray = OBJECT(SortArray) { string sort-array subclass } FUNCTION StringArray.Compare(a, b: Pointer): Boolean; OVERRIDE; END; FUNCTION SortArray.Compare(a, b: Pointer): Boolean; BEGIN { No statements--always overridden by subclass } END; { Compares two integer values using pointers. Returns TRUE if 1st } { value is > or = to the 2nd; otherwise, returns FALSE. } FUNCTION IntArray.Compare(a, b: Pointer): Boolean; VAR aPtr, bPtr: ^Integer; BEGIN { Convert generic pointers to integer pointers } aPtr := a; bPtr := b; Compare := aPtr^ >= bPtr^; END; { Compares two real values using pointers. Returns TRUE if 1st } { value is > or = to the 2nd; otherwise, returns FALSE. } FUNCTION RealArray.Compare(a, b: Pointer): Boolean; VAR aPtr, bPtr: ^Real; BEGIN { Convert generic pointers to real pointers } aPtr := a; bPtr := b; Compare := aPtr^ >= bPtr^; END; { Compares two string values using pointers. Returns TRUE if 1st } { value is > or = to the 2nd; otherwise, returns FALSE. } FUNCTION StringArray.Compare(a, b: Pointer): Boolean; VAR aPtr, bPtr: ^String; BEGIN { Convert generic pointers to string pointers } aPtr := a; bPtr := b; Compare := aPtr^ >= bPtr^; END; { Sorts the array } PROCEDURE SortArray.SortValues; VAR i, j: Integer; { indexes into the array } aPtr, bPtr, temp: Pointer; BEGIN { Allocate memory for temporary swap buffer } GetMem(temp, Self.fElementSize); FOR i := 1 TO Self.fNumElements - 1 DO FOR j := i + 1 TO Self.fNumElements DO BEGIN { Create pointers to the current two elements } aPtr := Ptr(Seg(Self.fStart^), Ofs(Self.fStart^) + (i - 1) * Self.fElementSize); bPtr := Ptr(Seg(Self.fStart^), Ofs(Self.fStart^) + (j - 1) * Self.fElementSize); IF (Self.fAscending) AND (Self.Compare(aPtr, bPtr)) THEN BEGIN Move(bPtr^, temp^, Self.fElementSize); Move(aPtr^, bPtr^, Self.fElementSize); Move(temp^, aPtr^, Self.fElementSize); END ELSE IF (NOT Self.fAscending) AND (NOT Self.Compare(aPtr, bPtr)) THEN BEGIN Move(bPtr^, temp^, Self.fElementSize); Move(aPtr^, bPtr^, Self.fElementSize); Move(temp^, aPtr^, Self.fElementSize); END; END; { Release memory allocated for temporary buffer } FreeMem(temp, Self.fElementSize); END; VAR { Declare objects and other variables } realValues: RealArray; intValues: IntArray; strValues: StringArray; array1 : ARRAY [1..10] OF Integer; array2 : ARRAY [1..10] OF Real; array3 : ARRAY [1..5] OF String; i: Integer; BEGIN { Allocate memory for each object } New(intValues); New(realValues); New(strValues); FOR i := 1 TO 10 DO array1[i] := Random(1000); FOR i := 1 TO 10 DO array2[i] := Random * 1000; array3[1] := 'AAA'; array3[2] := 'ZZZ'; array3[3] := 'BBB'; array3[4] := 'AAAA'; array3[5] := 'JJJ'; { Give values to the data fields of the objects } intValues.fStart := @array1[1]; intValues.fNumElements := 10; intValues.fElementSize := SizeOf(array1[1]); intValues.fAscending := FALSE; intValues.SortValues; { sort the array } realValues.fStart := @array2[1]; realValues.fNumElements := 10; realValues.fElementSize := SizeOf(array2[1]); realValues.fAscending := TRUE; realValues.SortValues; { sort the array } strValues.fStart := @array3[1]; strValues.fNumElements := 5; strValues.fElementSize := 256; strValues.fAscending := FALSE; strValues.SortValues; { sort the array } { Display the sorted arrays } FOR i := 1 TO 10 DO BEGIN Write(array1[i]:4); Write(array2[i]:15:3); IF (i <= 5) THEN Writeln(array3[i]:10) Else Writeln; END; { Release the memory allocated for the objects } Dispose(intValues); Dispose(realValues); Dispose(strValues); END. Figure 11 SHELL.PAS {$M+} { test for object memory allocation } PROGRAM ShellSort; { Filename: SHELL.PAS } { Creates classes, methods, and objects to sort (in ascending or } { descending order) an array of integers, an array of reals, and an } { array of character strings using a modified Shell sort to perform } { the sorting } TYPE SortArray = OBJECT { generic sort-array class } fStart: Pointer; { pointer to first element } fNumElements: Integer; { number of array elements } fElementSize: Integer; { element size in bytes } fAscending: Boolean; { TRUE if lower to higher } PROCEDURE SortArray.SortValues; { performs sort } FUNCTION SortArray.Compare(a, b: Pointer): Boolean;{ comp 2 vals} END; IntArray = OBJECT(SortArray) { integer sort-array subclass } FUNCTION IntArray.Compare(a, b: Pointer): Boolean; OVERRIDE; END; RealArray = OBJECT(SortArray) { real sort-array subclass } FUNCTION RealArray.Compare(a, b: Pointer): Boolean; OVERRIDE; END; StringArray = OBJECT(SortArray) { string sort-array subclass } FUNCTION StringArray.Compare(a, b: Pointer): Boolean; OVERRIDE; END; FUNCTION SortArray.Compare(a, b: Pointer): Boolean; BEGIN { No statements--always overridden by subclass } END; { Compares two integer values using pointers. Returns TRUE if 1st } { value is greater than or equal to 2nd; otherwise, returns FALSE. } FUNCTION IntArray.Compare(a, b: Pointer): Boolean; VAR aPtr, bPtr: ^Integer; BEGIN { Convert generic pointers to integer pointers } aPtr := a; bPtr := b; Compare := aPtr^ >= bPtr^; END; { Compares 2 real values using pointers. Returns TRUE if the 1st } { value is > or = to the 2nd; otherwise, returns FALSE. } FUNCTION RealArray.Compare(a, b: Pointer): Boolean; VAR aPtr, bPtr: ^Real; BEGIN { Convert generic pointers to real pointers } aPtr := a; bPtr := b; Compare := aPtr^ >= bPtr^; END; { Compares two string values using pointers. Returns TRUE if 1st } { value is > or = to the 2nd; otherwise, returns FALSE. } FUNCTION StringArray.Compare(a, b: Pointer): Boolean; VAR aPtr, bPtr: ^String; BEGIN { Convert generic pointers to string pointers } aPtr := a; bPtr := b; Compare := aPtr^ >= bPtr^; END; { Implements a modified Shell sort to sort the array } PROCEDURE SortArray.SortValues; VAR index, gap: Integer; { indexes into the array } aPtr, bPtr, temp: Pointer; exchangeOccurred: Boolean; BEGIN { Allocate memory for temporary swap buffer } GetMem(temp, Self.fElementSize); gap := Self.fNumElements DIV 2; REPEAT REPEAT exchangeOccurred := FALSE; FOR index := 1 TO Self.fNumElements - GAP DO BEGIN { Create pointers to 2 elements whose indexes differ by } { the value of the gap } aPtr := Ptr(Seg(Self.fStart^), Ofs(Self.fStart^) + (index - 1) * Self.fElementSize); bPtr := Ptr(Seg(Self.fStart^), Ofs(Self.fStart^) + (index + gap - 1) * Self.fElementSize); IF (Self.fAscending) AND (Self.Compare(aPtr, bPtr)) THEN BEGIN exchangeOccurred := TRUE; Move(bPtr^, temp^, Self.fElementSize); Move(aPtr^, bPtr^, Self.fElementSize); Move(temp^, aPtr^, Self.fElementSize); END ELSE IF (NOT Self.fAscending) AND (NOT Self.Compare(aPtr, bPtr)) THEN BEGIN exchangeOccurred := TRUE; Move(bPtr^, temp^, Self.fElementSize); Move(aPtr^, bPtr^, Self.fElementSize); Move(temp^, aPtr^, Self.fElementSize); END; END; UNTIL (NOT exchangeOccurred); gap := gap DIV 2; UNTIL (gap = 0); { Release memory allocated for temporary buffer } FreeMem(temp, Self.fElementSize); END; VAR { Declare objects and other variables } realValues: RealArray; intValues: IntArray; strValues: StringArray; array1 : ARRAY [1..10] OF Integer; array2 : ARRAY [1..10] OF Real; array3 : ARRAY [1..5] OF String; index: Integer; BEGIN { Allocate memory for each object } New(intValues); New(realValues); New(strValues); FOR index := 1 TO 10 DO array1[index] := Random(1000); FOR index := 1 TO 10 DO array2[index] := Random * 1000; array3[1] := 'AAA'; array3[2] := 'ZZZ'; array3[3] := 'BBB'; array3[4] := 'AAAA'; array3[5] := 'JJJ'; { Give values to the data fields of the objects } intValues.fStart := @array1[1]; intValues.fNumElements := 10; intValues.fElementSize := SizeOf(array1[1]); intValues.fAscending := FALSE; intValues.SortValues; { sort the array } realValues.fStart := @array2[1]; realValues.fNumElements := 10; realValues.fElementSize := SizeOf(array2[1]); realValues.fAscending := TRUE; realValues.SortValues; { sort the array } strValues.fStart := @array3[1]; strValues.fNumElements := 5; strValues.fElementSize := 256; strValues.fAscending := FALSE; strValues.SortValues; { sort the array } { Display the sorted arrays } FOR index := 1 TO 10 DO BEGIN Write(array1[index]:4); Write(array2[index]:15:3); IF (index <= 5) THEN Writeln(array3[index]:10) ELSE Writeln; END; { Release the memory allocated for the objects } Dispose(intValues); Dispose(realValues); Dispose(strValues); END. Implementing DDE with Presentation Manager Object Windows Richard Hale Shaw In addition to the multithreaded, multitasking capabilities of the OS/2 operating system and its interprocess communications facilities, Dynamic Data Exchange (DDE) has the potential to enhance connectivity for OS/2 Presentation Manager (hereafter "PM") applications. But access to DDE is limited to PM programs: OS/2 kernel programs are usually forbidden from using it. This article discusses how you can use PM object windows to implement DDE in Vio applications. It also surveys alterations and additions to the PMServer application presented in "Accessing Presentation Manager Facilities from Within OS/2 Kernel Applications," MSJ (Vol. 5, No. 1) that will permit Vio programs to use DDE. The source code for the new version of PMServer will be published in the next installment. DDE, like the Clipboard, is a facility for transferring data from one application to another. But unlike the Clipboard, in which the user initiates and controls one-time transfers of data between applications by cutting and pasting, DDE is transparent. That is, a user can tell an application to initiate a DDE conversation with another program, and the two programs can then transfer and receive data without additional help. In other words, DDE allows applications to send or receive data updates on an asynchronous real-time basis. To understand the implications of DDE, consider the classic DDE scenario: A PM communications program is downloading stock prices from the Dow Jones News/Retrieval(R) Service. Using a DDE conversation initiated by Microsoft(R) Excel, the communications program sends the worksheet the price information on selected stocks. Microsoft Excel uses the data in two ways: to update the cells of a worksheet containing a financial portfolio and to update a chart derived from the worksheet and displayed in another worksheet window. Changes to the worksheet cells trigger DDE transactions in a different DDE conversation. The worksheet transfers the contents of a range of cells to a PM word processor, which places the updated portfolio figures in a document table of a financial report. The result is that the user sees a real-time image of the stock portfolio on the chart, while the worksheet maintains the latest prices and the report document is kept up to date. Thus, DDE can be considered as a bridge between diverse applications: by providing a uniform means of transferring data between otherwise dissimilar applications, it offers applications a level of integration that has been long talked about but never, until now, achieved. DDE opens the door for a host of third-party programs and add-ins used by a variety of applications with which they have nothing in common--except DDE. As I noted, applications that support DDE must be PM applications. Moreover, as discussed in "A Presentation Manager Primer," MSJ (Vol. 5, No.1, pp.14-16), PM applications are fundamentally distinguished from OS/2 kernel programs by the existence of a PM message queue, where messages are received from PM about input events. PM applications differ architecturally from OS/2 kernel programs since they do not use the OS/2 Vio, Kbd, and Mou subsystems to request input or generate output. Instead, they receive messages from PM that indicate when the user has pressed a key or clicked a mouse button; consequently, they also send PM a message when they want it to display information on the screen. The same is true for applications using DDE, since all DDE activity and signaling is carried out by broadcasting and posting PM messages to other applications. When one application transfers data to another via DDE, it passes a shared memory selector as a component of the message. DDE Conversations and Transactions DDE is a facility for transferring data from one application to another. Since the bulk of DDE activity is under program (not user) control, however, DDE is largely a protocol that regulates how and when data is passed between applications--it is not a public memory manager as is the Clipboard. Each time an application uses DDE, it initiates a conversation consisting of one or more DDE transactions. The application initiating the conversation is always the DDE client and the application providing the data is the DDE server. In a single conversation, a client can request data from more than one server, but an application can act as a server to only one client. Since the client application always initiates a conversation, a DDE server application that needs data from another application must initiate a new conversation and become a DDE client. Thus, an application may be a DDE client and server simultaneously, by passing data it receives as a client in one conversation to the client of another conversation (in which it is a server). A DDE conversation between two applications always takes place between two PM windows--one window per application. Each window is identified by its window handle, which is one of the components of a DDE message. The message processing code can be included in an application's primary window procedure, but it is more useful to create a new invisible window for each DDE conversation. DDE conversations use a three-level hierarchy to identify a unit of data: an application name, a topic name, and an item name. A client may specify an application's name when initiating a DDE conversation. Or, it can omit the application name, inviting any application to participate in the conversation. The topic name provides a logical context for each conversation; it can also be omitted by the client, allowing the client to converse with an application on any topic. Finally, each data item is specifically named. Thus, a client could make a request for stock quotes from Microsoft Excel by specifying "Microsoft Excel" as the application name, and "STOCKS.XLS" (the name of a stock quote worksheet) as the topic name. The item name would be a range of cells in the the worksheet. After a DDE client initiates a conversation with one or more servers, it continues via one or more transactions, that is, solicitations for particular DDE actions by the servers. There are six fundamental types of DDE transactions: REQUEST, ADVISE, UNADVISE, POKE, EXECUTE, and TERMINATE. A DDE client may REQUEST data that the server will immediately provide, if possible. Or a client may ask the server to ADVISE it if the specified data changes. In the latter case, the client may ask the server to provide it with updated data automatically; or it may ask simply to be notified that the data has changed. A client can also tell the server to UNADVISE it when information is no longer needed. Additionally, a client may POKE a server in an attempt to offer an unsolicited data item. Or a client may pass the server a command string to EXECUTE. Finally, either the client or the server may TERMINATE the DDE conversation. Note that TERMINATE is the only transaction that either party may issue; the other transactions must be issued by the client. As mentioned above, if an application playing the server role wishes to receive data from a client application, it must initiate a new DDE conversation in which it becomes the client. Starting a DDE Conversation To initiate a conversation, an application has a window procedure call the WinDdeInitiate function, which takes the window handle, the application name, and the topic name as parameters. This function automatically creates a shared memory segment containing the DDEINIT structure, defined in PMWIN.H: typedef struct _DDEINIT { USHORT cb; // length of block PSZ pszAppName; // application name PSZ pszTopic; // topic name } DDEINIT; The WinDdeInitiate function allocates the shared memory segment with enough room to contain the application name and topic name; it then copies these names into the segment. Next, WinDdeInitiate sends a WM_DDE_INITIATE to every frame window whose parent is HWND_DESKTOP and provides a copy of the shared memory segment as a message parameter. Because the message is sent and not posted, the client window will wait until all the responses have been processed. Any window that supports DDE will process the WM_DDE_INITIATE and determine if it can support the proposed DDE conversation by examining the application name and topic name in the DDEINIT segment. Again, every frame window whose parent is HWND_DESKTOP gets the message; it is up to the window that receives the message to process it (instead of passing it on) and determine whether the proposed DDE conversation can be supported. The application name and topic name merely provide a logical context for the conversation; a window can respond to such a message regardless of its specific application name. Note that the initiating window may pass either name as a NULL and that the receiving window can treat a NULL name much like a filename wildcard (*). If a receiving window decides it can support the proposed conversation (that is, it can add to the conversation by offering data that pertains to the application or topic name), it responds with WinDdeRespond, passing it the window handle of the DDE client window, its own window handle, and the application and topic names it received in the WM_DDE_INITIATE message. The receiving window always responds separately for each topic it supports; that is, if a client window sends a NULL topic name, the server window calls WinDdeRespond once for each topic supported. The client window receives a WM_DDE_INITIATEACK subsequently for every call to WinDdeRespond by a DDE server window. This message contains a shared memory segment with a DDEINIT structure that the client uses to examine the application name and topic name and see which DDE conversations are being supported by server windows. The client posts a WM_DDE_TERMINATE message at this point to the server of each DDE conversation it does not intend to continue. Creating a REQUEST Transaction After the client window has initiated the DDE conversation, it begins to request data from the remaining DDE server windows. To do so, the client window posts a WM_DDE_REQUEST message to each server, which includes a shared memory segment containing a DDESTRUCT structure: typedef struct _DDESTRUCT { ULONG cbData; // length of block USHORT fsStatus; // status word USHORT usFormat; // format word USHORT offszItemName; // offset into block for // item name USHORT offabData; // offset into block of // data } DDESTRUCT; Both the DDE client and server windows use this structure in subsequent transactions. The segment needs to be large enough to contain the structure and data (if any) being transferred (including the item name). The offszItemName and offabData members indicate the item name offset and data offset from the beginning of the segment. The bits of the fsStatus word indicate additional status information. The process of sending a DDESTRUCT segment (whether DDE client or server) should always allocate the segment with the SEG_GIVEABLE flag and make it shareable by the receiving process with a call to DosGiveSeg. (Since DosGiveSeg requires the process ID of the recipient, the allocating window can call WinQueryWindowProcess with the recipient's window handle to retrieve that window's process ID.) The client window can use this structure to specify the name of the data item it is requesting as well as the format of the data (typically DDEFMT_TEXT). It can define its own data formats, but they must be registered with the system via the system atom table, where the formats are made available to other applications. Finally, the client posts the WM_DDE_REQUEST message and the shared segment to each server via WinDdePostMessage and then frees the segment with DosFreeSeg. (After a shared DDESTRUCT segment has been posted to a recipient window, the allocating window calls DosFreeSeg to notify OS/21 that it no longer owns the segment. OS/2 can then discard the segment once it has been freed by the last process that owned it.) While this may sound like a lot of work, some generalized functions will be included with PMServer to encapsulate the process of allocating and setting up the DDESTRUCT segment, posting the message and segment to a recipient with WinDdePostMsg, and freeing the segment. Responding to a REQUEST Transaction Upon receiving a WM_DDE_REQUEST, a DDE server window either posts the data item back to the client window or replies with a negative acknowledgment. In either case, the server uses the described approach for creating a DDESTRUCT segment. If the server window can satisfy the request in the specified format, it places the DDESTRUCT structure and the data item in a shareable segment, and includes the segment in a WM_DDE_DATA message that it posts to the client window via WinDdePostMsg. Upon receiving the message, the client processes the data item and frees the segment. If the server window is unable to satisfy a WM_DDE_REQUEST, it replies with a negative acknowledgment. To do this, it sets the DDE_NOTPROCESSED status bit (in the fsStatus member of the DDESTRUCT structure) and posts a WM_DDE_ACK message to the client window (again via WinDdePostMsg). If the application is busy, a server can also set the DDE_FBUSY bit. If the server wants the client to acknowledge receipt of the message (regardless of whether the server is posting a WM_DDE_DATA or a negative acknowledgment), it sets the DDE_FACKREQ status bit. Upon receiving a negative acknowledgment from the server window, a client can ask for the same data with a different DDE format (it should request the most complex format first, then step down from there). If the server was busy, the client can wait and ask for the data again later. Creating an ADVISE Transaction A client window can also use a conversation with a server to create a permanent link to a specific data item. After it does this, the server either notifies the client when the data has changed or posts a copy of the changed data item directly to the client. This data link will remain in place until it is disconnected. To establish a data link, a client window posts a WM_DDE_ADVISE with the requisite DDESTRUCT segment containing the name of the data item. To receive a notification that the data item has changed, the client sets the DDE_FNODATA status bit. If the server can comply (that is, it has access to the item in the desired format), it records the data link in an internal table or list and posts a positive acknowledgment to the client window: a WM_DDE_ACK with the DDE_NOTPROCESSED status bit clear (in fsStatus) and the DDE_FRESPONSE bit set. Then, every time the data item changes, the server posts a WM_DDE_DATA message with the DDE_FNODATA status bit set. The client window can ignore the notifications or request the latest copy of the data item with a conventional request transaction via WM_DDE_REQUEST. To receive the newly updated data, the client posts the same WM_DDE_ADVISE with the DDE_FNODATA status bit clear. This causes the server to include the data with the WM_DDE_DATA messages. In either case, if the server cannot comply with the WM_DDE_ADVISE, it posts a negative acknowledgment. The server sets the DDE_FACK bit if it wishes the client to return a positive acknowledgment of its negative acknowledgment. To terminate a data link established via a WM_DDE_ADVISE message, a client window posts a WM_DDE_UNADVISE to a server window. Since it is up to the server to keep track of which clients have data links, the server checks its internal table and sends a negative acknowledgment to a WM_DDE_UNADVISE posted by a client that has no data link. Otherwise, it removes the record of the data link from the table and responds with a positive acknowledgment. A client terminates the data links between it and a server by posting the WM_DDE_ADVISE with a NULL item name. Terminating a DDE Conversation A window can terminate a DDE conversation at any time by posting a WM_DDE_TERMINATE with a NULL DDESTRUCT pointer. Recall that this is the only DDE transaction that can be generated by either the client or the server. The WM_DDE_TERMINATE immediately stops all transactions for that conversation. The window should not process or post any further messages for that conversation. There is one exception, however: when the recipient receives a WM_DDE_TERMINATE, it immediately posts the same in reply. Thus, although the window that originated the WM_DDE_TERMINATE can consider the DDE conversation terminated upon posting the message, its receipt of the same from the recipient window is the final acknowledgment of the transaction. Once the recipient replies to a WM_DDE_TERMINATE by posting another, it may destroy its DDE window. The recipient, however, should never send a negative, busy, or positive acknowledgment in response to a WM_DDE_TERMINATE. The window that originated the WM_DDE_TERMINATE will ignore any subsequent messages received from the recipient window. Besides, the recipient may have destroyed its window before a response could be received and acted on. Adding DDE Support to PMServer As presented in the previous installment, PMServer provides Clipboard services to non-PM Vio applications. It does this by accepting messages from its Vio clients via a kernel queue monitored by a separate, windowless Queue Manager thread. When a kernel application wishes to communicate with PMServer, it sends the latter a message via this queue. After it retrieves the message from the queue, Queue Manager posts another message to PMServer's client window (via WinPostMessage), where its window procedure can act on it. To cut or copy, a Vio client writes a message (including the data to be copied) to the kernel queue, which the Queue Manager passes to the client window (which copies the data to the Clipboard). If a Vio client has registered a kernel call-back queue of its own with the Queue Manager, it can ask if Clipboard data is available. The previously described process takes place with the exception that PMServer's client window will post a message to the Vio client (via its call-back queue) indicating the presence or absence of Clipboard data. Upon receiving a positive notification, the Vio client can request the Clipboard data for pasting, which ultimately results in PMServer's client window writing the Clipboard data to the Vio client's call-back queue. You can see the similarity between DDE and Clipboard transactions used by PMServer. Each transaction requires a series of distinct actions on the part of the client and PMServer's client window, with the Queue Manager acting as an intermediary. The Queue Manager is the layer that makes it possible for PMServer's client window to receive messages from a client. The importance of this layered approach will become more apparent in the following discussion. As described earlier, each DDE conversation is unique: it involves one DDE client with one or more DDE servers, where each participant is a PM window. If one of its Vio client applications wishes to be a DDE client, PMServer must be able to represent it as a DDE client that can maintain a conversation with one or more DDE servers. Conversely, PMServer must be able to act as a DDE server for any conversation that one of its Vio applications can support. And it must be able to provide either of these services to more than one of these applications at the same time. To add DDE support to PMServer, PMServer must be able to track each DDE conversation. At first, this is easy. Every time a Vio client, such as PMAccess, asks PMServer to initiate a DDE conversation on its behalf, PMServer records the client's process ID, call-back queue handle, and DDE application and topic names in a table (see Figure 1). Then, if any other PM windows respond to the WinDdeInitiate made on the part of that client, PMServer can store the window's handle in the table entry for that conversation. The process gets more complex when a DDE conversation is underway. Suppose two Vio clients have initiated DDE conversations via PMServer to which several PM windows have responded. Suppose further that one of the PM windows has responded to both Vio clients, signaling that it will be a DDE server to each. As each Vio client makes a request for data, PMServer will post a WM_DDE_REQUEST to the server's window. The server window responds with WM_DDE_DATA. But how will PMServer know which WM_DDE_DATA message is associated with which DDE conversation and Vio client? When a WM_DDE_DATA is received, there is no information in the DDESTRUCT segment to identify the conversation. A DDE server assumes that the window that initiated the conversation is the one destined to receive the data, just as a DDE client assumes that a DDE server window is the process supplying the data. Both WM_DDE_DATA messages are received by PMServer, which is unable to distinguish between them, particularly since they were posted by the same DDE server window. What's needed is a new layer that can distinguish between and manage different DDE conversations, in the same way that the Queue Manager thread manages messages sent from PMServer's Vio clients. Invisible Windows The key is to create a new, invisible window for each DDE conversation that the application engages in. Although you can include the DDE message processing code in any application's primary window procedure, it would be difficult to keep track of and manage several different DDE conversations. Moreover, in the context of an application like PMServer, it's impossible. It's easier to write two new types of window procedures--one dedicated to being a DDE client and the other to being a DDE server. This procedure is also good programming practice. We can encapsulate the details of processing DDE messages in these two new windows. And since these windows do not need to perform any I/O, their message processing code will be limited to processing DDE messages. In order to ensure efficiency, each new window will have its own message queue and run in its own thread of execution. Although PMServer's Queue Manager runs in its own thread, it does not use a PM message queue for input. Thus, it is called a queueless or windowless thread. Each new window will run in its own thread of execution and have its own message queue. These are called message queue threads or, in PM terminology, object windows. After defining the window procedures for the new object windows, several other tasks must be addressed: how PMServer will communicate with the object window; how the object window will communicate with PMServer; how the object window will communicate with the PMServer kernel client; when PMServer will create an object window; and when PMServer will destroy an object window. The first three are easy: PMServer and the object windows will communicate by posting messages to each other via their PM queues. Moreover, an object window can talk directly to a PMServer kernel client via the client's call-back queue. What remains is to determine when PMServer will create and destroy an object window and how an object window's window procedure is defined. Adding Object Windows Since a DDE conversation takes place between a client and one or more servers, the object windows need to be involved early. Thus, when PMServer receives a request from one of its Vio clients to initiate a DDE conversation, it creates the table entry described above, and follows this by creating a new object window to manage the conversation. This is done by allocating the object window's thread stack and calling _beginthread to create the new thread. The new thread function begins by routinely creating a new PM message queue and the invisible window. The new window notifies the PMServer window procedure that it has been created and initiates a new DDE conversation with a call to WinDdeInitiate. Defining the object window's window procedure is fairly easy: the procedure must be able to manage an ordinary DDE conversation but with the added twist that the object window must deal with the PMServer Vio client. If the object window receives any WM_DDE_INITIATEACK messages from other PM windows, it posts a message to the Vio client's call-back queue, telling the Vio client that a conversation has been initialized. The object window maintains a list of DDE server windows in the table entry created by PMServer when the Vio client first made the request. When the Vio client requests DDE data, it posts a message to the PMServer Queue Manager thread, which passes the request to the object window. The object window then posts a WM_DDE_REQUEST to each DDE server. As the responses are received (be they WM_DDE_DATA messages or negative acknowledgments), it writes them to the Vio client's call-back queue. If the Vio client wishes to be advised, the object window generates the WM_DDE_ADVISE and posts the updates to the Vio client's call-back queue; the Vio client later tells the object window to generate a WM_DDE_UNADVISE. When a DDE server sends a WM_DDE_TERMINATE, the object window removes the server's window handle from the table entry and notifies the Vio client if the last DDE server has been removed from the table. When the Vio client wishes to terminate the conversation, the object window is no longer needed. It therefore posts a WM_DDE_TERMINATE to its remaining DDE servers and a message to PMServer's window procedure indicating that it is terminating. Finally, it posts a WM_CLOSE message to itself, causing the window procedure to break out of its message processing loop and terminate the thread. Then PMServer deallocates the thread's stack. (Note that PMServer can also post a termination message to the object window.) Although this discussion concerns DDE client object windows, most of it applies to a DDE server object window as well (see Figure 2). PMServer could maintain a table of Vio clients and DDE server windows, but this would require that PMServer look up the related kernel client in the table every time it receives a DDE message from a DDE client. Thus, it's simpler to devise an object window procedure to be a DDE server and let it handle the requests from a client for data, retrieving the data from a Vio client acting as the DDE server and passing the data back to the DDE client. There are two points to be aware of: first, a Vio client must notify PMServer that it is capable of providing data as a DDE server on a specified application and topic name. Otherwise, there is no way for PMServer to know which of its clients are capable of doing so. Second, when a Vio client receives a request for data from a DDE server object window in its call-back queue, it should immediately respond by sending the data or a negative acknowledgment via PMServer's Queue Manager. Otherwise, it will keep the DDE client at the other end of the conversation waiting. In the next part of this series, I'll add a DDE object window to PMServer so that it can provide DDE data to Vio programs. I'll also present an expanded version of PMAccess that can use PMServer's DDE capabilities to receive requested data from PM programs. ────────────────────────────────────────────────────────────────────────────

Volume 5 - Number 4

──────────────────────────────────────────────────────────────────────────── An Introduction to Microsoft Windows Version 3.0: A Developer's Viewpoint Michael Geary The Microsoft Windows[tm] graphical environment Version 3.0 is here! The new version of Windows1 presents a revamped memory management system and scores of other improvements for both users and developers. Let's take a quick tour of Version 3.0 from a user's perspective, and then we'll get into some programming details. You will notice one difference even before you open the box. There's now one version of Windows; Windows/286[tm] and Windows/386[tm] have been merged so the same version works on all machines. (As in the past, Windows quietly supports 8088/8086 machines, but runs well only on the fastest of them.) Once the box is open, the first thing you have to do is run SETUP. The Windows 2.x SETUP program confronted the user with screen after screen of questions about every detail of installation. Now, SETUP begins with a few character-mode screens containing only the most critical information (about your display, mouse, keyboard, network): enough to get Windows up and running. Then a graphics screen comes up and the remainder of SETUP runs as a Windows application to complete the installation. The improved look of 3.0 is already apparent here, at least on a VGA or 8514/a monitor. The buttons are three-dimensional, there are color icons, and the proportional system font is used everywhere. The graphical part of SETUP has an advice window at the bottom of the screen that always has a suggestion about what to do next. Full help is always available too, via the F1 key or a click on a question mark icon. The help system is a spiffed-up version of the hypertext help introduced in Microsoft Word for Windows Version 1.0. Key phrases in the help text for each topic are underlined; they lead to other help topics when you click them. When you click on words and phrases with dotted underlines, a glossary window pops up with the appropriate definition. The best part is that this help system is used by all the mini-applications shipped with Windows, and can also be incorporated into your own Windows applications. When SETUP wants to update your CONFIG.SYS and AUTOEXEC.BAT files, it doesn't just change them without your input as so many install programs do. It offers you three choices: to update them automatically, to save the edited versions under different names, or to edit them interactively in a little dual-file editing window that shows the old and new versions. The old SETUP had its own routine to install printers, which was completely different from how you would install them later using Control Panel. The new SETUP actually runs Control Panel in a guided session-that advice window is still there-so you are already learning about the tools you will be using later on. In the same way, SETUP runs Notepad to view the various README files instead of using the funky file viewer in the old SETUP. The idea here is to use the Windows tools instead of special purpose tools, so you have immediate practice in using Windows. The biggest change in SETUP doesn't become apparent until some time after you have installed Windows 3.0. Every Windows 2.1 user who has changed their display card or mouse knows what a pain that is-you have to reinstall Windows from scratch. Many developers used the "slow boot" method of installing Windows to make it easier to change drivers, but this method wasn't really available to end users. All that has changed. Now Windows always installs itself with the drivers in separate files, and there is a version of SETUP you can run inside Windows to change your display, mouse, keyboard, or network drivers (see Figure 1). If you ask for a driver that isn't on your hard disk, SETUP will ask you for the appropriate diskette and copy it to the hard disk. For drivers that have already been copied to the hard disk, it will just switch back and forth using those copies. This gives users the ability to change their Windows hardware configuration painlessly. After Windows is fully set up, it can run in one of three modes: real mode, standard mode, or 386 enhanced mode. Real mode is the "traditional" way of running Windows with all the limitations it always had. Standard mode and 386 enhanced mode are where things get exciting: both exploit the protected mode of the 286 and 386 processors. This permits Windows applications to access all the physical memory in the machine. The 386 enhanced mode goes even further; it provides virtual memory by swapping 4Kb pages out to disk. It's neat to open up the About box the first time and see that you have perhaps twice as much available memory as actually exists in your machine. Windows by default runs in the best configuration it can. Unless other protected-mode software prevents it, this usually means 386 enhanced mode on a 386 or standard mode on a 286. You can always select a lesser mode if you need to by running WIN /S for standard mode or WIN /R for real mode. The most common reason for doing this would be to run some older Windows application that doesn't work in protected mode; WIN /R allows most older applications to run. One surprise in installing the new Windows is that it doesn't have the PIF directory with dozens of PIFs (Program Information Files) any more. PIFs are not used as extensively as in the past: Windows will now run just about any non-Windows application without a PIF. Part of what made this possible is that Windows no longer attempts to run a non-Windows application in a window, except in 386 enhanced mode. In real or standard mode, the only way to run a non-Windows application is to switch it to a full screen. This is no great loss, since so few non-Windows applications worked in a window anyway. Besides eliminating the need for the "Directly modifies screen" option in the PIF Editor, this change means that most applications can now run with the same default settings founds in _DEFAULT.PIF. PIFs are now used more to fine-tune settings for improved performance rather than to make it possible for non-Windows applications to run at all. Each PIF contains settings for real, standard and 386 enhanced mode. In 386 enhanced mode, the advanced options in PIFEDIT.EXE (see Figure 2) permit you to customize the application's behavior as much as you want. Non-Windows applications in 386 enhanced mode run a lot faster these days and seem to be more reliable. There are two things that SETUP doesn't do for you that it really should. One is set up a permanent swap file for 386 enhanced mode. If you're running Windows on a 386 and haven't done this yet, run SWAPFILE now. Setting up a permanent swap file of contiguous disk space does wonders for Windows' performance. The other thing SETUP could do is put a WIN command at the end of AUTOEXEC.BAT. Pretty radical, I know, but this is the first version of Windows where it's really feasible to make it your primary environment. Program Manager If any feature of the old Windows has received more well-deserved criticism than the old SETUP, it is the MS-DOS Executive. The good news is that the MS-DOS Executive is no longer the primary shell used by Windows. Now, Windows starts up in the Program Manager, an icon-based program starter. Instead of presenting just a list of whatever files happen to be in the current directory, the Program Manager's main window uses the Multiple Document Interface (MDI) to present several program group windows in its main window (see Figure 3). The program groups contain icons for applications or documents. The Program Manager is able to look inside Windows executables to find their icons; you can either use an application's usual icon or substitute one from any other file. SETUP creates an initial set of these program groups, which contains the standard applications that ship with Windows. It also optionally scans your hard disks looking for other applications and creates groups for them. Later you can add or remove icons, rearrange the groups, and so on. The Program Manager is easy to customize within its limitations: all it does is let you manually start programs, one by one. It would be nice to be able to put together a group of programs that work together and somehow just say "Start this group." Ideally, you would even be able to save the state of several running programs so you could restart them later in the same screen arrangement with the same documents open. Third parties have filled in this gap in Windows; for example, hDC FirstApps includes a program called Work Sets, which saves the states of multiple applications and lets you restart them as a group. Perhaps a more serious omission in the Program Manager is the inability to specify an initial directory for an application. The Program Manager provides for only a single pathname with a program, usually the directory containing the program itself. You can use this pathname to specify a directory other than the one the program is in, but only if the program is in your PATH. Windows does let you specify separate program and initial directories, but only via PIFs for non-Windows applications! File Manager Unlike the MS-DOS Executive, the Program Manager has no file-management facilities. These facilities are now in a separate program, File Manager. The Windows File Manager is nearly identical to the OS/2 1.2 File Manager; it uses MDI to allow several views of your file system. The File Manager first shows one directory tree window that displays the first level directories of the current drive. This window lets you expand and collapse any level of the directory tree to see the directories you want (see Figure 4). To look at files, double-click (or press Enter) on a directory; another window will open up. Once you get to that window, all the usual file operations are available: copy, rename, delete, and so on. These operations work the same way as in the MS-DOS Executive. However, to copy or move files now, you can just grab them with the mouse and drag them into a new directory. The File Manager is a good program for manipulating files, but it does have some annoying design flaws inherited from OS/2's File Manager. The worst is having only one directory tree. If you want to view the tree of a different drive, you can't open up a new tree window, you've got to switch drives on the single tree window. In the process you lose track of which directories were expanded and collapsed in the previous drive. Working with multiple drives in the File Manager is an exercise in frustration. For DOS diehards like me, the best bet may be to open a COMMAND.COM window and use good old DOS commands. Especially in 386 enhanced mode, DOS command windows are really handy. (A tip here-make yourself a COMMAND.PIF file and set the Display Usage option to Windowed, so COMMAND.COM will start up in a window instead of a full screen.) Even the MS-DOS Executive is still available. It's in your Windows directory, little changed from previous versions except for its use of lowercase in filenames. I still use the MS-DOS Executive more often than I'd expect. It's not as fancy as File Manager but it starts up a lot faster, since it doesn't have to read through all your directories. So a File Manager fan I'm not: your mileage may vary. The third new feature of the Windows shell is the Task List. You can open this window by typing Ctrl-Esc or double-clicking on the screen background. It's a simple list of all the current top-level application windows, with buttons at the bottom to switch to a window, terminate a program, arrange the icons at the bottom of the screen, or cascade or tile all application windows. The tiling feature is handy, but it would be better if options were provided to push window borders off the screen and to tile by rows instead of columns. I prefer tiling in rows, because if you have two application windows on a typical 640x480 screen, it's often more useful to put one above the other rather than side by side. Enhanced Look Windows 3.0 applications look different from Version 2.1 applications. The proportional system font gives them more readable text in their menus, title bars, and dialog boxes, and the screen is a little more colorful because the standard EGA and VGA drivers have been upgraded from eight colors to sixteen. The Video 7 VGA and 8514/a drivers provide 256 colors at a time, out of a much larger palette of available colors. Also, icons are now full-color instead of black-and-white. Three-dimensional push buttons, scroll bars, and minimize/maximize icons contribute to the enhanced appearance. But it does seem a little strange to see the system menu icon, flat as always, after all the other three-dimensional features. Also, in some cases, the three-dimensional look was implemented in a rather cumbersome way. For the scroll bars and min/max icons, there are actually two bitmap resources for each item-one for the normal state and one for the pressed state. It's quite easy to produce a good three-dimensional button effect without two different bitmaps. That's how push buttons are implemented: you create a bitmap that includes just the "surface" of the button, not the edges. Surround the bitmap with lighter pixels on the top and left and darker pixels on the bottom and right. To "push" the button, slide the "surface" down and to the right and redraw some darker pixels on the top and left. Although it takes some work to write this routine, you have your three-dimensional effect working consistently, using any bitmap you like. I'd recommend this approach for your own programs rather than the two-bitmap approach. New and Improved Applications Most of the mini-applications packaged with Windows have been improved; some were rewritten entirely. No one will be too sorry to learn that Paint is gone, replaced by the much better Windows Paintbrush from ZSoft. Paintbrush isn't without flaws. The Zoom In feature is absurd for small images-the zoomed image is crammed into the same tiny space as the original image, leaving hardly any room to edit the pixels. And don't even try to paste in a full screen image captured with the PrtSc key-it will get clipped to the current window size. But Paintbrush supports color and larger images, and it can create BMP files as well as its own PCX format. I'll take it over Paint any day. The old Terminal is also gone, replaced by a new terminal program from Future Soft. It's a much more capable program, with modem command strings that can be customized, definable function keys, VT100[tm] emulation, and XMODEM and Kermit file transfers. My little Hewlett-Packard 48SX calculator happens to speak Kermit too, so I plugged it into the new Terminal and they happily sent files back and forth. The old Terminal wasn't good for much, but it did have one feature that I really liked-the scrollback buffer would save up to 999 lines. The new Terminal only allows up to 399 lines of scrollback. Another curiosity is that the Modem Commands dialog box lets you specify a string to put the modem into auto-answer mode, but there is no command anywhere to let you use this string. If you want to answer a call, you'll most likely have to type in a Hayes "AT" command, like ATS0=1, yourself. Control Panel has been redone, and everything is more logically organized. In particular, setting up printers is done far more sensibly. You can set them up from one dialog box instead of having to bounce back and forth between the Printers and Connections dialogs as in the old Control Panel. Setting up screen colors is simpler, too. You can choose from a dozen predefined color schemes, or you can customize each screen element (see Figure 5, and the sidebar "WIN.INI Color Settings"). There is now a keyboard speed setting in Control Panel, but anyone who has written or used other programs to control keyboard speed will be surprised to see there's only one speed setting. PC keyboards have two settings: initial delay and repeat rate. Control Panel unfortunately always sets the initial delay to be somewhat on the slow side. To solve this, remove the KeyboardSpeed= line in your WIN.INI file, which will disable the Windows keyboard speed setting. Then simply put your favorite key speed setting program in your AUTOEXEC.BAT. It's best if this is the kind of program that just sets the speed in the keyboard hardware, not a program that stays resident and uses timer interrupts and such. Doing this lets Windows leave your speed setting alone. If you ever click open the Keyboard dialog box in Control Panel, Windows will go back to its own setting-even if you Cancel the dialog box. Other new mini-applications include Recorder, a program that uses Windows' journaling hooks to record and play back keyboard and mouse activity. It's reasonably easy to use and very useful for recording quick shortcuts and longer sessions for testing or demos. Print Manager replaces the old Print Spooler and provides more control over the print jobs. Notepad now supports files of almost 64Kb. Clipboard has several new options, including the ability to save and load files containing the clipboard contents. Calculator can now switch between being a full-fledged scientific/programmer's calculator and a simple "four-function" model. And not to be left out, Clock now has an appropriately hard-to-read digital option. Finally, there's a dangerously addictive Solitaire program with terrific graphics (see Figure 6). Developing in Windows The big news in Windows 3.0 for developers is of course protected mode. Windows applications are finally free of many of the memory constraints that bogged down previous versions of Windows. True, Windows 2.x, with its support for EMS and XMS, was an improvement over 1.03, but it did not solve The Problem: you could buy all the memory you wanted, but you couldn't get at it with a GlobalAlloc. Working with memory that's constantly moving has terrorized a generation of Windows programmers. Show me a Windows programmer who really likes GMEM_MOVEABLE and I'll show you someone who hasn't seen their first Invalid Global Heap. In simplest terms, running in protected mode means that all extended memory is directly available through normal calls like GlobalAlloc and GlobalLock. Protected mode permits any application to allocate as much memory as it needs, up to the limit of physical memory, unlike EMS, where more applications can be run but each application is still subject to the 1Mb limit. And in 386 enhanced mode, you can go beyond the limit of physical memory to allocate virtual memory, which is swapped to disk when physical memory is overcommitted. Either way, the same memory management functions that a Windows application used in real mode now work in protected mode. In fact, normal well-behaved Windows code will run identically in both modes. The strange thing is that the handle-based movable memory system is quite literally a simulation in real mode of protected-mode addressing. The protected mode of the 286 and 386 provides direct hardware support for the kind of memory management that Windows provides, without the hassles on the application programming side. If it had been planned this way from the start, and if (a big if) real mode support could be dropped, Windows programming could be a lot simpler. A lot of the extra memory management work you do in traditional Windows programming is simply to help Windows simulate protected mode in real mode. This happened, as they say, "for historical reasons." Back in the days when 64Kb was more than enough memory, our CP/M and Apple II systems addressed it in the simplest way: linear physical addressing. Each byte of memory had a unique physical address, and you accessed memory directly by using its physical address: no mapping, no funny tricks (see Figure 7). Since the width of the address registers determines how much memory you can access, an 8080/Z80 machine with a 16-bit address could use 216, or 65,536 (64Kb) bytes of memory. Addresses 0 (0000H) and 65,535 (FFFFH) were indeed the first and last bytes of memory (assuming you could afford a full 64Kb). Ah, life was simple. Then it was time to put the 8086 (and 8088) together, and the designers at Intel got clever. Too clever, some might say. They wanted to provide access to more than 64Kb, while at the same time sticking with 16-bit registers to keep things simple-simple for the chip, that is. So take a 16-bit register and shift it four bits, and you can address a full megabyte (220 bytes). The only problem is that you can only address in 16-byte increments. But add in another 16-bit register (not shifted) and you have our beloved 16:16 bit segmented addressing system (see Figure 8). The less said about this, the better. Memory fragmentation is a big problem in most memory management schemes; memory compaction with movable blocks is one sure way to avoid fragmentation. But movable memory means that you can't use just conventional pointers-the memory you're pointing to might move (as every Windows programmer is painfully aware). Due to the relative difficulty of programming this kind of system, most memory management packages have opted for the fixed allocation approach even if it means suffering some fragmentation. The C run-time library functions malloc and free are a good example. They're easy to use, but you can't avoid fragmentation with these. Graphical user interfaces gobble up memory compared to simple character-based programs, so both Windows and the Macintosh use handle-based movable memory systems. This permits developers to manage memory without incurring fragmentation and write larger programs with more consistent memory behavior. However, these two systems present the movable memory to the application programmer differently. Both have a table of master pointers, pointing to each movable block. When a block gets moved, its master pointer gets updated. The handle you get back when you allocate a block is in some form an index into this master pointer table. But from the application programmer's point of view, the Mac's method is more convenient by far: the handle for a movable block is literally a pointer to the master pointer for that block. All you have to do to get to a block's data is a double indirection instead of the single indirection you would use with a straight pointer. In C, you simply use ** instead of * in what otherwise looks like a normal pointer dereference. You have to be careful-the memory you are looking at is not locked down and could move if you somehow cause a compaction-but all in all it's a wonderfully convenient technique. There are Lock and Unlock calls for when you want to play it safe, but they don't need to be used all that often. In Windows, on the other hand, you go through a lot more aggravation dealing with movable memory. You aren't allowed to do much with the handle you get back from GlobalAlloc directly. (For a fixed segment, of course, the return value from GlobalAlloc is actually the segment address, and you can directly construct a far pointer from that.) You have to pass the handle to GlobalLock or GlobalHandle to get a pointer to the data (see Figure 9). A global handle is in fact an offset into a table of master pointers, found in the ever-mysterious BurgerMaster segment. With near and far pointers to deal with, a simple double indirection may not have flown, but some kind of simple macro, or even a special C pointer type, could have provided direct access to movable data. The Windows designers chose, however, to make you go through the explicit GlobalLock call to get to the data. This has the advantage of always locking the data when you're using it, but I think anyone who has programmed both the Mac and Windows will agree that the Mac's double indirection is a lot easier to deal with. GlobalLock is a pain, but there may have been method in Microsoft's madness. In protected mode the 286 uses mapped memory, not direct physical addressing. 16:16-bit real-mode segmented addressing uses a direct physical address calculation. In protected mode the segment becomes instead a selector, which is really an index into a table where the physical address can be found. Two tables, the Global Descriptor Table (GDT) and Local Descriptor Table (LDT), are used by the protected mode addressing hardware along with the segment selector to find the physical address (see Figure 10). If this sounds a lot like BurgerMaster and GlobalLock to you, you're right. Just as Windows in real mode keeps BurgerMaster up to date with segment movement, it keeps the GDT and LDT up to date in protected mode. There is still a global heap with segments that move around, but application software doesn't have to deal with the segment movement. Each reference to memory goes through the addressing hardware, which does the GDT/LDT lookup and calculates the correct physical address. It's just like what GlobalLock does for you when it looks up the handle in BurgerMaster, but it happens behind the scenes. This means that protected mode provides the benefits of movable memory without the drawbacks. How can Windows take advantage of it in a way that is reasonably transparent to existing code? First, when you call GlobalAlloc(GMEM_MOVEABLE), the "handle" you get back is actually a selector. You could go ahead and use this selector to access the memory, but if your code was written for real mode, it regards the handle as a "magic cookie:" an identifier and nothing more. You pass the handle to GlobalLock when you want the address, and GlobalLock returns the address-the same selector! (It's formatted into a far pointer, of course, and the low order bit of the selector is turned on. In the "handle" form of the selector, the low order bit was off. There is a reason for this, as we'll see.) GlobalLock can get away with this because the protected-mode memory addressing hardware is doing its work. If a selector is automatically dereferenced through the GDT/LDT as needed, there isn't much left for GlobalLock to do but convert it into a far pointer. Even the locking aspect goes out the window. In real mode, a segment is locked to keep it from moving or being discarded. The GDT and LDT of protected mode make these operations redundant, because they indicate whether a segment is in memory at all as well as its physical address if it is. If you try to reference a segment that isn't in memory, the hardware traps that and Windows takes care of it. So there's not much need for a lock count, and GlobalLock's work is easy. There's one case in real mode where GlobalLock is nearly this lazy. A GMEM_FIXED segment isn't going to move, so Windows doesn't bother with a BurgerMaster entry. When you call GlobalAlloc(GMEM_FIXED) in real mode, the "handle" you get back is in fact the actual segment address. All GlobalLock does with it is construct a far pointer with the segment and a zero offset. There is no low-order-bit twiddling as I mentioned above, because the "handle" already has the low order bit on. This is how GlobalLock figures out whether the "handle" you pass it is a BurgerMaster offset or a GMEM_FIXED segment. If the low order bit is off, it's a handle; on, it's a segment. That is, handles are even, segments are odd. Microsoft might prefer that you didn't know this, because you might write code that inadvertently or even-dare I say-intentionally depends on it, and they might want to change it in the future. So you didn't hear it from me. But for the time being, that is how it works. (I'm ignoring several other special cases that GlobalLock has to check.) Because segments do not move around (at least their selectors don't change) in protected mode, all sorts of other simplifying assumptions are possible. Reload thunks, those little SAR/JMP sequences generated for movable/discardable far functions in real mode, go away completely. You need these in real mode because there has to be a fixed entry point, even for a movable function, so you can stash its address away in a pointer variable, and because there has to be a way to know when to load in a segment that's not in memory. The SAR/JMP changes to an INT 3FH for functions that aren't in memory; that INT 3FH is how Windows can magically load in a discarded code segment when it's called. But in protected mode, every segment looks like it's FIXED because it always has the same selector. Also, if you try to reference a segment that isn't in memory, the CPU will cause a Not Present exception interrupt, which tells Windows when to load in your segment. Reload thunks are just part of Windows' real-mode emulation of protected-mode memory management, so you won't find any if you're not in real mode. You can play similar tricks yourself. If you know you are in protected mode, you don't have to lock and unlock segments all the time. You get a chunk of memory with GlobalAlloc and perform just a single GlobalLock call on it-or just make a pointer from the segment selector returned by a GlobalAlloc(GMEM_FIXED)-and keep using that pointer until you are finished with the memory. You won't be interfering with Windows memory management as you would be in real mode; remember that Windows can still move physical memory around all it likes and never tell you about it, as long as it keeps the GDT and LDT up to date. (There are functions, GlobalFix and GlobalPageLock, that actually lock down a segment in the physical linear address space, and they would of course cause the same fragmentation problems as always. Few programs should ever have to use these.) This Can't Be Real! Here's the bad news. Real mode is still with us. There's no mercy here. Just when we discover how nice it is for our applications to have enough memory to run and we don't have to worry about dangling far pointers, some user pops our balloon by starting Windows with a WIN /R command, or by running on an 8088 or 8086 (see Figure 11). This puts us back where we started, complete with all the hassles of EMS. Windows does run fairly well on a fast (10MHz or better) 8086 machine, but it can't run protected mode on one. So we still have real mode to kick around. There are some improvements in Windows 3.0 for managing memory in real mode. GlobalLock and GlobalUnlock have a completely undeserved reputation for speed. But you don't have to use them as much-there's an alternative called the private handle table, which is a table in your DGROUP that contains a list of global segment addresses. A DefineHandleTable call tells Windows where your table is. When you allocate a GMEM_MOVEABLE segment, call GlobalHandle to get its current (unlocked) segment address, and put this segment address in an empty spot in your table. Internally in your code, you then use a private handle of your own, which is really an offset or a pointer into your table. You can define this private handle any way you want, as long as it gets you to a table entry. Whenever Windows moves a segment in memory, it scans all private handle tables looking for references to that segment and updates them. When you need to get to a segment in memory, use your private handle to get to the correct entry in your table, and you will find the current segment address. The private handle table really isn't anything new; it's been in Windows since Version 2.0, it's just not a secret anymore. The one catch with the private handle table is, of course, that its addresses are the addresses of unlocked segments. Segments can still move around in real mode; therefore, a far pointer derived from the handle table has a lifetime shorter than a mayfly. In addition to the well-known situations in which memory can move, any far call to a discardable segment can also move memory. So don't hold onto one of these pointers for very long. To protect against this, I add a nested block in C with the segment pointer declared inside that block. The block is as short as possible and includes only the code that needs to use the pointer. Since the pointer variable name is local to the block, the compiler will give me an error message if I use this pointer farther down in my code after I might have made a far call. Now, alarms go off in my head when I see a pair of curly braces without some keyword above them. It looks like there is a missing statement above the opening brace. One solution is to add a new keyword to the C language, scope, that does nothing but look better than a blank line. Code then looks like this (assuming a SEGDEREF macro that takes a private handle and returns the current far pointer to a segment): // define a no-op placeholder keyword #define scope o o o // We are inside a function with a private handle // variable "hfoo" that points to a segment // containing a FOO structure, and we need to copy // a couple of items out of the structure into // local variables. // The "scope" keyword does nothing, but clarifies // the purpose of the extra curly braces. scope { LPFOO lpfoo = SEGDEREF(hfoo); item1 = lpfoo->item1; item2 = lpfoo->item2; } // Now item1 and item2 have our data. The "lpfoo" // pointer variable is safely undefined, so we // can't accidentally use it after memory moves. The private handle table works in both real and protected mode. In protected mode, Windows never bothers updating the table, but then the "segments" it gives you are selectors that aren't going to change anyway. At least that's true for segments of 64Kb or less. With segments greater than 64Kb, Windows may have to give you back a different selector on a GlobalReAlloc call, because it has to find a contiguous range of selectors to allow addressing the entire segment. The private handle table doesn't take this into account, but other than this, it works in either mode. If you want to be a Good Programmer, you write your code so it runs in both real and protected mode. For many programs, all you have to do is code as you always have for real mode, avoiding any violations of the protected-mode rules set forth in the Software Development Kit (SDK). You start running into problems with large applications. When your programs get past a certain size, there's just no room left in real mode and you have to resort to drastic measures like supporting EMS or writing your own disk swapping routines. It's tempting just to switch to protected mode completely and avoid all that, but then you'd have to give up the 8088/8086 market. The best approach is to support all memory modes whenever it's possible. If your application is small enough to run reasonably well in real mode, by all means code it so that it's compatible with both modes. Or if you already have code that uses EMS or swaps to disk to let you run a bigger app in real mode, keep that code around-it can still serve you in real mode even if it isn't needed in protected mode. A typical approach here is to try GlobalAlloc first, and if that fails try allocating EMS or swapping something else out to disk. Under 3.0, this gives you the best of both worlds. In real mode, it will work just like under 2.1. In protected mode, the GlobalAlloc will fail much less often because you have so much memory available. When it does, you would skip EMS because it isn't available in protected mode and your disk swapping code would kick in. But if your app is bursting at the seams in real mode, if you're facing months of tuning for real-mode performance to be acceptable, if you're having to write memory management code instead of writing your application, maybe it's time to switch to protected-mode-only programming. You won't run on 8086/8088 machines, but an application that large probably wouldn't run very well on them anyway. If you're really backed into a corner with real mode, go ahead and require protected mode. But be warned: if you are lucky enough to be able to write protected-mode-only code, you will never, ever want to go back to coding for real mode. Even in protected mode, there are still places you can run out of memory long before global memory is exhausted. Your own local heap is one, of course, but you should also consider GDI's local heap and USER's local heap. USER's local heap usually runs out first. Every window created uses space in USER's heap, as does the text of each menu item and window title. You can see this in action if you have a lot of icons and program groups in Program Manager-when I added a bunch of icons to my original groups, the Free System Resources percentage displayed in Program Manager's About box dropped from close to 90 percent down to 65 percent. (The Free System Resources number is the percentage of free space in either USER's heap or GDI's heap, whichever is currently more crowded.) Those innocent little icons were eating up 25 percent of USER's available heap space! Standard versus Enhanced Mode Although standard mode and 386 enhanced mode look nearly identical from the point of view of a Windows application, they're implemented quite differently. In standard mode, a simple DOS extender, DOSX.EXE, handles switching between real and protected mode, selector allocation, and other services that Windows needs (see Figure 12). DOSX does not provide multitasking for non-Windows applications, nor does it create virtual machines-it has to be compatible with both 286 and 386. It does use a few 386-specific features like faster mode switching when it is on a 386. The 386 enhanced mode is completely different. Instead of the simple DOSX.EXE, there's a Virtual Machine Manager that creates virtual machines and multitasks them, as well as a set of virtual device drivers needed for virtual machines (see Figure 13). This, of course, was developed from the Windows/386 code, but goes beyond it. In particular, Windows 3.0 virtual machines include not only the virtual 8086 environment that the 386 hardware provides, but also an optional protected-mode portion. The first virtual machine created-the System VM-has a protected-mode portion used for running Windows applications. Other virtual machines may have only an 8086 (DOS) portion, but they can just as easily have a protected-mode portion, allowing other kinds of protected-mode applications to run in 386 enhanced mode. Proportional System Font After protected mode, the change in Version 3.0 that may affect the most existing code is the switch to a proportional system font. At the very least, dialog boxes need to be redone to allow for changes in text string size. Any code that assumed a fixed-pitch system font-for example, code that gets the height and width of one character and multiplies that by the number of characters to calculate string length-will have to change. You must now use GetTextExtent or GetCharWidth to calculate string lengths. The Windows edit control code had to be completely rewritten for Version 3.0, largely because of the switch to the proportional font. You may also have some code that knows the physical size of the dialog units used in specifying dialog coordinates. For example, I've worked on a couple of programs that created windows that acted a lot like dialog boxes. I kept track of window sizes in units directly proportional to the dialog unit size, to achieve the same modest degree of device-independence that dialog boxes have. I just did a GetTextMetrics on the system font and divided the height by eight and the width by four. I thought this might be OK for a proportional system, if TextMetric.tmAveWidth was used instead of TextMetric.tmMaxWidth. Not quite. Windows does a special calculation of the system font's average width (tmAveWidth obviously wasn't good enough). To get the system font average height and width, you have to call GetDialogBaseUnits, still dividing by eight and four if you want the dialog units. Since you also can't use spaces to line up columns of text with a proportional font as you can with a fixed-pitch font, list boxes and edit controls now support tab stops. Send a LB_SETTABSTOPS or EM_SETTABSTOPS to specify the tab positions, then embed tab characters in your text to line up your columns. For text you are painting explicitly in your own code, use the new GetTabbedTextExtent and TabbedTextOut functions to support your own tab stops. The proportional system font will cause extra coding pain for some applications, but for others it could save a huge amount of work. Because all the standard control classes now support the proportional system font, they support all fonts. (All bitmap fonts, anyway.) You can send a WM_SETFONT message to any control to select its display font and WM_GETFONT to find out a control's current font. If you were about to write your own edit class just because you needed to support proportional fonts, stop. The same is true for list boxes and the other classes. They will all work with any font you can name, making the standard controls useful in more cases than before. GDI Goodies One of the most visible changes in the new Windows is the greatly increased use of color, especially color bitmaps. The old Windows bitmap format was not quite good enough for providing this extra color. In the old format, a color bitmap was organized to match a specific display device; monochrome bitmaps were portable, but color bitmaps weren't, which is why you didn't see them very often. The bitmap portability problem is solved in Windows 3.0 by the appropriately named Device Independent Bitmap (DIB). Instead of being tied to a specific display, the DIB is in a more general format that is portable across devices but can still be converted reasonably quickly into any particular display format. Each pixel in a DIB can be represented by 1 bit (monochrome), 4 bits (16-color EGA/VGA), 8 bits (256-color Super VGA/8514), or 24 bits (the software is ahead of the hardware for a change, at least for display adapters in common use). In a 24-bit-per-pixel DIB, the 3 bytes representing each pixel directly specify an RGB color value. When there are fewer than 24 bits per pixel, the bits representing each pixel are actually an index into a color table at the beginning of the bitmap. This color table provides the full RGB values, allowing a bitmap to be saved in a more compact format as long as it uses a small number of colors, even if those colors themselves might be composite. DIBs are easy to create and use. When you save a file in Windows Paintbrush, the Options button gives you the choice of any of the four DIB formats as well as the PCX format. The four DIB formats use the BMP extension even though they are not in the old bitmap format; you can make them into resources and use LoadBitmap on them just like before. LoadBitmap converts the bitmap from DIB format to the appropriate display format when it loads it. Alternatively, there is a set of functions-CreateDIBitmap, CreateDIBPatternBrush, GetDIBits, SetDIBits-that can be used to create memory bitmaps and pattern brushes from DIBs directly. Another option is to skip the memory bitmap step completely and use SetDIBitsToDevice (StretchDIBits if stretching is required) to transfer the DIB to the destination device directly. This saves memory, but it can be slower for bitmaps that are painted repeatedly because the format must be converted each time the bitmap is drawn. Another memory saver is the compression in DIBs-there are several run-length encoding (RLE) methods used to save space. The new multiple resolution icons and cursors are similar to DIBs, but implemented differently. When you create an icon or cursor using SDKPaint, you have the option of including multiple images for different display devices, tuning the images for each device. Icons can be in full color on color devices, but cursors are still limited to black-and-white. LoadCursor and LoadIcon select the best match for the current display out of the images you supply. As before, if there is not an exact match, the functions choose the best match they can find and scale it to fit the resolution. It's convenient that DIB pixels can be lookups into a list of RGB color values, because that's exactly the capability many display cards have-there are many color choices, but only a fraction of them can be shown onscreen at one time. For example, 8514 and Super VGA monitors can display 256 simultaneous colors out of 262,144 possible choices. It's better to be able to specify which colors go into that usable palette; you can do a lot more with 256 colors when you get to pick them yourself. The only problem is that in Windows, you can run several applications at once. If your application changed the display palette, it would disturb every other application since they're all sharing one screen. Palette Manager The new Windows Palette Manager was designed for this. This facility mediates among applications that use many colors and need to modify the display palette. Applications can create their own logical palettes containing color values they need, then use those color values in normal GDI calls. The Palette Manager maintains a separate system palette with the current physical display colors. The active application gets first choice of those colors; any leftover colors are used for inactive applications. The Palette Manager matches the logical palettes with the available physical colors, giving inactive applications the closest matches it can find after the active application has taken all the colors it needs. To allow title bars, menus, and the like to be painted in their normal colors, 20 colors are reserved. That way, even if the active application takes all the colors it can get, the Windows screen will pretty much look correct. An application with a great many colors can, with a little extra work, even temporarily steal all 20 system colors, except black and white. Using the Palette Manager is fairly easy. You create a logical palette with CreatePalette, which takes a list of RGB colors and transforms them into a GDI logical palette object. Select the palette into a device context with SelectPalette, and call RealizePalette to have the Palette Manager match your colors with the system palette and install the system colors it needs for you. Then you can use your palette in GDI functions like CreateSolidBrush by using specially coded color values. The color parameter to such functions is now called a COLORREF, and its high order byte tells what kind of color reference it is. A zero in the high order byte indicates that the color reference is an ordinary RGB color. You can still use a direct RGB color in Version 3.0 when you're working with the Palette Manager, but you will probably prefer to use the palette that you set up. If the high byte has a value of 1, the low order 16 bits of the COLORREF are an index into the list of colors in your palette. A device-independent bitmap can use palette colors by listing palette indices in its color table instead of RGB colors. If you're running on a 24-bit color device, the palette system gets in the way. Why bother with a list of colors when the entire spectrum of RGB colors is available directly? The best method of managing colors would be to specify an RGB color for the 24-bit case but still be able to use your logical palette when on a palette device. There is a way to do that. When the high order byte of a COLORREF contains 2, the low 3 bytes are interpreted as a palette-relative RGB value. On a 24-bit system, this is used directly as a 24-bit RGB value. On a palette system, the Palette Manager matches this RGB value with your logical palette and uses that palette entry as if you had specified its index directly. Watch out for the high order byte when converting existing code! That byte was ignored in the past, but if you fiddle with a COLORREF-for example, performing a binary NOT to obtain the complement of a color-be sure to zero out the high order byte if the COLORREF is supposed to be an RGB color. Odds and Ends With the increased use of color, there's a problem in using the FloodFill function. FloodFill expects a border color parameter and fills outward in all directions until it encounters that color. This is fine for monochrome images, but in color you may have a border of several different colors and an area with a single color that you want to change to a different color. For color, use the ExtFloodFill function; it fills all pixels that match a given color, instead of those that don't match a border color. This function is like FloodFill but takes one more parameter specifying the fill mode: FLOODFILLBORDER, which makes the function work like FloodFill, or FLOODFILLSURFACE, which will perform the new filling method I just described. You can see FLOODFILLSURFACE used if you try the fill tool in Paintbrush. There are of course new META_xxxxxx metafile records to support all the new GDI functions, including those for palette management and DIBs. A serious omission in Windows 2.1 meant that a metafile could create brushes, pens, and other objects but could not delete them, leading to nasty memory leaks as objects were created but not destroyed. This has been corrected. The new META_DELETEOBJECT record allows you to destroy these objects. MDI My favorite Version 3.0 API feature is one that users will never notice, but is a boon for developers: support for the MDI. For the last couple of years developers have been told to use MDI, but there was practically no Windows MDI support other than for child windows. Adding MDI support to an application could mean an additional thousand lines of code or more. Fortunately there's now clean, simple MDI support in the Windows API. There is one new predefined window class, MDIClient, four new functions, and a dozen new messages. Setting up MDI is easy: create the outermost frame window as you normally would, then create a window of the MDIClient class in its client area. This window is the MDI workspace. It can fill up the frame window's client area or you can leave room for objects such as a status line (see Figure 4). To create each document window inside the MDI workspace, you send the MDIClient window a WM_MDICREATE message. Similarly, you destroy a document window by sending a WM_MDIDESTROY to the MDIClient window. Instead of calling DefWindowProc, the frame window calls DefFrameProc, and the document windows call DefMDIChildProc. The only thing remaining is to include a call to TranslateMDISysAccel in your main message loop. All the required MDI behavior is handled by the MDIClient window and the three new functions: even the window's menu is maintained properly by MDI. Anyone who has programmed MDI under 2.1, or more likely has left MDI support out of their application because of its difficulty, will be happy when they see that this support is built into Windows. (For a more detailed discussion of the Windows 3.0 MDI API, see "A New Multiple Document Interface API Simplifies MDI Application Development," p. 53-Ed.) Control Class Enhancements As I mentioned, the Edit control class now supports proportional fonts. Other things work now that never did before, too. Did you ever try the ES_CENTER or ES_RIGHT styles in previous releases? Text would show up in the wrong places and sometimes even would jump out of your way when you tried to click on it. In 3.0, these styles work correctly-not as entertaining perhaps, but certainly more useful. New Edit control styles make it easy to do some things that before you had to subclass or worse: ES_UPPERCASE and ES_LOWERCASE force input to uppercase or lowercase; ES_PASSWORD prevents normal display of the edit text, displaying asterisks instead (see Figure 14). If you don't like the asterisk, you can pick a different character with an EM_SETPASSWORDCHAR message. Other new messages include the EM_SETTABSTOPS that I mentioned and EM_EMPTYUNDOBUFFER, which prevents a subsequent EM_UNDO or Alt-Backspace keystroke from having any effect (until the next undoable edit, of course). ListBox Class The ListBox class wasn't completely rewritten like Edit was, but it did receive more new features (see Figure 15). The first new feature is the owner draw facility. Items in a list box can now be anything you draw, instead of having to be text strings. The ListBox class still takes care of the keyboard and mouse interface, scrolling and keeping track of what lines are visible and it still sends you a simple WM_DRAWITEM message for each line to be painted. You can have the owner draw list box store a list of strings for you by using the LBS_HASSTRINGS style. Without this style, the list box will store 32 bits of data for each item. You can put what you want in them, including a far pointer to a string, a far pointer to any data structure of your choosing, or any kind of arbitrary 32-bit value. The LB_ADDITEM and LB_INSERTITEM messages take this 32-bit value in lParam in place of the normal string pointer. Each WM_DRAWITEM includes this 32-bit value along with the other information it passes; you can also look at it with a LB_GETITEMDATA message or change it with LB_SETITEMDATA. These two messages work with any list box, not just owner draw list boxes. This is really handy when you want to associate some invisible data with each item, but you don't need anything fancier than strings drawn in the list box. Owner draw would be overkill; just do a LB_SETITEMDATA on an ordinary list box and you're all set. The LBS_SORT style gets a little tricky with owner draw list boxes. If LBS_HASSTRINGS is set, it's just as easy as before-Windows will take care of the sorting on its own. But without LBS_HASSTRINGS, there's no way for Windows to determine the sort order. So when you send an LB_ADDSTRING message to insert an entry, Windows sends back one or more WM_COMPAREITEM messages to the list box's owner. Like the compare routine called back by the C run-time library's qsort function, this message lets you specify the sort ordering by comparing pairs of items. Items in owner draw list boxes can all be the same height (as in a normal list box) or different heights. You select these items using the LBS_OWNERDRAWFIXED and LBS_OWNERDRAWVARIABLE window styles. The list box returns WM_MEASUREITEM messages to determine the size of the list items. A fixed-height list box sends one of these messages; a variable-height box sends one for each item. List boxes can now be scrolled horizontally. To add a horizontal scroll bar to a normal (vertically scrolling) list box, just add the WS_HSCROLL style, then send a LB_SETHORIZONTALEXTENT message to specify the scroll range. This is a simple feature, but it should be useful to many developers-I had to code my own list box class last year only because the standard class wouldn't scroll horizontally. You can also make a list box that's similar to the old MS-DOS Executive in "short view"-a horizontally scrolling block of items in rows and columns in which you can move the cursor in all four directions. Use the LBS_MULTICOLUMN style and send the list box an LB_SETCOLUMNWIDTH message to tell it how wide the columns are. It's now easier to handle list box keyboard input. If you want to handle any of the list box's keystrokes, give it the LBS_WANTKEYBOARDINPUT style. The list box's owner is then sent WM_VKTOITEM and WM_CHARTOITEM messages, which correspond to WM_KEYDOWN and WM_CHAR messages but include the number of the item in the list box. You can then take the default action or modify it as needed. As part of your processing, you might want to use the new LB_FINDSTRING message to locate an item by text value and LB_SETTOPINDEX to scroll the list box to a specific location. Multiple selection list boxes have changed, mostly for the better. If you used the LBS_MULTIPLESEL style in the past, the user always had to hold the Shift or Ctrl key down to perform a multiple selection. This was really annoying; you might take a couple of minutes carefully selecting various items in a list, but then one unshifted mouse click or cursor key would wipe out that selection. Some developers took a saner approach: they subclassed list boxes so that a mouse click toggled the selection on the clicked item without affecting any other selection, and cursor keys moved the focus rectangle as if the Ctrl key were down; the user pressed the space bar to toggle an item's selection. Unfortunately, this approach wasn't standard and whether the ease of use in this approach was worth the inconsistency was arguable. But now all LBS_MULTIPLESEL list boxes work as I just described. In a variation of multiple selection list boxes, the LBS_EXTENDEDSEL style, an unshifted click or arrow key removes all previous selections, selecting the item clicked or moved to. Or a Ctrl-click adds individual items to a discontinuous selection. You can also select a continuous range using the Ctrl-arrow key or just by dragging the mouse across a range of items. Other than the mouse dragging, this is all nearly identical to the old LBS_MULTIPLESEL behavior. The idea is to make it easy to select a range of items at once with the keyboard or the mouse. It does get a little tricky if you want to make a discontinuous selection with the keyboard. You have to press Shift-F8 first to put the list box into a special selection mode. Then the keyboard works as it does when you use LBS_MULTIPLESEL, where arrow keys just move the selection cursor and the space bar sets or clears the selection on the current item. I'm not sure why there are two different kinds of multiple selection list boxes. They have quite different user interfaces, and surely this can't help users become more comfortable using Windows. Some operations are easier with LBS_MULTIPLESEL, and others are easier with LBS_EXTENDEDSEL. I don't know which style a developer is supposed to use. I wonder why these two styles weren't combined into one style with consistent behavior. Buttons Push buttons have a very different appearance in 3.0, but no new APIs are needed to support it. The old calls and messages work with the new look-after all, the button still has just pushed and not pushed states; it only looks different. Check boxes and radio buttons are unchanged, except that they use the gray text system color instead of a dithered gray when disabled. Since they're not three-dimensional, they look curiously flat compared to push buttons. The only API change for buttons is the BS_OWNERDRAW style, which lets you draw your own buttons using techniques consistent with the owner draw list boxes and menus. (The old BS_USERBUTTON style and BN_PAINT message are no longer listed in the SDK documentation and are not recommended for new applications, but obviously they're still supported so existing applications can work.) ComboBox Class As its name implies, the new ComboBox control class combines the behavior of the existing Edit and ListBox controls. It links input between the two controls so that you can either type into the edit control or scroll through the list box, picking an entry that way (see Figure 5). The three kinds of combo boxes are determined by the window style bits. Least often used is the CBS_SIMPLE style, in which the list box is visible all the time and the edit and list windows are linked back and forth. Much more common is the CBS_DROPDOWN style, which has an edit control you can type into and the down arrow icon next to it, which opens up the drop-down list box. CBS_DROPDOWNLIST is also used frequently; it's like CBS_DROPDOWN except that the edit control is disabled (that is, you can't type into it but can change its value by hitting the up or down arrows or by opening up the list box). A combo box, depending on its style, actually does create an edit control and temporarily a list box if needed, so it's no surprise that many of the same style options, messages, and notifications for edit and list controls are also used for combo boxes. The messages and styles aren't exactly the same-they've got CB_ and CBS_ prefixes in their names-but everything should be fairly familiar if you've worked with edit controls and list boxes. Whichever window is actually doing the work-the combo box itself, edit control, or list box-you normally exchange messages with the combo box only. Only two messages are unique to combo boxes, and they will probably be rarely used: CB_SHOWDROPDOWN, to open or close the drop-down list box explicitly, and CB_GETDROPPEDCONTROLRECT, to determine the drop-down list box dimensions. The only notification unique to combo boxes is CBN_DROPDOWN, which tells you when the drop-down list box is opened or closed. Class and Window Architecture Subclassing is a powerful technique used in many Windows applications. The most common method of subclassing a window is to call CreateWindow on an existing class and then use SetWindowLong to change the window function pointer. This is easy, but prevents you from intercepting the WM_NCCREATE or WM_CREATE messages, from adding additional cbWndExtra to your window, and from changing other class-specific information such as the icon. To avoid these problems, gather the WNDCLASS information for an existing window class, change what needs to be changed (such as the function address), and register a new class of your own with this new WNDCLASS information. Then you can create windows of this class that are immediately subclassed. To get the WNDCLASS information you need, use the new GetClassInfo function. It will give you this information without your having to create a window. You can then save the window function pointer, tweak the WNDCLASS as needed, and call RegisterClass to create your new class. One window that was especially difficult to subclass in Windows 2.x was the screen or desktop window. This window encompasses the entire screen background; all top-level windows (WS_OVERLAPPED and WS_POPUP) are really its children. Subclassing this window allows you to do things such as put up your own screen background. Windows 2.x tried to prevent you from getting to the screen window handle, but naturally there was a very sneaky way to get around this. (The call was HIWORD(GetWindowLong(GWW_PARENT-2)), if that's sneaky enough for you.) Sneaky tricks are no longer required-all you have to do in Version 3.0 is call GetDesktopWindow and it returns the screen window handle. In fact, the screen window handle is used by the desktop section of the Windows 3.0 Control Panel, allowing end users to use any bitmap as their screen background (see Figure 16). (Readers of WINDOWS.H will note the function GetDesktopHwnd. It's the same function with a different name, which carries on the tradition of GetCurrentTime and GetTickCount, also synonyms for one function.) When you're finished with a window class, call UnregisterClass to get rid of it. This allows you to register temporary classes and unregister them at various times during your program. When a dynamic-link library (DLL) exits, its classes are destroyed-previously this happened automatically only for applications. If you do have a DLL that creates public window classes, be sure to put the new CS_GLOBALCLASS style on them or they will be private to the DLL. Windows 3.0 has some new WS_ style bits. The problem is where to put them. The flWndStyle parameter to CreateWindow is more than full, in fact, it's overloaded. You may already have noticed that WS_MINIMIZEBOX and WS_MAXIMIZEBOX are the same bits as WS_GROUP and WS_TABSTOP. (This fact has been a minor nuisance in MDI programming under Version 2.1.) Since there was no place to put any more style bits, an entire new doubleword of extended style flags, with WS_EX_ prefixes, has been added. The CreateWindow function doesn't know about these flags, so a new CreateWindowEx function does. (And yes, CreateWindow now is just a call to CreateWindowEx with the extended style flags zeroed out.) There's plenty of room to grow, because only two of these flags are defined: WS_EX_DLGMODALFRAME, providing the new movable modal dialog style, and WS_EX_NOPARENTNOTIFY, which shuts off the new WM_PARENTNOTIFY message. It may seem silly to have to add another 32 bits of style flags and use only two of them, but Microsoft probably wanted to running out of bits again for a long time. DeferWindowPos One thing that's always been more difficult than it should be is positioning several windows at once and avoiding sloppy screen painting. You could just make some SetWindowPos calls, but it may be hard to avoid extra screen painting as you make the calls. Windows has always had internal functions to position a group of windows cleanly; these are now available to "the rest of us." Just replace each SetWindowPos call with DeferWindowPos, and bracket the whole thing with BeginDeferWindowPos and EndDeferWindowPos. After the EndDeferWindowPos call, Windows positions all the windows, optimizing the painting for the cleanest appearance. OS/2 Presentation Manager programmers may notice that it is similar to WinSetMultWindowPos. In fact, BeginDeferWindowPos creates and DeferWindowPos fills in a structure much like the SWP array used by WinSetMultWindowPos; EndDeferWindowPos processes this structure in the same way as the PM function. So it's really the same thing with two rather different APIs. Menus You can do much more with menus in 3.0. To create hierarchical menus with practically no work at all, use a POPUP statement instead of MENUITEM when you want a hierarchical menu, just like you do at the top menu level. After the POPUP, of course, is a BEGIN and END with a list of MENUITEMs in between for the nested menu items. The only thing different in Windows 3.0 is that you can nest POPUP statements instead of being limited to one level of POPUP menus. If I were converting a Version 2.1 application I'd probably put in hierarchical menus first, just because it would be fun and easy. With a little more work, you can explicitly bring up a pop-up menu anywhere on the screen. Create a pop-up menu with a CreateMenu call (or just use GetSubMenu to grab one loaded from a resource) then call TrackPopupMenu to display the menu. TrackPopupMenu handles all the user's keyboard and mouse input for the menu and returns when the menu is closed. One of the parameters to TrackPopupMenu is the handle of an owner window, which receives the WM_COMMAND and other messages generated by the pop-up menu. As with list boxes and combo boxes, there are now owner draw menus. You always could specify a bitmap for each menu item, but owner draw menus are more flexible than that. They work like the other owner draw controls-the application window gets WM_MEASUREITEM and WM_DRAWITEM messages if you put the MF_OWNERDRAW flag on a menu item. You can also change menu appearance by defining your own check mark bitmap. Instead of always displaying a check mark when the MF_CHECKED flag is on, Windows lets you associate individual checked and unchecked bitmaps with each menu item. One minor improvement that makes things a little less confusing is that the multipurpose ChangeMenu function is replaced by specific functions for the individual functions it performed. The new functions are AppendMenu, DeleteMenu, InsertMenu, ModifyMenu, and RemoveMenu. ChangeMenu still exists, so you don't have to change existing code right away. Some existing code could be affected by a new system menu item, SC_TASKLIST, but this should be transparent as long as you don't assume how many system menu items there are. With all the menu improvements, one omission surprised me: menus still do not support scrolling. In a number of other GUIs, if a menu has too many items to fit on the screen, the menu scrolls up and down to reveal all the items. Not so in Windows. A menu must fit completely on the screen or the user just won't be able to click on the hidden items. This is really unfortunate. For example, it means that a Windows application can never have a simple Font menu listing all available fonts. If there are too many fonts, the user won't be able to select some of them. (Yes, I know many Windows applications do have a Font menu like this, but unless they switch to a list box, they will be in trouble when there are many fonts installed.) Similarly, the Window menu in an MDI application cannot list all open windows. If there are too many, it adds a "More Windows" item to the bottom, which brings up a list box for the windows that don't fit in the menu. Elegant this isn't. Dialog Box Changes The dialog manager has been restructured. In Version 2.1 we pretty much had to use the standard dialog window class. Theoretically it was possible to create a dialog box using your own window class, but the standard dialog window class still had some of the functionality you needed. The only easy way to make things work was to use the standard class and then subclass it, and this approach had its own problems. All that special functionality has been moved out of the standard dialog class and into a new DefDlgProc function. Thus, you can use a window class of your own for a dialog box, calling DefDlgProc instead of DefWindowProc. Another dialog problem that has been solved is how to pass parameters to a dialog function. The lpParam of CreateWindow is missing from the existing dialog creation functions, forcing programmers to use static variables and various kludges to get information into dialog functions. This doesn't seem like that much of a problem until you get to situations like multiple instances of a modeless dialog box; then it's a real nuisance. Now the dialog creation functions all have versions that include a long parameter like in CreateWindow. The odd part is that because there were four ways to create a dialog box (using DialogBox, DialogBoxIndirect, CreateDialog, or CreateDialogIndirect), there are now four new functions, one of each of the above with "Param" tacked on the end. A few new dialog styles are worth noting. DS_MODALFRAME gives you the new movable modal dialog box; it corresponds to WS_EX_DLGMODALFRAME in CreateWindowEx. DS_NOIDLEMSG turns off those pesky WM_ENTERIDLE messages. If you're tired of not knowing what that message really does, you can get rid of it. Or just look it up in the documentation; it's documented now. Last, DS_SETFONT now specifies a font to be used by the controls in the dialog box. You can do the same thing with the WM_SETFONT message; it's just a convenient way to set the font for every control in the dialog. You're most likely to use DS_SETFONT in its guise as the FONT statement in an RC file; DS_SETFONT is the actual style bit you'd use in a CreateDialogIndirect call. Message Box Changes Message boxes have one new feature, the MB_TASKMODAL flag. This flag disables all other windows owned by your application while the message box is open. You might think that MB_APPLMODAL would do this, but no. MB_APPLMODAL, the default, simply disables the owner (also known as parent) window listed in the MessageBox call. But it doesn't disable any other top-level windows (WS_OVERLAPPED and WS_POPUP) owned by your application. If, for example, you had both a main application window and a modeless dialog box open, a MB_APPLMODAL message box would disable only the one listed as its owner. In many cases, they should both be disabled, and MB_TASKMODAL does that. Rather than relying on the owner window parameter, it simply disables all top-level applications owned by your task, and then reenables them when the MessageBox call exits. It also notes any windows that you may have already disabled, so that it doesn't inadvertently enable them when it exits. They remain disabled as they should. It's a shame this feature is only available for message boxes. Dialog boxes really need it, and for that matter ordinary windows do as well (for example, it would be useful in conjunction with a window that looks and acts like a modal dialog but is created with an explicit CreateWindow call). I've had to implement task-modal dialog boxes myself in my last two applications, and I'm sure other developers have also had to do this. It isn't hard, just one of those nuisances that you aren't even likely to realize you need until you find your modal dialog box only disables one of your top-level windows, not all of them. New Printing Support In the past, there hasn't been any easy way to maintain individual printer settings for different documents. You could call the DeviceMode printer driver function from your application, but that function keeps data in a device-dependent format. (It's usually stored in WIN.INI, but there is no guarantee of this, and exactly what gets stored is different for each printer.) Furthermore, in Windows 2.x, DeviceMode would always bring up the printer settings dialog box, which is not what you want if you just need to switch to a different setting. The ExtDeviceMode function now deals with all this in a more useful manner. The call has options to fetch and set the current printer settings, to change the WIN.INI defaults, and to bring up the printer settings dialog box. Note that the dialog box is optional, allowing you simply to change to a new printer setting saved with a document (or elsewhere). The settings are stored in a DEVMODE structure, which combines device-independent settings with device-dependent ones very cleanly. The first part of the structure contains many common printer settings: paper size and orientation, number of copies, resolution (either a device-independent high/medium/low/draft or DPI), and even input paper bin and duplex printing options. You can manipulate any of these settings in the DEVMODE structure directly. At the end of DEVMODE is a variable length device-dependent section, in which the driver keeps any additional information needed for a particular printer. Naturally, you shouldn't manipulate this section unless you're very cozy with a particular printer driver. There are also two dozen new printer escapes. Most of these are specific to PostScript printers, thus beefing up Windows PostScript support. New escapes make it easier to print Encapsulated PostScript (EPS) files and to manipulate the transformation matrix used by PostScript printers. Windows Flags With all the different CPUs and memory modes Windows 3.0 supports, it would be a disaster if you had to puzzle out the current mode and CPU. Fortunately, a handy function called GetWinFlags returns a set of flags indicating your CPU type, Windows memory configuration, and presence or absence of a math coprocessor (see Figure 17). On the CPU side, flags distinguish among 8088/8086, 80186 (yes, there is such a processor), 80286, 80386, and 80486 processors. Additional flags tell whether Windows is running in real mode, standard mode, or 386 enhanced mode. If you just want to find out if you're in protected mode or not, there's a flag specifically for that. In real mode, there are additional flags identifying small- and large-frame EMS. Most Windows code doesn't need to know what CPU or what mode it is running in, but when you do need to know, GetWinFlags is the ticket. In assembly language, you can directly import the absolute symbol __WINFLAGS like this: externA __WINFLAGS globalD flMyWinFlags, __WINFLAGS Files and Disks In previous versions of Windows, there was a set of undocumented file I/O functions that everybody who read Charles Petzold's Programming Windows (Microsoft Press, 1988) knew about: _lopen, _lcreat, _llseek, _lread, _lwrite, and _lclose. These functions still exist, and they're now documented and supported. They are nothing fancy, just direct calls to the equivalent INT 21H DOS services. OpenFile sports a few new options to support file sharing. It allows all the sharing modes that SHARE.EXE supports: compatibility, deny none, deny read, deny write, and exclusive. If you run out of file handles, you can now get more by calling SetHandleCount. Although OpenFile looks for files in the Windows and Windows system directories automatically, you may need to know those directory names, and a GetWindowsDirectory or GetSystemDirectory call will do the trick. Strings Attached As with the file functions, the "undocumented" string functions lstrcat, lstrcpy, and lstrlen are now supported and documented. One string function, lstrcmp, still exists in name, but has a new meaning in 3.0. In Windows 2.1, lstrcmp was not case-sensitive, unlike the C run-time library's case-sensitive strcmp function. In Windows 3.0, both kinds are available and the names match the C style: lstrcmp is now case-sensitive and lstrcmpi is not case-sensitive. Both these functions were rewritten to work better with international versions of Windows and to be faster. The C string compare functions have the advantage of not having to worry about different character sets, so they are faster than the Windows versions, but they don't handle international character sets at all. Because of this, it's better to use the Windows functions. Since C's character classification functions don't work with international character sets either, Windows now has its own: IsCharAlpha, IsCharAlphaNumeric, IsCharLower, and IsCharUpper. Several string conversion functions now have variants that take a character array of a specified length instead of a zero-terminated string. The new ones are AnsiLowerBuff, AnsiToOemBuff, AnsiUpperBuff, and OemToAnsiBuff. Another new function, ToAscii, converts a virtual key code into an ANSI character code. Besides the virtual key code, you have to provide the scan code and a key state table as returned by GetKeyboardState-in other words, the same information Windows and the keyboard driver normally have on hand when they do this translation inside TranslateMessage to generate WM_CHAR messages. The best new string functions are wsprintf and wvsprintf, which are replacements for C's sprintf and vsprintf. True, you can call sprintf and vsprintf from a Windows application, but they add bulk to your code, and don't work at all from DLLs. The Windows equivalents don't have these disadvantages, and are therefore very helpful for DLL developers. They don't support all the format specifiers from sprintf-floating point and pointer types are missing-but they do include %c, %d, %u, %x (and %X) in both short and long, together with the , #, 0, width, and precision prefixes. Of course %s is supported, but it expects a far string pointer regardless of your memory model, and there are no %F or %N prefixes to modify this. Just make sure to cast any near string pointers to (LPSTR). The only Windows functions that the C compiler won't let you do this with are wsprintf and wvsprintf, because there are no prototypes for these particular parameters. (The lpOutput and lpFormat parameters do receive the normal near-to-far casting, so it's best not to cast them. That way the C compiler can do a better job of diagnosing incorrect parameters.) As for using wvsprintf, once you see one example you'll discover that this is the function you've been looking for for years. Many Windows programs include code something like this to display formatted text: wsprintf( szBuf, "A string: %s", sz ); TextOut( hdc, x1, y1, szBuf, strlen(szBuf) ); wsprintf( szBuf, "Two numbers: %d %d", i, j ); TextOut( hdc, x2, y2, szBuf, strlen(szBuf) ); That's clumsy. It would be a lot nicer if you could just write something like this: TextOutF( hdc, x1, y1, "A string: %s", sz ); TextOutF( hdc, x2, y2, "Two numbers: %d %d", i, j ); But of course you can't do that. How can you get those varying parameters at the end into a wsprintf call? It just can't be done. But that's exactly what wvsprintf (and vsprintf for normal C programs) is for-a building block for writing your own printf-style functions. The TextOutF function can be easily coded thus: // Formatted printf-style TextOut BOOL _far _cdecl TextOutF( HDC hdc, int x, int y, LPSTR lpszFormat, ... ) { char szText[256]; int cbText; cbText = wvsprintf( szText, lpszFormat, (LPSTR)lpszFormat + sizeof(lpszFormat) ); return TextOut( hdc, x, y, szText, cbText ); } As you can see, calling this one function is better than having to call both wsprintf and TextOut each time. Two caveats on this sample code: it assumes that the 256- character szText buffer is long enough, and it does not use the recommended va_start macro to access the varying parameters at the end. I didn't use va_start because that function is model-dependent and I wanted this code to be model-independent. It's hard to avoid making assumptions about the buffer size, but a good safeguard is always to use explicit precision specifications on strings in your format. For example, the format "A string: %.80s" would never overrun the buffer no matter how long the string is. Process and Memory Support In the past, Windows did not provide a straightforward way for one program to launch another. The DOS INT 21H/4BH Exec service has been used for this, but it's not really documented. Now, there's a convenient WinExec function. Just pass it a command-line argument and an nCmdShow parameter (which is passed to the target application's WinMain) and off you go. Another option is to call LoadModule. This function takes the same parameters as the Exec call in DOS-pointers to the filename and the Exec parameter block-so it's more complex to use. However, you can provide an environment string for the target app instead of having it receive a copy of the Windows environment. Version 2.1 had a problem shutting down programs. Applications have always "known" when they were exiting, because they they themselves initiate the shutdown. (WM_QUIT doesn't magically shut down a Windows app. What it does is cause GetMessage to return FALSE, breaking out of a WinMain message loop. WinMain returns to its caller, which is the run-time support code linked into the application. That code, after cleaning up, executes a thoroughly conventional INT 21H/4CH to terminate the application.) DLLs have not had this luxury. They "knew" when they were started because their initialization function was called, but there wasn't any notification of exit. Under 3.0, a new function called WEP (Windows Exit Point) must be defined by each DLL. When Windows unloads the DLL, it calls WEP so the DLL can clean up after itself. GetCurrentPDB is a new function that returns the segment address of the running application's DOS Program Segment Prefix (PSP; which Windows refers to as the Program Data Base). Little known fact: this information has always been available to WinMain, as the segment portion of the lpszCmdLine parameter. When Windows starts an application, it sets up a normal DOS PSP for it in a fixed nonbanked segment, complete with a weirdly formatted command-line tail at offset 80H, so that DOS will do things like maintain file handles for each application. A Windows application actually receives this PSP address in the ES register, just like a DOS application, and the run-time start-up code linked into the application that cleans up the command-line tail, converting it into the lpszCmdLine parameter. (It skips over leading blanks and zeros out the CR character at the end.) This cleanup is done in place, so the segment portion of lpszCmdLine is always the PSP address, and the offset portion is always 81H plus the number of leading blanks in the command tail. Another new function, GetCodeInfo, provides several pieces of interesting information about code segments in currently running programs. You pass it either a function address or a module handle and segment number, and it returns a set of flags describing the segment, together with the segment's location in its executable file and both its length in the file and the memory size that should be allocated for it. Combined with GetModuleFileName, this gives you enough information to load a code segment from a Windows executable in some unusual way should you need to. That's assuming you don't mind doing all your own relocation fix ups on the segment, of course. CodeView for Windows Windows in real mode has been at best a badly cramped programming environment. The worst crowding occurred when you tried to squeeze in a debugger with Windows and an application. SYMDEB with its symbol tables took up more than enough space; Microsoft CodeView for Windows debugger (hereafter CVW) was even worse. CVW tried bravely to squeeze itself into memory and keep as much in EMS as possible, but as anyone who tried to use it with any large application knows, it just didn't cut it. You had to turn off symbols for much of the application to get it to fit at all and to avoid the dreaded "CVW Crawl." MagicCVW by NuMega did a good job of making Windows 2.1 CVW more usable, but now CVW itself runs in protected mode, which takes care of the memory problem nicely. With more room, it was time to put some improvements into CVW. If you have Microsoft C Version 6.0, you know what the improvements are, so I won't go into great detail here (CVW is the C 6.0 CodeView with Windows extensions). If you didn't like the old CVW, this one will change your mind. If you did like the old one, you'll be thrilled. The new CVW is infinitely faster. I am not exaggerating. When I ran a large program in the old CVW, if I threw in a couple of Watch variables it could approach a speed of 1/c. The new CVW is so much faster, it can display a Locals window showing all the parameters and local variables of the current function. You can add Watch expressions and as many as two source windows and two memory display windows, all updating live, and the thing still doesn't slow down. It's not only faster, it's a more usable and powerful debugger all around. CVW now starts up the right way. Instead of running CVW from DOS and having it start Windows, it works the other way around. CVW is now a real Windows application; you run it under Windows and it starts up and takes control of your application. Finally, you don't have to restart Windows each time you want to use CodeView! The Windows extensions to CVW include (lh) and (gh) type casts, which dereference local and global memory handles into pointers, along with several Windows-specific commands. The most interesting are wwm (Windows Watch Message) and wbm (Windows Break Message). These commands set watchpoints and breakpoints, not just at particular locations, but on specific messages or classes of messages. This really helps with debugging window functions. There are other commands to display the local and global heaps and the list of modules, and to terminate the application being debugged. One caveat: you can no longer run CVW on a serial debugging terminal. It must be on a second display on the same computer running Windows. For a classic bus machine, the usual setup is to run Windows on a color display and CVW on an auxiliary monochrome card and monitor. It's tough to find mono cards for a PS/2, so there the hot setup is to run Windows on an 8514/a and CVW on the VGA. Then CVW runs in color 50 line mode, which is handy. Another Debugger SYMDEB is still supported for real-mode debugging, and it works as well-ahem-as it always did. There's even a SYMDEB-style debugger for protected mode called WDEB386. Yes, you restart Windows each time, it's purely a command-line system, and it doesn't have any source code access at all. You'd use it if you didn't have a second display for CVW, or if you were debugging certain types of device drivers. Printer drivers are easily debugged under CVW, because they are just demand-loaded DLLs, but to debug any of the drivers that load during start-up, you have to use WDEB386. CVW won't work because it can't start until after all the start-up drivers are loaded. WDEB386 also has a few little things it will do that CVW won't, so it's worth keeping. But CVW is clearly the debugger of choice; it really makes debugging Windows applications easier. Profiler Previous versions of the SDK did not provide much in the way of performance measurement tools. Duncan Booth, a Windows programmer in England, helped fill the gap with a profiler called WinET (Windows Execution Timer). WinET uses a simple but effective technique: it hooks into a timer interrupt and records the current code segment (CS) and instruction pointer (IP) on timer ticks. After you leave Windows, it matches these up against specified SYM files and prints a report showing where the time was spent. The Windows 3.0 SDK includes a profiler that works on the same principle as WinET, and it's better and easier to use. It supports real mode and 386 enhanced mode (but not standard mode), so you can profile any kind of Windows application. It's better to use 386 enhanced mode if you can, so that the memory the profiler consumes won't get in the way of your application. To use the profiler, add ProfStart and ProfStop calls to your code, bracketing the sections of code you want profiled. (You wouldn't want the profiler on all the time-for example, you generally wouldn't want to leave it on across a GetMessage or PeekMessage call because other applications would then be running instead of yours.) To start Windows with the profiler installed, either add a driver line to SYSTEM.INI in 386 mode or start PROF.COM in real mode. When you run your program, the profiler samples the instruction pointer periodically when it is activated with ProfStart; when you shut down Windows or call ProfFinish, it writes this information out to a disk file. Then, just run SHOWHITS with the correct options and it will report where the CPU time was spent. SHOWHITS gets its name because it shows how often the instruction pointer "hit" each part of the code. The most frequently executed code is listed in a portion of the report appropriately titled "Top 10 Hits." Swap Analyzer The profiler gives one view of your program's execution; it's also useful to know something about calls across segment boundaries. In real mode, you want to find out which code segments are being swapped too often. While not often a problem in protected mode, too many far calls are, because they're very expensive compared to near calls. Since you'll want to know what far calls are being made, use the swap kernel, SKERNEL.EXE. Strangely, the swap kernel runs only in real mode, because it's a special version of the real-mode kernel that puts extra debugging code into the reload thunks it generates. Since there are no thunks in protected mode, this trick doesn't work there. To use swap kernel, put SwapRecording calls in your code to start and stop swap recording; after you exit Windows, run the Swap utility to view the results. An option in SwapRecording lets you specify the events to record: segment loads and discards only, or all far calls. Other Tools HeapWalker naturally had to be updated to work in protected mode, but real mode wasn't forgotten. Under large-frame EMS in Version 2.1, HeapWalker was only able to look at its own EMS bank. Other applications were essentially invisible because their code and data were in other banks. However, in Windows 3.0 real mode, a new EMSWalk menu now lists the other running applications so that you can view any of their EMS banks. The Dialog Editor can now add new control classes and support them as fully as the built-in classes. If you write a DLL that registers a control class and provides a set of entry points and messages that is defined in the SDK, Dialog Editor can load it, add that control to its palette, and let you put the control in dialog boxes.(For more information on control classes, see "Extending the Windows[tm] 3.0 Interface with Installable Custom Controls," p. 29-Ed.) SDKPaint improves upon the old Icon Editor by providing more drawing tools and support for color images as well as multiple-resolution cursors and icons (see Figure 18). As with Paintbrush, I'd like to see more flexible zooming. Optional grid lines would be a help too, but SDKPaint is still pretty good as is. Don't overlook the new Recorder that is included with Windows 3.0 as a development tool. Simply by recording and playing back a macro, you can run your application and make sure that you don't break things that once worked. The most fun of all the SDK tools has to be ZoomIn. Play with it for a couple of minutes and if you don't chuckle, see a doctor immediately. It has a practical use, too, but that is secondary to the amusement factor (see Figure 19). New Libraries There were several problems with the LIB files shipped in previous SDKs. Library functions were not compiled with the Windows prolog/epilog they needed and it was unclear which functions chould be used in DLLs or applications. The Windows import library ?LIBW.LIB came in different versions for each memory model-even though most of this library was common to all models. And just to keep things interesting, C 5.1 and the Windows SDK each offered several different naming conventions for the libraries. It sounded convenient-you could have either DOS, OS/2, or Windows libraries as the default-but it meant no one's MAKE files would be compatible with anyone else's. C 6.0 and the Windows 3.0 SDK have standard names for all libraries, with an R suffix for DOS, P for OS/2, and W for Windows. You have to use explicit library names when linking files for Windows, but since you pretty much need to use a MAKE file anyway it's no big deal. Besides, anyone else will then be able to build your program without fiddling with the MAKE file. There is a single import library, LIBW.LIB. The small amount of model-specific code that was in the ?LIBW.LIB files is now in the ?LIBC?W.LIB files instead. A separate set of files for DLLs named ?DLLC?W.LIB contain only the C run-time routines that are safe to use in DLLs. And for applications and DLLs that don't use the C run-time library, there are new libraries that omit the routines but have the other start-up code needed. Header Files WINDOWS.H has been reorganized. It's in a more logical order, but more importantly, numerous coding errors are fixed. Functions that had been declared with only () for the argument list are declared as void, to avoid compiler warnings. Some functions with no return value had been erroneously declared to return int; these now have void returns. Far pointers are used in macros where needed, for example in MAKEPOINT. This macro used to work only with data in the default data segment in the small or medium model; now it works wherever the data is. And assembly-language programmers are no longer neglected stepchildren-WINDOWS.INC is up to date and complete. DPMI Most of protected mode's restrictions, such as segment limit checking, help more than they hurt. But one restriction can be onerous: you can't easily communicate between protected mode and real mode. If you have a DOS TSR that your application communicates with through a software interrupt or maybe a shared memory buffer, it's not going to work in protected mode. You can't use a real-mode address in protected mode and expect it to point to the same thing. The segment portion of the address is interpreted differently in the two modes. And you can't call just any real-mode interrupt from Windows protected mode. INT 21H calls and a few others such as NETBIOS calls are all right, but only because Windows handles them specially. What's needed is some kind of interfacing service to hook up protected-mode and real-mode programs. The DOS Protected Mode Interface (DPMI) provides this sevice. DPMI is a set of protected-mode functions that allocate real-mode memory, simulate real-mode interrupts and function calls, intercept real-mode interrupt vectors, play games with selectors, and more. Using these calls, you can communicate with TSRs from protected mode just as you would in real mode. It's pretty straightforward, if your code is organized so that the actual communication with the TSR is done in just a few places. If various kinds of direct interfaces with the TSR are interspersed through your code, it will be tougher. (For more information about DPMI, see "Supporting Protected-Mode Applications in a DOS-based Environment," p. 92-Ed.) DPMI calls use an INT 31H interrupt interface, not a Windows-style dynamic-link interface. You load registers and call INT 31H, just as you would make a BIOS or DOS call. DPMI works this way because its real purpose is not to provide an interface between Windows applications and real mode (that's a side benefit) but to give DOS extenders a way to work cleanly under Windows. DOS extenders, such as DOS16M by Rational Systems, let DOS applications run in protected mode to access extended memory. They switch as needed to protected mode for application code and real mode for DOS and other real-mode calls. Rational Systems and others were able to make this work under Windows/286 and DOS by switching modes on their own. Windows/286 did not know it was happening, because the DOS extender would always go back to real mode before returning control to Windows. This didn't work in Windows/386, where the non-Windows application using the DOS extender was actually running in a virtual machine, so the usual tricks didn't work. Therefore, the INT 31H DPMI interface was implemented to provide the same services that DOS extenders must use internally to run their applications. The DOS extender can determine that it is running under a DPMI system-protected-mode Windows, or eventually other systems such as OS/2 and UNIX/XENIX-and make INT 31H DPMI calls instead of pulling its own mode switching and selector translation tricks. This is a real boon to users. To cite the classic case, Lotus 1-2-3 Version 3.1, which uses Rational Systems' latest DPMI-compatible DOS16M, can fully exploit Windows 3.0 extended memory in any memory mode. It's a bonus that Windows programmers can use INT 31H to solve the problem of communicating with real mode. There is a little support for real-mode interfacing in Windows proper. You can allocate a block of memory in real-mode address space by calling GlobalDosAlloc and free it with GlobalDosFree. GlobalDosAlloc returns both a real-mode segment value and a Windows selector. This "selector" is equal to the segment value if Windows is running in real mode; in protected mode the two values are different. In either case, your Windows code can use the "selector" to address the memory, passing the segment value to any real-mode code. That brings us back to the problem of how to call real-mode code in the first place, pointing back toward the DPMI approach, since there are functions to simulate a real-mode interrupt or function call. Even if you're not communicating with a TSR, you may need to break some other protected-mode rules. Normally, the 286/386/486 hardware prevents you from executing instructions from a data segment, and you can't write into a code segment. (There is another level of protection available, a code segment that you can't even read from. That's not used often because it is common to read from a code segment, especially in compiled C code.) This is a big problem in those situations where you literally do need to write your code or run your data. Any kind of interactive compiler or code generator would have to do this. Amazingly enough, even the Windows display drivers use self-generating code techniques. When the display driver receives a BitBlt (bit block transfer) call, it compiles a small subroutine on the stack, containing the customized loops that make up that particular BitBlt operation. Then it calls that subroutine to perform the actual transfer. Windows makes it easy to "cheat" and accomplish these things. All you have to do is create a code selector and a data selector that point to the same physical memory. Then you can do anything you could do in real mode; you just have to remember to use the code selector when you're executing machine instructions and the data selector when you're writing to memory. You can always read from memory with either selector. To get an executable code selector for a data segment, call AllocDStoCSAlias. Or you can create a selector that isn't associated with any memory by calling AllocSelector, then calling ChangeSelector any number of times to make this selector an alias for various other segments. Either way, when you are finished with the "alias" selector, call FreeSelector. One thing to watch out for : if Windows has to move the segment in question (remember, segments still can move around in the linear address space) the alias selector won't get updated and it will point nowhere. To be safe, lock down the segment in linear memory by calling GlobalFix before aliasing it, and call GlobalUnFix when you're done. Also, ChangeSelector was inadvertently omitted from the SDK import libraries, so if you use it you need to explicitly import it in your DEF file: IMPORTS CHANGESELECTOR = KERNEL.177 You may discover that you can use ChangeSelector to create a data alias for a code segment as well as a code alias for a data segment. Although this works, it isn't recommended because it won't be supported in future environments such as OS/2 2.0. Creating a code alias for a data segment is the preferred method and should be compatible with future products. Besides, you wouldn't want to receive a Programmer General's Warning: Cheating with selectors can be hazardous to your code. If you have to talk with a hardware device, you should get or write a Windows DLL that supports the device instead of using DPMI to call a real-mode TSR. As for this business about writing into code segments or running code in data segments, you saw the warnings in the SDK: "Use of these functions violates preferred Windows programming practices." You probably also use GOTOs and write self-modifying Assembler code and break the 55 MPH speed limit too. Bad Programmer! Conclusion One of the running jokes about Windows has always been that it was fine for users, but you couldn't develop Windows applications inside Windows. Even under Windows/386, editing and compiling within Windows just wasn't worth the trouble. The 386 mode made the compiles too slow, and you had to restart Windows anyway if you wanted to use CodeView. It's a different story now. As I write this using a Windows-based word processor, I have a DOS window compiling a test program I'm working on, with my source code loaded into BRIEF in another window, while another DOS window copies some files to diskette, and in the background FAXit sends a fax I created as another word processor document. I'm finally getting my money's worth out of this 386 system! I don't have to shell out of BRIEF to run DOS commands; all I have to do is switch to a DOS window. Debugging is easy now that CodeView runs inside Windows. The entire Windows SDK reference is now a Windows Help document, making it very convenient to develop inside Windows. The whole thing is cross-referenced so you can click your way through any topic. It's really nice. If you're looking at the description of BeginPaint and want to see what the fields in the PAINTSTRUCT are called, you don't have to go flipping to the back of the book. Just click on PAINTSTRUCT on the screen and you're there. Another click and you're back to reading about BeginPaint. You won't want to develop outside Windows after using this. I can't believe I did this, but I just added a WIN line to the end of my AUTOEXEC.BAT file! I tried booting up a couple of times and I don't miss that old DOS prompt. No, I take that back. I use the DOS prompt all the time-it's still the easiest way to do many things. But it's even handier to have as many DOS prompts as I want, along with the SDK help and all the rest. I guess I'm just going to have to get used to being a Windows user. WIN.INI Color Settings The predefined Windows 3.0 color schemes are all too bright and have too many contrasts for my taste-they look like they were designed for snappy screen shots instead of daily use. With the high-intensity white window backgrounds, they're much brighter than typical DOS applications. I looked through Control Panel's predefined color schemes to see if any were easier on the eye. No, they all use the same bright white window background, so I used the Color Palette option to change the window background to the new light gray color. But disabled (grayed-out) check boxes and radio buttons also use the light gray color instead of a dithered gray, so they're invisible against the light gray window background. I discovered that Control Panel doesn't let you set this color nor several other system colors, including the highlight (selection) text and background colors. You have to edit WIN.INI to set any of these. (Also, since many Windows 3.0 controls are painted with pairs of fixed bitmaps representing the pushed and unpushed states, playing with the ButtonShadow and ButtonFace values has no effect on most three-dimensional controls.) It's also no longer correct to invert colors to indicate highlighting; you are supposed to use the highlight text and background colors instead. Here's the color scheme I came up with after much experimenting. I think its low contrast is easier to look at than any of the standard schemes. Try pasting the following code into your WIN.INI. It may look rather dim at first, but I'll bet that's because you have been turning down the brightness on your display to compensate for the overly bright standard Windows colors. Set the brightness to the normal level that you use with DOS applications and this should be just about right. [Colors] ; Ones that Control Panel knows about ActiveBorder= 128 255 255 ActiveTitle= 0 128 64 AppWorkspace= 64 128 128 Background= 128 128 128 InactiveBorder= 192 192 192 InactiveTitle= 128 128 128 Menu= 0 128 128 MenuText= 0 0 0 Scrollbar= 191 191 191 TitleText= 255 255 255 Window= 192 192 192 WindowFrame= 0 0 0 WindowText= 0 0 0 ; New ones that Control Panel knows not ;ButtonFace= ;ButtonShadow= ButtonText= 0 0 0 GrayText= 128 128 128 Hilight= 128 0 0 HilightText= 255 255 0 The following is a similar scheme that I designed to help test for painting bugs. It uses nonstandard colors for most elements and avoids colors that are complements of each other, to try to aggravate any misuse of colors and make problems obvious. [Colors] ; Ones that Control Panel knows about ActiveBorder= 128 255 255 ActiveTitle= 0 128 64 AppWorkspace= 64 128 128 Background= 128 128 128 InactiveBorder= 192 192 192 InactiveTitle= 128 128 128 Menu= 0 128 128 MenuText= 0 0 0 Scrollbar= 129 129 129 TitleText= 0 0 0 Window= 192 192 192 WindowFrame= 0 0 128 WindowText= 128 0 0 ; New ones that Control Panel knows not ;ButtonFace= ;ButtonShadow= ButtonText= 0 0 0 GrayText= 128 128 128 Hilight= 0 0 255 HilightText= 255 255 0 Figure 17 ■ Syntax: DWORD GetWinFlags() ■ This function returns a 32-bit value containing flags that specify the memory configuration under which Windows is running. ■ This function has no parameters. ■ The return value contains flags specifying the current memory configuration. These flags may be any of the following values: ■ ■ Value Meaning ■ WF_CPU086 System CPU is an 8086. ■ WF_CPU186 System CPU is an 80186. ■ WF_CPU286 System CPU is an 80286. ■ WF_CPU386 System CPU is an 80386. ■ WF_CPU486 System CPU is an 80486. ■ WF_ENHANCED Windows is running in 386 enhanced mode. The WF_PMODE flag is always set when WF_ENHANCED is set. ■ WF_LARGEFRAME Windows is running in EMS large-frame memory configuration ■ WF_PMODE Windows is running in protected-mode. This flag is always set when either WF_ENHANCED or WF_STANDARD is set. ■ WF_SMALLFRAME Windows is running in EMS small-frame memory configuration ■ WF_STANDARD Windows is running in standard mode. The WF_PMODE flag is always set when WF_STANDARD is set. ■ ■ If WF_PMODE is not set, Windows is running in real mode. ■ ■ Extending the Windows 3.0 Interface with Installable Custom Controls Kevin P. Welch The ability to create and formally install custom controls is a feature of the Microsoft Windows[tm] graphical environment Version 3.0 that will be of great interest to software developers. This ability permits the developer to extend the user interface of Windows1 and encourages the creation of small, self-contained, reusable software objects that can be shared between applications. This article describes the design and definition of new control classes, discusses the new interface supported by Windows 3.0 for such installable controls, and demonstrates the use of the interface with a sample Page selection control. The sample control can be used with the new dialog editor supplied with the Windows Version 3.0 Software Development Kit (SDK). A few fairly simple steps greatly simplify the process of designing a new installable control class. While these guidelines are helpful when creating any new window class, they are particularly applicable when designing an installable control. The first step is to name the control. As with any other window, each control must be associated with a class name. This class name is used in your RC file whenever you wish to use the control explicitly in a dialog box. When naming the control, make sure not to use one of the public class names defined by Windows or other applications. If you do, the registration process for your control class will not succeed. Second, you must design the user interface. This is usually the most difficult aspect of creating a control. The interface you design for your control determines to a large degree how difficult the control will be for you to implement, and how the end user will perceive it. When you design a new control, you are extending the functionality of the Windows user interface. Your new control should look and act similarly to those already provided by Windows. For example, you should be consistent with the commonly used mechanisms to indicate selection and keyboard input focus. You should also avoid device dependencies when you design your control. As you know, Windows is device-independent so it can run on a wide variety of display subsystems, with varying screen-aspect ratios, pixel densities, and color capabilities. For instance, you probably shouldn't put fixed-size bitmaps in the control interface. Since aspect ratios and pixel densities vary widely, the bitmap will probably be scaled or distorted, resulting in a less-than-optimal interface. Another issue to consider is the use of colors. Predefined system colors work best. Don't assume, for example, that the window background color will always be white and the window frame black. Although it takes a little extra work to incorporate such flexibility into your control, it is almost always necessary when producing controls for commercial use. Finally, if you are creating an active control, be prepared to support some mechanism to define the input focus. The third step is to define the interface. Almost all controls interact with other windows in some way or another. Although this interaction can occur using any of the mechanisms supported by Windows, almost all the controls predefined by Windows (except the scroll bar) use a completely message-based interface. Therefore, you must decide which messages your control will work with. You should also design the style flags your control will support. Whenever a new instance of your control is created, these style flags are combined with flags that are predefined by Windows and are passed to the system as a parameter in the CreateWindow function. After you have designed the control interface, you should create a public header file defining each supported style, command, and notification message. This header file can then be used by any other module that uses the control. Fourth, define the instance variables. By their nature, controls must be completely reentrant and totally modular. As a result, the data structures used by your control must be dynamically allocated and readily accessible by each instance. Although you could create your own scheme for defining instance variables, in most cases you should consider allocating extra class or window bytes when the class is registered, or perhaps use property lists or atoms. The fifth step is to define the window function. Each control must be associated with a window function that processes all related messages. By definition, controls must be able to work with the Windows dialog box manager. Consequently, your window function should handle at least the following messages: Message Window Function Response WM_CREATE Initialize any instance variables and perform any other necessary steps to use the control. WM_DESTROY Release all the resources associated with the control instance, including the removal of property lists and deletion of any atoms. WM_PAINT Update the client region of the control. Carefully tune the update operations for a variety of display devices, paying special attention to the quality of the result. This is often one of the most tedious aspects of the entire control development process. If your control is to be capable of receiving input from both the keyboard and the mouse, your window function should also be able to handle the following messages: Message Window Function Response WM_GETDLGCODE This message is sent to your control by the dialog box manager. The response to this message determines how the control will be managed inside a dialog box. By responding with one of the following return codes, your control can manage a particular type of input and process the information itself: DLGC_WANTALLKEYS Intercept all keyboard input DLGC_WANTARROWS Intercept cursor keys DLGC_WANTCHARS Intercept WM_CHAR messages DLGC_WANTMESSAGE Intercept all messages DLGC_WANTTAB Intercept the TAB key WM_KEYDOWN Received when a key is held down. By processing the virtual key codes within this message, you can perform any necessary action. When an action is taken that changes the state of the control, be sure to provide appropriate notification codes to the parent window. WM_KILLFOCUS Received when the input focus changes to another window. If the control displays some sort of highlight, it should be destroyed at this time. WM_LBUTTONDOWN Received when the user presses the left mouse button inside the control client area. You will want to capture the system input focus, display your highlight, and start capturing mouse movements. If the mouse click results in a new selection, you may also want to notify the parent window of the change. WM_LBUTTONUP Received when the user releases the left mouse button. If you are capturing mouse movements, you should be careful to release the mouse capture at this time. Note that it is entirely possible to receive extra button-down and button-up messages. If you want to animate a drag operation you will need to capture the mouse and process each of the WM_MOUSEMOVE messages. WM_SETFOCUS When this message is received, a highlight should indicate that the control has the system input focus. In addition to these messages, your window function should also be capable of processing the command messages you define as part of your message-based interface. Note that you should be careful to return the appropriate response for each message received, especially when an internal error occurs. The next step in the process is to define related utility functions. The implementation of even a simple control often requires the definition of several related utility functions that perform often-repeated processes. As with any Windows program, you should structure and organize these functions carefully, preferably placing them in logical groups using separate, discardable code segments. Finally, you must physically create the control modules. Depending on your needs, you can either link each control object module directly into your application or combine them into a library and let the linker extract the required components. Be aware that if your control uses any resources, you must also incorporate them into your application. You might also want to modularize your control completely (with all associated resources) into a self-contained dynamic-link library (DLL). This extra degree of encapsulation can be very beneficial, enabling you to use the control simultaneously in several applications. You will also be able to use it with the new Windows 3.0 Dialog Editor by adhering to a few simple standards. Creating Installable Control Libraries When creating an installable control you must follow the procedures just outlined, plus a few additional steps that permit the control to exist in a DLL. DLLs form the foundation upon which Windows is built. Unfortunately, DLLs impose certain programming restrictions. Perhaps the most significant is the SS != DS issue. Unlike conventional Windows programs, DLLs have no stack segment and only one data segment. Consequently, when an application calls a function within a DLL, it has to use its own stack segment and the library's data segment. The result is that many of the standard C run-time library functions cannot be used within a DLL. Library Module Definition File To create an installable control, first define a DLL containing a series of exported functions that are associated with a series of predefined ordinal values. Create the DLL using the following module definition file, where Xxxx represents the class name of the control: LIBRARY Xxxx DESCRIPTION Xxxx Control Library' EXETYPE WINDOWS STUB WINSTUB.EXE' CODE MOVEABLE DISCARDABLE SHARED PRELOAD DATA MOVEABLE SINGLE PRELOAD HEAPSIZE 1024 EXPORTS WEP @1 RESIDENTNAME XxxxInfo() @2 XxxxStyle() @3 XxxxFlags() @4 XxxxWndFn() @5 XxxxDlgFn() @6 You should always use the same predefined ordinal values for each of these exported functions. The XxxxDlgFn function associated with the XxxxStyle function must also be exported since it is used directly by Windows. Library Entry Points In addition to the module definition file, you will need to create several different source code modules, each containing one or more of the exported functions discussed below. These functions must be present for the Windows 3.0 Dialog Editor to use the control. The next section describes each of these required functions in detail. Note that all the required data structures are defined in CONTROL.H (see Figure 1), one of the files used to build the sample control discussed later in this article. BOOL FAR PASCAL XxxxInit( hInstance ) The XxxxInit function is responsible for all the initialization needed to use the DLL. This function is normally called by the assembly language entry point to the library. After saving the library instance handle using a global static variable, this function should register the control class and initialize the local heap (if necessary) by calling LocalInit. Parameter Type Description hInstance HANDLE Library instance handle The return value indicates the success or failure of the library initialization. A return value of FALSE indicates that the initialization process has failed; TRUE indicates that the control class has been registered and initialization has succeeded. HANDLE FAR PASCAL XxxxInfo() The XxxxInfo function provides the calling application with information about the control library. Based on the information returned, the application can create instances of the control using one of the supported styles. If the control was used with the Windows Dialog Editor, for example, this function could be used to query the library about the different control styles it can support. The return value is a handle to a CTLINFO data structure. This information becomes the property of the caller and must be explicitly released using the GlobalFree function when it is no longer needed. If there is insufficient memory available to define this structure, a NULL handle is returned. The CTLINFO structure, defined in CONTROL.H (see Figure 1), specifies the class name and version number and lists each of the supported control styles with suggested size information and a short description. /* general style & size definitions */ #define CT_STD 0x0000 #define CTLTYPES 12 #define CTLDESCR 22 #define CTLCLASS 20 #define CTLTITLE 94 typedef struct { WORD wType; WORD wWidth; WORD wHeight; DWORD dwStyle; char szDescr[CTLDESCR]; } CTLTYPE; typedef struct { WORD wVersion; WORD wCtlTypes; char szClass[CTLCLASS]; char szTitle[CTLTITLE]; char szReserved[10]; CTLTYPE Type[CTLTYPES]; } CTLINFO; typedef CTLINFO * PCTLINFO; typedef CTLINFO FAR * LPCTLINFO; The CTLTYPE structure has the following fields: Field Description wType Defines the type of control. While only the CT_STD option is currently supported, other types may be defined in the future. wWidth Defines the suggested width of the control. If the most significant bit is zero, the remaining bits specify the default width in RC coordinates; otherwise, the remaining bits specify the default width in pixels. One horizontal RC unit is the width of the system font divided by four. wHeight Defines the suggested height of the control. If the most significant bit is zero, the remaining bits specify the default height in RC coordinates; otherwise, the remaining bits specify the default height in pixels. One vertical RC unit is the height of the system font divided by eight. dwStyle Specifies the initial style flags used to obtain this particular control variant. This value includes both the control-defined style flags in the low-order word and the Windows-defined flags in the high-order word. szDescr Defines the name to be used by other applications when referencing this particular variant of the base control class. The CTLINFO structure contains the following fields: Field Description wVersion Specifies the control version number. Although you can start your numbering scheme from 1, most implementations use the lower two digits to represent minor releases. Using this scheme, the first version would be numbered 100. wCtlTypes Specifies the number of control types supported by this class. This value should always be greater than zero and less than or equal to CTLTYPES. szClass Specifies with a NULL-terminated string the control class name supported by the DLL. szTitle Specifies with a NULL-terminated string copyright or author information relating to the control library. Type Specifies an array of CTLTYPE data structures containing information relating to each of the control types supported by the class. BOOL FAR PASCAL XxxxStyle( hWnd, hCtlStyle, lpfnStrToId, lpfnIdToStr ) The XxxxStyle function enables the calling application to edit the style of a particular control. When this function is called, a modal dialog box should be displayed that permits the user to edit the CTLSTYLE parameters. The display of this dialog box should be consistent with Style dialog boxes provided by existing controls. Parameter Type Description hwnd HWND Handle to parent window hCtlStyle HANDLE Handle to CTLSTYLE data lpfnStrToId LPFNSTRTOID String-to-ID conversion function lpfnIdToStr LPFNIDTOSTR ID-to-string conversion function The XxxxStyle function returns a value of TRUE if the CTLSTYLE structure was changed. Otherwise, it returns FALSE. The CTLSTYLE structure specifies the various attributes of the selected control, including the current style flags, location, dimensions, and associated text. #define CTLCLASS 20 #define CTLTITLE 94 typedef struct { WORD wX; WORD wY; WORD wCx; WORD wCy; WORD wId; DWORD dwStyle; char szClass[CTLCLASS]; char szTitle[CTLTITLE]; } CTLSTYLE; typedef CTLSTYLE * PCTLSTYLE; typedef CTLSTYLE FAR * LPCTLSTYLE; The CTLSTYLE structure contains the following fields: Field Description wX Specifies in screen coordinates the X origin of the control relative to the client region of the parent window. wY Specifies in screen coordinates the Y origin of the control relative to the client region of the parent window. wCx Specifies the current width of the control in screen coordinates. wCy Specifies the current height of the control in screen coordinates. wId Specifies the current ID number of the control. Although you can allow the user to change this value, in most situations changes should be made in conjunction with the string-to-ID and ID-to-string conversion functions provided as parameters (see below). dwStyle Specifies the current style of the control. The low-order word contains the control-specific flags; the high-order word contains the Windows-specific flags. You should let the user change these flags to any values supported by your control library. szClass Specifies a NULL-terminated string representing the name of the current control class. Don't allow the user to edit this field-it is provided for informational purposes only. szTitle Specifies the text associated with the control with a NULL-terminated string. This text is usually displayed inside the control or may be used to store other associated information required by the control. The lpfnStrToId and lpfnIdToStr functions require a little further explanation. The primary purpose of the XxxxStyle function is to display a dialog box through which the user can edit the various parameters associated with the control. As you know, each control is associated with an ID value. This value is used by the parent window or dialog box to keep track of each of its child windows. /* ID to string translation function prototypes */ typedef WORD (FAR PASCAL *LPFNIDTOSTR)( WORD, LPSTR, WORD ); typedef DWORD (FAR PASCAL *LPFNSTRTOID)( LPSTR ); The lpfnIdToStr function, an optional parameter provided by the calling application, enables you to translate the ID from the CTLSTYLE data structure to a text string. This text string can then be displayed in place of the numeric value. The first parameter is the control ID; the second, a long pointer to an empty text string; the third parameter is the maximum length of this string. The value returned by this function is the number of characters copied to the string. If a value of zero is returned, it can be assumed that the function call failed. The lpfnStrToId function operates exactly in reverse. This function takes a string containing an ID value as a parameter and returns a doubleword result. If the LOWORD of the result is nonzero, then the HIWORD contains the new control ID value (which you can use to update the CTLSTYLE structure). If the LOWORD of the result is zero, the ID string was undefined. The calling application (in this case, usually the Dialog Editor) keeps track of the ID strings and their corresponding values. The control style function accesses this information using the lpfnStrToId and lpfnIdToStr functions. The calling application can produce a header file containing these ID strings and values for use by the application under development. WORD FAR PASCAL XxxxFlags( wStyle, lpszStyle, wMaxString ) The XxxxFlags function translates the control style flags provided into a corresponding text string for output to a text or RC file. Note that you should return the same control style definitions specified in the #include file that accompanies your control library. Parameter Type Description wStyle WORD Control style lpszStyle LPSTR Style flags string wMaxString WORD Maximum string length The return value is the length of the resulting lpszStyle string. If an error occurs, zero is returned. LONG FAR PASCAL XxxxWndFn( hwnd, wMsg, wParam, lParam ) The XxxxWndFn function is the conventional window procedure responsible for processing all the messages sent to the control. The messages that this function should be capable of handling were described earlier. Parameter Type Description hwnd HWND Handle to the control window wMsg WORD Message identifier wParam WORD Additional message data lParam LONG Additional message data The return value indicates the result of message processing. The possible return values depend on the actual message sent. BOOL FAR PASCAL XxxxDlgFn( hDlg, wMsg, wParam, lParam ) The XxxxDlgFn function is a conventional window procedure that processes all the messages sent to the control style dialog box. This dialog box should display the control ID, title, and other related information. The user should be able to define different control styles and change the associated ID and title with this dialog box. Parameter Type Description hDlg HWND Handle to the style dialog box wMsg WORD Message identifier wParam WORD Additional message data lParam LONG Additional message data The return value indicates the result of message processing. If a nonzero value is returned, the dialog box manager will assume that the dialog function has processed the message. If zero is returned, the dialog box manager will process the message. Building the Control Library After you have defined the control library components, you can create the installable control much as you would any other Windows application. Make sure you use the appropriate compilation flags, verifying especially that SS != DS. To be consistent, consider structuring the various modules and components of your library in the following manner: Name Description Xxxx Make file Xxxx.H Public header file Xxxx.D Private header file Xxxx.DEF Module definition file Xxxx.RC Resource file Xxxx0.ASM DLL entry module Xxxx1.C Initialization module Xxxx2.C Message processing module Xxxx3.C Style dialog box module Xxxx4.C Information module Xxxx5.C Style flags module Sample Control To put all this in perspective, let's work through a sample control. The Page selection control is a relatively simple control that facilitates the selection of a numbered page within a small range of pages. You can select a particular page by clicking the mouse inside one of the control's page symbols or by using the cursor when the control has the keyboard input focus (see Figure 2). You might use this control to support the selection of a particular page in a document, specify a print range, or perhaps enable the user to select a particular page of a multipage data entry form. The library PAGE.DLL completely defines the installable Page control. When an instance is created, the Page control responds to the following set of messages, as defined in the public header file, PAGE.H (see Figure 3). PM_SETRANGE Used to set the number of pages displayed inside the control (numbered from 1 to n). Note that the currently selected page is not reset. The default range is set to the maximum number of pages that can be displayed. Parameter Description wParam The new number of pages lParam (not used) The return value is nonzero if the new range was successfully defined. If an error occurs, zero is returned. PM_GETRANGE Used to retrieve the current number of pages displayed inside the control. Parameter Description wParam (not used) lParam (not used) The return value indicates the number of pages displayed. PM_SETSELECT Used to define the currently selected page. Parameter Description wParam An index for the newly selected page in the range of 0 to n1, where n is the current page range lParam (not used) The return value is the index of the newly selected page; in most cases this is identical to the wParam value. PM_GETSELECT Used to retrieve the index to the currently selected page. Parameter Description wParam (not used) lParam (not used) The return value is the index to the currently selected page (from 0 to n1). PN_SELCHANGE Whenever the user selects a particular page using the control, this notification message is sent to the parent window, indicating that the selected page has changed. Notification messages are used to inform the control's parent window of the control of changes that the parent may be interested in. As with all control notification messages, the selection change notification message is sent to the parent window in the following format: Parameter Description wMsg WM_COMMAND wParam Page control ID lParam Low-order word is the window handle High-order word is the notification code (PN_SELCHANGE) Page Control Components The Page control library is constructed from the following files. File Description Figure Number PAGE Make file 4 PAGE.H Public header file 3 PAGE.D Private header file 5 PAGE.DEF Module definition file 6 PAGE.RC Resource file 7 PAGE0.ASM DLL entry module 8 PAGE1.C Initialization module 9 PAGE2.C Message processing module 10 PAGE3.C Style dialog box module 12 PAGE4.C Information module 13 PAGE5.C Style flags module 14 The make file, PAGE (see Figure 4), is almost identical to that used by any Windows application except that it uses the Aw and D_WINDLL compile flags. These flags instruct the C compiler to generate code in a DLL format with SS != DS. The next file, PAGE.H (see Figure 3), is the public header file associated with the control library. Because this file is designed to be used by applications using the control, it contains only the control commands, notification codes, and function prototypes. The private header file PAGE.D (see Figure 5) is associated with PAGE.H. This file defines the values used throughout the control library that do not need to be made public. For example, the following fragment from PAGE.D defines various constants that are used when registering the Page window class and when returning information about the control: #define PAGE_CLASSEXTRA 0 #define PAGE_WNDEXTRA 18 #define PAGE_VERSION 100 #define PAGE_NAME "Page" #define PAGE_COLOR NULL #define PAGE_WNDSTYLE WS_CHILD|WS_VISIBLE|WS_TABSTOP #define PAGE_CLASSSTYLE CS_HREDRAW|CS_VREDRAW|CS_DBLCLKS|CS_GLOBALCLASS Note the specification of the CS_DBLCLKS and CS_GLOBALCLASS class style flags-they are extremely important. If you don't specify CS_DBLCLKS, you will not be able to bring up the control style dialog box inside the dialog editor by double-clicking inside the control. The CS_GLOBALCLASS flag is also necessary, as the control class must be made publicly available. PAGE.DEF (see Figure 6), the module definition file, is almost identical to that described previously in the article, except that obviously Xxxx is replaced with the class name Page. The library resource file, PAGE.RC (see Figure 7), defines the PageStyle dialog box used by the PageDlgFn function. This dialog box displays the information passed to the PageStyle function, allowing the user to define the control identifier when used in conjunction with the dialog editor. PAGE0.ASM (see Figure 8) is the assembly language entry point to the DLL. Remember that in order to create DLLs, an assembly language entry point must call the control initialization function PageInit whenever the control is loaded. The PAGE1.C module (see Figure 9) contains the PageInit function. If this is the first attempt to initialize the control, this function defines the Page control class and registers it. If the registration process succeeds, the library instance handle is saved and a value of TRUE is returned. The instance handle is saved because it will be used throughout the other modules in the library. The next module, PAGE2.C (see Figure 10), is the most complex. It contains the window message processing function PageWndFn. Most of this function (and its associated helper functions) is quite typical, but two sections might need further explanation. The first section requiring explanation is the update of the Page control window, which is performed when a WM_PAINT message is received or when the user selects a different page number. Although most of this code consists of straightforward Graphics Device Interface (GDI) calls, you should know how the update colors are defined. The update colors involve the use of a new pen and background brush. The pen is created using the current system window frame color and selected into the display device context. The background brush is obtained from the parent window, enabling it to define the background color of the control, just as Windows does with the system buttons. /* define appropriate pen */ hNewPen = CreatePen( PS_SOLID, 1, GetSysColor(COLOR_WINDOWFRAME) ); hOldPen = (hNewPen) ? SelectObject(Ps.hdc,hNewPen) : NULL; /* define appropriate brush and text colors*/ hNewBrush = (HBRUSH)SendMessage( PARENT (hwnd), WM_CTLCOLOR, Ps.hdc, MAKELONG(hwnd,CTLCOLOR_BTN) ); hOldBrush = hNewBrush ? SelectObject(Ps.hdc,hNewBrush) : NULL; Creating the three-dimensional look of each page icon inside the control (see Figure 11), also requires some explanation. Unless this is done carefully, the result may not look right when viewed with different system colors and display drivers. Generally speaking, three-dimensional objects in Windows 3.0 are filled using a solid brush based on the COLOR_BTNFACE system color. The top and left edges are colored with a solid brush based on the COLOR_WINDOW system color, while the bottom and right edges are colored with a brush based on the COLOR_BTNSHADOW system color. These values must be used for the three-dimensional look to appear correctly on a monochrome display. The DrawPage function uses these system colors when drawing each page icon in the following manner: /* draw page outline */ hOldBrush = SelectObject( hDC, CreateSolidBrush(GetSysColor(COLOR_BTNFACE)) ); Polygon( hDC, ptPage, 7 ); DeleteObject( SelectObject(hDC,hOldBrush) ); /* draw white border */ hOldPen = SelectObject( hDC, CreatePen(PS_SOLID,1,GetSysColor(COLOR_WINDOW)) ); MoveTo( hDC, ptPage[5].x + 1, ptPage[5].y - 1 ); LineTo( hDC, ptPage[5].x + 1, ptPage[6].y + 1 ); LineTo( hDC, ptPage[1].x, ptPage[1].y + 1 ); DeleteObject( SelectObject(hDC,hOldPen) ); /* draw shadow */ hOldPen = SelectObject( hDC, CreatePen(PS_SOLID,1,GetSysColor(COLOR_BTNSHADOW)) ); MoveTo( hDC, ptPage[5].x + 1, ptPage[5].y - 1 ); LineTo( hDC, ptPage[4].x - 1, ptPage[4].y - 1 ); LineTo( hDC, ptPage[3].x - 1, ptPage[3].y ); MoveTo( hDC, ptPage[5].x + 2, ptPage[5].y - 2 ); LineTo( hDC, ptPage[4].x - 2, ptPage[4].y - 2 ); LineTo( hDC, ptPage[3].x - 2, ptPage[3].y ); DeleteObject( SelectObject(hDC,hOldPen) ); The control style dialog box module, PAGE3.C (see Figure 12), contains the PageStyle and PageDlgFn functions. The PageStyle function is relatively simple; it sets a few temporary global variables and brings up the control style dialog box. The PageDlgFn function is also quite simple, but further explanation might be necessary in two areas. The first involves the initial display of the control ID value. If the lpfnIdToStr and lpfnStrToId parameters have been defined, the control ID is passed to the lpfnIdToStr function and converted into a string. The resulting text string is then displayed in place of the numeric value. /* define page id field */ if ( lpfnIdToStrTemp && lpfnStrToIdTemp ) { (*lpfnIdToStrTemp)( lpCtlStyle->wId, (LPSTR)szId, sizeof(szId) ); SetDlgItemText( hDlg, ID_IDENTIFER, szId ); } When the user closes the style dialog box, the updated text string is retrieved and translated back into an identifier using the lpfnStrToId function. If the LOWORD of the returned double word is nonzero, then the HIWORD contains the new control ID. /* retrieve identifier string */ wSize = GetDlgItemText( hDlg, ID_IDENTIFIER, szId, sizeof(szId) ); szId[wSize] = NULL; /* translate identifier */ dwResult = (*lpfnStrToId)( (LPSTR)szId ); if ( LOWORD( dwResult ) ) lpCtlStyle->wId = HIWORD( dwResult ); The PAGE4.C module (see Figure 13) contains the PageInfo function that is responsible for allocating and defining a CTLINFO data structure. Once this structure has been defined, a handle is returned to the calling application. It then becomes the calling application's responsibility to release the memory associated with this handle when it is no longer needed. The last file, PAGE5.C (see Figure 14), contains the PageFlags function. As mentioned previously, this function is responsible for converting control style flags into their text string equivalents. In situations where multiple flags are appropriate, they can be combined with the bitwise OR operator. In this case, since the Page control does not define any private style flags, no action is taken. To build the Page control library, you need the Windows 3.0 SDK, Microsoft C 5.1 or 6.0, and Microsoft Macro Assembler. The result will be PAGE.DLL, a DLL that you can experiment with using the Windows 3.0 Dialog editor or reference in your own application using the associated import library. Using Installable Controls in Your Applications Before you can use an installable control in an application, you must include the appropriate header file and register the control with the system. If your control is defined in a module linked into your application, you must ensure that the initialization function is called before the control class is used in a window or dialog box. If your control is incorporated into a library with the mechanisms described previously, you can load the library (and thus register the class) in one of three ways. The first method is referencing the library initialization function explicitly in the import section of the module definition file (DEF) associated with your application. If you do this, Windows will automatically load all the referenced libraries when it loads your application. If you wanted to use the Page control in your application, for example, the following statement in your DEF file would cause the library to be loaded: IMPORTS Page.PageInit The second method involves an explicit reference to one of the control library functions inside your application. For example, in certain situations you might want to call the control information or style functions. If you create an import library for your installable control (using the IMPLIB facility) and include it with the list of object libraries searched during the linking of your application, Windows will automatically load the control library when resolving these external references. In the process of loading the library, the control initialization function will be called, and your control will be registered with the system. The third approach is a manual one in which you explicitly load and free the desired control library. The advantage of this approach is that you completely control how the library is used, even to the extent of enabling the user to determine which library is used (much as is done by the Dialog Editor). The following code fragment demonstrates this: /* definitions */ HANDLE hCtlLibrary; /* load the library */ hCtlLibrary = LoadLibrary( "PAGE.DLL" ); if ( hCtlLibrary ) { /* USE THE CONTROL HERE */ /* release the library */ FreeLibrary( hCtlLibrary ); } Using the Control Once you have made sure that your library will be loaded successfully, you can use the new control throughout your application, even as a child of an existing window. If you wish, you can create child windows based on the control class using an explicit CreateWindow function call. The following code fragment shows how an instance of the Page control can be created: hWnd = CreateWindow( "Page", "", WS_CHILD | WS_TABSTOP, 8, 8, 64, 64, hWndParent, ID_PAGE, hInstance, NULL ); Note the specification of the control class ("Page"), the use of appropriate style flags, and the definition of a unique ID value that will be associated with the control. Once the control has been created, the parent window will be responsible for managing many of its activities, such as displaying or hiding the control, repositioning it whenever the parent window size changes, and processing all notification messages generated by the control. Using installable controls in dialog boxes is easier than creating them yourself, because Windows performs many of the routine tasks for you. Windows creates dialog boxes from templates defined in your resource file. Once your control library has been loaded, all you need to do to use the control is reference the public header file associated with it in your resource file, and use the following control placement statement inside each dialog box template: CONTROL <text>, <id>, <class>, <flags>, <x>, <y>, <width>, <height> For example, the following statements define two instances of the Page control: CONTROL "", ID_PAGE1, "Page", WS_CHILD|WS_TABSTOP, 8, 8, 64, 64 CONTROL "", ID_PAGE2, "Page", WS_CHILD, 24, 24, 64, 64 Using Installable Controls with the Dialog Editor Another way to use installable controls in your applications is with the new Windows 3.0 Dialog Editor. Before you can use an installable control in the Dialog Editor, you must formally make it part of the Windows environment. One way to do this is to edit your WIN.INI file manually, creating a section that specifies the association between each control class name and the DLL responsible for it. To add the Page control to Windows this way, you would add a statement such as the following to your WIN.INI file: PAGE=C:\WINDOWS\SYSTEM\PAGE.DLL The second and perhaps easier method is to load the Dialog Editor and select Add Custom Control under the File menu. When you do this, the dialog box in Figure 15 will be displayed. Using this dialog box you can specify installable controls with either of two options. The first option specifies controls permanently and requires the use of a DLL. First, copy the DLL to your SYSTEM subdirectory. Then select the first option in the dialog box and enter the full pathname of the library supporting the control. This option causes an entry to be made automatically in your WIN.INI file associating the control class with this DLL (as previously described). From then on, this control can be used in the Dialog Editor in the same manner as any predefined system control. The advantage of this process is that the control is represented accurately on the screen and you can select the control styles defined by the library. The second option is temporary and does not require the use of a DLL to define the control. If you choose this option, simply enter the name of the control class in the edit field provided. The Dialog Editor will then let you work with this control class. It will be represented on the screen simply as a rectangle with the class name centered inside (see Figure 16). Temporary control classes must be redefined each time you run the Dialog Editor. Once you have defined an installable control, you can start using it inside your dialog boxes. To place a custom control in the new dialog box, select the Custom option under the Control menu. This will display the dialog box shown in Figure 17. Select a control class from this dialog box. A preview of the control you have selected is displayed on the right side of the dialog box. You will then be returned to the editor to place the control in the same manner used for predefined system controls. Or you can simply use the Dialog Editor toolbox. Make the toolbox visible by selecting the Toolbox option under the Options menu. Select the tool in the lower right corner to display the control selection dialog box (see Figure 17). You can change the style of a custom control after it has been placed. Select the control, then either double-click inside the control or select the Styles option under the Edit menu. A dialog box, defined by the control's DLL itself, will be displayed. For example, when the Page control is selected, the dialog box in Figure 18 is displayed. Using this dialog box you can define the identifier associated with the control. For more sophisticated controls you can experiment with the various styles supported by the control class. In some cases, this dialog box will also display a small sample of the current control style in the lower right corner. When you have specified the new control style, click OK. The control in your dialog box will be updated automatically to reflect the changes you have specified. You can also use the Dialog Editor to remove temporary or permanent control classes. Select the Remove Custom Control option under the File menu; a dialog box that lets you select the control class to be removed will be displayed. If the control was permanent, the Dialog Editor will release the associated DLL and update your WIN.INI file. However, the library may remain in memory if other applications are still using this control class. Also, you cannot remove a class (either permanent or temporary) in the dialog box currently being edited. When you remove custom controls from the Dialog Editor, the DLL associated with the removed control is not deleted from your hard disk. Conclusion Although the process of designing, creating, and using an installable control can be quite complicated, the end result is well worth the trouble. With a little effort, you should be able to create a custom collection of installable controls that you can use over and over again when building Windows applications. 1 For ease of reading, "Windows" refers to the Microsoft Windows graphical environment. "Windows" refers only to this Microsoft product and is not intended to refer to such products generally. A New Multiple Document Interface API Simplifies MDI Application Development Charles Petzold The Multiple Document Interface (MDI) is a specification for applications written for the Microsoft Windows[tm] environment or OS/2 Presentation Manager programs. The specification describes a window structure and user interface that allow the user to work with multiple documents in a single application (such as text documents in a word processing program or spreadsheets in a spreadsheet program). Just as Windows1 maintains multiple application windows within a single screen, an MDI application maintains multiple document windows within a single client area. The MDI specification dates back to Windows Version 2.0 and was first used in Microsoft Excel for Windows. Windows programmers who wrote MDI applications before the release of Windows Version 3.0 must be commended for their bravery and stamina. I am certainly not one of them. When I first saw a beta version of Microsoft Excel, I was intrigued by several aspects of the program that appeared to exhibit nonstandard behavior for a Windows application. Using SYMDEB, I spent most of a day tracing through the Microsoft Excel code attempting to determine what magic its programmers had performed. It turned out not to be magic at all, but simply an enormous amount of work. With Windows 3.0, however, much of that work has already been done for the applications developer. Windows 3.0 includes one new window class, four new function calls, two new data structures, and eleven new messages specifically designed to simplify MDI applications. The Elements of MDI The Multiple Document Interface is described in the Systems Application Architecture, Common User Access: Advanced Interface Design Guide (IBM, 1989). The main application window of an MDI program is conventional: it has a title bar, a menu, a sizing border, a system menu icon, and minimize/maximize icons. However, the client area (called a workspace) is not directly used to display program output. This workspace contains zero or more child windows, each of which displays a document. These document child windows look like normal application windows, except they do not have a menu. The menu on the main application window applies to the document windows. Only one document window is active at any time. The active window appears in front of all the other document windows and is indicated by a highlighted title bar. All the document child windows are clipped to the workspace area and never appear outside the application window. Figure 1 shows several documents in Microsoft Excel. At first, MDI seems a fairly straightforward job for the Windows programmer. All you need to do is create a WS_CHILD or WS_POPUP window for each document, making the program's main application window the parent of the document window. But with a little exploration of an MDI application such as Microsoft Excel, you'll find some complications that require some difficult code. The first intricacy is that an MDI document window can be maximized. The title bar of the document window (normally used to show the filename of the document in the window) disappears, and the filename appears appended to the application name in the application window's title bar. The system menu icon of the document window becomes the first item in the top-level menu of the application window. The icon to restore the size of the document window becomes the last item in the top-level menu and appears to the far right. Second, although some MDI programs don't allow this, a document window should also be minimizable. Its icon should appear at the bottom of the workspace. (Generally an MDI application will use different icons for the main application window and each type of document.) The third complication is that the system keyboard accelerator to close a document window is the same as that used to close the main window, but it uses the Ctrl key rather than Alt. That is, Alt-F4 closes the application window while Ctrl-F4 closes the document window. In addition, Ctrl-F6 switches among the child document windows within the active MDI application. Alt-Spacebar invokes the system menu of the main window, as usual. Alt-Minus invokes the system menu of the active child document window. Fourth, when using the cursor keys to move among items on the menu in an MDI application, control passes from the application system menu, to the currently active document system menu, then back to the first item on the menu bar. (Standard windows pass control from the system menu directly to the first item on the menu bar.) Fifth, if the application is capable of supporting several different types of child windows (for example, the sheet and chart documents in Microsoft Excel), the menu should reflect the operations associated with that type of document. This requires that the program change the menu when a different document window becomes active. In addition, when no document window exists, the menu should be stripped down to only those operations involved in opening a new document. Finally, the top-level menu bar has an item called "Window." By convention, this is the last item on the top-level menu except for Help. The "Window" submenu generally has options to arrange the document windows within the workspace. Document windows can be cascaded from the upper left or tiled so that each document window is fully visible. This submenu also has a list of all the document windows. Selecting one moves that document window to the foreground. All these aspects of MDI are supported in Windows 3.0. Some overhead is required, of course (as will be shown in a sample program), but it's not anything like the code you'd have to write to support all these features directly. Windows Version 3.0 and MDI Some new terminology is necessary when discussing Windows 3.0 MDI support. (I'm afraid this terminology will be a little confusing to OS/2 Presentation Manager programmers. The use of the terms frame window and client window in a Windows MDI program isn't the same as in Presentation Manager.) The main application window is called the frame window. Just as in a conventional Windows program, this is a window in the WS_OVERLAPPEDWINDOW style. An MDI application also creates a client window based on the predefined window class MDICLIENT. The client window is created by a call to CreateWindow using the WS_CHILD style. The last parameter to CreateWindow is a pointer to a small structure of type CLIENTCREATESTRUCT (see Figure 2). This client window covers the client area of the frame window and is responsible for much of the MDI support. The color of this client window is the system color COLOR_APPWORKSPACE. The document windows, called child windows, are created by initializing a structure of type MDICREATESTRUCT (see Figure 2) and sending the client window a WM_MDICREATE message with a pointer to this structure. The document windows are children of the client window, which in turn is a child of the frame window. The parent-child hierarchy is shown in Figure 3. You need a window class (and window procedure) for the frame window and each type of child window supported by the application. You don't need a window procedure for the client window because the window class is preregistered. Sample Program The Windows 3.0 Software Development Kit (SDK) includes a sample program called MULTIPAD that demonstrates how to write an MDI program. However, MULTIPAD contains quite a bit of code that has nothing to do with MDI. It might be easier for you to get a better feel for MDI programming by examining a smaller program that does little except demonstrate the MDI features. The components of this program, called MDIDEMO, are shown in Figure 4. To compile MDIDEMO, you need Microsoft C Version 6.0 and the Windows 3.0 SDK. MDIDEMO (see Figure 5) supports two types of extremely simple document windows: one displays "Hello, World!" in the center of its client area and the other displays a series of random rectangles. (In the source code listings and identifier names, these are referred to as the Hello document and the Rect document.) Different menus are associated with these two types of document windows (compare Figures 5 and 6). The document window that displays "Hello, World!" has a menu that allows changing the color of the text. Three Menus Let's turn first to the MDIDEMO.RC resource script (see Figure 4), which defines three menu templates used by the program. The program displays the MdiMenuInit menu when no document windows are present. This menu simply allows the creation of a new document or exit from the program. The MdiMenuHello menu is associated with the document window that displays "Hello, World!" The File submenu allows the opening of a new document of either type, the closing of the active document, and exiting from the program. The Color submenu lets you set the text color. The Window submenu has options to arrange the document windows in cascade or tile fashion, to arrange the document icons, and to close all the windows. This submenu will also list all the document windows as they are created. The MdiMenuRect menu is associated with the random rectangle document. This menu is the same as the MdiMenuHello menu except that it does not include the Color submenu. The MDIDEMO.H header file (see Figure 4) defines all the menu identifiers as well as three constants: #define INIT_MENU_POS 0 #define HELLO_MENU_POS 2 #define RECT_MENU_POS 1 These identifiers indicate the position of the "Window" submenu in each of the three menu templates. This information is needed by the program to inform the client window where the document list is to appear. Of course, the MdiMenuInit menu doesn't have a "Window" submenu, so I've indicated that the list should be appended to the first submenu (position 0). The list will never actually be viewed there, however. You'll see why when I discuss the program. The IDM_FIRSTCHILD identifier doesn't correspond to a menu item. This is the identifier that will be associated with the first document window in the list that will appear in the "Window" submenu. You should choose this identifier to be greater than all the other menu IDs. Program Initialization In MDIDEMO.C (see Figure 4), WinMain begins by registering window classes for the frame window and the two child windows. The window procedures are called FrameWndProc, HelloWndProc, and RectWndProc. Normally, different icons should be associated with these window classes. For simplicity, I've used the standard IDI_APPLICATION icon and no icon for the Rect window. (Note that I've defined the hbrBackground field of the WNDCLASS structure for the frame window class to be the COLOR_APPWORKSPACE system color. This is not entirely necessary because the client area of the frame window is covered up by the client window, and the client window is this color anyway. However, using this color looks a little better when the window is first displayed.) The lpszMenuName field is set to NULL for each of these three window classes. For the Hello and Rect child window classes, this is normal. For the frame window class, I have chosen to indicate the menu handle in the CreateWindow function when creating the frame window. The window classes for the Hello and Rect child windows allocate extra space for each window using a nonzero value as the cbWndExtra field of the WNDCLASS structure. This space will contain a local memory handle that will reference a block of memory used to store information unique to each document window. This block of memory is the size of the HELLODATA or RECTDATA structures defined near the top of MDIDEMO.C. Next, WinMain uses LoadMenu to load the three menus and saves their handles in global variables. Three calls to the GetSubMenu function obtain handles to the "Window" submenu on which the document list will be appended. These handles are also saved in global variables. A call to CreateWindow in WinMain creates the frame window. During the processing of WM_CREATE in FrameWndProc, the frame window creates the client window. This involves another call to CreateWindow. The window class is set to MDICLIENT, which is the preregistered class for MDI client windows. The last parameter to CreateWindow must be set to a pointer to a structure of type CLIENTCREATESTRUCT. This structure has two fields, hWindowMenu and idFirstChild. The first field, hWindowMenu, is the handle of the submenu on which the document list will be appended. In MDIDEMO, this is hMenuInitWindow, which was obtained during WinMain. You'll see later how the menu is changed. The second field, idFirstChild, is the menu ID to be associated with the first document window in the document list. This is simply IDM_FIRSTCHILD. Back in WinMain, MDIDEMO displays the newly created frame window and enters the message loop. The message loop is a little different from usual-after obtaining the message from the message queue with a call to GetMessage, an MDI program passes the message to TranslateMDISysAccel. This function translates any keystroke that may correspond to the special MDI accelerators (such as Ctrl-F6) into a WM_SYSCOMMAND message. If the function TranslateMDISysAccel returns TRUE (indicating that a message was translated), TranslateMessage and DispatchMessage are not called. MDIDEMO uses its own accelerators also, so the message loop should call the TranslateMessage and DispatchMessage functions only if neither TranslateAccelerator nor TranslateMDISysAccel returns TRUE. Creating the Child Windows The bulk of FrameWndProc is devoted to processing WM_COMMAND messages that signal menu selections. As usual, the wParam parameter to FrameWndProc contains the menu ID number. For wParam values of IDM_NEWHELLO and IDM_NEWRECT, FrameWndProc must create a new document window. This is done by initializing the fields of an MDICREATESTRUCT structure (most of which correspond to CreateWindow parameters) and sending the client window a WM_MDICREATE message with lParam set to a pointer to this structure. The client window then creates the child document window. Normally, the szTitle field of the MDICREATESTRUCT structure would be the file name corresponding to the document. The style field can be set to the window styles WS_HSCROLL or WS_VSCROLL or both to include scroll bars in the document window. The style field can also include WS_MINIMIZE or WS_MAXIMIZE to display the document window initially in a minimized or maximized state. The lParam field of the MDICREATESTRUCT structure provides a way for the frame window and the child window to share some variables. This could be set to a local or global memory handle that references a block of memory containing a structure. During the WM_CREATE message in the child document window, lParam is a pointer to a CREATESTRUCT structure; the lpCreateParams field of this structure is a pointer to the MDICREATESTRUCT structure used to create the window. On receipt of the WM_MDICREATE message, the client window creates the child document window and adds the title of the window to the bottom of the submenu specified in the MDICLIENTSTRUCT structure. When the MDIDEMO program creates its first document window, the submenu used is the File submenu of the MdiMenuInit menu. We'll see later how this document list gets moved to the Window submenu of the MdiMenuHello and MdiMenuRect menus. The menus lists as many as nine documents, each preceded by an underlined number (1 to 9). If more than nine document windows are created, this list is followed by a "More windows..." item on the menu. This item invokes a dialog box containing a list box that lists all the document windows (see Figure 7). The automatic maintenance of this document list is one of the nicest features of the Windows MDI support. More Frame Window Message Processing Let's continue with FrameWndProc message processing before turning to the child document windows. When you select Close from the File menu, MDIDEMO closes the active child window, and it obtains the handle to the active child window by sending the client window a WM_MDIGETACTIVE message. If the child window responds affirmatively to a WM_QUERYENDSESSION message, then MDIDEMO sends the client window a WM_MDIDESTROY message to close the child window. Processing the Exit option from the File menu requires only that the frame window procedure send itself a WM_CLOSE message. Processing the Tile, Cascade, and Arrange Icons options from the Window submenu is a snap, requiring only that the WM_MDITILE, WM_MDICASCADE, and WM_MDIICONARRANGE messages be sent to the client window. The Close All option is more complex. FrameWndProc calls EnumChildWindows to enumerate the child windows through the callback function CloseEnumProc. This function sends the child a WM_MDIRESTORE message and then WM_QUERYENDSESSION. If the child window responds affirmatively to a WM_QUERYENDSESSION message, CloseEnumProc closes the child window by sending the WM_MDIDESTROY message to the client window. However, the icon title windows (the text beneath each document icon) must not be destroyed. You can check for these windows by a non-NULL return from GetWindow with a GW_OWNER parameter. You'll notice that FrameWndProc does not process any of the WM_COMMAND messages signaling that one of the colors has been selected from the Color menu. These messages are really the responsibility of the document window. For this reason, FrameWndProc sends all unprocessed WM_COMMAND messages to the active child window so that the child window can process those messages that pertain to its window. All the messages that the frame window procedure chooses not to process must be passed to DefFrameProc. This is one of the new MDI functions in Windows 3.0. It replaces DefWindowProc in the frame window procedure. Even if a frame window procedure traps the WM_MENUCHAR, WM_NEXTMENU, WM_SIZE, or WM_SETFOCUS messages, they must also be passed to DefFrameProc. Unprocessed WM_COMMAND messages must also be passed to DefFrameProc. In particular, FrameWndProc does not process any of the WM_COMMAND messages resulting from the user selecting one of the documents from the list in the Window submenu. (The wParam values for these options begin with IDM_FIRSTCHILD.) These messages are passed to DefFrameProc and processed there. Notice that the frame window does not need to maintain a list of window handles of all the document windows it creates. If these handles are ever needed (such as when processing the Close All option from the menu), they can be obtained using GetWindow. Child Document Windows Now let's look at HelloWndProc, which is the window procedure used for the child document windows that display "Hello, World!" As with any window class used for more than one window, static variables defined in the window procedure (or any function called from the window procedure) are shared by all windows based on that window class. Data that is unique to each window must be stored using a method other than static variables. One such technique uses window properties. Another approach (the one I employed) uses memory space that is reserved by defining a nonzero value in the cbWndExtra field of the WNDCLASS structure used to register the window class. In MDIDEMO, I use this space to store a local memory handle that references a block of memory the size of the HELLODATA structure. HelloWndProc allocates this memory during the WM_CREATE message, locks it, initializes the two fields (which indicate the currently checked menu item and the text color), unlocks the block, and stores the local memory handle using SetWindowWord. When processing a WM_COMMAND message for changing the text colors (recall that these messages originate in the frame window procedure), HelloWndProc uses GetWindowWord to obtain a handle to the memory block containing the HELLODATA structure. Using this structure, HelloWndProc unchecks the checked menu item, checks the selected menu item, and saves the new color. A document window procedure receives the WM_MDIACTIVATE message whenever the window becomes active or inactive (indicated by a TRUE or FALSE value in wParam). You'll recall that MDIDEMO has three different menus: MdiMenuInit, used when no documents are present; MdiMenuHello, used when a Hello document window is active; and MdiMenuRect, used when a Rect document window is active. The WM_MDIACTIVATE message provides an opportunity for the document window to change the menu. If wParam is TRUE (meaning the window is becoming active), HelloWndProc changes the menu to MdiMenuHello. If wParam is FALSE, HelloWndProc changes the menu to MdiMenuInit. HelloWndProc changes the menu by sending a WM_MDISETMENU message to the client window. The client window processes this message by removing the document list from the current menu and appending it to the new menu. This is how the document list is transferred from the MdiMenuInit menu (which was in effect when the first document is created) to the MdiMenuHello menu. The SetMenu function should not be used to change a menu in an MDI application. Another little chore involves the checkmarks on the Color submenu. Program options such as these should be unique to each document. For example, you should be able to set blue text in one window and red text in another. The menu checkmarks should reflect the option chosen in the active window (see Figure 8). For this reason, HelloWndProc unchecks the selected menu item when the window is becoming inactive and checks the appropriate item when the window is becoming active. The window procedure gets the first WM_MDIACTIVATE message with wParam set to TRUE when the window is first created, and the last (with wParam set to FALSE) when the window is destroyed. When the user switches from one document to another, the first document window receives a WM_MDIACTIVATE message with wParam set to FALSE (at which time it sets the menu to MdiMenuInit) and the second document window receives a WM_MDIACTIVATE message with wParam set to TRUE (at which time it sets the menu to MdiMenuHello or MdiMenuRect as appropriate). If all the windows are closed, the menu is left as MdiMenuInit. You'll recall that FrameWndProc sends the child window a WM_QUERYENDSESSION when the user selects Close or Close All from the menu. HelloWndProc processes the WM_QUERYENDSESSION and WM_CLOSE messages by displaying a message box and asking the user if the window can be closed. (In a real program, this message box would ask if a file needed to be saved.) If the user indicates that the window should not be closed, the window procedure returns zero. During the WM_DESTROY message, HelloWndProc frees the local memory block allocated during the WM_CREATE message. All unprocessed messages must be passed on to DefMDIChildProc (not DefWindowProc) for default processing. The messages shown below must be passed to DefMDIChildProc whether the child window procedure does something with them or not. WM_CHILDACTIVATE WM_GETMINMAXINFO WM_MENUCHAR WM_MOVE WM_NEXTMENU WM_SETFOCUS WM_SIZE WM_SYSCOMMAND RectWndProc is fairly similar to HelloWndProc, but it's a little simpler (no menu options are involved and the window does not verify with the user if it can be closed), so I needn't discuss it. But note that RectWndProc breaks after processing WM_SIZE so it is passed to DefMDIChildProc. Finishing Up I mentioned earlier that the MDI support in Windows 3.0 involves one new window class, two new data structures, four new function calls, and eleven new messages. The window class, MDICLIENT, is used for creating the client window. The two new data structures, CLIENTCREATESTRUCT and MDICREATESTRUCT, are used for creating the client window and the child document windows. I discussed three of the four new function calls: TranslateMDISysAccel, DefFrameProc, and DefMDIChildProc. The fourth new function call is ArrangeIconicWindows, which performs the same function in an MDI application as the WM_MDIICONARRANGE message. MDIDEMO demonstrates nine of the eleven new messages. These messages are: WM_MDIACTIVATE WM_MDICASCADE WM_MDICREATE WM_MDIDESTROY WM_MDIGETACTIVE WM_MDIICONARRANGE WM_MDIRESTORE WM_MDISETMENU WM_MDITILE The other two messages are WM_MDIMAXIMIZE and WM_MDINEXT. But because Windows takes care of all the maximizing and switching of child windows, applications usually don't have to use these messages. One aspect of the Multiple Document Interface described in the IBM book about Common User Access (CUA), and used in Microsoft Excel, is splitting windows in half or into quadrants using small controls to the top and left of the scroll bars. This is not part of the MDI support in Windows 3.0. However, all other features of MDI described in CUA are provided. The new features described here make MDI accessible to Windows programmers and are implemented in a way that illustrates the power of the object-oriented architecture of Windows. Providing DDE Facilities to OS/2 Kernel Applications Richard Hale Shaw In previous articles, I've discussed how Dynamic Data Exchange (DDE) can be used to transfer data among Presentation Manager (hereafter "PM") applications. DDE is basically a protocol that uses PM message queues to regulate the passing of data. But since DDE can be used only by programs that open PM message queues, OS/2 kernel applications cannot access DDE directly. If a kernel application does open a PM message queue, all I/O has to come through Presentation Manager, which means that the application will not be able to access the OS/2 subsystems (Vio, Kbd, Mou) from its screen group. When I wanted to provide Clipboard services to OS/2 kernel applications, I wrote a PM program, PMServer (see "Accessing Presentation Manager Facilities from Within OS/2 Kernel Applications," MSJ, Vol. 5, No. 1). This month, I'm expanding PMServer to provide DDE services to kernel applications. As I discussed in the first PMServer article, the natural vehicle for this expansion is a Presentation Manager object window-specifically, an object window for each kernel client that PMServer is servicing. Once I've added object window facilities to PMServer, I'll expand the sample program, PMAccess, that was originally presented with PMServer (see Figure 1). CLIENT.C PMServer's DDE processing code is contained in CLIENT.C (see Figure 2). This module consists of three functions: CreateClient, the object window thread function; DdeClientProc, the window procedure function for the object window; and MakeDDESeg, a support function that creates data packets suitable for transfer via DDE. Since the purpose of CreateClient is to create a window and message queue (even though the queue is in a separate thread of execution), it's not unlike the main function of PMServer. It takes one parameter, a pointer to a CLIENT data type: typedef struct _client { USHORT isddeclient; HQUEUE qhandle; PID kclient_pid; HWND hwnd; HWND hwndpmserver; PVOID threadstack; CHAR appname[MAX_APPLICATION_LEN]; CHAR topicname[MAX_TOPIC_LEN]; } CLIENT; typedef CLIENT *PCLIENT; This definition is in PMSVMSGS.H (see Figure 2). The header file contains all the message definitions and structures common to PMSERVER.C and CLIENT.C. The CLIENT data type is an expanded version of that used in the earlier form of PMServer. Originally, it contained only a queue handle and a process ID for each kernel client that registered itself with PMServer. Now it has been expanded to include several additional items: a Boolean switch that indicates whether a client is engaged in a DDE conversation, the handle of the object window managing the conversation, PMServer's window handle, a pointer to the object window's stack, and the application and topic names that identify a DDE conversation. PMServer initializes all these items except for the object window handle, which won't be initialized until the object window has been created by CreateClient. CreateClient inserts the handle into the CLIENT structure when it creates the object window. As a result, the CLIENT structure provides information that can be used by any thread (the main thread, the Queue Manager thread, or the object window thread) or by either window (the main window or the object window). CreateClient begins by saving the CLIENT pointer passed to it from _beginthread. Then it calls WinInitialize, opens the new message queue, and registers the new DdeClient window class. The CLIENT pointer, anchor block handle, and window handle are all stored in a CDATA structure: typedef struct clientdata { HAB hab; HWND hwndServer; PCLIENT pclient; } CDATA; typedef CDATA *PCDATA; Using WinCreateWindow, the address of this structure is passed to the newly created object window as the first message parameter accompanying WM_CREATE. This makes all three components of CDATA and the elements of the CLIENT structure available to the new window. Note that this was accomplished in a single call; there is no need for additional messaging and interprocess communication. What happens when DdeClientProc, the object window's window procedure, receives the WM_CREATE message? First, the CDATA parameter is saved from mp1, and a local CLIENT pointer is initialized from the one stored in the structure. Then WinSetWindowPtr is called to save the CDATA pointer in the reserved area of a window, allowing subsequent calls to WinQueryWindowPtr (at the beginning of the function) to reinitialize the pointer upon each call to the window procedure. Next, the pointer's hwndServer member is initialized to NULL. The value of this handle will be used to determine whether a PM application has joined the proposed DDE conversation as a server. Finally, the function calls WinDdeInitiate to propose the conversation (by broadcasting WM_DDE_INITIATE to every PM application). Note that the call to WinDdeInitiate is followed by a call to WinStartTimer. This function starts a timer that will generate WM_TIMER messages at specified intervals, in this case every tenth of a second. When the object window receives a WM_TIMER message, it looks to see if the hwndServer handle is NULL. If so, it assumes that no Presentation Manager program has posted a WM_DDE_INITIATEACK to respond positively to the WM_DDE_INITIATE and posts a PMS_DDE_INITNAK to the kernel client and a WM_QUIT to itself. (The consequences of this message are described below.) If a WM_DDE_INITIATEACK is received, the handle of the responding server application (the DDE server) is saved in the hwndServer of the CDATA pointer and a PMS_DDE_INITACK message is sent (via MsgQSend) to the kernel client, signaling the start of the DDE conversation. Managing the DDE Conversation Once the object window is in place and the DDE conversation has been initiated, the kernel client can begin to make data requests and receive data. A kernel client can post a PMS_DDE_REQUEST or a PMS_DDE_ADVISE message to the Queue Manager to initiate a data request or receive data updates. These messages should be accompanied by the data item name. The Queue Manager will comply by sending a PMSV_REQUEST or PMSV_ADVISE message to the object window handle. Upon receiving either of these messages, the object window will create a DDESTRUCT data block (via a call to MakeDDESeg) and post a WM_DDE_REQUEST or a WM_DDE_ADVISE via WinDdePostMsg. If no data is available, the DDE server will set the appropriate bits and reply with a WM_DDE_ACK. The object window will then post a PMS_DDE_NODATA message to the kernel client. However, if data is received from the DDE server, it will arrive as a DDESTRUCT data block in a WM_DDE_DATA message. Upon receiving this message, the object window will allocate a piece of memory and reference it with a pointer to the ITEMREQ structure: typedef struct itemreq { CHAR item[MAX_ITEM_LEN]; CHAR value[1]; } ITEMREQ; typedef ITEMREQ *PITEMREQ; The item component is then copied as a NULL-terminated string from the data block into the ITEMREQ structure and is followed by the data component. Since PMServer supports only the transfer of text (CF_TEXT) data via DDE, the data is assumed to be text and therefore a NULL-terminator is placed after it in the ITEMREQ structure. Because the ITEMREQ space is big enough for an ITEMREQ structure plus the data component, there is plenty of room for both in the new space. After the data is copied into the ITEMREQ structure, the new data package is passed to the kernel client as part of a PMS_DDE_DATA message. Then the object window frees the DDESTRUCT segment and the ITEMREQ pointer. Note that the code for handling WM_DDE_DATA specifically checks to see if the application name is Microsoft Excel. The documentation for DDE specifies that the data size component of a DDESTRUCT (the cbData member) contains the size of the entire data block. However, Microsoft Excel sets the size of the cbData member as the size of the data component only, so an exception has to be made when interpreting a DDESTRUCT passed from this application. Closing the Object Window When the DDE server terminates the conversation, it sends the object window a WM_DDE_TERMINATE message. The object window will respond with a WM_DDE_TERMINATE of its own, then it posts a PMS_DDE_TERMINATE message to the kernel client and a WM_QUIT message to itself. The same thing occurs if PMServer needs to terminate the object window or if the object window receives a WM_CLOSE. Upon receiving a WM_QUIT, the object window thread breaks out of the message processing loop in CreateClient. It calls WinDestroyWindow and WinDestroyMsgQueue to remove the window and the message queue. It then posts a PMSV_THREAD_TERMINATE to PMServer, calls WinTerminate to discard the PM resources used by the thread, and finally calls DosExit to terminate the thread. Since receipt of PMSV_THREAD_TERMINATE from an object window thread will cause PMServer to deallocate the thread's stack, it's essential that it not do so until the thread executing CreateClient has terminated. Thus, a call to DosEnterCritSec has been inserted before the point where the PMSV_THREAD_TERMINATE message is posted. This temporarily freezes all the other threads in the process and allows CreateClient to complete execution. Once the CreateClient thread has terminated, the other threads are unfrozen as if DosEndCritSec had been called. PMServer can then safely process the termination message from the object window. Note that the object window is capable of generating WM_DDE_EXECUTE and WM_DDE_POKE messages, although PMAccess does not take specific advantage of it. These messages are used for submitting commands and unsolicited data to the DDE server. The object window can also process two other commands from the kernel client: one command lets it receive notification of data changes without an update (a variation of WM_DDE_ADVISE), and another command ceases the notifications (WM_DDE_UNADVISE). PMAccess With the DDE object window code in place in PMServer, an OS/2 kernel application can use it to receive data from PM applications via DDE. The sample program that demonstrated PMServer's Clipboard-handling facilities, PMAccess, will now be expanded to display its new DDE capabilities (see Figure 3). The original PMAccess is a message-based Vio program that has five buttons at the bottom of its window. It enables the user to perform cut, copy, and paste operations on text. You may recall that PMAccess uses OS/2 queues to let its threads communicate with each other. Architecturally this means that PMAccess is largely a reactive program not unlike a Presentation Manager application, rather than a proactive one as a DOS program usually is. For instance, the keyboard and mouse threads pass messages to a queue where they are received by PMAccess' main thread. This thread can then interpret the messages and perform the appropriate actions (such as moving the cursor, clearing the screen, or interacting with PMServer to copy or paste data from the clipboard). This method of message handling also allows PMAccess to react to screen buttons, so it can respond when a message from the mouse thread indicates that a button has been pressed. Thus, when the user presses a screen button (by clicking it with the mouse or pressing the button's accelerator key), the main thread receives a message associated with the button. For example, if the user presses the Copy button, PMAccess will receive the MSG_COPY message and pass the selected screen text to PMServer as part of a PMS_CLPBRD_COPY message. PMServer will then put the text in the Clipboard. If PMAccess receives a PMS_CLPBRD_DATA message, PMServer is indicating that Clipboard data is available in response to a previously generated PMS_CLPBRD_QUERY-probably sent by PMAccess' Request thread, which periodically queries PMServer about the state of the Clipboard. The most natural way, therefore, to expand PMAccess to take advantage of PMServer's DDE facilities is by adding support for the additional set of "PMS_" messages (now defined in PMSERVER.H, Figure 4). To initiate a DDE conversation, PMAccess will have to send a PMS_DDE_INIT to PMServer, accompanied by the application and topic names. Upon receiving a PMS_DDE_INITACK from PMServer (indicating that a DDE server is available), PMAccess can send PMS_DDE_REQUEST or PMS_DDE_ADVISE messages to request data or receive updates when the data changes. These messages must be accompanied by the data item name. PMServer will respond to these with either PMS_DDE_DATA (if data was received) or PMS_DDE_NODATA. Finally, PMAccess can send PMS_DDE_TERMINATE if it wishes to end the DDE conversation. Along with internal support for these messages, PMAccess required additions to its user interface. Provisions had to be made to allow a user to initiate a DDE conversation, request data or updates, and terminate the conversation. Thus four new buttons (with associated messages) were added to the user interface: Initiate, Request, Advise, and Terminate (see Figure 5). When the user presses Initiate, for example, a MSG_INIT message is produced, and PMAccess executes the code to generate a PMS_DDE_INIT to PMServer. Three entry buttons were added to allow a user to specify the Application, Topic, and Item Name parameters of a DDE conversation. When the user clicks one of the entry buttons with the mouse, the cursor is moved to that button so that he or she enters the appropriate DDE parameter from the keyboard. (To do this, a change had to be made in the handling of the MSG_B1DOWN message, which processes left mouse button presses. This handler now resets the cursor to the position of the mouse pointer when clicked in the text display/entry area above the buttons. This gives the user a means of getting out of the entry buttons.) When a user presses the Initiate button, PMAccess' main thread will read the characters contained in the Application and Topic name button fields from their positions on the screen. This is facilitated by a new function, ButtonRead in BUTTON.C (see Figure 3), which uses VioReadCharStr to read the screen. For instance, to communicate with Microsoft Excel (after it has been started), the Application Name button should contain "excel." The Topic Name button should contain the name of a worksheet, such as the sample worksheet, "east.xls," that comes with Microsoft Excel (see Figure 6). The thread then sets up a packet for PMServer containing its own process ID and the application and topic name retrieved from the entry buttons. The packet is then sent as part of a PMS_DDE_INIT message via MsgQSend. Once PMServer processes the PMS_DDE_INIT, starts the new object window, and the object window receives a WM_DDE_INITIATEACK from a PM application, the object window posts a PMS_DDE_INITACK back to PMAccess. When PMAccess receives this message, it will highlight the Initiate button, indicating that a DDE conversation has been established. The user can then fill in the item button with the name of the data item to be requested. Citing the earlier example, the user can request rows 15 and columns 23 by entering "r1c2:r5c3" in the Item button. The user should then click the Request or Advise buttons (see Figure 7). When a user selects Request or Advise, PMAccess will again set up a message packet to be sent to PMServer. The message, either PMS_DDE_REQUEST or PMS_DDE_ADVISE, will be accompanied by PMAccess' process ID and the data item name (again, read from the Item Name button). Once PMAccess receives a PMS_DDE_DATA message in response, it will display the data received at the current cursor position in the upper part of the display. This is the same code used to process a PMS_CLPBRD_DATA message, except that the data item name is checked against the one most recently read from the Item entry button. The user can terminate the DDE conversation by clicking the Terminate button, which causes PMAccess to send a PMS_DDE_TERMINATE message to PMServer. The latter will then terminate the DDE conversation with the DDE server and shut down the associated object window. An interesting consequence of managing the new buttons was the decision to have PMAccess' main message-processing thread generate messages to itself, as is done in Presentation Manager programs. Instead of duplicating the code necessary to turn the Paste and Initiate buttons on and off if the user pressed the Clear button, PMAccess generates a new message to itself to reset the buttons accordingly. You can find this in the processing of the MSG_CLR and PMS_CLPBRD messages, which generate MSG_RESETPASTEBUTTON or MSG_RESETINITBUTTON (see PMACCESS.C in Figure 3). Conclusion Adding DDE and Clipboard facilities to PMAccess was an interesting experience, but not as interesting as writing PMServer. With its use of multiple threads, IPC, and access to PM facilities, PMServer is hopefully a good model for your own multithreaded PM programs, as well as a means of letting your Vio applications make use of PM's Clipboard and DDE facilities. u Figure 1 Initiate 1. After filling in the Application, Topic, and Item Name fields, the user clicks the Initiate button on PMAccess' interface. 2. PMAccess' mouse thread reads button event and generates a MSG_INIT to PMAccess' main thread. 3. Upon receiving MSG_INIT, the main thread sends PMS_DDE_INIT, with Application and Topic names, through queue to Queue Manager thread of PMServer. 4. Queue Manager reacts to PMS_DDE_INIT, sets up CLIENT data structure, allocates stack space for an object window thread, and starts a new thread with a call to _beginthread. 5. New thread begins execution of CreateClient, opens new (Object) window, enters message loop with DdeClientProc as its window procedure. 6. Object window gets WM_CREATE from PM and calls WinDdeInitiate to broadcast WM_DDE_INITATE. 7. Microsoft Excel receives WM_DDE_INITIATE, sees that it can support DDE conversations on requested Topic (worksheet name), calls WinDdeRespond to post WM_DDE_INITIATEACK. 8. Object window receives WM_DDE_INITIATEACK from Microsoft Excel, and sends PMS_DDE_INITACK to PMAccess through the latter's queue. 9. PMAccess receives PMS_DDE_INITACK, highlights Initiate button for user, and beeps. Request 10. User clicks Request button. 11. Mouse thread generates MSG_REQUEST to main thread. 12. Main thread sends PMS_DDE_REQUEST, with Item Name (worksheet range of cells), through queue to Queue Manager. 13. Queue Manager posts PMSV_REQUEST to Object window. 14. Object window posts WM_DDE_REQUEST to Microsoft Excel via WinPostDdeMsg. 15. Microsoft Excel, seeing that the requested range of cells is available, posts them back to Object window as part of WM_DDE_DATA message. 16. Object window sends data with PMS_DDE_DATA message to PMAccess through the latter's queue. 17. PMAccess, upon receiving PMS_DDE_DATA, writes the accompanying data to the display. Terminate 18. User clicks Terminate button. 19. Mouse thread generates MSG_TERM to main thread. 20. PMAccess' main thread sends PMS_DDE_TERMINATE through queue to Queue Manager thread. 21. Queue Manager posts PMSV_TERMINATE to Object window. 22. Object window posts WM_DDE_TERMINATE to Microsoft Excel to terminate DDE conversation. 23. It follows with a PMS_DDE_TERMINATE to PMAccess... 24. ...and a WM_QUIT to itself. 25. Object window thread breaks out of message loop when WM_QUIT is received, then calls DosEnterCritSec, temporarily freezing the other threads. Then it posts PMSV_THREAD_TERMINATE to PMServer, and calls WinTerminate and DosExit to terminate itself, thereby unfreezing the other threads. PMServer's window procedure deallocates thread stack upon receiving PMSV_THREAD_TERMINATE. 26. PMAccess receives PMS_DDE_TERMINATE; the user sees it clear the fields as it readies itself for additional DDE instructions from the user. Learning Windows Part I: The Message-based Paradigm Marc Adler The Microsoft Windows[tm] graphical environment has been getting a lot of attention lately. Perhaps you're interested in programming for Windows1 but don't know how or where to start, or you're unconvinced that Windows is the way to go. MSJ will introduce you to Windows programming with a series of articles starting in this issue, featuring brand-new Windows Version 3.0. This article will develop a small but useful Windows application, examining each component as it is added. Why is Windows in the spotlight so much these days? In the past, graphical user interfaces (GUIs) such as Windows were the domain of expensive computer systems with more CPU power than the average person could afford. GUIs were certainly out of the question on the original 4.77 MHz PC. The introduction of the IBM PC/AT in 1984 and the EGA video standard made graphical systems more feasible, but the screen drawing speed was still sluggish enough to dissuade people from using GUIs. Today, a 386[tm] running at 16 MHz with a VGA card, color monitor, and a mouse seems to be standard in much of the corporate world as well as among many developers. As hardware prices fall, it seems likely that graphical 386/486[tm] systems will become even more widespread. As affordable computers capable of handling this type of system proliferate, many predict that GUIs will become the standard user interface. The intuitive, object-oriented, icon-based method will entice more and more computer-wary people into using the computer as an everyday tool. Most major computer companies realize this and are developing their products accordingly. IBM has created a blueprint for all of its future applications called Systems Application Architecture[tm] (SAA[tm]). The part of SAA governing how the user interacts with the program is called the Common User Access (CUA). Adherence to CUA standards ensures that operating methods remain consistent among a wide variety of products, thus lessening the amount of time it takes a new user to become proficient with an application. For example, pressing F1 in a CUA-compliant application invokes context-sensitive help regardless of the user's location in the application. The user also knows that help screens will be in a certain format and that they will lead to an index of commands and a table of command keystrokes. Consistency among applications reduces the substantial cost of training employees on new software. Windows is fully compliant with CUA and will continue to be, which is one attractive reason to program in it. Another reason is device-independence. Properly written Windows applications run on a wide variety of hardware, from monochrome monitors to Super-VGA adapters hooked up to 19" displays. The device-independence even extends as far as printing. As long as Windows supports your printer (and it supports a lot of them), you will be able to print a document with embedded graphics without having to worry about printer-dependent code. Other reasons to program in Windows include its ability to multitask applications; its sophisticated memory management, with multitasking among large applications; and its ability to take advantage of protected mode automatically on a 386-based system. Sample Application The sample Windows application that will be built is a small stock portfolio analysis program. Not only will it demonstrate each of the elements of a Windows program, it's something that might be of use to you. You'll learn how to design and create the application's pull-down, multilevel menu system. Several dialog boxes with different kinds of control windows will collect data from the user regarding the stocks as well as the daily trading information for each stock. You will perform subclassing of existing windows classes when you implement the dialog boxes and eventually may experiment with the new custom control facility of Windows 3.0. The sample application will be able to display several different types of charts, including a line graph of a stock's price over a period of time and a bar chart of the stock's daily volume. Since the user will probably want hardcopy of the graphs, you'll also see how printing is done under Windows. Not only does the stock application permit exploration of important features of Windows, it also provides a base for enhancements. For example, the ability to download stock data at the end of the day from a commercial online database, such as Dow Jones or CompuServe, would be a useful feature. This would entail exploring the communications routines in Windows. If you think of each stock as being a separate document, you can experiment with the Windows Multiple Document Interface (MDI). Windows now has a built-in API for creating an MDI-based application. (For more on MDI, see "A New Multiple Document Interface API Simplifies MDI Application Development" p. 53-Ed.) Finally, you will set up a Dynamic Data Exchange (DDE) conversation with a Microsoft Excel spreadsheet. Windows Programming Paradigm Let's begin by discussing the Windows programming model. The single most important adjustment you have to make to go from standard C programming to Windows programming is the realization that all Windows applications are based on the concept of message passing. Every Windows program requires a message loop and a message handler for each window in the application. Messages are generated by Windows whenever an event occurs or when an action needs to be taken. When you move the mouse, Windows generates a message that tells your application that the mouse has moved; embedded in the message are the coordinates of the point the mouse has moved to. When you choose something from a menu, Windows informs your application that an item was selected via a message. Windows will frequently send your application messages when "interesting" things happen, such as when a window needs to be redrawn. The essence of programming a Windows application is to fetch a message, direct that message to the window the message is meant for, act as quickly as you can to process that message, and go back and read another message. Why have a message loop? Why send messages at all? Windows is a multitasking system, so multiple programs will be running at the same time. Because each program requires CPU time, no one program should be able to hog the CPU for a substantial period of time. If a program uses the CPU for a long period, other time-critical applications running simultaneously may fail to work properly. Preemptive multitasking systems such as UNIX will interrupt an application when its allocated time slice expires and pass control of the CPU to another application, even if the first application is in the middle of an important operation. Windows multitasking is nonpreemptive. An application must explicitly yield control to another application-Windows will not preempt a task on its own. Windows does its multitasking whenever an application examines the message queue. If there are no pending messages for an application, Windows will check for messages meant for other running applications. If another application has a message pending, Windows will give control to that application. That application in turn will fetch its message, act on it, then try to read another message. At this point, Windows resumes the cycle. You must structure your Windows application to cooperate with this "pass-the-baton" method of operation. If you don't, your application might still run correctly, but other applications executing at the same time probably will not run. In other words, you can't use getc's any more! Messages Let's examine a message. In Windows, all messages are in a fixed, compact format that facilitates rapid message processing. If you look at the huge WINDOWS.H include file that comes with the Windows Software Development Kit (SDK), you can find the exact structure of a message (see Figure 1). The first member of the structure, hwnd, tells which window the message is targeted for. Each window has a number associated with it, known as a window handle, that uniquely identifies the window in a Windows session. The second member, message, is the message identifier. It tells the application exactly what event has occurred. A list of all of the messages that Windows knows about can also be found in WINDOWS.H (see Figure 2). All message names start with WM_ followed by a descriptive name such as WM_PAINT, WM_CHAR, or WM_COMMAND. The third and fourth members, wParam and lParam, contain additional information about the message. Their contents are dependent on the message. A complete listing of the messages and their parameters can be found in the SDK Programmer's Reference. The last two members of the message structure, while always present, are not used as frequently as the other members. The first of these, "time," is the time at which the message was placed into the message queue; "pt" contains the coordinates of the mouse at the time that the message was placed into the queue. When a message is generated by an input event, Windows places it in the system message queue. Windows maintains a single system message queue that is common to all applications. The system message queue is used to store all events generated by the keyboard and the mouse (see Figure 3). Each application also has its own message queue. Windows places application-specific messages directly into the application queue, entirely bypassing the system queue. Writing the Message Loop A sample Windows message loop, as it appears in many Windows applications, is shown below. MSG msg; o o o while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } Windows handles messages in the following sequence: when GetMessage is called, Windows looks first in the application's message queue. If it finds no messages, it scans the system queue for keyboard and mouse events. If it still finds no messages, it checks for timer events. If there are none, the application is put to sleep. The GetMessage function suspends the application until a message arrives in its individual message queue, at which point it removes the message from the head of the queue and resumes the application. As mentioned previously, while the application is suspended, Windows gives other applications their turn with the CPU. GetMessage gives an application the option to retrieve all messages, a message meant for a specific window within the application, or a message that falls within a particular range of messages. Most of the time, you want to retrieve any message that is meant for your application. I'll cover the details of GetMessage's parameters later. GetMessage returns FALSE if the message received is a WM_QUIT message. When your application receives the WM_QUIT message, it means that your application wants to exit. A return value of FALSE should break the application out of the message loop. The second function in the message loop is TranslateMessage. When the user presses a key, Windows dissects the keypress into key-up and key-down events. Windows then translates these two events into a message that represents a single keypress event. TranslateMessage operates only on keyboard messages; it translates the up/down events into one of four messages: WM_CHAR, WM_DEADCHAR, WM_SYSCHAR, or WM_SYSDEADCHAR. Most applications can ignore the deadkey messages. The system character messages are generated when the user presses Alt with another key. Alt-key combinations usually signal Windows to take some sort of action, such as activating a menu or switching the active window to another application. All other keys will generate a WM_CHAR message. The last function used in the message loop of most Windows applications is DispatchMessage. This function merely sends the message to the window the message was meant for. The message is actually sent to the message handler for that window, which is known as the window procedure. (Experienced programmers will notice that the TranslateAccelerator function is omitted. This will come up in a later article when the accelerator keys are added to the application.) Posting versus Sending Messages Windows is not the only place where messages can originate. Any application can source a message,which it can send to itself or to any other application currently running. Two very different methods are used to pass messages to a window: posting and sending. When you post a message to a window, you are placing that message in the application queue, with the hwnd field of the message structure set to the target window's handle. If there are other messages already in the application queue, the window will not receive that message until the application processes the messages ahead of it. Yes, the window will eventually receive that message, but it could be a long time. When you send a message to a window, you are in essence calling its window procedure directly. The message completely bypasses the application queue. You send a message to a window when you want that window to act on something immediately. The two functions used to pass messages to a window are PostMessage and SendMessage. Their parameters are listed below. PostMessage(HWND hWnd, WORD message, WORD wParam, DWORD lParam); SendMessage(HWND hWnd, WORD message, WORD wParam, DWORD lParam); Note that the functions' parameters are identical to the first four members of the MSG structure (see Figure 1).You can pass Windows-defined messages or you can create your own messages to send. If you examine the list of messages defined in the WINDOWS.H file, you'll notice a symbol called WM_USER. Windows reserves all values under WM_USER for its own use; all values above WM_USER can be used by your application. You can define a new message thus: #define WM_MYMSG (WM_USER + 0x10) Now that you know the basics of messaging, the main object in Windows can be discussed-the window! Windows and WinProcs Every window that you create under Microsoft Windows has a message handler associated with it. This message handler is referred to as a window procedure or WinProc. When a message is sent to a window using SendMessage, or when DispatchMessage passes a message to a window, the WinProc of a window is actually being called. A WinProc must be declared like this: LONG FAR PASCAL WindowProc(hWnd, msg, wParam, lParam) HWND hWnd; WORD msg; WORD wParam; LONG lParam; Again, the four parameters of a window procedure correspond to the first four fields of the MSG structure. The Pascal calling convention used in this declaration is a relatively recent Microsoft innovation that makes the size of the compiled code slightly smaller. Two things distinguish a function using this calling convention from a function using the C calling convention: first, the arguments are placed on the stack from left to right, as opposed to right to left. Second, all stack maintenance is done by the function being called, not by the caller of the function as in the C calling convention (see Figure 4). If a function is called many times with the C calling convention, the same stack maintenance code must appear in the caller's code every time this function is called. This can add up to quite a bit of overhead. If the stack maintenance is done once and only once in the callee's code, the compiler no longer has to generate the stack maintenance code for each invocation of the function, saving code space and execution time. Microsoft estimates that using the Pascal calling convention reduced the code size of Windows by about 7 percent. The WinProc receives every message sent to a window. You can choose to handle a message by writing some code to react to that message, or you can pass it on to the default window procedure. The default window procedure, DefWindowProc, can handle all of the messages that can possibly be sent to a window. This WinProc has default processing for every Windows message. The Windows SDK even contains a file called DEFWND.C that lists the source code for DefWindowProc. (Note: DefWindowProc does not handle the Windows-defined control classes, each of which has its own internal WinProc.) If your WinProc does not handle a message, it must pass the message on to DefWindowProc. A skeleton of a typical WinProc is shown in Figure 5. As you can see, this WinProc handles the WM_PAINT and the WM_DESTROY messages that are sent to the window and passes all other messages on to the default window procedure for default processing. If you forget to pass these messages on, your window would certainly not behave correctly. Window Classes Every window you create in a Windows program must belong to a window class. Before you create a window, you must make sure that its class is registered. Informally, a window class defines a window's behavior and its appearance. Windows belonging to the scroll bar class look and behave in a certain way, while windows of the button class behave entirely differently. Formally, a window class is defined by the contents of a WNDCLASS data structure (see Figure 6). The class structure contains, among other elements, the name of the class, the name of the menu that will be attached to each window of that class, the number of extra bytes that will be allocated to each window of that class, the style of each window, and most importantly, the WinProc for each window of that class. By processing certain messages sent to the WinProc of a given class, you can define the behavior of each window created of that class. When you register a class, you fill a WNDCLASS structure with the desired information and call the RegisterClass function. Once an application registers a window class, the class is available to any copy of the same application that is running. In previous versions of Windows, when you registered a window class, it was available to any application that ran during that Windows session. Initialization Code The message-passing loop, window classes, and window procedures have been discussed. Now it's time to begin writing the initialization code for the application. Every Windows program must have a function called WinMain that serves as the entry point to the application. This is analogous to the main routine of a non-Windows program. However, the arguments to WinMain are very different. WinMain looks like this : int PASCAL WinMain(hInstance, hPrevInstance, lpCmdLine, nCmdShow) HANDLE hInstance; HANDLE hPrevInstance; LPSTR lpCmdLine; int nCmdShow; The instance identifier is hInstance. Under Windows, you can have multiple copies (called instances) of an application running at the same time. This does not mean, however, that you have multiple copies of the executable file in memory. Because Windows does not allow you to write into a code segment, the same code image remains unchanged for all copies of a single application. If there are multiple copies of an application running, Windows will keep only one copy of the code segment in memory. However, Windows will give each copy its own data and stack segments. The instance handle serves as a unique program identifier, so you can identify which copy of an application is running by examining the instance handle. hPrevInstance is the instance handle of the previously invoked copy of an application. The first time you run an application, hPrevInstance will be NULL. You only need the handle of the previous instance to see if you need to do application-specific initialization. You need to register your application's window classes only once, no matter how many instances of the application are invoked. (Remember that when you register a window class, it is available to all instances of that application, so it's redundant to register that class again.) lpCmdLine is a null-terminated string representing the command line with which the user invoked the application. In non-Windows C programs, the command line is parsed by the C start-up code; a pointer to each token is placed in the argv array for you. Windows does not pass the argv array to the WinMain, but you can use the following variables if you are programming in C: extern int _argc; /* argument count */ extern char **_argv; /* array of arguments */ extern char **_envp; /* environment strings */ The final parameter, nCmdShow, specifies how the application will be initially displayed; that is, as a running, visible window or as an icon. It should be used only once in your WinMain function, as the second argument to the ShowWindow function. But more on this later. At this point, you can write code to initialize the application (see Figure 7). Every window that is created using class MainWindowClass will use MainWindowProc as its window procedure. I have also specified the icon to use to represent the application when it is in the minimized state, the kind of mouse pointer or cursor desired, the brush that Windows will use to paint the window's background, the name of the menu that will be attached to the window, and the number of bytes in the per-window and per-class data structures. A brush is simply a bitmap of a pattern that Windows uses to paint with. Windows has several predefined stock brushes; I will use a solid white "pattern" to paint the background of the main window. Now that you have registered the main window class, create the main window. You must use the CreateWindow function to create any kind of window (see Figure 8). Among the arguments CreateWindow expects is the name of the class the window belongs to, the string that will be used as the window's title, the style of the window, the location of the upper-left corner of the window, the dimensions of the window, and a handle to the parent of the window, if any. I use a strange value for the upper-left corner coordinates, the width, and the height of the window. By specifying CW_USEDEFAULT, the Windows desktop manager decides where to place the main window and how big it should be. This is another example of device-independence in Windows; you don't have to specify the actual physical coordinates of the corners of the window. The main window has been created, and will now be displayed. When you create a window using CreateWindow, the window will not be initially displayed. You must call ShowWindow to make the window appear. ShowWindow(hWndMain, nCmdShow); UpdateWindow(hWndMain); For the very first invocation of ShowWindow (that is, the call to display your main window), you must give nCmdShow as the second parameter. The UpdateWindow function sends a WM_PAINT message to your main window, telling it that it must redraw the contents of its client area. In the sample WinProc (see Figure 9), you ignore the WM_PAINT message, letting the default WinProc handle the drawing. The last thing to do in the main routine is enter the famous message loop. This loop will fetch messages from the application's message queue and send the messages to the WinProc for the main window. The loop will be exited when a WM_QUIT message is received, which is usually the result of the user closing the main window. while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; /* the return code from PostQuitMessage... */ } Finally, a simple WinProc is added to the code (Figure 9). All this WinProc does is post a WM_QUIT message when it detects that the main window is being destroyed. This is done by calling the PostQuitMessage function. All other messages are passed on to the default window procedure. This code merely displays the main window on the screen and waits for the user to close the window. Summary So far I've introduced the message-passing paradigm that your applications must follow, and discussed window classes and window procedures. I began the application by showing the initialization code, which entailed registering the window classes, creating the initial window, and entering the standard Windows message loop. In the next installment, I will begin to fill in some of the information that goes into the resource file, such as the definition of the icon, and show you how to compile and link a Windows program. I will also discuss menus and set up the menu system for our application. ────────────────────────────────────────────────────────────────────────────

Volume 5 - Number 5

──────────────────────────────────────────────────────────────────────────── Creating a Multipage Data-Entry and Reporting Library for Windows[tm] Version 3.0 Kevin Welch Many traditional character-based applications that are ported to the Microsoft Windows[tm] graphical environment require validated data entry and reporting more complex than what Windows provides. These data-handling tasks, which are commonplace in corporate computing, are time consuming and tedious to implement in Windows1, typically requiring a great deal of programming. This article, the first of two parts, addresses the problem by presenting a tool that can be easily adapted to a variety of data-entry and reporting situations. It shows how to create a dynamic-link library, FORM.DLL, that supports multipage data entry and reporting using a series of linked dialog box and report templates specified in a resource file. Using this DLL, you can create complex multipage data-entry forms and reports without a single line of C programming. Design The initial design philosophy behind the library is to encapsulate a single, multipage data-entry form and report into one DLL. Then, any application desiring to use a specific form or report only needs to load a particular library and call the appropriate editing or reporting function. An additional advantage of this architecture is that applications could dynamically link to specific forms and reports based on user input. The second design philosophy is to separate the supporting code from form and report definitions. This process is absolutely essential if new forms and reports are to be created quickly and easily. As a result of this design goal, all data-entry form and report specifications need to be extracted by the program from a data file associated with the library. For ease of implementation, this could best be accomplished by storing the form and report specifications in the resource file that is associated with the DLL. The third design philosophy is to make using the library as simple as possible for the host application. Based on this goal, the library needs to contain only two entry points-one for data entry and one for generating formatted reports. Now that you know the design philosophies behind the library, you can focus on the two entry points. The first entry point in the library, FormEdit, provides formatted data entry; it is defined as follows: HANDLE FormEdit( hWndParent, hData ); The parameters and return values of this function are defined below. hWndParent A handle to the parent window of the data-entry dialog box. This dialog box would be automatically centered inside the parent window using this handle. hData A handle to a global block of memory, which contains formatted text that defines the data to be associated with each entry field. Each entry should be terminated with a single NULL character, and the last entry by an extra NULL character. The data block should be formatted in the following manner: FIELDNAME:DATAVALUE <NULL> o o o FIELDNAME:DATAVALUE <NULL> <NULL> In cases where a new form is being entered, this data block should contain default values for all required fields. If no fields are required, then the handle provided should reference a data block consisting of a single NULL character. Not all fields need to be specified; the system will automatically assume that the unspecified fields are not defined. The actual association of data values to specific data-entry fields in each form is defined by cross referencing the field name to a specific control ID. This cross referencing is defined by a table defined in the resource file associated with the library. There is more on this later in the article. <return value> The value returned by this function should be a handle to the updated data block. If a NULL handle is returned, then it can be assumed that the user canceled data entry. Calling FormEdit causes a dialog box (see Figure 1) to be displayed. This dialog box contains a data-entry area (inside which the actual data-entry form is displayed), a field comment area, push buttons that manage the data-entry process, and a page-selection area to use with multipage forms. When this dialog box is initially displayed, the caption automatically indicates the data-entry form in use and the currently selected page. The data-entry area contains various fields and data values associated with the first page of the multipage form. When developing each form, the specification and arrangement of the various controls used to display data inside each page is performed using the dialog editor provided with the Windows Software Development Kit. Each dialog box you define should be of identical dimensions. It can contain any combination of Windows controls you desire. When you have specified each dialog box page (remembering that what you define will fit in the data entry area), you can integrate the template into the resource file associated with the library. To assist the user in selecting a particular data-entry page, the currently selected page number is displayed in the lower-right corner of the main dialog box. Rather than developing a new control for this purpose, the installable Page control described in "Extending the Windows[tm] 3.0 Interface With Installable Custom Controls," MSJ (Vol. 5 No. 4) will be used. With this control, the currently selected data-entry page is indicated by a reverse-video page icon. If desired, the user can select a different data-entry page by clicking the mouse inside another page icon or by using the keyboard cursor keys to move the selector to a different page. When the user enters data inside the form, the field comment area in the lower-left corner of the main dialog box contains a short description of the currently active data field. Each time the user presses the Tab key, the input focus moves from field to field and a new comment is displayed in the field comment area. If the user presses Tab from the last field in the form, the next page is automatically displayed. Similarly, pressing Shift-Tab in the first field of the form displays the previous page. Each control in the form is subclassed to facilitate the use of the Page Down and Page Up keys for moving to the next and previous data entry forms. This assists the user in rapid data entry. Now that the data-entry functionality has been defined, the reporting portion of the library can be specified. The second function in the library will create a formatted report using the data provided: HANDLE FormPrint( hWndParent, hData ); The parameters and return values of this function are almost identical to that of the FormEdit function. The only difference is that if a NULL handle is returned, you can assume that the user canceled the report-generation process. Calling this function generates a formatted report on the default output device using the data provided. The report will be formatted according to the report specifications defined in the resource file associated with the library. While the report is being printed, a dialog box is displayed. Besides indicating that a report is being generated, this dialog box allows the user to cancel the report. Now that the entry points in the library have been defined, you can examine the mechanisms used to specify each form and report. As already mentioned, to facilitate the creation of new multipage data-entry forms and reports, the dialog box and report specifications are defined in the resource file associated with the library. Because of this, a new form or report can be created merely by changing the RC file and recompiling it with the library, using a different name for the new DLL. The specification of the resource file associated with the library assumes the creation of an include file (FORM.H) that defines the identifiers used throughout the RC file. These identifiers are specified so that each field name in the form or report is assigned a unique number starting at 0100H, regardless of which data-entry page they appear in. The identifiers in the range 0000H to 00FFH are reserved by the library for internal use. An example of the RC file required by FORM.DLL is shown in Figure 2. The library used this file to produce the three page data-entry form in Figure 1. (Assume all the IDn_FIELD values are defined in an associated FORM.H.) The FORM RCDATA portion of the RC file is used to specify the number of pages in the data-entry form and the main title of the form. For example, if a value of 3 is specified for the number of pages, the library assumes that three dialog boxes of identical size will be defined using the names Page1, Page2, and Page3. The REPORT RCDATA portion of the RC file is used to specify the report to be associated with the entire data-entry form. The specified data consists of a series of NULL-terminated text lines. Embedded inside each line can be C-like formatting statements in the following syntax: %FMTSPEC = % [alignflag] [ [width] [.precision]]FIELDNAME alignflag text alignment flags (in this implementation, limited to left and right justification) width minimum number of characters to output precision maximum number of characters to output (Note: in this implementation, you are limited to string data). Using these embedded formatting statements, the library will create the report using the field names specified when defining specific data values. Additionally, linefeed and carriage return characters will be automatically appended, and page breaks will be inserted as required. To provide additional flexibility when creating reports, several predefined field names will be provided. In this implementation, it is useful to have the following field names: LINENUMBER = current line number PAGENUMBER = current page number Finally, to provide greater reporting flexibility, it is useful to be able to specify page breaks and simple headers or footers. This will be accomplished using meta commands located in the first column: %FORMFEED %HEADER [header text including formatted data fields] %FOOTER [footer text including formatted data fields] The STRINGTABLE portion of the RC file is used to specify the relationship between each field name, the internal identifier, and the associated field comment text. Whenever a particular data entry form is displayed, the string associated with each internal dialog box identifier is retrieved and the corresponding field name is defined. The data block that is provided during the FormEdit call is then searched for a matching entry. If one is found, the associated data value is then displayed inside the related control (see Figure 3). Additionally, when the user enters data into a form, a comment is always displayed at the bottom of the main dialog box that describes the field that has the input focus. This comment, defined in the string table, can be used to indicate whether the field is required, to indicate what data format is expected, or to elaborate on the exact meaning of the information being entered. The remainder of the RC file contains dialog box definitions relating to each page of the multipage data entry form. Each definition is named Page1, Page2, and so on, up to the number of pages specified in the FORM RCDATA portion of the RC file. The caption associated with each dialog box is appended to the title of the main dialog box. This lets you define a title for the entire form and for each data-entry page. When creating each data-entry dialog box page with the dialog editor, the only limitations are that each page have the same dimensions and that each control use an identifier that is defined in the FORM.H include file and referenced in the string table portion of the RC file. Form Library Now that the structure and contents of the form library have been defined, you can investigate the implementation. In this article you will implement the basic library with all associated entry points, but the actual mechanics will be left for the next installment. Like most other DLLs, the form library is constructed from the following kinds of files (see Figure 4): ■ FORM make file ■ FORM.DEF module definition file ■ FORM.H include file ■ FORM.RC resource file ■ FORM0.ASM entry point module ■ FORM1.C initialization module ■ FORM2.C form edit module ■ FORM3.C form print module The make file, FORM, is almost identical to that used by any Windows application except that it uses the -Aw and -D_WINDLL compile flags. These flags instruct the C compiler to generate code in a DLL format with SS != DS. FORM.DEF is the module definition file. This file defines the module as a library, imports the page selection control initialization function, and exports the various form library entry points. Because the form library uses the page selection control, you need to have the page control DLL with its associated import library present. The FORM.H file used for the first version of FORM.DLL does not contain field identifier definitions, but it does have prototypes for the two library entry points, FormEdit and FormPrint. In this implementation, FORM.RC serves as a placeholder. In the next article the contents of this file will be expanded to contain the form, report, string table, and dialog box definitions. FORM0.ASM is the assembly language entry point to the DLL. This module calls the FormInit function contained in FORM1.C, which initializes the library. The FORM2.C module contains the FormEdit function. For now, this function displays a message box that indicates the form is being edited, and returns a handle to the formatted data block provided. The last module, FORM3.C, contains the FormPrint function. Almost identical to the FormEdit function, it also displays a message box to indicate a report is being generated. Form Viewer In order for this form library to be tested, you need to construct an application that lets you load an arbitrary library and test it with manually entered data. The form viewer is such an application (see Figure 5). With it you can manually load a form library, enter a block of formatted data, and send the formatted data to either the FormEdit or FormPrint functions. Figure 6 shows the relationship between the Viewer and a library being tested. The form viewer is constructed from the following files (see Figure 7): ■ VIEWER make file ■ VIEWER.DEF module definition file ■ VIEWER.H include file ■ VIEWER.RC resource file ■ VIEWER.C initialization module As with most other include files, VIEWER.H contains the definitions associated with the application. Of these definitions, note the specification of two property list values used by the Viewer window to store a child window handle and the handle to the active form library: #define VIEWER_HWNDEDIT MAKEINTRESOURCE(100) #define VIEWER_HFORMLIB MAKEINTRESOURCE(101) Also note the definition of the ordinal numbers used to access the various form library entry points: #define FORM_INIT MAKEINTRESOURCE(2) #define FORM_EDIT MAKEINTRESOURCE(3) #define FORM_PRINT MAKEINTRESOURCE(4) Finally, note the form FormEdit and FormPrint function prototypes. Since these two functions are indirectly referenced by the Viewer application, the two prototypes are defined using function pointers: typedef HANDLE (FAR PASCAL * LPEDITFN) ( HWND, HANDLE ); typedef HANDLE (FAR PASCAL * LPPRINTFN) ( HWND, HANDLE ); VIEWER.C contains the code associated with the application. Of this, three functions need mentioning. The WinMain function is responsible for the definition and registration of the Viewer window class. Note that the background brush is NULL. Because the ViewerWndFn creates a child edit window inside its client area, the window background does not need to be updated. Once the Viewer window class has been registered, the WinMain function will create the Viewer window and retrieve/dispatch all associated messages. This continues until the Viewer window is destroyed. The ViewerWndFn function is responsible for a number of tasks. When the window is initially created, it defines the window property list and creates a child edit window inside its client area. The resulting child window handle is saved using one of the property list values and is used when the window is resized or a formatted data block is retrieved. When the user selects the Form Open... menu option, ViewerWndFn calls OpenDlgFn. This brings up a dialog box in which the user can specify which form library to use. When the user selects the Form Edit... or Form Print... menu option, the ViewerWndFn is responsible for dynamically calling the appropriate form library function. The process of dynamically calling a library function is accomplished as follows: hForm = GetProp(hWnd, VIEWER_HFORMLIB); if ( hForm ) { lpEditFn = GetProcAddress(hForm, FORM_EDIT); (*lpEditFn)(hWnd, hData); } Finally, when the viewer window is destroyed, the ViewerWndFn is responsible for releasing the current form library (if present) and removing all associated window properties. The OpenDlgFn function centers the dialog box when displayed and loads the library specified by the user. If the load library operation is successful, the previous library (if present) is released and the viewer window caption is updated. Once you have created all the Viewer files and compiled them, you can start using the Viewer with the Form library described in the previous section. Although you won't be able to start editing data or generating reports in this installment, you will at least be able to load the library. With the code that accompanies the next installment, the Viewer will be fully operable. Conclusion Now that the basic framework has been defined for the Form Library and the Viewer application is in place, you can start implementing the required internals. The next installment will focus on these internals, creating a complete library that you can easily adapt to your needs. u Spawn Programs from Within Your DOS Application Using Almost No Additional Memory Marc Adler Many PC applications have a command that permits you to escape to the operating system without having to terminate the application. While working on a document with a word processor, you might suddenly realize that one of the files you need to import into your document is archived. This means you have to go to the operating system, dearchive the file, and return to the point where you left off in the word processor. For another example, many program editors allow you to compile the file that you are editing. To do this, the editor must load the compiler into memory, run the compiler, and return you to the point in your file where you were editing. The Microsoft C run-time library provides functions that allow you to suspend your program temporarily, load another program into the memory area that the current application does not occupy, and run the new program. To run a new program, simply make the following call: system("myapp"); The system call will load a copy of COMMAND.COM above your application. The system will pass its string argument to COMMAND.COM to be used as the command line. COMMAND.COM will then attempt to execute this command. However, if myapp is a large program, then COMMAND.COM will probably be unable to execute it. This is usually because there is not enough contiguous unallocated memory left in your system to load the new program. Resident in low memory are the ROM BIOS, DOS device drivers and buffers, and the resident portion of COMMAND.COM, which can be from 20Kb to 60Kb in size depending on which version of DOS you are running. Any Terminate-and-Stay-Resident (TSR) programs will also take up memory. Last is the code and data space taken up by the current application. Most applications allocate and free chunks of memory while running, causing a problem known as fragmentation. (For more on memory management and fragmentation, see "Creating a Virtual Memory Manager to Handle More Data in Your Applications," MSJ Vol. 4 No. 3.) Memory is fragmented when free blocks of memory are scattered among allocated blocks. Unfortunately, DOS1 does not perform garbage collection and compaction on its own, and neither do most applications. The result is that DOS is sometimes unable to load a program not because of a lack of free memory, but because of a lack of contiguous free memory. One solution is to take a complete snapshot of the application's memory image, store it somewhere, free all of the memory taken up by the application, and run the new program. When the new program is finished, the memory snapshot is loaded back into RAM. Then the old application should be exactly as it was before it was swapped out-byte for byte. This approach has been implemented in software developed by Thomas Wagner of Ferrari Electronic GmbH in Berlin, West Germany. (The code can be downloaded from any MSJ bulletin board.) One of the nice features of this code is that it permits you to swap your program to either a hard disk or expanded memory. If you have plenty of expanded memory free, the swapping is very fast, and the user perceives almost no delay. When a program is swapped out, it leaves only about 1Kb of code in memory. Swap API Applications must make a single call to swap a program out and load another one in. The function is defined as follows: int pascal do_exec(char *pszCommand, char *pszArgs, int iMode, unsigned uMemNeeded, char **pEnviron) The name of the file to execute is pszCommand. For instance, using the string "cl" would invoke the C compiler. If an absolute pathname is not given, this function will search for the program in the current directory, and then search the directories specified in the DOS PATH environment variable. Do_exec will try to locate a file with a COM extension first and then a file with the EXE extension. The function will not attempt to execute batch files. If an empty string (that is, a string with \0 in the first byte) is passed as the first argument to do_exec, a copy of COMMAND.COM (or whatever command interpreter is specified in the COMSPEC environment variable) will be executed. The second argument to do_exec, pszArgs, is a string containing the arguments to the program. If you're invoking the C compiler, pszArgs might be a string like this: /c /AL foo.c The third argument, iMode, tells do_exec how to swap out the old application. If iMode is 0, you will be returned to the DOS level instead of the old application when the new application is terminated. If iMode is 1, EMS will not be used for swapping. If iMode is 1 (which it should be in most cases), the function will return to the old application after the spawned program terminates. The fourth argument, uMemNeeded, specifies the maximum amount of free memory required by the new application. If this amount is greater than the amount of contiguous free memory in the system at the time of the swapping, the old application will be swapped out to disk or to EMS. However, if there is enough free memory, the old application will not be swapped. This means swapping will never take place for a uMemNeeded of 0, while a value of FFFFH for uMemNeeded will always cause swapping to take place, regardless of the amount of free memory available. The last argument, pEnviron, specifies the environment to pass to the new program. If this argument is NULL, a copy of the parent application's environment will be passed to the new program. Otherwise, pEnviron must point to an array of pointers to environment strings, with the final member being NULL. The sample call to do_exec, shown below, swaps out the current application and spawns a copy of the C compiler: int rc; rc = do_exec("cl", "/c /AL foo.c", 1, 0xFFFF, NULL); Now let's examine the C function do_exec (see Figure 1) and the lower level assembly language routine do_spawn (see Figure 2). For the most part, these routines prepare data structures for the DOS EXEC function, which is interrupt 21H, service 4BH. (For more on this function, see "Everything You Always Wanted to Know about the MS-DOS EXEC Function. . . ," MSJ, Vol. 4 No. 1.) The first thing that do_exec does is copy the argument string into a separate buffer and prefix the buffer with a 1-byte length indicator. The DOS EXEC service expects to see the parameter list in this form. Unfortunately, DOS puts a 128-byte restriction on the length of the parameter string. In addition, a length specifier is placed before the name of the command to execute. If the calling program specifies a custom environment to pass to the new program, you must allocate a buffer large enough to hold all the environment strings. Each NULL-terminated environment string is copied in sequence into the buffer; the final environment string is terminated with two NULL bytes. The amount of free memory available can be determined by calling the DOS memory allocation function interrupt 21H, service 48H. If you pass the value FFFFH as the number of memory paragraphs to allocate, this function will always fail (since FFFFH x 16 bytes = 1Mb of conventional memory). However, as a result of this, the function instead returns the amount of memory available in the system. If this amount is less than what the new program needs, the old program will need to be swapped out. While swapping the old program to disk, a temporary file with the unlikely name of $$AAAAAA.AAA will be created. The DOS environment is searched for a variable named TMP or TEMP; if found, the swap file is created there. These environment variables usually specify a directory that applications can use to store temporary information in (such as virtual memory swap files or spooler output) while they are running. If these environment variables are not found, the swap file is created in the current directory. Next the assembly language routine do_spawn is called to perform the actual swapping. The do_spawn routine is called with the iMode argument, the name of the swap file, the name of the command (with the length-specifier byte), the parameter string (also with the length-specifier), a copy of the environment, and the length of the environment. Here is an overview of what do_spawn does. First, do_spawn traverses the memory control blocks allocated to the current program, adding up the size of all the blocks in the process. If swapping to expanded memory, the routine determines how many 1Kb blocks of expanded memory to allocate and allocates the expanded memory pages. If instead you're swapping to disk, do_spawn creates the temporary disk file. Next, the routine traverses the memory control block chain, writing the contents of the memory allocated to the control block to disk or to expanded memory. The routine then moves its reload code to low memory and executes the new program. When control returns from the executed program, do_spawn reads the memory image back in from disk or from expanded memory and resumes execution of the original application. Memory Control Blocks To understand how do_spawn works, you must be familiar with memory control blocks. When a program asks DOS for some memory, DOS allocates the memory, then returns the address of the free block to the program. DOS places a 16-byte data structure, called a memory control block (MCB), before the allocated memory area. The MCB is used to record information about the allocated block. The structure of an MCB is shown in Figure 3. The first byte in the MCB indicates whether the block is the last block in the MCB chain: if it is not the last block in the chain, it's the letter M; if it is the last block, it's the letter Z. The next two bytes contain the program identifier of the program requesting the memory allocation. Every program DOS loads is given a 256-byte header called a Program Segment Prefix (PSP). The PSP contains information such as the length of the program, a copy of the program's command string, and so on (see Figure 4). The PSP is always located on a paragraph boundary. Since there is a unique PSP for each program that is running, it's convenient to use the segment where the PSP is located to identify a program. The next two bytes of the MCB contain the number of 16-byte paragraphs allocated to this block. The MCB's remaining 11 bytes are not used by DOS. The do_spawn routine takes advantage of this, using the last 8 bytes of the MCB to store some of its own information. It is always dangerous to rely on the undocumented features and data structures of DOS, but do_spawn has been used successfully under DOS 2.1 through DOS 4.01. As mentioned, do_spawn will move its reload code down to low memory, starting at the 92nd byte of the swapped program's PSP. Do_spawn can do this because the information that had been at PSP+92 is not needed while the spawned program is running. Therefore, do_spawn saves it in a special area. Then the reload code and data from the swapped program can be stored in PSP+92 while the other program is running. Do_spawn traverses the program's MCB chain, gathering information about each memory block. As mentioned above, the last eight bytes of each MCB are used to store some of do_spawn's information. The revised structure of the MCB is shown in Figure 5. The do_spawn routine starts at the first MCB attached to the program, located 16 bytes before the PSP. It then marches down the MCB chain, and for every MCB that has been allocated to this program, it records information about the MCB in the last eight bytes. It also determines the number of 1Kb blocks that must be allocated from expanded memory to store the old program in. You might ask why do_spawn needs to create its own chaining information, given that DOS maintains the regular MCB chain. Assume that a TSR program is active in your system while this program is running. Since a TSR can ask DOS for a block of memory at any given moment, another program's memory blocks might be found in this chain. DOS could easily allocate a block of memory from your program's memory space and give it to the TSR. To ensure that your program swaps out memory that belongs to it and not to another program, do_spawn must create a separate chain of MCBs instead of using the full DOS chain of MCBs. Once the MCBs are scanned, the swapping is performed (presuming that swapping was specified in the call to do_spawn). If do_spawn wants to use expanded memory to hold the swapped memory, it must see if an expanded memory manager is installed and ensure that there are enough expanded memory pages to hold the program's memory image. Otherwise, it has to create a temporary file on disk and swap the program image to that file. EXEC Function Now that the entire memory image of the program has been swapped to either expanded memory or to disk, you must prepare a data structure called a parameter block for the DOS EXEC function. This parameter block (see Figure 6) is passed as an argument to the EXEC function. If you specified that the spawned program should use the first environment, segEnvironment will be set to 0. If not, do_spawn puts the segment of the buffer where the user-defined environment string is stored into segEnvironment and adds the size of the environment string to the number of bytes that will stay resident when the second program is spawned. The do_spawn routine moves the argument string to a storage area in low memory and assigns the address to pszArguments. It also relocates the command string there. Then do_spawn fills the two dummy File Control Block (FCB) structures in the parameter block with zeros, parses the argument string into both FCBs, and assigns their addresses to pFCB1 and pFCB2. Finally, We Swap The memory has been swapped and the EXEC parameter block has been prepared. All that is left to do before the EXEC is to reduce the program to the smallest memory image possible. Things that must be preserved in memory include a small data area for do_spawn that contains information about the program that will be swapped, such as the EMS memory handle or the name of the temporary file, the saved code from PSP+92, and so on. The code that performs the actual EXEC must also be saved as well as the code that reloads the memory image. If the latter two items are not preserved, when the spawned program terminates, control will be returned possibly to garbage code. This information is stored starting at PSP+133; it takes up about 1Kb. (You can play with the PSP like this because the first thing that do_spawn did was save the contents of PSP+92 in a storage area.) All systems are go for DOS to EXEC the new program. Do_spawn merely passes the EXEC function the name of the command to execute and a pointer to the EXEC parameter block, and off we go. We can only hope that some TSR has not fragmented memory too much and that the new program has enough contiguous memory to load. When the spawned program terminates, it returns control to do_spawn's reloading code, which was previously placed in low memory. The reloading code determines whether the original program was swapped to disk or to expanded memory, and then opens the appropriate device. It reloads the memory image, retrieves the return code from the EXEC function, deletes the swap file, and returns control to the point in do_exec where do_spawn was called. The possible return codes from do_spawn and do_exec are listed in Figure 7. If your application gets an error code from do_exec, you might want to call do_exec again, this time asking do_exec to load a copy of COMMAND.COM and letting COMMAND.COM try to execute the command. You would do this if you were trying to execute a BAT file, or if your command specified some input or output redirection or piping. COMMAND.COM must be used to interpret and execute the commands in a batch file or to handle redirection, which the DOS EXEC function does not recognize. Sample code to do this is shown in Figure 8. Enhancements The code presented here permits applications to load and run specialized programs, making the overall application environment more powerful. These routines could be the kernel of a more sophisticated task-switching system for specialized applications. You'd only have to add data structures that will keep track of program information associated with each swapped program and a user-interface mechanism to allow the user to switch between the various tasks easily. Based Pointers: Combining Far Pointer Addressability and the Small Size of Near Pointers Richard Hale Shaw Based pointers, a new data type introduced in Microsoft C Version 6.0, are a practical alternative to traditional pointers for some applications. Based pointers combine the size of near pointers with the ability of far pointers to address objects in segments outside your program's default data segment; that is, far segments. Their name comes from the way they are used-you specify the segment on which they are based. When you use a based pointer, the compiler usually generates code to load the ES register with a specific base (the segment upon which the pointer is based). You probably won't use based pointers to address objects in your program's default data segment, but they are useful when working on objects in parallel. They offer some distinct advantages over far pointers, including a flexibility that far pointers lack. A brief review of far, near, and huge pointers follows. Near Pointers Near pointers are native to memory models that use a single 64Kb segment for code or data. For data pointers, this includes the tiny, small, and medium memory models, in which data segments don't exceed 64Kb. Near code pointers (function pointers) are found in models that limit code to 64Kb, like the tiny, small, and compact models. For the purposes of this article, however, further discussion focuses on pointers to data. Near pointers are simple 16-bit pointers that contain an offset to an object in a program's data segment. The compiler combines the program's data segment with the offset to create a 32-bit address from the near pointer. Since the DS register contains this segment value most of the time, there is usually no need to reload this register. Besides, a number of assembly language operations assume that DS contains a data object's segment address. While near pointers are the smallest and fastest pointers to use, they are also the most limited. You can access only 64Kb of data with them, the 64Kb in the program's data segment. If you compile a small model program and attempt to increment a near pointer past the 65,536th byte, the pointer will be reset to 0. For example, the following code leaves the pointer set to the base of the data segment, byte 0. // set pointer to last byte char _near *p = (char _near *)0xffff; p++; // increment pointer (Note that the _near keyword is unnecessary if you are compiling with the small or tiny model, just as _far can be omitted when compiling with the large model.) Since near pointers don't usually require segment register reloads and use only 2 bytes, they have an advantage of size and speed over far pointers. However, their ability to address only the current data segment's 64Kb limits their usefulness in large or complex applications. Far Pointers A far pointer is a variation of a near pointer. Rather than let the compiler use the program's data segment as the segment portion of the address (as near pointers do), the segment value is stored in the pointer. Thus, far pointers use 4 bytes to store a complete 32-bit address. Since this address contains both the segment and offset of the object being addressed, far pointers can be used by any memory model. However, far data pointers are native to the compact and large models. Because far pointers contain both a segment and an offset, the segment portion of a far pointer must be reloaded when it is used. Thus, they are slower than near pointers. On the other hand, far pointers can reference any data object, anywhere in memory. Far pointers cannot address more than 64Kb at a time, or access all of a data object more than 64Kb in size. Far pointers behave like near pointers as far as address arithmetic is concerned. When you increment a far pointer, only the offset is affected: if you increment it past the end of the segment, the offset will wraparound and begin incrementing from 0 while the segment portion remains the same: // set to B800:FFFFH char _far *fp = (char _far *)0xb800ffff; fp++; // increment to B800:0000H In this example, the far pointer is set to the last byte of the segment at B8000H. Once the pointer is incremented, the pointer is set to the first byte of the segment, at B800:0000H. Thus, like near pointers, far pointers can only contain offset values from 0 to 65535. While they occupy more space and are slower to manipulate, they can reference any object in any segment, as long as the object itself does not extend beyond the end of the specified segment. Huge Pointers Huge pointers are the only true pointers in the K&R sense of the word. While physically the same as far pointers, storing a 32-bit address in a 4-byte space, huge pointers behave differently. When you increment a huge pointer beyond the end of a segment, it increments into the next segment. Therefore, huge pointers can address any object of any size-even objects that extend over several segments. For instance, the following code causes the huge pointer to address the next physical byte, from B800:FFFFH to C800:0000H. // set to B800:FFFFH char _huge *hp = (char _huge *)0xb800ffff; hp++; // increment to C800:0000H It does not wraparound like near and far pointers. Using huge pointers, you can write a program to access every memory location in your machine. // define 1 megabyte macro #define MAXBYTES (1024L*1024L) main(void) { long l; unsigned char _huge *uch; FP_SEG(uch) = 0; FP_OFF(uch) = 0; for(l = 0L; l < MAXBYTES; l++, uch++) *uch = \0'; } While this is a nonsensical program (nonsensical in that it would attempt to overwrite every writable memory location in your PC, including itself and DOS), it illustrates the addressing ability of huge pointers. This is why huge pointers are the only pointers supported by Microsoft C that behave like pointers in a flat-addressing scheme. Huge pointers are always slower, since the compiler must generate code that can perform pointer arithmetic on the entire 32-bit value, unlike near and far pointers where address arithmetic is only performed on the offset component. Near and far pointers are strictly products of the segmented architecture of the 80x86 CPU family on which DOS PCs are based1. Clearly, pointers in Microsoft C have specific attributes. Huge pointers can address any object of any size; near and far pointers cannot address any object outside of their designated segments. Far and huge pointers contain a 32-bit address and occupy 4 bytes; near pointers occupy 2 bytes and contain a 16-bit address. Based Pointers As mentioned, based pointers combine the intersegment addressing features of far pointers with the size and possibly the speed of near pointers. Like near pointers, based pointers occupy only 2 bytes to create a 16-bit offset; like far pointers, they can address up to 64Kb of any segment. However, far addressing still requires a 32-bit address, while based pointers contain only the 16-bit offset. The base segment of near pointers is implicitly the program's data segment. The "base" of far and huge pointers is contained in the segment portion of the pointer. When using based pointers, you specify the base, but it is not stored in the pointer itself. Where does the segment portion of a based pointer come from? It's actually a compiler trick. Using the new based pointer notation of C 6.0, you can declare a based pointer and specify its segment base. Then, depending on what type of based pointer you use, the compiler "knows" from the declaration which segment the based pointer is based on. There are six types of based pointers: segment based pointers, variable based pointers, void based pointers, named segment based pointers, pointer based pointers, and pointers based on self. When it encounters a based pointer in use, the compiler generates code to load a segment register (usually ES) with the pointer's associated segment base. Then the compiler proceeds with the operation entailing the based pointer. Of course, the based pointer itself supplies the offset portion of the address. Segment Based Pointers Suppose you wanted to base a pointer on the segment associated with the video display of an EGA or VGA adapter: _segment videoseg = 0xb800; unsigned _based(videoseg) *bpvid = 0; This small piece of code does several things. It creates videoseg, which is a _segment data type. The _segment data type, new in C 6.0, is similar to an unsigned integer. It is used to store a segment value for pointers based on it. The next line of code uses the _based keyword to create a based pointer, bpvid, whose base is the segment stored in videoseg. The _based keyword must be followed by a valid base expression in parentheses. However, you cannot use an unsigned integer constant as below: // WRONG! unsigned _based((_segment)0xb800) *bpvid; For illustrative purposes, here's how to use a far pointer to address the same location: unsigned _far *fpvid = (unsigned _far *)0xb8000000; While this far pointer is set to the same location, it's not the same as the based pointer. To use fpvid on another segment, you have to reset the entire pointer (or at least use the FP_SEG macro to reset the segment portion of the pointer). To use bpvid on another segment, you only have to change videoseg. The big advantage here, of course, is that you can create an entire set of pointers based on the same _segment variable, and change their base by making a single change to the _segment variable. In the following code, pspptr is a pointer to the program segment prefix (PSP) structure, but it's based on the segment value stored in psp. #include<dos.h> _segment psp; typedef struct _psp { unsigned int20; unsigned allocblockseg; char reserved0; unsigned char dosfunctdispatch[5]; unsigned long int22; unsigned long int23; unsigned long int24h; char reserved1[22]; unsigned envseg; } PSP; PSP _based(psp) *pspptr = 0; If an application knows the segment address of another application's PSP, it can access it by assigning that segment address to psp. Then pspptr can reference members of the other application's PSP. Thus, pspptr>envseg provides the segment address of a program's copy of the environment. For a more explicit example, see the listings of ENVIRON.C (see Figure 1). You can compile this program using EN.BAT (also in Figure 1). ENVIRON.C uses a based pointer to access its own PSP and another based pointer (whose base is a segment variable set to the environment set address in the PSP) to access the program's copy of the environment strings. Variable Based Pointers You can also use _segment as a cast to extract the segment address of an object. Thus, a based pointer can be based on the storage segment of another object. In the following code, cbpsv is based on the segment in which count is stored, and cbpsp is based on the segment that contains cp. unsigned count; char *cp; char _based((_segment)&count) *cbpsv; char _based((_segment)cp) *cbpsp; The _segment cast is read as "segment of" in this context. This usage differs from pointers based on other pointers, which is discussed below. Using explicit segment addresses is only one way to declare a based pointer's base. You can use the segment's name or derive the segment from the address pointed to by a pointer. Based pointers that are members of a structure can be based on the segment in which the structure resides. Or, as shown in the next section, you can omit the segment declaration altogether. Void Based Pointers You don't always have to include a based pointer's segment with the pointer declaration. unsigned _based(void) *ubpv = 0; Here, ubpv is a based pointer whose base segment is omitted from the declaration. But a based pointer must be based on something! This method lets you defer naming the segment only until the based pointer is used. Whenever you reference a void based pointer, you must specify a segment value. _segment videoseg = 0xb800; unsigned _based(void) *ubpv = 0; *(videoseg:>ubpv) = (0x70 << 8) + H'; This piece of code uses videoseg (declared earlier) to provide a base for ubpv. The code displays a reverse-video H at the upper-left corner of an EGA or VGA screen. The new ":>" base operator lets you combine the segment address stored in videoseg with the offset in ubpv. You can only use the base operator on pointers based on void, however. If you attempt to use it with a based pointer not based on void, the code wilol not compile. The big advantage of void based pointers is that you can set them to a particular offset and then use them with different base addresses. You can also access the same data in different segments using the same pointer: simply reference a different base each time. (Rather than changing a _segment variable, you can change the base whenever you use the pointer.) Most of the time you'll want to base a based pointer on a specific segment address, as discussed above. But you may instead want to base it on the address of a named segment. Named Segment Based Pointers Programmers who are writing large or medium model applications often make each program module a named segment, with near calls and near data inside the module. While this is a useful trick, it's nearly impossible to reference the named segment outside of the module. Modules such as these are usually named by the compiler via a compile line switch. The based pointer support in C 6.0 lets you create new segments on-the-fly. Additionally, it lets you designate which objects and pointers are based in these segments. For instance, the following code creates a pointer whose base is the program's code segment: void _based(_segname("_CODE")) *vbpc; The next line of code creates a pointer based on the program's data segment: void _based(_segname("_DATA")) *vbpd; This isn't really that useful, since it's the functional equivalent of a near data pointer: void *vptr; However, the ability to base pointers on specific named segments can be extremely useful. The _segname operator must be followed by a string that names a segment in parentheses. This can be any of the predefined segment names used in Microsoft C such as _CODE, _DATA, _CONST, or _STACK. If you use a segment name that differs from these, the compiler will create a new segment. For instance the following creates a pointer based on a new segment, _NEWSEG: void _based(_segname("_NEWSEG")) *vbpm; The compiler, upon encountering this line of code, will create the new segment. But because it doesn't do you any good to base a pointer on a segment that doesn't contain anything, you can also embed objects in the new segment. For instance, the following will create a string array that is stored in _NEWSEG and referenced via newseg_message. char _based(_segname("_NEWSEG")) newseg_message[] = "This string is stored in the segment, _NEWSEG"; Note that if you make newseg_message a pointer instead of an array, you'll create a based pointer to a near string: char _based(_segname("_NEWSEG")) *newseg_message = (char _based(_segname("_NEWSEG")) *) "This string is stored in the segment, _NEWSEG"; The cast is required to eliminate a compiler warning. If you've nearly exhausted data space in a small model program, you might be able to avoid moving to the compact model by placing some of the data in another segment. For instance, if your code segment is still well below 64Kb, you can store some of the program's data there. This code stores the string at codeseg_message in the _CODE segment. char _based(_segname("_CODE")) codeseg_message[] = "This string is stored in the _CODE segment"; Keep in mind that any data referenced from another segment may require that a segment register be reloaded, so access to this data will be slower than that in the default data segment. Pointers Based on Pointers Suppose you want to base a pointer on the address another pointer points to. unsigned _near *ip; unsigned _far *fip; unsigned _based(ip) *ibpip; unsigned _based(fip) *ibpfip; Here are two pointers and two based pointers. Each of the based pointers is based on the address that one of the "regular" pointers points to. Therefore, if the far pointer, fip, points to B800:0000H and ibpfip is set to 5, then *ibpfip addresses B800:0005H. Any change to fip will affect ibpfip: ibpfip will always be set to an address calculated by adding its value to the address of fip. This construction can be used in many ways. Suppose an application uses a multidimensional array and makes changes to more than one dimension of the same element. You can use one pointer to access the array's first dimension and based pointers (based on the address the first pointer points to) to access the other dimensions. If the based pointers are each set one dimension apart, you can easily access the other dimensions in parallel. Alternatively, you could allocate a far segment, and create a far pointer to reference the segment base. Then you could use a based pointer (based on the far pointer) to access parts of the same segment. You might be thinking that this would be useful to create a linked list in a far segment. But another type of based pointer would be more useful. Pointers Based on Self The remaining type of based pointer is self based; that is, its base is the segment in which it is stored. Functionally speaking, near pointers are self based. But you cannot use a near pointer in a far segment, nor is a far pointer based on the segment in which it is stored. For instance, consider the following linked list code: typedef struct _list LIST; struct _list { void *item; LIST _based((_segment)_self) *left; LIST _based((_segment)_self) *right; }; void main(void) { LIST _based(_segname("LISTSEG")) list; . . . } The LIST object type is defined, including two self based pointers, using the new _self keyword. These pointers will be based on the segment in which the LIST is created. As you can see, list is declared as being stored in the LISTSEG segment. Therefore, the left and right pointers will point to offsets in the LISTSEG segment, since they are self based. Self based pointers are specifically designed to be used in far segments. They make it easy to define complex data structures, as well as making them portable and easy to declare. Like pointers based on void, self based pointers defer choosing a base. (In a data structure, a self based pointer will be based on the segment in which the data structure is stored.) That is, its segment must be statically known at compile time. That way it will point to an offset within the segment. Linked List Manager To illustrate the features, properties and benefits of based pointers further, I've written a linked list manager (see the sidebar "The List Manager API" and its accompanying code). As most programmers know, linked lists are incredibly useful tools. They are generally simpler and faster to use than disk files. List items can be added, deleted, and sorted quickly and with little overhead. In single-linked lists, each node contains a pointer to the next node in the list, allowing a search from any node forward. In the List Manager, each node contains pointers to the nodes that precede and follow it. You can use these double-linked lists to search in either direction. I believe that linked lists would be used more frequently if not for the limitations inherent in most implementations. Lists are notorious for fragmenting and exhausting a program's heap through the constant addition and deletion of nodes. It's also quite a chore to save list items to disk: you have to write each item to the file one at a time. It's even more complex to restore the list from the disk file. In addition to reading the data, the list linkages have to be restored. The functions in the List Manager presented here use based pointers to alleviate these problems. Instead of maintaining the list in the calling program's data segment, the list is stored in a new segment. The List Manager allocates this segment outside the program's data space and uses based pointers to manage suballocation within the segment. Since a separate segment is associated with each list, you can use and maintain multiple lists of up to 64Kb each. Even if you're compiling with the small or tiny memory model, the list will occupy no more than 4 bytes (for a handle) in your program's default data segment. Also, the lists won't wreak havoc on your program by fragmenting the heap. Placing the list in a single separate segment also facilitates its storage. The entire segment can be written as a single unit, making it a snap to save it and restore it from disk. Since the linkages are retained inside the segment, you can read them back from the disk file without having to rebuild them. You can also extend the List Manager to use EMS memory if you wish. Limiting a list to a single 64Kb segment might seem constraining, especially if you're using it to juggle large objects such as graphic images. Each node therefore contains a far pointer that can be used to track objects stored outside the list's segment. Using a far pointer, you can track objects up to 64Kb each in size, and even store them in EMS. Finally, each node offers a pointer to the node's name. This can also be used to store string data if the object pointer isn't needed. Based pointers make all of this possible. While your application uses a far pointer as a list handle, the nodes and linkages are connected via based pointers. Since allocation of the based pointers has to take place inside a far segment, the new _bmalloc functions are used. These functions, discussed in the sidebar, allow an application to suballocate a previously allocated far segment. Therefore, the list functions call _bheapseg to allocate a new far segment, _bmalloc to suballocate a new node within the segment, and _bfree to free the node. Other functions (discussed in the sidebar) are available although not used by the list functions. How the List Manager Works Since the List Manager has to be able to create new lists dynamically, it uses a far memory segment that is allocated while an application is running. Because of this, there is no previously declared base for pointers based on this segment. The data structures for the list header and nodes (items) therefore cannot use self based pointers, since a self based object requires a specific base expression when an instance of it is created. Instead, the List Manager maintains a static segment variable, _tempseg, which is used as the base for the List Manager's pointers. Since the List Manager allows the use of multiple lists, _tempseg must always contain the segment of the list being manipulated by a List Manager function. Thus, each function begins with the _listinit macro, which initializes _tempseg with the address of the segment in which the list is stored. The List Manager makes extensive use of two data structures, both of which are stored in the list's far segment. The first of these is a control block for the entire list: typedef struct _list LIST; typedef struct _litem LISTITEM; struct _list { _segment seg; // list segment unsigned num; // number of nodes unsigned segsize; // size of segment char _based(_tempseg) *name // name of list LISTITEM _based(_tempseg) *item; // first node or item }; The LIST data type contains the list's segment address, segment size, and the number of nodes. It also contains pointers to the list's name and the first node or item in the list. Each list item is maintained in the LISTITEM structure as follows: struct _litem { void far *object; // object pointer unsigned objectsize; // object's size char _based(_tempseg) *name; // item name LISTITEM _based(_tempseg) *prev; // previous item in list LISTITEM _based(_tempseg) *next; // next item in list }; An instance of this structure is allocated for each item in the list. It lets the List Manager maintain pointers to the previous and next list items, as well as a pointer to a far object. The object pointer can point to any object anywhere in memory, allowing maximum flexibility. You can set this pointer to an object maintained by your application, or it can point to a large object stored in its own segment (for an example, see the animation routines discussed below). The name pointer allows further flexibility. If you want to name a list item or the object it points to, you can use this pointer. Or, you can use it instead of the object pointer to store the actual data item to which the node refers. All the pointers (with the exception of the object pointer) are based on the _tempseg variable, which is always loaded with a particular list's segment. Every component of a list that is allocated by the List Manager is stored in that list's segment, making it easy to maintain a list, as well as save and restore it to and from a disk file. List Manager API The List Manager can be placed in a library, or linked into an application as an object file. In either case, the calling application does not need to know anything about the inner workings of the List Manager. A small API is available to use instead. An application calls ListInit to create a list, and it returns a handle (a void far pointer) to the new list. ListInit requires the list name and the suggested number of nodes that will be used. ListInit then uses this number to "guess" the size of the far segment to be allocated. Internally, ListInit calls _ListInit to create the new segment and return a LIST pointer to it. _ListInit uses _bheapseg to create the new far segment, and then calls _bmalloc to suballocate space for the LIST header structure. Since _bmalloc returns _NULLOFF instead of NULL upon failure, any uninitialized pointers in the List Manager structures are set to _NULLOFF. After a program creates a LIST and receives a far pointer to it, the program can call other List Manager routines to build and manipulate the list. For example, ListAdd calls _bmalloc to suballocate a new list item and add the item to the tail of the list. ListDelete will remove an item from the list; if the item has a related object (via the object pointer), the pointer is returned to the calling application to free it. The API also contains two functions, ListDump and ListDumpItem, for debugging list nodes and pointers. Other functions are briefly described in the sidebar "The List Manager API." Note the two functions, ListSave and ListRestore. ListSave lets you save a list to a specified disk file. It does this by first writing a structure that includes the segment size and the offset portion of the list handle pointer. Next, ListSave writes the entire list segment to the disk file, and then writes each item's object (if it has one) to the file. ListRestore can restore an entire list from the disk. The function first opens the disk file and reads the structure written at the beginning of the file. It uses the segment size to allocate a segment of the proper size without having to read the list segment from the disk. While ListRestore calls _ListInit to create the new segment, the LIST pointer it returns may not correspond to the one originally saved in the file. Therefore, the offset portion of the LIST handle is set to the offset portion of the list handle pointer that was previously stored at the beginning of the file. After this, ListRestore reads the entire segment into the newly allocated segment, and reads each item's objects into far memory segments that it creates. Sample Programs Several sample programs are presented with this article. TESTLIST.C (see Figure 2) creates a list, adds several items to it, then calls ListDump to display the list items and connections. MAKANIM.C (see Figure 3) and ANIMATE.C (see Figure 4) display a brief, repetitive animation sequence using a List Manager's linked list. I used a screen capture program to create a series of screen shots in which a box is drawn and bounces between the top and bottom of the screen. MAKANIM should be compiled and run first: it creates the list by reading the screen shot files (which are available on all MSJ bulletin boards) and making each an object related to a list node. Since the objects are designed for display on an EGA or VGA monitor, only the first 4Kb of each screen shot file (.ACP) is used. The program then saves the list in a file, ANIMATE.LST. ANIMATE.EXE restores the list from ANIMATE.LST, displaying the screen shots over and over in a loop. A key press terminates the loop. Once you've compiled MAKANIM.C and ANIMATE.C, run MAKANIM, followed by ANIMATE. MAKANIM ANIMATE ANIMATE.LST [/b] The optional /b switch will cause ANIMATE to call a based pointer screen display routine. You'll find that the difference in speed between a based pointer and a far pointer is almost unnoticeable. Conclusion Based pointers are a product of segmented architecture. They further blur the boundaries between code and data by allowing you to store data in a code segment. They offer an alternative that is similar to near pointers, while allowing access to data typically reached via a far pointer. They also make it easier to transfer complex data structures between disk, EMS, and conventional memory. Based pointers are not without limitations. The compiler is sometimes too strict about operations such as assigning one based pointer to another. If both pointers use different base names that refer to the same physical base, the compiler generates a warning. You'll have to use a cast to eliminate this. Also, since the compiler generates code to load the ES register when a based pointer is used, repeated reloads of that register can generate slow-performing code. This can be a problem especially when using a variety of based pointers with different bases. On the other hand, the generated code can be very fast if you're using several pointers with the same base. u CHECKERS for PM Part IV: Adding the Game-Playing Strategy Charles Petzold This is turning out to be a lot harder than I thought it would be." That's not an uncommon reaction after getting into a new programming project. If everything were as easy as it first seemed, deadlines would be met with no problem. But missing deadlines is a chronic problem in the software industry. The bigger the program, the worse the problem. Most programs published in books and magazines are very small. The code is intended to show programming techniques in isolation from the often-considerable requirements of real applications. I believe this is perfectly legitimate, because the smaller the code sample is, the more inclined readers are to look at it and learn something from it. Those of us who write these small programs for books and magazines sometimes think that writing a real application is simply a matter of putting all these little techniques together. This is not so. When the MSJ editors and I began to discuss writing a checkers program for OS/2 Presentation Manager (hereafter "PM"), one of our goals was to show the proper way to assemble a fairly large and complex PM program. I believe now that that idea is fundamentally wrong. It is one thing to show how to set up parameters for a call to GpiBitBlt-where there can be very little variation in the code-and quite another to show the overall structure of a large program. There is no one right way to write a large program. The lessons to be learned are in the concepts and methodology rather than the execution. As I'll describe, I have shown one possible solution to structuring CHECKERS, but it is not the only one. Every programmer who might design such a program would probably do it differently, whereas everyone's code to move a piece across the board using GpiBitBlt would probably look pretty much the same. The New Installment This installment of CHECKERS (which may be the last since I'm not sure if I am going to add additional functionality) has a new module, CKRSTRAT.C. It contains a checkers-playing strategy based on straightforward look-ahead techniques. This allows you to play a game with the program. The source code for CHECKERS is now more than fifty pages long, so it's impractical to reprint it here. You can download the source code and CHECKERS.EXE from one of the MSJ bulletin boards. Overall Structure I decided to structure CHECKERS around the windowing and messaging mechanism built into PM. Each major component of the program uses a window procedure to send messages to (and receive messages from) the other components. Figure 1 shows the six source code modules of the program and their relationships. Four modules contain window procedures. These modules communicate with each other using user-defined messages, which are messages that begin with WM_USER (the value 1000H). The other two modules contain support functions. CHECKERS.C contains the program's main function and creates a standard window using ClientWndProc as the window procedure for the Client window. While processing the WM_CREATE message, ClientWndProc creates the program's other three windows. ClientWndProc also processes menu commands. BoardWndProc (contained in CKRBOARD.C) is the window procedure for a child window that is the same size as the Client window and completely covers it. BoardWndProc is responsible for drawing the board (using functions in CKRDRAW.C), for processing keyboard and mouse messages to allow the user to move a piece on the board, and for moving a piece based on the result of the checkers-playing strategy. Both JudgeWndProc (contained in CKRJUDGE.C) and StratWndProc (contained in CKRSTRAT.C) are window procedures for object windows. Object windows are not displayed on the screen but they can send and receive messages just like normal windows. JudgeWndProc controls the game. It instructs BoardWndProc when to allow the user to move a piece, it tells StratWndProc when to calculate the best move in response to the user's move, and it tells BoardWndProc to move a piece based on the decision made by StratWndProc. JudgeWndProc can also service requests from BoardWndProc to determine legal moves. In effect, when the user attempts to pick up a piece with the mouse, BoardWndProc sends a message to JudgeWndProc asking if it's legal to pick up that piece. Based on routines contained in CKRMOVES.C, JudgeWndProc responds affirmatively or negatively. I will refer to these four windows as the Client, the Board, the Judge, and the Strategy. User-Defined Messages The user-defined messages that the window procedures use for communication are defined in the CHECKERS.H header file. First, let's examine the messages that ClientWndProc sends to (and receives from) JudgeWndProc and BoardWndProc. After ClientWndProc creates the other three windows, it sets up a NEWGAME structure (defined in CHECKERS.H) and sends a WM_NEW_GAME message to JudgeWndProc. This message initiates a new game. The message can also occur when the user selects "New Game" from the program's menu. In this case, ClientWndProc displays a dialog box that allows the user to select black or white pieces and a level of difficulty. This NEWGAME structure is defined in CHECKERS.H and contains eight fields (see Figure 2). The first two fields are the window handles of the two players in the game. (I will refer to these as the Player windows.) The next three fields are the handles of the Board, Judge, and Client windows. In the current version of the program, either hwndBlack or hwndWhite will be the same as hwndBoard, depending on whether the user wants to play the black or white pieces. The other is set to the handle of the Strategy window. Conceivably, in a future version of the program hwndBlack and hwndWhite could refer to two windows that communicate with two different external dynamic-link libraries that also contain checkers-playing strategies. In this case neither hwndBlack nor hwndWhite would be the same as hwndBoard. The Board window would be used solely to show the user how the game is progressing. The sixth field of the NEWGAME structure is a BOARD structure, which is also defined in CHECKERS.H: typedef struct { ULONG ulBlack ; ULONG ulWhite ; ULONG ulKing ; } BOARD ; As I've discussed in previous installments, these three 32-bit integers define the layout of the board, with each bit position corresponding to one of the 32 squares on which a piece may reside. A game begins with ulBlack equal to 00000FFFH, ulWhite equal to FFF00000H, and ulKing equal to zero. However, a special test mode (which I'll discuss toward the end of this article) allows overriding of this layout. After receiving the WM_NEW_GAME message, the Judge sends the message on to the two Player windows and, if neither of the two Player windows is the same as the Board window, to the Board window also. Each window that receives the WM_NEW_GAME message (the Judge, the two Players, and the Board) extracts the fields important to it and saves them in static variables. For example, the Judge saves the two Player window handles because it must instruct the Players when to make a move. It also needs the Board window handle to tell the Board when to move a piece based on the response of the Strategy window, and it needs the Client handle for notifying the Client that the game is over (as I'll describe shortly). The Board window saves the Judge window handle because it will need to ask the Judge (through messages) whether a user is attempting to make a valid move. The Board window also saves the sBottom field, which indicates which color should be at the "bottom" of the board (the part of the board that seems closer to the user). Currently, this depends on whether the user is playing black or white pieces. The Strategy window saves the Judge window handle because it will need to inform the Judge of the move it has decided is best. It also saves the sLevel field, which can range from 0 to 3 and indicates the level of difficulty used in the checkers-playing strategy. When JudgeWndProc determines that the game is over, it sends a WM_TELL_CLIENT_GAME_OVER message back to the Client window, indicating which color (black or white) has won. In this version of the program, ClientWndProc simply displays a message box indicating the winner. This message and the WM_NEW_GAME message are the only two messages sent between the Client and the Judge. When the user selects a menu option to change the color of the board, ClientWndProc sends a WM_TELL_BOARD_COLOR_DIALOG or a WM_TELL_BOARD_STANDARD_COLORS message to BoardWndProc. These messages instruct BoardWndProc to display a dialog box (whose dialog box procedure is contained in CKRDRAW.C) allowing the user to choose new colors, or to set the standard colors. These are the only two messages the Client sends to the Board. The Board does not send any messages to the Client. Making Moves The Judge window notifies the two Player windows when they should make a move by sending them WM_JUDGE_SAYS_MAKE_MOVE messages. It sends the first make-move message to hwndBlack and waits for a response. The second make-move message is then sent to hwndWhite. This continues until the game is over (which occurs when a player cannot make a move) or until the user chooses to begin a new game or ends the program. When sending this message, the Judge really doesn't care whether the message is going to the Board window or the Strategy window. When the Strategy window calculates a best move in response to the make-move message, it responds by posting the Judge a WM_TELL_JUDGE_STRAT_MOVE_ENDED message with the move. This and the make-move message are the only two messages between the Judge window and the Strategy window. The messages between the Judge window and the Board window are more complex. When the Board window receives a WM_JUDGE_SAYS_MAKE_MOVE message, it displays a mouse pointer that looks like a little hand and allows the user to pick up a piece with the mouse and move it. During this time, the Board sends several messages to the Judge to determine if the move is legal. These messages include: WM_QUERY_JUDGE_PICKUP_PIECE WM_QUERY_JUDGE_PUTDOWN_PIECE WM_QUERY_JUDGE_CONTINUE_MOVE When the user has completed a move, the Board sends the Judge a WM_TELL_JUDGE_BOARD_MOVE_ENDED message. At this time the Judge sends the Strategy window a WM_JUDGE_SAYS_MAKE_MOVE message. When the Judge receives a WM_TELL_JUDGE_STRAT_MOVE_ENDED message from the Strategy window, the Judge must inform the Board window to move the piece. It does this using three messages: WM_JUDGE_SAYS_MOVE_PIECE WM_JUDGE_SAYS_KING_PIECE WM_JUDGE_SAYS_REMOVE_PIECE On receipt of the WM_JUDGE_SAYS_MOVE_PIECE message, the Board window starts a timer by calling the WinStartTimer function. When it receives the WM_TIMER messages, the Board window progressively moves the piece from its origin square to its destination square. It does this by calling four new functions I've added to CKRDRAW.C specifically for this purpose: CkdExternalSave CkdExternalShow CkdExternalRestore CkdExternalMove These functions are similar to those discussed in Part III of this series that move a piece based on the mouse pointer. The Judge sends the Board a WM_JUDGE_SAYS_KING_PIECE to indicate to the Board that a piece has reached the end and must be kinged, and a WM_JUDGE_SAYS_REMOVE_PIECE to indicate to the Board that a piece has been jumped and must be removed from the board. When the Board has finished moving a piece in response to a WM_JUDGE_SAYS_MOVE_PIECE message, the Board sends the Judge a WM_TELL_JUDGE_PIECE_MOVED message. At this point, the Judge instructs the Board to allow the user to move a piece by sending the Board a WM_JUDGE_SAYS_MAKE_MOVE message. The Judge and the Strategy As I mentioned, the Judge window informs the Strategy window when to calculate a best move by sending it a WM_JUDGE_SAYS_MAKE_MOVE message. The mp1 message parameter that accompanies this message is a pointer to a structure of type MOVE, defined in CKRSTRAT.H as follows: typedef struct { SHORT sColor ; SHORT sKing ; SHORT cSubMoves ; BOOL fNewKing ; SUBMOVE asubmove [10] ; } MOVE ; The Judge sets the sColor field to 0 for Black or 1 for White, depending on whether it wants the Strategy window to calculate the best Black move or best White move. The next three fields are initialized to zero. The last field of the structure requires a bit of an explanation. A checkers program is complicated somewhat by the rules governing jumps. If a player's piece can jump an opponent's piece, then it must jump the piece. If after jumping that piece, another jump is available, it must make that jump also. This must continue until no more jumps are available. I refer to each of these jumps in a single move as a submove. It is possible to set up a checkerboard so that a piece can jump nine pieces in a single move. This is highly unlikely, of course, and perhaps even impossible. Regardless, I've allowed for nine submoves by defining an array of ten SUBMOVE structures in the MOVE structure. (I'll discuss shortly why the extra array element is required.) The SUBMOVE structure is defined in CKRSTRAT.H as follows: typedef struct { BOARD brd ; SHORT iBeg, iEnd, iJmp ; } SUBMOVE ; Before passing a pointer to the MOVE structure to the Strategy window, the Judge initializes the BOARD structure in the first element of the SUBMOVE array. (I discussed the BOARD structure earlier.) This contains the current layout of the board. When the Strategy window receives a WM_JUDGE_SAYS_MAKE_MOVE message, it can save the value of mp1: pmove = PVOIDFROMP (mp1) ; where pmove is defined as a pointer to a MOVE structure. The Strategy window knows what color piece to move by the value of: pmove->sColor It knows the current layout of the board by the BOARD structure obtained as follows: pmove->asubmove[0].brd The Strategy window already knows the difficulty level because of the sLevel field passed in the NEWGAME structure. This is the bare minimum of information the Strategy window needs to calculate a best move. When the Strategy window has finished calculating the best move, it alters the MOVE structure in ways that I will describe below, and then posts a WM_TELL_JUDGE_STRAT_MOVE_ENDED message to the Judge window. The Judge can then determine which move was made by analyzing the MOVE structure whose pointer it passed to the Strategy window. The Strategy window is responsible for setting the sKing field of the MOVE structure to 1 if the piece it moved is a king. The fNewKing field is set to TRUE if the piece becomes a king at the end of the move. The cSubMoves field is set to the number of submoves, which can range from zero (if no move was available) to nine (indicating it jumped nine of the opponent's pieces). For each submove, the Strategy window is also required to set the fields of the SUBMOVE structures to define the move. For example, if cSubMoves is set to 1, which means that the move consisted of a non-jump or a single jump, then the following still represents the original layout of the board. pmove->asubmove[0].brd The iBeg and iEnd fields of the asubmove[0] structure are the board positions (ranging from 0 to 31) of the origin square and destination square of the piece that moved. The iJmp field is the board position of the opponent's piece that was jumped, or 1 for a non-jump move. The following is the layout of the board after the move. pmove->asubmove[1].brd If the move consisted of nine submoves, each submove is indicated in the elements of the SUBMOVE array, with the following indicating the final layout of the board. pmove->asubmove[9].brd In general, the layout of the board following the move can be obtained from: pmove->asubmove[pmove->cSubMoves].brd The Judge window uses the information in the MOVE structure to pass messages to the Board window to carry out the move on the board. More Structures The Strategy window uses a straightforward look-ahead system to determine the best move. Let's suppose that the Strategy window is playing the White pieces. Based on the current layout of the board (obtained from the MOVE structure as described above), the Strategy window can determine all legal White moves based on that board layout, all Black moves in response to all those White moves, all White moves in response to all those Black moves, and so forth. Keeping track of all these possible moves requires a tree of self-referential structures. The Strategy window first uses a structure called MOVEBLOCK to store all the legal moves available in a particular board layout (see Figure 3). The first field indicates the number of possible moves. The Strategy window must allocate memory for a MOVEBLOCK structure of sufficient size for cMoves MOVEP structures. The MOVEP structure is similar to the MOVE structure except that it contains an additional field, a pointer to a MOVEBLOCK structure that contains all the moves in response to this move (see Figure 4). This pointer is defined as a VOID pointer because the structure is defined in the source code before the MOVEBLOCK structure is defined. These two structures are used for building the tree of possible moves. Suppose the user is playing Black and the Strategy window is playing White and that there are five possible White moves. The Strategy window allocates a memory block sufficient to hold a MOVEBLOCK structure plus four additional MOVEP structures. The cMoves field is set to five and the five elements of the amovep array are set to the five possible moves. For each of those five possible moves, there may be several countermoves by Black. Five additional blocks of memory (for each of these five sets of countermoves) are allocated for five MOVEBLOCK structures. Pointers to these five MOVEBLOCK structures are assigned to the pmoveblk fields of the five MOVEP structures in the original MOVEBLOCK structure. In practice, however, it is usually impossible to determine the number of possible moves at the outset. The problem, as is typical with checkers programs, is in working with multiple jump moves. There may be a jump available, and after that jump there may be two possible second jumps. This requires that the MOVEBLOCK structure be reallocated as the moves are being determined. Recursion As you may have guessed by now, the checkers-playing strategy cries out for recursive functions (functions that call themselves). The CKRSTRAT.C module has several recursive functions. The most obvious need for a recursive function is in determining moves and countermoves and counter-countermoves and so forth. This is handled by the CksGetCounterMoves function in CKRSTRAT.C. Another recursive function is useful for analyzing multiple jumps. After the program has determined that a jump is available, it must search for additional jumps. This is handled by the recursive CksGetAllJumps function. After all the MOVEBLOCK structures in this tree have been allocated and analyzed, it is necessary to free all the memory. A third recursive function, call CksFreeMoveBlock, handles this chore. Checkers-Playing Strategy This version of CHECKERS supports four different levels of strategy, Simple, Beginner, Intermediate, and Advanced. In this discussion I'll assume that the user is playing Black and the program determines the best White move in response to that. The Simple level is indeed quite simple. The checkers-playing strategy makes random (but legal) moves. This is not quite as perverse as you might think: in the early stages of the game, it's sometimes difficult to determine whether you're playing a computer or a small child. The playing level I call Beginner goes one level deep in the look-ahead. It surveys all the possible White moves and picks the one with the best score. The result of this is not very significant: if White has several possible jump moves, it will pick the one that jumps the most pieces. The Intermediate playing level looks three levels deep. It looks at all the possible White moves, all the possible Black moves in response to those, and the White moves in response to those Black moves. When considering how to use this information, your immediate instinct, as programmer of this algorithm, might be to find the best White move in this third level, and take the original White move that would result in this. But that's wrong because it assumes that the user (playing Black) is stupid. What you have to assume is that the user is almost as smart as the program, but not quite. For each of the first White moves, the program must find the best Black move in response to that move, and then the best White move in response to those best Black moves. The best White move then determines which original move to make. For example, Figure 5 shows a simple three-level tree, assuming that there are three possible White moves, three Black moves in response to those, and three White moves in response to each Black move. For some of these moves, the player's board advantage (which I'll discuss shortly) is shown in a circle. For the White moves, this is the White advantage, and for the Black moves, it's the Black advantage. (Advantages don't need to be calculated for the moves with empty circles.) Look at the Black moves first. For each of the original White moves, Figure 5 indicates the best Black countermove with a heavy circle. Now look at the White responses to those best Black moves. Again for each set, the best White move in each set is indicated with a heavy circle. Overall, the best White advantage is 300, which results from the best White move to the best Black move to the second original White move. So the program picks the second White move. The Advanced level goes five levels deep. It picks the best Black move on the fourth level, the best White moves on the fifth level in response to those, and based on the best White move on the fifth level, determines the best original White move. When determining the best moves, it may be that several moves result in the same advantage. In this case, the program chooses randomly among these best moves. Calculating the Advantage This version of CHECKERS calculates an advantage based on a simple analysis of the layout of the board. I'll discuss this based on the Black advantage; simply taking the negative of the number converts to a White advantage. If there are no White pieces left, or if there are no legal moves or jumps that a White piece can make, then White has lost and the advantage is set at 10,000. Similarly if there are no Black pieces or if Black cannot make a legal move or jump, then Black has lost and the advantage is set at 10,000. Otherwise, the advantage is calculated by subtracting the number of White pieces from the number of Black pieces, where a non-king piece is counted as 100 and a king is counted as 175. (Thus, it is assumed that a king is worth 75 percent more than a non-king.) This is perhaps the simplest approach to calculating the advantage. More sophisticated techniques involve searching for various advantageous patterns on the board and weighting each of them. The program could then change these weights based on experience. If you are interested in this, a good place to begin research are two papers by Arthur L. Samuel titled "Machine Learning Using the Game of Checkers" reprinted in Computer Games I (Springer-Verlag, 1988), edited by David N.L. Levy. Testing the Code Testing a program such as this can be difficult. For a while, I simply played games with the program and tried to make sure it wasn't making dumb moves. But this was obviously unsatisfactory. The solution I chose was to include a special "Setup" option on the New Game dialog box. This creates another dialog box that allows you to interactively define the initial layout of the board. Restricting the board to just a few pieces allowed me to determine more easily if the program was performing correctly. Of course, a programmer working alone is rarely able to debug any but the simplest programs entirely. Fortunately, I can call upon perhaps the finest group of beta-testers in the world for help-the readers of MSJ! Learning Windows[tm] Part II: Resources and the Menuing System Marc Adler The first article in this series introduced the concepts behind programming an application for the Microsoft Windows[tm] graphical environment. It demonstrated the message-based architecture of Windows1 and showed how Windows uses a message loop to maintain cooperative processing among the various tasks running in a session. I defined a window as not only a visual object in which data is displayed, but also as an object that receives and processes messages. Each window has a message handler, called a window procedure, that processes the messages sent to it. While the first article contained some initialization code, I really did not begin writing the sample stock- charting application. In this issue, I will create the main window and add a menu system to it. The user will choose a command from the menu, the command will be sent to the main window, and the main window will respond in some way. All that you need to do to create a complete pull-down menu system is define the menu in the application's resource file. Resource files are therefore the first items I'll discuss. Next, menuing systems and the API functions and messages associated with them are described. Finally, I'll discuss shortcut keys, called accelerators, that permit the user to invoke any command in your application. Resources A resource is a binary description of a Windows object. Windows resources include dialog boxes, menus, strings, accelerators, fonts, icons, bitmaps, cursors, and user-defined binary data. A resource file is an ASCII text file that contains an English-like description of the resources used by an application. This file contains the actual definitions of some of the resources such as menus, strings, accelerators, and dialog boxes. Other resources, such as icons, fonts, and bitmaps, cannot be defined in ASCII. Therefore, the ASCII resource file contains the names of files in which these resources are stored in binary form. The non-ASCII resources must be created with special tools. For a bitmap image, for example, you can use the SDKPAINT program in the Windows 3.0 Software Development Kit (SDK). After you have created the bitmap image, you can use a good old text editor to modify the resource file yourself, adding a reference to the bitmap file you've just created. The resource file usually has the same name as the application but has an RC extension. After an RC file is made, it must be "compiled" into a binary resource file. Binary images of resources are much faster to load into an application than ASCII resource descriptions because the application doesn't have to interpret them. The resource compiler generates binary images from the resources defined in ASCII format and combines these with the binary images stored in external files. The result is a file with a RES extension. To compile the RC file into a RES, invoke the Windows Resource Compiler (RC.EXE) on the resource file using the -r option. rc -r app.rc The last step in compiling and linking any Windows application is merging together the binary resource file (RES) and the application's EXE file. To tack the RES file onto the EXE file, invoke the resource compiler a second time without the -r option: rc app This is the last command you invoke when building a Windows program. There are several good reasons to use resources. First, a resource is not initially loaded into memory when its application is. A resource can be loaded explicitly at the request of the application programmer, or loaded implicitly by Windows when Windows needs the resource. This means that the memory taken up by the program is less than it would be if the resource were stored statically within the data segment. Second, compiling a resource file is extremely fast. If you need to change, for example, the the layout of controls in a dialog box, all you need to do is change the RC file, recompile it with the resource compiler, tack it on to the application's EXE file, and rerun the application. There is no need for you to change your program's source code and then have to recompile and relink the program. Third, by isolating all of the text strings your program uses in a resource file, your program is much easier to adapt to a foreign language. For instance, if you wanted to create a Swedish version of an application, all you would have to do is to edit the strings, menus, and dialog boxes in the resource file and recompile the resource file. Now that you know what a resource file is, I will create one from scratch and define the pull-down menu system in it. Menus Most Windows applications rely on a pull-down menu system for their top-level user interface. Windows 3.0 improves on the standard pull-down menus. One addition is hierarchical pull-down menus. Using this menu style, individual pull-down menus can be attached not only to items on the menu bar, but also to items on other menus (see Figure 1). This facility permits an application to granularize its command set better and lets the user find the proper command more easily. It also eases the command overcrowding that characterizes the pull-down menus of large, complex applications. Another nice addition to the pull-down menu system is the floating pop-up menu. These menus are not attached to a menu bar. Windows gives you the ability to create or load a predefined pop-up menu and place it anywhere within a window. For example, the user can click on a button and have a menu of choices appear over that button. A menu can be activated in a variety of ways. You can click a menu bar item with the mouse; you can press the Alt key with the key that corresponds to the underlined letter of a menu bar item. This combination is called a hotkey. Or you can press Alt without pressing any other keys. This activates the menu bar, highlighting the first entry without activating an attached pull-down menu. You can use the cursor keys to move along the menu bar, pressing Enter when you reach an item whose pull-down menu you want to invoke. The other type of menu supported by Windows is the system menu, which is usually attached to each top-level or pop-up window. It is represented by an icon in the upper-left corner of the window. Certain predefined items appear on the system menu across each class of windows. For example, every top-level application window has a system menu with the entries Restore, Move, Size, Minimize, Maximize, Close, and Switch To. Almost all dialog boxes have a system menu with only two entries, Move and Close. Most Windows applications do not alter the system menu, although the Windows API provides that option. Users expect to find certain entries in the system menu, so it's best to keep these standard throughout Windows applications. Menus and Resource Files The first step in defining a menu system in the sample stock-charting application is to create the resource file. I used a text editor to create a file called STOCK.RC (see Figure 2). The first line in any resource file should be the following: #include <windows.h> WINDOWS.H, the main include file used by Microsoft Windows programs, is a large file that defines most of the constants, messages, data structures, and function prototypes in Windows. As far as resource files are concerned, WINDOWS.H contains the #defines of the keys used in the definitions of the accelerator keys and the constants for the style bits used by control windows. Now it is time to define the application's menu system. The syntax of a menu definition is shown in Figure 3. The POPUP keyword defines a pop-up menu that is activated when the user selects the associated string from a lower-level menu. The string following the POPUP keyword is actually placed on the pop-up menu's parent menu. In fact, most menu bars are defined as a series of pop-up menus, with each string associated with the pop-up menu placed in sequence in the menu bar. The MENUITEM keyword defines an atomic menu item, meaning that selecting this item does not invoke another pop-up. The MENUITEM definition contains the text string associated with that item, a command identifier used to distinguish a command from others in the application, and a list of menu attributes. A menu item can have several attributes associated with it that serve as visual cues for the end user. An ampersand in a menu item string indicates to Windows that the character following the ampersand will be the hotkey for that item at run time. The ampersand itself is invisible. On a menu bar, you invoke the menu by pressing Alt-character. In a pull-down menu, pressing the hotkey will move the selection bar to that menu item. One thing to note in Figure 2 is a multilevel pop-up menu. The Graph pull-down menu has an entry called "&Grid." Pressing Enter at this menu item reveals a higher-level pull-down menu that prompts the user to draw the horizontal or vertical grid lines. Windows 2.x programmers had to add two menu items to the Graph menu, "Horizontal Grid" and "Vertical Grid." In Windows 3.0, you need only one generalized entry for the grid, simply adding a lower-level submenu. Also note that the "Help" menu bar item has the characters "\a" prefixed to the string. A menu item with this prefix will be right-justified in the menu bar. This style is usually reserved for the menu that invokes the application's help facility. After the menu system is defined, you can run it through the resource compiler with the following command: rc -r stock Loading Menus Into an Application When an application starts up, a binary description of the menu system sits within the executable file on disk. One way to get it into memory and attach it to a window is to associate the menu with a window class. In the initialization code presented in the previous article, you saw how to register a window class by filling it with the elements of a WNDCLASS structure and calling the RegisterClass function. One of the members of the WNDCLASS structure is: LPSTR lpszMenuName; By assigning the name of the menu to lpszMenuName before calling RegisterClass, you tell Windows that every time a window of that class is created, the specified menu should be loaded into memory and attached to that window automatically. Recall that one of the arguments in the CreateWindow function was the handle of the menu that should be attached to the created window. If you specify NULL for the hMenu parameter when calling CreateWindow, the class menu (if any) will be attached to that window (see Figure 4). Another way to load a menu from a resource file is to call LoadMenu. The syntax for LoadMenu is: HMENU LoadMenu (HINST hInstance, LPSTR lpszMenuName) The first argument to LoadMenu is the instance handle of the instance of your Windows application; the second parameter is the name of the menu in the resource file. The name of the menu, or any resource, can be either a text name or a numeric identifier. If it is a numeric value, you must use the MAKEINTRESOURCE macro in order to cast the numeric value to an LPSTR. For example, hMenu = LoadMenu(hInst, "MyMenu"); or hMenu = LoadMenu(hInst, MAKEINTRESOURCE(ID_MYMENU)); LoadMenu returns the handle of the loaded menu, which can then be used as the hMenu parameter in the CreateWindow call. This is how you would attach a menu other than the class menu to a window. You can also create a menu on the fly using CreateMenu. This function creates an empty menu and returns its handle to the application. You can then use InsertMenu or AppendMenu to add both atomic items and pop-ups to this menu. HMENU hMenu; hMenu = CreateMenu( ); AppendMenu(hMenu, MF_STRING, ID_OPEN, "&Open..."); AppendMenu(hMenu, MF_STRING, ID_CLOSE, "&Close"); The WM_COMMAND Message When defining a menu system in a resource file, you saw that each MENUITEM had a command identifier associated with it. For instance, in the menu item defined by the following line, the constant ID_GRAPH_VOLUME is the command identifier associated with the "Volume" menu item. MENUITEM "&Volume", ID_GRAPH_VOLUME This command identifier should be a unique number that distinguishes the command from any other command in the application. Fortunately, Windows makes menu handling automatic, so the programmer doesn't have to worry about polling the menu system to determine whether the user has selected an item from a menu. When the user selects a menu item, Windows simply posts a WM_COMMAND message to the window procedure of the window that owns the menu. If the user selects an item from a window's system menu, Windows posts the similar WM_SYSCOMMAND message to that window. The previous article stated that when a message is sent to a window procedure, two other data elements are also passed to the window procedure. These two pieces of data, wParam and lParam, provide the window procedure with additional information concerning the message. When the WM_COMMAND message is sent to a window, the wParam field contains the command identifier of the command invoked. This command identifier is the same value specified for a menu item in the resource file. By examining the wParam field, you can tell which menu item the user selected. And, because you know which command was sent, you can call one of your own routines to handle that command. A piece of skeleton code for handling WM_COMMAND messages is shown in Figure 5. WM_SYSCOMMAND Message The WM_SYSCOMMAND message is sent to a window when the user selects an item from that window's system menu. Windows uses the low-order 4 bits of wParam internally, so to determine which command was chosen, you need to logical-AND wParam with FFF0H to mask off the low-order nibble. Some of the messages and operations the system menu allows for a standard window are shown in Figure 6. Most of the time, you would let the DefWindowProc handle the processing of the WM_SYSCOMMAND message. However, there might be special cases in which you want to prevent the user from maximizing or minimizing a window. You can do this by handling the WM_SYSCOMMAND message yourself, not passing it on to the DefWindowProc. Other Menu Messages When the user selects an item from the top-level menu bar, the WM_INITMENU message is sent to the window that owns the menu. This permits the application to alter the appearance of the menu bar if necessary, but most of the time this message will be simply passed on to DefWindowProc. Windows convention dictates that the appearance of the menu bar should remain constant, and that you should only change the appearance of the lower-level items appearing on the pop-up menus. To this end, Windows sends the WM_INITMENUPOPUP message to the window that owns the menu bar just before a pop-up is displayed. This gives you the opportunity to change the appearance of one or more of the pop-up's menu items. You can disable, enable, check, or uncheck certain items at this point. For example, many Windows applications with an Edit pop-up menu use the WM_INITMENUPOPUP message to disable the "Paste" menu item if the clipboard is empty. WM_INITMENUPOPUP contains the handle of the pop-up menu in wParam and the index of the pop-up item in the low word of lParam. To use this message to handle the "Paste" menu item, the following code could be used: case WM_INITMENUPOPUP : bTextToPaste = (AppClipboard.iLength > 0); EnableMenuItem(wParam, ID_PASTE, bTextToPaste); break; When the user moves the selection bar over a menu item, whether it's in the menu bar or in a pop-up list, Windows sends the WM_MENUSELECT message to the window that owns the menu bar. For atomic menu items, wParam contains the command identifier associated with that menu item. If a pop-up menu is attached to that menu item, wParam contains the handle of the menu. In both cases, lParam contains the current state of the menu item. Its state can be a combination of MF_BITMAP, MF_CHECKED, MF_DISABLED, MF_GRAYED, MF_HELP, MF_MOUSESELECT, MF_POPUP, and MF_SYSMENU. These state flags are discussed later. If you want to display some help text in a status window when the highlighted selection bar moves across a menu, use the WM_MENUSELECT message. An example is shown in Figure 7. People wonder how certain Windows applications, such as Microsoft Excel, manage to print the help text at the bottom of the main window as you move the highlight bar over a new menu selection. It is done in the following way. The WM_MENUSELECT message is sent to the window procedure of the main window (or to the window that owns the menu bar), with wParam set to the identifier of the currently highlighted menu item. You define a structure that maps each menu identifier to a string of help text and implement a small routine to scan the list of identifiers sequentially upon receipt of the WM_MENUSELECT message. The returned string is then displayed in a hypothetical status window, which is probably located at the bottom of the main window. The last menu message that you need to know about is the WM_MENUCHAR message. This message is sent by Windows to the owner of the menu bar when the user presses a hotkey character that does not correspond to an item on the menu. If the menu is composed solely of text strings, this message can be ignored and passed onto DefWindowProc. This message is more useful when a menu is composed of bitmaps or user-drawn items. Using the WM_MENUCHAR message, you can associate a hotkey with a bitmap on the menu. For example, assume that you have a pull-down menu in which each menu item is a color strip; you can associate each color with a hotkey corresponding to the first letter of the name of that color. If the user types "B," the menu selection would be the Blue color strip (see Figure 8). The Menu API The Windows menu system has a complete API that allows you to manipulate a menu in many ways. Functions exist that set and retrieve the text or state of a menu item; add, insert, remove, and change the text or state of an item; and create, destroy, and draw a menu. For a demonstration of these capabilities, examine the MENU program included with the Windows 3.0 SDK. Besides loading a menu from a resource file, you can also create a complete menu system on the fly. CreateMenu creates an empty menu bar, returning the handle to your application. You can then append items to this menu bar handle. A menu can be destroyed by calling the DestroyMenu function. DestroyMenu will free the memory occupied by the menu. If you programmed earlier versions of Windows, you probably relied on the ChangeMenu function as an all-purpose function to manipulate menus. Windows 3.0 augments the ChangeMenu function with several functions, each of which performs one of the tasks that ChangeMenu used to perform (see Figure 9). For the functions that have the nPosition parameter, you can refer to a menu item by either its zero-based position within the menu or its associated command identifier. You would logically OR the value MF_BYPOSITION with the other flags in the wFlags argument if you wanted to refer to the menu item by its position, and you would use the MF_BYCOMMAND flag if you wanted to refer to an item by its command identifier. /* Get rid of the first menu item */ DeleteMenu(hMenu, 1, MF_BYPOSITION); /* Attach a new pop-up menu to the ID_EDIT menu item */ InsertMenu(hMenu, ID_EDIT, MF_BYCOMMAND | MF_POPUP, hSubMenu, 0); Figure 10 lists the possible values for the wFlags argument. You can enable or disable a menu item by calling EnableMenuItem. A disabled item does not generate a WM_COMMAND message when selected. You can place or remove a check mark next to a menu item by calling CheckMenuItem. Check marks should be placed next to items whose states can be toggled. Windows 3.0 permits you to define a custom bitmap to check or uncheck a menu item. To create these bitmaps, you must first determine what its size should be to fit on a row of the menu. Starting with Windows 3.0, the GetMenuCheckMarkDimensions function is provided, which returns the height and width in pixels that the bitmap should be. StretchBlt can be used to shrink or expand a bitmap to a desired size, but more on this in a later article. Once you have the handles for the two bitmaps, call SetMenuItemBitmaps to associate them with the menu item. SetMenuitemBitmaps(hMenu, nPosition, wFlags, hBitMapCheckOff, hBitMapCheckOn); If either bitmap is NULL, nothing will be displayed next to the menu item when the item has a corresponding check state. For instance, if hBitMapCheckOff is NULL, nothing will be displayed next to the item when it is not checked. Owner Draw Items One nice feature of Windows 3.0 is the ability to draw custom menu items. For instance, imagine a Change Font pop-up menu in a word processor in which each item in the menu is drawn in a different font. Windows 3.0 lets you do this. One of the state flags associated with a menu item can be MF_OWNERDRAW. This flag tells Windows that the application will take care of drawing the menu item. When you create an owner-draw menu item, you can attach a 32-bit value to the item, which will assist you in the drawing of the item. For example, in the following call, the last parameter is the special 32-bit value. AppendMenu(hMenu, MF_OWNERDRAW | MF_BYCOMMAND, ID_HELVETICA, (LPSTR) ul32bitValue); This will take the place of the menu item's text string. When the owner-draw item needs to be drawn, Windows sends two messages to the window procedure of the window that owns the menu bar. The first message is WM_MEASUREITEM. The wParam is not used in this message; the lParam points to a MEASUREITEMSTRUCT data structure (see Figure 11). The window procedure must fill in both the itemWidth and the itemHeight fields of this structure in response to the WM_MEASUREITEM message. The second message sent is WM_DRAWITEM. In this message, the lParam field points to a DRAWITEMSTRUCT data structure (see Figure 12). This menu item can be drawn using any Windows graphics function. Floating Pop-up Menus As mentioned, Windows 3.0 allows you to create floating pop-up menus. A new function, CreatePopupMenu, has been added so you can create an empty pop-up menu. This function is analogous to the CreateMenu function that lets you create an empty menu bar. CreatePopupMenu returns the handle of an empty pop-up. Once you have this handle, you can then add menu items by calling AppendMenu or InsertMenu. Any function that operates on the menu bar will also operate on a pop-up menu. Once a pop-up menu has been created, you can position it anywhere on the screen. To do this, Windows 3.0 adds a new function called TrackPopupMenu. TrackPopupMenu(HWND hMenu, WORD wFlags, int x, int y, int cx, HWND hWnd, LPRECT lprect) The upper-left corner of the menu will be displayed at screen coordinates x,y. If you want to position the menu in a window at a certain point, you must use the ClientToScreen function to translate the window coordinates into screen coordinates. hWnd is the handle of the window that the menu notification messages will be sent to. You can create and use a floating pop-up menu with the following code fragment. hMenu = CreatePopup; AppendMenu(hMenu, ....); /* add menu items here */ o o o ClientToScreen(hWnd, (LPPOINT) &pt); TrackPopupMenu(hMenu, 0, pt.x, pt.y, 0, hWnd, NULL); DestroyMenu(hMenu); CreatePopup is called to create an empty pop-up menu, and then AppendMenu is called repeatedly to add menu items to it. Once all of the items have been added to the menu, you need to determine the absolute screen coordinates where the pop-up should be displayed. Suppose you want to have the upper-left corner of the menu 10 units from the top of the window hWnd and 20 units from the left border. Call ClientToScreen to derive the absolute screen coordinates from a set of window coordinates. Then you call TrackPopupMenu to display the menu. At this point, the second, fifth, and seventh parameters of TrackPopupMenu are reserved by Windows and must be set to zero. After the user makes a selection from the menu, you call DestroyMenu to get rid of the pop-up. Accelerators Another way for the end user to choose a command, besides picking an item from a menu, is to press a key combination known as an accelerator. An accelerator is a pairing of a keystroke and a command identifier. When the user presses an accelerator key, Windows sends a WM_COMMAND message to a window. You can define one or more tables of accelerators in your resource file. The syntax of an accelerator table definition is shown below. AccelName ACCELERATORS BEGIN event, idvalue, [type] [NOINVERT] [ALT] [SHIFT] [CONTROL] END The event field can be one of the following : o A single ASCII character enclosed in double quotes. If the character is preceded by a caret (^), the character is assumed to be a control character. o An integer representing an ASCII character. o An integer or double-quoted single character representing a virtual key. The type field must be VIRTKEY. A list of virtual key codes can be found in the WINDOWS.H file. Virtual key codes usually begin with the prefix VK_; for example, the virtual key code for the F1 key is VK_F1. The idvalue field contains the unique command identifier that will be sent as part of the WM_COMMAND message. The type field can either be VIRTKEY if you are using a virtual key combination, or ASCII if you are using an ASCII key. As you can see, the keystroke can contain any combination of the Control, Alt, or Shift shift states. To use accelerators, you must load the accelerator table from the resource file. This can be done by using the LoadAccelerators function: HANDLE LoadAccelerators(HINST hInstance, LPSTR lpszAccelerator) The first parameter is the instance handle for this instance of your application. The second parameter is the name of the accelerator table as defined in the resource file. LoadAccelerators returns a handle to the loaded accelerator table. Once the accelerator table is loaded, you must tell Windows to check the keyboard events to determine whether the user pressed an accelerator key. To do this, you must add the TranslateAccelerator function to the basic Windows message loop. The TranslateAccelerator function tries to translate WM_KEYDOWN and WM_KEYUP events into accelerators. The message loop now looks like this: while (GetMessage(&msg, hWnd, 0, 0)) { if (!TranslateAccelerator(hWnd, hAccel, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } } If the msg structure contains a keyboard event, TranslateAccelerator will search all of the accelerators in the accelerator table hAccel. If the keystroke corresponds to an accelerator, TranslateAccelerator puts the window handle hWnd into the msg.hWnd and sends a WM_COMMAND message to window hWnd. Since you do not have to call TranslateMessage and DispatchMessage (it has already been done), the program loops back to read another message. If you need to find out whether a WM_COMMAND message was sent by an accelerator key rather than a menu selection, you can examine the high word of lParam. If it is 1, the WM_COMMAND message was generated by an accelerator key; if 0, it was generated by a menu item. Putting It All Together Now that you have seen code to put up a window, created a resource file with menu and accelerator definitions, and created an icon for the application to use when a window is minimized, let's see how to combine everything into one glorious executable file and run the program. Included with the source code is a make file that you can submit to the Microsoft MAKE utility to automate the compile and link cycle. The files that are needed to develop a Windows application are listed in Figure 13, and the processes that each file must go through to create the final executable file are shown in Figure 14. First, the icon is created using SDKPAINT and written into a binary icon file. This icon file is referred to in the ASCII resource file. The resource file is compiled with the "-r" option to create a binary resource file. The C source code is then compiled into an object file. To compile an application for Windows, you must include the following switches : cl -c -Gsw -Zp -AS fileopen.c The -c switch tells the compiler to compile the code without linking it. The -Gsw switch disables stack checking and tells the compiler to emit special Windows code before and after each function that you have declared as FAR. The -Zp switch packs all structures. Since all structures in Windows are packed, you must use this switch when writing any Windows application. The -AS switch indicates that the small memory model is to be used. Now that the object file has been created, link it with the Windows run-time libraries to create an executable file. In the past, you had to use a special linker called LINK4, but if you use Microsoft C Version 6.0, you use the linker that comes with the C compiler. The Windows 3.0 SDK includes libraries that completely replace the C run-time libraries that came with your C compiler. These new libraries provide Windows-aware versions of every C function. A special user-written file called a definition file provides additional information about the linking process to the linker. The DEF file is required for every Windows application; in it is the name of your application and a description of it. It may also contain the name of a stub file, which is a normal DOS executable program that will be run if the user tries to run the Windows program from a DOS prompt. The SDK includes a sample stub program called WINSTUB.EXE, which merely displays the message "This program requires Microsoft Windows." You can also create your own stub file to do something more elaborate. The linker puts the stub file into the executable file, so there are really two programs contained in one EXE file-a Windows version and a non-Windows version. Another requirement of a DEF file is the specification of the executable header type. For Windows programs, you must specify "WINDOWS." So far, the definition file looks like this: NAME STOCK DESCRIPTION 'MSJ Stock Application' EXETYPE WINDOWS STUB 'WINSTUB.EXE' You can specify the size of the application's local heap and stack by using the HEAPSIZE and STACKSIZE statements. Windows gives your application a default heap size of zero, so if you need to do any kind of local memory allocation, you should specify some size for the heap. Add the following two lines to the DEF file: HEAPSIZE 2048 STACKSIZE 8192 Now you need to tell Windows about the code and data segments in your application. If memory space is at a premium in your system, Windows will try to move code and data segments around in memory to reduce memory fragmentation, and if necessary, discard code and data segments to free up memory. If Windows discards a segment, it will try to reread it from an application's EXE file on disk when it needs the segment again. Careful planning can reduce Windows swapping. Below is what you should use in this version of the application. CODE MOVEABLE DISCARDABLE PRELOAD DATA MOVEABLE MULTIPLE This tells the linker that the standard code segment is movable in memory and can be discarded if Windows runs low on memory. The code segment is loaded when the application is loaded, instead of being loaded on demand. You also tell the linker that each instance of the application has its own data segment, and that this data segment can be relocated in memory by Windows. You can also tailor the characteristics of each segment in the program by using the following statement: SEGMENTS segment name [CLASS classname] [minalloc] [FIXED|MOVEABLE] [DISCARDABLE] [SHARED|NONSHARED] [PRELOAD|LOADONCALL] In the last part of the DEF file, you usually define the functions that you want to export to Windows and the functions that you want to import from dynamic-link libraries. You generally need to export any functions that will be called directly by Windows, including the window procedures and callback functions for dialog boxes and timer messages. This is the export section for the stock application: EXPORTS MainWndProc StatusWndProc The linker then uses the DEF file to create an EXE file. Finally, use the resource compiler again to combine the binary resource file with the EXE file to form a new executable file with the binary resources tacked on. Once this is done, you have an executable file. To run the program, just enter the following command: WIN STOCK Finishing Up There are no fancy features in this stock-charting application yet, but at least I created some generic skeleton code that you can use in all of your future Windows programming. This version of the program doesn't really do very much except display menu help items in response to a command being chosen. You should now understand resource files, menus, and accelerators. You saw how menus and accelerators generate WM_COMMAND messages, which inform the application that the user chose a command. The next article will discuss child windows and control windows. ToolBook, an Interactive Development Environment for Windows Richard Hale Shaw Programming the Microsoft Windows[tm] graphical environment presents a formidable challenge to the applications developer. You must be proficient in C, ready to abandon the traditional, procedural programming you are accustomed to in favor of the Windows1 message-based architecture, and you need to master the Windows Application Programming Interface. ToolBook, by Asymetrix, is one alternative to programming Windows directly. It allows you to create Windows applications more quickly and easily. It doesn't require knowledge of C, and you won't have to fathom the depths of Windows programming to master its interactive development environment (sophisticated nonprogrammers should find it usable). Similar to traditional object-oriented programming environments in which objects encapsulate data and methods, ToolBook includes predefined objects such as buttons, fields and hypertext hotwords. You can also create your own objects by drawing them, or by importing bitmapped images. Using ToolBook's English-like, event-based programming language, OpenScript, you can associate blocks of code, called scripts, with objects such as push buttons. Authors and Readers A ToolBook application is composed of one or more files called books. Books have one or more pages, which in turn contain the other objects (fields, buttons, graphics, and so on) that compose a Toolbook application. In this vein, ToolBook application developers are referred to as Authors, and users of ToolBook applications are called Readers. ToolBook has two modes of operation: Reader level and Author level. When a book is read by a user, ToolBook is at Reader level. At Author level, you can modify books and objects. The authoring version of ToolBook comes with BookShelf, a cataloglike book that displays an icon for each book in the TOOLBOOK subdirectory (see Figure 1). (ToolBook book files always have a TBK extension.) If you double-click a book's icon, BookShelf will run the selected book by starting a new instance of ToolBook, which will open the new book and display the first page. The original instance of ToolBook will continue to run BookShelf. A run-time version of ToolBook, which allows you to work only at Reader level, is included with Windows Version 3.0; this version can be used to run the DEMO application presented with this article. The sample books "DayBook[tm]," a calendar and personal organizer, and "Introducing ToolBook," come with the run-time version. Readers have access to a subset of the commands made available for Authors; ToolBook's menu bar changes accordingly. Readers can add pages, search for text, and save a book with a new name. They can print one of the book's pages to a sheet of paper or print several pages per sheet; sheets may be previewed before being printed. Readers may also format text and access the Windows Clipboard. The complete authoring package includes an animation primer, books of script and page ideas, clip art, reference "books" for using Windows dynamic-link libraries (DLLs) with ToolBook, a hypermedia tutorial, a calculator, and the BookShelf. Authors can test their books by bringing up the Command Window and entering commands manually. This avoids having to go back to Reader level for testing (see Figure 2). Authors can use tools and commands to build books, and have access to the color drawing tools and palettes (see Figure 3). At Author level, ToolBook displays a status box to show what page you're on and the number of pages in the book. You can test the modifications you make to a book by switching back to Reader level. When the application is complete, you can assign a password to a book to prevent unauthorized access to Author level and avoid unauthorized opening or saving of the book. Pages Every book has at least one page and one background. Each page has a unique foreground, but can share its background with any other page in the same book. Backgrounds are like templates. They make it easy to put the same objects on several pages, since objects displayed on a background have the same size, style, and position on every page that shares the background. Obviously, when you change an object in a background, the change is reflected in every page that uses the background. Within the foreground and background, ToolBook objects lie in layers with one object in each layer, similar to a stack of transparencies. You can manipulate the order of the layers in a foreground or background to overlap objects. Objects ToolBook objects include buttons; fields; record fields; hotwords; imported or Author-drawn graphics; and group objects, which are several objects grouped together to form a complex visual image. Pages, backgrounds, and books are objects as well. Each ToolBook object has a specified appearance and behavior. When an object is created, its properties are preset to default values depending on its type (button, field, or hotword, and so on). For example, all buttons appear as rounded rectangles with the text label "Button" inside. The values of an object's properties determine its behavior and appearance. Since the object's behavior is predefined, all you have to do is determine which modifications or additions to the default behavior you need for your application. You can modify an object's behavior by changing the value of a property, although you don't need to know how an object changes its appearance or behavior: ToolBook encapsulates this into the object for you. For example, all ToolBook buttons have a borderStyle property, which indicates whether buttons are normal, rounded, or shadowed rectangles; radio buttons; or check boxes (see Figure 4). If you change a button's borderStyle property to radiobutton, ToolBook will change the object to a radio button. Text can be added to a page using borderless fields for labels or shadowed borders for titles; field text can word-wrap and be set to a variety of fonts and type sizes. Bordered fields are also available for data entry. You can create push buttons, radio buttons or check boxes and use them as labels. Hotwords provide hypertext links to other objects and pages-like push buttons embedded in a paragraph of text. After you designate a string of characters as the hotword, ToolBook will move to another page or trigger an action if the Reader clicks it. Hotwords let you build dynamic text screens, in which extra information can be given to Readers who need it, without cluttering the display for those who don't. Using record fields, you can easily create simple tables of information as well as data entry applications. Record fields themselves are used only in backgrounds, whereas contents of fields are stored in the foreground. This setup allows multiple foreground pages to contain different text for the same record field. Thus, you can establish a background composed of record fields that correspond to a table structure, using a different page to represent each record. Creating Objects You can use the built-in drawing tools to create graphical objects called draw objects, or import them from other programs to create objects called paint objects (see Figure 3). Palettes are used to create objects within ToolBook. Objects are chosen from the Tools palette. The Line Palette lets you control line thicknesses; the Pattern palette offers four pages of color patterns. A Polygon palette lets you set the number of sides in a polygon. Two other palettes, the Color Tray and the Spectrum Palette, are used to color backgrounds, draw objects, buttons, or fields. These palettes include color icons that display the currently selected stroke and fill colors and a color swatch that shows how text will look. The Color Tray contains 64 custom colors that you can customize with the Spectrum Palette, with its more than 16 million shades and tints. To create an object, you choose an icon in the Tools palette. You can select buttons, fields, record fields, and hotwords and position them in the foreground or background. The one exception is record fields, which can only be positioned on a background. Unfortunately, it's not at all obvious what some of the icons in the Tools palette represent. You can select a drawing tool to place lines, arcs, angled lines, curves, regular and irregular polygons, normal and rounded rectangles, and ellipses on a foreground or background. When you select a tool, an indicator at the top of the palette shows you what type of object is currently selected (the indicator will be set to a pointer arrow if you haven't selected a tool). A zoom tool lets you magnify the page with four levels of magnification, up to 16x (see Figure 5). Once you've placed an object, select it to make it the size, style, and color you want. Size the object by manipulating the square handles at its corners. To bring clip art and other Windows bitmap images into ToolBook, all you have to do is display them with any program that lets you transfer them to the Windows Clipboard, such as the PaintBrush program included with Windows 3.0. ToolBook, however, limits the size of imported bitmaps to 64Kb. And once these images have been imported, you cannot resize or modify them as you can in paint programs. New pages can be added to a book via a pull-down menu option or Ctrl-N. The new page is inserted after the current page; both will share the same background. You can add a new background for the current page via Ctrl-V. OpenScript All ToolBook objects have a script property. By default, this property has no value; that is, objects do not come with scripts already attached to them. ToolBook scripts are composed in OpenScript. You don't write programs in OpenScript as you would in C or Pascal, you create a script for any object whose behavior or appearance needs modification. For instance, to change a button named "COM1" to a radio button, you could use the following line: set borderStyle of button "COM1" to radiobutton Scripts are the power behind a ToolBook application. Each script contains instructions that define how an object should respond to a specific event, such as what to do when a user selects the object with the mouse. You can create scripts by recording actions you perform with the built-in script recorder or by writing them with the ToolBook Script Editor. Scripts are similar to macros, but considerably more powerful and sophisticated. For instance, besides changing the appearance of an object, scripts can modify the ToolBook graphical user interface itself, by adding menu items: add menu "&DDE" at reader add menuItem "&Retrieve Data" to menu "DDE" at reader add menuItem "&Send Data" to menu "DDE" at reader These OpenScript commands add a new menu item, "DDE," to the ToolBook menu bar, which can be activated via Alt-D. The commands install "Retrieve Data" and "Send Data" as items in the menu; these items can be selected by following the Alt-D with R or S. You can hide, gray-out, and check or uncheck these menu items just as you would in a C Windows program. An Author can modify a ToolBook object from a script or manually via its Properties dialog box. An object's properties include its name, visual style, and optionally, a script. (Buttons and hotwords almost always have scripts.) Some objects have properties specific to their object type. For instance, a page includes a page number property; a book includes a caption or title, the page size and orientation, and passwords; a background includes color and pattern properties; other objects' properties include their layer number, size, position, colors, and vertices (see Figure 6). Some properties, such as a book's password, cannot be set from OpenScript. The Properties dialog box for buttons and hotwords includes options for creating links between pages in the same book or different books (see Figure 7). For example, the Link To option lets you chain to another page when a button is pressed. The Link With option is the same as Link To, except it automatically creates a button on the destination page to return you to the original page. After choosing one of these linking options, select the other page (in the book you're in or another) and ToolBook will automatically link the objects. Messages and Handlers Like Windows, ToolBook is a message-based environment. ToolBook sends objects messages whenever a particular event occurs, such as when a Reader double-clicks an object. When it gets a message, the object's script interprets the message and reacts to it. If a script contains instructions for handling a particular message, ToolBook executes them. The object may have built-in behavior that is triggered when that message is received. For example, if a Reader navigates to a particular page of a book, ToolBook will send the page an enterPage message. If you click an object on a page, ToolBook sends a buttonDown message to that object when you press the mouse button, and buttonUp when you release it. Upon receiving either message, ToolBook checks to see if the object has a script that prescribes the object's behavior for that message. If the object doesn't have the instructions for handling the message (because it has no script, or its script lacks the instructions for handling that message), the message is passed up to the next object in the object hierarchy (see Figure 8). ToolBook processes any messages passed to it that are not handled by any object in the hierarchy. Each script is divided into sections called handlers. A handler contains OpenScript statements that tell ToolBook what actions to perform upon receipt of a given message. Thus, each handler defines how an object should respond to a particular event. All statements in a script (except comments) must be inside a handler. Variables are declared within handlers, user-defined functions and properties are defined within handlers, and all other instructions in scripts must be within handlers. There are three types of handlers: message handlers, "to set" handlers, and "to get" handlers. Message handlers tell ToolBook what actions to perform when the object whose script contains the handler receives a particular message. They are the most commonly used handlers. "To set" handlers define what happens when statements set a user-defined property. "To get" handlers define new functions or how to get the value of a user-defined property. The first statement of a handler begins with a phrase determined by the type of handler you're writing. A message handler starts with "to handle," followed by the message the handler responds to and any parameters. A "to set" handler begins with "to set," followed by the name of the property the handler defines, plus any parameters. "To get" handlers start with "to get," followed by the property that the handler returns the value of or the name of the function defined by the handler and its parameters. Handlers end with either "end" or "end" followed by the message, function, or property named in the handler's first statement. The key to mastering OpenScript is learning to use message handlers. This includes knowing when to use handlers for built-in messages, when to write handlers for your own messages, and where to place these handlers in the message hierarchy. You should also learn when certain objects' scripts should generate these messages themselves. If a message is received, and an object's script has no handler for it (or if the object doesn't have a script at all), ToolBook passes the message from object to object searching for a matching handler. An object hierarchy dictates the order in which messages are passed among objects. If the message is not processed by a handler among the objects in the hierarchy, the message is passed to ToolBook itself. The object hierarchy is how ToolBook implements object inheritance: the objects become progressively more abstract as you move up the hierarchy, so that messages are passed from lower-level objects to higher-level ones. Adding message handlers not only expands the capability of a book, it allows you to condense code. If the code in a handler is going to be repeated in the script of another object, you can write a single handler that will do the work for both objects. The trick is to place the handler high enough in the hierarchy to catch the messages each object receives that need servicing. If the handler is to be used throughout the book, place the handler in the script of the book itself. A special facility called the system book lets you trap widely used messages and functions. This is a special book whose script is available whenever another book or application is running. Using a system book, you can create global handlers for commonly generated messages. If you place a common handler in the script of the system book, you can avoid repeating the same handler in the scripts of several objects. When the corresponding message is generated, it will be passed up to the system book and handled there. OpenScript has a number of built-in messages. These messages include buttonUp (sent to the object whose area contains the mouse pointer when the left mouse button is released), enterBook (sent to a book immediately after it is opened), make (sent to an object just after it is created; this message is comparable to the Windows WM_CREATE message) and destroy (sent to an object just before it is deleted; this message is not unlike Windows WM_CLOSE). Messages that you define must be processed by your own handlers-if they are passed all the way up the object hierarchy to ToolBook, an error will occur. Creating Scripts OpenScript statements are easy enough to hand-write. OpenScript has no explicit data types as do C, Pascal, and BASIC. It's similar to the dBASE language in that data types are implied by the context of the command, function, or operator being used. However, dBASE commands only work with certain data types; in OpenScript you do not have to designate a value's type or what type of data variable it may contain. Also, data variables have only two forms of scope: locals extend for the life of the handler and system variables extend for the life of the application. For example, to initialize a local variable called x in the handler for buttonUp: to handle buttonUp set x to 0 o o o end OpenScript control structures (see Figure 9) include conditions/where (similar to the dBASE "do case") and step (like FOR in BASIC or C). Almost 50 functions exist (see Figure 10), primarily to perform mathematical functions and manipulate strings. More than 60 commands (see Figure 11) allow you to manipulate data files, navigate a book, control scripts and messages, modify the user interface and menus, print, and engage in dynamic data exchange (DDE) conversations with other applications. Although the OpenScript documentation says that you can create your own OpenScript functions, these "functions" are really user-defined message handlers that return a value. Whenever a handler in one object's script needs to have something done by another object, all you have to do is send the object a message. You can define your own messages and write handlers for them in the objects that will receive them, or you can place them in a system book. You can also extend ToolBook by accessing Windows DLLs. Any DLL that contains a published API can be called from ToolBook, including DLLs from third-party vendors as well as those you write yourself. As an example, a dBASE file-handling interface DLL is included with ToolBook. You can create OpenScript scripts with an editor, copy them to the Windows Clipboard, and paste them into an object's script. Or you can open the ToolBook Script window and write a script there. The Script window, available via the Properties dialog box, will check the syntax and compile an OpenScript script when you exit it. If a syntax error is found, ToolBook returns you to the Script window and places the highlight on the offending statement (see Figure 12). You can also test scripts a line at a time using the ToolBook Command window. In this way, you can enter single OpenScript commands, or test entire handlers by sending messages to them. In either case, if an error occurs, ToolBook invokes the Debug window, which shows you the statement that caused the error. You can also look at the values of data variables and invoke the Script window to correct the erroneous statement. You don't have to write every OpenScript script from scratch. As mentioned, when you use the Link To or Link With options in the Properties dialog box of a button or hotword, ToolBook creates a script that causes it to jump to the destination page when you click on the button or hotword. You can also create scripts by copying a script from one object to another or by using the Script Recorder. Script Recording The Script Recorder translates an Author's actions into OpenScript. Actions that can be recorded include choosing commands; filling in dialog boxes; creating, changing, and moving objects; navigating pages; opening books; and switching to Reader level. When you turn the Recorder off, ToolBook places the script in a private ToolBook clipboard. To assign it to an object, all you have to do is paste the clipboard's contents into the object's Script window. Then test the script by switching to Reader level and clicking the object. Incidentally, studying a recorded script is a good way of learning how OpenScript works. The Script Recorder makes it easy to animate an object. You can make a script recording while moving the object; the resulting script will move the object in precisely the same way (see Figure 13). The object's position is recorded only when it receives a buttonUp. At that point, the Script Recorder records an instruction to move the object to that absolute position. As you can see, you'll need to intersperse mouse clicks among the movements; if you don't, ToolBook will record only the first and last positions. This can be demonstrated if you click on the moving fish in the opening screen of the DEMO book. The fish's script includes a handler for buttonUp. When you click on the fish, it will loop around the large fish on that screen, then resume its regular motion from where it left off. The script for this fish was generated by the Script Recorder. Visual Effects A number of ToolBook commands can create interesting visual effects. You can use the fxDissolve command to dissolve one page into another, or fxWipe to wipe the screen from one side to another. The fxZoom command lets you zoom from one page to another; the hide and show commands hide or display any object. Simple application commands like ask and request let you query the user with pop-up windows, using a derivative of the WinMessageBox function in the Windows API. (Of course, you can also call this function directly since it's stored in a Windows DLL). You can also draw an object directly from a script. For example, the following line of code will draw a rectangle from those coordinates on the current page. draw rectangle from 1000,1000 to 5000,5000 DEMO Book I've written a small DEMO book that demonstrates some ToolBook features of interest to developers. You can download this book from any MSJ bulletin board. It's a good idea to run the book while reading the article, to see exactly how some of the techniques work. After loading the DEMO book, ToolBook sends it an enterBook message, notifying the book that execution has begun. The handler for this message, found in the script of the book (see Figure 14), begins by changing the menu: the menus found at Reader level (File, Edit, Page and Text) are removed, and a new menu is installed. If the book is run with the run-time version of ToolBook there will be a new File menu with a single menu item, Exit. If you are using the authoring version of ToolBook, the level menu will appear and the Author item will be available. Changing the menu requires checking the sysRuntime property of the book, which is set to TRUE if the book is running under the run-time version of ToolBook (TBOOK.EXE) or FALSE for the authoring version. The sysLockScreen property is set to TRUE during the menu changes, so the screen changes will not be visible until they are complete. Note that a handler for the leaveBook message (which follows the enterBook handler) restores the menu bar at the Reader level. If you run another book that doesn't restore the menus and didn't have this handler in DEMO, the Reader menu for the DEMO book would appear: ToolBook's properties remain set from book to book until you reset them or close ToolBook and restart it. Once the menu changes have been made, the enterBook handler alters the ToolBook main window size to fit the four corners of a VGA screen. This is done by modifying the bounds property of the main window (addressed as bounds of mainWindow in OpenScript). When a ToolBook property such as bounds includes more than one component, each must be addressed individually. Thus, the following statement sets the horizontal value of the bottom-right corner of the window to pixel 643. set item 3 of bounds of mainWindow to 643 The bounds property contains four items, so each must be addressed. You'll see another example of this in the script for the first page of the DEMO book, which alters the components of the background's color property. One other note on the book script: a handler for the user-defined movefish message controls the motion of the small fish ("MSJFish") that swims around the screen. This handler defines a system variable, pos, used only by the handler. The movefish handler uses the pos variable to select a screen position for the MSJFish. The handler's control structure is the OpenScript equivalent to a CASE construction in C or Pascal. There's a case for each value of pos from 0 to 40; when pos reaches 41, it's reset to 0. The First Page The first page (see Figure 15) has a script that contains two handlers: one handles the system-defined IDLE message, and the other handles a user-defined message, colorchange (see Figure 16). The IDLE message is sent by ToolBook to the current page when no other actions are occurring. When the IDLE handler receives an IDLE message, it generates a movefish message for the handler found in the book script; it sends a colorchange message for every eight IDLE messages. It does this by counting the IDLE messages in a system variable, colorcount; when colorcount is 8, the handler resets it to 0 and sends the colorchange message. When activated, the colorchange handler alters the color of the background using three system variables; hue, lightness, and intensity. Since ToolBook initially sets these to null, the handler initializes them for the color red (where hue is 0, and the lightness and intensity percentages are 10 percent and 100 percent, respectively). Hue values can range from 0 to 360 with each color represented in increments of 60; the other two components range from 0 to 100. When a colorchange message is received, the hue is incremented to the next color, lightness (whiteness) is increased and intensity is decreased. As these values reach their limits during subsequent calls to this handler, they are reset. The background color is altered with the following OpenScript statement, which changes the fillcolor property of the current background. set fillcolor of this background to hue, lightness, intensity A Reader moves to the next page in the book by clicking on the button at the bottom center of the page. The script of this button contains a handler for the buttonUp message, executing the following statement: fxDissolve fast to page "Changing Properties" The fxDissolve command allows you to dissolve the current page to black, white, gray, or another page. The speed parameter (slow, normal, or fast) controls the rate of the dissolve action. This statement makes the entire page appear to fade away exposing the next page, which seems to be underneath. The Second Page: Changing Properties The Changing Properties page demonstrates ToolBook's built-in objects; specifically, what happens when you change an object's properties (see Figure 17). A Reader can change the color of the rectangle in the upper part of the screen by adjusting any of the three scroll bars, then clicking the Apply button or the rectangle itself. When the Apply button or the rectangle receives a buttonUp message, they generate a changeboxcolor message, handled in the script of the current page. (The page sends itself a changeboxcolor message when it receives an enterPage message from ToolBook.) Upon receiving this message, the handler (see Figure 18) sets three variables to the appropriate hue, lightness, and intensity values and sets the fillcolor property of the rectangle. The color values are derived from the scroll property of each of the scroll bars displayed on the page. These are not true scroll bars, because unfortunately ToolBook does not offer a scroll bar object. If you want a scroll bar, you have to use a field with scroll bars, contracting the width of the field until only the scroll bar remains. Unfortunately, while fields can receive buttonUp messages, their scroll bars will not pass them on. Therefore, a scroll bar will respond to mouse messages by adjusting itself, but since it does not pass on a buttonUp message, a script cannot detect when the scroll bar has been clicked. This is why you cannot see dynamic color shifts taking place on the rectangle as you change the scroll values and why you have to click the Apply button for the color change to take effect. The lack of a scroll bar object is a serious deficiency in ToolBook. Another object of interest on this page is the ChangeButton, which dynamically changes its borderstyle property and text when a Reader clicks on it. It will alternate rounded, shadowed, radio button, rectangle and check box styles and adjust its text from Rounded Button to Shadowed Button. The button's script (see Figure 19) uses a system variable and a conditions structure to increment the variable and reset it in order to alternate button styles. There are several hotwords on the page that can be identified by their italic text style and by changes in the mouse pointer's shape as it passes over them. When clicked, these hotwords trigger message boxes that give you more information about how this page works. Three buttons at the bottom of the page allow you to navigate between pages. The first lets you dissolve back to the opening page. The second uses the fxWipe command to make it appear as if the page is being turned (from right to left) to the database demo page. This is accomplished with the following statement in this button's buttonUp handler: fxWipe left fast to first page of background "Data Entry" The third button uses the fxZoom command to zoom to the animation demo: fxZoom fast to page id 13 This command makes the target page appear to explode from the center of the current page outward. The Database Demo You click the second button, Database Demo, to move to the Data Entry screen (see Figure 20). This screen is a background with several record fields and buttons and one or more pages, sorted by the contents of the last name field. You enter name and address information into the current page, according to the fields in the background. The buttons are used to add a new blank record, delete the current record, and navigate to the previous and next records. Here, a record is synonymous with a page: to add a blank record, you only have to add a new page over this background. Similarly, deleting the current records destroys the current page. The background also maintains a record number field. If you look at the background's script, you can see how it works (see Figure 21). The background receives an enterBackground message (see its enterBackground handler) when records are added or deleted, or when a Reader navigates from record to record. Then it sends an updaterecnum message to update the record number field. The value displayed in the record number field is the value of the current page's pageNumber property minus three. (There are three pages in front of the first page of this background.) Since the Data Entry background is at the end of the book (it follows the background for the animation demo discussed below), you can add new pages to this background. If you study the updaterecnum handler, you'll see that it retrieves the current page's pageNumber property into a variable called "it." In many object-oriented languages, "it" is a variable that is globally available at all times: you don't have to declare "it." In the script, "it" is used to place the page number (minus 3) into the text of the record number field. The buttons and icons at the bottom of the background have relatively simple scripts in order to avoid unnecessary repetition of code used by each button. Each button-icon pair has a buttonUp handler that sends a message indicating what type of operation must take place: Add, Delete, Previous, and Next. For example, the Delete button and the Trash Can icon have the same script: to handle buttonUp send deleterecord end Each button-icon pair have the same script, but each pair sends a unique message. The handling for Add, Previous, and Next is relatively simple. To add a new record (or page), the addrecord handler in the background script sends a newPage message to the page, causing ToolBook to create a new blank page, insert it after the current page, and navigate to the new page. When this happens the recordfields on the background remain, but the data they contain is blank (since the page is blank). The handler follows by sending an updaterecnum message to update the record field. As for Previous and Next, the handlers for previousrecord and nextrecord use the go command, as shown below. go to next page of this background This is the key statement in the handler for nextrecord. The previousrecord handler contains the word previous in place of next. These handlers finish by sending an updaterecnum message to the background. The most complex handler in the background's script is the deleterecord handler. It begins by determining if only one page remains on the data entry background. If so, it sends a newPage message to the current page. This places the new page after the current one, so it navigates back to the original page. From there the delete operation can proceed uniformly: the page is selected and sent a clear message, making the page delete itself. Then the script navigates back to the new page (now page 1 of the background) and an updaterecnum message is sent. Note that the Reader sees only the results of the operation, since sysLockScreen is set to TRUE at the beginning of the operation and reset to FALSE afterwards. You can leave the database demo by pressing the appropriate button, which uses fxWipe to turn the page back to the right to return to the Changing Properties page. Animation Demo The animated fish tank is controlled by the script of Page Three (see Figure 22). The script uses the IDLE message to move the objects continually, and sends a message for each object that's to be moved (see Figure 23). All the fish move along a calculated track in the direction they are pointing. After they move offscreen, they reenter the screen from the opposite side. The seaweed moves in sequence between two positions, so that it looks like it's waving in the current. The starfish crawls between four positions. Creating the Images The bitmapped images used in this book were imported from bitmap drawings. The only exceptions are the backgrounds used in the animation demo (which were drawn with ToolBook) and the icons used in the data entry demo, which came from a variety of sources. The Trash Can icon was lifted via the Clipboard from an old Windows 2.0 application. Importing these images and having them display properly in ToolBook wasn't easy. The images were originally drawn transparently on a white (empty) background in a paint program. Once imported, colors in the background or colors in other objects would "bleed" through the white areas of the imported image. The solution was to create a white background for each image by tracing an irregular polygon around it (see Figure 24). If you try this with the authoring version of ToolBook, first draw the polygon transparently so you can see what you're tracing. Then the polygon's transparent property is turned off (via the Draw pull-down), and filled with white. The new polygon is placed behind the bitmap. Because every new object that you add to a page or a background is inserted in a layer above the topmost object, the easiest way to do this is to cut the image itself (not the white backdrop) to the Clipboard, and paste it back onto the page over the polygon backdrop. Creating an object group from the two objects fuses them together. The group can subsequently be moved or superimposed over other objects and backgrounds without losing the image's integrity. Pasting images seems to fragment ToolBook's heap, so that it may generate a message indicating that the page or background is full. Therefore, make sure that you do not have your only copy of an important bitmap in the Clipboard. Save the book to disk and then paste the image. If, after this, you still get the "full" message, you may have to omit some objects from your background or page. Some of the images planned for the animation demo could not be pasted into ToolBook. The large MSJ fish on the first page would simply not fit: ToolBook currently limits the size of imported bitmaps to 64Kb each. This image had to be bisected and pasted in two pieces. One piece is in the background and the other is lined up next to it on the foreground. If you have the authoring version of ToolBook, you can observe this by switching from foreground to background while in Author mode. Conclusion To run ToolBook, you'll need the minimum requirements of Windows 3.0 in protected mode, namely: an 80286 or 80386-based PC, DOS Version 3.1 or later, 640Kb of RAM plus 256Kb extended memory, a hard disk with 2 to 8Mb available, a mouse, a VGA or EGA monitor, and of course, Windows 3.0. As an Author, you'll be better off throwing as much firepower at ToolBook as possible: at least a 16MHz 386 with 2Mb or more of RAM. ToolBook is not terribly slow, but it's not terribly fast, either. To increase its speed, you might not want to force ToolBook to do some types of graphics redisplays in your books. (Unfortunately, some people have dismissed ToolBook's speed because of DayBook, which makes heavy use of graphics and is probably more complex than many ToolBook applications will be.) ToolBook is currently most suitable for prototyping Windows programs, writing on-line help systems and training facilities, creating presentations, and integrating text and graphics. ToolBook does suffer from problems with speed and it cannot manipulate complex graphics images easily (at least, not without a few workarounds). ToolBook lends itself well to designing training packages and courseware: information windows and panels can be tied to objects that cause a window to appear when the Reader clicks on them. This would be ideal for programmed learning systems. You might create books that teach or supply specialized information, or create specialized financial tools, in which bar charts change as data is received through DDE. ToolBook should also be considered to create front-ends for popular database systems. Even a process control application would be impressive under ToolBook: users who don't even know how to use a computer could control an assembly line using the mouse and animated graphics. It points the way to entirely new kinds of applications and application interfaces. 1 For ease of reading, Windows refers solely to the Microsoft Windows graphical environment and is not intended to refer to such products generally. Adapting the Parallel Port for Bidirectional Communication Ross M. Greenberg You probably don't consider the lowly parallel port as versatile as its cousin, the serial port. It has been relegated to handling only the most mundane of tasks-printing, plotting, perhaps an analog control function now and then, sitting lonely the rest of the time. The port is powerful, but there's only so much you can do with a unidirectional port, right? Not once you realize that the standard parallel port is actually bidirectional. In fact, if you want to, you can input or output on as many as twelve lines at once. Compare that to the serial card, which can only input or output on one line, one bit at a time. A parallel port can output a byte and a half in the time it takes a serial port to determine whether it's time to cause a singular transition. Of course, the RS-232 serial port is the standard when it comes to communicating with modems, WANs, and so forth, and a large variety of software that supports serial communication is available. Why, then, try to use the parallel port for the sort of communication usually associated with the serial port, other than the programming challenge? First, communication doesn't always need to be performed in ASCII-sometimes it's as easy as turning on or off a wire to a relay, or reading the on/off condition of a sensor. Using serial I/O would be overkill in these cases: it's sometimes easier to connect the real world to the parallel port. Second, since the parallel port is able to get multiple bits in or out simultaneously, it should be able to provide faster I/O than a serial port. Special code will be needed to take advantage of this bidirectional capability. First, however, you have to understand the hardware. Hardware The parallel port is one of the simplest components of your computer (see Figure 1). There are four distinct parts. The address decoding portion is activated when a specific address is on the bus. The write logic takes the data on the bus and gives it to the third part, the physical input/output portion. The fourth part makes the data from the input/output portion available to the bus when it is requested. Four addresses enable the parallel port, together with the or bus lines. These lines indicate that a port input or output operation is taking place on the bus. Normal memory reads and writes do not set these pins, so the card ignores those types of operations. The various combinations of wires allow for reading and writing the data and status lines on the DB25 connector. The characteristics of the output chips, the 374 for the data output and the 174 for the control signal output, cause the last value output to them to be destructively OR'ed with the data being read. This means that before data is read in from the corresponding 244 and 240 chips, a logical zero must be sent out the data and control ports. The diagram in Figure 1 shows that as many as 12 outputs can be made available to the DB25 at a given time, and that all 17 active pins (the other 8 are grounded) can be used for input of one form or another. Clearly, there is a shortage of output, not input, on the parallel adapter-not what you were expecting! Let's examine what happens if you attempt to connect the parallel ports of two PCs. Not all parallel adapters were created equal. When two adapters are fighting for simultaneous control of those data lines (Pins 29) often one of them will win out over the other, making the data read on those lines unreliable. The solution seems to be to wire the data output lines from one PC to the control input lines of the other, utilizing eight of the nine wires connected to Pins 1 and 1017. But deciding which wire to connect to each pin is a problem. Whereas the serial port is synchronized on a character-by-character basis with start and stop bits, the parallel port is not synchronized with anything, unless you explicitly synchronize it. Whatever those eight data lines are attached to, the data made available on them will be available at slightly different times. Also, the receiving machine must be told at some point that data is ready on those lines. That seems easy enough to do: you simply raise a signal on some line to tweak the remote machine when data is available. Since the parallel port has its own interrupt, IRQ7, using it for this purpose seems to be the logical way of doing things. You simply trigger an interrupt when data is available, and let the remote machine's interrupt service routine handle the rest. Unfortunately, this method would need two pins: the output line that you'd tweak would be connected to the ACK pin in the other machine that causes an interrupt to occur. If you exclude the data output pins from the potential pool of pins available for outputting the data ready signal (since they're already in use for the data), that leaves us with nine pins. Eight of these will be used for input from the remote, leaving only one free pin. You're one pin short of what is needed. If only the original design allowed for the ACK control bit to be a bidirectional one! If polled I/O is used instead of interrupt-driven I/O there isn't any problem: the full eight bits are transferred across at once, and strobing of a "data ready" pin can be accomplished through one of the bidirectional wires, such as Pin 1. Consider, though, how the data is actually transmitted: a cable allowing for bidirectional 8-bit transfer requires eight wires in each direction for the data, a common ground wire, and a means of communicating the "data ready" condition distinct from the "data received" condition. Once again, a problem can arise because one parallel printer adapter is a little stronger electrically than another. Not all Pin 1's were created equal; and there is no guarantee that a bidirectional port can be simply hooked up with a straight wire to another one. Eight-bit data transfer might be out of the question. What's a programmer to do? The answer is, split the byte in half and send one nibble at a time. If you take a look at the two input registers, the status and input ports 3BDH & 3BEH on a monochrome display/printer adapter card (379H and 37AH on a simple parallel printer adapter), the answer starts to become obvious (see Figure 2). A read on 3BEH/37AH allows for 4 bits of input directly from the pins (Pins 1, 14, 16 and 17) and an additional bit showing the interrupt enable state: not enough to have a control bit, alas. The status port, however, has 4 bits of data plus the ability to raise or lower a control line. Looks like the right port for the job. Now, however, another problem arises: each byte is going to be sent in two pieces, but each piece must still be synchronized. There is no guarantee that what appears to be a simultaneous output on five lines will be received as such on the remote site. A protocol must be established that permits a machine, regardless of clock speed, to determine when the data on its data lines is legitimate. Without timing loops, it seems that transition of the control pin from high to low and low to high can be used for this purpose. To demonstrate this technique, as well as the parallel port's capability for rapid transfer, SENDFILE.C was created (see Figure 3). All of the actual send/receive routines can be found in PAR_POLL.ASM (see Figure 4). On a 386 at 16MHz, a sustained transfer rate of over 35K bytes per second was measured. SENDFILE has successfully transferred files between AT- and MCA-style parallel ports. This is the algorithm used in PAR_POLL.ASM: Sender 1. Send the high nibble, after setting the high fifth bit, the control bit. Receiver 1. Stay in a tight polling loop until the high bit, or control bit, on the input control port is on. 2. Acknowledge that nibble by setting the high bit appropriately. Sender 2. Wait for the remote site to acknowledge the nibble by raising its high bit on the input control port, BASEPORT + 1. A tight polling loop is best for this. 3. Output the second nibble of the byte after turning off the high bit. Receiver 3. Stay in a tight polling loop while waiting for the control bit to go low, indicating the second nibble is ready. 4. Acknowledge the last nibble of the byte by turning off the high bit and outputting a dummy byte to the remote system. 5. Process the character just received. Sender 4. Wait for the remote site to acknowledge receipt of the second half of the byte by dropping the high bit read from the sender's input control port. 5. Repeat the process. This scheme has a number of advantages. First, because 3BDH/379H is used for both data and the control bit, the number of I/Os needed to accomplish a single byte's transfer is reduced. Only two inputs and two outputs are required of 3BDH/379H to get the two nibbles from the receiver's viewpoint, and only four I/Os are needed to get the nibbles and control information out from the sender's viewpoint. If the control pin used is Bit 6 of 3BDH/379H (Pin 10), interrupt processing can be used. This allows a TSR to read the initial nibble regardless of other processing in the foreground and potentially emulate BIOS service 14H, which is used for serial communications. A disadvantage is that having a control bit in the middle of a nibble makes processing two nibbles into a byte slightly more difficult. If a buffered I/O routine is needed, one that can store data as it arrives and return it only when requested, it might be implemented as a TSR. Interrupt processing is perfect as a background task for TSRs. The code must be changed substantially, however, and making it into a virtual BIOS service replacement for interrupt 14H is beyond the scope of this article. Figure 5 shows PAR_INT.ASM, an interrupt-driven parallel routine. The logic of an interrupt-driven routine differs greatly from the logic of a polled system. Interestingly enough, due to the high overhead incurred by the interrupt-driven routine, the throughput is substantially lower. Each character transmitted requires an interrupt to be generated on each system ( both the sender and the receiver) and some polling must take place, as well. (A hybrid system that generates only a single interrupt on the receiving system is possible, too, but would be less fun to write.) As an example of interrupt-driven I/O, PAR_INT.ASM demonstrates simple keyboard buffering techniques. It stores data in a buffer, then sends it when told to do so simply by initiating the first nibble, and letting interrupts handle the rest. This is the logic for the interrupt routine. Sender (send_byte subroutine) 1. Remove a character from the transmit circular buffer. 2. Take the low nibble of the character, and insert a logic one between the third and fourth bits of the nibble, left-shifting the fourth bit into the fifth bit position. Because the hardware will reverse the topmost bit, reverse it now. Send the byte out the port. Return to the calling subroutine. Receiver 1. Upon receiving the interrupt caused by the fourth bit being held high, get back the original low order nibble. 2. Generate an interrupt on the Sender side of the wire by outputting a high fourth bit. Sender 3. Upon receiving the interrupt, if in the middle of a send, vector off the main interrupt routine. (second_nibble_routine) 4. Send the final nibble of the character. This time, though, insert a zero bit in the fourth bit position to signal the remote system that it may process the second nibble. Receiver 3. Wait for the fourth bit position to clear on the port. 4. When clear, build the final nibble for the character, build the character and then stuff it to the input circular buffer. 5. Return from interrupt. Sender 5. If there are more characters in the transmit circular buffer, get the next one and call the send routine. 6. IRET back after clearing interrupts. A simple transfer program could be grafted on to PAR_INT.ASM, but be careful! The timing on this system bodes ill for bidirectional interrupt-driven polling I/O. Summary The standard PC parallel port could have been designed a little more carefully. If it had been, you could have gotten true 8-bit I/O, and potentially simultaneous bidirectional I/O, at an extraordinary throughput. Ideally, the output of a byte would require two port output and one port input instructions (output the data, strobe the port, read the ACK), and the receive would require one input and one output instruction (interrupt generated on the strobe, one read of the data, one output to acknowledge the data). However, the routines presented here are not substantially more resource-intensive, requiring a few more IN and OUT instructions and a carefully constructed low-level protocol. In my environment, where all of my serial ports are used up by modems, serial printers, and other devices, it's nice to be able to transfer files quickly through my unused parallel ports. Adding a few lines of code, you should be able to transfer files in the background on each machine, access remote devices transparently (including remote printing and printer sharing), and perhaps even use a remote machine's hard disk as a hot back-up disk. Questions & Answers - Windows Q: I'm doing a series of grouped CreateCompatibleDC, CreateBitmap and SelectObject calls to create many relatively large, concurrent bitmaps, followed by the proper symmetric series of SelectObject, DeleteObject and DeleteDC calls. Everything works fine in standard and 386[tm] enhanced modes, but in real mode the behavior is erratic. Sometimes the CreateBitmap calls succeed and other times they fail, without any apparent pattern. I know it's a memory management problem, but I do not know specifically what it is and how to solve it. A: The sequencing of your calls sometimes causes problems. Your sequence, in abbreviated form without error checking, is: auto HDC ahDC1, ahDC2; auto HBITMAP ahBM1, ahBM2; ahDC1 = CreateCompatibleDC(...); ahBM1 = CreateBitmap(...); ahBM1 = SelectObject(ahDC1, ahBM1); ahDC2 = CreateCompatibleDC(...); ahBM2 = CreateBitmap(...); ahBM2 = SelectObject(ahDC2, ahBM2); o o o ahBM2 = SelectObject(ahDC2, ahBM2); DeleteObject(ahBM2); DeleteDC(ahDC2); ahBM1 = SelectObject(ahDC1, ahBM1); DeleteObject(ahBM1); DeleteDC(ahDC1); Whenever a large bitmap is created, it is a GMEM_MOVEABLE global memory object. Once that bitmap is selected into a DC, it becomes locked. Your problem is that you are "sandbarring," or fragmenting memory in real mode by selecting your bitmaps into DCs, thereby locking them, before you really need to. This makes it increasingly unlikely that there will be chunks of contiguous free memory large enough for creating subsequent large bitmaps. Because of the somewhat random state that your global heap is in at the time, the behavior is erratic. Rearrange your code to read as follows: ahDC1 = CreateCompatibleDC(...); ahDC2 = CreateCompatibleDC(...); ahBM1 = CreateBitmap(...); ahBM2 = CreateBitmap(...); ahBM1 = SelectObject(ahDC1, ahBM1); ahBM2 = SelectObject(ahDC2, ahBM2); o o o ahBM2 = SelectObject(ahDC2, ahBM2); ahBM1 = SelectObject(ahDC1, ahBM1); DeleteObject(ahBM2); DeleteObject(ahBM1); DeleteDC(ahDC2); DeleteDC(ahDC1); You should probably also be doing a GlobalCompact((DWORD)-1L) before these calls to maximize the amount of memory in the global heap and allow Windows[tm] to clean it up. It is best to create your bitmaps (or any global memory object, for that matter) in order from largest to smallest memory requirement to increase the likelihood of getting all the global memory you need. ──────────────────────────────────────────────────────────────────────────── Q: My company has quite a few old 8MHz IBM PC/ATs, on which we run Windows 3.0 in standard mode. We run a lot of DOS applications concurrently with Windows and then access them through the Alt+Tab key combination. Because of these DOS applications, we have set up large RAM disks for the temporary files. Our problem is that when we Alt+Tab to a DOS application, the first few keys subsequently hit get lost (that is, the application does not receive them). Is this a bug in Windows? A: This effect is due to the way that Windows displays the standard mode DOS applications to the user, and it is not a bug. In standard mode, whenever you Alt+Tab to a DOS application from the Windows "screen group," Windows needs to save the contents of low memory (memory below 640Kb) to a hidden read-only file. This file, called ~WOA0000.TMP, is in your temporary subdirectory. While Windows is saving the contents of memory, the screen blanks out. After the memory is saved, Windows can then reload low memory with the DOS application. It does this from another hidden read-only file named ~WOAXXXX.TMP, where each X is a random hexadecimal digit. This file is basically a memory image of the DOS application and its video buffer as it existed when you last left DOS. Windows next reloads the video buffer as it existed for the DOS application, then reloads the roughly 500Kb of low DOS memory for the application. Next Windows resets the current disk and directory for the DOS application. Finally, Windows resets all the interrupt vectors set by the DOS application and any TSRs that exist. As a result, you see the DOS application's screen before the application itself is loaded and able to react to any keyboard interrupts. This is more pleasant visually, but it makes you think that something is wrong when the application does not immediately react to any keystrokes. All you can do is wait until the application is completely loaded and the interrupt vectors are reset. When you Alt+Tab out of a DOS application, the reverse happens. There is a slight delay as the video buffer is saved. Windows then displays the "Switching..." message while it saves the low memory containing the DOS application. Then it reloads the next application in the Alt+Tab chain. If you are running many DOS applications and Alt+Tab'ing between them, you can hold the Alt key down during the "Switching..." message, press Tab until you get to the DOS application name you want, then release the Alt key. This saves you from having to go back to the Program Manager or Task Manager to get to the next DOS application you need. Q: I'm writing an MDI-style application, where the focus may be on one of many windows. In a multiple-window application, what's the easiest way to check for messages across all windows without having to add code to each WndProc? I want to monitor and display the state of the Num Lock, Caps Lock and Scroll Lock keys, as is done in Microsoft Excel and Microsoft Word for Windows. I'd prefer to use an object-oriented method, so that I don't have similar code (or function calls) in numerous WndProcs. Microsoft Excel and WinWord seem to monitor both WM_MOUSEMOVEs and WM_KEYDOWN/WM_KEYUPs, but don't always show a changed key status unless they are the active application. A: The easiest way to check all your messages, no matter which MDI window has the focus, is to use a modified main GetMessage loop, as shown in the following example. auto MSG aMsg; while (GetMessage(&aMsg, 0, WM_NULL, WM_NULL)) { switch (aMsg.message) { case WM_XXXX: o o o continue; // do not Translate and Dispatch case WM_XXXX: o o o break; // do normal Translate and Dispatch } TranslateMessage(&aMsg); DispatchMessage(&aMsg); } Although this will allow you to monitor all the messages that go through the normal application message queue, you will not see messages here that are sent directly to your various WndProcs. Many common Windows messages are sent directly to the WndProcs. As an example, a WM_ERASEBKGND is sent directly as a result of a WM_PAINT BeginPaint call, so you could not successfully switch on it in the above code. For monitoring the Lock keys, consider setting a systemwide hook with SetWindowsHook. Unfortunately, this must be done in a DLL and is complicated. This is probably why Microsoft Excel and WinWord appear to monitor only those WM_MOUSEMOVEs and WM_KEYDOWN/WM_KEYUPs that come to them, and therefore do not accurately reflect the state of the Lock keys. Incidentally, when you study how these two applications operate, the Lock keys code they use appears to be very different. The best way to monitor the Lock keys is to simply do a SetTimer for your main WndProc for, say, every 125 milliseconds. This will allow you to represent the state accurately, whether or not you are the active application. On the WM_TIMER message, see if the state of each key (VK_NUMLOCK, VK_CAPITAL or VK_SCROLLLOCK) has toggled with: (GetKeyState & 0x00FF). If it has, redisplay the state appropriately. (Note: VK_SCROLLLOCK should be defined as 91H, because that key code is currently undocumented and not in WINDOWS.H.) Q: I can easily prevent multiple copies of a Windows application from running by coding the application so that it checks the WinMain PrevInstance argument. How do you prevent multiple copies of a regular DOS application from running concurrently in Windows? My goal is to protect a dedicated file from being corrupted by accidentally running two instances of the DOS application. A: There is no direct way of doing this within Windows. You can, however, accomplish this indirectly by using a BAT file to test for the existence of a "semaphore file" before running your DOS application under Windows. A semaphore is a signaling apparatus; in other words, a fancy name for a flag. Assume your DOS application is named DOSAPP.EXE and that it resides in your C:\DOSAPP subdirectory. Create a C:\DOSAPP\APP.BAT file with either Notepad or your editor as follows: @echo off c: cd \dosapp if exist %temp%\dosapp.sem goto sem echo %0 >%temp%\dosapp.sem dosapp %1 %2 %3 %4 %5 %6 %7 %8 %9 erase %temp%\dosapp.sem goto end :sem echo DOSAPP is already running! pause :end In the Program Manager, select the group that the application should be in or is in, choosing File New for the Program Group or File Properties on the Program Item. Instead of specifying C:\DOSAPP\DOSAPP.EXE in the Program Item Properties dialog box, set the Command Line to C:\DOSAPP\APP.BAT. In this example, you could also specify up to nine arguments (%1 to %9 in the BAT file) on the same Command Line. The above BAT file assumes that you have set an environment variable in your AUTOEXEC.BAT named TEMP indicating a subdirectory where temporary files should go, although it will still work without the TEMP variable. Ideally the subdirectory would be on a RAM disk, created with the RAMDRIVE.SYS program that comes with Windows. If TEMP points to a subdirectory on your hard disk (say C:\TEMP), you should also include the following line after the SET TEMP=C:\TEMP as a "reboot" handler in AUTOEXEC.BAT: if exist %temp%\*.sem erase %temp%\*.sem The two lines in the BAT file that move you into C:\DOSAPP (c: and cd\dosapp) are necessary only if you do not put the BAT file in C:\DOSAPP. They ensure that the current subdirectory is set properly before DOSAPP.EXE is executed, just as Windows does if you had specified C:\DOSAPP\DOSAPP.EXE on the Command Line. When a path is specified on the Command Line, Windows changes into that subdirectory before executing the EXE or BAT file. If the BAT file is in the same subdirectory as the EXE file, it should not have the same filename since EXE files are executed before BAT files. It would be more elegant to be able to use an environment variable as a semaphore instead of the file to block the second instance, but this will not work. Each EXE (Windows or DOS) or BAT file inherits the environment that exists at the time Windows is started. Setting an environment variable in the BAT file only applies to that specific invocation of the BAT file, so a second execution of the same BAT file would not "see" the environment variable set in the previous one. Unfortunately, the only common changeable "environment" in Windows is WIN.INI, and DOS sessions cannot access it. Another way to protect your files from multiple updates, although not foolproof, is to execute the DOS SHARE.EXE program in AUTOEXEC.BAT. This recommendation is documented in one of the "read me" files that come with Windows, and is often overlooked. Q: What are the recommended default sizes for the predefined window/control classes (Button, ComboBox, Edit, ScrollBar and Static)? I want to make sure my controls adhere to the standard. Also, how would these sizes be determined at run time? A: The width and height of the predefined window control classes is based on an average character being 4 logical units wide and 8 logical units high. The actual values for a unit then depend on the resolution and aspect ratio of the display, as evidenced by the size of the default system font. Based on this relative concept and these values, use the following table: ■ Class Style Width Height ■ Button BS_CHECKBOX 12 ■ BS_AUTOCHECKBOX 12 ■ BS_3STATE 12 ■ BS_AUTO3STATE 12 ■ BS_PUSHBOX 14 ■ BS_PUSHBUTTON 14 ■ BS_DEFPUSHBUTTON 14 ■ BS_RADIOBUTTON 12 ■ BS_AUTORADIOBUTTON 12 ■ ComboBox CBS_SIMPLE 12 ■ CBS_DROPDOWN 12 ■ CBS_DROPDOWNLIST 12 ■ CBS_OWNERDRAWFIXED 17 ■ CBS_OWNERDRAWVARIABLE 17 ■ Edit all 12 ■ ScrollBar SBS_HORZ 9 ■ SBS_VERT 9 ■ Static SS_LEFT 8 ■ SS_CENTER 8 ■ SS_RIGHT 8 ■ SS_SIMPLE 8 ■ SS_LEFTNOWORDWRAP 8 These values can be used directly in the Dialog Editor. If you need to convert these logical values to pixels on a particular display at run time, use the following code: lDlgBaseUnits = GetDialogBaseUnits(); AveHeight = HIWORD(lDlgBaseUnits); AveWidth = LOWORD(lDlgBaseUnits); You can then use the AveWidth and AveHeight to calculate the actual pixel values for a standard window/control size by multiplying them by the logical values from the table and dividing by the logical units of the width and height. aiButtonCheckboxDefH = AveHeight * 12 / 8; aiButtonPushboxDefH = AveHeight * 14 / 8; aiButtonPushbuttonDefH = AveHeight * 14 / 8; aiButtonRadiobuttonDefH = AveHeight * 12 / 8; aiComboboxNormalDefH = AveHeight * 12 / 8; aiComboboxOwnerdrawDefH = AveHeight * 17 / 8; aiEditDefH = AveHeight * 12 / 8; aiScrollbarHorzDefH = AveHeight * 9 / 8; aiScrollbarVertDefW = AveWidth * 9 / 4; aiStaticTextDefH = AveHeight * 8 / 8; ────────────────────────────────────────────────────────────────────────────

Volume 5 - Number 6

──────────────────────────────────────────────────────────────────────────── Port Windows[tm] Applications to OS/2 (Almost) Painlessly with the Software Migration Kit Eric Fogelin, David Wood, and Noel Bergman Porting applications from the Microsoft Windows[tm] graphical environment to OS/2 Presentation Manager (hereafter "PM") has just become much simpler. Microsoft has created a set of software tools, called the Microsoft Windows to OS/2 Software Migration Kit (SMK), which acts as an extension to the Microsoft Windows Software Development Kit Version 3.0 (SDK). The SMK reduces the conversion time for a typical large application from months to days, by supplying a mapping layer that translates Windows1 function calls into OS/2 function calls. This code layer, implemented as a set of OS/2 dynamic-link libraries (DLLs), sits between PM and the Windows application, translating calls to the Windows API at run time. When the Windows application calls an API function to draw a line, for example, the mapping layer accepts the Windows API call, interprets it, reorders and converts the parameters, and calls the corresponding PM API (see Figure 1). Converted SMK applications run as 16-bit OS/2 executables on Versions 1.2 and higher, and 2.x. Developers convert their Windows applications by relinking with a new set of libraries. Although to the user converted SMK applications appear as PM applications (see Figure 2), they behave internally as if they were running under Windows. SMK applications are OS/2 processes, so they receive many of the benefits of PM, including the ability to exchange data with other PM applications via the Clipboard, dynamic data exchange (DDE), or interprocess communication (IPC). You can enhance SMK applications with additional code that takes advantage of specific OS/2 functionality, such as multiple threads, and High Performance File System (HPFS) features, such as long filenames and extended attributes (EAs). A beta release of the SMK should be available to qualified developers shortly after press time, with a final release to follow several months later. SMK Contents and Installation The SMK includes several conversion utilities, import and C run-time libraries for four memory models (small, medium, compact, and large), debug and retail versions of the mapping DLLs, PM-compatible Windows fonts, an SMK version of Windows Help, on-line documentation, and sample source code. Figure 3 lists the SMK contents. The SMK SETUP and SMK tools are all bound applications so they can be run under DOS2 or OS/2 development environments. But since the migrated applications run only under OS/23, testing and debugging must be performed in OS/2 Version 1.2 or later using a protected-mode debugger. In addition to OS/2, you will need the Windows SDK 3.0 and Microsoft C Version 5.1 or 6.0. The Microsoft OS/2 Presentation Manager Toolkit Version 1.2 is not necessary for the conversion, unless you plan to enhance your SMK application with OS/2-specific features. SETUP is similar to the Windows SDK installer. It copies the SMK tools and samples, and builds only the memory model C run-time libraries you request, thus saving disk space. If you need more memory model libraries later, you can rerun SETUP without reinstalling the entire SMK. After you install the SMK, you must update the system environment variables used to build the Windows application (such as LIB and PATH) so that both the SMK and SDK are referenced. The SMK relies on SDK tools such as the resource compiler. You should rebuild the SMK samples to verify that the SMK and environment are correctly set up. If applications that are converted and tested under the beta SMK meet your standards of performance and reliability, you can ship your converted product accompanied with the beta version of the SMK DLLs. To ensure that the final release of the SMK DLLs does not affect ISVs that have shipped using the beta SMK, the DLLs have release-specific names. The SMK's beta DLLs have names with a _B suffix, such as: GDI_B.DLL, USER_B.DLL, and KERNEL_B.DLL. DLLs in the final release will have suffixes other than _B. It is recommended that all SMK DLLs be installed in the C:\OS2\DLL directory of OS/2. That way multiple SMK applications share a single set of the DLLs. This is not a requirement; the only requirement is that the DLLs are in a directory pointed to by the OS/2 LIBPATH environment variable. SMK Conversion Process Successful conversion of a Windows application using the SMK requires that the application is Windows 3.0-compliant and runs in protected mode. As in the Windows SDK, there are memory-model-dependent C run-time libraries but only one memory-model-independent import library, LIBMK_B.LIB. This import library contains all the Windows API imports to the SMK mapping layer DLLs. SMK-specific library names are used so as not to conflict with existing Windows libraries. To convert library names in your make files, the following SMK naming convention is used: ■ Windows SMK ■ LIBW.LIB LIBMK_B.LIB ■ xLIBCyW.LIB xLIBCyMK.LIB ■ xDLLCyW.LIB xDLLCyMK.LIB ■ x is the memory model (S, M, C or L) and y represents emulator or alternate math support (E or A) In addition to the SMK libraries, the OS/2 import library, OS2.LIB, is required since an OS/2 application is being created. OS2.LIB is shipped with the SMK. One of the most time-consuming tasks when performing a traditional port of a Windows application to OS/2 is converting the resource files, containing dialog boxes, menus, bitmaps, and so on, because Windows and OS/2 have incompatible resource definitions. SMK applications retain their Windows resources and the Windows resource compiler is still used to bind these resources to the SMK executable. The SMK mapping layer loads and manages the Windows resources. A subtle problem occurs when the SMK applications are displayed by the OS/2 Program Manager: it searches for an icon in the executable to display but recognizes only the PM icon format. Because the converted application's icon is still in Windows format, it always uses the unexciting default PM icon. The SMK comes with three utilities to resolve this problem. CONVICON converts a Windows 3.0 icon to OS/2 format. RCSMK spawns an SMK version of the OS/2 resource compiler (RCPM) to bind the PM icon to the SMK executable before the Windows resource compiler binds the Windows resources. The OS/2 Program Manager thus finds a PM icon to display and the SMK mapping layer knows to skip the PM icon when accessing the Windows resources. Figure 4 shows a series of Windows 3.0 applications displayed by the Windows 3.0 Program Manager and the SMK versions with converted PM icons displayed by the OS/2 Program Manager. Windows and OS/2 font file format differences make a font converter necessary. CONVFONT.EXE converts Windows font files into OS/2-compatible font files. The SMK also provides several fonts used in Windows that are not present in OS/2. WINMONO.FON, WINPRO.FON, and SYMBOL.FON must be installed via the OS/2 Control Panel as are any additional ISV fonts converted using CONVFONT.EXE. The SMK includes a version of the Windows Help Manager that has itself been converted via the SMK to operate under OS/2. As long as the Help Manager is in the path, spawning and interaction occur via the normal Windows help functions. To use WinHelp, you must ship a copy of WINHELP.EXE with your product. Your setup program should install it along with your application. Sample Conversion To prove how easy it is to convert Windows applications using the SMK, Windows code from a recent MSJ article ("A New Multiple Document Interface API Simplifies MDI Application Development," Vol. 5, No. 4) was converted. MDIDEMO was written to illustrate the Windows 3.0 MDI support. A native port of this application would have required significant source code changes. Using the SMK, however, requires no source code changes, only minor changes to the MDIDEMO.MAK make file to reference the SMK libraries (see Figure 5). As you can see, only the library names have changed. You must use the /nod linker switch when specifying the SMK libraries. If you don't, the linker will do a default library search and find the Windows libraries first. The compiler command-line switches are the same. Because MDIDEMO uses Windows resources, the Windows resource compiler is used to compile and bind MDIDEMO.RC to the executable. MDIDEMO did not originally have an icon. For the sake of illustration, a Windows icon was created to demonstrate the SMK utilities CONVICON and RCSMK. The make file in Figure 6 runs these utilities to convert the Window icon to PM format and bind this PM icon resource to the executable, so that the icon will appear in an OS/2 Program Manager group. (This and other sample SMK-converted applications and the DLLs needed to run them under OS/2 may be downloaded from any MSJ bulletin board.) Figure 7 shows MDIDEMO running on Windows 3.0 and OS/2 1.2. Without any source code changes, the Windows program successfully runs on OS/2 with equivalent functionality and MDI support. (Differences in the contents of the Rectangles child windows is due to MDIDEMO itself, which repeatedly creates random rectangles.) Architecture Because an SMK-converted application operates as if it's running under Windows, while looking like a PM application, it might seem that the SMK application has limited participation in the PM environment. However, the mapping layer allows converted applications to interact with other SMK and PM applications automatically. Converted applications and native PM applications have full Clipboard data transfer capability, including two-way metafile conversion between the Windows and PM metafile formats. The DDE message protocol, which enables dynamic communication with other PM applications, is also supported. The mapping layer handles code page translation of DDE and Clipboard text. Unfortunately, Windows has additional Clipboard formats not present in OS/2. The mapping layer cannot translate private formats or formats unsupported in OS/2. The mapping layer also resolves differences between Windows and OS/2 profile information storage. OS/2 uses the binary file OS2.INI for system configuration storage, while Windows uses a file named WIN.INI, which is in ASCII. Likewise, while both systems support private initialization files, OS/2 uses a binary format while Windows uses ASCII. Windows applications typically read and write configuration information with the functions GetProfileString, WriteProfileString, and GetProfileInt. The SMK intercepts these functions and maps system values (system colors, international country settings, fonts, and so on) to OS2.INI. All other values are assumed to be application-specific and are mapped to WIN.INI. The SMK searches the path for the first occurrence of WIN.INI. If WIN.INI does not exist along the path, the SMK creates it in the current directory. Windows and OS/2 share architectural characteristics such as dynamic linking, resource management, and windowing that allow a converted application to run as a true OS/2 process rather than requiring virtualization. The mapping layer does not rely on support from the DOS compatibility box or the virtual 8086 mode of the 80386 processor. Because Windows and OS/2 use an EXE format known as the New Executable Format (new compared to DOS), the OS/2 loader knows how to handle code preloading and dynamic linking. Therefore, the mapping layer handles Windows function imports via standard OS/2 dynamic linking and translates the Windows functions into OS/2 equivalent operations at run time. Many simple Windows functions are mapped directly to their OS/2 equivalents. A few that have no equivalents, such as the Windows sound functions, are simply stubbed out (see Figure 8). For most functions, the mapping layer retains context information across multiple Windows function calls. Window handles, window device contexts, and resources are managed by the mapping layer such that the relinked application behaves as if running under Windows. An interesting aspect of the mapping layer is that it creates and handles proxy window handles for all PM windows that an SMK application interacts with. For example, if an SMK application calls GetFocus while a PM window currently has the focus, a unique non-PM window handle that represents the PM window is returned to the SMK application. Handles to objects such as windows, bitmaps, and device contexts are not interchangeable between PM and SMK applications. These handles must be converted by the mapping layer during calls from the SMK application to the Windows API. Messages between SMK and PM applications are similarly manipulated by the mapping layer. The SMK must also contend with differences between Windows and OS/2 multitasking. OS/2 offers preemptive multitasking, in contrast to the cooperative, nonpreemptive, message-driven multitasking of Windows. Windows applications relinquish control of the CPU only when they call GetMessage or PeekMessage. This application-controlled "baton-passing" allows Windows applications to ignore certain synchronization and resource conflicts that can occur in a preemptive multitasking system such as OS/2. Resource manipulation in shared DLLs illustrate the problems of bringing Windows applications to preemptive OS/2. If a routine in a shared DLL manipulates a resource but reaches the end of its time slice before completing the operation, another process may execute the same DLL routine and stumble upon a corrupt or partially manipulated resource. Windows DLLs do not guard against this scenario because under Windows, all DLL operations are atomic until a message queue is checked. While a Windows DLL could be rewritten to use semaphores, the SMK mapping layer simplifies the process. The mapping layer duplicates Windows behavior-it will preempt an SMK application to schedule another SMK application only when the applications check their message queues via calls to GetMessage or PeekMessage. This synchronization of SMK threads does not affect native PM and OS/2 kernel threads. The SMK mapping layer cannot support some features of OS/2 due to architectural differences between Windows and OS/2 and how they exploit the 80x86 architecture. Under Windows protected and 386 enhanced mode, all Windows applications share one Local Descriptor Table (LDT). Each OS/2 process has a private LDT. Though it has been discouraged, some Windows applications may take advantage of the current LDT usage of Windows to support direct manipulation of another application's data. As an example, a Windows application that is designed to subclass another application's windows operates as intended under Windows 3.0, but must be redesigned if converted via the SMK to run on OS/2. Subclassing another application's windows is generally not supported under OS/2 and providing this functionality is outside the scope of this article. Windows hooks are supported under the SMK, though there are similar issues to those raised in subclassing. Hook procedures must reside in DLLs and the system will do an implicit LoadModule when the hook must be called from a different process. The SMK supports the Windows global shared memory mechanism, GMEM_DDESHARE. The mapping layer translates global memory requests to DosAllocSeg or DosAllocHuge transparently, granting access to this shared memory to other processes via DosGiveSeg and DosGetSeg. In addition to relying on different LDT designs, Windows and OS/2 also implement different ring architectures. All Windows applications and the Windows system run in Ring 1. OS/2 applications (including SMK applications) run at Ring 3, with the ability to create Ring 2 segments for executing port I/O. This causes problems when porting device drivers that cannot be addressed by the SMK. This is discussed further in the next section. Windows applications often use their own DLLs that are shipped with the product. Obviously, these DLLs must be converted; the SMK provides DLL-specific C run-time libraries to do so. OS/2 loads these SMK DLLs just like any other OS/2 DLL, creating a problem that cannot be addressed directly by the mapping layer. Windows and OS/2 DLLs typically rely on an assembler start-up module, which contains its entry point. LIBENTRY.ASM, included in the Windows 3.0 SDK, is used by most Windows developers. The Windows and OS/2 loaders call the DLL entry points with a different set of initialized registers. For example, in Windows the CX register contains the DLL heap size specified in the DEF file; in OS/2, the SI register contains this information. Other registers are similarly affected. An SMK tool, CONVDLL.EXE, modifies the DLL start-up code such that the standard Windows register set is restored. CONVDLL relies on a small source code change that must be inserted manually into LIBENTRY.ASM. CONVDLL.INC is a small include file containing the necessary signature information. The following illustrates the necessary code change to LIBENTRY.ASM. cProc LibEntry, <PUBLIC,FAR>; entry point into DLL INCLUDE CONVDLL.INC cBegin push di ; handle of the module instance push ds ; library data segment push cx ; heap size push es ; command line segment push si ; command line offset o o o The CONVDLL changes also assure that the SMK DLLs are properly initialized. In this way, Windows APIs may be called in LIBENTRY.ASM. Without CONVDLL, you could not be sure that the SMK DLLs are initialized during LIBENTRY execution. The SMK includes sample code illustrating the use of CONVDLL. SMK Limitations Most Windows API functions are supported in the SMK mapping layer, with the exception of sound, 32-bit memory management, some Graphic Device Interface (GDI) functions, and a few other functions (see Figure 8). These functions must be converted differently. Applications that call DOS system interrupts directly (such as INT 21H) should use the Windows 3.0 Dos3Call function for equivalent interrupt-level support. A list of supported DOS system interrupts can be found in the SMK documentation. Windows supports several types of device drivers (DOS, Windows, GDI) that are not supported by the SMK mapping layer. A Windows application that relies on a DOS device driver loaded via CONFIG.SYS or a TSR must use an OS/2 equivalent (many such drivers already exist). If one doesn't exist, it must be developed using the OS/2 Device Driver Kit. The SMK uses PM display and printer drivers for output. The mapping layer compensates for differences in the Windows and PM drivers, especially in printer escape support, but relies on PM drivers for actual output. PM does not support as many devices as Windows, but does cover the most popular displays (CGA, EGA, VGA, 8514/A, and so on) and printers (PostScript, Hewlett-Packard LaserJet series, Epson, and so on). The SMK handles the proper implicit mapping between these standard device drivers so that they function as if they were the actual Windows drivers. The mapping occurs as a result of normal API calls and does not include direct communication with devices via port I/O, interrupts, or memory mapped I/O. The differences between Windows and OS/2 ring architecture make it impossible to perform direct port I/O from a Windows application. Port I/O can be implemented in an OS/2 Ring 2 IOPL segment, but this requires source code modification and segregation of I/O calls to a separate code segment. It is also impossible to place interrupt service routines within DLLs as in Windows. Interrupts can be handled only at Ring 0 in a device driver under OS/2. Converted applications run 5 to 10 percent slower than native PM applications, because of the time required for the mapping layer to execute. In general, they also perform 5 to 15 percent slower than the original applications running in native Windows protected mode, depending upon the actions performed. SMK application load time and dialog box performance may appear slower at times due to the mapping overhead. The mapping layer must be loaded when an SMK application is first started, so total load time is longer. Intensely repetitive graphic operations involving creating and freeing Windows Device Contexts (DCs) also slow performance. On the other hand, running in OS/2 with HPFS greatly enhances file I/O operations. Advanced SMK Use Because an SMK application is an OS/2 process, it can take advantage of OS/2 features not found in Windows such as threads, long filenames, EAs, and IPC. Adding OS/2 features creates a hybrid application that may be difficult to support in future OS/2 releases. You should add OS/2 features only where appropriate. Threads are particularly useful when long calculations are involved, such as recalculating a spreadsheet; where there is a need to block for an indefinite period of time for a specific event, like blocking on a semaphore used for IPC; or when large amounts of information must be transferred, such as during large file manipulation or serial data exchange. Threading is affected by the mapping layer's synchronization of SMK threads. Only one SMK thread making Windows API calls can be called at a time. Therefore, no Windows APIs can be called in secondary threads; only OS/2 API functions such as DosCreateThread, DosOpen, DosRead, and DosAllocSeg can be called. The current SMK libraries cannot support a multiple-threaded C run-time. Secondary threads calling C run-time functions could corrupt buffers relied on by other application threads calling related C run-time functions. Applications that require full multithreaded C run-time support should use a separate OS/2 DLL (created via LLIBCDLL.LIB) or an OS/2 daemon process. If a daemon process is used, shared memory, pipes, or other IPC mechanisms can be used to communicate between the worker (daemon) process and the user interface (SMK application). There is one exception to the rule that Windows API calls cannot be made from secondary threads. PostMessage may be used in secondary threads as an asynchronous notification mechanism for the background thread to inform the primary SMK thread that an operation has completed. The main SMK thread can also poll background threads for status information. If global variables or shared memory or both are used to communicate between the SMK thread and background threads, critical sections and semaphores should be employed. The primary SMK thread should not block on a semaphore indefinitely or fail to check its message queue often. If it does, other SMK applications will not be scheduled and will receive no CPU time. This is as important for SMK applications as it is for native Windows applications. You may want your converted application to handle long filenames. A common problem in Windows is allocating filename buffers just large enough to hold FAT 8.3 filenames and path buffers just large enough to hold the maximum 64-character path length. When using HPFS, however, you can use the OS/2 function DosQSysInfo to determine the maximum length of a pathname in the current system and allocate your file and path buffers accordingly. If your application includes filename parsing code that depends on the 8.3 format, you should modify the code so that it can handle long filenames-specifically, filenames that contain embedded spaces and multiple dot delimiters. The Microsoft OS/2 Presentation Manager Toolkit fully documents parsing rules as well as other HPFS issues. Once the necessary buffer and parsing changes have been made, simply mark your application as being long-filename-aware by adding the LONGNAMES descriptor to the DEF file. The SMK mapping layer is already long-filename-aware. Once marked, OS/2 will pass long filenames to your application. For example, DlgDirList will now display full 256-character filenames in its list box when the SMK application is marked and run under OS/2. Sample code included with the SMK illustrates long filename and extended attribute manipulation in a modified Windows application. Conclusion The SMK enables Windows software developers to address the OS/2 platform by leveraging their existing Windows development efforts. The SMK conversion process is faster and easier than a native port, and is often as simple as a relink with the SMK libraries. No license fees are required to ship converted applications. You simply include the SMK DLLs with your application. The Windows Help engine (WINHELP.EXE) may also be shipped along with the SMK application, if required. Windows applications converted to OS/2 using the SMK should continue to run on OS/2 2.0 and above, because 2.0 is planned to be compatible with previous OS/2 releases. 1For ease of reading, "Windows" refers to the Microsoft Windows graphical environment. "Windows" refers only to this Microsoft product and is not intended to refer to such products generally. 2As used herein, "DOS" refers to the MS-DOS and PC-DOS operating systems. 3As used herein, "OS/2" refers to the OS/2 operating system jointly developed by IBM and Microsoft. Figure 3. Beta SMK Contents ■ ■ SETUP.CMD Setup (OS/2) ■ SETUP.BAT Setup (DOS) ■ SETUP.DAT Setup (data file) ■ README.TXT General information, release notes ■ PACKING.LST Disk contents description ■ SMK.HLP SMK manual in on-line Advisor format ■ ■ ■ ■ Conversion Tools ■ ■ ■ CONVFONT.EXE Windows to PM font file converter ■ CONVICON.EXE Windows to PM icon (.ICO) converter ■ CONVDLL.EXE Windows DLL conversion utility ■ CONVDLL.INC Include file for Windows DLL conversion ■ RCSMK.EXE SMK-specific resource compiler ■ RCPM.EXE OS/2 PM 1.2 resource compiler ■ WINHELP.EXE SMK Windows help engine (English) ■ ■ ■ ■ Converted Windows Fonts ■ ■ ■ WINMONO.FON Windows fixed pitch font ■ WINPRO.FON Windows dialog box system font (small) ■ SYMBOL.FON Windows symbol font ■ ■ ■ ■ Substitute Windows Libraries ■ ■ ■ BUILDLIB.CMD SMK library create tool (OS/2) ■ BUILDLIB.BAT SMK library create tool (DOS) ■ LIBMK_B.LIB General import LIB (replaces LIBW.LIB) ■ OS2.LIB OS/2 import LIB ■ ■ NOTE: The following files are created during the SMK install process. ■ ■ xLIBCyMK.LIB Replaces xLIBCyW.LIB ■ xDLLCyMK.LIB Replaces xDLLCyW.LIB ■ ■ ■ NOTE: x = model (S, M, L, C) ■ y = math package (E or A) ■ ■ ■ ■ SMK Mapping Layer DLLs (debug and retail) ■ ■ ■ USER_B.DLL USER ■ KERNEL_B.DLL KERNEL ■ GDI_B.DLL GDI ■ PMSMK_B.DLL (SMK only) Windows-equivalent DLLs ■ PMMETA_B.DLL (SMK only) ■ SOUND_B.DLL SOUND ■ KEYB_B.DLL KEYBOARD ■ W87EM_B.DLL WIN87EM ■ SYSTEM_B.DLL SYSTEM ■ ■ *.SYM Symbol files for debug versions of all DLL files ■ ■ ■ ■ Converted Windows Sample Applications ■ ■ ■ MAKEALL.CMD Batch file to build all samples (OS/2) ■ MAKEALL.BAT Batch file to build all samples (DOS) ■ ■ \GENERIC Basic Win 3.0 example ■ \REVERSI Advanced Win 3.0 example ■ (contains sample help file: REVERSI.HLP) ■ \WINDLL How to convert a Win DLL ■ \OS2DLL How to add an OS/2 DLL ■ \EDITFILE Long filenames and EAs ■ \CLOCK Dos3Call example ■ ■ ──────────────────────────────────────────────────────────────────────────── Figure 5. Make File Conversion Original MDIDEMO.MAK #----------------------- # MDIDEMO.MAK make file #----------------------- mdidemo.exe : mdidemo.obj mdidemo.def mdidemo.res link mdidemo, /align:16, NUL, /nod slibcew libw, mdidemo rc mdidemo.res mdidemo.obj : mdidemo.c mdidemo.h cl -c -Gsw -Ow -W2 -Zp mdidemo.c mdidemo.res : mdidemo.rc mdidemo.h rc -r mdidemo.rc The SMK version of MDIDEMO.MAK #------------------------------------- # SMK version of MDIDEMO.MAK make file #------------------------------------- mdidemo.exe : mdidemo.obj mdidemo.def mdidemo.res link mdidemo, /align:16, NUL, /nod slibcemk libmk_b os2, mdidemo rc mdidemo.res mdidemo.obj : mdidemo.c mdidemo.h cl -c -Gsw -Ow -W2 -Zp mdidemo.c mdidemo.res : mdidemo.rc mdidemo.h rc -r mdidemo.rc Figure 6. Make File with Icon Conversion #--------------------------------------- # RCSMK version of MDIDEMO.MAK make file #--------------------------------------- mdidemo.exe : mdidemo.obj mdidemo.def mdidemo.res mdiicon.res link mdidemo, /align:16, NUL, /nod slibcemk libmk_b os2, mdidemo rcsmk -PM mdiicon.res mdidemo.res mdiicon.res: mdidemo.ico convicon mdidemo.ico mdiicon mdidemo.obj : mdidemo.c mdidemo.h cl -c -Gsw -Ow -W2 -Zp mdidemo.c mdidemo.res : mdidemo.rc mdidemo.h rc -r mdidemo.rc Figure 8. Windows Functions Unsupported in SMK ■ ■ User ■ ■ ■ GetKeyboardState ■ IconSize ■ MenuItemState ■ SetKeyboardState ■ SetMenuItemBitmaps ■ TrackPopupMenu ■ ■ Kernel ■ ■ ■ AllocSelector ■ FreeSelector ■ GetCodeHandle ■ GetCodeInfo ■ GetCurrentPDB ■ ValidateFreeSpaces ■ LimitEmsPages ■ ■ 32-bit Memory Management ■ ■ ■ GetWinMem32Version ■ Global16PointerAlloc ■ Global16PointerFree ■ Global32Alloc ■ Global32CodeAlias ■ Global32CodeAliasFree ■ Global32Free ■ Global32Realloc ■ ■ GDI ■ ■ ■ ■ Palette Management ■ AnimatePalette ■ CreatePalette ■ GetPaletteEntries ■ GetNearestPaletteIndex ■ RealizePalette ■ ResizePalette ■ SelectPalette ■ SetPaletteEntries ■ UpdateColors ■ Region ■ CreateEllipticRgn ■ CreateEllipticRgnIndirect ■ CreatePolyPolygonRgn ■ CreatePolygonRgn ■ CreateRoundRectRgn ■ Bitmap ■ ExtFloodFill ■ FloodFill ■ Text ■ GetTextCharacterExtra ■ SetTextCharacterExtra ■ SetTextJustification ■ ■ ■ Keyboard (Limited SMK support) ■ ■ ■ ■ GetKBCodePage ■ GetKeyboardType ■ GetKeyNameText ■ MapVirtualKey ■ OEMKeyScan ■ SetSpeed ■ ToAscii ■ VkKeyScan ■ ■ All Sound APIs ■ Windows[tm] 3.0 Memory Management: Supporting Disparate 80x86 Architectures Paul Yao The latest version of the Microsoft Windows[tm] graphical environment, Version 3.0, has three different memory managers. From a memory management point of view, then, Windows1 is actually three products: real mode Windows, standard mode Windows, and 386[tm] enhanced mode Windows. Each exploits different Intel 80x86 processors, yet a common architecture allows a program to run unchanged under all modes. In Windows, you can determine the current operating mode by looking at the Program Manager's About box. A program can learn the current mode by calling the GetWinFlags routine. This Windows library routine provides a 32-bit value that contains a set of 1-bit flags. A code fragment showing one way to use these flags can be found in Figure 1. All three memory managers are accessed by running the start-up stub, WIN.COM. If you don't choose a mode, Windows picks one for you. On 8086/8088 machines, real mode is selected. Standard mode is selected on 80286 machines or 80386 machines with less than 2Mb of memory. On 80386 systems with at least 2Mb of RAM, Windows chooses 386 enhanced mode. Alternatively, you can force Windows into a specific mode from the command line. Real mode is started by specifying: win /r On 80286 and higher CPUs, standard mode is requested with: win /s And on computers with 80386 and higher CPUs, 386 enhanced mode is requested with: win /3 Each mode has unique characteristics, which I will describe. I'll start by reviewing how the 80x86 segmented address scheme works. Segmented Addressing Windows runs on top of DOS2, which means that Windows runs on members of the Intel-86 family of processors. This family includes the 8086, 8088, 80188, 80186, 80286, 80386SX, 80386, and i486[tm] microprocessors. Each processor addresses memory in segments. Under real mode Windows, segment values are converted directly into physical addresses. Standard and enhanced mode Windows run the processors in protected mode to gain the memory management support built into the higher-end CPUs. While there are important differences between real mode and protected mode operation, both access memory in segments. Windows and its programs inherit this segmented orientation, so that segments are the organizing units of memory used by Windows. There are basically four types of segments available to a Windows program: code segments, a single default data segment, dynamically allocated segments, and resource segments. Code segments are managed by the Windows dynamic linking mechanism, so that code can be demand-loaded from disk when needed and discarded when it is not. The default data segment contains a program's static data, stack, and local heap. Since Windows supports only a single default data segment, programs must be built using the small or medium compiler model. While a single data segment seems to limit a Windows program to a maximum of 64Kb of data, this apparent limitation is easily overcome through the use of dynamically allocated segments. This third category of memory segment is allocated by calling GlobalAlloc. The fourth type of segment, resources, is created for read-only data. Resource segments are used to store various user interface objects, such as menus, dialog box templates, and cursors. Resource segments are also used for GDI objects like fonts and bitmaps. A resource is often in a discardable segment and can be purged from system memory when not needed. This helps Windows conserve memory. Addressing Memory All of the Intel processors mentioned address memory with a 16-bit segment identifier paired with a 16-bit offset value. (The 80386 and higher CPUs can use a 32-bit offset, but the Windows Version 3.0 API only supports a 16-bit offset.) The segment identifier describes the segment being addressed, while the offset value describes the byte in that segment. This two-part memory address is sometimes referred to as a logical address. Software must access memory using a two-part logical address. Together the CPU and the operating system translate a logical address into a physical address. The physical address is a 20-bit, 24-bit, or 32-bit address (depending on the CPU) used to reference the physical memory installed in the system. In enhanced mode, a logical address is first translated into a 32-bit virtual address. The paging hardware built into the 80386 and 80486 CPUs then resolves this reference into a physical address in RAM. A diagram of the translation process for all three of the Windows operating modes is shown in Figure 2. To calculate the maximum amount of physical memory a CPU can address, count the number of address lines. That number is the power of two for the maximum addressable memory. With twenty address lines, the 8088 can address 220 or one megabyte of memory. The maximum addressable memory for each member of the Intel-86 family is shown in Figure 3. Although the 80386 and 80486 processors have 32 address lines, only 24 are available on the standard AT-style bus. For this reason, Windows limits itself to 16Mb of physical memory when running on these systems. The segmented address scheme is the reason that Windows programmers use the NEAR and FAR keywords. At any point in time, the current code and data segments are referenced using the code segment register (CS) and the data segment register (DS). To reference code or data outside the default segments requires FAR addressing. A FAR address requires more work, and is therefore slightly less efficient, than NEAR addressing. FAR addressing requires both the segment and the offset value. When a program references code or data inside one of the current default segments, the simpler NEAR addressing can be used. It is more efficient because only one value-the offset-must be specified. Windows Segment Attributes In Windows, a memory segment is managed according to an attribute selected when the segment is allocated. Figure 4 lists the basic segment attributes. New Windows programmers may find it hard to imagine a discardable memory object, but it is at the heart of Windows' dynamic-linking mechanism. You can think of dynamic linking as a sophisticated overlay manager, which permits any code segment to be purged from system memory. When code segments are needed again, they are loaded from disk into memory in a completely transparent manner. In this respect, dynamic linking is far superior to traditional overlay management software. Discardable segments are also used for segments containing resources, which can be purged from memory when not needed. The presence of moveable memory is not really new in the world of operating systems. It is, however, somewhat unusual for the movement of memory to be detectable from within a program. Real mode operation requires the presence of moveable memory. When a moveable or discardable segment is allocated, Windows issues a memory handle to identify the segment. To read or write in such a segment, the program calls the GlobalLock routine to retrieve the object's address. When a segment is not locked, the memory manager is free to move the segment to minimize the fragmentation of the global heap. GlobalLock also increments an object's lock count to prevent the object from being moved or discarded. (An important exception in protected mode is that GlobalLock does not increment the lock count of moveable objects. As you'll see, the protected mode hardware allows a moveable object to maintain its logical address even when it has been moved.) A fixed memory segment can be allocated for situations in which the first two types of memory cannot be used. This creates the most restrictions for the Windows memory manager, so it should be reserved for special circumstances, such as interrupt service routines in device drivers. Fixed memory objects restrict the memory manager's ability to reorganize system memory, which it must be able to do to recover the memory that would otherwise be lost due to fragmentation of the global heap. These three types of segments operate in a relatively straightforward manner in real mode Windows. In the other two modes, things get a little fuzzy. The built-in memory management of protected mode in the higher end CPUs takes over some of the tasks that Windows itself must do in real mode. As I discuss the Windows operating modes, I'll describe the unique twist put on these three types of segments by each mode. However, a description of the segment-locking process is needed first. Segment Locking As I've mentioned, when you work with moveable or discardable memory segments, you must first call GlobalLock, which asks Windows to tie down a segment. If you are an experienced Windows programmer, you can continue working with dynamically allocated segments in the ways you are familiar with. However, Version 3.0 makes some new things available. There are now four methods for locking a segment, as listed in Figure 5. The first method, a simple lock, is the most common way to fix segments. A simple lock is created with a call to GlobalLock, which returns a far pointer. The pointer is guaranteed to be valid until the lock is disabled with a call to GlobalUnlock. In real mode, when you access dynamically allocated segments, you'll lock the segment for a very short period of time-only as long as it takes to process a single message. Otherwise, you risk creating sandbars in the global heap that prevent the memory manager from reorganizing memory when it needs to. In protected mode, the situation is a little different. As you'll see, the Windows memory manager has help from the hardware. In protected mode, you can lock a segment once, and leave it locked until you are finished using the memory. Windows protected mode memory managers can still move memory around, and even swap memory to disk in enhanced mode. The other three types of locks are for special circumstances (see Figure 5). For example, in real mode, to lock a segment for a relatively long period of time, you should wire the segment by calling GlobalWire instead of GlobalLock. A wired segment is moved first so that it doesn't obstruct the movement of other memory objects. The third type of segment lock is for protected mode, to fix a segment at a given physical address. When a segment has a fixed physical address, a low-level lock prevents it from moving in the linear address space of protected mode Windows. Ordinarily, segments can be moved in protected mode Windows, but the hardware memory management fools you because the segment's logical address doesn't change when the segment moves. A fixed physical address lock prevents such movement. This is the most secure way to tie down a segment in standard mode but is overkill in most applications. It is required mainly for device drivers that need to make sure that the physical address of a dynamically allocated segment doesn't change. The fourth type of segment lock prevents a segment from being swapped to disk in 386 enhanced mode. The virtual memory system of this mode swaps memory pages to disk in a manner that is entirely transparent to your program. However, when a segment is page-locked, it cannot be swapped to disk. Page-locking also fixes the physical address. This is the most restrictive type of lock in Windows enhanced mode. Like the fixed physical address lock of standard mode, a page-locked segment is overkill for most applications but is useful for device drivers that need to be present in memory at all times. Consider the delay that might be caused if critical parts of the display or keyboard drivers were swapped to disk. Or imagine the problems that would be caused if the kernel's swapping code itself were swapped to disk. Inside the module definition file (DEF) of Windows programs and dynamic-link libraries (DLLs), there are entries that describe the memory disposition of code and data segments. For example, an entry like the following makes code moveable and discardable by default: CODE MOVEABLE DISCARDABLE In general, segments in a DLL that are FIXED inherit the most secure locking of each of the operating modes in Windows. For example, if a DLL's default data segment were declared fixed in real mode and standard mode, it would be fixed in the physical address space: DATA FIXED In enhanced mode, it would be page-locked to prevent it from being swapped. The rationale is that device drivers are implemented in DLLs, and therefore they should get this service automatically. In a Windows program, however, the FIXED attribute is treated a little differently. For example, assume an application has the following in its DEF file: CODE FIXED DATA FIXED In real mode, both code and data segments would be fixed. But in protected mode, the Windows memory manager overrides these flags and does not make them fixed or page-locked. The reason is simple: the Intel protected mode mechanism already gives fixed logical addresses to code and data segments without invoking the more restrictive types of memory locks. As this article goes to press, there is a known problem in Windows 3.0 running in protected mode. It involves the use of the GMEM_FIXED flag when dynamically allocating a segment. If you allocate a segment like the following from an application or a DLL, the segment will be allocated as fixed and page-locked. hmem = GlobalAlloc (GMEM_FIXED, dwSize); This is a problem because these higher segment- locking attributes should only apply when such a segment is allocated from a DLL. From an application, it should be sufficient to give it a fixed logical address and still allow the physical address to change. This will be altered in a future version of Windows. Real Mode Experienced DOS programmers will find much that is familiar in Windows real mode. Also, if you started programming for Windows with Version 1.x or Version 2.x, everything is pretty much the same in real mode under Windows Version 3.0. For those previous versions, Windows operated only in real mode. All 80x86 chips can run in real mode. In real mode, the size of the physical address space is 1Mb. For the sake of compatibility, the higher-end members of this family inherit this limitation when running in real mode. This is true even for chips that have more address lines than the 8088/8086 CPUs. Real mode gets its name from the fact that memory is accessed using real, physical addresses. As depicted in Figure 6, the logical-to-physical address translation process involves multiplying the segment identifier by 16 (shifting it left by 4 bits) to create a 20-bit base address. The offset is added to this to access a particular byte or set of bytes in the memory segment. The three Windows segment attributes-fixed, moveable, and discardable-were created with real mode in mind. They allow the Windows memory manager, in cooperation with Windows programs, to manage memory effectively. Even though a segment's logical address is identical to its physical address, memory in real mode can be moved because Windows does not provide a segment's address at segment allocation time. Instead, a memory handle is issued. As long as a segment isn't locked, the memory manager is free to move or discard segments as needed. To retrieve the address of a segment, a program locks the segment. The address is guaranteed to be valid until the segment is unlocked. The segment allocation and segment-locking mechanism work in an identical manner in real and protected mode. Thus, it is a simple matter to create programs that work the same way in all operating modes. But, as you'll see when I discuss protected mode operation, there are shortcuts that can be taken by a Windows program in protected mode. Before looking at the two protected modes in Windows, there is one more real mode topic to explore: expanded memory. Real Mode and Expanded Memory Expanded memory is memory that is made available according to the Expanded Memory Specification (EMS). It describes a bank switching mechanism for using memory that is temporarily mapped into unused portions of the real mode address space. From the beginning, Windows made use of EMS memory. In version 1.x, the use of EMS memory was limited to support for DOS applications running under Windows. When EMS was available, Windows 1.x swapped idle DOS applications to EMS memory instead of swapping them to disk. In Version 2.0, introduced in the fall of 1987, Windows started using EMS memory for Windows applications themselves (see "EMS Support Improves Microsoft Windows 2.0 Application Performance," MSJ, Vol 3., No. 1). The support under real mode Version 3.0 is identical to Windows 2.x EMS support. In Version 3.0, real mode Windows uses EMS memory, when available, to increase the size of the address space for each Windows program. An EMS bank is automatically allocated for every Windows program (see Figure 7). As a program runs, the Windows memory manager treats the EMS memory as an extension of the global heap. In fact, since each program has its own EMS bank, each program has its own private extension to system memory that does not have to be shared with other programs. EMS memory is largely transparent to programs, since Windows works behind the scenes to switch the correct EMS memory into view. The result is less competition for system memory and better overall system performance. When running in real mode, Windows uses EMS memory if it finds an EMS driver in the system. While EMS memory helped relieve the memory shortage in Version 2.x, it no longer plays a crucial role in Windows memory management. Banked EMS memory is abandoned by the other two operating modes, because they have access to extended memory. Extended memory is not banked; instead all extended memory is simultaneously accessible. Extended memory provides a more flexible approach to breaking the 1Mb boundary that has hounded Windows and DOS programmers for so long. Standard Mode From the beginning, it was intended that Windows would eventually run in protected mode, as it does in Windows Version 3.0. The most obvious difference between protected mode and real mode operation is that a larger address space is available: up to 16Mb of RAM can be addressed. Standard mode is sometimes described as the way Windows takes advantage of the protected mode of the 80286 processor. In fact, standard mode can run on the 80386 and higher processors as well. Even though standard mode is associated with the less powerful 80286 CPU, you might prefer it over enhanced mode. Standard mode actually runs a little faster than enhanced mode because it doesn't rely on the virtual device drivers. These drivers provide some nice features, such as allowing DOS programs to run in a window. But they slow things down a bit, particularly for those programs that access the disk heavily. Understanding how the protected modes of the Intel processors access memory will help you understand exactly how Windows uses these modes. As in real mode, protected mode uses a segmented addressing scheme. But the segment portion of the address does not refer to a physical memory location. Instead, it contains an index into a segment lookup table. In protected mode, there are two types of lookup tables: the global descriptor table (GDT) and the local descriptor table (LDT). Intel designed its protected modes flexibly, enabling different operating systems to use these two types of tables in several different configurations. For example, a single GDT could be used to reference all of an operating system's shared memory. Each program could then have a private LDT with which to access private segments. If you have programmed in OS/2 systems, you are probably aware that OS/23 uses the GDT itself and gives each process its own LDT. This minimizes the possibility of one program adversely affecting the operation of other programs. A program can only access data through its own LDT. While this helps protect programs from each other, it requires a little more work to share data. Data sharing in OS/2 requires the duplication of selector information. Each process that needs access to a shared segment must have an entry in its LDT. To simplify things, if a segment is allocated as shareable, its segment selector is the same for all processes in the system. A single segment is shared, then, by creating an entry in the LDT of every process that wishes to access a segment. Windows 3.0 sets up protected mode addressing using another configuration. The GDT is reserved for the protected mode memory manager, and a single LDT is created for all Windows libraries and applications. Every segment allocated for Windows programs and every segment allocated for DLLs is referenced from one systemwide LDT. A single LDT means that all programs share a common address space, which makes it easy to share segments between programs. But watch out! In a future version of Windows, it is likely that every program will be allocated a private LDT, just as is done in OS/2. When this happens, each Windows program will have a private address space. At that time, sharing data will require one of three methods: the Clipboard, Dynamic Data Exchange, or DLLs. Figure 8 depicts how a logical address is translated into a physical address using the LDT. The segment identifier, which is called a segment selector in protected mode, contains a descriptor table index. When a segment is referenced, its descriptor table information is loaded into a set of registers known as the shadow registers. The name comes from the fact that these registers are entirely invisible to application software. Among the values loaded into the shadow registers are a segment's base address, its size (also known as its limit), and various flags. Translating a logical address into a physical address involves adding the descriptor table's base address to the offset. Since the location of a segment is not part of the segment address itself, but instead is kept in the descriptor table, segments can be moved freely in protected mode without changing their logical address. This represents an important improvement over real mode addressing, in which the segment's logical address is the same as its physical address. Another improvement over real mode involves automatic error checking. Several checks are performed to make sure that the contents of memory are protected from unauthorized access. For one thing, when a program addresses memory, the CPU automatically checks for a valid segment selector. A valid segment selector is one that has a valid entry in one of the current descriptor tables (one LDT and one GDT table is current at any time). If a valid entry is not found, a protection exception occurs. At the hardware level, a protection exception creates a hardware interrupt 13H General Protection Fault (also known as a GP fault). Windows responds to this interrupt by displaying a message box with the fatal error message: "Unexpected Application Error," sometimes known as a "UAE Error." Other errors can cause a GP fault. For example, if a program tries to reference memory beyond the limit of a segment, a GP fault occurs. This prevents a program from overrunning the boundaries of a segment and possibly overwriting another program's data. Another cause of GP faults is attempted access to a protected segment, a violation of the privilege levels built into protected mode addressing. Associated with every segment is a 2-bit privilege flag that defines four privilege levels: 0 through 3. The highest privilege level, 0, is given to the most trusted software. OS/2 programmers know that the OS/2 kernel runs at privilege level 0, also known as Ring 0. OS/2 programs, and a large part of Presentation Manager, run at Ring 3. The rest of Presentation Manager, and all display and printer device drivers, run at Ring 2. In Windows 3.0, the protected mode control programs (DOSX.EXE in standard mode and WIN386.EXE in enhanced mode) run at Ring 0. Windows programs, and the Windows DLLs, run at Ring 1. If a future version of Windows runs at Ring 3, Windows will have room to grow. It will also minimize the differences between the protected mode environment of OS/2 and the protected mode of Windows and ease porting Windows programs to OS/2. 386 Enhanced Mode Windows 386 enhanced mode takes advantage of the paging hardware on 80386 and above CPUs to implement virtual memory (see Figure 9). When running in this mode, two memory managers are present: the 386 control program, WIN386.EXE, and the Windows kernel, KRNL386.EXE. The enhanced mode kernel uses the protected mode LDT to manage system memory in the same way as the standard mode kernel. But instead of referencing physical memory, the enhanced mode LDT references into a virtual address space that can be up to four times as large as the available RAM. The kernel treats the virtual address space as the global heap that it manages. The second memory manager, WIN386, is responsible for supporting this virtual address space. It does so by enabling the paging hardware built into the 80386 chip. The paging hardware makes use of memory tables that are similar to the LDT, but which operate at a lower level: the paging tables. The paging tables translate a 32-bit linear address into the actual physical addresses for a page of memory. When a page is accessed that is not present, a page fault occurs. At that time, the virtual memory manager runs to disk to find the needed page. It loads it into memory, fixes up the page tables to reflect the changed system configuration, and restarts the machine instruction that actually caused the page fault to occur. This way, virtual memory is entirely transparent to the software. Like virtual memory systems built on mainframe computers, the virtual memory of the 386 control program simulates a large linear address space by shuttling 4Kb pages between disk and RAM. The amount of virtual memory that can be created is limited by the disk space that is available for swapping, and by a limit built into the 386 control program that supports a virtual address space that is up to four times the available physical RAM. Therefore, with 5Mb of RAM and enough swap space on disk, a 20Mb virtual address space can be created. One feature of Windows 3.0 is that the enhanced mode kernel does not start to discard objects until it has reached the limit of the virtual address space. Although this means that the 386 control program may swap discardable objects, it is faster to page out discardable data than it is to reload and relocate an entire segment. Protected Mode and Memory Locking When running in real mode, programs must continually lock and unlock dynamically allocated segments. The reason, as I mentioned earlier, is that segments cannot be moved when locked. Since moving segments is crucial to the health and well-being of system memory, programs must not lock segments any longer than they have to. The situation is different in protected mode. Once a segment is allocated, it will retain the same logical address even though it may change physical locations in system memory. The logical address, after all, contains an index into a descriptor table. A memory object can be moved in protected mode by simply updating the corresponding entry in the descriptor table. Thus, the Windows memory manager can move memory in a manner that is completely transparent to applications. Windows programs running in protected mode can do something that is inconceivable in real mode: keep data segments continually locked. This means you can lock a segment immediately after you allocate it to retrieve the segment's logical address. You can keep the segment locked as long as you need to and not bother to unlock the segment until you are ready to free the memory object. Since this will not interfere with the management of system memory, you are free to use this approach without fear that you will somehow disrupt the system. Of course, to do this safely, you must first check the GetWinFlags routine to make sure that you are running in protected mode. Also, keep in mind that such a lock will keep discardable segments from being discarded. Therefore, you'll probably want to limit your use of this technique to moveable segments. In my next article, I'm going to discuss memory use by applications in Windows Version 3.0. I'll describe every available place for storing a byte of data and describe the impact of each on the system. 1For ease of reading, "Windows" refers to the Microsoft Windows graphical environment. "Windows" refers only to this Microsoft product and is not intended to refer to such products generally. 2As used herein, "DOS" refers to the MS-DOS and PC-DOS operating systems. 3As used herein, "OS/2" refers to the OS/2 operating system jointly developed by IBM and Microsoft. Figure 1. Call GetWinFlags to determine the current operating mode. DWORD dwFlags; dwFlags = GetWinFlags (); if (dwFlags & WF_PMODE) { if (dwFlags & WF_STANDARD) { /* STANDARD MODE */ } else { /* ENHANCED MODE */ } } else { /* REAL MODE */ } Figure 3. 80x86 Processor's Physical Address Space ■ ■ Processor Address Lines Physical Address Space ■ ■ ■ ■ 8088 20 1Mb ■ 8086 20 1Mb ■ 80186 20 1Mb ■ 80188 20 1Mb ■ 80286 24 16Mb ■ 80386SX 24 16Mb ■ 80386 32 4Gb ■ i486 32 4Gb ■ Figure 4. Windows Memory Segment Attributes ■ ■ Attribute Description ■ ■ ■ ■ Fixed Address of segment is fixed ■ Moveable Address of segment is not fixed, unless object ■ has been locked ■ Discardable Segment can be purged from memory as needed ■ Figure 5. Four Types of Windows Memory Locks Type of Lock Description Simple lock A segment is locked with a call to GlobalLock. This routine fixes a segment in place, and returns a far pointer to the data. Wired A segment is wired with a call to GlobalWire. When a segment must be fixed for a period of time, this routine first moves the segment to a lower part of memory before fixing a segment and returning a far pointer to the data. Fixed physical address A segment is fixed with a call to GlobalFix. In real mode, this is the same as a locked segment. In standard and 386 enhanced mode, this fixes a segment's physical address. Page-lock A segment is page-locked through a call to GlobalPageLock. In real mode and standard mode, this has the same effect as locking a segment. In enhanced mode, it prevents the virtual memory pages of a segment from being swapped to disk. Measure Your Machine's Activity and Learn How to Use OS/2 Threads with CPUMETER Bob Chiverton It's a miracle my first sports car didn't explode. It had only two speeds-legal and illegal. But I was 19, it was summer, and the car wasn't really mine. I borrowed it from my friend, Roman. And if Roman knew I pushed the tachometer into the red zone in every gear including reverse, you wouldn't be reading this now. I don't redline tachometers anymore, now that I pay for my auto repairs, but I still enjoy speed-especially in my computers. That's why I wrote CPUMETER, an OS/21 Presentation Manager program that shows when I'm redlining the CPU. It also taught me about using threads. CPUMETER is a tachometer for your computer. "Tachometer" is not quite right, though. CPUMETER is really a CPU activity meter. It shows you the percentage of time the CPU is busy running threads. In this context, the CPU is active (that is, busy) when it is executing a thread and inactive when all threads are blocked or frozen (that is, not runnable). When iconized, CPUMETER uses a needle gauge that goes from 0 to 100 percent in increments of 10 percent. When your computer is idle, the needle is at 0. At full load, the needle points to 100. The client window is somewhat more informative because it keeps track of where the needle has been and displays a constantly updated performance histogram. Does it surprise you to learn that sometimes no threads are running? It surprised me. The CPU doesn't pack up and leave town when all threads are blocked. It executes code inside the scheduler. The CPU loops, waiting for a thread to become ready. Some CPU cycles are spent servicing I/O time-outs, semaphore time-outs, and so on, but otherwise the CPU is polling for ready threads. So it's not that there are no runnable threads, it's that there are no runnable threads except for the OS/2 scheduler thread. CPUMET Before I discuss CPUMETER, I'd like to present two simpler versions, CPUMET and CPUMETE. CPUMET's main function creates a standard Presentation Manager (hereafter "PM") window (see Figure 1). After calling WinSetWindowPos to size and position the window on the screen, it calls _beginthread twice to create two additional threads of execution before dropping into the familiar WinGetMsg/WinDispatchMsg loop. There are two peculiar things about CPUMET. It looks odd, and it uses three threads. CPUMET has no client window-just a title bar window and a system menu (see Figure 2). CPUMET displays its output in the title bar. This is the easiest way I know to display text in a PM application: just call WinSetWindowText when you have a string of text to output. There are no presentation spaces, or GPI functions, which makes everything nice and simple. Using the title bar this way is also helpful during debugging. One application I wrote used two object windows to manage two separate DDE conversations. Object windows, by definition, are not visible, and mine had no need to be visible since they never interacted with the user. But when I debugged them, I made their title bars visible and displayed text in them that revealed their internal states (for example, "Waiting for Reply"). The CPUMET client window does exist, it's just hidden. To hide the client window, CPUMET creates the standard window by calling WinCreateStdWindow. hwndFrame = WinCreateStdWindow (HWND_DESKTOP, WS_VISIBLE, &flFrameFlags, szClientClass, NULL, 0L, NULL, NULL, &hwndClient) ; The frame creation flags that affect the client window (such as FCF_SIZEBORDER, FCF_MINMAX, FCF_HORZSCROLL) are not used. Since the client window isn't used, there's no need to maximize it or change its size. CPUMET doesn't use menus or an icon either, so the FCF_MENU and FCF_ICON flags aren't used, and the hIcon and hMenu parameters are NULL. CPUMET next calls WinSetWindowPos. WinSetWindowPos (hwndFrame, HWND_TOP, 0, 0, WinQuerySysValue (HWND_DESKTOP,SV_CXSCREEN)/3, WinQuerySysValue (HWND_DESKTOP,SV_CYTITLEBAR), SWP_SHOW | SWP_SIZE | SWP_MOVE); This positions the window in the lower left of the screen. Using a width of SV_CXSCREEN/3 and a height of SV_CYTITLEBAR, the frame window is one third the width of the screen and is just high enough to show the title bar. CPUMET uses three threads, which is unusual for such a tiny program. But because of the way the OS/2 scheduler works, you'd have difficulties using fewer. The OS/2 Scheduler and Threads The OS/2 scheduler starves threads. A priority-based scheduler, it always assigns the CPU to the highest-priority runnable thread. A thread is classified and runs in one of three priority classes: high priority, general priority, or low priority. These are also known as time-critical, normal, and idle-time, respectively. The special properties associated with them make CPU load calculations possible. The scheduler runs the highest priority thread for as long as it needs. It doesn't matter how many other threads are waiting. They will not get the CPU-they will starve-if a higher priority thread is running. (In multiple-CPU systems, the scheduler may execute the n highest priority threads.) The scheduler is rude, too. It will cut right into the middle of a thread's time slice and yank the CPU away if another thread with a higher priority becomes ready. I'd like to clear up some misconceptions about time slices. There are two kinds in OS/2: those you specify with the TIMESLICE directive in the CONFIG.SYS file and real time slices. I'll distinguish between them by calling the first a "TIMESLICE" and the second a "time slice." Real time slices are 31.25 milliseconds (msec) long. This is the length of time between two consecutive ticks of the system clock. With every tick, the scheduler sees if another thread with a higher priority is ready. If one is, the scheduler will preempt the current thread in favor of the higher priority one. When a thread gets a time slice, it's getting one of 31.25 msec duration. A thread can be preempted in the middle of a time slice, too, if it calls an API function and a higher priority thread has become ready to run since the last clock tick. TIMESLICES become important only when several threads of equal priority are all ready to run and are all CPU-bound. Then each thread will run for a full TIMESLICE before control is passed to the next thread of the same priority. Note that the value of TIMESLICE must be an integral number of 31.25 msec time slices. Calculating the Load In OS/2, an idle-time thread gets CPU cycles only if higher priority threads aren't available. This is the key to calculating the load. CPUMET simply uses an idle-time thread to soak up CPU cycles when no other threads are running. The busier the CPU is (with other threads), the fewer CPU cycles the idle-time thread will get. For example, suppose the idle-time thread is getting 10 percent of the total CPU cycles every second and the rest of the time the scheduler runs other threads. By this definition, the CPU load is 90 percent. The trick is to record the CPU cycles/second the idle-time thread gets somehow. CPUMET does this indirectly, using the global variable lCount and two threads. One thread that I'll call the counting thread runs idle-time and continually increments lCount. The other thread, the timing thread, runs time-critical and resets lCount to zero once every second. The idle-time thread function is CountingThread, shown below. VOID CountingThread () { DosSetPrty (PRTYS_THREAD, PRTYC_IDLETIME, 0, tidCounting); while (TRUE) lCount++; } This thread is created when the message queue thread calls _beginthread in the main function. tidCounting = _beginthread (CountingThread, iCountingThreadStack, THREADSTACKSIZE, NULL); First, the counting thread calls DosSetPrty to change its priority to idle-time. By default, a thread inherits the priority of its creator thread, which in this case is the message queue thread. The message queue thread, in turn, inherited its general-priority class from the thread in the parent process (the PM shell) that called DosExecPgm to launch CPUMET. Next, the counting thread drops into an infinite loop. while (TRUE) lCount++; Any CPU cycles this thread now receives will be used to increment lCount. This is the way to measure (indirectly) the CPU cycles/second the idle-thread gets. To get the "per second," you need the timing thread. The timing thread resets the value of lCount once per second. It is created by the message queue thread with the following call. tidTiming = _beginthread (TimingThread, iTimingThreadStack THREADSTACKSIZE, NULL); The first thing the timing thread does is change its priority to time-critical. The reason for this will be discussed in a moment. Next, it drops into an infinite loop. while (TRUE) { DosSleep (1000L); WinPostMsg (hwndClient, WM_SEM1, MPFROMLONG(lCount), NULL); lCount = 0L; } Once per second (every 1000 msec), the timing thread wakes up, posts the message queue thread a WM_SEM1 message with lCount in the mp1 parameter, resets lCount to zero, then goes back to sleep. This continues until CPUMET terminates. The timing thread runs time-critical to guarantee it will get the CPU when it wakes up (unless another time-critical thread is running). The thread has to be put to sleep with DosSleep until then so it doesn't hog the CPU. DosSleep is one of those API calls that allows the scheduler to preempt the running thread immediately (in this case the timing thread) and dispatch the next highest priority runnable thread (see Figure 3). Usually when you have a global variable like lCount that is modified by two or more threads, you protect the variable with a semaphore. But in this case no damage results if, in the highly unlikely event, the counting thread increments lCount between two DosSleep calls in the timing thread. Note that WinPostMsg uses the WM_SEM1 message. WM_SEM1 messages aren't stored in the message queue; instead, they are handled like WM_TIMER messages. Multiple WM_SEM1 messages are consolidated into one, ensuring that only the most recent lCount is retrieved and displayed. The message queue thread extracts lCount from the WM_SEM1 message and calls WinSetWindowText to display it in the title bar. You can tell how busy the CPU is by looking at the value in the title bar. The lower the number, the busier the CPU. Too Much of a Good Thing Only one instance of CPUMET must run at a time, because extra instances will load the CPU just as any other app would. Any CPU cycles that a single instance of CPUMET would have soaked up would then be divided equally among the instances. Because each counting thread has the same priority, the scheduler runs them on a round-robin basis (this is where TIMESLICE comes into play). For n instances, each counting thread will compute lCount/n counts per second. If CPUMET were a Windows application, you could use the hPrevInstance parameter in WinMain to detect multiple instances. Unfortunately, PM has no equivalent. But it has something Windows lacks-system semaphores. Upon entry to main, before doing anything else, CPUMET tests for the existence of another instance with the following statement: if ( DosCreateSem(CSEM_PUBLIC, &hSem, "\\sem\\cpumeter.sem") ) DosExit (EXIT_PROCESS, 0) ; The function DosCreateSem will attempt to create the system semaphore "\sem\cpumeter.sem". The first instance will succeed, but subsequent instances will fail and exit via DosExit. This is a somewhat untraditional role for a semaphore. It matters not one iota if the semaphore is set or clear, only that it exists. The existence of the semaphore represents the existence of a single instance of CPUMET. Of course, we have to destroy the semaphore when CPUMET terminates, which is why we need to store the handle in the global variable HSYSSEM hSem. The semaphore is destroyed in the ClientWndProc with the following code: case WM_DESTROY: { DosCloseSem (hSem) ; return 0 ; } This allows the next instance to recreate the semaphore and run. CPUMETE CPUMET calculates the CPU load. However, there is a problem with using lCount to represent it, due to the inverse relationship between lCount and the CPU load. The lower the value of lCount, the higher the load. It would be better to display an amount that varies in direct proportion to the load. Another problem is that lCount's value is dependent on a computer's CPU speed. A faster computer will calculate a higher value for lCount than a slower one, everything else being equal. I solved both problems in CPUMETE.EXE by introducing the variable lCountMax, which represents the maximum value lCount can attain, and a new thread to calculate lCountMax. This allows you to display the CPU load as a percentage of maximum load. Let's see how CPUMETE accomplishes this (see Figure 4). Adding a Calibration Thread If you know the maximum value lCount can be for a given computer, you can calculate CPU usage as a percentage of this maximum with the following simple equation. load = [1 - (lCount/lCountMax)] * 100% The trick is to calculate lCountMax, which is done by the calibration thread. The calibration thread function looks almost identical to the counting thread function. VOID CalibrationThread () { DosSetPrty (PRTYS_THREAD, PRTYC_TIMECRITICAL, 30, tidCalibration); while (TRUE) lCountMax++; } The differences are that it uses lCountMax instead of lCount, and it runs time-critical. Running time-critical guarantees the thread will get the maximum CPU cycles possible (assuming there are no other time-critical threads competing for the CPU). Notice that the DosSetPrty call uses 30 for the sChange parameter. Within a priority class, a thread is further distinguished by its relative priority level, ranging from 0 to 31. If two threads belong to the same priority class, the one with the higher level will get the CPU before the other, all else being equal. The calibration thread has a priority level of 30, one fewer than the maximum. You need to reserve level 31 for a higher priority thread so it can preempt the calibration thread after it runs for one second. The thread doing the interrupting is the message queue thread. Following is the sequence of code added to the main function for this purpose. DosSetPrty (PRTYS_THREAD, PRTYC_TIMECRITICAL, 31, 1); tidCalibration=_beginthread(CalibrationThread, iCalibrationThreadStack, THREADSTACKSIZE, NULL) ; DosSleep (1000L); DosSuspendThread (tidCalibration); DosSetPrty (PRTYS_THREAD, PRTYC_REGULAR, 0, 1); You identify the message queue thread with an ID of 1 in the DosSetPrty call. Thread IDs come on a first-come, first-serve basis, starting with 1. Don't expect your other n threads to follow in sequential order as 2, 3, . . ., n1. Some API calls generate captive threads-that is, captive within a DLL. Although you didn't explicitly create these threads, your process will assign thread IDs to them. The rule here is to not assume thread ID values; use the thread ID generated from a _beginthread or DosCreateThread call. After setting the message queue thread's priority, CPUMETE creates the calibration thread. Although it runs time-critical, the calibration thread will not run yet because the message queue thread has a higher priority level of 31. But then DosSleep is called, knocking out the message queue thread for 1000 msec. During this time, the calibration thread becomes the highest priority runnable thread in the system and gets all the CPU cycles. The calibration thread gets preempted after one second, when the message queue wakes up. By this time, lCountMax is calculated. The calibration thread is no longer needed and is put to sleep by DosSuspendThread. This freezes the thread; the scheduler will ignore it until it is unfrozen. In general, one thread cannot kill another thread. A thread can kill only itself or the entire process, by calling DosExit: DosExit (fTerminate, usExitCode) ; and using EXIT_THREAD or EXIT_PROCESS for the fTerminate parameter. Since the calibration thread doesn't call DosExit, the best you can do is freeze it from another thread. Finally, the message queue thread resets its priority so it doesn't hog the CPU. CPUMETE's title bar is more informative than CPUMET's (see Figure 5). Instead of just displaying lCount like CPUMET did, it displays three values in the following format: x / y -> z% where x represents lCount, y represents lCountMax, and z represents the load. Occasionally, x will exceed y. This does not represent a serious error, but rather inaccuracies associated with the 31.25 msec resolution of the system clock. Adding the Tachometer CPUMETE uses a set of eleven icons to display the CPU load when the app is iconized. HPOINTER hIcon [11] ; I created these icons with the Icon Editor. The hIcon array is initialized in the WM_CREATE code, case WM_CREATE: { for (i=0; i<=10; i++) hIcon [i] = WinLoadPointer (HWND_DESKTOP, NULL, (100+i)); return 0; } and the icons are released in the WM_DESTROY code. case WM_DESTROY: { for (i=0; i<=10; i++) WinDestroyPointer(hIcon[i]); DosCloseSem(hSem); return 0; } CPUMETE uses hIcon[0] when the CPU is at 0 percent, hIcon[1] at 10 percent, and so on, up to hIcon[10] at 100 percent. When the message queue thread receives a WM_SEM1 message, it extracts lCount from the mp1 parameter and calls the nearest_10_percent function. i = nearest_10_percent (LONGFROMMP (mp1)); This function uses the load equation presented earlier and returns an integer from 0 to 10, in increments of 1, corresponding to a CPU load of 0 percent to 100 percent. The return value i is used in the WM_SETICON message sent to the frame window to change the icon. WinSendMsg (hwndFrame, WM_SETICON, hIcon [i], NULL) ; To add icons, I needed to add the FCF_MINBUTTON and FCF_ICON frameflags to the WinCreateStdWindow call in main. I also changed the HICON parameter to ICON00 to represent an initial CPU load of 0 percent. Finally, I had to change the make file to accommodate the CPUMETE.RC resource file. CPUMETER CPUMETE's icons are useful for showing the instantaneous CPU load, but it would be better to see the load over a period of time. That would give us a clearer picture of the demands an application places on the CPU. What CPUMETE needs is a histogram. CPUMETER (see Figure 6), the final version of the program, displays a histogram of the CPU load in its client window (see Figure 7). The histogram is a simple bar graph, consisting of black vertical bars on a white background. The graph is updated each second. Each bar is one pixel wide, which means a client window 400 pixels wide can display up to 400 bars. On a VGA display, about 11 minutes of load history can be displayed in a maximized window. Each bar's height is directly proportional to the CPU load it represents and is scaled to the client window height. For example, given a client window that is 376 pixels high, a bar representing a 100 percent load will be 376 pixels high, a 50 percent load will be 188 pixels high, and so on. When the client window is resized vertically, the bars are resized vertically too. The histogram logic is contained in the file HISTGRAM.C (see Figure 8). This file contains just five functions, corresponding to the five messages explicitly processed by ClientWndProc: create_histogram (WM_CREATE), size_histogram (WM_SIZE), paint_histogram (WM_PAINT), update_histogram (WM_SEM1), and destroy_histogram (WM_DESTROY). The histogram is created when ClientWndProc processes the WM_CREATE message. WM_CREATE : o o o create_histogram (hab, hwnd) ; The hab and hwnd variables are used by create_histogram to get a handle, hPS, to a micro presentation space (micro PS) by calling WinOpenWindowDC and GpiCreatePS. hdc = WinOpenWindowDC (hwndClient) ; hPS = GpiCreatePS (hab, hdc, ...) ; Then hPS is used to set the foreground and background colors and the line type for the PS: GpiSetBackColor (hPS,...); GpiSetColor (hPS,...); GpiSetLineType (hPS, ...); A micro PS is a good choice for this application. With a cached micro PS, CPUMETER would have to reset the colors and line type each time it reacquired a handle to the PS (via GpiBeginPaint or GpiGetPS). Using a micro PS is more efficient because it retains its state information between GpiBeginPaint calls. Using a normal PS would be overkill because CPUMETER is only outputting to the screen; it doesn't need a normal PS's capability to reassociate another display context, such as a printer DC, with itself. Also, there's no need for the normal PS segments. The histogram's main data structure is an array of ints. Each element in the array corresponds to one bar in the histogram. There must be enough elements to accommodate a maximized client window. This is determined by calling WinQuerySysValue. iArraySize = WinQuerySysValue (HWND_DESKTOP, SV_CXSCREEN) ; What CPUMETER needs is an array like the following. INT anIntegerArray [iArraySize] ; But because iArraySize is determined at run time, the array is allocated dynamically, rather than statically. This is done by using the calls to WinCreateHeap and WinAllocMem shown below. hHeap = WinCreateHeap (0,0,(sizeof(INT)*iArraySize), 0,0,0) ; npArray = (PINT) WinAllocMem (hHeap,(sizeof(INT)*iArraySize)); WinCreateHeap sets up the heap in the automatic data segment. This is one difference between PM and the Microsoft Windows[tm] graphical environment. In Windows2, a local heap "comes standard" with your application, but in PM it's an option your application has to order. The variable npArray points to the first element in the array. If you feel uncomfortable with dynamic arrays, you can still use the familiar notation npArray[i] to reference element i. Each array element stores a CPU load, ranging in value from 0 to 100. When the client window receives a WM_PAINT message and calls paint_histogram, the histogram bars are redrawn. Each bar is scaled to the client window's height by scaling the array element value. ptl.y = (npArray[i] / 100.) * yClientHeight ; When CPUMETER terminates, the destroy_histogram function releases the micro PS and frees the heap. The heap, being on the automatic data segment, would be destroyed by OS/2 if CPUMETER didn't do it. But it's a good habit to clean up after yourself. To accommodate the histogram, the FCF_MINMAX and FCF_SHELLPOSITION flags were added to the flFrameFlags variable in main. The FCF_SHELLPOSITION flag tells PM to give our client window an initial size and position, so I removed the call to WinSetWindowPos that followed WinCreateStdWindow in CPUMET and CPUMETE. Finally, you need the new function nearest_1_percent to compute the CPU load to the nearest 1 percent. This is used in the code that processes WM_SEM1 messages in ClientWndProc to update the histogram. i = nearest_1_percent (...); o o o update_histogram (i) ; Conclusion CPUMETER is fun to watch and instructive. If you have access to the sample programs in Programming the OS/2 Presentation Manager (Microsoft Press, 1989), try running CPUMETER with the samples from Chapter 17. You'll discover that multithreaded programs make better use of the CPU than their single-threaded counterparts. That is, the multithreaded applications load the CPU more heavily than their single-threaded versions do. CPUMETER shows this quite clearly. On a warm summer night, start up CPUMETER, then bring up your favorite program and take it for a spin. And if you get the urge. . .redline it! 1As used herein, "OS/2" refers to the OS/2 operating system jointly developed by IBM and Microsoft. 2For ease of reading, "Windows" refers to the Microsoft Windows graphical environment. "Windows" refers solely to this Microsoft product and is not intended to refer to such products generally. Learning Windows[tm] Part III: Control Windows and MDI Support Marc Adler Learning Windows is a series of articles that explores programming the Microsoft Windows[tm] graphical environment Version 3.0. In the previous two articles I discussed the event-driven, message-passing nature of Windows1, initialization of an application, resource files, menu systems, and accelerators. In addition, I designed the skeleton of the sample stock-charting application. This article explains the types of control windows that Windows supports. I will also continue developing the stock-charting application by adding Multiple Document Interface (MDI) support. Let's begin by reviewing the term window. A window is a data object that has certain properties associated with it. It may occupy an area on the screen, but it doesn't have to. It can be invisible during the entire course of an application. (One use for a hidden window is to act like a "traffic cop," monitoring the activity in the application and processing messages. In OS/2 Presentation Manager, these windows are known as object windows.) A window's appearance and behavior is determined mostly by its window procedure (WinProc). Windows sends messages to a WinProc whenever something "interesting" happens to the window it is associated with. A default window procedure supplied by Windows, DefWindowProc, has a default action for every message sent to a window. DefWindowProc defines the behavior of a generic window. To give a different behavior or appearance to a window or an object within it, some or all of these messages must be handled by code in your WinProc, instead of DefWindowProc. For instance, you can process mouse messages to tell an on-screen control how to react when the control is clicked. Imagine implementing a custom light switch control, where a switch is toggled up and down when clicked on. When the mouse is clicked, the program should determine whether the switch is in the up or down position. Then the program should redraw the switch in the opposite position. This means intercepting the WM_LBUTTONDOWN message, negating the value of the state variable that is internal to this control, and redrawing the control, possibly using a different bitmap to represent the on and off positions. By writing varying amounts of message-processing code yourself, you can make a window act in any conceivable way. Controls Control windows are objects that assist a user in interacting with an application. Predefined Windows controls include scroll bars, radio buttons, check boxes, push buttons, list boxes, combo boxes, edit fields, and static text objects. You can design your own custom control windows, or purchase others from several third-party manufacturers. As Windows becomes more popular, you will see more custom control libraries appear on the marketplace, especially because Windows 3.0 introduces owner-draw controls. (For more about custom controls, see "Extending the Windows[tm] 3.0 Interface with Installable Custom Controls," MSJ, Vol. 5, No. 4.) There are two ways to create controls. You can use the CreateWindow function (and, in Windows Version 3.0, the function CreateWindowEx) or you can use the dialog editor and define controls as part of a dialog box. Although the latter method is more common, let's begin by discussing CreateWindow. The first article ("Learning Windows[tm] Part I: The Message-based Paradigm," MSJ, Vol. 5, No. 4) showed you how to use CreateWindow to create the stock-charting application's main window. Two parameters of the CreateWindow function are of particular interest: the name of the window class and the handle to the parent window. In the Windows environment, windows can have a parent-child relationship. Child windows appear inside the window that owns them, known as the parent. Child windows are clipped to the parent window's client area (the main, initially empty space of the window-the part that is not the caption bar, window border, menu bar, and scroll bars), meaning that any part of the child window extending past its parent window's borders will not be displayed. Certain behavioral aspects are inherited by the child from the parent. If a parent window is hidden or destroyed, its children are hidden or destroyed as well. The same thing goes for the children of the child windows, and so on. When a window is moved, any children are moved along with it. For control windows, the most important aspect of the parent-child relationship is that when certain events happen to a control window, it notifies its parent window of the event by sending it a WM_COMMAND message. (A scroll bar control sends a WM_VSCROLL or WM_HSCROLL message.) In the stock-charting application, the main window did not have a parent. You can see this by examining the hParent parameter of the CreateWindow function that created it. If the hParent parameter is NULL, the window has no parent. A window without a parent is called a top-level window. (Actually, the top-level window is a child of the desktop window, but this does not ever affect the Windows programmer.) Control windows are almost always children of another window. So, when you use CreateWindow to create a control, you must specify the handle of a window as its parent. As far as the class name of a control goes, Windows predefines a number of control window classes. A list of the control class names and the types of controls that you can create in each class is found in Figure 1. Using CreateWindow When you use CreateWindow to create a control, you must give one of the strings in Figure 1 as the class name if you are using a predefined control class. Often you must specify one or more flags for the third argument of the CreateWindow call. The following call creates a push button control. hButton = CreateWindow("Button", "&OK", BS_PUSHBUTTON | WS_CHILD | WS_TABSTOP, 100, 75, 32, 16, hWndMain, IDOK, hInstance, (LPSTR) NULL); This call has several interesting features. First, note the ampersand in the button's text, which indicates an accelerator. Second, the BS_PUSHBUTTON flag is one of of the style bits, which informs Windows that you wish to create a push button. Figure 2 contains a complete list of the flags you can use for each window class. The WS_CHILD flag tells Windows that this is a child window and to use hWndMain as the parent. Finally, note that the value of the hMenu parameter is IDOK. IDOK, a value defined in WINDOWS.H, is usually used for OK buttons. When you create a child window, the value in the hMenu parameter must be the control identifier of the window, not a handle to a menu. Windows overloads the menu handle and the control identifier fields, so that when you create a top-level window, the value of this parameter is considered the menu handle; when you create a child window, this value is considered the control identifier. The control identifier is a number that differentiates a child/control window from any other at that moment in your application. Because it is used in many of the messages passed between the parent and the control and in some Windows functions (such as CheckRadioButton), a control identifier should be unique. Dialog Boxes and Controls Dialog boxes will really be covered in the next article. However, control windows and dialog boxes are inextricable, so I am forced to digress a little. A dialog box is a window containing a number of control windows. It can be created on-the-fly, or, more commonly, defined in a resource file and loaded as needed. A dialog box can be modal or modeless. When a modal dialog box is displayed, the user cannot switch to another window in the same program. He or she must respond to the dialog box in some way, often by clicking an OK or Cancel push button. The user can switch to another program, unless the dialog box is a system modal dialog box, which must be responded to before the user does anything else in Windows. When the user brings up a modal dialog box, its parent window is disabled and the dialog box manager (the Windows code that creates the dialog box window and processes its messages) goes into its own internal message loop. The user must fill in the controls and accept or cancel the dialog box before the application continues with its own message loop. Modeless dialog boxes impose no restrictions on the user, who can move the focus on and off the modeless dialog box freely before responding to it. Two of the more important functions of the dialog box manager are processing keystrokes and assigning the input focus to the appropriate control. For example, when you press Tab or Backspace, the input focus should move to the next or previous control. When you press Enter, the default push button control should be activated. When you press Alt with another key, the dialog box manager must see if that keystroke corresponds to the accelerator associated with a particular control. If you use CreateWindow to create control windows instead of going through the dialog box manager, your application must handle keystrokes itself and decide if they cause a change of focus. This is a non-trivial task better left to the dialog box manager. Static Controls In this article, assume that all control windows (except scroll bars) are associated with dialog boxes. A static control differs from other types of controls in two respects. A static control cannot have the input focus, so you cannot give it keyboard or mouse input. Also, a static control does not notify its parent when an interesting event occurs. Static controls are used mainly to display text strings and rectangular frames. For typical static controls, see any dialog box containing an edit control: the text labeling the edit control is a static control window. It may seem strange to have an entire window structure (and therefore, a WinProc) behind simple text strings, but Windows offers maximum flexibility and extensibility for even the simplest objects. A style flag is associated with each type of static control. For hollow frames, you can specify SS_BLACKFRAME, SS_GRAYFRAME, or SS_WHITEFRAME. For solid rectangles, you can specify SS_BLACKRECT, SS_GRAYRECT, or SS_WHITERECT. Text can be displayed left-, center-, or right-justified by specifying SS_LEFT, SS_CENTER, or SS_RIGHT. The SS_SIMPLE style is used to display a simple rectangle with a single line of left-justified text. To disable the ampersand processing that associates an accelerator with a static control, specify the SS_NOPREFIX style. Finally, you can draw your own static controls using the SS_USERITEM style. Edit Controls Windows supports both single-line and multiline data entry controls. Actually, you can fashion a simple text editor just by creating a standard Windows multiline edit control-it's that powerful. The default capabilities give you rudimentary cursor motion (left, right, up, down, home/end of line, home/end of buffer, next/previous word) and cutting and pasting. In Windows 3.0, edit controls interpret tab stops correctly. Of course, the mouse is fully supported. A multiline edit control will also automatically perform wordwrapping. A multiline edit control has the ES_MULTILINE style. You can attach vertical and/or horizontal scroll bars by specifying the ES_AUTOVSCROLL and ES_AUTOHSCROLL styles. If you attach scroll bars to an edit control, the edit control processes its own scroll bar messages. In Windows 3.0, the ES_LEFT, ES_CENTER, and ES_RIGHT styles function properly. These flags tell the edit control to left-, center-, or right-justify the text within the edit window (see Figure 3). Windows is still weak in its data verification capabilities. Subclassing edit controls is a popular pastime among developers who need this kind of power. Windows 3.0 implements three new edit styles that ameliorate this somewhat. The ES_LOWERCASE and ES_UPPERCASE styles transform keyboard input into all lowercase or all uppercase characters. The ES_PASSWORD style directs an edit control to display a user-defined masking character instead of the normal text. The default masking character is an asterisk, but you can change this by using the EM_SETPASSWORDCHAR message. Hopefully the need for formatted data entry fields will be addressed in future versions of Windows. Edit Control Messages The edit control supports a great number of messages. Some of these messages direct the edit control to perform an editing operation on the text, while others query the state of the control. Most edit control messages begin with the prefix EN_. The four messages without this prefix direct an edit control to perform a cut-and-paste action. All text cut, copied, or pasted from an edit control is sent to the Windows Clipboard. The WM_CUT message copies the highlighted text to the clipboard and then deletes the text from the edit control. The WM_CLEAR message simple deletes the text. The WM_PASTE message inserts the text in the clipboard at the current editing position. The WM_UNDO control reverses the last editing operation. An edit control is limited to only one undo. In these messages, the wParam and lParam are not used, so you can use 0 as their values. Two of the more frequently used edit control messages deal with querying and setting the selected area of text. The EM_GETSEL message returns the starting position of the selected text in the LOWORD of the return value and the position of the end of the selection plus 1 in the HIWORD of the return value. If no text is selected, the current cursor is returned in the LOWORD. DWORD ulRet = SendMessage(hEdit, EM_GETSEL, 0, 0L); WORD iStart = LOWORD(ulRet); WORD iEnd = HIWORD(ulRet); The EM_SETSEL message sets the area of the selected text. The starting and ending positions of the selection are contained in lParam. If the LOWORD of lParam is 0 and the HIWORD is 7FFFH, the entire contents of the edit field will be selected. SendMessage(hEdit, EM_SETSEL, 0, MAKELONG(0, 0x7FFF)); You can replace the selected text with another piece of text using the EM_REPLACESEL message. In this message, lParam is a far pointer to the text string that will be inserted in place of the selected text. If the edit control is in a dialog box, you can use two functions to retrieve and set the contents of an edit control. All you need to know is the handle of the dialog box and the control identifier of the edit control. GetDlgItemText copies a specified number of characters from the edit control into a user-defined buffer. SetDlgItemText sets the contents of an edit control a certain string. Here is a function that changes the contents of an edit control to uppercase: GetDlgItemText(hDlg, ID_EDIT,(LPSTR) szBuf, sizeof(szBuf)-1); strupr(szBuf); SetDlgItemText(hDlg, ID_EDIT,szBuf); There are many more edit messages that may be of use with a multiline edit control. Refer to the Microsoft Windows Software Development Kit (SDK) for a complete description. The notification messages in Figure 4 are generated by an edit control when certain events occur and are sent to the edit control's parent window. Button Controls A button is a generic term for a control that generates a message and possibly changes its appearance when a user clicks on it (see Figure 5). Button controls include check boxes, radio buttons, push buttons, and group boxes. A group box doesn't respond when you click on it; it is used only to contain a group of one of the other kinds of buttons. When a button is clicked, it sends one or more WM_COMMAND messages to its parent window. The WM_COMMAND message notifies the parent that an interesting event has happened involving a button. The wParam of the message is set to the control identifier of the window. The LOWORD of lParam is set to the window handle of the control, and the HIWORD is set to the notification code. The notification code tells exactly which event occurred, and depending on this information, you might have to dismiss a dialog box or simply tell Windows to alter the state of a button. In addition, Windows supports owner-draw buttons. These are buttons the programmer draws. Windows sends owner-draw buttons a message when it needs to be drawn in both its pressed and unpressed states. You can have a lot of fun drawing buttons that resemble real objects. For instance, a musical Windows application could implement buttons shaped like the ones on stereo equipment. Push Buttons Push buttons are rectangular objects containing descriptive text in the center. Unlike check boxes and radio buttons, which are mainly used to show the state of an object visually, push buttons invoke action. When the user clicks a push button, it usually means that the user wants to perform an activity, such as dismissing a dialog box or activating a help window. There is only one style of push button, indicated by the BS_PUSHBUTTON flag. A push button can have a special property. If a user presses Enter in a dialog box, the dialog box manager will determine if there is a default push button control in the dialog box. If there is, the dialog box manager will simulate its pressing; in other words, the dialog box manager will generate a WM_COMMAND message with the identifier of the default push button in the wParam. Two Windows messages deal with default push buttons. The DM_GETDEFID message asks a dialog box if there is a default push button in it. The DM_SETDEFID message lets you make any push button control the default. The following code determines if the OK button is the current default button in the dialog box. If it is, it makes the CANCEL button the default push button. DWORD ulRet = SendMessage(hDlg, DM_GETDEFID, 0, 0L); if (HIWORD(ulRet) = = DC_HASDEFID && LOWORD(ulRet) = = IDOK) SendMessage(hDlg, DM_SETDEFID, IDCANCEL, 0L); Check Boxes A check box is a button control with two parts: a small rectangular frame that is either empty or contains a check mark, and a text string describing the button positioned on the right side of the frame. Check boxes indicate properties that are either on or off. For example, in a word processing program, a check box allows the user to turn wordwrap on or off. A standard check box has either the BS_CHECKBOX or the BS_AUTOCHECKBOX style. Windows automatically toggles the check state of a check box with the auto style when the user clicks on it. If it does not have the auto style, Windows sends a WM_COMMAND message to the check box's parent when the check box is clicked. Then CheckDlgButton must be called to check or uncheck the check box. A variation of the standard check box control is the three-state check box. In addition to a checked/unchecked state, a three-state check box has a grayed state when disabled. A three-state box has either the BS_3STATE or the BS_AUTO3STATE style. Other than that, they are the same as standard check boxes. Two API functions exist for setting and querying the state of a check box. CheckDlgButton sets the state of a standard check box or a three-state check box. Its format is shown below. CheckDlgButton(HWND hDlg, int idButton, WORD wCheck); The handle of the dialog box that contains this check box is hDlg, and idButton is the control identifier of the check box. wCheck is 0 if the button is unchecked, 1 if the button is checked, and for three-state buttons, 2 if it is grayed. The CheckDlgButton function simply determines the window handle of the check box and sends a BM_SETCHECK message to the check box's WinProc. The state of a check box can be determined by using the IsDlgButtonChecked function. IsDlgButtonChecked(HWND hDlg, int idButton); This function returns 0 if the check box is not checked, 1 if it is, and 2 for grayed three-state buttons. Radio Buttons Picture a car radio from the 1970s, before they went digital. There were usually five button presets. When you pressed one button, all the others would pop out. The Windows radio button class works the same way. Radio buttons always come in groups of two or more. When the user clicks one, the button is on, and all the other radio buttons in that group are off. A standard radio button is defined with the BS_RADIOBUTTON style. Like check boxes, there is a BS_AUTORADIOBUTTON style where Windows takes care of the checking and unchecking for you. The CheckDlgButton and the IsDlgButtonChecked functions set and test the state of a radio button. You can also use the CheckRadioButton function to check one button in the group. CheckRadioButton(HWND hDlg, int idFirst, int idLast, int idCheckButton); The second and third arguments of this function specify the control identifiers of the first and last radio buttons in a group. The fourth argument gives the control identifier of the radio button that you want to check. For this function to work effectively, all the radio buttons in the group should be numbered consecutively from idFirst to idLast. CheckRadioButton enumerates all the controls with identifiers in the range of idFirst to idLast, and sends BM_SETCHECK messages to them with wParam set to 0. Then it sends a BM_SETCHECK message with wParam set to 1 to the radio button passed in the last argument. Group Boxes Group boxes aren't really buttons, as I said previously. Simple rectangles used to enclose a group of buttons, group boxes are more like static text frames than buttons. They cannot accept input, nor can they generate WM_COMMAND messages, so they are used mainly for visual aesthetics or as a place marker for the end of a button group. List Boxes A list box is a control window containing a list of strings (in Windows 3.0, it can be a list of anything able to be displayed, but I will concentrate on strings for the time being). The user selects one or more strings from the list box by highlighting a string or by clicking on it. If all the strings cannot fit in the window, vertical or horizontal scroll bars can be used to scroll through the strings. Windows 3.0 beefed up the capabilities of the list box control. A list box can now grow horizontally instead of vertically so it can have more than one column. Any kind of item can be displayed in a list box, not just strings. List boxes now can be tabbed to align columns. An extended list box can be created, in which certain key combinations select multiple items. In fact, the Windows SDK contains a program called OWNCOMBO that demonstrates some of these new capabilities. You should take a few minutes to compile and run it. The available list box styles are shown in Figure 6. By default, a list box supports a single string selection and the strings are unsorted. To sort the strings, you must specify the LBS_SORT style. You should also specify the LBS_NOTIFY style if you want the list box to send WM_COMMAND messages to its parent when something interesting happens. The WINDOWS.H file defines the LBS_STANDARD style; it's simply a combination of LBS_SORT and LBS_NOTIFY. Now that I have defined a list box, I want to be able to add strings to it. To add a string, send a message to the list box with the lParam set to a far pointer to the string. There are two messages used to add strings, LB_ADDSTRING and LB_INSERTSTRING. LB_ADDSTRING appends a string to the end of the list box. If the list box is sorted, LB_ADDSTRING inserts the string in the sorted position. SendMessage(hListBox, LB_ADDSTRING, 0, (DWORD) (LPSTR) "Hello"); LB_INSERTSTRING inserts a string at any specified position in the list box. The zero-based position passed in the wParam will be adhered to, even if the list box is sorted. If you want to put the string at the end of the list box, you must pass 1 in wParam. SendMessage(hListBox, LB_INSERTSTRING, 12, (DWORD) (LPSTR) "Hello"); Windows allocates a 64Kb global memory block for each list box. This means that the sum of the lengths of all of the strings in a list box cannot be greater than 64Kb. If you exceed this limit, the LB_INSERTSTRING or LB_ADDSTRING message returns the value of LB_ERRSPACE to your application. To delete a string from the list box, use the LB_DELETESTRING message and pass the zero-based index of the string in wParam. SendMessage(hListBox, LB_DELETESTRING, 5, 0L); Unfortunately, no single message in Windows allows you to change the contents of a string. You must delete the string and then reinsert it. If you append or insert strings in a visible list box, an annoying phenomenon will take place. The list box refreshes itself after each string is added-not an aesthetically pleasing sight for the user. To avoid this, you have to tell the list box not to refresh itself until all of the strings have been added. The WM_SETREDRAW message sets or clears the list box's auto-refresh flag. Before you start inserting strings, call SendMessage with the third parameter set to FALSE. SendMessage(hListBox, WM_SETREDRAW, FALSE, 0L); It is now safe to insert strings. After the last string has been added, reenable the refresh flag by calling SendMessage with the third parameter TRUE. SendMessage(hListBox, WM_SETREDRAW, TRUE, 0L); Once the strings are in the list box and the user interacts with it, a rich set of messages is available to query the current state of the list box. The LB_GETCURSEL message returns the index of the currently selected item in a single-selection list box. If you would like to retrieve the text associated with that item, use the LB_GETTEXT message. int iSel; char szBuf[128]; if ((iSel = SendMessage(hListBox, LB_GETCURSEL, 0, 0L)) != LB_ERR) SendMessage(hListBox, LB_GETTEXT, iSel, (DWORD) (LPSTR) szBuf); To change the current selection to a different string, send the LB_SETCURSEL message with wParam set to the zero-based index of the string to select. To search for a string containing a certain prefix, you can use the LB_FINDSTRING message. To do both of these things, use LB_SELECTSTRING, which finds a string with a given prefix and makes it the current selection. Multiple-Selection List Boxes In a multiple-selection list box, more than one string can be selected at a time. Suppose you want to present the user with a list of files to delete. It would be a hassle to select one file, press delete, select another, press delete again, and so on. Instead, using a multiple-selection list box, the user could mark several files, and press delete once. Although most of the list box messages can be used for both single- and multiple-selection list boxes, several do not overlap. The LB_GETCURSEL and LB_SETCURSEL messages can only be used with single-selection list boxes, since there cannot be a single currently selected item in a multiple-selection list box. Instead, you use the LB_GETSEL and LB_SETSEL messages to query and set the selection state of an item. To use these messages effectively, you may need to know how many items are in the list box. You can determine the number of entries in the list box by sending the LB_GETCOUNT message. Once you have the number of items, you can march down the list box and query or set the selection state of each item. The following fragment of code reverses the selection state of every item in the list box. int nItems, i; BOOL bisSelected; nItems = (WORD) SendMessage(hListBox, LB_GETCOUNT, 0, 0L); for (i = 0; i < nItems; i++) { bIsSelected = (BOOL) SendMessage(hListBox, LB_GETSEL, i, 0L); SendMessage(hListBox, LB_SETSEL, !bIsSelected, (DWORD) i); } Two new Windows 3.0 messages help in the programming of multiple-selection list boxes. LB_GETSELCOUNT returns the number of items selected within a multiple-selection list box. LB_GETSELITEMS fills a buffer with an array of integers that are the indices of all of the selected items. The next fragment of code demonstrates these two messages. int nItems; nItems = (WORD) SendMessage(hListBox, LB_GETSELCOUNT, 0, 0L); if (nItems != LB_ERR) { HANDLE haItems = LocalAlloc(LMEM_MOVEABLE, nItems * sizeof(int)); LPINT lpaItems = (LPINT) LocalLock(haItems); SendMessage(hListBox, LB_GETSELITEMS, nItems, (DWORD) lpaItems); o o o } The new proportionally spaced system font in Windows 3.0 may disarray Windows 2.x applications that relied on the old monospace font to line up the contents of a list box. However, the new tab stop support can be used to line up columnar data. To do so, you must give the list box the LBS_USETABSTOPS style. The default position for tab stops is one every 32 horizontal dialog box units. (A horizontal dialog box unit is 1/4 of the dialog base width unit, a figure based on the width of the current system font.) To set your own tabs, you can use the new LB_SETTABSTOPS message. Multiple-Column List boxes Windows 3.0 supports list boxes that scroll horizontally or vertically, but not both. A list box that scrolls horizontally is called a multiple-column list box and is defined using the LBS_MULTICOLUMN style. Each column in the list box is given a default width of 15 characters. To change the column width, use the LB_SETCOLUMNWIDTH message. The new width is passed in wParam. Unfortunately, you cannot have variable width columns-but then again, if you did, there would be nothing to wish for in future versions of Windows. List Box Notification Codes Like any control, a list box notifies its parent window when certain things happen to it. Remember that the notification process occurs only if the list box has the LBS_NOTIFY or LBS_STANDARD styles. A list of these notification codes is found in Figure 7. The LBN_SETFOCUS and LBN_KILLFOCUS messages, new to Windows 3.0, inform the parent when the focus is gained or lost by a list box. The LBN_SETFOCUS message can be used, say, to highlight a default selection when the user tabs into a list box. The LBN_SELCHANGE message is used frequently when another control tracks the selected text in a list box. For example, almost all Windows file-directory dialog boxes have both a list box containing the file, directory, and drive names, and an edit control in which you enter the name of a file. In most cases, if the focus is set to the list box, the text of the currently selected item is displayed in the edit control. To do this, you need to know when the current selection in the list box changes. The LBN_SELCHANGE message is just the thing you need. Each time the list box selection changes, a WM_COMMAND message is sent to the dialog box with the LBN_SELCHANGE notification code in the HIWORD of the lParam. You can then retrieve the text of the currently selected item using the code shown earlier, and set the contents of the edit control to that text with the SetDlgItemText function. The LBN_DBLCLK message is generated when the user double clicks on a list box entry. Double-clicking usually means that the user wants to select a string and terminate the dialog box. Owner-Draw List Boxes The previous article discussed owner-draw menu items, in which Windows gives total responsibility to the application for displaying a menu item. This concept is carried over to list box controls, so that the user can choose anything in a list box control, not just alphanumeric text strings. This enables more powerful Windows user-interfaces-you can have a picture of an object rather than a descriptive text string. If the stock-charting application supported different types of graphs, it could prompt the user for the type of graph to display by showing a picture of the type of graph within a list box, not just a text string describing the graph. A miniature picture of a scatter graph would be much more useful than the words "Scatter Graph." To create an owner-draw list box, you must give your list box the LBS_OWNERDRAWFIXED or the LBS_OWNERDRAWVARIABLE styles. A list box created with the LBS_OWNERDRAWFIXED style assumes that all items in the list box will be of uniform height, while a list box created with the LBS_OWNERDRAWVARIABLE must know the height of every item. If you wanted to fill a list box with strings, with each string drawn in a different font (maybe to query the user for a font style selection), you would give the list box the LBS_HASSTRINGS style in addition to one of the two styles mentioned above. Assume that you created an owner-draw list box and want to add items to it. You can use the regular LB_ADDSTRING or LB_INSERTSTRING functions. However, the lParam does not have to be a far pointer to a string, unless the list box has the LBS_HASSTRINGS style. It can be any user-defined 32-bit value. This value can be a handle to a bitmap, an index into an array, or anything that can be used to map a list box item to a graphical object. Consider the stock-charting application. I would like to give the user a choice of grid styles to use when drawing the grid lines of a graph. To draw the grid lines, I use lines created with different Windows pens. (A pen is a bitmap that is used to draw a line.) Windows has a solid pen and several dashed pens. An owner-draw list box answers my needs perfectly. Instead of trying to describe the pen styles with a text string, I can create a list box in which each item is a line drawn with a different pen. The first thing I do is create a list box using the LBS_OWNERDRAWFIXED style, since the size of each line will be the same. Then I add the items to it. Each item has a user-defined value associated with it, which are the Windows-defined values for each pen style. /* Fill the list box with pen IDs */ int iPen; for (iPen = 0; iPen < PS_NULL; iPen++) SendMessage(hPenListBox, LB_ADDSTRING, 0, (LONG) iPen); What if an owner-draw list box had to be in some kind of order (that is, it has the LBS_SORT style)? The list box control does not know how to sort graphical objects. Fortunately, Windows 3.0 has a new message, WM_COMPAREITEM, which is sent to the parent window of the list box whenever the list box needs to sort two objects. When this message is sent, the value of lParam is a far pointer to a COMPAREITEMSTRUCT data structure. Among the elements of this structure are the indices and the user-defined values of the two items being compared. One of three possible values will be returned: 0 if the objects are equal, 1 if item 1 sorts before item 2, and 1 if item 1 sorts after item 2. Windows needs to know the size of each item in an owner-draw list box. To determine this, the WM_MEASUREITEM message is sent to the parent of the list box. If a list box has the LBS_OWNERDRAWFIXED style, this message is sent only once, since all items will be the same size. However, if it has the LBS_OWNERDRAWVARIABLE style, this message is sent once for each item in the list box. The value of lParam for the WM_MEASUREITEM message is a far pointer to a MEASUREITEMSTRUCT data structure. The previous article showed the details of the MEASUREITEMSTRUCT data structure, so I won't go into it here. Assume that each item in the pen-style list box has a width of 42 pixels and a height of 20 pixels. The following code processes the WM_MEASUREITEM message. case WM_MEASUREITEM : { LPMEASUREITEMSTRUCT lpMI = (LPMEASUREITEMSTRUCT) (LPSTR) lParam; lpMI->itemWidth = 42; lpMI->itemHeight = 20; break; } Finally, to draw each item, Windows sends the list box's parent the WM_DRAWITEM message. I'm borrowing a little code from the OWNCOMBO SDK demo program to show how to process this message (see Figure 8). I need to be able to draw a list box item in its normal, unselected state. I also have to draw an item in its selected state, and if the item is in a multiple-selection list box, I need to draw the item in its focus state. (When a standard list box text string has the focus, Windows draws a dashed frame around the string.) When an item is deleted from an owner-draw list box, Windows sends the parent window the WM_DELETEITEM message. In this message, lParam points to a DELETEITEMSTRUCT data structure. This structure contains the index and the user-defined value corresponding to that item. WM_DELETEITEM gives the user a chance to free resources associated with that item (such as a bitmap or a brush). Stock-ChartingApplication The stock-charting application's controls will really come into play once I have covered dialog boxes in the next installment. But since I do want to add functionality to the application in this article, and since "A New Multiple Document Interface API Simplifies MDI Application Development," MSJ (Vol. 5, No. 4) is probably still fresh in your mind, I put an MDI shell over the application. MDI really suits this application well. Each stock graph can be considered a separate MDI document, and the application as a whole seems to fall nicely into the user-interface scheme supported by the MDI specification (see Figure 9). I tied the File New menu item to the MDI child creation routine. When this menu item is chosen, a new MDI child window is created and placed at a default position within the client window. There is nothing displayed in the child windows yet; as I develop the application further, I will fill these windows with the stock graphs. The Window pull-down menu allows you to position the MDI children in certain ways on the screen (see Figure 10). You can automatically tile or cascade the child windows by choosing the Window Tile and the Window Cascade menu selections. Any window can be iconized; the icons are automatically arranged nicely at the bottom of the MDI client window. You can close all the children at once by choosing the Window Close All item. Some of the MDI code was shamelessly borrowed from the MULTIPAD demo program that comes with the SDK. There are many good demos and programming techniques available for examination in the SDK. I strongly suggest that if you are new to Windows, or if you want to catch up on some of the new features in Windows 3.0, you study some of this code. The first change in the new version of the stock- charting application is new logic in the main loop to recognize the MDI accelerator keys. So, before you call the TranslateAccelerator function, you need to call TranslateMDISysAccel. If any of these functions return a nonzero value, the keystroke was translated. if (!TranslateMDISysAccel(hwndMDIClient, &msg) && !TranslateAccelerator(hWndMain, hAccelTable, &msg)) The second change was registering a window class for the MDI child windows. Nothing in this code reveals that these child windows will be MDI children. /* Register MDI chld cls */ wc.lpfnWndProc = GraphWndProc; wc.hIcon = LoadIcon( hThisInstance, MAKEINTRESOURCE(ID_GRAPH)); wc.lpszMenuName = NULL; wc.cbWndExtra = CBWNDEXTRA; wc.lpszClassName = "GraphWindow"; if (!RegisterClass(&wc)) { return FALSE; } The third task was creating the MDI client window. This needs to be done when the main window is created. An easy way to do this is to create the MDI client window in response to the WM_CREATE message sent to the main window. The WM_CREATE message informs the WinProc that a window of a particular class was just created and informs the programmer that initialization of that window should be done. You fill in the two elements that comprise the MDI CLIENTCREATESTRUCT data structure and call CreateWindow to create the MDI client. case WM_CREATE: { CLIENTCREATESTRUCT ccs; /* Find window menu where children will be listed */ ccs.hWindowMenu = GetSubMenu(GetMenu(hWnd), 3); ccs.idFirstChild = ID_WINDOW_CHILDREN; /* Create the MDI client filling the client area */ hwndMDIClient = CreateWindow("mdiclient", NULL, WS_CHILD | WS_CLIPCHILDREN | WS_VSCROLL|WS_HSCROLL, 0,0,0,0, hWnd, 0xCAC, hThisInstance, (LPSTR) &ccs); ShowWindow(hwndMDIClient, SW_SHOW); You also need to add logic to the main window's WinProc to process some commands that deal with MDI. This code tells the MDI client to tile, cascade, and close the MDI children and to arrange the iconized windows. Be aware that the default WinProc for a window containing an MDI client window is not DefWindowProc. Instead you must call DefFrameProc to handle all of the default message processing. default : /* We might have chosen to change the focus to one of the MDI children. */ return DefFrameProc(hWnd, hwndMDIClient, msg, wParam, lParam); The final change needed for MDI support is to create an MDI child window in response to the user choosing the File New menu item. Process the WM_COMMAND message in the WinProc for the main window; if wParam is the identifier ID_NEW, call GraphCreateWindow. All that GraphCreateWindow does is fill out an MDICREATESTRUCT data structure and send a WM_MDICREATE message to the MDI client window (see Figure 11). The MDICREATESTRUCT contains information about the MDI child's class, caption title, dimensions, instance handle, and style. The WinProc for an MDI child window has no special processing associated with it, except that you must call DefMDIChildProc instead of DefWindowProc when you do not want it to process a message. default: /* Again, since the MDI default behavior is a * little different,call DefMDIChildProc instead * of DefWindowProc() */ return DefMDIChildProc(hWnd, msg, wParam, lParam); That's all there is to it! You just added MDI to the stock-charting application with a few statements. In previous versions of Windows, MDI was extremely complicated, but Windows 3.0 makes it much easier. The next article concludes the discussion of control windows, exploring combo boxes and scroll bars. The main topic will be the use of dialog boxes, with a brief discussion of some of the third-party tools that can assist you in creating a dialog box. I will integrate our knowledge of controls with dialog boxes and add these to the stock-charting application. 1For ease of reading, "Windows" refers to the Microsoft Windows graphical environment. "Windows" refers only to this Microsoft product and is not intended to refer to such products generally. Figure 1. Windows Control Styles ■ ■ Scrollbar Horizontal or vertical scroll bar ■ Edit Single- or multiline edit field ■ Listbox Single- or multiple-selection list box ■ Combobox Simple, drop-down, or drop-down list combo box ■ Static Left-, centered, or right-justified text strings, frames ■ Button Push button, radio button, check box, group box ■ MDIClient Multiple Document Interface client window (this is not a control window) ■ ──────────────────────────────────────────────────────────────────────────── Figure 2. Window Styles Style Meaning DS_LOCALEDIT Specifies that edit controls in the dialog box will use memory in the application's data segment. By default, all edit controls in dialog boxes use memory outside the application's data segment. This feature may be suppressed by adding the DS_LOCALEDIT flag to the STYLE command for the dialog box. If this flag is not used, EM_GETHANDLE and EM_ SETHANDLE messages must not be used since the storage for the control is not in the application's data segment. This feature does not affect edit controls created outside of dialog boxes. DS_MODALFRAME Creates a dialog box with a modal dialog-box frame that can be combined with a title bar and system menu by specifying the WS_CAPTION and WS_SYSMENU styles. DS_NOIDLEMSG Suppresses WM_ENTERIDLE messages that Windows would otherwise send to the owner of the dialog box while the dialog box is displayed. DS_SYSMODAL Creates a system-modal dialog box. WS_BORDER Creates a window that has a border. WS_CAPTION Creates a window that has a title bar (implies the WS_BORDER style). This style cannot be used with the WS_DLGFRAME style. WS_CHILD Creates a child window. Cannot be used with the WS_POPUP style. WS_CHILDWINDOW Creates a child window that has the WS_CHILD style. WS_CLIPCHILDREN Excludes the area occupied by child windows when drawing within the parent window. Used when creating the parent window. WS_CLIPSIBLINGS Clips child windows relative to each other; that is, when a particular child window receives a paint message, the WS_CLIPSIBLINGS style clips all other overlapped child windows out of the region of the child window to be updated. (If WS_CLIPSIBLINGS is not given and child windows overlap, it is possible, when drawing within the client area of a child window, to draw within the client area of a neighboring child window.) For use with the WS_CHILD style only. WS_DISABLED Creates a window that is initially disabled. WS_DLGFRAME Creates a window with a double border but no title. WS_GROUP Specifies the first control of a group of controls in which the user can move from one control to the next by using the arrow keys. All controls defined with the WS_GROUP style after the first control belong to the same group. The next control with the WS_GROUP style ends the style group and starts the next group (that is, one group ends where the next begins). Only dialog boxes use this style. WS_HSCROLL Creates a window that has a horizontal scroll bar. WS_ICONIC Creates a window that is initially iconic. For use with the WS_OVERLAPPED style only. WS_MAXIMIZE Creates a window of maximum size. WS_MAXIMIZEBOX Creates a window that has a maximize box. WS_MINIMIZE Creates a window of minimum size. WS_MINIMIZEBOX Creates a window that has a minimize box. WS_OVERLAPPED Creates an overlapped window. An overlapped window has a caption and a border. WS_OVERLAPPEDWINDOW Creates an overlapped window having the WS_OVERLAPPED, WS_CAPTION, WS_SYSMENU, WS_THICKFRAME, WS_MINIMIZEBOX, and WS_MAXIMIZEBOX styles. WS_POPUP Creates a pop-up window. Cannot be used with the WS_ CHILD style. WS_POPUPWINDOW Creates a pop-up window that has the WS_BORDER, WS_POPUP, and WS_SYSMENU styles. The WS_CAPTION style must be combined with the WS_POPUPWINDOW style to make the system menu visible. WS_SYSMENU Creates a window that has a system-menu box in its title bar. Used only for windows with title bars. WS_TABSTOP Specifies one of any number of controls through which the user can move by using the Tab key. The Tab key moves the user to the next control specified by the WS_TABSTOP style. Only dialog boxes use this style. WS_THICKFRAME Creates a window with a thick frame that can be used to size the window. WS_VISIBLE Creates a window that is initially visible. This applies to overlapped and pop-up windows. For overlapped windows, the Y parameter is used as a ShowWindow function parameter. WS_VSCROLL Creates a window that has a vertical scroll bar. Figure 4. Edit Notification Messages EN_CHANGE Indicates that the user has taken some action that may have changed the content of the text. EN_ERRSPACE Indicates that the edit control is out of space. EN_HSCROLL Indicates that the user has clicked the edit control's horizontal scroll bar with the mouse; the parent window is notified before the screen is updated. EN_KILLFOCUS Indicates that the edit control has lost the input focus. EN_MAXTEXT Specifies that the current insertion has exceeded a specified number of characters for the edit control. EN_SETFOCUS Indicates that the edit control has obtained the input focus. EN_UPDATE Specifies that the edit control will display altered text. EN_VSCROLL Indicates that the user has clicked the edit control's vertical scroll bar with the mouse; the parent window is notified before the screen is updated. Figure 7. List Box Notification Codes LBN_DBLCLK Sent when the user double-clicks a string with the mouse LBN_ERRSPACE Sent when the system is out of memory LBN_KILLFOCUS Indicates that a list box has lost input focus LBN_SELCHANGE Sent when the selection has been changed LBN_SETFOCUS Indicates that the list box has received input focus Figure 8. The WM_DRAWITEM Message case WM_DRAWITEM : { LPDRAWITEMSTRUCT lpdis = (LPDRAWITEMSTRUCT) (LPSTR) lParam; switch (lpdis->itemAction) { case ODA_DRAWENTIRE: DrawEntireItem(lpdis, -4); break; case ODA_SELECT: HandleSelectionState(lpdis, 0); break; case ODA_FOCUS: HandleFocusState(lpdis, -2); break; } break; } /******************************************************************** * * FUNCTION: HandleSelectionState(LPDRAWITEMSTRUCT, int) * * PURPOSE: Handles a change in an item selection state. If an item * is selected, a black rectangular frame is drawn around * that item; if an item is de-selected, the frame is * removed. * * COMMENT: The black selection frame is slightly larger than the * gray focus frame so they won't paint over each other. * *******************************************************************/ void FAR PASCAL HandleSelectionState(lpdis, inflate) LPDRAWITEMSTRUCT lpdis; int inflate; { RECT rc; HBRUSH hbr; /* Resize rectangle to place selection frame outside of the focus * frame and the item. */ CopyRect ((LPRECT)&rc, (LPRECT)&lpdis->rcItem); InflateRect ((LPRECT)&rc, inflate, inflate); if (lpdis->itemState & ODS_SELECTED) { /* selecting item -- paint a black frame */ hbr = GetStockObject(BLACK_BRUSH); } else { /* de-selecting item -- remove frame */ hbr = CreateSolidBrush(GetSysColor(COLOR_WINDOW)); } FrameRect(lpdis->hDC, (LPRECT)&rc, hbr); DeleteObject (hbr); } /******************************************************************* * * FUNCTION: HandleFocusState(LPDRAWITEMSTRUCT, int) * * PURPOSE: Handle a change in item focus state. If an item gains * the input focus, a gray rectangular frame is drawn * around that item; if an item loses the input focus, * the gray frame is removed. * * COMMENT: The gray focus frame is slightly smaller than the black * selection frame so they won't paint over each other. * *******************************************************************/ void FAR PASCAL HandleFocusState(lpdis, inflate) LPDRAWITEMSTRUCT lpdis; int inflate; { RECT rc; HBRUSH hbr; /* Resize rectangle to place focus frame between the selection * frame and the item. */ CopyRect ((LPRECT)&rc, (LPRECT)&lpdis->rcItem); InflateRect ((LPRECT)&rc, inflate, inflate); if (lpdis->itemState & ODS_FOCUS) { /* gaining input focus -- paint a gray frame */ hbr = GetStockObject(GRAY_BRUSH); } else { /* losing input focus -- remove (paint over) frame */ hbr = CreateSolidBrush(GetSysColor(COLOR_WINDOW)); } FrameRect(lpdis->hDC, (LPRECT)&rc, hbr); DeleteObject (hbr); } /******************************************************************** * * FUNCTION : DrawEntireItem(LPDRAWITEMSTRUCT, int) * * PURPOSE : Draws an item and frames it with a selection frame * and/or a focus frame when appropriate. * *******************************************************************/ void FAR PASCAL DrawEntireItem(lpdis, inflate) LPDRAWITEMSTRUCT lpdis; int inflate; { RECT rc; HANDLE hOldPen; HPEN hPen; /* Resize rectangle to leave space for frames */ CopyRect ((LPRECT)&rc, (LPRECT)&lpdis->rcItem); InflateRect ((LPRECT)&rc, inflate, inflate); hPen = CreatePen((int) lpdis->itemData, 1, RGB(0, 0, 0)); hOldPen = SelectObject(lpdis->hDC, hPen); MoveTo(lpdis->hDC, rc.left, rc.top + (rc.bottom - rc.top) / 2); LineTo(lpdis->hDC, rc.right, rc.top + (rc.bottom - rc.top) / 2); SelectObject(lpdis->hDC, hOldPen); DeleteObject(hPen); /* Draw or erase appropriate frames */ HandleSelectionState(lpdis, inflate + 4); HandleFocusState(lpdis, inflate + 2); } Figure 11. MIDICREATESTRUCT MDICREATESTRUCT mcs; static int nChildren = 1; if (!lpName) { /* If the lpName parameter is NULL, load the "Untitled" string * from STRINGTABLE and set the title field of the MDI * CreateStruct. */ sprintf(sz, "(Untitled - %d)", nChildren++); mcs.szTitle = (LPSTR) sz; } else { /* Title the window with the supplied filename */ AnsiUpper(lpName); mcs.szTitle = lpName; } mcs.szClass = "GraphWindow"; mcs.hOwner = hThisInstance; /* Use the default size for the window */ mcs.x = mcs.cx = CW_USEDEFAULT; mcs.y = mcs.cy = CW_USEDEFAULT; /* Set the style DWORD of the window to default */ mcs.style = 0L; /* tell the MDI Client to create the child */ hWnd = (WORD) SendMessage(hwndMDIClient, WM_MDICREATE, 0, (LONG) (LPMDICREATESTRUCT) &mcs); return hWnd; Extending Off-the-Shelf Windows Applications with Macros and DLLs Bill Eisner A new generation of software designed for sophisticated computing environments with graphical user interfaces offers a remarkable degree of customization. Products such as Microsoft Excel, Samna Ami Professional[tm], and Microsoft Word for Windows allow you to develop personalized shortcuts, alter menus, and change default characteristics. In-house developers and consultants can often satisfy the needs of end-users by tailoring and extending off-the-shelf products such as these, rather than developing a software solution from scratch. Even when custom software is needed, a high degree of integration between it and off-the-shelf products is often required. A new generation of highly alterable and extendable software simplifies this task. Although the techniques discussed here are illustrated using Microsoft Word for Windows (hereafter "Word"), its BASIC macro language, and DLL support, you can use similar facilities in a number of off-the-shelf Windows and OS/2 Presentation Manager environment-based applications. If you have a copy of Word, you can type the examples here or download them from any MSJ bulletin board. Word supports alteration of its entire menu structure, modification of the behavior of its keys, and "hot spots" that result in programmed actions when clicked with a mouse. A variety of tools support these modifications, including a document template facility; a dialog description language; a generalized field capability; table structures; dynamic data exchange; WordBASIC, a powerful macro language; and the ability to call dynamic-link libraries (DLLs). This article addresses the latter two features, WordBASIC and DLLs. WordBASIC is a fairly comprehensive programming facility embedded in Word. The development process for WordBASIC macros is facilitated by a macro recorder, which creates a WordBASIC program directly from keyboard actions, and an integrated debugging system that permits step-wise execution. DLLs in particular offer an extraordinary degree of flexibility. A simple DLL can be used to extend Word seamlessly or even replace existing capabilities of software packages supporting DLL extensions. DLLs DLLs are familiar to Windows1 and OS/2 programmers. The applications programming interface (API) and, in fact, most of the system code of these environments is implemented as a collection of DLLs accessed by systems and application software. Conventional C programs contain statements in one source program that call functions in other source programs or libraries of compiled programs. Usually, a linker connects external calls from compiled objects to entry points in other compiled objects or library routines. In a traditional "static" environment, linking a compiled object to a library routine results in compiled code from the library being added to the execution module by the linker. The Microsoft Windows Software Development Kit (SDK) introduced a new form of linking that delays the resolution of some library linkages until run time. This dynamic technique allows these library execution routines to be shared, since the object code is not bound with individual software packages. By inventing DLLs, Windows developers created a new form of execution object that resembles a conventionally linked program (so closely that DLLs used to be identified with the EXE extension), but that has additional entry point information so that individual functions in the library can be found. DLLs are supported by a new type of library called an import library (IMPLIB). An IMPLIB is used like a conventional library, except that when working with an import library, the linker merely adds a reference to an entry point in a DLL, instead of adding compiled code and data to the executable. The final linkage between the calling and the called objects within DLLs typically takes place as the program is loaded. However, it is also possible for a running program to load a DLL and call one of its functions. Word exploits this to allow WordBASIC programs to invoke DLL routines. Application of DLLs There are three types of DLLs that can be used to extend the capabilities of a package such as Word: native operating environment DLLs (used via the environment's standard API), commercial DLLs, and custom-built DLLs. Use of the first type of DLL is illustrated in Figure 1, a simple WordBASIC program. To execute this program, edit a new macro using the Macro Edit command. Enter the short program in Figure 1, then press the Start button. The Word title bar will be modified, demonstrating the power of the word processor and Windows itself. A large majority of the API functions in the Windows operating environment DLLs can be called directly from WordBASIC. A more complex use of the Windows API functions is shown in Figure 2. This macro invokes the Windows sound library to play music. If the character string "cdeccdecef+ g-ef+g/gAgf-ec/gAgf-ec-cG+c-cG+c" is highlighted when the macro is invoked, the Windows sound functions play "Frere Jacques." You can call functions other than the Windows functions directly from WordBASIC as well. Other APIs, such as the NETAPI.DLL functions that provide the interface to Microsoft LAN Manager, can be called directly except when the calls require data types not supported by WordBASIC. The highly interactive nature of WordBASIC makes it useful for exploring even complex API functionality. Commercial DLLs, the second type of DLLs, are more "promise" than "practice." Just as the wide acceptance of the C language has resulted in the proliferation of a variety of static C libraries, it is highly likely that the popularity of Windows will spawn commercial DLLs to expand its capabilities. Although a few vendors use DLL facilities to support expanded graphics, spell-checking, and other functionality, this commercial opportunity has yet to be significantly exploited. The opportunities for the third type, user-built DLLs, lie primarily in environments where a large number of users are provided with an integrated set of facilities to perform predefined tasks. Computing environments for corporate PC users are often enhanced with company-built templates and macros to simplify operation or to enhance functionality. While WordBASIC provides a great deal of flexibility for customization, a custom-written DLL provides even more, since all the features of the Windows environment can be exploited from inside it. Macros and DLLs Since DLL coding requires greater programming expertise and represents a substantial increase in programming investment over WordBASIC, it is important to balance the use of these two tools. Because programs can be developed and debugged more quickly in a macro language such as WordBASIC, it is often the tool of choice. Another reason is WordBASIC's comprehensive string handling, which can greatly simplify many typical operations even though it is limited to 32Kb strings. Also, WordBASIC functions can access the internal properties of Word and the documents in it directly. Until call-back functions exist that let DLLs retrieve information from the internals of Word, WordBASIC is the only reasonable means for accessing selected data, bookmarks, glossary information, and other word-processing-specific information. Therefore, even when DLLs are used, they must cooperate with WordBASIC calling routines that can move data between Word and the DLL. DLLs are superior to macros in three areas: speed, storage, and control. WordBASIC is interpreted, and executes even simple loops slowly. DLLs written in C and compiled in machine language speed up routines that scan and manipulate characters or perform complex mathematical calculations. The performance enhancement is offset somewhat by the extra time required to initiate a DLL routine, but long routines are substantially improved by substituting a compiled language. As for storage, the implementation of many programs' macro language limits both program and data space. DLLs relieve these constraints through the availability of medium model (multiple code segment) programming and access to the global allocation routines of Windows. Direct access to storage provides a great deal of flexibility for DLL routines. For example, while WordBASIC supports only two fundamental data types, real numbers and strings, DLLs coded in C can take advantage of a variety of types, structures, and unions. Furthermore, storage allocated by DLLs can be retained beyond the life of a single WordBASIC routine. One simple way to maintain storage after exiting a DLL relies on the fact that Windows will not release the storage of a DLL in use. If an initialization routine within an AutoNew or AutoOpen WordBASIC macro invokes the Windows LoadModule function with the name of the library, the data storage for the library will be kept. A termination routine can clean up storage by calling the FreeLibrary function. Perhaps the greatest benefit from writing DLLs is the degree of control available to the programmer. Dialog boxes are a good illustration. The WordBASIC dialog facility is quite flexible, but it supports only a limited user interface. It is not possible to change the contents of a combo box or list box based on the selection of a radio button or a list item. The NovaNew library routine (see Figure 3) demonstrates the adaptability of DLLs. The main function, NovaNew, replaces the standard FileNew function of Word, which confronts the user with a cryptic list of up-to-eight-character template names (see Figure 4). NovaNew's dialog box groups the template names into logical categories such as correspondence, legal, labels, and so on (see Figure 5). If you select a category in the left list box, NovaNew describes the document templates in another list. The NovaNew function relies on description files with an LB extension that are used as data for the list boxes (see Figure 6). This approach is quite appropriate to the common corporate use of Word in which standardized templates are provided for different types of documents by a central staff. The creation of a new template requires the updating of these lists, although you could write a simple WordBASIC macro to simplify that operation. The NovaNew Files The NovaNew DLL consists of the usual Windows files: make, source, header, link, module definition, resource, and dialog files. An additional file needed for libraries, LIBENTRY.ASM, can be found in the Windows SDK. After you install the Windows SDK, a C compiler, and a macro assembler, create the library NOVANEW.DLL with the command: nmake novanew The library should be placed with the other DLLs in the Windows SYSTEM directory. Replacing FileNew with NovaNew The FileNew routine is used to call the DLL routine. It relies on the fact that each Word command is initiated by a WordBASIC program. Often, these very simple programs merely call Word's internal functionality and can be replaced by a user-built function. The FileNew program is installed in Word by editing and saving the global macro "FileNew" resulting in a modification to the file NORMAL.DOT. In this manner, any of Word's high-level functions can be seamlessly replaced by WordBASIC routines, which can call DLLs. To add the new FileNew, use the Macro Edit command to specify "FileNew" in the Edit Macro Name dialog box. Replace the entire contents with the text shown in Figure 7. Then use the File SaveAll command to save the new macro. (Note that the substituted WordBASIC FileNew routine maintains the original contents in a subroutine named OLD, so that you can regain the original function while debugging. To revert back, simply change MAIN to NEW and OLD to MAIN.) The substituted FileNew routine declares the library's main function with the following statement. Declare Function NovaNew Lib "NovaNew.DLL"(Name$) As Integer This statement identifies the function name, NovaNew; the library name, NOVANEW.DLL; the function's argument type (in WordBASIC, a variable ending in $ signifies a string); and the return type, Integer. Once declared, DLL functions can be called using normal WordBASIC syntax, as shown below. If(NovaNew(A$) = 1) Then ... The NovaNew library function returns a 1 if successful and a 2 if the user has pressed Cancel. Conventional Windows dialog boxes indicate success by returning IDOK or IDCANCEL, which are defined in the Windows SDK as 1 and 2, respectively. If the routine is successful, the string variable, A$, has been filled by NovaNew with the name of the selected document template. Before being used as an argument, the length of A$ must be established using a BASIC statement such as the following. A$ = String$(50," ") Failure to specify a large enough string as an argument to a DLL will typically result in a general protection fault. NOVANEW.C The main entry point in NOVANEW.DLL is the function NovaNew. NovaNew uses the Windows function DialogBox to invoke the GET_TEMPLATE dialog and return its results to WordBASIC. NovaNew relies on a small amount of trickery in the use of the GetActiveWindow function. Typically, library routines that manipulate windows will have a calling routine pass a window handle. Since WordBASIC does not have such a window handle, you must assume that GetActiveWindow returns the window handle of Word itself. The heart of NOVANEW.C is the dialog function, fnNew. This function responds to calls from the Windows dialog manager that indicate user interaction with the dialog box. During initialization, a generalized function, FillListBox, is called to fill the left list box with the contents of TEMPLATE.LB. Whenever a left list box item is selected, the right list box is filled with the contents of a file with the LB extension. The names of these files are actually in the list boxes, hidden beyond the right edge of the window frame: there are simply plenty of blanks between the visible entry and the filename. The LastWord function extracts a filename from the list box entry text. The ExtractTemplate function creates a template filename from the list box entry and verifies that the template file exists whenever a right list box item is double-clicked or the OK button is pressed. The first two functions in NOVANEW.C, LibMain and WEP, come with the Windows SDK. Both are specific to the creation of DLLs because they use a unique initialization mechanism. LibMain is called by LIBENTRY.ASM when the library is first invoked by the WordBASIC Declare statement. In NOVANEW.C, LibMain retains the library's instance handle in a global variable so that it can be used for Windows functions that require an hInstance. WEP is called when the library is being removed from memory. Although it serves no useful function in this library, it could be used to perform exit processing, such as the freeing of allocated storage. Both functions return TRUE to indicate normal execution. SS != DS Although NOVANEW.C looks like a normal Windows routine, it uses a unique memory convention that requires special care when building a library or converting existing Windows routines. In normal Windows programs, the stack segment (SS) and the data segment (DS) are the same. When a DLL is called, the DLL's data segment is used, but the stack is within the caller's data segment. The implication of this convention is that you cannot legitimately pass a short pointer to an object on the stack to a function. You must either use functions that use long pointers or you must place the objects of pointers in static or global storage. Usually, this convention interferes only with the conventional use of C string functions such as strcpy. Microsoft provides special versions of string functions in the SDK such as lstrlen, lstrcpy, and lstrcat that use long pointers. The Microsoft C compiler also has a switch, -Aw, that is used when compiling a DLL to verify that the SS != DS convention has been followed. All of the short pointer C functions can be used against static or global objects, since they are located in the library's data segment. Consequently, NOVANEW.C's string buffers are defined as global, allowing functions such as strcpy and strcat to be used without concern. Using global arrays for large objects is actually preferable when writing Word DLLs. Although Word supports DLLs, it does not guarantee any specific amount of stack space for library routine execution. Thus, it is possible to overflow the stack with large automatic arrays or highly recursive routines. Allocating large arrays in the data segment avoids this problem and has few adverse consequences in the Windows environment Version 3.0. NOVANEW.LNK and NOVANEW.DEF The link control and module definition files used in constructing DLLs are very similar to those used in making Windows programs with some notable exceptions. The Windows SDK includes a set of libraries, such as SDLLCEW.LIB and MDLLCEW.LIB, that are specific to the creation of DLLs. The LIBRARY entry in the module definition file is also necessary to identify the type of module properly. The EXPORTS section of the definition file identifies entry points in the library. Any exported function in a DLL can be called from WordBASIC. Debugging a Word DLL WordBASIC and Microsoft CodeView debugger for Windows make a very strong debugging environment for DLLs. It is relatively simple to generate and quickly modify a range of test cases for a DLL using WordBASIC statements. Computed results can be presented using the Print statement, the MsgBox WordBASIC function, or even using Insert text$, which will place the results in the current file being edited. A bug in a DLL routine usually causes Word (if not Windows itself) to fail, so it is essential that WordBASIC test programs be saved frequently. A misbehaving DLL routine can be best diagnosed using CodeView2. To use CodeView requires that the DLL be compiled with the -Zi switch and linked with the /CO option. CodeView also requires a machine with two monitors, such as a VGA monitor combined with a monochrome monitor or an IBM 8514/A. With that setup, it is possible to watch the source code and variables on one monitor while running the program on another. When debugging DLLs with Word, rather than specifying WINWORD.EXE as the debugged object, it is usually more convenient to specify a simple program, such as NOTEPAD.EXE, along with your DLL name. This can be done using the Program Manager's Properties dialog box and an entry such as the following, which specifies running CodeView on Notepad with NOVANEW as a DLL. c:\windev\dbgtools\cvw.EXE /l e:system\novanew c:\windows\notepad.exe With a setup such as this, CodeView can be started to debug the DLL at any time, even after the DLL has been invoked. Conclusion Through the use of embedded macro languages and DLLs, systems developers and integrators can add value to off-the-shelf software. A total end-user solution can be constructed by combining canned applications and custom programming. Often, a quality hybrid system can be implemented with less effort than coding entirely new applications. 1 For ease of reading, "Windows" refers to the Microsoft Windows graphical environment. Windows refers only to this Microsoft product and is not intended to refer to such products generally. 2For ease of reading, "CodeView' refers to the Microsoft CodeView debugger for Windows. CodeView refers only to this Microsoft product and is not intended to refer to such products generally. Figure 2. A Program Using the Windows Sound DLL Declare Sub SetVoiceQueueSize Lib "sound"(voice As Integer, n As Integer) Declare Sub OpenSound Lib "sound"(foo As Integer) Declare Sub CloseSound Lib "sound"(foo As Integer) Declare Sub StartSound Lib "sound"(foo As Integer) Declare Sub StopSound Lib "sound"(foo As Integer) Declare Sub SetVoiceNote Lib "sound"(nVoice As Integer, nValue As Integer, nlength As Integer, nCdots As Integer) Declare Sub SetVoiceSound Lib "sound"(nVoice As Integer, nFreq As Integer, nDuration As Integer) Declare Sub SetVoiceAccent Lib "sound"(nVoice As Integer, nTempo As Integer, nVolume As Integer, nMode As Integer, nPitch As Integer) Sub MAIN Notes$ = "G1a2bc3d4ef5g6A7BC8D9EF0" Song$ = Selection$() voice = 1 OpenSound(1) StopSound(1) SetVoiceQueueSize(voice, 10 +(Len(song$) * 6)) Duration = 8 For i = 1 To Len(song$) If Mid$(Song$, i, 1) = "+" Then Duration = 4 ElseIf(Mid$(song$, i, 1)) = "-" Then Duration = 8 ElseIf(Mid$(song$, i, 1)) = "/" Then duration = 16 ElseIf InStr(Notes$, Mid$(Song$, i, 1)) <> 0 Then SetVoiceNote(voice, InStr(Notes$, Mid$(Song$, i, 1)) + 19, Duration, 0) End If Next i StartSound(1) REM song1 -- cdeccdecef+g-ef+g/gAgf-ec/gAgf-ec-cG+c-cG+c REM song 2 - +cc-cd+e-edef+gg/CCCgggeee+c-gfed+c REM song 3 - End Sub Figure 6. Template Description Files ■ ■ TEMPLATE.LB ■ ■ ■ CORRESPONDENCE CORRESPM ■ LEGAL LEGAL ■ LABELS LABELS ■ MISCELLANEOUS MISCELLA ■ EXAMPLES EXAMPLES ■ OTHER WINWORD WINWORD ■ ■ CORRESPM.LB ■ ■ ■ Normal Document NORMAL ■ Plain Letter PLNLETER ■ Letter LETTER ■ Memorandum MEMO ■ Envelope ENVELOPE ■ ■ LEGAL.LB ■ ■ ■ Legal Contract CONTRACT ■ Legal Brief BRIEF ■ ■ LABELS.LB ■ ■ ■ 1-inch labels in one column, continuous LBL1COLT ■ 1-inch labels in two columns, sheet feed LBL2COL ■ 1-inch labels in two columns, continuous LBL2COLT ■ 1-inch labels in three columns, sheet feed LBL3COL ■ 1-inch labels in three columns, continuous LBL3COLT ■ ■ EXAMPLES.LB ■ ■ ■ Article for a Periodical ARTICLE ■ Basic Template BASIC ■ Technical Paper PAPER ■ Report REPORT ■ Brochure BROCHURE ■ Data for a Brochure BROCHDAT ■ Brochure Merge BROCHMRG ■ Form Letter FRMLTR ■ Template for Data Entry DATADOC ■ Example Document EXAMPLES ■ ■ MISCELLA.LB ■ ■ ■ Independent Systems Vendor ISV ■ Display of Keycaps KEYCAPS ■ ■ WINWORD.LB ■ ■ ■ PCWORD PCWORD ■ SETUP SETUP ■ PRICMEMO PRICMEMO ■ Figure 7. The FileNew WordBASIC Routine Declare Function NovaNew Lib "NOVANEW.DLL"(Name$) As IntegerSub MAIN A$ = String$(50," ") If(NovaNew(A$) = 1) Then A$ = Left$(A$, InStr(A$, ".") - 1) Super FileNew .NewTemplate = 0, .Template = A$ End IfEnd SubSub OLDDim dlg As FileNewGetCurValues dlgDialog dlgSuper FileNew dlgEnd Sub Creating a Data-Entry and Reporting Library for Windows[tm] 3.0: Part II Kevin P. Welch The first article in this two-part series framed a multipage data-entry and reporting library for the Microsoft Windows[tm] graphical environment. This installment explores the actual form library, creates a sample form, and demonstrates how it can be used with the form viewer. The form library was written so that multipage data-entry forms and reports could be created without C programming. A single multipage data-entry form and report was encapsulated into one dynamic-link library (DLL). Then any application using this form simply loads the library and calls the appropri--ate editing or reporting API function. Since the actual specification of the data-entry form and report is completely contained in the resource file (RC) associated with the library, no programming is required when you create a new data-entry form or report. A form viewer application is also presented. This application enables you to load a form library and experiment with data-entry or reporting functions. Sample Library To build the combined form viewer and data-entry library, you need to compile the list of files in Figure 1. Then you can experiment. When you run the form viewer application (VIEWER.EXE) using either the Program Manager or the MS-DOS Executive, the window in Figure 2 is displayed. The client area of the form viewer window is initially empty. Before you can start entering data you need to load a form library (in this case FORM.DLL). To do this, select the Form Open menu option, and the dialog box shown in Figure 3 is displayed. In this dialog box you enter the name of the form library you wish to use. When you press the OK button, the library is automatically loaded, the caption of the viewer window is updated to indicate the currently active library, and the input focus is set to the client area of the viewer window. The client area is actually a multiline edit control into which you can type information to be used by the form library. As explained in the first installment, each line in the data block should be in the following format: <FIELDNAME> : <DATAVALUE> The form library example presented in this article defines a product order data-entry form (something that many readers might need). This library supports a three-page data-entry form and simple report that contains the field names shown in Figure 4. Based on this list, you could enter information into the form viewer like this: NAME: Audrey Hepburn COMPANY: Tiffany's PRODUCT: Diamonds When you have entered a few field names and data values, the client area of the form viewer should look something like that shown in Figure 5. Now that you have entered some initial information, bring up the first page of the data-entry form by selecting Edit under the Form menu. Page1 of the product order form is then created with the information you entered displayed in the appropriate fields. The cursor is automatically positioned inside the first data entry field and the field is described in the lower-left corner of the dialog box. You can move from field to field by pressing Tab (note the change in the field description) while entering data. You can select a different page by pressing the Page Down key, or by clicking the mouse inside one of the page selection icons located in the lower-right corner of the dialog box (see Figure 6). When you are finished editing, save the information by pressing the Save button. This creates an updated data block; the resulting changes will be displayed inside the form viewer client area. Similarly, you can produce a report based on the information inside the form viewer by selecting Print under the Form menu. A print dialog box is displayed while a report is sent to the default output device. To abort the printing process, press the Cancel button inside the print dialog box (see Figure 7). Form Library Now that you understand the operation of the form viewer and a sample data-entry library, you can take a more detailed look at the way a form library operates. As described in the previous article, the specification of the form and report is defined in the RC file associated with the library using two entry points; FormEdit is used to edit data, and FormPrint is used to generate reports. HANDLE FormEdit( hWndParent, hData ); HANDLE FormPrint( hWndParent, hData ); The code that supports these functions is divided into five modules. The first, FORM0.ASM, defines the assembly-language entry point to the library. FORM1.C contains functions that are called when the library is loaded (see Figure 8). FORM2.C contains the FormEdit function and most of the code to display and manage a data-entry form. The next module, FORM3.C, contains the FormPrint function and all the associated code to create a report. The last module, FORM4.C, defines utility and support functions that are used throughout the library. Although these modules contain a great deal of source code, they can be easily understood by tracing the flow of operations when the library is first loaded, when a form is edited, and finally when the data is printed. Figure 9 shows the hierarchy of functions in the forms library; Figure 10 lists all the functions on a file-by-file basis. Initialization The first time the library is loaded, the LibEntry function defined in FORM0.ASM is called. LibEntry calls the FormInit function (defined in FORM1.C) with the current library instance handle as the only parameter. If this is the first instance of the library (determined by the current and previous library instance handles), the FormInit function will initialize the global FormInfo data structure. This data structure contains the form name, the number of data-entry pages, the library instance handle, and other global data. Some of this information is extracted from the FORM RCDATA section of the resource file associated with the library. This is done by using a sequence of FindResource, LoadResource, and LockResource function calls. As is the case throughout the rest of the library, whenever an error situation is encountered, a warning message is displayed listing the module and line number where the error occurred. Data Entry The most complex operation supported by the form library is data entry. When the FormEdit function in FORM2.C is called, it extracts the dialog box template named Page1 from the resource file. From this template the dimensions of the main data-entry dialog box are calculated. Then the dimensions are passed to the system as part of a new, dynamically defined dialog box template via a DialogBoxIndirect call. Before DialogBoxIndirect is called, a system keyboard hook function is installed. This hook function intercepts the Page Down and Page Up keystrokes that move the user to the next or previous data entry pages. All of the messages relating to the dialog box that are created during the DialogBoxIndirect call are handled by FormEditDlgFn. This function simply dispatches those messages of interest to a series of utility functions. A WM_INITDIALOG message is received by FormEditDlgFn when the dialog box is first created, resulting in a call to the EditInit function. EditInit retrieves the dimensions of the data-entry dialog box and creates the supporting controls that surround the data-entry form pages defined in the resource file. The actual size and location of these child windows is calculated using the base dialog units value, which is found using the GetDialogBaseUnits function. Once the supporting controls have been dynamically created, the EditInit function extracts each dialog box and control template by successive calls to the GetDlg and GetCtl functions. In the process, each control is created (based on the extracted template), immediately hidden, its window handle appended to the dialog box property list, and the initial data value defined. The initial data value is defined in the following manner. First the field name associated with the control is retrieved from the string table via the GetCtlName function and the control ID. If the control or field name is successfully retrieved, the data associated with the field extracted (by the GetCltData function) from the global formatted data block is passed to the FormEdit function. If data exists, it is passed to the SetCtlData function, which performs the actions necessary to initialize the control. Although in this implementation the SetCtlData function supports only Button and Edit control classes, it could be easily extended to support other classes. All dialog box controls are hidden while the data entry fields are created and defined. Only when the entire process is completed does the EditInit function display the contents of the first page. The display is accomplished by calling the EditDisplay function, which hides all controls associated with the previous data entry page (initially, none), and displays all controls associated with the new data entry page. The next action performed by the FormEditDlgFn occurs when the WM_CTLCOLOR and WM_USER messages are received. After the messages are received, the EditControl function is called. This function defines the appropriate control color (ignored in this implementation) and updates the comment field associated with each control by using the GetCtlComment function. Next, FormEditDlgFn calls the EditCommand function whenever a WM_COMMAND message is received. This function either saves the dialog box contents (using the GetDlgData function), prints the current form (using the GetDlgData and FormPrint functions), cancels data entry, or displays a new dialog box page. The GetDlgData function (defined in FORM4.C) requires further explanation because it processes each dialog box item, retrieving the actual data associated with each control by calling the GetDlgCtlData function. Like the SetCltData function, in this implementation the GetDlgCtlData function supports only the Button and Edit control classes. Apart from this, the most complex thing the GetDlgCtlData function does is the dynamic reallocation of the resulting global data block into 32-byte chunks as data is retrieved from each control. When the WM_DESTROY message is received, FormEditDlgFn calls the EditDestroy function, which cleans up the dialog box property lists and releases all associated global data blocks. Report Generation Report generation from within a data-entry form or from an application is performed when the FormPrint function (contained in FORM3.C) is called. This function is interesting because it generates the desired report while checking to see if the user wishes to cancel the entire process. Prior to printing, the FormPrint function extracts the report template from the library resource file and dynamically creates the report status dialog box. As long as this status dialog box remains visible, the FormPrint function assumes that the user wishes to continue printing. If the user presses the Cancel button, the status dialog box is destroyed. Periodically during the printing process the FormPrint function checks to see if the status dialog box is still visible. If it has been destroyed, the printing process is terminated. The actual output of each report line is accomplished by successive calls to the PrintOutput function. Using the GetCtlData function, this function parses the output specification string to extract the desired field data values from the global data block provided. If a requested field is undefined, a NULL value is automatically substituted. Conclusion Using a text editor and the Dialog Editor in the Microsoft Windows Software Development Kit, you can use the basic library structure presented here to alter FORM.H, FORM.RC, and create new forms and reports quickly and easily. Enhancements you might consider include the support of extended data types such as dates and numbers; additional controls such as combo boxes, list boxes, and scroll bars; enhanced reporting and data formatting; and on-line help within each form library using the Windows help system. The most ambitious upgrade would probably be to separate the resource portion of the library from the actual code by parsing a RES file. Whatever your refinements, a little customization should make FORM.DLL an important part of your Windows toolbox. ──────────────────────────────────────────────────────────────────────────── Form Viewer (described in MSJ, Vol. 5, No. 5) ■ VIEWER Make file ■ VIEWER.DEF Module definition file ■ VIEWER.H Header file ■ VIEWER.RC Resource file ■ VIEWER.ICO Icon ■ VIEWER.C Source code ■ ■ Page Selection Control (described in MSJ, Vol. 5, No. 4) ■ ■ CONTROL.H Installable control header file ■ ■ PAGE Make file ■ PAGE.DEF Module definition file ■ PAGE.H Public header file ■ PAGE.D Private header file ■ PAGE.RC Resource file ■ PAGE0.ASM Entry point module ■ PAGE1.C Intialization module ■ PAGE2.C Message processing module ■ PAGE3.C Style dialog box module ■ PAGE4.C Information module ■ PAGE5.C Style flags module ■ ■ Form Library ■ ■ ■ FORM Make file ■ FORM.DEF Module definition file ■ FORM.H Public header file* ■ FORM.D Private header file ■ FORM.RC Resource file* ■ FORM0.ASM Entry point module ■ FORM1.C Initialization module ■ FORM2.C Form editing module ■ FORM3.C Form printing module ■ FORM4.C Misc. utility functions ■ ■ *Only these files need changing when creating a new form library. ■ ■ ■ You will also need the following software development tools: ■ ■ Microsoft C 5.1 or 6.0 ■ Microsoft Macro Assembler ■ Microsoft Windows Software Development Kit 3.0 ■ You can then create the form viewer and data-entry library by entering the following commands: ■ ■ MAKE VIEWER ■ MAKE PAGE ■ MAKE FORM ■ ■ The result will be the following executables: ■ ■ VIEWER.EXE Form viewer application ■ FORM.DLL Sample form library ■ PAGE.DLL Page selection control (must be in your path) ■ Figure 4. Form Library Field Names ■ NAME Customer name {first,initial,last} ■ COMPANY Company name {optional} ■ ADDRESS Company or customer street address ■ CITY Company or customer city ■ STATE Company or customer state ■ ZIPCODE Company or customer postal code ■ ■ PRODUCT Name and version of product ■ SHIPVIA Product shipment method ■ PAYMETHOD Product payment method ■ CARDNUM Credit card number {if used} ■ EXPDATE Credit card expiration date {if used} ■ ■ OPTION1 5.25 inch diskette option {TRUE or FALSE} ■ OPTION2 3.5 inch diskette option {TRUE or FALSE} ■ FRIEND Customer heard about product from a friend {TRUE or FALSE} ■ AD Customer heard about product from an ad ■ {TRUE or FALSE} ■ SHOW Customer heard about product at a tradeshow {TRUE or FALSE} ■ DEALER Customer heard about product from a dealer ■ {TRUE or FALSE} ■ REVIEW Customer heard about product in a review ■ {TRUE or FALSE} ■ ARTICLE Customer heard about product in an article ■ {TRUE or FALSE} ■ COMMENT Misc. comments regarding customer order ■ Figure 10. Catalog of Form Library Functions ■ ■ FORM0.ASM: Entry point module ■ LibEntry ■ FORM1.C: Initialization module ■ ■ FormInit( hInstance ) : HANDLE ■ WEP( bExit ) : VOID ■ ■ FORM2.C: Form editing module ■ ■ FormEdit( hWndParent, hData ) : HANDLE ■ FormEditDlgFn( hDlg, wMessage, wParam, lParam ) : BOOL ■ ■ EditInit( hDlg, wParam, lParam ) : BOOL ■ EditControl( hDlg, wParam, lParam ) : BOOL ■ EditCommand( hDlg, wParam, lParam ) : BOOL ■ EditDisplay( hDlg, wNewPage ) : VOID ■ EditDestroy( hDlg, wParam, lParam ) : BOOL ■ ■ FORM3.C: Form printing module ■ ■ FormPrint( hWndParent, hData ) : HANDLE ■ FormPrintDlgFn( hDlg, wMessage, wParam, lParam ) : BOOL ■ ■ PrintInit( hDlg, wParam, lParam ) : BOOL ■ PrintCommand( hDlg, wParam, lParam ) : BOOL ■ PrintOutput( hPrnDC, hData, lpszPattern, wPage, wLine, wPixels ): BOOL ■ PrintDestroy( hDlg, wParam, lParam ) : BOOL ■ ■ FORM4.C: Misc. utility functions ■ ■ GetDlg( lpResData, lpDlgBox ) : BOOL ■ GetDlgData( hDlg ) : HANDLE ■ GetDlgCtlData( hDlg, hWndCtl, lpszData, wMaxData ) : BOOL ■ ■ GetCtl( lpDlgData, wControl, lpDlgCtl ) : BOOL ■ GetCtlName( hInstance, wCtlID, lpszCtlName, wMaxCtlName ) : BOOL ■ GetCtlData( hData, lpszCtlName, lpszData, wMaxData ) : BOOL ■ GetCtlComment( hInstance, wCtlID, lpszComment, wMaxComment ) : BOOL ■ SetCtlData( hDlg, hWndCtl, lpDlgCtl, lpszData ) : BOOL ■ ■ StringMatch( lpszString, lpszPattern ) : BOOL ■ StringCat( lpszDest, lpszSrce, wMaxDest ) : VOID ■ StringCopy( lpszDest, lpszSrce, wMaxDest ) : VOID ■ StringJoin( lpszDest, lpszSrce, wMaxDest ) : VOID ■ ■ GetPrinterDC() : HDC ■ CenterPopup( hWnd, hWndParent ) : VOID ■ FormMsgHookFn( wContext, wCode, lData ) : WORD ■ Warning( hWndParent, lpszMessage, lpszFile, wLine ) : VOID ■ Questions & Answers - Windows Q: I'm writing a Windows application that I want to run single-instance-only, and I'm having two problems. First, it appears that a user can circumvent the WinMain hPrevInstance argument mechanism (by which Windows informs an application that a previous instance is already running) by simply renaming the appplication's EXE file. I would like the user to be able to rename the EXE to whatever he or she wants, but running an accidental second instance would corrupt a dedicated set of files that the application uses. If I can't count on hPrevInstance, what can I do? Second, if I can successfully retrieve a previous instance's hPrevInstance, how can I use hPrevInstance to make the previous instance of the application the active application instead of starting a second instance? I've noticed that the Windows 3.0 Control Panel does this under normal circumstances, and it's a nice effect. Bill Dow Alameda, CA A: Under the Windows[tm] graphical environment versions 1.x and 2.x, it was indeed possible to circumvent the hPrevInstance mechanism by renaming the EXE file. In Windows1 3.0, simply renaming the EXE file will no longer work, but it is still possible to trick Windows (intentionally or accidentally) by another method. All that Windows compares to detect previous instances are the up-to-eight-character external name (the EXE filename, ignoring any path) and the up-to-eight- character internal name (the NAME in the application's DEF file, which becomes embedded in the EXE file). Obviously, they may differ. For the external names, it is important to note that the path leading to the EXE file is totally ignored in previous-instance comparisons, since there is no path associated with the internal name that could be used in the comparison. To see a list of internal names, run the Windows Software Development Kit HeapWalker program and scan the OWNER-NAME column. Earlier versions of Windows compared only the external name of the application to be loaded with the internal name of all the applications already loaded. If a match was found, Windows assumed that a second instance of the first application was to be loaded. Windows did not need to reopen the EXE file for the second instance and look for its internal name, because it was already opened. If no match was found, Windows assumed that the application being loaded was a first instance, with the result being an hPrevInstance equal to 0. This is why renaming the application's EXE file would trick earlier versions of Windows into loading duplicate code segments and would fool the second instance of "single-instance" applications into always thinking it was the first instance. In Windows 3.0, renaming the EXE file will no longer cause that problem, because Windows now remembers the external names along with the internal names of all running applications. Renaming an EXE no longer matters; the second instance of your application will be able to use hPrevInstance to detect that it is the second instance. This new method does lead to an interesting side effect. Copy NOTEPAD.EXE to your temporary subdirectory and rename it CONTROL.EXE. Run it explicitly, that is, run C:\TMP\CONTROL.EXE. Now try to run Control Panel from the Program Manager. You'll get a second instance of Notepad! So how do you trick Windows 3.0 into running two copies of a single-instance application? Simply make a second copy of the single-instance application and give it a new name. Then rename the original application to a third name. The application then has two different external names that are both different from the internal name of the application. You can then run both EXE files as if they were different applications, even though they are not. You should ignore the WinMain hPrevInstance argument entirely, and go directly for the hWnd of the first instance. The first few lines of your WinMain function should look like this (assuming that your main window's class is named KillerApp). auto HWND ahWndPrev; if (ahWndPrev = FindWindow("KillerApp", NULL)) { BringWindowToTop(ahWndPrev); return 0; } All single-instance applications should do this! Not only does it ensure that only one instance of the application runs, it is much friendlier to the user than simply terminating or putting up a rude message box that the application is already running. This elegantly takes care of the situation where the first instance's window may be completely covered by another window. ──────────────────────────────────────────────────────────────────────────── Q: I have an application in which I need to display a number of different bitmaps both in the client area and on paper. I need to display them isotropically; in other words, the aspect ratio of the bitmaps needs to be maintained. My problem is that the bitmaps are of various sizes, known only at run time when the bitmaps are read, and are generated on devices with different resolutions and aspect ratios, also known only at run time. Windows offers the isotropic mapping mode, but I can't figure out a way to fit the bitmaps into that mapping model all at the same time. Rick Mosebach New York, NY A: I've written a short function named MakeIsotropic that will perform the isotropic calculations for you (see Figure 1). Do the isotropic calculations manually with MakeIsotropic, and then use BitBlt to draw the bitmaps with the width and height values generated by the function. You should do all this in the default MM_TEXT mapping mode, so that you have complete control over the coordinate system. The problem you describe involves eight variables, which result from the permutations for each bitmap of the source/destination, the pixels/pixels-per-inch in each direction, and the horizontal/vertical width/height: ■ ■ DPX: destination pixels horizontal ■ DPY: destination pixels vertical ■ DIX: destination pixels per inch horizontal ■ DIY: destination pixels per inch vertical ■ SPX: source pixels horizontal ■ SPY: source pixels vertical ■ SIX: source pixels per inch horizontal ■ SIY: source pixels per inch vertical Given these variables, the following formula will make them perfectly isotropic. DPX DIY SPX SIY -- x -- = -- x -- DPY DIX SPY SIX In your case, though, and in Windows in general, the DPX and DPY are a function of how the user sizes the destination. You have to convert these formulas so that you can calculate a new DPX or DPY based on the requested size of the target. You can calculate either DPX or DPY using the following formulas, which are essentially how the isotropic calculations are performed. DIX SPX SIY DPX = DPY x -- x -- x -- DIY SPY SIX DIY SPY SIX DPY = DPX x -- x -- x -- DIX SPX SIY MakeIsotropic works as follows. The developer must specify pointers to DPX and DPY and the values of the other six variables. Either DPX or DPY is decreased so that the resultant isotropic rectangle can fit in the original rectangle specified by these two variables. The while tests solve the potential overflow problem when calculating the numerator and denominator. For each component of the numerator and denominator, they check to see if the variables are not odd (therefore, even); while both are even, each is divided by two. Theoretically you could perform nine while tests for each numerator/denominator-component permutation, but these five should be sufficient. The numerator and denominator are then calculated, and another while loop is executed to reduce both of them to prevent overflow. Once this is done, a temporary value for DPY is calculated and compared to the original DPY. If the original value of DPY is less than the new isotropic value, DPX must be recalculated. Otherwise, the new lower value of DPY replaces the original value. DPX and DPY are now an isotropic rectangle within the original DPX and DPY. Q: How does SMARTDRV.SYS operate at a low level, and how can it be best used? T. J. Kelly Atlanta, GA A: SMARTDRV.SYS is a disk-caching device driver that operates with both DOS and Windows. It minimizes the number of times that your physical disks must be accessed as DOS/Windows is running, since disks are relatively slow. Generally, there are four ways in which disks can be cached: by sector, by cluster, by track, or by cylinder. SMARTDRV operates as a track cacher. Because it's a track cacher, SMARTDRV deals with tracks when it talks to the disk controller. When an application asks DOS or Windows to read a sector, SMARTDRV intervenes. It asks the disk controller for the entire track that the sector resides on, and passes the sector back to the application. This way, the entire track that the disk's read/write heads are over is obtained while the heads are there, minimizing head movement. (Head movement is fairly slow.) Then when the application asks for the next sector, SMARTDRV already has it in its memory buffers and returns it immediately. There are two considerations when using SMARTDRV (or any track cacher, for that matter) that users generally do not appreciate. The first is that not all disks are interleaved optimally, so reading an entire track as the disk spins may take longer than is actually possible. The second is that a high degree of file fragmentation destroys the benefits of a track cacher, since a particular file may be broken up inordinately across many tracks. Interleaving is the low-level way to refer to specific sectors on a hard disk. Standard IBM PC hard disks have 17 sectors per track. COMPAQ PCs are a little more varied, with sectors per track at the following points: 17, 25, 26, 33 and 34. A 1:1 interleave means that, on each track, sector 2 physically follows sector 1, sector 3 physically follows sector 2, and so on. 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,... A 2:1 interleave on a 17-sectors-per-track disk would physically be laid out like the following: 1, 10, 2, 11, 3, 12, 4, 13, 5, 14, 6,... And a 3:1 interleave on a 17-sectors-per-track disk would physically be laid out like this: 1, 7, 13, 2, 8, 14, 3, 9, 15, 4,... The result of all this is that a 2:1 interleaved disk takes twice as long to read as a 1:1 interleaved disk, because the disk has to rotate twice for each complete track to be read sequentially by sector, a 3:1 interleave takes three times as long, and so on. The reason that disks are interleaved at ratios other than 1:1 is that not all disk controllers in combination with the BIOS are fast enough to read 1:1 interleaved disks. Sector 2 on a 1:1 interleaved disk may already be passing by a slow controller when the controller needs to retrieve it after sector 1, thereby having to wait an entire disk revolution for it; a 2:1 interleave would be a better choice for that disk using that controller. Unfortunately, not all manufacturers calibrate the disks with the installed controllers. Many simply choose a safe 3:1 interleave. Programs such as Gibson Research SpinRite II can perform a nondestructive low-level format that takes all of this into account. On one of my computers, I went from a 3:1 interleave to a 1:1 interleave because of a fast new $125 Western Digital[tm] WD1006V-MM2 controller I bought-and Windows performance doubled! The disk light stayed on for a third of the time it used to each time SMARTDRV retrieved a track. The second consideration is that a lot of file fragmentation destroys the benefits of SMARTDRV, because files may be broken up inordinately across many tracks. If you specify a 512Kb-minimum SMARTDRV on a 17-sectors-per-track (8.5Kb-per-track) disk, SMARTDRV can cache 60 tracks (512Kb / 8.5Kb = 60). An 8Kb contiguous-sector file may reside entirely on a track, at best; at worst, if the file was highly fragmented, it may be spread across 4 tracks (on a 2Kb-per-cluster disk). In the worst case, an 8Kb file would be using up 34Kb (6.7 percent) of SMARTDRV's cache memory. Since Windows does a lot of object (code, icon, cursor, etc.) discarding and reloading, you must minimize file fragmentation to receive the benefits of SMARTDRV. To see how much your hard disks are fragmented, you can download CHKFRG.ZIP (which contains CHKFRAG.EXE) from the PC MagNet Utilities forum on CompuServe (GO PCMAGNET). Programs such as PC Tools can periodically unfragment your hard disks and improve the performance of SMARTDRV, DOS2, and Windows. Q: Some aspects of dialog boxes confuse me. Why are DialogBox dialog boxes modal and CreateDialog dialog boxes modeless? What is the difference between the return value of a DlgProc and the value specified in EndDialog, and why is the separate function necessary? Alan Cooper Menlo Park, CA A: When you call DialogBox, it first creates a dialog pop-up window using its own internal DlgProc (not to be confused with the DlgProc you supply), then creates all the control child windows using the standard internal CtlProcs (such as Button, Edit, Static). Next, DialogBox calls the DlgProc you furnished with a WM_INITDIALOG message, displays the dialog box window, and finally goes into a GetMessage / TranslateMessage / DispatchMessage loop. This loop is essentially in the dialog manager, and it is in control as far as you are concerned. By disabling the owner of the dialog box and going into its own loop, the dialog that appears is modal, since only a few messages (WM_PAINT, for example) get dispatched by the dialog manager to your WndProcs. When you call CreateDialog, it follows the same procedure as above, but does not go into a similar GetMessage / TranslateMessage / DispatchMessage loop. Instead, it returns control immediately to the routine containing the CreateDialog call. Because it doesn't go into the loop, the dialog box that appears is modeless and all messages are dispatched by your main GetMessage / TranslateMessage / DispatchMessage loop as they would normally. This is why you have to add IsDialogMessage calls to you main loop for modeless dialog boxes. When you make an IsDialogMessage call, the dialog manager manages the dialog controls just as it does in its internal loop in the DialogBox code. The dialog manager essentially becomes a stub off your main loop. In both cases, your DlgProc becomes a stub off the dialog manager code. The dialog manager handles most activities associated with the dialog using its own internal DlgProc, but calls your DlgProc in cases where you need to take some action. This is why you do not call DefWindowProc in your DlgProc; the DefWindowProc call is done inside the dialog manager's internal DlgProc. The return value of your DlgProc simply communicates information back to the dialog manager. In the case of DialogBox, you need to communicate explicitly to the dialog manager's message-processing loop when to terminate; this is done by the EndDialog function. Since the dialog manager's loop is not in control of dialog boxes created with CreateDialog, you do the DestroyWindow call yourself instead of calling EndDialog. 1 For ease of reading, "Windows" refers to the Microsoft Windows graphical environment. "Windows" refers only to this Microsoft product and is not intended to refer to such products generally. 2 As used herein, "DOS" refers to the MS-DOS and PC-DOS operating systems. Figure 1. MakeIsotropic extern int FAR PASCAL MakeIsotropic( WORD FAR * afpwDPX, // dst horz pixels WORD FAR * afpwDPY, // dst vert pixels WORD awDIX, // dst horz pixels per inch WORD awDIY, // dst vert pixels per inch WORD awSPX, // src horz pixels WORD awSPY, // src vert pixels WORD awSIX, // src horz pixels per inch WORD awSIY) // src vert pixels per inch { auto DWORD adwNum, // DIY SPY SIX adwDen; // DPY = DPX x --- x --- x --- auto WORD awTmp; // DIX SPX SIY if (*afpwDPX == 0 || awDIX == 0 || awSPX == 0 || awSIX == 0|| *afpwDPY == 0 || awDIY == 0 || awSPY == 0 || awSIY == 0) return -1; while (!(awDIX & 1) && !(awDIY & 1)) { awDIX /= 2; awDIY /= 2; } while (!(awSPX & 1) && !(awSPY & 1)) { awSPX /= 2; awSPY /= 2; } while (!(awSIX & 1) && !(awSIY & 1)) { awSIX /= 2; awSIY /= 2; } while (!(awDIX & 1) && !(awSIX & 1)) { awDIX /= 2; awSIX /= 2; } while (!(awDIY & 1) && !(awSIY & 1)) { awDIY /= 2; awSIY /= 2; } adwNum = (DWORD)awDIY * awSPY * awSIX; adwDen = (DWORD)awDIX * awSPX * awSIY; while (!((WORD)adwNum & 1) && !((WORD)adwDen & 1)) { adwNum /= 2; adwDen /= 2; } awTmp = (WORD)((DWORD)*afpwDPX * adwNum / adwDen); if (*afpwDPY < awTmp) *afpwDPX = (WORD)((DWORD)*afpwDPY * adwDen / adwNum); else *afpwDPY = awTmp; return 0; }