PCjs Machines

Home of the original IBM PC emulator for browsers.


Microsoft Systems Journal (Vol. 5)

The following document is from the Microsoft Programmer’s Library 1.3 CD-ROM.

Microsoft Systems Journal Volume 5


Volume 5 - Number 1


Accessing Presentation Manager Facilities from Within OS/2 Kernel

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

■    WinOpenClipbrd opens the Clipboard

■    WinEmptyClipbrd removes any stored data from the Clipboard

■    WinSetClipbrdData places data into the Clipboard for a particular data

■    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


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.


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 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

Figure 1


# 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  common header file for PMServer and client programs */


                            // 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];

#define CLIENTDATAMSG(ptr)    ((CLIENTDATA *)ptr)

#define MAXAPPNAME       50
 #define MAXTOPICNAME    50


/*  PM program that supplies PM services to VIO apps */

#define INCL_WIN
#define INCL_VIO
#define INCL_AVIO
#define INCL_DOS

#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;
HQUEUE  qhandle;

int main(void);
                              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
              sizeof(QMgrThreadStack),NULL) = = -1)

    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;

                              MPARAM mp2)
    SEL          ClipTextSel;
    PVOID        temp1,temp2;
    USHORT       len;
    PCH          temp3;
    USHORT       fmtInfo;

        // ************* Create processing **********
        case WM_CREATE:
            ThreadWindowHdl = hwnd;
            return 0;

        // **************Clipboard handling *********

        case WM_COPY:                 // place client data in Clpbrd
            temp1 = MAKEP(ClipTextSel,0);
            temp2 = PVOIDFROMMP(mp1);
            temp1 = temp2 = NULL;

            WinOpenClipbrd(hab);    // open the clipboard
            WinEmptyClipbrd(hab);    // empty it
    // put the new data in it
            WinSetClipbrdData(hab, (ULONG)ClipTextSel, CF_TEXT,
            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
            else    // nothing in clipboard
            return 0;

        case WM_PASTE_MSG:    // client query about Clpbrd
    // tell client whether avail
                    (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;


        temppid = 0;
        MsgQGet(qhandle, &msgdata, &msgsize, &msg);

            case PMS_COPY:
            case PMS_INIT:
            case PMS_TERMINATE:
            case PMS_CLPBRD_QUERY:
            case PMS_PASTE:
                temppid = (msgdata ? CLIENTDATAMSG(msgdata)->pid : 0);

            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

    // find open slot
                   for( i = 0; i < MAXCLIENTS &&
                       clients[i].clientpid; i++);
                    if(i = = MAXCLIENTS)    // no slots found
                    else    // got open slot
                        clients[i].clientpid =
                            clients[i].clientqhandle = temphandle;
                tempname = NULL;

            case PMS_COPY:    // client has data for the Clipboard
                msgdata = NULL;

            case PMS_CLPBRD_QUERY:
               // client wants to know if something's in the clipboard

            case PMS_PASTE:
               // client is requesting a copy of the Clipboard data

            case PMS_TERMINATE:
             // client is terminating or is no longer using PM Services
                    // if not last slot, shift down 1
                    if(position != MAXCLIENTS-1)

    // clear last slot

             // unknown message rec'd, notify client if it has a queue
        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 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  message queue routines */

#define    INCL_DOS

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))

// 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

    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)
    // create a segment for data
        if(retval = DosAllocSeg(size,&sel,(SEG_GIVEABLE)))
        DosGiveSeg(sel,qtable[i].qowner,&newsel);    // make it giveable
        qptr = MAKEP(newsel,0);
        memmove(qptr,event,size);    // put the data in it
        qptr = NULL;
   // write message+data to q
    if(retval = DosWriteQueue(qhandle,msg,size,(PBYTE)qptr,0))
    if((qptr != NULL) && (sel != newsel))    // free seg if not ours

// 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))
     *msg = qresult.usEventCode;

// close queue and remove from table
void MsgQClose(HQUEUE qhandle)
    USHORT i;

    for(i = 0; i < qtableindex; i++)
        if(qtable[i].qhandle = = qhandle)
    if(i != qtableindex)
               (sizeof(QTABLE)*(qtableindex -(i+1))));

Figure 7


# 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  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 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
    2. It allows the user to type text into any part of the input
    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

#define    INCL_SUB
#define    INCL_DOS


#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

/********************* Buttons ****************************/

#define     BUTTONLIST  6
/* 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,
{" 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;

    {"Clipboard Functions",     BORDERROW,     3},
    {"General Functions",       BORDERROW,     63}

PCHAR mainmsgqueue = MAINMSGQUEUE;

long MouSem = 0L, KbdSem = 0L;

unsigned    BlankCell = 0x0f20;

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
    if(retval = MsgQOpen(&pmshdl,PMSERVERQUE))
        error_exit(retval,"MsgQOpen - PM Server probably hasn't


    // start other threads
            sizeof(MouThreadStack),NULL) = = -1)
            sizeof(KbdThreadStack),NULL) = = -1)
            sizeof(RequestThreadStack),NULL) = = -1)

    DosSemWait(&MouSem,SEM_INDEFINITE_WAIT); // let threads get going

    VioGetBuf((PULONG)&bufptr, &msg, VIOHDL);

        MsgQGet(qhandle, &msgdata, &msgsize, &msg);


            case MSG_ESCAPE:
            case ESC:
                MsgQClose(qhandle);    // close the message queue
    // clear the screen
                VioScrollUp(0,0,-1,-1,-1,(char *)&BlankCell,VIOHDL);
                DosExit(EXIT_PROCESS,0);    // get out of dodge

            case MSG_CLR:    // user clearing screen
                           (char *)&BlankCell,VIOHDL);

            // ************* Mouse Message handling
            case MSG_B1DOWN:
                // clear any existing block and set up a new one
                valid_block = FALSE;
                capture = TRUE;
                rowBlockBeg = MOUMSG(msgdata)->row;
                colBlockBeg = MOUMSG(msgdata)->col;
                startBlock = (rowBlockBeg * ScreenCols) + colBlockBeg;

            case MSG_MOUSEMOVED:

            case MSG_B1UP:
                    capture = FALSE;
                    valid_block = TRUE;

            // ************* Keyboard message handling
            case MSG_CHAR:

                    case '\b':
                        VioWrtTTY(" \b",2,VIOHDL);

                    case '\r':
                        if(row = = ScreenRows)
                                    (char *)&BlankCell,VIOHDL);



            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
                    case MSG_UPARROW:
                        if(row > 0)

                    case MSG_DOWNARROW:
                        if(row < ScreenRows)

                    case MSG_LEFTARROW:
                        if(col > 0)

                    case MSG_RIGHTARROW:
                        if(col < ScreenCols)

                    case MSG_HOME:
                        col = 0;

                    case MSG_END:
                        col = ScreenCols-1;

// *******************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

                if(msg == MSG_CUT)    // reset screen
                valid_block = FALSE;

            case MSG_PASTE:    // user wants to paste

// **************** PMServer message handling ***********
            case PMS_CLPBRD:    // clpbrd data is available
                clipboard_data = TRUE;
                findbutton(" Paste ", &b);

            case PMS_CLPBRD_EMPTY:    // clpbrd is empty
                clipboard_data = FALSE;
                findbutton(" Paste ", &b);

            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;

                            (char *)&BlankCell,VIOHDL);
                    row -= j;


            case PMS_NO_INIT:     // server can't initialize
                error_exit(msg,"PMS_Init - unable to access PM Server");

            case PMS_INIT_ACK:    // server initialized
                clipboard_data = FALSE;    // then fall thru

        if(msgdata != NULL)

// terminates PMServer connection
void PMS_Terminate(HQUEUE serverhandle)
    // terminate connection
    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;
        i += len-1;
        while(buffer[i] = = ' ')
        buffer[++i] = '\r';
        buffer[++i] = '\n';
    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;
        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(&highlight_att,blocksize,startrow,startcol,V IOHDL);

// reset screen work area
void ResetScreen(void)
    USHORT i;

    for( i = STARTROW; i <= ENDROW; i++)

// blank screen text block
void BlankBlock(void)

// reset screen to normal
void ResetBlock(void)

 // 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
                                               // draw border line
    for( i = 0; i < SCREEN_LABELS; i++)        // write labels

// 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))
                case BPRESS:    // if a press button
                    ButtonPaint(b,BUTTON_ON);    // turn it on
                    DosSleep(100L);    // wait
                    ButtonPaint(b,BUTTON_OFF);    // turn it off

                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));

/******* end of main thread code **************************/

/******* start of request thread code *********************/

// periodically wakes up and queries PMServer about Clipboard
void RequestThread(void)
    USHORT count = 0;

        if(!clipboard_data)    // if flag invalidated
    // ask if pasting possible
        DosSleep(REQUESTTHREADSLEEPTIME);    // sleep a while
        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  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     text-button functions */

#define    INCL_SUB

#define    VIOHDL  0

extern BUTTON buttonlist[];

void InitButtons(void)
    BUTTON *b = buttonlist;

    for( ; b->text; 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;

void findbutton(char *text,BUTTON **bptr)
    BUTTON *b = buttonlist;

    for( ; b->text; b++)
            *bptr = b;

void DisplayButtons(void)
    BUTTON *b = buttonlist;

    for( ; b->text; 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
    // write the top line
    // write the 2nd corner char

    if(*text)    // if title, write it
    // write the left border
    text = b->text;    // reset pointer
    // write the message
                    startcol+1,&cell[ 1],VIOHDL);
    // write the right border
    // write the 3rd corner
    // write the bottom line
    // write the 4th corner

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++)

Figure 9


void KbdThread(void);

KBD.C Source Code

/* keyboard thread code for PMAccess */

#define INCL_SUB
#define INCL_DOS

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;


    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

        KbdCharIn(&KbdKeyInfo,IO_WAIT,KbdHandle); // get a character
        if(KbdKeyInfo.chChar)    // if Ascii code
        else if(event = AcceleratorPressed(KbdKeyInfo.chScan))    //
            MsgQSend(qhandle,NULL,0,event);    // if so, pass it on

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


void MouThread(void);


/* mou.c  mouse thread code for PMAccess */

#define INCL_DOS
#define INCL_SUB

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 buttons[3] =


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;


    if((retval = MouOpen((PSZ)NULL,(PHMOU)&MouHandle)))

    MouDrawPtr(MouHandle);    // display mouse pointer
    MouFlushQue(MouHandle);    // flush mouse queue
    MouGetNumButtons(&numbuttons,MouHandle);    // get button count
    DosSemClear(&MouSem);    // notify main thread

        {    // read the queue
        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
                buttondown = TRUE;
        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(!buttons[i].down)    // if button was previously up
    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)



    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))

                return MOUSECODE(b->left_button_val);
            else if(MouB2Pressed(ev->fs))
                return MOUSECODE(b->right_button_val);
                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 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

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

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

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

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

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

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

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

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

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

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

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

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

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

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.


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

ISMAXCONN    251    3            Maximum number of connections would be

ISNOTCONN    252        3    3    Client not connected

ISCONN    253    3            Client is connected

ISNULLPTR    254        3        GlobalLock of query block returned NULL

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. */
            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 hFileName1;
    GLOBALHANDLE hFileName2;
    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

    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. */


/* 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. */


/* 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. */
        iserrno = ISNULLPTR;
        return -1;
     /* Copy the record. */
    iRecLen = iRecLenTbl[isfd];
    lmemcpy(lpRec, (LPSTR)record, iRecLen);

    /* All fields filled, now unlock and make the request. */
    iRetVal = SendMessage(hWndServer, wmSendISAM, hWndClient,
                         MAKELONG(hRequestBlock, 0));
    if (iRetVal != ISOK) {
        /* Protocol error. */
        if (iRetVal = = 0L)
            iserrno = ISNOSERVER;
            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. */
        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);

    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;
            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,
                        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);

    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).
            /* No connected clients, ok to terminate. */
             return DefWindowProc(hWnd, iMessage, wParam, lParam);

    case WM_DESTROY:
   /* Can do some final cleanup here before the server terminates. */

    case WM_QUERYOPEN:
        /* The server is always an icon. */

        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.

            /* 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;
            /* Default message processing. */
            return DefWindowProc(hWnd, iMessage, wParam, lParam);


    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.
                clientTbl[i] = NULL;
            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


■    Permits larger applications by not residing in client address space
with EMS

    memory configurations

■    Data segments may reside in banked memory with EMS memory

■    Not limited to a single data segment


■    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.


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:

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 +

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.


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

typedef struct format
ULONG fFormatFlags;  /* formatting style bits */
NPFN pfnValidChar;   /* function to validate each
PSZ szPicture;        /* picture string */

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

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",

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 |


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",

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.


To avoid this, determine the correct value yourself and insert it directly
into the CTLDATA clause so that you have:


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 */
   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

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:

              "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)

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.


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


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



/*   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

  SUBMENU  "~File", 1
    MENUITEM "~Test...", ID_DLG

     DIALOG "FORMATTING", DLG_FORMAT, 92, 90, 213, 75,
        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",

CONTROL "AAAAAAAA", ID_ALPHA, 64, 29, 67, 8, "Formatted",
                 ES_LEFT | ES_MARGIN | WS_TABSTOP |
        CONTROL "", ID_ALNUM, 64, 10, 71, 8, "Formatted",
                 ES_LEFT | ES_MARGIN |
                 WS_TABSTOP | WS_VISIBLE
        CONTROL "OK", ID_OK, 159, 9, 38, 12, WC_BUTTON,




#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


DESCRIPTION     'Formatting Test'
HEAPSIZE    1024
EXPORTS    ClientWndProc



/*       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
FATTRS    FontAttrs;
LONG    IDCourier = 0;
HPS    hMyPS;

HWND hWndFrame,

PFNWP  pfnOldEditWndProc = (PFN) NULL;
PFNWP  pfnOldStaticWndProc = (PFN) NULL;


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 */

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 */
 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 |

     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. */

  return 0;


/* ClientWndProc()
/*   Main window procedure for this app. All messages to the client   */
/*   window get sent here.


     HPS hPS;

     switch (msg)
       case WM_PAINT :
         /*        Erase the window      */
    hPS = WinBeginPaint(hWnd, NULL, NULL);

       case WM_COMMAND :
         switch (COMMANDMSG(&msg)->cmd)
              case ID_DLG  :
                WinDlgBox(HWND_DESKTOP, hWnd, FmtDlgProc, NULL,
                          DLG_FORMAT, NULL);
              case ID_EXIT :
                WinPostMsg(hWnd, WM_CLOSE, 0L, 0L);
         return MRFROMSHORT(FALSE);

     } /* end switch */

  return WinDefWindowProc(hWnd, msg, mp1, mp2);


/* FmtDlgProc()
/* Driver for the sample dialog box.                                      */


  HWND    hEdit;
  struct    fldfmtinfo *pFld;
  PFNWP    pfn;
  PICINFO    *pi;
  int    i;

  switch (msg)
    case WM_INITDLG :
      /* Get the monospaced font (Courier) */
      hMyPS = WinGetPS(hDlg);

       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 :
      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.         */


  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)
       We create a format information structure and associate it with
      this control. */
    case WM_CREATE:

       /* Get the control ID from the create-structure */

      /* 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),

       /* 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 = ' ';
    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;

      /* 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)

      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,

          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,

 /* 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 */
             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,
          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:

              incr = (c = = VK_LEFT) ? -1 : 1;
                /* BUG - we don't know what rc is */
                rc = SHORT1FROMMR(pfnOldEditWndProc(hWnd,WM_CHAR,mp1,mp2));
                    while (rc && !CharToPicInfo(pFmt->szPicture[iPos +=

              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]) &&
                mp1 = MPFROMSH2CH(KC_VIRTUALKEY, 1, 0);
                mp2 = MPFROM2SHORT(0, VK_LEFT);
                if (!pfnOldEditWndProc(hWnd, WM_CHAR, mp1, mp2))

              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);

              return MRFROMSHORT(FALSE);

          } /* end switch (c) */
        } /* if picture */
      } /* end if KC_VIRTUALKEY */
      return MRFROMSHORT(TRUE);

      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? */

      /* Get the window's format structure */
      if ((pFmt = (PFORMAT) WinQueryWindowPtr(hWnd, 0)) == NULL ||

      /* 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))
      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);

      /* 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);

/* Return FALSE to signify that we processed the message ourselves. */
      return MRFROMSHORT(FALSE);

    case WM_DESTROY :
      if ((pFmt = (PFORMAT) WinQueryWindowPtr(hWnd, 0)))
  } /* 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,

/* CharToPicInfo()               Returns a pointer to the PICINFO       */
/*  structure associated with character c.                               */
PICINFO *CharToPicInfo(c)

  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.                     */
                 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);

      /* 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.               */
  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),
      /* If the matched font is 16x9, then let's use it. */
      if (FMCourier.lAveCharWidth = = 9L
          && FMCourier.lMaxBaselineExt >= 16L)

  return (i < nFonts);

/* FormatError()
/*   Sounds a little beep when the user presses a bad key.              */
  return TRUE;

/* lstrlen(), lstrcpy()
/*   Miscellaneous far-pointer routines                                   */
int lstrlen(PSZ s)
  int len = 0;
  while (*s++)
  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 */
  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

    successful, or FALSE if it is not.

EM_QUERYCHANGED (PM)    Returns TRUE if the text has changed since the


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

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

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

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

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.


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


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

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

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

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

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

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.


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.


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

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

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

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.


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

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 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

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,

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 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

              IDS_NAME "Sample"

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
           POPUP   "&File"
                   MENUITEM "&Open...", midFILEOPEN

   midFILEOPEN DIALOG 8, 20, 148, 102

     .... dialog box controls defined here


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

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

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 (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

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

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.


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

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.


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:


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

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.


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

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

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

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

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

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

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


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


#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_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



DESCRIPTION    'Checkers Version 0.10 (c) 1990, Charles Petzold'
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        ClientWndProc


#include <os2.h>
#include "checkers.h"

     SUBMENU "~Game",                   -1

MENUITEM "~Black on Bottom",       IDM_BOTTOM,, MIA_CHECKED

          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 "~Standard colors",       IDM_COLOR_STANDARD

#define GRP WS_GROUP

     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,
          DEFPUSHBUTTON "OK"              DID_OK, 80,    8,  40, 16, GRP


 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,
  RADIOBUTTON  "Green"      IDD_COLOR + CLR_GREEN,    20,    108,    64,
  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,
  RADIOBUTTON  "Pale Gray"  IDD_COLOR + CLR_PALEGRAY,    20,    38,    64,
  RADIOBUTTON  "Dark Gray"  IDD_COLOR + CLR_DARKGRAY,    94,    136    64,
  RADIOBUTTON  "Dark Blue"  IDD_COLOR + CLR_DARKBLUE,    94,    122    64,
  RADIOBUTTON  "Dark Green" IDD_COLOR + CLR_DARKGREEN,    94,    108,    64,
  RADIOBUTTON  "Dark Cyan"  IDD_COLOR + CLR_DARKCYAN,    94,    94,    64,
  RADIOBUTTON  "Dark Red"   IDD_COLOR + CLR_DARKRED,    94,    80,    64,
  RADIOBUTTON  "Dark Pink"  IDD_COLOR + CLR_DARKPINK,    94,    66,    64,
  RADIOBUTTON  "Brown"      IDD_COLOR + CLR_BROWN,    94,    52,    64,
  RADIOBUTTON  "White"      IDD_COLOR + CLR_WHITE,    94,    38,    64,
  DEFPUSHBUTTON "OK"        DID_OK,    16,    8,    52,    16,    GRP

PUSHBUTTON   "Cancel"     DID_CANCEL,    112,    8,    52,    16


   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"


MPARAM mp2) ;


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 ;

                               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,
                                         NULL, IDD_COLOR_DLG,
                                         &(COMMANDMSG (&msg)->cmd)))
                              return 0 ;

                         if (COMMANDMSG
                            (&msg)->cmd ==  IDM_COLOR_BLACK_PIECE ||
                            (&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) ;

                              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


                              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) ;


#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 ;

        CkdQuerySquareOrigin: Obtains lower left corner of square
static VOID CkdQuerySquareOrigin (SHORT x, SHORT y, POINTL *pptl)
       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)
  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 ;
  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

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

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

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

                              MPARAM mp2)
  static LONG     *pclr ;
  static SHORT    sColor ;
  CHAR            *pchHeading ;

  switch (msg)
    case WM_INITDLG:
       switch (* (PSHORT) PVOIDFROMMP (mp2))
          pchHeading = "Window Background Color" ;
          pclr = &clrBackground ;
          break ;

          pchHeading = "Black Square Color" ;
          pclr = &clrBlackSquare ;
          break ;

          pchHeading = "White Square Color" ;
          pclr = &clrWhiteSquare ;
          break ;

          pchHeading = "Black Piece Color" ;
          pclr = &clrBlackPiece ;
          break ;

          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,
            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)
  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

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)
  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

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

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.


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.

.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


hwndb1    DW  ?    ; set up local data for BasClientWndProc
hwndb2    DW  ?
msgb      DW  ?
mp1b1     DW  ?
mp1b2     DW  ?
mp2b1     DW  ?
mp2b2     DW  ?


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,

           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

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
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.
RegBas     ENDP

Figure 2

#define INCL_WIN
#include <os2.h>

extern MRESULT EXPENTRY ClientWndProc ( long near *, int near *,
                                        long near *, long near *);



   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



               HMODULE,    BYVAL HMODULE AS LONG,_

               LONG)    BYVAL aLONG AS LONG)

Figure 6


    DESCRIPTION    'PM Demo Program written in BASIC'
    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

     x AS LONG
     y AS LONG

     hwnd AS LONG
     msg  AS INTEGER
     mp1  AS LONG
     mp2  AS LONG
     time AS LONG
     ptl  AS LONG

'***** CONSTant definitions

CONST FCFTITLEBAR = &H00000001 : CONST FCFSYSMENU       = &H00000002
CONST FCFMENU     = &H00000004 : CONST FCFSIZEBORDER    = &H00000008
CONST HWNDDESKTOP = &H00000001 : CONST WSVISIBLE        = &H80000000

CONST WMSIZE      = &H0007 : CONST WMPAINT = &H0023_
                  : CONST WMCOMMAND = &H0020

'***** FUNCTION declarations
                                    BYVAL cmsg AS INTEGER)
            BYVAL pszCN AS LONG, BYVAL pfnWP AS LONG,_
            BYVAL flSty  AS LONG, BYVAL cbWD AS INTEGER)
                           BYVAL loword AS INTEGER)
            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)


            BYVAL msgFL AS INTEGER)
            BYVAL pqmsg AS LONG)
                      loword AS INTEGER)
                                    BYVAL pwrc AS LONG,_
                                    BYVAL fIC AS INTEGER)
            BYVAL hps AS LONG, BYVAL prcl AS LONG)


                             BYVAL msg AS INTEGER,_
                             BYVAL mp1 AS LONG,_
                             BYVAL mp2 AS LONG)
                                   BYVAL msg AS INTEGER,_
                                   BYVAL mp1 AS LONG,_
                                   BYVAL mp2 AS LONG)

'*********         Initialization section        ***********




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)))

'***********         Finalize section        ***************

bool% = WinDestroyWindow(hwndFrame&)
bool% = WinDestroyMsgQueue(hmq&)
bool% = WinTerminate(hab&)

'***********         Window procedure        ***************

FUNCTION ClientWndProc&(hwnd&, msg%, mp1&, mp2&) STATIC
  ClientWndProc& = 0

  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%)),_
    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&)


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>
 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

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 */


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 */

    printf("Type EXIT to continue");
    printf("Back in application - press Enter\n");

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 */
    while (*env)   /* display environment entries */

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 */

    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 */

    char static *new_env[4] = { "BOOK=C SSS",
    execle("SHOW.EXE", "SHOW", "A", "B", "C", NULL, new_env);

When you run this program, your screen shows


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 */

    static char *args[4] = { "SHOW",
                            "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 */

    static char *args[4] = { "SHOW",
    "B" };
    static char *new_env[4] = { "BOOK=C SSS",
    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

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

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 */

    static char *new_env[4] = { "BOOK=C SSS",

    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 */

    static char *new_env[4] = { "BOOK=C SSS",
    static char *args[4] = { "SHOW",
                             "B" };
    int result;

    result = spawnvpe(P_WAIT, "SHOW.EXE", args, new_env);
    printf("Back from spawn - Exit value is %d\n",

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

#include <signal.h>    /* needed for signal  */
#include <stdio.h>    /* needed for getchar */
#include <process.h>    /* needed for exit    */

    int ctrl_break();

    signal(SIGINT, ctrl_break);
    printf("Press Ctrl-Break 5 times to terminate
    static int count = 1;

    signal(SIGINT, SIG_IGN);
    if (count++ = = 5)
    printf("Program terminated by Ctrl-Break\n");
    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 */

    int abort_handler();

    signal(SIGABRT, abort_handler);
    signal(SIGABRT, SIG_IGN);
    printf("In abort handler -- closing all open

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

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

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

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

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

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

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

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

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

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

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

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

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

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.


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.


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

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

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:





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

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 ================================

#   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
    $(CC) $*.c

    $(ASM) $*.asm;

    $(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 ******************\
*       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;

#define VS_GETSCROLL                  6

#define MSG_APPL_ARROWCHAR          132


#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 usColumn, HVIO hvio);


/nod:slibce slibcep doscalls


MKB0000I: Command Line Arguments to  %1: %2 %3 %4


/********************************* 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

#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

 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

    (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];

   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}

 **   Collect the frame characters from the message and place them
 **   into a skeleton 3x3 array for the box.

VOID ParseFrameMsg( pchMsg, cbMsg )
PCH pchMsg;
    sscanf( (char *)pchMsg, "%x %x %x %x %x %x %x %x",
    ausBoxChar[1][1] = 0x20;

 **  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
       yTop,                        // not inclusive

    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;
 }  while ((i1 = = 1) && (yTop < yBottom));

} /* end MakeBox */

 ** 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
       yTop,                 // not inclusive
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

    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;
                 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,
                 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,
                 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


CC = cl -c -AS -G2sw -Od -W3 -Zpei
LF = /align:16 /codeview /map /NOD

    $(CC) $*.c

    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


#include <os2.h>
#include "typer.h"

    IDS_NAME,    "Typer"
    IDS_TITLE,   " - Simple Typewriter"

    SUBMENU  "~File", IDM_FILE {

    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



DESCRIPTION 'Microsoft OS/2 PM Simple Virtual Typewriter'



HEAPSIZE    8192


os2 slibcp/NOD


//  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



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
         // 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);

vhwndTyperFrame = WinCreateStdWindow(HWND_DESKTOP, WS_VISIBLE,

&ctlData, (PCH)szClassName,
                                         (PCH)szWindowTitle, 0L,
                                         NULL, IDR_TYPER,

    // Initialize the lead byte table

    // Poll messages from event queue
    while (WinGetMsg(vhab, &qmsg, NULL, 0, 0))
        WinDispatchMsg(vhab, &qmsg);

    // Standard cleanup code for PM applications
    DosExit(EXIT_PROCESS, 1);


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:

 case WM_CLOSE:
     WinPostMsg(hwnd, WM_QUIT, 0L, 0L);


     // Erase the background if this message is received
     return TRUE;

 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);

     WinQueryWindowRect(hwnd, &rclWindow);

     hps = WinGetPS(hwnd);
     WinDrawText(hps, 0xFFFF, vaszBuffer[vusCurrent],
   &rclWindow, 0L, 0L, DT_LEFT | DT_BOTTOM | DT_QUERYEXTENT);

     ((PRECTL) mp1)->xLeft = rclWindow.xRight;
     ((PRECTL) mp1)->yBottom = rclWindow.yBottom;

     return QCP_CONVERT;

 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);


     switch (COMMANDMSG(&usMsg)->cmd) {

   // Trap the About... menu item, and put up the dialog box
   case IDM_ABOUT:
       WinDlgBox(HWND_DESKTOP, hwnd, AboutDlgProc,

   default: break;

     return WinDefWindowProc(hwnd, usMsg, mp1, mp2);
    return 0L;


MPARAM mp2) {
    About... dialog procedure
    switch(usMsg) {
     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,

     // 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.

// 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 = = ' ')

       // Otherwise, complain audibly.

   } 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)

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;

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,
     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,

       // We increment the update pointer because there's one
       // fewer line to update.
 } 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

 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

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

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

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

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:

<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:


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

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.


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

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

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

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.


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:

      GetClassWord( GetClipboardOwner(), GCW_HMODULE ),

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

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;

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.


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


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

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

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_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









Display Text    CF_DSPTEXT

Display Bitmap    CF_DSPBITMAP    BITMAP.DLL


Figure 6


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,\
   rc viewer.res


NAME             VIEWER

DESCRIPTION      'Extensible Clipboard Viewer'

STUB             'WINSTUB.EXE'



HEAPSIZE         2048
STACKSIZE        2048

  AboutDlgFn        @1
  StatusWndFn       @2
  ClientWndFn       @3
  ViewerWndFn       @4
  AddFormatDlgFn    @5
  RemFormatDlgFn    @6


 * 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_SIZE          MAKEINTRESOURCE(3)

/* 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",\

/* library module data structure */
#define MAX_MODULE   16

typedef struct {
  WORD        hModule;
  WORD        wFormat;

typedef struct {
  WORD        wModules;
  MODULE      Module[MAX_MODULE];


/* viewer function definitions (viewer1.c) */

/* client and status function definitions (viewer2.c) */

/* dialog function definitions (viewer3.c) */

/* utility function definitions (viewer4.c) */
BOOL FAR PASCAL     CenterPopup( HWND, HWND );
WORD FAR PASCAL     GetClipboardFmtNumber( LPSTR );


/* undocumented internal function definitions */
int FAR PASCAL      lstrlen( LPSTR );
int FAR PASCAL      lstrcmp( LPSTR, LPSTR );


 * 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
  POPUP          "&Options"
     MENUITEM    "&Status",              IDM_STATUS
     MENUITEM    "&Add Format...",       IDM_ADDFMT
     MENUITEM    "&Remove Format...",    IDM_REMFMT
     MENUITEM    "E&xit",                IDM_EXIT
     MENUITEM    "A&bout...",            IDM_ABOUT
  POPUP          "&Display"
     MENUITEM    "&Erase...",            IDM_ERASE,GRAYED
     MENUITEM    "&1. (Empty)",          IDM_FORMATS,GRAYED


CAPTION "About Clipboard Viewer..."




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


CAPTION "Add Format to Viewer..."
             WS_BORDER | WS_TABSTOP | WS_CHILD, 38, 4, 90, 12
             WS_BORDER | WS_TABSTOP | WS_CHILD, 38, 18, 90, 12


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


CAPTION "Remove Format from Viewer..."


             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

Figure 7

 * 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 */

 * 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.

   HANDLE      hCrntInst,
   HANDLE      hPrevInst,
   LPSTR       lpszCmdLine,
   WORD        wInitShowCmd )
   /* local variables */
 MSG        msgViewer;
 WORD       wWndWidth;
 WORD       wWndHeight;
 HWND       hWndViewer;
 WNDCLASS   classViewer;

 /* warning level 3 compatibility */

 /* 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(
            "Clipboard Viewer",

      /* 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 =
        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) &&
            ) {

           /* create child windows */
             hWndSizebox = CreateWindow(

             hWndClient = CreateWindow(

           hWndStatus = CreateWindow(

           /* 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 );

 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);

 case WM_INITMENU : /* initialize menu */
     LPLIBRARY     lpLibrary;

     /* check status window visibility */
       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 );

 case WM_COMMAND : /* menu command */

   /* process sub-message */
   switch( wParam )
   case IDM_STATUS : /* toggle status bar visibility */
       RECT    rcClient;

       /* change status bar visibility */
            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) );

   case IDM_EXIT : /* exit application */
       PostQuitMessage( 0 );
   case IDM_ABOUT : /* about viewer */
     Dialog( hWnd, "ViewerAbout", AboutDlgFn );
   case IDM_ADDFMT : /* add a new clipboard format */
     Dialog( hWnd, "ViewerAdd", AddFormatDlgFn );
   case IDM_REMFMT : /* remove a clipboard format */
     Dialog( hWnd, "ViewerRemove", RemFormatDlgFn );
   case IDM_ERASE : /* erase clipboard contents */

     /* verify option */
     if ( MessageBox(hWnd,"Erase clipboard contents?",
                     "Clipboard Viewer",
                     MB_ICONQUESTION|MB_YESNO) = = IDYES ) {
       if ( OpenClipboard(hWnd) ) {
       } else
         WARNING( hWnd, "Unable to open clipboard!" );

   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 */

       /* 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!" );


 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;
            (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++].wFormat =

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!" );
 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!" );

 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-1].hModule =

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!" );

 case WM_SIZE : /* window being resized */

   /* reposition status window (even if invisible) */

   /* reposition client window (always visible) */
              (IsWindowVisible(GetProp(hWnd,HWNDSTATUS))) ?
                GetSystemMetrics(SM_CYMENU) : -1,
              (IsWindowVisible(GetProp(hWnd,HWNDSTATUS))) ?
              HIWORD(lParam)-GetSystemMetrics(SM_CYMENU)+1 :

   /* reposition sizebox window (always visible) */

 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-- )
             (wCrntEntry>2) ? NULL : "&1. (Empty)",
             (wCrntEntry>2) ? NULL : IDM_FORMATS,
             (wCrntEntry>2) ? MF_DELETE|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 */
             (wCrntEntry) ? NULL : IDM_FORMATS,
             IDM_FORMATS + wCrntFmt,
             (wCrntEntry) ? MF_APPEND : MF_CHANGE

           /* define selected format */
           if ( hCrntModule = = NULL ) {
             SetProp( hWnd, CBFORMAT, IDM_FORMATS+wCrntFmt );

           /* retrieve next available format */
           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 */

         /* 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!" );


 case WM_CHANGECBCHAIN : /* clipboard viewer chain being changed */

   /* re-link viewer chain */
   if ( wParam = = GetProp(hWnd,CBCHAIN) )
     SetProp( hWnd, CBCHAIN, LOWORD(lParam) );
     if ( GetProp(hWnd,CBCHAIN) )
       SendMessage( GetProp(hWnd,CBCHAIN), wMsg, wParam, lParam );

   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 );

   default : /* send to default */
      lResult = DefWindowProc( hWnd, wMsg, wParam, lParam );

   /* return normal result */
   return( lResult );


Figure 8

 * 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.

 HWND        hDlg,
 WORD        wMsg,
 WORD        wParam,
 LONG        lParam )
 BOOL      bResult;

 /* initialization */
 bResult = TRUE;

 /* process message */
 switch( wMsg )
   CenterPopup( hDlg, GetParent(hDlg) );
   EnableWindow( GetDlgItem(hDlg,IDADD), FALSE );
 case WM_COMMAND :

   /* process sub-message */
   switch( wParam )
   case IDCANCEL :
     EndDialog( hDlg, FALSE );
   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,
                    (LONG)(LPSTR)szLib );

   case IDFORMAT :
   case IDLIBRARY :

     /* enable or disable add button */
     if ( HIWORD(lParam) = = EN_CHANGE )
             ( SendMessage(GetDlgItem(hDlg,IDFORMAT),
                           WM_GETTEXTLENGTH,0,0L)      &&
             ) ? TRUE : FALSE

   default :
     bResult = FALSE;

 default :
   bResult = FALSE;

 /* 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.

 HWND        hDlg,
 WORD        wMsg,
 WORD        wParam,
 LONG        lParam )
 BOOL      bResult;

 /* initialization */
 bResult = TRUE;

 /* process message */
 switch( wMsg )
     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 */


InvalidateRect( hWndLibList, (LPRECT)NULL, TRUE );


 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 );

   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 );


   case IDCANCEL :
     EndDialog( hDlg, FALSE );
   default :
     bResult = FALSE;

 default :
   bResult = FALSE;

 /* 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.

 HWND      hDlg,
 WORD      wMsg,
 WORD      wParam,
 LONG      lParam )
 BOOL      bResult;

 /* warning level 3 compatibility */

 /* initialization */
 bResult = TRUE;

 /* process message */
 switch( wMsg )
   CenterPopup( hDlg, GetParent(hDlg) );
 case WM_COMMAND :

   /* process sub-message */
   switch( wParam )
   case IDOK :
     EndDialog( hDlg, TRUE );
   case IDCANCEL :
     EndDialog( hDlg, FALSE );
   default :
     bResult = FALSE;

 default :
   bResult = FALSE;

 /* return final result */
 return( bResult );


Figure 9



 * 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 */

/* status window properties */

 * 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) );

 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 );

 case WM_PAINT : /* paint window */
     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 */

     /* define clipboard format owner */
     if ( wCrntFmt && GetClipboardOwner() ) {
         GetClassWord( GetClipboardOwner(), GCW_HMODULE ),
       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" );
     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) ) {
             " - %u x %u%s",
             ((bmStruct.bmPlanes= =1)&&(bmStruct.bmBitsPixel= =1)) ?
              " - mono" : ""
           strcat( szFmtName, szFmtSize );
             "%ld bytes",
             sizeof(BITMAP) + ((LONG)bmStruct.bmWidthBytes *
                    bmStruct.bmPlanes * bmStruct.bmHeight)
         } else
           strcpy( szFmtSize, "(size unknown)" );

     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 );
              case MM_LOMETRIC :
                sprintf( szFmtSize, " - %u x %u - low metric",
                         lpmfStruct->xExt, lpmfStruct->yExt );
              case MM_HIMETRIC :
                sprintf( szFmtSize, " - %u x %u - high metric",
                         lpmfStruct->xExt, lpmfStruct->yExt );
              case MM_LOENGLISH :
                sprintf( szFmtSize, " - %u x %u - low english",
                         lpmfStruct->xExt, lpmfStruct->yExt );
              case MM_HIENGLISH :
                sprintf( szFmtSize, " - %u x %u - high english",
                         lpmfStruct->xExt, lpmfStruct->yExt );
              case MM_TWIPS :
                sprintf( szFmtSize, " - %u x %u - twips",
                         lpmfStruct->xExt, lpmfStruct->yExt );
              case MM_ISOTROPIC :
                sprintf( szFmtSize, " - isotropic" );
              case MM_ANISOTROPIC :
                sprintf( szFmtSize, " - anisotropic" );
              default :
                strcpy( szMapMode, "" );
           strcat( szFmtName, szFmtSize );
             "%ld bytes",
           UnlockData( hCrntData );
         } else
           strcpy( szFmtSize, "(size unknown)" );

     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) );

     /* 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,
     DrawText( Ps.hdc, szFmtSize, -1, &rcClient,

     /* end painting */
     SelectObject( Ps.hdc, hOldFont );
     EndPaint( hWnd, &Ps );

 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 );

 default : /* send to default */
      lResult = DefWindowProc( hWnd, wMsg, wParam, lParam );

   /* 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 );

 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 );

 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;
     case WM_HSCROLL :
       lpszLibrary = LIB_HSCROLL;
     case WM_VSCROLL :
       lpszLibrary = LIB_VSCROLL;
     case WM_PAINT :
       lpszLibrary = LIB_PAINT;

     /* 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 );

 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 );

 default : /* send to default */
      lResult = DefWindowProc( hWnd, wMsg, wParam, lParam );

   /* return normal result */
   return( lResult );


Figure 10

 * 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.

   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 */
   ( xPopup > 0 ) ? xPopup : 0,
   ( yPopup > 0 ) ? yPopup : 0,

 /* 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)" );
 case CF_TEXT : /* standard text */
   lstrcpy( lpszFmt, "Text" );
 case CF_BITMAP : /* standard GDI bitmap */
   lstrcpy( lpszFmt, "Bitmap" );
 case CF_METAFILEPICT : /* standard GDI metafile */
   lstrcpy( lpszFmt, "Picture" );
 case CF_SYLK : /* standard SYLK text */
   lstrcpy( lpszFmt, "SYLK" );
 case CF_DIF : /* standard DIF text */
   lstrcpy( lpszFmt, "DIF" );
 case CF_TIFF : /* standard binary TIFF data */
   lstrcpy( lpszFmt, "TIFF" );
 case CF_OEMTEXT : /* standard OEM text */
   lstrcpy( lpszFmt, "OEM Text" );
 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 );
 case CF_DSPTEXT : /* display text */
   lstrcpy( lpszFmt, "Display Text" );
 case CF_DSPBITMAP : /* display bitmap */
   lstrcpy( lpszFmt, "Display Bitmap" );
 case CF_DSPMETAFILEPICT : /* display picture */
   lstrcpy( lpszFmt, "Display Picture" );
 default : /* something else */
   if ( GetClipboardFormatName(wFmt,lpszFmt,wMax) = = 0 )
     lstrcpy( lpszFmt, "(Undefined)" );

 /* 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;
   if ( lstrcmp(lpszFmt,"Bitmap") = = 0 )
     wFmt = CF_BITMAP;
     if ( lstrcmp(lpszFmt,"Picture") = = 0 )
       wFmt = CF_METAFILEPICT;
       if ( lstrcmp(lpszFmt,"SYLK") = = 0 )
         wFmt = CF_SYLK;
         if ( lstrcmp(lpszFmt,"DIF") = = 0 )
           wFmt = CF_DIF;
           if ( lstrcmp(lpszFmt,"TIFF") = = 0 )
             wFmt = CF_TIFF;
             if ( lstrcmp(lpszFmt,"OEM Text") = = 0 )
               wFmt = CF_OEMTEXT;
               if ( lstrcmp(lpszFmt,"Owner Display") = = 0 )
                 wFmt = CF_OWNERDISPLAY;
                 if ( lstrcmp(lpszFmt,"Display Text") = = 0 )
                   wFmt = CF_DSPTEXT;
                   if ( lstrcmp(lpszFmt,"Display Bitmap") = = 0 )
                     wFmt = CF_DSPBITMAP;
                     if ( lstrcmp(lpszFmt,"Display Picture") = = 0 )
                       wFmt = CF_DSPMETAFILEPICT;
                       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


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


DESCRIPTION  'Bitmap Format Display Library '


 BitmapInit      @1
 BitmapCreate    @2
 BitmapSize      @3
 BitmapHScroll   @4
 BitmapVScroll   @5
 BitmapPaint     @6
 BitmapDestroy   @7


; LANGUAGE      : Microsoft Macro Assembler 5.1
; MODEL         : small
; ENVIRONMENT   : Microsoft Windows 2.1 SDK
; STATUS        : operational
         Extrn    BitmapInit:Near

         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


LibInit  ENDP

End      LibInit


 * 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;






 * 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 */

 /* 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.

 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.

 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 */

 /* 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.

 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;
   case SB_LINEDOWN : /* move right one line */
     wNewScrollPos = (wOldScrollPos+HORZ_STEPSIZE<=wOldScrollRange) ?
                      wOldScrollPos+HORZ_STEPSIZE : wOldScrollRange;
   case SB_PAGEUP :  /* move left one page */
     wNewScrollPos = (wOldScrollPos > wWndWidth) ?
                      wOldScrollPos-wWndWidth : 0;
   case SB_PAGEDOWN : /* move right one page */
     wNewScrollPos = (wOldScrollPos+wWndWidth <= wOldScrollRange) ?
                      wOldScrollPos+wWndWidth : wOldScrollRange;
   case SB_THUMBPOSITION : /* move to an absolute position */
   case SB_THUMBTRACK : /* track the current thumb position */
     wNewScrollPos = (wOldScrollRange > 1) ? LOWORD(lParam) : 0;
   case SB_TOP : /* move to the first line */
     wNewScrollPos = 0;
   case SB_BOTTOM : /* move to the last line */
     wNewScrollPos = wOldScrollRange;
   case SB_ENDSCROLL : /* end scrolling */
     wNewScrollPos = wOldScrollPos;

   /* perform scroll and update (if necessary) */
   if ( wNewScrollPos != wOldScrollPos ) {
     /* update window */
            lpDispInfo->rcWnd.bottom - lpDispInfo->rcWnd.top,
            lpDispInfo->ptOrg.y + VERT_BORDER,

     /* 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.

 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;
   case SB_LINEDOWN : /* move down one line */
     wNewScrollPos=(wOldScrollPos+VERT_STEPSIZE<=wOldScrollRange) ?
                    wOldScrollPos+VERT_STEPSIZE : wOldScrollRange;
   case SB_PAGEUP :  /* move up one page */
     wNewScrollPos=(wOldScrollPos > wWndHeight) ?
                    wOldScrollPos-wWndHeight : 0;
   case SB_PAGEDOWN : /* move down one page */
     wNewScrollPos=(wOldScrollPos+wWndHeight<=wOldScrollRange) ?
                    wOldScrollPos+wWndHeight : wOldScrollRange;
   case SB_THUMBPOSITION : /* move to an absolute position */
   case SB_THUMBTRACK : /* track the current thumb position */
     wNewScrollPos = (wOldScrollRange > 1) ? LOWORD(lParam) : 0;
   case SB_TOP : /* move to the first line */
     wNewScrollPos = 0;
   case SB_BOTTOM : /* move to the last line */
     wNewScrollPos = wOldScrollRange;
   case SB_ENDSCROLL : /* end scrolling */
     wNewScrollPos = wOldScrollPos;

   /* perform scroll and update (if necessary) */
   if ( wNewScrollPos != wOldScrollPos ) {

     /* update window */
            lpDispInfo->rcWnd.right - lpDispInfo->rcWnd.left,
            lpDispInfo->ptOrg.x + HORZ_BORDER,

     /* 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.

 HWND        hWnd,
 HANDLE      hDispInfo,
 WORD        wParam,
 LONG        lParam )
 HDC         hMemDC;
 HANDLE      hOldData;
 LPDISPINFO  lpDispInfo;

 /* warning level 3 compatibility */

 /* 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 );

   /* perform BitBlt operation */
   hMemDC = CreateCompatibleDC( Ps.hdc );
   if ( hMemDC ) {
     hOldData = SelectObject( hMemDC, lpDispInfo->hData );
            Ps.rcPaint.right - Ps.rcPaint.left,
            Ps.rcPaint.bottom - Ps.rcPaint.top,
            Ps.rcPaint.left + lpDispInfo->ptOrg.x,
            Ps.rcPaint.top + lpDispInfo->ptOrg.y,
     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.

 HWND      hWnd,
 HANDLE    hDispInfo )

 /* warning level 3 compatibility */

 /* 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


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

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

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

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

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

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.


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

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

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

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

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

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

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.


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

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:


The paths are optional. You can use paths in dependency lists also. For

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

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

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

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.


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")

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


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

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.


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

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

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).


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

    $(cp) $*.c

    masm $*.asm;

    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
    link @tracer.lnk
    rc tracer.res


//-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.
//----You may need to include EXTERNTRACERVARS--
//----in secondary modules if you want to call--
//----TRACER from those modules.----------------

//----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 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_SCREENSCROLL    13
#define ID_REFRESH         14

//#include <tracer.h>
#define TRACER_GOODBYE          WM_USER + 504
#define TRACER_MYCLOSE          WM_USER + 505

//                       DECLARATION SECTION
#define TRACERVARIABLES                                            \
unsigned short      sTracerSelector;                               \
         HSYSSEM    hTracerSysSem;                                 \
         BOOL       bTracerConnected;                              \

char far * szSelector_string;                             \
         HWND       hTracerWnd;

//                      WM_CREATE MESSAGE
#define TRACERHELLO                                                \
WinBroadcastMsg( hWnd, TRACER_REQUEST_HANDLE,                      \
                 MPFROMHWND( hWnd ), 0L,                           \

//                        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;

#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;



DESCRIPTION 'OS/2 PM Tracer Utility'




    TracerWndProc    @1
    HelpHook         @2
    TracerAboutDlg   @3
    TracerLogFileDlg @4


tracer.obj +
tracerp.obj +
os2 llibcmt doscalls/NOD


#include <os2.h>
#include "tracer.h"


   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
    "^A", IDMABOUT
    "^L", IDMLOG
    "^C", IDMCLEAR
    DIALOG "", 5, 10, 23, 190, 52, FS_NOBYTEALIGN | FS_DLGBORDER |
        CONTROL "OS/2 Debugging Utility", -1, 17, 35, 156, 14,
                WC_STATIC, SS_TEXT | DT_CENTER | DT_TOP | WS_GROUP |
        CONTROL "Version 1.00", -1, 57, 17, 74, 8, WC_STATIC,
        CONTROL "OK", 6, 5, 2, 38, 12, WC_BUTTON, BS_PUSHBUTTON |
        CONTROL "Daniel Hildebrand", 256, 55, 29, 82, 8, WC_STATIC,
                SS_TEXT | DT_LEFT | DT_TOP | WS_GROUP | WS_VISIBLE
    DIALOG "", 9, 10, 17, 209, 74, FS_NOBYTEALIGN | FS_DLGBORDER |
        CONTROL "Ok", 6, 7, 4, 38, 12, WC_BUTTON, BS_PUSHBUTTON |
        CONTROL "Cancel", 10, 51, 4, 38, 12, WC_BUTTON,
        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,
        CONTROL "Screen Scroll On/Off", 13, 106, 24, 103, 9,
        CONTROL "Log File On/Off", 12, 10, 24, 91, 9, WC_BUTTON,
        CONTROL "Help", 2, 97, 4, 38, 12, WC_BUTTON, BS_PUSHBUTTON |
        CONTROL "Truncate Log File before proceeding", 14, 10, 41,
                171, 10,


//-TRACER ( OS/2 PM Debugging Trace Facility )--
//-(c) 1990 Daniel Hildebrand-------------------

#define INCL_PM
#define INCL_WIN
#define INCL_DOS

#include <os2.h>
#include <process.h>
#include <stdlib.h>
#include <string.h>
#include "tracer.h"

//-Module declarations--------------------------
int               main          ( void );
                                  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--------------------------
void FAR PASCAL   TracerPaint      ( HWND hWnd, USHORT msg,
                                     MPARAM mp1, MPARAM mp2 );
void FAR PASCAL   ClearLB          ( HWND hLB );
VOID              LogToFile        ( PSZ selector_string );
MRESULT EXPENTRY  TracerAboutDlg   ( HWND hWndDlg,
                                     USHORT message,
                                     MPARAM mp1, MPARAM mp2 );
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       */


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,
                            0 ) )
        return( 0 );

    // file control flags
    ctldata = FCF_TITLEBAR      | FCF_SYSMENU  | FCF_BORDER |
              FCF_MINBUTTON     | FCF_MENU     | FCF_ICON   |

    // create standard window
    hParentFrm = WinCreateStdWindow( HWND_DESKTOP,
                                     (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,
                          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 );


                                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 ) )
                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 );

        // allocate an 81 byte data segment
        if ( DosAllocShrSeg( 81, TRACER_SEGMENT,
                                  &selector ) )
            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 );

        // 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 ) );


    case WM_COMMAND:

            // handle menu commands
            TracerCommand( hWnd, SHORT1FROMMP(mp1),
                           SHORT1FROMMP(mp2), SHORT2FROMMP(mp2));


         // a client has broadcast a request for handshake
                     MPFROMHWND(hWnd), 0L );



         // 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 );


    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 !
            WinMessageBox( HWND_DESKTOP, hWnd,
            "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 );
            DosCloseSem( hSysSem );  /* one for create */

        // continue close down
        WinPostMsg( hWnd, TRACER_MYCLOSE, 0L, 0L );



        // 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 );


    case WM_PAINT:

        // paint routine for TRACER window
        TracerPaint( hWnd, msg, mp1, mp2 );



        return( TRUE );



        return( WinDefWindowProc( hWnd, msg, mp1, mp2 ) );




     USHORT idTopic, USHORT idSubTopic, PRECTL prcPosition )

WinMessageBox( HWND_DESKTOP, hParentWnd,
"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:
      case HLPM_FRAME:

      // idtopic is frame identifier
      // idsubtopic is focus window identifier
      // prcposition is boundary of focus window

           switch ( idTopic )
             case ID_HELPBUTTON:

      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:

return TRUE;


VOID TracerCommand( HWND hWnd, SHORT id, SHORT source, BOOL mouse )

    switch( id )
        case IDMABOUT:

            WinDlgBox( HWND_DESKTOP, hWnd, (PFNWP)TracerAboutDlg,
                       NULL, IDMABOUT, NULL );

        case IDMLOG:

            WinDlgBox( HWND_DESKTOP, hWnd, (PFNWP)TracerLogFileDlg,
                       NULL, IDMLOG, NULL );

        case IDMCLEAR:

            ClearLB( hLB );



static VOID       HelpMessage   ( void )

 WinMessageBox( HWND_DESKTOP, hParentWnd,
 "TRACER.H has instructions on how to use TRACER in your app.",
                (PCH)"OS/2 TRACER HelpHook", NULL,
                MB_OK| MB_CUANOTIFICATION );


static VOID ClearSelector  ( PSZ selector_string )
int aiX;

    for ( aiX = 0; aiX < 80; aiX++ )
         selector_string[aiX] = ' ';


//-TRACERD.C Source code for TRACER file routines

#define INCL_PM
#define INCL_WIN
#define INCL_DOS

#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;


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 );


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 );

            return( FALSE );

        return( WinDefDlgProc( hWndDlg, message, mp1, mp2 ) );
    return( FALSE );


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 );

          case ID_OK:

            // if user wishes to truncate log file, do it
            if (
            bUserRequestsNewLog =
            (SHORT)WinSendDlgItemMsg( hWndDlg,
                 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 );


            return( FALSE );

        return( WinDefDlgProc( hWndDlg, message, mp1, mp2 ) );
    return( FALSE );


//-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;


                             MPARAM mp1, MPARAM mp2 )

HPS         hPS;
RECTL       rRect;

    // create listbox on first paint only
    if ( ! iFirstTimeOnly )
        hLB = WinCreateWindow( hParentWnd,
                               WS_VISIBLE | WS_SYNCPAINT,
                               4, 4, 5, 5,
                               0 );

        WinQueryWindowRect( hWnd, &rRect );
        WinSetWindowPos ( hLB, HWND_TOP, 6,
                          (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 );


        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

    $(cp) $*.c

    masm $*.asm;

    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



#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


int cdecl main( )
    QMSG      qmsg;
    ULONG     ctldata;

    hAB = WinInitialize( NULL );

    hmqClient = WinCreateMsgQueue( hAB, 0 );

    if ( !WinRegisterClass( hAB,
                           0) )
        return( 0 );

    ctldata = FCF_TITLEBAR      | FCF_SYSMENU       |
              FCF_SIZEBORDER    | FCF_MINMAX        |
              FCF_ICON          | FCF_SHELLPOSITION |
              FCF_TASKLIST ;

    hPanelFrm = WinCreateStdWindow( HWND_DESKTOP,
                                     (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 );


                                MPARAM mp1, MPARAM mp2 )

    switch (msg)
    case WM_CREATE:

         // TRACER step 3 - attach to TRACER

         return( WinDefWindowProc( hWnd, msg, mp1, mp2 ) );

    // TRACER step 4 - obtain system resources - Don't forget break;


    case WM_CLOSE:

         // TRACER step 5
         TRACER("Posting QUIT");
         WinPostMsg( hWnd, WM_QUIT, 0L, 0L );


    case WM_SETFOCUS:

         // TRACER step 5
         TRACER("Setting Focus");
         return( WinDefWindowProc( hWnd, msg, mp1, mp2 ) );


    case WM_ENABLE:

         return( WinDefWindowProc( hWnd, msg, mp1, mp2 ) );


    case WM_BUTTON1DOWN:

         sprintf( szTraceBuf, "WM_BUTTON1DOWN hit at %ld",
                  WinGetCurrentTime( hAB ) );

         TRACER( szTraceBuf );


    case WM_PAINT:

         ClientPaint( hWnd, msg, mp1, mp2 );



         return( TRUE );


         return( WinDefWindowProc( hWnd, msg, mp1, mp2 ) );



void FAR PASCAL ClientPaint( HWND hWnd, USHORT msg,
                             MPARAM mp1, MPARAM mp2 )

int         aiCharWidth;
HPS         hPS;
RECTL       rRect;
POINTL      pt;

    hPS = WinBeginPaint( hWnd, (HPS)NULL, (PWRECT)NULL );

    // TRACER step 5

    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 );







    ClientWndProc @1


#define CLIENTICON 1

extern   void FAR PASCAL ClientPaint( HWND hWnd, USHORT msg,
                                      MPARAM mp1, MPARAM mp2 );

int cdecl main( void );

char                szMessage[] = " Client App ";
char                szPClass[] = "PClass";
HAB                 hAB;
HMQ                 hmqClient;
HWND                hPanelWnd;
HWND                hPanelFrm;
char                szTraceBuf[81];


os2 llibcmt doscalls/NOD


#include "client.h"


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

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

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

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

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

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

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 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 resource script, Version 0.30

#include <os2.h>
#include "checkers.h"


     SUBMENU "~Game",                   -1
          MENUITEM "~New Game",              IDM_NEWGAME

MENUITEM "~Black on Bottom",       IDM_BOTTOM,, MIA_CHECKED

          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 "~Standard colors",       IDM_COLOR_STANDARD

#define GRP WS_GROUP

     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

 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,
  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,
  RADIOBUTTON  "Cyan"       IDD_COLOR + CLR_CYAN,    20,    66,    64,    12
  RADIOBUTTON  "Yellow"     IDD_COLOR + CLR_YELLOW,    20,    52,    64,
  RADIOBUTTON  "Pale Gray"  IDD_COLOR + CLR_PALEGRAY,    94,    38,    64,
  RADIOBUTTON  "Dark Gray"  IDD_COLOR + CLR_DARKGRAY,    94,    136,    64,
  RADIOBUTTON  "Dark Blue"  IDD_COLOR + CLR_DARKBLUE,    94,    122,    64,
  RADIOBUTTON  "Dark Red"   IDD_COLOR + CLR_DARKRED,    94,    108,    64,
  RADIOBUTTON  "Dark Pink"  IDD_COLOR + CLR_DARKPINK,    94,    94,    64,
  RADIOBUTTON  "Dark Green" IDD_COLOR + CLR_DARKGREEN,    94,    80,    64,
  RADIOBUTTON  "Dark Cyan"  IDD_COLOR + CLR_DARKCYAN,    94,    66,    64,
  RADIOBUTTON  "Brown"      IDD_COLOR + CLR_BROWN,    94,    52,    64,
  RADIOBUTTON  "White"      IDD_COLOR + CLR_WHITE,    20,    38,    64,
  DEFPUSHBUTTON "OK"        DID_OK,    16,    8,    52,    16, GRP

PUSHBUTTON   "Cancel"     DID_CANCEL,    112,    8,    52,    16


; CHECKERS.DEF module definition file, Version 0.30


DESCRIPTION    'Checkers Version 0.30 (c) 1990, Charles Petzold'
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        ClientWndProc


checkers ckrjudge ckrboard ckrmoves ckrdraw

 CHECKERS.LNK link script, Version 0.30


   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 ;


          // 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_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_NEW_GAME          (WM_USER + 2)

               // Messages from ClientWndProc to BoardWndProc


               // 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)

               // Messages from Board to Judge

#define WM_QUERY_JUDGE_IF_KING          (WM_USER + 32)


   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 header file, Version 0.30


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 -- OS/2 Presentation Manager Checkers Program, Version 0.30
                  (c) 1990, Charles Petzold

#define INCL_WIN
#include <os2.h>
#include "checkers.h"


MPARAM mp2);


MPARAM mp2);


MPARAM mp2);


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 ;


      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,
                                      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,
                                      MPFROMP (&COMMANDMSG (&msg)->cmd),
                                      MPFROMSHORT (FALSE)) ;
                          return 0 ;

                     case IDM_COLOR_BLACK_PIECE:
                     case IDM_COLOR_WHITE_PIECE:
                          WinSendMsg (hwndBoard,
                                      MPFROMP (&COMMANDMSG (&msg)->cmd),
                                      MPFROMSHORT (TRUE)) ;
                          return 0 ;
                          return 0 ;

                     case IDM_COLOR_STANDARD:
                          WinSendMsg (hwndBoard,
                                      NULL, NULL) ;
                          return 0 ;
                break ;
      return WinDefWindowProc (hwnd, msg, mp1, 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"


     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)
               hwndBoard = HWNDFROMMP (mp1) ;
               return 0 ;

          case WM_TELL_JUDGE_NEW_GAME:
               brd.ulBlack = 0x00000FFF ;
               brd.ulWhite = 0xFFF00000 ;
               brd.ulKing  = 0x00000000 ;

               sColor = BLACK ;




               return 0 ;

               * (PBOARD) PVOIDFROMMP(mp1) = brd ;
               return 0 ;

               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 ;
                    brd.ulWhite &= ~ulBit ;

               brd.ulKing &= ~ulBit ;

               return TRUE ;

          case WM_QUERY_JUDGE_IF_KING:
               return fKing ;

               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) ;
                    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)


               return TRUE ;

               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 ;
                         brd.ulWhite &= ~ulBit ;

                    brd.ulKing &= ~ulBit ;

                    return TRUE ;

               return FALSE ;

               if (sColor = = BLACK)
                    sColor = WHITE ;
                    WinSendMsg (hwndBoard, WM_JUDGE_SAYS_MOVE_WHITE,
                                NULL, NULL) ;
                    sColor = BLACK ;

WinSendMsg (hwndBoard, WM_JUDGE_SAYS_MOVE_BLACK,

               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"


     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,
                                                FALSE) ;
               return 0 ;

          case WM_SIZE:
               CkdResizePS (hps, hwnd) ;
               CkdDestroyPieces () ;
               CkdCreatePieces (hps) ;
               return 0 ;

               hwndJudge = HWNDFROMMP (mp1) ;
               return 0 ;

               sBottom = SHORT1FROMMP (mp1) ;
               WinInvalidateRect (hwnd, NULL, FALSE) ;
               return 0 ;

               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 ;

               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,

                         WinAlarm (HWND_DESKTOP, WA_ERROR) ;
                         return 0 ;

                    sKing = (SHORT) WinSendMsg (hwndJudge,
                                             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,
                         WinAlarm (HWND_DESKTOP, WA_ERROR) ;
                         return 0 ;
                              // restore area

                    CkdDragRestore (hps, &ptlMouse, sKing) ;

                    sKing = (SHORT) WinSendMsg (hwndJudge,

                              // 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,

                         CkdErasePiece (hps, x, y) ;
                         CkdDragSave (hps, &ptlLast, sKing) ;
                    else           // the move is over
                         fMovingPiece = FALSE ;
                         sColor       = -1 ;
                         WinSetPointer (HWND_DESKTOP, hptrArrow) ;
                         WinSendMsg (hwndJudge,
    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) ;

                    WinSetPointer (HWND_DESKTOP,
                         sBottom ^ sColor ? hptrDnHand : hptrUpHand) ;

               return 0 ;

          case WM_SETFOCUS:   // set the mouse pointer


WinShowPointer (HWND_DESKTOP,
                                    SHORT1FROMMP(mp2) ? TRUE : FALSE);

               if (fMovingPiece)
                    WinSetPointer (HWND_DESKTOP, NULL) ;

               else if (sColor = = -1)
                    WinSetPointer (HWND_DESKTOP, hptrArrow) ;

                    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,
                                                   NULL) ;
                         break ;

                         WinMapWindowPoints (hwnd, HWND_DESKTOP,
                                             &ptlMouse, 1) ;
                         WinSetPointerPos (HWND_DESKTOP,
                                           (SHORT) ptlMouse.x,
                                           (SHORT) ptlMouse.y) ;
                         break ;
               return 0 ;

               i = SHORT1FROMMP (mp1) ;
               CkdConvertIndexToCoords (i, &x, &y, sBottom) ;
               CkdErasePiece (hps, x, y) ;
               return 0 ;

               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) ;


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 ;
               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)) ;
          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
     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 ;
               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
     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)       ) ;
               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) ;


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


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

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

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

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

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

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

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


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

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


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

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

DosExit    Terminate current thread

DosExitCritSec    Restore context switching for other threads in same

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

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

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

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

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."


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:


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


For instance,


would search the help file device.hlp for the topic mouse_def, while


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


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

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

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

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 +
HelpDecomp((PB) pCompressBuffer, (PB) pTextBuffer,

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 =
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 */
  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

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.


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 */
/* map handle to a far address */
     char far *HelpLock(handle)
/* unlocks the memory pointed to by handle */
/* open a file on the path */
     handle = OpenFileOnPath(szFilename, fWrite)
/* close a file */
/* 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


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

.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

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

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

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

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

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

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

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

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.


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.


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.


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,

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

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

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

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

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


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

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

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

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
0040:0020  30 0B 3A 27 31 02 45 12-20 39 4C 26 32 03 30 0B   0.:'1.E.
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

               PAGE    60,132

;  A visual display of the keyboard scan code port. ;
;  Michael J. Mefford                               ;

               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
  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. ;

  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.

  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.                ;


  PUSH    AX                      ;Preserve registers.
  PUSH    BX
  PUSH    CX
  IN      AL,PORT_A               ;Get the scan code.
  CALL    HEX_OUTPUT              ;And display it.

  POP     CX                      ;Restore registers.
  POP     BX
  POP     AX
  JMP     DWORD PTR BIOS_INT_9    ;Jump to the BIOS INT 9 routine.




  MOV     BX,AX                   ;Store number in BX.
  MOV     CX,204H                 ;4 positions/word; 4bits/char.
  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.
  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
  MOV     AL,SPACE                ; between scan codes
  MOV     AH,0EH                  ; as delimiters.
  INT     10H



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.

  XOR     AH,AH                   ;Clear screen by setting mode.
  INT     10H
  MOV     AX,500H                 ;Make sure page zero.
  INT     10H

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


or enter the instructions directly while you are in Debug. The resulting
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.

MOV    AX,0040
XOR    Byte Ptr [0017],80
INT    20


E 109 40

E 109 20

E 109 10


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

0040:0082    1 word    BUFFER_END    Points to physical end of buffer

Figure 8


               TITLE   KEYVIEW.ASM
               PAGE    60,132

;  KeyView - Visual display of keyboard buffer. ;
;  Michael J. Mefford                           ;


               ORG     1AH

BUFFER_HEAD    DW      ?
BUFFER_TAIL    DW      ?

               ORG     71H

BIOS_BREAK     DB      ?

               ORG     80H



               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


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


PORT_A         EQU     60H
PORT_B         EQU     61H
EOI            EQU     20H


BIOS_INT_9     DW      ?,?

NORMAL         EQU     07H
INVERSE        DB      70H



F1_FLAG        DB      FALSE
ESC_FLAG       DB      FALSE

KEY_SUPPORT    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$"



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.
  MOV     AX,4C01H                ;Exit with error level 1.

  CLD                             ;All string moves forward.
  CALL    VIDEO                   ;Check the video equip. and CLS.

  MOV     DX,OFFSET COPYRIGHT     ;Display copyright.

; 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
  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.

  MOV     CX,15                   ;Try 15 times to retrieve
  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. ;

  MOV     SUPPORT_FLAG,1          ;Flag that supported.
  MOV     DX,OFFSET INT_16_MSG    ;Display support message.
  CALL    EXTENDED                ;Display "ACTIVE" message.
  MOV     DX,600H                 ;Move cursor to next line.

; Display Esc message regardless of keyboard support. ;

  MOV     DX,OFFSET ESC_MSG       ;Display Esc message.

; 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.

  PUSH    CX                          ;Save counter.
  MOV     CX,4                        ;Four rows per box.
  MOV     SI,OFFSET BUFFER_WINDOW     ;Point to buffer box.

  PUSH    CX                      ;Save counter.
  MOV     CX,65                   ;65 columns per box.
  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.                ;

  MOV     DX,1900H                ;Hide the cursor off screen
  CALL    SET_CURSOR              ; on row 25.
  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.
  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.

  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     LAST_HEAD,SI            ;Store new head.
  MOV     SI,OFFSET HEAD          ;Indicate head has moved

  CALL    UPDATE_BUFFER           ; and update visual buffer.
  JMP     SHORT GET_KEY           ;Next key.

  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.

  CALL    DISPLAY_CHAR            ;Retrieve character from buffer
  JMP     GET_KEY                 ; and display; next key.

  CALL    EXTENDED                ;Toggle extended support.
  MOV     SUPPORT_FLAG,1          ;Reset support flag.
  JMP     GET_KEY                 ;Next key.

  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.                                          ;

  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.
  INT     21H

MAIN           ENDP

               ;* SUBROUTINES *;

; This subroutine will toggle the extended keyboard   ;
; support on/off and display ACTIVE/INACTIVE message. ;


  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.
  XOR     KEY_SUPPORT,EXTENDED_CALL  ;Toggle support.
  JZ      DISPLAY_EXT                ;If zero, guessed right.
  INC     SI                         ;Else, bump pointer past "IN".
  INC     SI

  LODSB                              ;Display the message.


; All sixteen positions of visual buffer are updated   ;
; when program starts and when Ctrl Break is detected. ;


  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

  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     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.



; This is the INT 9 handler. ;


  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.

  MOV     ESC_FLAG,TRUE           ;If Esc pressed, flag and exit.

  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.      ;

  MOV     F1_FLAG,TRUE            ;Flag F1 pressed.
  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.


; When F1 is pressed, a character is retrieved  ;
; from the buffer and displayed.                ;


  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     AX,600H
  INT     10H
  MOV     AX,CHAR_START           ;Start at beginning of line.

  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.


; INPUT                              ;
;   BP = Last position.              ;
;   DI = New position.               ;


  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.

  PUSH    DX                      ;Save cursor position.
  CALL    SPACES                  ;Center char in top of box with
  CALL    WRITE_CHAR              ; three spaces on either side.

  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.
  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.

  PUSH    SI                      ;Save msg pointer.
  MOV     SI,OFFSET ERASE         ;Erase old msg.
  MOV     DX,DI                   ;Move to new position.
  INC     DL                      ;Move right one column.
  POP     SI
  CMP     SI,OFFSET HEAD          ;Display either head or tail msg.
  ADD     DH,5





  PUSH    AX                      ;Preserve AX.
  MOV     CX,3                    ;Display three spaces.
  POP     AX


; INPUT                       ;
;   BP = Character/scan code. ;


  MOV     CX,2                    ;Two codes to display.
  PUSH    CX                      ;Preserve counter.
  MOV     CX,204H                 ;4 positions/word; 4bits/char.
  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.
  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.
  POP     CX                      ;Do both char and scan code.


; INPUT                        ;
;   SI points to pointer text. ;


  MOV     CX,5                    ;Five characters to pointer.
  CALL    WRITE_CHAR              ;Write them.




  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.
  SUB     DL,16                   ;Else adjust offset.
  ADD     DH,BOX_TWO              ;Point to second box.
  SHL     DL,1                    ;Multiply by 4 to get cursor
  SHL     DL,1                    ; column.
  ADD     DL,BOX_COL + 1          ;Add offset of box start.


; INPUT                   ;
;   DX = cursor position. ;

  PUSH    AX                      ;Preserve AX.
  XOR     BH,BH                   ;Page zero.
  MOV     AH,2
  INT     10H                     ;Set cursor position.
  POP     AX                      ;Retrieve AX.

; INPUT                       ;
;   DX = new cursor position. ;
;   AL = Character to write.  ;


  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



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.

  XOR     AH,AH                   ;Set video mode.
  INT     10H
  MOV     AX,500H                 ;Set page zero.
  INT     10H

VIDEO          ENDP


  MOV     AH,9                    ;Print string via DOS.
  INT     21H

_TEXT          ENDS
               END     START


N KBDBUFF.COM                     ;Name: KBDBUFF.COM
A                                 ;Assemble
MOV AX,0040                       ;Move 40H into the data segment
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


;  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.  ;

               ORG     1AH
BUFFER_HEAD    DW      ?
BUFFER_TAIL    DW      ?
               ORG     80H
BUFFER_END     DW      ?


               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

CR             EQU     13
LF             EQU     10
CTRL_Z         EQU     26
SPACE          EQU     32
BOX            EQU     254



UNIT_CODE      DB      ?
STATUS         DW      ?
RESERVED       DQ      ?


DONE           EQU     0000000100000000B       ;Status codes.
UNKNOWN        EQU     1000000000000011B


INIT           STRUC

UNITS          DB      ?

INIT           ENDS

REQUEST_SEG    DW      ?

;              CODE AREA
;              ---------

; The only task of the strategy routine is to ;
; save the pointer to the request header.     ;


  MOV     CS:REQUEST_OFFSET,BX    ;Request header address is
  MOV     CS:REQUEST_SEG,ES       ; passed in ES:BX.


; The interrupt procedure will be called ;
; immediately after the strategy.        ;


  PUSH    AX                      ;Responsible for all registers.
  PUSH    BX
  PUSH    CX
  PUSH    DX
  PUSH    DS

  MOV     DS,CS:REQUEST_SEG       ;Retrieve request header pointer.

  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.

  MOV     CX,SS                   ;Save DOS stack.
  MOV     DX,SP
  MOV     AX,CS
  MOV     SS,AX                   ;Make new stack.
  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
  MOV     SS,CX
  MOV     SP,DX

  POPF                            ;Restore rest of registers.
  POP     DS
  POP     DX
  POP     CX
  POP     BX
  POP     AX
  RET                             ;Far return back to DOS.



;************* END OF RESIDENT PORTION *************;

BUFFER_MIN     EQU     16
BUFFER_MAX     EQU     200


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

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

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

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


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

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.


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

VCB_VTHUMB    Get string by thumb position    Thumb position    String
buffer    TRUE/FALSE

VCB_SEARCH    Get string by search string    String ID    String buffer

VCB_FIRST    Get first display string    String ID    String buffer

VCB_LAST    Get last display string    String ID    String buffer

VCB_NEXT    Get next display string    String ID    String buffer

VCB_PREV    Get previous display string    String ID    String buffer

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


Single selection    N/A    N/A    Pointer to selected string

Multiple selection    String number    N/A    Pointer to selected string


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



    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



Figure 2 Virtual List Box Source Code


**  VLB.H     Virtual List Box header file
**  Author: Robert A. Wood

#define VLB_CALLBACK  lpVLB->CallBack

#define VLBSTRLEN     255     // maximum virtual string length

//  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

// 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



// Internal functions

LONG FAR PASCAL VLBProc( HWND hCtl, unsigned message, 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 );


//*** END OF VLB.H **************************************************


**  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;

WORD FAR PASCAL Rpad( LPSTR str, WORD length );

**  Virtual List Box initialization function




   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 );
      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;
   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,


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



   // 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 )
   LPVLB    lpVLB;
   LONG     lThumb;

   switch ( message )
   case  WM_CHAR:

case  WM_SETCURSOR:            // override setting cursor


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 );

   case WM_SETFOCUS:

      hVLB = GetProp( hCtl, szVLBPropName );
      lpVLB = (LPVLB) GlobalLock( hVLB );


FrameFocusString( hCtl, lpVLB, TRUE );

      GlobalUnlock( hVLB );

      hVLB = GetProp( hCtl, szVLBPropName );
      lpVLB = (LPVLB) GlobalLock( hVLB );

      FrameFocusString( hCtl, lpVLB, FALSE );

InvertSelectedStrings( hCtl, lpVLB, (int)( lpVLB->FocusString -
                                       lpVLB->FirstDisplayString ) );


      GlobalUnlock( hVLB );

   case WM_VSCROLL:
      switch( wParam )



      case SB_LINEDOWN:
      case SB_LINEUP:
      case SB_PAGEDOWN:
      case SB_PAGEUP:
         ScrollVLB( hCtl, wParam, NULL );

   case WM_HSCROLL:
      hVLB = GetProp( hCtl, szVLBPropName );
      lpVLB = (LPVLB) GlobalLock( hVLB );

      lThumb = -1;

      switch( wParam )
         lThumb = (LONG)LOWORD( lParam );


&lThumb, 0L );
         LoadVLB( hCtl, RELOAD_STRINGS, 0L );

      case SB_LINEDOWN:

lThumb = VLB_CALLBACK( lpVLB->ListBoxId, VCB_RIGHT, 0, 0L );


      case SB_LINEUP:

lThumb = VLB_CALLBACK( lpVLB->ListBoxId, VCB_LEFT, 0, 0L );


      case SB_PAGEDOWN:



      case SB_PAGEUP:

lThumb = VLB_CALLBACK( lpVLB->ListBoxId, VCB_PAGELEFT,0,0L);


      if( lThumb >= 0 )
         lThumb = (LONG)min( (int)lpVLB->TotalWidth - 1,
                        max( 0, (int)lThumb ) );

SetScrollPos( hCtl, SB_HORZ, (int)lThumb, TRUE );


      GlobalUnlock( hVLB );


         SendMessage( GetParent( hCtl ), WM_COMMAND,
             GetWindowWord( hCtl, GWW_ID ),
             MAKELONG( hCtl, VLBN_DBLCLK ) );

      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 );

   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 );

      case VK_DOWN:

if( lpVLB->FocusString = = lpVLB->FirstDisplayString +
                                    lpVLB->DisplayStrings - 1 )


         SetFocusString( wParam, lpVLB );

         if( ! CtrlKey )
            SetSelectedString( hCtl, wParam, lpVLB );

         InvalidateRect( hCtl, NULL, TRUE );
         PaintVLB( hCtl, lpVLB );

      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 );

      case VK_NEXT:

if( lpVLB->FocusString = = lpVLB->FirstDisplayString +
                                    lpVLB->DisplayStrings - 1 )


         SetFocusString( wParam, lpVLB );

         if( ! CtrlKey )
            SetSelectedString( hCtl, wParam, lpVLB );

         InvalidateRect( hCtl, NULL, TRUE );
         PaintVLB( hCtl, lpVLB );

      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 );

      case VK_HOME:
         LoadVLB( hCtl, RELOAD_STRINGPOS, 0L );

         SetFocusString( wParam, lpVLB );

         if( ! CtrlKey )
            SetSelectedString( hCtl, wParam, lpVLB );

         InvalidateRect( hCtl, NULL, TRUE );

      case VK_END:

LoadVLB( hCtl, RELOAD_STRINGPOS, lpVLB->TotalStrings - 1 );

         SetFocusString( wParam, lpVLB );

         if( ! CtrlKey )
            SetSelectedString( hCtl, wParam, lpVLB );

         InvalidateRect( hCtl, NULL, TRUE );

      case VK_RIGHT:
      case VK_LEFT:
         int message;

         if( wParam = = VK_RIGHT )
            if( CtrlKey )
               message = VCB_PAGERIGHT;
               message = VCB_RIGHT;
            if( CtrlKey )
               message = VCB_PAGELEFT;
               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 );


         InvalidateRect( hCtl, NULL, TRUE );
      GlobalUnlock( hVLB );

case VLB_SETCURSEL:     // WM_USER+6   select specified StringId
   case VLB_SETSEL:        // WM_USER+7   sets selection of StringId


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);

                       lpSelectedStringIds + wParam, StringBuffer );

GlobalUnlock( lpVLB->hSelectedStringIds );


                       &lpVLB->SelectedStringId, StringBuffer );

            *StringBuffer = 0;

         lret = (LONG)(LPSTR)StringBuffer;
      else if( message = = VLB_GETSELID )
         if( lpVLB->MultiSelection )
            lret = lpVLB->hSelectedStringIds;
            lret = lpVLB->SelectedStringId;

      GlobalUnlock( hVLB );
      return( lret );

   case WM_DESTROY:
      hVLB = GetProp( hCtl, szVLBPropName );
      lpVLB = (LPVLB) GlobalLock( hVLB );


      GlobalFree( lpVLB->hDisplayBuffer );
      GlobalFree( lpVLB->hStringIds );
      if( lpVLB->MultiSelection )
         GlobalFree( lpVLB->hSelectedStringIds );
      GlobalUnlock( hVLB );
      GlobalFree( hVLB );

      RemoveProp( hCtl, szVLBPropName );


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;
      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 );
      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 );
   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



   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);


   // 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 )
                    &lpStringIds[0], StringBuffer );
   else if( wParam = = RELOAD_STRINGPOS )
      lpStringIds[0] = min( lParam,

lpVLB->TotalStrings - lpVLB->DisplayStrings );

lpVLB->FirstDisplayString = lpStringIds[0];
                    &lpStringIds[0], StringBuffer );
   else if( wParam = = RELOAD_STRINGID )
      lpStringIds[0] = lParam;
                    &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;

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);

   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;
         scroll = 1;
      lpVLB->ScrollWindow = 1;
   else if( wParam = = SB_LINEUP )
      if( lpVLB->FirstDisplayString = = 0 )
         ret = FALSE;
         scroll = 1;
      lpVLB->ScrollWindow = -1;
   else if( wParam = = SB_PAGEDOWN )

if( lpVLB->FirstDisplayString + lpVLB->DisplayStrings >
                                            lpVLB->TotalStrings - 1 )

ret = FALSE;
         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;
         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

         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     // 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


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,

   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;

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 ) );

      lpSelectedStringIds = (LONG FAR *)

GlobalLock( lpVLB->hSelectedStringIds );

lpSelectedStringId =

lpSelectedStringIds + lpVLB->TotalSelectedStrings++;

      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];


   case VK_NEXT:
   case VK_END:

*lpSelectedStringId = lpStringIds[lpVLB->DisplayStrings-1];


   case VK_PRIOR:
   case VK_HOME:
      *lpSelectedStringId = lpStringIds[0];

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;

         if( wParam = = VK_DOWN )

if( lpVLB->FocusString < lpVLB->TotalStrings - 1 )

            if( lpVLB->FocusString )

   case VK_NEXT:
      lpVLB->FocusString =

lpVLB->FirstDisplayString + lpVLB->DisplayStrings - 1;


   case VK_PRIOR:
      lpVLB->FocusString = lpVLB->FirstDisplayString;

   case VK_HOME:
      lpVLB->FocusString = 0;

   case VK_END:
      lpVLB->FocusString = lpVLB->TotalStrings - 1;


   int         x, y, first, last;
   HDC         hDC;
   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;
         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 );


   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 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 -+ $*;



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
   rc demo.res


#include <windows.h>
#include "demo.h"

demo MENU

rcinclude demo.dlg


NAME demo



HEAPSIZE    1024

   VLBPROC     @1
   DLGPROC     @2


**  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 );




CAPTION " Virtual List Box "
    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

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


**  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;
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 );


      case WM_DESTROY:
         PostQuitMessage( FALSE );


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 );

      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 ),



         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 );

      case VCB_PREV:
         if( *lplStringID <= 0 )
            return( FALSE );

      case VCB_FIRST:
         *lplStringID = 0;

      case VCB_LAST:
         *lplStringID = TotalStrings-1;

      case VCB_VTHUMB:
      case VCB_FULLSTRING:
      case VCB_STRING:

*lplStringID = (LONG)min( (int)(TotalStrings-1),
                              max( 0, (int)*lplStringID ) );


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 ) ) );

         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;
         lmemmove( lpszString, buf + Column, StrLength - Column );
         *( lpszString + StrLength - Column ) = 0;
   return( TRUE );


;  setup.h    header file for C utility functions


            push    ds
            pop     ax
            inc     bp
            push    bp
            mov     bp, sp
            push    ds
            mov     ds, ax
            ASSUME      DS:NOTHING
            sub     sp, X
            push    es
            push    si
            push    di


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



**  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           far version of memmove


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

                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.
                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
                EXIT    10

lmemmove        ENDP

CLIB            ENDS



;  lmemset      far version of memset


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
                shr     cx, 1
                rep     stosw           ; store word at a time
                adc     cx, cx
                rep     stosb           ; store final ("odd") byte
                mov     di, dx          ; Restore DI

                xchg    ax, bx          ; AX = Destination
                mov     dx, es          ; segment part of addr

                EXIT    8

lmemset         ENDP

CLIB            ENDS


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;

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

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 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

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

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)();

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.


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

/* 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 );

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.


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


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

#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

 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.


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

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

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 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


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.


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

  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....

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


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 ...

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).


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

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

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

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

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


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

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:

  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 }

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;


RealArray = OBJECT(SortArray) {real sort-array subclass}

FUNCTION RealArray.Compare(a, b: Pointer): Boolean; OVERRIDE;


StringArray = OBJECT(SortArray) {str sort-array subclass}
  FUNCTION StringArray.Compare(a, b: Pointer): Boolean; OVERRIDE;


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

FUNCTION IntArray.Compare(a, b: Pointer): Boolean;
  aPtr, bPtr: ^Integer;

  { Convert generic pointers to integer pointers }
  aPtr := a;
  bPtr := b;

  Compare := aPtr^ >= bPtr^;

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

After you compile and run the program, your screen displays the output shown

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:

  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 }


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

{ Filename: SORTINTS.PAS }

{ Creates the SortIntegers procedure and uses it to sort an array }

{ of 10 integers }

  IntArray = ARRAY [1..100] OF Integer;

PROCEDURE SortIntegers(VAR values: IntArray; numElements: Integer);

  i, j: Integer;  { indexes into the array }
  temp: Integer;  { temporary buffer for exchange }
  FOR i := 1 TO numElements - 1 DO
    FOR j := i + 1 TO numElements DO
        IF (values[i] > values[j]) THEN
            temp := values[i];
            values[i] := values[j];
            values[j] := temp;
  intValues: IntArray;
  i: Integer;
  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

Figure 6

{ Filename: SORTVALS.PAS }

{ Creates the SortIntegers, SortReals, and SortStrings procedures }
{ and uses them to sort three arrays of different types }

  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);

  index, index2: Integer;  { indexes into the array }
  temp: Integer;  { temporary buffer for exchange }

  FOR index := 1 TO numElements - 1 DO
    FOR index2 := index + 1 TO numElements DO
        IF (values[index] > values[index2]) THEN
            temp := values[index];
            values[index] := values[index2];
            values[index2] := temp;

{ Sorts an array of type RealArray }

PROCEDURE SortReals(VAR values: RealArray; numElements: Integer);

  index, index2: Integer;  { indexes into the array }
  temp: Real;     { temporary buffer for exchange }

  FOR index := 1 TO numElements - 1 DO
    FOR index2 := index + 1 TO numElements DO
        IF (values[index] > values[index2]) THEN
            temp := values[index];
            values[index] := values[index2];
            values[index2] := temp;

{ Sorts an array of type StringArray }

PROCEDURE SortStrings(VAR values: StringArray; numElements: Integer);

  index, index2: Integer;  { indexes into the array }
  temp: String;   { temporary buffer for exchange }

  FOR index := 1 TO numElements - 1 DO
    FOR index2 := index + 1 TO numElements DO
        IF (values[index] > values[index2]) THEN
            temp := values[index];
            values[index] := values[index2];
            values[index2] := temp;

  intValues: IntArray;
  realValues: RealArray;
  strValues: StringArray;
  index: Integer;

  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
      IF (index <= 5) THEN

Figure 8 SortArray.SortValues Method

PROCEDURE SortArray.SortValues;
  i, j: Integer;  { indexes into the array }
  aPtr, bPtr, temp: Pointer;

  { 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
        { 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
            Move(bPtr^, temp^, Self.fElementSize);
            Move(aPtr^, bPtr^, Self.fElementSize);
            Move(temp^, aPtr^, Self.fElementSize);

  { Release memory allocated for temporary buffer }
  FreeMem(temp, Self.fElementSize);

Figure 9 SORT.PAS

{$M+}  { test for object memory allocation }
{ Filename: SORT.PAS }

{ Creates classes, methods, and objects to sort an array of }
{ integers, an array of reals, and an array of character strings }


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}


IntArray = OBJECT(SortArray)       { integer sort-array subclass }
    FUNCTION IntArray.Compare(a, b: Pointer): Boolean;  OVERRIDE;


RealArray = OBJECT(SortArray)      { real sort-array subclass }
    FUNCTION RealArray.Compare(a, b: Pointer): Boolean; OVERRIDE;


StringArray = OBJECT(SortArray)    { string sort-array subclass }
    FUNCTION StringArray.Compare(a, b: Pointer): Boolean; OVERRIDE;


FUNCTION SortArray.Compare(a, b: Pointer): Boolean;
  { No statements--always overridden by a subclass }

{ 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;
  aPtr, bPtr: ^Integer;

  { Convert generic pointers to integer pointers }
  aPtr := a;
  bPtr := b;

  Compare := aPtr^ >= bPtr^;

{ 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;
  aPtr, bPtr: ^Real;

  { Convert generic pointers to real pointers }
  aPtr := a;
  bPtr := b;

  Compare := aPtr^ >= bPtr^;

{ 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;
  aPtr, bPtr: ^String;

  { Convert generic pointers to string pointers }
  aPtr := a;
  bPtr := b;

  Compare := aPtr^ >= bPtr^;

{ Sorts the array }
PROCEDURE SortArray.SortValues;
  i, j: Integer;  { indexes into the array }
  aPtr, bPtr, temp: Pointer;

  { 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
        { 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
            Move(bPtr^, temp^, Self.fElementSize);
            Move(aPtr^, bPtr^, Self.fElementSize);
            Move(temp^, aPtr^, Self.fElementSize);

  { Release memory allocated for temporary buffer }
  FreeMem(temp, Self.fElementSize);

  { 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;

  { Allocate memory for each object }

  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
      IF (i <= 5) THEN

  { Release the memory allocated for the objects }

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 }


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}


IntArray = OBJECT(SortArray)     { integer sort-array subclass }
    FUNCTION IntArray.Compare(a, b: Pointer): Boolean;  OVERRIDE;


RealArray = OBJECT(SortArray)    { real sort-array subclass }
    FUNCTION RealArray.Compare(a, b: Pointer): Boolean; OVERRIDE;


StringArray = OBJECT(SortArray)  { string sort-array subclass }
    FUNCTION StringArray.Compare(a, b: Pointer): Boolean; OVERRIDE;


FUNCTION SortArray.Compare(a, b: Pointer): Boolean;
  { No statements--always overridden by subclass }

{ 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;
  aPtr, bPtr: ^Integer;

  { Convert generic pointers to integer pointers }
  aPtr := a;
  bPtr := b;

  Compare := aPtr^ >= bPtr^;

{ 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;
  aPtr, bPtr: ^Real;

  { Convert generic pointers to real pointers }
  aPtr := a;
  bPtr := b;

  Compare := aPtr^ >= bPtr^;

{ 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;
  aPtr, bPtr: ^String;

  { Convert generic pointers to string pointers }
  aPtr := a;
  bPtr := b;

  Compare := aPtr^ >= bPtr^;

{ Sorts the array }
PROCEDURE SortArray.SortValues;
  i, j: Integer;  { indexes into the array }
  aPtr, bPtr, temp: Pointer;

  { 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
        { 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

            Move(bPtr^, temp^, Self.fElementSize);
            Move(aPtr^, bPtr^, Self.fElementSize);
            Move(temp^, aPtr^, Self.fElementSize);
        ELSE IF (NOT Self.fAscending) AND
          (NOT Self.Compare(aPtr, bPtr)) THEN
            Move(bPtr^, temp^, Self.fElementSize);
            Move(aPtr^, bPtr^, Self.fElementSize);
            Move(temp^, aPtr^, Self.fElementSize);

  { Release memory allocated for temporary buffer }
  FreeMem(temp, Self.fElementSize);

  { 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;

  { Allocate memory for each object }

  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
      IF (i <= 5) THEN

  { Release the memory allocated for the objects }

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 }

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}


IntArray = OBJECT(SortArray)       { integer sort-array subclass }
    FUNCTION IntArray.Compare(a, b: Pointer): Boolean;  OVERRIDE;


RealArray = OBJECT(SortArray)      { real sort-array subclass }
    FUNCTION RealArray.Compare(a, b: Pointer): Boolean; OVERRIDE;


StringArray = OBJECT(SortArray)    { string sort-array subclass }
    FUNCTION StringArray.Compare(a, b: Pointer): Boolean; OVERRIDE;


FUNCTION SortArray.Compare(a, b: Pointer): Boolean;
  { No statements--always overridden by subclass }

{ 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;
  aPtr, bPtr: ^Integer;

  { Convert generic pointers to integer pointers }
  aPtr := a;
  bPtr := b;

  Compare := aPtr^ >= bPtr^;

{ 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;
  aPtr, bPtr: ^Real;

  { Convert generic pointers to real pointers }
  aPtr := a;
  bPtr := b;

  Compare := aPtr^ >= bPtr^;

{ 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;
  aPtr, bPtr: ^String;

  { Convert generic pointers to string pointers }
  aPtr := a;
  bPtr := b;

  Compare := aPtr^ >= bPtr^;

{ Implements a modified Shell sort to sort the array }
PROCEDURE SortArray.SortValues;
  index, gap: Integer;  { indexes into the array }
  aPtr, bPtr, temp: Pointer;
  exchangeOccurred: Boolean;

  { Allocate memory for temporary swap buffer }
  GetMem(temp, Self.fElementSize);

  gap := Self.fNumElements DIV 2;

      exchangeOccurred := FALSE;

      FOR index := 1 TO Self.fNumElements - GAP DO

{ 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

              exchangeOccurred := TRUE;
              Move(bPtr^, temp^, Self.fElementSize);
              Move(aPtr^, bPtr^, Self.fElementSize);
              Move(temp^, aPtr^, Self.fElementSize);
          ELSE IF (NOT Self.fAscending) AND
            (NOT Self.Compare(aPtr, bPtr)) THEN
              exchangeOccurred := TRUE;
              Move(bPtr^, temp^, Self.fElementSize);
              Move(aPtr^, bPtr^, Self.fElementSize);
              Move(temp^, aPtr^, Self.fElementSize);
    UNTIL (NOT exchangeOccurred);
    gap := gap DIV 2;
  UNTIL (gap = 0);

  { Release memory allocated for temporary buffer }
  FreeMem(temp, Self.fElementSize);

  { 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;

  { Allocate memory for each object }

  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
      IF (index <= 5) THEN

  { Release the memory allocated for the objects }

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

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

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

typedef struct _DDEINIT
  {  USHORT  cb;         // length of block
     PSZ     pszAppName; // application name
     PSZ     pszTopic;   // topic name

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

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

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

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

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

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

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

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

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

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

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,

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

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

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

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 trap