Home of the original IBM PC emulator for browsers.
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
Applications
Richard Hale Shaw
You've just ported an important DOS1 application to the OS/2 operating
system. The program includes a module that offers full-screen editing with
cut and paste capabilities. Unfortunately, you don't know how to let the
cut-and-paste module access the Clipboard facility in OS/2 Presentation
Manager (hereafter "PM"). Fortunately, even though the Clipboard is
available only to PM programs, you can give OS/2 kernel programs access to
the Clipboard and Dynamic Data Exchange (DDE) facilities using the PMServer
program in this article.
This article presents both PMServer and a sample program, PMAccess, which
demonstrates how to access PMServer from a kernel program. First, I will
discuss briefly the PM Clipboard services and what they offer. I will follow
with a discussion of how PMServer uses the Clipboard. Next, I will present
PMServer, the way it works, and how OS/2 kernel programs can use it. And
finally, I willl discuss PMAccess. In a future article, I will present a
version of PMServer that includes DDE support for kernel programs, and I
will provide a version of PMAccess that takes advantage of this.
For further information on how PM programs work (specifically the use of
windows, messages, message queues, and window procedures), refer
to the sidebar "A Presentation Manager Primer." The sidebar contains the
essential issues and concepts that I will assume you have a basic
understanding of, particularly in the discussion of PMServer.
PM Clipboard
The PM Clipboard is a facility that offers general cut, copy, and paste
options for moving data to and from PM programs. These activities are not
automatic; because the user usually initiates a cut, copy, or paste option,
an application should not be able to initiate Clipboard operations without
explicit instructions from the user. For additional information about
automatic data transfer between applications, see "A Complete Guide to OS/2
Interprocess Communications and Device Monitors," MSJ (Vol. 4, No. 5).
The classic way to use a clipboard is to select some text from a document in
a word-processing program, cut or copy it into the clipboard, and then paste
the text somewhere else. You can also cut, copy, and paste graphic images to
transfer them between applications. The clipboard is only a temporary
storage location: a user can alter its contents at any time, but once the
computer is turned off, the contents of the clipboard are lost.
Data Formats
Although the user may move either text or graphic images into and out of the
PM Clipboard, three standard formats are used for transferring Clipboard
data: text, PM bitmaps, and PM metafiles. Text is an ASCII block that
includes tabs and spaces. Each line is delimited by a linefeed, and the
entire block is terminated with a NULL. PM bitmaps and PM metafiles are
exclusive Presentation Manager formats. Because each data format is a
different representation of the same Clipboard data (that is, the data most
recently placed in the Clipboard), an application can choose the format most
appropriate for its needs. Alternatively, an application can define new data
formats for special purposes.
Using the Clipboard
The PM API offers several functions for accessing and manipulating the
Clipboard:
■ WinOpenClipbrd opens the Clipboard
■ WinEmptyClipbrd removes any stored data from the Clipboard
■ WinSetClipbrdData places data into the Clipboard for a particular data
format
■ WinCloseClipbrd closes the Clipboard
■ WinQueryClipbrdData retrieves data from the Clipboard in a specified
data format
■ WinQueryClipbrdFmtInfo determines whether the data in the Clipboard is
available in a particular format
■ WinEnumClipbrdFmts enumerates the available Clipboard formats
An application program can become the Clipboard viewer, an application that
maintains a window which displays the current contents of the Clipboard; or
an application program can become the Clipboard owner, an application that
supplies data to the Clipboard when it's to be drawn in the viewer or when
it's being supplied to the Clipboard on a delayed basis. An application can
delay data delivery to the Clipboard if it supports one or more formats that
are time-consuming to render. Therefore, several additional Clipboard API
calls are available, used primarily by applications that must become the
Clipboard viewer or owner. These uses of the Clipboard are beyond the scope
of PMServer, however, so I've limited their discussion here.
Because more than one application must be able to access the Clipboard, it's
important that the Clipboard's data be shareable. When an application passes
a PM bitmap or a PM metafile to the Clipboard or receives one from the
Clipboard, the Clipboard functions automatically make it shareable. Text,
however, must be placed in a shared memory segment. The application does
this by calling DosAllocSeg (with the SEG_GIVEABLE flag) to create the
segment and then copying the text into the segment. Text is transferred
between the Clipboard and the application via a shared memory selector;
bitmaps and metafiles are transferred via a handle. Once a shared memory
selector or a handle has been passed to the Clipboard, the selector or
handle is invalid, and the application should not use it again. Furthermore,
once a selector or a handle to Clipboard data has been returned from the
Clipboard, the application must use it before closing the Clipboard--the
selector or handle will no longer be valid once the Clipboard is closed.
Thus, the application must copy the data to a local segment before closing
the Clipboard if it is to use the data after the Clipboard is closed.
Cutting and Copying
To cut or copy data to the Clipboard, an application must first call
WinOpenClipbrd. This function does not return until the Clipboard is
available, so if another application has already opened the Clipboard, this
function will wait until the Clipboard is available before it returns.
However, the application can still receive and respond to messages in its
window message queue.
Once an application has opened the Clipboard, it should clear it with a
call to WinEmptyClipbrd. It should then place the data into the Clipboard
via a call to WinSetClipbrdData for each data format that the application
supports (all clipboard formats coexist until the Clipboard is emptied
again). Again, the application should not use a selector or a handle to data
that has been passed to the Clipboard. Finally, the application should call
WinCloseClipbrd to close the Clipboard and release it for use by other
applications.
Pasting
Before an application can paste data from the Clipboard, it must retrieve
the data as it does with the cutting and copying operations. To retrieve
data from the Clipboard, an application first calls WinOpenClipbrd to open
the Clipboard. Then it should call WinQueryClipbrdData, which will, for a
specified format, return a selector or a handle to the data. If the data is
available in the format specified, WinQueryClipbrdData returns a 32-bit
integer, which is a handle for a bitmap or a metafile; if the data is text,
the low 16 bits contain a selector to a shared memory segment. If the
function returns a NULL, the specified format is not available, but the
application can continue to call WinQueryClipbrdData for any data format
that it supports, until either the data is returned or it runs out of
supported formats. Finally, the application should either use the data
immediately or copy it and then call WinCloseClipbrd.
PMSERVER
To PM and the Clipboard, PMServer (see Figure 1) looks like another PM
application, but it's actually a liaison between kernel applications and PM.
PMServer is, for the most part, an ordinary PM program; indeed, it
accommodates only a few messages. It has a couple of frame flags set, and
it's not visible. The only indication to the user that PMServer is running
is its name on the Task Manager's task list, which is where you should
terminate PMServer when you're ready.
A unique aspect of PM necessitated the creation of PMServer as a separate
program rather than as an object file or a DLL that can be linked to a
kernel application. Clipboard interface functions are only available to a PM
program with a PM message queue and a client window. Once a program opens a
PM message queue, all its input and output must be performed by Presentation
Manager, which means that a kernel application's attempts to use the Vio,
Kbd, and Mou subsystems are ignored once the PM message queue is open (you
can, of course, use AVio calls, but these still depend on PM programming).
Even if you place the Clipboard-handling code in another program, you cannot
run that program in the same session: once the program opens the window
message queue, the PM screen group handles all I/O for that session.
Therefore, the only convenient way to offer Clipboard services to a kernel
application is to place the Clipboard-handling code in a separate process
that runs in a separate session--a PM program.
The next step in designing PMServer was to build an interface that allowed
kernel applications to communicate with the PM program. Because it's crucial
that PM applications keep their window message processing as efficient as
possible, including the kernel program interface in PMServer's main thread
or its window procedure was a bad idea. The logical alternative was to put
this code in a separate thread of execution. PMServer thus creates a
separate thread to receive and manage requests from kernel applications.
To understand PMServer, you need to be familiar with PM programming
terminology and a message queue system for kernel applications. The PMServer
approach to programming OS/2 kernel applications uses OS/2 queues to pass
messages between threads. It allows you to write event-driven, message-based
applications in which a thread receives messages about events and reacts to
them accordingly. (If this sounds like PM, it should--the idea is to imitate
the approach to programming used in PM.) This system, contained in MSGQ.C
(see Figure 2), is used extensively in PMAccess. It also provides a
convenient means for a client application to communicate with PMServer and
vice versa. In this manner, a kernel application can pass messages and data
to PMServer by opening its queue and writing to it. If the application
provides a queue of its own, PMServer can pass messages and data back to it
in kind. Because the functions pass data by allocating a shareable segment,
you can use the message queue functions to pass data and messages across
applications the way they are used by PMServer and its clients. In addition,
the message queue functions can pass data between threads; a keyboard
thread can thus pass a variety of keyboard information to the main thread of
an application.
Before exploring PMServer's use of the Clipboard, let's enter the program
from the side door: the queue used by kernel programs to make requests, and
the thread that manages that queue.
Queue Manager Thread
PMServer's second thread is essentially a queue manager. Any kernel
application can open the PMServer queue and pass messages to it. These
messages are simple unsigned words (contained in PMSERVER.H) prefixed with
PMS_ in the source code. They are processed by PMServer's second thread,
which maintains a table of all the client processes using PMServer at any
one time. A client process should therefore register itself (using the
PMS_INIT message) with PMServer before attempting to use any of the services
offered, and it should deregister itself (using the PMS_TERMINATE message)
when it will no longer use PMServer or when it is about to terminate. In
addition, a client program can create its own queue for receiving messages
and Clipboard data from PMServer and can pass the name of the queue to
PMServer at registration time. If it does, PMServer opens the queue and
stores the queue handle in the table.
Although a kernel program should go through this registration process, it
doesn't always have to; it can bypass registration and the need to offer a
return queue when it is only placing data in the Clipboard. In that case,
PMServer only needs the data to be placed in the Clipboard, which the kernel
application can pass with a PMS_COPY message (see Figure 3). A kernel
application that only uses PMS_COPY can access PMServer without registering
with it or providing a return queue.
Copy Operation
PMServer's second thread processes messages from kernel applications that
are received in its OS/2 queue. Depending on the nature of these messages,
the second thread notifies PMServer's window procedure, which sets up and
calls the Clipboard interface functions. Therefore, if the second thread
finds a PMS_COPY message in the OS/2 queue, it calls WinPostMsg to place a
WM_COPY message (and the data to be placed in the Clipboard) in the window
message queue of PMServer's window procedure. Note that WinPostMsg merely
places the message in the queue and returns right away; the alternative,
WinSendMsg, calls the window procedure directly and doesn't return until the
window procedure has processed the message, which could be a long while if
its message queue is full. Because a long wait could tie up both the window
procedure and the second thread at the same time, this alternative wasn't
chosen. Instead, the second thread posts the message to the window procedure
with WinPostMsg and returns to process the next message received from a
kernel application.
Once PMServer's window procedure receives a WM_COPY message, it prepares the
data to place it in the Clipboard. It creates the segment with a call to
DosAllocSeg (with the SEG_GIVEABLE flag) and copies the data into the
segment. It then opens and empties the Clipboard. When it calls
WinSetClipbrdData to place the data in the Clipboard, it tells the Clipboard
that the data is text by using the CF_TEXT and CFI_SELECTOR flags. (PMServer
assumes that any data transferred between kernel applications is text. If
your kernel application uses another data format, modify PMServer and
include the data format information in the message passed from the kernel
application.) Finally, the window procedure closes the Clipboard.
Paste Operation
Although processing a PMS_COPY message from a kernel application is
relatively straightforward, providing it with paste services is not. The
problem is the difference in the way a PM application and a kernel
application notify users that paste services are available.
In a typical PM scenario, a user selects the Edit pull-down menu from the PM
application's menu bar. This action generates a message to display the menu,
which is received by the application's window procedure. The window
procedure can call WinQueryClipbrdFmtInfo before displaying the menu and
determine whether data for a particular format is available in the
Clipboard. It can then enable or disable the Paste entry in the Edit
pull-down menu before it is displayed.
Unfortunately, the lack of uniformity among character-based interfaces
necessitated a multistep approach to the paste operation. First, a kernel
application should post a PMS_CLPBRD_QUERY message to PMServer, which
generates a WM_PASTE_MSG message in PMServer's window message queue. Upon
receiving this message, the window procedure calls WinQueryClipbrdFmtInfo
and posts a reply message (either PMS_CLPBRD or PMS_CLPBRD_EMPTY) back to
the kernel application via its return queue. Therefore, the kernel
application can use PMS_CLPBRD_QUERY to determine whether Clipboard data is
available for pasting. Indeed, a kernel application can periodically send
this message to PMServer and be kept abreast of the status of the Clipboard,
a technique described in the discussion of PMAccess below.
If an application receives PMS_CLPBRD_EMPTY, it should notify the user that
no data is available in the Clipboard. If, however, the application receives
PMS_CLPBRD after posting a PMS_CLPBRD_QUERY, it should immediately reply
with PMS_PASTE, which will generate a WM_PASTE message in PMServer's window
message queue. Upon receiving this message, PMServer's window procedure will
open the Clipboard, retrieve the data, and pass the data back to the kernel
application with a PMS_CLPBRD_DATA message, indicating that it has received
data for pasting. The window procedure then closes the Clipboard.
As you can see, PMServer deals directly with the Clipboard: kernel
applications can use it to access the Clipboard's cut and paste operations,
without having to be PM applications themselves.
PMAccess
PMAccess is a program demonstrating the use of PMServer's Clipboard
facilities in an OS/2 kernel application. The program lets you use the mouse
to select text or program functions. Program functions are available via
screen buttons and accelerator hot keys (Esc for the Esc button, Alt-O for
Copy, Alt-U for Cut, Alt-P for Paste, and Alt-C for Clear).
When you run PMAccess (be sure to start PMServer first), it displays the
five buttons at the bottom of the window (see Figure 4). You can type text
into the input screen above these buttons and use the mouse to select the
text. To copy or cut a block of text to the Clipboard, simply select some
text and press the appropriate screen button with the mouse (see Figure 5).
The Paste button will remain nonfunctional until some data is available in
the Clipboard for pasting. Once Clipboard data is available, the Paste
button will light up. Then you can press this button and PMAccess will paste
the data at the current cursor location (see Figure 6).
PMAccess: A Look under the Hood
PMAccess (see Figure 7) relies heavily on several modules. MSGQ.C contains
the message queue functions that are the backbone of PMAccess's
event-driven, message-based architecture. BUTTON.C (see Figure 8) contains
the functions for generating and managing the screen buttons used in the
interface. The code for the keyboard and mouse threads is contained in KBD.C
(see Figure 9) and MOU.C (see Figure 10).
PMAccess's keyboard thread is relatively simple. It opens the message queue
to PMAccess, sets up the keyboard, and notifies PMAccess that it's ready by
clearing a semaphore. It then waits for a key and returns the key
information as part of a MSG_CHAR message to PMAccess's main thread.
The mouse thread is slightly more complex: it also opens a queue to
PMAccess, initializes the mouse, and notifies PMAccess by clearing a
semaphore. But as it receives information on each mouse event, it looks
first for mouse clicks on one of the screen buttons. Then it passes along
only those mouse clicks that fall in the input screen, filtering out those
that fall below (and did not activate one of the screen buttons).
In addition to the keyboard and mouse threads, PMAccess creates a third
thread, which periodically checks with PMServer about the availability of
Clipboard data (see Figure 11). This request thread posts a PMS_CLPBRD_QUERY
message to PMServer approximately every 30 seconds, causing PMServer to
reply with either PMS_CLPBRD (data is available) or PMS_CLPBRD_EMPTY (the
Clipboard is empty). When PMAccess receives one of these messages, it
highlights the Paste button (indicating that pasting is allowed) or sets it
to normal (pasting not allowed) so that the user will know whether he or she
can use the Paste facility.
The PMAccess program (found in PMACCESS.C) begins by obtaining its own
process ID (for use with some of the PMServer functions) and by creating its
main message queue. It then opens the message queue of PMServer and starts
the keyboard thread, the mouse thread and the request thread. After
initializing and displaying the buttons and the screen, PMAccess obtains a
pointer to its video buffer, which it will use to access selected blocks of
text. It then registers itself with PMServer via a call to PMS_Init.
The balance of PMAccess's code is a switch statement that contains responses
to the messages it expects to receive. These include responses for
processing screen button presses (MSG_ESCAPE, MSG_COPY, and so on), as well
as responses to mouse events (MSG_B1DOWN, MSG_MOUSEMOVED). When these
messages are received, PMAccess can access the appropriate keyboard or mouse
data via the MOUMSG or KBDMSG macros, which, along with the message
definitions, can be found in MSGS.H. To communicate with PMServer, PMAccess
needs to use only the messages in PMSERVER.H, which includes the name of
PMServer's message queue. The code for handling cut and copy requests from
the user (see the case for MSG_COPY and MSG_CUT) is virtually identical: the
difference lies in blanking out the text block for cut and resetting the
attributes of the highlighted area to normal video for copy.
PMServer offers features for OS/2 kernel applications that have been
previously limited to PM applications. You can make your own OS/2 kernel
programs work with PM Server by adapting PMAccess or by simply cribbing some
code from it. In a future article, I'll present expanded versions of
PMServer and PMAccess that provide access to Presentation Manager's DDE
facilities.
Figure 1
PMSERVER.MAK
#--------------------
# pmserver make file
#--------------------
COPT=/Lp /W3 /Zpiel /G2sw /I$(INCLUDE) /Od /Alfw
errexit.obj: errexit.c errexit.h
cl /c $(COPT) errexit.c
msgq.obj: msgq.c msgq.h
cl $(COPT) /c msgq.c
pmserver.obj : pmserver.c pmserver.h
cl /c $(COPT) pmserver.c
pmserver.exe : pmserver.obj pmserver.def errexit.obj msgq.obj
link pmserver errexit msgq, /align:16 /co, NUL,os2 llibcmt, pmserver
PMSERVER.H
/* pmserver.h common header file for PMServer and client programs */
#define PMSERVERQUE "\\QUEUES\\PMSERVER.QUE"
// messages sent by client
#define PMS_INIT 100 // client initializing
#define PMS_COPY 101 // copy data to clipboard
#define PMS_PASTE 103 // get data from clipboard
#define PMS_TERMINATE 104 // client is terminating
#define PMS_CLPBRD_QUERY 105 // is anything in clipboard?
// messages sent by server
#define PMS_CLPBRD 120 // clipboard data available
#define PMS_NO_INIT 121 // can't initialize client
#define PMS_INIT_ACK 122 // server acknowledges init
#define PMS_MSG_UNKNOWN 123 // server can't identify msg
#define PMS_CLPBRD_EMPTY 124 // clipboard data not avail
#define PMS_CLPBRD_DATA 125 // here's your clipboard data
typedef struct _clientdata
{
PID pid;
BYTE qname[21];
} CLIENTDATA;
#define CLIENTDATAMSG(ptr) ((CLIENTDATA *)ptr)
#define MAXAPPNAME 50
#define MAXTOPICNAME 50
PMSERVER.C
/* PM program that supplies PM services to VIO apps */
#define INCL_WIN
#define INCL_VIO
#define INCL_AVIO
#define INCL_DOS
#include<os2.h>
#include<mt\stdlib.h>
#include<mt\process.h>
#include<mt\string.h>
#include"pmserver.h"
#include"msgq.h"
#include"errexit.h"
#define WM_COPY (WM_USER + 0)
#define WM_PASTE (WM_USER + 1)
#define WM_PASTE_MSG (WM_USER + 2)
typedef struct _client
{
PID clientpid;
HQUEUE clientqhandle;
} CLIENT;
#define MAXCLIENTS 25
CLIENT clients[MAXCLIENTS];
HAB hab;
HWND ThreadWindowHdl;
#define QMGRTHREADSTACK 500
BYTE QMgrThreadStack[QMGRTHREADSTACK];
HQUEUE qhandle;
int main(void);
MRESULT EXPENTRY ClientWndProc(HWND hwnd, USHORT msg, MPARAM mp1,
MPARAM mp2);
void QMgrThread(void);
USHORT QMgrFindClient(PID pid,HQUEUE *qhandle,USHORT *position);
int main(void)
{
static CHAR szClientClass[] = "PMServer";
static ULONG flFrameFlags = FCF_TASKLIST;
HMQ hmq;
HWND hwndFrame, hwndClient;
QMSG qmsg;
MsgQCreate(&qhandle,PMSERVERQUE); // create input queue
hab = WinInitialize(0); // initialize window
// start queue manager thread
if(_beginthread(QMgrThread,QMgrThreadStack,
sizeof(QMgrThreadStack),NULL) = = -1)
error_exit(0,"_beginthread(QMgrThread)");
hmq = WinCreateMsgQueue(hab, 0); // create window message queue
// register window class
WinRegisterClass(hab, szClientClass, ClientWndProc, 0L, 0);
// create window
hwndFrame = WinCreateStdWindow(HWND_DESKTOP, 0L,
&flFrameFlags, szClientClass, NULL,
0L, NULL, 0L, &hwndClient);
while(WinGetMsg(hab, &qmsg, NULL, 0, 0)) // process messages
WinDispatchMsg(hab, &qmsg);
WinDestroyWindow(hwndFrame); // destroy window
WinDestroyMsgQueue(hmq); // destroy message queue
WinTerminate(hab); // terminate window
return 0;
}
MRESULT EXPENTRY ClientWndProc(HWND hwnd, USHORT msg, MPARAM mp1,
MPARAM mp2)
{
SEL ClipTextSel;
PVOID temp1,temp2;
USHORT len;
PCH temp3;
USHORT fmtInfo;
switch(msg)
{
// ************* Create processing **********
case WM_CREATE:
ThreadWindowHdl = hwnd;
return 0;
// **************Clipboard handling *********
case WM_COPY: // place client data in Clpbrd
DosAllocSeg(SHORT1FROMMP(mp2),&ClipTextSel,SEG_GIVEABLE);
temp1 = MAKEP(ClipTextSel,0);
temp2 = PVOIDFROMMP(mp1);
memmove(temp1,temp2,SHORT1FROMMP(mp2));
temp1 = temp2 = NULL;
WinOpenClipbrd(hab); // open the clipboard
WinEmptyClipbrd(hab); // empty it
// put the new data in it
WinSetClipbrdData(hab, (ULONG)ClipTextSel, CF_TEXT,
CFI_SELECTOR);
WinCloseClipbrd(hab); // close the clipboard
return 0;
case WM_PASTE: // get client data from Clpbrd
WinOpenClipbrd(hab); // open the clipboard
ClipTextSel = (SEL)WinQueryClipbrdData(hab, CF_TEXT);
if(ClipTextSel) // if data in clipboard
{
temp3 = temp1 = MAKEP(ClipTextSel,0);
for( len = 0; temp3[len]; len++); // get data length
len++; // include the NULL
// send data to client
MsgQSend(SHORT1FROMMP(mp1),temp1,len,PMS_CLPBRD_DATA);
}
else // nothing in clipboard
MsgQSend(SHORT1FROMMP(mp1),NULL,0,PMS_CLPBRD_EMPTY);
WinCloseClipbrd(hab);
return 0;
case WM_PASTE_MSG: // client query about Clpbrd
// tell client whether avail
MsgQSend(SHORT1FROMMP(mp1),NULL,0,
(WinQueryClipbrdFmtInfo(hab,CF_TEXT,&fmtInfo) ?
PMS_CLPBRD : PMS_CLPBRD_EMPTY));
return 0;
}
return WinDefWindowProc(hwnd, msg, mp1, mp2);
}
void QMgrThread(void)
{
PVOID msgdata = NULL;
USHORT msg,i,msgsize,position;
HQUEUE temphandle;
PBYTE tempname;
PID temppid;
memset(clients,0,sizeof(clients));
while(TRUE)
{
temppid = 0;
MsgQGet(qhandle, &msgdata, &msgsize, &msg);
switch(msg)
{
case PMS_COPY:
break;
case PMS_INIT:
case PMS_TERMINATE:
case PMS_CLPBRD_QUERY:
case PMS_PASTE:
default:
temppid = (msgdata ? CLIENTDATAMSG(msgdata)->pid : 0);
break;
}
switch(msg)
{
case PMS_INIT: // client is initializing to use the server
if(msgdata) // if valid message packet
{
tempname = CLIENTDATAMSG(msgdata)->qname;
if(*tempname) // client has return queue
MsgQOpen(&temphandle,tempname);
// find open slot
for( i = 0; i < MAXCLIENTS &&
clients[i].clientpid; i++);
if(i = = MAXCLIENTS) // no slots found
{
if(temphandle)
MsgQSend(temphandle,NULL,0,PMS_NO_INIT);
}
else // got open slot
{
clients[i].clientpid =
CLIENTDATAMSG(msgdata)->pid;
if(temphandle)
{
clients[i].clientqhandle = temphandle;
MsgQSend(temphandle,NULL,0,PMS_INIT_ACK);
}
}
}
tempname = NULL;
break;
case PMS_COPY: // client has data for the Clipboard
WinPostMsg(ThreadWindowHdl,WM_COPY,MPFROMP(msgdata),
MPFROMSHORT(msgsize));
msgdata = NULL;
break;
case PMS_CLPBRD_QUERY:
// client wants to know if something's in the clipboard
if(QMgrFindClient(temppid,&temphandle,&position))
WinPostMsg(ThreadWindowHdl,WM_PASTE_MSG,
MPFROMSHORT(temphandle),NULL);
break;
case PMS_PASTE:
// client is requesting a copy of the Clipboard data
if(QMgrFindClient(temppid,&temphandle,&position))
WinPostMsg(ThreadWindowHdl,WM_PASTE,
MPFROMSHORT(temphandle),NULL);
break;
case PMS_TERMINATE:
// client is terminating or is no longer using PM Services
if(QMgrFindClient(temppid,&temphandle,&position))
{
// if not last slot, shift down 1
if(position != MAXCLIENTS-1)
memmove(&clients[position],&clients[position+1],
(sizeof(CLIENT)*(MAXCLIENTS-position-1)));
// clear last slot
memset(&clients[MAXCLIENTS-1],0,sizeof(CLIENT));
}
break;
default:
// unknown message rec'd, notify client if it has a queue
if(QMgrFindClient(temppid,&temphandle,&position))
MsgQSend(temphandle,NULL,0,PMS_MSG_UNKNOWN);
break;
}
if(msgdata)
DosFreeSeg(SELECTOROF(msgdata));
msgdata = NULL;
}
}
// finds PMServer client and qhandle in table
USHORT QMgrFindClient(PID pid,HQUEUE *qhandle,USHORT *position)
{
USHORT i;
for( i = 0; i < MAXCLIENTS; i++)
if(clients[i].clientpid = = pid)
{
*qhandle = clients[i].clientqhandle;
*position = i;
return TRUE;
}
return FALSE;
}
Figure 2
MSGQ.H
/* msgq.h Message Queue prototypes */
void MsgQCreate(HQUEUE *qhandle, char *qname);
USHORT MsgQOpen(HQUEUE *qhandle, char *qname);
void MsgQSend(HQUEUE qhandle, PVOID event, USHORT size, USHORT msg);
void MsgQClose(HQUEUE qhandle);
void MsgQGet(HQUEUE qhandle, PVOID *event, USHORT *size, USHORT *msg);
MSGQ.C
/* msgq.c message queue routines */
#define INCL_DOS
#include<os2.h>
#include<string.h>
#include<malloc.h>
#include"errexit.h"
#include"msgq.h"
typedef struct _qtable // message queue table structure
{
HQUEUE qhandle;
PID qowner;
} QTABLE;
#define MAXQUEUES 15
QTABLE qtable[MAXQUEUES]; // message queue table
qtableindex = 0;
// creates a new message queue
void MsgQCreate(HQUEUE *qhandle, char *qname)
{
USHORT retval;
if(retval = DosCreateQueue(qhandle,QUE_FIFO,qname))
error_exit(retval,"DosCreateQueue");
}
// opens an existing message queue
USHORT MsgQOpen(HQUEUE *qhandle, char *qname)
{
PID qowner;
USHORT retval;
if(retval = DosOpenQueue(&qowner,qhandle,qname)) // open queue
{
*qhandle = 0;
return retval;
}
qtable[qtableindex].qhandle = *qhandle; // put handle and PID
qtable[qtableindex].qowner = qowner; // into table
qtableindex++;
return 0;
}
// sends a message
void MsgQSend(HQUEUE qhandle, PVOID event, USHORT size, USHORT msg)
{
USHORT retval,i;
SEL sel,newsel;
PVOID qptr;
if(event != NULL) // if data with the message
{
for(i = 0; i < qtableindex; i++) // find the queue
if(qtable[i].qhandle = = qhandle)
break;
// create a segment for data
if(retval = DosAllocSeg(size,&sel,(SEG_GIVEABLE)))
error_exit(retval,"DosAllocSeg");
DosGiveSeg(sel,qtable[i].qowner,&newsel); // make it giveable
qptr = MAKEP(newsel,0);
memmove(qptr,event,size); // put the data in it
}
else
qptr = NULL;
// write message+data to q
if(retval = DosWriteQueue(qhandle,msg,size,(PBYTE)qptr,0))
error_exit(retval,"DosWriteQueue");
if((qptr != NULL) && (sel != newsel)) // free seg if not ours
DosFreeSeg(sel);
}
// get a message from a queue
void MsgQGet(HQUEUE qhandle, PVOID *event, USHORT *size, USHORT *msg)
{
QUEUERESULT qresult;
BYTE priority;
USHORT retval;
if(retval = DosReadQueue(qhandle,&qresult,size,
(PVOID FAR *)event,0x0000,DCWW_WAIT,&priority,0L))
error_exit(retval,"DosReadQueue");
*msg = qresult.usEventCode;
}
// close queue and remove from table
void MsgQClose(HQUEUE qhandle)
{
USHORT i;
DosCloseQueue(qhandle);
for(i = 0; i < qtableindex; i++)
if(qtable[i].qhandle = = qhandle)
break;
if(i != qtableindex)
memmove(&qtable[i],&qtable[i+1],
(sizeof(QTABLE)*(qtableindex -(i+1))));
qtableindex--;
}
Figure 7
PMACCESS.MAK
#
# make file for pmaccess.c
#
#COPT=/Lp /W3 /Zp /Zl /G2s /Ox /I$(INCLUDE) /Alfw
COPT=/Lp /W3 /Zpiel /G2s /I$(INCLUDE) /Alfw /Od
kbd.obj: kbd.c kbd.h msgs.h msgq.h kbddefs.h button.h
cl $(COPT) /c kbd.c
mou.obj: mou.c mou.h msgs.h msgq.h moudefs.h button.h errexit.h
cl $(COPT) /c mou.c
msgq.obj: msgq.c msgq.h
cl $(COPT) /c msgq.c
button.obj: button.c button.h
cl $(COPT) /c button.c
errexit.obj: errexit.c errexit.h
cl /c $(COPT) errexit.c
pmaccess.obj: pmaccess.c pmaccess.h moudefs.h kbddefs.h msgq.h msgs.h
mou.h kbd.h
cl /c $(COPT) pmaccess.c
pmaccess.exe: pmaccess.c pmaccess.mak msgq.obj button.obj errexit.obj \
mou.obj kbd.obj
cl $(COPT) pmaccess msgq button errexit kbd mou /link /co /noi llibcmt
markexe windowcompat pmaccess.exe
PMACCESS.H
/* pmaccess.h macros and typedefs for dipop.c */
#define BTOGGLE 0x0001
#define BPRESS 0x0002
#define INPUT 0X0003
typedef struct _button
{
char *text; // text to be displayed
USHORT startrow; // upper row
USHORT startcol; // left column
USHORT endrow; // lower row
USHORT endcol; // right column
BYTE attribute; // color attribute
USHORT type; // type of object
USHORT left_button_val; // mouse left button event
USHORT right_button_val; // mouse right button event
USHORT accelerator; // keyboard event
USHORT state; // button state: on or off
} BUTTON;
#define MOUMSG(ptr) ((MOUEVENTINFO *)ptr)
#define KBDMSG(ptr) ((KBDKEYINFO *)ptr)
PMACCESS.C
/* pmaccess.c application that uses PMSERVER for accessing PM services
This application is a VIO app that offers the following services:
1. It allows the user to position the cursor anywhere in the input
screen
2. It allows the user to type text into any part of the input
screen
3. It allows the user to mark any text in the input screen
4. It allows the user to copy, cut, or paste any part of the input
screen.
*/
#define INCL_SUB
#define INCL_DOSERRORS
#define INCL_DOS
#include<os2.h>
#include<mt\stdio.h>
#include<mt\string.h>
#include<mt\stdlib.h>
#include<mt\process.h>
#include<mt\dos.h>
#include"mou.h"
#include"kbd.h"
#include"pmserver.h"
#include"moudefs.h"
#include"kbddefs.h"
#include"msgq.h"
#include"button.h"
#include"errexit.h"
#include"msgs.h"
#define MAINMSGQUEUE "\\QUEUES\\PMACCESS.QUE"
#define VIOHDL 0
#define KEYTHREADSTACK 500 // keyboard thread stack
#define REQUESTTHREADSTACK 500 // request thread stack
#define MOUTHREADSTACK 800 // mouse thread stack
#define BORDERCOL 79
#define BORDERROW 21
#define STARTROW 0
#define ENDROW 20
#define BUTTON_ON 0x70
#define BUTTON_OFF 0x0f
#define NORMAL BUTTON_OFF
#define HIGHLIGHT BUTTON_ON
#define beep() DosBeep(440,200)
#define Lastchar(str) (str[strlen(str)-1])
#define MAXREQUESTCOUNT 10
#define REQUESTTHREADSLEEPTIME 3000L
/********************* Buttons ****************************/
#define BUTTONLIST 6
BUTTON buttonlist[BUTTONLIST] =
{
/* text title row,col,row,col,attr, type left val, right val, accelerator
*/
{" Copy ","", 22, 0, 0, 0, BUTTON_OFF, BPRESS, MSG_COPY, MSG_COPY, ALT_O,
0},
{" Cut ","", 22, 9, 0, 0, BUTTON_OFF, BPRESS, MSG_CUT, MSG_CUT,
ALT_U, 0},
{" Paste ","", 22,17, 0, 0, BUTTON_OFF, BPRESS, MSG_PASTE, MSG_PASTE,
ALT_P, 0},
{" Clear ","", 22,63, 0, 0, BUTTON_OFF, BPRESS, MSG_CLR, MSG_CLR,
ALT_C, 0},
{" Esc ","", 22,73, 0, 0, BUTTON_OFF, BPRESS, MSG_ESCAPE, MSG_ESCAPE,
ESC, 0},
{NULL, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
};
typedef struct _screen_label
{
PCHAR text;
SHORT row;
SHORT column;
} SCREEN_LABEL;
#define SCREEN_LABELS 2
SCREEN_LABEL screen_labels[SCREEN_LABELS] =
{
{"Clipboard Functions", BORDERROW, 3},
{"General Functions", BORDERROW, 63}
};
PCHAR mainmsgqueue = MAINMSGQUEUE;
BYTE MouThreadStack[MOUTHREADSTACK];
BYTE KbdThreadStack[KEYTHREADSTACK];
BYTE RequestThreadStack[REQUESTTHREADSTACK];
long MouSem = 0L, KbdSem = 0L;
unsigned BlankCell = 0x0f20;
USHORT dirrow = BORDERROW;
USHORT DDEstartrow,DDEstartcol,DDEendrow,DDEendcol;
USHORT rowBlockBeg,colBlockBeg,startBlock,ScreenRows,ScreenCols,blo cksize;
USHORT startrow,startcol,endrow,endcol;
BYTE highlight_att = HIGHLIGHT,normal_att = NORMAL;
PBYTE blockptr, bufptr;
HQUEUE pmshdl;
BYTE tempbuffer[8192];
USHORT clipboard_data = TRUE;
PIDINFO pidinfo;
/********************* Function Prototypes ********************/
void main(void);
void RequestThread(void);
void InitScreen(void);
void SetBlock(USHORT row, USHORT col);
void ResetScreen(void);
void BlankBlock(void);
void ResetBlock(void);
void ButtonPress(USHORT *eventcode);
USHORT prepare_CLPBRDblock(void);
void PMS_Init(HQUEUE serverhandle,char *qname);
void PMS_Terminate(HQUEUE serverhandle);
USHORT readtextblock(PCHAR buffer,
USHORT srow, USHORT scol, USHORT erow, USHORT ecol);
void main(void)
{
USHORT pause = 0, msg, capture, valid_block, msgsize, retval, i;
USHORT post_on = FALSE,row,col;
HQUEUE qhandle;
PVOID *msgdata;
BUTTON *b;
PBYTE p;
DosGetPID(&pidinfo); // get process id
MsgQCreate(&qhandle,MAINMSGQUEUE);
if(retval = MsgQOpen(&pmshdl,PMSERVERQUE))
error_exit(retval,"MsgQOpen - PM Server probably hasn't
started...");
DosSemSet(&MouSem);
DosSemSet(&KbdSem);
// start other threads
if(_beginthread(MouThread,MouThreadStack,
sizeof(MouThreadStack),NULL) = = -1)
error_exit(0,"_beginthread(Mouse)");
if(_beginthread(KbdThread,KbdThreadStack,
sizeof(KbdThreadStack),NULL) = = -1)
error_exit(0,"_beginthread(Keyboard)");
if(_beginthread(RequestThread,RequestThreadStack,
sizeof(RequestThreadStack),NULL) = = -1)
error_exit(0,"_beginthread(Request)");
DosSemWait(&MouSem,SEM_INDEFINITE_WAIT); // let threads get going
DosSemWait(&KbdSem,SEM_INDEFINITE_WAIT);
InitButtons();
InitScreen();
DisplayButtons();
VioGetBuf((PULONG)&bufptr, &msg, VIOHDL);
PMS_Init(pmshdl,MAINMSGQUEUE);
while(TRUE)
{
MsgQGet(qhandle, &msgdata, &msgsize, &msg);
if(ButtonEvent(msg))
ButtonPress(&msg);
switch(msg)
{
case MSG_ESCAPE:
case ESC:
MsgQClose(qhandle); // close the message queue
// clear the screen
VioScrollUp(0,0,-1,-1,-1,(char *)&BlankCell,VIOHDL);
PMS_Terminate(pmshdl);
DosExit(EXIT_PROCESS,0); // get out of dodge
break;
case MSG_CLR: // user clearing screen
VioScrollUp(0,0,BORDERROW-1,ScreenCols,0xffff,
(char *)&BlankCell,VIOHDL);
InitScreen();
DisplayButtons();
MsgQSend(pmshdl,&pidinfo.pid,sizeof(PID),PMS_CLPBRD_QUERY);
VioSetCurPos(0,0,VIOHDL);
break;
// ************* Mouse Message handling
case MSG_B1DOWN:
// clear any existing block and set up a new one
valid_block = FALSE;
ResetScreen();
capture = TRUE;
rowBlockBeg = MOUMSG(msgdata)->row;
colBlockBeg = MOUMSG(msgdata)->col;
startBlock = (rowBlockBeg * ScreenCols) + colBlockBeg;
SetBlock(rowBlockBeg,colBlockBeg);
break;
case MSG_MOUSEMOVED:
if(capture)
SetBlock(MOUMSG(msgdata)->row,
MOUMSG(msgdata)->col);
break;
case MSG_B1UP:
if(capture)
{
capture = FALSE;
valid_block = TRUE;
SetBlock(MOUMSG(msgdata)->row,
MOUMSG(msgdata)->col);
}
break;
// ************* Keyboard message handling
case MSG_CHAR:
VioWrtTTY(&(KBDMSG(msgdata)->chChar),1,VIOHDL);
switch(KBDMSG(msgdata)->chChar)
{
case '\b':
VioWrtTTY(" \b",2,VIOHDL);
break;
case '\r':
VioGetCurPos(&row,&col,VIOHDL);
if(row = = ScreenRows)
VioScrollUp(0,0,BORDERROW-1,ScreenCols,1,
(char *)&BlankCell,VIOHDL);
else
VioWrtTTY("\n",1,VIOHDL);
break;
}
break;
case MSG_UPARROW:
case MSG_DOWNARROW:
case MSG_LEFTARROW:
case MSG_RIGHTARROW:
case MSG_HOME:
case MSG_END:
VioGetCurPos(&row,&col,VIOHDL); // get current cursor
// position
switch(msg)
{
case MSG_UPARROW:
if(row > 0)
row--;
break;
case MSG_DOWNARROW:
if(row < ScreenRows)
row++;
break;
case MSG_LEFTARROW:
if(col > 0)
col--;
break;
case MSG_RIGHTARROW:
if(col < ScreenCols)
col++;
break;
case MSG_HOME:
col = 0;
break;
case MSG_END:
col = ScreenCols-1;
break;
}
VioSetCurPos(row,col,VIOHDL);
break;
// *******************Clipboard handling*****************
case MSG_COPY: // user copy to clipboard
case MSG_CUT: // user cut to clipboard
if(!valid_block) // if no block selected
{
beep(); // warn user
break; // forget it
}
i = prepare_CLPBRDblock();
// pass data to server
MsgQSend(pmshdl,tempbuffer,i,PMS_COPY);
if(msg == MSG_CUT) // reset screen
BlankBlock();
else
ResetBlock();
valid_block = FALSE;
break;
case MSG_PASTE: // user wants to paste
MsgQSend(pmshdl,&pidinfo.pid,sizeof(PID),PMS_PASTE);
break;
// **************** PMServer message handling ***********
case PMS_CLPBRD: // clpbrd data is available
clipboard_data = TRUE;
findbutton(" Paste ", &b);
ButtonPaint(b,BUTTON_ON);
break;
case PMS_CLPBRD_EMPTY: // clpbrd is empty
clipboard_data = FALSE;
findbutton(" Paste ", &b);
ButtonPaint(b,BUTTON_OFF);
break;
case PMS_CLPBRD_DATA: // clipboard data rec'd
VioGetCurPos(&row,&col,VIOHDL); // get current cursor
// position
for(i = 0, p = (PBYTE)msgdata; *p; p++)
if(*p == 13) // if a linefeed
i++; // # of lines in text
if(row+i > ScreenRows)
{
int j = (row+i)-ScreenRows;
VioScrollUp(0,0,BORDERROW-1,ScreenCols,j,
(char *)&BlankCell,VIOHDL);
row -= j;
VioSetCurPos(row,col,VIOHDL);
}
VioWrtTTY((PCH)msgdata,msgsize-1,VIOHDL);
MsgQSend(pmshdl,&pidinfo.pid,sizeof(PID),
PMS_CLPBRD_QUERY);
break;
case PMS_NO_INIT: // server can't initialize
error_exit(msg,"PMS_Init - unable to access PM Server");
break;
case PMS_INIT_ACK: // server initialized
clipboard_data = FALSE; // then fall thru
default:
break;
}
if(msgdata != NULL)
DosFreeSeg(SELECTOROF(msgdata));
}
}
// terminates PMServer connection
void PMS_Terminate(HQUEUE serverhandle)
{
// terminate connection
MsgQSend(serverhandle,&pidinfo.pid,sizeof(PID),PMS_TERMINATE);
MsgQClose(serverhandle); // close queue
}
// initializes PMServer connection
void PMS_Init(HQUEUE serverhandle,char *qname)
{
CLIENTDATA clientdata;
strcpy(clientdata.qname,qname); // set up qname
clientdata.pid = pidinfo.pid; // set up PID
// initialize connection
MsgQSend(serverhandle,&clientdata,sizeof(clientdata),PMS _INIT);
}
// prepare data for clipboard
USHORT prepare_CLPBRDblock(void)
{
return readtextblock(tempbuffer,startrow,startcol,endrow,endcol);
}
// read text from screen
USHORT readtextblock(PCHAR buffer,USHORT srow, USHORT scol, USHORT erow,
USHORT ecol)
{
USHORT i,row,len,col;
for( row = srow, col = scol, i = 0; row <= erow; row++)
{
len = ScreenCols;
if(row != srow)
col = 0;
if(row = = erow)
len = ecol+1;
VioReadCharStr(&buffer[i],&len,row,col,VIOHDL);
i += len-1;
while(buffer[i] = = ' ')
i--;
buffer[++i] = '\r';
buffer[++i] = '\n';
i++;
}
buffer[i] = '\0';
return i+1; // return length of block
}
// set screen text block
void SetBlock(USHORT row, USHORT col)
{
USHORT endBlock = (row * ScreenCols) + col;
USHORT presize,postsize,start;
if(startBlock <= endBlock) // block end at/after start
{
// normal from top to startBlock-1,
// highlight from startBlock to endBlock
// normal from endBlock+1 to bottom
presize = startBlock;
startrow = rowBlockBeg;
startcol = colBlockBeg;
blocksize = endBlock-startBlock+1;
endBlock++;
endrow = (endBlock/ScreenCols);
endcol = (endBlock%ScreenCols);
postsize = (ScreenRows*ScreenCols)-endBlock+1;
}
else // block end before start
{
// normal from top to endBlock-1
// highlight from endBlock to startBlock
// normal from startBlock+1 to bottom
presize = endBlock;
startrow = row;
startcol = col;
blocksize = startBlock-endBlock+1;
start = startBlock+1;
endrow = (start/ScreenCols);
endcol = (start%ScreenCols);
postsize = (ScreenRows*ScreenCols)-start+1;
}
blockptr = bufptr+presize; // set to beginning of block
// now write pre-block and post-block normal; highlight block
VioWrtNAttr(&normal_att,presize,0,0,VIOHDL);
VioWrtNAttr(&highlight_att,blocksize,startrow,startcol,V IOHDL);
VioWrtNAttr(&normal_att,postsize,endrow,endcol,VIOHDL);
}
// reset screen work area
void ResetScreen(void)
{
USHORT i;
for( i = STARTROW; i <= ENDROW; i++)
VioWrtNAttr(&normal_att,BORDERCOL,STARTROW,0,VIOHDL);
}
// blank screen text block
void BlankBlock(void)
{
VioWrtNCell((PBYTE)&BlankCell,blocksize,
startrow,startcol,VIOHDL);
}
// reset screen to normal
void ResetBlock(void)
{
VioWrtNAttr(&normal_att,(ScreenCols*ScreenRows),0,0,VIOHDL);
}
// initialize screen
void InitScreen(void)
{
SHORT i;
VIOMODEINFO modeinfo;
VioGetMode(&modeinfo,VIOHDL); / get screen mode, rows, cols
ScreenCols = modeinfo.col;
ScreenRows = modeinfo.row;
if(ScreenRows > BORDERROW-1)
ScreenRows = BORDERROW-1;
VioScrollUp(0,0,-1,-1,-1,(char *)&BlankCell,VIOHDL);
// clear screen
VioSetCurPos(0,0,VIOHDL); // set cursor
VioWrtNChar("-",ScreenCols,BORDERROW,0,VIOHDL);
// draw border line
for( i = 0; i < SCREEN_LABELS; i++) // write labels
VioWrtCharStr(screen_labels[i].text,
strlen(screen_labels[i].text),
screen_labels[i].row,screen_labels[i].column,VIOHDL);
}
// manipulate buttons for specific event code and button type
void ButtonPress(USHORT *eventcode)
{
BUTTON *b = &buttonlist[0];
MOUSECODEOFF(*eventcode); // turn off mouse bit
for( ; b->text; b++) // find the button
if((b->left_button_val = = *eventcode) ||
(b->right_button_val = = *eventcode) ||
(b->accelerator = = *eventcode))
{
switch(b->type)
{
case BPRESS: // if a press button
ButtonPaint(b,BUTTON_ON); // turn it on
DosSleep(100L); // wait
ButtonPaint(b,BUTTON_OFF); // turn it off
break;
case BTOGGLE: // if a toggle button
b->state = !b->state; // toggle it
// and toggle the color
ButtonPaint(b,(b->state ?
(BYTE)BUTTON_ON : b->attribute));
break;
default:
break;
}
return;
}
}
/******* end of main thread code **************************/
/******* start of request thread code *********************/
// periodically wakes up and queries PMServer about Clipboard
void RequestThread(void)
{
USHORT count = 0;
while(TRUE)
{
if(!clipboard_data) // if flag invalidated
// ask if pasting possible
MsgQSend(pmshdl,&pidinfo.pid,sizeof(PID),PMS_CLPBRD_QUERY);
DosSleep(REQUESTTHREADSLEEPTIME); // sleep a while
count++;
if(count >= MAXREQUESTCOUNT) // if count exceeded limit
{
clipboard_data = FALSE; // invalidate flag
count = 0; // reset count
}
}
}
/******* end of request thread code ***********************/
Figure 8
BUTTON.H
/* button.h macros and typedefs for button.c */
#define BTOGGLE 0x0001
#define BPRESS 0x0002
#define INPUT 0X0003
typedef struct _button
{
char *text; // text to be displayed
char *title; // button title (optional)
USHORT startrow; // upper row
USHORT startcol; // left column
USHORT endrow; // lower row
USHORT endcol; // right column
BYTE attribute; // color attribute
USHORT type; // type of object
USHORT left_button_val; // mouse left button event
USHORT right_button_val; // mouse right button event
USHORT accelerator; // keyboard event
USHORT state; // button state: on or off
} BUTTON;
void InitButtons(void);
void ButtonInit(BUTTON *b);
void ResetButtons(void);
void findbutton(char *text,BUTTON **bptr);
void DisplayButtons(void);
void ButtonPaint(BUTTON *b, BYTE attribute);
void ButtonDisplay(BUTTON *b);
BUTTON.C
/* button.c text-button functions */
#define INCL_SUB
#include<os2.h>
#include<string.h>
#include"button.h"
#define VIOHDL 0
extern BUTTON buttonlist[];
void InitButtons(void)
{
BUTTON *b = buttonlist;
for( ; b->text; b++)
ButtonInit(b);
}
void ButtonInit(BUTTON *b)
{
b->endrow = (b->startrow+2); // startrow+#of ptrs-1
// startcol+strlen of text-1
b->endcol = (b->startcol+strlen(b->text)+1);
}
void ResetButtons(void)
{
BUTTON *b = buttonlist;
for( ; b->text; b++)
{
b->state = 0;
ButtonPaint(b,b->attribute);
}
}
void findbutton(char *text,BUTTON **bptr)
{
BUTTON *b = buttonlist;
for( ; b->text; b++)
if(!strncmp(b->text,text,strlen(text)))
{
*bptr = b;
return;
}
}
void DisplayButtons(void)
{
BUTTON *b = buttonlist;
for( ; b->text; b++)
ButtonDisplay(b);
}
void ButtonDisplay(BUTTON *b)
{
BYTE cell[2];
USHORT row = b->startrow;
USHORT endcol = b->endcol;
USHORT startcol = b->startcol;
char *text = b->title;
USHORT len = endcol - startcol - 1;
cell[0] = '=';
cell[1] = b->attribute;
// write the 1st corner char
VioWrtCharStrAtt("",1,row,startcol,&cell[1],VIOHDL);
// write the top line
VioWrtNCell(cell,len,row,startcol+1,VIOHDL);
// write the 2nd corner char
VioWrtCharStrAtt("",1,row,endcol,&cell[1],VIOHDL);
if(*text) // if title, write it
VioWrtCharStrAtt(text,strlen(text),row,startcol+2,
&cell[1],VIOHDL);
// write the left border
VioWrtCharStrAtt("",1,++row,startcol,&cell[1],VIOHDL);
text = b->text; // reset pointer
// write the message
VioWrtCharStrAtt(text,strlen(text),row,
startcol+1,&cell[ 1],VIOHDL);
// write the right border
VioWrtCharStrAtt("",1,row,endcol,&cell[1],VIOHDL);
// write the 3rd corner
VioWrtCharStrAtt("",1,++row,startcol,&cell[1],VIOHDL);
// write the bottom line
VioWrtNCell(cell,len,row,startcol+1,VIOHDL);
// write the 4th corner
VioWrtCharStrAtt("",1,row,endcol,&cell[1],VIOHDL);
}
void ButtonPaint(BUTTON *b, BYTE attribute)
{
USHORT row = b->startrow;
USHORT col = b->startcol;
USHORT endrow = b->endrow;
USHORT num = b->endcol-col+1;
for( ; row <= endrow; row++)
VioWrtNAttr(&attribute,num,row,col,VIOHDL);
}
Figure 9
KBD.H
void KbdThread(void);
KBD.C Source Code
/* keyboard thread code for PMAccess */
#define INCL_SUB
#define INCL_DOS
#include<os2.h>
#include"kbd.h"
#include"kbddefs.h"
#include"msgq.h"
#include"button.h"
#include"msgs.h"
extern long KbdSem;
extern PCHAR mainmsgqueue;
extern BUTTON buttonlist[];
USHORT AcceleratorPressed(unsigned char key);
void KbdThread(void)
{
KBDINFO kbdinfo;
KBDKEYINFO KbdKeyInfo;
HKBD KbdHandle = 0;
HQUEUE qhandle;
USHORT event;
MsgQOpen(&qhandle,mainmsgqueue);
KbdFlushBuffer(KbdHandle); // flush keyboard buffer
KbdGetStatus(&kbdinfo,KbdHandle); // get keyboard status
kbdinfo.fsMask &= ~COOKED; // turn off COOKED bit
kbdinfo.fsMask |= RAW; // turn on RAW bit
KbdSetStatus(&kbdinfo,KbdHandle); // set the keyboard status
DosSemClear(&KbdSem); // notify main thread
while(TRUE)
{
KbdCharIn(&KbdKeyInfo,IO_WAIT,KbdHandle); // get a character
if(KbdKeyInfo.chChar) // if Ascii code
MsgQSend(qhandle,&KbdKeyInfo,sizeof(KbdKeyInfo),MSG_CHAR);
else if(event = AcceleratorPressed(KbdKeyInfo.chScan)) //
Accelerator?
MsgQSend(qhandle,NULL,0,event); // if so, pass it on
else
MsgQSend(qhandle,&KbdKeyInfo,sizeof(KbdKeyInfo),
SCANCODE(KbdKeyInfo.chScan));
DosSleep(32L);
}
}
USHORT AcceleratorPressed(unsigned char key)
{
BUTTON *b = &buttonlist[0];
for( ; b->text; b++)
if(key = = (unsigned char)b->accelerator)
return MOUSECODE(b->left_button_val);
return 0;
}
Figure 10
MOU.H
void MouThread(void);
MOU.C
/* mou.c mouse thread code for PMAccess */
#define INCL_DOS
#define INCL_SUB
#include<os2.h>
#include"mou.h"
#include"moudefs.h"
#include"msgq.h"
#include"button.h"
#include"msgs.h"
#include"errexit.h"
extern long MouSem;
extern PCHAR mainmsgqueue;
extern BUTTON buttonlist[];
extern USHORT dirrow;
typedef struct _moubuttons
{
USHORT mask;
USHORT movedmask;
USHORT down;
USHORT upmsg;
USHORT downmsg;
} MOUBUTTONS;
MOUBUTTONS buttons[3] =
{
{ MOU_B1, MOUSE_MOTION_WITH_BN1_DOWN, FALSE, MSG_B1UP, MSG_B1DOWN },
{ MOU_B2, MOUSE_MOTION_WITH_BN2_DOWN, FALSE, MSG_B2UP, MSG_B2DOWN },
{ MOU_B3, MOUSE_MOTION_WITH_BN3_DOWN, FALSE, MSG_B3UP, MSG_B3DOWN }
};
USHORT ButtonPressed(MOUEVENTINFO *ev);
void MouThread(void)
{
MOUEVENTINFO MouEvent;
USHORT WaitOption = MOU_WAIT; // set to block on input
HQUEUE qhandle;
USHORT buttondown = FALSE, numbuttons,i;
USHORT retval, mouse_moved,event;
HMOU MouHandle;
MsgQOpen(&qhandle,mainmsgqueue);
if((retval = MouOpen((PSZ)NULL,(PHMOU)&MouHandle)))
error_exit(retval,"MouOpen");
MouDrawPtr(MouHandle); // display mouse pointer
MouFlushQue(MouHandle); // flush mouse queue
MouGetNumButtons(&numbuttons,MouHandle); // get button count
DosSemClear(&MouSem); // notify main thread
while(TRUE)
{ // read the queue
MouReadEventQue(&MouEvent,&WaitOption,MouHandle);
MouEventDropLowBit(MouEvent); // turn off the low bit
// notify if screen button pressed
if(!buttondown && IsMouButtonPressed(MouEvent))
// if mouse button pressed
if(event = ButtonPressed(&MouEvent))
// while on screen button
{
MsgQSend(qhandle,NULL,0,event);
buttondown = TRUE;
DosSleep(32L);
continue;
}
buttondown = FALSE;
if(MouEvent.row >= dirrow) // non-PM-like line:
continue; // protecting mouse buttons
for( i = 0, mouse_moved = FALSE; i < numbuttons; i++)
// if the button is down now
if(MouButtonPressed(MouEvent,buttons[i].mask))
{
if(!buttons[i].down) // if button was previously up
{
MsgQSend(qhandle,&MouEvent,sizeof(MouEvent),
buttons[i].downmsg);
buttons[i].down = TRUE;
}
// if the button was previously down but the mouse moved
else if(MouButtonPressed(MouEvent,buttons[i].movedmask))
mouse_moved = TRUE;
}
else // if button is not down now
{
if(buttons[i].down) // if button previously down
{
buttons[i].down = FALSE;
}
}
// notify of all mouse movement if button is down (PM gets all
// movement)
if(mouse_moved)
MsgQSend(qhandle,&MouEvent,sizeof(MouEvent),MSG_MOUSEMOVED);
DosSleep(32L);
}
MouClose(MouHandle);
}
USHORT ButtonPressed(MOUEVENTINFO *ev)
{
register USHORT row = ev->row, col = ev->col;
BUTTON *b = &buttonlist[0];
for( ; b->text; b++)
{
if((row >= b->startrow) && (row <= b->endrow) // if on button
&& (col >= b->startcol) && (col <= b->endcol))
if(MouB1Pressed(ev->fs))
return MOUSECODE(b->left_button_val);
else if(MouB2Pressed(ev->fs))
return MOUSECODE(b->right_button_val);
else
return 0;
}
return 0;
}
A Presentation Manager Primer
In the Presentation Manager programming environment, the program
architecture is largely the reverse of that found under DOS and most OS/2
kernel programs.
DOS programs are essentially active: a program must actively seek out input,
and format its own screen output. If you are programming for DOS, you must
design the program so that it can efficiently obtain and process input and
manage output. This structure places most of the overhead of managing I/O on
the program, so that you have to write code that juggles the program's
attention between different I/O modules, in an effort to see that an
incoming event is not lost or a user kept waiting too long. The same is
largely the case with OS/2 kernel programming, with the exception that you
can use multiple threads to manage diverse sources of input.
Under PM, a program is mostly reactive. Instead of actively seeking input, a
program remains in a fundamentally idle state. When PM receives input such
as a keypress or a mouse click, it routes the input to the appropriate
application in the form of messages. The application, after receiving a
message, makes the appropriate response to that event and returns to its
idle state. If the application needs to display text or graphics in a
window, it calls PM to do so. Thus, PM applications are said to be
message-based and event-driven.
Objects and Windows
PM's design was greatly influenced by the success of other object-oriented
programming environments. A PM window is an object, the building-block upon
which PM applications are built. A window is that part of a PM application
that actually receives, processes, and responds to messages sent to it by PM
or other windows. A PM window can have its own set of routines, and can have
data associated with it like a mini-program. The messages that a window
receives determine the operations that it performs on its data.
A window can interact with users, but only through PM. If a user clicks on a
control (such as a scroll bar) that belongs to a window, PM sends the window
a message telling it what happened. The window responds to the user by
calling PM functions, which is the equivalent of sending a message back to
PM.
PM offers many built-in objects. Some are visible, such as controls
(scroll-bars, list boxes, mouse pointers, and so on); while others, such as
anchor blocks, bitmaps, message queues, and metafiles, may not be. You can
also create your own customized windows to complete an application. The PM
programmer's primary task is to define the appropriate windows, their
attributes, and their responses to specific messages. PM windows can be
classed and subclassed. You can create more than one of a particular type or
class of window, in which each inherits the characteristics of the original
window but addresses a different aspect of an application's needs (a
subclass). Each window is unique whatever its class; it is identified by a
32-bit value or window handle.
"Window" does not necessarily refer to one of the overlapping frames of
information that appear on the PM screen. Although a window may create a
screen window to display its output or to interact with the user, the
creation of a window does not imply that its actions are visible onscreen.
An application composed of one or more windows may have none that are
visible. PMServer, for example, creates a window that does not produce any
visible screen activity other than PMServer's entry in the Task Manager
window, and even that can be eliminated.
PM Messages
PM messages tell a PM object what to do. You rarely get to see how an object
works unless you write it yourself. Each object is a black box--you must
look at the documentation to find out what messages it responds to and the
type of response. Each message contains arguments that give the object more
information to act on the message. The messages are stored in structures of
type QMSG, which you can find in PMWIN.H together with the predefined PM
messages and macros for accessing and interpreting them. PM can generate
about 100 general-purpose messages and about 150 special-purpose messages,
which service controls such as dialog box windows and scroll bars. You can
also create messages to use in your own applications to communicate between
windows.
Messages always contain an identifying value; they may also contain the
handle of the window to which the message is addressed. The message can
include two message parameters, each a 32-bit value that can contain
combinations of 16- and 8-bit values. Macros are supplied in PMWIN.H for
extracting or setting these values. The time the message originated and the
x and y coordinates of the mouse pointer at that moment are also included in
the message.
Message Queues
PM has two types of message queues--the system message queue and the
application message queue. When an input event occurs, PM converts the
information about the event into a message and places the message in its
system message queue. This message queue has the capacity for holding
information about 60 keystrokes or mouse clicks. A PM component, called the
input router, routes each message to the application that had input focus at
the time the message was generated and places the message in that
application's message queue. Other types of messages, such as window create
and destroy messages and Clipboard messages, are placed directly into the
message queue of the target application; they don't need to go through the
system message queue.
Every PM application has a message queue, an attribute that fundamentally
distinguishes it from an OS/2 kernel program. Once an application creates a
PM message queue, subsequent input must come to the application through PM.
Therefore, the Kbd, Mou, and Vio subsystems are no longer available to the
application--with the minor exception of Advanced Vio (AVio) calls, which
are managed by PM. After creating a PM message queue, every program running
in the application's screen group will have to use PM for subsequent I/O--a
consequence that has profound implications for kernel applications. For
instance, if you run (via DosExecPgm) a PM program from an OS/2 kernel
application that is running in a screen group other than PM's, the kernel
application will not be able to interface with the Kbd, Vio, or Mou
subsystems until the PM application destroys its message queue just before
termination. The same is also true for every other program in the
application's screen group.
As mentioned earlier, windows wait for messages to be sent to them by PM.
When a window receives a message, the message is processed by a window
procedure, a function that is registered with PM right after an application
creates its message queue. Every window has a window procedure associated
with it that is written to expect the same arguments (comprising several of
a message's components); PM calls the function with the appropriate
arguments for each message received by the application. The application
itself doesn't call the window procedure directly. Instead, PM calls the
window procedure when the window receives a message.
PM's built-in objects have their own window procedures, but programmers can
customize windows by supplying a window procedure, thus controlling what the
window actually does. The backbone of a window procedure is a C switch
statement, with a case for every message specifically handled by the window.
This block of code is called the message handler. There's no need to have a
case in the message handler for every possible message. One for each message
you specifically want the window to process is sufficient. The unprocessed
messages are passed on to a default window procedure provided by PM. Each
successfully processed message should result in a return value of FALSE;
unsuccessful ones should return TRUE. Message processing should also be
efficient. I recommend placing any code that requires more than 1/10th of a
second to execute in a separate thread.
There are two ways to pass messages from one window to another. A window can
send a message to another window directly by calling WinSendMsg. In this
case, the sender must wait until the target window has processed the message
(that is, while PM calls the target's window procedure to process the
message and return). Alternatively, a window can post a message to the other
window, which places the message in the target window's messge queue and
returns immediately. Using the second method, the window sourcing the
message does not wait while the target window processes it. PMServer's
second thread uses this method; it calls WinPostMsg to post messages to
PMServer's window.
PMServer's PM Code
Setting up and initializing a PM application with a message queue and a
window requires only 10-20 lines of code. The remainder of the
application is largely contained in the window procedure. This setup code
can be kept as a template and pasted into a source file when you're writing
a new program. As you can see, PM programming focuses on choosing which
controls and attributes an application's windows should have, what messages
they should respond to, and what the responses will be.
Look at PMServer's window startup code. Note that virtually every
identifier, macro, or window function discussed here can be found in
PMWIN.H. You might also note that because PMServer does not have any visible
windows, many of the parameters to these functions are either 0 or NULL.
PMServer's PM-specific code begins in the main function with the call to
WinInitialize. A thread should call this function before it calls any other
PM functions. The function returns an anchor block handle that is used in
many of the subsequent PM calls. This handle refers specifically to this
application.
Next, PMServer calls WinCreateMsgQueue, which creates the application
message queue and returns a handle to it. This function's second parameter
specifies the maximum queue size or 0 to accept the default queue size
(currently 10).
The next call is to WinRegisterClass. You must register every window's class
with PM before creating one or more instances of the window. Here, this
function creates a class of windows called "PMServer," designated by the
string passed as the second parameter. The third parameter is the address of
the window procedure, ClientWndProc. PMServer does not use the remaining
parameters. The fourth parameter specifies the default window style for any
instance of this window class, and the last parameter specifies the number
of bytes of reserved storage that can be allocated for each window in the
class.
At this point, PMServer has initialized itself and created a message queue,
but it hasn't yet created a window. The call to WinCreateStdWindow actually
creates a window and returns a handle to it. This function creates a window
that is a child of the PM desktop (the ancestor of all PM windows); thus,
the HWND_DESKTOP identifier represents the handle of the desktop window. The
second parameter specifies the frame window styles used to create the
window. PMServer does not use any of these, so 0L is passed. However,
PMServer does use one of the options that control how the frame window is
created; the next parameter, FCF_TASKLIST, adds the window to the Task
Manager's switch list. The window class name in the next parameter tells
WinCreateStdWindow that the window will be an instance of the previously
registered PMServer class. The window class name is followed by the window's
title bar text, which is NULL because PMServer has no title bar.
WinCreateStdWindow has four additional parameters, but PMServer uses only
one of them. The parameter that follows the title bar text specifies the
styles for client windows. Although none of these are used, the omission of
WS_VISIBLE ensures that PMServer will not be visible. The next two
parameters are a handle to a resource module (that contains definitions of
other PM objects such as menus) if one is provided, followed by the frame
window identifier in the resource file. The last parameter is the variable
that receives the handle of the newly created client window.
At this point PMServer is ready to receive and process messages. The while
loop containing WinGetMsg and WinDispatchMsg will retrieve a message from
PMServer's message queue and dispatch it to the newly created window. The
program will remain in this loop as long as PM does not place a WM_QUIT
message in the window's message queue. The WinGetMsg function requires the
anchor block handle, the address of a QMSG structure in which to place the
newly retrieved message, and three additional parameters (not used by
PMServer) that allow you to filter the messages as they are received. The
WinDispatchMsg function, on the other hand, calls PM to dispatch the message
to the window procedure associated with the message queue. WinDispatchMsg
will return when the window procedure has finished processing the message.
Once the program breaks out of the loop, only a few steps remain before the
program is terminated. WinDestroyWindow obviously destroys the window whose
handle is passed to it, as well as any child windows that belong to it.
WinDestroyMsgQueue terminates the message queue, and WinTerminate will
terminate the thread's use of PM.
As you can see, PMServer's PM setup and initialization code is relatively
short: the bulk of the program resides in the window procedure. The version
of PMServer in a future article will include DDE support to the window
procedure, with few changes to the PM code in the main function.
Simplifying Complex Windows Development Through the Use of a Client-Server
Architecture
Scott Kerber
The Microsoft Windows program is a fertile environment in which to develop
applications that can communicate with one another in a prescribed manner.
Windows1 supports numerous methods to encapsulate software functions
including libraries, dynamic-link libraries (DLLs), and client-server
applications. The communication mechanisms available in Windows, such as
message passing, user-definable message types, global data sharing, and
multitasking, make the implementation of client-server architectures
particularly convenient.
This article explores the Windows facilities for developing server-based
applications as implemented in a program called WinTrieve. A server is any
application offering a service that can be reached from any other
application running on the system. Servers accept requests, perform their
service, and return the results to the application that requested the
service. A client is an application that sends a request to a server and
waits for a response. Often communication between client and server is
synchronous; in other words, the client must wait for the results of a
request before continuing to process. Typically, the client-server
relationship is many-to-one: several clients are making concurrent requests
to a single server.
The benefits of the client-server model include reduced client program size,
the ability to take advantage of Windows multitasking, and the ability to
move the server more easily into a networked environment. These benefits
provide a basis for the design and implementation of server-specific
protocols. The ideas here are for servers that either do not require the
complexity or whose model of operation is inappropriate for utilizing an
asynchronous protocol such as the Dynamic Data Exchange (DDE). Although both
synchronous and asynchronous communications are supported in Windows, this
article concerns synchronous communications between client and server.
Examples (illustrations and group samples) of these concepts use WinTrieve,
which was developed by The Whitewater Group. WinTrieve consists of the
WinTrieve server, a C language Application Programming Interface (API), and
a set of Actor language classes for accessing the WinTrieve server. (Actor
is an object-oriented language and development environment for Windows
developed by The Whitewater Group.) The WinTrieve server is an Indexed
Sequential Access Method (ISAM) file manager that runs as a separate
application under Windows. It supports multiple concurrent access to ISAM
files, including file and record level locking and journaling. WinTrieve is
used to illustrate techniques for writing server applications and for
designing protocols under Windows.
Design Goals of WinTrieve
A major design goal of WinTrieve was to minimize the amount of memory
required from the client application's address space. Many hardware
configurations support Windows' ability to remove applications temporarily
from memory and thus map more applications into the limited 1Mb space
available. If many applications are running concurrently, Windows must
continually swap program segments, which often results in severe performance
degradation. (For an explanation of Windows memory management, see "EMS
Support Improves Microsoft Windows 2.0 Application Performance," MSJ, Vol.
3, No. 1.) Minimizing the required memory is a general concern for anyone
developing large applications targeted for a user environment that must
support several simultaneously running applications.
Another design goal was to make the low-level interface
language-independent. WinTrieve is targeted for C and Actor language
developers. C is supported through an Application Program Library (APL) and
Actor through a set of classes known as a class library.
Under tight memory constraints WinTrieve had to be carefully segmented to
allow competing applications as much memory as possible. Other long-term
goals for WinTrieve include support for networks (it could possibly be used
as a centralized network server), record change notification, and record
lock and unlock notification.
These design goals were best met by the server approach, which minimizes
client address space requirements, supports language-independent interfaces,
has minimal protocol overhead, and takes maximum advantage of Windows memory
management.
The Client-Server Model
Most people familiar with the client-server model equate it with networked
environments, but the client-server model can be used in any environment
that supports interprocess communications, such as Windows.
Generally, the lifespans of servers and clients are different. A server
starts execution before client-server interaction begins and continues to
accept requests and send responses without ever terminating. A client, on
the other hand, usually terminates after making a finite number of requests
to the server. To make requests, a client must connect to the server. A
connection identifies a communications path for both client and server.
Figure 1 shows several client applications accessing a single server. To
connect, the client must be able to identify the server of interest. Methods
for doing this under Windows are discussed later.
The life of a connection is known as a session, which consists of all
requests and responses from the time that a connection is accepted by the
server to the time that the client terminates the connection. A session is
governed by an agreed-upon method of communications known as a protocol,
which defines the communication conventions used by client and server.
In many cases, servers must be more complex than outlined here. A server
must be able to protect itself against malformed requests or against
requests that will abort the server. In preemptive environments, a server
may also have to handle multiple concurrent requests. In nonpreemptive
Windows, a server tends to behave more like a DLL with a single entry point.
Alternate Approaches
The most common alternate methods to the client-server approach of providing
shared services under Windows are object libraries,
terminate-and-stay-resident (TSR) programs, and DLLs.
Object libraries have been available on most computing systems for some
time. In DOS2, object libraries are created with the LIB utility, and
filenames have the LIB extension. When a program is linked, all program
object modules and object code in libraries that are referenced by the
program are combined into one executable file. Object library code is
physically copied into the program's executable file when it is linked. This
creates much redundancy of code. Each running program has its own copy of
common library modules. This wastes memory and reduces the address space
available to the application code.
Using object libraries involves other considerations. For example, WinTrieve
maintains tables that contain locking information about open files and
records. Using object libraries requires that each executable file has
access to a central lock table. The only true way to share memory among
applications in Windows is to allocate memory from the nonbanked area of the
global heap. Unfortunately, nonbanked memory is quite limited and used for
many purposes by Windows, so allocating large chunks of unbanked memory is
unwise. Using the server approach allows the lock table to be allocated
within the server's own address space. Access to the lock table is limited
to the server itself.
Terminate-and-stay-resident (TSR) programs are similar to the server
approach. A TSR can be written in the form of a library. Only one copy
executes at a time, eliminating the code redundancy problem inherent in
linkable libraries. An application communicates with a TSR through a
software interrupt. This type of TSR is known as a passive TSR because it
responds only when explicitly invoked by another program. An active TSR is
generally invoked by a hardware interrupt or keystroke.
A TSR is started from DOS before Windows is run, which causes a problem. DOS
loads the TSR into low memory (see Figure 2); Windows is loaded on top of
the TSR, starting from the lowest available memory. Depending on the size of
the TSR, the amount of memory available for Windows to run other
applications could be significantly reduced. For example, WinTrieve requires
approximately 120Kb total memory to run. If it were a TSR, 120Kb less memory
would be available for Windows. Windows has no options with a TSR, because
it can neither place the TSR in expanded memory (EMS), nor move or swap
segments to take advantage of available memory.
Dynamic-link libraries are the third alternative and also the most viable
under Windows. Windows itself consists of several DLLs. DLLs, put simply,
are shared libraries. A DLL that has been referenced by an application is
linked (external references to the DLL are resolved) to that application at
run time. This process is known as dynamic linking.
DLLs have many excellent features. For example, only one instance of a DLL
need reside in memory for all of the applications that reference it. This
solves the code redundancy problem inherent in object libraries. DLLs can be
segmented and code segments can be discarded when Windows needs memory for
other purposes. A DLL can have its own single data segment that is used for
global and static variable allocation and the local heap. For example, if
WinTrieve were a DLL, it could allocate memory for the lock table from its
local heap and then reference it with impunity. A DLL uses the stack of the
calling program.
Ultimately a DLL that is referenced by a program must be mapped into the
program's address space. Therefore, the DLL may limit the overall size of
the application. Figure 3 shows a sample memory map of an application and
WinTrieve if written as a DLL. Also, a DLL's data segment is allocated from
nonbanked memory, which is a limited resource; an application's data segment
is allocated from the banked portion of Windows memory. Banked memory is
often a more plentiful resource than nonbanked memory.
In addition to supporting bank-switching memory, Windows supports
discardable segments. If segments can be discarded, an application's total
size can be much larger than available memory. Windows decides which
segments to discard based on a least recently used (LRU) algorithm. The
application's developer can determine how Windows manages segments. An
application's module definition file (DEF) specifies program segment
attributes, such as whether a segment is discardable, movable, fixed,
preloaded, or loaded on call. Code segments are usually marked discardable.
Why then should the size of a DLL cause any concern? As an application runs,
it normally settles on a core of data and code that it uses most often. This
core is known as an application's working set. Occasionally the application
will go outside its working set, which might require code segments to be
discarded and others loaded into memory. If this happens seldom or
incrementally, the user should not notice. But if memory is insufficient to
contain an application's working set and Windows must continually discard
code segments to load in others, the result will be severe performance
degradation, a phenomenon known as thrashing.
For example, Actor requires a small code space but a very large data space.
Windows has few options in this case. Either the application won't run
because it and the minimal space required for the DLL exceed available
memory bounds, or performance becomes intolerable because segment swapping
occurs at an inordinate rate.
Empirical studies have shown that WinTrieve has a working set size of
approximately 100Kb, including its data segment and several code segments.
If only a smaller amount of memory is available, Windows will have to
continually swap commonly used code segments. For example, a routine
operation in an ISAM application is to read sequentially all records in a
file and calculate a total value from a field in each record. If for each
read operation performed Windows must discard and load several segments,
performance will be greatly reduced.
Using the server approach, Windows can place WinTrieve in a separate memory
space and bank it into 1Mb physical address space when a client sends a
request to it. Figure 4 shows a sample memory map of this situation. Now
compare Figure 4 and Figure 3. The overhead of doing the context switch and
banking WinTrieve into memory becomes much less than the overhead of
thrashing segments. Also, the possibility that a large application will not
be able to run at all is lessened because only a small amount of interface
code need reside with the client.
Designing a Server Under Windows
Figure 1 depicts a typical client-server scenario. Three client applications
labeled App 1, App 2, and App 3 are currently connected to the server. The
arrows between the applications and the server represent a connection, a
two-way communications path. Each end of the connection is identified by a
unique address. As will be seen later, a window handle is used as an
address.
As stated earlier, this discussion is limited to synchronous communications
between client and server. Synchronous communications are characterized by
the client blocking on a request while waiting for a response from the
server. After receiving the response, the client is unblocked and continues.
Synchronous communications are common. Often a client cannot continue in its
thread of execution until it receives data from the server. This is similar
to making a function call, except that control is passed from the client to
the server. Synchronous communications greatly simplifies the protocol
between client and server.
Windows fully supports the client-server model (see Figure 5). Windows
memory management allows programs a fairly large virtual address space
(discardable segments) and permits several large applications to run
simultaneously (bank switching).
Specialized protocols can be defined through Windows support of
user-definable message types. Request and response data are passed between
client and server via memory blocks allocated from the Windows global heap.
Messages under Windows are usually sent to other windows. A receiving window
may reside anywhere in the system and is identified by its window handle
(hWnd). An hWnd is analogous to an address as described above. Messages are
sent with the SendMessage function or with the PostMessage function. Figure
6 illustrates the differences between the two.
A call to SendMessage results in the receiver window procedure (WndProc)
being called directly. Control is transferred to the receiver WndProc, where
the message is processed. Upon return of the message, control is transferred
back to the point in the sender immediately after the SendMessage call.
SendMessage is akin to a function call except that the called function may
reside in another application. SendMessage is most useful for providing
synchronous communications between windows.
PostMessage, on the other hand, does not call the receiver WndProc directly.
Instead, the call to PostMessage results in the message being put into the
receiver window's message queue. The sending application never gives up
control to another and continues executing after the PostMessage call. The
receiver window will obtain the message through normal processing of its
get-translate-dispatch message loop. PostMessage provides a mechanism for
asynchronous communications between windows.
Normally when a client sends a request to a server, it must first put its
request into a packet. A packet is usually a buffer that the client
allocates. The client fills the buffer with the data required to make the
request. The data must be in a format previously agreed upon between client
and server. When ready, the client executes a network send primitive
(typically a function call) to send the request packet to the server. When
the server receives the packet, it is typically copied into the server
address space by the network receive message primitive.
Windows does not support this type of message passing directly, but it can
be simulated. A message in Windows consists of three parts: a message value,
a word parameter, and a long parameter. A packet can be simulated by
specifying a global memory handle in either the word parameter or the long
parameter when the message is sent.
Applications can share global memory in two ways. Allocation rules must be
followed if sharing of global memory is to work with all memory
configurations that Windows supports. Global memory is allocated by using
the Windows GlobalAlloc function.
In the first method, global memory is allocated from the nonbanked area of
Windows global heap. Nonbanked global memory is allocated by specifying the
GMEM_NON_BANKED flag in the GlobalAlloc function call. Any application can
read from and write into these memory blocks. Because they are in nonbanked
memory, memory blocks of this type are always mapped into every
application's address space. The number and size of nonbanked memory blocks
should be kept small; nonbanked memory is limited in size, and Windows uses
it for several other purposes.
By using nonbanked memory, applications can communicate with the lowest
overhead. Both client and server can allocate and use a single block of
nonbanked memory for request and response data. WinTrieve uses nonbanked
memory to communicate request and response data with client applications. A
sample WinTrieve client-server memory map is shown in Figure 7.
The second way that global memory can be allocated is with the GMEM_SHARE
flag, which is the same as the GMEM_DDESHARE flag. Memory blocks of this
type can be safely passed among running applications. Use of GMEM_SHARE
memory blocks is restricted. Applications that allocate this type of memory
are free to read from it and write to it. Other applications must treat it
as a read-only memory block. In large frame EMS configurations, Windows
allocates the global memory from the application's own EMS address space.
When another application locks the memory block, Windows detects that it was
allocated with GMEM_SHARE and that the application attempting to lock the
memory block is not its allocator. In this case, Windows copies the memory
block from the owner's address space into the locking application's address
space.
Figure 8 shows a sample memory map (large frame EMS) for the client-server
that communicates using GMEM_SHARE global memory blocks. The figure shows
the server mapped into memory to service a client request. The request data
packet, allocated by the client, contains the appropriate request data. The
global memory handle of the request data packet was passed by the client to
the server when the request message was sent. To send the request message,
the client calls the Windows function SendMessage. When the server calls
GlobalLock to lock the request data packet, Windows notes that the request
data packet was allocated with the GMEM_SHARE flag and copies it into the
server's address space. The server can now read the request data packet.
When the server calls the GlobalUnlock function to unlock the data packet,
the copy is freed from the server's address space.
To return data to the client, the server could allocate a response data
packet using the GMEM_SHARE flag. It could then return the handle of the
response data packet to the client in either the high or low word of the
long value returned by SendMessage. The unused word of the return value
could be used to store a protocol return code. After receiving the response
data packet, the client would copy it into its own address space. By
agreement, the client would be responsible for releasing the response data
packet memory. Typically, this would be stated as part of a formal protocol
specification.
Because GMEM_SHARE global memory allocations are only copies, they must be
treated as read-only by nonallocator applications. Note that in non-EMS or
small frame EMS configurations, Windows always allocates global memory from
the nonbanked portion of the global heap.
Sample Client-Server Session
Figure 9 shows a sample client-server session. Assume that the client is
Books Browser, an application that allows the user to view, change, or
modify a bookstore's inventory control system. The inventory control system
consists of a database of WinTrieve ISAM files. Books Browser accesses the
database through the WinTrieve server. When Books Browser is started, it
must first try to connect to WinTrieve, as the arrow labeled Initiate
Connection in Figure 9 shows.
WinTrieve, upon receiving the connection request, must decide whether to
accept the connection or reject it. If WinTrieve accepts the connection,
further requests from Books Browser are allowed. If it rejects the
connection request, for example, because of an error condition, then its
client connect table is full and further communications are not allowed.
WinTrieve accepts the connection and indicates this to Books Browser by
returning a connection acknowledgment.
Assume that with Books Browser the user can sequentially scan a Books ISAM
file one record at a time. The Books file contains records whose fields
consist of a book's ISBN number, its title, its author, its publisher, and
the number of copies in stock. The user views a record in the file by
selecting Next in the menu bar.
Upon selection of "next record," Books Browser sends a request message to
WinTrieve. If the request is accepted and processed, WinTrieve returns a
request acknowledgment. The user subsequently makes several more requests,
as shown in Figure 9.
Finally the user decides to close Books Browser. Before exiting, Books
Browser terminates the connection by sending a terminate connection message
to WinTrieve. WinTrieve then returns a terminate acknowledgment.
The WinTrieve Protocol
The synchronous protocol implemented for WinTrieve is not
WinTrieve-dependent. It should be relatively straightforward to implement
for other server applications. Code samples, although taken from WinTrieve,
have been rewritten to minimize or completely remove WinTrieve-specific
details.
The WinTrieve protocol supports three message types (see Figure 10 for the
message types and their descriptions). The three messages correspond to the
messages described above in the sample session with one exception: there is
no corresponding acknowledgment message. The WinTrieve protocol handles
acknowledgments as return values, not as messages.
Because the WinTrieve protocol is synchronous, clients send messages using
the Windows function SendMessage. The long value returned by SendMessage
represents the message acknowledgment. Return values are actually return
codes: one represents success and others represent errors. Figure 11 lists
the valid return codes.
The return code ISNOSERVER has special significance. SendMessage returns
zero if the specified receiver window handle is invalid (no window with that
handle is currently running). The receiving WndProc must not return a zero
value; if it did so, the sender would be unable to differentiate between a
SendMessage error and a protocol error. As defined by the WinTrieve
protocol, all return codes are nonzero. Figure 12 summarizes the SendMessage
argument values for each WinTrieve protocol message type.
Making the Connection
When the client wants to connect, it must first obtain the hWnd of the
server. The server needs a mechanism by which it can make itself known to
potential clients. This can be accomplished in at least two ways. The method
used by the WinTrieve protocol specifies that the server's communications
window have a well-known class and window name. The class name is defined
when the window class is registered, and the window name is defined when the
window is created. For WinTrieve the window name is the string ISAM SERVER.
When a client application wants to connect to WinTrieve, it calls the
WinTrieve C API function isconnect, a version of which is shown in Figure
13. Except for connection initiation and termination functions, the
WinTrieve C API completely insulates the developer of WinTrieve applications
from the WinTrieve server protocol. The WinTrieve C API is a small object
library that is linked with the client application.
The WinTrieve protocol message values are obtained by calling the Windows
function RegisterWindowMessage (see Figure 13). The Windows function
FindWindow is then called to obtain the handle of the server's
communications window. FindWindow generally returns the handle of the
window, identified by its class and window name. Note that using FindWindow
necessarily restricts the number of running servers of the same type to one.
WinTrieve enforces this restriction by immediately exiting if an instance of
itself is already running.
After obtaining the handle to the server's communications window, isconnect
sends an initiate connection message to the server. The wParam (word
parameter) argument of the SendMessage call is set to the handle of a client
application's window. The handle is passed as the sole argument to isconnect
and is normally the handle of the client's main window. The only restriction
on the client window handle is that it remain valid throughout the life of
the connection.
If the function completes successfully, the global variable hWndServer is
set to the handle of the server's communications window, and the global
variable hWndClient is set to the hWnd argument. These globals will be used
subsequently in communications with the server.
Making a Request
When a client is connected, it can make requests to the server. In order to
make a request, the client must first build a request block, which will
typically contain an opcode that specifies the request type and any other
necessary request data.
Figure 14 shows the C structure declaration for a WinTrieve protocol request
block. The structure is generalized because the fields of the structure
support all the possible opcodes. For any single opcode only a few fields
may actually be used.
"Request block" is something of a misnomer because the server also uses the
same request block to return response data. Remember that the WinTrieve
protocol specifies that the request block be allocated from the nonbanked
portion of the global heap. A client need allocate this block of memory only
once and use it throughout the lifetime of a session, thus minimizing the
amount of nonbanked memory allocated for each client. For example, Figure 15
shows the fields and their description for a WinTrieve open ISAM file
request.
Code for a sample WinTrieve C API request function is in Figure 16. This
function reads a record in an ISAM file.
All of the WinTrieve C API request functions follow the same basic
techniques as isread to make a request. The requisite fields for the request
are filled as specified by the protocol definition. After unlocking the
request block, the function sends a request message to the server. The
SendMessage return value is checked to determine if any errors occurred at
the protocol level. Response data is copied into the client's local address
space.
Note how isread introduces a second level of error handling that deals with
errors that occurred while processing the request. Request-processing errors
are returned in status fields of the request block (see Figure 14). The
first level, protocol level, deals with errors related to processing the
protocol. Two layer error handling tends to simplify protocol and make it
more generic for other server applications. Ultimately the WinTrieve C API
functions combine both levels of error handling into a single layer for the
user.
After making several requests, and before closing, the client application
must end the session with the server. It does this by sending a terminate
connection message to the server. Figure 17 shows sample code for the
WinTrieve C API function that terminates the connection.
Server Perspective
Servers can be built in layers as shown in Figure 18. The first layer is the
protocol manager, which mainly consists of the server's communications
window, WndProc, and associated routines. This layer is fairly generic and
can be applied easily to other server types. The next layer, the dispatch
manager, is more server-specific, but its overall form is that of a large
switch statement keyed off the request opcode. Subsequent layers are very
server-specific.
When a server is started, it must first create the window for client
applications to send messages. This window has been referred to as the
server's communications window. The only requirement when creating the
server's communications window is that its class and name conform to the
protocol specification.
Figure 19 shows sample code for creating the WinTrieve communications
window. The code is generic enough that other server applications can use it
by specifying a different class and window name.
Figure 20 shows sample code for the WinTrieve WndProc routine. Note that the
code is simplified to be generic and to improve clarity.
Loading the Server
A seeming disadvantage of the server approach is that the server must be
explicitly executed. This places an unnecessary burden on the end user.
Windows supports a method by which applications can be started transparently
with no user interaction.
This is done by specifying the executable file name in the load line of the
[windows] section of the WIN.INI file. Windows reads this file when it
starts. Any program listed in the load line is started (as an icon) after
Windows initially begins. As with WinTrieve, many servers will only run in
an iconic state. The only indication that the user will have that the server
is running is the server icon displayed in the lower-left portion of the
screen.
Language Interface
When building servers such as those described in this article, you should
strive to provide an API for the target application language also. The API
should hide the details of the protocol and give the appearance that the
user is calling a library and not a server.
WinTrieve, for example, provides two APIs. One is for C (see Figure 21 for a
summary of WinTrieve C API functions), and one is for Actor (Actor APIs take
the form of class libraries). Two C API functions, isconnect and
isdisconnect, were introduced to support the server approach. All other
functions remained the same.
Conclusion
In many situations, the client-server approach is the only way by which
large client applications can run. All other methods either directly or
indirectly reduce the client's address space. A disadvantage of the server
approach is that a certain amount of overhead is incurred for context switch
and data passing. Figure 22 summarizes the advantages and disadvantages of
the client-server approach under Windows.
Windows support for memory management, user-defined protocols, shared
memory, and message passing allows for the efficient implementation of the
client-server model that is largely application-independent.
Figure 10
Message Type Description
INITIATE Message sent by client to server to initiate a connection
SEND Message sent by client to server to request services
TERMINATE Message sent by client to server to terminate a connection
Figure 11
Error Constant Value INITIATE SEND TERMINATE Description
ISNOSERVER 250 3 3 3 Invalid server handle (server not
running)
ISMAXCONN 251 3 Maximum number of connections would be
exceeded
ISNOTCONN 252 3 3 Client not connected
ISCONN 253 3 Client is connected
ISNULLPTR 254 3 GlobalLock of query block returned NULL
pointer
ISOK 255 3 3 3 Message sent successfully
Figure 12
Message SendMessage Arguments
hWnd msg wParam lParam
INITIATE hWndServer WM_INIT_ISAM hWndClient 0L
SEND hWndServer WM_SEND_ISAM hWndClient High word contains
handle of client request block
TERMINATE hWndServer WM_TERM_ISAM hWndClient 0L
Figure 13
/* WinTrieve C API, error codes are returned in global variable, iserrno. If
an API function returns -1 it indicates an error. */
int iserrno;
/* These values need to be available to other WinTrieve C API functions. For
the example code given, assume these variables are globally defined. This
does not necessarily reflect the actual implementation. */
HWND hWndServer; /* server window handle */
HWND hWndClient; /* client window handle */
WORD wmInitISAM; /* WM_INITIATE_ISAM message value */
WORD wmSendISAM; /* WM_SEND_ISAM message value */
WORD wmTermISAM; /* WM_TERMINATE_ISAM message value */
/* Make connection to server. Argument hWnd is handle to a client window.
Should ensure that hWnd remains valid throughout the life of the connection.
*/
int isconnect(
HWND hWnd )
{
static char *szServerName = "ISAM SERVER";
static char *szInit = "WM_INITIATE_ISAM";
static char *szSend = "WM_SEND_ISAM";
static char *szTerm = "WM_TERMINATE_ISAM";
static int iFirstTime = 1;
long lRetVal;
/* Obtain WinTrieve protocol message values. */
if (iFirstTime) {
/* Only do this once. */
wmInitISAM = RegisterWindowMessage((LPSTR)szInit);
wmSendISAM = RegisterWindowMessage((LPSTR)szSend);
wmTermISAM = RegisterWindowMessage((LPSTR)szTerm);
iFirstTime = 0;
}
/* Get the handle of the server's communications window. */
hWndServer = FindWindow((LPSTR)szServerName, (LPSTR)szServerName);
if (hWndServer = = 0L) {
/* Error, server not running. */
iserrno = ISNOSERVER;
return -1; /* Error, server not running. */
}
lRetVal = SendMessage(hWndServer, WM_INITIATE_ISAM, hWnd, 0L);
if (lRetVal != ISOK) {
/* Error, two possible conditions, server must
* have died or maximum connections reached.
*/
if (lRetVal = = 0L) /* Assign error code to
iserrno = ISNOSERVER; / * global variable. */
else
iserrno = retVal;
return -1; /* Indicates error. */
}
/* Connection successful. */
hWndClient = hWnd;
return 0;
}
Figure 14
/* An abbreviated list of opcode constants. */
#define OPADDINDEX /* add an index */
#define OPBGNTRAN /* begin a transaction */
#define OPBUILD /* create an ISAM file */
#define OPCLOSE /* close an ISAM file */
#define OPCOMMIT /* commit a transaction */
#define OPDELETE /* delete a record */
#define OPOPEN /* open an ISAM file */
#define OPREAD /* read a record */
#define OPREWRITE /* update a record */
#define OPWRITE /* write a record */
struct isrequest {
int iOpcode;
int iIsfd;
int iMode;
int iKeynum;
int iReclen;
long lRecnum;
long lUniqid;
GLOBALHANDLE hBuffer;
GLOBALHANDLE hFileName1;
GLOBALHANDLE hFileName2;
GLOBALHANDLE hRecord;
char isstat1;
char isstat2;
int iserrno;
int iserrio;
int isretval;
};
Figure 15
Query Block Fields Description
Client iOpcode OPOPEN
Request iMode Access mode in effect while file is open. Arithmetic sum
of a read/write
Data mode and lock mode values.
hFilename1 Handle to a GMEM_NON_BANKED global memory that contains a
null-terminated character string identifying the ISAM file to be
opened.
iReclen File record length
Server isretval Return code; -1 indicates error such as file not
found. Otherwise, file
Response descriptor (isfd) identifies open file; used in subsequent
file operations.
Data iserrno ISAM error code
iserrio System error code
isstat1 Status 1 code
isstat2 Status 2 code
Figure 16
/* Handle to isrequest struct, previously allocated from the nonbanked
portion of the global heap. An appropriate place to allocate the request
block is in isconnect. */
GLOBALHANDLE hRequestBlock;
/* Table maintained by the C API that holds the record length of all open
ISAM files. Indexed by isfd of open file. The table record length is filled
in the table when a file is opened or created. */
int iRecLenTbl[MAX_FILE_DESCRIPTORS];
/* Function to read a record from an ISAM file. */
int isread(
int isfd, /* fd of previously opened ISAM file */
char *record, /* buf to copy read record into */
int mode) /* which record, next, prev, equal, etc */
{
struct isquery far *lpRB;
LPSTR lpRec;
int iRecLen;
int iRetVal;
lpRB = (struct isquery far *)GlobalLock(hRequestBlock);
if (lpRB = = NULL) {
/* Error, not able to lock query block. */
iserrno = ISNULLPTR;
return -1;
}
lpRB->iOpcode = OPREAD;
lpRB->iIsfd = isfd;
lpRB->iMode = mode;
/* WinTrieve allows the user to search for a record based on index key
fields. It is expected the values of interest are filled in the record
buffer to be passed to the server. For the sake of simplicity assume that
the global memory block specified by hRecord is large enough to hold the
record. */
lpRec = GlobalLock(lpRB->hRecord);
if (lpRec = = NULL) {
/* Error, not able to lock the record block. */
GlobalUnLock(hRequestBlock);
iserrno = ISNULLPTR;
return -1;
}
/* Copy the record. */
iRecLen = iRecLenTbl[isfd];
lmemcpy(lpRec, (LPSTR)record, iRecLen);
/* All fields filled, now unlock and make the request. */
GlobalUnlock(lpRB->hRecord);
GlobalUnlock(hRequestBlock);
iRetVal = SendMessage(hWndServer, wmSendISAM, hWndClient,
MAKELONG(hRequestBlock, 0));
if (iRetVal != ISOK) {
/* Protocol error. */
if (iRetVal = = 0L)
iserrno = ISNOSERVER;
else
iserrno = iRetVal;
return -1;
}
/* Lock the request block to get the response values. */
lpRB = (struct isquery far *)GlobalLock(hRequestBlock);
if (lpRB = = NULL) {
/* Error, not able to lock query block. */
iserrno = ISNULLPTR;
return -1;
}
/* This introduces the second level of error handling.
* These are errors related to the processing of the
* request which are differentiated from the processing
* of the protocol.
*/
iRetVal = lpRB->iRetVal;
if (iRetVal = = -1) {
iserrno = lpRB->iserrno;
return iRetVal;
}
lpRec = GlobalLock(lpRB->hRecord);
if (lpRec = = NULL) {
/* Error, not able to lock the record block. */
GlobalUnLock(hRequestBlock);
iserrno = ISNULLPTR;
return -1;
}
/* Copy the record that was read. Assume lmemcpy is
* long pointer version of C Runtime memcpy.
*/
lmemcpy((void far *)record, lpRec, iRecLen);
GlobalUnlock(lpRB->hRecord);
GlobalUnlock(hRequestBlock);
return iRetVal;
}
Figure 17
/* Function to terminate connection with server.
*/
int isdisconnect()
{
int iRetVal;
iRetVal = SendMessage(hWndServer, wmTermISAM, hWndClient, 0L);
if (iRetVal != ISOK) {
/* Error terminating connection. */
if (iRetVal = = 0L)
iserrno = ISNOSERVER;
else
iserrno = iRetVal;
return -1;
}
/* Successfully terminated connection with server. */
hWndServer = NULL;
return 0;
}
Figure 19
/* Create the server's communications window. If successful, returns window
handle. If error, returns NULL. */
HWND CreateCommWindow(HANDLE hInstance, HANDLE hPrevInstance )
{
/* Server's class and window name as defined by WinTrieve protocol
specification. */
static char *szCommName = "ISAM SERVER";
WNDCLASS wndclass;
HWND hWnd;
/* Only one instance of the server is allowed
* to run at any one time. Return error.
*/
if (hPrevInstance)
return NULL;
/* Register the server window class. Nothing special here. */
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = NULL;
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = szCommName;
if (!RegisterClass(&wndclass))
return NULL;
/* Create server communications window. */
hWnd = CreateWindow(szCommName, szCommName,
WS_OVERLAPPED|WS_SYSMENU,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0,
NULL, NULL, hInstance, NULL);
return hWnd;
};
Figure 20
/* Table of currently connected clients. The clientTbl is typically accessed
through the three routines getClient, putClient, delClient, and reapClient.
*/
static HWND clientTbl[MAX_CLIENTS];
static int putClient(HWND);
static int getClient(HWND);
static int delClient(HWND);
static int reapClient(HWND);
/* Server communications window WndProc. All client requests come through
here. */
long FAR PASCAL WndProc (
HWND hWnd,
unsigned iMessage,
WORD wParam,
LONG lParam )
{
static char *szInit = "WM_INITIATE_ISAM";
static char *szSend = "WM_SEND_ISAM";
static char *szTerm = "WM_TERMINATE_ISAM";
static WORD wmInitISAM;
static WORD wmSendISAM;
static WORD wmTermISAM;
GLOBALHANDLE hRequestBlock;
HWND hWndClient;
int iRetVal;
switch (iMessage) {
case WM_CREATE:
/* Register WinTrieve protocol messages. */
wmInitISAM = RegisterWindowMessage(szInit);
wmSendISAM = RegisterWindowMessage(szSend);
wmTermISAM = RegisterWindowMessage(szTerm);
break;
case WM_CLOSE:
/* Detect if any clients are still connected. */
if (reapClients()) {
/* There are still connected clients. What
* is done here is server dependent. WinTrieve
* displays a message box and returns (does
* not terminate).
*/
}
else
/* No connected clients, ok to terminate. */
return DefWindowProc(hWnd, iMessage, wParam, lParam);
break;
case WM_DESTROY:
/* Can do some final cleanup here before the server terminates. */
break;
case WM_QUERYOPEN:
/* The server is always an icon. */
break;
default:
if (iMessage = = wmSendISAM) {
/* Get handle to query block. */
hRequestBlock = HIWORD(lParam);
/* Check that client is connected. */
hWndClient = wParam;
if (getClient(hWndClient) < 0)
return ISNOTCONN;
/* Process the request, this is server dependent.
* If protocol error, for example, cannot lock
* query block returns an error code, otherwise
* returns ISOK.
*/
iRetVal = ProcessRequest(hWndClient, hRequestBlock);
return iRetVal;
}
else if (iMessage = = wmInitISAM) {
/* A server must be robust. Client handles
* that have become invalid are checked for here.
*/
reapClients();
/* Initiate a connection. */
hWndClient = wParam;
/* Check that client isn't already connected. */
if (getClient(hWndClient) >= 0)
return ISCONN;
/* Check for maximum connections. */
if (putClient(hWndClient) < 0)
return ISMAXCONN;
/* Client connected. */
return ISOK;
}
else if (iMessage = = wmTermISAM) {
/* Terminate a connection. */
hWndClient = wParam;
/* Check that client is connected. */
if (delClient(hWndClient) < 0)
return ISNOTCONN;
return ISOK;
}
else
/* Default message processing. */
return DefWindowProc(hWnd, iMessage, wParam, lParam);
break;
}
return 0L;
}
/* Put new client into client table. If successful, returns index of table
where the handle is stored. If the table is full, returns -1. */
static int putClient(
HWND hWnd )
{
register int i;
for (i = 0; i < MAXHANDLES; i++) {
if (clientTbl[i] = = NULL) {
clientTbl[i] = hWnd;
return i;
}
}
return -1; /* client table full */
}
/* Delete client from client table. If successful, returns index position of
table where the handle was deleted from. If handle not found, returns -1. */
static int delClient(hWnd)
HWND hWnd;
{
register int i;
for (i = 0; i < MAXHANDLES; i++) {
if (clientTbl[i] = = hWnd) {
clientTbl[i] = NULL;
return i;
}
}
return -1; /* handle not found */
}
/* Returns index position in client window handles table for the specified
client handle. If client handle not found, returns -1. */
static int getClient(hWnd)
HWND hWnd;
{
register int i;
for (i = 0; i < MAXHANDLES; i++) {
if (clientTbl[i] = = hWnd)
return i;
}
return -1;
}
/* Check the client table for any invalid client handles. For example
clients that died suddenly or clients that just plain forgot to terminate
the connection properly. Returns 0 if no more clients connected. */
static int reapClients()
{
int i;
int n;
HWND hWndClient;
/* Run through client table looking for connected clients. */
for (i = n = 0; i < MAX_CLIENTS; i++) {
if (clientTbl[i] != NULL) {
hWndClient = clientTbl[i];
/* Check if still valid handle by calling
* Windows function IsWindow.
*/
if (IsWindow(hWndClient) = = NULL) {
/* Client died. The function cleanUp
* is a server specific routine that
* removes the state of a client
* if it is no longer valid.
*/
cleanUp(hWndClient);
clientTbl[i] = NULL;
continue;
}
n++; /* increment connected clients count */
}
}
return n;
}
Figure 21
Function Description
isaddindex Add an index to an ISAM file
isbgntran Begin a transaction
isbuild Create an ISAM file
isclose Close an ISAM file
iscommit Commit a transaction
isconnect Initiate session with the server
isdelcurr Delete the current record
isdelete Delete record specified by primary key
isdelindex Delete and index
isdelrec Delete record specified by record number
isdisconnect Terminate session with the server
iserase Delete an entire ISAM file
isindexinfo Obtain information about an ISAM file
isjrnlmsg Add message to journal file
isjrnloff Turn off journaling
islock Lock an ISAM file
isopen Open an ISAM file
isread Read a record in an ISAM file
isrelease Unlocks all manually locked records in an ISAM file
isrename Rename an ISAM file
isrewcurr Rewrite the current record
isrewrec Rewrite the record identified by record number
isrewrite Rewrites a record identified by its primary key
isrollback Roll back the current transaction
issetjrnl Begin journaling
issetuniqid Set the ISAM file's unique ID value
isstart Select an index and locate a record
isuniqueid Get next unique ID value
isunlock Unlock an ISAM file
iswrcurr Write a new record and make it current
iswrite Write a new record into an ISAM file
Figure 22
Advantages
■ Permits larger applications by not residing in client address space
with EMS
memory configurations
■ Data segments may reside in banked memory with EMS memory
configurations
■ Not limited to a single data segment
Disadvantages
■ Server memory is not freed if no longer used
■ Overhead of context switch when server is called
■ Overhead of copying request and response data between client and server
■ Windows task overhead
Enhancing the Presentation Manager User Interface with Formatted Edit Fields
Marc Adler
Some applications, such as those designed for the financial sector, require
a series of formatted data entry screens. Although the data is frequently in
a specific format, such as a Social Security number or a date, the Microsoft
Windows and OS/2 Presentation Manager (hereafter "PM") environments do not
support editing and validating user-defined formats as a standard feature.
This article presents a formatted edit class for PM. This edit class offers
great control over user input by providing format validation on a
character-by-character basis. It also uses picture masks to define data
entry fields.
Code
The code for the formatted edit class is surrounded by a small shell that
constitutes a test program for the new class. The test program (see Figure
1) displays a dialog box consisting of several formatted edit fields and
then accepts input from the user. In the startup code (in the main function)
PM is initialized by a call to WinInitialize; a call to WinCreateMsgQueue
creates a message queue. These two functions must be present at the start of
every PM program.
The next step is to find the address of the default window procedure for the
normal edit class. The window procedure of the edit class does most of the
hard work (it takes care of most of the editing commands, for example); it
simply intercepts some of the messages directed to the new type of edit
controls. You must therefore get a pointer to the default edit procedure so
that at times you can let it insert the window procedure into the edit
buffer and display the new contents of the buffer.
To get the address of the default edit procedure, look into the edit class
information structure, the CLASSINFO structure. The CLASSINFO structure
contains the style bits associated with that class, the number of bytes
reserved per window for a data storage area, and the address of the class
window procedure. You use the PM function WinQueryClassInfo to get a copy of
this structure. The code to do this is:
CLASSINFO clsInfo;
WinQueryClassInfo(hAB, WC_ENTRYFIELD,
(PCLASSINFO) &clsInfo);
pfnOldEditWndProc = clsInfo.pfnWindowProc;
You must register the new class with PM in order to store the three elements
mentioned above into a CLASSINFO structure, which PM maintains. To do so,
use the following call:
WinRegisterClass(hAB, "Formatted", FmtWndProc,
CS_SIZEREDRAW,clsInfo.cbWindowData +
sizeof(PVOID));
By specifying "sizeof(PVOID)" in the last argument, you reserve space for a
pointer to the format information structure that will be attached to each
formatted edit control.
The same steps are repeated for the static text class to intercept the
static class. In this way you can intercept requests to draw the text on the
screen and instead use your own drawing routines. Finally, you create the
main window and go into the standard message polling loop that accompanies
every PM program.
The WinSubclassWindow function is not used to get the address of the default
window procedures for the various classes, because you must provide the
handle to the window that you want to subclass as your first argument. You
must first know the IDs of the edit controls to get their window handles.
Thus, a table in your program must contain the ID of each edit control in
each dialog box, and you must have the code to subclass each edit control in
each dialog box procedure. An example would be:
for (i = 0; idTable[i]; i++)
if ((hEdit = WinWindowFromID(hDlg, idTable[i])))
pfnOldProc = WinSubclassWindow(hEdit, MyWindowProc);
This is too much overhead in my opinion. That's why WinSubclassWindow was
not used in this application.
Structure
As mentioned above, every window can have a private data storage area. The
amount of memory allocated for this area is determined by the cbWindowData
field of the CLASSINFO structure. For each formatted edit control, an extra
four bytes of storage is reserved to hold a far pointer to a format
information block. The format information block has the following
information:
typedef struct format
{
ULONG fFormatFlags; /* formatting style bits */
NPFN pfnValidChar; /* function to validate each
character*/
PSZ szPicture; /* picture string */
} FORMAT, FAR *PFORMAT;
fFormatFlags contains style bits for the control (see Figure 2).
pfnValidChar is a pointer to a function that will be called in order to
validate each character that is typed. pfnValidChar will not be used if a
picture clause is associated with the control. szPicture can contain an
optional picture clause that is to be used for formatting. The formatting
characters that can be used within a picture clause are listed in Figure 3.
The PICINFO structure associates validation functions for each of the
formatting characters within a picture clause. For instance, if you type a
character in a column that has the picture character 9 attached to it, the
ValidDigit function will be called to ensure that the typed character is a
digit. If a character in a picture clause is not in the PICINFO structure,
it is assumed to be a protected character. Characters cannot be entered in a
column that has a protected picture character attached to it. For example,
the picture clause for a phone number would be:
(999) 999-9999
In this picture, the parentheses and the dash are protected characters. The
user will not be allowed to place the cursor in a protected column in the
code that implements the formatted edit class.
A control does not need a picture clause in order for character validation
to take place. It can use the styles FLD_NUMERIC, FLD_SIGNEDNUMERIC,
FLD_ALPHA, FLD_ALPHANUMERIC, and FLD_LOGICAL to impose a uniform character
type throughout the entire edit control. For instance, a control that has
the FLD_ALPHA style can accept only the letters A-Z and a-z as
input.
How is a formatted edit control defined as a member of a dialog box? You
simply specify the word Formatted as the control class in the resource file
definition. For example, the following defines a formatted edit control in
the application:
CONTROL "", ID_NUMERIC, 65, 49, 96, 8, "Formatted",
ES_LEFT | ES_MARGIN | WS_TABSTOP | WS_VISIBLE
The edit control's text field can be used to define a picture clause:
CONTROL "(999) 999-9999", ID_NUMERIC, 65, 49, 96, 8, "Formatted",ES_LEFT |
ES_MARGIN | WS_TABSTOP |
WS_VISIBLE
You can also use the CTLDATA clause to define other style bits. For
instance, to make the control above a required field (that is, a field with
the FLD_REQUIRED style), you can do the following:
#define FLD_REQUIRED 32
CONTROL "", ID_NUMERIC, 65, 49, 96, 8, "Formatted",
ES_LEFT | ES_MARGIN | WS_TABSTOP | WS_VISIBLE
CTLDATA FLD_REQUIRED
In the code, FLD_REQUIRED is defined as 20h or 32. The values specified in
the CTLDATA clause are passed to the window procedure by the WM_CREATE
message in the first parameter. They are found in the CREATESTRUCT
structure also, which is pointed to by the second parameter.
One drawback in using CTLDATA is that the current version of the resource
compiler, RC.EXE, does not allow the use of the bitwise OR operator in the
CTLDATA clause. Therefore, the following clause is not allowed.
CTLDATA FLD_NUMERIC | FLD_REQUIRED
To avoid this, determine the correct value yourself and insert it directly
into the CTLDATA clause so that you have:
CTLDATA 1056
Format Window Procedure
All of the messages that PM sends to an edit control will be routed not to
the default edit control window procedure, but to your own window procedure
for the formatted edit class, FormatWndProcedure. Because you want to
provide complete editing capabilities in your formatted controls, use the
existing default window procedure for edit controls to do most of the hard
work associated with editing. Simply use FormatWndProcedure to provide
pre-processing and post-processing to the normal editing actions.
When an edit control is created, a WM_CREATE message is sent to the class
window procedure. A pointer to a block of information, called the
CREATESTRUCT, is passed in the mp2 parameter of the WM_CREATE message. You
can examine the pszText member of this structure to see if a picture clause
was specified as the control's text. If so, save a copy of the picture and
set pszText to NULL, which tells PM that the edit control is initially
blank. Next, allocate a format info block and put its address in the edit
control's storage area for each of its windows. Call the default edit window
procedure to have the default creation processing take place, and then
provide some added value to the control. Examine the mp1 parameter, which
points to any data specified by the CTLDATA clause. If you find some data,
use the bitwise OR operator to flip these style bits with any style bits
that had been set by the picture clause.
If there are protected characters in a picture clause, you must ensure that
the user cannot place the cursor over a protected column. But what happens
if the user clicks the mouse over a protected column? The default behavior
for an edit control is to set the cursor to the column that the mouse was
clicked on. To prevent that, intercept the WM_SETFOCUS message. Because you
are only interested in whether you are gaining the input focus, not losing
it, examine the mp2 parameter. If mp2 is FALSE, you are losing focus, so you
should let the default edit window procedure handle it.
By sending the edit control an EM_QUERYSEL message, you can determine in
which column you are. If you are over a protected column, you send the edit
control a simulated RIGHT-ARROW keystroke until the cursor moves into a
nonprotected column.
The WM_DESTROY message must also be intercepted. This message is sent to a
control when the control is being destroyed and gives the application the
opportunity to release any resources allocated to that control. In this
case, you want to release the memory that was allocated to the format
information block. Failure to do so might result in a dangerous fragmented
memory situation if many formatted edit controls are used.
Before we get to the interesting part, let's look at what you should do with
the WM_PAINT message. You want to draw a shadow around the border of the
edit control in order to create a three-dimensional effect. You would also
like to use a monospaced font for the edit text. Because a different font is
being used, be sure that the cursor is in the correct column. The default
edit window procedure thinks you are using the system-proportional font and
therefore maps the logical cursor position into a physical column, using the
width tables associated with the system font. Because you are using a
different font and therefore different character widths, you must map
correctly the logical cursor position into a physical column.
FormatWndProcedure is sent a WM_PAINT message whenever the edit control
needs to be drawn. First, you use the GpiMove function to draw the border
around the window. Next use GpiBox to draw shadows under and to the right of
the borders. Use GpiCharStringAt to draw the text in the presentation space
associated with the monospaced font instead of in the default presentation
space. Finally, you calculate the correct physical column in which to place
the cursor. The WinCreateCursor call provides the ability to set the cursor
to a specific coordinate within a window.
WM_CHAR Message
Most of the work implementing the formatted edit control is in dealing with
keystrokes. If you type a printable character, you must determine the proper
validation routine to call if a picture clause is associated with the edit
control. If you use protected columns, after each character is typed you
must advance the cursor to the next unprotected column, rather than
advancing it by a single column.
The same is true for the cursor keys. The left, right, home, and end keys
must account for protected columns. To do this, send VK_RIGHT or VK_LEFT
keystrokes into the edit control until you move into an unprotected column.
This is easily accomplished in PM by sending the edit control a WM_CHAR
message with the mp1 and mp2 parameters set to the proper values. The code
is shown below.
while (cnt--)
{
/* Create a simulated keystroke */
mp1 = MPFROMSH2CH(KC_VIRTUALKEY, 1, 0);
mp2 = MPFROM2SHORT(0, VK_RIGHT);
pfnOldEditWndProc(hWnd, WM_CHAR, mp1, mp2);
}
A more complicated issue arises when you are trying to insert and delete
characters. If a picture clause has mixed character types, inserting or
deleting a character could throw off the entire field. I'll take the safe
way out by disallowing the insert, delete, or backspace characters to be
passed on to the formatted edit control. If an edit control has a picture
clause, it will always remain in overstrike mode. Overstrike mode is
implemented by selecting the character to the right of the one you inserted
and then deleting that character. The code to do this is:
/* Implement overstrike mode by deleting the next character */
pfnOldEditWndProc(hWnd, EM_SETSEL, MPFROM2SHORT(iPos,
iPos+1), 0L);
pfnOldEditWndProc(hWnd, EM_CLEAR, 0L, 0L);
/* Let the edit win procedure handle the insertion of the character */
pfnOldEditWndProc(hWnd, WM_CHAR, mp1, mp2);
Different Fonts
When developers received the official release of PM, the default system font
had been changed from a monospaced font to a proportionally spaced one. Many
applications that used list boxes to display columns of data had to be
redesigned. But what if you simply wanted Courier or a proportionally spaced
font different from the system font?
A presentation space (PS) is a data structure that maintains information
about an application's device-independent drawing environment, including the
current logical font the application uses when it writes text to the output
device. In this application, you set the current logical font to the
monospaced Courier font whenever dialog boxes, which contain formatted edit
fields, are used. For further information, see "The Graphics Programming
Interface: A Guide to OS/2 Presentation Spaces," MSJ (Vol. 3, No. 3).
Because the screen is the main concern for now, work with a cached micro-PS,
not the other types of presentation spaces (normal and standard micro).
Before a dialog box is shown, the WM_INITDLG message is sent to the dialog
box's window procedure; usually applications initialize the data inside the
dialog box's controls at this time. The application will substitute the
Courier font for the current font during this period.
The WinGetPS function is used to retrieve the cached presentation space.
This PS can be used to perform subsequent drawing. SetMonospacedFont is
called to locate the desired font and to load it into the PS. At the end of
the application, WinReleasePS must be called in order to give the PS back to
the cache.
At the start of SetMonospacedFont, the following code loads a copy of the
Courier font:
GpiLoadFonts(hAB, (PSZ) "C:\\os2\\dll\\courier.fon");
The fonts are in a dynamic-link library; each process that needs to access
the fonts must use GpiLoadFonts. However, the fonts are loaded into memory
only once; if any other process loads them, a reference count is simply
incremented.
To find the particular font that suits your needs after the fonts are
loaded, load the font metric information for each font in that family.
First, however, you must find out how many fonts of the specified family
have been loaded. To do this, you use the following:
nRequestFonts = 0L;
nFonts = GpiQueryFonts(hPS, QF_PUBLIC | QF_PRIVATE,
(PSZ) "Courier", &nRequestFonts,
0L, NULL);
Because nRequestFonts is set to 0, no font metric information is returned,
but the number of fonts available in that family is returned. Next, you
allocate a buffer to hold the font metric information. The font metrics are
retrieved by calling:
GpiQueryFonts(hPS, QF_PUBLIC | QF_PRIVATE, (PSZ)
"Courier", &nFonts,
(LONG)sizeof(FONTMETRICS), pMetrics);
It is necessary to go through all of the font metrics to find a font that
fits your specifications. GpiCreateLogFont is used to try to create a
logical font that matches your needs. If the value returned is 2, then you
have a match. Each created font is referenced by a local numeric identifier.
rcMatch = GpiCreateLogFont(hPS, (PSTR8) szFont,
(LONG) ++IDCourier, (PFATTRS) &FontAttrs);
if (rcMatch = = 2)
{
GpiSetCharSet(hPS,
IDCourier);
}
GpiCharSet will set the current font in the presentation space to the
created logical font referenced by the identifier IDCourier. Once the
logical font is associated with the presentation space, any text that is
drawn on the screen is drawn in that font.
Enhancements
Many things can be done to improve the formatted edit class presented here.
Not all formatting styles are implemented in this code because of space
considerations. I have not considered what happens when a user deletes or
copies a marked area that contains protected characters. Evidently, the
protected characters must remain intact, but should you allow a user to do
this? I have also not implemented insert mode; the edit controls are always
in overstrike mode because of the problem mentioned above. The code could be
modified to allow insertion and deletion if the result would not invalidate
the data.
I have also skimped on some of the other messages that edit controls should
handle. The WinSetWindowText function issues the WM_SETWINDOWPARAMS message,
for instance. For this message, you should detect if the user is trying to
set the edit control's text, and check the validity of the new text before
setting it. The WinQueryWindowText function issues the WM_QUERYWINDOWPARAMS
message. You may or may not want to insert the protected characters of the
picture into the string that is sent back to the user for this message.
You can also think about implementing the multiple-line edit class that PM
needs. Compare the power of the edit class in the Microsoft Windows
environment with that of PM, and you will find that much work needs to be
done. Figure 4 lists the messages that can be sent to an edit control in
both Windows and PM. As you can see, PM has a long way to go.
Figure 1
FMT
OBJS = fmt.obj
fmt.exe: $(OBJS) fmt.def fmt.res
link /co $(OBJS), /align:16, nul, os2, fmt.def
rc fmt.res
fmt.obj: fmt.c
cmd /c mcc fmt
fmt.res: fmt.rc
rc -r fmt.rc
FMT.RC
/*****************************************************************/
/* resource file for the formatted edit control test program */
/*****************************************************************/
#include <os2.h>
#include "fmt.h"
#define FLD_HASPICTURE 1
#define FLD_AUTONEXT 2
#define FLD_NOECHO 4
#define FLD_PROTECT 8
#define FLD_IGNORE 16
#define FLD_REQUIRED 32
#define FLD_TOUPPER 64
#define FLD_TOLOWER 128
#define FLD_CENTER 256
#define FLD_RJUST 512
#define FLD_NUMERIC 1024
#define FLD_SIGNEDNUMERIC 2048
#define FLD_ALPHA 4096
#define FLD_ALPHANUMERIC 8192
#define FLD_LOGICAL 16384
MENU DLG_FORMAT
{
SUBMENU "~File", 1
{
MENUITEM "~Test...", ID_DLG
MENUITEM "E~xit", ID_EXIT
}
}
DLGTEMPLATE DLG_FORMAT LOADONCALL MOVEABLE DISCARDABLE
BEGIN
DIALOG "FORMATTING", DLG_FORMAT, 92, 90, 213, 75,
FS_NOBYTEALIGN | FS_DLGBORDER | WS_VISIBLE |
WS_CLIPSIBLINGS | WS_SAVEBITS, FCF_TITLEBAR
BEGIN
CONTROL "NUMERIC", 256, 11, 49, 50, 8, "MonoStatic", SS_TEXT |
DT_LEFT | DT_TOP | WS_GROUP | WS_VISIBLE
CONTROL "ALPHA", 257, 12, 29, 29, 8, "MonoStatic", SS_TEXT |
DT_LEFT | DT_TOP | WS_GROUP | WS_VISIBLE
CONTROL "ALNUM", 258, 10, 11, 37, 8, "MonoStatic", SS_TEXT |
DT_LEFT | DT_TOP | WS_GROUP | WS_VISIBLE
CONTROL "(999) 999-9999", ID_NUMERIC, 65, 49, 96, 8, "Formatted",
ES_LEFT | ES_MARGIN | WS_TABSTOP | WS_VISIBLE CTLDATA 0
CONTROL "AAAAAAAA", ID_ALPHA, 64, 29, 67, 8, "Formatted",
ES_LEFT | ES_MARGIN | WS_TABSTOP |
WS_VISIBLE CTLDATA FLD_TOUPPER
CONTROL "", ID_ALNUM, 64, 10, 71, 8, "Formatted",
ES_LEFT | ES_MARGIN |
WS_TABSTOP | WS_VISIBLE
CTLDATA FLD_ALPHANUMERIC /* | FLD_REQUIRED */
CONTROL "OK", ID_OK, 159, 9, 38, 12, WC_BUTTON,
BS_PUSHBUTTON | BS_DEFAULT | WS_TABSTOP | WS_VISIBLE
END
END
FMT.H
#define DLG_FORMAT 300
#define ID_OK 262
#define ID_ALNUM 261
#define ID_ALPHA 260
#define ID_NUMERIC 259
#define ID_DLG 500
#define ID_EXIT 599
FMT.DEF
NAME fmt WINDOWAPI
DESCRIPTION 'Formatting Test'
PROTMODE
HEAPSIZE 1024
STACKSIZE 8192
EXPORTS ClientWndProc
FmtDlgProc
FmtWndProc
MonoStaticWndProc
FMT.C
/*****************************************************************/
/* Formatted edit routines for Presentation Manager. */
/*****************************************************************/
#define INCL_WIN
#define INCL_DOS
#define INCL_GPI
#include <os2.h>
#include <stdio.h>
#include <ctype.h>
#include <malloc.h>
#include <stdarg.h>
#include "fmt.h"
#define SimpleMessage(hWnd, msg) WinMessageBox(hWnd, hWnd, \
(PSZ) msg, (PSZ) "Message", 0, MB_OK)
#define LCID_COURIER 1L
FONTMETRICS FMCourier;
FATTRS FontAttrs;
LONG IDCourier = 0;
HPS hMyPS;
HWND hWndFrame,
hWndClient;
HAB hAB;
PFNWP pfnOldEditWndProc = (PFN) NULL;
PFNWP pfnOldStaticWndProc = (PFN) NULL;
MRESULT EXPENTRY ClientWndProc(HWND, USHORT, MPARAM, MPARAM);
MRESULT EXPENTRY FmtDlgProc(HWND, USHORT, MPARAM, MPARAM);
MRESULT EXPENTRY FmtWndProc(HWND, USHORT, MPARAM, MPARAM);
MRESULT EXPENTRY MonoStaticWndProc(HWND, USHORT, MPARAM, MPARAM);
void pascal EditSetCursor(HWND hWnd);
extern PSZ lstrcpy(PSZ s, PSZ t);
int pascal ValidDigit();
int pascal ValidDigitSignSpace();
int pascal ValidAlpha();
int pascal ValidAlphaNum();
int pascal ValidLogical();
int pascal ValidAny();
int pascal ConvertToUpper();
int pascal ConvertToLower();
typedef struct format
{
ULONG fFormatFlags;
#define FLD_HASPICTURE 0x0001L
#define FLD_AUTONEXT 0x0002L /* Go to next field when all filled */
#define FLD_NOECHO 0x0004L /* Don't echo chars (for passwds) */
#define FLD_PROTECT 0x0008L /* No data in this field */
#define FLD_IGNORE 0x0010L /* Cursor skips over this field */
#define FLD_REQUIRED 0x0020L /* User MUST enter data in this field */
#define FLD_TOUPPER 0x0040L /* Convert characters to uppercase */
#define FLD_TOLOWER 0x0080L /* Convert characters to lowercase */
#define FLD_CENTER 0x0100L /* Center the data in the field */
#define FLD_RJUST 0x0200L /* Right-justify the data in the field */
#define FLD_NUMERIC 0x0400L
#define FLD_SIGNEDNUMERIC 0x0800L
#define FLD_ALPHA 0x1000L
#define FLD_ALPHANUMERIC 0x2000L
#define FLD_LOGICAL 0x4000L
#define FLD_MIXEDPICTURE 0x10000L
NPFN pfnValidChar; /* Function to validate each char */
PSZ szPicture; /* Picture string */
} FORMAT, FAR *PFORMAT;
struct mask_to_func
{
ULONG mask;
NPFN pfnFunc;
} MaskToFunc[] =
{
FLD_NUMERIC, ValidDigit,
FLD_SIGNEDNUMERIC, ValidDigitSignSpace,
FLD_ALPHA, ValidAlpha,
FLD_ALPHANUMERIC, ValidAlphaNum,
FLD_LOGICAL, ValidLogical,
};
typedef struct picinfo
{
NPFN pfnPicFunc; /* Validation function corresponding to mask */
BYTE chPic; /* The mask character */
} PICINFO;
extern PICINFO *CharToPicInfo(int c);
PICINFO PicInfo[] =
{
ValidDigit, '9',
ValidDigitSignSpace, '#',
ValidAlpha, 'A',
ValidLogical, 'L',
ValidAlphaNum, 'N',
ValidAny, 'X',
ConvertToUpper, '!',
NULL, '\0'
};
/*******************************************************************/
/* main()
*/
/*******************************************************************/
int main(void)
{
HMQ hMQ;
QMSG qMsg;
ULONG flCreateFlags = FCF_TITLEBAR | FCF_SYSMENU |
FCF_SIZEBORDER |
FCF_MINMAX | FCF_TASKLIST |
FCF_SHELLPOSITION | FCF_MENU;
PSZ szClassName = "ClientClass";
CLASSINFO clsInfo;
/* Initialize the window and create the message queue */
hAB = WinInitialize(0);
hMQ = WinCreateMsgQueue(hAB, 0);
/*
Get the address of the default window proc for edit controls. We
will call this proc to do most of the processing for the new
formatted edit controls.
*/
if (!WinQueryClassInfo(hAB, WC_ENTRYFIELD, (PCLASSINFO) &clsInfo))
{
DEBUG("Could not get class info for WC_ENTRYFIELD");
goto bye;
}
pfnOldEditWndProc = clsInfo.pfnWindowProc;
/* Register the main window class */
WinRegisterClass(hAB, szClassName, ClientWndProc,
CS_SIZEREDRAW, sizeof(PVOID));
if (!WinRegisterClass(hAB, "Formatted", FmtWndProc, CS_SIZEREDRAW,
clsInfo.cbWindowData + sizeof(PVOID)))
{
DEBUG("Could not register class Formatted");
goto bye;
}
/* We have our own static class too! */
if (!WinQueryClassInfo(hAB, WC_STATIC, (PCLASSINFO) &clsInfo))
{
DEBUG("Could not get class info for WC_STATIC");
goto bye;
}
pfnOldStaticWndProc = clsInfo.pfnWindowProc;
if (!WinRegisterClass(hAB, "MonoStatic", MonoStaticWndProc,
CS_SIZEREDRAW, clsInfo.cbWindowData + sizeof(PVOID)))
{
DEBUG("Could not register class MonoStatic");
goto bye;
}
/* Create the main window */
hWndFrame = WinCreateStdWindow(HWND_DESKTOP, /* Parent */
WS_VISIBLE /* Window styles */
&flCreateFlags, /* Frame styles */
szClassName, /* Window title */
CS_SIZEREDRAW, /* Client style */
NULL, /* Resource ID */
DLG_FORMAT, /* Frame window id */
&hWndClient); /* Client handle */
/* Message Processing Loop. */
while (WinGetMsg(hAB, &qMsg, NULL, 0, 0))
{
WinDispatchMsg(hAB, &qMsg);
}
/* End of the program. Destroy the window and message queue. */
WinReleasePS(hMyPS);
WinDestroyWindow(hWndFrame);
bye:
WinDestroyMsgQueue(hMQ);
WinTerminate(hAB);
return 0;
}
/********************************************************************/
/* ClientWndProc()
*/
/* Main window procedure for this app. All messages to the client */
/* window get sent here.
*/
/********************************************************************/
MRESULT EXPENTRY ClientWndProc(HWND hWnd, USHORT msg, MPARAM mp1, MPARAM
mp2)
{
HPS hPS;
switch (msg)
{
case WM_PAINT :
/* Erase the window */
hPS = WinBeginPaint(hWnd, NULL, NULL);
GpiErase(hPS);
WinEndPaint(hPS);
return MRFROMSHORT(TRUE);
case WM_COMMAND :
switch (COMMANDMSG(&msg)->cmd)
{
case ID_DLG :
WinDlgBox(HWND_DESKTOP, hWnd, FmtDlgProc, NULL,
DLG_FORMAT, NULL);
break;
case ID_EXIT :
WinPostMsg(hWnd, WM_CLOSE, 0L, 0L);
break;
}
return MRFROMSHORT(FALSE);
} /* end switch */
return WinDefWindowProc(hWnd, msg, mp1, mp2);
}
/********************************************************************/
/* FmtDlgProc()
*/
/* Driver for the sample dialog box. */
/********************************************************************/
MRESULT EXPENTRY FmtDlgProc(HWND hDlg, USHORT msg, MPARAM mp1,
MPARAM mp2)
{
HWND hEdit;
struct fldfmtinfo *pFld;
PFNWP pfn;
PICINFO *pi;
int i;
switch (msg)
{
case WM_INITDLG :
/* Get the monospaced font (Courier) */
hMyPS = WinGetPS(hDlg);
SetMonospacedFont(hMyPS);
/*
for (i = 256; i <= 258; i++)
if ((hEdit = WinWindowFromID(hDlg, i)))
{
pfn = WinSubclassWindow(hEdit, MonoStaticWndProc);
if (!pfnOldStaticWndProc)
pfnOldStaticWndProc = pfn;
}
*/
return MRFROMSHORT(FALSE);
case WM_COMMAND :
switch (COMMANDMSG(&msg)->cmd)
{
case ID_OK :
WinDismissDlg(hDlg, TRUE);
return MRFROMSHORT(TRUE);
default :
break;
}
return MRFROMSHORT(TRUE);
}
return WinDefDlgProc(hDlg, msg, mp1, mp2);
}
/********************************************************************/
/* FmtWndProc()
*/
/* Window Proc for the new formatted edit control class. We will */
/* process some of the messages, but mostly, we rely on the */
/* standard edit control window proc to handle the hard stuff. */
/********************************************************************/
MRESULT EXPENTRY FmtWndProc(HWND hWnd, USHORT msg, MPARAM mp1,
MPARAM mp2)
{
PSZ szFmtMask;
PSZ szPicture = NULL;
char buf[128], *s;
char chFmt;
MRESULT mr;
PICINFO *pi;
HPS hPS;
int rc, iPos, picch, piclen;
int incr;
int cnt;
PFORMAT pFmt;
struct fldfmtinfo *pFld;
switch (msg)
{
/* WM_CREATE
We create a format information structure and associate it with
this control. */
case WM_CREATE:
{
PCREATESTRUCT pCr;
/* Get the control ID from the create-structure */
pCr = (PCREATESTRUCT) PVOIDFROMMP(mp2);
/* See if we have a picture clause in the edit control's title.
If so, save the picture and set the title to NULL so that
the edit control will be initially empty. */
if (pCr->pszText && pCr->pszText[0])
{
szPicture = pCr->pszText;
pCr->pszText = NULL;
}
/* Let the normal edit proc do its thing. */
pfnOldEditWndProc(hWnd, msg, mp1, mp2);
/* Search the list of formatted controls for this one */
/* Allocate a format structure and have the window point to it */
pFmt = (PFORMAT) malloc(sizeof(FORMAT));
WinSetWindowPtr(hWnd, 0, (PVOID) pFmt);
/* There is an address passed in mp1 - this means a char mask. */
if (szPicture)
{
/* Set a bit to signify that we have a character mask, and copy
the mask into some private storage area. */
pFmt->fFormatFlags = FLD_HASPICTURE;
pFmt->szPicture = lstrcpy((PSZ) malloc(lstrlen(szPicture)+1),
szPicture);
/* We want to display the edit control's "protected"
characters, but we don't want to display the mask
characters. We copy the mask into a character array,
translating the mask chars into blanks. If we have protected
characters in the mask, then set a bit to signify this. */
lstrcpy((PSZ) buf, szPicture);
for (s = buf; *s; s++)
if (CharToPicInfo(*s))
*s = ' ';
else
pFmt->fFormatFlags |= FLD_MIXEDPICTURE;
WinSetWindowText(hWnd, (PSZ) buf);
}
if (mp1)
{
/* We got a bitmask instead of a character mask.
We search the mask table for this mask, and set the
corresponding character validation function. */
struct mask_to_func *mf;
pFmt->fFormatFlags |= (long) (* (PUSHORT) mp1);
for (mf = MaskToFunc;
mf < MaskToFunc +
sizeof(MaskToFunc)/sizeof(MaskToFunc[0]); mf++)
if (pFmt->fFormatFlags & mf->mask)
{
pFmt->pfnValidChar = mf->pfnFunc;
break;
}
}
/* Set the cursor position to the first character */
pfnOldEditWndProc(hWnd, EM_SETSEL, 0L, 0L);
return MRFROMSHORT(FALSE);
}
/* WM_CHAR */
case WM_CHAR:
if ((pFmt = (PFORMAT) WinQueryWindowPtr(hWnd, 0)) == NULL)
break;
if (CHARMSG(&msg)->fs & KC_CHAR)
{
BYTE c = (BYTE) CHARMSG(&msg)->chr;
if (c = = '\t' || c = = '\n' || c = = 27)
return MRFROMSHORT(FALSE);
if (pFmt->szPicture)
{
iPos = SHORT1FROMMR(pfnOldEditWndProc(hWnd, EM_QUERYSEL, 0L,
0L));
piclen = strlen(pFmt->szPicture);
/* Don't let the user type beyond the last picture char */
if (iPos >= piclen)
return MRFROMSHORT(TRUE);
/* Validate the character */
pi = CharToPicInfo(picch = pFmt->szPicture[iPos]);
if (picch = = '!')
c = ConvertToUpper(c);
if (pi && pi->pfnPicFunc && (*pi->pfnPicFunc)(c) = = FALSE)
return MRFROMSHORT(FormatError());
/* Implement overstrike mode by deleting the next character */
pfnOldEditWndProc(hWnd, EM_SETSEL, MPFROM2SHORT(iPos,
iPos+1), 0L);
pfnOldEditWndProc(hWnd, EM_CLEAR, 0L, 0L);
/* Let the edit win proc handle the insertion of the character */
pfnOldEditWndProc(hWnd, WM_CHAR, mp1, mp2);
/* Find out what position we are in */
iPos = SHORT1FROMMR(pfnOldEditWndProc(hWnd, EM_QUERYSEL, 0L,
0L));
advance:
/* If the char typed was last one in the picture, go back to the left. */
if (iPos >= piclen)
{
c = VK_LEFT;
mp1 = MPFROMSH2CH(KC_VIRTUALKEY, 1, 0);
mp2 = MPFROM2SHORT(0, VK_LEFT);
goto rightleft;
}
/* Figure out how many "protected" columns to skip */
cnt = 0;
while (iPos < piclen && (picch = pFmt->szPicture[iPos]) &&
CharToPicInfo(picch) = = NULL)
iPos++, cnt++;
/* Skip 'em if there is a valid column to go to */
if (picch)
while (cnt--)
{
mp1 = MPFROMSH2CH(KC_VIRTUALKEY, 1, 0);
mp2 = MPFROM2SHORT(0, VK_RIGHT);
pfnOldEditWndProc(hWnd, WM_CHAR, mp1, mp2);
}
return MRFROMSHORT(TRUE);
}
/* NO MASK - we just have a validation function */
else
{
if (pFmt->pfnValidChar && (*pFmt->pfnValidChar)
(c) = = FALSE)
return MRFROMSHORT(FormatError());
if (pFmt->fFormatFlags & FLD_TOUPPER)
c = toupper(c);
else if (pFmt->fFormatFlags & FLD_TOLOWER)
c = tolower(c);
}
} /* end if KC_CHAR */
else if (CHARMSG(&msg)->fs & KC_VIRTUALKEY)
{
BYTE c = (BYTE) CHARMSG(&msg)->vkey;
if (c = = VK_TAB || c = = VK_BACKTAB || c = = VK_NEWLINE ||
c = = VK_ESC)
return MRFROMSHORT(FALSE);
if (pFmt->szPicture)
{
iPos = SHORT1FROMMR(pfnOldEditWndProc(hWnd, EM_QUERYSEL, 0L,
0L));
piclen = strlen(pFmt->szPicture);
/* Don't let the user type beyond the last picture char */
if (iPos >= piclen)
return MRFROMSHORT(TRUE);
switch (c)
{
case VK_LEFT:
case VK_RIGHT:
{
rightleft:
incr = (c = = VK_LEFT) ? -1 : 1;
do
{
/* BUG - we don't know what rc is */
rc = SHORT1FROMMR(pfnOldEditWndProc(hWnd,WM_CHAR,mp1,mp2));
}
while (rc && !CharToPicInfo(pFmt->szPicture[iPos +=
incr]));
EditSetCursor(hWnd);
return MRFROMSHORT(TRUE);
}
case VK_HOME:
/* Pass the HOME key onto the edit control & set position to 0 */
pfnOldEditWndProc(hWnd, WM_CHAR, mp1, mp2);
iPos = 0;
goto advance;
case VK_END:
/* Pass the END key onto the edit control, then get the position */
pfnOldEditWndProc(hWnd, WM_CHAR, mp1, mp2);
iPos = SHORT1FROMMR(pfnOldEditWndProc(hWnd,
EM_QUERYSEL, 0L, 0L));
/* Move left until we hit a maskable character */
while (iPos >= piclen ||
(picch = pFmt->szPicture[iPos]) &&
!CharToPicInfo(picch))
{
mp1 = MPFROMSH2CH(KC_VIRTUALKEY, 1, 0);
mp2 = MPFROM2SHORT(0, VK_LEFT);
if (!pfnOldEditWndProc(hWnd, WM_CHAR, mp1, mp2))
break;
iPos--;
}
EditSetCursor(hWnd);
return MRFROMSHORT(TRUE);
case VK_DELETE:
case VK_INSERT:
case VK_BACKSPACE:
/* If we have protected chars in this field, don't let the
user delete or toggle insert mode. */
if (pFmt->fFormatFlags & FLD_MIXEDPICTURE)
return MRFROMSHORT(TRUE);
break;
default:
break;
return MRFROMSHORT(FALSE);
} /* end switch (c) */
} /* if picture */
} /* end if KC_VIRTUALKEY */
return MRFROMSHORT(TRUE);
/* WM_SETFOCUS
We intercept this to insure that the cursor is not placed
over a protected mask character. */
case WM_SETFOCUS :
if (SHORT1FROMMP(mp2) == FALSE) /* losing focus? */
break;
/* Get the window's format structure */
if ((pFmt = (PFORMAT) WinQueryWindowPtr(hWnd, 0)) == NULL ||
!pFmt->szPicture)
break;
/* Let the normal edit proc do its thing for setting focus. */
rc = pfnOldEditWndProc(hWnd, msg, mp1, mp2);
/* Get current position. Move past all protected mask chars */
iPos = SHORT1FROMMR(pfnOldEditWndProc(hWnd,
EM_QUERYSEL, 0L, 0L));
while ((picch = pFmt->szPicture[iPos]) &&
CharToPicInfo(picch) == NULL)
{
/* We move past a mask character by simulating the user pressing
the RIGHT arrow key. */
mp1 = MPFROMSH2CH(KC_VIRTUALKEY, 1, 0);
mp2 = MPFROM2SHORT(0, VK_RIGHT);
if (!pfnOldEditWndProc(hWnd, WM_CHAR, mp1, mp2))
break;
iPos++;
}
return MRFROMSHORT(rc);
/* WM_PAINT
We do the drawing and shadowing ourselves. */
case WM_PAINT :
{
USHORT iLen;
HPS hPS;
char szText[128];
SWP swp;
POINTL ptL;
CURSORINFO cursorInfo;
/* Erase the edit control */
hPS = WinBeginPaint(hWnd, NULL, NULL);
GpiErase(hPS);
WinEndPaint(hPS);
/* Get the text and the coordinates of the control. */
iLen = WinQueryWindowText(hWnd, sizeof(szText), (PCH) szText);
WinQueryWindowPos(hWnd, (PSWP) &swp);
/* Draw the border around the control. */
ptL.x = swp.x; ptL.y = swp.y;
GpiMove(hMyPS, (PPOINTL) &ptL);
ptL.x += swp.cx;
GpiLine(hMyPS, (PPOINTL) &ptL);
ptL.y += swp.cy;
GpiLine(hMyPS, (PPOINTL) &ptL);
ptL.x -= swp.cx;
GpiLine(hMyPS, (PPOINTL) &ptL);
ptL.y -= swp.cy;
GpiLine(hMyPS, (PPOINTL) &ptL);
/* Draw the shadow */
ptL.x = swp.x + 4;
ptL.y = swp.y;
GpiMove(hMyPS, (PPOINTL) &ptL);
ptL.x += swp.cx;
ptL.y -= 4;
GpiBox(hMyPS, DRO_FILL, (PPOINTL) &ptL, 0L, 0L);
GpiMove(hMyPS, (PPOINTL) &ptL);
ptL.x = swp.x + swp.cx;
ptL.y += swp.cy;
GpiBox(hMyPS, DRO_FILL, (PPOINTL) &ptL, 0L, 0L);
/* Draw the edit text in a monospaced font. */
ptL.x = swp.x;
ptL.y = swp.y + (swp.cy - FMCourier.lMaxBaselineExt) / 2
+ FMCourier.lInternalLeading;
GpiCharStringAt(hMyPS, (PPOINTL) &ptL,
(LONG) iLen, (PCH) szText);
EditSetCursor(hWnd);
/* Return FALSE to signify that we processed the message ourselves. */
return MRFROMSHORT(FALSE);
}
case WM_DESTROY :
if ((pFmt = (PFORMAT) WinQueryWindowPtr(hWnd, 0)))
free(pFmt);
break;
} /* switch */
/* We got a message that we weren't interested in. Let the normal edit
procedure process the message. */
return pfnOldEditWndProc(hWnd, msg, mp1, mp2);
}
/*********************************************************************//*
EditSetCursor() */
/* Attempts to position the editing cursor correctly within a window */
/********************************************************************/ void
pascal EditSetCursor(HWND hWnd)
{
CURSORINFO cursorInfo;
SHORT iPos;
/* Find out the 0-based logical position of the cursor within the
edit field */
iPos = SHORT1FROMMR(pfnOldEditWndProc(hWnd, EM_QUERYSEL, 0L, 0L));
/* Get a copy of the cursor information */
WinQueryCursorInfo(HWND_DESKTOP, (PCURSORINFO) &cursorInfo);
/* The cursor is placed to the right of the current character. */
WinCreateCursor(hWnd, (SHORT) (iPos+1) *
FMCourier.lAveCharWidth -
FMCourier.lMaxCharInc, cursorInfo.y, 0, 0,
CURSOR_SETPOS, (PRECTL) NULL);
}
/********************************************************************/
/* CharToPicInfo() Returns a pointer to the PICINFO */
/* structure associated with character c. */
/********************************************************************/
PICINFO *CharToPicInfo(c)
{
PICINFO *p;
c = toupper(c);
for (p = PicInfo; p->chPic && p->chPic != c; p++)
;
return p->chPic ? p : NULL;
}
/*******************************************************************/
/* VALIDATION FUNCTIONS
*/
/*******************************************************************/
int pascal ValidDigit(c)
{
return isdigit(c);
}
int pascal ValidDigitSignSpace(c)
{
return isdigit(c) || isspace(c) || c = = '+' || c = = '-';
}
int pascal ValidAlpha(c)
{
return isalpha(c);
}
int pascal ValidAlphaNum(c)
{
return isalnum(c);
}
int pascal ValidLogical(c)
{
return strchr("TtFfYyNn", c) != NULL;
}
int pascal ValidAny(c)
{
return TRUE;
}
int pascal ConvertToUpper(c)
{
return toupper(c);
}
/********************************************************************/
/* MonoStaticWndProc()
*/
/* Used to "front-end" the normal behavior of a static text control */
/* so that we could print it in a mono-spaced font. */
/********************************************************************/
MRESULT EXPENTRY MonoStaticWndProc(HWND hWnd,USHORT msg,
MPARAM mp1,MPARAM mp2)
{
HPS hPS;
POINTL ptL;
SHORT iLen;
SWP swp;
char szText[128];
switch (msg)
{
case WM_PAINT :
/* Call Begin/EndPaint in order to satisfy PM. */
hPS = WinBeginPaint(hWnd, NULL, NULL);
WinEndPaint(hPS);
/* Get the text of the static control and its position */
iLen = WinQueryWindowText(hWnd, sizeof(szText), (PCH) szText);
WinQueryWindowPos(hWnd, (PSWP) &swp);
/* Write the string using our own presentation space */
ptL.x = swp.x; ptL.y = swp.y;
GpiCharStringAt(hMyPS, (PPOINTL) &ptL, (LONG) iLen, (PCH) szText);
/* Return FALSE if we processed the message ourselves */
return MRFROMSHORT(FALSE);
}
return pfnOldStaticWndProc(hWnd, msg, mp1, mp2);
}
/********************************************************************/
/* SetMonospacedFont()
*/
/* Sets the font of the current presentation space to a mono-spaced */
/* font. Returns TRUE if we found the font, FALSE if not. */
/********************************************************************/
SetMonospacedFont(hPS)
HPS hPS;
{
FONTMETRICS *pMetrics;
LONG nFonts, nRequestFonts;
int i;
LONG rcMatch;
ERRORID errID;
PSZ szFont = "Courier";
static BOOL bFirstTime = TRUE;
/* Load in the Courier font */
if (bFirstTime)
{
GpiLoadFonts(hAB, (PSZ) "C:\\os2\\dll\\courier.fon");
bFirstTime = FALSE;
}
/* Make a dummy call to GpiQueryFonts() to find out how many
courier fonts are available. */
nRequestFonts = 0L;
nFonts = GpiQueryFonts(hPS, QF_PUBLIC | QF_PRIVATE, (PSZ) szFont,
&nRequestFonts, 0L, NULL);
if (nFonts = = 0)
return FALSE;
/* Allocate temp space to hold the font-metrics info */
if ((pMetrics = malloc((unsigned int) nFonts *
sizeof(FONTMETRICS))) = = NULL)
return FALSE;
/* Get the font-metric info for all Courier fonts */
GpiQueryFonts(hPS, QF_PUBLIC | QF_PRIVATE, (PSZ) szFont, &nFonts,
(LONG) sizeof(FONTMETRICS), pMetrics);
for (i = 0; i < nFonts; i++)
{
/* Set up the FontAttrs structure in preparation for
GpiCreateLogFont() */
FontAttrs.usRecordLength = sizeof(FontAttrs);
FontAttrs.fsSelection = pMetrics[i].fsSelection;
FontAttrs.lMatch = pMetrics[i].lMatch;
strcpy(FontAttrs.szFacename, szFont);
FontAttrs.idRegistry = pMetrics[i].idRegistry;
FontAttrs.usCodePage = 850;
FontAttrs.lMaxBaselineExt = pMetrics[i].lMaxBaselineExt;
FontAttrs.lAveCharWidth = pMetrics[i].lAveCharWidth;
FontAttrs.fsType = FATTR_TYPE_FIXED;
FontAttrs.fsFontUse = 0;
/* Given the above attributes, try to find the best match in a
Courier font */
rcMatch = GpiCreateLogFont(hPS, (PSTR8) szFont,
(LONG) ++IDCourier, (PFATTRS) &FontAttrs);
/* We got a match if rcMatch is 2 */
if (rcMatch == 2)
{
GpiSetCharSet(hPS, IDCourier);
GpiQueryFontMetrics(hPS, (long) sizeof(FONTMETRICS),
&FMCourier);
/* If the matched font is 16x9, then let's use it. */
if (FMCourier.lAveCharWidth = = 9L
&& FMCourier.lMaxBaselineExt >= 16L)
break;
}
}
free(pMetrics);
return (i < nFonts);
}
/********************************************************************/
/* FormatError()
*/
/* Sounds a little beep when the user presses a bad key. */
/********************************************************************/
FormatError()
{
WinAlarm(HWND_DESKTOP, WA_WARNING);
return TRUE;
}
/********************************************************************/
/* lstrlen(), lstrcpy()
*/
/* Miscellaneous far-pointer routines */
/********************************************************************/
int lstrlen(PSZ s)
{
int len = 0;
while (*s++)
len++;
return len;
}
PSZ lstrcpy(PSZ s, PSZ t)
{
PSZ orig_s = s;
while (*s++ = *t++) ;
return orig_s;
}
/********************************************************************/
/* DEBUG()
*/
/* Simple diagnostic routine which displays a message box. */
/********************************************************************/
char szDebug[82];
DEBUG(char *fmt, ...)
{
va_list arg;
va_start(arg, fmt);
vsprintf(szDebug, fmt, arg); /* format the message */
va_end(arg);
SimpleMessage(hWndClient, szDebug); /* put it in a message box */
}
Figure 2
#define FLD_HASPICTURE 0x0001L
#define FLD_AUTONEXT 0x0002L /* Go to next field when all filled */
#define FLD_NOECHO 0x0004L /* Don't echo the chars (for passwds) */
#define FLD_PROTECT 0x0008L /* Can't enter data into this field */
#define FLD_IGNORE 0x0010L /* The cursor skips over this field */
#define FLD_REQUIRED 0x0020L /* User MUST enter data in this field */
#define FLD_TOUPPER 0x0040L /* Convert characters to uppercase */
#define FLD_TOLOWER 0x0080L /* Convert characters to lowercase */
#define FLD_CENTER 0x0100L /* Center the data in the field */
#define FLD_RJUST 0x0200L /* Right-justify the data in the field */
#define FLD_NUMERIC 0x0400L
#define FLD_SIGNEDNUMERIC 0x0800L
#define FLD_ALPHA 0x1000L
#define FLD_ALPHANUMERIC 0x2000L
#define FLD_LOGICAL 0x4000L
#define FLD_MIXEDPICTURE 0x10000L
Figure 3
A Alphabetic (a-z, A-Z)
9 Decimal digit
# Digit, sign, or blank
N Alphanumeric
L Logical (T,t,F,f,Y,y,N,n)
X Any character
! Convert char to upper case
Figure 4
EM_QUERYFIRSTCHAR (PM) Return the offset to the first character visible
at the left edge of the
control window.
EM_SETFIRSTCHAR (PM) Scrolls the text so that the character at the
specified offset is the first
character visible at the left edge of the control window. Returns TRUE
if
successful, or FALSE if it is not.
EM_QUERYCHANGED (PM) Returns TRUE if the text has changed since the
EM_GETMODIFY (Win) last EM_QUERYCHANGED message.
EM_QUERYSEL (PM) Returns a long word that contains the offsets for
EM_GETSEL (Win) the first and last characters of the current selection in
the control window.
EM_SETSEL (PM) Sets the current selection to the specified
EM_SETSEL (Win) character offsets.
EM_SETTEXTLIMIT (PM) Allocates memory from the control heap for the
specified maximum number
EM_LIMITTEXT (Win) of characters, returning TRUE if it is successful,
FALSE if it is not.
The following messages exist only in Windows:
EM_CANUNDO Queries an edit control to see if it can respond to the
EM_UNDO message.
EM_CLEAR Clears characters in the selected area of the edit control.
EM_FMTLINES Adds or removes end-of-line characters from wordwrapped
lines.
EM_GETHANDLE Retrieves the local handle of the edit buffer.
EM_GETLINE Copies a line from the edit control.
EM_GETLINECOUNT Retrieves the number of lines of text in the control.
EM_GETRECT Retrieves the formatting rectangle of an edit control.
EM_LINEFROMCHAR Returns the number of the line which contains the ith
character.
EM_LINEINDEX Returns the index of a line.
EM_LINELENGTH Returns the number of characters in a line.
EM_LINESCROLL Scrolls the control by a certain number of lines.
EM_REPLACESEL Replaces the currently selected text with new text.
EM_SCROLL Does scrollbar-like scrolling of an edit control.
EM_SETHANDLE Establishes the text buffer which will be used by an edit
control.
EM_SETMODIFY Sets the "data modified" flag of an edit control.
EM_SETRECT Sets the formatting rectangle of an edit control.
EM_SETRECTNP Sets the formatting rectangle of an edit control without
repainting.
EM_SETWORDBREAK Supplies a new wordbreak function to the edit control.
EM_UNDO Undoes the last edit.
Techniques for Tuning and Optimization of PM and Windows Applications
Kevin P. Welch
Something of a renaissance in the art of program tuning and optimization has
occurred during the last few years. The sophistication of recently
introduced environments has caused developers to rethink many techniques and
procedures typically used to refine applications. The Microsoft Windows and
OS/2 Presentation Manager environments, for example, are two of the most
challenging environments ever created in which to develop software for
personal computers. Part of the challenge of developing in these
environments is to meet or exceed user expectations. In the past, pleasing
users was considerably easier because their expectations were lower,
interfaces were less complicated, and programs were generally smaller.
Today, users expect highly refined, full-featured applications that perform
flawlessly at maximum speed in a variety of situations. Developers are
meeting those expectations with improved optimization and tuning techniques.
This article describes techniques I have used with measured success in
structuring and reshaping Windows1 and OS/2 Presentation Manager (hereafter
"PM") applications. Although many of these techniques are not new, using
them together is a more powerful optimizing and tuning approach.
Note, however, that these techniques are designed to complement, not
replace, good software development. Because the basic issue is management,
applying these techniques is an art, not a highly quantified science.
Furthermore, these techniques are most applicable to large, moderately
well-structured applications. They are of little benefit to small or very
poorly designed applications.
A Good Application
Before you can optimize a program, you must have a clear understanding of
the characteristics of a "good" Windows or PM application: sound software
design principles, effective memory management, and well-managed code.
Using sound software design principles is probably the most determining
element in developing a successful application. Almost without exception,
well-written Windows and PM applications are built from the bottom up, using
layered construction techniques that incorporate large numbers of highly
modular, self-contained objects that have few interdependencies.
Furthermore, these techniques are usually supplemented with comprehensive
product specifications, implementation plans, and internal software
documentation standards.
Next to good design, effective memory management is probably the most
significant development issue facing Windows and PM programmers. Although
most applications require at least a certain amount of memory, some require
prodigious quantities of it--further increasing the need for good memory
management techniques.
Generally, well-written applications are built on the assumption that memory
is an extremely precious and limited system resource that must be shared
among competing processes. As a result, they typically implement their own
virtual memory management scheme and use system resources sparingly.
Whenever possible, these applications also tend to use a small number of
larger, movable global memory blocks instead of a large number of small
ones. They always clean up after themselves, and ensure that all unwanted
memory fragments and resources are purged from the system. Finally, they
attempt to minimize the amount of memory restructuring required when the
user performs particular operations.
Good code management is another extremely significant characteristic of
outstanding Windows and PM applications. Almost all large applications are
well-organized; they group collections of related functions into segments,
each less than 4Kb in size.
These applications also tend to use the C run-time library sparingly. Before
each library function is used, the associated memory, object, and support
code ramifications are considered.
Finally, the Windows or PM API is used as efficiently as possible by
avoiding redundant operations and needless message processing. Often
lower-level calls are substituted for higher-level, more generalized ones.
Methodology
We now explore three techniques: review, analysis, and optimization. Where
appropriate, specific examples of the principles involved are included. Many
of these techniques are general and tend to be useful only when applied to
large applications, and they assume a solid understanding of Windows and PM
programming.
Review
Sometimes serious development errors can be identified by a skilled observer
after a few minutes reviewing the application's source code. Although some
problems result from an insufficient or improper understanding of the
environment on the part of the designers, many can be traced to a poor
overall structure--something that plagues programs in any operating
environment.
Because the typical Windows or PM application contains a large percentage of
code related to the user interface, special attention should be given to the
hierarchy and ownership of windows and the message passing scheme used to
coordinate their activities. Many times applications inadvertently use
message-passing schemes that result in reentrancy, which can easily create
problems much later in the development cycle.
Commercial tools do exist that can ease the code review process. Perhaps the
simplest are those utilities that generate cross-reference listings for the
entire application. The reports generated by such programs make it
considerably easier to trace the use of specific functions or variables.
Because the central problem in optimizing and tuning Windows and PM
applications is complexity, software metrics are useful. Software metrics
enable you to understand the general characteristics of a piece of software
in quantitative terms. They are perhaps the best source-code based mechanism
for analyzing large programs. Software metrics clearly identify areas of
complexity and enable you to allocate review, analysis, and testing
resources effectively.
The most commonly used software metric expresses the size of a function or
an application in lines of noncommented source code. Although this metric is
sufficient in certain situations, it is often simplistic when used with
Windows or PM applications.
In the late 1970s and early 1980s computer scientists started searching for
more precise methods for quantifying software complexity. Their goal was to
develop a set of metrics that would quantitatively describe the complexity
of a piece of source code. The result was the development of several useful
new metrics that can be helpful to a person when trying to understand and
refine program operation.
Today 15 metrics, some statistical in nature, can be used to describe a
piece of source code (see Figure 1). Although a complete discussion of these
metrics is beyond the scope of this article, a few are particularly
interesting and can be of great help when reviewing a large amount of source
code.
The program volume (V) metric can be used as an alternative to lines of
source code (LOC) when describing program size. It is based on the
assumption that if a program has n unique operators and operands then it
would take approximately log2(n) statements to represent them uniquely. If
there are N total usages of these operators and operands, then the number of
statements necessary to represent the program would be:
V = N * log2(n)
Because this metric is closely tied to the number of mental lookups required
to read and understand a particular program fully, it can be statistically
related to the number of errors that might be present in the work.
Independent research by psychologists and several empirical studies have
demonstrated, with some degree of confidence, that the resulting statistic
can be a useful starting point when allocating testing resources to a
specific project.
The software science effort (E) metric reflects the number of mental
discriminations that a programmer must perform to write a particular
program. Studies show that, in addition to being a good metric for
describing the complexity of a piece of source code, this metric can also be
used to estimate the amount of time needed to write the program. Although
the resulting estimates need to be carefully calibrated, using the
characteristics of the environment and the programmer in question, they can
be quite useful.
The cyclomatic complexity (VG1) metric is a popular metric that attempts to
quantify the flow of control in a program, based on the understanding that
the more decision points in an application, the more complex it will be.
Apart from being a fairly good estimate of complexity, this metric can be
calculated easily from the source code by counting decision-making
statements.
During the spring of 1988, I used software metrics to assist in analyzing
and optimizing Scrapbook+ (a product developed by David West and myself).
Although we were intimately familiar with the product's structure, we felt
that software metrics would give insight into the global workings of our
program.
We used an analysis tool called PC-METRIC to process each of the related C
source code files and generate the base statistics. Once the data was
collected, we created a huge series of linked Microsoft Excel spreadsheets
that enabled us to interpret and summarize graphically the resulting data by
function, module, and application. Figure 2 shows the basic metrics that we
collected from the analysis of each source code module in Scrapbook+.
Based on this master spreadsheet, we created a series of charts that show
many of the significant values. Figure 3 displays the number of lines
(noncommented) of source code, by module, for the entire application. As you
might expect, this graph correlates somewhat to the software volume shown in
Figure 4.
The analysis becomes a little more interesting, however, when we compare
Figure 3 with Figures 5 and 6. The extended cyclomatic complexity graph,
Figure 5, is more evenly distributed, taking into account the many short but
complex modules in the application. The software science effort graph,
Figure 6, clearly demonstrates that the bulk of the development effort (and
thus anticipated errors) went into creating SCRAP01.C, SCRAP22.C, and
SCRAP25.C.
These graphics helped us a great deal, especially considering that SCRAP01.C
contained the main window message processing function, SCRAP22.C the
initialization and cleanup related functions, and SCRAP25.C the database and
data formatting functions.
Using the metrics we had generated and our internal programming error rate,
we also statistically calculated the number of errors we should expect when
beta testing the product (see Figure 7). This value, which turned out to be
surprisingly accurate, was of great help when deciding to end testing and
make the product commercially available.
Analysis
After you have thoroughly reviewed the source code, you can begin more
specific tests to determine areas for improvement. One test commonly
overlooked is running the program in the same or similar conditions as those
of your potential customers. The working environment of many Windows and PM
programmers is often dramatically different from that of the average
software user. While analyzing your application, use a variety of hardware,
display adapters, and printers. Deliberately run the application while using
other programs and in low memory or out-of-disk-space situations.
Throughout this process, record all your observations, even to the extent of
timing various critical operations. These timings are especially valuable as
they quantify the changes you make, both good and bad. In most cases you
will find that tuning involves trade-offs, sometimes improving performance
in one area and degrading it in others.
Whenever possible, use additional software tools to assist you in this
evaluation. For example, in Windows you can use Spy to examine the window
hierarchy used by an application and watch the various messages exchanged
between windows. You should also run the program with Shaker to see how it
performs when segments are being moved rapidly.
You can use HeapWalk to investigate how code and data segments associated
with the application are loaded and discarded. Ideally, almost all segments
should be slightly less than 4Kb in size, be marked movable and discardable,
and have a lock count of zero. By comparing the MAP generated when the
program was linked with the information displayed by HeapWalk, you can get a
good idea of the "working set" of code required to perform a particular
operation. In addition, look for segments whose lock count never returns to
zero after use and for GDI objects that are never discarded.
Resource X-Ray by Eikon Systems, Inc (see Figure 8) is a tool that lets you
open an application and examine each of its associated resources. You can
look at each cursor, icon, menu, dialog box, font, string table, and
accelerator. Many times you can find improperly defined or unused resources
that would be difficult to identify any other way.
To look closer at a particular resource, you can copy it to the clipboard
and paste it into one of the resource editors provided with the Windows
Software Development Kit.
After you complete an external analysis of the application, you can proceed
to a more detailed internal analysis. By now you should know where the
application could be improved. Although you can accomplish internal analysis
in a number of ways, the goal is to understand clearly how the application
uses the API provided by the environment and its flow of execution.
The best way to understand how the system API is used is to create your own
debugging versions of critical system calls. These versions, in turn, can
output debugging information before calling the function provided by the
system. This is an excellent way to examine each parameter and its frequency
of use--information that is especially useful when functions with
considerable overhead are used.
To profile the sequence of function calls used inside the application you
can use the Microsoft CodeView debugger or insert your own debugging calls
at the beginning and end of each block of code. You can make these debugging
calls with a debugging utility such as the one described in the article
"Debug Microsoft Windows Programs More Effectively with a Simple Utility,"
MSJ (Vol. 3, No. 3).
Using these debugging calls, you can create a directed graph (see Figure 9)
that summarizes the flow of control through the application for a specific
operation or command. Each node in the graph should represent a specific
function and each arrow should represent a call from one function to
another. If multiple calls are made to the same function, you should
indicate how many times the call was made during the profile you are
creating, and note on the graph the current segment assignment and size in
bytes of each function.
Repeat this process in a variety of situations, until you have a series of
directed graphs that summarize the behavior of the application in each
circumstance. Although this data gathering can be tedious, it is extremely
valuable when optimizing the program. Figure 9 illustrates two such directed
graphs, each profiling the flow of control resulting from a particular
operation in a simple application. Note especially the reentrancy involved
in each operation and the current segment assignment of each function.
Optimization
After reviewing your source code and analyzing its operation, where and how
your application can be improved should be apparent. Optimization is
probably the most critical, least understood, and most time-consuming step.
In some situations be prepared to stop, accept the fact that your program
can no longer be improved, and start rewriting it immediately. Although this
is a rather drastic form of optimization, rewrites almost always turn out
much better than the previous version and often require only a fraction of
the original time to develop.
Generally, your application will be more than acceptable in certain
situations and less than acceptable in others. You must decide somewhat
arbitrarily what is most important for your users and begin to adjust
accordingly.
Approach optimization on two levels; on a line-by-line basis throughout the
source code, and on a functional or structural level taking into account the
entire application.
You are probably familiar with optimizing individual statements and
functions. Usually you verify that each function is based on a good
algorithm (not too simple yet not unnecessarily complex) and that it is well
implemented and thoroughly tested. In unacceptable cases, you may need to
rewrite one or two functions, perhaps using Assembler.
To a certain extent, you can use your compiler and linker to improve further
the performance of your application. Although compiler optimization
techniques, such as loop optimization and pointer aliasing, can result in
dramatic improvements, they are not a substitute for good design and
implementation. In addition, before using such compilation options, verify
that your code is valid under lower levels of optimization. Only then should
you activate additional optimization options. Finally, always carefully test
the optimized code, being prepared to disable optimization for particular
modules when problems arise.
Optimizing on a structural level assumes the presence of a collection of
well-written functions, to be arranged into carefully related segments
called working sets. By carefully restructuring your application into
carefully chosen working sets, you can minimize the number of intersegment
function calls, thus reducing the total amount of memory required by the
application. To do this restructuring, you need the information collected in
the review and analysis steps.
Because the ideal Windows or PM application uses a minimal amount of memory
and contains many small, interrelated code segments, each slightly less than
4Kb in size, you should avoid large data segments that are fixed for
extended lengths of time or several code segments that are repetitively
loaded and discarded. Unfortunately, determining an optimal arrangement for
a large number of such code and data segments is difficult if not
impossible.
Using the directed graphs you created in the analysis phase, you can create
a mathematically blended model that summarizes the flow of control
throughout your application. In this blending, you may choose to ignore
certain aspects of the program and focus on areas that are of more concern
to your users.
Figure 10 demonstrates the blending of the two operational profiles
described in Figure 9 into one directed graph. In this illustration, the two
profiles were considered equally important, and no mathematical averaging
was performed. If, for example, the first operational profile in Figure 9
was considered twice as important as the second profile, we would double
each of the function call values in the first profile before combining them.
Additionally, we could ignore relatively unimportant sections of the
profile.
Once the blended operational profile has been created, rearrange each of the
functions into segments such that the number of intersegment calls is
minimized. This rearrangement should be limited in size to less than 4Kb.
The resulting segment assignment in Figure 10 eliminates those previously
more than 4Kb and reduces the total number of intersegment references from
41 to 13.
Although your application will, in all probability, be considerably more
complex than this example, the same principles apply. You could also use
relatively conventional nonlinear optimization techniques, perhaps using the
duality of the original problem. In most cases, however, you will probably
want to do the work by hand. This enables you to minimize the amount of
restructuring required and group related functions into the same segment
whenever appropriate.
When you have determined a collection of working sets that minimize
intersegment references, you can start rearranging your source code. While
doing this, be sure to include the code segment that contains the run time
library functions. You may need to replace some of these functions with your
own or perhaps purchase the run-time library source code and recompile it
into several segments.
While we were developing Scrapbook+, we were made aware of this issue when
we discovered that the entire Microsoft C floating-point library was linked
into our product. Throughout development we had carefully avoided using any
floating point arithmetic and were mystified at the inclusion of this extra
support code. After a lengthy search, we discovered a single assignment
statement (out of approximately 50,000 lines) defining the product version
as 1.00. Needless to say, we took this statement out and eliminated the need
for the entire floating-point library--dramatically reducing the size of our
_TEXT segment.
After restructuring and recompiling your application, be sure to test for
regression errors. Once you have verified that the program has survived this
rearrangement, you can perform various timings to assess the benefits of the
changes you made. You will probably find that certain areas need additional
optimization or possibly even rewriting.
Conclusion
Optimizing a Windows or PM application can involve considerable work.
Ideally, all our applications would be perfectly designed and use the
environment optimally. Unfortunately, in the real world it is difficult, if
not impossible, to achieve this.
Consequently, the job of application tuning and optimization will probably
be with us for some time to come. In the future it is likely that many new
and powerful tools will become available to assist you with this process;
but for now, I hope the ideas in this article will help.
Figure 1
n1 # of unique operators
n2 # of unique operands
N1 total # of operators
N2 total # of operands
n program vocabulary (n1+n2)
N "length" of program (N1+N2)
N^ predicted length of program
P/R software purity ration
V program volume
E software science effort
VG1 cyclomatic complexity
VG2 extended cyclomatic complexity
LOC lines of source code
<;> executable semicolons
SP span of reference for variables
A Survey of Windows and Presentation Manager Prototyping Tools
Marc Adler
Prototyping tools and application generators are programs that can not only
relieve some of the tedium of writing code but that can also automatically
generate a substantial amount of it. This article examines several
prototyping tools and application generators for the Microsoft
Windows environment and OS/2 Presentation Manager (hereafter "PM"):
CASE:W and CASE:PM from CASEWORKS, Winpro/2 and Winpro/PM from Xian
Corporation, and WAPE from Intersoft. The discussion focuses on the
ways in which the programs interact with the user, how each program
generates code, and the quality of the generated code.
Prototyping tools have been most often associated with database programs.
Users draw data entry forms on the screen, and then the prototyping tool
generates all the code to create the form, display it on the screen, have
it interact with the user, validate the data, and return the data to the
database. With more ambitious application generators, you can put together a
powerful application with pull-down menus and linked forms, among other
features.
Probably the most important use of application generators is to assist an
inexperienced programmer or a nontechnical user in assembling working code.
These are people who want to put together an application as quickly as
possible without having to learn a language.
Another use for prototyping tools is to aid programmers unfamiliar with
Windows1 and PM. Many programmers are finding it difficult to adjust to a
message-passing model from a sequential style of programming. Prototyping
tools can help them become more accustomed to object-oriented programming.
CASE:W and CASE:PM
CASE:W comprises an inference engine and a knowledge base of Windows
programming techniques. It breaks the program design phase into four stages:
Program Configuration, Main Window, Menus, and Client Area. You can access
each from the menu bar item called Design.
When you begin to write your application, you need to select the Program
Configuration option to choose your compiler options (including memory
model, optimization, stack size, heap size, CodeView debugger support, and
so on) and the appropriate libraries with which to link. You also define the
options you want to associate with your code and data segments (for example,
MOVEABLE, DISCARDABLE, and so on), and you determine the routines that your
application will import and export. CASE:W uses this information when it
generates your make file and the application's DEF file.
The second stage will most likely be the design of the application's main
window. First, the main window's caption is defined. In CASE:W you can click
on an object if you want to modify it. In this case, with the mouse cursor
over the main window's title bar, you would click the left button; CASE:W
brings up the dialog box, which prompts you for the new title.
You can choose the icon, cursor, and font that the application will use. For
the icon and cursor, CASE:W presents you with the same kind of file-chooser
dialog box found in so many Windows applications. You can select the
appropriate ICO or CUR resource. The font dialog box gives you a choice of
fonts.
To finish designing the main window, you can select its foreground and
background colors and its initial size and location. Unfortunately, CASE:W
has no way to define the frequently used accelerator and string table
resources, and the generated code does not include statements to load them.
The next stage of design is the layout of the menu system. CASE:W includes a
simple menu editor for building most of your menu system; unfortunately, it
lacks enough features to make it worthwhile to use. For instance, you cannot
delete a pull-down menu item once it has been defined, and you cannot
associate style attributes with a pull-down menu item (such as CHECKED,
GRAYED, or SEPARATOR).
Using the menu editor, you can append, insert, delete, and move menu bar
items. When you define a menu item, you can tell CASE:W to generate code
that invokes a modal or modeless dialog box when that item is selected, or
you can tell CASE:W that some user-defined code will be executed when the
item is selected. To generate code that invokes a modal or modeless dialog
box, you choose a dialog box from a list of those currently available in
your application. If you haven't defined the dialog box you want yet, CASE:W
will let you bring up the Microsoft dialog box editor, DIALOG.EXE. After you
select a dialog box, CASE:W generates a skeleton dialog box procedure for
it. CASE:W also generates the call to invoke that dialog box when its
corresponding menu item is selected. To execute user-defined code when that
item is selected, CASE:W creates a placemarker where you can insert your own
code. Between regenerations of the application code, CASE:W can distinguish
between your code and the code that it generates itself, as long as you
place your code after these placemarkers.
The final design stage is to define the code that is associated with any
dialog boxes not linked to the menu system. CASE:W generates the dialog box
procedure skeletons for the dialog boxes that you select; however, you must
write the code to invoke the dialog box yourself.
Now that the difficult part of designing your application is finished, you
can tell CASE:W to generate the code. CASE:W will generate the appropriate
make file, the DEF definition file, the C source code, and the RC file.
After the code is generated, if you are running the Windows/386 program, you
can invoke MAKE from within CASE:W to compile and link your application. If
you are running the Windows/286 program, you must exit Windows to compile
the program. You can then run your application to see how it works. To add
some more code or do a little tweaking, CASE:W lets you bring up an editor
(or Microsoft's Notepad) on the generated C file. After you finish your
editing, you can regenerate the code, recompile, and test-run your
application again.
CASE:W (see Figure 1) lets you access whatever Windows development tools you
need during your application building session. By default, CASE:W gives you
access to the Microsoft dialog editor, the icon editor, and the font editor.
You can define other tools you want to invoke.
CASE:W uses an ASCII file called REGEN.C that contains the skeleton of the C
file to be generated. This skeleton contains #include definitions, function
prototypes, a WinMain, a WndProc for the main window, and dialog box
procedure skeletons.
The WinMain function handles the tasks that a WinMain program normally
handles, such as the class registration, creation of color brushes,
displaying the main window, and the message loop.
WndProc primarily handles the WM_COMMAND messages that are sent to a window
procedure by Windows whenever a menu item is selected. CASE:W generates a
large switch statement to handle the code for each menu item in the
application. If a menu item has a dialog box associated with it, the call to
invoke that dialog box is generated. WndProc also sets the main window's
color by processing the WM_CREATE message and calling SetTextColor,
CreatePen, and SelectObject.
For every dialog box in the application, CASE:W generates a dialog box
procedure called xxxMsgProc, where xxx is the name of the dialog box. Code
is generated to dismiss the dialog box whenever an OK or CANCEL push button
is pressed. CASE:W claims to read the DLG file that the dialog editor
creates and to generate case statements based on the control IDs of each
control window. However, when I tried this on a simple file-open dialog box
that contained check boxes, list boxes, and edit fields, no code was
generated for those controls. The only code that was generated was the case
statement to process the OK and CANCEL buttons. For this operation, Winpro/2
(see below) really excels--it generates intelligent code for most controls
in a dialog box.
CASE:W can certainly assist the beginning Windows programmer to create an
application, but the experienced programmer can also use it to generate a
working skeleton of an application and save a few hours. CASE:PM (see Figure
2) functions in Presentation Manager the same way CASE:W does in Windows.
Once you are familiar with CASE:W, using CASE:PM is easy--the only
difference is the code generated.
WINPRO/2
Winpro/2 is another Windows code generator. Winpro/2 and CASE:W produce the
same kind of code for a Windows application, but unlike CASE:W, Winpro/2 is
not actually a Windows program. It is a translator--that is, Winpro/2 takes
a resource (RC) file and turns out a full Windows program from it.
To invoke Winpro/2, just use the name of your RC file. For example, if I had
an RC file called SAMPLE.RC, I would invoke Winpro/2 thus:
wpr2 sample
The hard part is that you still have to generate the RC file using the
Windows dialog editor and a lot of hand coding. Here, CASE:W excels by
providing easy access to the various Windows tools together with its own
menu editor. Using Winpro/2(see Figure 3), you only need to write an RC
file; no header (H) files are necessary, because Winpro/2 generates them.
Winpro/2 imposes certain naming conventions and restrictions on your RC
file. You must define a string table with the identifier IDS_NAME in it.
Winpro/2 uses this identifier to generate the names of the resulting C files
and the functions within the files. For instance, if I had the lines
STRINGTABLE
BEGIN
IDS_NAME "Sample"
END
in my RC file, Winpro/2 would use the word "Sample" as the prefix for the
names of the functions it generates. The first four characters are also used
as the first four characters of the names of the generated source files.
If you have the entry IDS_TITLE in your string table, Winpro/2 will use the
definition as the caption of the main window.
IDS_TITLE "My first application"
The next step in creating the RC file is the definition of your menu
system. Winpro/2 allows a single MENU statement within the RC file,
which is sufficient for most applications. You must identify the MENU
with the same name as the one found in the IDS_NAME string. Also, all of
the identifiers that are assigned to the MENUITEM items must be symbolic,
not numeric, such as MENUITEM "&Paste," ID_PASTE. When Winpro/2 generates
the code, it takes these symbolic identifiers and automatically generates
#define directives for them.
If you define a menu item with the identifier IDEXIT, Winpro/2 generates
code to exit your application when the user selects that menu item.
For each MENUITEM, a case statement is generated that calls a separate
function corresponding to that item. In the generated function, a call will
be made to invoke a dialog box if the menu item has the same symbolic
identifier as the dialog box. If the menu item does not invoke a dialog box,
code to display a simple message box will be generated.
The next step is to define the icon and the accelerator table that your
application will use. In the generated WinMain, Winpro/2 creates the code to
load the icon and the accelerator table.
The final step in creating your RC file is the definition of the dialog
boxes. You use the dialog editor to create them and then read the generated
DLG file into your RC file. You then edit each dialog definition to assign
the appropriate symbolic identifiers. If one of your MENUITEMs invokes a
dialog box, the dialog box should be given the same symbolic identifier as
that MENUITEM. For example, here is part of a resource file that defines a
file-open dialog box invoked by a menu item:
Example2 MENU
BEGIN
POPUP "&File"
BEGIN
MENUITEM "&Open...", midFILEOPEN
■
■
■
END
■
■
■
END
midFILEOPEN DIALOG 8, 20, 148, 102
STYLE WS_DLGFRAME | WS_POPUP
BEGIN
.... dialog box controls defined here
END
Like CASE:W, Winpro/2 can generate code for unlinked dialog boxes (those not
invoked from a menu item). Winpro/2 will generate the dialog box procedure,
but the programmer must code the call that invokes it.
Generating code for dialog box procedures is where Winpro/2 really shines.
The dialog box procedure name is generated by placing the dialog box's
symbolic ID before the DlgWndProc string. If my dialog box has the
identifier ID_PRINT, a dialog box function called ID_PRINTDlgWndProc is
generated. Acceptable code is generated for the following types of controls:
■ check boxes
■ auto check boxes
■ 3-state boxes
■ auto 3-state boxes
■ radio buttons
■ edit fields
Winpro/2 recognizes the special identifiers IDOK and IDCANCEL as IDs of
buttons that are used to dismiss a dialog box. If these are found, Winpro/2
generates the code to transfer the data from the dialog box controls to
variables within your program (in the case of IDOK) and also generates code
to dismiss the dialog box.
For each dialog box control, Winpro/2 generates a global variable that will
be used to hold the data associated with it. For instance, if you define an
edit field in a dialog box, Winpro/2 declares a global character array that
will be used to transfer the data to and from that edit field. For check
boxes, Winpro/2 will declare an integer variable. For radio buttons, an
integer variable will be declared that holds the identifier of the selected
radio button within the radio group.
Winpro/2 generates code that responds to the WM_INITDLG message sent to your
dialog box procedure immediately before the dialog box is displayed. The
generated code will transfer all of the data between the generated global
variables and the dialog box controls. For radio buttons, code is generated
to check the appropriate button.
Winpro/2 generates reasonable code to respond to WM_COMMAND messages. These
messages are sent to a dialog box procedure by Windows when certain events
happen, such as the selection of a radio button or a check box, the press of
a push button, or a change in an edit field. In the case of a check box,
code is generated to toggle the state of the check box between on and off.
For radio buttons, code is generated to highlight the selected button and
unhighlight the other buttons within the radio group. For edit controls, no
real code is generated. An IF statement that tests for an EN_KILLFOCUS
message (received by the dialog box procedure when an edit control loses the
input focus) is, however, generated with a comment to insert any
user-defined data validation code at that point.
As mentioned above, Winpro/2 will generate code to transfer the contents of
the dialog box controls back to the global variables if it detects that you
have defined a push button with the identifier IDOK.
Winpro/2 generates several files from the RC file, including a make file, a
linker response file, and a DEF file that contains EXPORTs for all of the
dialog box procedures. Two include files are generated, one containing the
automatically generated #define directive for the RC file and the other
containing the generated global variables that correspond to the dialog box
controls. Five C files are generated: a DISCARDABLE initialization file, a
file that declares the global variables, a DISCARDABLE/LOADONCALL file that
contains the menu processing code, a DISCARDABLE file that contains the
dialog box procedures, and a file containing the WinMain function.
The file xxxxMAIN.C (where xxxx is the first four letters of your
application name) contains WinMain, a routine to paint your main window with
the default colors, a large switch statement to handle all of the
WM_COMMANDs that come from selecting a menu item, a routine that is called
when your application terminates (which asks you if you really want to
terminate it), and the main window's WinProc. The WinProc has code to handle
WM_ERASEBKGND, and WM_CLOSE followed by the WM_QUERYENDSESSION,
WM_ENDSESSION, and WM_DESTROY messages.
The file xxxxINIT.C contains routines to load in the string table, register
the main window class, initialize the default brushes, and create and
display the main window. The file xxxxMENU.C contains one function for each
menu item defined. If a menu item does not call a dialog box, code is
generated to display a simple message box that contains the function's name.
If a menu item does call a dialog box, code to invoke the dialog box is
generated.
The heart of the program is the generated xxxxDLG.C file, which contains all
of the dialog box processing and is where you will change the most code.
Unfortunately, unlike CASE:W, Winpro/2 does not seem to maintain any kind of
regeneration points within the generated code. Therefore, if you modify the
generated C files, do not expect your code to be retained if you tell
Winpro/2 to regenerate your C files. If you write a substantial amount of
code and decide later that your application needs several more dialog boxes,
you will have to generate the dialog procedures yourself.
Winpro/PM is a Xian product for PM that is similar to Winpro/2 (see Figure
4). The processing in Winpro/PM is similar to that in Winpro/2, except the
generated C files contain PM code. The code that Winpro/2 generates is good
and complete.
WAPE
WAPE (Windows Application Programming Environment) is from Intersoft
Corporation. It has a small API that combines several Windows API functions.
WAPE has a help system and performs four services: maintaining projects (a
"project" is simply a group of files), editing source files, generating
code, and supporting other Windows application development tools (such as
the dialog editor, font editor, and so on).
When you start a WAPE session, you can use the admin service to create or
open a project. The admin service also lets you delete files from projects
and copy files among projects. After you select a file from a project, you
can work on it with the WAPE editor. The editor is fairly primitive, lacking
features such as regular expressions, brace matching, and macros; it has
basically the same capabilities as the Windows multiline edit class (cut and
paste, simple undo, and cursor movement). The WAPE editor can search and
replace and lets you jump to the beginning of a file, the end of a file, and
to a particular line number.
WAPE can import existing code templates into your file. Intersoft has
templates for the three functions every Windows programmer must code
eventually--the WinMain procedure, a generic window procedure, and a generic
dialog procedure. You can modify these imported templates to suit your
purposes.
The most interesting aspect of WAPE is the code generation process, which is
actually an online help system. Intersoft has documented each function of
the Windows API, categorized it, and organized access to it from the main
menu.
When you ask for help for a certain function, you choose the various
arguments for that function from a dialog box. If the argument must be
chosen from among several predefined values, you can choose from a list of
the possible arguments. You can also get help on any argument. After you
choose the arguments to the function, the code for that function call is
inserted into your source file. This feature basically eliminates the need
for the Windows reference manual.
You can get this assistance for the WAPE API as well as for the Windows API.
The WAPE API consists of 32 functions that will perform some of the most
repetitive Windows tasks. For instance, the ZSetListBoxStrings function
fills a list box with each string from a passed array of strings. Although
this is not a terribly important function, it does its part in helping to
cut down coding time.
WAPE gives you access to the existing Windows tools that let you build
resources. You can choose to invoke the Windows dialog editor, the font
editor, or the icon editor. WAPE also provides a menu editor to help you
create your pull-down menu system--one of the more tedious tasks in Windows
programming. You can assign the various menu attributes to each menu item
(such as GRAYED, CHECKED, and INACTIVE). When you save the menu, an MNU text
file is generated that you can later include in your RC file, and a RES
resource file is generated.
WAPE (see Figure 5) is neither a prototyping tool nor an applications
generator. Its strengths are its comprehensive help system, its menu editor,
and its set of API functions. In my opinion, this tool will appeal mainly to
beginning Windows developers.
Summary
Windows and Presentation Manager code generators are new and powerful tools
that can substantially increase developers' productivity. Each of those
discussed here has advantages and disadvantages, and the one you choose will
depend on the project at hand. As the tools themselves are made more
comprehensive, they will be applicable to an even wider range of program
development.
Drawing the Checkerboard and Pieces Using GPI
Charles Petzold
In the last issue of MSJ, I presented an informal specification for a game
of checkers that I am writing for OS/2 Presentation Manager. In this and the
next few issues, I'll discuss the code I write for CHECKERS as I build the
program. In each issue, I plan to present a complete working program. The
goal for this article is a program that simply draws the checkerboard and
pieces, as shown in Figure 1.
The user can adjust the size of the CHECKERS window using the normal sizing
border. The board and pieces must adjust themselves to the new window size
while retaining the correct aspect ratio. When the program is minimized, a
miniature version of the CHECKERS board appears as an icon at the bottom of
the screen. Drawing an image that can adjust itself to the size of the
window while retaining the correct aspect ratio is one of the challenges for
the first version of the program.
CHECKERS also includes a menu with several options. You use these options to
show a standard "About" dialog box, to change the colors of the board and
the pieces, and to change the orientation of the board. By default, the
board is oriented so that the black pieces appear closest to the user.
Did I say black pieces? Do you see any black pieces in Figure 1? This may be
a little confusing at first. It is common in checkers to refer to black
squares and white squares and to black pieces and white pieces. In reality,
standard tournament boards are green and buff, and the pieces are red and
white. The board is always oriented with a black square (which is really
green) at the left corner of the side of the board closest to each player.
Pieces can be placed only on black squares. The player with the black pieces
(which are really red) always moves first.
The CHECKERS Files
Seven files are necessary for creating this first version of CHECKERS (see
Figures 2 and 3). These files are fairly standard components of a
Presentation Manager (hereafter "PM") program. CHECKERS is a make file used
to construct CHECKERS.EXE. If you have the Microsoft C Version 5.1 compiler
and the OS/2 1.1 Programmer's Toolkit installed, you can create CHECKERS.EXE
by entering:
MAKE CHECKERS
The main function of CHECKERS.C is the entry point to the program, the
window procedure for the program's client window, and the dialog procedure
for the "About" dialog box.
CHECKERS.RC is the resource script. This file contains a menu template and
two dialog box templates, one for the "About" box and the other to change
colors.
Currently, the CHECKERS.H header file contains mostly definitions of
identifiers used both in CHECKERS.C and CHECKERS.RC, but it will eventually
contain other information (such as structure definitions) that will be used
throughout the program.
CHECKERS.DEF is the module definition file. This file contains information
that LINK uses to construct the CHECKERS.EXE executable.
CKRDRAW.C (Figure 3) is the source code file for all the drawing functions
in CHECKERS. I expect CKRDRAW.C to be expanded in the next article. The
CKRDRAW.H header file contains declarations of functions in CKRDRAW.C that
are called by CHECKERS.C. All other functions in CKRDRAW.C are used only
within the CKRDRAW.C module.
The CHECKERS.C Module
CHECKERS.C is fairly simple and straightforward. The main function creates a
message queue, registers a window class for the program's client window, and
creates a standard window. It then enters the normal message loop. On
exiting the message queue, main destroys the window and the message queue
and terminates.
ClientWndProc, the window procedure for the client window, processes only
five messages: WM_CREATE (to perform program initialization), WM_SIZE (which
occurs when the window changes size), WM_PAINT (to handle the window
painting logic), WM_COMMAND (to handle messages from the menu indicating
user selections), and WM_DESTROY (to clean up before termination).
For all these messages, ClientWndProc calls one or more functions beginning
with the prefix Ckd (which stands for "checkers draw"). These functions are
in the CKRDRAW.C module; the function templates are in CKRDRAW.H. As I add
more source code modules to CHECKERS, I will consistently preface the
function name with an abbreviation of the name of the file in which the
function is defined. Doing so makes it easier to find functions in
multimodule programs.
The CKRDRAW.C Module
The CKRDRAW.C file isolates all the drawing functions in one file. The file
contains several global variables required in these functions, but these
variables are defined as static and hence are private to the module. Only
one variable is common to both CHECKERS.C and CKRDRAW.C: hab, the thread's
anchor block handle. This variable is obtained in main from the
WinInitialize call and is required in several GPI functions used in CKRDRAW.
CKRDRAW.C also contains a number of functions defined as static. These
functions are also private to the CKRDRAW.C module and not visible to other
modules. Only the nonstatic functions can be called from CHECKERS.C.
Invisible variables and functions in CKRDRAW.C make it easier to treat the
module as a black box. In a black box, a few functions provide an interface
to all the module's operations.
Board and Piece Dimensions
Near the top of CKRDRAW.C are several #define statements that define
constants representing the various dimensions of the checkerboard and the
pieces in an arbitrary coordinate system. Figure 4 shows how these constants
define the size of the various components of the checkerboard, including the
margins around the four sides. The first function in CKRDRAW.C,
CkdQueryBoardDimensions, calculates the size of the checkerboard (with the
margins) in the arbitrary coordinate system. The goal is to fit this
checkerboard inside the client window and to preserve the correct aspect
ratio regardless of the size of the window.
Figure 5 shows the three constants that define the size of the playing
pieces, in both normal and kinged versions. These constants will be used to
create bitmaps to display the pieces on the board.
The Presentation Page
It will be most convenient for the CHECKERS program to draw the checkerboard
on the window using the arbitrary coordinates. Somehow the coordinates must
be translated to the pixels of the program's window. This is the first
problem I'll attack. I will use the GPI presentation page and page viewport
for this job. The presentation page and page viewport together define the
GPI device transform.
A presentation page is a rectangular surface on which a Presentation Manager
program draws. The size and units of the presentation page are specified in
GpiCreatePS. In CKRDRAW.C, the presentation page size is set to the
dimensions of the checkerboard.
The dimensions of a presentation page are defined by a unit of measurement
called page units. For example, you can specify that your presentation page
is in units of inches or millimeters. If you do, images are drawn using
coordinates in units of inches or millimeters; GPI maps these coordinates to
the pixels of the window.
The PU_ARBITRARY page units chosen for CHECKERS are a little different.
PU_ARBITRARY indicates that the horizontal and vertical coordinates of the
presentation page are to be equal in physical size, even if the video
display has unequal horizontal and vertical resolution.
The first step is to create the presentation space in which the board and
pieces will be drawn. The ClientWndProc function in CHECKERS.C calls
CkdCreatePS while processing the WM_CREATE message. Next, CkdCreatePS calls
CkdQueryBoardDimensions to obtain the size of the checkerboard in the
arbitrary coordinates. This size (4500 units wide by 2900 units high) is
stored in a structure of type SIZEL. The structure is passed directly to the
GpiCreatePS function to specify the size of the presentation page. The page
units are specified in the last parameter to GpiCreatePS with the use of the
PU_ARBITRARY constant.
The Page Viewport
When a program calls GpiCreatePS to create a presentation space for a
window, GPI uses the page size, the page units, and the resolution of the
video display to define a page viewport. The page viewport is a rectangle in
units of pixels. It is equal in physical size to the presentation page in
page units. (For example, if you specify that your presentation page is two
inches by two inches, and the video display has a resolution of 72 dots per
inch, the page viewport is 144 pixels by 144 pixels.)
The page viewport rectangle is relative to the lower-left corner of the
window associated with the presentation space. When a program draws on a
presentation page in page units, GPI maps the points to the pixels of the
page viewport. The four corners of the presentation page map to the four
corners of the page viewport.
The PU_ARBITRARY page units force GPI to define a page viewport that can fit
in the screen with equal horizontal and vertical resolution in the
presentation page. For example, CHECKERS defines a presentation page that is
4500 units wide by 2900 units high--the size of the checkerboard. When you
run CHECKERS on an EGA monitor (which has a screen size of 640 by 350
pixels), the page viewport rectangle is set to 640 by 309 pixels. On a VGA
monitor, with a screen size of 640 by 480 pixels, the page viewport is 640
by 412 pixels.
If I wanted to run CHECKERS in full-screen mode only, I would now be
finished. But because I want to run CHECKERS in a window that is smaller
than the full screen, the page viewport rectangle must be adjusted based on
the size of the window.
When ClientWndProc receives a WM_SIZE message, it calls the CkdResizePS
function. This function uses the pixel size of the window (from
WinQueryWindowRect) and the original page viewport rectangle (obtained from
a call to GpiQueryPageViewport in CkdCreatePS) to calculate a scaling factor
that preserves the aspect ratio. This scaling factor is used to adjust the
page viewport rectangle, which is then centered in the client window.
Thus, the program can draw the checkerboard on the presentation page in the
arbitrary coordinates. GPI maps these page coordinates to the pixels of the
window (see Figure 6). Each time the size of the window changes, a new page
viewport is calculated.
Drawing the Board
The checkerboard and playing pieces are drawn on the screen when
ClientWndProc receives a WM_PAINT message. The window procedure calls three
functions in CKRDRAW.C: CkdDrawWindowBackground, CkdDrawWholeBoard, and
CkdDrawAllPieces.
The CkdDrawWindowBackground function is simple: it calls WinQueryWindowRect
and WinFillRect to color the whole window in the background color. Because
WinFillRect is not a GPI function, it draws the window in units of pixels
regardless of the presentation page and page viewport.
The CkdDrawWholeBoard function calls CkdDrawAllBoardSquares.
CkdDrawAllBoardSquares then calls CkdDrawBoardSquare 64 times to draw the 64
squares that make up the checkerboard. The values of x and y in Figure 7 are
a simple coordinate system used to identify each square.
CkdDrawBoardSquare draws one square (actually a trapezoid, but I'll call it
a square nevertheless). It sets a border color to black and an area color to
either the color used for the black square or the color used for the white
square. The function then calls GpiBeginArea to begin an area bracket.
The coordinates of the four corners of the square (in the arbitrary
presentation page units) are obtained from CkdQuerySquareCoords.
CkdQuerySquareCoords calls CkdQuerySquareOrigin four times to obtain these
coordinates. CkdDrawBoardSquare then calls GpiMove and GpiPolyLine in the
area bracket. The GpiEndArea function ends the area bracket, causing the
outline and interior of the square to be drawn.
(You'll notice that CkdDrawBoardSquare returns the value that was returned
from the GpiEndArea function. CkdDrawAllBoardSquares checks if this value is
equal to GPI_HITS. This has no meaning now, but I'll make use of it in the
next installment of the program to determine the square that the user clicks
with the mouse.)
CkdDrawWholeBoard then finishes by drawing the edge in the front of the
board, also using GpiBeginArea and GpiEndArea to define an area bracket.
Creating the Bitmaps
So far I have covered all the code necessary for drawing the checkerboard.
Drawing the playing pieces is a little more complex, because the program
must eventually allow a user to pick up a piece with the mouse and move it
to another square. This will require that CHECKERS be able to draw and erase
a piece quickly as it is moved across the window. To achieve satisfactory
performance, the playing pieces must be stored as bitmaps.
The bitmaps are created when ClientWndProc receives a WM_SIZE message and
calls CkdCreatePieces. (In subsequent WM_SIZE messages, any previous bitmaps
that exist are destroyed first by a call to CkdDestroyPieces. The bitmaps
are also destroyed during the WM_DESTROY message.) Recreating the bitmaps
during a WM_SIZE message is necessary because bitmaps are always in a
specific pixel size that depends on the size of the window.
CkdCreatePieces creates six bitmaps. The handles to four of these bitmaps
are stored in the global array ahbmPiece. (The ahbm prefix on this variable
name stands for "array of handles to bitmaps.") These four bitmaps are the
black and white pieces in both normal and kinged versions. The handles to
the other two bitmaps are stored in the array ahbmMask. I will show shortly
how these mask bitmaps allow me to use bitmaps to draw nonrectangular images
on the screen.
The CkdCreatePieces function creates a presentation space associated with
the memory device context. This presentation space is given the same
presentation page size and page viewport as the presentation space
associated with the window. Thus, we can draw the piece on the bitmap using
the arbitrary coordinates.
However, the bitmap must be created in a specific pixel size. The size of
the bitmap in page coordinates is stored in the sizlPiece variable; this is
converted to device coordinates by calling GpiConvert. The CkdCreatePieces
function then sends up a BITMAPINFOHEADER structure and calls
GpiCreateBitmap. The GpiSetBitmap function sets the bitmap to be the drawing
surface of the presentation space associated with the memory device context.
A call to CkdRenderPiece draws the piece into this presentation space and
hence on the bitmap.
First, CkdRenderPiece colors the background of the bitmap by calls to
GpiMove and GpiBox. It then uses two of the GPI ellipse functions
(GpiPointArc and GpiFullArc) to draw the top ellipse and bottom partial
ellipse that make up the piece. The two ellipses are connected with vertical
lines.
The four bitmaps for the pieces and the two bitmaps for the masks are all
drawn with calls to CkdRenderPiece, but the colors used in drawing the
bitmaps are different. The piece bitmap is drawn with a black background. (I
use the GPI identifier CLR_FALSE for this. CLR_FALSE really means that all
color bits are set to 0, which is equivalent to black on the video display.)
The foreground is the piece itself, using the current colors of the black
and white pieces. The mask bitmap is drawn with a CLR_TRUE background (which
means all color bits are set to 1 and the color is white) and a CLR_FALSE
foreground where the piece would appear. The mask resembles a white surface,
with a hole where the piece would appear. I'll describe shortly how the mask
and piece bitmaps are used to draw a piece on the board.
The BOARD Structure
Let's return to CHECKERS.C for a moment. Near the top of ClientWndProc is a
structure variable named brd, defined as type BOARD. This BOARD structure is
defined in CHECKERS.H. It has three ULONG (32-bit unsigned long integer)
fields: ulBlack, ulWhite, and ulKing.
The BOARD structure defines the state of the board at any point in time. In
the last issue, I discussed the standard checkers notation in which all the
black squares of the board (the squares on which pieces may be placed) are
numbered from 1 to 32. These numbers can also refer to the bit positions of
a 32-bit integer. To make the notation more convenient, subtract 1 from the
numbers used for standard checkers notation so that we have bit positions
from 0 through 31, where bit position 0 is the least significant bit of the
32-bit integer. The bits correspond to the black squares of the board (see
Figure 8).
A 1 bit in the ulBlack field of the BOARD structure means that a black piece
is on that square. A 1 bit in the ulWhite field means that a white piece is
on that square. A 1 bit in the ulKing field means that the corresponding
black or white piece has been kinged.
The BOARD structure is initialized thus for the beginning of a game: black
pieces in positions 0 through 11, white pieces in positions 20 through 31,
and no kings. During the WM_PAINT message, ClientWndProc draws the pieces on
the board by calling CkdDrawAllPieces, passing a pointer to the BOARD
structure.
CkdDrawAllPieces simply loops through all the possible board squares and
calls CkdDrawOnePiece. CkdDrawOnePiece calls CkdConvertCoordsToIndex, which
converts the coordinates shown in Figure 7 to the bit positions shown in
Figure 8. The CkdQuerySquarePieceOrigin determines the page coordinates of
the lower-left corner of the piece. This is converted to device coordinates
(pixels relative to the window) by a call to GpiConvert. CkdDrawOnePiece
then uses the index returned from CkdConvertCoordsToIndex with the fields of
the BOARD structure to test whether a square contains a black or a white
piece and if that piece is kinged. If it is, the function calls CkdShowPiece
to display the piece on the board.
Drawing the Pieces
The CkdShowPiece function is responsible for drawing a single piece on the
board. The POINTL structure passed to the function contains the position of
the lower-left corner of the piece measured in device coordinates.
You'll recall that the mask bitmaps were created with a white background
composed of 1 bits and a black foreground composed of 0 bits. The mask
bitmap is selected into the memory presentation space and displayed on the
screen using the GpiBitBlt function with a raster operation of ROP_SRCAND.
The raster operation of ROP_SRCAND means that the screen and the bitmap are
combined with a bitwise AND operation. The background of 1 bits leaves the
screen intact. The foreground of 0 bits creates a black hole that is the
size of the piece.
The piece bitmap is then selected into the memory presentation space and
displayed on the screen using the GpiBitBlt function with a raster operation
of ROP_SRCPAINT. This raster operation is a bitwise OR operation between the
screen and the bitmap. The black background leaves the screen intact. The
foreground (the piece itself) is, in effect, drawn over the black hole left
by the mask bitmap.
This process is shown in Figure 9 (A through E). It is similar to the way
that the Presentation Manager draws nonrectangular icons and mouse pointers
on the screen.
Colors and Orientation
So far I've been discussing how the program draws the checkerboard and the
pieces. This version of CHECKERS also has a menu with Game and Color
options.
The five options in the Color menu let you change any of the five colors
that the program uses for coloring the window background and drawing the
board and pieces. This menu's dialog box is shown in Figure 10. You can also
select Standard Colors from the Color menu, which causes all the colors to
revert to the default tournament standards.
The window procedure for the dialog box is named ColorDlgProc and is in
CKRDRAW.C. The dialog box template is defined in CHECKERS.RC. The same
dialog box and dialog box procedure is used for all five color selections.
First, CHECKERS.C passes a pointer to a menu command ID number as the last
parameter to WinDlgBox. This pointer is available in the dialog box
procedure during the WM_INITDLG message. The dialog procedure determines
which menu option has been selected and sets the text of the window
appropriately. The dialog procedure also saves a pointer to the appropriate
global color variable in pclr. If the user presses the OK button, the
selected color is saved in this variable.
When the dialog box is terminated, ClientWndProc invalidates the window so
that it can be repainted. But if the colors of the black or white pieces
have been changed, ClientWndProc calls CkdDestroyPieces and CkdCreatePieces
first to recreate the bitmaps using the new colors.
The first time you run the program, CHECKERS uses WinWriteProfileData to
store the color preferences in the OS2.INI file. The colors are saved during
the CkdDestroyPS function, which is called from ClientWndProc during the
WM_DESTROY message. The next time you run the program, the colors saved in
OS2.INI are retrieved during the CkdCreatePS function using calls to
WinQueryProfileData.
Normally, the program displays the black pieces on the bottom of the board,
the side that appears closest to the user. You can switch the board around
by unchecking the Black on Bottom option in the Game menu. The current
orientation of the board is stored in the sBottom variable in ClientWndProc.
This variable is passed as a parameter to CkdDrawAllPieces and eventually
affects the way coordinates are converted to bit position indexes in the
CkdConvertCoordsToIndex function.
Testing the Code
This version of CHECKERS is fairly easy to test. You can resize the window
and note that the checkerboard and pieces are redrawn to the new size. You
can also change colors from the menu and dialog boxes.
You may want to alter the fields of the BOARD structure defined in
ClientWndProc and then recompile the program to see how the pieces are
arranged on the board with the new values. When doing so, be sure the
ulBlack and ulWhite fields do not have any common bits set. The bits set in
the ulKing field should duplicate bits set in either the ulBlack or ulWhite
fields.
The Next Installment
In the next issue, I'll enlarge CHECKERS.C, add a few more functions to
CKRDRAW.C, and create another module or two. The goal will be to add
keyboard and mouse interfaces to CHECKERS and to add logic that tests valid
checker moves. With the version of CHECKERS in the next issue, you'll be
able to play a game of checkers by yourself, alternating moving the black
and white pieces.
Figure 2
CHECKERS
CC = cl -c -G2sw -W3
checkers.obj : checkers.c checkers.h ckrdraw.h
$(CC) checkers.c
ckrdraw.obj : ckrdraw.c checkers.h ckrdraw.h
$(CC) ckrdraw.c
checkers.res : checkers.rc checkers.h
rc -r checkers
checkers.exe : checkers.obj ckrdraw.obj checkers.def
link checkers ckrdraw, /align:16, NUL, os2, checkers
rc checkers.res checkers.exe
checkers.exe : checkers.res
rc checkers.res
CHECKERS.H
#define BLACK 0
#define WHITE 1
#define NORM 0
#define KING 1
typedef struct
{
ULONG ulBlack ;
ULONG ulWhite ;
ULONG ulKing ;
}
BOARD ;
#define ID_RESOURCE 1
#define IDM_BOTTOM 1
#define IDM_ABOUT 2
#define IDM_COLOR_BACKGROUND 11
#define IDM_COLOR_BLACK_SQUARE 12
#define IDM_COLOR_WHITE_SQUARE 13
#define IDM_COLOR_BLACK_PIECE 14
#define IDM_COLOR_WHITE_PIECE 15
#define IDM_COLOR_STANDARD 16
#define IDD_ABOUT_DLG 1
#define IDD_COLOR_DLG 2
#define IDD_HEADING 10
#define IDD_COLOR 20
CHECKERS.DEF
NAME CHECKERS WINDOWAPI
DESCRIPTION 'Checkers Version 0.10 (c) 1990, Charles Petzold'
PROTMODE
HEAPSIZE 1024
STACKSIZE 8192
EXPORTS ClientWndProc
AboutDlgProc
ColorDlgProc
CHECKERS.RC
#include <os2.h>
#include "checkers.h"
MENU ID_RESOURCE
{
SUBMENU "~Game", -1
{
MENUITEM "~Black on Bottom", IDM_BOTTOM,, MIA_CHECKED
MENUITEM SEPARATOR
MENUITEM "~About Checkers...", IDM_ABOUT
}
SUBMENU "~Colors", -1
{
MENUITEM "Wi~ndow Background...", IDM_COLOR_BACKGROUND
MENUITEM "~Black Square...", IDM_COLOR_BLACK_SQUARE
MENUITEM "~White Square...", IDM_COLOR_WHITE_SQUARE
MENUITEM "B~lack Piece...", IDM_COLOR_BLACK_PIECE
MENUITEM "W~hite Piece...", IDM_COLOR_WHITE_PIECE
MENUITEM SEPARATOR
MENUITEM "~Standard colors", IDM_COLOR_STANDARD
}
}
#define GRP WS_GROUP
DLGTEMPLATE IDD_ABOUT_DLG
{
DIALOG "", 0, 32, 32, 200, 100,, FCF_DLGBORDER
{
CTEXT "Checkers Version 0.10" -1, 10, 76, 180, 8
CTEXT "(Draws the Board and Pieces)" -1, 10 62, 180, 8
CTEXT "Microsoft Systems Journal,1/90" -1, 10, 48,
180, 8
CTEXT "(c) 1990, Charles Petzold" -1, 10, 34, 180,
8
DEFPUSHBUTTON "OK" DID_OK, 80, 8, 40, 16, GRP
}
}
DLGTEMPLATE IDD_COLOR_DLG
{
DIALOG "", 0, 32, 32, 180, 180,, FCF_DLGBORDER
{
CTEXT "", IDD_HEADING, 10, 166, 160, 8
GROUPBOX "Color" -1, 16, 32, 148, 130
RADIOBUTTON "Black" IDD_COLOR + CLR_BLACK, 20, 136, 64,
12, GRP
RADIOBUTTON "Blue" IDD_COLOR + CLR_BLUE, 20, 122, 64,
12
RADIOBUTTON "Green" IDD_COLOR + CLR_GREEN, 20, 108, 64,
12
RADIOBUTTON "Cyan" IDD_COLOR + CLR_CYAN, 20, 94, 64, 12
RADIOBUTTON "Red" IDD_COLOR + CLR_RED, 20, 80, 64, 12
RADIOBUTTON "Pink" IDD_COLOR + CLR_PINK, 20, 66, 64, 12
RADIOBUTTON "Yellow" IDD_COLOR + CLR_YELLOW, 20, 52, 64,
12
RADIOBUTTON "Pale Gray" IDD_COLOR + CLR_PALEGRAY, 20, 38, 64,
12
RADIOBUTTON "Dark Gray" IDD_COLOR + CLR_DARKGRAY, 94, 136 64,
12
RADIOBUTTON "Dark Blue" IDD_COLOR + CLR_DARKBLUE, 94, 122 64,
12
RADIOBUTTON "Dark Green" IDD_COLOR + CLR_DARKGREEN, 94, 108, 64,
12
RADIOBUTTON "Dark Cyan" IDD_COLOR + CLR_DARKCYAN, 94, 94, 64,
12
RADIOBUTTON "Dark Red" IDD_COLOR + CLR_DARKRED, 94, 80, 64,
12
RADIOBUTTON "Dark Pink" IDD_COLOR + CLR_DARKPINK, 94, 66, 64,
12
RADIOBUTTON "Brown" IDD_COLOR + CLR_BROWN, 94, 52, 64,
12
RADIOBUTTON "White" IDD_COLOR + CLR_WHITE, 94, 38, 64,
12
DEFPUSHBUTTON "OK" DID_OK, 16, 8, 52, 16, GRP
PUSHBUTTON "Cancel" DID_CANCEL, 112, 8, 52, 16
}
}
CHECKERS.C
/*------------------------------------------------------------------
CHECKERS.C source code file, version 0.10 (draws board and pieces)
(c) 1990, Charles Petzold
-----------------------------------------------------------------*/
#define INCL_WIN
#include <os2.h>
#include "checkers.h"
#include "ckrdraw.h"
MRESULT EXPENTRY ClientWndProc (HWND hwnd, USHORT msg, MPARAM mp1,
MPARAM mp2) ;
MRESULT EXPENTRY AboutDlgProc (HWND hwnd, USHORT msg, MPARAM mp1,
MPARAM mp2) ;
HAB hab ;
int main (void)
{
static CHAR szClientClass[] = "Checkers" ;
static ULONG flFrameFlags = FCF_TITLEBAR | FCF_SYSMENU |
FCF_SIZEBORDER | FCF_MINMAX |
FCF_SHELLPOSITION | FCF_TASKLIST |
FCF_MENU ;
HMQ hmq ;
HWND hwndFrame, hwndClient ;
QMSG qmsg ;
hab = WinInitialize (0) ;
hmq = WinCreateMsgQueue (hab, 0) ;
WinRegisterClass (hab, szClientClass, ClientWndProc,
CS_SIZEREDRAW, 0) ;
hwndFrame = WinCreateStdWindow (HWND_DESKTOP, WS_VISIBLE,
&flFrameFlags, szClientClass,
NULL, 0L, NULL, ID_RESOURCE,
&hwndClient) ;
while (WinGetMsg (hab, &qmsg, NULL, 0, 0))
WinDispatchMsg (hab, &qmsg) ;
WinDestroyWindow (hwndFrame) ;
WinDestroyMsgQueue (hmq) ;
WinTerminate (hab) ;
return 0 ;
}
MRESULT EXPENTRY ClientWndProc (HWND hwnd, USHORT msg, MPARAM mp1,
MPARAM mp2)
{
static BOARD brd = { 0x00000FFF, 0xFFF00000, 0x00000000 } ;
static HPS hps ;
static HWND hwndMenu ;
static SHORT sBottom = BLACK ;
switch (msg)
{
case WM_CREATE:
hwndMenu = WinWindowFromID (
WinQueryWindow (hwnd, QW_PARENT, FALSE),
FID_MENU) ;
hps = CkdCreatePS (hwnd) ;
return 0 ;
case WM_SIZE:
CkdResizePS (hps, hwnd) ;
CkdDestroyPieces () ;
CkdCreatePieces (hps) ;
return 0 ;
case WM_PAINT:
WinBeginPaint (hwnd, hps, NULL) ;
CkdDrawWindowBackground (hps, hwnd) ;
CkdDrawWholeBoard (hps) ;
CkdDrawAllPieces (hps, &brd, sBottom) ;
WinEndPaint (hps) ;
return 0 ;
case WM_COMMAND:
switch (COMMANDMSG (&msg)->cmd)
{
case IDM_BOTTOM:
WinSendMsg (hwndMenu, MM_SETITEMATTR,
MPFROM2SHORT (IDM_BOTTOM, TRUE),
MPFROM2SHORT (MIA_CHECKED,
sBottom ? MIA_CHECKED : 0)) ;
sBottom ^= 1 ;
WinInvalidateRect (hwnd, NULL, FALSE) ;
return 0 ;
case IDM_ABOUT:
WinDlgBox (HWND_DESKTOP, hwnd, AboutDlgProc,
NULL, IDD_ABOUT_DLG, NULL) ;
return 0 ;
case IDM_COLOR_BACKGROUND:
case IDM_COLOR_BLACK_SQUARE:
case IDM_COLOR_WHITE_SQUARE:
case IDM_COLOR_BLACK_PIECE:
case IDM_COLOR_WHITE_PIECE:
if (!WinDlgBox (HWND_DESKTOP, hwnd,
ColorDlgProc,
NULL, IDD_COLOR_DLG,
&(COMMANDMSG (&msg)->cmd)))
return 0 ;
if (COMMANDMSG
(&msg)->cmd == IDM_COLOR_BLACK_PIECE ||
COMMANDMSG
(&msg)->cmd == IDM_COLOR_WHITE_PIECE)
{
CkdDestroyPieces () ;
CkdCreatePieces (hps) ;
}
WinInvalidateRect (hwnd, NULL, FALSE) ;
return 0 ;
case IDM_COLOR_STANDARD:
CkdSetStandardColors () ;
CkdDestroyPieces () ;
CkdCreatePieces (hps) ;
WinInvalidateRect (hwnd, NULL, FALSE) ;
return 0 ;
}
break ;
case WM_DESTROY:
CkdDestroyPieces () ;
CkdDestroyPS (hps) ;
return 0 ;
}
return WinDefWindowProc (hwnd, msg, mp1, mp2) ;
}
MRESULT EXPENTRY AboutDlgProc (HWND hwnd, USHORT msg, MPARAM mp1,
MPARAM mp2)
{
switch (msg)
{
case WM_COMMAND:
switch (COMMANDMSG(&msg)->cmd)
{
case DID_OK:
case DID_CANCEL:
WinDismissDlg (hwnd, TRUE) ;
return 0 ;
}
}
return WinDefDlgProc (hwnd, msg, mp1, mp2) ;
}
Figure 3
CKRDRAW.H
MRESULT EXPENTRY ColorDlgProc (HWND hwnd, USHORT msg, MPARAM mp1,
MPARAM mp2) ;
HPS CkdCreatePS (HWND hwnd) ;
VOID CkdResizePS (HPS hps, HWND hwnd) ;
BOOL CkdDestroyPS (HPS hps) ;
VOID CkdSetStandardColors (VOID) ;
VOID CkdCreatePieces (HPS hps) ;
VOID CkdDestroyPieces (VOID) ;
VOID CkdDrawWindowBackground (HPS hps, HWND hwnd) ;
VOID CkdDrawWholeBoard (HPS hps) ;
VOID CkdDrawAllPieces (HPS hps, BOARD *pbrd, SHORT sBottom) ;
CKRDRAW.C
#define INCL_WIN
#define INCL_GPI
#include <os2.h>
#include <stdlib.h>
#include "checkers.h"
#include "ckrdraw.h"
/*----------------------------------------
Defines for board and piece dimensions
----------------------------------------*/
#define BRD_HORZFRONT 500
#define BRD_HORZBACK 350
#define BRD_VERT 300
#define BRD_EDGE 75
#define BRD_HORZMARGIN 250
#define BRD_FRONTMARGIN 250
#define BRD_BACKMARGIN 250
#define PIECE_XAXIS (BRD_HORZBACK - 50)
#define PIECE_YAXIS (BRD_VERT - 50)
#define PIECE_HEIGHT 50
/*-------------------------------------
Global variables external to module
-------------------------------------*/
extern HAB hab ;
/*-------------------------------------
Global variables internal to module
-------------------------------------*/
// Background, board, and piece colors
static LONG clrBackground = CLR_CYAN ;
static LONG clrBlackSquare = CLR_DARKGREEN ;
static LONG clrWhiteSquare = CLR_PALEGRAY ;
static LONG clrBlackPiece = CLR_RED ;
static LONG clrWhitePiece = CLR_WHITE ;
// Text strings for saving colors to OS2.INI
static CHAR szApplication [] = "Checkers" ;
static CHAR szClrBackground [] = "Background Color" ;
static CHAR szClrBlackSquare [] = "Black Square Color" ;
static CHAR szClrWhiteSquare [] = "White Square Color" ;
static CHAR szClrBlackPiece [] = "Black Piece Color" ;
static CHAR szClrWhitePiece [] = "White Piece Color" ;
// Original viewport for adjusting board to window size
static RECTL rclOrigViewport ;
// Bitmaps for drawing pieces
static HDC hdcMemory ;
static HPS hpsMemory ;
static HBITMAP ahbmPiece[2][2], ahbmMask[2] ;
static SIZEL sizlPiece[2] ;
/*--------------------------------------------------------------
CkdQueryBoardDimensions: Obtains size of board with margins
------------------------------------------------------------ */
static VOID CkdQueryBoardDimensions (SIZEL *psizlPage)
{
psizlPage->cx = 8 * BRD_HORZFRONT + 2 * BRD_HORZMARGIN ;
psizlPage->cy = 8 * BRD_VERT + BRD_FRONTMARGIN + BRD_BACKMARGIN ;
}
/*-----------------------------------------------------------
CkdQuerySquareOrigin: Obtains lower left corner of square
----------------------------------------------------------*/
static VOID CkdQuerySquareOrigin (SHORT x, SHORT y, POINTL *pptl)
{
pptl->x = BRD_HORZMARGIN + y * (BRD_HORZFRONT - BRD_HORZBACK) / 2 +
x * (y * BRD_HORZBACK + (8 - y) * BRD_HORZFRONT) / 8 ;
pptl->y = BRD_FRONTMARGIN + y * BRD_VERT ;
}
/*-----------------------------------------------------
CkdQuerySquareCoords: Obtains coordinates of square
-----------------------------------------------------*/
static VOID CkdQuerySquareCoords (SHORT x, SHORT y, POINTL aptl[])
{
CkdQuerySquareOrigin (x, y, aptl + 0) ;
CkdQuerySquareOrigin (x + 1, y, aptl + 1) ;
CkdQuerySquareOrigin (x + 1, y + 1, aptl + 2) ;
CkdQuerySquareOrigin (x, y + 1, aptl + 3) ;
}
/*-----------------------------------------------
CkdDrawBoardSquare: Draws one square of board
-----------------------------------------------*/
static LONG CkdDrawBoardSquare (HPS hps, SHORT x, SHORT y)
{
AREABUNDLE abnd ;
LINEBUNDLE lbnd ;
LONG lReturn ;
POINTL aptl[4] ;
GpiSavePS (hps) ;
lbnd.lColor = CLR_BLACK ;
GpiSetAttrs (hps, PRIM_LINE, LBB_COLOR, 0L, &lbnd) ;
abnd.lColor = (x + y) & 1 ? clrWhiteSquare : clrBlackSquare ;
GpiSetAttrs (hps, PRIM_AREA, LBB_COLOR, 0L, &abnd) ;
GpiBeginArea (hps, BA_ALTERNATE | BA_BOUNDARY) ;
CkdQuerySquareCoords (x, y, aptl) ;
GpiMove (hps, aptl + 3) ;
GpiPolyLine (hps, 4L, aptl) ;
lReturn = GpiEndArea (hps) ;
GpiRestorePS (hps, -1L) ;
return lReturn ;
}
/*----------------------------------------------------
CkdDrawAllBoardSquares: Draws all squares of board
----------------------------------------------------*/
static LONG CkdDrawAllBoardSquares (HPS hps)
{
SHORT x, y ;
for (y = 0 ; y < 8 ; y++)
for (x = 0 ; x < 8 ; x++)
if (CkdDrawBoardSquare (hps, x, y) == GPI_HITS)
return MAKELONG (x, y) ;
return MAKELONG (-1, -1) ;
}
/*----------------------------------------------------
CkdRenderPiece: Draws piece on bitmap in memory PS
----------------------------------------------------*/
static VOID CkdRenderPiece (HPS hpsMemory, LONG lColorBack,
LONG lColor, LONG lColorLine, SHORT sKing)
{
ARCPARAMS arcp ;
AREABUNDLE abnd ;
LINEBUNDLE lbnd ;
POINTL ptl, aptlArc[2] ;
SHORT s ;
GpiSavePS (hpsMemory) ;
// Draw background of bitmap
GpiSetColor (hpsMemory, lColorBack) ;
ptl.x = 0 ;
ptl.y = 0 ;
GpiMove (hpsMemory, &ptl) ;
ptl.x = PIECE_XAXIS ;
ptl.y = PIECE_YAXIS + (sKing + 1) * PIECE_HEIGHT ;
GpiBox (hpsMemory, DRO_FILL, &ptl, 0L, 0L) ;
// Set colors for areas and outlines
abnd.lColor = lColor ;
GpiSetAttrs (hpsMemory, PRIM_AREA, ABB_COLOR, 0L, &abnd) ;
lbnd.lColor = lColorLine ;
GpiSetAttrs (hpsMemory, PRIM_LINE, LBB_COLOR, 0L, &lbnd) ;
// Set arc parameters
arcp.lP = PIECE_XAXIS / 2 ;
arcp.lQ = PIECE_YAXIS / 2 ;
arcp.lR = 0 ;
arcp.lS = 0 ;
GpiSetArcParams (hpsMemory, &arcp) ;
// draw the piece
for (s = 0 ; s <= sKing ; s++)
{
GpiBeginArea (hpsMemory, BA_ALTERNATE | BA_BOUNDARY) ;
ptl.x = 0 ;
ptl.y = PIECE_YAXIS / 2 + (s + 1) * PIECE_HEIGHT ;
GpiMove (hpsMemory, &ptl) ;
ptl.y -= PIECE_HEIGHT ;
GpiLine (hpsMemory, &ptl) ;
aptlArc[0].x = PIECE_XAXIS / 2 ;
aptlArc[0].y = s * PIECE_HEIGHT ;
aptlArc[1].x = PIECE_XAXIS ;
aptlArc[1].y = PIECE_YAXIS / 2 + s * PIECE_HEIGHT ;
GpiPointArc (hpsMemory, aptlArc) ;
ptl.x = PIECE_XAXIS ;
ptl.y = PIECE_YAXIS / 2 + (s + 1) * PIECE_HEIGHT ;
GpiLine (hpsMemory, &ptl) ;
GpiEndArea (hpsMemory) ;
}
ptl.x = PIECE_XAXIS / 2 ;
ptl.y = PIECE_YAXIS / 2 + (sKing ? 2 : 1) * PIECE_HEIGHT ;
GpiMove (hpsMemory, &ptl) ;
GpiFullArc (hpsMemory, DRO_OUTLINEFILL, MAKEFIXED (1,0)) ;
GpiRestorePS (hpsMemory, -1L) ;
}
/*-------------------------------------------------------
CkdQuerySquareCenter: Obtains center of board square
------------------------------------------------------*/
static VOID CkdQuerySquareCenter (SHORT x, SHORT y,
POINTL *pptlCenter)
{
POINTL aptl[4] ;
CkdQuerySquareCoords (x, y, aptl) ;
pptlCenter->x = (aptl[0].x + aptl[1].x + aptl[2].x + aptl[3].x) / 4 ;
pptlCenter->y = (aptl[1].y + aptl[2].y) / 2 ;
}
/*--------------------------------------------------------------
CkdPieceOriginFromCenter: Converts center of square to piece
origin
-------------------------------------------------------------*/
static VOID CkdPieceOriginFromCenter (POINTL *pptl)
{
pptl->x -= PIECE_XAXIS / 2 ;
pptl->y -= PIECE_YAXIS / 2 ;
}
/*--------------------------------------------------------------
CkdQuerySquarePieceOrigin: Obtains origin of piece on a square
-------------------------------------------------------------*/
static VOID CkdQuerySquarePieceOrigin (SHORT x, SHORT y,
POINTL *pptlOrigin)
{
CkdQuerySquareCenter (x, y, pptlOrigin) ;
CkdPieceOriginFromCenter (pptlOrigin) ;
}
/*--------------------------------------------------------------
CkdConvertCoordsToIndex: Obtains index (0-31) from square
coordinates
-------------------------------------------------------------*/
static SHORT CkdConvertCoordsToIndex (SHORT x, SHORT y, SHORT sBottom)
{
if (x < 0 || x > 7 || y < 0 || y > 7)
return -1 ;
if ((x - (y & 1)) & 1)
return -1 ;
if (sBottom == WHITE)
{
x = 7 - x ;
y = 7 - y ;
}
return 3 ^ (4 * y + (x - (y & 1)) / 2) ;
}
/*-------------------------------------------------------------
CkdShowPiece: Draws a piece on the screen at specific point
------------------------------------------------------------*/
static VOID CkdShowPiece (HPS hps, POINTL *pptlOrg, SHORT sColor,
SHORT sKing)
{
POINTL aptl[3] ;
// Write out mask with bitwise AND
aptl[0] = *pptlOrg ;
aptl[1].x = pptlOrg->x + sizlPiece[sKing].cx ;
aptl[1].y = pptlOrg->y + sizlPiece[sKing].cy ;
aptl[2].x = 0 ;
aptl[2].y = 0 ;
GpiSetBitmap (hpsMemory, ahbmMask[sKing]) ;
GpiBitBlt (hps, hpsMemory, 3L, aptl, ROP_SRCAND, BBO_IGNORE) ;
// Write out piece with bitwise OR
aptl[0] = *pptlOrg ;
aptl[1].x = pptlOrg->x + sizlPiece[sKing].cx ;
aptl[1].y = pptlOrg->y + sizlPiece[sKing].cy ;
aptl[2].x = 0 ;
aptl[2].y = 0 ;
GpiSetBitmap (hpsMemory, ahbmPiece[sColor][sKing]) ;
GpiBitBlt (hps, hpsMemory, 3L, aptl, ROP_SRCPAINT, BBO_IGNORE) ;
GpiSetBitmap (hpsMemory, NULL) ;
}
/*---------------------------------------------------------------
CkdDrawOnePiece: Draws a piece on the board at specific
coordinates
-------------------------------------------------------------*/
static VOID CkdDrawOnePiece (HPS hps, SHORT x, SHORT y,
BOARD *pbrd, SHORT sBottom)
{
POINTL ptlOrigin ;
SHORT i, sKing ;
i = CkdConvertCoordsToIndex (x, y, sBottom) ;
if (i == -1)
return ;
CkdQuerySquarePieceOrigin (x, y, &ptlOrigin) ;
GpiConvert (hps, CVTC_PAGE, CVTC_DEVICE, 1L, &ptlOrigin) ;
sKing = pbrd->ulKing & 1L << i ? 1 : 0 ;
if (pbrd->ulBlack & 1L << i)
CkdShowPiece (hps, &ptlOrigin, BLACK, sKing) ;
if (pbrd->ulWhite & 1L << i)
CkdShowPiece (hps, &ptlOrigin, WHITE, sKing) ;
}
/*----------------------------------------------------
ColorDlgProc: Dialog procedure for changing colors
----------------------------------------------------*/
MRESULT EXPENTRY ColorDlgProc (HWND hwnd, USHORT msg, MPARAM mp1,
MPARAM mp2)
{
static LONG *pclr ;
static SHORT sColor ;
CHAR *pchHeading ;
switch (msg)
{
case WM_INITDLG:
switch (* (PSHORT) PVOIDFROMMP (mp2))
{
case IDM_COLOR_BACKGROUND:
pchHeading = "Window Background Color" ;
pclr = &clrBackground ;
break ;
case IDM_COLOR_BLACK_SQUARE:
pchHeading = "Black Square Color" ;
pclr = &clrBlackSquare ;
break ;
case IDM_COLOR_WHITE_SQUARE:
pchHeading = "White Square Color" ;
pclr = &clrWhiteSquare ;
break ;
case IDM_COLOR_BLACK_PIECE:
pchHeading = "Black Piece Color" ;
pclr = &clrBlackPiece ;
break ;
case IDM_COLOR_WHITE_PIECE:
pchHeading = "White Piece Color" ;
pclr = &clrWhitePiece ;
break ;
}
WinSetDlgItemText (hwnd, IDD_HEADING, pchHeading) ;
sColor = (SHORT) *pclr ;
WinSendDlgItemMsg (hwnd, IDD_COLOR + sColor, BM_SETCHECK,
MPFROM2SHORT (TRUE, 0), NULL) ;
WinSetFocus (HWND_DESKTOP, WinWindowFromID (hwnd,
IDD_COLOR + sColor)) ;
return 1 ;
case WM_CONTROL:
WinSendDlgItemMsg (hwnd, IDD_COLOR + sColor, BM_SETCHECK,
MPFROM2SHORT (FALSE, 0), NULL) ;
sColor = SHORT1FROMMP (mp1) - IDD_COLOR ;
WinSendDlgItemMsg (hwnd, IDD_COLOR + sColor, BM_SETCHECK,
MPFROM2SHORT (TRUE, 0), NULL) ;
return 0 ;
case WM_COMMAND:
switch (COMMANDMSG(&msg)->cmd)
{
case DID_OK:
*pclr = (LONG) sColor ;
WinDismissDlg (hwnd, TRUE) ;
return 0 ;
case DID_CANCEL:
WinDismissDlg (hwnd, FALSE) ;
return 0 ;
}
break ;
}
return WinDefDlgProc (hwnd, msg, mp1, mp2) ;
}
/*-------------------------------------------------
CkdCreatePS: Create PS for checker board window
-------------------------------------------------*/
HPS CkdCreatePS (HWND hwnd)
{
HDC hdc ;
HPS hps ;
SIZEL sizlPage ;
USHORT sDataSize ;
CkdQueryBoardDimensions (&sizlPage) ;
hdc = WinOpenWindowDC (hwnd) ;
hps = GpiCreatePS (hab, hdc, &sizlPage,
PU_ARBITRARY | GPIF_DEFAULT |
GPIT_MICRO | GPIA_ASSOC) ;
GpiQueryPageViewport (hps, &rclOrigViewport) ;
// Get colors from OS2.INI
sDataSize = sizeof (LONG) ;
WinQueryProfileData (hab, szApplication, szClrBackground,
&clrBackground, &sDataSize) ;
sDataSize = sizeof (LONG) ;
WinQueryProfileData (hab, szApplication, szClrBlackSquare,
&clrBlackSquare, &sDataSize) ;
sDataSize = sizeof (LONG) ;
WinQueryProfileData (hab, szApplication, szClrWhiteSquare,
&clrWhiteSquare, &sDataSize) ;
sDataSize = sizeof (LONG) ;
WinQueryProfileData (hab, szApplication, szClrBlackPiece,
&clrBlackPiece, &sDataSize) ;
sDataSize = sizeof (LONG) ;
WinQueryProfileData (hab, szApplication, szClrWhitePiece,
&clrWhitePiece, &sDataSize) ;
return hps ;
}
/*---------------------------------------------------------
CkdResizePS: Change page viewport for new window size
-------------------------------------------------------*/
VOID CkdResizePS (HPS hps, HWND hwnd)
{
LONG lScale ;
RECTL rclWindow, rclViewport ;
WinQueryWindowRect (hwnd, &rclWindow) ;
// Calculate scaling factor
lScale = min (65536L * rclWindow.xRight / rclOrigViewport.xRight,
65536L * rclWindow.yTop / rclOrigViewport.yTop) ;
// Adjust page viewport of memory PS
rclViewport.xLeft = 0 ;
rclViewport.yBottom = 0 ;
rclViewport.xRight = lScale * rclOrigViewport.xRight / 65536L ;
rclViewport.yTop = lScale * rclOrigViewport.yTop / 65536L ;
rclViewport.xLeft = (rclWindow.xRight - rclViewport.xRight) / 2 ;
rclViewport.yBottom = (rclWindow.yTop - rclViewport.yTop) / 2 ;
rclViewport.xRight += rclViewport.xLeft ;
rclViewport.yTop += rclViewport.yBottom ;
GpiSetPageViewport (hps, &rclViewport) ;
}
/*---------------------------------------------------
CkdDestroyPS: Destroy PS for checker board window
---------------------------------------------------*/
BOOL CkdDestroyPS (HPS hps)
{
// Save colors in OS2.INI
WinWriteProfileData (hab, szApplication, szClrBackground,
&clrBackground, sizeof (LONG)) ;
WinWriteProfileData (hab, szApplication, szClrBlackSquare,
&clrBlackSquare, sizeof (LONG)) ;
WinWriteProfileData (hab, szApplication, szClrWhiteSquare,
&clrWhiteSquare, sizeof (LONG)) ;
WinWriteProfileData (hab, szApplication, szClrBlackPiece,
&clrBlackPiece, sizeof (LONG)) ;
WinWriteProfileData (hab, szApplication, szClrWhitePiece,
&clrWhitePiece, sizeof (LONG)) ;
return GpiDestroyPS (hps) ;
}
/*------------------------------------------------------------
CkdSetStandardColors: Sets colors to tournament standards
----------------------------------------------------------*/
VOID CkdSetStandardColors (VOID)
{
clrBackground = CLR_CYAN ;
clrBlackSquare = CLR_DARKGREEN ;
clrWhiteSquare = CLR_PALEGRAY ;
clrBlackPiece = CLR_RED ;
clrWhitePiece = CLR_WHITE ;
}
/*--------------------------------------------------------------
CkdCreatePieces: Creates bitmaps to use for drawing pieces
------------------------------------------------------------ */
VOID CkdCreatePieces (HPS hps)
{
BITMAPINFOHEADER bmp ;
LONG alBitmapFormat[2] ;
RECTL rclViewport ;
SHORT sColor, sKing ;
SIZEL sizlPage ;
// Create memory DC's and PS's
CkdQueryBoardDimensions (&sizlPage) ;
hdcMemory = DevOpenDC (hab, OD_MEMORY, "*", 0L, NULL, NULL) ;
hpsMemory = GpiCreatePS (hab, hdcMemory, &sizlPage,
PU_ARBITRARY | GPIF_DEFAULT |
GPIT_MICRO | GPIA_ASSOC) ;
// Set page viewport for hpsMemory
GpiQueryPageViewport (hps, &rclViewport) ;
rclViewport.xRight -= rclViewport.xLeft ;
rclViewport.yTop -= rclViewport.yBottom ;
rclViewport.xLeft = 0 ;
rclViewport.yBottom = 0 ;
GpiSetPageViewport (hpsMemory, &rclViewport) ;
// Get bitmap format of video display
GpiQueryDeviceBitmapFormats (hps, 2L, alBitmapFormat) ;
// Loop through possible color and size combinations
for (sKing = 0 ; sKing < 2 ; sKing++)
{
// Determine pixel dimensions of bitmaps
sizlPiece[sKing].cx = PIECE_XAXIS ;
sizlPiece[sKing].cy = PIECE_YAXIS + (sKing + 1) * PIECE_HEIGHT ;
GpiConvert (hpsMemory, CVTC_PAGE, CVTC_DEVICE, 1L,
(PPOINTL) &sizlPiece[sKing]) ;
sizlPiece[sKing].cx ++ ;
sizlPiece[sKing].cy ++ ;
// Set up BITMAPINFOHEADER structure
bmp.cbFix = sizeof bmp ;
bmp.cx = (SHORT) sizlPiece[sKing].cx ;
bmp.cy = (SHORT) sizlPiece[sKing].cy ;
bmp.cPlanes = (SHORT) alBitmapFormat[0] ;
bmp.cBitCount = (SHORT) alBitmapFormat[1] ;
// Create ahbmPiece bitmaps
for (sColor = BLACK ; sColor <= WHITE ; sColor++)
{
ahbmPiece[sColor][sKing] =
GpiCreateBitmap (hps, &bmp, 0L, 0L, NULL) ;
GpiSetBitmap (hpsMemory, ahbmPiece[sColor][sKing]) ;
CkdRenderPiece (hpsMemory, CLR_FALSE,
sColor ? clrWhitePiece : clrBlackPiece,
CLR_BLACK, sKing) ;
}
// Create ahbmMask bitmaps
ahbmMask[sKing] = GpiCreateBitmap (hps, &bmp, 0L, 0L, NULL) ;
GpiSetBitmap (hpsMemory, ahbmMask[sKing]) ;
CkdRenderPiece (hpsMemory, CLR_TRUE, CLR_FALSE, CLR_FALSE,
sKing) ;
}
GpiSetBitmap (hpsMemory, NULL) ;
}
/*---------------------------------------------------
CkdDestroyPieces: Destroy bitmaps used for pieces
---------------------------------------------------*/
VOID CkdDestroyPieces (VOID)
{
SHORT sColor, sKing ;
for (sKing = 0 ; sKing < 2 ; sKing++)
{
for (sColor = BLACK ; sColor <= WHITE ; sColor++)
if (ahbmPiece[sColor][sKing] != NULL)
GpiDeleteBitmap (ahbmPiece[sColor][sKing]) ;
if (ahbmMask[sKing] != NULL)
GpiDeleteBitmap (ahbmMask[sKing]) ;
}
GpiDestroyPS (hpsMemory) ;
DevCloseDC (hdcMemory) ;
}
/*---------------------------------------------------------------
CkdDrawWindowBackground: Fills entire window with background
color
-------------------------------------------------------------*/
VOID CkdDrawWindowBackground (HPS hps, HWND hwnd)
{
RECTL rcl ;
WinQueryWindowRect (hwnd, &rcl) ;
WinFillRect (hps, &rcl, clrBackground) ;
}
/*------------------------------------------------------------
CkdDrawWholeBoard: Draws the board squares and front edge
-----------------------------------------------------------*/
VOID CkdDrawWholeBoard (HPS hps)
{
AREABUNDLE abnd ;
LINEBUNDLE lbnd ;
SHORT x ;
POINTL aptl[4] ;
CkdDrawAllBoardSquares (hps) ;
GpiSavePS (hps) ;
lbnd.lColor = CLR_BLACK ;
GpiSetAttrs (hps, PRIM_LINE, LBB_COLOR, 0L, &lbnd) ;
for (x = 0 ; x < 8 ; x++)
{
CkdQuerySquareCoords (x, 0, aptl) ;
aptl[2].x = aptl[1].x ;
aptl[2].y = aptl[1].y - BRD_EDGE ;
aptl[3].x = aptl[0].x ;
aptl[3].y = aptl[0].y - BRD_EDGE ;
abnd.lColor = x & 1 ? clrWhiteSquare : clrBlackSquare ;
GpiSetAttrs (hps, PRIM_AREA, LBB_COLOR, 0L, &abnd) ;
GpiBeginArea (hps, BA_ALTERNATE | BA_BOUNDARY) ;
GpiMove (hps, aptl + 3) ;
GpiPolyLine (hps, 4L, aptl) ;
GpiEndArea (hps) ;
}
GpiRestorePS (hps, -1L) ;
}
/*-----------------------------------------------------
CkdDrawAllPieces: Draws all the pieces on the board
-----------------------------------------------------*/
VOID CkdDrawAllPieces (HPS hps, BOARD *pbrd, SHORT sBottom)
{
SHORT x, y ;
for (y = 0 ; y < 8 ; y++)
for (x = 0 ; x < 8 ; x++)
CkdDrawOnePiece (hps, x, y, pbrd, sBottom) ;
}
Interfacing OS/2 Compiled BASIC to Presentation Manager
Lars Opstad and Arthur Hanson
Microsoft OS/2 Presentation Manager (hereafter "PM") offers a message-based
windowing system and high-resolution graphics. Until now, however, only C
programmers have been able to take advantage of the graphic and windowing
routines in the Microsoft OS/2 Presentation Manager Toolkit. This article
can serve as the basis for adapting the PM Toolkit to BASIC and possibly to
other languages. It assumes that you are familiar with the fundamental
concepts of OS/2 programming.
Adapting the PM Toolkit for use with BASIC involves several issues. Because
of the limitations of BASIC, several interface functions require another
language. These functions are presented here in both C and Assembler source
code. The conversion of the C header files, found in the PM Toolkit, to
BASIC INCLUDE files is demonstrated. Finally, a sample BASIC program using
PM is presented.
You will need, in addition to an OS/2 BASIC compiler, the OS/2 Presentation
Manager Toolkit and one of the following: the BASIC OS/2 Presentation
Manager Toolkit Supplement (an application note is available free of charge
from the BASIC language support group of Microsoft Product Support at (206)
454-2030), the Microsoft C Compiler, or Microsoft Macro Assembler. Although
the application note is not required, it will save many hours of work.
Interface Functions
An initial part of a PM program is registering a window class. To do this,
you need the address of the window's message-handling function. Because you
cannot get or manipulate the address of a function from within BASIC, some C
and/or Assembler routines are required (see Figures 1 and 2).
Both C and Assembler can return the address of a function. The sample
routine, RegBas, is used with a BASIC function of a predefined name
(ClientWndProc&). RegBas itself is used as the function pointer parameter
when making PM calls.
Because BASIC and C receive parameters differently in functions, an
intermediate non-BASIC function must be used with RegBas. BASIC receives all
parameters by reference; C receives them by value. Because PM follows the C
convention, a BASIC function cannot be called by PM directly. Whenever a
BASIC function must be registered with PM, an alternate non-BASIC routine
must be registered instead. This translator routine accepts parameters using
the C calling convention and then calls the BASIC routine using the BASIC
calling convention. RegBas actually registers the translator routine
BasClientWndProc, which in turn calls the BASIC routine ClientWndProc .
PM functions deal with far addresses as a single element. BASIC, however,
deals with segments and offsets, not with a combined far address. Again a
non-BASIC routine can be used to combine or separate segments and offsets to
allow BASIC to interface with PM.
The function MakeLong takes two integer values and combines them to form a
long. This is generally used to convert the values returned by VARSEG and
VARPTR into a single far address. For variable-length strings, MakeLong is
used with VARSEG and SADD to make the correct address.
BreakLong, the inverse of MakeLong, breaks apart a long into two integer
values. The first parameter is the long variable to be broken down. The
hi-word is returned in the second parameter and the lo-word is returned in
the third parameter. One use of BreakLong is to break a far address into a
segment and an offset, which can then be used with DEF SEG and PEEK or POKE.
Include File Conversion Process
After the interface functions are created, the C header files need to be
converted. The PM Toolkit contains the header files used to define the PM
functions for the C compiler; these need to be converted into BASIC INCLUDE
files for use with the BASIC compiler. The application note contains all the
header files in BASIC format.
The C header files in the Toolkit use the Hungarian naming convention, in
which the name of a type describes its use. For example, the pointer and
handle types are identified by the first character of each type: p for
pointers and h for handles. This naming convention allows types to be
identified easily for conversion to BASIC or to any other language.
In the first part of the conversion, the fundamental types are converted
from C types to BASIC types. All C types, except structures, can be
converted to three types in BASIC: STRING * 1, INTEGER, and LONG.
The STRING * 1 type can only be used in TYPE ... END TYPEs converted from C
structures containing CHAR, UCHAR, or BYTE elements. Because BASIC can
neither pass single-byte parameters by value (using the BYVAL keyword) nor
return single-byte FUNCTION values, all functions in either of these
categories should be commented out.
The INTEGER type is the most straightforward. The C types that correspond to
INTEGERs are INT, SHORT, USHORT, and BOOL.
The LONG is basically a catchall. Besides the obvious C types of LONG and
ULONG, all pointers and handles are converted to LONGs. For consistency, the
LONG type was chosen for the pointers instead of the segment and offset
convention. To pass pointers to data items, you need to use the MakeLong
function (from Figure 1 or 2) with the VARSEG and VARPTR functions built
into BASIC. (VARSEG and SADD should be used with MakeLong when referencing
variable-length strings.) In addition to these standard types, many
miscellaneous types (MPARAM, for example) fall into the LONG category.
After the fundamental types are converted, the functions and structures must
be converted (see Figure 3).
Because BASIC's compiler workspace is limited, some converted header files
must be split into smaller INCLUDE files. The smaller header files such as
PMSHL.H (9Kb) do not need further division, but PMWIN.H (78Kb) and PMGPI.H
(55Kb) each need to be divided.
PM Overview
PM uses a message-based windowed system. This means that all user input
comes to the program in the form of messages (see Figure 4).
Messages usually come from the user in the form of a mouse click, a key
press, or a menu selection. The message is then passed to the PM Message
Dispatcher to determine which program should receive the message; the PM
Message Dispatcher then places the message in that program's message queue.
PM calls the registered message-handling function to process the messages in
the program's message queue. In the sample program, the translator function
BasClientWndProc is called, which in turn calls the actual BASIC
message-handling function, ClientWndProc.
The program continually receives messages from and dispatches messages to
the appropriate window procedures. These messages are passed to the window's
procedure (discussed below) where they are processed or passed on to PM's
default window procedure.
Every PM program has a special structure that is used to provide a smooth
interface to the PM messaging system. In a PM-BASIC program, the interface
routines are needed to conform to the special structure (see Figure 5).
The first component of the PM-BASIC program--or of any OS/2 program--is a
definition file (see Figure 6). The definition file specifies the name of
the program, the type of the program, heapsize, stacksize, and global
procedures. The structure of a definition file is the same for a program in
any language.
The easiest way to show the relationship of the interface functions,
converted header files, and BASIC code is to examine a sample PM-BASIC
program (see Figure 7).
The first part of the source code contains constants and function
declarations converted from the PM Toolkit's C header files. The first
section of the actual code is the initialization section. During
initialization, an anchor block is set up (WinInitialize), a message queue
is created (WinCreateMsgQueue), a window class is registered
(WinRegisterClass), and a standard window is created (WinCreateStdWindow).
After WinInitialize creates an anchor block that links the program to the
system, a message queue can be set up by calling WinCreateMsgQueue. The
message queue stores messages returned by the system.
Every window is based on a window "class." A window class determines which
window procedure is used to process messages. WinRegisterClass is important
because it registers a new window class with PM and defines the function to
be called to process messages for that class of window. RegBas is used to
return the address of the BASIC message processing procedure.
WinCreateStdWindow next creates a window of the newly defined class. The
window's appearance is controlled by flFrameFlags. The window will have a
standard title bar, a system menu, minimize and maximize icons, and a border
for resizing. It will also have a menu and will appear on the system task
list. This is accomplished by ORing constants togther to set bits in
flFrameFlags.
The second part of the PM program is the message processing loop. This loop
consists of two function calls: WinGetMsg and WinDispatchMsg. WinGetMsg will
succeed (that is, return a nonzero) until a WMQUIT message is received. A
WMQUIT is sent whenever you close a standard window. Because PM programs are
message-based, it is imperative to keep this loop tight. PM requires that
messages be processed in a limited time period and will give an error
message when this limit is reached.
The code that actually processes messages is in the function ClientWndProc.
Because the naming convention of the interface routines is arbitrary, the
name ClientWndProc& must be used for the window function.
The preferred structure of a window procedure is a SELECT CASE statement
(equivalent to a C switch statement) with CASEs for the messages you want to
handle explicitly. For messages you don't want to process, transfer control
to the default window procedure, WinDefWindowProc.
The return value of ClientWndProc is initially set to zero, on the
assumption that the messages are processed correctly. Because only one CASE
returns a value other then zero (caused if the message must be passed to
PM's default message-handling procedure), assuming a zero return value saves
code space.
In this program three messages are processed. WMSIZE is received whenever
the user resizes the program window. Processing this message ensures that
the displayed picture is always proportional to the window size.
WMPAINT is used to display the picture. It is received whenever the contents
of the window need to be generated or redisplayed. Any program containing a
WMPAINT case must use the functions WinBeginPaint and WinEndPaint. In most
programs in which the window contents change, a WinInvalidateRect function
should be used before a WinBeginPaint function. The WinInvalidateRect
function tells PM what part of the current window needs to be repainted. For
example, if you move another window over part of the current window and then
remove it, that part of the current window would need to be repainted.
Specifying a zero as the second parameter in the call to WinInvalidateRect
invalidates the entire window. WinBegin/EndPaint is the routine that handles
repainting the windows. GpiErase erases the invalidated region by filling in
the entire region with the background color.
In the sample program, the actual drawing is accomplished with four other
GPI functions. First, the points for the drawing are chosen at random. After
the points have been assigned, a GpiMove is executed to the last generated
random point. This is used to set the starting location.
GpiBeginArea and GpiEndArea are used to mark a block of graphic statements,
which will be filled using a specified method. In this program, the filling
is specified as alternating fill (BAALTERNATE) with no boundary
(BANOBOUNDARY). The only actual graphics statement is GpiPolyFillet, which
draws a pattern specified by the array of points. This statement creates a
checkered pattern of randomly shaped areas.
Finally, WMCOMMAND is received whenever the user selects a menu item. The
user can select from the menu how many random points the drawing should
contain. It is necessary to use WinSendMsg with WMPAINT to cause the window
to be redrawn with the newly specified number of points.
If the received message is not one of the above three, control is passed to
PM's default message-handling procedure (WinDefWindowProc).
The last part of a PM program is the finalize section, which is very similar
to the initialization section, but the order is reversed. First you destroy
the window handle (WinDestroyWindow) and the message queue
(WinDestroyMsgQueue) and then release the anchor block (WinTerminate).
The third file necessary for this sample is a resource script file (see
Figure 8). Resources are used with the Resource Compiler (RC.EXE) from the
PM Toolkit to create menus, accelerators, and dialog boxes, to name a few.
The sample program's resource script file is simple because it deals only
with menus. The menu defined has one topic with two choices, Points and
Exit, and a menu separator between them. The Points option has a submenu
that lets the user choose the number of random points. Note that the tilde
(~) precedes the key to press to select the menu item.
Limitations
Although this article describes how to program PM in BASIC, several aspects
of the BASIC language limit which PM functions can be used.
The most significant limitation is that the BASIC compiler has a smaller
workspace than that of the C compiler. This limits the number of PM
functions that may be declared in a single BASIC module. You will encounter
this limit in developing most PM applications with BASIC. When you include
too many files, the compiler error "Out of Memory" will occur. The solution
is to break the program into smaller modules, which can be linked together.
Another limitation is that BASIC has no single byte type. Several PM
functions either return a CHAR or require a CHAR as a parameter; these
functions cannot be called from BASIC.
Finally, the BASIC run time is not reentrant. Therefore, neither
dynamic-link libraries (DLLs) nor multithreaded programs are possible with
BASIC. Without threads, problems can arise when a program has a lengthy
processing loop. Generally, a C program spawns a thread to deal with a
lengthy process; because this is not possible in BASIC, alternative methods
must be used. One such method is to start a timer that will cause a part of
the loop to be executed every time a WMTIMER message is received.
Adapting BASIC for use with the PM environment is not simple. The
guidelines presented here will simplify this task and allow access to the
extensive capabilities of PM. With slight modifications, these techniques
can retrofit other OS/2 compilers.
Figure 1
; Program Name: RegBas.ASM
;
; Functions :
; BreakLong
; MakeLong
; BasClientWndProc
; RegBas
;
; Description : This supplies interface routines for BASIC programs.
; RegBas is used to register window classes from BASIC.
; Also provided are the utility routines: BreakLong and
; MakeLong.
DOSSEG
.286 ; OS/2 runs on 286/386 machines
.model medium, PASCAL ; Medium memory model when interfacing with BASIC
; PASCAL and BASIC use same calling conventions
.data
hwndb1 DW ? ; set up local data for BasClientWndProc
hwndb2 DW ?
msgb DW ?
mp1b1 DW ?
mp1b2 DW ?
mp2b1 DW ?
mp2b2 DW ?
.code
EXTRN ClientWndProc:PROC ; BASIC function to call
PUBLIC BasClientWndProc, BreakLong, MakeLong, RegBas
; Breaklong(Long, hi_word, lo_word)
;
BreakLong PROC FAR USES bx, long1:WORD, long2:WORD, hiword:WORD,
loword:WORD
mov bx, long1 ; Get the hi-byte of the long
mov ax, bx
mov bx, hiword ; Return it to hiword
mov [bx], ax
mov bx, long2 ; Get the lo-byte
mov ax, bx
mov bx, loword ; Return it to loword
mov [bx], ax
ret
BreakLong ENDP
; MakeLong&(hi_word, low_word)
;
MakeLong PROC FAR USES bx, hiword:WORD, loword:WORD
mov bx, hiword
mov ax, bx
mov dx, ax ; Return hi-word in dx
mov bx, loword ; Lo-word in ax for function
mov ax, bx ; Returning a long
ret
MakeLong ENDP
BasClientWndProc PROC hwnd1:word, hwnd2:word, msg:word, mp11:word,
mp12:word, mp21:word, mp22:word
push ds
mov ax, @data ; Get our data segment
mov ds, ax
mov ax, hwnd2 ; Transfer the values passed
mov hwndb1, ax ; from PM to local variables
mov ax, hwnd1 ; for the call to BASIC
mov hwndb2, ax
mov ax, msg
mov msgb, ax
mov ax, mp12
mov mp1b1, ax
mov ax, mp11
mov mp1b2, ax
mov ax, mp22
mov mp2b1, ax
mov ax, mp21
mov mp2b2, ax
mov ax, OFFSET hwndb1 ; Set up for call to BASIC
push ax ; BASIC expects values to
mov ax, OFFSET msgb ; be passed by reference.
push ax
mov ax, OFFSET mp1b1
push ax
mov ax, OFFSET mp2b1
push ax
call ClientWndProc ; Call BASIC routine - note
pop ds ; return values are already
ret ; in dx, ax so we don't have
; to do anything.
BasClientWndProc ENDP
RegBas PROC
mov dx, SEG BasClientWndProc ; Return address of
mov ax, OFFSET BasClientWndProc ; BASIC routine.
ret
RegBas ENDP
Figure 2
#define INCL_WIN
#include <os2.h>
extern MRESULT EXPENTRY ClientWndProc ( long near *, int near *,
long near *, long near *);
MRESULT EXPENTRY BasClientWndProc(HWND hwnd, USHORT msg, MPARAM mp1,
MPARAM mp2)
{
static long near hwndb;
static int near msgb;
static long near mp1b;
static long near mp2b;
hwndb = (long)hwnd;
msgb = (int)msg;
mp1b = (long)mp1;
mp2b = (long) mp2;
return ClientWndProc(&hwndb, &msgb, &mp1b, &mp2b);
} /* BasClientWndProc */
long pascal far RegBas(void)
{
return (long) BasClientWndProc;
} /* regbas */
long pascal Makelong (long passedlong)
{
return passedlong;
} /* MakeLong */
void pascal BreakLong(int hiword, int loword, int *phiword,
int *ploword)
{
*phiword = hiword;
*ploword = loword;
} /* BreakLong */
Figure 3
After the types are set, the conversion of the constants, structures, and
functions begins. The steps for this are as follows:
1 Remove macros and "#ifdef"s since BASIC does not support these: substitute
inline code
2 For comments, do the following:
■ Change the open comments from "/* " to apostrophe and remove the
closing "*/ "
■ Place an apostrophe on each line of multi-line comment
■ Code cannot follow a comment in BASIC
C BASIC
/* This is a ' This is a
multi-line comment */ ' multi-line comment
3 For constants, do the following:
■ Change "#define" to "CONST"
■ Remove underscores from constant names
■ Change "0x" to "&H" for hexadecimal values
■ Change trailing "L"s to "&"s to indicate longs where necessary
■ Place equal signs
■ Note: Byte constants are not possible
C BASIC
#define A_LONG 0x01L CONST ALONG = &H01&
#define A_BYTE 27 'byte constant C name: A_BYTE, value:27
4 For structures, do the following:
■ Change "typedef struct <type name> {" to "TYPE <type name>"
■ Remove underscores from type names
■ Change C format <type> <elem> to BASIC format <elem> AS <type>
■ For any array types, change to <elem>0 .. <elem>n-1
■ Change "}" (to mark end of structure) to "END TYPE"
C BASIC
typedef struct A_STRUCT { TYPE ASTRUCT
int elem1 elem1 AS INTEGER
pvoid elem2 elem2 AS LONG
char elem3[2] elem30 AS STRING * 1
} elem31 AS STRING * 1
END TYPE
5 For functions, do the following:
■ Place "DECLARE FUNCTION" on each function line
■ Remove type from before function name and place appropriate suffix (& or
%)
■ Change param format from <type> <par> to "BYVAL <par> AS <type>". If no
<par> name is given, use C type name or slight variation)
■ Place underscore at end of each continued line
C BASIC
BOOL AFunc(int p1, DECLARE FUNCTION AFunc%(BYVAL p1 AS INTEGER,_
HMODULE, BYVAL HMODULE AS LONG,_
LONG) BYVAL aLONG AS LONG)
Figure 6
NAME PMBASIC WINDOWAPI
DESCRIPTION 'PM Demo Program written in BASIC'
PROTMODE
HEAPSIZE 1024
STACKSIZE 8192
EXPORTS BasClientWndProc
Figure 7
'+-------------------------------------------------------------------
'| Program Name: PMBasic.BAS
'|
'| Description: This program gives a simple demo of a Presentation
'| Manager program written with BASIC Compiler Version
'| 6.00 and the BASIC OS/2 Presentation Manager Toolkit
'| Supplement.
'| This program draws a figure with GpiBegin/EndArea and
'| GpiPolyFillet. It allows the user to choose--with
'| a menu defined in the resource script file,
'| PMBasic.RC--the number of random points used in the
'| area drawn.
'+------------------------------------------------------------------- '*****
Type definitions
TYPE POINTL
x AS LONG
y AS LONG
END TYPE
TYPE QMSG
hwnd AS LONG
msg AS INTEGER
mp1 AS LONG
mp2 AS LONG
time AS LONG
ptl AS LONG
END TYPE
'***** CONSTant definitions
CONST FCFTITLEBAR = &H00000001 : CONST FCFSYSMENU = &H00000002
CONST FCFMENU = &H00000004 : CONST FCFSIZEBORDER = &H00000008
CONST FCFMINMAX = &H00000030 : CONST FCFSHELLPOSITION = &H00000400
CONST FCFTASKLIST = &H00000800 : CONST CSSIZEREDRAW = &H00000004
CONST HWNDDESKTOP = &H00000001 : CONST WSVISIBLE = &H80000000
CONST WMSIZE = &H0007 : CONST WMPAINT = &H0023_
: CONST WMCOMMAND = &H0020
CONST BANOBOUNDARY = 0 : CONST BAALTERNATE = 0
'***** FUNCTION declarations
DECLARE FUNCTION WinInitialize& (BYVAL ushort AS INTEGER)
DECLARE FUNCTION WinCreateMsgQueue&(BYVAL hab AS LONG,_
BYVAL cmsg AS INTEGER)
DECLARE FUNCTION WinRegisterClass%(BYVAL hab AS LONG,_
BYVAL pszCN AS LONG, BYVAL pfnWP AS LONG,_
BYVAL flSty AS LONG, BYVAL cbWD AS INTEGER)
DECLARE FUNCTION MakeLong&(BYVAL hiword AS INTEGER,_
BYVAL loword AS INTEGER)
DECLARE FUNCTION RegBas&
DECLARE FUNCTION WinCreateStdWindow&(BYVAL hwndP AS LONG,_
BYVAL flS AS LONG, BYVAL pflCF AS LONG,_
BYVAL pszC AS LONG, BYVAL pszT AS LONG,_
BYVAL styC AS LONG, BYVAL hmod AS INTEGER,_
BYVAL idRes AS INTEGER, BYVAL phwnd AS LONG)
DECLARE FUNCTION WinGetMsg%(BYVAL hab AS LONG, BYVAL pqmsg AS LONG,_
BYVAL hwndF AS LONG, BYVAL msgFF AS INTEGER,_
BYVAL msgFL AS INTEGER)
DECLARE FUNCTION WinDispatchMsg&(BYVAL hab AS LONG,_
BYVAL pqmsg AS LONG)
DECLARE FUNCTION WinDestroyWindow%(BYVAL hwnd AS LONG)
DECLARE FUNCTION WinDestroyMsgQueue%(BYVAL hmq AS LONG)
DECLARE FUNCTION WinTerminate%(BYVAL hab AS LONG)
DECLARE SUB BreakLong(BYVAL along AS LONG, hiword AS INTEGER,_
loword AS INTEGER)
DECLARE FUNCTION WinInvalidateRect%(BYVAL hwnd AS LONG,_
BYVAL pwrc AS LONG,_
BYVAL fIC AS INTEGER)
DECLARE FUNCTION WinBeginPaint&(BYVAL hwnd AS LONG,_
BYVAL hps AS LONG, BYVAL prcl AS LONG)
DECLARE FUNCTION GpiErase%(BYVAL HPS AS LONG)
DECLARE FUNCTION GpiBeginArea%(BYVAL HPS AS LONG, BYVAL ULONG AS LONG)
DECLARE FUNCTION GpiMove%(BYVAL HPS AS LONG, BYVAL PPTL AS LONG)
DECLARE FUNCTION GpiPolyFillet&(BYVAL HPS AS LONG,_
BYVAL ALONG AS LONG, BYVAL PPTL AS LONG)
DECLARE FUNCTION GpiEndArea&(BYVAL HPS AS LONG)
DECLARE FUNCTION WinEndPaint%(BYVAL hps AS LONG)
DECLARE FUNCTION WinSendMsg&(BYVAL hwnd AS LONG,_
BYVAL msg AS INTEGER,_
BYVAL mp1 AS LONG,_
BYVAL mp2 AS LONG)
DECLARE FUNCTION WinDefWindowProc&(BYVAL hwnd AS LONG,_
BYVAL msg AS INTEGER,_
BYVAL mp1 AS LONG,_
BYVAL mp2 AS LONG)
'********* Initialization section ***********
DIM aqmsg AS QMSG
flFrameFlags& = FCFTITLEBAR OR FCFSYSMENU OR FCFSIZEBORDER OR_
FCFMENU OR FCFMINMAX OR FCFTASKLIST OR_
FCFSHELLPOSITION
Class$ = "ClassName" + CHR$(0)
hab& = WinInitialize(0)
hmq& = WinCreateMsgQueue(hab&, 0)
bool% = WinRegisterClass(hab&,_
MakeLong(VARSEG(Class$), SADD(Class$)),_
RegBas, CSSIZEREDRAW, 0)
hwndFrame& = WinCreateStdWindow(HWNDDESKTOP, WSVISIBLE,_
MakeLong(VARSEG(flFrameFlags&), VARPTR(flFrameFlags&)),_
MakeLong(VARSEG(Class$), SADD(Class$)), 0, 0, 0, 1,_
MakeLong(VARSEG(hwndClient&), VARPTR(hwndClient&)))
'************* Message loop ***************
WHILE WinGetMsg(hab&, MakeLong(VARSEG(aqmsg), VARPTR(aqmsg)), 0, 0, 0)
bool% = WinDispatchMsg(hab&, MakeLong(VARSEG(aqmsg), VARPTR(aqmsg)))
WEND
'*********** Finalize section ***************
bool% = WinDestroyWindow(hwndFrame&)
bool% = WinDestroyMsgQueue(hmq&)
bool% = WinTerminate(hab&)
END
'*********** Window procedure ***************
FUNCTION ClientWndProc&(hwnd&, msg%, mp1&, mp2&) STATIC
ClientWndProc& = 0
SELECT CASE msg%
CASE WMSIZE 'Store size to make area proportional to window
CALL BreakLong(mp2&, cyClient%, cxClient%)
CASE WMPAINT 'Paint window with PolyFillet with (pts%) rand pts
' Invalidate to paint whole window
bool% = WinInvalidateRect(hwnd&, 0, 0)
hps& = WinBeginPaint(hwnd&, 0, 0) 'Begin painting
bool% = GpiErase(hps&) 'Erase window
'***Set up array of random points. Number of points set w/ menu.
IF pts% = 0 THEN pts% = 50
REDIM aptl(pts%) AS POINTL
FOR I% = 0 to pts%
aptl(I%).x = cxClient% * RND : aptl(I%).y = cyClient% * RND
NEXT I%
'*** Start at last pt and draw PolyFillet through all pts
' alternating fill
bool% = GpiMove(hps&, MakeLong(VARSEG(aptl(pts%)),_
VARPTR(aptl(pts%))))
bool% = GpiBeginArea (hps&, BAALTERNATE OR BANOBOUNDARY)
bool% = GpiPolyFillet(hps&, pts% + 1,_
MakeLong(VARSEG(aptl(0)), VARPTR(aptl(0))))
bool% = GpiEndArea(hps&)
bool% = WinEndPaint(hps&)
CASE WMCOMMAND 'Menu item sets number of pts to use in drawing.
CALL BreakLong(mp1&, hiword%, pts%)
bool% = WinSendMsg(hwnd&, WMPAINT, 0, 0) 'Send WMPAINT to draw
CASE ELSE 'Pass control to system for other messages
ClientWndProc& = WinDefWindowProc(hwnd&, msg%, mp1&, mp2&)
END SELECT
END FUNCTION
Figure 8
/*--------------------------------------------------------------------
| Program Name: PMBasic.RC
|
| Description: This is the resource script file for PMBasic.BAS.
| A menu containing nested submenus is created to allow the user to
| select the number of points to be used in the figure in
| PMBasic.BAS.
|
| NOTE: Because the "Exit" item has the MIS_SYSCOMMAND, there is no
| command message to be processed in PMBasic.BAS. This item will
| automatically close the window.
+--------------------------------------------------------------------/
#include <os2.h>
MENU 1
{
SUBMENU "~PM Basic Demo", 1
{
SUBMENU "~Points", 2
{
MENUITEM "~10", 10
MENUITEM "~20", 20
MENUITEM "~30", 30
MENUITEM "~40", 40
MENUITEM "~50", 50
MENUITEM "~60", 60
MENUITEM "~70", 70
MENUITEM "~80", 80
MENUITEM "~90", 90
}
MENUITEM SEPARATOR
MENUITEM "E~xit", SC_CLOSE, MIS_SYSCOMMAND
}
}
DOS Commands Inside Your Code: Process Control and Signal Handling
Kris Jamsa
As the complexity of your applications increases, you might need to access
DOS1 commands, such as PRINT, BACKUP, or RESTORE, from within your programs.
You might even want to let the user exit your application temporarily to
issue commands at the DOS prompt. When the user wants to return to your
application, he or she can use the DOS EXIT command.
In this article we examine several ways to access DOS commands from within
your program. In addition, we look at several exceptions, such as
Ctrl-Break, that can occur during the execution of your program and how you
can install functions that handle them.
Invoking DOS Commands Inside Your Programs
The easiest method to invoke DOS commands from within your program is to use
the C run-time library function "system."
The following program (DOSDIR.C) uses the system function to execute the DOS
DIR command:
#include <process.h> /* needed for system */
main()
{
system("DIR");
}
The DOS DIR command is an internal DOS command. Had the program invoked an
external DOS command instead, such as DISKCOPY or BACKUP, DOS would first
search the location specified in the function argument, then the current
directory, and following that, the locations identified in the PATH=
environment entry.
The system function is helpful in other ways. Most users like the
convenience of working with familiar applications. By using system, your
programs can satisfy this preference by executing the end user's word
processor, spreadsheet, or other application. Likewise, system provides a
way for users to exit an application temporarily to look up files or to
perform other DOS commands.
The following program (SYS.C) uses system to exit the application
temporarily so that the user can issue DOS commands. When the user finishes
executing commands at the DOS prompt, he or she can type EXIT and press
Enter to return to the program.
#include <process.h> /* needed for system */
#include <stdio.h> /* needed for getchar */
main()
{
printf("Type EXIT to continue");
system("COMMAND");
printf("Back in application - press Enter\n");
getchar();
}
To give you greater control, the run-time library also provides two series
of routines that access DOS commands: exec functions and spawn functions.
The exec functions invoke a DOS command without returning control to the
program when the command completes. The C run-time library provides eight
forms of the exec function--execl, execle, execlp, execlpe, execv, execve,
execvp, and execvpe. In general, the differences among them are the ways the
calling program passes command line arguments and environment entries to the
command and the directories DOS searches to locate a command. The letters in
the function name suffix have the meanings described in Figure 1.
To demonstrate the way various exec functions work, let's create a program
you can invoke with them. Enter and compile the following program (SHOW.C),
which displays its command line arguments and environment entries:
main(int argc, char *argv[], char *env[])
{
while (*argv) /* display command line arguments */
puts(*argv++);
while (*env) /* display environment entries */
puts(*env++);
}
The following program (RUNSHOW.C) uses the execl function to execute
SHOW.EXE. The output of SHOW reveals that it receives the arguments
specified in RUNSHOW and a copy of the RUNSHOW environment entries.
#include <process.h> /* needed for execl */
#include <stdio.h> /* needed for NULL */
main()
{
execl("SHOW.EXE", "SHOW", "A", "B", "C", NULL);
printf("This line will never be executed\n");
}
Let's look at the arguments for execl. The first argument is a character
string that identifies the program to execute. If the program does not
reside in the current directory, specify a complete pathname. Remember,
execl does not support the PATH environment variable. The remaining string
arguments represent entries on the DOS command line, argv[0] through
argv[3]. The NULL pointer terminates the argument list. Note that the printf
statement never executes: when you use an exec function, your program does
not resume control.
You might modify RUNSHOW.C slightly by replacing execl with execlp. The new
version would support the PATH environment entry.
The following program (RUNSHOW2.C) uses the C run-time library function
execle to execute SHOW.EXE. RUNSHOW2.C defines a new environment and passes
it to SHOW.EXE:
#include <process.h> /* needed for execle */
#include <stdio.h> /* needed for NULL */
main()
{
char static *new_env[4] = { "BOOK=C SSS",
"CHAPTER=35",
"COMPILER=MSC" };
execle("SHOW.EXE", "SHOW", "A", "B", "C", NULL, new_env);
}
When you run this program, your screen shows
SHOW.EXE
A
B
C
BOOK=C SSS
CHAPTER=35
COMPILER=MSC
Each of the preceding exec functions passes the command line entries as a
variable number of arguments. The next set of exec functions passes the
command line as an array of pointers to character strings.
The following program (RUNSHOW3.C) defines an array of pointers to character
strings that serve as the command line arguments for SHOW.EXE. The program
uses the execv function to invoke SHOW.
#include <process.h> /* needed for execv */
main()
{
static char *args[4] = { "SHOW",
"A",
"B" };
execv("SHOW.EXE", args);
}
Try substituting execvp for execv in RUNSHOW3.C. Then experiment with
different locations of SHOW.EXE. As long as the new location is identified
in your PATH= entry, execvp is able to run SHOW.
The following program (RUNSHOW4.C) defines an array of pointers to the
command line and an array of pointers to environment entries. The program
calls execve, which uses these arrays to execute SHOW.EXE.
#include <process.h> /* needed for execve */
main()
{
static char *args[4] = { "SHOW",
"A",
"B" };
static char *new_env[4] = { "BOOK=C SSS",
"CHAPTER=35",
"COMPILER=MSC" };
execve("SHOW.EXE", args, new_env);
}
The exec functions are useful when you do not want control to return to your
program. But if your program needs to regain control when the command is
complete, the program must use one of the spawn functions.
The C run-time library provides eight varieties of spawn functions, just as
it does for exec. Let's examine the two primary spawn routines, spawnlpe and
spawnvpe.
The various suffix letters have the same meanings for spawn functions as
they do for the exec set of functions. Figure 2 summarizes the spawn
functions.
The spawn functions are convenient in that they let an application execute a
second program and then--if the mode flag is set to P_WAIT--regain control
when the program is complete. Consequently, most applications use the P_WAIT
flag when they call spawn functions. If an application does not need to
regain control after it executes a command, it can use an exec function.
The following program (RUNSHOW5.C) uses spawnlpe to execute SHOW.EXE. The
program creates an array of pointers to environment entries and passes them
to spawnlpe. When SHOW.EXE is complete, DOS returns control to RUNSHOW5,
which displays a message to verify the return of execution.
#include <process.h> /* needed for spawnlpe */
#include <stdio.h> /* needed for NULL */
main()
{
static char *new_env[4] = { "BOOK=C SSS",
"CHAPTER=35",
"COMPILER=MSC" };
spawnlpe(P_WAIT, "SHOW.EXE", "A", "B", NULL, new_env);
printf("Back from spawn\n");
}
Programs can return an exit status when they end. If you spawn a program,
the spawn function returns the exit status of the program.
The following program (RUNSHOW6.C) uses spawnvpe to execute SHOW.EXE. When
DOS returns control to RUNSHOW6, the program displays SHOW's exit status.
#include <process.h> /* needed for spawnvpe */
main()
{
static char *new_env[4] = { "BOOK=C SSS",
"CHAPTER=35",
"COMPILER=MSC" };
static char *args[4] = { "SHOW",
"A",
"B" };
int result;
result = spawnvpe(P_WAIT, "SHOW.EXE", args, new_env);
printf("Back from spawn - Exit value is %d\n",
result);
}
Signal Handling
As operating systems offer a richer set of services, we will find that the
operating system treats more and more events in the system as signals. A
signal can be viewed as an alarm the operating system rings when a specific
event occurs. Each unique signal has its own alarm. Under DOS, the three
signals your programs can receive are SIGINT, which occurs when a user
presses Ctrl-Break; SIGFPE, which occurs when a floating point error (such
as division by zero) is detected; and SIGABRT, which occurs when a program
aborts with an exit status of 3.
The C run-time library function signal, defined in the include file
signal.h, lets you install a function that executes when a specific signal
occurs. The signal.h file should not be used under OS/2.
The following program (CBREAK.C) defines the function ctrl_break and
installs it to handle the SIGINT signal, which occurs when the user presses
Ctrl-Break. Our handler ignores the signal until the user has pressed
Ctrl-Break five times. On the fifth occurrence of Ctrl-Break, the program
ends.
#include <signal.h> /* needed for signal */
#include <stdio.h> /* needed for getchar */
#include <process.h> /* needed for exit */
main()
{
int ctrl_break();
signal(SIGINT, ctrl_break);
printf("Press Ctrl-Break 5 times to terminate
program\n");
getchar();
}
ctrl_break()
{
static int count = 1;
signal(SIGINT, SIG_IGN);
if (count++ = = 5)
{
printf("Program terminated by Ctrl-Break\n");
exit();
}
signal(SIGINT, ctrl_break);
}
The function ctrl_break is the signal handler. It directs DOS to ignore
Ctrl-Break signals that occur while the handler is executing. The routine
then increments a count of the number of Ctrl-Break signals that have
occurred. Last, the function reinstalls itself as the Ctrl-Break handler.
Our ctrl_break function calls signal with SIG_IGN, which directs DOS to
ignore incoming signals of the same type while the handler executes. After
the handler performs its processing, it uses signal to reinstall itself as
the Ctrl-Break handler.
As you begin to write signal handlers, you will find that testing a handler
is, in some cases, quite difficult. To assist you, the C run-time library
provides the raise function.
The following program (RAISE.C) defines the function abort_handler and
installs it as the SIGABRT handler. Next the program raises the signal so
that you can test your handler.
#include <signal.h> /* needed for signal, raise, and
constants */
#include <stdio.h> /* needed for fcloseall */
main()
{
int abort_handler();
signal(SIGABRT, abort_handler);
raise(SIGABRT);
}
abort_handler()
{
signal(SIGABRT, SIG_IGN);
printf("In abort handler -- closing all open
files\n");
fcloseall();
}
By using raise in this manner, your program can test your handler and verify
that it responds to SIGABRT. You might then replace the call to raise with a
call to the run-time library function abort and so verify that DOS raises
the SIGABRT signal when the program aborts.
Signal processing is still in its infancy. With the advent of OS/2, you will
see its use grow, as signals become an important tool for interprocess
communication.
Figure 1 Meaning of Letters in Function Name Suffixes
Letter Meaning
l Passes a variable number of command line arguments,
the last being NULL
v Passes an array of pointers to command line arguments
p Supports the PATH environment variable
e Passes an array of pointers to environment entries
Figure 2 The spawn Functions Behave as Their Suffixes Specify
Function Name PATH= Support Environment Entries Command Line
Entries
spawnl No Copies current NULL-terminated list
spawnle No Array argument NULL-terminated list
spawnlp Yes Copies current NULL-terminated list
spawnlpe Yes Array argument NULL-terminated list
spawnv No Copies current Array argument
spawnve No Array argument Array argument
spawnvp Yes Copies current Array argument
spawnvpe Yes Array argument Array argument
────────────────────────────────────────────────────────────────────────────
Volume 5 - Number 2
────────────────────────────────────────────────────────────────────────────
Using the OS/2 National Language Support Services to Write International
Programs
Asmus Freytag and Michael Leu
The OS/2 operating system is designed to be an international operating
system. Localized editions of OS/21 Version 1.2 now exist for Japan, Korea,
and most of Europe. Application writers can use the National Language
Support (NLS) system services provided by OS/2 to write programs that
support many national languages automatically (see Figure 1). Programs such
as word processors that require more detailed linguistic knowledge can
bypass the default functionality of OS/2 to exploit its more advanced NLS
features.
The process of modifying a program for use in a different country is called
localization. Ideally, applications have only one set of source code for all
languages, instead of one version for each market. Localization decreases
source code maintenance costs and makes it much easier to release the
product simultaneously in multiple markets. This article discusses OS/2 NLS
functions and localization principles and demonstrates them using two sample
programs.
Localizing a piece of software involves much more than merely translating
text messages. Methods of processing and formatting data as well as handling
input may differ from country to country. In addition, an application may
have to deal with specialized, country-specific hardware.
Code Pages and Country Codes
First, you must understand how OS/2 uses code pages to represent text data.
Data is merely a stream of bytes. A code page is a table that maps byte
values (or code points) to a set of characters or glyphs (picture
characters). Code pages are uniquely numbered (for example, 437 is the U.S.
code page), so if you specify a code page number with a stream of bytes, you
have ensured that your data can always be interpreted correctly.
Another factor in determining how data will be processed is the country
code. A country code is a three digit number based on those used by the
international phone system to designate countries (001 represents the U. S.,
081 represents Japan, and so on). In OS/2, country codes specify the country
rules that should be used in operation (for example, how to format number,
date, and time strings); it also restricts the list of possible valid code
pages. Unlike the code page, the system country code cannot be changed at
run time. However, different values can be specified to OS/2 function calls
that take the country code as an argument.
Code pages 850 and 863 are shown in Figure 2. As you can see from the code
page 850 and 863 tables, the same code point may reference different
characters in different code pages. However, while code pages differ in
their representation of non-English characters and symbols, most map the
bytes in the 20H to 7EH range to the printable ASCII characters. This allows
ASCII strings to be preserved when the code page is changed.
As expected, many system functions depend on the characters or glyphs of the
passed arguments, not on their binary value. When the application calls a
function, the code page to be used for interpreting the binary values must
be established.
Specifying the Code Page and Country Code
In the CONFIG.SYS file, there are two statements that initialize the code
page and country code for all OS/2 Vio sessions. The COUNTRY configuration
command specifies the country code and the location of the file COUNTRY.SYS,
which contains country-specific information. The CODEPAGE configuration
command allows you to specify up to two code pages (restricted by the
country code), which are prepared, and which can be selected by the Change
Codepage command.
The DosGetCp call can be used by applications to obtain the prepared code
pages. If no code pages are specified, DosGetCp returns zero for the code
page value, and the system uses the character set supported in the hardware
(the ROM code page). For an application that needs to know its code page,
use the code in Figure 3. Usually this code is unnecessary since most Dos
APIs will accept zero as a code page argument and will then use the process
code page.
Each OS/2 process has an associated process code page. The process code page
is used by Dos API functions, such as the file system calls DosOpen,
DosFindFirst, and DosFindNext to interpret string parameters. This code page
is initially the primary code page defined in CONFIG.SYS. Furthermore, every
OS/2 subsystem, such as Vio for screen output and Kbd for keyboard handling,
has its own code page.
Given this, the code page is inherited as follows: DosSetCp will set the
process and all subsystem code pages or DosSetProcCp can be called to set
the process code page but not the subsystem code pages. The default code
page settings from CONFIG.SYS can be overridden by calling
subsystem-specific calls, such as VioSetCp and KbdSetCp. However, note that
subsystems are shared among all processes of a session and there is no
notification of a code page switch to the other processes.
In OS/2 Presentation Manager (hereafter "PM"), code pages are associated
with message queues and presentation spaces (PSs). To change the message
queue code page, call WinSetCp. This changes the keyboard translation tables
for that message queue (WM_CHAR messages will contain characters in the
specified code page); and it also defines the default code page of all new
PSs associated with the message queue. To override this inheritance
mechanism explicitly, the PS code page can be switched by calling GpiSetCp.
This will cause all output to the PS to be interpreted according to the
newly specified code page. Existing PSs are not affected by a switch of the
queue code page; however, WinBeginPaint implicitly creates a new PS each
time it is called. Thus, after a WinSetCp call, WinBeginPaint will create
PSs with the new queue code page.
Queue code pages may only be set to those code pages available to non-PM
programs as indicated by DosGetCp. (The only exception to this is the
output-only desktop publishing code page 1004.) If DosGetCp returns 0, the
queue code page will default to 850 (the multilingual code page) if the
country setting is European or American. In Asian OS/2, the queue code page
will default to the primary Asian code page.
The set of code pages available for PSs is a superset of the set of
available queue code pages. WinQueryCpList provides a list of all available
code pages for the GpiSetCp call. This allows the use of special desktop
publishing code pages as well as EBCDIC code pages for output purposes.
Note that even though WinSetCp and GpiSetCp have been used to set the queue
and PS code pages, the file system code page will still be dependent on the
process code page (which may be set using DosSetCp or DosSetProcCp).
Country settings can be set in the Control Panel. These options are stored
in the system profile and can be referenced with the appropriate Prf calls.
If these calls fail, it is recommended that DosGetCtryInfo be used.
Message Substitution
Once you understand code pages, you can tackle localization concerns. The
first step in localizing applications is the translation of the relevant
application strings. To make this translation step easy, the messages should
be separable from the source code. Furthermore, the strings should be
flexible, allowing simple word replacement.
OS/2 provides several means by which messages and program strings can be
isolated from the code. One method available to all OS/2 programs is to keep
strings in message files, using the DosGetMessage API to retrieve the
messages. Not only does this isolate the message text from the program
source, but it also has the advantage that DosGetMessage can take a variable
number of arguments that can be inserted into the text of the message. This
allows program strings to be generated regardless of the word order of the
sentence in the language (see Figure 4).
Most PM strings, such as those used in dialog boxes, menus, and other system
controls, are already stored in resource templates. Other program
informational strings (such as error messages) should be placed into string
tables and then loaded at run time with the WinLoadString instruction. PM is
capable of substituting strings using the WinSubstituteStrings instruction.
Besides the mechanics of replacing strings, there are other considerations.
Arrays that store messages should allocate up to 30 percent more storage for
messages, because the strings may grow when translated into other languages.
Dialog boxes should always be the right size to accommodate their string
contents. Also, since the byte values of frame characters are code
page-dependent, three MSG_APPL system messages have been defined in
OSO001.MSG to allow the user to access the appropriate frame characters.
These messages should be retrieved and used whenever frame characters are to
be drawn in Vio or DOS mode (see Figure 5). This facility is not as useful
in PM, because frame characters may not line up with proportional fonts, and
because frames can be drawn with GPI calls.
Country-Specific Formatting
Numbers, dates, and other information are formatted differently in different
countries. For example, January 1, 1990 is formatted 1 January 1990 in
England. OS/2 allows these settings to be customized in the Control Panel.
These options are then written to the OS2.INI system profile, with the
AppName PM_National and with the keys shown in Figure 6. Note that these
symbols are code page-dependent. For example, if a code page is specified
that does not contain the Yen () symbol, the Yen marker cannot be displayed.
The strings in Figure 6 can be retrieved from the system profile by making
the appropriate Prf calls (see Figure 7). If these profile queries fail, the
application writer can still fall back on the DosGetCtryInfo call, which
will retrieve these values from the COUNTRY.SYS file. It is a good idea to
use the Prf calls, because the formatting information can be specified by
the user at run time using the Control Panel. The information retrieved by
DosGetCtryInfo is more of a default, as it statically comes from the file
COUNTRY.SYS.
Data Processing
When data is processed in different code pages, special care must be taken.
In code page 850 (multilingual), there are accented characters, which have
code point values greater than 7FH. Because of this, be sure to declare
characters unsigned. (The -J switch can be specified to the Microsoft C
compiler Version 5.1 to make all characters default to unsigned.) Another
difficulty that arises is that many standard U.S. C run-time functions, such
as isalpha and strcmpi, will not work correctly when given strings that are
not in code page 437, the U.S. code page. (The C run-time functions strlen
and strcpy will work with all code pages, because they look only for the
NULL terminator.) To avoid these problems, use OS/2 system services to
perform code page-dependent operations such as case conversions (use
DosCaseMap, WinUpper, or WinUpperChar) and string comparisons (DosGetCollate
or WinCompareStrings). Two more useful instructions are WinCpTranslateChar
and WinCpTranslateString, which allow conversion between code pages. By
using these calls, files that are stored in one code page can be used by an
application running in a different code page.
String comparison is tricky because the order in which characters are sorted
depends on the language of the text. For example, some European languages
sort accented characters between the unaccented characters; some, like the
Scandinavian languages, sort them at the end. To make your program sort
correctly in every country, you can use DosGetCollate. DosGetCollate returns
a table of sorting weights for each character. Instead of sorting by the
byte value of the original characters, replace them by their sort weights
and compare these against each other. The sorting table returned by
DosGetCollate gives the same weights for uppercase and lowercase characters.
WinCompareStrings uses this collating table to sort strings correctly.
Another consideration is how accented characters can be entered. If the
keyboard does not have keys representing the accented characters, deadkeys
may be used. Deadkeys are keys that produce an accent mark used in
combination with another character. Deadkeys do not advance the cursor.
Deadkey handling is made much easier by internal PM handling; the WM_CHAR
message has three special KC_ flags that tell the state of deadkey
processing. The KC_DEADKEY flag means that the key pressed is a deadkey; the
character KC_COMPOSITE means that the deadkey's accent mark has been fused
with the current character to form a new character; and the KC_INVALIDCOMP
flag denotes an invalid composition, and returns the last character
selected. If KC_INVALIDCOMP is specified, and the current character is not a
space, the application should beep the speaker and display the new character
code. For an example of proper WM_CHAR message handling, refer to the
TyperChar subroutine discussed below.
Device Handling
This concern is not localization-specific, but a reminder that OS/2 is an
operating system that runs on many platforms. The application writer should
therefore be sure to use device-independent units as much as possible. For
example, an image of size 53-by-53 pixels is much smaller on a
high-resolution screen than on a low-resolution screen. Also, direct
manipulation of hardware devices (such as writing characters directly to the
physical video buffer) should be avoided at all costs.
Double Byte Character Set
European editions of OS/2 are essentially equivalent to the U.S. version.
The Japanese and Korean versions of OS/2, however, require support for
character sets that contain more than 256 characters. Support for the large
character sets is available in Japanese OS/2 Version 1.1 and in the U.S. and
European OS/2 with PM Version 1.2. U.S. and European PM can use code page
932, but since Japanese display drivers and ROM fonts are not included, the
special characters cannot be displayed.
Up to this point, we have only discussed code pages that have 256 entries
(one for each possible byte value). But Japanese has more than 5000
characters! Japanese is written in a mix of Kanji (ideograph) characters and
Kana (phonetic) signs. There are two kinds of Kana signs: Hiragana, the
Japanese alphabet of sounds, and Katakana, which is used to write foreign
words phonetically. Kana-Kanji conversion is a convenient method for typing
Kanji characters; Japanese keyboards are often labeled with Katakana to
allow the typing of Japanese words phonetically (see Figure 8). There are
118 Katakana and Hiragana characters and about 3500 Kanjis in general use
today.
To handle all these characters, a double byte character set (DBCS) was
introduced. (In this article, DBCS refers to a character set that contains
both single and double byte characters. Purists would call this type of
character set a mixed byte character set--MBCS. They would use "DBCS" for a
set that is strictly double byte.) In a double byte character set, certain
ranges of code points in the code page are designated as leading bytes.
Leading bytes have no character value themselves; instead, they indicate
that they together with the following byte represent a single character.
This second byte is called a trailing byte or trail byte. You must make
certain that the two bytes of a double byte character are always treated as
a unit; if they become separated, the character and/or the byte stream of
data will be processed incorrectly.
This article restricts itself to DBCS code page 932, the primary code page
used in Japan today. Figure 9 shows the first 256 bytes, which are divided
into several ranges. There are two main single byte ranges, in which one
byte corresponds to one character. In the range from 00H to 7FH, the byte
values are treated as ASCII (as in the European code pages; the differences
are that the byte value 5CH is used for the Yen marker instead of the
backslash, and that the character assignments in the control area from 01H
to 1FH represent different graphic characters). The second single byte range
is from A0H to DFH, and contains codes for the Japanese Katakana syllabary.
These codes correspond to keys on some standard Japanese keyboards, such as
the Architecture Extended (AX) (see Figure 8).
In code page 932, the ranges from 81H to 9FH and from E0H to FCH have been
reserved as lead bytes. (Each DBCS code page has a different set of lead
byte ranges. To determine these ranges, call DosGetDBCSEv; if no ranges are
found, you are in a SBCS code page.) In essence, each lead byte "points" to
a subsequent block of 256 characters. This scheme provides for the
representation of the characters in the first two Japanese Industrial
Standard (JIS) character set levels (see Figure 10) and leaves room for
future additions. A disadvantage is that trail bytes may have any value
except NULL, so applications must be careful when performing string
processing.
Data Processing
There are guidelines that will prevent DBCS data processing errors. For
instance, problems will arise any time a character stream is scanned
linearly for a particular byte value. This problem arises because the only
byte guaranteed never to be a trail byte is NULL (00H) and many applications
will scan a string for a special delimiter (such as backslash, space, or
tab). (Some delimiters such as the period and tab are not valid trailing
bytes in code page 932, but this may change in the future.) For example, 5CH
is a valid trail byte in code page 932 and it is also used in OS/2 as a path
delimiter. Therefore, it is necessary to scan a string by characters, noting
double byte characters as they occur.
DBCS string truncation must be performed carefully. Never allow a string to
end in a lead byte or begin with a trailing byte. If a string somehow starts
with a trailing byte, it should be either replaced with a single byte
padding character (such as a space), or the string pointer should be
advanced by a byte. When a string ends with a leading byte, the string can
be either shortened by a byte, or again, the offending byte can be replaced
by a single byte padding character. Note that backspace and delete
processing should always remove double byte characters completely. While
this sounds easy enough, from within a string it is not easy to determine
what the DBCS character boundaries are. Figure 11 demonstrates how DBCS
strings can become corrupted. A sure way to identify DBCS characters is to
scan from the beginning of the string, which can be done by calling
WinNextChar and WinPrevChar instead of blindly incrementing string pointers.
Also, as in other code pages, WinCompareStrings can be used to compare
strings properly.
DBCS output should be performed a full line at a time. By using system
services to do this, DBCS bisection problems can be dealt with by OS/2. For
example, the WinDrawText call performs correct word breaking, whatever the
language, based on the country code and code page.
Another guideline is always to scan strings forward. You should replace
backward searches by forward searches and replace pointer decrements by safe
macros or function calls. Using a forward search from the beginning of the
string is the easiest and safest way to ensure that characters are scanned
correctly.
Do not increment or decrement string pointers. Although the ++ operator in C
is a convenient way to scan an SBCS string, it does not take double byte
characters into consideration. Instead of incrementing string pointers, skip
entire DBCS characters by using safe functions such as WinPrevChar and
WinNextChar, or define your own macros if speed is critical.
Be very careful when matching special characters. Many special characters
are in the trail byte range (for example, backslash and tilde). Avoid
problems by skipping entire double byte characters when moving through the
string.
You should pass pointers to lead bytes or single byte characters, because
all system functions (except VioWrtTTY) assume that pointers point to a lead
byte or a single byte character. Application functions should also be
written this way, as a rule.
Most importantly, always treat double byte characters as a unit. Never allow
partial selection of double byte characters and never allow the cursor to
rest on the second byte of a double byte character.
Character Input and Output
To input DBCS characters in Vio sessions, the same API calls previously
discussed can be used. An application might use the KbdCharIn call to read
characters into a buffer. Since KbdCharIn returns characters a byte at a
time, the application writer must keep track of double byte handling,
buffering leading bytes as necessary, to make sure that partial DBCS strings
are not displayed. Trail bytes should never be left in the typeahead buffer:
either read the second byte, or discard the leading byte and flush the input
queue. Again, the delete and backspace keys should always delete or back up
entire DBCS characters.
PM input handling is easier, because double byte characters are returned as
a unit in mp2 by the WM_CHAR message. Single byte characters are also
returned in mp2, with the second byte to be ignored. This reinforces the
idea that double byte characters should be handled as a unit.
As for DBCS output, in Vio sessions all Vio text output calls may be used
(for example, VioCharStringAtt). However, two rules should be followed.
First, display updates must never result in the display of partial double
byte characters. To prevent this, never pass a string starting with a trail
byte, and never pass a string containing a lead byte with no trail byte.
(This rule does not hold for VioWrtTTY, which will buffer lead bytes that it
encounters at the end of strings. However, this feature may require the
application to keep track of when a lead byte is buffered.) The second rule
is that if character attributes such as reverse video are specified, use the
worldwide Logical Video Buffer (LVB) format (see Figure 12).
DBCS character handling is easier in PM than in full screen mode. As long as
complete DBCS strings are output, they will be clipped properly to the
window boundary.
Another useful OS/2 facility is the DT_WORDBREAK flag that can be specified
with the WinDrawText call. In European languages, word breaking occurs when
spaces are encountered; however, in Japanese, word breaks may occur between
double byte Kanji characters because each character may represent a concept
or word. WinDrawText will use the current PS code page to determine the
word-breaking rule to use. (The hook HK_FINDWORD can be used to define
word-breaking rules.)
Given the preceding, you should avoid detailed text handling by calling
system APIs with a full line of text or more at a time. In this manner,
word-breaking logic can be handled for you, as well as correct handling of
proportional fonts; the system will also handle the special cases where DBCS
strings can become corrupted.
Asian Input Methods
A PM facility introduced in Japanese OS/2 Version 1.1 is the ability to
specify alternative mnemonics. As you may know, a mnemonic is an underlined
character in menu items or on buttons in dialog boxes. Typing that character
on your keyboard selects the menu item or button. The mnemonic keystroke is
defined as the character following the tilde in the menu string provided in
the menu template, "~Item", for example. But because there are two types of
keyboards in Japan (labeled in Katakana or Hiragana and the Latin alphabet)
the simple U.S. scheme had to be extended. There are two kinds of touch
typists in Japan, those who are accustomed to the standard QWERTY layout and
those who are used to one of the native Japanese layouts. (The operator can
toggle between English and Japanese keytops with a special shift key.)
To solve this problem, mnemonics can be specified with either the tilde or
with the sequences \036 and \037. For example, consider a string "Item
(\036x\037X)", where "x" is the U.S. keyboard mnemonic, and "X" is the
Katakana mnemonic. Then if the U.S. keyboard mnemonic is active, the menu
item would appear as "Item (x)"; otherwise you'd see "Item (X)". The user
can select which of the two mnemonics is to be displayed by setting the
system value SV_ALTMNEMONIC from the Control Panel. SV_ALTMNEMONIC is TRUE
if the non-Latin mnemonic is active. The action of the mnemonic keystrokes
is such that when one of the sets of mnemonics is selected, the system
responds to that key regardless of the shift state of the keyboard. For an
illustration of how mnemonics appear, see Figure 13.
The most visible difference in the Japanese input scheme is that the bottom
of most screens (Vio screens and PM windows) contains a keyboard status
area. This status area should always be present if characters can be
entered. It is used to reflect the state of the keyboard and to allow
Kana-Kanji (KK) conversions. The possible keyboard states are whether the
keyboard is in English, Katakana, Hiragana, or Romanji (Roman character)
mode; whether the shift key is depressed; and whether or not KK conversion
is enabled.
As you might imagine, with more than 3500 Kanjis in use, it would be very
difficult to have a key cap for each possible Kanji character. Touch typing
would be rather tedious! To solve this problem, OS/2 allows the user to
enter Kana-Kanji conversion mode. In this mode, all typed input is placed in
a conversion window. The user inputs a Kana string (usually in Hiragana),
which is sent to a dictionary when the user presses the VK_DBE_CONV key. The
dictionary then returns a Kanji character with that Kana pronunciation. If
the Kanji character is not acceptable, the VK_DBE_CONV key can be used to
select an alternative Kanji string. If this Kanji character is acceptable,
it is selected with the VK_ENTER key, upon which the conversion area
disappears and the characters are relayed to the application.
The visual effect of this conversion is shown in Figure 14. In the Vio
screen, pop-up windows (conversion areas) are placed on the screen at the
current cursor location. In PM, the system sends the WM_QUERYCONVERTPOS
message to the window, asking where to place the conversion window. The
window can either process this message by specifying the coordinates to be
used, or it can pass the message to its parent. The standard frame window
procedure will place the conversion window on its status line if
FCF_DBE_APPSTAT has been specified (which is why this flag must be supplied
on frame windows when input is occurring).
In Vio screens, the bottom of the screen may have several lines reserved for
the status line and input conversion area. Applications can determine the
size of this area (in lines) by calling function number 7 of VioGetState.
Scrolling and cursor positioning are restricted automatically to exclude the
status area; therefore, to clear the screen without overwriting the status
area, VioScrollUp should be called with FFFFH as the number of lines to
scroll.
For PM applications from OS/2 Version 1.2 (OS/2 Version 1.1 in Japan)
onwards, the flag FCF_DBE_APPSTAT should be added to any frame window that
has children that process input (such as dialog boxes that contain entry
fields or list boxes). This flag currently has no effect in the U.S. and
Europe, but creates the status line if the queue code page is a DBCS code
page. This can be seen on the Japanese window in Figure 1. Note that other
than the fact that the client area is smaller by the height of the status
area, the application behavior is no different.
Other Considerations
The standard U.S. and European fonts (Times Roman, Helvetica, and Courier)
do not contain DBCS glyphs. Therefore, if a PM program loads these fonts,
Japanese characters cannot be displayed, even when running under code page
932. As a result, the font name string of font resources should be contained
in a resource file, just like the other language-dependent strings. Another
implication is that the code page of the font must be specified on creation;
if code page 0 is passed to GpiCreateLogFont, the font is created as a U.S.
and European font. (Technically, a Universal Glyph List, UGL, font is
created. However, the UGL in OS/2 Version 1.2 contains only U.S. and
European characters, so UGL fonts are not sufficient for Japan.)
The layout of the LVB in DBCS countries is often quite different from that
in the United States. There are many proprietary LVB formats; changing the
attribute bytes of a Vio screen may yield unpredictable results on different
hardware. The best approach is to use the worldwide LVB format (see Figure
12), which is a superset of the 3-byte attribute format in PM AVIO PSs.
You should not directly manipulate the Physical Video Buffer (PVB) in text
mode. Doing this not only exposes your program to vastly different hardware
standards, but it may also collide with the Asian input method conversion
software.
Printers must be switched explicitly to the code page of your print job,
which is done by calling DosDevIOCtl with the PRT_ACTIVEFONT command. In
addition, some printer drivers may support other IOCtl commands in other
countries (for example, to select Shift-JIS to JIS character encoding
translation and the like).
A few miscellaneous concerns: always use DosGetDBCSEv to find lead byte
ranges. Never hardcode ranges for lead bytes. Add FCF_DBE_APPSTAT to all
frame windows within which input can take place. Process the
WM_QUERYCONVERTPOS message if your application deals with general input
streams.
Sample Vio Program
MAKEBOX (see Figure 15) is a sample program that simply pops up a message
box. The first step in building MAKEBOX is constructing the message panel.
In DBCS countries, DosGetMessage retrieves system messages from different
files, depending on the process code page. This allows error messages to be
displayed in English when the process is not in a DBCS code page. However,
the process code page may or may not be the code page of the video
subsystem. Since DosGetMessage uses the process code page, you must
explicitly synchronize the process code page with the current video code
page for the duration of the message retrieval. A worldwide program is
likely to be run on machines, especially in Japan, where the familiar frame
drawing characters are not all available or have different character codes,
so be sure to use the MSG_APPL messages defined in Figure 5.
Traditionally, another area of hardware incompatibility is the layout of
character attributes. However, OS/2 Version 1.2 and Japanese OS/2 Version
1.1 introduced the worldwide LVB format with a standard layout for the
character attributes. This LVB format is fully supported by windowed Vio and
full screen sessions, as far as the hardware is capable. Unsupported
attributes are ignored. For example, in Japan, a common limitation is that
either the foreground color or the background color must be black. The
solution is to virtualize the character colors of your application and let
the user customize them in the installation process, with suitably
restricted defaults. MAKEBOX has a color definition table, although in this
sample its values are filled with constants.
Now that you know how to construct the message panel, the next consideration
is how to save and restore the original screen contents. This presents a
small challenge in the case of DBCS code pages. You will see shortly why it
is so necessary to plan ahead to write a truly worldwide program; designing
support for DBCS code pages from the start is much easier than trying to
graft it on after the fact.
The main problem with saving a rectangular section of a screen containing
Japanese text is that double byte characters often occupy two cells on the
display. Often, several of them straddle the boundary of the rectangle that
will be covered. When the message box is drawn, it will obscure part of
these characters. Since most display hardware is not capable of interpreting
partial characters, Vio automatically replaces the leftover half with a
space character. Therefore, the area to be saved should be wider by 1 column
than the sides of the message box, so that the padded characters will be
entirely restored. However, the saved rectangle may now contain other half
characters; Vio will not let us write cell strings that start or end with
incomplete DBCS characters. There are several solutions to this: the best
choice depends largely on the architecture of your program.
MAKEBOX relies entirely on the Vio API, so that its message box can be used
independent of the LVB format. This is especially important since many OEMs
in Japan provide LVB formats that are not compatible. If you have a
compelling reason to manipulate the LVB directly, you should make your
program more portable by using the worldwide LVB format. Avoiding direct LVB
manipulation makes it possible to use this sample code in a transparent Vio
pop up.
MAKEBOX.C saves an area of the screen, draws a message panel, and then
restores the original screen. To save the area underneath the message box,
use VioReadCellStr on each partial line, extending one column to the left
and right around the width of the message box. (A Vio cell consists of
characters and attributes. The Vio cell calls always handle complete
characters, whether single or double byte. They will never return half of a
double byte character; they will pad the screen with spaces if necessary.)
This may cause you to start by reading the trail byte of a DBCS character or
end by reading a lead byte cell. These orphaned halves of double byte
characters that start or end cell strings must be taken care of later when
you restore the rectangle.
Vio functions are used to draw the message box. Vio will replace any
orphaned halves of DBCS characters on the screen with spaces. When the
underlying rectangle is restored, the original DBCS characters must be
restored as well, but you must not partially overwrite any DBCS characters
already on the screen, lest Vio blank out additional cells. The function
VioCheckCharType is available to inspect any screen location and determine
whether it is a single cell (that is, alphabetic character) or a leading or
trailing half of a DBCS character. This function is called for each line's
boundary cells; if you encounter orphaned double byte characters, call
VioWrtCellStr with the entire saved cell string for that line. Thus, you can
correctly restore the screen contents.
Sample PM Program
As you might suspect, PM controls all handle deadkeys and double byte
characters properly. Therefore, the easiest way to write programs that
handle NLS concerns properly is to use PM controls wherever possible. TYPER
(see Figure 16) is a sample PM program. It allows the user to type in the
client area and has very primitive wordwrapping capability (see Figure 17).
The easiest way to write this application is to create a multiline edit
control (WC_MLE) in the client area and have the client window procedure
route WM_SIZE messages to it. However, to demonstrate proper NLS handling,
it has been modified to run in all OS/2 environments.
In the main routine of a simple PM application, the program will call
WinInitialize, WinCreateMsgQueue, create the main application window, code a
message loop, then clean up. In the main routine of TYPER.C, an
InitLeadByteTable call has been added to initialize the global array
vfIsLeadByte. After the call, this array will contain Boolean values that
tell whether value i is a DBCS lead byte in the current code page.
The other modifications in the main routine have been to load strings such
as the program title from the resource file (using WinLoadString) and to add
the FCF_DBE_APPSTAT flag to the frame window. Note that none of the
modifications made above have any effect when running the U.S. version of
OS/2; the lead byte table would contain FALSE for all entries, the strings
can always be loaded from resources, and FCF_DBE_APPSTAT has no effect in
non-DBCS code pages.
WM_CHAR Processing
The TyperChar subroutine is contained within TYPER.C (see Figure 16). This
call performs WM_CHAR processing, which involves adding characters to a
fixed-length line. The new concerns are deadkey combinations and making sure
that double byte characters are not mangled.
Deadkeys are handled as follows (assuming that deadkey and composite
characters are always single byte):
■ Do not increment the "cursor" (vcchLine).
■ If the next message is a composite character, overwrite the deadkey (it
will do this automatically since you write at cursor location vcchLine).
■ If the next message is an invalid composite and the character is a
single byte space, leave the deadkey character as is. Increment the
character pointer.
■ If the next message is an invalid composite and the character is not a
space, wipe out the deadkey.
There are two major concerns in handling DBCS characters. If there is only
one byte free at the end of the line, you must "wrap" the double byte
characters to the next line. Also, remember that if you backspace or delete
double byte characters, you must delete both bytes.
One nice OS/2 feature is that WM_CHAR will give entire double byte
characters in a DBCS code page. This makes it easy to check if you've been
given a double byte character. If you have, the application will just insert
both bytes into the string.
When deleting the character, since you cannot determine whether the
preceding byte is a trail byte or a single byte character, pass the
beginning of the string and the current offset to WinPrevChar and let it
compute how far you should retreat the character pointer.
WM_QUERYCONVERTPOS Processing
The WM_QUERYCONVERTPOS function is sent by OS/2 to ask where the
Kana-Kanji conversion window is to be placed. If this message is not
processed by the application, the conversion will occur on the status line.
If you choose to process the message, set the fields of the mp1 parameter,
which points to a PRECTL structure. The yBottom and xLeft values you specify
will be used to place the conversion window if QCP_CONVERT is returned. If
QCP_NOCONVERT is returned, KK conversion is not allowed.
In TYPER (see Figure 16), the conversion window is placed at the end of the
bottom text line. It determines the length of the text by calling
WinDrawText with the DT_QUERYEXTENT flag and uses WinQueryWindowRect to
determine the bottom of the client area of the window.
Resource Files
The resource file is essentially equivalent to the message file, in that it
stores all the strings that the application will display (such as prompts,
menu strings, and titles). For these resources, the default code page that
the resource compiler uses is code page 850 (the multilingual code page). If
this default is unacceptable (for example, if you are writing double byte
menu items in code page 932), the resource code pages can be specified by
using the CODEPAGE resource directive or by specifying the -cp flag to
RC.EXE. (This flag is -k in some versions of rc.exe.) In this sample
application, the strings should be translated in order to be read in the
language concerned.
The resource file is also the place that alternate mnemonics may be
specified on the menu items, if you are writing applications for Asian OS/2.
Summary
As you can see, PM programs handle NLS concerns better than Vio-based
programs. The WM_PAINT routine does not usually need to be modified since
clipping does not cause DBCS characters to be bisected. Wordwrapping
behavior already works properly because WinDrawText was used (with
DT_WORDBREAK).
The code modifications that had to be made to TYPER included initializing
the lead byte table, putting strings into the resource file, adding
FCF_DBE_APPSTAT to the frame window, fixing the WM_CHAR processing, and
adding processing for the WM_QUERYCONVERTPOS message. Nontrivial work, but
the result is a program that will run correctly with all versions of OS/2,
where the only additional localization has been restricted to the resource
file.
Figure 3
cpOld = WinQueryProcessCP();
if (!idCodePage)
idCodePage = cpOld;
else {
/*
* The user specified a code page, make sure it is in the
* list of valid code pages, as reported by the base.
* If the base fails to give us any code pages, we will only
* allow code page 850 to pass
*/
if (DosGetCp(sizeof(cp), &cp[0], &cb) || cp[0] = = 0) {
if (idCodePage != 850) {
return FALSE;
}
} else {
/* see if in the list */
for (cb /= 2, i=0; i < cb; i++) {
if (idCodePage = = cp[i])
break; /* found it */
}
if (i = = cb) {
return FALSE; /* did not find it, so return error */
}
}
}
/* Save the new code page in a process private cache, if this process
has only one message queue. */
if (cmqProcess= =1) {
uQueueCP = idCodePage;
fIsDBCS = fIsDBCSCp(idCodePage);
} else {
/* if cmqProcess>1, uQueueCP is invalid */
fIsDBCS |= fIsDBCSCp(idCodePage);
}
fPossibleDBCS |= fIsDBCS;
return TRUE;
Figure 5
In the message file OSO001.MSG, three special messages reveal the characters
to be used for drawing frames. These are most useful in the nongraphic
environment of Vio screens. Following are the message IDs that can be used
with DosGetMessage to obtain the frame characters and the order of the
characters in the message. Sample text shows the message contents for code
page 850.
Characters represented:
single top left corner
single top right corner
single bottom left corner
single bottom right corner
single left vertical
single right vertical
single top horizontal
single bottom horizontal
single left T
single right T
single top T
single bottom T
single cross
Characters represented:
double top left corner
double top right corner
double bottom left corner
double bottom right corner
double left vertical
double right vertical
double top horizontal
double bottom horizontal
double left T
double right T
double top T
double bottom T
double cross
Characters represented:
up arrow
down arrow
left arrow
right arrow
Figure 6
Key Meaning
iCountry Country code
iDate Date format
iCurrency Whether the currency symbol precedes the amount
iDigits Number of digits following decimal point
iTime Whether time is 12 or 24 hour format
iLzero Whether leading zeros should be displayed
s1159 NLS string describing AM
s2359 NLS string describing PM
sCurrency Currency symbol
sThousand Thousands separator
sDecimal Decimal marker
sDate Date separator
sTime Time separator
sList List separator
Figure 7
DosCaseMap Casemaps the characters in a string
DosGetCollate Retrieves a collating sequence table
DosGetCp Retrieves a list of code pages for the process
DosGetCtryInfo Retrieves country-dependent formatting information
DosGetDBCSEv Retrieves the DBCS environment vector
DosGetMessage Retrieves a message
DosSetCp Sets the code page for the current process
DosSetProcCp Sets the code page for a process
GpiQueryCp Retrieves the code page identifier
GpiSetCp Sets the graphics code page identifier
KbdGetCp Retrieves the current code page identifier
KbdOpen Opens a logical keyboard
KbdSetCp Sets a code page identifier for a logical keyboard
KbdSetCustXt Installs a custom translation table
VioCheckCharType Tells if a byte is a single, lead, or trail byte
VioGetCp Retrieves a video code page identifier
VioSetCp Sets a video code page
VioSetFont Sets a video font
WinCompareStrings Compares two strings
WinCpTranslateChar Translates a character between code pages
WinCpTranslateString Translates a string between code pages
WinNextChar Moves to the next character in a string
WinPrevChar Moves to the previous character in a string
WinQueryCp Retrieves the identifier of the queue code page
WinSubstituteStrings Performs a string substitution
PrfQueryProfile Retrieves profile filenames
PrfQueryProfileData Retrieves information from the profile file
PrfQueryProfileInt Retrieves an integer from the profile file
PrfQueryProfileSize Retrieves a key name's size from the program file
PrfQueryProfileString Retrieves a string from the profile file
Special Features:
HK_FINDWORD
FCF_DBE_APPSTAT
MSG_APPL
WM_QUERYCONVERTPOS
Figure 10
ASCII Acronym for American Standard Code for Information Interchange.
This standard defines characters to be associated with 7-bit values.
bisection Refers to the separation of a leading byte from its trailing
byte.
code page (CP) A table that describes the glyphs that correspond to
various byte (or double byte) values.
code point An index into a code page. For example, code point 20H in code
page 850 is the space character.
collation Sorting. One of the things obtainable with DosGetCtryInfo is
the collating sequence, which is a table that tells the sorted order of code
points for a given country code.
country code A structure that stores a country identifier and a code
page. Normally if this structure is passed with two zeroes, the current
process country and code page are used.
DBCS An acronym for double byte character set. Used as an adjective to
describe code pages.
DBCS-enabled A product is called DBCS-enabled if it handles all the
nuances of DBCS string handling properly.
deadkey A keystroke that is not processed but is consumed. This is one
way to enter accented characters at the keyboard; first hit the deadkey
(accent mark), then a character which can combine with it. In WM_CHAR
processing, deadkey handling is easier because KC_DEADKEY, KC_INVALIDCOMP,
and KC_COMPOSITE flags are set.
double byte character Refers to characters that require two bytes for
definition. In Japanese, these correspond mainly to the Kanji characters.
FCF_DBE_APPSTAT A new frame control flag that specifies that a status bar
is to be added to PM frame windows. This flag creates three child windows:
the keyboard status area, the application status line, and an area for
Kana-Kanji conversion. For Japan, the keyboard status area keeps information
about whether the keyboard is in Hiragana, Katakana, or English alphanumeric
mode; whether the shift key is depressed; and whether Kana-Kanji conversion
is active.
glyph A symbolic figure that has a well-defined meaning (such as an
exclamation mark). "a" and "A" would be considered two different glyphs,
although they have nearly the same meaning.
Hiragana Japanese phonetic syllabary. Mixed with Kanji characters to
write all of or parts of Japanese words phonetically.
ideograph Used to describe pictoral characters and words, such as in
Chinese characters or Japanese Kanji. This contrasts with Latin-based words,
which are composed of alphabetic characters.
JIS Japan Industrial Standard. Here used to refer to the various
standardized sets of Japanese Kanji characters. JIS level I contains about
2,000 of the most common characters, ordered by reading. Level II contains
the same characters, ordered by how the characters are written. A third
level has just been defined, which increases the character set. The JIS
standard is based on a 7-bit format, requiring special escape sequences to
enter or leave DBCS mode.
Kana-Kanji conversion The process of converting Hiragana (the Japanese
alphabet of sounds) to Kanji (ideographs). This provides a convenient way
for Japanese users to enter Kanji, namely, by how the Kanji are pronounced.
Kanji The Japanese characters that are ideographs (as opposed to being
part of a syllabary).
Katakana Japanese phonetic syllabary. Japanese keyboards are often
labeled with Katakana to allow typing Japanese words phonetically.
leading byte (lead byte) The first byte of a double byte character.
NLS National Language Support. PM is NLS-enabled, which means that PM
will deal with various international concerns correctly.
padding character Character used to fill out a string. In this context,
the character is used to replace invalid lead and trail bytes.
prepared code page A code page specified on the code page line of the
CONFIG.SYS file. This code page can be used by DOS, Kbd, and Vio calls; up
to two code pages can be prepared at any given time. Note that PM is not
restricted to the prepared code pages, but the file system is (because it
uses DOS calls).
Romanji The Japanese name for Latin characters.
Shift-JIS The name commonly used for the DBCS encoding scheme for the
JIS standard. Shift-JIS implements JIS in 8-bit format, allowing direct
(unescaped) access to the 128 ASCII characters for compatibility.
syllabary A scheme in which symbols represent all possible sounds of word
syllables. This is similar to an alphabet, except the sound units
represented are slightly larger.
trailing byte (trail byte) The second byte of a double byte character.
Figure 11
This figure demonstrates what a Japanese user in code page 932 might see if
DBCS strings are incorrectly handled. Suppose a DBCS string is processed, in
which "S" denotes a single byte character and "Dd" denotes a double byte
character. In the second row byte values are displayed, and in the third row
we see what would appear on a Japanese OS/2 screen.
If the third byte of the string is deleted, the damage is almost
unnoticeable. In this case, the trailing byte happens to be a single byte
character, so the string does not become too badly corrupted. However, a yen
mark (backslash) is now part of the string, and may cause the string to be
handled improperly (especially if it is a filename).
If the fourth byte is deleted instead, the damage is more evident. The
resulting string looks nothing like the original text (although most bytes
are the same!)
Clearly if double byte characters are to be deleted, they should be
completely deleted. Below, the fourth character has been properly deleted.
Figure 12
We strongly recommend that you use the worldwide LVB format for any
application that needs to handle attribute bytes. This LVB format is
supported by OS/2 Version 1.2 worldwide and by Japanese OS/2 from Version
1.1. The format ID is 70H and the attribute count is three (three attribute
bytes per character cell). The layout is as follows:
where C represents character code points
A represents attributes
The character can be a single byte character or one of the two bytes of a
double byte character.
The attributes for the screen are defined as follows:
1st byte
CGA attribute (background and foreground color)
2nd byte
bit 7 underscore
6 reverse video
5 blinking (ignored by AVio)
4 background transparency
(transparent = 1, opaque = 0)
2-3 grid attributes
3 Left Vertical
2 Top Horizontal
0-1 reserved (must be zero) (Font ID for AVio)
3rd byte
bit 7 trailing byte (1 iff trailing byte)
1-6 reserved (must be zero)
0 double byte character
(1 iff part of double byte character)
The attributes in the second byte are ignored where hardware cannot support
them, but are implemented fully in the DBCS AVio.
The third byte is under system control, and must be present when reading or
writing cells or cell strings, but application changes are ignored.
Figure 14
In the windows below, the keyboard status area is below the English words.
The Hiragana characters in the first window indicate that the keyboard is in
English single byte alphanumeric mode.
In the second window, the user has enabled Kana-Kanji conversion. The user
types the Hiragana string shown inside the black conversion box.
Then the typist hits the convert key. The Hiragana is converted to Kanji.
Since these Kanji characters are acceptable, the user hits enter and the
characters are sent to the application.
Figure 15
MAKEBOX
#======================= MAKEBOX ================================
#
# make file for VIO DBCS SAMPLE
#
# Assumes: MAKEBOX.C
# MAKEBOX.H
# SLIBCE.LIB
# SLICEP.LIB
# DOSCALLS.LIB
# plus OS/2 1.2 include files
#
# Generates:MAKEBOX.LNK
# MAKEBOX.OBJ
# MAKEBOX.EXE
# MAKEBOX.MAP
# MAKEBOX.SYM
#
#================================================================
#
# Default compilation macros
#
CC = cl -W2 -c -AS -G2sw -Od -Zpe
ASM = masm
MKMSG = mkmsgf
#================================================================
#
# Default inference rules
#
#================================================================
.c.obj:
$(CC) $*.c
.asm.obj:
$(ASM) $*.asm;
.txt.msg:
$(MKMSG) $*.txt $*.msg
#==============================================================
#
# A list of all of the object files
#
#==============================================================
ALL.OBJ = makebox.obj
#==============================================================
#
# Dependencies
#
#==============================================================
makebox.lnk: makebox
echo $(ALL.OBJ) >> makebox.lnk
echo makebox.exe >> makebox.lnk
echo makebox.map/map >> makebox.lnk
echo /nod:slibce slibcep doscalls >> makebox.lnk
echo NUL.DEF >> makebox.lnk
makebox.obj: makebox.c
makebox.msg: makebox.txt
makebox.exe: $(ALL.OBJ) makebox.lnk
link @makebox.lnk
mapsym makebox
MAKEBOX.H
/********************************* MAKEBOX.H ******************\
*
* HEADER FILE for VIO DBCS SAMPLE program
*
\**************************************************************/
#define CBMAXDBCS 10 // Max size of DosGetDBCSEv buffer
#define VIOHANDLE 0 // default handle for full screen VIO
extern int main(SHORT argc,char * *argv);
extern VOID MakeBox( USHORT yTop,USHORT xLeft, USHORT yBottom,
USHORT xRight );
extern VOID SaveArea(USHORT yTop,USHORT xLeft, USHORT yBottom,
USHORT xRight, BOOL fSave);
extern VOID ParseFrameMsg(PBYTE pchMsg, USHORT cbMsg);
/******************** for OS2 1.2 US ********************
* These defines are needed to run under 1.2 US
*********************************************************/
typedef struct t_VIOSCROLL { /* vioscroll */
USHORT cb;
USHORT type;
USHORT cnscrl;
} VIOSCROLL, FAR * PVIOSCROLL;
#define VS_GETSCROLL 6
#ifndef MSG_APPL_SINGLEFRAMECHAR
#define MSG_APPL_SINGLEFRAMECHAR 130
#define MSG_APPL_DOUBLEFRAMECHAR 131
#define MSG_APPL_ARROWCHAR 132
#endif
#ifndef VCC_SBCSCHAR
#define VCC_SBCSCHAR 0 // Cell contains SBCS character
#define VCC_DBCSFULLCHAR 1 // Cell contains Full DBCS character
#define VCC_DBCS1STHALF 2 // Cell contains leading byte of DBCS
#define VCC_DBCS2NDHALF 3 // Cell contains trailing byte of DBCS
USHORT APIENTRY VioCheckCharType (PUSHORT pType, USHORT usRow,
USHORT usColumn, HVIO hvio);
#endif
MAKEBOX.LNK
makebox.obj
makebox.exe
makebox.map/map
/nod:slibce slibcep doscalls
NUL.DEF
MAKEBOX.TXT
MKB
MKB0000I: Command Line Arguments to %1: %2 %3 %4
MAKEBOX.C
/********************************* MAKEBOX ************************\
*
* VIO DBCS SAMPLE program
*
* This program puts up panel containing a message into which
* the first 3 command line arguments have been substituted.
*
* This program will run on any OS/2 1.2 version.
* However, most of the DBCS machinery in it lies unused if run
* in a non-DBCS code page or on non-DBCS hardware.
*
* It is therefore typical of an 'international application' which
* also contains that bit of extra code to allow it to run
* everywhere.
*
* This program works so it can be run as a second process in a
* screen group, i.e. it adapts to the VIO mode, VIO code page etc
* that were set up by the first process. Alternatively this code
* would work well in a DLL.
*
\******************************************************************/
#define INCL_VIO
#define INCL_DOSNLS
#define INCL_DOSMISC
#define INCL_DOSPROCESS
#include <os2.h>
#include <ctype.h>
#include <stdio.h>
#include "makebox.h"
BYTE fbLead[256]; // lead byte table, initially all 0
// (i.e. FALSE or not a lead byte)
#define fIsDbcsLead( x ) (fbLead[(x)])
#define CBMAXMSG 200 // upper bound for message text
/** MAIN:
**
** Construct a message panel and display
** the arguments in a message text
**/
main( argc, argv )
SHORT argc;
CHAR ** argv;
{
USHORT ib, ich, iLo, iHi;
USHORT usCodePage; // code-page identifier
USHORT usReserved=0; // must be zero
USHORT cbBuf; // length of buffer
COUNTRYCODE ctryc; // structure for country code
CHAR abDbcsRange[CBMAXDBCS]; // buffer for DBCS info
CHAR achMsgBuf[CBMAXMSG]; // return buffer for Text
USHORT cbMsg; // # of message bytes returned
PCHAR apchVTable[10]; // pointer to table of pointers
// pointers to strings
SHORT iTmp;
USHORT xLeft, // Message box coordinates
xRight,
yTop,
yBottom;
VIOSCROLL vioscroll; // VioGetState scroll info block
VIOMODEINFO viomi; // Video mode information
// First, determine where to locate the message box.
// Get the current Vio mode to find the number of lines on the
// screen.
viomi.cb = sizeof(VIOMODEINFO);
VioGetMode(&viomi, 0);
// note: bottom screen line is used for keybd status in DCBS
// countries so we use VioGetState to return the size of this
// (nonscrollable) area
vioscroll.cb = sizeof(vioscroll);
vioscroll.type = VS_GETSCROLL;
VioGetState((PVIOSCROLL) &vioscroll, VIOHANDLE);
// Set Message box coordinates to stay inside screen area
xLeft = 10,
xRight = viomi.col - 9,
yTop = 5,
yBottom = viomi.row - vioscroll.cnscrl - 5;
// Get Current VIO Code Page
VioGetCp(0, // must be zero
&usCodePage, // code-page identifier
VIOHANDLE); // video handle
// Get Lead Byte range (if any) for current VIO code page
// (for SBCS code page abDbcsRange is all 0)
ctryc.country = 0;
ctryc.codepage = usCodePage;
DosGetDBCSEv(cbBuf, (PCOUNTRYCODE) &ctryc, (PCHAR) abDbcsRange);
// Construct lead byte table for quick parsing of DBCS strings
for (ib=0; ib < cbBuf && abDbcsRange[ib]; ib++){
if( ib % 1 ){
for( ich = iLo; ich <= abDbcsRange[ib]; ich++ )
fbLead[ich] = 1;
} else
iLo = abDbcsRange[ib];
}
// Synch Process and VIO code page, so DosGetMessage retrieves
// characters that VIO can display.
DosSetProcCp(usCodePage, usReserved);
// Save screen area to be obscured by message box.
SaveArea(yTop, xLeft, yBottom, xRight, TRUE);
// Retrieve frame characters
DosGetMessage(
(PCHAR FAR *) NULL, // pointer to table of ptrs to strings
0, // number of pointers in table
(PBYTE) achMsgBuf, // buffer receiving message
CBMAXMSG, // size of message buffer
MSG_APPL_SINGLEFRAMECHAR,// message number to retrieve
"oso001.msg", // name of file containing message
(PUSHORT) &cbMsg); // number of bytes in returned message
// Parse the frame characters message and draw a box
ParseFrameMsg( achMsgBuf, cbMsg);
MakeBox(yTop, xLeft, yBottom, xRight);
// Retrieve message text; merge w/ first few arguments from argv
argc = (argc > 4 ? 4 : argc );
for ( iTmp = 0; iTmp < argc; iTmp++) {
apchVTable[iTmp] = (PCHAR) argv[iTmp];
}
DosGetMessage(
apchVTable, // pointer to table of pointers to strings
argc, // number of pointers in table
(PCHAR) achMsgBuf, // buffer receiving message
CBMAXMSG, // size of message buffer
0, // message number to retrieve
"makebox.msg", // name of file containing message
(PUSHORT) &cbMsg); // number of bytes in returned message
// Set Cursor to beginning of message text
VioSetCurPos(yTop + 2, // cursor row
xLeft + 2, // cursor column
VIOHANDLE); // video handle
// Output message text
VioWrtTTY(achMsgBuf, cbMsg, VIOHANDLE);
/**
** User interaction here
■
■
■
**/
DosSleep( 2000L );
// Restore screen from saved area
SaveArea(yTop, xLeft, yBottom, xRight, FALSE);
return 0;
}
// The boxes defined here have black on red borders and a white
// interior. The attributes are ULONG so they work correctly
// with the world-wide LVB format's 3 byte attributes.
//
USHORT ausBoxChar[3][3]; // Skeleton for a box frame
static ULONG aulAttr[][3] = // Corresponding char attribute vals
{ {0x40, 0x40, 0x40},
{0x40, 0x70, 0x40},
{0x40, 0x40, 0x40}
};
/** PARSEFRAMEMSG:
**
** Collect the frame characters from the message and place them
** into a skeleton 3x3 array for the box.
**/
VOID ParseFrameMsg( pchMsg, cbMsg )
PCH pchMsg;
USHORT cbMsg;
{
sscanf( (char *)pchMsg, "%x %x %x %x %x %x %x %x",
&ausBoxChar[0][0],
&ausBoxChar[0][2],
&ausBoxChar[2][0],
&ausBoxChar[2][2],
&ausBoxChar[1][0],
&ausBoxChar[1][2],
&ausBoxChar[2][1],
&ausBoxChar[0][1]
);
ausBoxChar[1][1] = 0x20;
}
/** MAKEBOX:
**
** Expand Box skeleton to draw box of the desired size. Uses VIO
** calls exclusively in order to be portable to different H/W.
**/
VOID MakeBox(yTop, xLeft, yBottom, xRight)
USHORT xLeft, // Box coordinates
xRight,
yTop, // not inclusive
yBottom;
{
static int iTimes[] = {1, 1, 1};
int i1, i2, xTemp;
USHORT usChar;
iTimes[1] = xRight - xLeft;
xTemp = xLeft;
// The nested loops below repeatedly draw the center row and
// center column of the box skeleton, thereby expanding the
// box to the desired size.
for (i1 = 0; i1 < 3; i1++)
do {
for (i2 = 0; i2 < 3; i2++) {
// Write Frame Character
usChar = ausBoxChar[i1][i2];
VioWrtNChar((PCH)&usChar, iTimes[i2],yTop, xLeft, VIOHANDLE);
// Write corresponding attribute
// (need to write twice for DBCS frame chars)
VioWrtNAttr((PBYTE)&aulAttr[i1][i2], iTimes[i2],
yTop, xLeft, VIOHANDLE);
if( fIsDbcsLead( usChar & 0xFF)){
VioWrtNAttr((PBYTE)&aulAttr[i1][i2], iTimes[i2],
yTop, xLeft, VIOHANDLE);
}
xLeft + = iTimes[i2];
}
xLeft = xTemp;
yTop++;
} while ((i1 = = 1) && (yTop < yBottom));
} /* end MakeBox */
/** SAVEAREA:
**
** This function saves and restores the area of the screen that
** will be obscured by our message panel. Care is taken that any
** DBCS chars which might be straddling the edges of the rectangle
** are saved and restored correctly.
**
** This problem may exist if the rectangle to be saved doesn't
** extend to the screen edge. It is handled by saving an extra
** cell on each side. The saved buffer will contain the full DBCS
** character for any DBCS characters that were straddling the
** initial rectangle.
**
** Before restoring we need to know which of the extra cells
** contain such halves of DBCS characters necessary for completion.
** VioCheckCharType is used to check the cell in the video buffer.
**/
VOID SaveArea(yTop, xLeft, yBottom, xRight, fSave)
USHORT xLeft, // area coordinates
xRight,
yTop, // not inclusive
yBottom;
BOOL fSave;
{
static SEL selArea; // Selector for saved segment
static PBYTE pbArea; // Pointer to saved screen area
PBYTE pbLine; // Pointer to line being drawn
SHORT cbmax, cbCur, xCur, cbCell, cbArea;
VIOMODEINFO viomi; // Video mode information
USHORT usType; // Cell type
//Get the current Vio mode to find out whether we have 1 or 3
//attribute bytes per cell. Set the size of the saved area
//accordingly;
viomi.cb = sizeof(VIOMODEINFO);
VioGetMode(&viomi, 0);
cbCell = viomi.attrib+1;
// Calculate maximum number of bytes per line to be saved
cbmax = (xRight - xLeft + 3) * cbCell;
// Allocate buffer to hold saved area
if( fSave ){
cbArea = (yBottom - yTop + 1) * cbmax;
DosAllocSeg(cbArea, //bytes to allocate
&selArea, //address of selector
SEG_NONSHARED); //sharing flag
}
pbArea = MAKEP(selArea, 0); //convert selector to pointer
do {
// Number of bytes per line to be saved and
// starting offset for this line
cbCur = cbmax;
pbLine = pbArea + (yBottom - yTop) * cbmax;
// Extend area to be saved by 1 col Left & Right
// (unless we are already at the edge of the screen)
if(xLeft )
xCur = xLeft-1;
else
xCur=xLeft, cbCur-= cbCell;
if ( xRight = = viomi.col )
cbCur-= cbCell;
if( fSave ){
// Save one line
VioReadCellStr(pbLine,(PUSHORT) &cbCur,yTop,xCur,VIOHANDLE);
} else {
// Inspect the byte left of the obscured area:
// If it's a trail byte, our saved string
// begins with this trail byte which is illegal.
// Advance our string pointer and start column by one.
if(xLeft > xCur){
VioCheckCharType( (PUSHORT) &usType, yTop, xCur,
VIOHANDLE);
if( usType = = VCC_DBCS2NDHALF ) {
// skip trail byte on write
xCur = xLeft;
cbCur -= cbCell;
pbLine + = cbCell;
}
}
// As above, we cannot end a write on a lead byte.
if ( xRight < viomi.col ){
VioCheckCharType( (PUSHORT) &usType, yTop, xRight+1,
VIOHANDLE);
if( usType = = VCC_DBCS1STHALF )
// don't end write on lead byte
cbCur-= cbCell;
}
// Restore one line
VioWrtCellStr(pbLine, cbCur, yTop, xCur, VIOHANDLE);
}
} while (++yTop <= yBottom);
// Free memory after restore
if( !fSave ){
DosFreeSeg( selArea );
}
} /* end SaveArea */
Figure 16
TYPER
CC = cl -c -AS -G2sw -Od -W3 -Zpei
LF = /align:16 /codeview /map /NOD
.c.obj:
$(CC) $*.c
.rc.res:
rc -r $*.rc
typer.obj: typer.c typer.h
typer.res: typer.rc typer.h
typer.exe: typer.obj typer.res typer.def typer.lnk
link $(LF) typer, , , os2 slibcep, typer.def
rc typer.res
mapsym typer
TYPER.RC
#include <os2.h>
#include "typer.h"
STRINGTABLE {
IDS_NAME, "Typer"
IDS_TITLE, " - Simple Typewriter"
}
MENU IDR_TYPER {
SUBMENU "~File", IDM_FILE {
MENUITEM "A~bout...", IDM_ABOUT, MIS_TEXT
MENUITEM SEPARATOR
MENUITEM "E~xit", SC_CLOSE, MIS_SYSCOMMAND
}
}
DLGTEMPLATE IDD_ABOUT {
DIALOG "", 0, 10, 60, 160, 75, WS_SAVEBITS | FS_DLGBORDER {
CTEXT "OS/2 Presentation Manager" -1, 10, 60, 140, 10
CTEXT "Typing Sample" -1, 30, 50, 100, 10
CTEXT "Version 1.0" -1, 10, 30, 140, 10
CTEXT "Created by Microsoft Corp., 1990" -1, 10, 20, 140, 10
DEFPUSHBUTTON "~Ok" DID_OK, 64, 4, 32, 14, WS_GROUP
}
}
TYPER.DEF
NAME Typer WINDOWAPI
DESCRIPTION 'Microsoft OS/2 PM Simple Virtual Typewriter'
STUB 'OS2STUB.EXE'
CODE MOVEABLE
DATA MOVEABLE MULTIPLE
HEAPSIZE 8192
STACKSIZE 8192
TYPER.LNK
typer
typer/align:16
typer/map
os2 slibcp/NOD
typer.def
TYPER.H
// Resource ids
//
#define IDD_ABOUT 1 // About... dialog id
#define IDM_FILE 1 // File menu id
#define IDM_ABOUT 0x10 // About... menu item id
#define IDR_TYPER 1 // Main resource ID
#define IDS_NAME 1 // String ID for window class name
#define IDS_TITLE 2 // String ID for title bar text
// Useful constants
//
#define MAX_LINES 35 // Must be larger than window height in chars.
#define MAX_LINE_LENGTH 255 // Should be larger than
// window width in chars.
#define MAX_STRING 21 // Max length of strings to be loaded
#define TABLE_SIZE 12 // Current max is 5 lead byte pairs
// "Exported" procedure declarations
//
MRESULT EXPENTRY AboutDlgProc(HWND, USHORT, MPARAM, MPARAM);
MRESULT EXPENTRY TyperWndProc(HWND, USHORT, MPARAM, MPARAM);
TYPER.C
/*
TYPER.C -- based on the sample of the same name.
This program correctly handles DBCS and also demonstrates word
wrapping. It uses a circular buffer to store lines.
Limitations: This program does not recompute word breaks on
WM_SIZE messages.
*/
#define INCL_DOSNLS
#define INCL_PM
#define INCL_NLS
#include <os2.h>
#include <string.h>
#include "typer.h"
// Variable declarations
//
UCHAR vaszBuffer[MAX_LINES][MAX_LINE_LENGTH + 1];
// Circular queue of lines
USHORT vcchLine = 0; // Number of characters in current line
SHORT vcpelyChar = 0; // Character height
BOOL vfIsLeadByte[256]; // Is Byte i a DBCS lead byte?
HAB vhab; // Handle to the Anchor Block
HWND vhwndTyper; // Handle to the Client Area
HWND vhwndTyperFrame; // Handle to the Frame Window
USHORT vusCurrent = 0; // Current vaszBuffer line referenced
// Useful macros
//
#define LOADSTRING(id, sz) \
WinLoadString(vhab, NULL, id, sizeof(sz), (PCH) sz)
// Internal declarations
//
VOID main(VOID); // Main routine
VOID InitLeadByteTable(VOID); // Initialize lead byte table
VOID TyperChar(HWND, MPARAM, MPARAM);// WM_CHAR processing subrtn
VOID TyperCreate(HWND); // WM_CREATE processing subrtn
VOID TyperPaint(HWND, HPS, PRECTL); // WM_PAINT processing subrtn
VOID main(VOID) {
HMQ hmqTyper;
QMSG qmsg;
UCHAR szClassName[MAX_STRING];
UCHAR szWindowTitle[MAX_STRING];
ULONG ctlData;
// Standard initialization for a PM application
//
vhab = WinInitialize(NULL);
hmqTyper = WinCreateMsgQueue(vhab, 0);
// Register the Typer window class -- if not successful, exit.
//
LOADSTRING(IDS_NAME, szClassName);
if (!WinRegisterClass(vhab, (PCH) szClassName,
(PFNWP)TyperWndProc, CS_SIZEREDRAW, 0))
DosExit(EXIT_PROCESS, 0);
// Get the title of the application, and create the main window.
// Notice that FCF_DBE_APPSTAT is included because the client area
// will allow Kanji characters to be input.
//
LOADSTRING(IDS_TITLE, szWindowTitle);
ctlData = FCF_STANDARD & ~(FCF_ICON | FCF_ACCELTABLE) |
FCF_DBE_APPSTAT;
vhwndTyperFrame = WinCreateStdWindow(HWND_DESKTOP, WS_VISIBLE,
&ctlData, (PCH)szClassName,
(PCH)szWindowTitle, 0L,
NULL, IDR_TYPER,
(PHWND)&vhwndTyper);
// Initialize the lead byte table
//
InitLeadByteTable();
// Poll messages from event queue
//
while (WinGetMsg(vhab, &qmsg, NULL, 0, 0))
WinDispatchMsg(vhab, &qmsg);
// Standard cleanup code for PM applications
//
WinDestroyWindow(vhwndTyperFrame);
WinDestroyMsgQueue(hmqTyper);
WinTerminate(vhab);
DosExit(EXIT_PROCESS, 1);
}
MRESULT EXPENTRY TyperWndProc(HWND hwnd, USHORT usMsg, MPARAM mp1,
MPARAM mp2) {
/*
Note: This scrolling behavior may not be extensible if the chars
are to be output from top to bottom, right to left, as can be
seen in traditional Chinese and Japanese scripts.
*/
HPS hps;
RECTL rclPaint;
RECTL rclWindow;
switch (usMsg) {
case WM_CREATE:
TyperCreate(hwnd);
break;
case WM_CLOSE:
WinPostMsg(hwnd, WM_QUIT, 0L, 0L);
break;
case WM_ERASEBACKGROUND:
// Erase the background if this message is received
//
return TRUE;
break;
case WM_CHAR:
if (!(SHORT1FROMMP(mp1) & KC_KEYUP)) {
// Process the downstrokes entered.
//
TyperChar(hwnd, mp1, mp2);
// Invalidate the updated line.
//
WinQueryWindowRect(hwnd, &rclWindow);
rclWindow.yTop = vcpelyChar - 1;
WinInvalidateRect(hwnd, &rclWindow, TRUE);
WinPostMsg(hwnd, WM_PAINT, 0L, 0L);
}
break;
case WM_QUERYCONVERTPOS:
WinQueryWindowRect(hwnd, &rclWindow);
hps = WinGetPS(hwnd);
WinDrawText(hps, 0xFFFF, vaszBuffer[vusCurrent],
&rclWindow, 0L, 0L, DT_LEFT | DT_BOTTOM | DT_QUERYEXTENT);
WinReleasePS(hps);
((PRECTL) mp1)->xLeft = rclWindow.xRight;
((PRECTL) mp1)->yBottom = rclWindow.yBottom;
return QCP_CONVERT;
break;
case WM_PAINT:
// Repaint the invalid region
// Side effect: May scroll up one line as needed.
//
hps = WinBeginPaint(hwnd, NULL, &rclPaint);
TyperPaint(hwnd, hps, &rclPaint);
WinEndPaint(hps);
break;
case WM_COMMAND:
switch (COMMANDMSG(&usMsg)->cmd) {
// Trap the About... menu item, and put up the dialog box
//
case IDM_ABOUT:
WinDlgBox(HWND_DESKTOP, hwnd, AboutDlgProc,
NULL, IDD_ABOUT, NULL);
break;
default: break;
}
break;
default:
return WinDefWindowProc(hwnd, usMsg, mp1, mp2);
break;
}
return 0L;
}
MRESULT EXPENTRY AboutDlgProc(HWND hwndDlg,USHORT usMsg,MPARAM mp1,
MPARAM mp2) {
/*
About... dialog procedure
*/
switch(usMsg) {
case WM_COMMAND:
switch(COMMANDMSG(&usMsg)->cmd) {
case DID_OK: WinDismissDlg(hwndDlg, TRUE);
default: break;
}
default: return WinDefDlgProc(hwndDlg, usMsg, mp1, mp2);
}
return FALSE;
}
VOID TyperChar(HWND hwnd, MPARAM mp1, MPARAM mp2) {
/*
This routine does simple input processing.
Assumptions: There are no DBCS deadkeys, or invalid composites.
This also implies that a deadkey followed by a DBCS space results in
a DBCS space (without WinAlarm() being called).
*/
BOOL fDone;
PUCHAR pszLast;
UCHAR ch;
USHORT fs;
USHORT vkey;
ch = (UCHAR) CHAR1FROMMP(mp2);
fs = (USHORT) SHORT1FROMMP(mp1);
vkey = (USHORT) SHORT2FROMMP(mp2);
fDone = FALSE;
if (fs & KC_VIRTUALKEY) {
// Receiving a backspace...
//
if ((fDone = (vkey = = VK_BACKSPACE || vkey = = VK_DELETE))
&& vcchLine > 0) {
pszLast = WinPrevChar(vhab, 0, 0,
&vaszBuffer[vusCurrent][0],
&vaszBuffer[vusCurrent][vcchLine]);
// If DBCS, zero out the trailing byte.
//
if (vfIsLeadByte[*pszLast]) {
vaszBuffer[vusCurrent][--vcchLine] = '\0';
}
// Zero out one character (SBCS, or DBCS lead byte)
//
vaszBuffer[vusCurrent][--vcchLine] = '\0';
// Receiving a carriage return (CR)...
//
} else if (vkey == VK_NEWLINE || vkey = = VK_ENTER) {
fDone = TRUE;
// Save the line in the vaszBuffer
//
vusCurrent = (vusCurrent + 1) % MAX_LINES;
// Initialize the next text line
//
vcchLine = 0; vaszBuffer[vusCurrent][0] = '\0';
// Scroll window upwards, for efficient updating.
//
WinScrollWindow(hwnd,0,vcpelyChar,NULL,NULL,NULL,NULL,
SW_INVALIDATERGN);
}
}
// If we haven't encountered VK_BACKSPACE, VK_DELETE, VK_NEWLINE, or
// VK_ENTER, process KC_CHAR values. We do this because some valid
// characters are also valid vkeys (like Space).
//
if (!fDone) {
// If DBCS character fits, add it.
//
if (fs & KC_CHAR) {
if (vfIsLeadByte[ch] && (vcchLine + 2 < MAX_LINE_LENGTH)) {
// Add DBCS to line array
//
vaszBuffer[vusCurrent][vcchLine++] = ch;
vaszBuffer[vusCurrent][vcchLine++] = CHAR2FROMMP(mp2);
vaszBuffer[vusCurrent][vcchLine] = '\0';
// If SBCS character fits, add it.
//
} else if (vcchLine + 1 < MAX_LINE_LENGTH) {
if (fs & KC_INVALIDCOMP) {
// If we have a space, advance over the current character.
//
if (ch = = ' ')
vcchLine++;
// Otherwise, complain audibly.
//
else
WinAlarm(HWND_DESKTOP, WA_WARNING);
} else {
// Add character to line array
//
vaszBuffer[vusCurrent][vcchLine++] = ch;
}
vaszBuffer[vusCurrent][vcchLine] = '\0';
// If it's a deadkey, reposition so that we'll overwrite
// the deadkey on KC_INVALIDCOMP or KC_COMPOSITE.
//
if (fs & KC_DEADKEY)
vcchLine--;
}
}
}
}
VOID TyperCreate(HWND hwnd) {
HPS hps;
FONTMETRICS fmTyper;
USHORT i;
// Initialize text buffer
//
for (i = 0; i < MAX_LINES; i++)
vaszBuffer[i][0] = '\0';
// Get the character height
//
hps = WinGetPS(hwnd);
GpiQueryFontMetrics(hps, (LONG) sizeof(FONTMETRICS), &fmTyper);
vcpelyChar = (SHORT) fmTyper.lMaxBaselineExt + 1;
WinReleasePS(hps);
}
VOID TyperPaint(HWND hwnd, HPS hps, PRECTL prclUpdate) {
USHORT usUpdateTop;
USHORT usUpdateBottom;
RECTL rclArea;
RECTL rclWindow;
UCHAR *pszTmp;
USHORT cchDrawn;
USHORT cchTmp;
USHORT usWhich;
USHORT usUpdate;
SHORT sNew = -1;
// Compute the lines to be updated.
// NOTE: This assumes that screen coordinates start with y = 0.
//
usUpdateTop = (USHORT) (prclUpdate->yTop / vcpelyChar);
usUpdateBottom = (USHORT) (prclUpdate->yBottom / vcpelyChar);
// The following code is functionally equivalent to
// usWhich = (vusCurrent - usUpdateBottom) % MAX_LINES;
//
sNew = (vusCurrent - usUpdateBottom);
usWhich = ((sNew >= 0) ?
sNew % MAX_LINES :
MAX_LINES - (-sNew % MAX_LINES));
// Initialize the width of the text line
//
WinQueryWindowRect(hwnd, &rclWindow);
rclArea.xLeft = rclWindow.xLeft;
rclArea.xRight = rclWindow.xRight;
// For all the relevant lines...
//
for (usUpdate = usUpdateBottom;
usUpdate <= usUpdateTop;
usUpdate++) {
// Initialize the height of the text line
//
rclArea.yBottom = vcpelyChar * usUpdate;
rclArea.yTop = rclArea.yBottom + (vcpelyChar - 1);
// Load the line to be printed
//
pszTmp = vaszBuffer[usWhich];
cchTmp = strlen(pszTmp);
// Always draw in a word wrapped fashion
//
if (cchTmp) {
cchDrawn = WinDrawText(hps, cchTmp, pszTmp, &rclArea,
CLR_BLACK, CLR_BACKGROUND,
DT_ERASERECT | DT_LEFT | DT_TOP | DT_WORDBREAK);
if (usWhich = = vusCurrent) {
while (cchDrawn && cchDrawn < cchTmp) {
// Fix up code, line's longer than expected!
// We must perform additional word wrapping.
//
// Add a new line to the line buffer (this
// code assumes that we must be on the last
// line to wrap).
//
sNew = (vusCurrent + 1) % MAX_LINES;
vaszBuffer[sNew][0] = '\0';
strcpy(vaszBuffer[sNew], vaszBuffer[vusCurrent] + cchDrawn);
// Adjust old line to reflect character deletion
//
vaszBuffer[vusCurrent][cchDrawn] = '\0';
// Set up new parameters for TyperChar()
//
vcchLine -= cchDrawn;
vusCurrent = sNew;
// Scroll the window, then fill in the invalid region
//
WinScrollWindow(hwnd, 0, vcpelyChar,
NULL, NULL, NULL, &rclArea, 0);
pszTmp = vaszBuffer[vusCurrent];
cchTmp -= cchDrawn;
cchDrawn = WinDrawText(hps, cchTmp, pszTmp, &rclArea,
CLR_BLACK, CLR_BACKGROUND,
DT_ERASERECT | DT_LEFT | DT_TOP | DT_WORDBREAK);
// We increment the update pointer because there's one
// fewer line to update.
usUpdate++;
}
}
} else {
// Clear the rectangle
//
WinFillRect(hps, &rclArea, CLR_BACKGROUND);
}
// Point to the next line to be drawn
// This is functionally equivalent to
// usWhich = (usWhich - 1) % MAX_LINES;
//
usWhich = (usWhich ? (usWhich - 1) : MAX_LINES - 1);
}
}
VOID InitLeadByteTable(VOID) {
/*
This routine initializes the array which tells if index "i" is
a valid leading byte in the current codepage.
*/
COUNTRYCODE ctryc; // Used with DosGetDBCSEv() call
UCHAR vachDBCSEv[TABLE_SIZE]; // Lead Byte range table
SHORT i, j; // Temporary variables
// Initialize the array
//
for (i = 0; i <= 0xFF; i++)
vfIsLeadByte[i] = FALSE;
// Get the valid lead byte ranges
// Use (country, codepage) = (0, 0) for current process settings
//
ctryc.country = ctryc.codepage = 0;
DosGetDBCSEv(TABLE_SIZE, &ctryc, vachDBCSEv);
// Fill in the array, "blacking out" all the returned ranges
//
while (vachDBCSEv[j] && vachDBCSEv[j + 1]) {
for (i = vachDBCSEv[j]; i <= vachDBCSEv[j + 1]; i++)
vfIsLeadByte[i] = TRUE;
j + = 2;
}
}
Common Questions and Answers
Why aren't code pages sorted in glyph order?
The data would be misrepresented upon code page switch. It was decided that
it would be better to force the 00H to 7FH range to be mainly ASCII, and
have the code pages differ in the 80H to FEH range.
What is the code point for the DBCS space character?
Any DBCS code page has two code points for space characters. In code page
932, they are 20H, the single byte space, and 8140H, the double byte space.
The double byte space covers the area of two single byte spaces on a Vio
screen.
How does DBCS affect filenames?
On a FAT file system a file can have an 8-byte filename and a 3-byte
extension. In DBCS, the only rule is that the filename and extension must be
legal DBCS strings (they do not start with a trailing byte or end with a
leading byte), so the filename can contain up to four double byte
characters, with up to one double byte character in the extension. For file
systems with long name support, the 254 character limit translates into a
254 byte limit; again, with the provision that the filename is a legal DBCS
string. It is recommended, however, that filenames be in ASCII range from
20H to 7FH if it is critical to interchange files worldwide.
Are the C run-time library calls DBCS-enabled?
No, they aren't, at least not the U.S. editions. The functions strlen and
strcpy will work with NULL-terminated strings, because NULL isn't a valid
trailing byte, but most of the other string functions might not (such as
scanf). The Japanese run-time library calls will work properly with code
page 932, but not with other DBCS code pages.
What is the maximum number of disjoint lead byte ranges in a code page?
Currently, the maximum number of ranges returned by the system is five.
How should sorting be handled in DBCS code pages?
The DosGetCollate function will return a table of sorted single byte and
lead byte characters, so double byte characters should be sorted according
to the value of their lead bytes. If two double byte characters have the
same lead byte, sort them by their trail bytes (the double byte ranges are
in sorted order). For code page 932, the collating sequence is English, then
Kana, then double byte characters.
Building an Extensible Clipboard Viewer Through the Use of Dynamic-Link
Libraries
Kevin P. Welch
VIEWER is an application for the Microsoft Windows environment that
demonstrates advanced clipboard handling techniques. Similar in many ways to
CLIPBRD, VIEWER (see Figure 1) functions as an extensible viewer for the
display and manipulation of clipboard data. Like CLIPBRD, VIEWER allows you
to view the current clipboard contents. Unlike CLIPBRD, VIEWER allows you to
choose from among all available data formats (see Figure 2), and extend its
viewing capabilities through the use of dynamically linked display
libraries. With this capability you can interactively add or remove
libraries from the viewer, and support the display and manipulation of
private or predefined clipboard formats. In addition, these libraries can
provide other information associated with the selected format, such as full
name, owner, object dimensions, and size.
When you first bring up the VIEWER in Windows it will be incapable of
displaying clipboard data. You will, however, be able to tell which formats
the clipboard itself has available at any given moment by using the status
window and the Display pull-down menu.
To display clipboard data you must install a display library. BITMAP.DLL and
OWNER.DLL, two such libraries, are provided with the VIEWER. If you copy
both libraries to your Windows directory you can install them into the
VIEWER.
You should install the BITMAP.DLL library for both the Bitmap and Display
Bitmap formats and the OWNER.DLL library for Owner Display. Although most
programs work with many more than these three formats, you will be able to
see some of the possibilities the VIEWER creates.
These display libraries are installed into the VIEWER using the Add
Format... command under the Options menu. When selected, the dialog box
shown in Figure 3 is displayed. It enables you to enter the name of the
dynamic library you wish to install and the corresponding clipboard format
for which it will be responsible.
When you enter a clipboard format, be careful to type the exact characters
that make up the name, including their case. If you wish, you can reference
one of the predefined clipboard formats by entering a name exactly from the
table in Figure 4. VIEWER will consider the entry of a name not on this
table as user-defined and will automatically register this name as a new
clipboard format.
The library field should contain the full name and path of the dynamically
linked display library that will support the clipboard format. In its
current state, the program is incapable of detecting invalid libraries, so
be sure to specify the correct library.
When you press the Add button the system will automatically load the dynamic
library and update WIN.INI to reference the new library. VIEWER's client
area will also immediately reflect the changes. If the newly supported
format is selected, the data will be displayed in the client area. Note that
you can use the same library to support multiple clipboard formats. This
allows you, for example, to use the same generalized text handling library
for the support of the CF_TEXT, CF_DIF, CF_SYLK, and perhaps even PostScript
formats.
You can also replace an existing display library by installing a new one
using the same clipboard format name. VIEWER will inform you that the format
is currently supported and ask if you wish to replace the installed library.
As is the case when installing a new display library, when you replace an
existing one the window's client area will automatically reflect any changes
made.
You can remove a display library from the application at any time. This is
done with the Remove Format command under the Options menu. When selected,
the dialog box shown in Figure 5 will be displayed. It lists all supported
clipboard formats and their associated dynamic display libraries.
Both the Add and Remove Format commands manipulate a section of WIN.INI (the
Windows initialization and configuration file), saving the current list of
supported clipboard formats and their associated display libraries. The
format of the VIEWER entry in WIN.INI is as follows:
[Clipboard]
<Format>=<Dynamic Library Name>
If you wish, you can manually create this section in WIN.INI using a text
editor and enter the formats and libraries. When VIEWER is first run it will
automatically load all the clipboard display libraries listed here. For
example (assuming you had the dynamic display libraries present), the
following entry in WIN.INI would define support for the CF_TEXT, CF_BITMAP,
and CF_METAFILEPICT clipboard formats:
[Clipboard]
Text=C:\WINDOWS\DLL\TEXT.DLL
Bitmap=C:\WINDOWS\DLL\BITMAP.DLL
Picture=C:\WINDOWS\DLL\PICTURE.DLL
VIEWER Components
VIEWER is built from the series of source files shown in Figures 6 through
10. The VIEWER make file is like that of any Windows application. It
references all the viewer components and lists an ordered sequence of
instructions that assists construction of the application. Note that VIEWER
uses the medium programming model (far code and near data references),
placing each source code module in its own discardable segment. This
segmentation is important since it breaks down the application into a number
of smaller modules, each around 4Kb in size.
VIEWER.DEF defines the operating characteristics of VIEWER, including the
specification for the default attributes of each code segment. Note that all
of the segments are marked as discardable. This allows Windows1 to remove
all the application code from memory if necessary, retaining only its own
internal data structures that manage the program.
Although the DEF file doesn't show it, the segment _TEXT is also present,
assuming MOVEABLE and DISCARDABLE as default code attributes. This segment
is always present when an application uses the C run-time library and
references the Windows API. If you look at the MAP file created by the
linker when you build VIEWER, you will notice that _TEXT is one of the
largest segments. Although not a problem in this case, with a larger
application _TEXT can grow well beyond the optimal 4Kb Windows code segment
size. In this situation you would be well-advised to obtain the source code
to the C run-time library and recompile it into a number of carefully
selected segments. For additional information on program optimization, see
"Techniques for Tuning and Optimization of PM and Windows Applications," MSJ
(Vol.5, No.1).
The VIEWER.H include file is referenced by the application resource file and
each of the source code modules. It provides definitions of general
interest, as well as function prototypes and a number of useful programming
macros.
Note the private message definitions in this file. Structurally speaking,
VIEWER consists of a main, top-level window that manages three child windows
(the status, client, and size box windows), and several dialog boxes. The
dialog boxes, status, and client windows communicate with the top-level
window using these private messages.
The WM_UPDATE message is sent by the top-level window when a change occurs
to the clipboard or when the user selects a different format for display.
The WM_ADDFMT message is sent by the Add Format dialog box to the top-level
window whenever a new display library is to be installed. Although the data
structure that maintains the display libraries is publicly defined, it is
managed by the top-level window only.
The WM_GETFMT message is sent by the Remove Format dialog box to the
top-level window to retrieve the list of supported clipboard formats and
their associated display libraries. This message, in effect, circumvents the
Windows limitation that prevents the transfer of data to a dialog box at the
time it is created.
Analogous to WM_ADDFMT, the WM_REMFMT message is sent by the Remove Format
dialog box to the top-level window when the user selects a particular
display library for removal. The top-level window is then responsible for
updating the display library data structure and for notifying each child
window of the change in application capabilities.
Following the private messages in VIEWER.H are a series of definitions that
specify display library function addresses. These ordinal values are used
throughout the application to reference particular routines in the
dynamically linked display libraries. VIEWER must explicitly create
procedure addresses for each function in the display library since the
module handle is unknown at load time.
The last item of interest in VIEWER.H is the library module data structure.
Currently, VIEWER is arbitrarily limited to 16 display libraries. Each of
these libraries is listed in a fixed length array that contains the library
module handle (necessary when dynamically creating function addresses), and
the associated clipboard format number for which it is responsible.
VIEWER.RC defines the VIEWER menu and the various associated dialog boxes.
Throughout the dialog box templates you will notice the use of the CONTROL
statement in place of such identifiers as LISTBOX, BUTTON, and so on. This
is because the dialog boxes used in the RC file were created by the Windows
dialog box editor. The dialog box editor distributed with Version 2.1 of the
Windows Software Development Kit uses only the CONTROL identifier when
specifying the size, position, and style of a control in a DLG file.
Other items to notice are the size and position values used for the dialog
box controls. VIEWER uses only even numbered values. Since they are
specified in RC units, Windows transforms them into screen units according
to the following formula when the control is created:
screenX = rcX * <Avg Character Width> / 4
screenY = rcY * <Avg Character Height> / 8
This scaling is dependent on the characteristics of the default system font;
therefore, you can expect it to change from device to device. If the average
character width is a multiple of four, or if the character height is a
multiple of eight, then the RC coordinate system is identical to the display
coordinate system. On some systems this is, unfortunately, not the case.
Well-written applications anticipate these scaling changes and attempt to
use multiples of four horizontally and multiples of eight vertically.
Although this isn't always possible, it does eliminate positioning errors
when your application is run with unusual system fonts. At the very least,
positioning errors can be minimized if you use multiples of two.
VIEWER1.C
The VIEWER1.C module (see Figure 7) contains the application's WinMain, the
message processing loop, and the function responsible for handling all the
messages relating to the top-level window. At the beginning of this module
you will notice the definition of identifiers for each of the VIEWER child
windows. Although they are not really used in the program, we could
enumerate all the child windows and recognize them by their identifiers.
Following the identifiers is a list of properties maintained by the
top-level window. In addition to providing a handle to the next member of
the clipboard viewer chain (CBCHAIN) and the currently selected clipboard
format (CBFORMAT), this property list also maintains the data structure used
in the management of the dynamically linked display libraries (CBLIBRARY).
WinMain is listed after the property list definitions. This function is
perhaps in a different form than what you are used to. It registers the
VIEWER window class, creates it in an appropriate size and location on the
screen, then retrieves and dispatches all the application messages until the
program is ended.
Note how the initial width and height of the window are calculated. You
could use the default values, but for users with high-resolution displays
these values result in unusually large windows. Because of this the maximum
window size is limited by using the current system metrics.
Following WinMain is the heart of the application, ViewerWndFn. This
function handles all the messages related to the top-level application
window, including those generated when menu items are selected. This
function usually acts like a large switch statement, intercepting those
messages of interest and passing the remainder on to the system for default
handling. Various messages encountered by the ViewerWndFn are described as
follows.
The first message processed by ViewerWndFn is WM_CREATE. When this message
is received the function immediately defines the current and previous
application instance handles. If you notice carefully, during the
CreateWindow call in WinMain, the handle to the previous application
instance is passed to the ViewerWndFn using one of the creation parameters.
This value is retrieved inside the function and used to block the
registration of child window classes when previous instances exist.
After defining the default window property lists, the function creates the
initial dynamic display library structure, loading the libraries listed in
WIN.INI. Following this the associated child window classes are defined,
registered, and created. When all are successfully created the internal
property lists are updated and the child windows are displayed.
The WM_GETMINMAXINFO message is used by the system to retrieve the minimum
and maximum window dimensions. The ViewerWndFn responds to this message by
setting the minimum window size to an arbitrary multiple of appropriate
system metrics.
The WM_INITMENU message is received whenever the user selects the top-level
application menu. Rather than saving the current menu state, the ViewerWndFn
resets the entire menu each time it is selected, enabling or disabling
options based on the current state of the application. Although this takes a
little extra code, it simplifies the program and eliminates additional data
items.
The WM_COMMAND message is received whenever one of the menu items is
selected. By design, the application contains a variable number of menu
items under the Display pull-down menu. The menu is used to list each of the
formats currently on the clipboard. The identifiers attached to these menu
items are equal to the clipboard format number plus IDM_FORMATS. This
implies that the user has selected one of the clipboard formats when a value
greater than IDM_FORMATS is received.
The first menu item handled under WM_COMMAND is IDM_STATUS. When Status is
selected the status window display is toggled, making it either hidden or
visible, followed by a forced resizing of all top-level child windows.
Resizing is accomplished when the SendMessage function call sends a message
to itself using the new window dimensions as parameters.
The use of such reentry mechanisms is hotly debated in many Windows
programming circles. In one sense they can be thought of as function calls,
executing a specific and known set of code. In another sense, they are more
complicated than a simple function call, sometimes requiring considerable
additional stack space and linkage to other code resources. Like most issues
of this kind, you will probably be the ultimate judge of its efficacy. At
the very least you should be aware of the issues involved and clearly
understand the ensuing flow of control that might result.
Following IDM_STATUS are a number of other menu items that bring up assorted
dialog boxes, erase the current clipboard contents, or select new clipboard
formats. Selecting a new clipboard format is perhaps of greatest interest.
In this process the clipboard is opened, the current format defined, the
data retrieved, and a search commenced for a display library capable of
displaying the format.
Note how the CBGETDATA property is set to TRUE before GetClipboardData is
called. This acts as a semaphore, blocking any and all WM_DRAWCLIPBOARD
messages received during the GetClipboardData call. This situation occurs
when a request is made for data that is provided as part of a delayed
rendering scheme. Without this semaphore, a number of undesirable activities
might be performed when you least expect them.
As soon as the selected data item is retrieved and the responsible display
library determined, each of the associated child windows is notified using a
WM_UPDATE command. The child windows can then respond to this new
information, updating their respective client areas.
The WM_ADDFMT message is sent to the top-level window by the Add Format
dialog box whenever the user adds a new display library to the system. In
this case wParam is equal to the new clipboard format and lParam is equal to
a long pointer to the display library name.
While handling this message, ViewerWndFn checks to see if the clipboard
format is already supported by a display library. If it is, you will be
asked if you wish to replace the existing library with the new one. If the
format is unsupported, the new library will be appended to the end of the
library data structure and the display will be updated. In both cases
WIN.INI is updated to indicate the installation of the new library.
The WM_GETFMT message is sent to the top-level window by the Remove Format
dialog box to retrieve the current display library list. The resulting
concatenated series of strings can then be used when removing one of the
display libraries.
The WM_REMFMT message is sent to the top-level window by the Remove Format
dialog box when a display library is selected for removal. When received,
the library is released, the library data structure updated, WIN.INI
modified, and the entire application notified of the change in display
capabilities. This notification process is especially visible if you remove
a library while viewing a rendering of the clipboard using that particular
format: the VIEWER client area goes blank.
The WM_SIZE command is received when the top-level window is first created,
or whenever the window size changes. This message causes the size and
location of each top-level child window to be recalculated using the
parameters provided with the message. The end result is an updated display
that reflects each of the adjusted window sizes. Note that the WM_PAINT
message normally follows the WM_SIZE message.
The WM_DRAWCLIPBOARD message is usually received whenever the clipboard
contents change. This message is first passed down the clipboard viewer
chain. Following this, the old clipboard format names are removed from the
application menu and the new ones are enumerated. The order in which each
new format is encountered is identical to that used by the copying
application.
When the clipboard contents have been enumerated, the data for the first
supported format is retrieved. To prevent unwanted WM_DRAWCLIPBOARD
messages, it is again surrounded by a semaphore. Finally, the clipboard is
closed, the menu updated to reflect the selected format, and all the child
windows informed of the change.
The WM_CHANGECBCHAIN message is received whenever a change is made to the
clipboard viewer chain. If the window being removed is the next in the
chain, then the link is updated to reference the following window. In all
other cases the message is sent down the chain after a check to see if a
valid handle exists for the next window. This prevents the sending of
messages to an invalid or NULL window handle.
The WM_DESTROY message is one of the last received by the ViewerWndFn. When
received, all display libraries are released, the application unlinked from
the clipboard viewer chain, and all associated window properties released.
VIEWER2.C
The VIEWER2.C module (see Figure 8) contains the window message handling
functions for the status and client windows. The status window displays the
name and size of the selected clipboard format and the current clipboard
owner. The client window, immediately below the status window, is
responsible for the actual display of the data object.
The StatusWndFn processes all the status window related messages. Although
most of the actions performed by this function are reasonably
straightforward, there are a few clipboard-related operations that warrant
further explanation.
The status window function, like any other window function, remains dormant
until a message is received. Only WM_UPDATE and WM_PAINT result in any
significant action within the status window. The WM_UPDATE message informs
the status window that the clipboard contents have changed. The parameters
provided with this message include the new clipboard format and an
associated data handle. These values are subsequently saved for future
reference using window property lists.
Although the status window saves a handle to the current clipboard data,
note that, in general, this is not good programming practice. The use of
this technique requires that the status window be immediately notified
whenever the clipboard contents change. Failure to do so could result in the
use of an invalid data handle.
The WM_PAINT message is received whenever a portion of the status window
needs repainting. Like many other Windows applications, the StatusWndFn
routine simplifies this request and repaints the entire window.
The actual update process involves a number of clipboard-related activities.
The first of these is the retrieval of the current clipboard owner name.
This is done by retrieving the window handle of the clipboard owner,
retrieving the module handle from the class data structure defining the
window, and finally retrieving the module file name. The following code
fragment from the WM_PAINT case illustrates this sequence:
GetModuleFileName(
GetClassWord( GetClipboardOwner(), GCW_HMODULE ),
szFmtSize,
sizeof(szFmtSize)
);
After retrieving the module file name the code determines the size and other
characteristics of the current clipboard data object. For most formats this
is relatively easy since it involves a simple call to GlobalSize using the
current data handle as a parameter.
In the case of bitmaps and metafiles this approach is insufficient. Both of
these clipboard formats involve the use of a special data structure that
contains another handle referencing the actual bits or encoded GDI function
calls.
For the CF_BITMAP and CF_DSPBITMAP formats this process involves retrieving
a BITMAP data structure using the GDI GetObject function. This data
structure (defined in WINDOWS.H) contains the height, width, color format,
and bit values of a logical bitmap, as follows:
typedef struct tagBITMAP {
short bmType;
short bmWidth;
short bmHeight;
short bmWidthBytes;
BYTE bmPlanes;
BYTE bmBitsPixel;
LPSTR bmBits;
} BITMAP;
The size of the bitmap is calculated by multiplying, in bytes, the bitmap
height by the bitmap width by the number of color planes. Although only one
type of logical bitmap is in use at present, this may not be the case in the
future. For this reason you should check the bitmap type before directly
manipulating the bitmap bits.
To calculate the metafile size for the CF_METAFILEPICT and
CF_DSPMETAFILEPICT formats, a different process is used. Unlike the bitmap
case, the metafile data handle references a global memory object in the
following format:
typedef struct tagMETAFILEPICT {
int mm;
int xExt;
int yExt;
HANDLE hMF;
} METAFILEPICT;
The metafile size, in addition to other supplemental information, is
calculated by locking the handle and summing the respective global size of
the METAFILEPICT data structure (defined in WINDOWS.H) and actual metafile
bits referenced by the hMF handle.
The ClientWndFn processes all the client window related messages.
Internally, this function maintains three distinct window properties--the
current clipboard format, a handle to the display library module responsible
for displaying the format, and an internal data structure for exclusive use
by the display library when managing the display of the clipboard data.
The actual format of the display data structure is determined by the active
display library. The client window serves only as an agent to store and
retrieve the information. In most cases the display library will use this
data structure to save a handle to the currently selected clipboard data in
addition to other relevant information.
Like the status window, the client window processes only a few selected
messages, passing the remainder on to the DefWindowProc. Of the processed
messages, all but WM_CREATE involve some interaction with a dynamically
linked display library.
As it does in the status window, the WM_UPDATE message informs the client
window that the clipboard contents have changed. The ClientWndFn function
first responds to this message by asking the currently active display
library to restore the window to its original state, then by asking the new
display library to reinitialize the window.
Since the display libraries were manually loaded by the top-level VIEWER
window, the ClientWndFn must explicitly link to the library functions using
predefined ordinal values. Both the old and new display libraries must be
present in memory when the WM_UPDATE command is received. This is because
the old library is responsible for restoring the window to its original
condition, and the new library for reinitializing the window. The end result
is that the VIEWER window cannot release a library until the client window
has finished updating itself.
Although VIEWER is responsible for loading and unloading the library, the
display library determines how the selected format is displayed and what
changes to the client window are necessary to display the information. For
example, if you developed a library for the display of CF_OWNERDISPLAY data,
the library would be responsible for dispatching the WM_PAINTCLIPBOARD,
WM_SIZECLIPBOARD, and all other owner display related messages to the
current clipboard owner.
VIEWER3.C
The VIEWER3.C module simply contains the code that manages the various
dialog boxes used by the VIEWER application (see Figure 9). The AddFmtDlgFn
manages the Add Format dialog box. This function waits until the user enters
both the clipboard format name and the library responsible for handling its
display. When you press the Add button these names are retrieved and passed
to the parent window using a WM_ADDFMT message. The parent window (in this
case, VIEWER) responds to this message by loading the library and inserting
it into its internal library data structure.
The RemFmtDlgFn is responsible for managing the Remove Format dialog box.
When the dialog box is created, this function retrieves a list of the
currently installed display libraries from the VIEWER window. This list of
libraries and their associated clipboard formats is displayed in a list box.
After selecting a library, you can remove it from the system by clicking on
the Remove button. This causes a WM_REMFMT message to be sent to the VIEWER
window, which updates the status and client windows and releases the display
library.
VIEWER4.C
The fourth and last module, VIEWER4.C (see Figure 10), contains the code for
a number of utility functions used throughout the VIEWER application. Two of
these functions are of particular interest.
The GetClipboardFmtName function is an extension of the Windows API
function, GetClipboardFormatName. However, GetClipboardFmtName can return
the name of any clipboard format, including CF_OWNERDISPLAY and the formats
predefined in WINDOWS.H, unlike its Windows equivalent.
In the case of predefined clipboard formats, the function returns the
commonly used name of the format. Although these names may differ slightly
from program to program, most applications use these values consistently
when translating a clipboard format number to a corresponding text string.
For CF_OWNERDISPLAY data, the function sends a WM_ASKCBFORMATNAME message to
the current clipboard owner, asking it to provide the full name of the
format. If the clipboard is without an owner, the text string "Owner
Display" is returned by default.
For those people who are unfamiliar with the CF_OWNERDISPLAY clipboard
format, it is one of the most interesting, yet underutilized clipboard
formats supported by Windows. When data is supplied in this format, an
application can manage the display or the information in the clipboard
viewer window. For example, Windows Write uses this technique to display
formatted text copied to the clipboard.
In the case of formats not predefined in WINDOWS.H, GetClipboardFmtName
assumes that they have been manually registered. Their formal name is
retrieved using the GetClipboardFormatName function.
The GetClipboardFmtNumber function is similar to GetClipboardFmtName in that
it returns the internal clipboard format number associated with the text
string provided. In the case of predefined formats it returns the value
defined in WINDOWS.H. For all others, it manually registers the name
provided using RegisterClipboardFormat and returns the result.
Creating Display Libraries
Two display libraries, with complete source code, are provided with the
VIEWER application. (Full source code can be downloaded from any of the MSJ
bulletin boards listed on the inside back cover--Ed.) As mentioned
previously, the BITMAP.DLL (see Figure 11) library supports the display of
color and monochrome bitmaps and the OWNER.DLL library the display of
CF_OWNERDISPLAY data.
Although these two libraries take very different approaches while supporting
their respective data formats, they use an identical set of entry points,
function parameters, and return values. In addition, since these display
modules are DLLs, they are subject to the standard SS != DS programming
constraint and require the use of an assembly language entry point.
The sidebar describes each of the entry points and parameter lists that must
be supported when creating your own display libraries for the VIEWER.
When creating a display library, the module definition file must explicitly
export each of the functions using the ordinal values shown in Figure 12.
This article has covered a number of important clipboard-related topics and
demonstrated how an extensible clipboard viewer can be created that
separates the management and display of information into two distinct tasks.
Although the concepts presented here are relatively simple, the definition
of dynamic-link libraries that support the display and manipulation of
specific data types can have broad application to commercial Windows
products.
Figure 2
Clipboard Formats
Format Description DLL to Use
CF_TEXT Conventional ASCII text
CF_BITMAP Windows GDI bitmap BITMAP.DLL
CF_METAFILEPICT Windows metafile picture
CF_SYLK Microsoft Symbolic Link format (spreadsheet data)
CF_DIF Software Arts' Data Interchange Format
CF_TIFF Tagged Image File Format
CF_OEMTEXT OEM text
CF_OWNERDISPLAY Owner display format OWNER.DLL
CF_DSPTEXT Display text
CF_DSPBITMAP Display bitmap BITMAP.DLL
CF_DSPMETAFILEPICT Display metafile picture
CF_PRIVATEFIRST First privately owned data object
CF_PRIVATELAST Last privately owner data object
CF_GDIOBJFIRST First GDI data object
CF_GDIOBJLAST Last GDI data object
BIFF Binary Interchange File Format
CSV Comma Separated Variable
Printer_Bitmap Printer bitmap
Printer_Picture Printer metafile picture
Rich Text Rich text format
Postscript PostScript text
Figure 4
Name Constant DLL to Use
Text CF_TEXTCF
Bitmap CF_BITMAP BITMAP.DLL
Picture CF_METAFILEPICT
SYLK CF_SYLK
DIF CF_DIF
TIFF CF_TIFF
OEM TEXT CF_OEMTEXT
Owner Display CF_OWNERDISPLAY OWNER.DLL
Display Text CF_DSPTEXT
Display Bitmap CF_DSPBITMAP BITMAP.DLL
Display Picture CF_DSPMETAFILEPICT
Figure 6
VIEWER
STDFLAGS=-c -u -AM -FPa -Gsw -Os -Zep -W2
viewer.res: viewer.rc viewer.h viewer.ico
rc -r viewer.rc
viewer1.obj: viewer1.c viewer.h
cl $(STDFLAGS) -NT _VIEWER viewer1.c
viewer2.obj: viewer2.c viewer.h
cl $(STDFLAGS) -NT _DISPLAY viewer2.c
viewer3.obj: viewer3.c viewer.h
cl $(STDFLAGS) -NT _DIALOG viewer3.c
viewer4.obj: viewer4.c viewer.h
cl $(STDFLAGS) -NT _UTILITY viewer4.c
viewer.exe: viewer1.obj viewer2.obj viewer3.obj \
viewer4.obj viewer.res viewer.def
link4 viewer1+viewer2+viewer3+viewer4 /AL:16 /NOE,\
viewer,,mlibw,viewer.def
rc viewer.res
VIEWER.DEF
NAME VIEWER
DESCRIPTION 'Extensible Clipboard Viewer'
STUB 'WINSTUB.EXE'
DATA MOVEABLE MULTIPLE
CODE MOVEABLE DISCARDABLE
SEGMENTS
_VIEWER MOVEABLE DISCARDABLE LOADONCALL
_DISPLAY MOVEABLE DISCARDABLE LOADONCALL
_DIALOG MOVEABLE DISCARDABLE LOADONCALL
_UTILITY MOVEABLE DISCARDABLE LOADONCALL
HEAPSIZE 2048
STACKSIZE 2048
EXPORTS
AboutDlgFn @1
StatusWndFn @2
ClientWndFn @3
ViewerWndFn @4
AddFormatDlgFn @5
RemFormatDlgFn @6
VIEWER.H
/*
* WINDOWS CLIPBOARD VIEWER - GENERAL HEADER FILE
*
* LANGUAGE : Microsoft C 5.1
* ENVIRONMENT : Microsoft Windows 2.1 SDK
* STATUS : operational
*
* 1.01-Kevin P. Welch-add param to GetClipboardFmtName.
*
*/
/* main menu definitions */
#define IDM_STATUS 0x0100
#define IDM_ADDFMT 0x0101
#define IDM_REMFMT 0x0102
#define IDM_EXIT 0x0103
#define IDM_ABOUT 0x0104
#define IDM_ERASE 0x0105
#define IDM_FORMATS 0x0106
/* private message definitions */
#define WM_UPDATE (WM_USER+1)
#define WM_ADDFMT (WM_USER+2)
#define WM_GETFMT (WM_USER+3)
#define WM_REMFMT (WM_USER+4)
/* dialog box definitions */
#define IDADD 0x0100
#define IDFORMAT 0x0101
#define IDLIBRARY 0x0102
#define IDREMOVE 0x0100
#define IDLIBLIST 0x0101
/* library function addresses */
#define LIB_INIT MAKEINTRESOURCE(1)
#define LIB_CREATE MAKEINTRESOURCE(2)
#define LIB_SIZE MAKEINTRESOURCE(3)
#define LIB_HSCROLL MAKEINTRESOURCE(4)
#define LIB_VSCROLL MAKEINTRESOURCE(5)
#define LIB_PAINT MAKEINTRESOURCE(6)
#define LIB_DESTROY MAKEINTRESOURCE(7)
/* general programming extensions */
#define ID(x) GetWindowWord(x,GWW_ID)
#define PARENT(x) GetWindowWord(x,GWW_HWNDPARENT)
#define INSTANCE(x) GetWindowWord(x,GWW_HINSTANCE)
#define WARNING(x,y) MessageBox(x,y,"Clipboard Viewer",\
MB_OK|MB_ICONEXCLAMATION)
/* library module data structure */
#define MAX_MODULE 16
typedef struct {
WORD hModule;
WORD wFormat;
} MODULE;
typedef struct {
WORD wModules;
MODULE Module[MAX_MODULE];
} LIBRARY;
typedef LIBRARY FAR * LPLIBRARY;
/* viewer function definitions (viewer1.c) */
LONG FAR PASCAL ViewerWndFn( HWND, WORD, WORD, LONG );
/* client and status function definitions (viewer2.c) */
LONG FAR PASCAL ClientWndFn( HWND, WORD, WORD, LONG );
LONG FAR PASCAL StatusWndFn( HWND, WORD, WORD, LONG );
/* dialog function definitions (viewer3.c) */
BOOL FAR PASCAL Dialog( HWND, LPSTR, FARPROC );
BOOL FAR PASCAL AboutDlgFn( HWND, WORD, WORD, LONG );
BOOL FAR PASCAL RemFormatDlgFn( HWND, WORD, WORD, LONG );
BOOL FAR PASCAL AddFormatDlgFn( HWND, WORD, WORD, LONG );
/* utility function definitions (viewer4.c) */
BOOL FAR PASCAL CenterPopup( HWND, HWND );
WORD FAR PASCAL GetClipboardFmtNumber( LPSTR );
HANDLE FAR PASCAL GetClipboardModule( WORD, HANDLE );
WORD FAR PASCAL GetClipboardFmtName( WORD, LPSTR, WORD, BOOL );
/* undocumented internal function definitions */
int FAR PASCAL lstrlen( LPSTR );
int FAR PASCAL lstrcmp( LPSTR, LPSTR );
LPSTR FAR PASCAL lstrcpy( LPSTR, LPSTR );
LPSTR FAR PASCAL lstrcat( LPSTR, LPSTR );
VIEWER.RC
/*
* WINDOWS CLIPBOARD VIEWER - RESOURCE FILE
*
* LANGUAGE : Microsoft C5.1
* ENVIRONMENT : Microsoft Windows 2.1 SDK
* STATUS : operational
*
* 1.01 - Kevin P. Welch - make listboxes unsorted.
*
*/
#include <style.h>
#include <debug.h>
#include "viewer.h"
ViewerIcon ICON Viewer.ico
ViewerMenu MENU
BEGIN
POPUP "&Options"
BEGIN
MENUITEM "&Status", IDM_STATUS
MENUITEM SEPARATOR
MENUITEM "&Add Format...", IDM_ADDFMT
MENUITEM "&Remove Format...", IDM_REMFMT
MENUITEM SEPARATOR
MENUITEM "E&xit", IDM_EXIT
MENUITEM "A&bout...", IDM_ABOUT
END
POPUP "&Display"
BEGIN
MENUITEM "&Erase...", IDM_ERASE,GRAYED
MENUITEM SEPARATOR
MENUITEM "&1. (Empty)", IDM_FORMATS,GRAYED
END
END
ViewerAbout DIALOG LOADONCALL MOVEABLE DISCARDABLE 32, 32, 200, 92
CAPTION "About Clipboard Viewer..."
STYLE WS_BORDER | WS_CAPTION | WS_DLGFRAME | WS_POPUP | WS_VISIBLE
BEGIN
CONTROL "&Ok", IDOK, "button", BS_DEFPUSHBUTTON | WS_TABSTOP |
WS_GROUP | WS_CHILD, 164, 4, 32, 14
CONTROL "Extensible Clipboard Viewer", -1, "static", SS_LEFT |
WS_GROUP | WS_CHILD, 4, 6, 144, 8
CONTROL "Microsoft Systems Journal", -1, "static", SS_LEFT |
WS_CHILD, 4, 14, 144, 8
CONTROL "March 1990", -1, "static", SS_LEFT | WS_CHILD,
4, 22, 144, 8
CONTROL "Designed && Developed by:", -1, "static", SS_LEFT |
WS_CHILD, 4, 38, 144, 8
CONTROL "Kevin P. Welch", -1, "static", SS_LEFT | WS_CHILD,
4, 54, 144, 8
CONTROL "Eikon Systems, Inc.", -1, "static", SS_LEFT | WS_CHILD,
4, 62, 144, 8
CONTROL "989 East Hillsdale Blvd, Suite 260", -1, "static",
SS_LEFT | WS_CHILD, 4, 70, 144, 8
CONTROL "Foster City, California 94404", -1, "static",
SS_LEFT | WS_CHILD, 4, 78, 136, 8
CONTROL "", -1, "static", SS_BLACKFRAME | WS_CHILD,
160, 0, 1, 92
CONTROL "ViewerIcon", -1, "static", SS_ICON | WS_CHILD,
180, 56, 16, 32
END
ViewerAdd DIALOG LOADONCALL MOVEABLE DISCARDABLE 32, 32, 172, 86
CAPTION "Add Format to Viewer..."
STYLE WS_BORDER | WS_CAPTION | WS_DLGFRAME | WS_POPUP
BEGIN
CONTROL "", IDFORMAT, "edit", ES_LEFT | ES_AUTOHSCROLL |
WS_BORDER | WS_TABSTOP | WS_CHILD, 38, 4, 90, 12
CONTROL "", IDLIBRARY, "edit", ES_LEFT | ES_AUTOHSCROLL |
WS_BORDER | WS_TABSTOP | WS_CHILD, 38, 18, 90, 12
CONTROL "&Add", IDADD, "button", BS_DEFPUSHBUTTON | WS_GROUP |
WS_TABSTOP | WS_CHILD, 136, 4, 32, 14
CONTROL "&Cancel", IDCANCEL, "button", BS_PUSHBUTTON |
WS_CHILD, 136, 22, 32, 14
CONTROL "&Format :", -1, "static", SS_LEFT | WS_GROUP |
WS_CHILD, 4, 6, 32, 8
CONTROL "&Library:", -1, "static", SS_LEFT | WS_CHILD,
4, 20, 32, 8
CONTROL "To add a format to the viewer", -1, "static",
SS_LEFT | WS_CHILD, 4, 40, 124, 8
CONTROL "you must enter the name of the", -1, "static",
SS_LEFT | WS_CHILD, 4, 48, 120, 8
CONTROL "clipboard format, followed by", -1, "static",
SS_LEFT | WS_CHILD, 4, 56, 120, 8
CONTROL "the name of the dynamic library", -1, "static",
SS_LEFT | WS_CHILD, 4, 64, 126, 8
CONTROL "that will support the format.", -1, "static",
SS_LEFT | WS_CHILD, 4, 72, 120, 8
CONTROL "Text", -1, "static", SS_BLACKFRAME | WS_CHILD,
132, 0, 1, 86
END
ViewerRemove DIALOG LOADONCALL MOVEABLE DISCARDABLE 32, 32, 216, 84
CAPTION "Remove Format from Viewer..."
STYLE WS_BORDER | WS_CAPTION | WS_DLGFRAME | WS_POPUP
BEGIN
CONTROL "", IDLIBLIST, "listbox", LBS_NOTIFY | LBS_STANDARD |
WS_BORDER | WS_VSCROLL | WS_TABSTOP | WS_CHILD, 4, 4, 168, 74
CONTROL "&Remove", IDREMOVE, "button", BS_DEFPUSHBUTTON |
WS_GROUP | WS_TABSTOP | WS_CHILD, 180, 4, 32, 14
CONTROL "&Cancel", IDCANCEL, "button", BS_PUSHBUTTON |
WS_CHILD, 180, 22, 32, 14
CONTROL "Text", -1, "static", SS_BLACKFRAME | WS_CHILD,
176, 0, 1, 85
END
Figure 7
/*
* WINDOWS CLIPBOARD VIEWER - SOURCE CODE
*
* LANGUAGE : Microsoft C 5.1
* MODEL : medium
* ENVIRONMENT : Microsoft Windows 2.1 SDK
* STATUS : operational
*
* 1.01-Kevin P. Welch- add param to GetClipboardFmtName.
*
*/
#define NOCOMM
#include <windows.h>
#include <process.h>
#include <memory.h>
#include <string.h>
#include <stdio.h>
#include "viewer.h"
/* viewer window child id numbers */
#define ID_STATUS 1
#define ID_CLIENT 2
#define ID_SIZEBOX 3
/* viewer window properties */
#define CBCHAIN MAKEINTRESOURCE(1)
#define CBFORMAT MAKEINTRESOURCE(2)
#define CBGETDATA MAKEINTRESOURCE(3)
#define CBLIBRARY MAKEINTRESOURCE(4)
#define HWNDSTATUS MAKEINTRESOURCE(5)
#define HWNDCLIENT MAKEINTRESOURCE(6)
#define HWNDSIZEBOX MAKEINTRESOURCE(7)
/*
* WinMain( hInstance, hPrevInstance, lpszCmd, wCmdShow ) : VOID
*
* hCrntInst current instance handle
* hPrevInst previous instance handle
* lpszCmdLine current command line
* wInitShowCmd initial show window command
*
* This function is responsible for registering and creating
* the clipboard viewer window. Once the window has been created,
* it is also responsible for retrieving and dispatching all the
* messages related to the window.
*
*/
VOID PASCAL WinMain(
HANDLE hCrntInst,
HANDLE hPrevInst,
LPSTR lpszCmdLine,
WORD wInitShowCmd )
{
/* local variables */
MSG msgViewer;
WORD wWndWidth;
WORD wWndHeight;
HWND hWndViewer;
WNDCLASS classViewer;
/* warning level 3 compatibility */
lpszCmdLine;
/* define viewer class */
memset( &classViewer, 0, sizeof(WNDCLASS) );
classViewer.lpszClassName =(LPSTR)"Viewer";
classViewer.hCursor = LoadCursor( NULL, IDC_ARROW );
classViewer.lpszMenuName = (LPSTR)"ViewerMenu";
classViewer.style = CS_HREDRAW | CS_VREDRAW;
classViewer.lpfnWndProc = ViewerWndFn;
classViewer.hInstance = hCrntInst;
classViewer.hIcon = LoadIcon( hCrntInst, "ViewerIcon" );
/* register class if no previous instance */
if ( (hPrevInst) || (RegisterClass(&classViewer)) ) {
/* calculate initial width & height */
wWndWidth = ( GetSystemMetrics(SM_CXSCREEN) >
40 * GetSystemMetrics(SM_CXVSCROLL) ) ?
40 * GetSystemMetrics(SM_CXVSCROLL ) :
2 * GetSystemMetrics(SM_CXSCREEN) / 3;
wWndHeight = ( GetSystemMetrics(SM_CYSCREEN) >
30 * GetSystemMetrics(SM_CYHSCROLL) ) ?
30 * GetSystemMetrics(SM_CYHSCROLL ) :
2 * GetSystemMetrics(SM_CYSCREEN) / 3;
/* create viewer window */
hWndViewer = CreateWindow(
"Viewer",
"Clipboard Viewer",
WS_TILEDWINDOW|WS_CLIPCHILDREN,
CW_USEDEFAULT,
CW_USEDEFAULT,
wWndWidth,
wWndHeight,
(HWND)NULL,
(HMENU)NULL,
hCrntInst,
(LPSTR)(DWORD)hPrevInst
);
/* continue if successful */
if ( hWndViewer ) {
/* display window */
ShowWindow( hWndViewer, wInitShowCmd );
/* process all related messages */
while ( GetMessage( &msgViewer, NULL, 0, 0 ) ) {
TranslateMessage( &msgViewer );
DispatchMessage( &msgViewer );
}
/* normal exit */
exit( msgViewer.wParam );
} else
WARNING( NULL, "Unable to create Clipboard Viewer!" );
} else
WARNING( NULL, "Unable to register Clipboard Viewer!" );
/* abnormal exit */
exit( TRUE );
}
/*
* ViewerWndFn( hWnd, wMsg, wParam, lParam ) : LONG
*
* hWnd window handle
* wMsg message number
* wParam additional message information
* lParam additional message information
*
* This window function processes all the messages related to
* the clipboard viewer window. When created this window
* registers and creates the associated status and client windows.
*
*/
LONG FAR PASCAL ViewerWndFn( hWnd, wMsg, wParam, lParam )
HWND hWnd;
WORD wMsg;
WORD wParam;
LONG lParam;
{
LONG lResult;
/* initialization */
lResult = FALSE;
/* process each message */
switch( wMsg )
{
case WM_CREATE : /* create window */
{
char * pKey;
HANDLE hFmtLib;
HANDLE hLibrary;
HANDLE hCrntInst;
HANDLE hPrevInst;
LPLIBRARY lpLibrary;
HWND hWndStatus;
HWND hWndClient;
HWND hWndSizebox;
WNDCLASS classStatus;
WNDCLASS classClient;
char szLib[128];
char szList[128];
/* define instance handles */
hCrntInst = INSTANCE(hWnd);
hPrevInst = (HANDLE)((LPCREATESTRUCT)lParam)->lpCreateParams;
/* define initial property lists */
SetProp( hWnd, CBCHAIN, NULL );
SetProp( hWnd, CBFORMAT, NULL );
SetProp( hWnd, CBGETDATA, NULL );
SetProp( hWnd, HWNDSTATUS, NULL );
SetProp( hWnd, HWNDCLIENT, NULL );
SetProp( hWnd, HWNDSIZEBOX, NULL );
/* define library data structure */
hLibrary = GlobalAlloc( GHND, (DWORD)sizeof(LIBRARY) );
if ( hLibrary ) {
lpLibrary = (LPLIBRARY)GlobalLock( hLibrary );
if ( lpLibrary ) {
/* initialization */
lpLibrary->wModules = 0;
/* retrieve list of supported formats */
if ( GetProfileString("Clipboard",NULL,"",
szList,sizeof(szList)) ) {
pKey = &szList[0];
while ( *pKey ) {
GetProfileString("Clipboard",pKey,"", szLib, sizeof(szLib));
if ( szLib[0] > ' ' ) {
hFmtLib = LoadLibrary( szLib );
if ( hFmtLib >= 32 ) {
lpLibrary->Module[lpLibrary->wModules].hModule = hFmtLib;
lpLibrary->Module[lpLibrary->wModules++].wFormat =
GetClipboardFmtNumber(pKey);
}
}
pKey += strlen(pKey) + 1;
}
}
/* unlock library & save handle */
GlobalUnlock( hLibrary );
SetProp( hWnd, CBLIBRARY, hLibrary );
/* define status class */
memset( &classStatus, 0, sizeof(WNDCLASS) );
classStatus.lpszClassName =(LPSTR)"ViewerStatus";
classStatus.hCursor = LoadCursor( NULL, IDC_ARROW );
classStatus.style = CS_HREDRAW | CS_VREDRAW;
classStatus.lpfnWndProc = StatusWndFn;
classStatus.hInstance = hCrntInst;
classStatus.hbrBackground =(HBRUSH)(COLOR_WINDOW + 1);
/* define client class */
memset( &classClient, 0, sizeof(WNDCLASS) );
classClient.lpszClassName =(LPSTR)"ViewerClient";
classClient.hCursor = LoadCursor( NULL, IDC_ARROW );
classClient.style = CS_HREDRAW | CS_VREDRAW;
classClient.lpfnWndProc = ClientWndFn;
classClient.hInstance = hCrntInst;
classClient.hbrBackground =(HBRUSH)(COLOR_APPWORKSPACE+1);
/* register classes if no previous instance */
if ( (hPrevInst) ||
(RegisterClass(&classStatus) &&
RegisterClass(&classClient))
) {
/* create child windows */
hWndSizebox = CreateWindow(
"Scrollbar",
"",
WS_CHILD|SBS_SIZEBOX,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
hWnd,
(HMENU)ID_SIZEBOX,
hCrntInst,
(LPSTR)(DWORD)hPrevInst
);
hWndClient = CreateWindow(
"ViewerClient",
"",
WS_CHILD|WS_BORDER|
WS_HSCROLL|WS_VSCROLL|
WS_CLIPSIBLINGS,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
hWnd,
(HMENU)ID_CLIENT,
hCrntInst,
(LPSTR)(DWORD)hPrevInst
);
hWndStatus = CreateWindow(
"ViewerStatus",
"",
WS_CHILD|WS_BORDER|
WS_CLIPSIBLINGS,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
hWnd,
(HMENU)ID_STATUS,
hCrntInst,
(LPSTR)(DWORD)hPrevInst
);
/* continue if successful */
if ( hWndStatus && hWndClient && hWndSizebox ) {
/* update property lists */
SetProp( hWnd, HWNDSTATUS, hWndStatus );
SetProp( hWnd, HWNDCLIENT, hWndClient );
SetProp( hWnd, HWNDSIZEBOX, hWndSizebox );
SetProp( hWnd, CBCHAIN, SetClipboardViewer(hWnd) );
/* make child windows visible */
ShowWindow( hWndStatus, SW_SHOWNA );
ShowWindow( hWndClient, SW_SHOWNA );
ShowWindow( hWndSizebox, SW_SHOWNA );
} else {
WARNING( hWnd, "Unable to create child windows!" );
PostQuitMessage( 4 );
}
} else {
WARNING( hWnd, "Unable to register child windows!" );
PostQuitMessage( 3 );
}
} else {
WARNING( hWnd, "Unable to access instance data!" );
PostQuitMessage( 2 );
}
} else {
WARNING( hWnd, "Insufficient memory!" );
PostQuitMessage( 1 );
}
}
break;
case WM_GETMINMAXINFO : /* retrieve window size constraints */
/* set minimum tracking size */
((LPPOINT)lParam)[3].x = 22 * GetSystemMetrics(SM_CXVSCROLL);
((LPPOINT)lParam)[3].y = 10 * GetSystemMetrics(SM_CYCAPTION);
break;
case WM_INITMENU : /* initialize menu */
{
LPLIBRARY lpLibrary;
/* check status window visibility */
CheckMenuItem(
wParam,
IDM_STATUS,
IsWindowVisible(GetProp(hWnd,HWNDSTATUS)) ?
MF_CHECKED : MF_UNCHECKED
);
/* enable-disable format library menu options */
lpLibrary = (LPLIBRARY)GlobalLock( GetProp(hWnd,CBLIBRARY) );
if ( lpLibrary ) {
EnableMenuItem(wParam,IDM_ADDFMT,(lpLibrary->wModules <
MAX_MODULE) ? MF_ENABLED : MF_GRAYED );
EnableMenuItem(wParam,IDM_REMFMT,(lpLibrary->wModules) ?
MF_ENABLED : MF_GRAYED );
GlobalUnlock( GetProp(hWnd,CBLIBRARY) );
} else
WARNING( hWnd, "Unable to access instance data!" );
/* enable-disable erase menu option */
EnableMenuItem( wParam, IDM_ERASE, (GetProp(hWnd,CBFORMAT) >
IDM_FORMATS) ? MF_ENABLED : MF_GRAYED );
}
break;
case WM_COMMAND : /* menu command */
/* process sub-message */
switch( wParam )
{
case IDM_STATUS : /* toggle status bar visibility */
{
RECT rcClient;
/* change status bar visibility */
ShowWindow(
GetProp(hWnd,HWNDSTATUS),
IsWindowVisible(
GetProp(hWnd,HWNDSTATUS)) ? SW_HIDE : SW_SHOWNA
);
/* force resizing of child windows */
GetClientRect( hWnd, &rcClient );
SendMessage( hWnd, WM_SIZE, 0,
MAKELONG(rcClient.right,rcClient.bottom) );
}
break;
case IDM_EXIT : /* exit application */
PostQuitMessage( 0 );
break;
case IDM_ABOUT : /* about viewer */
Dialog( hWnd, "ViewerAbout", AboutDlgFn );
break;
case IDM_ADDFMT : /* add a new clipboard format */
Dialog( hWnd, "ViewerAdd", AddFormatDlgFn );
break;
case IDM_REMFMT : /* remove a clipboard format */
Dialog( hWnd, "ViewerRemove", RemFormatDlgFn );
break;
case IDM_ERASE : /* erase clipboard contents */
/* verify option */
if ( MessageBox(hWnd,"Erase clipboard contents?",
"Clipboard Viewer",
MB_ICONQUESTION|MB_YESNO) = = IDYES ) {
if ( OpenClipboard(hWnd) ) {
EmptyClipboard();
CloseClipboard();
} else
WARNING( hWnd, "Unable to open clipboard!" );
}
break;
default : /* one of the selected formats */
/* open clipboard */
if ( OpenClipboard(hWnd) ) {
WORD wCrntFmt;
HANDLE hCrntData;
HANDLE hCrntModule;
/* attempt to retrieve clipboard data - semaphore call */
wCrntFmt = wParam - IDM_FORMATS;
if ( wCrntFmt ) {
SetProp( hWnd, CBGETDATA, TRUE );
hCrntData = GetClipboardData( wCrntFmt );
hCrntModule = GetClipboardModule( wCrntFmt,
GetProp(hWnd,CBLIBRARY) );
SetProp( hWnd, CBGETDATA, NULL );
if ( hCrntData = = NULL )
WARNING( hWnd, "Unable to retrieve clipboard data!" );
} else {
hCrntData = NULL;
hCrntModule = NULL;
}
/* close clipboard */
CloseClipboard();
/* update checked menu item - even if data inaccessible */
CheckMenuItem( GetMenu(hWnd), GetProp(hWnd,CBFORMAT),
MF_UNCHECKED );
SetProp( hWnd, CBFORMAT, wParam );
CheckMenuItem( GetMenu(hWnd), wParam, MF_CHECKED );
/* notify child windows - note that clipboard is now closed */
SendMessage( GetProp(hWnd,HWNDSTATUS), WM_UPDATE, wCrntFmt,
MAKELONG(hCrntData,hCrntModule) );
SendMessage( GetProp(hWnd,HWNDCLIENT), WM_UPDATE, wCrntFmt,
MAKELONG(hCrntData,hCrntModule) );
} else
WARNING( hWnd, "Unable to open clipboard!" );
break;
}
break;
case WM_ADDFMT : /* add a new clipboard library */
{
WORD wEntry;
HANDLE hFmtLib;
HANDLE hOldLib;
LPLIBRARY lpLibrary;
char szFmt[64];
char szLib[64];
char szMsg[80];
/* copy name of library */
lstrcpy( szLib, (LPSTR)lParam );
/* lock down instance data */
lpLibrary = (LPLIBRARY)GlobalLock( GetProp(hWnd,CBLIBRARY) );
if ( lpLibrary ) {
/* define clipboard format name */
GetClipboardFmtName( wParam, szFmt, sizeof(szFmt), FALSE );
/* check to see if format already listed */
for (wEntry=0;
(wEntry<lpLibrary->wModules)&&
(wParam!=lpLibrary->Module[wEntry].wFormat); wEntry++ );
if ( wEntry < lpLibrary->wModules ) {
/* entry already present - ask if replace? */
GetModuleFileName( lpLibrary->Module[wEntry].hModule,
szLib, sizeof(szLib) );
sprintf( szMsg, "Replace %s?", szLib );
if ( MessageBox(hWnd,szMsg,"Clipboard Viewer",
MB_ICONQUESTION|MB_YESNO) = = IDYES ) {
hFmtLib = LoadLibrary( (LPSTR)lParam );
if ( hFmtLib >= 32 ) {
lResult = TRUE;
hOldLib = lpLibrary->Module[wEntry].hModule;
lpLibrary->Module[wEntry].hModule = hFmtLib;
SendMessage( hWnd, WM_COMMAND, GetProp(hWnd,CBFORMAT),
0L );
WriteProfileString( "Clipboard", szFmt, szLib );
FreeLibrary( hOldLib );
}
}
} else {
/* check if space available internally */
if ( lpLibrary->wModules < MAX_MODULE ) {
hFmtLib = LoadLibrary( (LPSTR)lParam );
if ( hFmtLib >= 32 ) {
lResult = TRUE;
lpLibrary->Module[lpLibrary->wModules].hModule=hFmtLib;
lpLibrary->Module[lpLibrary->wModules++].wFormat =
wParam;
SendMessage( hWnd, WM_COMMAND, GetProp(hWnd,CBFORMAT),
0L );
WriteProfileString("Clipboard", szFmt, (LPSTR)lParam );
}
} else
WARNING( hWnd, "Insufficient memory to add library!" );
}
/* unlock library data */
GlobalUnlock( GetProp(hWnd,CBLIBRARY) );
} else
WARNING( hWnd, "Unable to access instance data!" );
}
break;
case WM_GETFMT : /* retrieve clipboard format list */
{
LPSTR lpStr;
WORD wEntry;
LPLIBRARY lpLibrary;
char szFmt[64];
char szLib[80];
char szEntry[128];
/* lock down instance data */
lpLibrary = (LPLIBRARY)GlobalLock( GetProp(hWnd,CBLIBRARY) );
if ( lpLibrary ) {
lpStr = (LPSTR)lParam;
lResult = lpLibrary->wModules;
for (wEntry=0; wEntry<lpLibrary->wModules; wEntry++) {
GetClipboardFmtName( lpLibrary->Module[wEntry].wFormat,
szFmt, sizeof(szFmt), FALSE );
GetModuleFileName( lpLibrary->Module[wEntry].hModule,
szLib, sizeof(szLib) );
sprintf( szEntry, "%s - %s", szFmt, szLib );
lstrcpy( lpStr, szEntry );
lpStr + = strlen(szEntry) + 1;
}
lstrcpy( lpStr, "" );
GlobalUnlock( GetProp(hWnd,CBLIBRARY) );
} else
WARNING( hWnd, "Unable to access instance data!" );
}
break;
case WM_REMFMT : /* remove a listed clipboard library */
{
HANDLE hOldLib;
LPLIBRARY lpLibrary;
char szFmt[64];
/* lock down instance data & remove library module */
lpLibrary = (LPLIBRARY)GlobalLock( GetProp(hWnd,CBLIBRARY) );
if ( lpLibrary ) {
if ( wParam < lpLibrary->wModules ) {
lResult = TRUE;
hOldLib = lpLibrary->Module[wParam].hModule;
GetClipboardFmtName( lpLibrary->Module[wParam].wFormat,
szFmt, sizeof(szFmt), FALSE );
while ( ++wParam < lpLibrary->wModules ) {
lpLibrary->Module[wParam-1].wFormat =
lpLibrary->Module[wParam].wFormat;
lpLibrary->Module[wParam-1].hModule =
lpLibrary->Module[wParam].hModule;
}
lpLibrary->wModules--;
SendMessage( hWnd, WM_COMMAND, GetProp(hWnd,CBFORMAT), 0L );
WriteProfileString( "Clipboard", szFmt, "" );
FreeLibrary( hOldLib );
} else
WARNING( hWnd, "Attempt to remove invalid library!" );
GlobalUnlock( GetProp(hWnd,CBLIBRARY) );
} else
WARNING( hWnd, "Unable to access instance data!" );
}
break;
case WM_SIZE : /* window being resized */
/* reposition status window (even if invisible) */
MoveWindow(
GetProp(hWnd,HWNDSTATUS),
-1,
-1,
LOWORD(lParam)+2,
GetSystemMetrics(SM_CYMENU)+2,
TRUE
);
/* reposition client window (always visible) */
MoveWindow(
GetProp(hWnd,HWNDCLIENT),
-1,
(IsWindowVisible(GetProp(hWnd,HWNDSTATUS))) ?
GetSystemMetrics(SM_CYMENU) : -1,
LOWORD(lParam)+2,
(IsWindowVisible(GetProp(hWnd,HWNDSTATUS))) ?
HIWORD(lParam)-GetSystemMetrics(SM_CYMENU)+1 :
HIWORD(lParam)+2,
TRUE
);
/* reposition sizebox window (always visible) */
MoveWindow(
GetProp(hWnd,HWNDSIZEBOX),
LOWORD(lParam)-GetSystemMetrics(SM_CXVSCROLL)+1,
HIWORD(lParam)-GetSystemMetrics(SM_CYHSCROLL)+1,
GetSystemMetrics(SM_CXVSCROLL),
GetSystemMetrics(SM_CYHSCROLL),
TRUE
);
break;
case WM_DRAWCLIPBOARD : /* clipboard contents changing */
{
HMENU hSubMenu;
WORD wCrntFmt;
WORD wCrntEntry;
HANDLE hCrntData;
HANDLE hCrntModule;
char szFmtName[48];
char szMenuName[48];
/* filter out spurious WM_DRAWCLIPBOARD messages */
if ( GetProp(hWnd,CBGETDATA) = = NULL ) {
/* pass the message down the chain first */
if ( GetProp(hWnd,CBCHAIN) )
SendMessage( GetProp(hWnd,CBCHAIN), wMsg, wParam, lParam );
/* retrieve handle to view sub-menu */
hSubMenu = GetSubMenu( GetMenu(hWnd), 1 );
/* remove old clipboard formats */
SetProp( hWnd, CBFORMAT, IDM_FORMATS );
for ( wCrntEntry = GetMenuItemCount(hSubMenu)-1; wCrntEntry>1;
wCrntEntry-- )
ChangeMenu(
hSubMenu,
wCrntEntry,
(wCrntEntry>2) ? NULL : "&1. (Empty)",
(wCrntEntry>2) ? NULL : IDM_FORMATS,
(wCrntEntry>2) ? MF_DELETE|MF_BYPOSITION :
MF_CHANGE|MF_GRAYED|MF_BYPOSITION
);
/* open clipboard */
if ( OpenClipboard(hWnd) ) {
/* enumerate available clipboard formats */
wCrntEntry = 0;
hCrntModule = NULL;
wCrntFmt = EnumClipboardFormats( NULL );
while ( wCrntFmt ) {
/* define new menu entry */
GetClipboardFmtName( wCrntFmt, szFmtName,
sizeof(szFmtName), TRUE );
sprintf( szMenuName, "&%u. %s", wCrntEntry+1,
(szFmtName[0]) ? szFmtName : "Undefined" );
/* update view menu */
ChangeMenu(
hSubMenu,
(wCrntEntry) ? NULL : IDM_FORMATS,
szMenuName,
IDM_FORMATS + wCrntFmt,
(wCrntEntry) ? MF_APPEND : MF_CHANGE
);
/* define selected format */
if ( hCrntModule = = NULL ) {
SetProp( hWnd, CBFORMAT, IDM_FORMATS+wCrntFmt );
hCrntModule=GetClipboardModule(wCrntFmt,
GetProp(hWnd,CBLIBRARY));
}
/* retrieve next available format */
wCrntEntry++;
wCrntFmt = EnumClipboardFormats( wCrntFmt );
}
/* attempt to retrieve data handle - semaphore call */
wCrntFmt = GetProp(hWnd,CBFORMAT) - IDM_FORMATS;
if ( wCrntFmt ) {
SetProp( hWnd, CBGETDATA, TRUE );
hCrntData = GetClipboardData( wCrntFmt );
SetProp( hWnd, CBGETDATA, NULL );
} else {
hCrntData = NULL;
hCrntModule = NULL;
}
/* close clipboard */
CloseClipboard();
/* mark selected format */
CheckMenuItem(hSubMenu, GetProp(hWnd,CBFORMAT), MF_CHECKED);
/* notify child windows - clipboard now closed */
SendMessage( GetProp(hWnd,HWNDSTATUS), WM_UPDATE, wCrntFmt,
MAKELONG(hCrntData,hCrntModule) );
SendMessage( GetProp(hWnd,HWNDCLIENT), WM_UPDATE, wCrntFmt,
MAKELONG(hCrntData,hCrntModule) );
} else
WARNING( hWnd, "Unable to open clipboard!" );
}
}
break;
case WM_CHANGECBCHAIN : /* clipboard viewer chain being changed */
/* re-link viewer chain */
if ( wParam = = GetProp(hWnd,CBCHAIN) )
SetProp( hWnd, CBCHAIN, LOWORD(lParam) );
else
if ( GetProp(hWnd,CBCHAIN) )
SendMessage( GetProp(hWnd,CBCHAIN), wMsg, wParam, lParam );
break;
case WM_DESTROY : /* destroy window */
{
WORD wEntry;
LPLIBRARY lpLibrary;
/* retrieve & lock module library */
lpLibrary = (LPLIBRARY)GlobalLock( GetProp(hWnd,CBLIBRARY) );
if ( lpLibrary ) {
/* free each listed library */
for ( wEntry=0; wEntry<lpLibrary->wModules; wEntry++ )
FreeLibrary( lpLibrary->Module[wEntry].hModule );
/* unlock data structure */
GlobalUnlock( GetProp(hWnd,CBLIBRARY) );
}
/* free allocated memory & unlink from clipboard chain */
GlobalFree( RemoveProp(hWnd,CBLIBRARY) );
ChangeClipboardChain( hWnd, RemoveProp(hWnd,CBCHAIN) );
/* remove remaining properties */
RemoveProp( hWnd, CBFORMAT );
RemoveProp( hWnd, CBGETDATA );
RemoveProp( hWnd, HWNDSTATUS );
RemoveProp( hWnd, HWNDCLIENT );
RemoveProp( hWnd, HWNDSIZEBOX );
/* end it all */
PostQuitMessage( 0 );
}
break;
default : /* send to default */
lResult = DefWindowProc( hWnd, wMsg, wParam, lParam );
break;
}
/* return normal result */
return( lResult );
}
Figure 8
/*
* WINDOWS CLIPBOARD VIEWER - DIALOG SOURCE CODE
*
* LANGUAGE : Microsoft C 5.1
* MODEL : medium
* ENVIRONMENT : Microsoft Windows 2.1 SDK
* STATUS : operational
*
*/
#define NOCOMM
#include <windows.h>
#include <string.h>
#include "viewer.h"
/*
* Dialog( hParentWnd, lpszTemplate, lpfnDlgProc ) : BOOL
*
* hParentWnd handle to parent window
* lpszTemplate dialog box template
* lpfnDlgProc dialog window function
*
* This utility function displays the specified dialog box, using the
* template provided. It automatically makes a new instance of the
* dialog box function. Note that this function will NOT work
* correctly if an invalid or NULL parent window handle is provided.
*
*/
BOOL FAR PASCAL Dialog( hParentWnd, lpszTemplate, lpfnDlgProc )
HWND hParentWnd;
LPSTR lpszTemplate;
FARPROC lpfnDlgProc;
{
/* local variables */
BOOL bResult;
FARPROC lpProc;
/* display palette dialog box */
lpProc = MakeProcInstance( lpfnDlgProc, INSTANCE(hParentWnd) );
bResult = DialogBox( INSTANCE(hParentWnd), lpszTemplate,
hParentWnd, lpProc );
FreeProcInstance( lpProc );
/* return result */
return( bResult );
}
/*
* AddFormatDlgFn( hDlg, wMsg, wParam, lParam ) : BOOL ;
*
* hDlg handle to dialog box
* wMsg message or event
* wParam word portion of message
* lParam long portion of message
*
* This function is responsible for adding a new dynamic library
* to the list of supported formats. While doing so it checks to
* make sure that the specified format is not referenced twice.
* After adding a new format, this function also updates WIN.INI to
* reference the dynamic library.
*
*/
BOOL FAR PASCAL AddFormatDlgFn(
HWND hDlg,
WORD wMsg,
WORD wParam,
LONG lParam )
{
BOOL bResult;
/* initialization */
bResult = TRUE;
/* process message */
switch( wMsg )
{
case WM_INITDIALOG :
CenterPopup( hDlg, GetParent(hDlg) );
EnableWindow( GetDlgItem(hDlg,IDADD), FALSE );
break;
case WM_COMMAND :
/* process sub-message */
switch( wParam )
{
case IDCANCEL :
EndDialog( hDlg, FALSE );
break;
case IDADD :
{
char szFmt[32];
char szLib[64];
/* retrieve format & library names */
GetDlgItemText( hDlg, IDFORMAT, szFmt, sizeof(szFmt) );
GetDlgItemText( hDlg, IDLIBRARY, szLib, sizeof(szLib) );
/* upshift library name */
strupr( szLib );
/* end dialog & add library to list */
EndDialog( hDlg, TRUE );
SendMessage( GetParent(hDlg), WM_ADDFMT,
GetClipboardFmtNumber(szFmt),
(LONG)(LPSTR)szLib );
}
break;
case IDFORMAT :
case IDLIBRARY :
/* enable or disable add button */
if ( HIWORD(lParam) = = EN_CHANGE )
EnableWindow(GetDlgItem(hDlg,IDADD),
( SendMessage(GetDlgItem(hDlg,IDFORMAT),
WM_GETTEXTLENGTH,0,0L) &&
SendMessage(GetDlgItem(hDlg,IDLIBRARY),
WM_GETTEXTLENGTH,0,0L)
) ? TRUE : FALSE
);
break;
default :
bResult = FALSE;
break;
}
break;
default :
bResult = FALSE;
break;
}
/* return final result */
return( bResult );
}
/*
* RemFormatDlgFn( hDlg, wMsg, wParam, lParam ) : BOOL ;
*
* hDlg handle to dialog box
* wMsg message or event
* wParam word portion of message
* lParam long portion of message
*
* This function is responsible for removing the display dynamic
* library support for a particular clipboard format. While doing
* so is automatically updates WIN.INI and the parent window
* instance variables.
*
*/
BOOL FAR PASCAL RemFormatDlgFn(
HWND hDlg,
WORD wMsg,
WORD wParam,
LONG lParam )
{
BOOL bResult;
/* initialization */
bResult = TRUE;
/* process message */
switch( wMsg )
{
case WM_INITDIALOG :
{
WORD wEntry;
WORD wModules;
HWND hWndLibList;
char * pszEntry;
char szList[512];
/* center window */
CenterPopup( hDlg, GetParent(hDlg) );
EnableWindow( GetDlgItem(hDlg,IDREMOVE), FALSE );
/* initialize dialog box */
wModules = (WORD)SendMessage( GetParent(hDlg), WM_GETFMT, 0,
(LONG)(LPSTR)szList );
if ( wModules > 0 ) {
/* initialize listbox */
hWndLibList = GetDlgItem( hDlg, IDLIBLIST );
SendMessage(hWndLibList,WM_SETREDRAW, (WORD)FALSE, (LONG)0 );
SendMessage(hWndLibList,LB_RESETCONTENT, (WORD)0, (LONG)0 );
/* retrieve and display each listed library module */
pszEntry = &szList[0];
for ( wEntry=0; wEntry<wModules; wEntry++ ) {
SendMessage( hWndLibList, LB_ADDSTRING, (WORD)0,
(LONG)(LPSTR)pszEntry );
pszEntry + = strlen(pszEntry) + 1;
}
/* display listbox */
SendMessage(hWndLibList,WM_SETREDRAW,(WORD)TRUE,(LONG)0);
InvalidateRect( hWndLibList, (LPRECT)NULL, TRUE );
}
}
break;
case WM_COMMAND :
/* process sub-message */
switch( wParam )
{
case IDLIBLIST :
/* enable remove button if library selected */
if (HIWORD(lParam) = = LBN_SELCHANGE)
EnableWindow( GetDlgItem(hDlg,IDREMOVE), TRUE );
break;
case IDREMOVE :
{
WORD wEntry;
/* retrieve selected library index & notify parent */
wEntry = (WORD)SendMessage( GetDlgItem(hDlg,IDLIBLIST),
LB_GETCURSEL, 0, 0L );
if ( SendMessage(GetParent(hDlg),WM_REMFMT,wEntry,0L) )
EndDialog( hDlg, TRUE );
}
break;
case IDCANCEL :
EndDialog( hDlg, FALSE );
break;
default :
bResult = FALSE;
break;
}
break;
default :
bResult = FALSE;
break;
}
/* return final result */
return( bResult );
}
/*
* AboutDlgFn( hDlg, wMsg, wParam, lParam ) : BOOL ;
*
* hDlg handle to dialog box
* wMsg message or event
* wParam word portion of message
* lParam long portion of message
*
* This function is responsible for processing all the messages
* that relate to the Viewer about dialog box. About the only
* acts this function performs is to center the dialog box and
* wait for the Ok button to be pressed.
*
*/
BOOL FAR PASCAL AboutDlgFn(
HWND hDlg,
WORD wMsg,
WORD wParam,
LONG lParam )
{
BOOL bResult;
/* warning level 3 compatibility */
lParam;
/* initialization */
bResult = TRUE;
/* process message */
switch( wMsg )
{
case WM_INITDIALOG :
CenterPopup( hDlg, GetParent(hDlg) );
break;
case WM_COMMAND :
/* process sub-message */
switch( wParam )
{
case IDOK :
EndDialog( hDlg, TRUE );
break;
case IDCANCEL :
EndDialog( hDlg, FALSE );
break;
default :
bResult = FALSE;
break;
}
break;
default :
bResult = FALSE;
break;
}
/* return final result */
return( bResult );
}
Figure 9
/*
* WINDOWS CLIPBOARD VIEWER - STATUS & CLIENT WINDOW SOURCE CODE
*
* LANGUAGE : Microsoft C 5.1
* MODEL : medium
* ENVIRONMENT : Microsoft Windows 2.1 SDK
* STATUS : operational
*
* Note that both the client and status windows save the current
* clipboard format and data handle. In general this is NOT good
* programming practice. In this situation we assume that we will
* always be notifed when ANY change occurs to the clipboard.
*
* 1.01 - Kevin P. Welch - add param to GetClipboardFmtName.
*
*/
#define NOCOMM
#include <windows.h>
#include <string.h>
#include <stdio.h>
#include "viewer.h"
/* client window properties */
#define CLIENT_FORMAT MAKEINTRESOURCE(1)
#define CLIENT_MODULE MAKEINTRESOURCE(2)
#define CLIENT_DISPINFO MAKEINTRESOURCE(3)
/* status window properties */
#define STATUS_DATA MAKEINTRESOURCE(1)
#define STATUS_FORMAT MAKEINTRESOURCE(2)
#define STATUS_DISPFONT MAKEINTRESOURCE(3)
/*
* StatusWndFn( hWnd, wMsg, wParam, lParam ) : LONG
*
* hWnd window handle
* wMsg message number
* wParam additional message information
* lParam additional message information
*
* This window function processes all the messages related to
* the status child window of the clipboard viewer.
*
*/
LONG FAR PASCAL StatusWndFn( hWnd, wMsg, wParam, lParam )
HWND hWnd;
WORD wMsg;
WORD wParam;
LONG lParam;
{
LONG lResult;
/* initialization */
lResult = FALSE;
/* process each message */
switch( wMsg )
{
case WM_CREATE : /* window being created */
{
HDC hDC;
LOGFONT LogFont;
TEXTMETRIC TextMetric;
/* retrieve system font metrics */
hDC = GetDC( hWnd );
GetTextMetrics( hDC, &TextMetric );
ReleaseDC( hWnd, hDC );
/* define display font */
LogFont.lfHeight = TextMetric.tmHeight;
LogFont.lfWidth = TextMetric.tmAveCharWidth;
LogFont.lfEscapement = 0;
LogFont.lfOrientation = 0;
LogFont.lfWeight = FW_MEDIUM;
LogFont.lfItalic = FALSE;
LogFont.lfUnderline = FALSE;
LogFont.lfStrikeOut = FALSE;
LogFont.lfCharSet = ANSI_CHARSET;
LogFont.lfOutPrecision = OUT_STROKE_PRECIS;
LogFont.lfClipPrecision = CLIP_STROKE_PRECIS;
LogFont.lfQuality = PROOF_QUALITY;
LogFont.lfPitchAndFamily = DEFAULT_PITCH|FF_MODERN;
strcpy( LogFont.lfFaceName, "Helv" );
/* define property lists */
SetProp( hWnd, STATUS_DATA, NULL );
SetProp( hWnd, STATUS_FORMAT, NULL );
SetProp( hWnd, STATUS_DISPFONT, CreateFontIndirect(&LogFont) );
}
break;
case WM_UPDATE : /* update command from parent */
/* update property lists */
SetProp( hWnd, STATUS_DATA, LOWORD(lParam) );
SetProp( hWnd, STATUS_FORMAT, wParam );
/* force update of window */
InvalidateRect( hWnd, NULL, TRUE );
break;
case WM_PAINT : /* paint window */
{
PAINTSTRUCT Ps;
WORD wCrntFmt;
HFONT hOldFont;
RECT rcClient;
HANDLE hCrntData;
TEXTMETRIC TextMetric;
char szFmtSize[32];
char szFmtName[80];
char szFmtOwner[80];
/* retrieve property list data */
hCrntData = GetProp( hWnd, STATUS_DATA );
wCrntFmt = GetProp( hWnd, STATUS_FORMAT );
/* define clipboard format name */
GetClipboardFmtName(wCrntFmt,szFmtName,sizeof(szFmtName),TRUE);
/* define clipboard format owner */
if ( wCrntFmt && GetClipboardOwner() ) {
GetModuleFileName(
GetClassWord( GetClipboardOwner(), GCW_HMODULE ),
szFmtSize,
sizeof(szFmtSize)
);
sprintf( szFmtOwner, "%s from %s", szFmtName,
strrchr(szFmtSize,'\\')+1 );
strcpy( szFmtName, szFmtOwner );
}
/* define clipboard format size */
switch( wCrntFmt )
{
case NULL : /* empty */
strcpy( szFmtSize, "0 bytes" );
break;
case CF_BITMAP : /* standard GDI bitmap */
case CF_DSPBITMAP : /* display bitmap */
{
BITMAP bmStruct;
/* retrieve bitmap object */
if (GetObject(hCrntData,sizeof(BITMAP),(LPSTR)&bmStruct)= =
sizeof(BITMAP) ) {
sprintf(
szFmtSize,
" - %u x %u%s",
bmStruct.bmWidth,
bmStruct.bmHeight,
((bmStruct.bmPlanes= =1)&&(bmStruct.bmBitsPixel= =1)) ?
" - mono" : ""
);
strcat( szFmtName, szFmtSize );
sprintf(
szFmtSize,
"%ld bytes",
sizeof(BITMAP) + ((LONG)bmStruct.bmWidthBytes *
bmStruct.bmPlanes * bmStruct.bmHeight)
);
} else
strcpy( szFmtSize, "(size unknown)" );
}
break;
case CF_METAFILEPICT : /* standard GDI metafile */
case CF_DSPMETAFILEPICT : /* display metafile */
{
LPMETAFILEPICT lpmfStruct;
char szMapMode[32];
/* retrieve bitmap object */
lpmfStruct = (LPMETAFILEPICT)GlobalLock( hCrntData );
if ( lpmfStruct ) {
switch( lpmfStruct->mm )
{
case MM_TEXT :
sprintf( szFmtSize, " - %u x %u - text",
lpmfStruct->xExt, lpmfStruct->yExt );
break;
case MM_LOMETRIC :
sprintf( szFmtSize, " - %u x %u - low metric",
lpmfStruct->xExt, lpmfStruct->yExt );
break;
case MM_HIMETRIC :
sprintf( szFmtSize, " - %u x %u - high metric",
lpmfStruct->xExt, lpmfStruct->yExt );
break;
case MM_LOENGLISH :
sprintf( szFmtSize, " - %u x %u - low english",
lpmfStruct->xExt, lpmfStruct->yExt );
break;
case MM_HIENGLISH :
sprintf( szFmtSize, " - %u x %u - high english",
lpmfStruct->xExt, lpmfStruct->yExt );
break;
case MM_TWIPS :
sprintf( szFmtSize, " - %u x %u - twips",
lpmfStruct->xExt, lpmfStruct->yExt );
break;
case MM_ISOTROPIC :
sprintf( szFmtSize, " - isotropic" );
break;
case MM_ANISOTROPIC :
sprintf( szFmtSize, " - anisotropic" );
break;
default :
strcpy( szMapMode, "" );
break;
}
strcat( szFmtName, szFmtSize );
sprintf(
szFmtSize,
"%ld bytes",
GlobalSize(hCrntData)+GlobalSize(lpmfStruct->hMF)
);
UnlockData( hCrntData );
} else
strcpy( szFmtSize, "(size unknown)" );
}
break;
case CF_TEXT : /* standard text */
case CF_SYLK : /* standard SYLK text */
case CF_DIF : /* standard DIF text */
case CF_TIFF : /* standard binary TIFF data */
case CF_OEMTEXT : /* standard OEM text */
case CF_OWNERDISPLAY : /* owner display */
case CF_DSPTEXT : /* display text */
default : /* something else */
sprintf( szFmtSize, "%ld bytes", GlobalSize(hCrntData) );
break;
}
/* make sure entire window is updated */
InvalidateRect( hWnd, NULL, TRUE );
/* start painting */
BeginPaint( hWnd, &Ps );
GetTextMetrics( Ps.hdc, &TextMetric );
hOldFont = SelectObject( Ps.hdc, GetProp(hWnd,
STATUS_DISPFONT) );
/* retrieve & adjust client rectangle for drawing */
GetClientRect( hWnd, &rcClient );
rcClient.left + = TextMetric.tmAveCharWidth;
rcClient.right -= TextMetric.tmAveCharWidth;
/* update clipboard format name & size */
DrawText( Ps.hdc, szFmtName, -1, &rcClient,
DT_LEFT|DT_VCENTER|DT_SINGLELINE );
DrawText( Ps.hdc, szFmtSize, -1, &rcClient,
DT_RIGHT|DT_VCENTER|DT_SINGLELINE );
/* end painting */
SelectObject( Ps.hdc, hOldFont );
EndPaint( hWnd, &Ps );
}
break;
case WM_CLOSE : /* window being closed */
/* delete display font */
DeleteObject( GetProp(hWnd,STATUS_DISPFONT) );
/* remove property lists */
RemoveProp( hWnd, STATUS_DATA );
RemoveProp( hWnd, STATUS_FORMAT );
RemoveProp( hWnd, STATUS_DISPFONT );
/* end it all */
DestroyWindow( hWnd );
break;
default : /* send to default */
lResult = DefWindowProc( hWnd, wMsg, wParam, lParam );
break;
}
/* return normal result */
return( lResult );
}
/*
* ClientWndFn( hWnd, wMsg, wParam, lParam ) : LONG
*
* hWnd window handle
* wMsg message number
* wParam additional message information
* lParam additional message information
*
* This window function processes all the messages related to
* the client area of the clipboard viewer.
*
*/
LONG FAR PASCAL ClientWndFn( hWnd, wMsg, wParam, lParam )
HWND hWnd;
WORD wMsg;
WORD wParam;
LONG lParam;
{
FARPROC lpFn;
LONG lResult;
/* initialization */
lResult = FALSE;
/* process each message */
switch( wMsg )
{
case WM_CREATE : /* window being created */
/* define property lists */
SetProp( hWnd, CLIENT_FORMAT, NULL );
SetProp( hWnd, CLIENT_MODULE, NULL );
SetProp( hWnd, CLIENT_DISPINFO, NULL );
break;
case WM_UPDATE : /* update command from parent */
/* check if clipboard data present */
if (GetProp(hWnd,CLIENT_FORMAT) && GetProp(hWnd,CLIENT_MODULE)) {
lpFn=GetProcAddress(GetProp(hWnd,CLIENT_MODULE),LIB_DESTROY );
(*lpFn)( hWnd, GetProp(hWnd,CLIENT_DISPINFO) );
}
/* update property lists */
SetProp( hWnd, CLIENT_FORMAT, wParam );
SetProp( hWnd, CLIENT_MODULE, HIWORD(lParam) );
if ( wParam && HIWORD(lParam) ) {
lpFn = GetProcAddress( GetProp(hWnd,CLIENT_MODULE), LIB_CREATE);
SetProp( hWnd, CLIENT_DISPINFO, (*lpFn)(hWnd,LOWORD(lParam)) );
} else {
SetProp( hWnd, CLIENT_DISPINFO, NULL );
SetScrollPos( hWnd, SB_HORZ, 0, TRUE );
SetScrollPos( hWnd, SB_VERT, 0, TRUE );
}
/* force update of window */
InvalidateRect( hWnd, NULL, TRUE );
break;
case WM_SIZE : /* window being sized */
case WM_HSCROLL : /* horizontal scroll request */
case WM_VSCROLL : /* vertical scroll request */
case WM_PAINT : /* window being painted */
/* determine if a responsible library function available */
if (GetProp(hWnd,CLIENT_FORMAT) && GetProp(hWnd,CLIENT_MODULE)) {
LPSTR lpszLibrary;
/* determine which library is responsible */
switch( wMsg )
{
case WM_SIZE :
lpszLibrary = LIB_SIZE;
break;
case WM_HSCROLL :
lpszLibrary = LIB_HSCROLL;
break;
case WM_VSCROLL :
lpszLibrary = LIB_VSCROLL;
break;
case WM_PAINT :
lpszLibrary = LIB_PAINT;
break;
}
/* call display library function */
lpFn = GetProcAddress(GetProp(hWnd,CLIENT_MODULE),lpszLibrary);
(*lpFn)( hWnd, GetProp(hWnd,CLIENT_DISPINFO), wParam, lParam );
} else
lResult = DefWindowProc( hWnd, wMsg, wParam, lParam );
break;
case WM_CLOSE : /* window being closed */
/* pass to responsible destroy library function */
if (GetProp(hWnd,CLIENT_FORMAT) && GetProp(hWnd,CLIENT_MODULE)) {
lpFn = GetProcAddress(GetProp(hWnd,CLIENT_MODULE),LIB_DESTROY );
(*lpFn)( hWnd, GetProp(hWnd,CLIENT_DISPINFO) );
}
/* remove properties */
RemoveProp( hWnd, CLIENT_FORMAT );
RemoveProp( hWnd, CLIENT_MODULE );
RemoveProp( hWnd, CLIENT_DISPINFO );
/* end it all */
DestroyWindow( hWnd );
break;
default : /* send to default */
lResult = DefWindowProc( hWnd, wMsg, wParam, lParam );
break;
}
/* return normal result */
return( lResult );
}
Figure 10
/*
* WINDOWS CLIPBOARD VIEWER - UTILITY SOURCE CODE
*
* LANGUAGE : Microsoft C 5.1
* MODEL : medium
* ENVIRONMENT : Microsoft Windows 2.1 SDK
* STATUS : operational
*
* 1.01- Kevin P. Welch - add param to GetClipboardFmtName.
*
*/
#define NOCOMM
#include <windows.h>
#include "viewer.h"
/*
* CenterPopup( hWnd, hParentWnd ) : BOOL
*
* hWnd window handle
* hParentWnd parent window handle
*
* This routine centers the popup window in the screen or display
* using the window handles provided. The window is centered over
* the parent if the parent window is valid. Special provision
* is made for the case when the popup would be centered outside
* the screen - in this case it is positioned at the appropriate
* border.
*
*/
BOOL FAR PASCAL CenterPopup(
HWND hWnd,
HWND hParentWnd
)
{
/* local variables */
int xPopup; /* popup x position */
int yPopup; /* popup y position */
int cxPopup; /* width of popup window */
int cyPopup; /* height of popup window */
int cxScreen; /* width of main display */
int cyScreen; /* height of main display */
int cxParent; /* width of parent window */
int cyParent; /* height of parent window */
RECT rcWindow; /* temporary window rect */
/* retrieve main display dimensions */
cxScreen = GetSystemMetrics( SM_CXSCREEN );
cyScreen = GetSystemMetrics( SM_CYSCREEN );
/* retrieve popup rectangle */
GetWindowRect( hWnd, (LPRECT)&rcWindow );
/* calculate popup extents */
cxPopup = rcWindow.right - rcWindow.left;
cyPopup = rcWindow.bottom - rcWindow.top;
/* calculate bounding rectangle */
if ( hParentWnd ) {
/* retrieve parent rectangle */
GetWindowRect( hParentWnd, (LPRECT)&rcWindow );
/* calculate parent extents */
cxParent = rcWindow.right - rcWindow.left;
cyParent = rcWindow.bottom - rcWindow.top;
/* center within parent window */
xPopup = rcWindow.left + ((cxParent - cxPopup)/2);
yPopup = rcWindow.top + ((cyParent - cyPopup)/2);
/* adjust popup x-location for screen size */
if ( xPopup+cxPopup > cxScreen )
xPopup = cxScreen - cxPopup;
/* adjust popup y-location for screen size */
if ( yPopup+cyPopup > cyScreen )
yPopup = cyScreen - cyPopup;
} else {
/* center within entire screen */
xPopup = (cxScreen - cxPopup) / 2;
yPopup = (cyScreen - cyPopup) / 2;
}
/* move window to new location & display */
MoveWindow(
hWnd,
( xPopup > 0 ) ? xPopup : 0,
( yPopup > 0 ) ? yPopup : 0,
cxPopup,
cyPopup,
TRUE
);
/* normal return */
return( TRUE );
}
/*
* GetClipboardFmtName( wFmt, lpszFmt, wMax, bInquire ) : WORD;
*
* wFmt clipboard format number
* lpszFmt name of clipboard format
* wMax maximum name size
* bInquire inquire full name if owner-display
*
* This utility function is identical to GetClipboardFormatName, but
* is capable of defining the name of ANY clipboard format, including
* predefined and owner-display ones. Value returned is the number
* of bytes copied to the clipboard format name. If this value is
* zero then the clipboard format number specified is undefined.
*
* Note that if the bInquire flag is TRUE, this function will attempt
* to ask the clipboard owner for the full name of the owner display
* data. This may result in misleading information if this function
* is called with an owner display format number when the real owner
* is not currently present!
*
*/
WORD FAR PASCAL GetClipboardFmtName(
WORD wFmt,
LPSTR lpszFmt,
WORD wMax,
BOOL bInquire )
{
HANDLE hTemp;
LPSTR lpszTemp;
/* initialization */
lpszFmt[0] = 0;
/* define format name */
switch( wFmt )
{
case NULL : /* empty */
lstrcpy( lpszFmt, "(Clipboard Empty)" );
break;
case CF_TEXT : /* standard text */
lstrcpy( lpszFmt, "Text" );
break;
case CF_BITMAP : /* standard GDI bitmap */
lstrcpy( lpszFmt, "Bitmap" );
break;
case CF_METAFILEPICT : /* standard GDI metafile */
lstrcpy( lpszFmt, "Picture" );
break;
case CF_SYLK : /* standard SYLK text */
lstrcpy( lpszFmt, "SYLK" );
break;
case CF_DIF : /* standard DIF text */
lstrcpy( lpszFmt, "DIF" );
break;
case CF_TIFF : /* standard binary TIFF data */
lstrcpy( lpszFmt, "TIFF" );
break;
case CF_OEMTEXT : /* standard OEM text */
lstrcpy( lpszFmt, "OEM Text" );
break;
case CF_OWNERDISPLAY : /* owner display */
lstrcpy( lpszFmt, "Owner Display" );
if ( bInquire && GetClipboardOwner() ) {
hTemp = GlobalAlloc( GHND, (DWORD)64 );
if ( hTemp ) {
lpszTemp = GlobalLock( hTemp );
if ( lpszTemp ) {
SendMessage( GetClipboardOwner(), WM_ASKCBFORMATNAME,
63, (LONG)lpszTemp );
lstrcpy( lpszFmt, lpszTemp );
GlobalUnlock( hTemp );
}
GlobalFree( hTemp );
}
}
break;
case CF_DSPTEXT : /* display text */
lstrcpy( lpszFmt, "Display Text" );
break;
case CF_DSPBITMAP : /* display bitmap */
lstrcpy( lpszFmt, "Display Bitmap" );
break;
case CF_DSPMETAFILEPICT : /* display picture */
lstrcpy( lpszFmt, "Display Picture" );
break;
default : /* something else */
if ( GetClipboardFormatName(wFmt,lpszFmt,wMax) = = 0 )
lstrcpy( lpszFmt, "(Undefined)" );
break;
}
/* return size of string */
return( (WORD)lstrlen(lpszFmt) );
}
/*
* GetClipboardFmtNumber( lpszFmt ) : WORD;
*
* lpszFmt name of clipboard format
*
* This function retrieves (and if necessary, defines) the internal
* clipboard format number for the specified string. Before checking
* registered clipboard formats, this function checks to see if one
* of the predefined formats is referenced.
*
*/
WORD FAR PASCAL GetClipboardFmtNumber(
LPSTR lpszFmt )
{
WORD wFmt;
/* check predefined formats */
if ( lstrcmp(lpszFmt,"Text") = = 0 )
wFmt = CF_TEXT;
else
if ( lstrcmp(lpszFmt,"Bitmap") = = 0 )
wFmt = CF_BITMAP;
else
if ( lstrcmp(lpszFmt,"Picture") = = 0 )
wFmt = CF_METAFILEPICT;
else
if ( lstrcmp(lpszFmt,"SYLK") = = 0 )
wFmt = CF_SYLK;
else
if ( lstrcmp(lpszFmt,"DIF") = = 0 )
wFmt = CF_DIF;
else
if ( lstrcmp(lpszFmt,"TIFF") = = 0 )
wFmt = CF_TIFF;
else
if ( lstrcmp(lpszFmt,"OEM Text") = = 0 )
wFmt = CF_OEMTEXT;
else
if ( lstrcmp(lpszFmt,"Owner Display") = = 0 )
wFmt = CF_OWNERDISPLAY;
else
if ( lstrcmp(lpszFmt,"Display Text") = = 0 )
wFmt = CF_DSPTEXT;
else
if ( lstrcmp(lpszFmt,"Display Bitmap") = = 0 )
wFmt = CF_DSPBITMAP;
else
if ( lstrcmp(lpszFmt,"Display Picture") = = 0 )
wFmt = CF_DSPMETAFILEPICT;
else
wFmt = RegisterClipboardFormat( lpszFmt );
/* return format */
return( wFmt );
}
/*
* GetClipboardModule( wCrntFmt, hLibrary ) : HANDLE;
*
* wCrntFmt current clipboard foramt
* hLibrary handle to clipboard support library
*
* This function searches the clipboard module library for one which
* can support the format in question. If none is present a value of
* NULL is returned. This should be interpreted that the current
* format is not supported by the system.
*
*/
HANDLE FAR PASCAL GetClipboardModule(
WORD wCrntFmt,
HANDLE hLibrary )
{
WORD wEntry;
HANDLE hModule;
LPLIBRARY lpLibrary;
/* initialization */
hModule = NULL;
lpLibrary = (LPLIBRARY)GlobalLock( hLibrary );
if ( lpLibrary ) {
/* search library for module */
for ( wEntry=0;(wEntry<lpLibrary->wModules)&&(hModule= =NULL);
wEntry++ )
if ( lpLibrary->Module[wEntry].wFormat = = wCrntFmt )
hModule = lpLibrary->Module[wEntry].hModule;
/* unlock library */
GlobalUnlock( hLibrary );
}
/* return module handle */
return( hModule );
}
Figure 11
BITMAP
DLLFLAGS=-c -u -Asnw -FPa -Gsw -Os -W2 -Zep
bitmap1.obj: bitmap1.asm
masm bitmap1;
bitmap2.obj: bitmap2.c
cl $(DLLFLAGS) bitmap2.c
bitmap.dll: bitmap1.obj bitmap2.obj bitmap.def
link4 bitmap1+bitmap2 /AL:16,bitmap.dll,,swinlibc+slibw,bitmap.def
BITMAP.DEF
LIBRARY BITMAP
STUB 'WINSTUB.EXE'
DESCRIPTION 'Bitmap Format Display Library '
CODE MOVEABLE DISCARDABLE SHARED
DATA MOVEABLE SINGLE
EXPORTS
BitmapInit @1
BitmapCreate @2
BitmapSize @3
BitmapHScroll @4
BitmapVScroll @5
BitmapPaint @6
BitmapDestroy @7
BITMAP1.ASM
;
; WINDOWS CLIPBOARD VIEWER - BITMAP FORMAT SOURCE CODE
;
; LANGUAGE : Microsoft Macro Assembler 5.1
; MODEL : small
; ENVIRONMENT : Microsoft Windows 2.1 SDK
; STATUS : operational
;
;
Extrn BitmapInit:Near
_TEXT SEGMENT BYTE PUBLIC 'CODE'
ASSUME CS:_TEXT
PUBLIC LibInit
LibInit PROC FAR
Push DI ; hInstance
Push DS ; Data Segment
Push CX ; Heap Size
Push ES
Push SI ; Command Line
Call BitmapInit
Ret
LibInit ENDP
_TEXT ENDS
End LibInit
BITMAP2.C
/*
* WINDOWS CLIPBOARD VIEWER - BITMAP FORMAT SOURCE CODE
*
* LANGUAGE : Microsoft C5.1
* MODEL : small
* ENVIRONMENT : Microsoft Windows 2.1 SDK
* STATUS : operational
*
*/
#define NOCOMM
#include <windows.h>
#define HORZ_BORDER 2
#define VERT_BORDER 2
#define HORZ_STEPSIZE 8
#define VERT_STEPSIZE 8
typedef struct {
HANDLE hBmp;
HANDLE hData;
POINT ptOrg;
RECT rcWnd;
RECT rcBmp;
HDC hScrDC;
HDC hMemDC;
} DISPINFO;
typedef DISPINFO FAR * LPDISPINFO;
BOOL PASCAL BitmapInit( HANDLE, WORD, WORD, LPSTR );
HANDLE FAR PASCAL BitmapCreate( HWND, HANDLE );
HANDLE FAR PASCAL BitmapSize( HWND, HANDLE, WORD, LONG );
HANDLE FAR PASCAL BitmapHScroll( HWND, HANDLE, WORD, LONG );
HANDLE FAR PASCAL BitmapVScroll( HWND, HANDLE, WORD, LONG );
HANDLE FAR PASCAL BitmapPaint( HWND, HANDLE, WORD, LONG );
HANDLE FAR PASCAL BitmapDestroy( HWND, HANDLE );
/* BOOL
* BitmapInit( hInstance, wDataSegment, wHeapSize, lpszCmdLine )
*
* hInstance library instance handle
* wDataSegment library data segment
* wHeapSize default heap size
* lpszCmdLine command line arguments
*
* This function performs all the initialization necessary to use
* the bitmap viewer display dynamic library. It is assumed that no
* local heap is used, hence no call to LocalInit. A non-zero value
* is returned if the initialization was sucessful.
*
*/
BOOL PASCAL BitmapInit(hInstance,wDataSegment,wHeapSize,lpszCmdLine)
HANDLE hInstance;
WORD wDataSegment;
WORD wHeapSize;
LPSTR lpszCmdLine;
{
/* warning level 3 compatibility */
hInstance;
wDataSegment;
wHeapSize;
lpszCmdLine;
/* sucessful return */
return( TRUE );
}
/*
* BitmapCreate( hWnd, hClipData ) : HANDLE;
*
* hWnd handle to display window
* hClipData handle to current clipboard data
*
* This function performs all the initialization necessary in order
* to view a bitmap clipboard format. A handle to a display infor-
* mation data block (the internal format of which is only known
* inside this module) which the owner is responsible for saving.
*
*/
HANDLE FAR PASCAL BitmapCreate(
HWND hWnd,
HANDLE hClipData )
{
BITMAP Bitmap;
HANDLE hDispInfo;
WORD wWndWidth;
WORD wWndHeight;
WORD wScrollWidth;
WORD wScrollHeight;
LPDISPINFO lpDispInfo;
/* reset scroll bars */
SetScrollPos( hWnd, SB_HORZ, 0, TRUE );
SetScrollPos( hWnd, SB_VERT, 0, TRUE );
/* attempt to allocate data */
hDispInfo = GlobalAlloc( GHND, (DWORD)sizeof(DISPINFO) );
if ( hDispInfo ) {
/* lock it down */
lpDispInfo = (LPDISPINFO)GlobalLock( hDispInfo );
if ( lpDispInfo ) {
/* define bitmap data */
lpDispInfo->hData = hClipData;
/* define bitmap dimensions */
GetObject( hClipData, sizeof(BITMAP), (LPSTR)&Bitmap );
lpDispInfo->rcBmp.top = 0;
lpDispInfo->rcBmp.left = 0;
lpDispInfo->rcBmp.right = Bitmap.bmWidth;
lpDispInfo->rcBmp.bottom = Bitmap.bmHeight;
/* define window origin in bitmap coordinates */
lpDispInfo->ptOrg.x = -HORZ_BORDER;
lpDispInfo->ptOrg.y = -VERT_BORDER;
/* define window dimensions */
GetClientRect( hWnd, &lpDispInfo->rcWnd );
lpDispInfo->rcWnd.top = VERT_BORDER;
lpDispInfo->rcWnd.left = HORZ_BORDER;
lpDispInfo->rcWnd.right -= HORZ_BORDER;
lpDispInfo->rcWnd.bottom -= VERT_BORDER;
OffsetRect( &lpDispInfo->rcWnd, -HORZ_BORDER, -VERT_BORDER );
IntersectRect( &lpDispInfo->rcWnd, &lpDispInfo->rcWnd,
&lpDispInfo->rcBmp );
OffsetRect( &lpDispInfo->rcWnd, HORZ_BORDER, VERT_BORDER );
wWndWidth = lpDispInfo->rcWnd.right - lpDispInfo->rcWnd.left;
wWndHeight = lpDispInfo->rcWnd.bottom - lpDispInfo->rcWnd.top;
/* define scrollbar ranges */
wScrollWidth = ( Bitmap.bmWidth > wWndWidth ) ?
Bitmap.bmWidth - wWndWidth : 1;
wScrollHeight = ( Bitmap.bmHeight > wWndHeight ) ?
Bitmap.bmHeight - wWndHeight : 1;
SetScrollRange( hWnd, SB_HORZ, 0, wScrollWidth, FALSE );
SetScrollRange( hWnd, SB_VERT, 0, wScrollHeight, FALSE );
/* unlock data */
GlobalUnlock( hDispInfo );
} else {
GlobalFree( hDispInfo );
hDispInfo = NULL;
}
}
/* return display info handle */
return( hDispInfo );
}
/*
* BitmapSize( hWnd, hDispInfo, wParam, lParam ) : HANDLE;
*
* hWnd handle to display window
* hDispInfo handle to display information block
* wParam word parameter of WM_SIZE message
* lParam long parameter of WM_SIZE message
*
* This function resets the display information data block whenever
* the size of the display region is changed. If successful, a
* handle to the new display information data block is returned.
* Failure to call this function whenever the size of the display
* region is changed will cause unusual display results.
*
*/
HANDLE FAR PASCAL BitmapSize(
HWND hWnd,
HANDLE hDispInfo,
WORD wParam,
LONG lParam )
{
WORD wWndWidth;
WORD wBmpWidth;
WORD wWndHeight;
WORD wBmpHeight;
WORD wScrollWidth;
WORD wScrollHeight;
LPDISPINFO lpDispInfo;
/* warning level 3 compatibility */
wParam;
lParam;
/* reset scroll bars */
SetScrollPos( hWnd, SB_HORZ, 0, TRUE );
SetScrollPos( hWnd, SB_VERT, 0, TRUE );
/* lock it down display information block */
lpDispInfo = (LPDISPINFO)GlobalLock( hDispInfo );
if ( lpDispInfo ) {
/* define window origin in bitmap coordinates */
lpDispInfo->ptOrg.x = -HORZ_BORDER;
lpDispInfo->ptOrg.y = -VERT_BORDER;
/* define bitmap dimensions */
wBmpWidth = lpDispInfo->rcBmp.right - lpDispInfo->rcBmp.left;
wBmpHeight = lpDispInfo->rcBmp.bottom - lpDispInfo->rcBmp.top;
/* define window dimensions */
GetClientRect( hWnd, &lpDispInfo->rcWnd );
lpDispInfo->rcWnd.top = VERT_BORDER;
lpDispInfo->rcWnd.left = HORZ_BORDER;
lpDispInfo->rcWnd.right -= HORZ_BORDER;
lpDispInfo->rcWnd.bottom -= VERT_BORDER;
OffsetRect( &lpDispInfo->rcWnd, -HORZ_BORDER, -VERT_BORDER );
IntersectRect( &lpDispInfo->rcWnd, &lpDispInfo->rcWnd,
&lpDispInfo->rcBmp );
OffsetRect( &lpDispInfo->rcWnd, HORZ_BORDER, VERT_BORDER );
wWndWidth = lpDispInfo->rcWnd.right - lpDispInfo->rcWnd.left;
wWndHeight = lpDispInfo->rcWnd.bottom - lpDispInfo->rcWnd.top;
/* define scrollbar ranges */
wScrollWidth=(wBmpWidth>wWndWidth) ? wBmpWidth-wWndWidth : 1;
wScrollHeight=(wBmpHeight>wWndHeight) ? wBmpHeight-wWndHeight : 1;
SetScrollRange( hWnd, SB_HORZ, 0, wScrollWidth, FALSE );
SetScrollRange( hWnd, SB_VERT, 0, wScrollHeight, FALSE );
/* unlock data */
GlobalUnlock( hDispInfo );
} else
hDispInfo = NULL;
/* return final result */
return( hDispInfo );
}
/*
* BitmapHScroll( hWnd, hDispInfo, wParam, lParam ) : HANDLE;
*
* hWnd handle to display window
* hDispInfo handle to display information block
* wParam current scroll code
* lParam current scroll parameter
*
* This function is responsible for handling all the horizontal
* scroll messages received when viewing a bitmap clipboard format.
* If necessary, changes to the display information block can be
* made. As currently implemented, no action is taken. The value
* returned is the handle to the updated display information block.
*/
HANDLE FAR PASCAL BitmapHScroll(
HWND hWnd,
HANDLE hDispInfo,
WORD wParam,
LONG lParam )
{
WORD wWndWidth;
WORD wBmpWidth;
WORD wOldScrollPos;
WORD wNewScrollPos;
WORD wOldScrollRange;
LPDISPINFO lpDispInfo;
/* access display information block */
lpDispInfo = (LPDISPINFO)GlobalLock( hDispInfo );
if ( lpDispInfo ) {
/* initialization */
wWndWidth = lpDispInfo->rcWnd.right - lpDispInfo->rcWnd.left;
wBmpWidth = lpDispInfo->rcBmp.right - lpDispInfo->rcBmp.left;
wOldScrollPos = lpDispInfo->ptOrg.x + HORZ_BORDER;
wOldScrollRange=(wBmpWidth > wWndWidth) ? wBmpWidth-wWndWidth : 0;
/* define display contexts (if necessary) */
if ( lpDispInfo->hScrDC == NULL ) {
lpDispInfo->hScrDC = GetDC( hWnd );
lpDispInfo->hMemDC = CreateCompatibleDC( lpDispInfo->hScrDC );
lpDispInfo->hBmp = SelectObject( lpDispInfo->hMemDC,
lpDispInfo->hData );
}
/* process scroll message */
switch( wParam )
{
case SB_LINEUP : /* move left one line */
wNewScrollPos = (wOldScrollPos > HORZ_STEPSIZE) ?
wOldScrollPos-HORZ_STEPSIZE : 0;
break;
case SB_LINEDOWN : /* move right one line */
wNewScrollPos = (wOldScrollPos+HORZ_STEPSIZE<=wOldScrollRange) ?
wOldScrollPos+HORZ_STEPSIZE : wOldScrollRange;
break;
case SB_PAGEUP : /* move left one page */
wNewScrollPos = (wOldScrollPos > wWndWidth) ?
wOldScrollPos-wWndWidth : 0;
break;
case SB_PAGEDOWN : /* move right one page */
wNewScrollPos = (wOldScrollPos+wWndWidth <= wOldScrollRange) ?
wOldScrollPos+wWndWidth : wOldScrollRange;
break;
case SB_THUMBPOSITION : /* move to an absolute position */
case SB_THUMBTRACK : /* track the current thumb position */
wNewScrollPos = (wOldScrollRange > 1) ? LOWORD(lParam) : 0;
break;
case SB_TOP : /* move to the first line */
wNewScrollPos = 0;
break;
case SB_BOTTOM : /* move to the last line */
wNewScrollPos = wOldScrollRange;
break;
case SB_ENDSCROLL : /* end scrolling */
wNewScrollPos = wOldScrollPos;
break;
}
/* perform scroll and update (if necessary) */
if ( wNewScrollPos != wOldScrollPos ) {
/* update window */
BitBlt(
lpDispInfo->hScrDC,
HORZ_BORDER,
VERT_BORDER,
wWndWidth,
lpDispInfo->rcWnd.bottom - lpDispInfo->rcWnd.top,
lpDispInfo->hMemDC,
wNewScrollPos,
lpDispInfo->ptOrg.y + VERT_BORDER,
SRCCOPY
);
/* update origin & horizontal scrollbar */
lpDispInfo->ptOrg.x = wNewScrollPos - HORZ_BORDER;
SetScrollPos( hWnd, SB_HORZ, wNewScrollPos, TRUE );
}
/* release display context (if necessary) */
if ( (wParam = = SB_ENDSCROLL)||(wParam = = SB_THUMBPOSITION) ) {
SelectObject( lpDispInfo->hMemDC, lpDispInfo->hBmp );
DeleteDC( lpDispInfo->hMemDC );
ReleaseDC( hWnd, lpDispInfo->hScrDC );
lpDispInfo->hScrDC = NULL;
}
/* unlock data */
GlobalUnlock( hDispInfo );
} else
hDispInfo = NULL;
/* return result */
return( hDispInfo );
}
/*
* BitmapVScroll( hWnd, hDispInfo, wParam, lParam ) : HANDLE;
*
* hWnd handle to display window
* hDispInfo handle to display information block
* wParam current scroll code
* lParam current scroll parameter
*
* This function is responsible for handling all the vertical scroll
* messages received when viewing a bitmap clipboard format. If
* necessary, changes to the display information block can be made.
* As currently implemented, no action is taken. The value returned
* is the handle to the updated display information block.
*/
HANDLE FAR PASCAL BitmapVScroll(
HWND hWnd,
HANDLE hDispInfo,
WORD wParam,
LONG lParam )
{
WORD wWndHeight;
WORD wBmpHeight;
WORD wOldScrollPos;
WORD wNewScrollPos;
WORD wOldScrollRange;
LPDISPINFO lpDispInfo;
/* access display information block */
lpDispInfo = (LPDISPINFO)GlobalLock( hDispInfo );
if ( lpDispInfo ) {
/* initialization */
wWndHeight = lpDispInfo->rcWnd.bottom - lpDispInfo->rcWnd.top;
wBmpHeight = lpDispInfo->rcBmp.bottom - lpDispInfo->rcBmp.top;
wOldScrollPos = lpDispInfo->ptOrg.y + VERT_BORDER;
wOldScrollRange = (wBmpHeight > wWndHeight) ?
wBmpHeight-wWndHeight : 0;
/* define display contexts (if necessary) */
if ( lpDispInfo->hScrDC = = NULL ) {
lpDispInfo->hScrDC = GetDC( hWnd );
lpDispInfo->hMemDC = CreateCompatibleDC( lpDispInfo->hScrDC );
lpDispInfo->hBmp = SelectObject( lpDispInfo->hMemDC,
lpDispInfo->hData );
}
/* process scroll message */
switch( wParam )
{
case SB_LINEUP : /* move up one line */
wNewScrollPos = (wOldScrollPos > VERT_STEPSIZE) ?
wOldScrollPos-VERT_STEPSIZE : 0;
break;
case SB_LINEDOWN : /* move down one line */
wNewScrollPos=(wOldScrollPos+VERT_STEPSIZE<=wOldScrollRange) ?
wOldScrollPos+VERT_STEPSIZE : wOldScrollRange;
break;
case SB_PAGEUP : /* move up one page */
wNewScrollPos=(wOldScrollPos > wWndHeight) ?
wOldScrollPos-wWndHeight : 0;
break;
case SB_PAGEDOWN : /* move down one page */
wNewScrollPos=(wOldScrollPos+wWndHeight<=wOldScrollRange) ?
wOldScrollPos+wWndHeight : wOldScrollRange;
break;
case SB_THUMBPOSITION : /* move to an absolute position */
case SB_THUMBTRACK : /* track the current thumb position */
wNewScrollPos = (wOldScrollRange > 1) ? LOWORD(lParam) : 0;
break;
case SB_TOP : /* move to the first line */
wNewScrollPos = 0;
break;
case SB_BOTTOM : /* move to the last line */
wNewScrollPos = wOldScrollRange;
break;
case SB_ENDSCROLL : /* end scrolling */
wNewScrollPos = wOldScrollPos;
break;
}
/* perform scroll and update (if necessary) */
if ( wNewScrollPos != wOldScrollPos ) {
/* update window */
BitBlt(
lpDispInfo->hScrDC,
HORZ_BORDER,
VERT_BORDER,
lpDispInfo->rcWnd.right - lpDispInfo->rcWnd.left,
wWndHeight,
lpDispInfo->hMemDC,
lpDispInfo->ptOrg.x + HORZ_BORDER,
wNewScrollPos,
SRCCOPY
);
/* update origin & horizontal scrollbar */
lpDispInfo->ptOrg.y = wNewScrollPos - VERT_BORDER;
SetScrollPos( hWnd, SB_VERT, wNewScrollPos, TRUE );
}
/* release display context (if necessary) */
if ( (wParam = = SB_ENDSCROLL)||(wParam = = SB_THUMBPOSITION) ) {
SelectObject( lpDispInfo->hMemDC, lpDispInfo->hBmp );
DeleteDC( lpDispInfo->hMemDC );
ReleaseDC( hWnd, lpDispInfo->hScrDC );
lpDispInfo->hScrDC = NULL;
}
/* unlock data */
GlobalUnlock( hDispInfo );
} else
hDispInfo = NULL;
/* return result */
return( hDispInfo );
}
/*
* BitmapPaint( hWnd, hDispInfo, wParam, lParam ) : HANDLE;
* hWnd handle to display window
* hDispInfo handle to display information block
* wParam WM_PAINT word parameter
* lParam WM_PAINT long parameter
*
* This function is responsible for handling all the paint related
* aspects for the bitmap clipboard format. This function calculates
* the required update portion of the window and BitBlts the bitmap
* into the region. The handle returned by this function is to the
* update display information block.
*/
HANDLE FAR PASCAL BitmapPaint(
HWND hWnd,
HANDLE hDispInfo,
WORD wParam,
LONG lParam )
{
PAINTSTRUCT Ps;
HDC hMemDC;
HANDLE hOldData;
LPDISPINFO lpDispInfo;
/* warning level 3 compatibility */
wParam;
lParam;
/* access display information block */
lpDispInfo = (LPDISPINFO)GlobalLock( hDispInfo );
if ( lpDispInfo ) {
/* start paint operation */
BeginPaint( hWnd, &Ps );
/* define update region */
IntersectRect(&Ps.rcPaint,&Ps.rcPaint,&lpDispInfo->rcWnd );
OffsetRect(&Ps.rcPaint,lpDispInfo->ptOrg.x,lpDispInfo->ptOrg.y );
IntersectRect(&Ps.rcPaint,&Ps.rcPaint,&lpDispInfo->rcBmp );
OffsetRect(&Ps.rcPaint,-lpDispInfo->ptOrg.x,-lpDispInfo->ptOrg.y);
/* perform BitBlt operation */
hMemDC = CreateCompatibleDC( Ps.hdc );
if ( hMemDC ) {
hOldData = SelectObject( hMemDC, lpDispInfo->hData );
BitBlt(
Ps.hdc,
Ps.rcPaint.left,
Ps.rcPaint.top,
Ps.rcPaint.right - Ps.rcPaint.left,
Ps.rcPaint.bottom - Ps.rcPaint.top,
hMemDC,
Ps.rcPaint.left + lpDispInfo->ptOrg.x,
Ps.rcPaint.top + lpDispInfo->ptOrg.y,
SRCCOPY
);
SelectObject( hMemDC, hOldData );
DeleteDC( hMemDC );
}
/* unlock data & end paint operation */
GlobalUnlock( hDispInfo );
EndPaint( hWnd, &Ps );
} else
hDispInfo = NULL;
/* return final result */
return( hDispInfo );
}
/*
* BitmapDestroy( hWnd, hDispInfo ) : HANDLE;
*
* hWnd handle to display window
* hDispInfo handle to display information block
*
* This function is to be called whenever the display region is being
* destroyed. It is responsible for restoring the system to it's
* original state and for releasing any memory or resources defined.
* The value returned is the handle to the OLD display information
* block. This handle should NEVER be used after this function is
* called. If an error occurs a value of NULL is returned.
*/
HANDLE FAR PASCAL BitmapDestroy(
HWND hWnd,
HANDLE hDispInfo )
{
/* warning level 3 compatibility */
hWnd;
/* free allocated memory block & return old handle */
GlobalFree( hDispInfo );
return( hDispInfo );
}
Figure 12
LibInit @1
LibCreate @2
LibSize @3
LibHScroll @4
LibVScroll @5
LibPaint @6
LibDestroy @7
Sidebar
BOOL FAR PASCAL LibInit( hInstance, wDataSeg, wHeapSize, lpszCmdLine )
The LibInit function is responsible for all the initialization necessary to
use the dynamic library. This function is normally called by your assembly
language entry point to the library. If necessary, this function should
initialize the local heap by calling LocalInit. In some cases it may also be
necessary to register a clipboard format in this function.
Parameter Type Description
hInstance HANDLE library instance handle
wDataSeg WORD library data segment
wHeapSize WORD default library heap size
lpszCmdLine LPSTR initial command line arguments
The return value determines library initialization status. A value of TRUE
indicates successful initialization. By returning FALSE you indicate to your
assembly language entry point that the initialization has failed. Normally
this should cause the library load operation to fail.
HANDLE FAR PASCAL LibCreate( hWnd, hClipData )
The LibCreate function is called whenever a WM_UPDATE message is sent to the
client window. It performs all the initialization necessary to display the
data provided. Since each display library is manually associated with a
clipboard format by the user, this function has to assume that the data is
in a usable format. As part of its initialization, this function should
allocate a block of global memory and use it to store whatever information
it deems necessary to display the provided data.
Parameter Type Description
hWnd HWND handle to display window
hClipData HANDLE handle to new clipboard data
The return value identifies the block of global memory allocated by the
LibCreate function. This handle is saved by the client window and passed
back whenever one of the other functions is called.
HANDLE FAR PASCAL LibSize( hWnd, hDispInfo, wParam, lParam )
The LibSize function is called whenever a WM_SIZE message is received by the
display window. The revised width and height are extracted from message
parameters and passed on to this function. It should respond by updating its
internal display data structures to account for this change.
Parameter Type Description
hWnd HWND handle to display window
hDispInfo HANDLE handle to display information data block
wParam WORD word parameter of WM_SIZE message
lParam LONG long parameter of WM_SIZE message
The return value identifies the updated block of global memory allocated by
the LibCreate function. The return of an invalid or NULL handle indicates
that the function call has failed.
HANDLE FAR PASCAL LibHScroll( hWnd, hDispInfo, wParam, lParam )
The LibHScroll function is called whenever a WM_HSCROLL message is received
by the display window. The actual scroll parameters received by the display
window are passed through to this function unmodified. LibHScroll should
respond by interpreting the parameters provided and by performing the
horizontal scroll operation.
Parameter Type Description
hWnd HWND handle to display window
hDispInfo HANDLE handle to display information data block
wParam WORD word parameter of WM_HSCROLL message
lParam LONG long parameter of WM_HSCROLL message
The return value identifies the updated block of global memory allocated by
the LibCreate function. The return of an invalid or NULL handle indicates
that the function call has failed.
HANDLE FAR PASCAL LibVScroll( hWnd, hDispInfo, wParam, lParam )
The LibVScroll function is called whenever a WM_VSCROLL message is received
by the display window. Like the LibHScroll function, the actual scroll
parameters received by the display window are passed through to this
function unmodified. LibVScroll should respond by interpreting the
parameters provided and perform the vertical scroll operation.
Parameter Type Description
hWnd HWND handle to display window
hDispInfo HANDLE handle to display information data block
wParam WORD word parameter of WM_VSCROLL message
lParam LONG long parameter of WM_VSCROLL message
The return value identifies the updated block of global memory allocated by
the LibCreate function. The return of an invalid or NULL handle indicates
that the function call has failed.
HANDLE FAR PASCAL LibPaint( hWnd, hDispInfo, wParam, lParam )
The LibPaint function is called whenever a WM_PAINT message is received by
the display window. This function should respond by performing a BeginPaint
operation, updating the window contents, and calling EndPaint. Failure to do
so will leave a portion of the display unvalidated, resulting in an endless
sequence of WM_PAINT messages.
Parameter Type Description
hWnd HWND handle to display window
hDispInfo HANDLE handle to display information data block
wParam WORD word parameter of WM_PAINT message
lParam LONG long parameter of WM_PAINT message
The return value identifies the updated block of global memory allocated by
the LibCreate function. The return of an invalid or NULL handle indicates
that the function call has failed.
HANDLE FAR PASCAL LibDestroy( hWnd, hDispInfo )
The LibDestroy is called whenever a WM_UPDATE message is received by the
display window or the display window is destroyed. This function restores
the display window to its original state and releases any allocated
resources.
Parameter Type Description
hWnd HWND handle to display window
hDispInfo HANDLE handle to display information data block
The return value identifies the updated block of global memory allocated by
the LibCreate function. The return of an invalid or NULL handle indicates
that the function failed. Note that the handle returned should only be used
to determine the success or failure of the function call and should never be
used to access the memory block.
Microsoft C Version 6.0 Provides an Integrated Development Environment
Noel J. Bergman
The Microsoft C Version 6.0 Professional Development System contains the new
C compiler and improved development tools. While maintaining the command
line interface, it adds a complete integrated environment, the new
Programmer's WorkBench (hereinafter "PWB"). The PWB (see Figure 1) is
similar to that of the Microsoft QuickC compiler but is more flexible and
advanced, offering a built-in project make facility and a new source-level
code browser. The latest version of Microsoft QuickC1 is also included in
the Professional Development System for doing rapid compilations. Another
type of pointer, a based pointer, is introduced. Other enhancements include
new run-time library routines; OS/2 systems support; ILINK, an incremental
linker; broader on-line support; and a new version of the Microsoft CodeView
debugger.
Setting up the development system is straightforward. A new version of the
standard Microsoft languages SETUP program looks very much like the tool
used to install C Version 5.1 and asks many of the same questions. There are
a number of command line switches for SETUP. The /L switch directs SETUP to
build libraries only. This is useful for installing additional memory models
or floating point support libraries after your initial installation.
Installing all memory and floating point models for DOS2, the Microsoft
Windows environment, and OS/23--including special support for building
dynamic-link libraries (DLLs) and multithreaded programs--takes up almost 10
megabytes for the libraries alone. Therefore, the ability to go back and
install a new library set when you need it will undoubtedly prove very
useful to people with limited disk storage.
The /COPY switch, another useful switch, decompresses files--the C 6.0
package is so large that Microsoft compressed all the files. This switch not
only decompresses the requested file, it knows exactly which disk needs to
be inserted to find the file. This means no more searching through all of
the subdirectories on a half dozen floppy disks. The /HELP switch lists and
explains all of the available options.
After the files have been copied and the libraries installed, the remaining
steps for installation are similarly straightforward. New versions of
HIMEM.SYS, SMARTDRV.SYS, and RAMDRIVE.SYS are included in the package. These
drivers are designed to work together, using HIMEM.SYS to provide all of the
memory management facilities for the others. Programs that use extended
memory such as the CodeView debugger also work with these drivers. However,
this HIMEM.SYS is incompatible with Version 2.11 of the Windows/386
environment.
New Environment Variables
In order for PWB to work properly, two environment variables must be set.
INIT, the first variable, should point to the directory containing
TOOLS.INI, which is where PWB will place its status file, CURRENT.STS. If
you do not set INIT, PWB will litter your directories with CURRENT.STS files
using the current directory rather than the INIT directory. The other
environment variable, HELPFILES, tells PWB and the new QuickHelp where to
look for the on-line reference files.
TOOLS.INI is the file used by all of the tools (NMAKE, PWB, PWB extensions,
CodeView4, and so on) to store configuration information. It is structured
similarly to a Windows5 WIN.INI file, with each tool setting up one or more
labeled sections to contain settings. The settings are then used as defaults
for later executions of each program. You should merge the provided
TOOLS.PRE file into any existing TOOLS.INI file you have or rename TOOLS.PRE
to TOOLS.INI if there isn't an existing TOOLS.INI file.
When you change the settings of an editor (for example, going into 50 line
mode in PWB), saved changes will be recorded in TOOLS.INI. PWB settings are
temporary unless you save them. Only the changes are recorded in TOOLS.INI,
not the entire list of possible settings.
CURRENT.STS is the current status file for PWB. The settings for the project
you were working on last are recorded in CURRENT.STS and remain in effect
until you issue a Set Program List. At that time any new settings specified
in the project's make file or STS file will take effect.
For this reason, you must make certain that INIT= is set up properly. If it
is not, PWB won't be able to retrieve the old settings, unless there happens
to be a CURRENT.STS in the current directory.
I have a useful little trick that I use to keep both Version 5.10 and
Version 6.0 of C on my system at the same time. First, I moved all the
programs, libraries, and include files to subdirectories called \C510 and
\C600. I used SUBST to create a pseudodisk for the C compiler package. SUBST
M: C:\C510 "creates" the \BIN, \PBIN, \RBIN, \LIB, and \INCLUDE directories
on the M drive. SUBST M: C:\C600 does the same thing for the C 6.00 package.
The PATH, INCLUDE and LIB environment variables all use references to
M:\<dir>. This allows me to switch between them very easily. Unfortunately,
this trick doesn't work with OS/2, because there is no OS/2 protected-mode
version of the SUBST command.
The Programmer's WorkBench
If you can imagine the Microsoft Editor enhanced both internally and with
extensions to resemble the Microsoft QuickC Version 2.0 environment, you
have a rough idea of what PWB is like to work with. In addition to the
editor, PWB provides access to the compiler (see Figure 2), linker, make
facility, debugger, on-line reference, and source browser.
In PWB, the Microsoft Editor, M, has been enhanced through extensions to
provide facilities for source code browsing, creating and modifying make
files, building programs, and accessing on-line reference material from
within the editor.
One useful feature is Bookmarks (both temporary and stored in files).
Bookmarks allows you to move quickly to specific named points in source
files. The Search and Replace facility supports regular expressions, in both
UNIX and M syntax. This facility extends to text already loaded into the
editor and to files on disk as well. Another useful feature is a search for
files on disk. Most of you undoubtedly have standalone programs for this
purpose, but this feature lets you find files without leaving the
environment.
Finally, you can extend PWB with both extensions written in C and macros.
Most C extensions written for the Microsoft Editor will move easily into
PWB. To prove the power of C extensions, the Browser, MS C Advisor, and
other PWB tools are all implemented as extensions. I have also seen a large
set of public domain extensions to make the Microsoft Editor look a lot like
the popular editor BRIEF (from Solution Systems).
PWB also lets you customize the Microsoft Editor. Most of you undoubtedly
already use an editor you are happy with--if you are going to switch over to
the Microsoft Editor, you want to do so with a minimum of pain and effort.
To get help for settings, press F1, which will bring up the
context-sensitive Microsoft C Advisor. However, for some of the changes, the
tools for getting help are not overly practical. When you want to change the
color, you can get a list of hex values for different colors, but there
isn't a color chart or a set-up dialog box.
One major problem that I have with the Microsoft Editor is the lack of
generalized support for keybinding. You can bind any function or macro to
almost any key, but only to a single key. WordStar, EMACS, and many other
editors use multiple keystrokes to access some features. Epsilon, a popular
editor based on EMACS from Lugaru Software, has more than 60 commands bound
to pairs of keys. Using the EMACS model for keybinding, Epsilon can be
tailored to mimic any editor's keyboard interface.
One way to get around this problem is to write an extension. This extension
would be attached to the first key of a multikey sequence, and would be
responsible for taking the next key or keys and dispatching the correct
command. WS.C in the PWB on-line reference is an example of this kind of
extension; it performs some of the WordStar commands.
All in all, though, the PWB editor is quite serviceable. The feature set and
extensibility are good, and it is fully integrated with the rest of the
environment. If you use an editor that accesses all commands with single
keystrokes, you should be able to make the PWB editor react the way you want
it to, even if not all of the features of your editor are present.
Project Management Facilities
Project management is one area where PWB shines, although for some
developers the built-in support for NMAKE may be useless.
The key to the project make facility is the Program List (see Figure 3). A
Program List is a list of files (C, ASM, OBJ, LIB, and so on), upon which
the project target (a single EXE or DLL) is dependent. For each ASM and C
file, the necessary rules will be added to build the OBJ, which in turn will
be used to build the project target. Please note that you should add only
LIB files that are part of your own project and likely to change; these will
appear as dependencies for the project target. Other libraries, such as
GRAPHICS.LIB, should be added as additional libraries under the LINK OPTIONS
menu.
Once you have added the source files the project target is dependent on, use
the Set Dependencies command. This scans all the source files and adds any
necessary header files to their dependencies lists.
Another use for this feature is to update any PWB-compatible make files that
have hardcoded pathnames for the header files. Set Dependencies looks for
the headers along the INCLUDE path and updates that path in the make file.
Another serviceable feature in PWB's set of project management tools is the
Build Options dialog box (see Figure 4A). This is used to maintain a list of
predefined build configurations. The defined configurations serve as
templates to build new make files. Among the list of stock configurations
are EXEs for DOS, Windows, OS/2, and OS/2 Presentation Manager (hereafter
"PM"); DLLs for OS/2 and PM that incorporate C run-time library routines;
DLLs and EXEs that use C run-time library DLLs; and special C run-time
library DLLs (see Figure 4B). The Save Current Configuration command saves
new named configurations for later use. Although its name implies otherwise,
the command is used to create a newly defined configuration for future
projects, not to save the current settings for use in the current project.
Each configuration has two smaller configurations within it. The first
configuration has the options for building a target for debugging; the
second configuration has the options for building a target for release.
These two configurations allow you to make a complete switch from one set of
compiler and linker options to another very easily and quickly.
It is very important to know that PWB stores project information both in a
make file and in an STS file. There are a number of interactions between
these files. If you ever want to make changes to the make file, you should
delete the STS file or your changes may be lost when PWB loads the STS file.
Editing Make Files
You can edit a PWB-generated make file (although not while it is the
selected Program List), but there are some limits. PWB only understands a
limited subset of NMAKE capabilities as make files. For example, you can't
make a LIB target from a bunch of OBJs and use the LIB to build the EXE. You
can build the LIB another way and use it as a dependency for the EXE. Also,
you cannot have multiple EXE or DLL targets in the make file. What you can
have is a single EXE or DLL target that is made from many source, object,
and library files.
The restrictions that cause PWB to reject a make file as a non-PWB file are
sometimes unclear. Certainly, multiple targets in a make file will cause PWB
to reject the file, but other types of activity can cause a rejection, too.
When PWB rejects a make file, it asks you if you want to use it as a
foreign make file. Foreign make files are make files too complex for PWB to
work with. To designate a make file as foreign, precede its name with the @
symbol when you use Set Program List. Otherwise PWB will tell you if it is
problematic. Once PWB knows that a make file is foreign, it disables most of
the menu items that control building.
One item that remains available with a foreign make file is NMAKE OPTIONS.
This item has great utility when working around some of the limits of
foreign make files. Remember that PWB make files understand how to build
both debugging and release versions of a project; this is controlled by
conditional directives in the make file. If you follow similar conventions
in your own make files, you can use NMAKE OPTIONS to control their
execution. You could set some macros that control debug and release,
localization, and other version-related information, for example.
Source Browser
One of the best features provided by PWB is a new Source Level Browser
extension that allows you, for example, to browse code for large projects;
to look at the varied relationships between routines, variables, and symbols
in many files; to find where something is used and/or defined; and so on
(see Figure 5).
The Browser consists of a set of dialog boxes. One dialog box goes to
definitions, one goes to references, one creates a list of references, one
creates a call tree for a function or file, one generates an outline (sort
of the reverse of the list of references), and one lets you browse
relationships between various entities in the project.
The GOTO commands, the cross-reference commands, and the call tree generator
are the easiest Browse options to use. The View Relationships dialog is not
as straightforward, but it is the most powerful. In essence, View
Relationships is a sophisticated hub from which to explore your program. All
the power of the other options, and more, is available using View
Relationships, although the operations are not always formatted the same
way. It is also possible to see the history of the relationships you have
viewed for the file.
To provide this information, the Browser maintains a database about your
project, called a Browser Symbol Cache (BSC file). If you make the project
with PWB and select Generate Browser Information in the Browser Options
dialog box, PWB ensures that the BSC file is kept up to date. If you write
your own make files or work from the command line, you have to keep the BSC
file up to date yourself. A new compiler option (/Fr) writes browser
information for each file to an SBR file. PWBRMAKE updates the BSC file from
the SBR files, truncating the SBR file in the process.
The project name, which is also the name of the program list, is a magic
name for certain PWB extensions. One example is the Browser, which looks for
a BSC file with the same name as the program list (a MAK file). This
information will be helpful when you make your own foreign make files.
Compilers
The Professional Development System comes with both the C optimizing
compiler and the QuickC compiler. The optimizing compiler generates the
fastest, most optimized code. The quick compiler generates code much more
quickly and also supports incremental compiling. Incremental compiling means
that only routines that have been changed since the last compilation are
recompiled.
Typically, the quick compiler is used to build debugging versions of a
program and the optimizing compiler is used to build release versions of a
program. Use the /qc switch for CL to select the QuickC compiler. The
predefined build configurations throw that switch when making debugging
versions.
Both compilers have been enhanced with new features in Version 6.0. There
are now six memory models--the old small, medium, compact, large, and huge
models and a new tiny (/AT) model. The tiny model generates COM files,
putting both code and data in the same 64Kb segment.
ANSI conformity has been further addressed in the compiler revisions. All of
the extended Microsoft keywords are now prefixed with underscore (for
example, _near), although the old versions are still supported. Programmers
should start making their own code conform to the ANSI standards as well,
adding the underscore as needed to the keywords.
The long-awaited semantic support for the "volatile" keyword has been
implemented. This keyword is important when writing multithreaded OS/2
programs or Windows code, when working with hardware devices, and at any
time when the compiler needs to be told that a change may have been made to
memory without the compiler being aware of it.
There are some new features to make programs run faster. The optimizing
compiler supports global register optimization (/Oe) and global
optimizations and common subexpression (/Og). Both compilers support a new
register-based parameter-passing scheme known as fastcall (/Or or _fastcall)
and in-line assembler code (_asm). If you use one of the other function
modifiers (such as _cdecl, _pascal, and _export), _fastcall will be disabled
for that function. This means that headers for prebuilt calls (for example,
third-party object libraries, OS/2, and Windows) must specify the correct
modifiers, rather than make assumptions about the compiler options. That was
already the case if you wanted to use /Gc in earlier compiler versions.
Based Pointers
A new kind of pointer has been added to Microsoft C. Based pointers combine
attributes of near and far pointers, giving you the addressability of a far
pointer and the size and speed of a near pointer. A based pointer is an
offset off of ES. Once the segment or selector has been set up, all
references are as fast and compact as they would be using near pointers.
There are new keywords to support based pointers. The _based keyword defines
a pointer or object as based. It is a lot like the _near and _far modifiers,
but it takes an argument in parentheses that specifies the base. For
example, you can use a pointer as the base for other pointers:
char *p;
char _based(p) *bp1;
char _based(p) *bp2;
bp1 = (char *) 0; // bp1 = p+0
bp2 = (char *) 2 // bp1 = p+2
The base can also be indicated in one of several other ways. You can specify
a named segment as a base using _segname; for example,
char _based (_segname("COPYR")) copyright[] = \
"(C) 1990 Microsoft"
A new data type, _segment, is likely to be the most common way to specify
bases. You can declare variables to be of type _segment or cast addresses to
type segment. When _segment is used to cast a near address, the result is
the current value in DS. If the address is a far address, the result is the
segment for that far address. This is the case whether the address is the
contents of a pointer or taken using &. The difference between _based(p) and
_based((_segment)p) is that the former is based upon the address in the
pointer including its offset, and the latter is based upon the segment
address for the pointer (DS for near pointers, the segment and selector for
far pointers).
Based pointers can also be based on void or on _self. Pointers based on void
do not have an implicit base and can be combined with a segment using the
new :> operator:
_segment segvar;
int _based(void) *bp;
int screen[screen_size], i;
segvar = (color) ? 0xB800 : 0xB000;
for (i=0,bp=0 ; i<screen_size ; i++,bp++)
screen[i] = *(segvar:>bp);
In a sense, _self is similar, but instead of not having a base, it says that
the base is relative. _self based pointers are declared as
_based((_segment)_self). Uses for _self could be in tree structures or
linked lists. You can create a segment to hold the entire tree or linked
list. Each of the pointers within the tree or linked list would be _self
based, which tells that compiler that they all use the same ES.
Based pointers have many advantages over near and far pointers. Near
pointers are like based pointers that are fixed on DS, so based pointers
have more addressability. Far pointers are much slower than near pointers,
so based pointers have a tremendous advantage in speed over them. Also,
based pointers are relative to their base, so you can move segments around
in memory, save it off to disk and reload it, or any number of other
operations without invalidating the based pointer. This will make it easy
to implement persistent structures in C. If you allocate segments, and use
based pointers within them (for example, _self), you can save the entire
segment and restore it later, while preserving the integrity of your
pointers.
Run-time Library Enhancements
Microsoft C Version 6.0 adds a lot of new run-time library (RTL)
enhancements. These enhancements have been made to support system features
such as based pointers and to make programming for OS/2 easier. The standard
documentation for the C RTL is much shorter than it has been in the past.
Each function is listed with its name, prototype, parameter descriptions, a
one or two sentence description of the routine, and a compatibility table
(ANSI, DOS, OS/2, UNIX, and XENIX systems). Detailed documentation and
examples are present in the on-line references.
Figure 6 contains the new C RTL routines for based pointers. Most of these
functions also have analogues in the default (memory-model-dependent) near
and far heaps.
OS/2 support is one of the major enhancements in the C 6.0 package. The
libraries are better organized to deal with all of the details involved in
building DLLs and multithreaded applications, and the headers have been
combined using conditional compilation directives. This makes the whole
process of working with DOS, OS/2, DLLs, and so on, more straightforward and
streamlined. CL has been enhanced with new options to turn on the necessary
compiler directives for the target environment.
The new OS/2 support is in the form of a few new libraries and CL options.
OS/2 applications that do not need a reentrant C library can continue to use
the standard xLIBCyP libraries. Standalone, multithreaded applications use
the /MT switch, which causes the LLIBCMT library to be used for the program.
The new version of this library is enhanced in C 6.0 to support as many
threads as OS/2 will allow, not the former arbitrary limit of 32. For this
reason, you can now use _beginthread for all threads, without having to
worry about a limit.
Among the new libraries for OS/2 is GRTEXTP.LIB, which provides a
text-oriented subset of the GRAPHICS.LIB functionality for OS/2 protected
mode.
If you are building a DLL that uses the C run-time library, you use the /ML
switch. This causes the LLIBCDLL library to be used. Using /ML sets /FPa
because DLLs must not use the floating point coprocessor (except in a
special case noted below).
To build a standalone DLL, link your source code with the following files:
OS2.LIB (OS/2 import library); LLIBCDLL.LIB (a multithreaded library for
standalone DLLs); DLLINIT.OBJ (an optional module for providing user-defined
DLL initialization); and DLLTERM.OBJ (the termination counterpart to
DLLINIT.OBJ).
Standalone programs and standalone DLLs do not share C run-time code or
structures with each other. Another type of DLL, a private C run-time DLL,
provides C run-time support to a closely related set of programs and DLLs
and contains the working set of C run-time library functions necessary for
the package. It is also the only way to allow DLLs to use the math
coprocessor. To build a private C run-time library, link the following
files: OS2.LIB (OS/2 import library); CDLLOBJS.LIB (dynamic-link C run-time
library); CRTLIB.OBJ (start-up code for a C RT DLL); USER.DEF (a definition
file listing the necessary C run-time functions). To find the functions you
need, look at CDLLOBJS.DEF, which lists all of the available functions.
Simply remove the unnecessary routines (to save space) when you have time.
After you have built the private DLL, use IMPLIB to build an import library
and LIB to add CDLLSUPP.LIB to your import library. This step is important,
since CDLLSUPP contains C run-time routines that cannot be dynamically
linked.
To use your new DLL from another DLL in your application suite, use CL's /MD
option, and link the object files and DEF file for your DLL with the
following files: OS2.LIB (OS/2 import library); MYIMPORT.LIB (the import
library for the private DLL); CRTDLL.OBJ (the start-up code for a DLL using
a C run-time DLL); and CRTDLL_I.OBJ (an optional file that replaces
CRTDLL.OBJ and supports user-defined DLL initialization). There is no
termination module because you can properly handle termination using atexit
or DosExitList.
The program that uses the private C run-time DLL and any other associated
DLLs is built by linking the object code and DEF file with: OS2.LIB (OS/2
import library); MYCIMPORT.LIB (import library for the private C run-time
DLL); MYDLLIMPORT.LIB (an import library for another DLL); and CRTEXE.OBJ
(start-up code for a program using a C run-time DLL).
All the programs and DLLs that share a common private C run-time library DLL
will be sharing C run-time library structures such as the file handle table.
It is not a good idea to build a single C run-time DLL and use it for all of
your programs. Each set of programs and DLLs that make up a single
application should share their own private C run-time DLL.
Intelligent Linking
With all the support in the compiler for getting compilations done quickly,
it would be a shame if you had to wait forever for a link to finish.
Fortunately, the package includes ILINK, the incremental linker.
After you have done a full link with LINK's /INC option, you can do most
links with ILINK. ILINK basically adds the new or changed routines, although
if it has to, ILINK will call LINK to do another full link. When you are
ready to build a release version, you do another full link without the /INC
option. This gives you the best combination of speed, convenience and
compactness for your programs. The best part, of course, is the minimized
link time.
NMAKE
As previously stated, PWB is somewhat limited in the complexity of projects
that it can build. The new NMAKE tool, which is used by PWB, has no such
limitations. NMAKE (for New MAKE) is a superset of the XENIX make program
written by Microsoft, and a superset of the UNIX-like make programs familiar
to many developers who gave up on Microsoft's earlier make program. If you
are a user of one of those make systems, you should be pleased with NMAKE.
NMAKE, like other UNIX-style make tools, disregards the ordering in the make
file. NMAKE is given the name of the target to build, rather than building
all targets in order. That normal parameter to NMAKE is the name of the
target to be built, not the name of the make file. Unless you specify a new
name for the make file, using the /F option, NMAKE assumes that the name is
MAKEFILE.
If you are using Microsoft's old make program, you will need to make a few
changes. The easiest way to convert from MAKE to NMAKE is to add a
pseudotarget, ALL, as the first target in the make file. ALL should be
dependent upon all of the high-level targets in your make file. This does
not mean each of the OBJs, unless those are final outputs from the make
file, but just the EXEs, DLLs, and so on, that are the desired final result.
You can also put other pseudotargets into a make file. For example, you
could have one called SRCLIB that is dependent upon all of your source files
and cause a source archive to be updated. Another common pseudotarget is
CLEAN, which can be used to delete OBJ, BAK, and other nonrequired files
from the project directory.
There are some other nice features in NMAKE that you may want to take
advantage of in your own make files. For example, as alluded to earlier,
NMAKE supports directives in a make file. Such directives include !IF,
!IFDEF, !IFNDEF, !ELSE, and !ENDIF; these are used by PWB to implement the
conditional make files that build both debug and release versions of
programs. Working directly with the full power of NMAKE, you can build even
more options into your make files. When you invoke NMAKE, you add command
line options of the form "macro = value." The NMAKE OPTIONS menu in PWB
allows you to add command line options from within PWB.
Another nice feature is that you can specify search paths to NMAKE. Search
paths can be used in two places. First, you can use paths in inference
rules. The syntax for inference rules is:
{frompath}.fromext{topath}.toext
commands
The paths are optional. You can use paths in dependency lists also. For
example:
prog.obj : {\private\src; \group\src}prog.c
tells NMAKE to look first in your private source library for possibly
modified code for prog.c and to look in the group library if it doesn't find
prog.c in yours. A macro, SRCPATH, could be defined and used throughout the
make file.
NMAKE supports string substitutions within macros, too. For example, suppose
you have a macro SRCFILES that lists all of the source files necessary for a
program. $(SRCFILES:.c=.obj) replaces all occurrences of .c with .obj in
that macro, resulting in a list of all the OBJ files.
Another option lets you process a list of files. The "!" command modifier
tells NMAKE to execute the command for each file in the dependency list if
the command uses either $? (out-of-date dependencies) or $** (all
dependencies).
On-line References
Microsoft QuickHelp improves in its latest version in the C development
system, although the improvement is based more on the use of QuickHelp
throughout the product than on a change in the technology. Each of the tools
(CL, PWB, LINK, ILINK, BIND, LIB, and so on) supports the command line
option /HELP and reacts to it by invoking QuickHelp on its topic. This makes
it very easy to get detailed hypertext-like help on all of the tools. Help
is also available through the QuickHelp program and through the Microsoft C
Advisor built into PWB.
The development system comes with help files for each of the tools, the
utilities, the C run-time library, the C language, PWB, and writing PWB
extensions (see Figure 7). A table of contents and an index have been added.
When you enter QuickHelp, you can ask it to get the table of contents for
the available help files. As distributed, the package will show you a table
of contents for PWB and for the resource compiler, RC.
PWB's table of contents contains entries for the tools, the languages, the
various APIs (OS/2, PWB extensions, network, and so on), and other useful
categories. Not all of these files may be available on your system. For
example, you may not have the help file for BASIC Version 7.0 or the network
API. Also, you may want to add new help entries of your own to the table of
contents.
HELPMAKE is the tool used to build help files. It can also decode help files
if they have not been locked. Microsoft's languages group does not lock
their help files. You can use HELPMAKE to decode the PWB help file, find the
table of contents, modify it as desired, and then use HELPMAKE again to
rebuild the help file. Several groups have adopted commenting conventions
for their file and function headers that can be scanned by AWK scripts and
turned into source files for HELPMAKE. They can then call up custom help on
each of their own functions and function categories as easily as on OS/2
programming and the OS/2 API.
CodeView Version 3.0
The CodeView debugger has been enhanced in the Professional Development
System. CodeView can now make use of some extended memory to cut down on the
memory crunch often experienced by users (see Figure 8). The windowing
package used by CodeView is now similar to that of PWB and Microsoft QuickC,
Microsoft QuickBASIC, and so on. This windowing package permits you to open
multiple windows of the same kind.
There are now menu items to supplement the OS/2 thread debugging commands.
And, as with all of the other tools in the package, CodeView Version 3.0
interfaces with a QuickHelp database, making it easier than ever to get help
while debugging.
I still do not find the CodeView user interface as easy to use and as
powerful as that in the OS/2 Presentation Manager version of the MultiScope
debugger by Logitech, but it is clearly moving in the right direction. The
ability to write extensions such as those for PWB should be added to
CodeView.
In addition, the CodeView format has changed again. This means that those of
you who use the MultiScope debugger for OS/2 will have to get a new version
to work with the changed format.
Up and Running from Version 5.1
Moving up to Version 6.0 from Version 5.1 is relatively painless. I have
already noted a trick that you can use to keep them happily coresident while
you make the change, and how to convert MAKE files to NMAKE files with
little effort.
One utility that you will not find on the disks is the venerable EXEMOD. A
new version of EXEHDR contains all of the functionality of EXEMOD, as well
as its own. It is now the only tool for working with the headers of
executable files.
There are a couple of other things that you may want to keep in mind. The C
compiler generates code to call internal library routines for certain
operations. These are called helper functions and are used for such things
as shifting a long integer. The names of the helper routines have changed in
C 6.0. Old third-party libraries that use the old names are all right since
the new library supports them; other run-time libraries, such as those for
Windows Version 2.11 that provide the helper functions, don't provide the
names expected by C 6.0. The -Gh switch tells the C compiler to generate the
old helper names, rather than the new ones.
Finally, in the process of upgrading to ANSI support, Microsoft made a few
changes in the language semantics. One example is the conversion of unsigned
short now going to a signed long instead of an unsigned long.
Summary
All in all, the Microsoft C Version 6.0 Professional Development System is a
major step forward for developers. Though more refinements are necessary,
for example, supporting more complex make files within the PWB environment
without having to make them foreign make files, it shows the direction that
Microsoft is going to take their development tools over the next few years.
Figure 6 RTL Enhancements
Function Description
_bfreeseg Free a based heap
_bheapseg Create a based heap
_bcalloc Calloc within a based heap
_bexpand Expand block (not moved)
_bfree Free allocate block
_bheapadd Expand size of heap
_bheapchk Consistency check heap
_bheapmin Release unused heap space
_bheapset Check and fill free blocks
_bheapwalk Walk the heap
_bmalloc Malloc in the based heap
_brealloc Realloc in the based heap
TRACER: A Debugging Tool for OS/2 Presentation Manager Development
Daniel Hildebrand
The introduction of a new machine or operating system compels developers to
determine how well the associated development environment meets their
debugging needs. Developers often need a variety of debugging methodologies
not satisfied by out-of-the-box tools. Though all environments include
standard debuggers that allow developers to monitor machine registers and
trace through programs at a microscopic level, tools that permit logical
debugging at a higher level may also be needed. In addition to standard
debugging, facilities within the OS/2 development environment provide the
ability to monitor messages critical to some programs. The toolkit falls
short, however, of providing a way to perform the simplest form of
debugging: the trace display of formatted program output to some auxiliary
device. This article examines some existing debugging tools and their
effectiveness and then presents a simple program called TRACER that is
useful for tracing and debugging OS/2 Presentation Manager (hereafter "PM")
applications.
For the purposes of this article, I would like to distinguish between types
of debugging processes. The first, "exception" debugging, is what is done
when a fatal error is encountered. Based on a diagnosis of the problem, a
post-mortem debugging mode is entered to recreate the scenario and isolate
the bug. The second, "development" debugging, is used during the evolution
of code to monitor progress and prevent potential bugs. Commonly, a series
of trace statements is placed within the code to monitor the values of
selected variables, ensure that array boundaries are respected, verify that
return codes are correct, check program states, and so on. These trace
mechanisms usually redirect formatted output to devices such as files,
printers, or auxiliary screens. Usually bugs are caught before they happen
and when a bug does occur it is fairly simple to identify by examining the
memory and the state of the program via the output of the trace mechanism.
This simple form of development tracing solves many of the problems that
developers encounter.
Debugging Environment
Versions 1.1 or higher of the OS/2 Software Development Kit come with a
debugging version of the device driver PMDD.SYS. It can be used by adding
the line
DEVICE=\OS2\DEBUG\PMDD.SYS /M
to the CONFIG.SYS file. The /M switch tells the system to output debugging
information to a secondary monitor. When this special driver is used,
debugging is activated for applications running within the PM screen group.
Useful information is displayed for PM applications, including messages
telling the developer that window handles have been created, memory has been
allocated or freed, OS2.INI is being queried or written to, and so on.
Although this information lets developers see the underlying actions being
carried out by the system, the facility does not allow developers to send
their own messages to the secondary monitor.
The Spy program is familiar to Windows and OS/2 programmers. Spy is very
helpful for resolving certain kinds of bugs and for learning about
message-based architecture. For example, by observing message traffic in
normal circumstances, you can often determine which messages are missing or
required in exceptional circumstances. Spy cannot, however, find certain
problems. A pointer problem, for example, will usually not be discovered
using Spy. Sometimes, merely loading the Spy program into memory will change
the target location that the bug has been affecting and lessen your chances
of finding or even duplicating the bug. The PM version of Spy displays
process and thread information and is superior to the Windows version, but
is not designed for development debugging.
There are some very good facilities for doing interrupt-level debugging in
OS/21. The Microsoft CodeView debugger and Multiscope (a debugging tool
available from Logitech Corporation) are very useful for trapping memory
corruption bugs. Both can step through a program while dynamically revealing
variable values, dumping the contents of structures, evaluating loop counts,
and so on. However, the larger executable generated by compiling an
application for use with the CodeView or Multiscope debuggers often moves
the bug around in memory, making it sometimes hard to find and difficult to
fix. These tools are good for what they do, but again, they do not provide
efficient development debugging.
Tracing in Windows and PM
Development debugging in the Windows2 environment is quite a bit more
complicated than in DOS3. Printf functions sprinkled among a program's
regular output are no longer useful, since Windows provides a purely
graphical user interface (GUI) and manages the video output fully.
Substituting the Windows API function TextOut, which displays text strings
within a window's client area, doesn't work well for debugging purposes
either. With a secondary monochrome monitor attached to your system, you can
redirect formatted output to that monitor from within your program. This is
done by placing the file OX.SYS onto your hard disk, booting your machine
with the statement device=\ox.sys in your CONFIG.SYS file, and then using
fprintf functions in your code to "print" to the auxiliary screen. The
device driver OX.SYS, a small program available from Microsoft, redirects
stdaux output to the monochrome video address B0000H. This is a very useful
technique employed by many Windows developers.
In OS/2, however, the protected mode rules of the 80286 are adhered to; the
average PM application may no longer directly manipulate hardware. Although
the debugging version of PM can itself output strings directly to a
secondary monitor, I know of no facility that will enable you to do the same
thing from within your PM application. Of course, whether you're developing
in Windows or OS/2, if you don't have a secondary monitor you can't write to
it.
One alternative is to come up with a program that allows clients to display
messages from within their programs in a window and redirect those messages
to files if we desire. This method is also ideal for studying run-time
problems encountered on end-user systems in which it is impossible to
install development software.
Under Windows, writing this program is simple. Writing a trace window that
can support multiple applications is no problem because of the nonpreemptive
multitasking of Windows. A window can be created with a client area whose
sole purpose is to display text messages. Applications then simply send a
user-defined Windows message to the trace window, with a far pointer (LPSTR)
to the text string in the lParam of the message. The window need only use
TextOut to display the text string, and the job is complete. Developing this
trace window for PM, on the other hand, is more complex.
TRACER
The preemptive multitasking of OS/2 means that a PM tracing window must
contain code to explicitly support multiple applications running
simultaneously This is especially true if serial interprocess communication
(IPC) and shared memory between segments are used in the application.
TRACER is a PM window that displays messages from client PM applications
(see Figure 1). IPC, named shared memory, and system semaphores allow TRACER
to be used by multiple concurrent applications.
The Options menu choice allows you to toggle the screen display and/or the
file display on and off. It also allows you to truncate the file and/or
empty the window before proceeding.
TRACER.C (see Figure 2) contains the main section of the application,
including the window procedure that manages the TRACER handshaking messages
and display of debug strings. TRACERP.C contains code to manage the TRACER
output window; TRACERD.C contains code to write output strings to a file.
Only one instance of TRACER may run at a time. This is important because the
program uses system resources (a semaphore and named shared memory) and must
not attempt to access more than one copy of those resources at a time. Also,
since TRACER shares those resources with clients, TRACER may not be closed
until all clients have released the resources by closing down first. TRACER
attempts to return those resources to the system; it will wreak havoc with
the system if it tries to return them while the usage counts for those
resources are nonzero. TRACER provides MessageBox and auditory responses if
an attempt is made to start more than one copy of it or to shut it down
while clients are still running. It does not matter whether TRACER is
brought up before or after the client application: the code performs
handshaking in both instances.
TRACER is straightforward to use from within a client program. The following
discussion provides a five-step process to use TRACER. I have included a
sample application using TRACER, called CLIENT.C (see Figure 3), that can be
referenced as you go through these steps. The file TRACER.H (see Figure 2),
which is included by this client, contains a macro to be used at each step.
First, include the file TRACER.H in your code.
Step two involves the macro TRACERVARIABLES, which contains declarations for
the variables that the client will need. These variables include the name of
the selector to the global named shared memory, a far pointer to that
memory, a handle to a system semaphore, the TRACER window handle, and a
variable called bTracerConnected, which is used to determine whether the
client is connected to TRACER. TRACER allocates 80 bytes of memory to share
with clients. Clients use the memory to transfer the text string for display
to TRACER. You need to add the line TRACERVARIABLES above your main. At the
bottom of TRACER.H is a macro called EXTERNTRACERVARS that should be used in
any secondary code segments (that is, other source files)in which you want
to use TRACER. EXTERNTRACERVARS declares the TRACERVARIABLES as extern.
Somewhere between the client's WM_CREATE and the subsequent break statement,
add the macro TRACERHELLO. TRACERHELLO, the third step, is used to sign on,
so to speak, to TRACER. The macro sends a message called
TRACER_REQUEST_HANDLE to every message queue application in the system.
TRACERHELLO passes its own window handle in mParam1 of the message so that
when TRACER receives the message, it can return the TRACER window handle to
the client (see Figure 3). When TRACER receives the message, it places its
window handle into the mParam1 of a TRACER_RECEIVE_HANDLE message and posts
it back to the client.
To review: the client passes its window handle to TRACER by broadcasting a
message to the system. If TRACER is running, it returns its own window
handle to the client in the TRACER_RECEIVE_HANDLE message.
Step four occurs when both applications have each other's window handle, the
client must then retrieve permission from the system to access the named
shared memory segment allocated by TRACER. To serialize requests for
services, the client must also retrieve a handle to a system semaphore that
will be used by TRACER to block requests by clients until it is their turn
to use TRACER.
To illustrate the importance of blocking requests, imagine two clients, A
and B, that want to display a message string in TRACER and request the
service at almost the same time. The OS/2 scheduler has one purpose--to pass
control to threads in the system based on their readiness to run and then to
retake control and pass it to the next thread that is ready to run. Consider
the mess that will occur if client A's message is half-written to the screen
when the scheduler takes control and gives it to client B, which then has
its message written where the second half of client A's message should have
been. Using a system semaphore blocks threads that may concurrently request
a TRACER service.
To implement the semaphore, add the macro TRACERATTACH as a separate case in
the client window procedure. TRACERATTACH receives the shared segment
selector and the handle to the semaphore from the system.
You are now ready for step five, which is to use the TRACER macro freely.
This macro has one parameter in it, the text string you want TRACER to print
in its window. When the TRACER macro is called, the client waits for the
system semaphore to be cleared; when it is, the client sets the semaphore
for its own use.
At this point the text message is prepared and a WinSendMessage (that is, a
function call to the TRACER window procedure) is made requesting action
using the TRACER_REQUEST_ACTION message. Note the TRACER call in the client
window procedure for WM_BUTTON1DOWN. This demonstrates how a client might
use sprintf to formulate its own output string for output to TRACER. Here,
the client formats a buffer to include the message name and the current time
(actually the number of milliseconds elapsed since the system was started)
and sends the formatting string to TRACER. Try pressing the first mouse
button on the client's window. The client may construct any number of
arbitrarily complex strings for display within TRACER. TRACER evaluates
whether the user wishes messages to be displayed in the window. If it does,
it simply sends the appropriate list box control messages to the window and
the job is done. Likewise, if the user wants the message to be echoed to the
debugging trace file, TRACER writes the message to the file as well. The
TRACER message then breaks, and the client clears the semaphore and
continues processing, thereby freeing TRACER for its next request for
action.
The bTracerConnected variable protects a client from trying to send output
to the application when TRACER is not running, thereby preventing the client
from trying to set a system semaphore that is not available or to access a
data segment outside of its process. To initiate contact with unconnected
clients, the WM_CREATE message of TRACER sends its own
TRACER_RECEIVE_HANDLE to the system. Connected clients ignore the message.
A small problem with TRACER is that a certain number of messages must be
exchanged between the client and TRACER before the TRACER macro will work.
Because of this, you may not see trace messages that occur very early in the
life of your PM program (those within the response to the WM_CREATE message,
for example).
Conclusion
Although TRACER is designed to assist the developer during the construction
of PM programs, it is also an excellent tool to debug problems in production
environments. TRACER can be used to record the events leading to fatal
problems. A post-mortem analysis of the TRACER file may lead to a resolution
of the problem.
While this article's purpose is to provide a simple and useful debugging
tool for PM developers, the TRACER facility is also presented in order to
review the use of some interesting and important OS/2 mechanisms, such as
system semaphores and named shared memory.
Figure 2 TRACER
#-------------------------------------------------------------------
#
# Standard command line definitions
#
#-------------------------------------------------------------------
cp=cl -c -W3 -Alfw -G2sw -Os -Zpei
#-------------------------------------------------------------------
#
# Default inference rules
#
#-------------------------------------------------------------------
.c.obj:
$(cp) $*.c
.asm.obj:
masm $*.asm;
.rc.res:
rc -r $*.rc
#-------------------------------------------------------------------
#
# Dependencies
#
#-------------------------------------------------------------------
tracer.obj: tracer.c tracer.h
tracerp.obj: tracerp.c tracer.h
tracerd.obj: tracerd.c tracer.h
tracer.res: tracer.rc tracer.ico tracer.h
tracer.exe: tracer.obj tracerp.obj tracerd.obj tracer.res tracer.lnk
tracer.def
link @tracer.lnk
rc tracer.res
TRACER.H
//----------------------------------------------
//-TRACER.H-------------------------------------
//----------------------------------------------
//----------------------------------------------
//-To use TRACER, follow these 5 steps.---------
//----------------------------------------------
//----1. Include this header in your program.---
// Includes necessary macros.
//----------------------------------------------
//----2. Add macro TRACERVARIABLES above main().
// Defines necessary variables / defines.-
//----------------------------------------------
//----3. Add macro TRACERHELLO in your WM_CREATE
// Put between WM_CREATE and break;
// Initiates handshaking with debugger.
//----------------------------------------------
//----4. Add macro TRACERATTACH in your WndProc.
// Put as a separate case in your WndProc.
// Completes handshaking with debugger.
//----------------------------------------------
//----!!! When placing the TRACERATTACH in your-
//--------window procedure, don't forget the----
//--------break; statement after it. !!!--------
//----------------------------------------------
//----5. Use macro TRACER("string") freely.-----
// This is the debug statement.
//----------------------------------------------
//----Note:-------------------------------------
//----You may need to include EXTERNTRACERVARS--
//----in secondary modules if you want to call--
//----TRACER from those modules.----------------
//----------------------------------------------
//----------------------------------------------
//-Note:----------------------------------------
//----------------------------------------------
//----In the TRACERHELLO macro, there is a param
//----called hWnd. You need to pass your window
//----handle to me so that I can send mine back-
//----to you. You may need to change "hWnd" to-
//----whatever your wndproc parameter name is---
//----for window handle ( e.g. MyhWnd ).--------
//----------------------------------------------
#define TRACER_SEGMENT "\\SHAREMEM\\TRACER.SEG"
#define TRACER_SEMAPHORE "\\SEM\\TRACER.SEM"
#define TRACERICON 1
#define ID_HELPBUTTON 2
#define ID_TRACERLB 3
#define IDMOPTIONS 4
#define IDMABOUT 5
#define ID_OK 6
#define ID_MENU 7
#define IDMCLEAR 8
#define IDMLOG 9
#define ID_CANCEL 10
#define ID_LOGFILEEDIT 11
#define ID_LOGFILESCROLL 12
#define ID_SCREENSCROLL 13
#define ID_REFRESH 14
// Step 1 - INCLUDE THIS FILE IN YOUR APP CODE AS FOLLOWS
//#include <tracer.h>
#define TRACER_REQUEST_HANDLE WM_USER + 501
#define TRACER_RECEIVE_HANDLE WM_USER + 502
#define TRACER_REQUEST_ACTION WM_USER + 503
#define TRACER_GOODBYE WM_USER + 504
#define TRACER_MYCLOSE WM_USER + 505
// Step 2 - INCLUDE THE TRACERVARIABLES STATEMENT IN YOUR
// DECLARATION SECTION
#define TRACERVARIABLES \
unsigned short sTracerSelector; \
HSYSSEM hTracerSysSem; \
BOOL bTracerConnected; \
char far * szSelector_string; \
HWND hTracerWnd;
// Step 3 - INCLUDE THE TRACERHELLO STATEMENT IN YOUR
// WM_CREATE MESSAGE
#define TRACERHELLO \
WinBroadcastMsg( hWnd, TRACER_REQUEST_HANDLE, \
MPFROMHWND( hWnd ), 0L, \
BMSG_FRAMEONLY | BMSG_POSTQUEUE );
// Step 4 - INCLUDE THE TRACERATTACH STATEMENT IN YOUR
// MESSAGE WNDPROC
#define TRACERATTACH \
case TRACER_RECEIVE_HANDLE: \
if ( bTracerConnected ) \
break; \
hTracerWnd = HWNDFROMMP( mp1 ); \
bTracerConnected = 1; \
if ( DosGetShrSeg( TRACER_SEGMENT, &sTracerSelector ) ) \
bTracerConnected = 0; \
if ( DosOpenSem( &hTracerSysSem, TRACER_SEMAPHORE ) ) \
bTracerConnected = 0;
// Step 5 - THEN USE THE TRACER("HELLO, WORLD") MACRO FREELY
#define TRACER(s) \
if ( bTracerConnected ) \
{ \
DosSemRequest( hTracerSysSem, -1L ); \
szSelector_string = \
( char far * )( ( unsigned long )sTracerSelector << 16 ); \
strncpy( ( char far * )szSelector_string, ( char far * )s, 80 ); \
szSelector_string[80] = '\0'; \
WinSendMsg( hTracerWnd, TRACER_REQUEST_ACTION, 0L, 0L ); \
DosSemClear( hTracerSysSem ); \
}
#define EXTERNTRACERVARS \
extern unsigned short sTracerSelector; \
extern HSYSSEM hTracerSysSem; \
extern BOOL bTracerConnected; \
extern char far * szSelector_string; \
extern HWND hTracerWnd;
TRACER.DEF
NAME TRACER WINDOWAPI
DESCRIPTION 'OS/2 PM Tracer Utility'
STUB 'OS2STUB.EXE'
CODE MOVEABLE
DATA MOVEABLE MULTIPLE
HEAPSIZE 1024
STACKSIZE 4096
EXPORTS
TracerWndProc @1
HelpHook @2
TracerAboutDlg @3
TracerLogFileDlg @4
TRACER.LNK
tracer.obj +
tracerp.obj +
tracerd.obj
tracer/align:16
tracer/map
os2 llibcmt doscalls/NOD
tracer.def
TRACER.RC
#include <os2.h>
#include "tracer.h"
POINTER TRACERICON tracer.ico
MENU TRACERICON PRELOAD
BEGIN
SUBMENU "~Options", IDMOPTIONS
BEGIN
MENUITEM "~Clear Message Area\t^C", IDMCLEAR, MIS_TEXT
MENUITEM "~Log Messages to File...\t^L", IDMLOG, MIS_TEXT
MENUITEM "~About...\t^A", IDMABOUT, MIS_TEXT
END
MENUITEM "~F1=Help", ID_HELPBUTTON, MIS_HELP | MIS_BUTTONSEPARATOR
END
ACCELTABLE TRACERICON
BEGIN
"^A", IDMABOUT
"^L", IDMLOG
"^C", IDMCLEAR
END
DLGTEMPLATE IDMABOUT LOADONCALL MOVEABLE DISCARDABLE
BEGIN
DIALOG "", 5, 10, 23, 190, 52, FS_NOBYTEALIGN | FS_DLGBORDER |
WS_CLIPSIBLINGS | WS_SAVEBITS
BEGIN
CONTROL "OS/2 Debugging Utility", -1, 17, 35, 156, 14,
WC_STATIC, SS_TEXT | DT_CENTER | DT_TOP | WS_GROUP |
WS_VISIBLE
CONTROL "Version 1.00", -1, 57, 17, 74, 8, WC_STATIC,
SS_TEXT | DT_CENTER | DT_TOP | WS_GROUP | WS_VISIBLE
CONTROL "OK", 6, 5, 2, 38, 12, WC_BUTTON, BS_PUSHBUTTON |
BS_DEFAULT | WS_TABSTOP | WS_VISIBLE
CONTROL "Daniel Hildebrand", 256, 55, 29, 82, 8, WC_STATIC,
SS_TEXT | DT_LEFT | DT_TOP | WS_GROUP | WS_VISIBLE
END
END
DLGTEMPLATE 9 LOADONCALL MOVEABLE DISCARDABLE
BEGIN
DIALOG "", 9, 10, 17, 209, 74, FS_NOBYTEALIGN | FS_DLGBORDER |
WS_CLIPSIBLINGS | WS_SAVEBITS
BEGIN
CONTROL "Ok", 6, 7, 4, 38, 12, WC_BUTTON, BS_PUSHBUTTON |
BS_DEFAULT | WS_TABSTOP | WS_VISIBLE
CONTROL "Cancel", 10, 51, 4, 38, 12, WC_BUTTON,
BS_PUSHBUTTON | BS_DEFAULT | WS_TABSTOP | WS_VISIBLE
CONTROL "Log File: ", -1, 6, 57, 50, 10, WC_STATIC, SS_TEXT
| DT_CENTER | DT_TOP | WS_GROUP | WS_VISIBLE
CONTROL "\\tracer.fle", 11, 63, 59, 100, 8, WC_ENTRYFIELD,
ES_LEFT | ES_AUTOSCROLL | ES_MARGIN | WS_TABSTOP |
WS_VISIBLE
CONTROL "Screen Scroll On/Off", 13, 106, 24, 103, 9,
WC_BUTTON, BS_AUTOCHECKBOX | WS_TABSTOP | WS_VISIBLE
CONTROL "Log File On/Off", 12, 10, 24, 91, 9, WC_BUTTON,
BS_AUTOCHECKBOX | WS_TABSTOP | WS_VISIBLE
CONTROL "Help", 2, 97, 4, 38, 12, WC_BUTTON, BS_PUSHBUTTON |
BS_DEFAULT | BS_HELP | WS_TABSTOP | WS_VISIBLE
CONTROL "Truncate Log File before proceeding", 14, 10, 41,
171, 10,
WC_BUTTON, BS_AUTOCHECKBOX | WS_TABSTOP | WS_VISIBLE
END
END
TRACER.C
//----------------------------------------------
//-TRACER ( OS/2 PM Debugging Trace Facility )--
//----------------------------------------------
//-(c) 1990 Daniel Hildebrand-------------------
//----------------------------------------------
#define INCL_PM
#define INCL_WIN
#define INCL_DOS
#define INCL_WINLISTBOXES
#define INCL_WINHOOKS
#define INCL_ERRORS
#include <os2.h>
#include <process.h>
#include <stdlib.h>
#include <string.h>
#include "tracer.h"
//----------------------------------------------
//-Module declarations--------------------------
//----------------------------------------------
int main ( void );
MRESULT EXPENTRY TracerWndProc ( HWND hWnd, USHORT msg,
MPARAM mp1, MPARAM mp2 );
VOID TracerCommand ( HWND hWnd, SHORT id,
SHORT source, BOOL mouse );
static VOID ClearSelector ( char far * selector_string );
BOOL EXPENTRY HelpHook ( HAB hab, USHORT usMode,
USHORT idTopic, USHORT idSubTopic,
PRECTL prcPosition );
static VOID HelpMessage ( void );
//----------------------------------------------
//-External references--------------------------
//----------------------------------------------
extern
void FAR PASCAL TracerPaint ( HWND hWnd, USHORT msg,
MPARAM mp1, MPARAM mp2 );
extern
void FAR PASCAL ClearLB ( HWND hLB );
extern
VOID LogToFile ( PSZ selector_string );
extern
MRESULT EXPENTRY TracerAboutDlg ( HWND hWndDlg,
USHORT message,
MPARAM mp1, MPARAM mp2 );
extern
MRESULT EXPENTRY TracerLogFileDlg ( HWND hWndDlg,
USHORT message,
MPARAM mp1, MPARAM mp2 );
//----------------------------------------------
//-Global variables-----------------------------
//----------------------------------------------
HAB hAB; /* anchor block */
HMQ hmqTracer; /* handle queue */
HSYSSEM hSysSem; /* sys semaphore */
HFILE pLogFile; /* handle to file */
HWND hParentWnd; /* client hWnd */
HWND hParentFrm; /* frame hWnd */
HWND hLB; /* handle ListBox */
BOOL bUserRequestsScroll = TRUE; /* Scroll ON ? */
BOOL bUserRequestsNewLog = TRUE; /* Truncate Log? */
BOOL bUserRequestsLog = FALSE; /* LogFile ON ? */
BOOL bLoadFail; /* TRACER Load OK?*/
char szMessage[] = " PM TRACER ";
/* Log File */
char szLogFile[] = "\\tracer.fle";
/* Window Class */
char szParentClass[] = "PClass";
char far *selector_string; /* pointer to named shared mem */
int iNumberItems; /* # error strings*/
USHORT selector; /* selector to named shared mem*/
USHORT pusAction; /* file IO */
USHORT pusBytesWritten; /* file IO */
//----------------------------------------------
//-main()---------------------------------------
//----------------------------------------------
int main( )
{
QMSG qmsg;
ULONG ctldata;
SWP swpCurrent;
int aiGen;
// initialize this process
hAB = WinInitialize( NULL );
// create a PM message queue
hmqTracer = WinCreateMsgQueue( hAB, 0 );
// register window class
if ( !WinRegisterClass( hAB,
(PCH)szParentClass,
(PFNWP)TracerWndProc,
CS_SIZEREDRAW,
0 ) )
return( 0 );
// file control flags
ctldata = FCF_TITLEBAR | FCF_SYSMENU | FCF_BORDER |
FCF_MINBUTTON | FCF_MENU | FCF_ICON |
FCF_SHELLPOSITION | FCF_TASKLIST | FCF_ACCELTABLE;
// create standard window
hParentFrm = WinCreateStdWindow( HWND_DESKTOP,
WS_SYNCPAINT,
&ctldata,
(PCH)szParentClass,
NULL,
0L,
(HMODULE)NULL,
TRACERICON,
(HWND FAR *)&hParentWnd );
// if semaphore was obtained
// if named shared memory segment was obtained
// if tracer file was opened
// ...
if ( ! bLoadFail )
{
// auditory feedback to user that app coming up OK.
for ( aiGen = 0; aiGen < 12; aiGen += 2 )
DosBeep( ((aiGen + 1) * 100), 1 );
WinSetWindowText( hParentFrm, "OS\\2 TRACER Version 1.0");
WinSetWindowPos ( hParentFrm, HWND_TOP,
20,
10,
500,
125,
SWP_SIZE | SWP_MOVE | SWP_SHOW );
}
// enter message loop
while( WinGetMsg( hAB, (PQMSG)&qmsg, (HWND)NULL, 0, 0 ) )
WinDispatchMsg( hAB, (PQMSG)&qmsg );
// destroy TRACER window
WinDestroyWindow( hParentFrm );
// destroy message queue
WinDestroyMsgQueue( hmqTracer );
// bye bye
WinTerminate( hAB );
}
//----------------------------------------------
//-TracerWndProc()------------------------------
//----------------------------------------------
//----------------------------------------------
MRESULT EXPENTRY TracerWndProc( HWND hWnd, USHORT msg,
MPARAM mp1, MPARAM mp2 )
{
RECTL rRect;
int aiGen;
switch (msg)
{
case WM_CREATE:
// if semaphore already exists ...
if ( DosCreateSem( CSEM_PUBLIC, &hSysSem,
TRACER_SEMAPHORE ) )
// ... and we cannot open it, then abort
if ( DosOpenSem( &hSysSem, TRACER_SEMAPHORE ) )
{
DosBeep(300,300);
WinMessageBox( HWND_DESKTOP, hWnd,
(PCH)"Failed to Load!",
(PCH)"OS/2 TRACER Version 1.0", NULL,
MB_OK| MB_ICONEXCLAMATION );
bLoadFail = 1;
WinPostMsg( hWnd, WM_QUIT, 0L, 0L );
break;
}
// allocate an 81 byte data segment
if ( DosAllocShrSeg( 81, TRACER_SEGMENT,
&selector ) )
{
DosBeep(300,300);
WinMessageBox( HWND_DESKTOP, hWnd,
(PCH)"Failed to Load!",
(PCH)"OS/2 TRACER Version 1.0", NULL,
MB_OK| MB_ICONEXCLAMATION );
bLoadFail = 1;
WinPostMsg( hWnd, WM_QUIT, 0L, 0L );
break;
}
// open TRACER file
DosOpen( (PSZ)szLogFile, &pLogFile, &pusAction, 100L,
0, 0x11, 0x41, 0L );
// obtain a pointer to the named segment
selector_string =
(char far *)((unsigned long)selector << 16);
// clear the segment
ClearSelector( selector_string );
// F1 help hook
WinSetHook( hAB, hmqTracer, HK_HELP, (PFN)HelpHook, NULL );
// Send to anyone who was brought up BEFORE the TRACER
WinBroadcastMsg( hWnd, TRACER_RECEIVE_HANDLE,
MPFROMHWND( hWnd ), 0L,
BMSG_FRAMEONLY | BMSG_POSTQUEUE );
return( WinDefWindowProc( hWnd, msg, mp1, mp2 ) );
break;
case WM_COMMAND:
// handle menu commands
TracerCommand( hWnd, SHORT1FROMMP(mp1),
SHORT1FROMMP(mp2), SHORT2FROMMP(mp2));
break;
case TRACER_REQUEST_HANDLE:
// a client has broadcast a request for handshake
WinPostMsg( HWNDFROMMP(mp1), TRACER_RECEIVE_HANDLE,
MPFROMHWND(hWnd), 0L );
break;
case TRACER_REQUEST_ACTION:
// client has issued a TRACER() request
// is screen toggled ON ?
if ( bUserRequestsScroll )
{
selector_string[80] = '\0';
WinSendMsg( hLB, LM_INSERTITEM, (MPARAM)-1L,
(MPARAM)(PCH)selector_string );
WinSendMsg( hLB, LM_SELECTITEM,
(MPARAM)(iNumberItems - 1), (MPARAM)FALSE );
WinSendMsg( hLB, LM_SELECTITEM,
(MPARAM)iNumberItems++, (MPARAM)TRUE );
WinSendMsg( hLB, LM_SETTOPINDEX,
(MPARAM)(iNumberItems - 1),
(MPARAM)TRUE );
}
// is file toggled ON ?
if ( bUserRequestsLog )
{
selector_string[80] = '\0';
LogToFile( selector_string );
}
ClearSelector( selector_string );
break;
case WM_CLOSE:
// return semaphore to system
DosCloseSem( hSysSem );
// is semaphore available to be obtained ?
if ( DosCreateSem( CSEM_PUBLIC, &hSysSem,
TRACER_SEMAPHORE ) )
{
// that's no good. a client must still have it opened !
DosBeep(75,200);
WinMessageBox( HWND_DESKTOP, hWnd,
(PCH)
"Will not shut down while client(s) are still attached!",
(PCH)"OS/2 TRACER Version 1.0", NULL,
MB_OK| MB_ICONEXCLAMATION );
DosOpenSem( &hSysSem, TRACER_SEMAPHORE );
break;
}
else
DosCloseSem( hSysSem ); /* one for create */
// continue close down
WinPostMsg( hWnd, TRACER_MYCLOSE, 0L, 0L );
break;
case TRACER_MYCLOSE:
// free the named shared data segment
if ( DosFreeSeg( selector ) ) DosBeep( 100, 450 );
// release the F1 help hook
WinReleaseHook ( hAB, hmqTracer, HK_HELP,
(PFN)HelpHook, NULL );
// flush the buffer to the TRACER file / close it
DosBufReset( pLogFile );
DosClose( pLogFile );
// auditory feedback to user that app coming down OK.
for ( aiGen = 12; aiGen > 0; aiGen -= 2 )
DosBeep( ((aiGen - 1) * 100), 1 );
WinPostMsg( hWnd, WM_QUIT, 0L, 0L );
break;
case WM_PAINT:
// paint routine for TRACER window
TracerPaint( hWnd, msg, mp1, mp2 );
break;
case WM_ERASEBACKGROUND:
return( TRUE );
break;
default:
return( WinDefWindowProc( hWnd, msg, mp1, mp2 ) );
break;
}
return(0L);
}
//----------------------------------------------
//--HelpHook------------------------------------
//----------------------------------------------
//----------------------------------------------
BOOL EXPENTRY HelpHook( HAB hab, USHORT usMode,
USHORT idTopic, USHORT idSubTopic, PRECTL prcPosition )
{
WinMessageBox( HWND_DESKTOP, hParentWnd,
(PCH)
"TRACER.H has instructions on how to use TRACER in your app.",
(PCH)"OS/2 TRACER HelpHook", NULL,
MB_OK| MB_CUANOTIFICATION );
#ifdef HelpHook
switch ( usMode )
{
case HLPM_MENU:
// idtopic is submenu identifier
// idsubtopic is item identifier
// prcposition is boundary of item
switch ( idTopic )
{
case ID_HELPBUTTON:
HelpMessage();
}
break;
case HLPM_FRAME:
// idtopic is frame identifier
// idsubtopic is focus window identifier
// prcposition is boundary of focus window
switch ( idTopic )
{
case ID_HELPBUTTON:
HelpMessage();
}
break;
case HLPM_WINDOW:
// idtopic is parent of focus window
// idsubtopic is focus window identifier
// prcposition is boundary of focus window
switch ( idTopic )
{
case ID_HELPBUTTON:
HelpMessage();
}
break;
}
#endif
return TRUE;
}
//----------------------------------------------
//--TracerCommand-------------------------------
//----------------------------------------------
//----------------------------------------------
VOID TracerCommand( HWND hWnd, SHORT id, SHORT source, BOOL mouse )
{
switch( id )
{
case IDMABOUT:
WinDlgBox( HWND_DESKTOP, hWnd, (PFNWP)TracerAboutDlg,
NULL, IDMABOUT, NULL );
break;
case IDMLOG:
WinDlgBox( HWND_DESKTOP, hWnd, (PFNWP)TracerLogFileDlg,
NULL, IDMLOG, NULL );
break;
case IDMCLEAR:
ClearLB( hLB );
break;
}
}
//----------------------------------------------
//-HelpMessage()--------------------------------
//----------------------------------------------
//----------------------------------------------
static VOID HelpMessage ( void )
{
WinMessageBox( HWND_DESKTOP, hParentWnd,
(PCH)
"TRACER.H has instructions on how to use TRACER in your app.",
(PCH)"OS/2 TRACER HelpHook", NULL,
MB_OK| MB_CUANOTIFICATION );
return;
}
//----------------------------------------------
//-ClearSelector()------------------------------
//----------------------------------------------
//----------------------------------------------
static VOID ClearSelector ( PSZ selector_string )
{
int aiX;
for ( aiX = 0; aiX < 80; aiX++ )
selector_string[aiX] = ' ';
return;
}
TRACERD.C
//-----------------------------------------------
//-TRACERD.C Source code for TRACER file routines
//-----------------------------------------------
#define INCL_PM
#define INCL_WIN
#define INCL_DOS
#define INCL_WINLISTBOXES
#define INCL_WINBUTTONS
#define INCL_WINHOOKS
#define INCL_ERRORS
#include <os2.h>
#include "tracer.h"
//----------------------------------------------
//-External references--------------------------
//----------------------------------------------
extern char szLogFile[];
extern BOOL bUserRequestsScroll;
extern BOOL bUserRequestsLog;
extern BOOL bUserRequestsNewLog;
extern HFILE pLogFile;
extern USHORT pusAction;
extern USHORT pusBytesWritten;
//----------------------------------------------
//--LogToFile()---------------------------------
//----------------------------------------------
VOID LogToFile ( PSZ selector_string )
{
// write text string to log file
DosWrite( pLogFile, (char far *)selector_string,
(USHORT)80, &pusBytesWritten );
DosWrite( pLogFile, (char far *)"\r\n",
(USHORT)2, &pusBytesWritten );
return;
}
//----------------------------------------------
//--TracerAboutDlg()----------------------------
//----------------------------------------------
MRESULT EXPENTRY TracerAboutDlg( HWND hWndDlg, USHORT message,
MPARAM mp1, MPARAM mp2 )
{
switch( message )
{
case WM_COMMAND:
/* the user has pressed a button */
switch( SHORT1FROMMP( mp1 ) )
{
case ID_OK:
WinDismissDlg( hWndDlg, TRUE );
break;
default:
return( FALSE );
}
break;
default:
return( WinDefDlgProc( hWndDlg, message, mp1, mp2 ) );
}
return( FALSE );
}
//----------------------------------------------
//--TracerLogFileDlg()--------------------------
//----------------------------------------------
MRESULT EXPENTRY TracerLogFileDlg( HWND hWndDlg, USHORT message,
MPARAM mp1, MPARAM mp2 )
{
switch( message )
{
case WM_INITDLG:
// options controls init
WinSendDlgItemMsg( hWndDlg, ID_LOGFILESCROLL, BM_SETCHECK,
MPFROM2SHORT( (bUserRequestsLog) ? 1 : 0, 0), 0L );
WinSendDlgItemMsg( hWndDlg, ID_SCREENSCROLL, BM_SETCHECK,
MPFROM2SHORT( (bUserRequestsScroll) ? 1 : 0, 0), 0L );
case WM_COMMAND:
switch( SHORT1FROMMP( mp1 ) )
{
case ID_CANCEL:
WinDismissDlg( hWndDlg, TRUE );
break;
case ID_OK:
// if user wishes to truncate log file, do it
if (
bUserRequestsNewLog =
(SHORT)WinSendDlgItemMsg( hWndDlg,
ID_SCREENSCROLL, BM_QUERYCHECK, 0L, 0L ) )
DosNewSize( pLogFile, 0L );
// does user wish to see messages on screen ?
bUserRequestsScroll =
(SHORT)WinSendDlgItemMsg( hWndDlg, ID_SCREENSCROLL,
BM_QUERYCHECK, 0L, 0L );
// does user wish to log messages to file ?
bUserRequestsLog =
(SHORT)WinSendDlgItemMsg( hWndDlg, ID_LOGFILESCROLL,
BM_QUERYCHECK, 0L, 0L );
WinDismissDlg( hWndDlg, TRUE );
break;
default:
return( FALSE );
}
break;
default:
return( WinDefDlgProc( hWndDlg, message, mp1, mp2 ) );
}
return( FALSE );
}
TRACERP.C
//-----------------------------------------------
//-TRACERP.C Source code for TRACER paint routine
//-----------------------------------------------
#define INCL_PM
#define INCL_WIN
#define INCL_DOS
#include <os2.h>
#include <string.h>
#include "tracer.h"
//----------------------------------------------
//-Module declarations--------------------------
//----------------------------------------------
void FAR PASCAL ClearLB( HWND hLB );
static int iFirstTimeOnly;
//----------------------------------------------
//-External references--------------------------
//----------------------------------------------
extern HWND hLB;
extern HWND hParentWnd;
extern int iNumberItems;
//----------------------------------------------
//-TRACERPaint()--------------------------------
//----------------------------------------------
void FAR PASCAL TRACERPaint( HWND hWnd, USHORT msg,
MPARAM mp1, MPARAM mp2 )
{
HPS hPS;
RECTL rRect;
// create listbox on first paint only
if ( ! iFirstTimeOnly )
{
hLB = WinCreateWindow( hParentWnd,
WC_LISTBOX,
"",
WS_VISIBLE | WS_SYNCPAINT,
4, 4, 5, 5,
hParentWnd,
HWND_TOP,
ID_TRACERLB,
NULL,
0 );
WinQueryWindowRect( hWnd, &rRect );
WinSetWindowPos ( hLB, HWND_TOP, 6,
4,
(SHORT)(rRect.xRight - rRect.xLeft - 14),
(SHORT)(rRect.yTop - rRect.yBottom - 4),
SWP_SIZE | SWP_MOVE | SWP_SHOW );
ClearLB( hLB );
iFirstTimeOnly = 1;
}
// quick and dirty
hPS = WinBeginPaint( hWnd, (HPS)NULL, (PWRECT)NULL );
GpiErase( hPS );
WinEndPaint( hPS );
}
//----------------------------------------------
//-ClearLB()------------------------------------
//----------------------------------------------
void FAR PASCAL ClearLB( HWND hLB )
{
iNumberItems = 0;
WinSendMsg( hLB, LM_DELETEALL,
(MPARAM)-1L, (MPARAM)0L );
WinSendMsg( hLB, LM_INSERTITEM,
(MPARAM)-1L, (MPARAM)(PCH)"Begin..." );
WinSendMsg( hLB, LM_SELECTITEM, (MPARAM)iNumberItems++,
(MPARAM)TRUE );
}
Figure 3 CLIENT
#-------------------------------------------------------------------
#
# Standard command line definitions
#
#-------------------------------------------------------------------
cp=cl -c -W3 -Alfw -G2sw -Os -Zpei
#-------------------------------------------------------------------
#
# Default inference rules
#
#-------------------------------------------------------------------
.c.obj:
$(cp) $*.c
.asm.obj:
masm $*.asm;
.rc.res:
rc -r $*.rc
#-------------------------------------------------------------------
#
# Dependencies
#
#-------------------------------------------------------------------
client.obj: client.c client.h
client.res: client.rc client.ico client.h
client.exe: client.obj client.res client.lnk client.def
link @client.lnk
rc client.res
CLIENT.C
//------------------------
//-CLIENT.C------
//------------------------
#define INCL_PM
#define INCL_WIN
#define INCL_DOS
#include <os2.h>
#include <stdio.h>
#include <string.h>
#include "client.h"
// TRACER step 1 - include tracer.h
#include "tracer.h"
// TRACER step 2 - declare TRACER VARIABLES
TRACERVARIABLES
//----------------------------------------------
//-main()--------------------------------
//----------------------------------------------
int cdecl main( )
{
QMSG qmsg;
ULONG ctldata;
hAB = WinInitialize( NULL );
hmqClient = WinCreateMsgQueue( hAB, 0 );
if ( !WinRegisterClass( hAB,
(PCH)szPClass,
(PFNWP)ClientWndProc,
CS_SIZEREDRAW,
0) )
return( 0 );
ctldata = FCF_TITLEBAR | FCF_SYSMENU |
FCF_SIZEBORDER | FCF_MINMAX |
FCF_ICON | FCF_SHELLPOSITION |
FCF_TASKLIST ;
hPanelFrm = WinCreateStdWindow( HWND_DESKTOP,
WS_DISABLED,
&ctldata,
(PCH)szPClass,
NULL,
0L,
(HMODULE)NULL,
CLIENTICON,
(HWND FAR *)&hPanelWnd );
WinEnableWindow( hPanelFrm, TRUE );
WinShowWindow( hPanelFrm, TRUE );
while( WinGetMsg( hAB, (PQMSG)&qmsg, (HWND)NULL, 0, 0 ) )
WinDispatchMsg( hAB, (PQMSG)&qmsg );
WinDestroyWindow( hPanelFrm );
WinDestroyMsgQueue( hmqClient );
WinTerminate( hAB );
}
//----------------------------------------------
//-ClientWndProc()--------------
//----------------------------------------------
MRESULT EXPENTRY ClientWndProc( HWND hWnd, USHORT msg,
MPARAM mp1, MPARAM mp2 )
{
switch (msg)
{
case WM_CREATE:
// TRACER step 3 - attach to TRACER
TRACERHELLO
return( WinDefWindowProc( hWnd, msg, mp1, mp2 ) );
break;
// TRACER step 4 - obtain system resources - Don't forget break;
TRACERATTACH
break;
case WM_CLOSE:
// TRACER step 5
TRACER("Posting QUIT");
WinPostMsg( hWnd, WM_QUIT, 0L, 0L );
break;
case WM_SETFOCUS:
// TRACER step 5
TRACER("Setting Focus");
return( WinDefWindowProc( hWnd, msg, mp1, mp2 ) );
break;
case WM_ENABLE:
return( WinDefWindowProc( hWnd, msg, mp1, mp2 ) );
break;
case WM_BUTTON1DOWN:
sprintf( szTraceBuf, "WM_BUTTON1DOWN hit at %ld",
WinGetCurrentTime( hAB ) );
TRACER( szTraceBuf );
break;
case WM_PAINT:
ClientPaint( hWnd, msg, mp1, mp2 );
break;
case WM_ERASEBACKGROUND:
return( TRUE );
break;
default:
return( WinDefWindowProc( hWnd, msg, mp1, mp2 ) );
break;
}
return(0L);
}
//----------------------------------------------
//-ClientPaint()------------------
//----------------------------------------------
void FAR PASCAL ClientPaint( HWND hWnd, USHORT msg,
MPARAM mp1, MPARAM mp2 )
{
int aiCharWidth;
HPS hPS;
FONTMETRICS fm;
RECTL rRect;
CHARBUNDLE cb;
POINTL pt;
hPS = WinBeginPaint( hWnd, (HPS)NULL, (PWRECT)NULL );
// TRACER step 5
TRACER("ClientPaint()");
GpiErase( hPS );
GpiQueryFontMetrics( hPS, (LONG)sizeof fm , &fm);
aiCharWidth = (SHORT)fm.lAveCharWidth;
WinQueryWindowRect( hWnd, &rRect );
pt.x = (rRect.xRight / 2) -
((strlen(szMessage) / 2) * aiCharWidth);
pt.y = (rRect.yTop / 2);
cb.lColor = CLR_BLACK;
GpiSetAttrs( hPS, PRIM_CHAR, CBB_COLOR, 0L, (PBUNDLE)&cb );
GpiCharStringAt( hPS, &pt, (LONG)strlen( szMessage ), szMessage );
WinEndPaint( hPS );
}
CLIENT.DEF
NAME CLIENT WINDOWAPI
DESCRIPTION 'Client to TRACER'
STUB 'OS2STUB.EXE'
CODE MOVEABLE
DATA MOVEABLE MULTIPLE
HEAPSIZE 1024
STACKSIZE 4096
EXPORTS
ClientWndProc @1
CLIENT.H
#define CLIENTICON 1
extern void FAR PASCAL ClientPaint( HWND hWnd, USHORT msg,
MPARAM mp1, MPARAM mp2 );
MRESULT EXPENTRY ClientWndProc( HWND, USHORT, MPARAM, MPARAM );
int cdecl main( void );
char szMessage[] = " Client App ";
char szPClass[] = "PClass";
HAB hAB;
HMQ hmqClient;
HWND hPanelWnd;
HWND hPanelFrm;
char szTraceBuf[81];
CLIENT.LNK
client.obj
client/align:16
client/map
os2 llibcmt doscalls/NOD
client.def
CLIENT.RC
#include "client.h"
POINTER CLIENTICON client.ico
CHECKERS for Presentation Manager Part III: Moving the Pieces
Charles Petzold
When asked to write a checkers program for the OS/2 Presentation Manager
(hereafter "PM"), I started researching bulletin boards to see what other
noncommercial checkers games were available. I found one that was several
years old and had been programmed in BASIC. Although it displayed a
checkerboard on the screen, the user interface was awkward and primitive.
You had to type a number to indicate the piece you wanted to move, and
another number for the destination square.
I wanted my checkers program for PM to be just like a real game. My
program's players can therefore move a piece using the mouse as a surrogate
for their fingers. This makes the program difficult to write, but then
nobody ever claimed that programming for an interactive, graphical
environment was easy.
In the last issue, I described how the checkerboard and pieces are depicted
on the program's window. The next step is to add an interface to move the
pieces using the mouse or the keyboard. Complete validation of moves and
jumps, including logic to remove jumped pieces and logic to king those
pieces reaching the opposite side of the board, has also been added. In
fact, this version of CHECKERS lets you play an entire game, albeit only by
yourself, by alternating the black and white pieces. (All the source code
and the CHECKERS.EXE executable file may be downloaded from any MSJ bulletin
board--Ed.)
Mouse Pointers
A CHECKERS game in progress is shown in Figure 1. On the right side of the
board, a customized mouse pointer in the shape of a hand indicates which
player's turn it is to move. A hand with fingers pointing up means that it's
black's turn. After a black move, the pointer changes to a hand with fingers
pointing down, indicating a white move.
Originally I thought that the mouse pointer should resemble a checkers piece
when the user selects a piece to move. But the size of a PM mouse pointer is
fixed, based on the resolution of the video display--only by
coincidence would this pointer be the same size as the pieces shown on the
board. Instead, I decided to store the playing pieces as bitmaps. When a
piece is selected with the mouse, the program hides the normal mouse pointer
and moves the piece bitmap around the window based on the position of the
mouse. This complicates things considerably (in effect, I have to duplicate
mouse pointer drawing logic), but it looks much nicer (see Figure 2).
Experienced users of the Windows and PM environments who have seen CHECKERS
are unhappy about the way the mouse buttons move a piece on the board. They
expect to drag a piece from one square to another while the mouse button is
depressed. Releasing the mouse button would set the piece down on the new
square. But that's not the way this version of CHECKERS works. You must
click (press and release) the mouse button to pick up the piece, and click
again to set it down. That may seem like an insignificant difference, but
it's different from dragging operations in other programs.
Dragging wasn't used because it causes problems with multiple jumps.
According to the rules of checkers, if a player jumps a piece and it is
possible to jump another piece, the move must continue. Using dragging, the
player would release the mouse button to indicate the destination square of
the first jump, but for the second jump, the player would still have control
of the piece without any button depressed.
To avoid this problem, CHECKERS requires you to press and release the mouse
button to pick up a piece, and press and release the button again to set it
down. The program indicates that there are more moves to be made by not
changing the mouse pointer back to one of the hands; it's as if the program
will not let you put down the piece until you've finished the move.
A Little Restructuring
In the last installment, I had two C source code files: CHECKERS.C contained
the main function, the client window procedure, and a dialog box procedure
for the "About" box. CKRDRAW.C contained all the drawing functions,
including a dialog box for changing the colors. I've added three files and
two additional window procedures to the new version. Their relationship is
shown in Figure 3.
The three window procedures communicate with each other through user-defined
messages found in CHECKERS.H. JudgeWndProc in CKRJUDGE.C calls functions in
CKRMOVES.C to determine valid moves and jumps. BoardWndProc in CKRBOARD.C
handles user input and calls functions in CKRDRAW.C in order to draw the
board.
Although there are a number of global variables in CKRDRAW.C, only hab, the
infamous anchor block handle, is global to more than one source code file.
The anchor block handle is obtained by a call to WinInitialize in main and
used as a parameter to several other PM functions in other modules.
CHECKERS.C and the associated files for compilation are shown in Figure 4.
The ClientWndProc window procedure processes menu commands and creates
JudgeWndProc and BoardWndProc. A new menu command, New Game, causes
everything to be reset and the board to be redrawn for starting a new game.
In future versions of the program, ClientWndProc will display a dialog box
when you select this option. This dialog box will let you select the type of
game you want to play, such as a game against the program itself or against
another player across a network. ClientWndProc will then instruct
JudgeWndProc to begin the game.
CKRJUDGE.C is shown in Figure 5. JudgeWndProc is the window procedure for an
object window (that is, a child of HWND_OBJECT). Object windows do not
appear on the screen and do not process user input, but they can send and
receive messages just like normal windows.
JudgeWndProc controls the game by maintaining a description of the current
board layout in a structure of type BOARD (defined in CHECKERS.H), by
telling a player when to make a move, and by determining whether a move is
valid. In future versions of the program, the Judge will maintain a log of
the game and communicate with modules that implement other parts of the
CHECKERS program, particularly the checkers-playing strategy and the network
communications.
CKRBOARD.C is shown in Figure 6. BoardWndProc is the window procedure for a
child of the client window. This window is set to the same size as the
client window and completely covers it. BoardWndProc communicates with the
user by drawing the board and processing keyboard and mouse input. In a
future version of the program, BoardWndProc will also move a piece, on
command from the Judge, to make a move chosen by the checkers-playing
strategy.
Hit-Testing With GPI
When the user clicks the mouse button on the CHECKERS window, the program
must determine which board square (if any) the mouse pointer is positioned
over. This process is called hit-testing, and is one of the many techniques
you must learn to program for a graphical interface such as PM. For more
information on hit-testing, see chapter 9 of Programming the OS/2
Presentation Manager (Microsoft Press, 1989).
It's easy to hit-test on a square grid like the checkerboard. But I had to
make things hard for myself. To give the board a three-dimensional effect, I
chose to draw the checkerboard as a grid of trapezoids, not squares.
Although determining whether a point falls within a particular trapezoid is
not particularly difficult, I decided to let the graphics programming
interface (GPI) do the hit-testing for me.
The built-in hit-testing facility in GPI is not well known, probably because
there is no single call that does this function. Indeed, the actual
hit-testing is performed by calling the same functions you call to draw
graphics on the window.
When you click on the board, BoardWndProc calls the CkdQueryHitCoords
function in CKRDRAW.C (see Figure 7). This function calls
GpiSetPickApertureSize and GpiSetPickAperturePosition to begin hit-testing.
The pick aperture position is the point you want to test (in this case, the
mouse pointer position). In some cases, you may want the hit-test to have
some tolerance and not require the user to hit the graphic directly. This
tolerance is the pick aperture size.
Calling GpiSetDrawControl with the DCTL_DISPLAY and DCTL_OFF parameters sets
the graphics display off. This prevents any subsequently called drawing
functions from drawing on the window. This is an unusual feature in a
graphics programming language, but a useful one for the next step.
To turn on hit-testing, GpiSetDrawControl is called with the DCTL_CORRELATE
and DCTL_ON parameters. Any graphics drawing functions subsequently called
return a special value (GPI_HITS, defined in PMGPI.H as 2L) if any part of
the graphic is within the pick aperture size of the pick aperture position.
After these preliminaries, CkrQueryHitCoords calls CkdDrawAllBoardSquares
(see Figure 8). This function calls CkdDrawBoardSquare 64 times to draw the
checkerboard squares. CkdDrawBoardSquare uses an area bracket (GpiBeginArea
and GpiEndArea) to draw each square. The function returns the return value
of GpiEndArea, which can be GPI_HITS if the pick aperture position is within
the area being drawn. When CkdDrawAllBoardSquares detects a return value of
GPI_HITS, it combines the x and y coordinates (each of which can range from
0 to 7) in a LONG and returns it from the function.
Although GPI's built-in hit-testing simplifies programming a great deal,
there's one problem with it in the CHECKERS program--it's just too
slow. Try clicking on a white square near the top of the board. It takes a
while before CHECKERS responds with an error beep. GPI is obviously
structured to do a generalized hit-test based on a scan-line conversion of
an enclosed area. A very specific trapezoid hit-test should be much faster.
I'll leave the program as it is for now; it does demonstrate the useful GPI
hit-testing facility. Perhaps I'll sneak in a more efficient routine in a
later version.
This version of CHECKERS also has a keyboard interface. You can move the
mouse pointer using the cursor arrow keys; the Home, End, Page Up, and Page
Down keys move the mouse pointer to the four corners of the checkerboard.
Pressing the space bar simulates a mouse click.
The keyboard interface was fairly easy to implement because I designed it to
emulate the mouse interface. During the WM_CHAR message in BoardWndProc
(contained in CKRBOARD.C), the space bar is converted into a WM_BUTTON1UP
message. The cursor keys are processed by a call to WinSetPointerPos, which
results in a WM_MOUSEMOVE message being sent to the window procedure.
Move Validation
When BoardWndProc gets a mouse click, it must determine if the move the user
is attempting to make is valid. This validation has several stages. Any
invalid move is signaled by an error beep.
First, if CkdDrawAllBoardSquares returns -1, the mouse pointer is not
positioned over one of the squares of the board. That's the easy one.
Second, BoardWndProc passes the x and y coordinates to the
CkdConvertCoordsToIndex function in CKRDRAW.C to convert them into an index
ranging from 0 through 31 (see Figure 9). If CkdConvertCoordsToIndex returns
-1, then the coordinates indicate an illegal white square--no
piece can ever be located on this square.
The third step occurs when BoardWndProc sends a WM_QUERY_JUDGE_PICKUP_PIECE
message to JudgeWndProc. JudgeWndProc returns TRUE or FALSE indicating
whether or not the piece can be picked up for a move or a jump.
JudgeWndProc uses several functions in CKRMOVES.C (see Figure 10) to
determine valid moves. These look more like the blackboard scribbles of a
demented Boolean algebraist than normal C functions, though! A group of
identifiers near the top of CKRMOVES.C make the Boolean expressions in the
various functions somewhat readable, even if incomprehensible at first. (To
review the method used to maintain the state of the board, see "Representing
the Board"--Ed.)
The CkmQueryAllMoveablePieces function uses the valnojump (valid no jump)
structure in CKRMOVES.C and the board layout to determine all pieces that
can move without jumping. CkmQueryAllJumpablePieces similarly determines all
pieces that can make jumps using the valjumps (valid jumps) structure. This
structure has two increments to indicate the piece being jumped and the
destination of the piece doing the jumping. The piece being jumped must be
the opposite color and the destination must be empty.
JudgeWndProc calls both CkmQueryAllMoveablePieces and
CkmQueryAllJumpablePieces. If the user is selecting a piece that cannot make
a jump, and a jump is available, JudgeWndProc returns FALSE. (Remember,
CHECKERS enforces the rule that jumps must be taken.) Likewise, BoardWndProc
sends messages to JudgeWndProc to determine if a destination for a piece is
valid, and to determine whether additional jumps are available.
One enhancement I am considering is for BoardWndProc and JudgeWndProc to
send messages to ClientWndProc when the user attempts to make an illegal
move. ClientWndProc would display a message on the window such as "You can't
move that piece. You have a jump available." Right now, all you get is a
beep. This is sometimes confusing. When I hear a beep, I usually have to
scan the board again to find the jump I've conveniently missed.
Moving the Pieces
I discussed in the last issue how CKRDRAW.C creates several bitmaps (stored
in the arrays ahbmPiece and ahbmMask) to draw the pieces on the
checkerboard. The ahbmMask bitmaps have a white background and black
foreground. There is one bitmap for a kinged piece and one for an unkinged
piece. These are drawn by first using GpiBitBlt with a raster operation of
ROP_SRCAND, which performs a bitwise AND operation between the board and the
bitmap, leaving a black hole where the piece would be. The ahbmPiece bitmaps
have an image of the piece with a black background. Four of these bitmaps
are required: one black kinged, one black unkinged, one white kinged, and
one white unkinged. GpiBitBlt draws the piece with a raster operation of
ROP_SRCPAINT (a bitwise OR operation).
In this version, CKRDRAW.C creates two more sets of bitmaps, ahbmSave and
ahbmMove. The ahbmSave bitmaps (for kinged and unkinged pieces) are the same
size as the ahbmPiece and ahbmMask bitmaps. The ahbmMove bitmaps are twice
the width and height of the other bitmaps. These bitmaps are used for moving
a piece across the window when the user picks it up.
Interactively moving a bitmap across the screen involves some fairly
standard techniques. If the piece begins its existence at point A, there are
two steps involved.
1. Save the screen area at point A in ahbmSave.
2. Draw the piece on the screen at point A using ahbmMask and ahbmPiece.
Now the user moves the mouse from point A to point B, meaning that the piece
must be erased from point A and drawn at point B. This is accomplished by
the following three steps:
1. Restore the area at point A from ahbmSave. This effectively erases the
piece.
2. Save the area at point B in ahbmSave.
3. Draw the piece at point B using ahbmMask and ahbmPiece.
This continues until the user deposits the piece on a square. The three
functions in CKRDRAW.C that perform these operations are shown in Figure 11.
Although this works all right, I was unsatisfied. Moving a piece with the
mouse resulted in an annoying flicker caused by the delay between erasing
the piece from one point and drawing at another point. I hate when something
like this happens--the only thing worse than buggy code is functional
code that performs disappointingly. I wanted to see smooth, flickerless
piece movement and I wasn't getting it. Finally, I decided to make the extra
effort of writing the CkdDragMove function (see Figure 12).
If points A and B are far enough away so there is no overlap of the old
piece at A and the new piece at B, this function performs the three steps
listed above. Otherwise, it does a more complex operation:
1. Copy the screen area encompassing points A and B to the ahbmMove
bitmap. (Remember that this bitmap is twice the width and height of the
pieces.)
2. Restore the area corresponding to point A on the ahbmMove bitmap from
the ahbmSave bitmap. This erases the image of the piece from ahbmMove.
3. Save the area corresponding to point B on the ahbmMove bitmap to the
ahbmSave bitmap.
4. Draw the piece at point B on the ahbmMove bitmap.
5. Copy the ahbmMove bitmap to the screen.
The results (after getting the function to work properly) were impressive.
The piece moves smoothly and without flicker because the updating of the
image is done offscreen. Nothing is drawn on the window until step 5, when
the new image replaces the old one. This is the part of the program I am
proudest of, the part that most users will take for granted. Few people are
going to say "Wow! No flicker!" But I'm glad I made the change. It makes a
big difference.
The Next Installment
CHECKERS has made some considerable progress, but playing a game by yourself
gets boring quickly. In the next installment, CHECKERS will include
game-playing logic so you will be able to play a game against the program. I
will also include a specification for a dynamic-link library interface, so
you will be able to write your own checkers-playing logic.
Figure 4
CHECKERS
#----------------------------------
# CHECKERS make file, Version 0.30
#----------------------------------
CC = cl -c -G2sw -W3
checkers.obj : checkers.c checkers.h
$(CC) checkers.c
ckrjudge.obj : ckrjudge.c checkers.h ckrmoves.h
$(CC) ckrjudge.c
ckrboard.obj : ckrboard.c checkers.h ckrdraw.h
$(CC) ckrboard.c
ckrmoves.obj : ckrmoves.c checkers.h ckrmoves.h
$(CC) ckrmoves.c
ckrdraw.obj : ckrdraw.c checkers.h ckrdraw.h
$(CC) ckrdraw.c
checkers.res : checkers.rc checkers.h ckruhand.ptr ckrdhand.ptr
rc -r checkers
checkers.exe : checkers.obj ckrjudge.obj ckrboard.obj \
ckrmoves.obj ckrdraw.obj checkers.def
link @checkers.lnk
rc checkers.res checkers.exe
checkers.exe : checkers.res
rc checkers.res
CHECKERS.RC
/*-------------------------------------------
CHECKERS.RC resource script, Version 0.30
-------------------------------------------*/
#include <os2.h>
#include "checkers.h"
POINTER IDP_UPHAND ckruhand.ptr
POINTER IDP_DNHAND ckrdhand.ptr
MENU ID_RESOURCE
{
SUBMENU "~Game", -1
{
MENUITEM "~New Game", IDM_NEWGAME
MENUITEM SEPARATOR
MENUITEM "~Black on Bottom", IDM_BOTTOM,, MIA_CHECKED
MENUITEM SEPARATOR
MENUITEM "~About Checkers...", IDM_ABOUT
}
SUBMENU "~Colors", -1
{
MENUITEM "Wi~ndow Background...", IDM_COLOR_BACKGROUND
MENUITEM "~Black Square...", IDM_COLOR_BLACK_SQUARE
MENUITEM "~White Square...", IDM_COLOR_WHITE_SQUARE
MENUITEM "B~lack Piece...", IDM_COLOR_BLACK_PIECE
MENUITEM "W~hite Piece...", IDM_COLOR_WHITE_PIECE
MENUITEM SEPARATOR
MENUITEM "~Standard colors", IDM_COLOR_STANDARD
}
}
#define GRP WS_GROUP
DLGTEMPLATE IDD_ABOUT_DLG
{
DIALOG "", 0, 32, 32, 200, 100,, FCF_DLGBORDER
{
CTEXT "Checkers Version 0.30" -1, 10, 76, 180, 8
CTEXT "(A Game With Yourself)" -1, 10 62, 180, 8
CTEXT "Microsoft Systems Journal, 3/90" -1, 10, 48, 180, 8
CTEXT "(c) 1990, Charles Petzold" -1, 10, 34, 180, 8
DEFPUSHBUTTON "OK" DID_OK, 80, 8, 40, 16, GRP
}
}
DLGTEMPLATE IDD_COLOR_DLG
{
DIALOG "", 0, 32, 32, 180, 180,, FCF_DLGBORDER
{
CTEXT "", IDD_HEADING, 10, 166, 160, 8
GROUPBOX "Color" -1, 16, 32, 148, 130
RADIOBUTTON "Black" IDD_COLOR + CLR_BLACK, 20, 136, 64,
12, GRP
RADIOBUTTON "Blue" IDD_COLOR + CLR_BLUE, 20, 122, 64,
12
RADIOBUTTON "Red" IDD_COLOR + CLR_RED, 20, 108, 64, 12
RADIOBUTTON "Pink" IDD_COLOR + CLR_PINK, 20, 94, 64, 12
RADIOBUTTON "Green" IDD_COLOR + CLR_GREEN, 20, 80, 64,
12
RADIOBUTTON "Cyan" IDD_COLOR + CLR_CYAN, 20, 66, 64, 12
RADIOBUTTON "Yellow" IDD_COLOR + CLR_YELLOW, 20, 52, 64,
12
RADIOBUTTON "Pale Gray" IDD_COLOR + CLR_PALEGRAY, 94, 38, 64,
12
RADIOBUTTON "Dark Gray" IDD_COLOR + CLR_DARKGRAY, 94, 136, 64,
12
RADIOBUTTON "Dark Blue" IDD_COLOR + CLR_DARKBLUE, 94, 122, 64,
12
RADIOBUTTON "Dark Red" IDD_COLOR + CLR_DARKRED, 94, 108, 64,
12
RADIOBUTTON "Dark Pink" IDD_COLOR + CLR_DARKPINK, 94, 94, 64,
12
RADIOBUTTON "Dark Green" IDD_COLOR + CLR_DARKGREEN, 94, 80, 64,
12
RADIOBUTTON "Dark Cyan" IDD_COLOR + CLR_DARKCYAN, 94, 66, 64,
12
RADIOBUTTON "Brown" IDD_COLOR + CLR_BROWN, 94, 52, 64,
12
RADIOBUTTON "White" IDD_COLOR + CLR_WHITE, 20, 38, 64,
12
DEFPUSHBUTTON "OK" DID_OK, 16, 8, 52, 16, GRP
PUSHBUTTON "Cancel" DID_CANCEL, 112, 8, 52, 16
}
}
CHECKERS.DEF
;---------------------------------------------------
; CHECKERS.DEF module definition file, Version 0.30
;---------------------------------------------------
NAME CHECKERS WINDOWAPI
DESCRIPTION 'Checkers Version 0.30 (c) 1990, Charles Petzold'
PROTMODE
HEAPSIZE 1024
STACKSIZE 8192
EXPORTS ClientWndProc
JudgeWndProc
BoardWndProc
AboutDlgProc
ColorDlgProc
CHECKERS.LNK
checkers ckrjudge ckrboard ckrmoves ckrdraw
/align:16
NUL
os2
checkers
----------------------------------------
CHECKERS.LNK link script, Version 0.30
----------------------------------------
CHECKERS.H
/*--------------------------------------
CHECKERS.H header file, Version 0.30
--------------------------------------*/
// Constants and structures
#define BLACK 0
#define WHITE 1
#define NORM 0
#define KING 1
typedef struct
{
ULONG ulBlack ;
ULONG ulWhite ;
ULONG ulKing ;
}
BOARD ;
typedef BOARD FAR *PBOARD ;
// Menu ID
#define ID_RESOURCE 1
// Pointer ID's
#define IDP_UPHAND 2
#define IDP_DNHAND 3
// Menu ID's
#define IDM_NEWGAME 1
#define IDM_BOTTOM 2
#define IDM_ABOUT 3
#define IDM_COLOR_BACKGROUND 11
#define IDM_COLOR_BLACK_SQUARE 12
#define IDM_COLOR_WHITE_SQUARE 13
#define IDM_COLOR_BLACK_PIECE 14
#define IDM_COLOR_WHITE_PIECE 15
#define IDM_COLOR_STANDARD 16
// Dialog Box ID's
#define IDD_ABOUT_DLG 1
#define IDD_COLOR_DLG 2
#define IDD_HEADING 10
#define IDD_COLOR 20
// User-Defined Messages
// Messages from ClientWndProc to JudgeWndProc
#define WM_TELL_JUDGE_BOARD_HANDLE (WM_USER + 1)
#define WM_TELL_JUDGE_NEW_GAME (WM_USER + 2)
// Messages from ClientWndProc to BoardWndProc
#define WM_TELL_BOARD_JUDGE_HANDLE (WM_USER + 10)
#define WM_TELL_BOARD_NEW_ORIENTATION (WM_USER + 11)
#define WM_TELL_BOARD_COLOR_DIALOG (WM_USER + 12)
#define WM_TELL_BOARD_STANDARD_COLORS (WM_USER + 13)
// Messages from Judge to Board
#define WM_JUDGE_SAYS_RESET_BOARD (WM_USER + 20)
#define WM_JUDGE_SAYS_MOVE_BLACK (WM_USER + 21)
#define WM_JUDGE_SAYS_MOVE_WHITE (WM_USER + 22)
#define WM_JUDGE_SAYS_REMOVE_PIECE (WM_USER + 23)
// Messages from Board to Judge
#define WM_QUERY_JUDGE_CURRENT_BOARD (WM_USER + 30)
#define WM_QUERY_JUDGE_PICKUP_PIECE (WM_USER + 31)
#define WM_QUERY_JUDGE_IF_KING (WM_USER + 32)
#define WM_QUERY_JUDGE_PUTDOWN_PIECE (WM_USER + 33)
#define WM_QUERY_JUDGE_CONTINUE_MOVE (WM_USER + 34)
#define WM_TELL_JUDGE_BOARD_MOVE_ENDED (WM_USER + 35)
CKRMOVES.H
/*-------------------------------------
CKRMOVES.H header file, Version 0.30
-------------------------------------*/
ULONG CkmQueryAllMoveablePieces (BOARD *pbrd, SHORT sColor) ;
ULONG CkmQueryAllJumpablePieces (BOARD *pbrd, SHORT sColor) ;
ULONG CkmQueryMoveDestinations (BOARD *pbrd, SHORT sColor,
ULONG ulPiece) ;
ULONG CkmQueryJumpDestinations (BOARD *pbrd, SHORT sColor,
ULONG ulPiece) ;
SHORT CkmQueryJumpedPiece (SHORT sBeg, SHORT sEnd) ;
CKRDRAW.H
/*------------------------------------
CKRDRAW.H header file, Version 0.30
------------------------------------*/
MRESULT EXPENTRY ColorDlgProc (HWND hwnd, USHORT msg, MPARAM mp1,
MPARAM mp2) ;
HPS CkdCreatePS (HWND hwnd) ;
VOID CkdResizePS (HPS hps, HWND hwnd) ;
BOOL CkdDestroyPS (HPS hps) ;
VOID CkdSetStandardColors (VOID) ;
VOID CkdCreatePieces (HPS hps) ;
VOID CkdDestroyPieces (VOID) ;
VOID CkdDrawWindowBackground (HPS hps, HWND hwnd) ;
VOID CkdDrawWholeBoard (HPS hps) ;
VOID CkdDrawAllPieces (HPS hps, BOARD *pbrd, SHORT sBottom) ;
VOID CkdErasePiece (HPS hps, SHORT x, SHORT y) ;
VOID CkdQueryHitCoords (HPS hps, POINTL ptlMouse, SHORT *px,
SHORT *py) ;
SHORT CkdConvertCoordsToIndex (SHORT x, SHORT y, SHORT sBottom) ;
VOID CkdConvertIndexToCoords (SHORT i, SHORT *px, SHORT *py,
SHORT sBottom) ;
VOID CkdDragSave (HPS hps, POINTL *pptlMouse, SHORT sKing) ;
VOID CkdDragRestore (HPS hps, POINTL *pptlMouse, SHORT sKing) ;
VOID CkdDragShow (HPS hps, POINTL *pptlMouse, SHORT sColor,
SHORT sKing) ;
VOID CkdDragMove (HPS hps, POINTL *pptlFrom, POINTL *pptlTo,
SHORT sColor, SHORT sKing) ;
VOID CkdDragDeposit (HPS hps, SHORT x, SHORT y, SHORT sColor,
SHORT sKing) ;
VOID CkdQueryNearestXYFromPoint (HPS hps, POINTL *pptlMouse,
SHORT *px, SHORT *py) ;
VOID CkdQuerySlightOffsetFromXY (HPS hps, SHORT x, SHORT y,
POINTL *pptl) ;
CHECKERS.C
/*--------------------------------------------------------------------
CHECKERS.C -- OS/2 Presentation Manager Checkers Program, Version 0.30
(c) 1990, Charles Petzold
-------------------------------------------------------------------*/
#define INCL_WIN
#include <os2.h>
#include "checkers.h"
MRESULT EXPENTRY ClientWndProc (HWND hwnd, USHORT msg, MPARAM mp1,
MPARAM mp2);
MRESULT EXPENTRY JudgeWndProc (HWND hwnd, USHORT msg, MPARAM mp1,
MPARAM mp2);
MRESULT EXPENTRY BoardWndProc (HWND hwnd, USHORT msg, MPARAM mp1,
MPARAM mp2);
MRESULT EXPENTRY AboutDlgProc (HWND hwnd, USHORT msg, MPARAM mp1,
MPARAM mp2);
HAB hab ;
int main (void)
{
static CHAR szClientClass[] = "Checkers" ;
static ULONG flFrameFlags = FCF_TITLEBAR | FCF_SYSMENU |
FCF_SIZEBORDER | FCF_MINMAX |
FCF_SHELLPOSITION | FCF_TASKLIST |
FCF_MENU ;
HMQ hmq ;
HWND hwndFrame, hwndClient ;
QMSG qmsg ;
hab = WinInitialize (0) ;
hmq = WinCreateMsgQueue (hab, 0) ;
WinRegisterClass (hab, szClientClass, ClientWndProc,
CS_SIZEREDRAW, 0) ;
hwndFrame = WinCreateStdWindow (HWND_DESKTOP, WS_VISIBLE,
&flFrameFlags, szClientClass,
NULL, 0L, NULL, ID_RESOURCE,
&hwndClient) ;
while (WinGetMsg (hab, &qmsg, NULL, 0, 0))
WinDispatchMsg (hab, &qmsg) ;
WinDestroyWindow (hwndFrame) ;
WinDestroyMsgQueue (hmq) ;
WinTerminate (hab) ;
return 0 ;
}
MRESULT EXPENTRY ClientWndProc (HWND hwnd, USHORT msg, MPARAM mp1,
MPARAM mp2)
{
static CHAR szJudgeClass[] = "Checkers.Judge",
szBoardClass[] = "Checkers.Board" ;
static HWND hwndJudge, hwndBoard, hwndMenu ;
static SHORT sBottom = BLACK ;
switch (msg)
{
case WM_CREATE:
// Register Judge class & create window
WinRegisterClass (hab, szJudgeClass, JudgeWndProc,
CS_SIZEREDRAW, 0) ;
hwndJudge = WinCreateWindow (HWND_OBJECT,
szJudgeClass, NULL,
0L, 0, 0, 0, 0,
hwnd, HWND_BOTTOM, 1,
NULL, NULL) ;
// Register Board class & create window
WinRegisterClass (hab, szBoardClass, BoardWndProc,
CS_SIZEREDRAW, 0) ;
hwndBoard = WinCreateWindow (hwnd, szBoardClass, NULL,
WS_VISIBLE, 0, 0, 0, 0,
hwnd, HWND_BOTTOM, 2,
NULL, NULL) ;
// Inform windows of each other's handles
WinSendMsg (hwndJudge, WM_TELL_JUDGE_BOARD_HANDLE,
MPFROMHWND (hwndBoard), NULL) ;
WinSendMsg (hwndBoard, WM_TELL_BOARD_JUDGE_HANDLE,
MPFROMHWND (hwndJudge), NULL) ;
// Begin a new game
WinSendMsg (hwndJudge, WM_TELL_JUDGE_NEW_GAME,
NULL, NULL) ;
// Obtain handle of menu window
hwndMenu = WinWindowFromID (
WinQueryWindow (hwnd, QW_PARENT,
FALSE), FID_MENU) ;
return 0 ;
case WM_SIZE:
// Resize Board window
WinSetWindowPos (hwndBoard, NULL, 0, 0,
SHORT1FROMMP (mp2),
SHORT2FROMMP (mp2),
SWP_MOVE | SWP_SIZE) ;
return 0 ;
case WM_CHAR:
// Send keystrokes to Board window
return WinSendMsg (hwndBoard, WM_CHAR, mp1, mp2) ;
case WM_COMMAND:
// Process menu commands
switch (COMMANDMSG (&msg)->cmd)
{
case IDM_NEWGAME:
WinSendMsg (hwndJudge,
WM_TELL_JUDGE_NEW_GAME, NULL,
NULL) ;
return 0 ;
case IDM_BOTTOM:
WinSendMsg (hwndMenu, MM_SETITEMATTR,
MPFROM2SHORT (IDM_BOTTOM, TRUE),
MPFROM2SHORT (MIA_CHECKED,
sBottom ? MIA_CHECKED : 0)) ;
sBottom ^= 1 ;
WinSendMsg (hwndBoard,
WM_TELL_BOARD_NEW_ORIENTATION,
MPFROMSHORT (sBottom), NULL) ;
return 0 ;
case IDM_ABOUT:
WinDlgBox (HWND_DESKTOP, hwnd, AboutDlgProc,
NULL, IDD_ABOUT_DLG, NULL) ;
return 0 ;
case IDM_COLOR_BACKGROUND:
case IDM_COLOR_BLACK_SQUARE:
case IDM_COLOR_WHITE_SQUARE:
WinSendMsg (hwndBoard,
WM_TELL_BOARD_COLOR_DIALOG,
MPFROMP (&COMMANDMSG (&msg)->cmd),
MPFROMSHORT (FALSE)) ;
return 0 ;
case IDM_COLOR_BLACK_PIECE:
case IDM_COLOR_WHITE_PIECE:
WinSendMsg (hwndBoard,
WM_TELL_BOARD_COLOR_DIALOG,
MPFROMP (&COMMANDMSG (&msg)->cmd),
MPFROMSHORT (TRUE)) ;
return 0 ;
return 0 ;
case IDM_COLOR_STANDARD:
WinSendMsg (hwndBoard,
WM_TELL_BOARD_STANDARD_COLORS,
NULL, NULL) ;
return 0 ;
}
break ;
}
return WinDefWindowProc (hwnd, msg, mp1, mp2) ;
}
MRESULT EXPENTRY AboutDlgProc (HWND hwnd, USHORT msg, MPARAM mp1,
MPARAM mp2)
{
switch (msg)
{
case WM_COMMAND:
switch (COMMANDMSG(&msg)->cmd)
{
case DID_OK:
case DID_CANCEL:
WinDismissDlg (hwnd, TRUE) ;
return 0 ;
}
}
return WinDefDlgProc (hwnd, msg, mp1, mp2) ;
}
Figure 5
/*---------------------------------------------------------------
CKRJUDGE.C -- JudgeWndProc for controlling game, Version 0.30
(c) 1990, Charles Petzold
---------------------------------------------------------------*/
#include <os2.h>
#include "checkers.h"
#include "ckrmoves.h"
MRESULT EXPENTRY JudgeWndProc (HWND hwnd, USHORT msg, MPARAM mp1,
MPARAM mp2)
{
static BOOL fKing, fNewKing, fMustJump ;
static BOARD brd = { 0x00000FFF, 0xFFF00000, 0x00000000 }, brdLast ;
static HWND hwndBoard ;
static SHORT sColor = BLACK, iBegin, iEnd, iJump ;
ULONG ulBit, ulMove, ulJump ;
switch (msg)
{
case WM_TELL_JUDGE_BOARD_HANDLE:
hwndBoard = HWNDFROMMP (mp1) ;
return 0 ;
case WM_TELL_JUDGE_NEW_GAME:
brd.ulBlack = 0x00000FFF ;
brd.ulWhite = 0xFFF00000 ;
brd.ulKing = 0x00000000 ;
sColor = BLACK ;
WinSendMsg (hwndBoard, WM_JUDGE_SAYS_RESET_BOARD, NULL,
NULL) ;
WinSendMsg (hwndBoard, WM_JUDGE_SAYS_MOVE_BLACK, NULL,
NULL) ;
return 0 ;
case WM_QUERY_JUDGE_CURRENT_BOARD:
* (PBOARD) PVOIDFROMMP(mp1) = brd ;
return 0 ;
case WM_QUERY_JUDGE_PICKUP_PIECE:
iBegin = SHORT1FROMMP (mp1) ;
ulBit = 1L << iBegin ;
fKing = brd.ulKing & ulBit ? TRUE : FALSE ;
fMustJump = FALSE ;
fNewKing = FALSE ;
ulMove = CkmQueryAllMoveablePieces (&brd, sColor) ;
ulJump = CkmQueryAllJumpablePieces (&brd, sColor) ;
if (ulJump != 0) // ie, some possible jumps
{
if (!(ulBit & ulJump))
return FALSE ; // invalid piece for jumping
fMustJump = TRUE ;
}
else // no possible jumps
{
if (!(ulBit & ulMove)) // invalid piece for moving
return FALSE ;
}
// save board and adjust it
brdLast = brd ;
if (sColor = = BLACK)
brd.ulBlack &= ~ulBit ;
else
brd.ulWhite &= ~ulBit ;
brd.ulKing &= ~ulBit ;
return TRUE ;
case WM_QUERY_JUDGE_IF_KING:
return fKing ;
case WM_QUERY_JUDGE_PUTDOWN_PIECE:
ulBit = 1L << iBegin ;
ulMove = CkmQueryMoveDestinations (&brdLast, sColor,
ulBit) ;
ulJump = CkmQueryJumpDestinations (&brdLast, sColor,
ulBit) ;
iEnd = SHORT1FROMMP (mp1) ;
ulBit = 1L << iEnd ;
if (fMustJump)
{
if (!(ulBit & ulJump))
/* not a valid jump destination */
return FALSE ;
iJump = CkmQueryJumpedPiece (iBegin, iEnd) ;
}
else
{
if (!(ulBit & ulMove))
/* not a valid move destination */
return FALSE ;
iJump = -1 ;
}
// adjust board
brdLast = brd ;
if (sColor = = BLACK)
{
brd.ulBlack |= ulBit ;
if (iJump != -1)
brd.ulWhite &= ~(1L << iJump) ;
if (iEnd >= 28 || fKing = = TRUE)
{
brd.ulKing |= ulBit ;
if (!fKing)
fNewKing = TRUE ;
}
}
else // (sColor = = WHITE)
{
brd.ulWhite |= ulBit ;
if (iJump != -1)
brd.ulBlack &= ~(1L << iJump) ;
if (iEnd <= 3 || fKing = = TRUE)
{
brd.ulKing |= ulBit ;
if (!fKing)
fNewKing = TRUE ;
}
}
if (fNewKing)
fKing = TRUE ;
// inform board of jumped piece
if (iJump != -1)
WinSendMsg (hwndBoard, WM_JUDGE_SAYS_REMOVE_PIECE,
MPFROMSHORT (iJump), NULL) ;
return TRUE ;
case WM_QUERY_JUDGE_CONTINUE_MOVE:
if (fNewKing)
return FALSE ;
if (!fMustJump)
return FALSE ;
iBegin = SHORT1FROMMP (mp1) ;
ulBit = 1L << iBegin ;
ulJump = CkmQueryAllJumpablePieces (&brd, sColor) ;
if (ulBit & ulJump)
{
fMustJump = TRUE ;
brdLast = brd ;
if (sColor = = BLACK)
brd.ulBlack &= ~ulBit ;
else
brd.ulWhite &= ~ulBit ;
brd.ulKing &= ~ulBit ;
return TRUE ;
}
return FALSE ;
case WM_TELL_JUDGE_BOARD_MOVE_ENDED:
if (sColor = = BLACK)
{
sColor = WHITE ;
WinSendMsg (hwndBoard, WM_JUDGE_SAYS_MOVE_WHITE,
NULL, NULL) ;
}
else
{
sColor = BLACK ;
WinSendMsg (hwndBoard, WM_JUDGE_SAYS_MOVE_BLACK,
NULL, NULL) ;
}
return 0 ;
}
return WinDefWindowProc (hwnd, msg, mp1, mp2) ;
}
Figure 6
/*---------------------------------------------------------------
CKRBOARD.C -- BoardWndProc for user interaction, Version 0.30
(c) 1990, Charles Petzold
---------------------------------------------------------------*/
#define INCL_WIN
#include <os2.h>
#include <stdlib.h>
#include "checkers.h"
#include "ckrdraw.h"
MRESULT EXPENTRY BoardWndProc (HWND hwnd, USHORT msg, MPARAM mp1,
MPARAM mp2)
{
static BOOL fMovingPiece ;
static HPS hps ;
static HPOINTER hptrUpHand, hptrDnHand, hptrArrow ;
static HWND hwndJudge ;
static POINTL ptlLast ;
static SHORT sBottom = BLACK, sColor = -1, sKing = 0 ;
BOARD brd ;
POINTL ptlMouse ;
SHORT x, y, i ;
switch (msg)
{
case WM_CREATE:
hps = CkdCreatePS (hwnd) ;
hptrUpHand = WinLoadPointer (HWND_DESKTOP, NULL,
IDP_UPHAND) ;
hptrDnHand = WinLoadPointer (HWND_DESKTOP, NULL,
IDP_DNHAND) ;
hptrArrow = WinQuerySysPointer (HWND_DESKTOP,
SPTR_ARROW,
FALSE) ;
return 0 ;
case WM_SIZE:
CkdResizePS (hps, hwnd) ;
CkdDestroyPieces () ;
CkdCreatePieces (hps) ;
return 0 ;
case WM_TELL_BOARD_JUDGE_HANDLE:
hwndJudge = HWNDFROMMP (mp1) ;
return 0 ;
case WM_TELL_BOARD_NEW_ORIENTATION:
sBottom = SHORT1FROMMP (mp1) ;
WinInvalidateRect (hwnd, NULL, FALSE) ;
return 0 ;
case WM_TELL_BOARD_COLOR_DIALOG:
if (!WinDlgBox (HWND_DESKTOP, hwnd, ColorDlgProc,
NULL, IDD_COLOR_DLG, mp1))
return 0 ;
if (SHORT1FROMMP (mp2))
{
CkdDestroyPieces () ;
CkdCreatePieces (hps) ;
}
WinInvalidateRect (hwnd, NULL, FALSE) ;
return 0 ;
case WM_TELL_BOARD_STANDARD_COLORS:
CkdSetStandardColors () ;
CkdDestroyPieces () ;
CkdCreatePieces (hps) ;
WinInvalidateRect (hwnd, NULL, FALSE) ;
return 0 ;
case WM_BUTTON1UP:
if (sColor = = -1)
return 0 ;
WinSetActiveWindow (HWND_DESKTOP, hwnd) ;
// get mouse coords and index
ptlMouse.x = MOUSEMSG(&msg)->x ;
ptlMouse.y = MOUSEMSG(&msg)->y ;
CkdQueryHitCoords (hps, ptlMouse, &x, &y) ;
i = CkdConvertCoordsToIndex (x, y, sBottom) ;
if (i = = -1) // didn't hit black square
{
WinAlarm (HWND_DESKTOP, WA_ERROR) ;
return 0 ;
}
if (!fMovingPiece) // ie, picking up piece
{
if (!WinSendMsg (hwndJudge, WM_QUERY_JUDGE_PICKUP_PIECE,
MPFROMSHORT (i), NULL))
{
WinAlarm (HWND_DESKTOP, WA_ERROR) ;
return 0 ;
}
sKing = (SHORT) WinSendMsg (hwndJudge,
WM_QUERY_JUDGE_IF_KING,
NULL, NULL) ;
// Remove the mouse pointer
WinSetPointer (HWND_DESKTOP, NULL) ;
// Erase, save area, and show piece at mouse
CkdErasePiece (hps, x, y) ;
CkdDragSave (hps, &ptlMouse, sKing) ;
CkdDragShow (hps, &ptlMouse, sColor, sKing) ;
// Prepare for WM_MOUSEMOVE
fMovingPiece = TRUE ;
ptlLast = ptlMouse ;
}
else // ie, attempt to set down piece
{
if (!WinSendMsg (hwndJudge,
WM_QUERY_JUDGE_PUTDOWN_PIECE,
MPFROMSHORT (i), NULL))
{
WinAlarm (HWND_DESKTOP, WA_ERROR) ;
return 0 ;
}
// restore area
CkdDragRestore (hps, &ptlMouse, sKing) ;
sKing = (SHORT) WinSendMsg (hwndJudge,
WM_QUERY_JUDGE_IF
_KING NULL, NULL) ;
// set down the piece on the square
CkdDragDeposit (hps, x, y, sColor, sKing) ;
// check for continued jumps
if (WinSendMsg (hwndJudge, WM_QUERY_JUDGE_CONTINUE_MOVE,
MPFROMSHORT (i), NULL))
{
CkdErasePiece (hps, x, y) ;
CkdDragSave (hps, &ptlLast, sKing) ;
}
else // the move is over
{
fMovingPiece = FALSE ;
sColor = -1 ;
WinSetPointer (HWND_DESKTOP, hptrArrow) ;
WinSendMsg (hwndJudge,
WM_TELL_JUDGE_BOARD_MOVE_ENDED,
NULL, NULL) ;
}
}
return 0 ;
case WM_MOUSEMOVE:
ptlMouse.x = MOUSEMSG(&msg)->x ;
ptlMouse.y = MOUSEMSG(&msg)->y ;
// set the mouse pointer and move the piece
if (fMovingPiece)
{
WinSetPointer (HWND_DESKTOP, NULL) ;
CkdDragMove (hps, &ptlLast, &ptlMouse, sColor,
sKing) ;
ptlLast = ptlMouse ;
}
else if (sColor = = -1)
WinSetPointer (HWND_DESKTOP, hptrArrow) ;
else
WinSetPointer (HWND_DESKTOP,
sBottom ^ sColor ? hptrDnHand : hptrUpHand) ;
return 0 ;
case WM_SETFOCUS: // set the mouse pointer
if (WinQuerySysValue(HWND_DESKTOP,SV_MOUSEPRESENT) = =0)
WinShowPointer (HWND_DESKTOP,
SHORT1FROMMP(mp2) ? TRUE : FALSE);
if (fMovingPiece)
WinSetPointer (HWND_DESKTOP, NULL) ;
else if (sColor = = -1)
WinSetPointer (HWND_DESKTOP, hptrArrow) ;
else
WinSetPointer (HWND_DESKTOP,
sBottom ^ sColor ? hptrDnHand : hptrUpHand) ;
return 0 ;
case WM_CHAR:
if (CHARMSG(&msg)->fs & KC_KEYUP)
return 0 ;
if (!(CHARMSG(&msg)->fs & KC_VIRTUALKEY))
return 0 ;
// convert pointer position to x, y coords
WinQueryPointerPos (HWND_DESKTOP, &ptlMouse) ;
WinMapWindowPoints (HWND_DESKTOP, hwnd, &ptlMouse, 1) ;
CkdQueryNearestXYFromPoint (hps, &ptlMouse, &x, &y) ;
// move the coordinates
switch (CHARMSG(&msg)->vkey)
{
case VK_HOME: x = 0 ; y = 7 ; break ;
case VK_END: x = 0 ; y = 0 ; break ;
case VK_PAGEUP: x = 7 ; y = 7 ; break ;
case VK_PAGEDOWN: x = 7 ; y = 0 ; break ;
case VK_UP: y = min (y + 1, 7) ; break ;
case VK_DOWN: y = max (y - 1, 0) ; break ;
case VK_RIGHT: x = min (x + 1, 7) ; break ;
case VK_LEFT: x = max (x - 1, 0) ; break ;
case VK_SPACE: break ;
default: return 0 ;
}
// process keystrokes like mouse messages
CkdQuerySlightOffsetFromXY (hps, x, y, &ptlMouse) ;
switch (CHARMSG(&msg)->vkey)
{
case VK_SPACE:
WinSendMsg (hwnd, WM_BUTTON1UP,
MPFROM2SHORT ((SHORT) ptlMouse.x,
(SHORT)ptlMouse.y),
NULL) ;
break ;
default:
WinMapWindowPoints (hwnd, HWND_DESKTOP,
&ptlMouse, 1) ;
WinSetPointerPos (HWND_DESKTOP,
(SHORT) ptlMouse.x,
(SHORT) ptlMouse.y) ;
break ;
}
return 0 ;
case WM_JUDGE_SAYS_REMOVE_PIECE:
i = SHORT1FROMMP (mp1) ;
CkdConvertIndexToCoords (i, &x, &y, sBottom) ;
CkdErasePiece (hps, x, y) ;
return 0 ;
case WM_JUDGE_SAYS_RESET_BOARD:
fMovingPiece = FALSE ;
sColor = -1 ;
WinSetPointer (HWND_DESKTOP, hptrArrow) ;
WinInvalidateRect (hwnd, NULL, FALSE) ;
return 0 ;
case WM_JUDGE_SAYS_MOVE_BLACK:
sColor = BLACK ;
WinSetPointer (HWND_DESKTOP,
sBottom = = BLACK ? hptrUpHand :
hptrDnHand) ;
return 0 ;
case WM_JUDGE_SAYS_MOVE_WHITE:
sColor = WHITE ;
WinSetPointer (HWND_DESKTOP,
sBottom = = BLACK ? hptrDnHand :
hptrUpHand) ;
return 0 ;
case WM_PAINT:
WinBeginPaint (hwnd, hps, NULL) ;
WinSendMsg (hwndJudge, WM_QUERY_JUDGE_CURRENT_BOARD,
MPFROMP (&brd), NULL) ;
CkdDrawWindowBackground (hps, hwnd) ;
CkdDrawWholeBoard (hps) ;
CkdDrawAllPieces (hps, &brd, sBottom) ;
if (fMovingPiece)
{
WinQueryPointerPos (HWND_DESKTOP, &ptlMouse) ;
WinMapWindowPoints (HWND_DESKTOP,hwnd,&ptlMouse, 1) ;
CkdDragSave (hps, &ptlMouse, sKing) ;
CkdDragShow (hps, &ptlMouse, sColor, sKing) ;
ptlLast = ptlMouse ;
}
WinEndPaint (hps) ;
return 0 ;
case WM_DESTROY:
CkdDestroyPieces () ;
CkdDestroyPS (hps) ;
return 0 ;
}
return WinDefWindowProc (hwnd, msg, mp1, mp2) ;
}
Figure 7
/*---------------------------------------------------------------
CkdQueryHitCoords: Obtains coords from mouse pointer position
-----------------------------------------------------------------*/
VOID CkdQueryHitCoords (HPS hps,POINTL ptlMouse, SHORT *px,SHORT *py)
{
LONG lCoords ;
SIZEL sizlAperture ;
sizlAperture.cx = 1 ;
sizlAperture.cy = 1 ;
GpiSetPickApertureSize (hps, PICKAP_REC, &sizlAperture) ;
GpiConvert (hps, CVTC_DEVICE, CVTC_PAGE, 1L, &ptlMouse) ;
GpiSetPickAperturePosition (hps, &ptlMouse) ;
GpiSetDrawControl (hps, DCTL_DISPLAY, DCTL_OFF) ;
GpiSetDrawControl (hps, DCTL_CORRELATE, DCTL_ON) ;
lCoords = CkdDrawAllBoardSquares (hps) ;
GpiSetDrawControl (hps, DCTL_DISPLAY, DCTL_ON) ;
GpiSetDrawControl (hps, DCTL_CORRELATE, DCTL_OFF) ;
*px = LOUSHORT (lCoords) ;
*py = HIUSHORT (lCoords) ;
}
Figure 8
/*----------------------------------------------------
CkdDrawAllBoardSquares: Draws all squares of board
----------------------------------------------------*/
static LONG CkdDrawAllBoardSquares (HPS hps)
{
SHORT x, y ;
for (y = 0 ; y < 8 ; y++)
for (x = 0 ; x < 8 ; x++)
if (CkdDrawBoardSquare (hps, x, y) = = GPI_HITS)
return MAKELONG (x, y) ;
return MAKELONG (-1, -1) ;
}
Figure 9
/*-----------------------------------------------------------------
CkdConvertCoordsToIndex: Obtains index (0-31) from square coordinates
------------------------------------------------------------------*/
SHORT CkdConvertCoordsToIndex (SHORT x, SHORT y, SHORT sBottom)
{
if (x < 0 || x > 7 || y < 0 || y > 7)
return -1 ;
if ((x - (y & 1)) & 1)
return -1 ;
if (sBottom = = WHITE)
{
x = 7 - x ;
y = 7 - y ;
}
return 3 ^ (4 * y + (x - (y & 1)) / 2) ;
}
Figure 10
/*--------------------------------------------------------------------
CKRMOVES.C -- Ckm routines for determining valid moves, Version 0.30
(c) 1990, Charles Petzold
------------------------------------------------------------------*/
#include <os2.h>
#include "checkers.h"
#include "ckrmoves.h"
// Some handy constants
#define B pbrd->ulBlack
#define W pbrd->ulWhite
#define K pbrd->ulKing
#define E (~B & ~W) // empty squares
#define MP valnojmp[0][i].ulGrid // valid no-jump moves (positive)
#define MN valnojmp[1][i].ulGrid // valid no-jump moves (negative)
#define IP valnojmp[0][i].incr // valid no-jump increments (pos)
#define IN valnojmp[1][i].incr // valid no-jump increments (neg)
#define JP valjumps[0][i].ulGrid // valid jumps (positive)
#define JN valjumps[1][i].ulGrid // valid jumps (negative)
#define IP1 valjumps[0][i].incr1 // valid jumps increment 1 (pos)
#define IN1 valjumps[1][i].incr1 // valid jumps increment 1 (neg)
#define IP2 valjumps[0][i].incr2 // valid jumps increment 2 (pos)
#define IN2 valjumps[1][i].incr2 // valid jumps increment 2 (neg)
static struct // Valid No-Jump Moves
{
SHORT incr ;
ULONG ulGrid ;
}
valnojmp[2][3] = { 3, 0x00E0E0E0, 4, 0x0FFFFFFF, 5, 0x07070707,
3, 0x07070700, 4, 0xFFFFFFF0, 5, 0xE0E0E0E0 } ;
static struct // Valid Jump Moves
{
SHORT incr1, incr2 ;
ULONG ulGrid ;
}
valjumps[2][4] = { 3, 4, 0x00E0E0E0, 4, 3, 0x000E0E0E,
4, 5, 0x00707070, 5, 4, 0x00070707,
3, 4, 0x07070700, 4, 3, 0x70707000,
4, 5, 0x0E0E0E00, 5, 4, 0xE0E0E000 } ;
/*--------------------------------------------------------------------
Obtain 32-bit ULONG of all pieces that can be moved without jumping
------------------------------------------------------------------*/
ULONG CkmQueryAllMoveablePieces (BOARD *pbrd, SHORT sColor)
{
SHORT i ;
ULONG S = 0 ; // stands for "source"
for (i = 0 ; i < 3 ; i++)
{
if (sColor = = BLACK)
{
S |= (((B & MP) << IP) & E) >> IP ;
S |= (((B & K & MN) >> IN) & E) << IN ;
}
else
{
S |= (((W & MN) >> IN) & E) << IN ;
S |= (((W & K & MP) << IP) & E) >> IP ;
}
}
return S ;
}
/*-------------------------------------------------------
Obtain 32-bit ULONG of all pieces that can make jumps
-------------------------------------------------------*/
ULONG CkmQueryAllJumpablePieces (BOARD *pbrd, SHORT sColor)
{
SHORT i ;
ULONG S = 0 ;
for (i = 0 ; i < 4 ; i++)
{
if (sColor = = BLACK)
{
S |= ((((B & JP) << IP1 ) & W) >> IP1 ) &
((((B & JP) << (IP1 + IP2)) & E) >> (IP1 + IP2)) ;
S |= ((((B & K & JN) >> IN1 ) & W) << IN1 ) &
((((B & K & JN) >> (IN1 + IN2)) & E) << (IN1 + IN2)) ;
}
else
{
S |= ((((W & JN) >> IN1 ) & B) << IN1 ) &
((((W & JN) >> (IN1 + IN2)) & E) << (IN1 + IN2)) ;
S |= ((((W & K & JP) << IP1 ) & B) >> IP1 ) &
((((W & K & JP) << (IP1 + IP2)) & E) >> (IP1 + IP2)) ;
}
}
return S ;
}
/*-----------------------------------------------------------------
Obtain all destinations of a particular move-without-jump piece
----------------------------------------------------------------*/
ULONG CkmQueryMoveDestinations (BOARD *pbrd, SHORT sColor, ULONG
ulPiece)
{
SHORT i ;
ULONG P = ulPiece, D = 0 ;
for (i = 0 ; i < 3 ; i++)
{
if (sColor = = BLACK)
{
D |= ((P & B & MP) << IP) & E ;
D |= ((P & B & K & MN) >> IN) & E ;
}
else
{
D |= ((P & W & MN) >> IN) & E ;
D |= ((P & W & K & MP) << IP) & E ;
}
}
return D ;
}
/*------------------------------------------------------
Obtain all destinations of a particular jumping piece
------------------------------------------------------*/
ULONG CkmQueryJumpDestinations (BOARD *pbrd, SHORT sColor, ULONG
ulPiece)
{
SHORT i ;
ULONG P = ulPiece, D = 0 ;
for (i = 0 ; i < 4 ; i++)
{
if (sColor = = BLACK)
{
D |= ((((P & B & JP) << IP1 ) & W) << IP2) &
((((P & B & JP) << (IP1 + IP2)) & E) ) ;
D |= ((((P & B & K & JN) >> IN1 ) & W) >> IN2) &
((((P & B & K & JN) >> (IN1 + IN2)) & E) ) ;
}
else
{
D |= ((((P & W & JN) >> IN1 ) & B) >> IN2) &
((((P & W & JN) >> (IN1 + IN2)) & E) ) ;
D |= ((((P & W & K & JP) << IP1 ) & B) << IP2) &
((((P & W & K & JP) << (IP1 + IP2)) & E) ) ;
}
}
return D ;
}
/*----------------------------------------------------------
Obtain index of a jumped piece based on jumper's indices
----------------------------------------------------------*/
SHORT CkmQueryJumpedPiece (SHORT sBeg, SHORT sEnd)
{
return (sBeg + sEnd) / 2 + (sBeg & 4 ? 0 : 1) ;
}
Figure 11
/*--------------------------------------------------------------------
CkdConvertIndexToCoords: Obtains square coordinates from index (0-31)
-------------------------------------------------------------------*/
VOID CkdConvertIndexToCoords (SHORT i, SHORT *px, SHORT *py, SHORT sBottom)
{
if (i <= 0 || i >= 32)
{
*px = -1 ;
*py = -1 ;
}
*py = i / 4 ;
*px = 2 * ((i ^ 3) % 4) + (*py & 1) ;
if (sBottom = = WHITE)
{
*px = 7 - *px ;
*py = 7 - *py ;
}
}
/*----------------------------------------------------
CkdDragSave: Saves screen area when dragging piece
----------------------------------------------------*/
VOID CkdDragSave (HPS hps, POINTL *pptlMouse, SHORT sKing)
{
POINTL ptlOrigin, aptl[3] ;
CkdPieceOriginFromCenterDevice (&ptlOrigin, pptlMouse) ;
aptl[0].x = 0 ;
aptl[0].y = 0 ;
aptl[1].x = sizlPiece[sKing].cx ;
aptl[1].y = sizlPiece[sKing].cy ;
aptl[2] = ptlOrigin ;
GpiSetBitmap (hpsMemory, ahbmSave[sKing]) ;
GpiBitBlt (hpsMemory, hps, 3L,aptl,ROP_SRCCOPY,BBO_IGNORE) ;
GpiSetBitmap (hpsMemory, NULL) ;
}
/*----------------------------------------------------------
CkdDragRestore: Restores screen area when dragging piece
----------------------------------------------------------*/
VOID CkdDragRestore (HPS hps, POINTL *pptlMouse, SHORT sKing)
{
POINTL ptlOrigin, aptl[3] ;
CkdPieceOriginFromCenterDevice (&ptlOrigin, pptlMouse) ;
aptl[0] = ptlOrigin ;
aptl[1].x = ptlOrigin.x + sizlPiece[sKing].cx ;
aptl[1].y = ptlOrigin.y + sizlPiece[sKing].cy ;
aptl[2].x = 0 ;
aptl[2].y = 0 ;
GpiSetBitmap (hpsMemory, ahbmSave[sKing]) ;
GpiBitBlt (hps, hpsMemory, 3L,aptl,ROP_SRCCOPY,BBO_IGNORE) ;
GpiSetBitmap (hpsMemory, NULL) ;
}
/*-------------------------------------------------------------
CkdDragShow: Shows piece in new position when being dragged
-------------------------------------------------------------*/
VOID CkdDragShow (HPS hps, POINTL *pptlMouse, SHORT sColor,
SHORT sKing)
{
POINTL ptlOrigin, aptl[3] ;
CkdPieceOriginFromCenterDevice (&ptlOrigin, pptlMouse) ;
// Write out mask with bitwise AND
aptl[0] = ptlOrigin ;
aptl[1].x = ptlOrigin.x + sizlPiece[sKing].cx ;
aptl[1].y = ptlOrigin.y + sizlPiece[sKing].cy ;
aptl[2].x = 0 ;
aptl[2].y = 0 ;
GpiSetBitmap (hpsMemory, ahbmMask[sKing]) ;
GpiBitBlt (hps, hpsMemory, 3L, aptl, ROP_SRCAND, BBO_IGNORE) ;
// Write out piece with bitwise OR
aptl[0] = ptlOrigin ;
aptl[1].x = ptlOrigin.x + sizlPiece[sKing].cx ;
aptl[1].y = ptlOrigin.y + sizlPiece[sKing].cy ;
aptl[2].x = 0 ;
aptl[2].y = 0 ;
GpiSetBitmap (hpsMemory, ahbmPiece[sColor][sKing]) ;
GpiBitBlt (hps, hpsMemory, 3L, aptl, ROP_SRCPAINT, BBO_IGNORE) ;
GpiSetBitmap (hpsMemory, NULL) ;
}
Figure 12
/*-------------------------------------------------------------
CkdDragMove: Moves piece from one part of screen to another
-------------------------------------------------------------*/
VOID CkdDragMove (HPS hps, POINTL *pptlFrom, POINTL *pptlTo,
SHORT sColor, SHORT sKing)
{
POINTL ptlCenter, ptlOrigin, aptl[3], ptlFrom, ptlTo ;
if ((labs (pptlFrom->x - pptlTo->x) > sizlPiece[sKing].cx) ||
(labs (pptlFrom->y - pptlTo->y) > sizlPiece[sKing].cy))
{
CkdDragRestore (hps, pptlFrom, sKing) ;
CkdDragSave (hps, pptlTo, sKing) ;
CkdDragShow (hps, pptlTo, sColor, sKing) ;
return ;
}
ptlCenter.x = min (pptlFrom->x, pptlTo->x) ;
ptlCenter.y = min (pptlFrom->y, pptlTo->y) ;
CkdPieceOriginFromCenterDevice (&ptlOrigin, &ptlCenter) ;
// Copy screen into ahbmMove bitmap in hpsMemory2
aptl[0].x = 0 ;
aptl[0].y = 0 ;
aptl[1].x = sizlMove[sKing].cx ;
aptl[1].y = sizlMove[sKing].cy ;
aptl[2] = ptlOrigin ;
GpiSetBitmap (hpsMemory2, ahbmMove[sKing]) ;
GpiBitBlt (hpsMemory2, hps, 3L, aptl, ROP_SRCCOPY, BBO_IGNORE) ;
// Do restore, save, & show to hpsMemory2
ptlFrom.x = pptlFrom->x - ptlOrigin.x ;
ptlFrom.y = pptlFrom->y - ptlOrigin.y ;
ptlTo.x = pptlTo->x - ptlOrigin.x ;
ptlTo.y = pptlTo->y - ptlOrigin.y ;
CkdDragRestore (hpsMemory2, &ptlFrom, sKing) ;
CkdDragSave (hpsMemory2, &ptlTo, sKing) ;
CkdDragShow (hpsMemory2, &ptlTo, sColor, sKing) ;
// Copy ahbmMove bitmap in hpsMemory2 to screen
aptl[0] = ptlOrigin ;
aptl[1].x = ptlOrigin.x + sizlMove[sKing].cx ;
aptl[1].y = ptlOrigin.y + sizlMove[sKing].cy ;
aptl[2].x = 0 ;
aptl[2].y = 0 ;
GpiBitBlt (hps, hpsMemory2, 3L, aptl, ROP_SRCCOPY, BBO_IGNORE) ;
GpiSetBitmap (hpsMemory2, NULL) ;
}
Sidebar
As I discussed in "Checkers Part I" MSJ (Vol. 4, No. 6), I am using
techniques discussed in Christopher S. Strachey's paper "Logical
Nonmathematical Programs" originally published in Proceedings of the
Association for Computing Machinery Conference, Toronto (1952, pp. 46-49)
and reprinted in Computer Games I, edited by David N.L. Levy
(Springer-Verlag, 1988).
At any time, the board is represented by three 32-bit integers, which we can
refer to as B (black), W (white), and K (king). The 32 bits correspond to
the 32 black squares. A 1 bit means that the square is occupied or (in the
case of K) that the piece on the square is kinged. Any bit set in K must
have a corresponding bit set in B or W. These three integers are stored in
the CHECKERS program in the BOARD structure.
The game begins with the integers set as follows (in hexadecimal):
B = 00000FFF
W = FFF00000
K = 00000000
The empty squares can be represented by:
E = ~B & ~W
All squares occupied by black kings can be determined using:
B & K
You can also use this representation for the squares from which pieces can
be moved. For example, there are nine squares from which a black piece can
move to a position three higher in number:
M = 00E0E0E0
(The above value was printed incorrectly in MSJ Vol. 4, No. 6--Ed.) The
current black pieces that may be able to move to squares that are 3 higher
in number are:
B & M
The destinations of these pieces are:
(B & M) << 3
The move is only valid if the destination is empty:
((B & M) << 3) & E
So, the complete set of black pieces that can move to a position 3 higher in
number is:
(((B & M) << 3) & E) >> 3
There can also be moves to positions 4 and 5 higher in number. For white
pieces (or black kings), moves can be to positions 3, 4, or 5 lower in
number.
────────────────────────────────────────────────────────────────────────────
Volume 5 - Number 3
────────────────────────────────────────────────────────────────────────────
OS/2 Version 2.0: Exploiting the 32-bit Architecture of 80386- and
80486-based Systems
Ray Duncan
The long-awaited 32-bit 80386-specific version of the OS/21 operating
system, OS/2 Version 2.0, represents two major milestones in the evolution
of personal computer systems software. It is the last stage in the
transition from the 16-bit real-mode world of the Intel(R) 8086 to the
32-bit protected-mode world of the 80386 and 80486. And it is the first
significant step in the operating system's evolution towards a truly
portable system that can support high-performance, graphical applications on
multiple dissimilar CPU architectures.
This article will survey the features of OS/2 Version 2.0 that are of most
interest to veterans of protected-mode programming under OS/2 Versions 1.0
through 1.2. It is true that OS/2 Version 2.0 has the same commitment to
backwards compatibility as its predecessors, and will run DOS2 and OS/2
Version 1.x applications unchanged. But the full benefits of OS/2 2.0, and
the potential for painless migration to other hardware platforms, can only
be enjoyed by programs that are converted and recompiled for OS/2 2.0's
32-bit Application Program Interface (API).
32-bit Application Memory Model
The most far-reaching architectural improvement in Version 2.0 is its
support of a nonsegmented, linear address space (sometimes referred to as
the flat memory model or 0:32 addressing) for 32-bit application programs.
This feature is extremely important because it is essentially identical to
the memory model used on many minicomputers and Motorola 680x0-based or
RISC-based workstations. The disappearance of segmentation means that it
will be much easier to port sophisticated applications and programming tools
from these other architectures, while the performance penalty for using
high-level languages instead of assembly language will be significantly
reduced.
To understand the so-called flat model fully, we first must review the
differences between 80286 and 80386 protected-mode address generation. (For
our purposes here, we will assume that the 80486 behaves exactly like the
combination of an 80386 and an 80387.)
On the 80286 in protected mode, a far pointer is composed of a 16-bit
selector and a 16-bit offset (see Figure 1). The upper 13 bits of the
selector are an index to an entry in a descriptor table--a system data
structure that is maintained by the operating system and interpreted by the
hardware. Each descriptor in a descriptor table is composed of many fields
that completely describe the corresponding memory segment: a 24-bit base
address, a 16-bit length, a segment type (read-only, read-write,
executable), the privilege level required to access the segment, a present
bit that indicates whether the segment's data is physically resident in RAM,
and so on.
In short, a selector is analogous to a file handle, in that it symbolizes
data but its value has no direct correspondence to that data's location.
Whenever an instruction references memory, a segment register is selected
either implicitly (by the instruction's operand) or explicitly (by a segment
register override). The CPU looks up the descriptor that corresponds to the
selector in the segment register and uses the information in the descriptor
to validate the memory access. If it finds no reason to disallow the access,
it forms a physical memory address by combining the base address from the
descriptor with a 16-bit offset from the instruction and/or one or more
registers.
On the 80386 in 32-bit protected mode, the relatively simple scheme used by
the 80286 is altered in several ways (see Figure 2). First, a far pointer is
defined as a 16-bit selector and 32-bit offset. Second, the format of
descriptor table entries is extended to support 32-bit segment base
addresses, corresponding to a physical address space of 4Gb, and segment
sizes as large as 4Gb. Third, an additional (optional) layer of address
translation called paging is added. The 32-bit result of the hardware's
interpretation of a selector and offset is referred to as a linear address,
which can then be remapped in 4Kb chunks or pages into a 32-bit physical
address.
The 80386's paging unit operates completely independently of the
segmentation mechanism; it relies on its own data structures--page
tables and a page table directory--which, like descriptor tables, are
maintained by the operating system and interpreted by the hardware. The
paging hardware even provides its own memory protection mechanisms: bit
fields in the page table entries determine whether each page is writable or
read-only, and whether the page is accessible to an application or only to
the operating system. The page table entries also contain present, accessed,
and dirty bits that aid the implementation of an efficient virtual memory
manager.
OS/2 2.0 exploits the 80386 paging hardware for nearly all of its memory
management, allowing it to make segmentation essentially invisible to 32-bit
application programs. Both the operating system and the programs that run
under it live in a single huge 4Gb segment. The operating system and its
private data structures and buffers are mapped into the segment at the high
end; the application program's code and data are mapped into the segment at
the low end (see Figure 3). Of course, protected mode does mandate that all
segment registers contain valid selectors, but only two selectors are
required: an executable/readable selector in CS and a data read/write
selector in DS, ES, FS, GS, and SS. The descriptors for both selectors are
called aliases because they map to the same linear memory addresses.
The application program's portion of the segment is further divided into
three areas. The lower end contains the program's static code, static data,
and dynamically allocated private memory such as thread stacks and heaps.
The upper end contains dynamic-link library code and data--including
all operating system entry points--as well as any dynamically allocated
shared memory. Free memory (or more properly, free address space) lies
between, and as the program allocates additional memory, the private and
shared areas grow towards one another until the linear address space, or
more typically the physical RAM plus disk swapping space, is exhausted. In
OS/2 2.0, the total application address space is limited to 512Mb, and a
minimum address space of 64Mb is enforced for both the private and shared
areas.
The single-segment scheme just described has many beneficial consequences.
For a 32-bit application, all memory offsets, jumps, and calls are near; it
never needs to load or even be aware of the segment registers and the
selectors that they contain (hence the term 0:32 addressing). Since segment
register loads are relatively expensive, and the messy selector arithmetic
that is necessary to support data structures larger than 64Kb under OS/2
Version 1.x is even more expensive, the performance of very large programs
(or of programs that manipulate very large data structures) should be
dramatically enhanced under OS/2 Version 2.0. Even more importantly, the
elimination of both far pointers and the 80286's 64Kb segment limit vastly
simplifies the implementation and use of high-level languages. There is no
longer any place for the plethora of memory models--tiny, small,
compact, medium, large, and huge--that was prevalent in OS/2 Version
1.x programming, each with its own idiosyncratic benefits, drawbacks, and
run-time library. Instead, all 32-bit compilers for OS/2 2.0 can be shipped
with a single run-time library that serves all purposes and programming
situations. The compiler vendor does not even need to provide separate
libraries for software and hardware (numeric coprocessor) floating point,
because OS/2 Version 2.0 has a floating point emulator built in.
Memory Protection and Virtual Memory Management
Although such topics are not usually the concern of the applications
programmer, it is also interesting to contrast the ways OS/2 Versions 1.x
and 2.0 carry out the fundamental memory management chores of a
protected-mode, multitasking, virtual memory operating system: protection of
operating system code and data from applications, segregation of one
application's code and data from another, sharing of memory between
applications, and memory overcommit (that is, the use by applications of a
logical address space larger than the physical address space).
OS/2 Version 1.x, being an 80286 operating system, must rely solely on the
selector/descriptor table mechanism for memory management. The 80286 allows
two descriptor tables to be active at any given time: a Global Descriptor
Table (GDT), whose base address is loaded into the Global Descriptor Table
Register (GDTR), and a Local Descriptor Table (LDT), which resides in a
segment with a GDT descriptor and whose GDT selector is loaded into the
Local Descriptor Table Register (LDTR). Fortunately, the existence of these
two descriptor tables is sufficient to allow OS/2 1.x to carry out all its
memory management chores.
OS/2 Version 1.x uses the GDT for the segments containing its own code and
data and for any segments that must be accessible to all processes (such as
the global information segment). It prevents applications from manipulating
these segments either by making the segments read-only or setting the
privilege level required for access to the segments higher than the
privilege level given to any application. OS/2 1.x segregates one
application from another by putting the descriptors for each program's code
and data in a separate LDT, then enabling the appropriate LDT as part of a
context switch. As a result, the memory that does not belong to an
application is simply not "visible" to that application. Memory sharing is
accomplished by building descriptors in each application's LDT that refer to
the same physical segment.
Virtual memory in OS/2 Version 1.x is also implemented at the segment level;
if the virtual memory manager (VMM) must roll a segment's contents out to
the swap file on disk, it simply clears the present bit in the segment's
descriptor. The next time the segment is referenced, the hardware generates
a "segment not present" exception, which is fielded by the operating system
and passed to the VMM. The VMM then allocates some physical memory to hold
the segment (possibly swapping or discarding one or more other segments to
obtain the memory), reads the segment back in from disk, updates the
descriptor with the new physical base address, sets the "present" bit in the
descriptor, and executes an interrupt return to restart the instruction that
caused the exception in the first place.
The main use of the selector/descriptor mechanism when running a 32-bit
application in OS/2 2.0 is to protect the operating system's code and data.
The system's memory manager creates kernel mode (ring 0) and user mode (ring
3) CS and DS selectors whose descriptors differ only in their segment length
("limit") fields; the user mode descriptors map the bottom 512Mb of the
linear address space, while the kernel mode descriptors map the entire 4Gb.
The application can invoke any operating system service with near calls
while running on the user mode selectors, because all API entry points lie
within the shared memory area below the 512Mb boundary. If a particular API
function requires access to code and data in the operating system's private
area above 512Mb, it will pass through a call gate (invisible to the
application) that allows a switch to the kernel mode selectors.
Maintenance of a separate "addressing" context for each 32-bit application,
on the other hand, is carried out by OS/2 2.0 solely through the paging
hardware. There is no need for a per-process LDT at all. Instead, the
operating system edits the page tables and page table directory during a
context switch to make the physical memory owned by the previously executing
process invisible and the memory owned or shared by the new process visible
and addressable. In the case of shared memory areas, the page table entries
for each application that has access to the memory are merely aliases to the
same physical addresses.
Since both the operating system and the application coexist in a single huge
segment, segment motion and swapping is no longer a practical basis for
virtual memory management in OS/2 2.0. Instead, physical memory is always
allocated and swapped in units of 4Kb pages; the VMM relies entirely on the
present, accessed, and dirty bits in the page table entries. This scheme,
called demand paged virtual memory, generally improves throughput. Small
working sets of pages tend to stay in memory for all active applications,
data never needs to be copied from one location to another to coalesce free
memory (since disjoint physical pages can be mapped at will onto contiguous
linear addresses), and swap file management is simplified by the constant
page size.
The 32-Bit API
The 32-bit API of OS/2 2.0 is not a simple translation of the 16-bit API
found in OS/2 1.2 (and also exported by OS/2 2.0 to run 16-bit
protected-mode applications). In fact, there are significant differences
between the 16-bit and 32-bit APIs at several levels. The impact of these
differences on the conversion of an existing application for 32-bit mode
depends on the language in which the program was written and the techniques
employed by the program to interact with the user.
First, let's look at the most fundamental aspects of the two APIs. Most of
the 16-bit API's characteristics follow from the original design objective
that programs written in high-level languages should be able to invoke
operating system services directly, without the need for intermediary
library functions written in assembly language. The 16-bit API functions are
invoked by far calls to individual named entry points, whose addresses are
resolved by the system loader using the technique known as dynamic linking.
Parameters are passed to the 16-bit API functions on the stack, using the
far _pascal calling convention (a practice OS/2 Version 1.x inherited from
real-mode Microsoft(R) Windows). This means that when an API function call
is written in C, the arguments are pushed left to right, and the called
routine clears the stack. A 16-bit status code or other result (often a
handle) is returned in AX; additional results are returned in variables
(typically structures) in the caller's address space. Addresses are always
passed or returned in the form of far pointers, consisting of a 16-bit
selector and 16-bit offset.
Several of the changes found in the 32-bit API at this level are perfectly
predictable. The addresses of operating system entry points are still fixed
up at load time by dynamic linking, but the API entry points are reached by
near calls rather than far. Parameters are still passed on the stack, but
the far pointers used in the 16-bit API are superseded by 32-bit offsets,
and most single-precision parameters such as handles, byte counts, and error
codes are simply extended to 32 bits. A more surprising discovery is that
the far _pascal calling convention has been abandoned. Instead, the 32-bit
API uses a new calling convention called _syscall, where parameters are
pushed right to left and the caller clears the stack. The _syscall
convention is identical to the _cdecl convention used in the C run-time
library, except that the compiler does not prefix an underscore to function
names.
In any event, calling conventions and parameter passing methods are properly
the business of the compiler and linker. The low-level differences between
the 16-bit and 32-bit APIs that I have just described are largely hidden
from the C language programmer who uses the header files found in the
Microsoft OS/2 Programmer's Toolkit (although users of other languages are
going to run into a fair number of problems in the near term). Let's move
on, therefore, to the more abstract differences between the 16-bit and
32-bit APIs that are not handled transparently by the Programmer's Toolkit
header files.
The functions in Version 1.x's API fall naturally into two groups: kernel
functions with names in the form of Dosxxx, Vioxxx, Kbdxxx, and Mouxxx , and
Presentation Manager (hereafter "PM") functions with names in the form of
Winxxx, GPIxxx, and so on. The kernel functions embrace character- or
byte-oriented file and device I/O, timers and maintenance of the time and
date, interprocess communication, memory management, and basic multitasking
services; while the PM functions are concerned with management of windows,
menus, dialog boxes, graphical drawing operations, and all the other vital
elements of a graphical user interface.
The natural division of API functions into these two groups persists in the
32-bit API, but the two groups have met with rather different fates
regarding upward compatibility. The PM functions have survived virtually
unscathed, which is something of a testament to the quality of their
original design--not only have they lived up to their charter of device
independence, they have proven to be reasonably independent of the host
CPU's architecture as well. In fact, 16-bit application programs that were
compiled in the large model and called only the PM functions and the
standard C run-time library functions need little or no editing before they
can be recompiled for 32-bit protected mode under OS/2 Version 2.0.
In the 32-bit kernel API, on the other hand, changes are everywhere: some
functions have been renamed but take the same arguments and have the same
actions, a number of new functions have appeared, and many of the old
functions have disappeared. Taken as a whole, these modifications to the
kernel API have a single overriding objective: to bring the kernel API as
close as possible to the state of device and architectural independence
already achieved in the PM API. But the immediate consequence is somewhat
startling: character-oriented kernel applications, which are considerably
less complex than graphically-oriented PM applications, require
paradoxically far more effort to port from 16-bit protected mode to 32-bit
protected mode.
Figures 4 and 5 provide an exhaustive cross-reference between the 16-bit and
the 32-bit kernel API functions. You may find it helpful to refer to these
tables as you read the remainder of this article and when converting your
own programs to 32-bit protected mode.
Changes to the 32-bit Kernel API
The renaming of kernel functions has been carried out on a grand scale in
the 32-bit API. In general, these renamings have taken place to normalize
the kernel API function names by putting them into a more symmetric,
predictable form. For example, the 16-bit API contains many functions that
obtain information from the operating system, and the names of these
functions have three distinctly different templates: DosQxxx, DosQueryxxx,
and DosGetxxx. In the 32-bit API, all of the functions that obtain
information but do not allocate some system resource (with a couple of
inexplicable exceptions) have names in the form of DosQueryxxx. Where
necessary, names have also been transformed into a verb-object format
symmetric with the PM API; DosCWait has become DosWaitChild, DosBufReset has
been changed to DosResetBuffer, and so on.
Actually, renaming is even more pervasive than it appears at first glance,
because the entry points for all of the 32-bit API functions have true names
in the form of Dos32xxx for the purposes of dynamic linking. This allows the
16-bit and 32-bit entry points for functions with otherwise identical names
to be distinguished, so that a 16-bit protected-mode program can selectively
call 32-bit entry points and a 32-bit protected-mode program can call 16-bit
entry points. Under normal circumstances, fortunately, the programmer can
ignore the Dos32xxx forms because they are all aliased to the form Dosxxx in
the header files with #define statements.
The 16-bit kernel functions that have vanished altogether in the 32-bit API
are principally those with implicit or explicit machine dependencies. This
includes the entire battery of Vioxxx, Kbdxxx, and Mouxxx functions; all the
DosMonxxx functions; and miscellaneous other Dosxxx functions such as
DosSetVec, which is the API equivalent of poking an 80x86 interrupt vector;
DosCLIAccess, DosPortAccess, DosCallBack, and DosR2StackRealloc, all of
which are intimately related to the 80x86 architecture's "ring" scheme of
privilege levels, and DosGetInfoSeg, which returns selectors for two
read-only information segments.
Of all the mutations in the 32-bit kernel API, I suspect that the
disappearance of the Vioxxx, Kbdxxx, and Mouxxx functions is going to cause
the most controversy and dismay. Many programmers of the old school
(including myself) who learned their trade in DOS or even CP/M have been
using the existence of these functions as an excuse to avoid learning how to
write event-driven graphical applications. Although the 16-bit entry points
for the Vioxxx, Kbdxxx, and Mouxxx functions still exist in OS/2 Version 2.0
and in fact can be called by 32-bit applications, it is clearly the
intention of the system's designers that all 32-bit programs will either be
true PM applications or will use DosRead and DosWrite with the standard
input and standard output handles. (At press time, a limited 32-bit Vio
interface for Version 2.0 was under discussion--Ed.)
The completely new functions in the 32-bit kernel API fall into four main
areas: memory management, thread management, interprocess communication, and
exception handling. There are also significant file system interface issues
for programmers who have not yet adapted their products for the OS/2 1.2
kernel API and installable file systems. In the following sections, we'll
discuss the new functions in the context of a more general survey of the
32-bit kernel API by functional group.
32-bit File and Device API Functions
The 32-bit file and device API (see Figure 6) is essentially parallel to the
16-bit file and device API found in OS/2 Version 1.2, allowing for some
renamings and the disappearance of the 16-bit functions DosQFileMode,
DosSetFileMode, DosReadAsync, and DosWriteAsync. Those few programmers who
have already modified their applications to handle gracefully the long
filenames and extended attributes that first appeared in OS/2 1.2 will find
virtually no work to do in this area when converting their programs to
32-bit mode. Those who did not will be faced with considerable work.
Although there is not room in this article to discuss file issues in depth,
the key issues are listed as follows. The maximum length of a pathname may
differ from one version of the system to another, so a program should obtain
this information at run time using DosQuerySysInfo and allocate its filename
and pathname buffers dynamically. Filename parsing routines and file open
dialogs of OS/2 1.0 or 1.1 vintage may not work properly in OS/2 1.2 and 2.0
because of the latter's support for long, mixed-case filenames with embedded
blanks and multiple dot (.) delimiters. To make matters even more complex, a
single OS/2 system may well have file systems of several different types
on-line simultaneously, each with a different set of restrictions on
filename lengths and formats.
With respect to extended attributes, a program must be careful not to damage
existing extended attributes on any file that it touches and to replicate
these attributes when it backs up or copies a file. Furthermore, a program
is responsible for associating certain standard extended attributes (such as
.TYPE) with all files that it creates or "owns," and must ensure that the
values of these extended attributes remain consistent with the file's actual
name and contents. The key to manipulation of extended attributes is mastery
of the EAOP, FEAList, and GEAList structures defined in the Microsoft PTK
header files. The 32-bit functions DosOpen (equivalent to the 16-bit
function DosOpen2), DosQueryFileInfo, DosQueryPathInfo, DosSetFileInfo, and
DosSetPathInfo accept a pointer to the EAOP structure, which in turn
contains pointers to FEAList and GEAList structures.
A more detailed discussion of installable file systems, long filenames, and
extended attributes can be found in the Power Programming columns in PC
Magazine (Vol. 9, Nos. 6 through 9).
32-bit Disk and Directory API Functions
The functions in this group (see Figure 7) hold few surprises. The 32-bit
disk and directory API functions correspond almost exactly to the
corresponding OS/2 Version 1.2 functions. Note that the 32-bit functions
DosCreateDir and DosFindFirst are equivalent to the 16-bit functions
DosMkDir2 and DosFindFirst2, in that they are "extended-attribute-aware" and
can accept a pointer to an EAOP structure as one of their parameters. The
modifications made to this group were all related to installable file
systems, long filenames, or extended attributes.
32-bit Memory Management API Functions
Of all the groups of API functions, the 32-bit memory management API (see
Figure 8) merits the closest study, because it holds the key to
understanding the most crucial differences between OS/2 Versions 1.x and
2.0. In OS/2 1.x, the currency of every memory transaction was the selector,
and the fundamental unit of memory management was the segment. In OS/2 2.0,
segments and selectors are almost always irrelevant to 32-bit applications;
memory is managed in terms of objects, the size of an object is always a
multiple of 4Kb (the 80386 page size), and the handle for a memory object is
the 32-bit offset of its base.
One of the most important new concepts in OS/2 2.0 memory management is the
differentiation between allocation and commitment. Allocation reserves a
range of addresses in the linear address space, but does not in itself make
those addresses valid and accessible. Commitment reserves physical memory
and/or swap file space for part or all of a memory object on a page-by-page
basis, causing the addresses within those pages to become valid.
A private memory object is allocated by a call to the 32-bit function
DosAllocMem, which can be thought of as analogous to the 16-bit function
DosAllocSeg. The pages within the object can all be committed at allocation
time, or they can be selectively committed later by a call to DosSetMem.
Access characteristics (read-only, read-write, executable, or guard) can
also be specified at allocation time or on a page-by-page basis with
DosSetMem. Later, a program can retrieve commitment and access type
information for a range of pages within an object with DosQueryMem.
A shared memory object is allocated with the 32-bit function
DosAllocSharedMem (similar to the 16-bit function DosAllocShrSeg). As with
DosAllocMem, the pages within a shared object can be committed and given
access rights at the time of allocation, or later on a page-by-page basis
with DosSet Mem. The shared object can either have a global name in the form
\SHAREMEM\objectname.extension
or it can be allocated as an anonymous object by setting the function call
parameter for the object's name to a null pointer.
A named shared memory object can be accessed by another process with the
DosGetNamedSharedMem API function (analogous to the 16-bit function
DosGetShrSeg), which establishes addressability for the object and returns
the 32-bit offset of its base. When it is allocated, an anonymous shared
object must be declared giveable or gettable so that later it can be made
addressable for another process with the functions DosGetSharedMem and
DosGiveSharedMem (equivalent in their behavior to the 16-bit functions
DosGetSeg or DosGiveSeg).
Both private and shared memory objects are deallocated as a unit by calling
DosFreeMem, which is analogous to the 16-bit function DosFreeSeg. After an
object is freed, the linear addresses within the pages previously occupied
by that object are no longer valid, the characteristics and access rights of
those pages (guard, read-only, and so on) are no longer relevant, and any
reference to those addresses causes a GP Exception.
The 16-bit function DosAllocHuge has no counterpart in the 32-bit API
because it is not needed; the memory objects created by DosAllocMem and
DosAllocSharedMem can (theoretically, at least) be any size up to the
application's entire linear address space. Likewise, the 16-bit functions
DosGetHugeShift, DosSizeSeg, DosLockSeg, DosUnlockSeg, DosR2StackRealloc,
and DosMemAvail are not found in the 32-bit API; they have also been made
irrelevant by the transition from segment-based to page-based memory
management.
The 16-bit functions DosReallocSeg and DosReallocHuge have no successors in
the 32-bit API either, but the reasons here are less obvious. Since the
handle for a memory object is a 32-bit offset, which is susceptible to
arithmetic manipulations, the system cannot move memory objects around with
impunity as it could the physical memory assigned to segments. Consequently,
when you allocate a memory object, you should make it large enough for its
worst-case requirements, then call DosSetMem to commit increasingly large
portions of the object only as you need them. Alternatively, when data
outgrows its memory object, you can allocate and commit a new larger object,
copy the data from the old object to the new, then release the original
memory object.
The behavior of the DosSubxxx functions is essentially unchanged in the
32-bit API, with the addition of a new DosSubUnset function; programmers may
actually find these functions useful now that they can employ them for the
management of local heaps and address those heaps with near pointers.
32-bit Multitasking API Functions
The 32-bit multitasking API (see Figure 9) can be considered as consisting
of three subgroups of functions: thread control, process control, and
session control. The process and session functions are nominally the same as
the 16-bit API, allowing for renamings and the usual exchanges of far
pointers for 32-bit offsets. The DosStartSession function has some minor
enhancements that allow protected-mode applications to launch real-mode
(MS-DOS(R)) programs in a virtual DOS machine (VDM) and specify certain
characteristics of the VDM such as the keyboard polling rate and amount of
emulated EMS memory.
The thread-related 32-bit functions, on the other hand, have been
significantly improved. Stack space for new threads is allocated
automatically by DosCreateThread and reclaimed automatically when the thread
terminates. The entry point specified in the DosCreateThread call is a
32-bit offset rather than a far pointer, the new thread can be passed a
doubleword argument on its stack, and the thread can be started in either an
active or suspended state. There is also a new function, DosWaitThread, that
allows one thread to wait for any other thread to die; it is analogous to
the action of DosWaitChild (formerly DosCWait) for processes.
System level, per-process, and per-thread information is no longer obtained
from global and local information segments, which no longer exist for pure
32-bit apps. The values of system constants are found by calling
DosQuerySysInfo, while per-process and per-thread information is obtained
from the 32-bit function DosGetThreadInfo. DosGetThreadInfo returns two
structures containing (among other things) the process ID, parent process's
ID, module handle, command line pointer, environment pointer, current thread
ID, current thread priority, thread stack size and base, and a pointer to
the thread's exception handler chain.
Aside from the differences that are evident in the thread-oriented API
functions, the operating system also has significant internal architectural
improvements in this area. OS/2 2.0 can support as many as 4096 threads and
4096 processes, as opposed to a maximum of 511 threads and 255 processes for
OS/2 1.2, and there is no additional per-process limit on the number of
threads as there was in OS/2 Version 1.x. The Version 2.0 scheduler and
dispatcher has been extensively overhauled so that the process of picking
and running the proper thread from the list of all eligible threads is much
more efficient.
32-bit Interprocess Communication API Functions
In the important area of interprocess communication (IPC), there's good
news, there's great news, and there's also a little bad news. The good news
is that the changes that were made to the queue and pipe functions when they
were ported to the 32-bit API are trivial; code using these functions will
need, at most, some minor mechanical editing (see Figure 10). (The shared
memory functions have already been discussed.) The great news is that the
16-bit semaphore and signal functions, which weren't very well thought out
anyway, have been completely replaced with new and much better 32-bit
functions. The bad news is that every line of code that uses semaphores and
signals will need to be rethought and rewritten.
The 16-bit semaphore API has proven somewhat troublesome because it supports
three subtly different types of semaphores--system semaphores, RAM
semaphores, and Fast-Safe RAM semaphores. Furthermore, the first two types
can be used in two incompatible ways: for signaling an event, in which case
the semaphore is either set or cleared, or for mutual exclusion, after which
the semaphore is considered to be either owned or not owned. Overlap and
ambiguity in the 16-bit API functions adds to the confusion. For example,
DosSemRequest acquires ownership of a semaphore, while DosSemSet sets a
semaphore, but DosSemClear is used either to clear a semaphore used for
signaling, or to release ownership of a semaphore used for mutual exclusion.
The RAM semaphores can also be combined into lists, which require the use of
still other special-purpose API functions, but only when the semaphores are
used for signaling! Another chronic problem in the 16-bit semaphore API has
been the status of system semaphores as a critical and often
application-limiting resource. System semaphores are valuable because they
support counting or nested request calls, they are not swappable, they can
be accessed by device drivers in either real mode or protected mode, and the
operating system assists with clean up if a process dies owning a system
semaphore. But these characteristics also ensure that relatively few system
semaphores can be available in OS/2 1.x. System semaphores must be located
below the 640Kb boundary, and thus take away memory from the DOS
compatibility environment. In addition, OS/2 itself must use a large
proportion of the available system semaphores.
The 32-bit semaphore API solves these historical problems by tossing out the
old semaphore classes and functions completely. It defines three new types
of semaphores--mutual exclusion (mutex) semaphores, event (signaling)
semaphores, and multiple-wait (muxwait) semaphores--and three new,
disjoint sets of API functions to manipulate them. There is no longer any
distinction between RAM semaphores and system semaphores, although it
remains possible to create an anonymous semaphore that another process can't
access; all semaphores are controlled by the system, are shareable, and can
be cleaned up. There is also no longer any chance of two processes or
threads coming into conflict in the way they access a semaphore; if the
wrong API call is used with a particular semaphore, an error code will be
returned but no other damage will be done.
Signals as we knew them in OS/2 1.x have essentially disappeared, which I
for one consider to be a blessing. Due to their UNIX(R) heritage, signals
always seemed grafted in for the benefit of the C run-time library.
Architecturally, signals were alien to OS/2 1.x, and the meaning of some of
the signals (such as the Ctrl-C signal SIGINTR) became increasingly unclear
in the PM environment. In Version 2.0, signal handling has been merged with
hardware fault handling (such as divide by zero) and placed under the
control of four new 32-bit functions: DosRaiseException,
DosSetExceptionHandler, DosUnsetExceptionHandler, and DosUnwindException.
The new exception mechanisms are powerful and general, and will simplify
application code and improve application performance.
Four special aspects of the new, unified exception handling are worth
mentioning. First, exception handlers can be chained and nested, and an
exception handler in the chain can decide whether to let later handlers in
the chain execute or not. Second, OS/2 2.0 allows applications to register
exception handlers for general protection (GP) faults, which is a boon to
the authors of interpreters, incremental compilers, and products such as
editors that support macro languages. Third, Version 2.0 does not allow
applications to trap "coprocessor not found" faults; it provides transparent
per-thread floating point emulation instead. This allows all application
programs to be compiled and distributed with in-line numeric coprocessor
instructions and eliminates the need for each high-level language to have
its own floating point emulation library. Fourth, OS/2 2.0 exception
handlers can be written entirely in a high-level language.
32-bit Time and Date API Functions
Allowing for renamings and the replacement of 16-bit parameters with
32-bits, the time and date functions of OS/2 2.0 are equivalent to those in
OS/2 1.x (see Figure 11). There are no substantive conversion issues in this
group of functions.
32-bit Dynamic Linking API Functions
The 32-bit dynamic-linking functions, like the 32-bit time and date
functions, are an almost exact superset of their 16-bit predecessors (see
Figure 12). But note that the 32-bit DosGetResource and DosQueryProcAddr
return 32-bit offsets rather than selectors and offsets, and that the 32-bit
DosGetResource is more nearly parallel to the 16-bit DosGetResource2 of OS/2
Version 1.2 rather than the 16-bit DosGetResource of Versions 1.0 and 1.1.
32-bit Internationalization Support API Functions
The functions in the 32-bit National Language Support API help the software
developer adapt an application to the character sets and keyboard layouts,
and the currency, date, and time formats used in Europe and Asia. The 32-bit
internationalization functions (see Figure 13) are parallel to those found
in the 16-bit API, with the exception of the 16-bit DosPFSxxx functions
which were not carried forward.
Miscellaneous 32-bit API Functions
Finally, the 32-bit functions in this group (see Figure 14) behave as
expected; the group is mainly remarkable for the 16-bit API functions that
have no 32-bit equivalents. The 16-bit functions DosCallBack, DosCLIAccess,
and DosPortAccess are obsolete because Version 2.0 uses paged memory
management instead of segmented memory management. The function
DosGetMachineMode is absent because all 32-bit applications run in protected
mode by definition. The 16-bit functions DosGetVersion, DosGetEnv, and
DosGetInfoSeg are superseded by the 32-bit functions DosGetThreadInfo and
DosQuerySysInfo.
Summary
Compared to its 16-bit predecessors, OS/2 Version 2.0 is based on several
new architectural and philosophical tenets: use of demand-paged virtual
memory instead of segment motion and swapping, a flat memory model for
32-bit applications, and movement of the kernel API toward device and
CPU-architecture independence. Pure Presentation Manager applications are
only slightly affected by these changes in direction, while applications
that make extensive use of the OS/2 kernel API to take full advantage of the
memory management, multitasking, and interprocess communication facilities
in OS/2 may require extensive source code conversion. This article was based
on a prerelease version of OS/2 2.0. The retail version of the system may
differ from the system described here--Ed.
Figure 6
Function Description
DosClose Close file or device handle
DosCopy Copy file(s) with extended attributes
DosDelete Delete a file
DosDupHandle Duplicate file or device handle
DosEditName Build new pathname using edit string
DosEnumAttribute Obtain extended attribute names for file
DosFileIO Combined seek, lock, read or write, and unlock operation
DosMove Rename file and/or move to another directory
DosOpen Open, replace, or create a file, or open a device
DosQueryFHState Return access and sharing attributes for handle
DosQueryFileInfo Return file size, file attributes, and date/time stamps
for handle
DosQueryHType Return handle type (file, device, or pipe)
DosQueryPathInfo Return extended attributes, fully qualified name, or
file system identifier for pathname
DosRead Read data from file, pipe, or device
DosResetBuffer Flush file buffers, update directory
DosSetFHState Set sharing and access characteristics for handle
DosSetFileInfo Set file attributes or time/date stamps for handle
DosSetFileLocks Lock or unlock file region
DosSetFilePtr Set file pointer position for next read or write
DosSetFileSize Extend or truncate file
DosSetMaxFH Set maximum number of handles for process
DosSetPathInfo Set extended attributes for pathname
DosWrite Write data to file, pipe, or device
Figure 7
Function Description
DosCreateDir Create new directory
DosDeleteDir Remove directory
DosFindClose Close search context
DosFindFirst Initialize search for file or directory
DosFindNext Continue search for file or directory
DosFSAttach Associate logical volume with file system
DosFSCtl File system-specific commands and information
DosPhysicalDisk Return information about disk partitions
DosQueryCurrentDir Return name of current directory for drive
DosQueryCurrentDisk Return identifier for current drive
DosQueryFSAttach Return identify of file system for specified volume
DosQueryFSInfo Return file system information or volume label
DosQueryVerify Return state of read-after-write verify flag
DosSearchPath Searches list of directories for file
DosSetCurrentDir Select current directory for drive
DosSetDefaultDisk Select current drive
DosSetFSInfo Set volume label for drive
DosSetVerify Set state of read-after-write verify flag
DosShutDown Notify file system to prepare for system power-down or reset
Figure 8
Function Description
DosAliasMem Create address alias for object
DosAllocMem Allocate private memory object
DosAllocSharedMem Allocate shared memory object
DosFreeMem Release memory object
DosGetNamedSharedMem Get base address of named shared memory object
DosGetSharedMem Make shared memory object addressable by the current
process
DosGiveSharedMem Make shared memory object addressable by another process
DosQueryMem Get characteristics of memory object
DosSetMem Set characteristics of memory object
DosSubAlloc Allocate memory from local heap
DosSubFree Free memory in local heap
DosSubSet Creates or resets size of local heap
DosSubUnset Destroy local heap
Figure 9
Function Description
DosCreateThread Create new thread of execution in current process and
allocate stack
DosEnterCritSec Suspend context switching for other threads in current
process
DosExit Terminate current thread
DosExitCritSec Restore context switching for other threads in same
process
DosGetThreadInfo Return thread and process information
DosResumeThread Reactivate thread in same process
DosSetPriority Set execution priority of thread or of other process
DosSuspendThread Suspend execution of other thread in same process
DosWaitThread Wait for termination of other thread
DosDebug Debugging interface for controlled execution of other process
DosDynamicTrace Log execution information for process
DosExecPgm Create child process
DosExit Terminate current process
DosExitList Register routines to be executed at process termination
DosKillProcess Unilaterally terminate another process
DosWaitChild Wait for child process to terminate
DosSelectSession Switch session into foreground
DosSetSession Set session characteristics
DosStartSession Create new session and start process within that session
DosStopSession Terminate session
Figure 10
Function Description
DosAddMuxWaitSem Add semaphore to multiple-wait list
DosCloseEventSem Close event (signaling) semaphore
DosCloseMutexSem Close mutual-exclusion semaphore
DosCloseMuxWaitSem Close multiple-wait semaphore
DosCreateEventSem Create event (signaling) semaphore
DosCreateMutexSem Create mutual-exclusion semaphore
DosCreateMuxWaitSem Create multiple-wait semaphore list
DosDeleteMuxWaitSem Remove semaphore from multiple-wait list
DosOpenEventSem Return handle for event (signaling) semaphore
DosOpenMutexSem Return handle for mutual-exclusion semaphore
DosOpenMuxWaitSem Return handle for multiple-wait semaphore list
DosPostEventSem Set event (signaling) semaphore
DosQueryEventSem Return state of event (signaling) semaphore
DosQueryMutexSem Return state of mutual-exclusion semaphore
DosQueryMuxWaitSem Return state of multiple-wait semaphore list
DosReleaseMutexSem Release ownership of mutual-exclusion semaphore
DosRequestMutexSem Wait for ownership of mutual-exclusion semaphore
DosResetEventSem Clear event (signaling) semaphore
DosWaitEventSem Wait for event (signaling) semaphore to be cleared
DosWaitMuxWaitSem Wait for one of a list of semaphores to be cleared
DosCallNPipe Open, write, read, and close named pipe
DosConnectNPipe Wait for client to open pipe
DosCreateNPipe Create named pipe
DosCreatePipe Create anonymous pipe
DosDisConnectNPipe Unilaterally close named pipe
DosPeekNPipe Inspect data in named pipe without removing it from pipe
DosQueryNPHState Return modes for named pipe handle
DosQueryNPipeInfo Return characteristics of named pipe
DosQueryNPipeSemState Return information for pipe associated with
semaphore
DosRawReadNPipe Read raw data from named pipe
DosRawWriteNPipe Write raw data to named pipe
DosSetNPHState Set characteristics of named pipe
DosSetNPipeSem Associate semaphore with named pipe
DosTransactNPipe Write then read named pipe
DosWaitNPipe Wait for availability of named pipe
DosCloseQueue Close queue (also destroy if queue creator)
DosCreateQueue Create named queue
DosOpenQueue Obtain handle for named queue
DosPeekQueue Inspect queue message without removing it from queue
DosPurgeQueue Discard all messages in queue
DosQueryQueue Return number of messages waiting in queue
DosReadQueue Read and remove message from queue
DosWriteQueue Write message into queue
DosRaiseException Create exception condition for current thread or
another process
DosSetExceptionHandler Register handler for specified exception
DosUnsetExceptionHandler Restore default processing for specified
exception
DosUnwindException Remove handlers from list for current exception
Figure 11
Function Description
DosAsyncTimer Start asynchronous one-shot timer
DosGetDateTime Return current date, time, and day of
the week
DosSetDateTime Set current date and time
DosSleep Suspend current thread for specified interval
DosStartTimer Start asynchronous repeating timer
DosStopTimer Stop asynchronous repeating timer
Figure 12
Function Description
DosFreeModule Release handle for dynamic-link library
DosFreeResource Release read-only program resource
DosGetResource Return offset of read-only program resource
DosLoadModule Load dynamic-link library if not already loaded
DosQueryModuleHandle Return handle for dynamic-link library
DosQueryModuleName Return pathname for dynamic-link library
DosQueryProcAddr Return entry point for function in dynamic-link library
Figure 13
Function Description
DosMapCase Translate ASCII string in place
DosQueryCollate Return collating sequence table
DosQueryCp Return current code page
DosQueryCtryInfo Return internationalization information
DosQueryDBCSEnv Return table of double byte character set codes
DosSetCp Set code page for current session
DosSetProcessCp Set code page for current process
Figure 14
Function Description
DosBeep Generate tone
DosDevConfig Return system configuration information
DosDevIOCtl Device-specific commands and information
DosErrClass Return information about error code
DosError Disable or enable system's critical error handler
DosGetMessage Retrieve message text from disk file
DosInsertMessage Insert variable text into body of message
DosPutMessage Send message to file, pipe, or device
DosQueryAppType Return application type (PM-aware, PM- compatible, etc.)
DosQuerySysInfo Return system information such as version, maximum
pathname length, and page size
DosScanEnv Search environment for variable and return its value
Adding Hypertext-based Help to Your Application Using Microsoft Help
Technology
Marc Adler
If you were to look at the thousand most popular software programs, you
would probably find a thousand help systems. Until now, there has been no
attempt to standardize the implementation of these systems. Some products
store the help information in an external ASCII file, some compress the help
text and store it in an external file, and some embed the help text within
the program code. As for user access of the help text, some programs allow
users to modify the help system, while other programs do not.
To improve on-line help systems, Microsoft will be making its help
technology available by license to the software development community. This
help technology is embedded in products such as the Microsoft(R) C Version
6.0 Programmer's WorkBench, the Microsoft QuickC(R) Compiler, and Microsoft
QuickBasic. The help system provides a help compiler together with a help
access library, which allows you to create a hypertext-based on-line help
system with very little overhead. If you are programming OS/2 systems, all
you have to do is provide the screen management; if the program is to run
under DOS1, you must write a few memory management routines.
Creating a Help File
A help file is comprised of a series of topics and text describing each
topic. The help text can contain embedded formatting codes and hyperlinks to
other topics contained in the same help file or other files. A help file can
be formatted in QuickHelp format (an ASCII file with embedded codes), Rich
Text Format (RTF), or in minimally formatted ASCII. Once a help file is
written, it is "compiled" into a special compressed format that can be read
by either Microsoft QuickHelp or by an application program that uses the
Microsoft help library (see Figure 1). The format you use depends on your
application; however, be aware that you need an RTF-aware word processor if
you choose Rich Text Format. Because the QuickHelp format is the default
format used by the help compiler, this article concentrates on the QuickHelp
format.
The first step in creating on-line help is to organize the help information
into topics. For example, if you are trying to write help for a text editor,
possible topics might be "Deleting a Line," "Inserting a Line," and "Opening
a Window." These topics are called context strings. Context strings must be
able to be displayed in a single line on the screen, even if composed of
more than one word.
A context string is defined by using the .context command, one of several
dot commands recognized by the help compiler. For example, the line
.context open
defines a context string containing the word "open." Following this line,
you insert the help text that describes the "open" command.
Multiple context strings can refer to the same topic text. For example, the
contexts open, close, read, and write might refer to a single topic that
describes file operations in C. You can associate multiple context strings
with the same help text like this:
.context open.context read.context write < help text for file operations
goes here >
The word open is an implicit cross reference. When the user clicks on "open"
anywhere in the help text, the associated topic text will be displayed,
because "open" has been defined as a context string in the help file. It is
an implicit reference because you do not have to write a special help code
to link each instance of the word "open" to another topic.
Microsoft help technology defines several standard context strings that an
application can use to adhere to the look and feel of the Microsoft products
using this help engine (see Figure 2). For example, if you are referencing a
third-party QuickHelp file from within your application and you want to
bring up an index of the available topics, you could ask the help engine for
a topic called "h.index."
Hyperlinks
Using this technique, you can embed special formatting characters within the
topic text to emphasize certain words or sentences, and you can also create
cross-references to other topics. Hyperlinks, or explicit cross-references,
provide an intuitive way of browsing through help information. A hyperlink
is a pair of strings that represent a connection between two topics. The
first string in the pair is embedded in the topic text and is displayed on
the screen with the rest of the help text (it is usually displayed in a
different color). The second string of the pair is a context string and is
not displayed on the screen as part of the topic text. If the user selects
the first string, the topic text associated with the second string is
displayed. A hyperlink is an explicit cross reference because you must
actually code these links when writing your help file.
An example of a hyperlink is shown in Figure 3. The word mouse is tied to
the context string mouse_def although mouse_def is not displayed as part of
the help text. When the user selects the word mouse within the text
associated with "Selecting Lines," the help system will search for the
mouse_def topic. If the topic is located, the topic text associated with
mouse_def will be displayed.
The help facility supports the concept of a distributed help database by
allowing a hyperlink to reference a topic in another file (even one across a
network), or to reference a single file (such as a C language include file)
as a topic. The latter feature lends itself to new kinds of applications in
source code browsing.
The HELPMAKE compiler recognizes several special formatting flags that can
be used to emphasize parts of your help text or to create hyperlinks (see
Figure 4).
A hyperlink is defined by:
hyperlink\vcontext\v
For example, in the topic text
The mouse\vmouse_def\v is an integrated part of WB.
the word mouse has a hyperlink to the context mouse_def.
By default, the hyperlink is considered to be the word that precedes the
invisible context string. To use several words as a hyperlink, an anchor
must be used to tell HELPMAKE where the start of the hyperlink is. For
example, in the following sentence the phrase "Programmer's WorkBench" is
hyperlinked to the context string wb.
\aProgrammer's WorkBench\vwb\v is an integrated tool.
If you click the mouse within the phrase, QuickHelp will show the help text
associated with context wb.
You can reference a topic in an external file by using the form
filename!context-string
For instance,
mouse\vdevice.hlp!mouse_def\v
would search the help file device.hlp for the topic mouse_def, while
mouse\v$HELPFILES:device.hlp!mouse_def\v
would search the path referred to by the HELP environment variable for the
file device.hlp, and once located, would search device.hlp for the context
string. Note that the environment variable HELPFILES is the standard
variable that all upcoming Microsoft language applications will use to
locate their help files.
As you saw above, you can use dot commands to control the way QuickHelp
displays the help information (see Figure 5). The most common dot command is
.context, which defines context strings for topics. Other commands tell
QuickHelp to start and end a paste section, to display a list of topics, to
display a pop-up window, and to display a list of strings for the References
menu.
HELPMAKE
The HELPMAKE help compiler is a utility that is distributed with new
Microsoft language products such as C Version 6.0. It will take a help file
written in RTF, QuickHelp format, or minimally formatted ASCII and transform
it into a file that can be read or manipulated by QuickHelp or an
application using the Microsoft help library. You must use HELPMAKE if you
are going to use the help library from within your application or if you
want QuickHelp to be able to read your help files (except if you use
minimally formatted ASCII files--they can be read directly, without
compression, by the help engine).
One of HELPMAKE's tasks is to compress the massive amount of text usually
taken up by ASCII help files. Compression saves a great deal of disk space
and improves the speed of the help engine. HELPMAKE can use one or more of
the following compression methods: run-length compression, keyword
compression, extended keyword compression, and Huffman compression.
Another task of HELPMAKE is to create all the hotlinks between context
strings and topic text (see Figure 6). All context strings and
cross-reference strings are given a unique 4-byte context number. For
instance, the word "Copy" might be given the context number of 17 and the
word "Paste" might be given the context number 20. Each of these context
numbers must then be mapped into the topic numbers they are associated with.
For instance, if "Copy" and "Paste" both refer to the same help text, the
context numbers 17 and 20 must map into the same topic number. Finally, each
topic number maps into a value that represents a seek position in the help
file where the compressed topic text is found. The following C statement
will produce a seek position from a context number.
filePos = fposTopics[ ContextMap [ ContextNumber ]];
Knowing each topic's seek position allows rapid random access to any topic
no matter which topic you are currently viewing.
The format of a compiled help file is shown in Figure 7. The first data
structure is a file header that contains items such as the signature, the
number of topics, and the number of contexts. Following that is an index of
the seek positions of the compressed text for each of the topics. Next is a
list of the context strings used in the help database. Each of these strings
maps to a unique context number. Then comes an array of numbers that maps
the context numbers to topic numbers. Next are the two tables used in the
decompression algorithms, the keyword table and the Huffman decoding tree.
The Huffman tree is used to map a sequence of bits into an 8-bit ASCII
character. At the end is the actual compressed topic text.
HELPMAKE can decode any help file into QuickHelp format, unless the help
text developer compiled it with the locking option. Once it's in QuickHelp
format, the help text can be edited with most word processors or text
editors and recompiled into a binary help file. By decompiling a help file,
you can integrate your own help information into an existing help database,
such as the ones provided by Microsoft C Version 6.0. This allows more
integration of third-party development tools into the Programmer's WorkBench
environment.
Help Library
The Microsoft help library provides an API for the developer who wants to
use the text generated by HELPMAKE within an application. The API provides
routines to control multiple help file management, context lookup and topic
retrieval, browsing, and help text formatting. In addition, there are some
underlying routines to perform text retrieval with text decompression. This
section shows you how to implement a simple help system in OS/22 using
several of the major functions.
The first step the application programmer must take is to open a help file.
As many as 50 help files may be open simultaneously. The function to do this
is:
ncInitContext = HelpOpen(char far *szHelpFileName);
If the specified help file was opened, the initial context number is
returned. The initial context number is important for you to keep in mind,
as it will be used as an anchor point for the various text retrieval
routines.
Once the file is open, you can search for a context string and the help
topic text associated with it. In order to do this, you must ask the help
"engine" to map the context string to a context number. The routine to
perform this mapping is :
ncContext = HelpNc(char far *szContext,
nc ncInitialContext);
The first argument is the context string to search for, and the second
argument specifies the context at which the search begins. Most of the time,
the value is the anchor point that is returned by HelpOpen.
After you have the context number, you need to allocate a buffer large
enough to hold the decompressed topic text, retrieve the compressed topic
text, and decompress it into something that is understandable by your
application. The sequence of calls to perform the topic retrieval is:
nCompressedBytes = HelpNcCb(ncContext);
pCompressBuffer = (PB) MyAllocate(nCompressedBytes);
nDecompressedBytes = HelpLook(ncContext,
(PB) pCompressBuffer);
pTextBuffer = (PB) MyAllocate(nDecompressedBytes +
sizeof(topichdr));
HelpDecomp((PB) pCompressBuffer, (PB) pTextBuffer,
ncContext);
MyFree(pCompressBuffer);
You must provide the help engine a buffer to place the compressed topic text
in and an additional buffer for the uncompressed text (plus a small header
that contains information about the uncompressed text). HelpNcCb tells you
the number of bytes that the compressed topic text takes. Using this value,
you can allocate a buffer of the right size to hold the compressed text and
call HelpLook to retrieve the compressed text. HelpLook returns the number
of bytes that the uncompressed text will occupy. This value is used to
allocate a second buffer to hold the uncompressed text plus the topic
header. Finally, HelpDecomp will decompress the topic text and transfer it
into the specified buffer.
Now that you have the topic text, you will probably want to display it on
the screen in a cohesive manner. Because the help engine is simply a text
retrieval tool, it does not have any information about the application that
contains it. Therefore, screen display and screen management routines are up
to you. However, the help engine does contain routines to return the
uncompressed help text to your application, one line at a time. You can
retrieve the color attribute information for each line also. HelpGetLine
retrieves the help text only; HelpGetCells retrieves the help text and the
physical color attributes each character should be displayed with:
nBytesTransferred = HelpGetLine(
ushort iLineNumber,
ushort cbMaxBytes,
char far *szDestination,
PB pTextBuffer);
nBytesTransferred = HelpGetCells(
ushort iLineNumber,
ushort cbMaxBytes,
char far *szDestination,
PB pTextBuffer,
unsigned char far *szAttributes);
HelpGetCells will map the logical text attributes (underlined, bold, and so
on) into screen colors.
The help engine also provides a routine to retrieve only the attribute
information associated with a line (HelpGetLineAttr) and a routine to find
out the number of lines in the decompressed topic text (HelpcLines).
HelpcLines is useful in creating a window that is the exact size of the
topic text.
The help text is now on the screen, so most of your job is finished. The
final step is to allow the user to browse through the help database. Any
simple help system interface will allow the user to move sequentially
forward and backward through the help database by topic. The Microsoft help
library assists you in implementing this by providing some useful functions.
HelpNcNext takes a context number as an argument and returns the context
number of the next help topic that physically follows the passed context in
the help database. Similarly, HelpNcPrev returns the context number of the
help topic that is before the passed context.
ncNextContext = HelpNcNext(ncCurrentContext); ncPrevContext =
HelpNcPrev(ncCurrentContext);
HelpNcRecord(ncCurrentContext);
ncPrevContext = HelpNcBack;
The help engine also maintains a back-trace list that can keep track of the
topics the user has asked for help on. The HelpNcRecord function places a
context number on this stack and the HelpNcBack returns the context number
which is on the top of the stack.
Hyperlinks Revisited
The final step in putting your help system together is allowing the user to
traverse the hyperlinks. In our sample help system, we would like the user
to invoke a hyperlink by selecting a hyperlinked string with the mouse or by
tabbing through the strings with the left and right arrow keys and pressing
the ENTER key.
The help engine provides two functions to help you navigate through
hyperlinked information. Both functions use a hotspot structure to map a
point in the help text to a cross-reference string. The hotspot structure
looks like this:
typedef struct
{
/* the line number with the xref */
ushort line;
/* the starting column of xref */
ushort col;
/* the ending column of xref */
ushort ecol;
/* pointer to the xref string */
uchar far *pXref;
} hotspot;
The HelpXRef function takes a pointer to the help text and a hotspot
structure as arguments and returns a context string. Before calling
HelpXRef, you must fill the hotspot structure with the line number and
starting column. If the row and column position corresponds to a hyperlinked
string, the cross reference for that string is returned:
hotspot hs;
■
■
■
hs.line = CurrTopic.cursor.iCurrLine;
hs.col = CurrTopic.cursor.iCurrColumn;
if ((HelpXRef(CurrTopic.pbBuffer,
(hotspot far *) &hs)) != NULL)
{
/* We have a cross-referenced string */
uchar far *szXref = hs.pXref;
/* Invoke help system on cross-ref'ed string */
MyDisplayHelpTopic(szXref);
}
else
MessageBox("No help text", "for that topic",
NULL, "Error!", MB_OK);
To assist the user in navigating through the hotlinks associated with a
screen of help text, the help engine provides the HelpHlNext function that,
given a hotspot structure, returns the position of the next or previous
hotlink.
rc = HelpHlNext(int clead, PB pbTopic,
hotspot far *pHotSpot);
The first argument controls whether the topic text is searched forward or
backward for the next or previous hotlink or whether it searches forward or
backward for a hotlink beginning with a certain character.
Finishing Up
When you are finished using a help file, close it by calling HelpClose.
HelpClose(ncContext);
The help file that is associated with the context string is closed and all
memory allocated to process that help file is released.
The functions discussed here are all you need to implement a sophisticated
help system for your application--if you are programming under OS/2. If
you are developing a DOS application, you must provide some call-back
functions. These call-back functions are involved with opening and closing
files, reading data from a file, allocating and deallocating memory, and
locking and unlocking memory. The help library imposes this burden upon the
programmer because of the limited resources available under DOS. If the help
engine were to use the standard malloc or _dos_allocmem functions to obtain
memory for itself, it would probably find itself running out of memory if it
were embedded into a large program, or possibly conflicting with the
program's own memory management scheme. For example, many large programs
manage their memory using a virtual memory management scheme. By forcing the
application to provide call-back functions to control memory allocation, the
help engine never intrudes on the application's own memory management
scheme. These call backs are :
/* allocate nBytes of memory */
handle = HelpAlloc(nBytes)
/* release the memory pointed to by handle */
HelpDealloc(handle)
/* map handle to a far address */
char far *HelpLock(handle)
/* unlocks the memory pointed to by handle */
HelpUnlock(handle)
/* open a file on the path */
handle = OpenFileOnPath(szFilename, fWrite)
/* close a file */
CloseFile(handle)
/* read nbytes from a file into buf */
ReadHelpFile(handle, fpos, buf, nbytes)
The final consideration in deciding to use the help engine is how much extra
code it costs. Depending on the memory model and environment used, the extra
code varies between 4Kb and 6Kb bytes. In addition, the application
generally requires about 4Kb of code to interface the engine to its own
environment. The help engine takes about 100 bytes of static data and 115
bytes for each open help file.
A friendly user interface to HELPMAKE called HELPCOMP.EXE is available on
any bulletin board containing MSJ files (see the inside back cover for a
list). The HELPCOMP utility displays dialog boxes that let you choose among
the various HELPMAKE options for encoding or decoding a help file. It also
allows you to save your options in a state file that resembles the state
files used by the Programmer's WorkBench. HELPCOMP is the basis for an
integrated environment that lets you construct and test help files.
Microsoft's help technology is defining a new standard for help systems. The
HELPMAKE help compiler together with the help library allows software
developers to embed sophisticated on-line help systems into their products
with little overhead.
Figure 2
Context
String Associated Topic
h.default The default help topic, typically displayed when
the user requests help at the "top level" in the application.
The topic is generally devoted to information on using help.
h.contents The help topic displayed when the user requests a
table of contents for a help database.
h.index The help topic displayed when the user requests an index
for a help database.
Figure 3
In the help file, you have defined
.context Selecting Lines
You can use a \bmouse\b\vmouse_def\v to select lines.
.context mouse_def
A mouse is an input device.
Displayed on the screen:
You can use a mouse to select lines.
After clicking on the word mouse the text changes to:
A mouse is an input device.
Figure 4
Formatting flags are used to highlight parts of the help file and to mark
links in the help text. Each formatting flag consists of a backslash
character (\) followed by a single character. To insert a backslash
character without having it interpreted as a formatting flag, use two
backslash characters (\\).
The following flags are used to change the highlighting of text:
Flag Action
\b Toggles boldface on or off
\i Toggles italics
\u Toggles underlining
\p Turns off all attributes
Two formatting flags are used to define explicit links:
Flag Action
\a Anchors text-defining hotspots
\v Toggles invisibility around link
Figure 5
Dot Command Action
.comment <string> Comments your source file. The entire line is ignored.
Comments are useful for documenting the purpose of a link.
.context <string> Defines a context string for a topic.
.end Ends a paste section.
.freeze <n> "Freezes" <n> lines at the top of the topic. Frozen lines do
not scroll. This code can be used to prevent a screen title or row
of "buttons" from scrolling out of view when the user scrolls the text in
the help topic.
.length <n> Sizes the help window to <n> vertical lines. The line with
this code should follow the last context definition for the topic.
.list Indicates that the current topic contains a list of topics.
QuickHelp will display a list box of these topics and it allows you
to choose the topic to view.
.paste <pastename> Begins a paste section. The <pastename> appears in the
QuickHelp Paste menu.
.popup Tells the help system to display the current topic as a pop-up
window instead of a scrollable list box.
.ref <string(s)> Displays the list of <string> topics in the References
menu. If you are listing more than one topic, separate each with a
comma.
.topic <text> Defines <text> as the name of the topic. The application
may use this name to title the help window when displaying the
topic. The line with this code should follow the last context definition for
the topic.
An In-depth Exploration of the PC Keyboard and its Interrupt Service
Routines
Michael J. Mefford
From the time you strike a key on your keyboard to the time an application
receives the keystroke and displays a character on the screen, a whole
sequence of events takes place. This article begins with an overview of the
interrupts that direct keyboard events. It then discusses the hardware and
programming particulars concerning the keyboard and its interrupts. (Some
understanding of hardware terminology will be presumed.) Next, it presents
some programming examples that illustrate the inner workings of the
keyboard. Finally the article presents several utilities to enhance the
functionality of your keyboard.
In most applications, as soon as you press a key a character instantly
appears on the screen. This response suggests that there is a simple direct
connection between the keyboard and the monitor. Actually, a myriad of
components are involved and a flurry of activity takes place with each
keystroke.
The first step in understanding keystroke processing is knowing how the PC's
interrupt-handling system works. An interrupt, as its name implies, causes
the CPU to stop what it is doing and do something else. The 80x86 family of
CPUs provides two fundamentally different types, software and hardware
interrupts. A hardware interrupt occurs whenever a hardware device needs
service. A program has little control over when the interruption occurs
short of totally disabling it, which is rarely done. In fact, most programs
are not even aware of the brief interruptions caused by hardware.
A software interrupt is an elective interrupt initiated from within a
program itself. A software interrupt can be thought of as a program choosing
to interrupt itself in order to execute a subroutine or a short specialty
program outside of its own code. Raising a software interrupt is similar to
calling a function, except that you don't have to write the function
yourself. One of the primary purposes of the operating system (DOS1 or
OS/22) is to provide a standard set of software interrupt service routines
so that an application doesn't have to write its own set of instructions for
tasks common to all programs.
When you press a key on your PC's keyboard, the keyboard's circuitry informs
the CPU that it has a keystroke that should be processed immediately. This
is because the keyboard can remember only about four consecutive keystrokes.
If the keyboard can't pass on these keystrokes, it will lose them and "raise
a flag." To avoid this, the CPU saves its place in its current task (for
example, recalculating a spreadsheet) and runs a special program for
processing keystrokes. This program is called INT 9H, the keyboard hardware
interrupt. Because keystroke processing is such a basic system function, INT
9H is built into ROM as part of the BIOS.
INT 9H Interrupt
The main function of the INT 9H BIOS program is to get a keystroke from the
keyboard via I/O Port A, interpret it, and store it in a keyboard buffer.
The keyboard buffer is in low memory in a reserved part of RAM called the
BIOS data area. The keystroke has to be interpreted, because all an
interrupt handler program gets from the keyboard is a number called a scan
code that represents the physical key that was pressed or released. For
example, when you press the "A" key, the number 1EH is sent. A lookup table
tells INT 9H that this is the A key. INT 9H also looks at the states of
other keys, such as the Caps Lock and right and left Shift keys, to
determine whether the keystroke should be stored as a lowercase or uppercase
A. Both the scan code and its ASCII interpretation are then stored in the
keyboard buffer. Once the keystroke is stored in the buffer, the INT 9H
program terminates. The CPU then picks up exactly where it left off when it
was interrupted. The system is restored so well that the interrupted program
has no idea that it was momentarily frozen.
If INT 9H works so transparently, how does an application know that a
keystroke is waiting for it in the buffer? INT 16H, the keyboard software
interrupt, tells an application if there are any keystrokes available in the
BIOS data area's keyboard buffer.
INT 16H Interrupt
Hardware interrupt INT 9H has only one duty: to interpret and buffer
keystrokes. Software interrupt INT 16H has more flexibility, offering three
subservices numbered 0-2. An application designates which subservice
it desires by passing the appropriate function number to INT 16H in the
CPU's AH register.
The first service (function 0) returns keystrokes from the keyboard buffer
to the application. The problem with function 0 is that the buffer will be
empty more often than not, and INT 16H will not return empty. Rather,
function 0 sits in a loop wasting valuable CPU time waiting until a
keystroke appears in the buffer.
This takes us back to INT 9H. Whenever you press a key, an INT 9H is
generated that interrupts the INT 16H function 0 buffer watch and
transparently makes a deposit in the buffer while INT 16H is suspended. When
INT 9H finishes, INT 16H suddenly finds the keystroke and returns to the
application.
Function 1 of INT 16H is more efficient. It lets an application inquire if
there are any keystrokes in the buffer, immediately returning with a simple
"yes" or "no." If the buffer is empty the application quickly regains
control, which is a more efficient process. All an application has to do is
periodically use function 1. If it returns with a "yes," the application can
immediately retrieve the character via function 0.
The last function of INT 16H, subservice 2, returns the current shift status
information for the keys Caps Lock, Insert, Num Lock, Scroll Lock, Alt,
Ctrl, and left and right Shift. We'll return to this function later.
The difference between INT 9H and INT 16H is, therefore, that INT 9H is a
hardware interrupt provided by the BIOS and used exclusively by the keyboard
for storing keystrokes, whereas INT 16H is a software interrupt provided by
the ROM BIOS and used by application programs for retrieving stored
keystrokes.
Interrupts in Greater Detail
Since the interrupt concept is the key to understanding how the keyboard
processes function, let's take a closer look at how interrupts actually
work. The Intel 80x86 family of CPUs perform the same sequence of operations
for the software interrupt (INT) instruction as for a hardware-generated
interrupt. Both give temporary control to a program known as an interrupt
handler. The INT instruction has a complementary instruction, IRET, used by
the interrupt handler to return control to the interrupted program. To see
how this is done, let's first look at the INT instruction.
When an INT instruction occurs, whether it is initiated externally by the
hardware or internally by a program, the CPU preserves the current state of
the FLAGS register by pushing it on the stack. It then clears the interrupt
flag (IF) and trap flag (TP) to 0. Clearing the IF has the same effect as a
Clear Interrrupts (CLI) instruction--it disables interrupts and ensures
that the CPU is not interrupted until the INT procedure performs its
critical task. An example of a time-critical task is retrieving a character
from a communications port and buffering it. An interrupt handler should
enable interrupts as soon as it is safe to do so, using a Set Interrupts
(STI) instruction so other pending time-critical interrupts can occur. An
STI can be omitted, however, if the handler's code is very short.
Next, the CPU preserves the current Code Segment (CS) register by pushing it
on the stack. The CS is then loaded with the segment address of the handler
in the interrupt vector table. The interrupt vector table of addresses is
kept at the bottom of memory. The table is indexed by multiplying the INT
number by 4 and then adding 2. The 2 is added to comply with the backward
storage of numbers used by Intel, namely, offset then segment.
The current Program Counter (PC) or Instruction Pointer (IP) is preserved by
pushing it on the stack. The IP is loaded with the value of the offset
address of the handler found in the interrupt vector table. The table is
indexed by multiplying the INT number by 4.
This process gives control to the procedure pointed to by the new CS:IP
pair, the interrupt program. As mentioned, one of the first things the
interrupt handler usually does is turn interrupts back on with an STI. Any
registers that may be modified by the handler are then preserved by pushing
them on the stack. The interrupt goes about its business; when it is done it
reinstates the registers by popping the values back off the stack. If the
handler is a hardware interrupt, it informs the Programmable Interrupt
Controller (PIC) that the interrupt is about to end by sending it an End of
Interrupt (EOI).
The last thing the handler does is issue an IRET instruction. An IRET gives
control back to the interrupted process by doing the opposite of an INT
instruction. Specifically, the top two bytes (one word) are popped into the
program counter. This is the offset of the next instruction to be executed.
The next word is popped into the code segment register, which is the segment
of the next instruction to be executed. The last word is popped into the
flag's register.
This process gives control back to the program at the same place it was
interrupted. Since the state of the machine (especially the registers) is
restored to what it was, the interrupted program isn't aware that it was
suspended and continues without missing a beat.
The interrupt vector lookup table technique provides a very clean way to
make modifications to the interrupt programs without changing the way the
CPU functions. All that has to be done is to change the interrupt's entry
point address in the table. The most common change in interrupt service
programs happens when you upgrade an operating system. DOS Version 3.x, for
example, has different interrupt vectors for system services than DOS 2.x.
Similarly, the BIOS interrupt services of an IBM(R) PC/AT(R) and clone PC
will be different, but the CPU finds the correct address in both cases.
INT 9H in Depth
Recall that all keyboard events can be divided into two interrupts--the
hardware INT 9H responsible for buffering keystrokes from the keyboard and
the software INT 16H that retrieves those keystrokes from the buffer and
delivers them to your application. Let's consider the hardware INT 9H in
depth (see Figure 1).
Almost all PC keyboards contain the 8048 chip. The 8048 is a self-contained
computer consisting of an 8-bit microprocessor, about 64 bytes of RAM, a ROM
with the keyboard scan code assignments, and a BIOS. When you press a key (a
key make) or release a key (a key break), the 8048 looks in its ROM for the
scan code assigned to that key. The scan code is not the same as the ASCII
value of the key; the "A" key, for example, has a scan code of 1EH. INT 9H
is responsible for decoding the scan code 1EH into an ASCII code of 41H for
an uppercase A and a 61H for a lowercase a. A key break is distinguished
from a key make of the same key by setting the scan code's high bit.
After the correct scan code is assembled, the 8048 prepares to send the
1-byte code serially to the LS322 support chip within the PC's system unit.
(The chips inside your computer might not have the same numbers mentioned
here--while different classes of machines and clone manufacturers may
use different chips, the net effect is the same.) The scan code is sent as
long as the 8048 is given a green light by the S74 chip in the system unit,
acknowledging that the last code that was sent has been read and deciphered
by INT 9H.
If the code can't be sent right away, it is buffered into a RAM buffer in
the keyboard's 8048, which I'll call the scan code buffer. (This buffer
should not be confused with the 16-character circular buffer in the system
unit's BIOS area, where INT 9H stores characters.) If the 8048's scan code
buffer is full, an overrun occurs and the scan code data is lost. In that
case, the 8048 informs the system unit of the problem by sending a special
scan code of FFH, and the INT 9H handler notifies the user with a beep. An
overrun, however, is not very likely. Even if you're an explosive typist,
the chances are next to nil that you can cause an overrun short of pressing
several keys at once with the palm of your hand. The real purpose of the
overrun is to detect the event of something like a book dropping on the
keyboard. The beep the BIOS issues, by the way, is the same tone emitted by
a stuffed BIOS data keyboard buffer. In contrast, a full keyboard buffer can
easily happen if an application is slow in processing keystrokes.
Returning to our example: after the keyboard serially transmits the scan
code for A, 1EH, it is received by the LS322 chip, an 8-bit shift register
in the system unit (see step 1 and the following steps in Figure 1). The
scan code is sent serially. The data is prefaced with a "start" bit, for a
total of 9 bits sent. The receiving shift register has an initial value of
0. (It was cleared at the end of the last key processing.) As each bit is
received, including the start bit, room is made for it by shifting the
previous 8 bits of the shift register left by one. When the final data bit
is received, the shifting and storage will have reassembled the scan code
back to its original 1 byte unit. The shift register received a total of 9
bits, but it can hold only 8. Therefore, the shifting of the last data bit
will have pushed that first start bit off the end.
The start bit is not lost, however. It is used to set a flip-flop in the S74
support chip high 2, which triggers the Interrupt Request (IRQ1) line of INT
9H to go high . This flip-flop simultaneously sends a low signal to the 8048
in the keyboard, a "red light" signal back on the same data line that sent
the scan code. This suspends any further data transfer by the keyboard and
puts the 8048 into the buffer mode mentioned earlier.
An IRQ is a request from an external device, such as a keyboard, for the
CPU's attention. The 8259A PIC is the mediator of IRQs and decides, on a
priority basis, what will get serviced first in case of multiple requests
for service. The PIC has eight IRQ lines. (The PIC, however, is not limited
to eight IRQs. The IRQs can be chained together, with one PIC as the master
and the chained PICs as slaves. The AT and PS/2 take advantage of a slave
PIC to add additional IRQ lines.)
When the PIC receives the IRQ it sets a bit in the Interrupt Request
Register (IRR) of the PIC representing that IRQ. The IRR is an 8-bit
register, one bit for each IRQ, used to keep track of which IRQs need
servicing. The priority resolver of the PIC uses this register to pick the
IRQ of highest priority to be serviced.
As an example, assume the keyboard IRQ1 is the only one needing service. The
PIC sets its INT line high 4, which passes on the keyboard's service request
by setting the CPU's Interrupt Request (INTR) line high. The CPU will grant
the request as long as interrupts have previously been enabled with a STI
instruction. Interrupts enabled is the normal state of the processor; they
are disabled with a CLI only when a program needs to do a critical task like
changing the Stack Pointer or reading a communications port. An interrupt
itself, which is about to happen here, will also disable interrupts.
Assume the CPU will accept an interrupt. The CPU finishes its current
instruction and then acknowledges the PIC's interrupt request indirectly by
sending a three bit signal to the 8288 Bus Controller (BC, as shown in 5).
The BC makes sure data gets to its destination. The 3-bit CPU code can
control seven types of requests to the BC, including the signal that the CPU
intends to receive or send data to memory or a port. That is, it
distinguishes between IN or OUT instructions intended for a port and a MOV
instruction targeted to or from memory.
In the case of an interrupt request, the 3-bit signal sent by the CPU is the
code for an acknowledge signal ("green light") to be routed by the 8288 BC
back to the 8259A through the 1-bit Interrupt Acknowledge (INTA) line 6. The
first INTA sent to the 8259A is used for timing purposes, similar to the
start bit of serial communications. It takes another 3-bit acknowledge from
the CPU to the BC, which sends another INTA to 8259A to begin the next set
of events. The 8259A next sends the interrupt vector number, INT 9H in the
case of IRQ1, to the CPU -. It also sets the IRQ's In-Service Register
(ISR), another 8-bit register, so the PIC can keep track of which interrupt
is being serviced.
How did the 8259A know that an IRQ1 was to map to a 9? The 8259A knows to
send the CPU a 9 for an IRQ of 1 because it is programmed with several
parameters as part of the boot process. One of those parameters is the top 5
bits of a byte representing the interrupt number to be passed to the CPU.
The bottom 3 bits are programmed to the number of the IRQ when the IRQ
occurs. With these 3 bottom bits you can represent neatly all of the 8
IRQs--IRQ0 through IRQ7. The boot process programs the PIC's top 5 bits
to 00001B. Thus, an IRQ of 1 tacks on a bottom 3 bits of 001B, making the
total byte 00001001B or 9H. The 8 IRQs map into INT 8H through INT FH (15
decimal).
Now the CPU uses the interrupt number as an index to the vector table
containing the address of the interrupt program, as I previously discussed.
Since the vector table is at the bottom of memory, the INT 0's vector can be
found at the first memory address (0000:0000). Each address or vector takes
4 bytes: 2 for the segment and 2 for the offset. To find the vector
associated with the interrupt, the CPU multiplies the interrupt number by 4
and uses the result as the index into the vector table. This address is
loaded into the CS:IP of the CPU, effectively giving control to INT 9H.
Finally, the interrupt 9H is in progress.
The first thing the INT 9H BIOS program has to do is retrieve the scan code.
Remember, the scan code is in the shift register, LS322. INT 9H doesn't have
direct access to this chip and the scan code. But the LS322 has sent the
scan code, on 8 parallel lines (instead of serially), to Port B (address at
60H) of another chip called the 8255-A Programmable Peripheral Interface
(PPI).
Besides handling keyboard functions, the 3-port PPI reads the PC's system
board switches and controls its speaker. For INT 9H, it serves as the source
of the scan code 8. The INT 9H program reads the scan code from Port A,
interprets it into an ASCII character, and stores the character with the
scan code in the keyboard buffer. Note that INT 9H is not the only program
that can read Port A. Since reading this port, unlike many ports, does not
destroy the scan code, any program can safely read it.
Now that INT 9H has read the scan code, it is safe to open the gate. This is
done by momentarily setting bit 7 of Port B of the PPI high (see 9 and
Figure 2 for the assembly language instructions.) Bit 7 is connected to the
"clear" line of both the LS322 and S74. The LS322 clear line zeros out the
shift register so it will be ready to receive the next scan code. The S74
clear line resets the IRQ1 flip-flop, which in turn informs the keyboard's
8048 that it may now send a new scan code.
Once the scan code and its ASCII equivalent have been stored in the keyboard
buffer and the keyboard is reenabled, the last chore of INT 9H is to tell
the PIC that it has finished its task and is about to return control to the
program it interrupted 10. This is done by sending an EOI encoded as 20H to
port 20H of the PIC. The 20H for both the code and the port is entirely
coincidental. For more on this, see the sidebar "How an Interrupt Handler
Resets the Programmable Interrupt Controller." The EOI signal resets the ISR
and IRR bit associated with IRQ1, reenabling that request line. The PIC can
now attend to waiting hardware interrupts of lower priority, including
another IRQ1.
Now that INT 9H has finished its job, it passes control back to the
interrupted program by issuing an IRET instruction. For more information on
the 8259A, see the Peripheral Handbook (Intel, 1990), available from Intel
Literature at (800) 548-4725.
Keyboard Scan Codes
As I've mentioned, INT 9H interprets the scan code that arrives at Port A of
the 8255-A PPI. In fact, INT 9H is called into action every time there is
any keyboard activity. This includes key presses, key releases, and
typematic responses issued when you hold down a key. The keyboard simply
sends a scan code for the pressed key to the PC and INT 9H. INT 9H
determines how it should be interpreted. The BIOS INT 9H deciphers the scan
codes sent by the keyboard into meaningful ASCII characters that can be used
by an application program. One thing to keep in mind is that INT 9H is
hardware-specific, so the interrupt program for the 83-key keyboard is
different than the 101-key extended keyboard version. The latter obviously
needs additional code to cope with the additional keys.
To understand scan codes, refer to the PORT-A.ASM program (see Figure 3),
which displays in real time the scan codes sent by the keyboard as you press
and release keys. (The keyboard delivers scan codes to Port A, address 60H,
of the PPI.) PORT-A intercepts the scan codes before INT 9H gets to them by
"hooking" INT 9H. Hooking an interrupt means replacing an interrupt
handler's address in the interrupt vector table with your address. The
original address is saved so that once PORT-A, or any hooked interrupt
program, is done, it can pass control to the former interrupt handler.
Passing control in the case of an INT 9H handler gives the BIOS INT 9H
keyboard program a chance at the scan code. (Although hooking an interrupt
is how TSRs get control of the PC when you press a hot key, PORT-A.ASM is
not a TSR.) When you press Esc, PORT-A will put the vector table back to the
way it was, replacing the saved address of the previous owner and returning
you to the DOS prompt. I'll leave the rest of the programming particulars of
PORT-A to the comments found in the assembly listing. You can assemble the
source code listed, but the easiest way to obtain PORT-A.ASM as well as the
other programs mentioned in this article is from one of the bulletin boards
listed on the inside back cover.
When you run PORT-A, whenever you press a key, the scan code for that key
make will appear on the screen. When you release the same key break, another
scan code will appear. For the keys in the 83-key keyboard, the key break
will be the key make plus 80H (1000 0000B), which sets the high bit of the
scan code. This is also true for most of the same keys of the 101-key
keyboard, but for the new keys--dedicated cursor keys and some key
combinations--a series of scan codes is sent, some of which have the
high bit set for a key make. Before the 101-key keyboard, programmers could
easily distinguish a key make from a key break by checking the high bit of
the scan code. That approach does not work with the 101-key keyboard.
Programmers may find PORT-A a valuable tool to determine what scan codes an
INT 9H interceptor program can expect when the user presses specific keys.
Running PORT-A is self-explanatory; the meaning of its scan code output may
be obscure at first but will become clear once INT 9H's operation is
understood. I will therefore examine INT 9H's decoding logic step by step.
The scan code INT 9H receives from reading Port A is, for the most part, a
number representing the relative position of the key on the keyboard. Thus,
the scan code 01H is for the Esc key, 02H for the !/1 key, 03H for the @/2
key, and so on, left to right on the keyboard. (Unlike most computer
numbering schemes, there is no key associated with a 0 scan code. INT 9H
uses 0 as a pseudo-scan code to mean Ctrl-Break.) When you get to the
Backspace key, the sequential numbering resumes with the Tab key of the
QWERTY row. After the typewriter-style keys, the numbering system continues
with the function keys, followed by the keypad. Some keys, such as the Esc
and function keys, are in different places on different keyboards, so for
them the numbering system does not make sense. To remain compatible, the
scan codes remain the same but remember, the scan code has nothing to do
with the ASCII value of the key.
INT 9H has a lookup table to decode scan codes. A series of decision trees
are followed as INT 9H looks for a match for the scan code in the table.
Once the scan code is deciphered, INT 9H stores the ASCII character
interpretation along with the scan code in the keyboard buffer and returns
control to the interrupted program. Before the translation process begins,
however, INT 9H checks to see if the scan code is FFH. As I mentioned, FFH
is the keyboard's code signaling that an overrun has occurred: INT 9H warns
you of this with a beep.
Assuming no overrun, INT 9H checks the translation table in a logical order
for a matching scan code. The first keys checked are the function-altering
shift keys: right and left Shift, Ctrl, and Alt; and the four toggle keys:
Insert, Caps Lock, Num Lock, and Scroll Lock. If the scan code is for any of
the four shift state keys, it is noted by setting a bit in the KD_FLAG info
byte found at 40:17H of the BIOS data area (see Figure 4). When a key break
scan code of a shift key is detected, the appropriate bit is reset back to
0. This method enables INT 9H to know if any of the shift keys are
depressed.
Shift Keys
The best way to illustrate the logic of the Shift keys is to watch what
happens before and after INT 9H decodes the scan code. First let's observe
the after, or processed, scan code by watching what happens when you press a
few keys at the DOS prompt.
At the DOS prompt, press and hold down the left Shift key. Nothing seems to
happen. While you are still holding the left Shift, press and hold the "A"
key. A capital A will appear as expected (assuming Caps Lock is not on) and
will quickly be followed by a stream of capital A's from the typematic
response of the keyboard. In the middle of the capital A output, release the
left Shift but continue to hold down the A key. The capital A will change to
a lowercase "a" and the typematic response will continue.
With the aid of PORT-A, you can see what is actually happening with the scan
codes. Press Esc to cancel the meaningless AAAAAaaaaaa command and run
PORT-A.COM. While PORT-A is running, repeat what you did at the DOS prompt,
starting by holding down the left Shift. A scan code of 2AH will appear and
quickly start to repeat. (The keyboard starts a typematic response after
about a half-second delay. This is true for all keys, including the Shift
keys.) Still keeping the left Shift depressed, press the A key. The stream
of 2AH shift codes will be replaced by 1EH--the scan code for the A
key. (A typematic response stops when the keyboard detects that another key
has been depressed; if the second key is held, it becomes the new typematic
key.)
Next, release the left Shift key but not the A key. One AAH scan code will
appear in the stream of the A key's 1EH scan codes, then the typematic 1EH's
will continue. Note that, unlike what happened at the DOS prompt when the
uppercase A changed to an lowercase a, the same scan code (1EH) for the A
key is displayed after the left Shift is released. Release the A key; the
output will end with a 9EH, the key break (1EH + 80H = 9EH).
As you saw, INT 9H interprets the scan codes into an uppercase, lowercase
response. What actually happens is that when you hold down the left Shift,
INT 9H sees the 2A key make, turns on (using the bitwise OR operator) the
left shift bit (bit 1) of the KD_FLAG, then exits. The same logic takes
place with each subsequent typematic left Shift response. The continuous
turning on of the same bit causes no additional change in the key depressed
state. When you change the input by pressing the A key, INT 9H finds the 1EH
scan code that matches a lowercase a. At the same time, INT 9H checks the
state of the two Shift keys by looking at the KD_FLAG; it finds that the
left Shift key is still depressed, so the "a" is capitalized.
Before storing the uppercase A in the keyboard buffer, INT 9H checks the
Caps Lock bit (7) of KD_FLAG. If Caps Lock is off, INT 9H stores the
uppercase A as is and exits. If it's locked, it changes the A back to
lowercase, effectively canceling the Caps Lock state.
Thus, when you release the left Shift key and INT 9H sees the scan code AAH,
it knows that the left Shift has just been released (AAH = 2AH + 80H, a left
Shift key break scan code). INT 9H turns off the KD_FLAG left shift bit.
With the next typematic 1EH scan code "a," INT 9H finds that the left Shift
is no longer depressed (neither is the right Shift nor is Caps Lock locked),
so the lowercase "a" matched with the 1EH is stored as is.
This same function-altering logic works with the numeric keypad. If Num Lock
is on, pressing either the left or right Shift key before pressing a number
key on the keypad will undo the status, thereby changing the keys back to
cursor keys. This flexibility is great for 83-key keyboard users who
normally leave the keypad in Num Lock. (When it's necessary to use the
keypad for cursor control, instead of toggling the Num Lock key, you can
hold down the left Shift key and massage the cursor keys as usual. Just the
opposite can be done if you use the keypad mainly for cursor control and
wish to enter numbers temporarily.)
Of course with the 101-key keyboard, there are both dedicated cursor control
keys and a keypad. The machines with these keyboards generally boot with Num
Lock on, so the keypad is ready for numeric entry. For some of us, however,
habits die hard and you still reach for the keypad for cursor control. Since
the extended keyboard boots in Num Lock, invariably the first cursor control
attempt results instead in a string of numbers across the screen. For the
83-key keyboard and Num Lock advocate, the same frustration occurs in
reverse. You can, however, toggle these keys from a batch file, such as
AUTOEXEC.BAT (see Figure 5).
Toggle Keys
The logic for the four toggle keys (Insert, Caps Lock, Num Lock, and Scroll
Lock) works somewhat differently than the Shift keys. For the Caps Lock,
when INT 9H sees the scan code for one of the toggle keys it prepares to set
(using the bitwise OR operator) the appropriate bit in KD_FLAG1. (Since all
the KD_FLAG bits are already assigned, a second byte is used to hold the
state of these keys; see Figure 6.) Before it is set, however, INT 9H checks
to see if the bit is already set. If it is, INT 9H knows that this key make
is a typematic response and exits without taking action. You can see from
PORT-A that if you press and hold Caps Lock, it reacts like all the other
keys, sending multiple scan codes from the typematic response. Without the
additional logic for toggle keys, the typematic would cause uncontrolled and
undesirable toggling.
If the bit is not already set, it is set now, indicating the key is
depressed. Also, the bit in KD_FLAG is flipped to indicate a change in
status. This is done by XORing the bit (bit 6 in the case of Caps Lock) in
the KD_FLAG, having the effect of toggling the bit. To see the KD_FLAGs on
your machine, read the sidebar "Examining the Keyboard Shift Status and
Shift State Bytes."
Alt Input
INT 9H gives special consideration to the Alt and keypad combinations. If
Alt is pressed (INT 9H can tell from bit 3 of KD_FLAG), and if the scan code
for one of the number keys of the keypad is detected, INT 9H adds the number
to a byte at 40:19 of the BIOS data area--ALT_INPUT. The previous
ALT_INPUT is multiplied by 10 (to move the decimal point to the right one
position) before the new number is added. When the Alt key is released, this
number will be interpreted as a decimal ASCII equivalent and placed in the
keyboard buffer. As an example, from the DOS prompt, press and hold down the
Alt key. Next, press and release the 6; then press and release the 5 on the
keypad. Finally, release the Alt key; an "A," the ASCII of 65, will appear.
When Alt is released, INT 9H resets ALT_INPUT back to 0. If ALT_INPUT is 0
when you release Alt, no input is assumed, which explains why you can't
enter a null (ASCII 0) with this method.
This alternate input method only works with the keypad, not with the number
keys on the top row. The status of the keypad's Num Lock does not matter.
Using the Alt input method is handy for entering the ASCII control
characters below the space and the high bit characters, especially box and
line drawing characters.
Hold State
If a match is not found for any of the Shift or toggle keys, INT 9H next
checks bit 3 of KD_FLAG1 to see if Pause is active. If it is set, meaning
the machine is frozen, it is reset and INT 9H exits. Bit 3 is set by
pressing Ctrl-Num Lock on the 83-key keyboard or the dedicated Pause key on
the 101-key keyboard. To freeze the machine, INT 9H goes into a loop after
the pause bit is set and waits until the bit is reset. Resetting occurs when
you press any key except any of the shift keys. The placement of the shift
keys' tests before the pause release explains why none of the shift keys
will end the pause.
For example, enter the DOS DIR command, then quickly press Ctrl-Num Lock on
an 83-key keyboard or Pause on a 101-key keyboard to freeze the output. (Be
sure to ask for a large directory.) Now press any of the shift keys. The
shift state will change, but DOS will not continue the DIR listing.
Entering Ctrl-S (^S) will also pause a DIR output. This, however, is not an
INT 9H implementation. Ctrl-S is processed and placed in the keyboard buffer
by INT 9H like any other character and it's DOS that interprets it as a
pause command.
Ctrl-Alt-Del
The Ctrl-Alt-Del key combination also has special meaning. If found, a value
of 1234H is placed in the RESET_FLAG found at memory location 40:72H of the
BIOS data area and a direct jump is made to the initialization procedure
that gets control when you turn the power on. The boot program skips the
power-on self-test if the RESET_FLAG has the value of 1234H, resulting in a
warm boot.
If you look at the KD_FLAG you'll notice there is a bit reserved to indicate
the state of Ctrl and Alt but none for Del. Nor is there a Del key state bit
in KD_FLAG1. This means INT 9H has no way of remembering if the Del key is
depressed. Therefore to get a warm boot, you have to press and hold Ctrl and
Alt before pressing Del. It does not matter in which order Ctrl and Alt are
pressed, as long as Del is pressed last.
Ctrl-Break
If Ctrl-Break is detected, INT 9H does several things. First it clears the
keyboard buffer by setting the head to the tail. INT 9H then sets the high
bit of the BIOS_BREAK bit at address 40:71H of the BIOS data area to 1. Any
program can later examine this bit to detect a Ctrl-Break press. Then it
calls the Ctrl-Break interrupt, INT 1BH. Normally INT 1BH points to DOS, but
any program can replace that address and thereby handle Ctrl-Break. If DOS
is called by INT 1BH, an internal flag is set. If a program later uses
certain DOS function calls and Ctrl-Break has not been turned off (the
Ctrl-Break in DOS, not in INT 9H's), either by you with the DOS BREAK OFF
command or by the program itself, the program is terminated. DOS displays ^C
and you are returned to the DOS prompt. The last thing INT 9H does with
Ctrl-Break is place a zero character and scan code in the keyboard buffer.
Entering Ctrl-C (^C) is also interpreted as Ctrl-Break. This command,
however, like the ^S interpretation, is not an INT 9H implementation. Ctrl-C
is processed by INT 9H like any other character; it's DOS that interprets it
as a break command.
Print Screen
If Shift-PrtScrn on the 83-key keyboard or the dedicated Print Screen key on
the 101-key keyboard is detected, INT 9H simply calls INT 5H to send the
screen dump to the printer. INT 9H doesn't actually do the printing.
Alphanumeric
If INT 9H does not find any of the special scan codes, it knows the scan
code is for an alphanumeric ASCII character. For the alpha characters, this
is when INT 9H checks the Caps Lock status. INT 9H then stores both the
interpreted ASCII character and the scan code in the keyboard buffer. This
gives an application the opportunity to make decisions based on the ASCII
character or on the particular key pressed. Before anything can be stored in
the keyboard buffer, however, there must be room. If there is not enough
room, INT 9H beeps the speaker and exits without storing the character.
Keyboard Buffer
So far, you have followed a keystroke's scan code from the keyboard to a
chip inside the system unit, where it is temporarily stored. We've seen how
the ROM BIOS INT 9H program retrieves the scan code from the chip, converts
it into an ASCII character code, and stores both the scan code and the
character code in the keyboard buffer. Next, we'll examine this keyboard
buffer using a program called KEYVIEW.ASM.
The keyboard buffer is a classic example of a circular buffer, although it
isn't actually circular and it doesn't reside in the keyboard. Its
implementation gives it its name. The keyboard buffer consists of 32
consecutive, linear (not circular) byte addresses very close to the
beginning of the CPU's RAM. This buffer memory is reserved by the ROM BIOS
programs INT 9H and INT 16H as a communication area to store and pass
keystroke information. INT 9H gets keystrokes from the keyboard and stores
them in the buffer; INT 16H retrieves the keystrokes from the buffer and
returns them to an application.
Address 400H (102410) starts the second kilobyte of memory and is the area
used by the BIOS programs for data storage of, among other things, the
keyboard buffer. The buffer's memory location starts at address 41EH
(105410) and ends at, but does not include, address 43EH (108610).
The buffer's 32 bytes are grouped by twos, one byte for storing the INT
9H-interpreted ASCII character code followed by one byte for the scan code.
The buffer start and ending word offset addresses can be thought of as an
indexed distance from the start of the 400H second kilobyte of memory. These
offset addresses, 001EH and 003EH, are stored in another part of the BIOS
data area in two consecutive words at addresses 40:80H and 40:82H,
respectively. Figure 7 shows the memory locations for the keyboard buffer
and its pointers. The pointers enable INT 9H to find the keyboard buffer;
they can be changed to alter the location and size of the buffer.
Two additional pointers, the head and tail of the buffer, are used to keep
track of what's in the buffer. These pointers are also located in the BIOS
data area; they are at the consecutive offset addresses 001AH and 001CH,
respectively.
The KEYVIEW.ASM program (see Figure 8) displays a graphic image of the
keyboard buffer and lets you see its operation as INT 9H and INT 16H store
and retrieve keystrokes. The keyboard buffer is displayed as two rectangular
bands, one atop the other (see Figure 9). The bands are divided into eight
boxes, for a total of the 16 2-byte memory locations of the keyboard buffer.
The buffer's sequential addressing moves left to right starting with the
buffer start (offset 1EH) in the leftmost box of the top band and ending
with the buffer in the rightmost box of the band on the second row. The
bottom of each box contains the buffer's current ASCII character code to the
left next to its scan code pair to the right. The ASCII char/scan code order
is the same as it appears in memory. Above each char/scan code pair is the
display representation of the ASCII character. It is included for reference
and does not exist as part of the keyboard buffer. Programmers may find
KEYVIEW a valuable tool to determine the ASCII and scan codes a program
using INT 16H can expect when a user presses specific keys--especially
with the extended keyboard.
When you run KEYVIEW, one of the first things you will notice in the buffer
is the individual letters of the KEYVIEW command, including the carriage
return (an ASCII 0DH displayed as a quarter note) just as you entered them
at the DOS prompt. If you have a 101-key keyboard, the carriage return will
be followed by an FF FF char/scan combination, placed there by KEYVIEW
during initialization to detect an enhanced keyboard. (This is the method
recommended in the BIOS Technical Reference.) See the KEYVIEW assembly
listing for more information. The head and tail will both be pointing to the
storage location right after the carriage return (or the FF FF if you have a
101-key keyboard.) The tail points to the location at which the next
keystroke will be stored, and the head points to the logical start or first
stored character. When the head equals the tail, both INT 9H and INT 16H
know the buffer is empty. DOS emptied the buffer when you entered the
command to run KEYVIEW. By empty, I don't mean the buffer's memory is empty
or null. The KEYVIEW execution command was not touched when it was retrieved
from the buffer. As you will see in a moment, all that changes is the head
pointer.
Make sure Caps Lock is off; then press the "a" key to store a character. The
ASCII code of 61H, along with the scan code of 1EH, will be stored at the
tail's location. The stored keystroke will be highlighted so you can see the
buffered keystroke easily. In addition, the tail will move up to the next
available storage location. The head stays put, pointing to "a," the logical
start of the buffer. Continue to press the "a" key until all 16 buffer slots
are filled and you will discover two things. When the tail reaches the end
of the buffer (the far right of the bottom band), it goes to the beginning
of the buffer in the top left of the top band. This wrapping or circular
pattern accounts for the keyboard buffer being called a circular buffer. You
will also discover that the last box will not accept a keystroke. Instead,
you will hear the same beep that is emitted when the buffer is full.
This happens because the tail has circumnavigated the buffer, thereby
catching up with the logical head of the buffer. If INT 9H filled this last
slot, the tail would have to be moved up one, but that would place the tail
at the same position as the head. Remember, when the head equals the tail it
means the buffer is empty, so this last slot can never be filled. Thus, the
16-word keyboard buffer can only hold 15 characters.
Now retrieve the keystrokes by pressing F1; that instructs KEYVIEW to ask
INT 16H for a buffered keystroke. The character at the head will return and
be displayed at the bottom of the screen. INT 16H does not physically remove
or erase the character from the buffer. It just reads the memory and notes
that the character has been "removed" by moving the head pointer up one.
KEYVIEW also removes the highlight from the retrieved location. Continue to
press F1 until all the "a's" are retrieved and the head catches up with the
tail. If you press F1 requesting a read from an empty buffer, KEYVIEW will
beep. This circular buffer technique very neatly implements the FIFO logic
necessary to keep keystrokes in the same serial order they were struck.
With KEYVIEW you can see the subtle difference a program can detect between
different keys that elicit the same ASCII character. For example, press the
minus key on the top row between the 0 and = keys and the minus key on the
key pad. KEYVIEW will display the minus ASCII code 2DH for both keypresses,
but it will display different scan codes--0CH and 4AH, respectively.
These key-specific scan codes enable a program to know exactly which key you
pressed. You can observe similar differences between the entire top row of
number keys and the keypad numbers. (When pressing the keypad numbers be
sure Num Lock is on.) The same difference can also be seen by pressing the
two asterisk keys.
Pressing F3 at the DOS prompt displays the last command entered. If you
press F3 while running KEYVIEW, all that INT 9H stores in the buffer is 00H
3DH. F3 has no special meaning to INT 9H and is implemented as a "repeat
last command" only by DOS, which explains why it won't work with in other
programs.
If you have an 101-key keyboard, the KEYVIEW menu will have an F2 toggle to
activate/deactivate the extended INT 16H function calls. When this function
is Active, KEYVIEW takes advantage of the extended INT 16H function calls
for the extended keyboard support. The extended functions enable programs to
distinguish between things like the dedicated and keypad cursor keys.
One of the more interesting things you can observe with KEYVIEW is what INT
9H does with the buffer when you press Ctrl-Break. First, buffer some
keystrokes, then press Ctrl-Break. INT 9H first clears the buffer by making
the head equal to the tail. (Again, the actual memory is untouched. Only the
pointers are changed.) Depending on the BIOS version, the buffer is cleared
by moving the tail to the current head position or the head and tail are
both moved to the start of the buffer. Then a pseudo-char/scan code of 00H
00H is stored and the tail is moved up.
As I've mentioned, Ctrl-Break is an INT 9H command and Ctrl-C is a DOS
command. Although they are thought to be interchangeable, you can see with
KEYVIEW that INT 9H, instead of clearing the buffer, stores a Ctrl-C
keypress as if it was any other character. Ctrl-C is a low ASCII control
character of 3H (displayed as a heart) and has a scan code of 2EH. The same
sometimes-misunderstood relationship can be seen with the INT 9H-interpreted
Ctrl-Num Lock pause command on the 83-key keyboard (the same as the
dedicated Pause key on the 101-key keyboard) and the DOS Ctrl-S pause
command. Press either Ctrl-Num Lock or Pause, whichever applies to your
keyboard; nothing is stored in the buffer as INT 9H places the machine in a
pause state. Press any other key to unfreeze the machine. That key also is
not stored; it merely sets things back in motion. Next, press Ctrl-S; an
ASCII character 13H, a double exclamation point, is stored along with the
scan code of 1FH. The machine is not frozen; only if DOS gets hold of the
Ctrl-S, for example, when it's displaying a DIR listing, is Ctrl-S treated
as a pause command.
Special Scan Codes
Although I said earlier that INT 9H stores the scan code that comes from the
keyboard along with the ASCII character interpretation, this isn't entirely
accurate. For some key combinations, INT 9H makes up its own scan code. For
example, as you saw with PORT-A, a keyboard may send the same scan code for
a key no matter what the state of the Shift keys. Thus when the !/1 key is
pressed, the same scan code is sent regardless of the status of the Caps
Lock or the Shift keys. If you pressed the Alt key and held it and then
pressed the !/1 key, PORT-A (see Figure 3) would display the same 02H scan
code for the !/1 key as when the Alt key was not depressed. With KEYVIEW, a
!/1 keypress is stored as a 31H (the ASCII code for 1) along with the
original 02H scan code as expected. But with an Alt !/1 keypress, INT 9H
stores 00H ASCII code with a scan code of 78H. The 78H (12010) is larger
than the total number of keys on a keyboard. INT 9H has converted this key
combination into a special char/scan code so an application can quickly
detect additional special key combinations without having to sift through
the shift status byte. Both the ASCII code and the scan code have been
assigned by INT 9H and do not reflect the scan code that actually came from
the keyboard. The returned 00H ASCII code lets an application program know
that the accompanying scan code is for a special key combination. For DOS
and other high-level language keyboard function calls, the 00H code cues the
program to repeat the call to get the special scan code. A partial list of
other key combinations that store special scan codes are Alt, Ctrl or Shift
function keys, Num Lock keypad keys, and a Shift Tab.
The 101-Key Keyboard
The advent of the 101-key keyboard introduced two new function keys, F11 and
F12. In an effort to make these keys available for new applications, as well
as to provide downward compatibility for applications written before these
keys existed, the 101-key keyboard INT 16H ROM BIOS routine uses some clever
logic. Programmers are familiar with the three INT 16H calls, functions
0-2, providing the Keyboard Read, Keystroke Status, and Shift Status
services. The 101-key keyboard BIOS support added three new calls known as
extended function calls. These new calls, 10H, 11H, and 12H, have the same
function as the old 00H, 01H, 02H calls except they return specific
information about the new 101-key keyboard keys, including F11 and F12. The
new BIOS applies a special formula for Keyboard Read and Keystroke
Status--the calls that pre-101-key keyboard applications used and most
new programs still use.
The old AH = 00H (Keyboard Read) and AH = 01H (Keystroke Status) INT 16H
function calls return scan and character codes by converting like codes to
compatible codes and extracting the scan code/character code combination
until a compatible combination is found.
If you use the PORT-A program, you will notice that some keys, like the
dedicated cursor keys, return with a whole series of scan codes. These
additional codes enable INT 9H to tell the difference between, for example,
a left arrow that comes from the dedicated cursors and one that comes from
the keypad. If you press these two different left arrow keys while running
KEYVIEW, you will see that the same scan code is stored for both (4BH), but
the dedicated left cursor has an ASCII code of E0H while the keypad has one
of 00H. (The 00H null ASCII code is a signal that a special key combination
like a cursor arrow has been entered. If you experiment with KEYVIEW, you
will see that the E0H code is used for all the dedicated keys in a similar
manner.) The extended INT 16H functions pass these special scan codes to
your program so it can also detect the difference between a keypad arrow and
a dedicated arrow.
In the BIOS translation logic, the first rule means that an old INT 16H
function call will convert a dedicated left arrow to a keypad left arrow so
that an old program will not know the difference. Remember, applications
before the 101-key keyboard don't know the new keys exist. You can see the
translation easily with the aid of KEYVIEW. For example, buffer a couple of
dedicated left arrow keypresses. What you will see stored is two E0H 4BH
char/scan pairs. (With the PORT-A, program you can also see that the
keyboard sends an extra E0H along with the 4BH scan code.) Now retrieve one
left arrow by pressing F1 while the F2 extended support is Active. (This
will only appear as a menu option if you have an extended keyboard. KEYVIEW
can tell if you have a 101-key keyboard from the assembly 101-key keyboard
detection routine mentioned above.) KEYVIEW will retrieve one of the left
arrows using the AH = 10H extended keyboard read call and display the
returned ASCII value of E0H, which looks like an odd lowercase a. That is,
the extended keyboard read call returns the E0H scan code as is.
Next press F2 to toggle the extended function support off or Inactive. Press
F1; KEYVIEW will retrieve the second left arrow using the old INT 16H, AH =
00H function call. You will see a null that looks like a space character
blank. INT 16H has converted the E0H to a 00H ASCII code, removing the
distinction between the two types of arrows.
Now to see what happens with the F11 and F12 keys, clear the KEYVIEW buffer,
if it isn't already, by pressing F1 and making sure the F2 extended keyboard
function support is toggled Active. Buffer five keystrokes by pressing the A
key, F10, F11, F12, and B in that order. As expected, the tail moves as
these five keystrokes are buffered. Retrieve all five keystrokes by pressing
F1. Next press F2 to deactivate the extended functions; enter the same five
keystrokes again--A, F10, F11, F12, B. Retrieve the keystrokes one at a
time by pressing F1. The A comes back as an A and the F10 as a blank (an
ASCII null scan code). But after you retrieved the F10, the head skipped
over the F11 and F12, effectively removing them from the buffer, and stopped
at the B. Press F1 and the B is retrieved as normal.
The explanation for this seemingly bizarre behavior is that when you
deactivated the extended support, KEYVIEW used the old INT 16H function
calls to retrieve the characters. The old functions do not know about F11
and F12, so keystrokes were extracted and ignored by INT 16H until it found
a code it did understand--the compatible B. (KEYVIEW is programmed to
continually call INT 16H, function 01H or 11H, whichever is active, to find
out if a keystroke is available. Calling the old 01H Status function is
KEYVIEW clears the F11 and F12 keystrokes from the buffer.) While the
extended keyboard support is Inactive, press F11 or F12. The keystrokes are
stored, the tail moves up, and the head follows quickly. Since the head
equals the tail, the buffer looks empty to an application. Any application
that uses the old INT 16H BIOS calls--and most do, including newly
written applications--will, unwittingly, never see an F11 or F12.
Now you might think all this supports the argument that a program should go
through the operating system instead of directly to the BIOS for keystrokes.
I haven't talked at all about the DOS keyboard function call support and I'm
not going to start now, other than to say that DOS ends up calling the BIOS
INT 16H to get the keystroke information anyway, so why bother going through
the operating system. (Don't get me wrong. DOS keyboard functions do have
their place. For example, DOS INT 21H, function 0AH, provides an easy way to
implement buffered keyboard input. This and all the other DOS functions are
amply described in the DOS Technical Reference.)
There's a point I want to make regarding DOS. No version of DOS, not even
DOS Version 3.3 or DOS 4.0, is aware of the F11 and F12 keys. DOS uses the
old INT 16H calls. This can be demonstrated by entering the DOS PAUSE
command at the DOS prompt; type PAUSE and press enter. Nothing will appear
to happen. DOS is waiting for you to press a key before returning the DOS
prompt. Press both the F11 and F12 keys; nothing happens. Press F10 (or any
other key), and things will be put back in motion.
Now try this. At the DOS prompt, enter a DOS DIR command on a large
directory and freeze the output by pressing the dedicated Pause key.
Pressing F11 or F12 in this case will put the directory listing back in
action. The Pause key function is a BIOS INT 9H implementation and, as you
would expect, INT 9H knows about the INT 16H F11 and F12 keys. The
conclusion is that going through the operating system does not always ensure
machine-independent compatibility. DOS does not support these extended keys.
You have to go to the BIOS directly and use the extended calls for F11 and
F12 support to detect the difference between the dedicated and keypad cursor
keys. As you may know, direct BIOS calls cannot be done in OS/2 programming.
However, OS/2 keyboard calls have been updated and are aware of F11 and F12.
Expanding the Keyboard Buffer
As we've seen, the main problem with the keyboard buffer is its limited
15-keystroke capacity. In some situations, it's desirable to type commands
into the buffer faster than an application can process them. If the
15-keystroke capacity is reached, the buffer will fill, the speaker will
beep, and you will be stopped from making any additional keyboard entries.
A filled keyboard buffer is rare in applications that spend most of their
time processing keystrokes, such as word processors. But data crunching
applications, such as spreadsheet recalculations or source code compilation,
often get behind. Some applications manage their own buffers to circumvent
the miniature INT 9H keyboard buffer. These programs will store keystrokes
as they occur and process them when they get time. These are the exception,
however, not the rule. I will present two programming solutions to expand
the keyboard buffer, preventing a full buffer and that annoying beep.
To understand the logical solution to expanding the buffer, it is helpful to
consider all the pros and cons of the existing keyboard buffer. As I
mentioned, a negative feature is that the buffer is only 16 words long and
can actually hold only 15 characters. There are, however, pointers to the
start and end of the buffer. When you change the pointers, you change the
location and size of the buffer. To explore the buffer further, see the
sidebar "The Keyboard Buffer and its Pointers."
Another negative is that the pointers are only 16-bit offset addresses. The
segment base value used by the INT 9H and INT 16H programs is always 0040H
of the BIOS data area. This can't be changed. That means the keyboard buffer
must be located within 64Kb of memory segment 0040H. The good news is there
is some memory within this boundary that seems to be used only during the
boot process and is available to expand the keyboard buffer. The bad news is
this same memory is not managed or allocated by DOS, which means other
similar programs may use this memory and destroy the new buffer, or vice
versa.
The simplest way to expand the keyboard buffer is to take advantage of this
unused low memory. The apparent unused memory vacuum is the 256 bytes
starting at address 0040:0200H (0040:0200H can also be expressed as
0060:0000H; see Figure 10, the PC memory map). With the nonmemory management
caveat just mentioned in mind, I'm going to give you a short program named
KBDBUFF.COM that moves the keyboard buffer to this memory location.
KBDBUFF's moving of the buffer is a simple matter of changing the BIOS data
keyboard buffer start and end offset pointers to point to 0040:0200H and
0040:0300H, respectively. The buffer head and tail pointers also have to be
changed to point inside the new buffer. This is accomplished by setting both
pointers to the start of the buffer, which also initializes the buffer as
empty.
To create KBDBUFF.COM, create a file named KBDBUFF.SCR containing the Debug
commands shown in Figure 11. Then redirect the commands to Debug by
entering:
Debug < KBDBUFF.SCR
KBDBUFF will be created. Since there are so few instructions, you can start
up Debug and type them in directly. (The script file method is preferred
because it's easier to correct errors.) Be sure to include the blank line
after the INT 20H instruction. Once KBDBUFF.COM is created, place the
KBDBUFF command at the beginning of your AUTOEXEC.BAT. Thereafter, every
time you boot, the keyboard buffer will be moved and expanded to the
absolute memory location of 420H with a new maximum capacity of 256/2, or
128 keystrokes.
KBDBUFF is a cheap and easy remedy to the buffer problem, but it may not
work in all environments if another application is also using the same small
hole in low memory. For this situation, I've written a better program called
KBBUFFER.CTL (see Figure 12), which is available on bulletin boards,
including those of MSJ. This program gets its memory allocation for the
keyboard buffer through DOS and is, therefore, more reliable. The cost of
being well behaved makes for a longer program; however, the additional use
of memory is negligible.
KBBUFFER.CTL is a device driver that will expand the keyboard buffer. Add to
your CONFIG.SYS file:
DEVICE = [path]KBBUFFER.CTL [buffer size]
The optional buffer size parameter is a decimal number from 16 to 200
indicating the desired keystroke capacity of the new keyboard buffer. The
default size of the buffer is 80 keystrokes and should suffice in most
situations.
KBBUFFER.CTL is a fake device driver; that is, it doesn't drive anything.
All KBBUFFER does is take advantage of the loading order of device drivers
to get within range of the 64Kb offset requirement of the new buffer. Before
I go on, a little background on how DOS uses memory is necessary.
When a PC boots, (a complicated process of which I will present an
abbreviated rendition) DOS allocates memory in the following order. First,
the two hidden system files, IBMBIO.COM and IBMDOS.COM, are loaded (in that
order) in two contiguous memory areas starting at memory location 0070:0000H
just above the BIOS data area. (These programs might be tagged IO.SYS and
MSDOS.SYS with your version of DOS, but they are essentially the same
programs.) IBMBIO.COM contains the system resident device drivers CON, PRN,
AUX and is the interface between DOS and the ROM BIOS. IBMDOS.COM is the DOS
kernel that contains all the DOS 21H function call services.
Part of the system files' initialization process is to allocate a work area
right after the resident system files for things like file control blocks
and disk buffers. Default size values are used for these work areas if
specific requests are not found in the CONFIG.SYS BUFFERS, FILES, and FCB
commands. It's during this CONFIG.SYS examination that DOS loads and
initializes any device drivers found in the CONFIG.SYS file.
Finally, COMMAND.COM is loaded and given control. COMMAND.COM processes the
AUTOEXEC.BAT file and displays the command prompt. COMMAND.COM exists in two
parts, a resident portion that is located right after the device drivers and
the transient portion located at the top of RAM. The resident portion is the
interface between you and the DOS kernel, including those sometimes nebulous
error messages. The transient portion processes batch files and the internal
commands like DIR and TYPE and any service that is not needed when a program
is running. The transient portion is, therefore, expendable and its memory
is made available to any program when it is loaded. (Loading and executing
programs is also a task of the resident portion.) When the application exits
back to DOS, resident COMMAND.COM does a checksum on the transient portion
to see if it was overwritten by the application and needs to be reloaded.
(If you have a floppy system, this is when and why you are prompted for the
boot disk in drive A. That enables COMMAND.COM to reload its transient
portion.) Collectively, the system files, tables, and COMMAND.COM is what's
referred to as DOS.
Getting back to the keyboard buffer problem, at first glance it might appear
that a simple TSR that would reserve a small amount of memory and change the
pointers like KBDBUFF would do the trick. TSRs occupy memory right after
COMMAND.COM; for most versions of DOS, the size of COMMAND.COM puts any TSR,
even if it's the first one loaded, out of reach of the 64Kb offset
requirement of the new keyboard buffer. Since device drivers are loaded
before COMMAND.COM and the system files take much less than 64Kb, a device
driver will always be within reach of the BIOS data segment 64Kb maximum.
This is the key to the success of KBBUFFER.CTL. (Note that since device
drivers are loaded in the same order as they are listed in the CONFIG.SYS
file, KBBUFFER.CTL should appear first to keep it within range.)
Device Drivers
When a device driver is loaded, it is briefly given control to do any
necessary initialization. This is when a "real device" would do things like
inquire what hardware is available, set it up, and hook any pertinent
interrupt vectors. When KBBUFFER.CTL is given the opportunity to initialize,
it changes the BIOS data keyboard buffer pointers to point to itself. At the
end of its initialization, a device driver passes control back to DOS and
tells DOS how much memory it needs to function. KBBUFFER.CTL's
initialization code, like a TSR's, will not be needed again so it is
returned to the system memory pool. KBBUFFER.CTL only needs enough memory to
serve as the new keyboard buffer. This is calculated from the argument (if
any) found on KBBUFFER.CTL's command line. The only memory displaced by
KBBUFFER.CTL, therefore, is the few hundred bytes for the new keyboard
buffer.
A device driver has a different format than executable COM and EXE files.
Instead of program code, the file starts with a device header with codes
that tell DOS, among other things, what type of a device driver it is and
what DOS services the device driver will render. KBBUFFER.CTL seems to be a
character device (like ANSI.SYS as opposed to a block device like a disk
driver) and tells DOS that it can't handle any DOS services. The latter
guarantees that KBBUFFER.CTL will not be called on again by DOS after the
initialization and, since it hasn't hooked any vectors, KBBUFFER.CTL will
remain undisturbed. The exception is that INT 9H and INT 16H will use the
now protected memory for the new keyboard buffer.
One last note: KEYVIEW demands the keyboard buffer be in the default 16 word
BIOS data area in order to operate. This is because the screen isn't large
enough for KEYVIEW to display a larger keyboard. KEYVIEW will, therefore,
refuse to run when either KBDBUFF.COM or KBBUFFER.CTL have been used.
If you've ever wondered how keystrokes are processed, you should certainly
have some idea by now. The programming examples presented should help
clarify the operation of your PC's keyboard, and the utilities will make
your keyboard more functional.
Examining the Keyboard Shift Status and Shift State Bytes
(Be sure to remove any keyboard TSRs you may be using, such as NDOSEDIT,
from your AUTOEXEC.BAT and reboot before trying this exercise--Ed.)
To see the two KD_FLAGs change, start Debug from the DOS prompt and at the
minus sign prompt enter
D 40:17 L1
(dump segment 40H, the BIOS data segment, offset 17H, length one byte).
Debug will display the KD_FLAG status byte (see Figure 4). It will be
easiest to follow along in this Debug session if this value is 0. If it is
not 0, it means you have one of the shift keys locked. If necessary, unlock
the shift keys (Insert, Caps Lock, Num Lock and/or Scroll Lock) and verify
by entering the above command again. You should see 0. Throughout this Debug
session you can press F3 to display the last command.
Press F3 to display the D 40:17 L1 command, but don't press Enter yet. First
press the Insert key to toggle Insert on; now press Enter. You will see 80H,
indicating Insert is active. Give Insert another tap to toggle it off; then
toggle each of the other locking shift keys on (Caps Lock, Num Lock and
Scroll Lock) one at a time. Press F3 and Enter after you shift each one on
to see its effect; then toggle it off. Check that the value you get is the
same as the top four values in Figure 4.
Next, toggle on both the Caps Lock and Num Lock shift keys. Press F3 and
Enter. This time you will see the combined value of the individual bits,
namely, 60H (40H + 20H). Note that you cannot observe the Ctrl and Alt shift
states of KB_FLAG because Ctrl-Enter and Alt-Enter do not produce carriage
returns.
Now do the same sort of thing with the BIOS data area key state KB_FLAG1 at
address 40:18. Enter:
D 40:18 L1
You will see a 0 this time regardless of the locking keys status. KB_FLAG1
is used by the BIOS to keep track of multiple shift keys depressed at the
same time. To see it, repeat the above command with F3 (don't press Enter);
then press and hold down the Insert key while you press Enter. You should
see 80H, indicating that the state of the Insert key is pressed. Follow the
same procedure to experiment with the other shift keys. Enter Q to quit when
you wish to return to DOS.
The Keyboard Buffer and its Pointers
All of the keyboard buffer related information is in the bottom of RAM and
can be easily viewed with the aid of Debug. Run Debug and at the minus sign
prompt enter the command:
D 40:80 L4
That command tells Debug to dump the 4 bytes at segment address 40 with an
offset of 80. At 40:80 the 2-byte offset address of the start of the
keyboard buffer is stored and at 40:82 the offset of the end of the buffer
is stored. The result of the above instruction is:
0040:0080 1E 00 3E 00 ..>.
Since the Intel(R) storage technique is backward, the 2-byte offset value
stored at address 40:80 is reconstructed to 001E and the 40:82 offset value
to 003E. Only offset values are used because the BIOS assumes a segment
value of 40 for the keyboard buffer. The start of the keyboard buffer,
therefore, can be found in the consecutive bytes starting at address
0040:001E and ending, but not including, the byte at 0040:003E.
You can confirm the 16 entry buffer size by subtracting the following two
numbers. 3EH minus 1EH is 20H, or 32 decimal. Since there are two bytes
(scan code plus ASCII code) for every keystroke, 32/2 equals a 16 keystroke
buffer. To see the buffer, enter
D 40:1E L20
which means dump segment 40, offset 1EH (the buffer start) for a length of
20 hex bytes (16). Note that the prefacing zeros (0040:001E) are not needed
in the command. The output will be similar to:
0040:0010 34 05
4.
0040:0020 30 0B 3A 27 31 02 45 12-20 39 4C 26 32 03 30 0B 0.:'1.E.
9L&2.0.
0040:0030 0D 1C 20 39 4C 26 34 05-0D 1C 44 20 20 39 .. 9L&4...D 9
The display to the far right is Debug's attempt to show everything that is
alphanumeric in the memory dump in ASCII. That includes the scan codes that
appear as every other odd memory address to the left of its ASCII
equivalent. If you look closely, you can pick out the dump command (D 40:1E
L20) you just entered. The 9s are the ASCII interpretation of the scan code
for space bar (39H). Their accompanying 20H is the command's actual
delimiting spaces.
In the above dump the command starts with the D toward the end of the third
line, continues with the 4 at the end of the first line, the 0 at the start
of the third line, and so on. Since the keyboard buffer is a circular buffer
and the current head and tail keep moving, the order in which the commands
appear on your machine will differ from what you see here.
The current head and tail of the buffer is stored in consecutive words (2
bytes) at location 40:1A and 40:1C. To view these words enter:
D 40:1A L4
My output was
0040:0010 28 00 28 00 (.(.
Again the storage is backward--28 00 is 0028. The current head and tail
are, therefore, both at 0040:0028. The output you get will differ, but the
values will equal each other, indicating that the head equals the tail and
the buffer is currently empty. Enter the above commands again, and you will
see the values change as the keystrokes scamper around the circular buffer.
Enter Q to return to DOS.
Figure 2
IN AL,20H ;Get the current state of Port B.
OR AL,80H ;Set the high bit.
JMP $ + 2 ;Jump delay for fast machines.
OUT 20H,AL ;Reset the keyboard.
AND AL,NOT 80H ;Turn the high bit back off.
JMP $ + 2 ;Delay.
OUT 20H,AL ;Set Port B to its original state.
Figure 3
TITLE PORT-A.ASM
PAGE 60,132
;---------------------------------------------------;
; A visual display of the keyboard scan code port. ;
; Michael J. Mefford ;
;---------------------------------------------------;
_TEXT SEGMENT PUBLIC 'CODE'
ASSUME CS:_TEXT
ASSUME DS:_TEXT
ORG 100H
START: JMP MAIN
; DATA AREA
; ---------
DB CR,SPACE,SPACE,SPACE,CR,LF
COPYRIGHT DB "PORT-A 1.0 (c) 1990 "
PROGRAMMER DB "Michael J. Mefford",CR,LF,LF,"$"
DB CTRL_Z
CR EQU 13
LF EQU 10
CTRL_Z EQU 26
SPACE EQU 32
BOX EQU 254
ESC_SCAN EQU 1
PORT_A EQU 60H
BIOS_INT_9 DW ?,?
MENU LABEL BYTE
DB "Press and release any key to see "
DB "make and break scan code",CR,LF
DB "Press Esc to Exit",CR,LF,LF,"$"
; CODE AREA
; ---------
MAIN PROC NEAR
CALL CLS ;Clear the screen.
MOV DX,OFFSET COPYRIGHT ;Display Copyright and menu.
MOV AH,9
INT 21H
MOV DX,OFFSET MENU
INT 21H
MOV AX,3509H ;Get keyboard interrupt.
INT 21H
MOV BIOS_INT_9[0],BX ;Save old interrupt.
MOV BIOS_INT_9[2],ES
MOV DX,OFFSET PORT_A_INT_9 ;Install new interrupt.
MOV AX,2509H
INT 21H
;-----------------------------------;
; Loop here until Esc is detected. ;
;-----------------------------------;
GET_KEY:
XOR AH,AH ;Go and wait for a keystroke.
INT 16H
CMP AH,ESC_SCAN ;If it's Esc, exit.
JNZ GET_KEY ;Else, continue.
EXIT:
MOV DX,BIOS_INT_9[0] ;Restore old INT 9.
MOV DS,BIOS_INT_9[2]
MOV AX,2509H
INT 21H
CALL CLS ;Clear the screen.
MOV AX,4C00H ;Exit with error level zero.
INT 21H
MAIN ENDP
;***************;
;* SUBROUTINES *;
;***************;
;--------------------------------------------------------;
; This "hooked" INT 9 procedure will be called whenever ;
; a key is pressed which gives us the opportunity to get ;
; a look at the scan code and display it. ;
;--------------------------------------------------------;
PORT_A_INT_9 PROC NEAR
ASSUME DS:NOTHING
PUSH AX ;Preserve registers.
PUSH BX
PUSH CX
IN AL,PORT_A ;Get the scan code.
CALL HEX_OUTPUT ;And display it.
OLD_INT_9:
POP CX ;Restore registers.
POP BX
POP AX
JMP DWORD PTR BIOS_INT_9 ;Jump to the BIOS INT 9 routine.
PORT_A_INT_9 ENDP
;--------------------------;
HEX_OUTPUT PROC NEAR
MOV BX,AX ;Store number in BX.
MOV CX,204H ;4 positions/word; 4bits/char.
ROTATE_HEX:
ROL BL,CL ;Move highest bits to lowest.
MOV AL,BL ;Store number in AL.
AND AL,1111B ;Mask off all but four lowest.
ADD AL,"0" ;Convert to ASCII.
CMP AL,"9" ;Is it alpha?
JLE PRINT_HEX ;If no, print it.
ADD AL,7 ;Else, adjust.
PRINT_HEX:
MOV AH,0EH ;Print via BIOS.
INT 10H
DEC CH ;Done all four positions?
JNZ ROTATE_HEX ;If no, get next.
MOV CX,2 ;Display two spaces
DELIMIT:
MOV AL,SPACE ; between scan codes
MOV AH,0EH ; as delimiters.
INT 10H
LOOP DELIMIT
RET
HEX_OUTPUT ENDP
;--------------------------;
CLS PROC NEAR
MOV AH,0FH ;Get current video mode.
INT 10H
CMP AL,7 ;Is is mono?
JZ CLEAR_SCREEN ;If yes, clear screen.
MOV AL,3 ;Else, make sure in a text mode.
CLEAR_SCREEN:
XOR AH,AH ;Clear screen by setting mode.
INT 10H
MOV AX,500H ;Make sure page zero.
INT 10H
RET
CLS ENDP
_TEXT ENDS
END START
Figure 5 Programs to Toggle Shift Status
Either create the following script file and feed it to debug by entering
Debug < TOGGLE.SCR
or enter the instructions directly while you are in Debug. The resulting
short files, INSERT.COM, CAPSLOCK.COM, NUMLOCK.COM, and SCROLOCK.COM, can be
used in batch files to toggle the appropriate shift states. For some
extended keyboards, these toggle programs will not change the keyboard LED
indicators. The resulting shift state, however, will be correct.
RCX
C
A
MOV AX,0040
MOV DS,AX
XOR Byte Ptr [0017],80
INT 20
N INSERT.COM
W
E 109 40
N CAPSLOCK.COM
W
E 109 20
N NUMLOCK.COM
W
E 109 10
N SCROLOCK.COM
W
Q
Figure 7
Address Size Name Function
0040:001A 1 word BUFFER_HEAD Points to logical start of buffer
0040:001C 1 word BUFFER_TAIL Points to logical end of buffer
0040:001E 16 words KB_BUFFER ASCII character/scan code storage
0040:003E Buffer end (exclusive of buffer)
0040:0080 1 word BUFFER_START Points to physical start of buffer
(001EH)
0040:0082 1 word BUFFER_END Points to physical end of buffer
(003EH)
Figure 8
KEYVIEW.ASM
TITLE KEYVIEW.ASM
PAGE 60,132
;-----------------------------------------------;
; KeyView - Visual display of keyboard buffer. ;
; Michael J. Mefford ;
;-----------------------------------------------;
BIOS_DATA SEGMENT AT 40H
ORG 1AH
BUFFER_HEAD DW ?
BUFFER_TAIL DW ?
ORG 71H
BIOS_BREAK DB ?
ORG 80H
KBD_BUFF_START DB ?
BIOS_DATA ENDS
_TEXT SEGMENT PUBLIC 'CODE'
ASSUME CS:_TEXT
ASSUME DS:_TEXT
ORG 100H
START: JMP MAIN
; DATA AREA
; ---------
DB CR,SPACE,SPACE,SPACE,CR,LF
COPYRIGHT DB "KEYVIEW 1.0 (c) 1990 "
DB "Michael J. Mefford",CR,LF,LF,"$"
DB CTRL_Z
CR EQU 13
LF EQU 10
CTRL_Z EQU 26
SPACE EQU 32
BOX EQU 254
BUFFER_START EQU 1EH
BUFFER_END EQU 3EH
UP_ARROW EQU 24
DN_ARROW EQU 25
TRUE EQU 1
FALSE EQU 0
BOX_ROW EQU 8
BOX_COL EQU 7
BOX_TWO EQU 6
CHAR_START EQU 1400H
ESC_SCAN_CODE EQU 1
F1_SCAN_CODE EQU 3BH
F2_SCAN_CODE EQU 3CH
PORT_A EQU 60H
PORT_B EQU 61H
COMMAND_PORT EQU 20H
EOI EQU 20H
;-------------------------;
BIOS_INT_9 DW ?,?
NORMAL EQU 07H
INVERSE_BLUE EQU 17H
INVERSE DB 70H
LAST_POS DW CHAR_START
LAST_HEAD DW BUFFER_START
LAST_TAIL DW BUFFER_START
F1_FLAG DB FALSE
ESC_FLAG DB FALSE
EXTENDED_CALL EQU 10H
KEY_SUPPORT DB 0
SUPPORT_FLAG DB 0
HEAD DB "Head",DN_ARROW
TAIL DB "Tail",UP_ARROW
ERASE DB 5 DUP (SPACE)
INVALID_MSG DB "Keyboard Buffer not supported",CR,LF,"$"
MENU LABEL BYTE
DB "Press any key to add to keyboard buffer",CR,LF
DB "Press F1 to retrieve a character from buffer",CR,LF,"$"
INT_16_MSG DB "Press F2 to toggle extended keyboard "
DB "support; Support is now: ",CR,LF,"$"
INT_16_LEN EQU $ - INT_16_MSG - 3
ESC_MSG DB "Press Esc to Exit$"
INACTIVE_MSG DB "INACTIVE "
INACTIVE_LEN EQU $ - INACTIVE_MSG - 2
BUFFER_WINDOW LABEL BYTE
DB 201,7 DUP(7 DUP(205),203),7 DUP(205),187
DB 2 DUP(186,7 DUP(7 DUP(32),186),7 DUP(32),186)
DB 200,7 DUP(7 DUP(205),202),7 DUP(205),188
; CODE AREA
; ---------
MAIN PROC NEAR
;--------------------------------------;
; Exit with non-support message if ;
; original keyboard buffer not active. ;
;--------------------------------------;
MOV AX,SEG BIOS_DATA ;Point to BIOS data.
MOV ES,AX
CMP ES:KBD_BUFF_START,BUFFER_START ;Original buffer active?
JZ GOOD_BUFFER ;If yes, continue.
MOV DX,OFFSET INVALID_MSG ;Else, display invalid message.
CALL PRINT_STRING
MOV AX,4C01H ;Exit with error level 1.
JMP TERMINATE
GOOD_BUFFER:
CLD ;All string moves forward.
CALL VIDEO ;Check the video equip. and CLS.
MOV DX,OFFSET COPYRIGHT ;Display copyright.
CALL PRINT_STRING
MOV DX,OFFSET MENU
CALL PRINT_STRING
;----------------------------------------------------------;
; BIOS Tech Ref recommended 101-keyboard detection method. ;
;----------------------------------------------------------;
MOV DL,2 ;Make two attempts to write
MOV CX,0FFFFH ; char/scan code of FFFFh to
KBD_WRITE:
MOV AH,05H ; buffer via extended keyboard
INT 16H ; write funtion.
OR AL,AL ;Was it successful?
JZ RETRIEVE ;If yes, extended supported.
MOV AH,10H ;Else, maybe buffer full. Make
INT 16H ; room by retrieving char.
DEC DL ;Is this the second time through?
JNZ KBD_WRITE ;If no, try to write again.
JMP SHORT MENU_END ;Else, done here; no support.
RETRIEVE:
MOV CX,15 ;Try 15 times to retrieve
SEARCH_KBD:
MOV AH,11H ; the FFFFh scan code.
INT 16H ;Buffer empty?
JZ MENU_END ;If yes, no support.
MOV AH,10H ;Else, extended Kbd read.
INT 16H
CMP AX,0FFFFH ;Did we find the FFFFh?
JZ EXTENDED_KBD ;If yes, extended Kbd support.
LOOP SEARCH_KBD ;Else, search all 15 possible.
JMP SHORT MENU_END ;If feel through, no support.
;-------------------------------------------------------------;
; If 101-keyboard exists, display additional support message. ;
;-------------------------------------------------------------;
EXTENDED_KBD:
MOV SUPPORT_FLAG,1 ;Flag that supported.
MOV DX,OFFSET INT_16_MSG ;Display support message.
CALL PRINT_STRING
CALL EXTENDED ;Display "ACTIVE" message.
MOV DX,600H ;Move cursor to next line.
CALL SET_CURSOR
;-----------------------------------------------------;
; Display Esc message regardless of keyboard support. ;
;-----------------------------------------------------;
MENU_END:
MOV DX,OFFSET ESC_MSG ;Display Esc message.
CALL PRINT_STRING
;----------------------------------------------------------;
; Display my visual interpretation of the keyboard buffer. ;
;----------------------------------------------------------;
MOV DX,BOX_ROW SHL 8 + BOX_COL ;Start pos. of first box.
MOV BL,INVERSE ;Display in inverse color.
MOV CX,2 ;Two boxes to display.
NEXT_BOX:
PUSH CX ;Save counter.
MOV CX,4 ;Four rows per box.
MOV SI,OFFSET BUFFER_WINDOW ;Point to buffer box.
NEXT_ROW:
PUSH CX ;Save counter.
MOV CX,65 ;65 columns per box.
NEXT_BYTE:
LODSB ;Get a byte
CALL WRITE_CHAR ; and display it.
LOOP NEXT_BYTE ;Repeat for all 65 columns.
MOV DL,BOX_COL ;Box column start
INC DH ;Next row.
POP CX ;Retrieve counter.
LOOP NEXT_ROW ;Repeat for all four rows.
MOV DX,(BOX_ROW + BOX_TWO) SHL 8 + BOX_COL ;2nd box start.
POP CX ;Retrieve counter.
LOOP NEXT_BOX ;Do both boxes.
CALL INITIALIZE ;Fill the visual buffer.
;-------------------------;
; Hook our INT 9 handler. ;
;-------------------------;
MOV AX,3509H ;Get keyboard interrupt.
INT 21H
MOV BIOS_INT_9[0],BX ;Save old interrupt.
MOV BIOS_INT_9[2],ES
MOV DX,OFFSET KEYVIEW_INT_9 ;Install new interrupt.
MOV AX,2509H
INT 21H
MOV AX,SEG BIOS_DATA ;Point to BIOS data area.
MOV ES,AX
;--------------------------------------------------------------;
; Ready for action. Hide the cursor off screen so it's not ;
; distracting. Retrieve characters if F1 pressed. Toggle ;
; extended support on/off if F2 pressed. Clear visual buffer ;
; if Ctrl Break detected. Exit if Esc pressed. ;
;--------------------------------------------------------------;
GET_KEY:
MOV DX,1900H ;Hide the cursor off screen
CALL SET_CURSOR ; on row 25.
CK_F1:
CMP F1_FLAG,TRUE ;Was F1 pressed?
JZ CK_AVAILABLE ;If yes, retrieve a character.
MOV AH,1 ;This will extract extended
OR AH,KEY_SUPPORT ; codes if extended support
INT 16H ; is not active.
CMP SUPPORT_FLAG,2 ;F2 pressed and extended
JZ DO_F2 ; support? If yes, toggle.
CMP ESC_FLAG,TRUE ;Was Esc pressed?
JZ EXIT ;If yes, exit.
TEST ES:BIOS_BREAK,10000000B ;Was Ctrl Break pressed?
JNZ CTRL_BREAK ;If yes, clear visual buffer.
CLI ;No interrupts.
MOV DI,ES:BUFFER_TAIL ;Retrieve buffer tail and head.
MOV SI,ES:BUFFER_HEAD
STI ;Interrupts OK now.
MOV BP,LAST_TAIL ;Retrieve last tail.
CMP DI,BP ;Has the tail moved?
JZ CK_HEAD ;If no, check head.
MOV LAST_TAIL,DI ;Else, store new tail.
MOV SI,OFFSET TAIL ;Indicate tail moved
JMP SHORT DO_BUFFER ; and update visual buffer.
CK_HEAD:
MOV BP,LAST_HEAD ;Retrieve last head.
MOV DI,SI ;Assume head moved.
CMP DI,BP ;Has head moved?
JZ CK_F1 ;If no, nothing to do; Check F1.
MOV SI,BP ;Else, next head position.
INC SI
INC SI
CMP SI,BUFFER_END ;If moved past end of buffer
JNZ STORE_HEAD ; circle to beginning of buffer.
MOV SI,BUFFER_START
STORE_HEAD:
MOV LAST_HEAD,SI ;Store new head.
MOV SI,OFFSET HEAD ;Indicate head has moved
DO_BUFFER:
CALL UPDATE_BUFFER ; and update visual buffer.
JMP SHORT GET_KEY ;Next key.
CK_AVAILABLE:
MOV F1_FLAG,FALSE ;Reset F1 flag.
MOV AH,1 ;Get keystroke status
OR AH,KEY_SUPPORT ; add in type keyboard support.
INT 16H
JNZ DO_F1 ;If key available, get it.
MOV AX,0E07H ;Else, beep speaker.
INT 10H
JMP SHORT CK_F1 ;Done here.
DO_F1:
CALL DISPLAY_CHAR ;Retrieve character from buffer
JMP GET_KEY ; and display; next key.
DO_F2:
CALL EXTENDED ;Toggle extended support.
MOV SUPPORT_FLAG,1 ;Reset support flag.
JMP GET_KEY ;Next key.
CTRL_BREAK:
AND ES:BIOS_BREAK,NOT 80H ;Reset Ctrl Break bit.
CALL INITIALIZE ;Clear the visual buffer.
JMP GET_KEY ;Next key.
;---------------------------------------------------------;
; Clear the buffer by setting head = tail. Clear screen. ;
; Restore INT 9. ;
;---------------------------------------------------------;
EXIT:
CLI ;No interrupts.
MOV ES:BUFFER_HEAD,BUFFER_START ;Set head = buffer start.
MOV ES:BUFFER_TAIL,BUFFER_START ;Set tail = buffer start.
STI ;Interrupts OK now.
CALL VIDEO ;Clear screen.
MOV DX,BIOS_INT_9[0] ;Restore old INT 9.
MOV DS,BIOS_INT_9[2]
MOV AX,2509H
INT 21H
MOV AX,4C00H ;Error level zero.
TERMINATE:
INT 21H
MAIN ENDP
;***************;
;* SUBROUTINES *;
;***************;
;-----------------------------------------------------;
; This subroutine will toggle the extended keyboard ;
; support on/off and display ACTIVE/INACTIVE message. ;
;-----------------------------------------------------;
EXTENDED PROC NEAR
MOV DX,4 SHL 8 + INT_16_LEN ;Row 4; column at end of msg.
MOV CX,INACTIVE_LEN ;Length of message.
MOV BL,NORMAL ;Normal attribute.
MOV SI,OFFSET INACTIVE_MSG ;Assume INACTIVE msg.
XOR KEY_SUPPORT,EXTENDED_CALL ;Toggle support.
JZ DISPLAY_EXT ;If zero, guessed right.
INC SI ;Else, bump pointer past "IN".
INC SI
DISPLAY_EXT:
LODSB ;Display the message.
CALL WRITE_CHAR
LOOP DISPLAY_EXT
RET
EXTENDED ENDP
;------------------------------------------------------;
; All sixteen positions of visual buffer are updated ;
; when program starts and when Ctrl Break is detected. ;
;------------------------------------------------------;
INITIALIZE PROC NEAR
MOV BP,LAST_TAIL ;Retrieve last tail.
MOV DI,ES:BUFFER_HEAD ;Retrieve current head.
PUSH DI ;Preserve last tail.
MOV LAST_TAIL,DI ;Make last tail = head.
MOV SI,OFFSET TAIL ;Point to tail msg.
CALL UPDATE_BUFFER ;Update the visual buffer.
MOV BP,LAST_HEAD ;Retrieve last head.
MOV AX,BP ;Save in AX
NEXT_INIT:
PUSH AX ;Preserve AX.
MOV DI,BP ;Move head up one.
INC DI
INC DI
CMP DI,BUFFER_END ;If head = buffer end
JNZ DO_INIT ; then head = buffer start.
MOV DI,BUFFER_START
DO_INIT:
MOV SI,OFFSET HEAD ;Point to head msg.
CALL UPDATE_BUFFER ;Update visual buffer.
MOV BP,DI
POP AX ;Retrieve last head.
CMP BP,AX ;Did we do all 16 positions?
JNZ NEXT_INIT ;If not, continue until done.
POP DI ;Retrieve last tail.
MOV LAST_HEAD,DI ;Last head = last tail.
MOV SI,OFFSET HEAD ;Point to head msg.
CALL UPDATE_BUFFER ;Update visual buffer.
RET
INITIALIZE ENDP
;----------------------------;
; This is the INT 9 handler. ;
;----------------------------;
KEYVIEW_INT_9 PROC NEAR
ASSUME DS:NOTHING
STI ;Interrupts OK.
PUSH AX ;AX will be destroyed; preserve.
IN AL,PORT_A ;Get the scan code.
CMP AL,F1_SCAN_CODE ;Is it F1 make?
JZ RESET_MAKE ;If yes, flag and reset KBD.
CMP AL,F1_SCAN_CODE OR 80H ;Else, is it F1 break?
JZ RESET_BREAK ;If yes, ignore.
CMP AL,ESC_SCAN_CODE ;Else, is it Esc?
JZ ESC_EXIT ;If yes, flag Esc.
CMP SUPPORT_FLAG,1 ;Extended keyboard support?
JNZ OLD_INT_9 ;If no, done here.
CMP AL,F2_SCAN_CODE OR 80H ;Else, is it F2 break?
JZ RESET_BREAK ;If yes, ignore.
CMP AL,F2_SCAN_CODE ;Else, is it F2 make?
JNZ OLD_INT_9 ;If no, done here.
MOV SUPPORT_FLAG,2 ;Else, flag pressed.
JMP SHORT RESET_BREAK ;Reset the KBD.
ESC_EXIT:
MOV ESC_FLAG,TRUE ;If Esc pressed, flag and exit.
OLD_INT_9:
POP AX ;Restore AX.
JMP DWORD PTR BIOS_INT_9 ;Go to BIOS INT 9 routine.
;------------------------------------------------------;
; If F1 or F2 pressed, don't let BIOS store character. ;
; Instead reset KBD and throw away the keystroke. ;
;------------------------------------------------------;
RESET_MAKE:
MOV F1_FLAG,TRUE ;Flag F1 pressed.
RESET_BREAK:
IN AL,PORT_B ;Retrieve Port B.
OR AL,80H ;Turn bit 7 on to reset
JMP $ + 2 ;I/O delay.
OUT PORT_B,AL ;Reset KBD.
AND AL,NOT 80H ;Turn bit 7 back off.
JMP $ + 2 ;I/O delay.
OUT PORT_B,AL ;Restore port.
CLI ;Interrupts off.
MOV AL,EOI ;Send End Of Interrupt
OUT COMMAND_PORT,AL ; to 8259A PIC.
POP AX ;Restore AX.
IRET ;Interrupt return.
ASSUME DS:_TEXT
KEYVIEW_INT_9 ENDP
;-----------------------------------------------;
; When F1 is pressed, a character is retrieved ;
; from the buffer and displayed. ;
;-----------------------------------------------;
DISPLAY_CHAR PROC NEAR
MOV AX,LAST_POS ;Get last char display column.
CMP AL,78 ;Was it column 78?
JBE RESTORE_POS ;If below or equal, OK.
MOV CX,CHAR_START ;Else, clear the line.
MOV DX,CHAR_START + 78
MOV BH,NORMAL
MOV AX,600H
INT 10H
MOV AX,CHAR_START ;Start at beginning of line.
RESTORE_POS:
MOV DX,AX ;Cursor position in DX.
INC AX ;Bump to new cursor position
MOV LAST_POS,AX ; and save for next time.
MOV AH,0 ;Retrieve character; include
OR AH,KEY_SUPPORT ; appropriate keyboard support.
INT 16H
MOV BL,INVERSE ;Display in inverse video.
CALL WRITE_CHAR
RET
DISPLAY_CHAR ENDP
;------------------------------------;
; INPUT ;
; BP = Last position. ;
; DI = New position. ;
; SI = OFFSET HEAD or OFFSET TAIL. ;
;------------------------------------;
UPDATE_BUFFER PROC NEAR
MOV DX,BP ;Last position in DX.
CALL CURSOR_POS ;Calculate cursor position.
PUSH DX ;Preserve cursor position.
ADD DH,2 ;Move two columns right.
MOV AX,ES:[BP] ;Retrieve last scan/char code.
MOV BL,INVERSE ;Display in inverse
CMP SI,OFFSET TAIL ; if tail moved.
JZ DO_CHAR
MOV BL,NORMAL ;Else, head moved;display normal.
DO_CHAR:
PUSH DX ;Save cursor position.
CALL SPACES ;Center char in top of box with
CALL WRITE_CHAR ; three spaces on either side.
CALL SPACES
POP DX ;Retrieve cursor position.
INC DH ;Next row.
XCHG AL,AH ;Swap scan code and ASCII char.
PUSH AX ;Preserve.
MOV AL,SPACE ;Display a space.
CALL WRITE_CHAR
POP BP ;Retrieve char/scan code.
CALL HEX_OUTPUT ;Display in hex.
POP DX ;Retrieve cursor position.
INC DL ;Next column.
MOV BL,NORMAL ;Normal attribute.
CMP SI,OFFSET HEAD ;Erase either head or tail msg.
JZ DO_ERASE
ADD DH,5 ;Tail at bottom of box.
DO_ERASE:
PUSH SI ;Save msg pointer.
MOV SI,OFFSET ERASE ;Erase old msg.
CALL DISPLAY_POINT
MOV DX,DI ;Move to new position.
CALL CURSOR_POS
INC DL ;Move right one column.
POP SI
CMP SI,OFFSET HEAD ;Display either head or tail msg.
JZ DO_POINTER
ADD DH,5
DO_POINTER:
CALL DISPLAY_POINT
RET
UPDATE_BUFFER ENDP
;--------------------------;
SPACES PROC NEAR
PUSH AX ;Preserve AX.
MOV CX,3 ;Display three spaces.
MOV AL,SPACE
NEXT_SPACE:
CALL WRITE_CHAR
LOOP NEXT_SPACE
POP AX
RET
SPACES ENDP
;-----------------------------;
; INPUT ;
; BP = Character/scan code. ;
;-----------------------------;
HEX_OUTPUT PROC NEAR
MOV CX,2 ;Two codes to display.
NEXT_NIBBLE:
PUSH CX ;Preserve counter.
MOV CX,204H ;4 positions/word; 4bits/char.
ROTATE_HEX:
ROL BP,CL ;Move highest bits to lowest.
MOV AX,BP ;Char/scan code in AX.
AND AL,1111B ;Mask off all but four lowest.
ADD AL,"0" ;Convert to ASCII.
CMP AL,"9" ;Is it alpha?
JLE PRINT_HEX ;If no, print it.
ADD AL,7 ;Else, adjust.
PRINT_HEX:
CALL WRITE_CHAR ;And write them.
DEC CH ;Done all four positions?
JNZ ROTATE_HEX ;If no, get next.
MOV AL,SPACE ;Delimit with a space.
CALL WRITE_CHAR
POP CX ;Do both char and scan code.
LOOP NEXT_NIBBLE
RET
HEX_OUTPUT ENDP
;------------------------------;
; INPUT ;
; SI points to pointer text. ;
;------------------------------;
DISPLAY_POINT PROC NEAR
MOV CX,5 ;Five characters to pointer.
NEXT_POINT:
LODSB
CALL WRITE_CHAR ;Write them.
LOOP NEXT_POINT
RET
DISPLAY_POINT ENDP
;--------------------------;
CURSOR_POS PROC NEAR
SUB DX,BUFFER_START ;Difference = offset.
MOV DH,BOX_ROW - 1 ;Point to starting row of box.
CMP DL,16 ;If offset less than 16 OK.
JB GET_COLUMN
SUB DL,16 ;Else adjust offset.
ADD DH,BOX_TWO ;Point to second box.
GET_COLUMN:
SHL DL,1 ;Multiply by 4 to get cursor
SHL DL,1 ; column.
ADD DL,BOX_COL + 1 ;Add offset of box start.
RET
CURSOR_POS ENDP
;-------------------------;
; INPUT ;
; DX = cursor position. ;
;-------------------------;
SET_CURSOR:
PUSH AX ;Preserve AX.
XOR BH,BH ;Page zero.
MOV AH,2
INT 10H ;Set cursor position.
POP AX ;Retrieve AX.
RET
;-----------------------------;
; INPUT ;
; DX = new cursor position. ;
; AL = Character to write. ;
;-----------------------------;
WRITE_CHAR PROC NEAR
PUSH AX ;Preserve AX and CX.
PUSH CX
CALL SET_CURSOR ;Set cursor position.
MOV CX,1 ;One character to write.
MOV AH,9
INT 10H
INC DL
POP CX ;Restore registers.
POP AX
RET
WRITE_CHAR ENDP
;--------------------------;
VIDEO PROC NEAR
MOV AH,0FH ;Retrieve current video mode.
INT 10H
CMP AL,7 ;Is it mono?
JZ CLEAR_SCREEN ;If yes, clear screen.
CMP AL,2 ;Is it black and white CGA?
JZ CLEAR_SCREEN ;If yes, clear screen.
MOV INVERSE,INVERSE_BLUE ;Else, use color attributes.
MOV AL,3 ; and video mode CO80.
CLEAR_SCREEN:
XOR AH,AH ;Set video mode.
INT 10H
MOV AX,500H ;Set page zero.
INT 10H
RET
VIDEO ENDP
;--------------------------;
PRINT_STRING:
MOV AH,9 ;Print string via DOS.
INT 21H
RET
_TEXT ENDS
END START
Figure 11 KBDBUFF.COM
N KBDBUFF.COM ;Name: KBDBUFF.COM
A ;Assemble
MOV AX,0040 ;Move 40H into the data segment
MOV DS,AX
CLI ;No interrupts
MOV Word Ptr [001A],0200 ;Buffer head = offset 200
MOV Word Ptr [001C],0200 ;Buffer tail = offset 200
MOV Word Ptr [0080],0200 ;Buffer start = offset 200
MOV Word Ptr [0082],0300 ;Buffer end = offset 300
STI ;Interrupts back on
INT 20 ;Terminate
;Quit assembly
RCX ;Register CX is file length
21 ;File length = 21H bytes
W ;Write the file
Q ;Quit Debug
Figure 12 KBBUFFER.ASM
;---------------------------------------------------------------;
; KBBUFFER.CTL * Michael J. Mefford ;
; Is loaded as a device driver just to get within offset ;
; range of the BIOS data area and the keyboard buffer so ;
; it can replace the default 15 key buffer with a larger one. ;
;---------------------------------------------------------------;
BIOS_DATA SEGMENT AT 40H
ORG 1AH
BUFFER_HEAD DW ?
BUFFER_TAIL DW ?
ORG 80H
BUFFER_START DW ?
BUFFER_END DW ?
BIOS_DATA ENDS
_TEXT SEGMENT PUBLIC 'CODE'
ASSUME CS:_TEXT,DS:_TEXT,ES:_TEXT,SS:_TEXT
ORG 0H
;COPYRIGHT DB "KBBUFFER.CTL 1.0 (c) 1990 ",CR,LF
;PROGRAMMER DB "Michael J. Mefford",CR,LF,CTRL_Z
;************* DEVICE_HEADER *************;
POINTER DD -1
ATTRIBUTE DW 1000000000000000B
DEVICE_STRAG DW STRATEGY
DEVICE_INT DW INTERRUPT
DEVICE_NAME DB "BUFFERCTL"
CR EQU 13
LF EQU 10
CTRL_Z EQU 26
SPACE EQU 32
BOX EQU 254
;-------------------------;
REQUEST_HEADER STRUC
HEADER_LENGTH DB ?
UNIT_CODE DB ?
COMMAND_CODE DB ?
STATUS DW ?
RESERVED DQ ?
REQUEST_HEADER ENDS
DONE EQU 0000000100000000B ;Status codes.
UNKNOWN EQU 1000000000000011B
;-------------------------;
INIT STRUC
HEADER DB (TYPE REQUEST_HEADER) DUP(?)
UNITS DB ?
ENDING_OFFSET DW ?
ENDING_SEGMENT DW ?
ARGUMENTS_OFF DW ?
ARGUMENTS_SEG DW ?
INIT ENDS
REQUEST_OFFSET DW ?
REQUEST_SEG DW ?
; CODE AREA
; ---------
;---------------------------------------------;
; The only task of the strategy routine is to ;
; save the pointer to the request header. ;
;---------------------------------------------;
STRATEGY PROC FAR
MOV CS:REQUEST_OFFSET,BX ;Request header address is
MOV CS:REQUEST_SEG,ES ; passed in ES:BX.
RET
STRATEGY ENDP
;----------------------------------------;
; The interrupt procedure will be called ;
; immediately after the strategy. ;
;----------------------------------------;
INTERRUPT PROC FAR
PUSH AX ;Responsible for all registers.
PUSH BX
PUSH CX
PUSH DX
PUSH DS
PUSHF
MOV DS,CS:REQUEST_SEG ;Retrieve request header pointer.
MOV BX,CS:REQUEST_OFFSET
OR STATUS[BX],DONE ;Tell DOS we are done.
CMP COMMAND_CODE[BX],0 ;Is it INIT command?
JZ MAKE_STACK ;If yes, do our stuff.
OR STATUS[BX],UNKNOWN ;Else, exit with confused
JMP SHORT UNKNOWN_EXIT ; message to DOS.
MAKE_STACK:
MOV CX,SS ;Save DOS stack.
MOV DX,SP
MOV AX,CS
CLI
MOV SS,AX ;Make new stack.
MOV SP,0FFFEH
STI
PUSH CX ;Save old stack pointers on new.
PUSH DX
PUSH ES ;Save rest of registers.
PUSH SI
PUSH BP
CALL INITIALIZE ;Go do our stuff.
POP BP ;Restore registers.
POP SI
POP ES
POP DX ;Restore old DOS stack.
POP CX
CLI
MOV SS,CX
MOV SP,DX
STI
UNKNOWN_EXIT:
POPF ;Restore rest of registers.
POP DS
POP DX
POP CX
POP BX
POP AX
RET ;Far return back to DOS.
INTERRUPT ENDP
KBBUFFER_CTL_END LABEL WORD
;************* END OF RESIDENT PORTION *************;
BUFFER_DEFAULT EQU 80
BUFFER_MIN EQU 16
BUFFER_MAX EQU 200
HEADING LABEL BYTE
DB "KBBUFFER.CTL 1.0 (c) 1990 "
DB "Michael J. Mefford",CR,LF,LF,"$"
Creating Windows List Boxes that Support Virtualized Scrolling
Robert A. Wood
One of the standard resources the Microsoft(R) Windows environment provides
to programmers is the list box. The standard list box works best in
instances that involve a relatively small number of short strings: the
"file/open..." dialog box provided by most Windows applications is a good
example. But what if an application needs to support a more complex string
selection facility? For example, a user might have to view and select long
records from a large database application with hundreds of thousands of
records. One of the limitations of a standard list box is the memory
required to store the list box strings. These list box strings must be
stored in a single segment of memory with a maximum size of 64Kb. As the
number and size of the strings increase, it takes much longer to load the
list box. It takes even longer to fill the list box when you have it sort
the strings, since the sort is performed every time you add a string.
Another limitation is the width of a standard list box's display string.
List boxes display no more than 76 characters in a single line.
One solution is to create a virtual list box (VLB) facility that can provide
arbitrary amounts of vertical and horizontal scrolling. Linked into your
application, the VLB described here manages a short list of strings in
memory. When the user scrolls horizontally or vertically, the VLB updates
the contents of the list box with additional strings obtained from your
application. We will first describe the callback function your application
must supply, then the VLB subclass itself, followed by a sample program.
Callback Function
Because the VLB manages only the strings currently visible within the list
box, it relies on a specialized string retrieval callback function provided
by your application when the VLB is initialized. The callback function
responds to requests from the VLB for list box strings and related
information. The specific source of the strings is irrelevant to the VLB.
The callback function receives four types of calls from the VLB:
initialization, virtual limits, string retrieval, and horizontal scrolling.
The "messages" for each type are listed in Figure 1.
When responding to string retrieval requests (messages) from the VLB, the
callback function must assign a unique long integer string ID to each
retrieved string. This ID can be a database record number, a file offset, a
numeric key value, or any other numeric value. The VLB subsequently uses
this string ID to identify each string and reference it when communicating
with the callback function. The VLB passes a far pointer to a long integer
string ID to the callback function to be used as a basis for a string
retrieval. The new string's ID must be provided by the callback function via
the pointer.
String Management
The VLB stores the string IDs for the visible strings in memory. An
application can request the total number of selected strings with the
message VLB_GETSELCOUNT, and request the selected string IDs with the
message VLB_GETSELID. For a single selection list box, this message returns
the selected string ID. For a multiselection list box, it returns a handle
to an array of the selected string IDs.
The message VLB_GETSELSTR returns a pointer to the entire selected string.
When the message is for a multiselection list box, the wParam value of the
VLB_GETSELSTR message specifies the selected string to be retrieved: to get
the first string, wParam is zero.
When the VLB needs another string for the list box, it sends one of the
string retrieval messages to the callback function. The message may include
a string ID, and it always includes a pointer to a string buffer to be
loaded with the retrieved display string.
Creating a VLB
You create a VLB object like any other standard control: with the Dialog
Editor or by editing the *.rc file. The VLB is created as a subclass of the
standard list box or edit control class.
If you use the list box class, a horizontal scroll bar cannot be displayed
although horizontal scrolling is still available via the keyboard. To
display the horizontal scroll bar, you must specify the edit class with both
scroll bars, although the edit class has a problem displaying the border of
the VLB. To circumvent this, include a static class frame behind the edit
control; this makes the frame appear to border the VLB edit control. The
static control must have the same coordinates as the edit control.
The VLB Procedure
A VLB (see Figure 2) must be initialized by the application using it with
the InitVLB function. This function will allocate memory and initialize a
structure of information for the VLB. If the VLB is used in a dialog box,
InitVLB would be called while processing the WM_INITDIALOG message. The
InitVLB function requires an instance handle, parent handle, list box ID,
and the name of the callback function your application provides.
First, the list box ID is saved for sending messages to the callback
function and for sending notifications to the parent. The control's style is
checked for multiple selection. The character width and height text metrics
are retrieved. The client area size is saved; it is then used with the
character width and height metrics to calculate the number of characters
that will fit in a display string and the number of strings that will fit in
the window. The VLB will keep track of which string has the focus
(designated by a dotted frame). The position of the focus string, the
position of the first displayed string, and the total number of selected
strings are initialized to zero. The VLB callback function is also saved.
Next, the VLB allocates a buffer to hold the displayed strings. The size of
the buffer is calculated by multiplying the number of characters per display
string (plus one for the null byte) by the number of display strings. The
VLB then allocates memory for an array of longs for the string ID of each
display string. The string IDs are initialized to -1. The handles to
these memory blocks are stored in the VLB information structure (see Figure
3).
For a single selection list box, the selected string ID is saved in a long
integer. It is initialized to -1. For a multiple selection list box,
another allocation is done. It is an array of longs for the selected string
IDs. The size of the array starts at the total number of display strings. A
maximum number of selected strings is saved and initialized to the total
number of display strings. This value is used to determine if more memory
needs to be allocated for the array of selected string IDs when multiple
strings are being selected. The array is then initialized to -1. The
handle to the allocated memory block is stored in the VLB information
structure.
Next, MakeProcInstance is performed on the VLB window procedure, VLBProc.
The VLB is subclassed with the function SetWindowLong. The handle to the
allocated VLB information structure is added to the control property list
using the function SetProp. Create messages are then sent to the VLB
procedure and to the callback function.
The callback function is sent a VCB_LENGTH message to query the number of
virtual strings. The vertical scroll bar range is set from zero to the
number of virtual strings less the number of display strings less one. This
allows the thumb to be at the bottom of the scroll bar for the last page of
display strings.
If the window includes a horizontal scroll bar, the callback function is
next sent a VCB_WIDTH message to obtain the maximum number of characters in
the display string. The horizontal scroll bar range is set from zero to the
virtual string width minus one.
Finally, InitVLB loads the list box. The VLB procedure is sent a VLB_RELOAD
message with the wParam equal to RELOAD_STRINGPOS and lParam equal to zero.
This tells the VLB to reload its list box starting with string position
zero.
The initial request to load the list box causes the VLB to send a VCB_VTHUMB
message with a string ID equal to zero to the callback function. The
callback function will put its first display string in the display string
buffer and will assign the new string ID to the far pointer to a long
parameter. The callback function will receive VCB_NEXT messages for the
remaining display strings. The string ID will be equal to the previously
retrieved string ID.
When the VLB receives new display strings, it adjusts its internal buffer of
display strings and its array of displayed string IDs. After all the
necessary strings are retrieved and the adjustments are made to the internal
information, the VLB processes the paint message.
The VLB painting process is done in three steps. First, each display string
is written to the client area with the TextOut function. Second, the
selected string or strings are inverted. Third, the focus string is framed
with a dotted border if it is visible.
The VLB sends a WM_CTLCOLOR message to the parent of the list box before
painting. The wParam specifies the handle to the device context, hCtl, and
lParam is equal to
MAKELONG( hCtl, CTLCOLOR_LISTBOX)
The display string IDs are compared to the selected string ID for a single
selection list box and to the array of selected string IDs for a multiple
selection list box.
The VLB processes the WM_GETDLGCODE message and returns DLGC_WANTARROWS.
When the VLB receives the WM_SETFOCUS message, it puts a dotted border
around the focus string if it is visible and sends a VCB_SETFOCUS message to
the callback function. When the VLB receives the WM_KILLFOCUS message, it
removes the dotted border and sends a VCB_KILLFOCUS message to the callback
function. These messages can be processed by the callback function to open
and close the source file.
The WM_VSCROLL messages cause the VLB to send the appropriate messages to
the callback function to get the new display strings. If the wParam of
WM_VSCROLL equals SB_THUMBPOSITION, the list box is completely reloaded
based on the new position. If wParam equals SB_LINEDOWN, the VLB moves its
internal buffer of display strings back one string and requests a new last
display string from the callback function. Similarly, for SB_LINEUP, the VLB
moves its internal buffer of display strings forward one string and requests
a new first display string from the callback function. If wParam equals
SB_PAGEUP, the VLB moves its first display string to the last display string
position and sends VCB_PREV messages to the callback function for the
previous display strings. If wParam equals SB_PAGEDOWN, the VLB moves its
last display string to the first display string position and sends VCB_NEXT
messages to the callback function for the next set of display strings. When
the VLB display string buffer is updated the array of string IDs is also
updated.
The WM_HSCROLL messages cause the VLB to send VCB_. . . messages to the
callback function to set the new starting column of the display strings. If
wParam equals SB_THUMBPOSITION, the new starting column is the thumb
position. If wParam equals SB_LINEUP, the starting column should be moved
one character to the left. If wParam equals SB_LINEDOWN, the starting column
should be moved one character to the right. If wParam equals SB_PAGEUP or
SB_PAGEDOWN, the starting column should be moved left or right one data
column, respectively. The callback function stores an array of tab stops
for the beginning of each data column. After processing a WM_HSCROLL
message, the currently displayed strings are reloaded based on their new
starting column.
The arrow keys and other direction keys cause the VLB to scroll its
contents. These keys also change the selected string. The Control key and
left or right arrow keys simulate the horizontal page left or right.
The left mouse button click selects the string above the mouse cursor. The
left mouse button double click causes a VLBN_DBLCLK notification to be sent
to the parent.
When the VLB receives the WM_DESTROY message it frees all memory allocated
for the VLB information structure and removes the handle to the VLB
information structure from the control's property list. All messages that
the VLB procedure does not handle are passed on to the DefWindowProc.
DEMO Program
DEMO (see Figure 4) displays a VLB created from a multiline edit control
with a static frame control as a border. The VLB has both horizontal and
vertical scroll bars and is a single-selection list box.
The callback function shown here, VLBfile, retrieves strings from a sample
file we've included named VLB.TXT (see Figure 5). The VLB.TXT file contains
a listing of a subdirectory with five columns of data. The callback function
has an array of five tab stops for the VCB_PAGERIGHT and VCB_PAGELEFT
messages. It also stores a static int called Column that sets the starting
column of the display string. Column is manipulated by the VCB_. . .
horizontal scrolling messages.
The unique string ID is the physical record number. The strings are
retrieved by first performing an lseek to the record offset. The offset is
specified by multiplying the string ID by the record length.
DEMO processes the VLBN_SELCHANGE notification by requesting the selected
string from the VLB and setting the string in the static text control above
the list box.
Enhancements
Although the VLB is completely functional, possible enhancements might
include support for proportional fonts and moving the implementation into a
dynamic-link library. The VLB could also be modified to allow resizing of
the list box. The ScrollWindow function might be used to speed up painting
of the list box. Finally, in a commercial product, the VLB should support
mouse dragging for multiple selections and the use of the shift key to
deselect strings.
Figure 1
VLB Procedure Messages to the VLB Callback Function
Message Description LONG FAR * LPSTR Return Value
1. Initialization
VCB_CREATE VLB has just been created N/A N/A N/A
VCB_DESTROY VLB is going to be destroyed N/A N/A N/A
VCB_SETFOCUS VLB has just received focus N/A N/A N/A
VCB_KILLFOCUS VLB is going to lose focus N/A N/A N/A
2. Virtual limits
VCB_LENGTH Request total virtual strings N/A N/A Total strings
VCB_WIDTH Request virtual string width N/A N/A String width
3. String retrieval
VCB_FULLSTRING Get full string by string ID String ID String
buffer TRUE/FALSE
VCB_STRING Get string by string ID String ID String buffer
TRUE/FALSE
VCB_VTHUMB Get string by thumb position Thumb position String
buffer TRUE/FALSE
VCB_SEARCH Get string by search string String ID String buffer
TRUE/FALSE
VCB_FIRST Get first display string String ID String buffer
TRUE/FALSE
VCB_LAST Get last display string String ID String buffer
TRUE/FALSE
VCB_NEXT Get next display string String ID String buffer
TRUE/FALSE
VCB_PREV Get previous display string String ID String buffer
TRUE/FALSE
4. Horizontal scrolling
VCB_HTHUMB New start of display string Thumb position N/A N/A
VCB_LEFT New start of display string N/A N/A N/A
VCB_RIGHT New start of display string N/A N/A N/A
VCB_PAGELEFT New start of display string N/A N/A N/A
VCB_PAGERIGHT New start of display string N/A N/A N/A
Application Messages to the VLB Procedure
Message wParam lParam Return Value
VLB_GETCOUNT N/A N/A Total virtual strings
VLB_GETSELCOUNT N/A N/A Total selected strings
VLB_GETSTRLEN N/A N/A Virtual string length
VLB_GETSELSTR
Single selection N/A N/A Pointer to selected string
Multiple selection String number N/A Pointer to selected string
VLB_GETSELID
Single selection N/A N/A Selected string ID
Multiple selection N/A N/A Handle to array of selected string IDs
VLB_SETCURSEL N/A String ID N/A
VLB_SETSEL TRUE/FALSE String ID N/A
VLB_RELOAD RELOAD_STRINGS N/A N/A
RELOAD_STRINGPOS String position N/A
RELOAD_STRINGID String ID N/A
VLB Procedure Notifications to the Dialog Procedure
Message wParam lParam Return Value
VLBN_DBLCLK N/A N/A N/A
VLBN_ERRSPACE N/A N/A N/A
VLBN_SELCHANGE N/A N/A N/A
Figure 2 Virtual List Box Source Code
VLB.H
/*
** VLB.H Virtual List Box header file
**
** Author: Robert A. Wood
**
*/
#define VLB_CALLBACK lpVLB->CallBack
#define VLBSTRLEN 255 // maximum virtual string length
typedef LONG (FAR PASCAL * VLBPROC)( WORD, WORD, LONG FAR *, LPSTR );
// VCB Virtual CallBack function Messages
//*******************************************************************
#define VCB_CREATE 1 // list box has just been created
#define VCB_DESTROY 2 // list box is to be destroyed
#define VCB_SETFOCUS 3 // list box has received focus
#define VCB_KILLFOCUS 4 // list box is to lose focus
#define VCB_LENGTH 5 // request for total strings
#define VCB_WIDTH 6 // request for vitual width
#define VCB_FULLSTRING 7 // get the full string specified
#define VCB_STRING 8 // get string specify by StringId
#define VCB_VTHUMB 9 // get string specify by thumb
#define VCB_SEARCH 10 // get string for search criteria
#define VCB_FIRST 11 // get the first string
#define VCB_LAST 12 // get the last string
#define VCB_NEXT 13 // get the next string
#define VCB_PREV 14 // get the previous string
#define VCB_HTHUMB 15 // move str start specify thumb
#define VCB_LEFT 16 // move str start left one char
#define VCB_RIGHT 17 // move str start right one char
#define VCB_PAGELEFT 18 // move str start left one col
#define VCB_PAGERIGHT 19 // move str start right one col
// VLB Messages Sent to the VLB Procedure
//*******************************************************************
#define VLB_GETCOUNT WM_USER+1 // get total virtual strings
#define VLB_GETSELCOUNT WM_USER+2 // get total selected strings
#define VLB_GETSTRLEN WM_USER+3 // get total string length
#define VLB_GETSELSTR WM_USER+4 // get the select String
#define VLB_GETSELID WM_USER+5 // get the select StringId(s)
#define VLB_SETCURSEL WM_USER+6 // selects the specify StringId
#define VLB_SETSEL WM_USER+7 // sets selection of a StringId
#define VLB_RELOAD WM_USER+8 // reload curr. display strings
// VLB_RELOAD wParam options
//*******************************************************************
#define RELOAD_STRINGS 0
#define RELOAD_STRINGPOS 1
#define RELOAD_STRINGID 2
// Notifications Sent to the VLB Parent by the VLB Procedure
//*******************************************************************
#define VLBN_DBLCLK LBN_DBLCLK // double clicked on a string
#define VLBN_ERRSPACE LBN_ERRSPACE // can't allocate memory
#define VLBN_SELCHANGE LBN_SELCHANGE // selected string changed
// VLB Control Information
//*******************************************************************
typedef struct
{
WORD ListBoxId; // list box control id
BOOL MultiSelection; // Single or Multi selection
RECT ClientRect; // client area rectangle
WORD CharWidth; // character width
WORD CharHeight; // character height
BYTE DisplayStrings; // max number of displayed strings
BYTE DisplayChars; // max characters in displayed strings
LONG FocusString; // string position of focus frame
LONG TotalStrings; // number of virtual strings
LONG TotalWidth; // number of virtual chars per string
LONG FirstDisplayString; // number of first displayed string
LONG TotalSelectedStrings; // number of selected strings
LONG MaxSelectedStrings; // maximum number of selected strings
VLBPROC CallBack; // VLB Callback function
HANDLE hDisplayBuffer; // handle to buffer of display strings
HANDLE hStringIds; // handle - array of display StringIds
HANDLE hSelectedStringIds; // handle - array of select StringIds
LONG SelectedStringId; // single selection selected StringId
int ScrollWindow; // scroll lines for ScrollWindow()
} VLB, FAR *LPVLB; // 62 bytes
// Function called by application to initialize VLB
//*******************************************************************
BOOL FAR PASCAL InitVLB( HANDLE, HWND, WORD, VLBPROC );
// Internal functions
//*******************************************************************
LONG FAR PASCAL VLBProc( HWND hCtl, unsigned message, WORD wParam ,
LONG lParam );
BOOL FAR PASCAL LoadVLB( HWND hCtl, WORD wParam, LONG lParam );
BOOL FAR PASCAL ScrollVLB( HWND hCtl, WORD wParam, int Scroll );
VOID FAR PASCAL SetSelectedString( HWND hCtl, WORD wParam, LPVLB);
VOID FAR PASCAL SetFocusString( WORD wParam, LPVLB lpVLB );
VOID InvertSelectedStrings( HDC hCtl, LPVLB lpVLB, int StringPos );
VOID FrameFocusString( HWND hCtl, LPVLB lpVLB, BOOL draw );
VOID FAR PASCAL PaintVLB( HWND hCtl, LPVLB lpVLB );
VOID FAR PASCAL GetVLBColors( HWND hCtl, HANDLE hDC );
//*** END OF VLB.H **************************************************
VLB.C
/*
** vlb.c Virtual List Box Source Code file
**
** Author: Robert A. Wood
** Executive Micro Systems
** 1716 Azurite Trail
** Plano, TX 75075
**
** Microsoft C version 5.1 / medium memory model
** Microsoft Windows SDK version 2.1
** Runtime: Windows 286 version 2.1
**
*/
#include <windows.h>
#undef min
#undef max
#include <stdlib.h>
#include <stdio.h>
#include "vlb.h"
#include "lmem.h"
static char StringBuffer[ VLBSTRLEN + 1 ];
static char szVLBPropName[] = "VLB";
static FARPROC lpfnVLBProc = NULL;
LPSTR FAR PASCAL lstrcpy (LPSTR, LPSTR) ;
WORD FAR PASCAL Rpad( LPSTR str, WORD length );
/*
** Virtual List Box initialization function
********************************************************************/
BOOL FAR PASCAL InitVLB( HANDLE hInstance, HWND hDlg,
WORD ListBoxId, VLBPROC VLBCallBack )
{
int x;
HANDLE hCtl;
HANDLE hVLB;
LPVLB lpVLB;
HDC hDC;
TEXTMETRIC tm; // need info about character sizes
LONG FAR * lpStringIds;
// check that a call back function address was passed in
if( ! VLBCallBack )
{
MessageBeep( 0 );
MessageBox( hDlg, "Invalid VLBCallBack", NULL,
MB_ICONHAND | MB_OK );
return( FALSE );
}
// check for existence of List boxId in specified dialog box
if( ( hCtl = GetDlgItem( hDlg, ListBoxId ) ) = = NULL )
{
MessageBeep( 0 );
MessageBox( hDlg, "Invalid List Box Id", NULL,
MB_ICONHAND | MB_OK );
return( FALSE );
}
// allocate VLB control information structure
if( hVLB = GlobalAlloc( GHND, (LONG)sizeof( VLB ) ) )
lpVLB = (LPVLB)GlobalLock( hVLB );
else
{
SendMessage( hDlg, WM_COMMAND, ListBoxId,
MAKELONG( hCtl, VLBN_ERRSPACE ) );
return( FALSE );
}
hDC = GetDC( hCtl );
GetTextMetrics( hDC, &tm );
ReleaseDC( hCtl, hDC );
lpVLB->ListBoxId = ListBoxId;
lpVLB->MultiSelection = (BOOL)
(GetWindowLong( hCtl, GWL_STYLE ) & LBS_MULTIPLESEL);
GetClientRect( hCtl, &lpVLB->ClientRect );
lpVLB->CharWidth = tm.tmAveCharWidth;
lpVLB->CharHeight = tm.tmHeight + tm.tmExternalLeading;
lpVLB->DisplayStrings = (BYTE)
lpVLB->ClientRect.bottom / lpVLB->CharHeight;
lpVLB->DisplayChars = (BYTE)
lpVLB->ClientRect.right / lpVLB->CharWidth;
lpVLB->FocusString = 0;
lpVLB->FirstDisplayString = 0;
lpVLB->TotalSelectedStrings = 0;
lpVLB->MaxSelectedStrings = lpVLB->DisplayStrings;
VLB_CALLBACK = VLBCallBack;
lpVLB->SelectedStringId = -1;
// allocate buffer for displayed strings
if( !(lpVLB->hDisplayBuffer = GlobalAlloc( GHND, (LONG)
(lpVLB->DisplayStrings * (lpVLB->DisplayChars + 1) ) ) ) )
{
GlobalUnlock( hVLB );
GlobalFree( hVLB );
SendMessage( hDlg, WM_COMMAND, ListBoxId,
MAKELONG( hCtl, VLBN_ERRSPACE ) );
return( FALSE );
}
// allocate array of longs for the string Ids of displayed strings
if( !(lpVLB->hStringIds = GlobalAlloc( GHND, (LONG)
(lpVLB->DisplayStrings * sizeof( LONG ) ) ) ) )
{
GlobalUnlock( lpVLB->hDisplayBuffer );
GlobalFree( lpVLB->hDisplayBuffer );
GlobalUnlock( hVLB );
GlobalFree( hVLB );
SendMessage( hDlg, WM_COMMAND, ListBoxId,
MAKELONG( hCtl, VLBN_ERRSPACE ) );
return( FALSE );
}
// initialize string Ids to -1
lpStringIds = (LONG FAR *) GlobalLock( lpVLB->hStringIds );
for( x = 0; x < lpVLB->DisplayStrings; x++, lpStringIds[x] = -1L )
;
GlobalUnlock( lpVLB->hStringIds );
// setup for a multiselection list box
if( lpVLB->MultiSelection )
{
LONG FAR * lpSelectedStringIds;
// allocate array of longs for the selected string Id's
if( !(lpVLB->hSelectedStringIds = GlobalAlloc( GHND, (LONG)
(lpVLB->MaxSelectedStrings * sizeof( LONG ) ) ) ) )
{
GlobalUnlock( lpVLB->hStringIds );
GlobalFree( lpVLB->hStringIds );
GlobalUnlock( lpVLB->hDisplayBuffer );
GlobalFree( lpVLB->hDisplayBuffer );
GlobalUnlock( hVLB );
GlobalFree( hVLB );
SendMessage( hDlg, WM_COMMAND, ListBoxId,
MAKELONG( hCtl, VLBN_ERRSPACE ) );
return( FALSE );
}
// initialize selected string Ids to -1
lpSelectedStringIds = (LONG FAR *)
GlobalLock( lpVLB->hSelectedStringIds );
for( x = 0; x < lpVLB->DisplayStrings;
x++, lpSelectedStringIds[x] = -1L )
;
GlobalUnlock( lpVLB->hSelectedStringIds );
}
if( !lpfnVLBProc )
lpfnVLBProc = MakeProcInstance( (FARPROC) VLBProc, hInstance );
// subclass VLB
SetWindowLong( hCtl, GWL_WNDPROC, (LONG) lpfnVLBProc );
// store handle to VLB structure in the control's property list
SetProp( hCtl, szVLBPropName, hVLB );
// send create messages to control and to the VLB callback func
SendMessage( hCtl, WM_CREATE, 0, 0L );
VLB_CALLBACK( ListBoxId, VCB_CREATE, 0, 0L );
// get the vitual list box length and set the scroll range
lpVLB->TotalStrings = VLB_CALLBACK( ListBoxId, VCB_LENGTH, 0, 0L);
SetScrollRange( hCtl, SB_VERT, 0, (int)
lpVLB->TotalStrings - lpVLB->DisplayStrings - 1, TRUE );
// check for horizontally scrolling
if( GetWindowLong( hCtl, GWL_STYLE ) & WS_HSCROLL )
{
// get the vitual list box width and set the scroll range
lpVLB->TotalWidth = VLB_CALLBACK( ListBoxId, VCB_WIDTH, 0, 0L);
SetScrollRange( hCtl, SB_HORZ, 0, (int)
lpVLB->TotalWidth - 1, TRUE );
}
// load list box
VLB_CALLBACK( ListBoxId, VCB_SETFOCUS, 0, 0L );
SendMessage( hCtl, VLB_RELOAD, RELOAD_STRINGPOS, 0L );
VLB_CALLBACK( ListBoxId, VCB_KILLFOCUS, 0, 0L );
// I'm out of here
GlobalUnlock( hVLB );
return( TRUE );
}
/*
** Virtual List Box Procedure
********************************************************************/
LONG FAR PASCAL VLBProc( HWND hCtl, unsigned message,
WORD wParam, LONG lParam )
{
HANDLE hVLB;
LPVLB lpVLB;
LONG lThumb;
switch ( message )
{
case WM_CHAR:
break;
case WM_SETCURSOR: // override setting cursor
break;
case WM_GETDLGCODE: // don't let dialog manager have
return( DLGC_WANTARROWS ); // his way with the arrow keys
case WM_CREATE: // sent just after being subclassed
return( 1L );
case VLB_RELOAD: // reload the list box
LoadVLB( hCtl, wParam, lParam );
return( 1L );
case WM_PAINT: // this is where it all takes shape
hVLB = GetProp( hCtl, szVLBPropName );
lpVLB = (LPVLB) GlobalLock( hVLB );
PaintVLB( hCtl, lpVLB );
GlobalUnlock( hVLB );
break;
case WM_SETFOCUS:
HideCaret(hCtl);
hVLB = GetProp( hCtl, szVLBPropName );
lpVLB = (LPVLB) GlobalLock( hVLB );
VLB_CALLBACK( lpVLB->ListBoxId, VCB_SETFOCUS, 0, 0L );
FrameFocusString( hCtl, lpVLB, TRUE );
GlobalUnlock( hVLB );
break;
case WM_KILLFOCUS:
hVLB = GetProp( hCtl, szVLBPropName );
lpVLB = (LPVLB) GlobalLock( hVLB );
FrameFocusString( hCtl, lpVLB, FALSE );
InvertSelectedStrings( hCtl, lpVLB, (int)( lpVLB->FocusString -
lpVLB->FirstDisplayString ) );
VLB_CALLBACK( lpVLB->ListBoxId, VCB_KILLFOCUS, 0, 0L );
GlobalUnlock( hVLB );
break;
case WM_VSCROLL:
switch( wParam )
{
case SB_THUMBPOSITION:
LoadVLB( hCtl, RELOAD_STRINGPOS, (LONG)LOWORD( lParam ) );
break;
case SB_LINEDOWN:
case SB_LINEUP:
case SB_PAGEDOWN:
case SB_PAGEUP:
ScrollVLB( hCtl, wParam, NULL );
break;
}
break;
case WM_HSCROLL:
hVLB = GetProp( hCtl, szVLBPropName );
lpVLB = (LPVLB) GlobalLock( hVLB );
lThumb = -1;
switch( wParam )
{
case SB_THUMBPOSITION:
lThumb = (LONG)LOWORD( lParam );
lThumb = VLB_CALLBACK( lpVLB->ListBoxId, VCB_HTHUMB,
&lThumb, 0L );
LoadVLB( hCtl, RELOAD_STRINGS, 0L );
break;
case SB_LINEDOWN:
lThumb = VLB_CALLBACK( lpVLB->ListBoxId, VCB_RIGHT, 0, 0L );
LoadVLB( hCtl, RELOAD_STRINGS, 0L );
break;
case SB_LINEUP:
lThumb = VLB_CALLBACK( lpVLB->ListBoxId, VCB_LEFT, 0, 0L );
LoadVLB( hCtl, RELOAD_STRINGS, 0L );
break;
case SB_PAGEDOWN:
lThumb = VLB_CALLBACK( lpVLB->ListBoxId,VCB_PAGERIGHT,0,0L);
LoadVLB( hCtl, RELOAD_STRINGS, 0L );
break;
case SB_PAGEUP:
lThumb = VLB_CALLBACK( lpVLB->ListBoxId, VCB_PAGELEFT,0,0L);
LoadVLB( hCtl, RELOAD_STRINGS, 0L );
break;
}
if( lThumb >= 0 )
{
lThumb = (LONG)min( (int)lpVLB->TotalWidth - 1,
max( 0, (int)lThumb ) );
SetScrollPos( hCtl, SB_HORZ, (int)lThumb, TRUE );
}
GlobalUnlock( hVLB );
break;
case WM_MOUSEMOVE:
break;
case WM_LBUTTONDBLCLK:
SendMessage( GetParent( hCtl ), WM_COMMAND,
GetWindowWord( hCtl, GWW_ID ),
MAKELONG( hCtl, VLBN_DBLCLK ) );
break;
case WM_LBUTTONDOWN:
{
LONG lFocusString;
hVLB = GetProp( hCtl, szVLBPropName );
lpVLB = (LPVLB) GlobalLock( hVLB );
lpVLB->FocusString = lpVLB->FirstDisplayString +
( HIWORD( lParam ) / lpVLB->CharHeight );
SetSelectedString( hCtl, NULL, lpVLB );
InvalidateRect( hCtl, NULL, TRUE );
GlobalUnlock( hVLB );
break;
}
case WM_KEYDOWN:
{
BOOL CtrlKey = HIBYTE( GetKeyState( VK_CONTROL ) );
hVLB = GetProp( hCtl, szVLBPropName );
lpVLB = (LPVLB) GlobalLock( hVLB );
switch( wParam )
{
case ' ':
{
SetSelectedString( hCtl, wParam, lpVLB );
InvalidateRect( hCtl, NULL, TRUE );
break;
}
case VK_DOWN:
if( lpVLB->FocusString = = lpVLB->FirstDisplayString +
lpVLB->DisplayStrings - 1 )
ScrollVLB( hCtl, SB_LINEDOWN, NULL );
SetFocusString( wParam, lpVLB );
if( ! CtrlKey )
SetSelectedString( hCtl, wParam, lpVLB );
InvalidateRect( hCtl, NULL, TRUE );
PaintVLB( hCtl, lpVLB );
break;
case VK_UP:
if( lpVLB->FocusString = = lpVLB->FirstDisplayString )
ScrollVLB( hCtl, SB_LINEUP, NULL );
SetFocusString( wParam, lpVLB );
if( ! CtrlKey )
SetSelectedString( hCtl, wParam, lpVLB );
InvalidateRect( hCtl, NULL, TRUE );
PaintVLB( hCtl, lpVLB );
break;
case VK_NEXT:
if( lpVLB->FocusString = = lpVLB->FirstDisplayString +
lpVLB->DisplayStrings - 1 )
ScrollVLB( hCtl, SB_PAGEDOWN, NULL );
SetFocusString( wParam, lpVLB );
if( ! CtrlKey )
SetSelectedString( hCtl, wParam, lpVLB );
InvalidateRect( hCtl, NULL, TRUE );
PaintVLB( hCtl, lpVLB );
break;
case VK_PRIOR:
if( lpVLB->FocusString = = lpVLB->FirstDisplayString )
ScrollVLB( hCtl, SB_PAGEUP, NULL );
SetFocusString( wParam, lpVLB );
if( ! CtrlKey )
SetSelectedString( hCtl, wParam, lpVLB );
InvalidateRect( hCtl, NULL, TRUE );
PaintVLB( hCtl, lpVLB );
break;
case VK_HOME:
LoadVLB( hCtl, RELOAD_STRINGPOS, 0L );
SetFocusString( wParam, lpVLB );
if( ! CtrlKey )
SetSelectedString( hCtl, wParam, lpVLB );
InvalidateRect( hCtl, NULL, TRUE );
break;
case VK_END:
LoadVLB( hCtl, RELOAD_STRINGPOS, lpVLB->TotalStrings - 1 );
SetFocusString( wParam, lpVLB );
if( ! CtrlKey )
SetSelectedString( hCtl, wParam, lpVLB );
InvalidateRect( hCtl, NULL, TRUE );
break;
case VK_RIGHT:
case VK_LEFT:
{
int message;
if( wParam = = VK_RIGHT )
{
if( CtrlKey )
message = VCB_PAGERIGHT;
else
message = VCB_RIGHT;
}
else
{
if( CtrlKey )
message = VCB_PAGELEFT;
else
message = VCB_LEFT;
}
lThumb = VLB_CALLBACK( lpVLB->ListBoxId, message, 0, 0L );
lThumb = (LONG)min( (int)lpVLB->TotalWidth - 1,
max( 0, (int)lThumb ) );
SetScrollPos( hCtl, SB_HORZ, (int)lThumb, TRUE );
LoadVLB( hCtl, RELOAD_STRINGS, 0L );
InvalidateRect( hCtl, NULL, TRUE );
break;
}
}
GlobalUnlock( hVLB );
break;
}
case VLB_SETCURSEL: // WM_USER+6 select specified StringId
case VLB_SETSEL: // WM_USER+7 sets selection of StringId
break;
case VLB_GETCOUNT: // WM_USER+1 get total virtual strings
case VLB_GETSELCOUNT: // WM_USER+2 get total selected strings
case VLB_GETSTRLEN: // WM_USER+3 get total string length
case VLB_GETSELSTR: // WM_USER+4 get the selected String
case VLB_GETSELID: // WM_USER+5 get selected StringId(s)
{
LONG lret = 0;
hVLB = GetProp( hCtl, szVLBPropName );
lpVLB = (LPVLB) GlobalLock( hVLB );
if( message = = VLB_GETCOUNT )
lret = lpVLB->TotalStrings;
else if( message = = VLB_GETSELCOUNT )
lret = lpVLB->TotalSelectedStrings;
else if( message = = VLB_GETSTRLEN )
lret = lpVLB->CharWidth;
else if( message = = VLB_GETSELSTR )
{
if( lpVLB->TotalSelectedStrings )
{
if( lpVLB->MultiSelection )
{
LONG FAR *lpSelectedStringIds =
(LONG FAR *)GlobalLock( lpVLB->hSelectedStringIds);
VLB_CALLBACK( lpVLB->ListBoxId, VCB_FULLSTRING,
lpSelectedStringIds + wParam, StringBuffer );
GlobalUnlock( lpVLB->hSelectedStringIds );
}
else
VLB_CALLBACK( lpVLB->ListBoxId, VCB_FULLSTRING,
&lpVLB->SelectedStringId, StringBuffer );
}
else
*StringBuffer = 0;
lret = (LONG)(LPSTR)StringBuffer;
}
else if( message = = VLB_GETSELID )
{
if( lpVLB->MultiSelection )
lret = lpVLB->hSelectedStringIds;
else
lret = lpVLB->SelectedStringId;
}
GlobalUnlock( hVLB );
return( lret );
}
case WM_DESTROY:
hVLB = GetProp( hCtl, szVLBPropName );
lpVLB = (LPVLB) GlobalLock( hVLB );
VLB_CALLBACK( lpVLB->ListBoxId, VCB_DESTROY, 0, 0L );
GlobalFree( lpVLB->hDisplayBuffer );
GlobalFree( lpVLB->hStringIds );
if( lpVLB->MultiSelection )
GlobalFree( lpVLB->hSelectedStringIds );
GlobalUnlock( hVLB );
GlobalFree( hVLB );
RemoveProp( hCtl, szVLBPropName );
break;
default:
return( DefWindowProc( hCtl, message, wParam, lParam ) );
}
return( 0L );
}
//*******************************************************************
VOID FrameFocusString( HWND hCtl, LPVLB lpVLB, BOOL draw )
{
RECT Rect;
HDC hDC;
// is the focus string visible
if( lpVLB->FocusString >= lpVLB->FirstDisplayString &&
lpVLB->FocusString < lpVLB->FirstDisplayString +
lpVLB->DisplayStrings )
{
hDC = GetDC( hCtl );
Rect.left = lpVLB->ClientRect.left + 1;
Rect.right = lpVLB->ClientRect.right;
Rect.top = (WORD) ( (lpVLB->FocusString -
lpVLB->FirstDisplayString) * lpVLB->CharHeight) + 1;
Rect.bottom = Rect.top + lpVLB->CharHeight;
if( draw )
FrameRect( hDC, &Rect, GetStockObject( GRAY_BRUSH ) );
else // redraw string without frame
{
LPSTR lpString = (LPSTR)GlobalLock( lpVLB->hDisplayBuffer );
WORD nLine = (WORD)
( lpVLB->FocusString - lpVLB->FirstDisplayString );
// move display buffer pointer to focus string
lpString += nLine * ( lpVLB->DisplayChars + 1 );
TextOut ( hDC, 1, ( nLine * lpVLB->CharHeight ) + 1,
lpString, lpVLB->DisplayChars );
GlobalUnlock( lpVLB->hDisplayBuffer );
}
ReleaseDC( hCtl, hDC );
}
}
//*******************************************************************
VOID InvertSelectedStrings( HDC hCtl, LPVLB lpVLB, int StringPos )
{
int x;
LONG y;
RECT Rect;
int FirstString, LastString;
LONG FAR *lpStringIds;
HDC hDC;
hDC = GetDC( hCtl );
lpStringIds = (LONG FAR *)GlobalLock( lpVLB->hStringIds );
Rect.left = lpVLB->ClientRect.left + 1;
Rect.right = lpVLB->ClientRect.right;
if( StringPos < 0 ) // process all displayed strings
{
FirstString = 0;
LastString = (int)lpVLB->DisplayStrings - 1;
}
else
FirstString = LastString = StringPos;
if( lpVLB->MultiSelection )
{
LONG FAR *lpSelectedStringIds =
(LONG FAR *)GlobalLock( lpVLB->hSelectedStringIds );
for( x = FirstString; x <= LastString; x++ )
{
for( y = 0; y < lpVLB->TotalSelectedStrings; y++ )
{
if( lpStringIds[x] == lpSelectedStringIds[ y ] )
{
Rect.top = ( x * lpVLB->CharHeight ) + 1;
Rect.bottom = Rect.top + lpVLB->CharHeight;
InvertRect( hDC, &Rect );
break;
}
}
}
GlobalUnlock( lpVLB->hSelectedStringIds );
}
else // single selection
{
for( x = FirstString; x <= LastString; x++ )
{
if( lpStringIds[x] = = lpVLB->SelectedStringId )
{
Rect.top = ( x * lpVLB->CharHeight ) + 1;
Rect.bottom = Rect.top + lpVLB->CharHeight;
InvertRect( hDC, &Rect );
break;
}
}
}
ReleaseDC( hCtl, hDC );
GlobalUnlock( lpVLB->hStringIds );
}
//*******************************************************************
WORD FAR PASCAL Rpad( LPSTR str, WORD length )
{
LPSTR cp = str; // pointer to string
int x; // current string position
for( x = 0; *cp; x++, cp++ ) // skip to end of string
;
for( ; x < length; x++, *cp++ = ' ' ) // pad string with spaces
;
*cp = 0; // NULL terminate string
return( length ); // return new string length
}
//*******************************************************************
BOOL FAR PASCAL LoadVLB( HWND hCtl, WORD wParam, LONG lParam )
{
int x;
HANDLE hVLB = GetProp( hCtl, szVLBPropName );
LPVLB lpVLB = (LPVLB)GlobalLock( hVLB );
LPSTR lpString = (LPSTR)GlobalLock( lpVLB->hDisplayBuffer );
LONG FAR *lpStringIds = (LONG FAR *)GlobalLock(lpVLB->hStringIds);
LONG l;
// initialize the DisplayBuffer and StringIds
lmemset( lpString, 0,
lpVLB->DisplayStrings * (lpVLB->DisplayChars + 1) );
for( x = 0; x < lpVLB->DisplayStrings; x++, lpStringIds[x] = -1L )
;
if( wParam = = RELOAD_STRINGS )
VLB_CALLBACK( lpVLB->ListBoxId, VCB_STRING,
&lpStringIds[0], StringBuffer );
else if( wParam = = RELOAD_STRINGPOS )
{
lpStringIds[0] = min( lParam,
lpVLB->TotalStrings - lpVLB->DisplayStrings );
lpVLB->FirstDisplayString = lpStringIds[0];
VLB_CALLBACK( lpVLB->ListBoxId, VCB_VTHUMB,
&lpStringIds[0], StringBuffer );
}
else if( wParam = = RELOAD_STRINGID )
{
lpStringIds[0] = lParam;
VLB_CALLBACK( lpVLB->ListBoxId, VCB_STRING,
&lpStringIds[0], StringBuffer );
}
// load first DisplayString
StringBuffer[ lpVLB->DisplayChars ] = 0; // Null @ Display width
lstrcpy( lpString, StringBuffer ); // advance to next string
Rpad( lpString, lpVLB->DisplayChars ); // pad with spaces
for( x = 1; x < lpVLB->DisplayStrings; x++ )
{
l = lpStringIds[x - 1];
if( VLB_CALLBACK( lpVLB->ListBoxId, VCB_NEXT,
&l, StringBuffer ) )
{
StringBuffer[ lpVLB->DisplayChars ] = 0; // Null Terminate
lpString += lpVLB->DisplayChars + 1; // go to next str
lstrcpy( lpString, StringBuffer ); // copy buf to str
Rpad( lpString, lpVLB->DisplayChars ); // pad with spaces
lpStringIds[x] = l;
}
else
break;
}
InvalidateRect( hCtl, NULL, TRUE ); // Force WM_PAINT message
// set new scroll bar thumb postion
SetScrollPos( hCtl, SB_VERT, (int)lpVLB->FirstDisplayString,TRUE);
GlobalUnlock( lpVLB->hDisplayBuffer );
GlobalUnlock( lpVLB->hStringIds );
GlobalUnlock( hVLB );
return( TRUE );
}
//*******************************************************************
BOOL FAR PASCAL ScrollVLB( HWND hCtl, WORD wParam, int Scroll )
{
int x, scroll, ret = TRUE;
HANDLE hVLB = GetProp( hCtl, szVLBPropName );
LPVLB lpVLB = (LPVLB)GlobalLock( hVLB );
LPSTR lpString = (LPSTR)GlobalLock( lpVLB->hDisplayBuffer );
LONG FAR *lpStringIds = (LONG FAR *)GlobalLock(lpVLB->hStringIds);
LONG l;
LPSTR lpGoodStrings;
int nGoodStrings;
// Check if scroll request is possible
if( wParam = = SB_LINEDOWN )
{ // at the end of the total strings
if( lpVLB->FirstDisplayString + lpVLB->DisplayStrings >
lpVLB->TotalStrings - 1 )
ret = FALSE;
else
scroll = 1;
lpVLB->ScrollWindow = 1;
}
else if( wParam = = SB_LINEUP )
{
if( lpVLB->FirstDisplayString = = 0 )
ret = FALSE;
else
scroll = 1;
lpVLB->ScrollWindow = -1;
}
else if( wParam = = SB_PAGEDOWN )
{
if( lpVLB->FirstDisplayString + lpVLB->DisplayStrings >
lpVLB->TotalStrings - 1 )
ret = FALSE;
else
scroll = min( (int)lpVLB->DisplayStrings - 1,
(int)(lpVLB->TotalStrings - ( lpVLB->FirstDisplayString +
lpVLB->DisplayStrings ) ) );
lpVLB->ScrollWindow = scroll;
}
else if( wParam = = SB_PAGEUP )
{
if( lpVLB->FirstDisplayString = = 0 )
ret = FALSE;
else
scroll = min( (int)lpVLB->DisplayStrings - 1,
(int)lpVLB->FirstDisplayString );
lpVLB->ScrollWindow = scroll * -1;
}
else // scroll number of the Scroll 3rd paramater
{
lpVLB->ScrollWindow = Scroll;
if( Scroll < 0 )
{
Scroll = abs( Scroll ); // absolute scroll amount
wParam = SB_LINEUP; // scroll up
}
else if( Scroll > 0 )
wParam = SB_LINEDOWN; // scroll down
else // if( Scroll = = 0 ) // no scroll amt specified
ret = FALSE;
if( Scroll >= lpVLB->DisplayStrings ) // scroll less than #
ret = FALSE; // of displayed strings
else
scroll = Scroll;
}
if( ret = = FALSE )
{
lpVLB->ScrollWindow = FALSE;
GlobalUnlock( lpVLB->hDisplayBuffer );
GlobalUnlock( lpVLB->hStringIds );
GlobalUnlock( hVLB );
return( FALSE );
}
// pointer to strings that will still be displayed
lpGoodStrings = lpString + ( (lpVLB->DisplayChars + 1) * scroll );
// number of strings that will still be displayed
nGoodStrings = ( lpVLB->DisplayStrings - scroll );
// adjust strings that will still be displayed and get new strings
if( wParam = = SB_LINEUP || wParam = = SB_PAGEUP )
{
lpVLB->FirstDisplayString -= scroll;
// push good strings & StringIds down in their buffers
lmemmove( lpGoodStrings, lpString,
nGoodStrings * (lpVLB->DisplayChars + 1) );
lmemmove( lpStringIds + scroll, lpStringIds,
nGoodStrings * sizeof( LONG ) );
// move Display string pointer to last string to get
lpString + = ( scroll - 1 ) * (lpVLB->DisplayChars + 1);
// get the previous scroll number of strings
for( x = 0; x < scroll; x++ )
{
// get the StringId of the last string read
l = lpStringIds[ scroll - x ];
if( VLB_CALLBACK( lpVLB->ListBoxId, VCB_PREV,
&l, StringBuffer ) )
{
StringBuffer[ lpVLB->DisplayChars ] = 0;// Null Terminate
lstrcpy( lpString, StringBuffer ); // copy buf to str
Rpad( lpString, lpVLB->DisplayChars ); // pad with spaces
lpString -= lpVLB->DisplayChars + 1; // go to next str
lpStringIds[ ( scroll - x ) - 1 ] = l; // save StringId
}
else
break;
}
}
else // if( wParam = = SB_LINEDOWN || wParam = = SB_PAGEDOWN )
{
lpVLB->FirstDisplayString + = scroll;
// move good strings & StringIds to the beginning of their bufs
lmemmove( lpString, lpGoodStrings,
nGoodStrings * (lpVLB->DisplayChars + 1) );
lmemmove( lpStringIds, lpStringIds + scroll,
nGoodStrings * sizeof( LONG ) );
// move Display string pointer to first string to get
lpString += nGoodStrings * (lpVLB->DisplayChars + 1);
// get the next scroll number of strings
for( x = 0; x < scroll; x++ )
{
// get the StringId of the last string read
l = lpStringIds[ ( nGoodStrings + x ) - 1 ];
if( VLB_CALLBACK( lpVLB->ListBoxId, VCB_NEXT,
&l, StringBuffer ) )
{
StringBuffer[ lpVLB->DisplayChars ] = 0;// Null Terminate
lstrcpy( lpString, StringBuffer ); // copy buf to str
Rpad( lpString, lpVLB->DisplayChars ); // pad with spaces
lpString += lpVLB->DisplayChars + 1; // go to next str
lpStringIds[ nGoodStrings + x ] = l; // save StringId
}
else
break;
}
}
InvalidateRect( hCtl, NULL, TRUE ); // Force WM_PAINT message
// set new scroll bar thumb postion
SetScrollPos( hCtl, SB_VERT, (int)lpVLB->FirstDisplayString,TRUE);
GlobalUnlock( lpVLB->hDisplayBuffer );
GlobalUnlock( lpVLB->hStringIds );
GlobalUnlock( hVLB );
return( TRUE );
}
//*******************************************************************
VOID FAR PASCAL SetSelectedString( HWND hCtl, WORD wParam,
LPVLB lpVLB )
{
LONG FAR *lpStringIds;
LONG FAR *lpSelectedStringId;
lpStringIds = (LONG FAR *)GlobalLock( lpVLB->hStringIds );
if( lpVLB->MultiSelection )
{
LONG FAR *lpSelectedStringIds;
if( ! HIBYTE( GetKeyState( VK_SHIFT ) ) )
lpVLB->TotalSelectedStrings = 0;
else
{
if(lpVLB->TotalSelectedStrings = = lpVLB->MaxSelectedStrings)
{
lpVLB->MaxSelectedStrings + = lpVLB->DisplayStrings;
if( ! ( lpVLB->hSelectedStringIds =
GlobalReAlloc( lpVLB->hSelectedStringIds,
lpVLB->MaxSelectedStrings * sizeof( LONG ), GHND ) ) )
{
lpVLB->MaxSelectedStrings -= lpVLB->DisplayStrings;
SendMessage( GetParent( hCtl ), WM_COMMAND,
lpVLB->ListBoxId, MAKELONG( hCtl, VLBN_ERRSPACE ) );
return;
}
}
}
lpSelectedStringIds = (LONG FAR *)
GlobalLock( lpVLB->hSelectedStringIds );
lpSelectedStringId =
lpSelectedStringIds + lpVLB->TotalSelectedStrings++;
}
else
{
lpVLB->TotalSelectedStrings = 1;
lpSelectedStringId = &lpVLB->SelectedStringId;
}
switch( wParam )
{
case NULL: // mouse left button click
case ' ': // space bar select focus string
case VK_DOWN:
case VK_UP:
*lpSelectedStringId =
lpStringIds[lpVLB->FocusString - lpVLB->FirstDisplayString];
break;
case VK_NEXT:
case VK_END:
*lpSelectedStringId = lpStringIds[lpVLB->DisplayStrings-1];
break;
case VK_PRIOR:
case VK_HOME:
*lpSelectedStringId = lpStringIds[0];
break;
}
SendMessage( GetParent( hCtl ), WM_COMMAND, lpVLB->ListBoxId,
MAKELONG( hCtl, VLBN_SELCHANGE ) );
if( lpVLB->MultiSelection )
GlobalUnlock( lpVLB->hSelectedStringIds );
GlobalUnlock( lpVLB->hStringIds );
}
//*******************************************************************
VOID FAR PASCAL SetFocusString( WORD wParam, LPVLB lpVLB )
{
switch( wParam )
{
case VK_DOWN:
case VK_UP:
if( lpVLB->FocusString < lpVLB->FirstDisplayString )
lpVLB->FocusString = lpVLB->FirstDisplayString;
else if( lpVLB->FocusString >
lpVLB->FirstDisplayString +lpVLB->DisplayStrings - 1 )
lpVLB->FocusString =
lpVLB->FirstDisplayString + lpVLB->DisplayStrings - 1;
else
{
if( wParam = = VK_DOWN )
{
if( lpVLB->FocusString < lpVLB->TotalStrings - 1 )
lpVLB->FocusString++;
}
else
if( lpVLB->FocusString )
lpVLB->FocusString--;
}
break;
case VK_NEXT:
lpVLB->FocusString =
lpVLB->FirstDisplayString + lpVLB->DisplayStrings - 1;
break;
case VK_PRIOR:
lpVLB->FocusString = lpVLB->FirstDisplayString;
break;
case VK_HOME:
lpVLB->FocusString = 0;
break;
case VK_END:
lpVLB->FocusString = lpVLB->TotalStrings - 1;
break;
}
}
//*******************************************************************
VOID FAR PASCAL PaintVLB( HWND hCtl, LPVLB lpVLB )
{
int x, y, first, last;
HDC hDC;
PAINTSTRUCT ps;
LPSTR lpString;
RECT Rect;
LONG FAR * lpStringIds;
// let's get the display string buffer and each string's Id
lpString = (LPSTR)GlobalLock( lpVLB->hDisplayBuffer );
lpStringIds = (LONG FAR *)GlobalLock( lpVLB->hStringIds );
hDC = BeginPaint( hCtl, &ps );
GetVLBColors( hCtl, hDC );
first = 0;
last = lpVLB->DisplayStrings - 1;
for( x = first, y = 1; x <= last; x++, y + = lpVLB->CharHeight )
{
if( lpStringIds[x] >= 0 )
{
TextOut ( hDC, 1, y, lpString, lpVLB->DisplayChars );
lpString += lpVLB->DisplayChars + 1;
}
else
{
lmemset( lpString, ' ', lpVLB->DisplayChars );
TextOut ( hDC, 1, y, lpString, lpVLB->DisplayChars );
}
}
EndPaint( hCtl, &ps );
if( lpVLB->TotalSelectedStrings )
InvertSelectedStrings( hCtl, lpVLB, -1 );
if( hCtl == GetFocus() )
FrameFocusString( hCtl, lpVLB, TRUE );
lpVLB->ScrollWindow = FALSE;
GlobalUnlock( lpVLB->hDisplayBuffer );
GlobalUnlock( lpVLB->hStringIds );
}
//*******************************************************************
VOID FAR PASCAL GetVLBColors( HWND hCtl, HANDLE hDC )
{
HANDLE hBrush;
SetBkColor( hDC, GetSysColor( COLOR_WINDOW ) );
SetTextColor( hDC, GetSysColor( COLOR_WINDOWTEXT ) );
if( hBrush = (HANDLE)SendMessage( GetParent( hCtl ), WM_CTLCOLOR,
hDC, MAKELONG( hCtl, CTLCOLOR_LISTBOX ) ) )
SelectObject( hDC, hBrush );
}
//*** END OF VLB.C **************************************************
Figure 3 VLB Information Structure
typedef struct
{
WORD ListboxId; // list box control ID
BOOL MultiSelection; // single or multi selection
RECT ClientRect; // client area rectangle
WORD CharWidth; // character width
WORD CharHeight; // character height
BYTE DisplayStrings; // maximum number of displayed strings
BYTE DisplayChars; // maximum chars in displayed strings
LONG FocusString; // string position of focus frame
LONG TotalStrings; // number of virtual strings
LONG TotalWidth; // number of virtual chars per string
LONG FirstDisplayString; // number of first displayed string
LONG TotalSelectedStrings; // number of selected strings
LONG MaxSelectedStrings; // maximum number of selected strings
VLBPROC CallBack; // VLB Callback function
HANDLE hDisplayBuffer; // handle to buffer of display strings
HANDLE hStringIds; // handle to array - display StringIDs
HANDLE hSelectedStringIds; // handle to array of select StringIDs
LONG SelectedStringId; // single selection selected StringID
int ScrollWindow; // scroll lines for ScrollWindow()
} VLB, FAR *LPVLB; // 62 bytes
Figure 4 DEMO Source Code and Build Files
DEMO
#
# demo make file
#
COM = cl -c -AM -Gsw -Oas -Zlpe -W2 $*.c
CVCOM = cl -c -AM -Gsw -Od -Zilpe -W2 $*.c
LNK = link4 demo vlb /align:16,,, lmem mlibw mlibcew, demo.def
CVLNK = link4 demo vlb /align:16,,, lmem mlibw mlibcew, demo.def /CO
ASM = MASM $*.ASM;
LIB = lib lmem -+ $*;
.c.obj:
$(COM)
.asm.obj:
$(ASM)
$(LIB)
demo.res: demo.dlg demo.rc demo.h
rc -r demo.rc
demo.obj: demo.c demo.h vlb.h
vlb.obj: vlb.c vlb.h
lmemmove.obj: lmemmove.asm setup.h
lmemset.obj: lmemset.asm setup.h
demo.exe: demo.obj demo.res vlb.obj
$(LNK)
rc demo.res
DEMO. RC
#include <windows.h>
#include "demo.h"
demo MENU
BEGIN
MENUITEM "Dialog...", IDM_DIALOG
END
rcinclude demo.dlg
DEMO.DEF
NAME demo
STUB 'WINSTUB.EXE'
CODE MOVEABLE
DATA MOVEABLE MULTIPLE
HEAPSIZE 1024
STACKSIZE 4096
EXPORTS
VLBPROC @1
DLGPROC @2
MAINWNDPROC @3
DEMO.H
/*
** demo.h header file for demo of Virtual List Box
**
** Author: Robert A. Wood
** Executive Micro Systems
** 1716 Azurite Trail
** Plano, TX 75075
**
*/
#define IDD_LISTBOX 100
#define IDD_TEXT 101
#define IDM_DIALOG 100
BOOL MainInit( HANDLE );
LONG FAR PASCAL MainWndProc( HWND, unsigned, WORD, LONG );
BOOL FAR PASCAL DlgProc( HWND, unsigned, WORD, LONG );
LONG FAR PASCAL VLBfile( WORD, unsigned, LONG FAR *, LPSTR );
DEMO.DLG
DEMO DIALOG LOADONCALL MOVEABLE DISCARDABLE 27, 28, 107, 110
CAPTION " Virtual List Box "
STYLE WS_BORDER | WS_CAPTION | WS_DLGFRAME | WS_POPUP
BEGIN
CONTROL "", 102, "static", SS_BLACKFRAME | WS_CHILD,
10, 20, 84, 57
CONTROL "", 100, "edit", WS_BORDER | WS_VSCROLL |
WS_HSCROLL | WS_TABSTOP | WS_CHILD, 10, 20, 84, 57
CONTROL "OK", 1, "button", BS_DEFPUSHBUTTON | WS_TABSTOP |
WS_CHILD, 11, 90, 32, 14
CONTROL "Cancel", 2, "button", BS_PUSHBUTTON | WS_TABSTOP |
WS_CHILD, 56, 90, 32, 14
CONTROL "Selected String", 101, "static", SS_LEFT | WS_CHILD,
10, 6, 80, 8
END
DEMO.C
/*
** demo.c demonstration of VLB (virtual list box)
**
** Author: Robert A. Wood
**
** Microsoft C version 5.1 / medium memory model
** Microsoft Windows SDK version 2.1
** Runtime: Windows 286 version 2.1
**
*/
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <io.h>
#include <fcntl.h>
#include "demo.h"
#include "vlb.h"
#include "lmem.h"
HWND hWnd;
HANDLE hInst;
char szAppName[] = "Demo";
WORD FAR PASCAL WinMain( HANDLE hInstance, HANDLE hPrevInstance,
LPSTR lpCmdLine, WORD nCmdShow )
{
MSG msg;
if( ! hPrevInstance )
MainInit( hInstance );
hWnd = CreateWindow( szAppName, // window class
szAppName, // window name
WS_OVERLAPPEDWINDOW, // window style
0, // x position
0, // y position
CW_USEDEFAULT, // width
0, // height
NULL, // parent handle
NULL, // menu or child ID
hInstance, // instance
NULL); // additional info
hInst = hInstance;
ShowWindow( hWnd, nCmdShow );
UpdateWindow( hWnd );
while( GetMessage( &msg, NULL, 0, 0 ) )
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}
return( msg.wParam );
}
/*
** Initializes window data and registers window class
********************************************************************/
BOOL MainInit( HANDLE hInstance )
{
WNDCLASS Class;
Class.hCursor = LoadCursor(NULL,IDC_ARROW);
Class.hIcon = NULL;
Class.cbClsExtra = 0;
Class.cbWndExtra = 0;
Class.lpszMenuName = szAppName;
Class.lpszClassName = szAppName;
Class.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);;
Class.hInstance = hInstance;
Class.style = CS_VREDRAW | CS_HREDRAW;
Class.lpfnWndProc = MainWndProc;
Class.style = NULL;
return( RegisterClass( &Class ) );
}
/*
** Main Window Procedure
********************************************************************/
LONG FAR PASCAL MainWndProc( HWND hWnd, unsigned message,
WORD wParam, LONG lParam )
{
FARPROC lpfnDlgProc;
switch( message )
{
case WM_COMMAND:
if( wParam = = IDM_DIALOG )
{
lpfnDlgProc = MakeProcInstance( DlgProc, hInst );
DialogBox( hInst, "DEMO", hWnd, lpfnDlgProc );
FreeProcInstance(lpfnDlgProc);
}
break;
case WM_DESTROY:
PostQuitMessage( FALSE );
break;
default:
return( DefWindowProc( hWnd, message, wParam, lParam ) );
}
return( FALSE );
}
/*
** Dialog Procedure
********************************************************************/
BOOL FAR PASCAL DlgProc( HWND hDlg, unsigned message,
WORD wParam, LONG lParam )
{
switch ( message )
{
case WM_INITDIALOG:
InitVLB( hInst, hDlg, IDD_LISTBOX, VLBfile );
break;
case WM_COMMAND:
if ( wParam = = IDOK )
EndDialog( hDlg, TRUE );
if ( wParam = = IDCANCEL )
EndDialog( hDlg, FALSE );
if( wParam = = IDD_LISTBOX )
{
switch( HIWORD( lParam ) )
{
case VLBN_DBLCLK:
case VLBN_SELCHANGE:
SendMessage( GetDlgItem( hDlg, IDD_TEXT ),
WM_SETTEXT, 0, SendMessage( LOWORD( lParam ),
VLB_GETSELSTR, 0, 0L ) );
break;
}
}
break;
default:
return( FALSE );
}
return( TRUE );
}
/*
** demo VLB CallBack function for an ASCII file
********************************************************************/
LONG FAR PASCAL VLBfile( WORD wListBoxId, // control id
unsigned message, // message
LONG FAR *lplStringID, // string ID
LPSTR lpszString ) // string
{
static int hFile = 0; // file handle
static LONG TotalStrings = 0; // total records in file
static Tabs[] = { 0, 9, 15, 23, 33 }; // tabs for pageleft & right
static int TotalTabs = 5; // total tabs
static int Column = 0; // 1st col of display string
static int StrLength = 40; // total string length
static int RecLength = 42; // total record len (CR/LF)
static char filename[] = "vlb.txt"; // VLB filename
char buf[60]; // temp buffer to read to
switch( message )
{
case VCB_LENGTH:
return( TotalStrings );
case VCB_WIDTH:
return( Tabs[ TotalTabs-1 ] );
case VCB_CREATE:
{
long length;
if( ( hFile = open( filename, O_RDONLY ) ) = = NULL )
return( FALSE );
length = lseek( hFile, 0L, SEEK_END );
lseek( hFile, 0L, SEEK_SET );
TotalStrings = length / RecLength;
Column = 0; // 1st column of displayed string
return( TRUE );
}
case VCB_SETFOCUS:
case VCB_KILLFOCUS:
return( TRUE );
case VCB_DESTROY:
close( hFile );
return( TRUE );
case VCB_NEXT:
if( *lplStringID >= TotalStrings - 1 )
return( FALSE );
(*lplStringID)++;
break;
case VCB_PREV:
if( *lplStringID <= 0 )
return( FALSE );
(*lplStringID)--;
break;
case VCB_FIRST:
*lplStringID = 0;
break;
case VCB_LAST:
*lplStringID = TotalStrings-1;
break;
case VCB_VTHUMB:
case VCB_FULLSTRING:
case VCB_STRING:
*lplStringID = (LONG)min( (int)(TotalStrings-1),
max( 0, (int)*lplStringID ) );
break;
case VCB_LEFT: // move string start one char left
return( Column = max( 0, Column - 1 ) );
case VCB_RIGHT: // move string start one char right
return( Column = min( Tabs[ TotalTabs-1 ], Column + 1 ) );
case VCB_PAGELEFT: // move string start one column left
{
int x;
if( Column <= 0 )
return( Column = Tabs[0] );
for( x = 0; Column > Tabs[x]; x++ )
;
Column = Tabs[x-1];
return( Column = max( 0, Column ) );
}
case VCB_PAGERIGHT: // move string start one column right
{
int x;
if( Column >= Tabs[ TotalTabs - 1 ] )
return( Column = Tabs[ TotalTabs - 1 ] );
for( x = 0; Column >= Tabs[x]; x++ )
;
Column = Tabs[x];
return( Column = min( Tabs[ TotalTabs - 1 ], Column ) );
}
case VCB_HTHUMB: // move string start specified by thumb
return( Column = min( StrLength-1,
max( 0, (int)*lplStringID ) ) );
default:
return( FALSE );
}
if( message = = VCB_NEXT || message = = VCB_PREV ||
message = = VCB_VTHUMB || message = = VCB_STRING ||
message = = VCB_FIRST || message = = VCB_LAST ||
message = = VCB_FULLSTRING )
{
if( hFile < 0 ) // file open failed
return( FALSE );
lseek( hFile, *lplStringID * RecLength, SEEK_SET );
read( hFile, buf, RecLength );
if( message = = VCB_FULLSTRING )
{
lmemmove( lpszString, buf, StrLength );
*( lpszString + StrLength ) = 0;
}
else
{
lmemmove( lpszString, buf + Column, StrLength - Column );
*( lpszString + StrLength - Column ) = 0;
}
}
return( TRUE );
}
SETUP.H
;
; setup.h header file for C utility functions
;
_DATA SEGMENT WORD PUBLIC 'DATA'
_DATA ENDS
CONST SEGMENT WORD PUBLIC 'CONST'
CONST ENDS
_BSS SEGMENT WORD PUBLIC 'BSS'
_BSS ENDS
DGROUP GROUP CONST, _BSS, _DATA
ASSUME DS: DGROUP, SS: DGROUP
ENTRY MACRO X
push ds
pop ax
nop
inc bp
push bp
mov bp, sp
push ds
mov ds, ax
ASSUME DS:NOTHING
sub sp, X
push es
push si
push di
ENDM
EXIT MACRO X ; X should always be even -
; can't pass a single byte.
; X is used for PASCAL type stack clean-up.
pop di
pop si
pop es
sub bp, 2
mov sp, bp
pop ds
ASSUME DS:DGROUP
pop bp
dec bp
ret X
ENDM
LMEM.H
/*
** lmem.h header file for far memory functions
*/
VOID FAR PASCAL lmemmove( VOID FAR * lpDest,
VOID FAR * lpSrc,
WORD wCount );
VOID FAR PASCAL lmemset( VOID FAR * lpDest,
int Char,
WORD wCount );
LMEMMOVE.ASM
;
; lmemmove.asm far version of memmove
;
INCLUDE SETUP.H
CLIB SEGMENT WORD PUBLIC 'CODE'
CLIB ENDS
CLIB SEGMENT
ASSUME CS: CLIB
PUBLIC lmemmove
lmemmove PROC FAR
Destination EQU DWORD PTR [bp] + 12
Source EQU DWORD PTR [bp] + 8
Count EQU WORD PTR [bp] + 6
ENTRY 0
lds si, Source ; DS:SI = Source
les di, Destination ; ES:DI = Destination
mov ax, di ; Dest. in AX for return
mov cx, Count ; cx = number bytes to move
jcxz done ; if cx = 0, nothing to copy
;
; Check for overlapping buffers:
; If segments are different, assume no overlap
; Do normal (Upwards) Copy
; Else If (Dest. <= Source) Or (Dest.>= Source + Count) Then
; Do normal (Upwards) Copy
; Else
; Do Downwards Copy to avoid propogation
;
mov ax, es ; compare the segments
cmp ax, WORD PTR (Source+2)
jne CopyUp
cmp di, si ; Source <= Destination ?
jbe CopyUp
mov ax, si
add ax, cx
cmp di, ax ; Dest. >= (Source + Count) ?
jae CopyUp
;
; Copy Down to avoid propagation in overlapping buffers
;
mov ax, di ; AX = return value (offset)
add si, cx
add di, cx
dec si ; DS:SI = Source + Count - 1
dec di ; ES:DI = Dest. + Count - 1
std ; Set Direction Flag = Down
rep movsb
cld ; Set Direction Flag = Up
jmp short done
CopyUp:
mov ax, di ; AX = return value (offset)
;
; There are 4 types of word alignment of "Source" and "Destination":
; 1. Source and Destination are both even (best case)
; 2. Source is even and Destination is odd
; 3. Source is odd and Destination is even
; 4. Source and Destination are both odd (worst case)
;
; Case #4 is much faster if a single byte is copied before the
; REP MOVSW instruction. Cases #2 and #3 are effectively unaffected
; by such an operation. To maximum the speed of this operation,
; only DST is checked for alignment. For cases #2 and #4, the first
; byte will be copied before the REP MOVSW.
;
test al, 1 ; fast check for Dest. odd address
jz move
movsb ; move a byte to improve alignment
dec cx
;
; Now the bulk of the copy is done using REP MOVSW. This is much
; faster than a REP MOVSB if the Source and Dest. addresses are both
; word aligned and the processor has a 16-bit bus. Depending on
; the initial alignment and the size of the region moved, there
; may be an extra byte left over to be moved. This is handled
; by the REP MOVSB, which moves either 0 or 1 bytes.
;
move:
shr cx, 1 ; Shift CX for Count of words
rep movsw ; CF set if 1 byte left over
adc cx, cx ; CX = 1 or 0, - Carry Flag
rep movsb ; possible final byte
;
; Return the "Destination" address in AX/DX:AX
;
done:
EXIT 10
lmemmove ENDP
CLIB ENDS
END
LMEMSET.ASM
;
; lmemset far version of memset
;
INCLUDE SETUP.H
CLIB SEGMENT WORD PUBLIC 'CODE'
CLIB ENDS
CLIB SEGMENT
ASSUME CS: CLIB
PUBLIC lmemset
lmemset PROC FAR
Destination EQU DWORD PTR [bp] + 10
FillChar EQU BYTE PTR [bp] + 8
Count EQU WORD PTR [bp] + 6
ENTRY 0
les di, Destination ; ES:DI = Destination
mov bx, di ; save a copy of DST
mov cx, Count
jcxz toend ; if no work to do
mov al, FillChar ; the byte FillChar to store
mov ah, al ; store it as a word
test di, 1 ; is Destination address odd?
jz dowords ; yes: proceed
stosb ; store byte for word align
dec cx
dowords:
shr cx, 1
rep stosw ; store word at a time
adc cx, cx
rep stosb ; store final ("odd") byte
toend:
mov di, dx ; Restore DI
xchg ax, bx ; AX = Destination
mov dx, es ; segment part of addr
EXIT 8
lmemset ENDP
CLIB ENDS
END
Figure 5 VLB.TXT
1059A HEX 5707 1-12-89 3:42p
ABC TXT 42 7-01-88 12:00a
ATRM1111 FNT 5966 7-26-88 12:00a
B PIF 369 6-08-89 11:03a
CALC EXE 28000 7-01-88 12:00a
CALENDAR EXE 38896 7-01-88 12:00a
CARDFILE EXE 39264 7-01-88 12:00a
CLIPBRD EXE 10800 7-01-88 12:00a
CLOCK EXE 8960 7-01-88 12:00a
COMMAND PIF 369 6-08-89 11:42a
CONTROL EXE 58064 7-01-88 12:00a
COURC FON 13040 9-07-88 12:00a
COURD FON 21328 9-07-88 12:00a
COURE FON 23808 9-07-88 12:00a
CTRN PIF 369 9-19-89 1:04p
CVTPAINT EXE 5712 7-01-88 12:00a
DIALOG EXE 56864 7-26-88 12:00a
DOTHIS TXT 493 7-01-88 12:00a
EPSON9 DRV 43776 9-07-88 12:00a
FONTEDIT EXE 35536 7-26-88 12:00a
FREEMEM EXE 4223 2-01-89 9:40a
FSLPT2 PCL 4950 8-25-89 11:42a
FSOUTPUT PCL 7258 8-25-89 11:37a
GETPOS EXE 3328 11-17-88 11:09p
HEAPWALK EXE 30624 7-26-88 12:00a
HELVC FON 38960 9-07-88 12:00a
HELVD FON 58144 9-07-88 12:00a
HELVE FON 64784 9-07-88 12:00a
HOOK EXE 3077 7-26-88 12:00a
HPPCL DRV 212096 9-07-88 12:00a
HWG00 TXT 8023 4-13-89 10:04a
ICONEDIT EXE 37184 7-26-88 12:00a
ICONMENU EXE 18272 8-29-89 11:26a
KLU ICO 1038 1-31-89 2:44p
KLU PIF 369 1-18-89 12:25p
MAPMEM PIF 369 2-14-89 8:59a
MEMSET EXE 36099 9-07-88 12:00a
MENU HEX 1232 1-05-89 10:04a
MODERN FON 7584 9-07-88 12:00a
MSDOS ZAP 1 11-08-88 9:16a
NOTEPAD EXE 19072 7-01-88 12:00a
PAINT EXE 93280 7-01-88 12:00a
PC3270 PIF 369 2-14-89 8:54a
PIFEDIT EXE 30288 7-01-88 12:00a
PRACTICE WRI 2944 7-01-88 12:00a
QD PIF 369 6-08-89 11:08a
QUIT EXE 4368 8-27-89 6:50a
READCVW TXT 15566 7-26-88 12:00a
README TXT 16383 7-01-88 12:00a
README WRI 65280 7-26-88 12:00a
READMEE9 TXT 1228 9-07-88 12:00a
REVERSI EXE 15552 7-01-88 12:00a
ROMAN FON 11120 9-07-88 12:00a
SCRIPT FON 10304 9-07-88 12:00a
SETPOS EXE 3216 11-17-88 11:10p
SETUP EXE 68779 9-07-88 12:00a
SHAKER EXE 8864 7-26-88 12:00a
SNAP EXE 8048 7-26-88 12:00a
SPIT RC 559 1-05-89 12:19p
SPOOLER EXE 14336 7-01-88 12:00a
SPY EXE 20160 7-26-88 12:00a
STATE RST 199 12-19-89 12:59p
TERMINAL EXE 48640 7-01-88 12:00a
TMSRC FON 37824 9-07-88 12:00a
TMSRD FON 57184 9-07-88 12:00a
TMSRE FON 58304 9-07-88 12:00a
TT PIF 369 12-21-88 7:58a
TTY DRV 6224 9-07-88 12:00a
WFINDER EXE 13072 6-22-89 5:36p
WIN CLR 3798 3-01-89 4:51p
WIN COM 5489 11-08-88 9:14a
WIN INI 4501 3-05-90 10:52a
WIN OLD 2496 11-07-88 2:06p
WIN SAV 3786 3-01-89 11:38a
WIN200 BIN 231520 11-08-88 9:14a
WIN200 OVL 259984 11-08-88 9:14a
WIN87EM EXE 11459 2-22-89 3:37p
WIN87EM OLD 11331 7-26-88 12:00a
WINOLDAP GRB 3574 11-08-88 9:16a
WINOLDAP MOD 60464 7-01-88 12:00a
WINSTUB EXE 570 7-26-88 12:00a
WRITE EXE 198368 7-01-88 12:00a
Using Object-Oriented Methodologies in Windows Applications
Kevin P. Welch
Even a casual observer of the software development community would have to
agree that object-oriented programming has been receiving a great deal of
attention lately from both programmers and the press. Object-oriented
techniques can simplify programming for modern complex systems and enable us
to create and utilize reusable software objects. This article shows you how
to use certain object-oriented methodologies when developing applications
for the Microsoft(R) Windows graphical environment.
Fundamental to most object-oriented programming is the concept of a software
object or class. Three principles describe how objects relate to each other:
encapsulation, inheritance, and dynamic binding.
Window Classes
As you probably know, almost everything of visual importance in Windows1 is
created using a window class. These classes, which are either defined by
Windows or formally registered with the system by your program, serve as the
framework of applications. You can associate methods with each class by
supporting a set of system and private messages or defining a library of
class-specific functions. (A method is a set of actions or functions,
associated with a particular class, that performs tasks.)
Before an application can create and use a window, it must define and
register a class or template. Classes are used to define systemwide software
objects. When a window is created, the system uses this class information
and the CreateWindow parameters to define a new window structure. This
structure contains additional information, specific to each window instance,
that is used and managed by the system until the window is destroyed.
The elements of a window class largely define the default behavior of those
windows created from the base template. Your application can create a new
window class by initializing the following WNDCLASS data structure and
passing it as a parameter to the RegisterClass function:
typedef struct tagWNDCLASS {
WORD style;
LONG (FAR PASCAL * lpfnWndProc)();
int cbClsExtra;
int cbWndExtra;
HANDLE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPSTR lpszMenuName;
LPSTR lpszClassName;
} WNDCLASS;
The values you define when initializing this structure enable you to specify
how the system handles the class; the function responsible for processing
all messages relating to the class; the amount of extra information
associated with each class and window instance; the module supporting the
class; the class icon, cursor, background color and menu; and the class
name.
The class name field is particularly important because it uniquely
identifies the window. However, class names can easily conflict--in
Windows Version 2.1 all window class names are publicly defined. One
solution is to prefix each window class name with the application name or
module instance handle. This should make the class name unique.
In some situations (especially when defining control classes), you may want
to define a window class to be used by several applications. To do this,
choose a class name that does not conflict with one of the predefined
Windows classes: edit, static button, list box, and scrollbar.
Encapsulation
Encapsulation is a technique in which data is automatically associated with
each instance of an object or the class that defines that object. In
traditional object-oriented programming environments such as Smalltalk or
Actor(R), encapsulation is supported by the system so that little additional
development effort is required. Encapsulation facilitates the creation of
software objects that can be easily reused. For example, an edit field in a
dialog box is a reusable software object defined by the system that uses
encapsulation to separate data between various instances. Although they are
less transparent, three Windows techniques provide some degree of
encapsulation.
The first technique is the use of extra class words. When you register a new
window class using the RegisterClass function, Windows creates a new
internal class data structure based on the information in the WNDCLASS data
structure.
By specifying a value for the cbClsExtra field you can instruct Windows to
allocate space for a small amount of additional information (say 8 to 16
bytes) at the end of the class data structure. The information stored in
this area is shared among all instances of the window class.
When the class has been registered, the template will remain active until
the responsible application termi-nates. When this occurs, Windows will
automatically unregister all associated class definitions. Unfortunately,
this is only true for applications: for window classes registered inside
dynamic-link libraries, the class will remain permanently defined until the
Windows session is ended. Additionally, multiple instances of the same
application should be careful not to reregister the same class, and
different applications should prefix their class names with their module
name to avoid name conflicts.
Once a window class has been registered, the only way to set and retrieve
information relating to the class data structure, including encapsulated
data, is to use the functions in Figure 1. Unfortunately, because each
function requires a valid window handle, it is not possible to query the
system about a class before an instance has been created. These functions
require that you specify an offset (defined in WINDOWS.H) to access specific
elements of the class data structure (see Figure 2).
Note that each of the offsets is negative in value. This is because the
internal pointer used by Windows to reference the class data structure
points to the beginning of the extra class data. This enables you to define
a set of positive offsets to use when referencing this data.
Also note that if you change one of the values referenced by the predefined
negative offsets, the results may never appear in those windows based on the
class. For example, if you change the icon for a particular class, the
changes will not become apparent until one of the windows is minimized. All
other existing minimized windows based on that class will continue to
display the old icon until a display update occurs.
For example, if you had allocated space for 4 extra bytes at the end of the
class data structure when it was registered, you could access the
information like this:
/* class data definitions */
#define CLASS_WORD1 0
#define CLASS_WORD2 2
/* code to set class data */
SetClassWord( hWnd, CLASS_WORD1, X);
SetClassWord( hWnd, CLASS_WORD2, Y);
/* code to retrieve class data */
X = GetClassWord( hWnd, CLASS_WORD1 );
Y = GetClassWord( hWnd, CLASS_WORD2 );
Any changes you make to a class data structure will at some time affect all
the windows based on that class. Because of this you cannot use the
GCW_HCURSOR offset to indicate a special edit or selection mode in your
window when multiple instances are present. If you do use this technique,
the same cursor will appear in all instances of the window class, regardless
of their current editing or selection mode.
There are many uses for extra class data, ranging from the storage of simple
variables to handles to GDI objects and global data blocks. One that I have
found particularly effective is the storage of font handles that are shared
between different instances of the same window class. There is one important
limitation, however: because these fonts are not managed by the system, the
host application must release any allocated resources (in this case, fonts)
when the last instance of the program terminates.
The second encapsulation technique is using extra window words. Whenever you
create a new window with a CreateWindow function call, you are in effect
defining a new structure that is associated with that particular window
instance. Although not described in WINDOWS.H, this structure contains
additional information relating to the particular window, beyond that
maintained by the class structure:
typedef struct tagWNDSTRUCT {
DWORD dwStyle;
WORD wId;
HANDLE hszText;
HWND hWndParent;
HANDLE hInstance;
LONG (FAR PASCAL * lpfnWndProc)();
} WNDSTRUCT;
By specifying a value for the cbWndExtra field (when registering the window
class) you can instruct Windows to allocate space for a small amount of
additional information (8 to 16 bytes) at the end of each window data
structure. Like the additional space you can allocate with the class
structure, you can use this area to store data. The difference is that extra
window words are associated only with a specific window instance and are not
shared by the entire class.
Once a window has been created, the only way to set and retrieve information
relating to the window structure is to use the set of functions in Figure 3.
Like their class structure counterparts, these functions require that you
provide a valid window handle and specify an offset (defined in WINDOWS.H)
to access specific elements of the window data structure (see Figure 4).
Again, note that each of the preceding offsets is negative in value, like
the class offsets mentioned previously. Using the same mechanism used for
class offsets, you can define your own set of positive offsets for use when
referencing your window-specific data.
The third and final technique that supports data encapsulation is the use of
property lists. Although somewhat underdocumented in the Windows Software
Development Kit, property lists represent one of the more powerful
mechanisms provided by Windows for data encapsulation. Property lists
facilitate the association of a named block of data with a particular window
handle. To support this, Windows defines the set of functions found in
Figure 5.
You can use property lists to store any double-byte numeric value, including
handles to global and local data blocks. For example, if you wished to use
property lists to store, access, and remove a simple numeric value, you
would use the following series of function calls:
/* define a property */
SetProp( hWnd, "Index", wIndex );
/* access a property */
wIndex = GetProp( hWnd, "Index" );
/* remove a property */
RemoveProp( hWnd, "Index" );
Also, because property lists are maintained by the system, any application
can access a particular window's property list. But be careful--only
simple numeric data values or handles to global or system objects are
shareable. Local memory handles cannot be shared.
Another issue is that a window's property list, like other window-related
data, belongs to Windows and is allocated from the user heap (the local heap
of the user library). Although there is no specified limit to the number of
entries in a property list, the actual maximum is dependent on the amount of
space available in the user heap. Because of this uncertainty, you should
define as few properties as possible.
One often-overlooked issue is the removal of properties. When a window is
destroyed, the window must remove the property and any data associated with
each property, because Windows does not automatically remove them.
Inheritance
Generally, inheritance is characterized as a mechanism whereby one software
object can assume, extend, or replace all or part of the characteristics and
function of another object. In a traditional object-oriented programming
environment, inheritance is supported by the enforcement of a strict set of
rules that specify how objects are named (classes), what they do (methods),
and how they are related (class hierarchy).
Windows significantly differs from most object-oriented environments in that
mechanisms by which named objects may be related are not strictly enforced,
that these mechanisms usually apply only to visual objects, and that the
programmer is responsible for using these mechanisms consistently.
When you define a window class, you are in effect formally defining a static
class hierarchy based on a preexisting set of window classes. A particular
window can be a derivative of some other window class or combine several
classes into a more integrated one. This hierarchy should not be confused
with the parent-child relationship that specifies the operational
interaction between windows.
Although this area is perhaps the most weakly developed of the
object-oriented mechanisms supported by Windows, two techniques exist that
roughly approximate the general concept of inheritance.
The first and most common of these techniques is subclassing. Subclassing
involves replacing the message processing function responsible for a
particular window with a new one. Using this technique you can intercept and
process particular messages, passing others to the original window function.
The following code fragment demonstrates how subclassing can be activated
and deactivated with a new window message processing function in an
application:
/* activate subclassing */
lpWndfn = MakeProcInstance((FARPROC)NewWndFn, hInstance);
lpWndOldfn = SetWindowLong( hWnd, GWL_WNDPROC, lpWndfn );
/* the new window function now is responsible for processing messages until
removed - it can pass these messages if necessary to the old window function
for default handling */
/* deactivate subclassing */
SetWindowLong( hWnd, GWL_WNDPROC, lpWndOldfn );
FreeProcInstance(lpWndfn);
Subclassing is most appropriate when you want to restrict the functionality
of a single window instance. For example, if you wanted to define a
specialized edit control that accepted only numeric input, you could create
a standard edit control and intercept all non-numeric characters. The result
would be a particular instance of the predefined edit window class with
slightly altered functionality.
Although subclassing is a powerful technique, it has some disadvantages. The
first disadvantage is that you must subclass each window instance
individually, possibly making your application unnecessarily complicated.
Second, unless you have access to the source code for the base window class,
it can be extremely difficult to use subclassing to extend the existing
functionality. If you attempt to do this, your new window message processing
function often tends to replicate much of the code supporting the base
class. Third, it is possible to subclass a window at the class level by
replacing the default window function in the class data structure. Since the
change will affect only newly created window instances based on the class,
you should avoid doing this.
The second and less used subclassing technique is something I call
"superclassing." Superclassing involves the definition of a new window class
that utilizes one or more preexisting or application-defined classes.
Returning to the numeric edit control example, you could use superclassing
to define a new window class (called, say, EditNumeric) that utilizes a
single child window of class edit. Whenever an instance of the EditNumeric
class is created, it creates a standard edit control in its place and
automatically subclasses it to provide the desired functionality. The
EditNumeric class could be used whenever a numeric data entry field was
required, ignoring the way it was actually implemented.
In more complicated situations, you can use superclassing to define
extremely complex window classes that are based on several predefined or
preexisting classes. You will probably need to create your own set of
predefined messages that enable other windows to interact efficiently with
this new object.
Although the concept of superclassing is simple, with some effort you can
create an entire application using layers of reusable objects with only a C
compiler and the Windows Software Development Kit. Also, if the window
classes are completely message-based with carefully encapsulated data, they
could be easily extracted and integrated into other programs.
Dynamic Binding
Dynamic binding, another important characteristic of most object-oriented
environments, defines how various software objects are integrated and
related at run time. In most traditional object-oriented environments, all
objects (not just windows) are managed by some sort of internal object
management facility. With this facility a programmer can easily add new
objects to the environment and replace or remove existing ones. The changes
made are immediately put into effect and apply to all subsequent operations.
Although not so well-defined, Windows provides several mechanisms that
support this characteristic. The most important is dynamic-link libraries
(DLLs). DLLs provide a way to place software objects in reusable modules.
These modules (with their associated window classes) can be explicitly or
implicitly referenced or imported into other applications. When different
functionality is desired, the DLL can be easily replaced, with the change in
effect instantly.
With a little work, it should be relatively simple to incorporate these
object-oriented techniques into applications you are presently developing.
Great productivity gains can be achieved if you consistently apply these
techniques--despite their shortcomings and dissimilarities with a true
object-oriented programming environment. Best of all, these methodologies
can be used immediately with your existing set of development tools. My next
article will demonstrate these object-oriented Windows programming
techniques in a sample program.
Figure 1
Function Description
GetClassWord Retrieve words from the class data structure
GetClassLong Retrieve double-words from the class data structure
SetClassWord Replace words in the class data structure
SetClassLong Replace double-words in the class data structure
Figure 2
Class Elements Definition Offset
Extra Data (application defined) 0
Name (undefined) - 4
Menu Name GCL_MENUNAME - 8
Background Brush GCW_HBRBACKGROUND - 10
Cursor GCW_HCURSOR - 12
Icon GCW_HICON - 14
Module Handle GCW_HMODULE - 16
Extra Window Bytes GCW_CBWNDEXTRA - 18
Extra Class Bytes GCW_CBCLSEXTRA - 20
Window Function GCL_WNDPROC - 24
Style Flags GCW_STYLE - 26
Figure 3
Function Description
GetWindowWord Retrieve words from the window data structure
GetWindowLong Retrieve double-words from the window data structure
SetWindowWord Replace words in the window data structure
SetWindowLong Replace double-words in the window data structure
Figure 4
Window Elements Definition Offset
Extra Data (application-defined) 0
Function GWL_WNDPROC - 4
Instance GWW_HINSTANCE - 6
Parent GWW_HWNDPARENT - 8
Text GWW_HWNDTEXT - 10
ID GWW_ID - 12
Style GWL_STYLE - 16
Figure 5
SetProp Define a named handle or word
GetProp Retrieve a named handle or word
EnumProps Enumerate all window properties
RemoveProp Remove a named handle or word
Fundamental Recommendations on C Programming Style
Greg Comeau
Programming savvy is hard to develop. Transfixed in front of your editor, it
is easy to hack at one specific problem and lose sight of the task at large.
Having periodic sessions away from your editor can help you concentrate
instead on the overall design of your code and your programming style.
Attention to fundamentals up front can prevent subsequent programming and
maintenance problems. This article discusses a number of recommendations on
C programming style.
I'm not going to discuss issues like "you must indent only four spaces" or
"the opening brace of an if statement must go on the same line as the if."
While these issues are important, they're really just matters of
taste--one way is often as good as another. It's more important to
develop a consistent, intelligent style of programming, in your own code or
when programming with a team of developers.
Macros
Most C programmers already follow the first simple guideline, which is that
macro names should always be written in uppercase. This draws attention to
them in code, and should be done for both constants and functionlike macros.
There is usually no good reason to break this rule. You might notice that
compiler files such as stdio.h sometimes have getchar and other macros
written in lowercase: consider stdio.h an exception. Because getchar is
sometimes a real function and not a macro, compilers that allow these names
to be either usually have a switch to control their use. If you must
explicitly select the run-time library function, use the ugly notation
(getchar)()
Second, make sure a macro maps into what you expect it to. This is also done
with the proper use of parentheses within the macro. For instance,
#define abadmacro 1 + 1
a = b * abadmacro;
maps into
a = b * 1 + 1;
which is b + 1, not b * 2, as you probably wanted. You should have coded it
like this:
#define agoodmacro (1 + 1)
Some compilers provide command-line options that allow you to examine the
output of the preprocessor.
Given macros such as the following, do not end them with semicolons unless
you are positive of their usage.
#define AMACRO(arg) ...arg++...
#define ANOTHERMACRO(arg) (arg + arg)
You should be aware of side effects in macro invocations. For example, in
the above code, the incrementing of arg within AMACRO might not be readily
apparent. Calling ANOTHERMACRO with an argument of i++ will probably not
produce the result you wanted nor increment i the number of times you wanted
it to be incremented. Remember, functionlike macros are not real functions.
Using #define directives liberally can make your program more readable and
easier to change. Too often, I see a piece of code like the following:
int array[20];
int i;
for (i = 0; i < 20; i++)
array[i] = 0;
This has potential problems because of its haphazard use of constants. There
are a couple of ways to fix this. Using a #define directive allows you to
change the array size as well as the loop termination safely.
#define SIZEOFARRAY 20
int array[SIZEOFARRAY];
int i;
for (i = 0; i < SIZEOFARRAY; i++)
array[i] = 0;
You can use the invaluable sizeof operator to determine the size of
fundamental types, structs, and arrays. The following macro calculates the
number of elements in an array:
#define HBOUND(array) \
(sizeof(array) / sizeof(array[0]))
(A macro such as this cannot be used, however, if you've declared an array
as extern and haven't specified the array's dimension.) Using the HBOUND
macro, the loop will automatically handle arrays of varying length:
for (i = 0; i < HBOUND(array); i++)
array[i] = 0;
It's worthwhile to investigate the useful offsetof macro from stddef.h,
which determines the "absolute" offset of a structure member from the
beginning of a given structure tag. This macro lets you avoid having to
calculate and hand-code structure member offsets. Structure member offset
values are not predictable: they can even change on the same machine with
the same compiler if you vary structure packing requirements via a
command-line switch or using a #pragma compiler directive.
Macros, while useful, can also be problematic because the C preprocessor
"doesn't know" C. Strange things can easily occur. Also, transferring
constant or expression use to a macro is not always helpful. For instance
given:
#define RED 0
#define ORANGE 1
#define GREEN 2
■
■
■
#define MAUVE 147
How can I easily add YELLOW between RED and ORANGE without renumbering
everything? The solution is to use an enumeration instead of a series of
#defines:
enum colors {RED, YELLOW, ORANGE, ..., MAUVE};
Since enum is a C keyword, code such as the following is useful:
enum colors mycolors; mycolors = RED;
Unfortunately, C will also allow the following code without complaint:
mycolors = 999;
I think this is a quality of implementation issue: I would like to see at
least a compile-time warning for such situations. The ability to associate a
constant value with a name that the C compiler knows about still outweighs
this disadvantage. Because enum is now part of the C language, more
compilers are likely to issue proper debugging records.
Declarations
If your compiler accepts function prototypes, use them. They are there to
serve as documentation and to ensure various type checking and efficiency
concerns. The UNIX C compiler is perhaps one of the last compilers with a
large user base to add function prototypes: UNIX System V Release 4 (SVR4)
is now an ANSI C-compliant compiler with prototype support. To port C code
to compilers that still don't have function prototypes, utilities such as
sed are available commercially and in the public domain.
There are a few things to be aware of about prototypes. First, it is
generally unwise to mix prototypes with "old style" (K&R) code. Use either
one or the other throughout all the source files of a project. For example,
when using function prototypes, the following combination is not
recommended:
int foo(int arg1, int arg2); /* Fcn prototype */
■
■
■
int foo(arg1, arg2) /* Old-style fcn definition */
int arg1;
int arg2;
{
■
■
■
}
The function definition should instead be:
int foo(int arg1, int arg2) /* New-style definition */
{
■
■
■
}
The former combination is especially a problem when the prototype and the
definition are in two different source files. If you mix styles, you'll
eventually confuse the compiler, yourself, or other developers.
Second, including identifier names in prototype declarations is helpful
from a documentation standpoint. The identifier names serve as comments to
the function's arguments. I suggest that you prefix each identifier
appearing within prototype declarations with an underscore (note that I'm
not referring to the function definition here), because you may have
inadvertently used a #define to create an identifier with the same name. If
the defined name contains a token representation such as [ ] or ( ), the C
compiler will be happy but you'll get a bogus-looking warning or error
message where the function is called in your code. Maybe worse, the
replacement could be an acceptable type so the compiler wouldn't warn you at
all.
Third, if a declaration refers to an external function or data, add the
extern keyword to it. There are often relaxed meanings of how a C definition
looks. This implies flexibility in the way declarations and definitions are
allowed and handled both by various compilers and linkers. Maximum
portability is assured by adding extern to declarations.
This leads us to the difference between declarations and definitions. A
definition is a declaration that allocates storage for the object or
function. A declaration only serves as a reference to that storage.
Furthermore, an identifier with external or internal linkage should be
initialized in only one place--its definition. Doing otherwise is quite
confusing: an error will occur if the compiler finds two or more definitions
of an identifier, each initialized with different values. Fortunately most
linkers will warn of this situation when resolving references.
Placing more than one declaration on a line is a potentially serious
problem. For example, the following code is a declaration of a single char *
and a single char, not of two char *.
char * p, c;
You can avoid many declaration problems by typedef'ing as many functions and
data objects as possible. Remember that typedef is a part of C, so that the
points previously raised during the enum versus #define discussion generally
apply here also. Typedef statements make declarations easier to read, write,
and understand: they also help with the abstraction of data elements. You
should use systemwide and projectwide typedefs as appropriate. Also don't
waste time inventing or ignoring typedefs that already exist. At the very
least you should scan or grep your compiler's include files as well as your
project's include files to become intimate with the typedefs and other
information used within them.
You might want to consider using the static keyword more often to "hide"
names from the linker. This not only avoids name space pollution of your
compiler system's symbol table, it makes identifiers invisible to other
source files. It does prohibit reentrant routines since statics are often
used as state variables; keep this in mind if you are encapsulating the
functionality of a group of routines.
Be sure that you end all typedefs, structures, and unions with semicolons.
There is nothing worse than:
File c.h: struct {
■
■
■
}
File c.c: #include <c.h>
■
■
■
Since the struct in c.h is missing a semicolon, this error will not show up
until used within c.c, most likely one or two lines after the c.h is
included. This can confuse the compiler and the programmer since the
compile-time error eventually emitted usually has little to do with the
actual problem. I know programmers who have spent hours tracking this
problem down.
As for declarations, use void * as the proper generic pointer--void *
was invented for this purpose. Do not use a pointer to char (char *). Use
void * when comparing or swapping memory that you don't necessarily need to
know the type of, as in generic functions, for example. Also use void * when
using incompatible types, as generic arguments to functions, or as return
types for functions. Standard routines such as malloc, memcmp, or qsort can
serve as models.
Functions
Functions are relatively straightforward: you should generally "adhere to
their policies." For instance, if a function returns a value, you should
usually utilize that value for something. You can scoff at this practice,
but there's plenty of broken production code out there because of lax
attitudes with regard to this. As an example, nonzero error return codes are
often the result of an unexpected run-time condition. Not scrupulously
dealing with these possible errors at development time may mean that your
user will have to at run time.
Although C does not require you to code the return type of a function or the
types of all arguments, not doing so is bad style. Also, if a function
doesn't return a value, then say so in your code. Make sure that the
function has a void return type and that it does not issue a return
statement. For example, you should write:
#include <stdio.h>
void func(void) { printf("I'm a void func!\n"); }
instead of
func() { printf("I'm a void func!\n"); }
Similarly if you have a function that takes no arguments, be sure to say so.
Writing the following
void foo(void)
ensures that your compiler will bark at
foo(param1)
Be sure that any functions that return values do not mistakenly reach the
end of the function or issue a return statement without actually returning
an expression. Although some C compilers warn of these situations, it is a
bad idea to depend upon the compiler for this guidance. It's better to code
it correctly in the first place.
It is sometimes a good idea to pass structures to functions by reference
(&some_structure, for example) rather than by value. Remember that
structures, like any other argument of a function, get placed on the stack;
passing them by value can result in large stack usage, inefficient programs,
and stack overflows. The same is true of functions that return structures.
Statements
Program statements should be used appropriately. If a loop needs a break or
continue statement, write one. Understand the difference between while and
do . . .while loops and use them both: don't rely on one over the other
merely because of force of habit. Understand the full power of for, but
beware of prefix versus postfix incrementing: was the value of the final
loop iteration really what you thought or was it one short? Use switch
statements with reasonable case labels instead of complex if statements
wherever possible.
All C programmers should understand side-effects. Take the time to
understand why statements such as the following do not have predictable
outcomes even though they may appear to function properly with your
compiler.
a[i++] = i++;
func(i++, i++, i++);
Many things are just not guaranteed by C and you should be aware of them.
Include Files
As already mentioned, create and use projectwide, systemwide, and operating
system or compiler files since they provide helpful resources. Also, don't
use "in-line" declarations such as
extern int func(int);
in a source file. Use the header file containing the declaration of func
instead. The only exception is if the function declaration is strictly a
forward reference for another function (such as a function defined later in
that same source file) that doesn't need a header for its functions.
Don't assume anything about functions. For example, many programs call
malloc without regard for its argument type or return type: it is assumed to
return an integer. This assumption can wreak havoc on machines on which
pointers and ints are mutually exclusive. Even on machines where they are,
this can be upset simply, say by changing the memory model.
It is also not a good idea to define or initialize identifiers in a header
file. Header files exist only to serve as declaration references, so do
nothing but declare in them. This leaves no room for initialization, so
create your own init function of some sort that contains respective
definitions as well as appropriate initializations or initial values to
those identifiers or variables via assignments.
Another good idea is to "envelope" header files. Doing this avoids
redeclaration problems and eliminates the need to synchronize header files.
For example, the following code guarantees trouble:
File a.h: #include <stdio.h>
■
■
■
File b.h: #include <stdio.h>
■
■
■
File c.c: #include "a.h"
#include "b.h"
To avoid this, wrap every header file you write. For instance, your C
compiler should have stdio.h in a form such as this:
#ifndef STDIO_H
#define STDIO_H
...#defines, decls, etc for stdio.h....
#endif
This way, if stdio.h or any other header file is processed a second time,
the #ifndef declaration will fail because the control variable, STDIO_H,
would have been defined the first time stdio.h was included. And if you were
thinking that perhaps a.h and b.h should not both be using stdio.h, stop!
There is nothing wrong with such a construction.
You should also know the relationships between header files. Instead of
including endless lists of them in your code, determine which .H files can
be included into other include files. This makes them simpler and neater to
use.
General
Make certain that you use the = sign correctly. Too often a programmer will
slip and leave out the second = of a Boolean expression. Some compilers warn
about this. When a constant is used on one side of a Boolean test, a very
handy way to pinpoint mistakes automatically is to put the constant on the
left hand side. If
if (0 = = a)
is erroneously entered as:
if (0=a)
the compiler will bark.
Using C idioms is still considered acceptable practice:
i++; i =+ 3; while ((c = getchar()) != EOF)...
Such constructs were originally used in order to generate better code, but
these days compilers are so good that the use of such idioms is often
unnecessary. Nevertheless, these idioms are popular constructs and soon
become quite natural.
Avoid union type puns. Remember, a union is for the reuse of storage and not
for the redefinition of that storage. If you really feel the need to,
consider using a type cast instead.
Use the const and volatile type qualifiers. Yes, these are relatively new
features in C but they are valuable. It should be considered that some
variables should not be modified and others should only be changed at set
points. Why not clarify aspects of your code with these keywords?
Nesting comments or miscoding a / * , * / pair is common. If you find
yourself doing that, use the following instead:
#if 0
... code possibly including comments ...
#endif
This is invaluable for debugging. Changing the 0 to 1 and vice versa is
quick and harmless; adding a #else can also help. The overuse of this method
can make code look ugly, however. Finally, stay away from nonportable
constructs if you can.
For further information, see:
"C Scope and Linkage: The Keys to Understanding Identifier Accessibility,"
MSJ (Vol. 3, No. 6); "A Guide to Understanding Even the Most Complex C
Declarations," MSJ (Vol. 3, No. 5); "Organizing Data in Your C Program with
Structures, Unions, and Typedefs," MSJ (Vol. 4, No. 2); "Advanced Techniques
for Using Structures and Unions in Your C Code," MSJ (Vol. 4, No. 3).
ANSI C/C++
The X3J11 draft ANSI C proposal is now an official standard. For a copy of
this standard call CBEMA (Computer and Business Equipment Manufacturers
Association) at 202-737-8888. The price is $65.
The main features of ANSI C include function prototypes, a massive clean-up
of the C preprocessor, a finite language specification, and a comprehensive
list of the gray areas of C. The standard can help you answer many of your C
questions.
Also, an X3J16 committee has formed and is responsible for producing ANSI
C++ and influencing ISO C++. They have already met twice. For information
about the X3J16 committee, call the author at 718-945-0009.
Examining Object-Oriented Techniques Using QuickPascal(TM)
Kris Jamsa
Pascal is not inherently an object-oriented language. It is possible,
however, to enjoy some of the benefits of object-oriented programming (OOP)
by developing your software with certain object-oriented techniques. This
article demonstrates these techniques with a set of object-oriented sorting
routines using the Microsoft(R) QuickPascal compiler. By creating the
objects as shown here, your programs can use one procedure called SortArray
to sort arrays of type Integer, arrays of type Real, or even arrays of type
String.
The Sorting Algorithm
Before you examine the object-oriented sorting routines, you need to have a
general understanding of the sorting algorithm used in the programs in this
article. To start, the sorting routines will use ascending order. By the end
of the article, you will have examined routines that can sort in either
direction.
Suppose, for example, that an array contains the values shown in Figure 1.
To sort the array, compare each value in the array to the first element. If
another array element is smaller than the value in the first element, you
exchange the values and then compare the next value with the new first
element. If you compare each value with the value of the first element and
exchange values as necessary, the array values are moved as shown in Figure
2.
Because the first element is now the smallest in the array and is in the
correct position, you can repeat the process of comparing and exchanging for
the second element (see Figure 3). Repeating these steps for the third,
fourth, and fifth elements produces the results shown in Figure 4.
The sample program (see Figure 5) creates a procedure named SortIntegers
that performs the preceding sorting algorithm. The program uses the
procedure to sort an array of 10 integer values.
After you compile and run this program, your screen displays the following
output:
0
129
174
308
350
449
556
619
619
648
The sorting procedure sorts the values correctly; however, the program works
only for arrays defined as type IntArray that contain only values of type
Integer. If the array size changes, you have to edit the program and
recompile. Also, if your program needs to sort an array of type Real, you
must write a second procedure. The program shown in Figure 6 creates
procedures that sort arrays of type Integer, of type Real, and of type
String.
When you compile and run this program, your screen displays the following
sorted values:
0 82.930 AAA
129 128.538 AAAA
174 292.222 BBB
308 464.632 XXXX
350 575.029 ZZZ
449 599.333
556 609.789
619 623.415
619 736.158
648 905.790
For each array type that it sorted, the program needed to create a different
procedure. The procedure code in each case was identical. The only
difference was the array type that each procedure supported.
As you will learn in the next section, you can use QuickPascal's
object-oriented capabilities to write a single sorting procedure that
supports all array types.
Creating an Object-Oriented Sort
One of the goals of object-oriented programming is to combine routines and
associated data into encapsulated objects. As a result, an object in this
example has as data fields the following attributes of an array:
■ the starting address of the first element in memory
■ the number of elements
■ the size of an element in bytes
Using these attributes, you can define a class--in this case named
SortArray--as follows:
TYPE
SortArray = OBJECT { generic sort-array class }
fStart: Pointer; { pointer to first element }
fNumElements: Integer; { number of array elements }
fElementSize: Integer; { element size in bytes }
PROCEDURE SortArray.SortValues; { performs sort }
FUNCTION SortArray.Compare(a, b: Pointer):Boolean;
{ compares 2 values }
END;
The class's first field, fStart, is a pointer to the array's first element.
By using pointers, the object is not limited to using an array of only a
fixed number of elements. Instead, the array can be any size. The
fNumElements field contains the number of elements in the array. Likewise,
the fElementSize field contains the number of bytes that each array element
occupies. Using the array's starting address and the element size, the
sorting procedure can determine the location of each element in memory.
Figure 7 shows how the procedure can determine the location of the third
element in an array of type Integer and in an array of type Real.
The definition of the SortArray class shows that it contains two methods:
SortArray.SortValues and SortArray.Compare. SortArray.SortValues, the first
method, is the method your programs use to sort each array. The second
method, SortArray.Compare, compares two values. As shown below, the program
defines three subclasses (one for each of the QuickPascal data types
Integer, Real, and String) that override this method:
IntArray = OBJECT(SortArray){integer sort-array subclass}
FUNCTION IntArray.Compare(a, b: Pointer): Boolean; OVERRIDE;
END;
RealArray = OBJECT(SortArray) {real sort-array subclass}
FUNCTION RealArray.Compare(a, b: Pointer): Boolean; OVERRIDE;
END;
StringArray = OBJECT(SortArray) {str sort-array subclass}
FUNCTION StringArray.Compare(a, b: Pointer): Boolean; OVERRIDE;
END;
Each subclass uses a function to compare two values. The function receives
pointers to each of the values that will be compared. If the first value
specified is greater than or equal to the second value, the function returns
the value TRUE. If the first value is less than the second value, the
function returns the value FALSE. The following function implements the
IntArray.
FUNCTION IntArray.Compare(a, b: Pointer): Boolean;
VAR
aPtr, bPtr: ^Integer;
BEGIN
{ Convert generic pointers to integer pointers }
aPtr := a;
bPtr := b;
Compare := aPtr^ >= bPtr^;
END;
The function receives two untyped pointers. To correctly compare values of
type Integer, the function assigns the pointers to typed integer pointers
and then dereferences them. Each of the methods that compares values uses
this technique. The method first assigns the untyped pointer to a pointer of
the correct type and then dereferences the pointer during the comparison.
Each class inherits and uses the SortArray.SortValues method, which sorts
the array's contents (see Figure 8).
Getting Started with OOP
The method begins by allocating space that is used for the temporary storage
location when two values in the array are exchanged. Because the method
might sort values of type Integer, values of type Real, or values of type
String, the size of the temporary variable will differ. As a result, the
method uses the fElementSize data field to allocate a memory location only
large enough to store one element of the array being sorted.
The method uses two FOR loops to compare the array elements. Within the
nested FOR loop, the program first creates pointers to the two elements to
be compared. The program calculates the location of each element in the
manner described earlier in the discussion of the fStart and fElementSize
data fields. The program calls these pointers aPtr and bPtr. Next the method
passes the pointers to the comparison function that is specific to each
subclass. If the values need to be exchanged, the procedure uses the
QuickPascal Move procedure to do so. Move copies the specified number of
bytes from one block of memory to another. Because the array types that the
method sorts can differ, the Move procedure lets the method exchange the
correct number of bytes for each type. Last, after the array is sorted, the
procedure releases the memory it allocated for the temporary buffer.
The sample program in Figure 9 uses the object-oriented sort method to sort
an array of type Integer, an array of type Real, and an array of type
String.
After you compile and run the program, your screen displays the output shown
below:
0 82.930 AAA
129 128.538 AAAA
174 292.222 BBB
308 464.632 JJJ
350 575.029 ZZZ
449 599.333
556 609.789
619 623.415
619 736.158
648 905.790
The program first calls the New procedure to allocate space for each object
and then assigns values to three arrays that are of different types. Next
the program assigns the data field values to each object and calls the
SortValues method for each object. Last the program displays the sorted
contents of each array and uses the Dispose procedure to release the memory
allocated for each object. By using object-oriented techniques, this program
was able to replace several sorting procedures with only one method.
Sorting in Ascending or Descending Order
The preceding program used object-oriented techniques to create a method
that sorts an array of any type. Unfortunately, the program sorts values
only in ascending order. To provide ascending and descending sorting
capabilities, the class definition below adds a new field called fAscending
to the SortArray class:
TYPE
SortArray = OBJECT { generic sort-array class }
fStart: Pointer; { pointer to first element }
fNumElements: Integer; { number of array elements }
fElementSize: Integer; { element size in bytes }
fAscending: Boolean; { TRUE if lower to higher }
PROCEDURE SortArray.SortValues; { performs sort }
FUNCTION SortArray.Compare(a,b:Pointer): Boolean; { compares 2 vals }
END;
When the fAscending field contains the value TRUE, the program sorts the
values in ascending order. When the fAscending field contains the value
FALSE, the program sorts the values in descending order. The program in
Figure 10 uses objects of the above class to sort arrays in either order.
Before the program sorts the array, it assigns either the value TRUE or the
value FALSE to the ascending data field to select the desired order. Notice
also the changes in the SortArray.SortValues method that were necessary to
enable sorting in both orders.
Improving the Sort Algorithm
Many computer scientists devote time and effort to enhancing sorting
algorithms for top performance. Although the simple sorting algorithm used
in the preceding programs generates the desired results, the algorithm takes
a long time to complete if the array is large. A better sorting algorithm is
the Shell sort, which is named after its inventor, Donald Shell. To improve
sorting performance, the Shell sort eliminates much of an array's disorder
early in the sort by exchanging elements that are spaced farther apart in
the array.
As an example, we use a modified Shell sort. This sort uses the same concept
as does the Shell sort, but it is changed slightly for the sake of
readability. The modified Shell sort establishes the size of the gap between
the elements to be compared. To begin, the gap is set to the number of
elements in the array, divided by 2. Given an eight-element array, the
initial gap value is 4. The modified Shell sort examines array elements in
which the index to the second element is the index of the first element plus
4. If the value of the first element is larger than the value of the second,
the elements are exchanged. The sort examines the elements whose indexes
differ by 4 until no more exchanges occur. Next the sort divides the size of
the gap by 2, which in this case creates a gap of 2. The modified Shell sort
then repeatedly tests for elements whose indexes differ by 2 until no
exchanges occur and then divides the gap by 2 again, yielding a gap value of
1. The sort repeatedly examines elements whose indexes differ by 1 until the
array is sorted.
The program in Figure 11 uses the modified Shell sort described above to
sort arrays faster, either in ascending or in descending order. The only
program changes required to implement the Shell sort algorithm were within
the SortArray.SortValues method.
Object-oriented programming techniques can enhance your code. By
encapsulating data and code into objects, a single method can process
multiple data types. And while the ideas presented here are straightforward,
they have a broad range of application.
Figure 5
PROGRAM SortInts;
{ Filename: SORTINTS.PAS }
{ Creates the SortIntegers procedure and uses it to sort an array }
{ of 10 integers }
TYPE
IntArray = ARRAY [1..100] OF Integer;
PROCEDURE SortIntegers(VAR values: IntArray; numElements: Integer);
VAR
i, j: Integer; { indexes into the array }
temp: Integer; { temporary buffer for exchange }
BEGIN
FOR i := 1 TO numElements - 1 DO
FOR j := i + 1 TO numElements DO
BEGIN
IF (values[i] > values[j]) THEN
BEGIN
temp := values[i];
values[i] := values[j];
values[j] := temp;
END;
END;
END;
VAR
intValues: IntArray;
i: Integer;
BEGIN
FOR i := 1 TO 10 DO
intValues[i] := Random(1000); { fill the array }
SortIntegers(intValues, 10);
{ Display the sorted array }
FOR i := 1 TO 10 DO
Writeln(intValues[i]);
END.
Figure 6
PROGRAM SortVals;
{ Filename: SORTVALS.PAS }
{ Creates the SortIntegers, SortReals, and SortStrings procedures }
{ and uses them to sort three arrays of different types }
TYPE
IntArray = ARRAY [1..100] OF Integer;
RealArray = ARRAY [1..100] OF Real;
StringArray = ARRAY [1..100] OF String;
{ Sorts an array of type IntArray }
PROCEDURE SortIntegers(VAR values: IntArray; numElements: Integer);
VAR
index, index2: Integer; { indexes into the array }
temp: Integer; { temporary buffer for exchange }
BEGIN
FOR index := 1 TO numElements - 1 DO
FOR index2 := index + 1 TO numElements DO
BEGIN
IF (values[index] > values[index2]) THEN
BEGIN
temp := values[index];
values[index] := values[index2];
values[index2] := temp;
END;
END;
END;
{ Sorts an array of type RealArray }
PROCEDURE SortReals(VAR values: RealArray; numElements: Integer);
VAR
index, index2: Integer; { indexes into the array }
temp: Real; { temporary buffer for exchange }
BEGIN
FOR index := 1 TO numElements - 1 DO
FOR index2 := index + 1 TO numElements DO
BEGIN
IF (values[index] > values[index2]) THEN
BEGIN
temp := values[index];
values[index] := values[index2];
values[index2] := temp;
END;
END;
END;
{ Sorts an array of type StringArray }
PROCEDURE SortStrings(VAR values: StringArray; numElements: Integer);
VAR
index, index2: Integer; { indexes into the array }
temp: String; { temporary buffer for exchange }
BEGIN
FOR index := 1 TO numElements - 1 DO
FOR index2 := index + 1 TO numElements DO
BEGIN
IF (values[index] > values[index2]) THEN
BEGIN
temp := values[index];
values[index] := values[index2];
values[index2] := temp;
END;
END;
END;
VAR
intValues: IntArray;
realValues: RealArray;
strValues: StringArray;
index: Integer;
BEGIN
FOR index := 1 TO 10 DO
intValues[index] := Random(1000);
FOR index := 1 TO 10 DO
realValues[index] := Random * 1000;
strValues[1] := 'AAA';
strValues[2] := 'ZZZ';
strValues[3] := 'AAAA';
strValues[4] := 'BBB';
strValues[5] := 'XXXX';
SortIntegers(intValues, 10);
SortReals(realValues, 10);
SortStrings(strValues, 5);
{ Display the sorted arrays }
FOR index := 1 TO 10 DO
BEGIN
Write(intValues[index]:4);
Write(realValues[index]:15:3);
IF (index <= 5) THEN
Writeln(strValues[index]:10)
ELSE
Writeln;
END;
END.
Figure 8 SortArray.SortValues Method
PROCEDURE SortArray.SortValues;
VAR
i, j: Integer; { indexes into the array }
aPtr, bPtr, temp: Pointer;
BEGIN
{ Allocate memory for temporary swap buffer }
GetMem(temp, Self.fElementSize);
FOR i := 1 TO Self.fNumElements - 1 DO
FOR j := i + 1 TO Self.fNumElements DO
BEGIN
{ Create pointers to the current two elements }
aPtr := Ptr(Seg(Self.fStart^), Ofs(Self.fStart^) +
(i - 1) * Self.fElementSize);
bPtr := Ptr(Seg(Self.fStart^), Ofs(Self.fStart^) +
(j - 1) * Self.fElementSize);
IF (Self.Compare(aPtr, bPtr)) THEN
BEGIN
Move(bPtr^, temp^, Self.fElementSize);
Move(aPtr^, bPtr^, Self.fElementSize);
Move(temp^, aPtr^, Self.fElementSize);
END;
END;
{ Release memory allocated for temporary buffer }
FreeMem(temp, Self.fElementSize);
END;
Figure 9 SORT.PAS
{$M+} { test for object memory allocation }
PROGRAM Sort;
{ Filename: SORT.PAS }
{ Creates classes, methods, and objects to sort an array of }
{ integers, an array of reals, and an array of character strings }
TYPE
SortArray = OBJECT { generic sort-array class }
fStart: Pointer; { pointer to first element }
fNumElements: Integer; { number of array elements }
fElementSize: Integer; { element size in bytes }
PROCEDURE SortArray.SortValues; { performs sort }
FUNCTION SortArray.Compare(a, b: Pointer): Boolean;{comp 2 vals}
END;
IntArray = OBJECT(SortArray) { integer sort-array subclass }
FUNCTION IntArray.Compare(a, b: Pointer): Boolean; OVERRIDE;
END;
RealArray = OBJECT(SortArray) { real sort-array subclass }
FUNCTION RealArray.Compare(a, b: Pointer): Boolean; OVERRIDE;
END;
StringArray = OBJECT(SortArray) { string sort-array subclass }
FUNCTION StringArray.Compare(a, b: Pointer): Boolean; OVERRIDE;
END;
FUNCTION SortArray.Compare(a, b: Pointer): Boolean;
BEGIN
{ No statements--always overridden by a subclass }
END;
{ Compares two integer values using pointers. Returns TRUE if 1st }
{ value is > or = to the second; otherwise, returns FALSE. }
FUNCTION IntArray.Compare(a, b: Pointer): Boolean;
VAR
aPtr, bPtr: ^Integer;
BEGIN
{ Convert generic pointers to integer pointers }
aPtr := a;
bPtr := b;
Compare := aPtr^ >= bPtr^;
END;
{ Compares two real values using pointers. Returns TRUE if 1st }
{ value is greater than or equal to 2nd; otherwise, returns FALSE. }
FUNCTION RealArray.Compare(a, b: Pointer): Boolean;
VAR
aPtr, bPtr: ^Real;
BEGIN
{ Convert generic pointers to real pointers }
aPtr := a;
bPtr := b;
Compare := aPtr^ >= bPtr^;
END;
{ Compares two string values using pointers. Returns TRUE if 1st }
{ value is greater than or equal to 2nd; otherwise, returns FALSE. }
FUNCTION StringArray.Compare(a, b: Pointer): Boolean;
VAR
aPtr, bPtr: ^String;
BEGIN
{ Convert generic pointers to string pointers }
aPtr := a;
bPtr := b;
Compare := aPtr^ >= bPtr^;
END;
{ Sorts the array }
PROCEDURE SortArray.SortValues;
VAR
i, j: Integer; { indexes into the array }
aPtr, bPtr, temp: Pointer;
BEGIN
{ Allocate memory for temporary swap buffer }
GetMem(temp, Self.fElementSize);
FOR i := 1 TO Self.fNumElements - 1 DO
FOR j := i + 1 TO Self.fNumElements DO
BEGIN
{ Create pointers to the current two elements }
aPtr := Ptr(Seg(Self.fStart^), Ofs(Self.fStart^) +
(i - 1) * Self.fElementSize);
bPtr := Ptr(Seg(Self.fStart^), Ofs(Self.fStart^) +
(j - 1) * Self.fElementSize);
IF (Self.Compare(aPtr, bPtr)) THEN
BEGIN
Move(bPtr^, temp^, Self.fElementSize);
Move(aPtr^, bPtr^, Self.fElementSize);
Move(temp^, aPtr^, Self.fElementSize);
END;
END;
{ Release memory allocated for temporary buffer }
FreeMem(temp, Self.fElementSize);
END;
VAR
{ Declare objects and other variables }
realValues: RealArray;
intValues: IntArray;
strValues: StringArray;
array1 : ARRAY [1..10] OF Integer;
array2 : ARRAY [1..10] OF Real;
array3 : ARRAY [1..5] OF String;
i: Integer;
BEGIN
{ Allocate memory for each object }
New(intValues);
New(realValues);
New(strValues);
FOR i := 1 TO 10 DO
array1[i] := Random(1000);
FOR i := 1 TO 10 DO
array2[i] := Random * 1000;
array3[1] := 'AAA';
array3[2] := 'ZZZ';
array3[3] := 'BBB';
array3[4] := 'AAAA';
array3[5] := 'JJJ';
{ Give values to the data fields of the objects }
intValues.fStart := @array1[1];
intValues.fNumElements := 10;
intValues.fElementSize := SizeOf(array1[1]);
intValues.SortValues; { sort the array }
realValues.fStart := @array2[1];
realValues.fNumElements := 10;
realValues.fElementSize := SizeOf(array2[1]);
realValues.SortValues; { sort the array }
strValues.fStart := @array3[1];
strValues.fNumElements := 5;
strValues.fElementSize := 256;
strValues.SortValues; { sort the array }
{ Display the sorted arrays }
FOR i := 1 TO 10 DO
BEGIN
Write(array1[i]:4);
Write(array2[i]:15:3);
IF (i <= 5) THEN
Writeln(array3[i]:10)
ELSE
Writeln;
END;
{ Release the memory allocated for the objects }
Dispose(intValues);
Dispose(realValues);
Dispose(strValues);
END.
Figure 10 ORDER.PAS
{$M+} { test for object memory allocation }
PROGRAM OrderSort;
{ Filename: ORDER.PAS }
{ Creates classes, methods, and objects to sort (in either }
{ ascending or descending order) an array of integers, an array }
{ of reals, and an array of character strings }
TYPE
SortArray = OBJECT { generic sort-array class }
fStart: Pointer; { pointer to first element }
fNumElements: Integer; { number of array elements }
fElementSize: Integer; { element size in bytes }
fAscending: Boolean; { TRUE if lower to higher }
PROCEDURE SortArray.SortValues; { performs sort }
FUNCTION SortArray.Compare(a, b: Pointer): Boolean;{comp 2 vals}
END;
IntArray = OBJECT(SortArray) { integer sort-array subclass }
FUNCTION IntArray.Compare(a, b: Pointer): Boolean; OVERRIDE;
END;
RealArray = OBJECT(SortArray) { real sort-array subclass }
FUNCTION RealArray.Compare(a, b: Pointer): Boolean; OVERRIDE;
END;
StringArray = OBJECT(SortArray) { string sort-array subclass }
FUNCTION StringArray.Compare(a, b: Pointer): Boolean; OVERRIDE;
END;
FUNCTION SortArray.Compare(a, b: Pointer): Boolean;
BEGIN
{ No statements--always overridden by subclass }
END;
{ Compares two integer values using pointers. Returns TRUE if 1st }
{ value is > or = to the 2nd; otherwise, returns FALSE. }
FUNCTION IntArray.Compare(a, b: Pointer): Boolean;
VAR
aPtr, bPtr: ^Integer;
BEGIN
{ Convert generic pointers to integer pointers }
aPtr := a;
bPtr := b;
Compare := aPtr^ >= bPtr^;
END;
{ Compares two real values using pointers. Returns TRUE if 1st }
{ value is > or = to the 2nd; otherwise, returns FALSE. }
FUNCTION RealArray.Compare(a, b: Pointer): Boolean;
VAR
aPtr, bPtr: ^Real;
BEGIN
{ Convert generic pointers to real pointers }
aPtr := a;
bPtr := b;
Compare := aPtr^ >= bPtr^;
END;
{ Compares two string values using pointers. Returns TRUE if 1st }
{ value is > or = to the 2nd; otherwise, returns FALSE. }
FUNCTION StringArray.Compare(a, b: Pointer): Boolean;
VAR
aPtr, bPtr: ^String;
BEGIN
{ Convert generic pointers to string pointers }
aPtr := a;
bPtr := b;
Compare := aPtr^ >= bPtr^;
END;
{ Sorts the array }
PROCEDURE SortArray.SortValues;
VAR
i, j: Integer; { indexes into the array }
aPtr, bPtr, temp: Pointer;
BEGIN
{ Allocate memory for temporary swap buffer }
GetMem(temp, Self.fElementSize);
FOR i := 1 TO Self.fNumElements - 1 DO
FOR j := i + 1 TO Self.fNumElements DO
BEGIN
{ Create pointers to the current two elements }
aPtr := Ptr(Seg(Self.fStart^), Ofs(Self.fStart^) +
(i - 1) * Self.fElementSize);
bPtr := Ptr(Seg(Self.fStart^), Ofs(Self.fStart^) +
(j - 1) * Self.fElementSize);
IF (Self.fAscending) AND (Self.Compare(aPtr, bPtr)) THEN
BEGIN
Move(bPtr^, temp^, Self.fElementSize);
Move(aPtr^, bPtr^, Self.fElementSize);
Move(temp^, aPtr^, Self.fElementSize);
END
ELSE IF (NOT Self.fAscending) AND
(NOT Self.Compare(aPtr, bPtr)) THEN
BEGIN
Move(bPtr^, temp^, Self.fElementSize);
Move(aPtr^, bPtr^, Self.fElementSize);
Move(temp^, aPtr^, Self.fElementSize);
END;
END;
{ Release memory allocated for temporary buffer }
FreeMem(temp, Self.fElementSize);
END;
VAR
{ Declare objects and other variables }
realValues: RealArray;
intValues: IntArray;
strValues: StringArray;
array1 : ARRAY [1..10] OF Integer;
array2 : ARRAY [1..10] OF Real;
array3 : ARRAY [1..5] OF String;
i: Integer;
BEGIN
{ Allocate memory for each object }
New(intValues);
New(realValues);
New(strValues);
FOR i := 1 TO 10 DO
array1[i] := Random(1000);
FOR i := 1 TO 10 DO
array2[i] := Random * 1000;
array3[1] := 'AAA';
array3[2] := 'ZZZ';
array3[3] := 'BBB';
array3[4] := 'AAAA';
array3[5] := 'JJJ';
{ Give values to the data fields of the objects }
intValues.fStart := @array1[1];
intValues.fNumElements := 10;
intValues.fElementSize := SizeOf(array1[1]);
intValues.fAscending := FALSE;
intValues.SortValues; { sort the array }
realValues.fStart := @array2[1];
realValues.fNumElements := 10;
realValues.fElementSize := SizeOf(array2[1]);
realValues.fAscending := TRUE;
realValues.SortValues; { sort the array }
strValues.fStart := @array3[1];
strValues.fNumElements := 5;
strValues.fElementSize := 256;
strValues.fAscending := FALSE;
strValues.SortValues; { sort the array }
{ Display the sorted arrays }
FOR i := 1 TO 10 DO
BEGIN
Write(array1[i]:4);
Write(array2[i]:15:3);
IF (i <= 5) THEN
Writeln(array3[i]:10)
Else
Writeln;
END;
{ Release the memory allocated for the objects }
Dispose(intValues);
Dispose(realValues);
Dispose(strValues);
END.
Figure 11 SHELL.PAS
{$M+} { test for object memory allocation }
PROGRAM ShellSort;
{ Filename: SHELL.PAS }
{ Creates classes, methods, and objects to sort (in ascending or }
{ descending order) an array of integers, an array of reals, and an }
{ array of character strings using a modified Shell sort to perform }
{ the sorting }
TYPE
SortArray = OBJECT { generic sort-array class }
fStart: Pointer; { pointer to first element }
fNumElements: Integer; { number of array elements }
fElementSize: Integer; { element size in bytes }
fAscending: Boolean; { TRUE if lower to higher }
PROCEDURE SortArray.SortValues; { performs sort }
FUNCTION SortArray.Compare(a, b: Pointer): Boolean;{ comp 2 vals}
END;
IntArray = OBJECT(SortArray) { integer sort-array subclass }
FUNCTION IntArray.Compare(a, b: Pointer): Boolean; OVERRIDE;
END;
RealArray = OBJECT(SortArray) { real sort-array subclass }
FUNCTION RealArray.Compare(a, b: Pointer): Boolean; OVERRIDE;
END;
StringArray = OBJECT(SortArray) { string sort-array subclass }
FUNCTION StringArray.Compare(a, b: Pointer): Boolean; OVERRIDE;
END;
FUNCTION SortArray.Compare(a, b: Pointer): Boolean;
BEGIN
{ No statements--always overridden by subclass }
END;
{ Compares two integer values using pointers. Returns TRUE if 1st }
{ value is greater than or equal to 2nd; otherwise, returns FALSE. }
FUNCTION IntArray.Compare(a, b: Pointer): Boolean;
VAR
aPtr, bPtr: ^Integer;
BEGIN
{ Convert generic pointers to integer pointers }
aPtr := a;
bPtr := b;
Compare := aPtr^ >= bPtr^;
END;
{ Compares 2 real values using pointers. Returns TRUE if the 1st }
{ value is > or = to the 2nd; otherwise, returns FALSE. }
FUNCTION RealArray.Compare(a, b: Pointer): Boolean;
VAR
aPtr, bPtr: ^Real;
BEGIN
{ Convert generic pointers to real pointers }
aPtr := a;
bPtr := b;
Compare := aPtr^ >= bPtr^;
END;
{ Compares two string values using pointers. Returns TRUE if 1st }
{ value is > or = to the 2nd; otherwise, returns FALSE. }
FUNCTION StringArray.Compare(a, b: Pointer): Boolean;
VAR
aPtr, bPtr: ^String;
BEGIN
{ Convert generic pointers to string pointers }
aPtr := a;
bPtr := b;
Compare := aPtr^ >= bPtr^;
END;
{ Implements a modified Shell sort to sort the array }
PROCEDURE SortArray.SortValues;
VAR
index, gap: Integer; { indexes into the array }
aPtr, bPtr, temp: Pointer;
exchangeOccurred: Boolean;
BEGIN
{ Allocate memory for temporary swap buffer }
GetMem(temp, Self.fElementSize);
gap := Self.fNumElements DIV 2;
REPEAT
REPEAT
exchangeOccurred := FALSE;
FOR index := 1 TO Self.fNumElements - GAP DO
BEGIN
{ Create pointers to 2 elements whose indexes differ by }
{ the value of the gap }
aPtr := Ptr(Seg(Self.fStart^), Ofs(Self.fStart^) +
(index - 1) * Self.fElementSize);
bPtr := Ptr(Seg(Self.fStart^), Ofs(Self.fStart^) +
(index + gap - 1) * Self.fElementSize);
IF (Self.fAscending) AND (Self.Compare(aPtr, bPtr)) THEN
BEGIN
exchangeOccurred := TRUE;
Move(bPtr^, temp^, Self.fElementSize);
Move(aPtr^, bPtr^, Self.fElementSize);
Move(temp^, aPtr^, Self.fElementSize);
END
ELSE IF (NOT Self.fAscending) AND
(NOT Self.Compare(aPtr, bPtr)) THEN
BEGIN
exchangeOccurred := TRUE;
Move(bPtr^, temp^, Self.fElementSize);
Move(aPtr^, bPtr^, Self.fElementSize);
Move(temp^, aPtr^, Self.fElementSize);
END;
END;
UNTIL (NOT exchangeOccurred);
gap := gap DIV 2;
UNTIL (gap = 0);
{ Release memory allocated for temporary buffer }
FreeMem(temp, Self.fElementSize);
END;
VAR
{ Declare objects and other variables }
realValues: RealArray;
intValues: IntArray;
strValues: StringArray;
array1 : ARRAY [1..10] OF Integer;
array2 : ARRAY [1..10] OF Real;
array3 : ARRAY [1..5] OF String;
index: Integer;
BEGIN
{ Allocate memory for each object }
New(intValues);
New(realValues);
New(strValues);
FOR index := 1 TO 10 DO
array1[index] := Random(1000);
FOR index := 1 TO 10 DO
array2[index] := Random * 1000;
array3[1] := 'AAA';
array3[2] := 'ZZZ';
array3[3] := 'BBB';
array3[4] := 'AAAA';
array3[5] := 'JJJ';
{ Give values to the data fields of the objects }
intValues.fStart := @array1[1];
intValues.fNumElements := 10;
intValues.fElementSize := SizeOf(array1[1]);
intValues.fAscending := FALSE;
intValues.SortValues; { sort the array }
realValues.fStart := @array2[1];
realValues.fNumElements := 10;
realValues.fElementSize := SizeOf(array2[1]);
realValues.fAscending := TRUE;
realValues.SortValues; { sort the array }
strValues.fStart := @array3[1];
strValues.fNumElements := 5;
strValues.fElementSize := 256;
strValues.fAscending := FALSE;
strValues.SortValues; { sort the array }
{ Display the sorted arrays }
FOR index := 1 TO 10 DO
BEGIN
Write(array1[index]:4);
Write(array2[index]:15:3);
IF (index <= 5) THEN
Writeln(array3[index]:10)
ELSE
Writeln;
END;
{ Release the memory allocated for the objects }
Dispose(intValues);
Dispose(realValues);
Dispose(strValues);
END.
Implementing DDE with Presentation Manager Object Windows
Richard Hale Shaw
In addition to the multithreaded, multitasking capabilities of the OS/2
operating system and its interprocess communications facilities, Dynamic
Data Exchange (DDE) has the potential to enhance connectivity for OS/2
Presentation Manager (hereafter "PM") applications. But access to DDE is
limited to PM programs: OS/2 kernel programs are usually forbidden from
using it. This article discusses how you can use PM object windows to
implement DDE in Vio applications. It also surveys alterations and additions
to the PMServer application presented in "Accessing Presentation Manager
Facilities from Within OS/2 Kernel Applications," MSJ (Vol. 5, No. 1) that
will permit Vio programs to use DDE. The source code for the new version of
PMServer will be published in the next installment.
DDE, like the Clipboard, is a facility for transferring data from one
application to another. But unlike the Clipboard, in which the user
initiates and controls one-time transfers of data between applications by
cutting and pasting, DDE is transparent. That is, a user can tell an
application to initiate a DDE conversation with another program, and the two
programs can then transfer and receive data without additional help. In
other words, DDE allows applications to send or receive data updates on an
asynchronous real-time basis.
To understand the implications of DDE, consider the classic DDE scenario: A
PM communications program is downloading stock prices from the Dow Jones
News/Retrieval(R) Service. Using a DDE conversation initiated by
Microsoft(R) Excel, the communications program sends the worksheet the price
information on selected stocks. Microsoft Excel uses the data in two ways:
to update the cells of a worksheet containing a financial portfolio and to
update a chart derived from the worksheet and displayed in another worksheet
window. Changes to the worksheet cells trigger DDE transactions in a
different DDE conversation. The worksheet transfers the contents of a range
of cells to a PM word processor, which places the updated portfolio figures
in a document table of a financial report. The result is that the user sees
a real-time image of the stock portfolio on the chart, while the worksheet
maintains the latest prices and the report document is kept up to date.
Thus, DDE can be considered as a bridge between diverse applications: by
providing a uniform means of transferring data between otherwise dissimilar
applications, it offers applications a level of integration that has been
long talked about but never, until now, achieved. DDE opens the door for a
host of third-party programs and add-ins used by a variety of applications
with which they have nothing in common--except DDE.
As I noted, applications that support DDE must be PM applications. Moreover,
as discussed in "A Presentation Manager Primer," MSJ (Vol. 5, No.1,
pp.14-16), PM applications are fundamentally distinguished from OS/2 kernel
programs by the existence of a PM message queue, where messages are received
from PM about input events. PM applications differ architecturally from OS/2
kernel programs since they do not use the OS/2 Vio, Kbd, and Mou subsystems
to request input or generate output. Instead, they receive messages from PM
that indicate when the user has pressed a key or clicked a mouse button;
consequently, they also send PM a message when they want it to display
information on the screen.
The same is true for applications using DDE, since all DDE activity and
signaling is carried out by broadcasting and posting PM messages to other
applications. When one application transfers data to another via DDE, it
passes a shared memory selector as a component of the message.
DDE Conversations and Transactions
DDE is a facility for transferring data from one application to another.
Since the bulk of DDE activity is under program (not user) control, however,
DDE is largely a protocol that regulates how and when data is passed between
applications--it is not a public memory manager as is the Clipboard.
Each time an application uses DDE, it initiates a conversation consisting of
one or more DDE transactions. The application initiating the conversation is
always the DDE client and the application providing the data is the DDE
server. In a single conversation, a client can request data from more than
one server, but an application can act as a server to only one client. Since
the client application always initiates a conversation, a DDE server
application that needs data from another application must initiate a new
conversation and become a DDE client. Thus, an application may be a DDE
client and server simultaneously, by passing data it receives as a client in
one conversation to the client of another conversation (in which it is a
server).
A DDE conversation between two applications always takes place between two
PM windows--one window per application. Each window is identified by
its window handle, which is one of the components of a DDE message. The
message processing code can be included in an application's primary window
procedure, but it is more useful to create a new invisible window for each
DDE conversation.
DDE conversations use a three-level hierarchy to identify a unit of data: an
application name, a topic name, and an item name. A client may specify an
application's name when initiating a DDE conversation. Or, it can omit the
application name, inviting any application to participate in the
conversation. The topic name provides a logical context for each
conversation; it can also be omitted by the client, allowing the client to
converse with an application on any topic. Finally, each data item is
specifically named. Thus, a client could make a request for stock quotes
from Microsoft Excel by specifying "Microsoft Excel" as the application
name, and "STOCKS.XLS" (the name of a stock quote worksheet) as the topic
name. The item name would be a range of cells in the the worksheet.
After a DDE client initiates a conversation with one or more servers, it
continues via one or more transactions, that is, solicitations for
particular DDE actions by the servers. There are six fundamental types of
DDE transactions: REQUEST, ADVISE, UNADVISE, POKE, EXECUTE, and TERMINATE.
A DDE client may REQUEST data that the server will immediately provide, if
possible. Or a client may ask the server to ADVISE it if the specified data
changes. In the latter case, the client may ask the server to provide it
with updated data automatically; or it may ask simply to be notified that
the data has changed. A client can also tell the server to UNADVISE it when
information is no longer needed. Additionally, a client may POKE a server in
an attempt to offer an unsolicited data item. Or a client may pass the
server a command string to EXECUTE. Finally, either the client or the server
may TERMINATE the DDE conversation. Note that TERMINATE is the only
transaction that either party may issue; the other transactions must be
issued by the client. As mentioned above, if an application playing the
server role wishes to receive data from a client application, it must
initiate a new DDE conversation in which it becomes the client.
Starting a DDE Conversation
To initiate a conversation, an application has a window procedure call the
WinDdeInitiate function, which takes the window handle, the application
name, and the topic name as parameters. This function automatically creates
a shared memory segment containing the DDEINIT structure, defined in
PMWIN.H:
typedef struct _DDEINIT
{ USHORT cb; // length of block
PSZ pszAppName; // application name
PSZ pszTopic; // topic name
} DDEINIT;
The WinDdeInitiate function allocates the shared memory segment with enough
room to contain the application name and topic name; it then copies these
names into the segment. Next, WinDdeInitiate sends a WM_DDE_INITIATE to
every frame window whose parent is HWND_DESKTOP and provides a copy of the
shared memory segment as a message parameter. Because the message is sent
and not posted, the client window will wait until all the responses have
been processed.
Any window that supports DDE will process the WM_DDE_INITIATE and determine
if it can support the proposed DDE conversation by examining the application
name and topic name in the DDEINIT segment. Again, every frame window whose
parent is HWND_DESKTOP gets the message; it is up to the window that
receives the message to process it (instead of passing it on) and determine
whether the proposed DDE conversation can be supported. The application name
and topic name merely provide a logical context for the conversation; a
window can respond to such a message regardless of its specific application
name. Note that the initiating window may pass either name as a NULL and
that the receiving window can treat a NULL name much like a filename
wildcard (*).
If a receiving window decides it can support the proposed conversation (that
is, it can add to the conversation by offering data that pertains to the
application or topic name), it responds with WinDdeRespond, passing it the
window handle of the DDE client window, its own window handle, and the
application and topic names it received in the WM_DDE_INITIATE message. The
receiving window always responds separately for each topic it supports; that
is, if a client window sends a NULL topic name, the server window calls
WinDdeRespond once for each topic supported.
The client window receives a WM_DDE_INITIATEACK subsequently for every call
to WinDdeRespond by a DDE server window. This message contains a shared
memory segment with a DDEINIT structure that the client uses to examine the
application name and topic name and see which DDE conversations are being
supported by server windows. The client posts a WM_DDE_TERMINATE message at
this point to the server of each DDE conversation it does not intend to
continue.
Creating a REQUEST Transaction
After the client window has initiated the DDE conversation, it begins to
request data from the remaining DDE server windows. To do so, the client
window posts a WM_DDE_REQUEST message to each server, which includes a
shared memory segment containing a DDESTRUCT structure:
typedef struct _DDESTRUCT
{ ULONG cbData; // length of block
USHORT fsStatus; // status word
USHORT usFormat; // format word
USHORT offszItemName; // offset into block for
// item name
USHORT offabData; // offset into block of
// data
} DDESTRUCT;
Both the DDE client and server windows use this structure in subsequent
transactions. The segment needs to be large enough to contain the structure
and data (if any) being transferred (including the item name). The
offszItemName and offabData members indicate the item name offset and data
offset from the beginning of the segment. The bits of the fsStatus word
indicate additional status information.
The process of sending a DDESTRUCT segment (whether DDE client or server)
should always allocate the segment with the SEG_GIVEABLE flag and make it
shareable by the receiving process with a call to DosGiveSeg. (Since
DosGiveSeg requires the process ID of the recipient, the allocating window
can call WinQueryWindowProcess with the recipient's window handle to
retrieve that window's process ID.)
The client window can use this structure to specify the name of the data
item it is requesting as well as the format of the data (typically
DDEFMT_TEXT). It can define its own data formats, but they must be
registered with the system via the system atom table, where the formats are
made available to other applications.
Finally, the client posts the WM_DDE_REQUEST message and the shared segment
to each server via WinDdePostMessage and then frees the segment with
DosFreeSeg. (After a shared DDESTRUCT segment has been posted to a recipient
window, the allocating window calls DosFreeSeg to notify OS/21 that it no
longer owns the segment. OS/2 can then discard the segment once it has been
freed by the last process that owned it.)
While this may sound like a lot of work, some generalized functions will be
included with PMServer to encapsulate the process of allocating and setting
up the DDESTRUCT segment, posting the message and segment to a recipient
with WinDdePostMsg, and freeing the segment.
Responding to a REQUEST Transaction
Upon receiving a WM_DDE_REQUEST, a DDE server window either posts the data
item back to the client window or replies with a negative acknowledgment. In
either case, the server uses the described approach for creating a DDESTRUCT
segment. If the server window can satisfy the request in the specified
format, it places the DDESTRUCT structure and the data item in a shareable
segment, and includes the segment in a WM_DDE_DATA message that it posts to
the client window via WinDdePostMsg. Upon receiving the message, the client
processes the data item and frees the segment.
If the server window is unable to satisfy a WM_DDE_REQUEST, it replies with
a negative acknowledgment. To do this, it sets the DDE_NOTPROCESSED status
bit (in the fsStatus member of the DDESTRUCT structure) and posts a
WM_DDE_ACK message to the client window (again via WinDdePostMsg). If the
application is busy, a server can also set the DDE_FBUSY bit. If the server
wants the client to acknowledge receipt of the message (regardless of
whether the server is posting a WM_DDE_DATA or a negative acknowledgment),
it sets the DDE_FACKREQ status bit. Upon receiving a negative acknowledgment
from the server window, a client can ask for the same data with a different
DDE format (it should request the most complex format first, then step down
from there). If the server was busy, the client can wait and ask for the
data again later.
Creating an ADVISE Transaction
A client window can also use a conversation with a server to create a
permanent link to a specific data item. After it does this, the server
either notifies the client when the data has changed or posts a copy of the
changed data item directly to the client. This data link will remain in
place until it is disconnected.
To establish a data link, a client window posts a WM_DDE_ADVISE with the
requisite DDESTRUCT segment containing the name of the data item. To receive
a notification that the data item has changed, the client sets the
DDE_FNODATA status bit. If the server can comply (that is, it has access to
the item in the desired format), it records the data link in an internal
table or list and posts a positive acknowledgment to the client window: a
WM_DDE_ACK with the DDE_NOTPROCESSED status bit clear (in fsStatus) and the
DDE_FRESPONSE bit set. Then, every time the data item changes, the server
posts a WM_DDE_DATA message with the DDE_FNODATA status bit set. The client
window can ignore the notifications or request the latest copy of the data
item with a conventional request transaction via WM_DDE_REQUEST.
To receive the newly updated data, the client posts the same WM_DDE_ADVISE
with the DDE_FNODATA status bit clear. This causes the server to include the
data with the WM_DDE_DATA messages.
In either case, if the server cannot comply with the WM_DDE_ADVISE, it posts
a negative acknowledgment. The server sets the DDE_FACK bit if it wishes the
client to return a positive acknowledgment of its negative acknowledgment.
To terminate a data link established via a WM_DDE_ADVISE message, a client
window posts a WM_DDE_UNADVISE to a server window. Since it is up to the
server to keep track of which clients have data links, the server checks its
internal table and sends a negative acknowledgment to a WM_DDE_UNADVISE
posted by a client that has no data link. Otherwise, it removes the record
of the data link from the table and responds with a positive acknowledgment.
A client terminates the data links between it and a server by posting the
WM_DDE_ADVISE with a NULL item name.
Terminating a DDE Conversation
A window can terminate a DDE conversation at any time by posting a
WM_DDE_TERMINATE with a NULL DDESTRUCT pointer. Recall that this is the only
DDE transaction that can be generated by either the client or the server.
The WM_DDE_TERMINATE immediately stops all transactions for that
conversation. The window should not process or post any further messages for
that conversation. There is one exception, however: when the recipient
receives a WM_DDE_TERMINATE, it immediately posts the same in reply. Thus,
although the window that originated the WM_DDE_TERMINATE can consider the
DDE conversation terminated upon posting the message, its receipt of the
same from the recipient window is the final acknowledgment of the
transaction.
Once the recipient replies to a WM_DDE_TERMINATE by posting another, it may
destroy its DDE window. The recipient, however, should never send a
negative, busy, or positive acknowledgment in response to a
WM_DDE_TERMINATE. The window that originated the WM_DDE_TERMINATE will
ignore any subsequent messages received from the recipient window. Besides,
the recipient may have destroyed its window before a response could be
received and acted on.
Adding DDE Support to PMServer
As presented in the previous installment, PMServer provides Clipboard
services to non-PM Vio applications. It does this by accepting messages from
its Vio clients via a kernel queue monitored by a separate, windowless Queue
Manager thread. When a kernel application wishes to communicate with
PMServer, it sends the latter a message via this queue. After it retrieves
the message from the queue, Queue Manager posts another message to
PMServer's client window (via WinPostMessage), where its window procedure
can act on it.
To cut or copy, a Vio client writes a message (including the data to be
copied) to the kernel queue, which the Queue Manager passes to the client
window (which copies the data to the Clipboard). If a Vio client has
registered a kernel call-back queue of its own with the Queue Manager, it
can ask if Clipboard data is available. The previously described process
takes place with the exception that PMServer's client window will post a
message to the Vio client (via its call-back queue) indicating the presence
or absence of Clipboard data. Upon receiving a positive notification, the
Vio client can request the Clipboard data for pasting, which ultimately
results in PMServer's client window writing the Clipboard data to the Vio
client's call-back queue.
You can see the similarity between DDE and Clipboard transactions used by
PMServer. Each transaction requires a series of distinct actions on the part
of the client and PMServer's client window, with the Queue Manager acting as
an intermediary. The Queue Manager is the layer that makes it possible for
PMServer's client window to receive messages from a client. The importance
of this layered approach will become more apparent in the following
discussion.
As described earlier, each DDE conversation is unique: it involves one DDE
client with one or more DDE servers, where each participant is a PM window.
If one of its Vio client applications wishes to be a DDE client, PMServer
must be able to represent it as a DDE client that can maintain a
conversation with one or more DDE servers. Conversely, PMServer must be able
to act as a DDE server for any conversation that one of its Vio applications
can support. And it must be able to provide either of these services to more
than one of these applications at the same time.
To add DDE support to PMServer, PMServer must be able to track each DDE
conversation. At first, this is easy. Every time a Vio client, such as
PMAccess, asks PMServer to initiate a DDE conversation on its behalf,
PMServer records the client's process ID, call-back queue handle, and DDE
application and topic names in a table (see Figure 1). Then, if any other PM
windows respond to the WinDdeInitiate made on the part of that client,
PMServer can store the window's handle in the table entry for that
conversation.
The process gets more complex when a DDE conversation is underway. Suppose
two Vio clients have initiated DDE conversations via PMServer to which
several PM windows have responded. Suppose further that one of the PM
windows has responded to both Vio clients, signaling that it will be a DDE
server to each. As each Vio client makes a request for data, PMServer will
post a WM_DDE_REQUEST to the server's window. The server window responds
with WM_DDE_DATA. But how will PMServer know which WM_DDE_DATA message is
associated with which DDE conversation and Vio client?
When a WM_DDE_DATA is received, there is no information in the DDESTRUCT
segment to identify the conversation. A DDE server assumes that the window
that initiated the conversation is the one destined to receive the data,
just as a DDE client assumes that a DDE server window is the process
supplying the data. Both WM_DDE_DATA messages are received by PMServer,
which is unable to distinguish between them, particularly since they were
posted by the same DDE server window. What's needed is a new layer that can
distinguish between and manage different DDE conversations, in the same way
that the Queue Manager thread manages messages sent from PMServer's Vio
clients.
Invisible Windows
The key is to create a new, invisible window for each DDE conversation that
the application engages in. Although you can include the DDE message
processing code in any application's primary window procedure, it would be
difficult to keep track of and manage several different DDE conversations.
Moreover, in the context of an application like PMServer, it's impossible.
It's easier to write two new types of window procedures--one dedicated
to being a DDE client and the other to being a DDE server. This procedure is
also good programming practice. We can encapsulate the details of processing
DDE messages in these two new windows. And since these windows do not need
to perform any I/O, their message processing code will be limited to
processing DDE messages.
In order to ensure efficiency, each new window will have its own message
queue and run in its own thread of execution. Although PMServer's Queue
Manager runs in its own thread, it does not use a PM message queue for
input. Thus, it is called a queueless or windowless thread. Each new window
will run in its own thread of execution and have its own message queue.
These are called message queue threads or, in PM terminology, object
windows.
After defining the window procedures for the new object windows, several
other tasks must be addressed: how PMServer will communicate with the object
window; how the object window will communicate with PMServer; how the object
window will communicate with the PMServer kernel client; when PMServer will
create an object window; and when PMServer will destroy an object window.
The first three are easy: PMServer and the object windows will communicate
by posting messages to each other via their PM queues. Moreover, an object
window can talk directly to a PMServer kernel client via the client's
call-back queue. What remains is to determine when PMServer will create and
destroy an object window and how an object window's window procedure is
defined.
Adding Object Windows
Since a DDE conversation takes place between a client and one or more
servers, the object windows need to be involved early. Thus, when PMServer
receives a request from one of its Vio clients to initiate a DDE
conversation, it creates the table entry described above, and follows this
by creating a new object window to manage the conversation. This is done by
allocating the object window's thread stack and calling _beginthread to
create the new thread. The new thread function begins by routinely creating
a new PM message queue and the invisible window. The new window notifies the
PMServer window procedure that it has been created and initiates a new DDE
conversation with a call to WinDdeInitiate.
Defining the object window's window procedure is fairly easy: the procedure
must be able to manage an ordinary DDE conversation but with the added twist
that the object window must deal with the PMServer Vio client. If the object
window receives any WM_DDE_INITIATEACK messages from other PM windows, it
posts a message to the Vio client's call-back queue, telling the Vio client
that a conversation has been initialized. The object window maintains a list
of DDE server windows in the table entry created by PMServer when the Vio
client first made the request.
When the Vio client requests DDE data, it posts a message to the PMServer
Queue Manager thread, which passes the request to the object window. The
object window then posts a WM_DDE_REQUEST to each DDE server. As the
responses are received (be they WM_DDE_DATA messages or negative
acknowledgments), it writes them to the Vio client's call-back queue. If the
Vio client wishes to be advised, the object window generates the
WM_DDE_ADVISE and posts the updates to the Vio client's call-back queue; the
Vio client later tells the object window to generate a WM_DDE_UNADVISE. When
a DDE server sends a WM_DDE_TERMINATE, the object window removes the
server's window handle from the table entry and notifies the Vio client if
the last DDE server has been removed from the table.
When the Vio client wishes to terminate the conversation, the object window
is no longer needed. It therefore posts a WM_DDE_TERMINATE to its remaining
DDE servers and a message to PMServer's window procedure indicating that it
is terminating. Finally, it posts a WM_CLOSE message to itself, causing the
window procedure to break out of its message processing loop and terminate
the thread. Then PMServer deallocates the thread's stack. (Note that
PMServer can also post a termination message to the object window.)
Although this discussion concerns DDE client object windows, most of it
applies to a DDE server object window as well (see Figure 2). PMServer could
maintain a table of Vio clients and DDE server windows, but this would
require that PMServer look up the related kernel client in the table every
time it receives a DDE message from a DDE client. Thus, it's simpler to
devise an object window procedure to be a DDE server and let it handle the
requests from a client for data, retrieving the data from a Vio client
acting as the DDE server and passing the data back to the DDE client. There
are two points to be aware of: first, a Vio client must notify PMServer that
it is capable of providing data as a DDE server on a specified application
and topic name. Otherwise, there is no way for PMServer to know which of its
clients are capable of doing so. Second, when a Vio client receives a
request for data from a DDE server object window in its call-back queue, it
should immediately respond by sending the data or a negative acknowledgment
via PMServer's Queue Manager. Otherwise, it will keep the DDE client at the
other end of the conversation waiting.
In the next part of this series, I'll add a DDE object window to PMServer so
that it can provide DDE data to Vio programs. I'll also present an expanded
version of PMAccess that can use PMServer's DDE capabilities to receive
requested data from PM programs.
────────────────────────────────────────────────────────────────────────────
Volume 5 - Number 4
────────────────────────────────────────────────────────────────────────────
An Introduction to Microsoft Windows Version 3.0: A Developer's Viewpoint
Michael Geary
The Microsoft Windows[tm] graphical environment Version 3.0 is here! The new
version of Windows1 presents a revamped memory management system and scores
of other improvements for both users and developers. Let's take a quick tour
of Version 3.0 from a user's perspective, and then we'll get into some
programming details. You will notice one difference even before you open the
box. There's now one version of Windows; Windows/286[tm] and Windows/386[tm]
have been merged so the same version works on all machines. (As in the past,
Windows quietly supports 8088/8086 machines, but runs well only on the
fastest of them.)
Once the box is open, the first thing you have to do is run SETUP. The
Windows 2.x SETUP program confronted the user with screen after screen of
questions about every detail of installation. Now, SETUP begins with a few
character-mode screens containing only the most critical information (about
your display, mouse, keyboard, network): enough to get Windows up and
running. Then a graphics screen comes up and the remainder of SETUP runs as
a Windows application to complete the installation. The improved look of 3.0
is already apparent here, at least on a VGA or 8514/a monitor. The buttons
are three-dimensional, there are color icons, and the proportional system
font is used everywhere.
The graphical part of SETUP has an advice window at the bottom of the screen
that always has a suggestion about what to do next. Full help is always
available too, via the F1 key or a click on a question mark icon. The help
system is a spiffed-up version of the hypertext help introduced in
Microsoft Word for Windows Version 1.0. Key phrases in the help text for
each topic are underlined; they lead to other help topics when you click
them. When you click on words and phrases with dotted underlines, a glossary
window pops up with the appropriate definition. The best part is that this
help system is used by all the mini-applications shipped with Windows, and
can also be incorporated into your own Windows applications.
When SETUP wants to update your CONFIG.SYS and AUTOEXEC.BAT files, it
doesn't just change them without your input as so many install programs do.
It offers you three choices: to update them automatically, to save the
edited versions under different names, or to edit them interactively in a
little dual-file editing window that shows the old and new versions.
The old SETUP had its own routine to install printers, which was completely
different from how you would install them later using Control Panel. The new
SETUP actually runs Control Panel in a guided session-that advice window is
still there-so you are already learning about the tools you will be using
later on. In the same way, SETUP runs Notepad to view the various README
files instead of using the funky file viewer in the old SETUP. The idea here
is to use the Windows tools instead of special purpose tools, so you have
immediate practice in using Windows.
The biggest change in SETUP doesn't become apparent until some time after
you have installed Windows 3.0. Every Windows 2.1 user who has changed their
display card or mouse knows what a pain that is-you have to reinstall
Windows from scratch. Many developers used the "slow boot" method of
installing Windows to make it easier to change drivers, but this method
wasn't really available to end users. All that has changed. Now Windows
always installs itself with the drivers in separate files, and there is a
version of SETUP you can run inside Windows to change your display, mouse,
keyboard, or network drivers (see Figure 1). If you ask for a driver that
isn't on your hard disk, SETUP will ask you for the appropriate diskette and
copy it to the hard disk. For drivers that have already been copied to the
hard disk, it will just switch back and forth using those copies. This gives
users the ability to change their Windows hardware configuration painlessly.
After Windows is fully set up, it can run in one of three modes: real mode,
standard mode, or 386 enhanced mode. Real mode is the "traditional" way of
running Windows with all the limitations it always had. Standard mode and
386 enhanced mode are where things get exciting: both exploit the protected
mode of the 286 and 386 processors. This permits Windows applications to
access all the physical memory in the machine. The 386 enhanced mode goes
even further; it provides virtual memory by swapping 4Kb pages out to disk.
It's neat to open up the About box the first time and see that you have
perhaps twice as much available memory as actually exists in your machine.
Windows by default runs in the best configuration it can. Unless other
protected-mode software prevents it, this usually means 386 enhanced mode on
a 386 or standard mode on a 286. You can always select a lesser mode if you
need to by running WIN /S for standard mode or WIN /R for real mode. The
most common reason for doing this would be to run some older Windows
application that doesn't work in protected mode; WIN /R allows most older
applications to run.
One surprise in installing the new Windows is that it doesn't have the PIF
directory with dozens of PIFs (Program Information Files) any more. PIFs are
not used as extensively as in the past: Windows will now run just about any
non-Windows application without a PIF. Part of what made this possible is
that Windows no longer attempts to run a non-Windows application in a
window, except in 386 enhanced mode. In real or standard mode, the only way
to run a non-Windows application is to switch it to a full screen. This is
no great loss, since so few non-Windows applications worked in a window
anyway. Besides eliminating the need for the "Directly modifies screen"
option in the PIF Editor, this change means that most applications can now
run with the same default settings founds in _DEFAULT.PIF. PIFs are now used
more to fine-tune settings for improved performance rather than to make it
possible for non-Windows applications to run at all. Each PIF contains
settings for real, standard and 386 enhanced mode. In 386 enhanced mode, the
advanced options in PIFEDIT.EXE (see Figure 2) permit you to customize the
application's behavior as much as you want. Non-Windows applications in 386
enhanced mode run a lot faster these days and seem to be more reliable.
There are two things that SETUP doesn't do for you that it really should.
One is set up a permanent swap file for 386 enhanced mode. If you're running
Windows on a 386 and haven't done this yet, run SWAPFILE now. Setting up a
permanent swap file of contiguous disk space does wonders for Windows'
performance. The other thing SETUP could do is put a WIN command at the end
of AUTOEXEC.BAT. Pretty radical, I know, but this is the first version of
Windows where it's really feasible to make it your primary environment.
Program Manager
If any feature of the old Windows has received more well-deserved criticism
than the old SETUP, it is the MS-DOS Executive. The good news is that the
MS-DOS Executive is no longer the primary shell used by Windows. Now,
Windows starts up in the Program Manager, an icon-based program starter.
Instead of presenting just a list of whatever files happen to be in the
current directory, the Program Manager's main window uses the Multiple
Document Interface (MDI) to present several program group windows in its
main window (see Figure 3). The program groups contain icons for
applications or documents. The Program Manager is able to look inside
Windows executables to find their icons; you can either use an application's
usual icon or substitute one from any other file.
SETUP creates an initial set of these program groups, which contains the
standard applications that ship with Windows. It also optionally scans your
hard disks looking for other applications and creates groups for them. Later
you can add or remove icons, rearrange the groups, and so on. The Program
Manager is easy to customize within its limitations: all it does is let you
manually start programs, one by one. It would be nice to be able to put
together a group of programs that work together and somehow just say "Start
this group." Ideally, you would even be able to save the state of several
running programs so you could restart them later in the same screen
arrangement with the same documents open. Third parties have filled in this
gap in Windows; for example, hDC FirstApps includes a program called Work
Sets, which saves the states of multiple applications and lets you restart
them as a group.
Perhaps a more serious omission in the Program Manager is the inability to
specify an initial directory for an application. The Program Manager
provides for only a single pathname with a program, usually the directory
containing the program itself. You can use this pathname to specify a
directory other than the one the program is in, but only if the program is
in your PATH. Windows does let you specify separate program and initial
directories, but only via PIFs for non-Windows applications!
File Manager
Unlike the MS-DOS Executive, the Program Manager has no file-management
facilities. These facilities are now in a separate program, File Manager.
The Windows File Manager is nearly identical to the OS/2 1.2 File Manager;
it uses MDI to allow several views of your file system. The File Manager
first shows one directory tree window that displays the first level
directories of the current drive. This window lets you expand and collapse
any level of the directory tree to see the directories you want (see Figure
4). To look at files, double-click (or press Enter) on a directory; another
window will open up. Once you get to that window, all the usual file
operations are available: copy, rename, delete, and so on. These operations
work the same way as in the MS-DOS Executive. However, to copy or move files
now, you can just grab them with the mouse and drag them into a new
directory.
The File Manager is a good program for manipulating files, but it does have
some annoying design flaws inherited from OS/2's File Manager. The worst is
having only one directory tree. If you want to view the tree of a different
drive, you can't open up a new tree window, you've got to switch drives on
the single tree window. In the process you lose track of which directories
were expanded and collapsed in the previous drive. Working with multiple
drives in the File Manager is an exercise in frustration. For DOS diehards
like me, the best bet may be to open a COMMAND.COM window and use good old
DOS commands. Especially in 386 enhanced mode, DOS command windows are
really handy. (A tip here-make yourself a COMMAND.PIF file and set the
Display Usage option to Windowed, so COMMAND.COM will start up in a window
instead of a full screen.) Even the MS-DOS Executive is still available.
It's in your Windows directory, little changed from previous versions except
for its use of lowercase in filenames. I still use the MS-DOS Executive more
often than I'd expect. It's not as fancy as File Manager but it starts up a
lot faster, since it doesn't have to read through all your directories. So a
File Manager fan I'm not: your mileage may vary.
The third new feature of the Windows shell is the Task List. You can open
this window by typing Ctrl-Esc or double-clicking on the screen background.
It's a simple list of all the current top-level application windows, with
buttons at the bottom to switch to a window, terminate a program, arrange
the icons at the bottom of the screen, or cascade or tile all application
windows. The tiling feature is handy, but it would be better if options were
provided to push window borders off the screen and to tile by rows instead
of columns. I prefer tiling in rows, because if you have two application
windows on a typical 640x480 screen, it's often more useful to put one above
the other rather than side by side.
Enhanced Look
Windows 3.0 applications look different from Version 2.1 applications. The
proportional system font gives them more readable text in their menus, title
bars, and dialog boxes, and the screen is a little more colorful because the
standard EGA and VGA drivers have been upgraded from eight colors to
sixteen. The Video 7 VGA and 8514/a drivers provide 256 colors at a time,
out of a much larger palette of available colors. Also, icons are now
full-color instead of black-and-white.
Three-dimensional push buttons, scroll bars, and minimize/maximize icons
contribute to the enhanced appearance. But it does seem a little strange to
see the system menu icon, flat as always, after all the other
three-dimensional features. Also, in some cases, the three-dimensional look
was implemented in a rather cumbersome way.
For the scroll bars and min/max icons, there are actually two bitmap
resources for each item-one for the normal state and one for the pressed
state. It's quite easy to produce a good three-dimensional button effect
without two different bitmaps. That's how push buttons are implemented: you
create a bitmap that includes just the "surface" of the button, not the
edges. Surround the bitmap with lighter pixels on the top and left and
darker pixels on the bottom and right. To "push" the button, slide the
"surface" down and to the right and redraw some darker pixels on the top and
left. Although it takes some work to write this routine, you have your
three-dimensional effect working consistently, using any bitmap you like.
I'd recommend this approach for your own programs rather than the two-bitmap
approach.
New and Improved Applications
Most of the mini-applications packaged with Windows have been improved; some
were rewritten entirely. No one will be too sorry to learn that Paint is
gone, replaced by the much better Windows Paintbrush from ZSoft. Paintbrush
isn't without flaws. The Zoom In feature is absurd for small images-the
zoomed image is crammed into the same tiny space as the original image,
leaving hardly any room to edit the pixels. And don't even try to paste in a
full screen image captured with the PrtSc key-it will get clipped to the
current window size. But Paintbrush supports color and larger images, and it
can create BMP files as well as its own PCX format. I'll take it over Paint
any day.
The old Terminal is also gone, replaced by a new terminal program from
Future Soft. It's a much more capable program, with modem command strings
that can be customized, definable function keys, VT100[tm] emulation, and
XMODEM and Kermit file transfers. My little Hewlett-Packard 48SX calculator
happens to speak Kermit too, so I plugged it into the new Terminal and they
happily sent files back and forth. The old Terminal wasn't good for much,
but it did have one feature that I really liked-the scrollback buffer would
save up to 999 lines. The new Terminal only allows up to 399 lines of
scrollback. Another curiosity is that the Modem Commands dialog box lets you
specify a string to put the modem into auto-answer mode, but there is no
command anywhere to let you use this string. If you want to answer a call,
you'll most likely have to type in a Hayes "AT" command, like ATS0=1,
yourself.
Control Panel has been redone, and everything is more logically organized.
In particular, setting up printers is done far more sensibly. You can set
them up from one dialog box instead of having to bounce back and forth
between the Printers and Connections dialogs as in the old Control Panel.
Setting up screen colors is simpler, too. You can choose from a dozen
predefined color schemes, or you can customize each screen element (see
Figure 5, and the sidebar "WIN.INI Color Settings").
There is now a keyboard speed setting in Control Panel, but anyone who has
written or used other programs to control keyboard speed will be surprised
to see there's only one speed setting. PC keyboards have two settings:
initial delay and repeat rate. Control Panel unfortunately always sets the
initial delay to be somewhat on the slow side. To solve this, remove the
KeyboardSpeed= line in your WIN.INI file, which will disable the Windows
keyboard speed setting. Then simply put your favorite key speed setting
program in your AUTOEXEC.BAT. It's best if this is the kind of program that
just sets the speed in the keyboard hardware, not a program that stays
resident and uses timer interrupts and such. Doing this lets Windows leave
your speed setting alone. If you ever click open the Keyboard dialog box in
Control Panel, Windows will go back to its own setting-even if you Cancel
the dialog box.
Other new mini-applications include Recorder, a program that uses Windows'
journaling hooks to record and play back keyboard and mouse activity. It's
reasonably easy to use and very useful for recording quick shortcuts and
longer sessions for testing or demos. Print Manager replaces the old Print
Spooler and provides more control over the print jobs. Notepad now supports
files of almost 64Kb. Clipboard has several new options, including the
ability to save and load files containing the clipboard contents. Calculator
can now switch between being a full-fledged scientific/programmer's
calculator and a simple "four-function" model. And not to be left out, Clock
now has an appropriately hard-to-read digital option. Finally, there's a
dangerously addictive Solitaire program with terrific graphics (see Figure
6).
Developing in Windows
The big news in Windows 3.0 for developers is of course protected mode.
Windows applications are finally free of many of the memory constraints that
bogged down previous versions of Windows. True, Windows 2.x, with its
support for EMS and XMS, was an improvement over 1.03, but it did not solve
The Problem: you could buy all the memory you wanted, but you couldn't get
at it with a GlobalAlloc. Working with memory that's constantly moving has
terrorized a generation of Windows programmers. Show me a Windows programmer
who really likes GMEM_MOVEABLE and I'll show you someone who hasn't seen
their first Invalid Global Heap.
In simplest terms, running in protected mode means that all extended memory
is directly available through normal calls like GlobalAlloc and GlobalLock.
Protected mode permits any application to allocate as much memory as it
needs, up to the limit of physical memory, unlike EMS, where more
applications can be run but each application is still subject to the 1Mb
limit. And in 386 enhanced mode, you can go beyond the limit of physical
memory to allocate virtual memory, which is swapped to disk when physical
memory is overcommitted. Either way, the same memory management functions
that a Windows application used in real mode now work in protected mode. In
fact, normal well-behaved Windows code will run identically in both modes.
The strange thing is that the handle-based movable memory system is quite
literally a simulation in real mode of protected-mode addressing. The
protected mode of the 286 and 386 provides direct hardware support for the
kind of memory management that Windows provides, without the hassles on the
application programming side. If it had been planned this way from the
start, and if (a big if) real mode support could be dropped, Windows
programming could be a lot simpler. A lot of the extra memory management
work you do in traditional Windows programming is simply to help Windows
simulate protected mode in real mode. This happened, as they say, "for
historical reasons."
Back in the days when 64Kb was more than enough memory, our CP/M and Apple
II systems addressed it in the simplest way: linear physical addressing.
Each byte of memory had a unique physical address, and you accessed memory
directly by using its physical address: no mapping, no funny tricks (see
Figure 7). Since the width of the address registers determines how much
memory you can access, an 8080/Z80 machine with a 16-bit address could use
216, or 65,536 (64Kb) bytes of memory. Addresses 0 (0000H) and 65,535
(FFFFH) were indeed the first and last bytes of memory (assuming you could
afford a full 64Kb). Ah, life was simple.
Then it was time to put the 8086 (and 8088) together, and the designers at
Intel got clever. Too clever, some might say. They wanted to provide access
to more than 64Kb, while at the same time sticking with 16-bit registers to
keep things simple-simple for the chip, that is. So take a 16-bit register
and shift it four bits, and you can address a full megabyte (220 bytes). The
only problem is that you can only address in 16-byte increments. But add in
another 16-bit register (not shifted) and you have our beloved 16:16 bit
segmented addressing system (see Figure 8). The less said about this, the
better.
Memory fragmentation is a big problem in most memory management schemes;
memory compaction with movable blocks is one sure way to avoid
fragmentation. But movable memory means that you can't use just conventional
pointers-the memory you're pointing to might move (as every Windows
programmer is painfully aware). Due to the relative difficulty of
programming this kind of system, most memory management packages have opted
for the fixed allocation approach even if it means suffering some
fragmentation. The C run-time library functions malloc and free are a good
example. They're easy to use, but you can't avoid fragmentation with these.
Graphical user interfaces gobble up memory compared to simple
character-based programs, so both Windows and the Macintosh use handle-based
movable memory systems. This permits developers to manage memory without
incurring fragmentation and write larger programs with more consistent
memory behavior. However, these two systems present the movable memory to
the application programmer differently. Both have a table of master
pointers, pointing to each movable block. When a block gets moved, its
master pointer gets updated. The handle you get back when you allocate a
block is in some form an index into this master pointer table. But from the
application programmer's point of view, the Mac's method is more convenient
by far: the handle for a movable block is literally a pointer to the master
pointer for that block. All you have to do to get to a block's data is a
double indirection instead of the single indirection you would use with a
straight pointer. In C, you simply use ** instead of * in what otherwise
looks like a normal pointer dereference. You have to be careful-the memory
you are looking at is not locked down and could move if you somehow cause a
compaction-but all in all it's a wonderfully convenient technique. There are
Lock and Unlock calls for when you want to play it safe, but they don't need
to be used all that often.
In Windows, on the other hand, you go through a lot more aggravation dealing
with movable memory. You aren't allowed to do much with the handle you get
back from GlobalAlloc directly. (For a fixed segment, of course, the return
value from GlobalAlloc is actually the segment address, and you can directly
construct a far pointer from that.) You have to pass the handle to
GlobalLock or GlobalHandle to get a pointer to the data (see Figure 9). A
global handle is in fact an offset into a table of master pointers, found in
the ever-mysterious BurgerMaster segment. With near and far pointers to deal
with, a simple double indirection may not have flown, but some kind of
simple macro, or even a special C pointer type, could have provided direct
access to movable data. The Windows designers chose, however, to make you go
through the explicit GlobalLock call to get to the data. This has the
advantage of always locking the data when you're using it, but I think
anyone who has programmed both the Mac and Windows will agree that the Mac's
double indirection is a lot easier to deal with.
GlobalLock is a pain, but there may have been method in Microsoft's madness.
In protected mode the 286 uses mapped memory, not direct physical
addressing. 16:16-bit real-mode segmented addressing uses a direct physical
address calculation. In protected mode the segment becomes instead a
selector, which is really an index into a table where the physical address
can be found. Two tables, the Global Descriptor Table (GDT) and Local
Descriptor Table (LDT), are used by the protected mode addressing hardware
along with the segment selector to find the physical address (see Figure
10).
If this sounds a lot like BurgerMaster and GlobalLock to you, you're right.
Just as Windows in real mode keeps BurgerMaster up to date with segment
movement, it keeps the GDT and LDT up to date in protected mode. There is
still a global heap with segments that move around, but application software
doesn't have to deal with the segment movement. Each reference to memory
goes through the addressing hardware, which does the GDT/LDT lookup and
calculates the correct physical address. It's just like what GlobalLock does
for you when it looks up the handle in BurgerMaster, but it happens behind
the scenes.
This means that protected mode provides the benefits of movable memory
without the drawbacks. How can Windows take advantage of it in a way that is
reasonably transparent to existing code? First, when you call
GlobalAlloc(GMEM_MOVEABLE), the "handle" you get back is actually a
selector. You could go ahead and use this selector to access the memory, but
if your code was written for real mode, it regards the handle as a "magic
cookie:" an identifier and nothing more. You pass the handle to GlobalLock
when you want the address, and GlobalLock returns the address-the same
selector! (It's formatted into a far pointer, of course, and the low order
bit of the selector is turned on. In the "handle" form of the selector, the
low order bit was off. There is a reason for this, as we'll see.)
GlobalLock can get away with this because the protected-mode memory
addressing hardware is doing its work. If a selector is automatically
dereferenced through the GDT/LDT as needed, there isn't much left for
GlobalLock to do but convert it into a far pointer. Even the locking aspect
goes out the window. In real mode, a segment is locked to keep it from
moving or being discarded. The GDT and LDT of protected mode make these
operations redundant, because they indicate whether a segment is in memory
at all as well as its physical address if it is. If you try to reference a
segment that isn't in memory, the hardware traps that and Windows takes care
of it. So there's not much need for a lock count, and GlobalLock's work is
easy.
There's one case in real mode where GlobalLock is nearly this lazy. A
GMEM_FIXED segment isn't going to move, so Windows doesn't bother with a
BurgerMaster entry. When you call GlobalAlloc(GMEM_FIXED) in real mode, the
"handle" you get back is in fact the actual segment address. All GlobalLock
does with it is construct a far pointer with the segment and a zero offset.
There is no low-order-bit twiddling as I mentioned above, because the
"handle" already has the low order bit on. This is how GlobalLock figures
out whether the "handle" you pass it is a BurgerMaster offset or a
GMEM_FIXED segment. If the low order bit is off, it's a handle; on, it's a
segment. That is, handles are even, segments are odd. Microsoft might prefer
that you didn't know this, because you might write code that inadvertently
or even-dare I say-intentionally depends on it, and they might want to
change it in the future. So you didn't hear it from me. But for the time
being, that is how it works. (I'm ignoring several other special cases that
GlobalLock has to check.)
Because segments do not move around (at least their selectors don't change)
in protected mode, all sorts of other simplifying assumptions are possible.
Reload thunks, those little SAR/JMP sequences generated for
movable/discardable far functions in real mode, go away completely. You need
these in real mode because there has to be a fixed entry point, even for a
movable function, so you can stash its address away in a pointer variable,
and because there has to be a way to know when to load in a segment that's
not in memory. The SAR/JMP changes to an INT 3FH for functions that aren't
in memory; that INT 3FH is how Windows can magically load in a discarded
code segment when it's called. But in protected mode, every segment looks
like it's FIXED because it always has the same selector. Also, if you try to
reference a segment that isn't in memory, the CPU will cause a Not Present
exception interrupt, which tells Windows when to load in your segment.
Reload thunks are just part of Windows' real-mode emulation of
protected-mode memory management, so you won't find any if you're not in
real mode.
You can play similar tricks yourself. If you know you are in protected mode,
you don't have to lock and unlock segments all the time. You get a chunk of
memory with GlobalAlloc and perform just a single GlobalLock call on it-or
just make a pointer from the segment selector returned by a
GlobalAlloc(GMEM_FIXED)-and keep using that pointer until you are finished
with the memory. You won't be interfering with Windows memory management as
you would be in real mode; remember that Windows can still move physical
memory around all it likes and never tell you about it, as long as it keeps
the GDT and LDT up to date. (There are functions, GlobalFix and
GlobalPageLock, that actually lock down a segment in the physical linear
address space, and they would of course cause the same fragmentation
problems as always. Few programs should ever have to use these.)
This Can't Be Real!
Here's the bad news. Real mode is still with us. There's no mercy here. Just
when we discover how nice it is for our applications to have enough memory
to run and we don't have to worry about dangling far pointers, some user
pops our balloon by starting Windows with a WIN /R command, or by running on
an 8088 or 8086 (see Figure 11). This puts us back where we started,
complete with all the hassles of EMS. Windows does run fairly well on a fast
(10MHz or better) 8086 machine, but it can't run protected mode on one. So
we still have real mode to kick around.
There are some improvements in Windows 3.0 for managing memory in real mode.
GlobalLock and GlobalUnlock have a completely undeserved reputation for
speed. But you don't have to use them as much-there's an alternative called
the private handle table, which is a table in your DGROUP that contains a
list of global segment addresses. A DefineHandleTable call tells Windows
where your table is. When you allocate a GMEM_MOVEABLE segment, call
GlobalHandle to get its current (unlocked) segment address, and put this
segment address in an empty spot in your table. Internally in your code, you
then use a private handle of your own, which is really an offset or a
pointer into your table. You can define this private handle any way you
want, as long as it gets you to a table entry. Whenever Windows moves a
segment in memory, it scans all private handle tables looking for references
to that segment and updates them. When you need to get to a segment in
memory, use your private handle to get to the correct entry in your table,
and you will find the current segment address. The private handle table
really isn't anything new; it's been in Windows since Version 2.0, it's just
not a secret anymore.
The one catch with the private handle table is, of course, that its
addresses are the addresses of unlocked segments. Segments can still move
around in real mode; therefore, a far pointer derived from the handle table
has a lifetime shorter than a mayfly. In addition to the well-known
situations in which memory can move, any far call to a discardable segment
can also move memory. So don't hold onto one of these pointers for very
long. To protect against this, I add a nested block in C with the segment
pointer declared inside that block. The block is as short as possible and
includes only the code that needs to use the pointer. Since the pointer
variable name is local to the block, the compiler will give me an error
message if I use this pointer farther down in my code after I might have
made a far call. Now, alarms go off in my head when I see a pair of curly
braces without some keyword above them. It looks like there is a missing
statement above the opening brace. One solution is to add a new keyword to
the C language, scope, that does nothing but look better than a blank line.
Code then looks like this (assuming a SEGDEREF macro that takes a private
handle and returns the current far pointer to a segment):
// define a no-op placeholder keyword
#define scope
o
o
o
// We are inside a function with a private handle
// variable "hfoo" that points to a segment
// containing a FOO structure, and we need to copy
// a couple of items out of the structure into
// local variables.
// The "scope" keyword does nothing, but clarifies
// the purpose of the extra curly braces.
scope
{
LPFOO lpfoo = SEGDEREF(hfoo);
item1 = lpfoo->item1;
item2 = lpfoo->item2;
}
// Now item1 and item2 have our data. The "lpfoo"
// pointer variable is safely undefined, so we
// can't accidentally use it after memory moves.
The private handle table works in both real and protected mode. In protected
mode, Windows never bothers updating the table, but then the "segments" it
gives you are selectors that aren't going to change anyway. At least that's
true for segments of 64Kb or less. With segments greater than 64Kb, Windows
may have to give you back a different selector on a GlobalReAlloc call,
because it has to find a contiguous range of selectors to allow addressing
the entire segment. The private handle table doesn't take this into account,
but other than this, it works in either mode. If you want to be a Good
Programmer, you write your code so it runs in both real and protected mode.
For many programs, all you have to do is code as you always have for real
mode, avoiding any violations of the protected-mode rules set forth in the
Software Development Kit (SDK).
You start running into problems with large applications. When your programs
get past a certain size, there's just no room left in real mode and you have
to resort to drastic measures like supporting EMS or writing your own disk
swapping routines. It's tempting just to switch to protected mode completely
and avoid all that, but then you'd have to give up the 8088/8086 market.
The best approach is to support all memory modes whenever it's possible. If
your application is small enough to run reasonably well in real mode, by all
means code it so that it's compatible with both modes. Or if you already
have code that uses EMS or swaps to disk to let you run a bigger app in real
mode, keep that code around-it can still serve you in real mode even if it
isn't needed in protected mode. A typical approach here is to try
GlobalAlloc first, and if that fails try allocating EMS or swapping
something else out to disk. Under 3.0, this gives you the best of both
worlds. In real mode, it will work just like under 2.1. In protected mode,
the GlobalAlloc will fail much less often because you have so much memory
available. When it does, you would skip EMS because it isn't available in
protected mode and your disk swapping code would kick in.
But if your app is bursting at the seams in real mode, if you're facing
months of tuning for real-mode performance to be acceptable, if you're
having to write memory management code instead of writing your application,
maybe it's time to switch to protected-mode-only programming. You won't run
on 8086/8088 machines, but an application that large probably wouldn't run
very well on them anyway. If you're really backed into a corner with real
mode, go ahead and require protected mode. But be warned: if you are lucky
enough to be able to write protected-mode-only code, you will never, ever
want to go back to coding for real mode.
Even in protected mode, there are still places you can run out of memory
long before global memory is exhausted. Your own local heap is one, of
course, but you should also consider GDI's local heap and USER's local heap.
USER's local heap usually runs out first. Every window created uses space in
USER's heap, as does the text of each menu item and window title. You can
see this in action if you have a lot of icons and program groups in Program
Manager-when I added a bunch of icons to my original groups, the Free System
Resources percentage displayed in Program Manager's About box dropped from
close to 90 percent down to 65 percent. (The Free System Resources number is
the percentage of free space in either USER's heap or GDI's heap, whichever
is currently more crowded.) Those innocent little icons were eating up 25
percent of USER's available heap space!
Standard versus Enhanced Mode
Although standard mode and 386 enhanced mode look nearly identical from the
point of view of a Windows application, they're implemented quite
differently. In standard mode, a simple DOS extender, DOSX.EXE, handles
switching between real and protected mode, selector allocation, and other
services that Windows needs (see Figure 12). DOSX does not provide
multitasking for non-Windows applications, nor does it create virtual
machines-it has to be compatible with both 286 and 386. It does use a few
386-specific features like faster mode switching when it is on a 386.
The 386 enhanced mode is completely different. Instead of the simple
DOSX.EXE, there's a Virtual Machine Manager that creates virtual machines
and multitasks them, as well as a set of virtual device drivers needed for
virtual machines (see Figure 13). This, of course, was developed from the
Windows/386 code, but goes beyond it. In particular, Windows 3.0 virtual
machines include not only the virtual 8086 environment that the 386 hardware
provides, but also an optional protected-mode portion. The first virtual
machine created-the System VM-has a protected-mode portion used for running
Windows applications. Other virtual machines may have only an 8086 (DOS)
portion, but they can just as easily have a protected-mode portion, allowing
other kinds of protected-mode applications to run in 386 enhanced mode.
Proportional System Font
After protected mode, the change in Version 3.0 that may affect the most
existing code is the switch to a proportional system font. At the very
least, dialog boxes need to be redone to allow for changes in text string
size. Any code that assumed a fixed-pitch system font-for example, code that
gets the height and width of one character and multiplies that by the number
of characters to calculate string length-will have to change. You must now
use GetTextExtent or GetCharWidth to calculate string lengths. The Windows
edit control code had to be completely rewritten for Version 3.0, largely
because of the switch to the proportional font.
You may also have some code that knows the physical size of the dialog units
used in specifying dialog coordinates. For example, I've worked on a couple
of programs that created windows that acted a lot like dialog boxes. I kept
track of window sizes in units directly proportional to the dialog unit
size, to achieve the same modest degree of device-independence that dialog
boxes have. I just did a GetTextMetrics on the system font and divided the
height by eight and the width by four. I thought this might be OK for a
proportional system, if TextMetric.tmAveWidth was used instead of
TextMetric.tmMaxWidth. Not quite. Windows does a special calculation of the
system font's average width (tmAveWidth obviously wasn't good enough). To
get the system font average height and width, you have to call
GetDialogBaseUnits, still dividing by eight and four if you want the dialog
units.
Since you also can't use spaces to line up columns of text with a
proportional font as you can with a fixed-pitch font, list boxes and edit
controls now support tab stops. Send a LB_SETTABSTOPS or EM_SETTABSTOPS to
specify the tab positions, then embed tab characters in your text to line up
your columns. For text you are painting explicitly in your own code, use the
new GetTabbedTextExtent and TabbedTextOut functions to support your own tab
stops.
The proportional system font will cause extra coding pain for some
applications, but for others it could save a huge amount of work. Because
all the standard control classes now support the proportional system font,
they support all fonts. (All bitmap fonts, anyway.) You can send a
WM_SETFONT message to any control to select its display font and WM_GETFONT
to find out a control's current font. If you were about to write your own
edit class just because you needed to support proportional fonts, stop. The
same is true for list boxes and the other classes. They will all work with
any font you can name, making the standard controls useful in more cases
than before.
GDI Goodies
One of the most visible changes in the new Windows is the greatly increased
use of color, especially color bitmaps. The old Windows bitmap format was
not quite good enough for providing this extra color. In the old format, a
color bitmap was organized to match a specific display device; monochrome
bitmaps were portable, but color bitmaps weren't, which is why you didn't
see them very often.
The bitmap portability problem is solved in Windows 3.0 by the appropriately
named Device Independent Bitmap (DIB). Instead of being tied to a specific
display, the DIB is in a more general format that is portable across devices
but can still be converted reasonably quickly into any particular display
format. Each pixel in a DIB can be represented by 1 bit (monochrome), 4 bits
(16-color EGA/VGA), 8 bits (256-color Super VGA/8514), or 24 bits (the
software is ahead of the hardware for a change, at least for display
adapters in common use). In a 24-bit-per-pixel DIB, the 3 bytes representing
each pixel directly specify an RGB color value. When there are fewer than 24
bits per pixel, the bits representing each pixel are actually an index into
a color table at the beginning of the bitmap. This color table provides the
full RGB values, allowing a bitmap to be saved in a more compact format as
long as it uses a small number of colors, even if those colors themselves
might be composite.
DIBs are easy to create and use. When you save a file in Windows Paintbrush,
the Options button gives you the choice of any of the four DIB formats as
well as the PCX format. The four DIB formats use the BMP extension even
though they are not in the old bitmap format; you can make them into
resources and use LoadBitmap on them just like before. LoadBitmap converts
the bitmap from DIB format to the appropriate display format when it loads
it. Alternatively, there is a set of functions-CreateDIBitmap,
CreateDIBPatternBrush, GetDIBits, SetDIBits-that can be used to create
memory bitmaps and pattern brushes from DIBs directly. Another option is to
skip the memory bitmap step completely and use SetDIBitsToDevice
(StretchDIBits if stretching is required) to transfer the DIB to the
destination device directly. This saves memory, but it can be slower for
bitmaps that are painted repeatedly because the format must be converted
each time the bitmap is drawn. Another memory saver is the compression in
DIBs-there are several run-length encoding (RLE) methods used to save space.
The new multiple resolution icons and cursors are similar to DIBs, but
implemented differently. When you create an icon or cursor using SDKPaint,
you have the option of including multiple images for different display
devices, tuning the images for each device. Icons can be in full color on
color devices, but cursors are still limited to black-and-white. LoadCursor
and LoadIcon select the best match for the current display out of the images
you supply. As before, if there is not an exact match, the functions choose
the best match they can find and scale it to fit the resolution.
It's convenient that DIB pixels can be lookups into a list of RGB color
values, because that's exactly the capability many display cards have-there
are many color choices, but only a fraction of them can be shown onscreen at
one time. For example, 8514 and Super VGA monitors can display 256
simultaneous colors out of 262,144 possible choices. It's better to be able
to specify which colors go into that usable palette; you can do a lot more
with 256 colors when you get to pick them yourself. The only problem is that
in Windows, you can run several applications at once. If your application
changed the display palette, it would disturb every other application since
they're all sharing one screen.
Palette Manager
The new Windows Palette Manager was designed for this. This facility
mediates among applications that use many colors and need to modify the
display palette. Applications can create their own logical palettes
containing color values they need, then use those color values in normal GDI
calls. The Palette Manager maintains a separate system palette with the
current physical display colors. The active application gets first choice of
those colors; any leftover colors are used for inactive applications. The
Palette Manager matches the logical palettes with the available physical
colors, giving inactive applications the closest matches it can find after
the active application has taken all the colors it needs. To allow title
bars, menus, and the like to be painted in their normal colors, 20 colors
are reserved. That way, even if the active application takes all the colors
it can get, the Windows screen will pretty much look correct. An application
with a great many colors can, with a little extra work, even temporarily
steal all 20 system colors, except black and white.
Using the Palette Manager is fairly easy. You create a logical palette with
CreatePalette, which takes a list of RGB colors and transforms them into a
GDI logical palette object. Select the palette into a device context with
SelectPalette, and call RealizePalette to have the Palette Manager match
your colors with the system palette and install the system colors it needs
for you. Then you can use your palette in GDI functions like
CreateSolidBrush by using specially coded color values. The color parameter
to such functions is now called a COLORREF, and its high order byte tells
what kind of color reference it is. A zero in the high order byte indicates
that the color reference is an ordinary RGB color. You can still use a
direct RGB color in Version 3.0 when you're working with the Palette
Manager, but you will probably prefer to use the palette that you set up. If
the high byte has a value of 1, the low order 16 bits of the COLORREF are an
index into the list of colors in your palette. A device-independent bitmap
can use palette colors by listing palette indices in its color table instead
of RGB colors.
If you're running on a 24-bit color device, the palette system gets in the
way. Why bother with a list of colors when the entire spectrum of RGB colors
is available directly? The best method of managing colors would be to
specify an RGB color for the 24-bit case but still be able to use your
logical palette when on a palette device. There is a way to do that. When
the high order byte of a COLORREF contains 2, the low 3 bytes are
interpreted as a palette-relative RGB value. On a 24-bit system, this is
used directly as a 24-bit RGB value. On a palette system, the Palette
Manager matches this RGB value with your logical palette and uses that
palette entry as if you had specified its index directly. Watch out for the
high order byte when converting existing code! That byte was ignored in the
past, but if you fiddle with a COLORREF-for example, performing a binary NOT
to obtain the complement of a color-be sure to zero out the high order byte
if the COLORREF is supposed to be an RGB color.
Odds and Ends
With the increased use of color, there's a problem in using the FloodFill
function. FloodFill expects a border color parameter and fills outward in
all directions until it encounters that color. This is fine for monochrome
images, but in color you may have a border of several different colors and
an area with a single color that you want to change to a different color.
For color, use the ExtFloodFill function; it fills all pixels that match a
given color, instead of those that don't match a border color. This function
is like FloodFill but takes one more parameter specifying the fill mode:
FLOODFILLBORDER, which makes the function work like FloodFill, or
FLOODFILLSURFACE, which will perform the new filling method I just
described. You can see FLOODFILLSURFACE used if you try the fill tool in
Paintbrush.
There are of course new META_xxxxxx metafile records to support all the new
GDI functions, including those for palette management and DIBs. A serious
omission in Windows 2.1 meant that a metafile could create brushes, pens,
and other objects but could not delete them, leading to nasty memory leaks
as objects were created but not destroyed. This has been corrected. The new
META_DELETEOBJECT record allows you to destroy these objects.
MDI
My favorite Version 3.0 API feature is one that users will never notice, but
is a boon for developers: support for the MDI. For the last couple of years
developers have been told to use MDI, but there was practically no Windows
MDI support other than for child windows. Adding MDI support to an
application could mean an additional thousand lines of code or more.
Fortunately there's now clean, simple MDI support in the Windows API. There
is one new predefined window class, MDIClient, four new functions, and a
dozen new messages.
Setting up MDI is easy: create the outermost frame window as you normally
would, then create a window of the MDIClient class in its client area. This
window is the MDI workspace. It can fill up the frame window's client area
or you can leave room for objects such as a status line (see Figure 4). To
create each document window inside the MDI workspace, you send the MDIClient
window a WM_MDICREATE message. Similarly, you destroy a document window by
sending a WM_MDIDESTROY to the MDIClient window. Instead of calling
DefWindowProc, the frame window calls DefFrameProc, and the document windows
call DefMDIChildProc. The only thing remaining is to include a call to
TranslateMDISysAccel in your main message loop. All the required MDI
behavior is handled by the MDIClient window and the three new functions:
even the window's menu is maintained properly by MDI. Anyone who has
programmed MDI under 2.1, or more likely has left MDI support out of their
application because of its difficulty, will be happy when they see that this
support is built into Windows. (For a more detailed discussion of the
Windows 3.0 MDI API, see "A New Multiple Document Interface API Simplifies
MDI Application Development," p. 53-Ed.)
Control Class Enhancements
As I mentioned, the Edit control class now supports proportional fonts.
Other things work now that never did before, too. Did you ever try the
ES_CENTER or ES_RIGHT styles in previous releases? Text would show up in the
wrong places and sometimes even would jump out of your way when you tried to
click on it. In 3.0, these styles work correctly-not as entertaining
perhaps, but certainly more useful.
New Edit control styles make it easy to do some things that before you had
to subclass or worse: ES_UPPERCASE and ES_LOWERCASE force input to uppercase
or lowercase; ES_PASSWORD prevents normal display of the edit text,
displaying asterisks instead (see Figure 14). If you don't like the
asterisk, you can pick a different character with an EM_SETPASSWORDCHAR
message. Other new messages include the EM_SETTABSTOPS that I mentioned and
EM_EMPTYUNDOBUFFER, which prevents a subsequent EM_UNDO or Alt-Backspace
keystroke from having any effect (until the next undoable edit, of course).
ListBox Class
The ListBox class wasn't completely rewritten like Edit was, but it did
receive more new features (see Figure 15). The first new feature is the
owner draw facility. Items in a list box can now be anything you draw,
instead of having to be text strings. The ListBox class still takes care of
the keyboard and mouse interface, scrolling and keeping track of what lines
are visible and it still sends you a simple WM_DRAWITEM message for each
line to be painted. You can have the owner draw list box store a list of
strings for you by using the LBS_HASSTRINGS style. Without this style, the
list box will store 32 bits of data for each item. You can put what you want
in them, including a far pointer to a string, a far pointer to any data
structure of your choosing, or any kind of arbitrary 32-bit value. The
LB_ADDITEM and LB_INSERTITEM messages take this 32-bit value in lParam in
place of the normal string pointer.
Each WM_DRAWITEM includes this 32-bit value along with the other information
it passes; you can also look at it with a LB_GETITEMDATA message or change
it with LB_SETITEMDATA. These two messages work with any list box, not just
owner draw list boxes. This is really handy when you want to associate some
invisible data with each item, but you don't need anything fancier than
strings drawn in the list box. Owner draw would be overkill; just do a
LB_SETITEMDATA on an ordinary list box and you're all set.
The LBS_SORT style gets a little tricky with owner draw list boxes. If
LBS_HASSTRINGS is set, it's just as easy as before-Windows will take care of
the sorting on its own. But without LBS_HASSTRINGS, there's no way for
Windows to determine the sort order. So when you send an LB_ADDSTRING
message to insert an entry, Windows sends back one or more WM_COMPAREITEM
messages to the list box's owner. Like the compare routine called back by
the C run-time library's qsort function, this message lets you specify the
sort ordering by comparing pairs of items.
Items in owner draw list boxes can all be the same height (as in a normal
list box) or different heights. You select these items using the
LBS_OWNERDRAWFIXED and LBS_OWNERDRAWVARIABLE window styles. The list box
returns WM_MEASUREITEM messages to determine the size of the list items. A
fixed-height list box sends one of these messages; a variable-height box
sends one for each item.
List boxes can now be scrolled horizontally. To add a horizontal scroll bar
to a normal (vertically scrolling) list box, just add the WS_HSCROLL style,
then send a LB_SETHORIZONTALEXTENT message to specify the scroll range. This
is a simple feature, but it should be useful to many developers-I had to
code my own list box class last year only because the standard class
wouldn't scroll horizontally.
You can also make a list box that's similar to the old MS-DOS Executive in
"short view"-a horizontally scrolling block of items in rows and columns in
which you can move the cursor in all four directions. Use the
LBS_MULTICOLUMN style and send the list box an LB_SETCOLUMNWIDTH message to
tell it how wide the columns are.
It's now easier to handle list box keyboard input. If you want to handle any
of the list box's keystrokes, give it the LBS_WANTKEYBOARDINPUT style. The
list box's owner is then sent WM_VKTOITEM and WM_CHARTOITEM messages, which
correspond to WM_KEYDOWN and WM_CHAR messages but include the number of the
item in the list box. You can then take the default action or modify it as
needed. As part of your processing, you might want to use the new
LB_FINDSTRING message to locate an item by text value and LB_SETTOPINDEX to
scroll the list box to a specific location.
Multiple selection list boxes have changed, mostly for the better. If you
used the LBS_MULTIPLESEL style in the past, the user always had to hold the
Shift or Ctrl key down to perform a multiple selection. This was really
annoying; you might take a couple of minutes carefully selecting various
items in a list, but then one unshifted mouse click or cursor key would wipe
out that selection. Some developers took a saner approach: they subclassed
list boxes so that a mouse click toggled the selection on the clicked item
without affecting any other selection, and cursor keys moved the focus
rectangle as if the Ctrl key were down; the user pressed the space bar to
toggle an item's selection. Unfortunately, this approach wasn't standard and
whether the ease of use in this approach was worth the inconsistency was
arguable. But now all LBS_MULTIPLESEL list boxes work as I just described.
In a variation of multiple selection list boxes, the LBS_EXTENDEDSEL style,
an unshifted click or arrow key removes all previous selections, selecting
the item clicked or moved to. Or a Ctrl-click adds individual items to a
discontinuous selection. You can also select a continuous range using the
Ctrl-arrow key or just by dragging the mouse across a range of items. Other
than the mouse dragging, this is all nearly identical to the old
LBS_MULTIPLESEL behavior. The idea is to make it easy to select a range of
items at once with the keyboard or the mouse. It does get a little tricky if
you want to make a discontinuous selection with the keyboard. You have to
press Shift-F8 first to put the list box into a special selection mode. Then
the keyboard works as it does when you use LBS_MULTIPLESEL, where arrow keys
just move the selection cursor and the space bar sets or clears the
selection on the current item.
I'm not sure why there are two different kinds of multiple selection list
boxes. They have quite different user interfaces, and surely this can't help
users become more comfortable using Windows. Some operations are easier with
LBS_MULTIPLESEL, and others are easier with LBS_EXTENDEDSEL. I don't know
which style a developer is supposed to use. I wonder why these two styles
weren't combined into one style with consistent behavior.
Buttons
Push buttons have a very different appearance in 3.0, but no new APIs are
needed to support it. The old calls and messages work with the new
look-after all, the button still has just pushed and not pushed states; it
only looks different. Check boxes and radio buttons are unchanged, except
that they use the gray text system color instead of a dithered gray when
disabled. Since they're not three-dimensional, they look curiously flat
compared to push buttons. The only API change for buttons is the
BS_OWNERDRAW style, which lets you draw your own buttons using techniques
consistent with the owner draw list boxes and menus. (The old BS_USERBUTTON
style and BN_PAINT message are no longer listed in the SDK documentation and
are not recommended for new applications, but obviously they're still
supported so existing applications can work.)
ComboBox Class
As its name implies, the new ComboBox control class combines the behavior of
the existing Edit and ListBox controls. It links input between the two
controls so that you can either type into the edit control or scroll through
the list box, picking an entry that way (see Figure 5). The three kinds of
combo boxes are determined by the window style bits. Least often used is the
CBS_SIMPLE style, in which the list box is visible all the time and the edit
and list windows are linked back and forth. Much more common is the
CBS_DROPDOWN style, which has an edit control you can type into and the down
arrow icon next to it, which opens up the drop-down list box.
CBS_DROPDOWNLIST is also used frequently; it's like CBS_DROPDOWN except that
the edit control is disabled (that is, you can't type into it but can change
its value by hitting the up or down arrows or by opening up the list box).
A combo box, depending on its style, actually does create an edit control
and temporarily a list box if needed, so it's no surprise that many of the
same style options, messages, and notifications for edit and list controls
are also used for combo boxes. The messages and styles aren't exactly the
same-they've got CB_ and CBS_ prefixes in their names-but everything should
be fairly familiar if you've worked with edit controls and list boxes.
Whichever window is actually doing the work-the combo box itself, edit
control, or list box-you normally exchange messages with the combo box only.
Only two messages are unique to combo boxes, and they will probably be
rarely used: CB_SHOWDROPDOWN, to open or close the drop-down list box
explicitly, and CB_GETDROPPEDCONTROLRECT, to determine the drop-down list
box dimensions. The only notification unique to combo boxes is CBN_DROPDOWN,
which tells you when the drop-down list box is opened or closed.
Class and Window Architecture
Subclassing is a powerful technique used in many Windows applications. The
most common method of subclassing a window is to call CreateWindow on an
existing class and then use SetWindowLong to change the window function
pointer. This is easy, but prevents you from intercepting the WM_NCCREATE or
WM_CREATE messages, from adding additional cbWndExtra to your window, and
from changing other class-specific information such as the icon. To avoid
these problems, gather the WNDCLASS information for an existing window
class, change what needs to be changed (such as the function address), and
register a new class of your own with this new WNDCLASS information. Then
you can create windows of this class that are immediately subclassed.
To get the WNDCLASS information you need, use the new GetClassInfo function.
It will give you this information without your having to create a window.
You can then save the window function pointer, tweak the WNDCLASS as needed,
and call RegisterClass to create your new class.
One window that was especially difficult to subclass in Windows 2.x was the
screen or desktop window. This window encompasses the entire screen
background; all top-level windows (WS_OVERLAPPED and WS_POPUP) are really
its children. Subclassing this window allows you to do things such as put up
your own screen background. Windows 2.x tried to prevent you from getting to
the screen window handle, but naturally there was a very sneaky way to get
around this. (The call was HIWORD(GetWindowLong(GWW_PARENT-2)), if that's
sneaky enough for you.) Sneaky tricks are no longer required-all you have to
do in Version 3.0 is call GetDesktopWindow and it returns the screen window
handle. In fact, the screen window handle is used by the desktop section of
the Windows 3.0 Control Panel, allowing end users to use any bitmap as
their screen background (see Figure 16). (Readers of WINDOWS.H will note the
function GetDesktopHwnd. It's the same function with a different name,
which carries on the tradition of GetCurrentTime and GetTickCount, also
synonyms for one function.)
When you're finished with a window class, call UnregisterClass to get rid of
it. This allows you to register temporary classes and unregister them at
various times during your program. When a dynamic-link library (DLL) exits,
its classes are destroyed-previously this happened automatically only for
applications. If you do have a DLL that creates public window classes, be
sure to put the new CS_GLOBALCLASS style on them or they will be private to
the DLL.
Windows 3.0 has some new WS_ style bits. The problem is where to put them.
The flWndStyle parameter to CreateWindow is more than full, in fact, it's
overloaded. You may already have noticed that WS_MINIMIZEBOX and
WS_MAXIMIZEBOX are the same bits as WS_GROUP and WS_TABSTOP. (This fact has
been a minor nuisance in MDI programming under Version 2.1.) Since there was
no place to put any more style bits, an entire new doubleword of extended
style flags, with WS_EX_ prefixes, has been added. The CreateWindow function
doesn't know about these flags, so a new CreateWindowEx function does. (And
yes, CreateWindow now is just a call to CreateWindowEx with the extended
style flags zeroed out.) There's plenty of room to grow, because only two of
these flags are defined: WS_EX_DLGMODALFRAME, providing the new movable
modal dialog style, and WS_EX_NOPARENTNOTIFY, which shuts off the new
WM_PARENTNOTIFY message. It may seem silly to have to add another 32 bits of
style flags and use only two of them, but Microsoft probably wanted to
running out of bits again for a long time.
DeferWindowPos
One thing that's always been more difficult than it should be is positioning
several windows at once and avoiding sloppy screen painting. You could just
make some SetWindowPos calls, but it may be hard to avoid extra screen
painting as you make the calls. Windows has always had internal functions to
position a group of windows cleanly; these are now available to "the rest of
us." Just replace each SetWindowPos call with DeferWindowPos, and bracket
the whole thing with BeginDeferWindowPos and EndDeferWindowPos. After the
EndDeferWindowPos call, Windows positions all the windows, optimizing the
painting for the cleanest appearance. OS/2 Presentation Manager programmers
may notice that it is similar to WinSetMultWindowPos. In fact,
BeginDeferWindowPos creates and DeferWindowPos fills in a structure much
like the SWP array used by WinSetMultWindowPos; EndDeferWindowPos processes
this structure in the same way as the PM function. So it's really the same
thing with two rather different APIs.
Menus
You can do much more with menus in 3.0. To create hierarchical menus with
practically no work at all, use a POPUP statement instead of MENUITEM when
you want a hierarchical menu, just like you do at the top menu level. After
the POPUP, of course, is a BEGIN and END with a list of MENUITEMs in between
for the nested menu items. The only thing different in Windows 3.0 is that
you can nest POPUP statements instead of being limited to one level of POPUP
menus. If I were converting a Version 2.1 application I'd probably put in
hierarchical menus first, just because it would be fun and easy.
With a little more work, you can explicitly bring up a pop-up menu anywhere
on the screen. Create a pop-up menu with a CreateMenu call (or just use
GetSubMenu to grab one loaded from a resource) then call TrackPopupMenu to
display the menu. TrackPopupMenu handles all the user's keyboard and mouse
input for the menu and returns when the menu is closed. One of the
parameters to TrackPopupMenu is the handle of an owner window, which
receives the WM_COMMAND and other messages generated by the pop-up menu.
As with list boxes and combo boxes, there are now owner draw menus. You
always could specify a bitmap for each menu item, but owner draw menus are
more flexible than that. They work like the other owner draw controls-the
application window gets WM_MEASUREITEM and WM_DRAWITEM messages if you put
the MF_OWNERDRAW flag on a menu item. You can also change menu appearance by
defining your own check mark bitmap. Instead of always displaying a check
mark when the MF_CHECKED flag is on, Windows lets you associate individual
checked and unchecked bitmaps with each menu item.
One minor improvement that makes things a little less confusing is that the
multipurpose ChangeMenu function is replaced by specific functions for the
individual functions it performed. The new functions are AppendMenu,
DeleteMenu, InsertMenu, ModifyMenu, and RemoveMenu. ChangeMenu still exists,
so you don't have to change existing code right away. Some existing code
could be affected by a new system menu item, SC_TASKLIST, but this should be
transparent as long as you don't assume how many system menu items there
are.
With all the menu improvements, one omission surprised me: menus still do
not support scrolling. In a number of other GUIs, if a menu has too many
items to fit on the screen, the menu scrolls up and down to reveal all the
items. Not so in Windows. A menu must fit completely on the screen or the
user just won't be able to click on the hidden items. This is really
unfortunate. For example, it means that a Windows application can never have
a simple Font menu listing all available fonts. If there are too many fonts,
the user won't be able to select some of them. (Yes, I know many Windows
applications do have a Font menu like this, but unless they switch to a list
box, they will be in trouble when there are many fonts installed.)
Similarly, the Window menu in an MDI application cannot list all open
windows. If there are too many, it adds a "More Windows" item to the bottom,
which brings up a list box for the windows that don't fit in the menu.
Elegant this isn't.
Dialog Box Changes
The dialog manager has been restructured. In Version 2.1 we pretty much had
to use the standard dialog window class. Theoretically it was possible to
create a dialog box using your own window class, but the standard dialog
window class still had some of the functionality you needed. The only easy
way to make things work was to use the standard class and then subclass it,
and this approach had its own problems. All that special functionality has
been moved out of the standard dialog class and into a new DefDlgProc
function. Thus, you can use a window class of your own for a dialog box,
calling DefDlgProc instead of DefWindowProc.
Another dialog problem that has been solved is how to pass parameters to a
dialog function. The lpParam of CreateWindow is missing from the existing
dialog creation functions, forcing programmers to use static variables and
various kludges to get information into dialog functions. This doesn't seem
like that much of a problem until you get to situations like multiple
instances of a modeless dialog box; then it's a real nuisance. Now the
dialog creation functions all have versions that include a long parameter
like in CreateWindow. The odd part is that because there were four ways to
create a dialog box (using DialogBox, DialogBoxIndirect, CreateDialog, or
CreateDialogIndirect), there are now four new functions, one of each of the
above with "Param" tacked on the end.
A few new dialog styles are worth noting. DS_MODALFRAME gives you the new
movable modal dialog box; it corresponds to WS_EX_DLGMODALFRAME in
CreateWindowEx. DS_NOIDLEMSG turns off those pesky WM_ENTERIDLE messages. If
you're tired of not knowing what that message really does, you can get rid
of it. Or just look it up in the documentation; it's documented now. Last,
DS_SETFONT now specifies a font to be used by the controls in the dialog
box. You can do the same thing with the WM_SETFONT message; it's just a
convenient way to set the font for every control in the dialog. You're most
likely to use DS_SETFONT in its guise as the FONT statement in an RC file;
DS_SETFONT is the actual style bit you'd use in a CreateDialogIndirect call.
Message Box Changes
Message boxes have one new feature, the MB_TASKMODAL flag. This flag
disables all other windows owned by your application while the message box
is open. You might think that MB_APPLMODAL would do this, but no.
MB_APPLMODAL, the default, simply disables the owner (also known as parent)
window listed in the MessageBox call. But it doesn't disable any other
top-level windows (WS_OVERLAPPED and WS_POPUP) owned by your application.
If, for example, you had both a main application window and a modeless
dialog box open, a MB_APPLMODAL message box would disable only the one
listed as its owner. In many cases, they should both be disabled, and
MB_TASKMODAL does that. Rather than relying on the owner window parameter,
it simply disables all top-level applications owned by your task, and then
reenables them when the MessageBox call exits. It also notes any windows
that you may have already disabled, so that it doesn't inadvertently enable
them when it exits. They remain disabled as they should.
It's a shame this feature is only available for message boxes. Dialog boxes
really need it, and for that matter ordinary windows do as well (for
example, it would be useful in conjunction with a window that looks and acts
like a modal dialog but is created with an explicit CreateWindow call). I've
had to implement task-modal dialog boxes myself in my last two applications,
and I'm sure other developers have also had to do this. It isn't hard, just
one of those nuisances that you aren't even likely to realize you need until
you find your modal dialog box only disables one of your top-level windows,
not all of them.
New Printing Support
In the past, there hasn't been any easy way to maintain individual printer
settings for different documents. You could call the DeviceMode printer
driver function from your application, but that function keeps data in a
device-dependent format. (It's usually stored in WIN.INI, but there is no
guarantee of this, and exactly what gets stored is different for each
printer.) Furthermore, in Windows 2.x, DeviceMode would always bring up the
printer settings dialog box, which is not what you want if you just need to
switch to a different setting. The ExtDeviceMode function now deals with all
this in a more useful manner. The call has options to fetch and set the
current printer settings, to change the WIN.INI defaults, and to bring up
the printer settings dialog box. Note that the dialog box is optional,
allowing you simply to change to a new printer setting saved with a document
(or elsewhere). The settings are stored in a DEVMODE structure, which
combines device-independent settings with device-dependent ones very
cleanly. The first part of the structure contains many common printer
settings: paper size and orientation, number of copies, resolution (either a
device-independent high/medium/low/draft or DPI), and even input paper bin
and duplex printing options. You can manipulate any of these settings in the
DEVMODE structure directly. At the end of DEVMODE is a variable length
device-dependent section, in which the driver keeps any additional
information needed for a particular printer. Naturally, you shouldn't
manipulate this section unless you're very cozy with a particular printer
driver.
There are also two dozen new printer escapes. Most of these are specific to
PostScript printers, thus beefing up Windows PostScript support. New escapes
make it easier to print Encapsulated PostScript (EPS) files and to
manipulate the transformation matrix used by PostScript printers.
Windows Flags
With all the different CPUs and memory modes Windows 3.0 supports, it would
be a disaster if you had to puzzle out the current mode and CPU.
Fortunately, a handy function called GetWinFlags returns a set of flags
indicating your CPU type, Windows memory configuration, and presence or
absence of a math coprocessor (see Figure 17). On the CPU side, flags
distinguish among 8088/8086, 80186 (yes, there is such a processor), 80286,
80386, and 80486 processors. Additional flags tell whether Windows is
running in real mode, standard mode, or 386 enhanced mode. If you just want
to find out if you're in protected mode or not, there's a flag specifically
for that. In real mode, there are additional flags identifying small- and
large-frame EMS. Most Windows code doesn't need to know what CPU or what
mode it is running in, but when you do need to know, GetWinFlags is the
ticket. In assembly language, you can directly import the absolute symbol
__WINFLAGS like this:
externA __WINFLAGS
globalD flMyWinFlags, __WINFLAGS
Files and Disks
In previous versions of Windows, there was a set of undocumented file I/O
functions that everybody who read Charles Petzold's Programming Windows
(Microsoft Press, 1988) knew about: _lopen, _lcreat, _llseek, _lread,
_lwrite, and _lclose. These functions still exist, and they're now
documented and supported. They are nothing fancy, just direct calls to the
equivalent INT 21H DOS services.
OpenFile sports a few new options to support file sharing. It allows all the
sharing modes that SHARE.EXE supports: compatibility, deny none, deny read,
deny write, and exclusive. If you run out of file handles, you can now get
more by calling SetHandleCount. Although OpenFile looks for files in the
Windows and Windows system directories automatically, you may need to know
those directory names, and a GetWindowsDirectory or GetSystemDirectory call
will do the trick.
Strings Attached
As with the file functions, the "undocumented" string functions lstrcat,
lstrcpy, and lstrlen are now supported and documented. One string function,
lstrcmp, still exists in name, but has a new meaning in 3.0. In Windows 2.1,
lstrcmp was not case-sensitive, unlike the C run-time library's
case-sensitive strcmp function. In Windows 3.0, both kinds are available and
the names match the C style: lstrcmp is now case-sensitive and lstrcmpi is
not case-sensitive. Both these functions were rewritten to work better with
international versions of Windows and to be faster. The C string compare
functions have the advantage of not having to worry about different
character sets, so they are faster than the Windows versions, but they don't
handle international character sets at all. Because of this, it's better to
use the Windows functions.
Since C's character classification functions don't work with international
character sets either, Windows now has its own: IsCharAlpha,
IsCharAlphaNumeric, IsCharLower, and IsCharUpper. Several string conversion
functions now have variants that take a character array of a specified
length instead of a zero-terminated string. The new ones are AnsiLowerBuff,
AnsiToOemBuff, AnsiUpperBuff, and OemToAnsiBuff. Another new function,
ToAscii, converts a virtual key code into an ANSI character code. Besides
the virtual key code, you have to provide the scan code and a key state
table as returned by GetKeyboardState-in other words, the same information
Windows and the keyboard driver normally have on hand when they do this
translation inside TranslateMessage to generate WM_CHAR messages.
The best new string functions are wsprintf and wvsprintf, which are
replacements for C's sprintf and vsprintf. True, you can call sprintf and
vsprintf from a Windows application, but they add bulk to your code, and
don't work at all from DLLs. The Windows equivalents don't have these
disadvantages, and are therefore very helpful for DLL developers. They don't
support all the format specifiers from sprintf-floating point and pointer
types are missing-but they do include %c, %d, %u, %x (and %X) in both short
and long, together with the , #, 0, width, and precision prefixes. Of course
%s is supported, but it expects a far string pointer regardless of your
memory model, and there are no %F or %N prefixes to modify this. Just make
sure to cast any near string pointers to (LPSTR). The only Windows functions
that the C compiler won't let you do this with are wsprintf and wvsprintf,
because there are no prototypes for these particular parameters. (The
lpOutput and lpFormat parameters do receive the normal near-to-far casting,
so it's best not to cast them. That way the C compiler can do a better job
of diagnosing incorrect parameters.)
As for using wvsprintf, once you see one example you'll discover that this
is the function you've been looking for for years. Many Windows programs
include code something like this to display formatted text:
wsprintf( szBuf, "A string: %s", sz );
TextOut( hdc, x1, y1, szBuf, strlen(szBuf) );
wsprintf( szBuf, "Two numbers: %d %d", i, j );
TextOut( hdc, x2, y2, szBuf, strlen(szBuf) );
That's clumsy. It would be a lot nicer if you could just write something
like this:
TextOutF( hdc, x1, y1, "A string: %s", sz );
TextOutF( hdc, x2, y2, "Two numbers: %d %d", i, j );
But of course you can't do that. How can you get those varying parameters at
the end into a wsprintf call? It just can't be done. But that's exactly what
wvsprintf (and vsprintf for normal C programs) is for-a building block for
writing your own printf-style functions. The TextOutF function can be easily
coded thus:
// Formatted printf-style TextOut
BOOL _far _cdecl TextOutF( HDC hdc, int x, int y, LPSTR lpszFormat, ... )
{
char szText[256];
int cbText;
cbText = wvsprintf( szText, lpszFormat,
(LPSTR)lpszFormat + sizeof(lpszFormat) );
return TextOut( hdc, x, y, szText, cbText );
}
As you can see, calling this one function is better than having to call both
wsprintf and TextOut each time. Two caveats on this sample code: it assumes
that the 256- character szText buffer is long enough, and it does not use
the recommended va_start macro to access the varying parameters at the end.
I didn't use va_start because that function is model-dependent and I wanted
this code to be model-independent. It's hard to avoid making assumptions
about the buffer size, but a good safeguard is always to use explicit
precision specifications on strings in your format. For example, the format
"A string: %.80s" would never overrun the buffer no matter how long the
string is.
Process and Memory Support
In the past, Windows did not provide a straightforward way for one program
to launch another. The DOS INT 21H/4BH Exec service has been used for this,
but it's not really documented. Now, there's a convenient WinExec function.
Just pass it a command-line argument and an nCmdShow parameter (which is
passed to the target application's WinMain) and off you go. Another option
is to call LoadModule. This function takes the same parameters as the Exec
call in DOS-pointers to the filename and the Exec parameter block-so it's
more complex to use. However, you can provide an environment string for the
target app instead of having it receive a copy of the Windows environment.
Version 2.1 had a problem shutting down programs. Applications have always
"known" when they were exiting, because they they themselves initiate the
shutdown. (WM_QUIT doesn't magically shut down a Windows app. What it does
is cause GetMessage to return FALSE, breaking out of a WinMain message loop.
WinMain returns to its caller, which is the run-time support code linked
into the application. That code, after cleaning up, executes a thoroughly
conventional INT 21H/4CH to terminate the application.) DLLs have not had
this luxury. They "knew" when they were started because their initialization
function was called, but there wasn't any notification of exit. Under 3.0, a
new function called WEP (Windows Exit Point) must be defined by each DLL.
When Windows unloads the DLL, it calls WEP so the DLL can clean up after
itself.
GetCurrentPDB is a new function that returns the segment address of the
running application's DOS Program Segment Prefix (PSP; which Windows refers
to as the Program Data Base). Little known fact: this information has always
been available to WinMain, as the segment portion of the lpszCmdLine
parameter. When Windows starts an application, it sets up a normal DOS PSP
for it in a fixed nonbanked segment, complete with a weirdly formatted
command-line tail at offset 80H, so that DOS will do things like maintain
file handles for each application. A Windows application actually receives
this PSP address in the ES register, just like a DOS application, and the
run-time start-up code linked into the application that cleans up the
command-line tail, converting it into the lpszCmdLine parameter. (It skips
over leading blanks and zeros out the CR character at the end.) This cleanup
is done in place, so the segment portion of lpszCmdLine is always the PSP
address, and the offset portion is always 81H plus the number of leading
blanks in the command tail.
Another new function, GetCodeInfo, provides several pieces of interesting
information about code segments in currently running programs. You pass it
either a function address or a module handle and segment number, and it
returns a set of flags describing the segment, together with the segment's
location in its executable file and both its length in the file and the
memory size that should be allocated for it. Combined with
GetModuleFileName, this gives you enough information to load a code segment
from a Windows executable in some unusual way should you need to. That's
assuming you don't mind doing all your own relocation fix ups on the
segment, of course.
CodeView for Windows
Windows in real mode has been at best a badly cramped programming
environment. The worst crowding occurred when you tried to squeeze in a
debugger with Windows and an application. SYMDEB with its symbol tables took
up more than enough space; Microsoft CodeView for Windows debugger
(hereafter CVW) was even worse. CVW tried bravely to squeeze itself into
memory and keep as much in EMS as possible, but as anyone who tried to use
it with any large application knows, it just didn't cut it. You had to turn
off symbols for much of the application to get it to fit at all and to avoid
the dreaded "CVW Crawl." MagicCVW by NuMega did a good job of making Windows
2.1 CVW more usable, but now CVW itself runs in protected mode, which takes
care of the memory problem nicely.
With more room, it was time to put some improvements into CVW. If you have
Microsoft C Version 6.0, you know what the improvements are, so I won't go
into great detail here (CVW is the C 6.0 CodeView with Windows extensions).
If you didn't like the old CVW, this one will change your mind. If you did
like the old one, you'll be thrilled. The new CVW is infinitely faster. I am
not exaggerating. When I ran a large program in the old CVW, if I threw in a
couple of Watch variables it could approach a speed of 1/c. The new CVW is
so much faster, it can display a Locals window showing all the parameters
and local variables of the current function. You can add Watch expressions
and as many as two source windows and two memory display windows, all
updating live, and the thing still doesn't slow down. It's not only faster,
it's a more usable and powerful debugger all around.
CVW now starts up the right way. Instead of running CVW from DOS and having
it start Windows, it works the other way around. CVW is now a real Windows
application; you run it under Windows and it starts up and takes control of
your application. Finally, you don't have to restart Windows each time you
want to use CodeView!
The Windows extensions to CVW include (lh) and (gh) type casts, which
dereference local and global memory handles into pointers, along with
several Windows-specific commands. The most interesting are wwm (Windows
Watch Message) and wbm (Windows Break Message). These commands set
watchpoints and breakpoints, not just at particular locations, but on
specific messages or classes of messages. This really helps with debugging
window functions. There are other commands to display the local and global
heaps and the list of modules, and to terminate the application being
debugged.
One caveat: you can no longer run CVW on a serial debugging terminal. It
must be on a second display on the same computer running Windows. For a
classic bus machine, the usual setup is to run Windows on a color display
and CVW on an auxiliary monochrome card and monitor. It's tough to find mono
cards for a PS/2, so there the hot setup is to run Windows on an 8514/a and
CVW on the VGA. Then CVW runs in color 50 line mode, which is handy.
Another Debugger
SYMDEB is still supported for real-mode debugging, and it works as
well-ahem-as it always did. There's even a SYMDEB-style debugger for
protected mode called WDEB386. Yes, you restart Windows each time, it's
purely a command-line system, and it doesn't have any source code access at
all. You'd use it if you didn't have a second display for CVW, or if you
were debugging certain types of device drivers. Printer drivers are easily
debugged under CVW, because they are just demand-loaded DLLs, but to debug
any of the drivers that load during start-up, you have to use WDEB386. CVW
won't work because it can't start until after all the start-up drivers are
loaded. WDEB386 also has a few little things it will do that CVW won't, so
it's worth keeping. But CVW is clearly the debugger of choice; it really
makes debugging Windows applications easier.
Profiler
Previous versions of the SDK did not provide much in the way of performance
measurement tools. Duncan Booth, a Windows programmer in England, helped
fill the gap with a profiler called WinET (Windows Execution Timer). WinET
uses a simple but effective technique: it hooks into a timer interrupt and
records the current code segment (CS) and instruction pointer (IP) on timer
ticks. After you leave Windows, it matches these up against specified SYM
files and prints a report showing where the time was spent.
The Windows 3.0 SDK includes a profiler that works on the same principle as
WinET, and it's better and easier to use. It supports real mode and 386
enhanced mode (but not standard mode), so you can profile any kind of
Windows application. It's better to use 386 enhanced mode if you can, so
that the memory the profiler consumes won't get in the way of your
application.
To use the profiler, add ProfStart and ProfStop calls to your code,
bracketing the sections of code you want profiled. (You wouldn't want the
profiler on all the time-for example, you generally wouldn't want to leave
it on across a GetMessage or PeekMessage call because other applications
would then be running instead of yours.) To start Windows with the profiler
installed, either add a driver line to SYSTEM.INI in 386 mode or start
PROF.COM in real mode. When you run your program, the profiler samples the
instruction pointer periodically when it is activated with ProfStart; when
you shut down Windows or call ProfFinish, it writes this information out to
a disk file. Then, just run SHOWHITS with the correct options and it will
report where the CPU time was spent. SHOWHITS gets its name because it shows
how often the instruction pointer "hit" each part of the code. The most
frequently executed code is listed in a portion of the report appropriately
titled "Top 10 Hits."
Swap Analyzer
The profiler gives one view of your program's execution; it's also useful to
know something about calls across segment boundaries. In real mode, you want
to find out which code segments are being swapped too often. While not often
a problem in protected mode, too many far calls are, because they're very
expensive compared to near calls. Since you'll want to know what far calls
are being made, use the swap kernel, SKERNEL.EXE. Strangely, the swap kernel
runs only in real mode, because it's a special version of the real-mode
kernel that puts extra debugging code into the reload thunks it generates.
Since there are no thunks in protected mode, this trick doesn't work there.
To use swap kernel, put SwapRecording calls in your code to start and stop
swap recording; after you exit Windows, run the Swap utility to view the
results. An option in SwapRecording lets you specify the events to record:
segment loads and discards only, or all far calls.
Other Tools
HeapWalker naturally had to be updated to work in protected mode, but real
mode wasn't forgotten. Under large-frame EMS in Version 2.1, HeapWalker was
only able to look at its own EMS bank. Other applications were essentially
invisible because their code and data were in other banks. However, in
Windows 3.0 real mode, a new EMSWalk menu now lists the other running
applications so that you can view any of their EMS banks.
The Dialog Editor can now add new control classes and support them as fully
as the built-in classes. If you write a DLL that registers a control class
and provides a set of entry points and messages that is defined in the SDK,
Dialog Editor can load it, add that control to its palette, and let you put
the control in dialog boxes.(For more information on control classes, see
"Extending the Windows[tm] 3.0 Interface with Installable Custom Controls,"
p. 29-Ed.)
SDKPaint improves upon the old Icon Editor by providing more drawing tools
and support for color images as well as multiple-resolution cursors and
icons (see Figure 18). As with Paintbrush, I'd like to see more flexible
zooming. Optional grid lines would be a help too, but SDKPaint is still
pretty good as is.
Don't overlook the new Recorder that is included with Windows 3.0 as a
development tool. Simply by recording and playing back a macro, you can run
your application and make sure that you don't break things that once worked.
The most fun of all the SDK tools has to be ZoomIn. Play with it for a
couple of minutes and if you don't chuckle, see a doctor immediately. It has
a practical use, too, but that is secondary to the amusement factor (see
Figure 19).
New Libraries
There were several problems with the LIB files shipped in previous SDKs.
Library functions were not compiled with the Windows prolog/epilog they
needed and it was unclear which functions chould be used in DLLs or
applications. The Windows import library ?LIBW.LIB came in different
versions for each memory model-even though most of this library was common
to all models. And just to keep things interesting, C 5.1 and the Windows
SDK each offered several different naming conventions for the libraries. It
sounded convenient-you could have either DOS, OS/2, or Windows libraries as
the default-but it meant no one's MAKE files would be compatible with anyone
else's.
C 6.0 and the Windows 3.0 SDK have standard names for all libraries, with an
R suffix for DOS, P for OS/2, and W for Windows. You have to use explicit
library names when linking files for Windows, but since you pretty much need
to use a MAKE file anyway it's no big deal. Besides, anyone else will then
be able to build your program without fiddling with the MAKE file. There is
a single import library, LIBW.LIB. The small amount of model-specific code
that was in the ?LIBW.LIB files is now in the ?LIBC?W.LIB files instead. A
separate set of files for DLLs named ?DLLC?W.LIB contain only the C run-time
routines that are safe to use in DLLs. And for applications and DLLs that
don't use the C run-time library, there are new libraries that omit the
routines but have the other start-up code needed.
Header Files
WINDOWS.H has been reorganized. It's in a more logical order, but more
importantly, numerous coding errors are fixed. Functions that had been
declared with only () for the argument list are declared as void, to avoid
compiler warnings. Some functions with no return value had been erroneously
declared to return int; these now have void returns. Far pointers are used
in macros where needed, for example in MAKEPOINT. This macro used to work
only with data in the default data segment in the small or medium model; now
it works wherever the data is. And assembly-language programmers are no
longer neglected stepchildren-WINDOWS.INC is up to date and complete.
DPMI
Most of protected mode's restrictions, such as segment limit checking, help
more than they hurt. But one restriction can be onerous: you can't easily
communicate between protected mode and real mode. If you have a DOS TSR that
your application communicates with through a software interrupt or maybe a
shared memory buffer, it's not going to work in protected mode. You can't
use a real-mode address in protected mode and expect it to point to the same
thing. The segment portion of the address is interpreted differently in the
two modes. And you can't call just any real-mode interrupt from Windows
protected mode. INT 21H calls and a few others such as NETBIOS calls are all
right, but only because Windows handles them specially.
What's needed is some kind of interfacing service to hook up protected-mode
and real-mode programs. The DOS Protected Mode Interface (DPMI) provides
this sevice. DPMI is a set of protected-mode functions that allocate
real-mode memory, simulate real-mode interrupts and function calls,
intercept real-mode interrupt vectors, play games with selectors, and more.
Using these calls, you can communicate with TSRs from protected mode just as
you would in real mode. It's pretty straightforward, if your code is
organized so that the actual communication with the TSR is done in just a
few places. If various kinds of direct interfaces with the TSR are
interspersed through your code, it will be tougher. (For more information
about DPMI, see "Supporting Protected-Mode Applications in a DOS-based
Environment," p. 92-Ed.)
DPMI calls use an INT 31H interrupt interface, not a Windows-style
dynamic-link interface. You load registers and call INT 31H, just as you
would make a BIOS or DOS call. DPMI works this way because its real purpose
is not to provide an interface between Windows applications and real mode
(that's a side benefit) but to give DOS extenders a way to work cleanly
under Windows. DOS extenders, such as DOS16M by Rational Systems, let DOS
applications run in protected mode to access extended memory. They switch as
needed to protected mode for application code and real mode for DOS and
other real-mode calls. Rational Systems and others were able to make this
work under Windows/286 and DOS by switching modes on their own. Windows/286
did not know it was happening, because the DOS extender would always go back
to real mode before returning control to Windows. This didn't work in
Windows/386, where the non-Windows application using the DOS extender was
actually running in a virtual machine, so the usual tricks didn't work.
Therefore, the INT 31H DPMI interface was implemented to provide the same
services that DOS extenders must use internally to run their applications.
The DOS extender can determine that it is running under a DPMI
system-protected-mode Windows, or eventually other systems such as OS/2 and
UNIX/XENIX-and make INT 31H DPMI calls instead of pulling its own mode
switching and selector translation tricks. This is a real boon to users. To
cite the classic case, Lotus 1-2-3 Version 3.1, which uses Rational Systems'
latest DPMI-compatible DOS16M, can fully exploit Windows 3.0 extended memory
in any memory mode. It's a bonus that Windows programmers can use INT 31H to
solve the problem of communicating with real mode.
There is a little support for real-mode interfacing in Windows proper. You
can allocate a block of memory in real-mode address space by calling
GlobalDosAlloc and free it with GlobalDosFree. GlobalDosAlloc returns both a
real-mode segment value and a Windows selector. This "selector" is equal to
the segment value if Windows is running in real mode; in protected mode the
two values are different. In either case, your Windows code can use the
"selector" to address the memory, passing the segment value to any real-mode
code. That brings us back to the problem of how to call real-mode code in
the first place, pointing back toward the DPMI approach, since there are
functions to simulate a real-mode interrupt or function call.
Even if you're not communicating with a TSR, you may need to break some
other protected-mode rules. Normally, the 286/386/486 hardware prevents you
from executing instructions from a data segment, and you can't write into a
code segment. (There is another level of protection available, a code
segment that you can't even read from. That's not used often because it is
common to read from a code segment, especially in compiled C code.) This is
a big problem in those situations where you literally do need to write your
code or run your data. Any kind of interactive compiler or code generator
would have to do this. Amazingly enough, even the Windows display drivers
use self-generating code techniques. When the display driver receives a
BitBlt (bit block transfer) call, it compiles a small subroutine on the
stack, containing the customized loops that make up that particular BitBlt
operation. Then it calls that subroutine to perform the actual transfer.
Windows makes it easy to "cheat" and accomplish these things. All you have
to do is create a code selector and a data selector that point to the same
physical memory. Then you can do anything you could do in real mode; you
just have to remember to use the code selector when you're executing machine
instructions and the data selector when you're writing to memory. You can
always read from memory with either selector.
To get an executable code selector for a data segment, call
AllocDStoCSAlias. Or you can create a selector that isn't associated with
any memory by calling AllocSelector, then calling ChangeSelector any number
of times to make this selector an alias for various other segments. Either
way, when you are finished with the "alias" selector, call FreeSelector. One
thing to watch out for : if Windows has to move the segment in question
(remember, segments still can move around in the linear address space) the
alias selector won't get updated and it will point nowhere. To be safe, lock
down the segment in linear memory by calling GlobalFix before aliasing it,
and call GlobalUnFix when you're done. Also, ChangeSelector was
inadvertently omitted from the SDK import libraries, so if you use it you
need to explicitly import it in your DEF file:
IMPORTS
CHANGESELECTOR = KERNEL.177
You may discover that you can use ChangeSelector to create a data alias for
a code segment as well as a code alias for a data segment. Although this
works, it isn't recommended because it won't be supported in future
environments such as OS/2 2.0. Creating a code alias for a data segment is
the preferred method and should be compatible with future products.
Besides, you wouldn't want to receive a Programmer General's Warning:
Cheating with selectors can be hazardous to your code. If you have to talk
with a hardware device, you should get or write a Windows DLL that supports
the device instead of using DPMI to call a real-mode TSR. As for this
business about writing into code segments or running code in data segments,
you saw the warnings in the SDK: "Use of these functions violates preferred
Windows programming practices." You probably also use GOTOs and write
self-modifying Assembler code and break the 55 MPH speed limit too. Bad
Programmer!
Conclusion
One of the running jokes about Windows has always been that it was fine for
users, but you couldn't develop Windows applications inside Windows. Even
under Windows/386, editing and compiling within Windows just wasn't worth
the trouble. The 386 mode made the compiles too slow, and you had to restart
Windows anyway if you wanted to use CodeView. It's a different story now. As
I write this using a Windows-based word processor, I have a DOS window
compiling a test program I'm working on, with my source code loaded into
BRIEF in another window, while another DOS window copies some files to
diskette, and in the background FAXit sends a fax I created as another word
processor document. I'm finally getting my money's worth out of this 386
system! I don't have to shell out of BRIEF to run DOS commands; all I have
to do is switch to a DOS window. Debugging is easy now that CodeView runs
inside Windows.
The entire Windows SDK reference is now a Windows Help document, making it
very convenient to develop inside Windows. The whole thing is
cross-referenced so you can click your way through any topic. It's really
nice. If you're looking at the description of BeginPaint and want to see
what the fields in the PAINTSTRUCT are called, you don't have to go flipping
to the back of the book. Just click on PAINTSTRUCT on the screen and you're
there. Another click and you're back to reading about BeginPaint. You won't
want to develop outside Windows after using this.
I can't believe I did this, but I just added a WIN line to the end of my
AUTOEXEC.BAT file! I tried booting up a couple of times and I don't miss
that old DOS prompt. No, I take that back. I use the DOS prompt all the
time-it's still the easiest way to do many things. But it's even handier to
have as many DOS prompts as I want, along with the SDK help and all the
rest. I guess I'm just going to have to get used to being a Windows user.
WIN.INI Color Settings
The predefined Windows 3.0 color schemes are all too bright and have too
many contrasts for my taste-they look like they were designed for snappy
screen shots instead of daily use. With the high-intensity white window
backgrounds, they're much brighter than typical DOS applications. I looked
through Control Panel's predefined color schemes to see if any were easier
on the eye. No, they all use the same bright white window background, so I
used the Color Palette option to change the window background to the new
light gray color. But disabled (grayed-out) check boxes and radio buttons
also use the light gray color instead of a dithered gray, so they're
invisible against the light gray window background. I discovered that
Control Panel doesn't let you set this color nor several other system
colors, including the highlight (selection) text and background colors. You
have to edit WIN.INI to set any of these. (Also, since many Windows 3.0
controls are painted with pairs of fixed bitmaps representing the pushed and
unpushed states, playing with the ButtonShadow and ButtonFace values has no
effect on most three-dimensional controls.) It's also no longer correct to
invert colors to indicate highlighting; you are supposed to use the
highlight text and background colors instead.
Here's the color scheme I came up with after much experimenting. I think its
low contrast is easier to look at than any of the standard schemes. Try
pasting the following code into your WIN.INI. It may look rather dim at
first, but I'll bet that's because you have been turning down the brightness
on your display to compensate for the overly bright standard Windows colors.
Set the brightness to the normal level that you use with DOS applications
and this should be just about right.
[Colors]
; Ones that Control Panel knows about
ActiveBorder= 128 255 255
ActiveTitle= 0 128 64
AppWorkspace= 64 128 128
Background= 128 128 128
InactiveBorder= 192 192 192
InactiveTitle= 128 128 128
Menu= 0 128 128
MenuText= 0 0 0
Scrollbar= 191 191 191
TitleText= 255 255 255
Window= 192 192 192
WindowFrame= 0 0 0
WindowText= 0 0 0
; New ones that Control Panel knows not
;ButtonFace=
;ButtonShadow=
ButtonText= 0 0 0
GrayText= 128 128 128
Hilight= 128 0 0
HilightText= 255 255 0
The following is a similar scheme that I designed to help test for painting
bugs. It uses nonstandard colors for most elements and avoids colors that
are complements of each other, to try to aggravate any misuse of colors and
make problems obvious.
[Colors]
; Ones that Control Panel knows about
ActiveBorder= 128 255 255
ActiveTitle= 0 128 64
AppWorkspace= 64 128 128
Background= 128 128 128
InactiveBorder= 192 192 192
InactiveTitle= 128 128 128
Menu= 0 128 128
MenuText= 0 0 0
Scrollbar= 129 129 129
TitleText= 0 0 0
Window= 192 192 192
WindowFrame= 0 0 128
WindowText= 128 0 0
; New ones that Control Panel knows not
;ButtonFace=
;ButtonShadow=
ButtonText= 0 0 0
GrayText= 128 128 128
Hilight= 0 0 255
HilightText= 255 255 0
Figure 17
■ Syntax: DWORD GetWinFlags()
■ This function returns a 32-bit value containing flags that specify the
memory configuration under which Windows is running.
■ This function has no parameters.
■ The return value contains flags specifying the current memory
configuration. These flags may be any of the following values:
■
■ Value
Meaning
■ WF_CPU086
System CPU is an 8086.
■ WF_CPU186
System CPU is an 80186.
■ WF_CPU286
System CPU is an 80286.
■ WF_CPU386
System CPU is an 80386.
■ WF_CPU486
System CPU is an 80486.
■ WF_ENHANCED
Windows is running in 386 enhanced mode. The WF_PMODE flag is always
set when WF_ENHANCED is set.
■ WF_LARGEFRAME
Windows is running in EMS large-frame memory configuration
■ WF_PMODE
Windows is running in protected-mode. This flag is always set when
either WF_ENHANCED or WF_STANDARD is set.
■ WF_SMALLFRAME
Windows is running in EMS small-frame memory configuration
■ WF_STANDARD
Windows is running in standard mode. The WF_PMODE flag is always set
when WF_STANDARD is set.
■
■ If WF_PMODE is not set, Windows is running in real mode.
■
■
Extending the Windows 3.0 Interface with Installable Custom Controls
Kevin P. Welch
The ability to create and formally install custom controls is a feature of
the Microsoft Windows[tm] graphical environment Version 3.0 that will be of
great interest to software developers. This ability permits the developer to
extend the user interface of Windows1 and encourages the creation of small,
self-contained, reusable software objects that can be shared between
applications.
This article describes the design and definition of new control classes,
discusses the new interface supported by Windows 3.0 for such installable
controls, and demonstrates the use of the interface with a sample Page
selection control. The sample control can be used with the new dialog editor
supplied with the Windows Version 3.0 Software Development Kit (SDK).
A few fairly simple steps greatly simplify the process of designing a new
installable control class. While these guidelines are helpful when creating
any new window class, they are particularly applicable when designing an
installable control.
The first step is to name the control. As with any other window, each
control must be associated with a class name. This class name is used in
your RC file whenever you wish to use the control explicitly in a dialog
box. When naming the control, make sure not to use one of the public class
names defined by Windows or other applications. If you do, the registration
process for your control class will not succeed.
Second, you must design the user interface. This is usually the most
difficult aspect of creating a control. The interface you design for your
control determines to a large degree how difficult the control will be for
you to implement, and how the end user will perceive it.
When you design a new control, you are extending the functionality of the
Windows user interface. Your new control should look and act similarly to
those already provided by Windows. For example, you should be consistent
with the commonly used mechanisms to indicate selection and keyboard input
focus.
You should also avoid device dependencies when you design your control. As
you know, Windows is device-independent so it can run on a wide variety of
display subsystems, with varying screen-aspect ratios, pixel densities, and
color capabilities. For instance, you probably shouldn't put fixed-size
bitmaps in the control interface. Since aspect ratios and pixel densities
vary widely, the bitmap will probably be scaled or distorted, resulting in a
less-than-optimal interface.
Another issue to consider is the use of colors. Predefined system colors
work best. Don't assume, for example, that the window background color will
always be white and the window frame black. Although it takes a little extra
work to incorporate such flexibility into your control, it is almost always
necessary when producing controls for commercial use.
Finally, if you are creating an active control, be prepared to support some
mechanism to define the input focus.
The third step is to define the interface. Almost all controls interact with
other windows in some way or another. Although this interaction can occur
using any of the mechanisms supported by Windows, almost all the controls
predefined by Windows (except the scroll bar) use a completely message-based
interface. Therefore, you must decide which messages your control will work
with.
You should also design the style flags your control will support. Whenever a
new instance of your control is created, these style flags are combined with
flags that are predefined by Windows and are passed to the system as a
parameter in the CreateWindow function.
After you have designed the control interface, you should create a public
header file defining each supported style, command, and notification
message. This header file can then be used by any other module that uses the
control.
Fourth, define the instance variables. By their nature, controls must be
completely reentrant and totally modular. As a result, the data structures
used by your control must be dynamically allocated and readily accessible by
each instance. Although you could create your own scheme for defining
instance variables, in most cases you should consider allocating extra class
or window bytes when the class is registered, or perhaps use property lists
or atoms.
The fifth step is to define the window function. Each control must be
associated with a window function that processes all related messages. By
definition, controls must be able to work with the Windows dialog box
manager. Consequently, your window function should handle at least the
following messages:
Message Window Function Response
WM_CREATE Initialize any instance variables and perform any other
necessary steps to use the control.
WM_DESTROY Release all the resources associated with the control
instance, including the removal of property lists and deletion of any atoms.
WM_PAINT Update the client region of the control. Carefully tune the
update operations for a variety of display devices, paying special attention
to the quality of the result. This is often one of the most tedious aspects
of the entire control development process.
If your control is to be capable of receiving input from both the keyboard
and the mouse, your window function should also be able to handle the
following messages:
Message Window Function Response
WM_GETDLGCODE This message is sent to your control by the dialog box
manager. The response to this message determines how the control will be
managed inside a dialog box. By responding with one of the following return
codes, your control can manage a particular type of input and process the
information itself:
DLGC_WANTALLKEYS Intercept all keyboard input
DLGC_WANTARROWS Intercept cursor keys
DLGC_WANTCHARS Intercept WM_CHAR messages
DLGC_WANTMESSAGE Intercept all messages
DLGC_WANTTAB Intercept the TAB key
WM_KEYDOWN Received when a key is held down. By processing the virtual
key codes within this message, you can perform any necessary action. When an
action is taken that changes the state of the control, be sure to provide
appropriate notification codes to the parent window.
WM_KILLFOCUS Received when the input focus changes to another window. If
the control displays some sort of highlight, it should be destroyed at this
time.
WM_LBUTTONDOWN Received when the user presses the left mouse button
inside the control client area. You will want to capture the system input
focus, display your highlight, and start capturing mouse movements. If the
mouse click results in a new selection, you may also want to notify the
parent window of the change.
WM_LBUTTONUP Received when the user releases the left mouse button. If
you are capturing mouse movements, you should be careful to release the
mouse capture at this time. Note that it is entirely possible to receive
extra button-down and button-up messages. If you want to animate a drag
operation you will need to capture the mouse and process each of the
WM_MOUSEMOVE messages.
WM_SETFOCUS When this message is received, a highlight should indicate
that the control has the system input focus.
In addition to these messages, your window function should also be capable
of processing the command messages you define as part of your message-based
interface. Note that you should be careful to return the appropriate
response for each message received, especially when an internal error
occurs.
The next step in the process is to define related utility functions. The
implementation of even a simple control often requires the definition of
several related utility functions that perform often-repeated processes. As
with any Windows program, you should structure and organize these functions
carefully, preferably placing them in logical groups using separate,
discardable code segments.
Finally, you must physically create the control modules. Depending on your
needs, you can either link each control object module directly into your
application or combine them into a library and let the linker extract the
required components. Be aware that if your control uses any resources, you
must also incorporate them into your application.
You might also want to modularize your control completely (with all
associated resources) into a self-contained dynamic-link library (DLL). This
extra degree of encapsulation can be very beneficial, enabling you to use
the control simultaneously in several applications. You will also be able to
use it with the new Windows 3.0 Dialog Editor by adhering to a few simple
standards.
Creating Installable Control Libraries
When creating an installable control you must follow the procedures just
outlined, plus a few additional steps that permit the control to exist in a
DLL. DLLs form the foundation upon which Windows is built. Unfortunately,
DLLs impose certain programming restrictions. Perhaps the most significant
is the SS != DS issue. Unlike conventional Windows programs, DLLs have no
stack segment and only one data segment. Consequently, when an application
calls a function within a DLL, it has to use its own stack segment and the
library's data segment. The result is that many of the standard C run-time
library functions cannot be used within a DLL.
Library Module Definition File To create an installable control, first
define a DLL containing a series of exported functions that are associated
with a series of predefined ordinal values. Create the DLL using the
following module definition file, where Xxxx represents the class name of
the control:
LIBRARY Xxxx
DESCRIPTION Xxxx Control Library'
EXETYPE WINDOWS
STUB WINSTUB.EXE'
CODE MOVEABLE DISCARDABLE SHARED PRELOAD
DATA MOVEABLE SINGLE PRELOAD
HEAPSIZE 1024
EXPORTS
WEP @1 RESIDENTNAME
XxxxInfo() @2
XxxxStyle() @3
XxxxFlags() @4
XxxxWndFn() @5
XxxxDlgFn() @6
You should always use the same predefined ordinal values for each of these
exported functions. The XxxxDlgFn function associated with the XxxxStyle
function must also be exported since it is used directly by Windows.
Library Entry Points In addition to the module definition file, you will
need to create several different source code modules, each containing one or
more of the exported functions discussed below. These functions must be
present for the Windows 3.0 Dialog Editor to use the control. The next
section describes each of these required functions in detail. Note that all
the required data structures are defined in CONTROL.H (see Figure 1), one of
the files used to build the sample control discussed later in this article.
BOOL FAR PASCAL XxxxInit( hInstance )
The XxxxInit function is responsible for all the initialization needed to
use the DLL. This function is normally called by the assembly language entry
point to the library. After saving the library instance handle using a
global static variable, this function should register the control class and
initialize the local heap (if necessary) by calling LocalInit.
Parameter Type Description
hInstance HANDLE Library instance handle
The return value indicates the success or failure of the library
initialization. A return value of FALSE indicates that the initialization
process has failed; TRUE indicates that the control class has been
registered and initialization has succeeded.
HANDLE FAR PASCAL XxxxInfo()
The XxxxInfo function provides the calling application with information
about the control library. Based on the information returned, the
application can create instances of the control using one of the supported
styles. If the control was used with the Windows Dialog Editor, for example,
this function could be used to query the library about the different control
styles it can support.
The return value is a handle to a CTLINFO data structure. This information
becomes the property of the caller and must be explicitly released using the
GlobalFree function when it is no longer needed. If there is insufficient
memory available to define this structure, a NULL handle is returned.
The CTLINFO structure, defined in CONTROL.H (see Figure 1), specifies the
class name and version number and lists each of the supported control styles
with suggested size information and a short description.
/* general style & size definitions */
#define CT_STD 0x0000
#define CTLTYPES 12
#define CTLDESCR 22
#define CTLCLASS 20
#define CTLTITLE 94
typedef struct {
WORD wType;
WORD wWidth;
WORD wHeight;
DWORD dwStyle;
char szDescr[CTLDESCR];
} CTLTYPE;
typedef struct {
WORD wVersion;
WORD wCtlTypes;
char szClass[CTLCLASS];
char szTitle[CTLTITLE];
char szReserved[10];
CTLTYPE Type[CTLTYPES];
} CTLINFO;
typedef CTLINFO * PCTLINFO;
typedef CTLINFO FAR * LPCTLINFO;
The CTLTYPE structure has the following fields:
Field Description
wType Defines the type of control. While only the CT_STD option is
currently supported, other types may be defined in the future.
wWidth Defines the suggested width of the control. If the most
significant bit is zero, the remaining bits specify the default width
in RC coordinates; otherwise, the remaining bits specify the default width
in pixels. One horizontal RC unit is the width of the system
font divided by four.
wHeight Defines the suggested height of the control. If the most
significant bit is zero, the remaining bits specify the default height
in RC coordinates; otherwise, the remaining bits specify the default height
in pixels. One vertical RC unit is the height of the system font
divided by eight.
dwStyle Specifies the initial style flags used to obtain this
particular control variant. This value includes both the control-defined
style flags in the low-order word and the Windows-defined flags in the
high-order word.
szDescr Defines the name to be used by other applications when
referencing this particular variant of the base control class.
The CTLINFO structure contains the following fields:
Field Description
wVersion Specifies the control version number. Although you can start
your numbering scheme from 1, most implementations use the lower
two digits to represent minor releases. Using this scheme, the first version
would be numbered 100.
wCtlTypes Specifies the number of control types supported by this
class. This value should always be greater than zero and less than
or equal to CTLTYPES.
szClass Specifies with a NULL-terminated string the control class
name supported by the DLL.
szTitle Specifies with a NULL-terminated string copyright or author
information relating to the control library.
Type Specifies an array of CTLTYPE data structures containing
information relating to each of the control types supported by
the class.
BOOL FAR PASCAL XxxxStyle( hWnd, hCtlStyle, lpfnStrToId, lpfnIdToStr )
The XxxxStyle function enables the calling application to edit the style of
a particular control. When this function is called, a modal dialog box
should be displayed that permits the user to edit the CTLSTYLE parameters.
The display of this dialog box should be consistent with Style dialog boxes
provided by existing controls.
Parameter Type Description
hwnd HWND Handle to parent window
hCtlStyle HANDLE Handle to CTLSTYLE data
lpfnStrToId LPFNSTRTOID String-to-ID conversion function
lpfnIdToStr LPFNIDTOSTR ID-to-string conversion function
The XxxxStyle function returns a value of TRUE if the CTLSTYLE structure was
changed. Otherwise, it returns FALSE.
The CTLSTYLE structure specifies the various attributes of the selected
control, including the current style flags, location, dimensions, and
associated text.
#define CTLCLASS 20
#define CTLTITLE 94
typedef struct {
WORD wX;
WORD wY;
WORD wCx;
WORD wCy;
WORD wId;
DWORD dwStyle;
char szClass[CTLCLASS];
char szTitle[CTLTITLE];
} CTLSTYLE;
typedef CTLSTYLE * PCTLSTYLE;
typedef CTLSTYLE FAR * LPCTLSTYLE;
The CTLSTYLE structure contains the following fields:
Field Description
wX Specifies in screen coordinates the X origin of the control relative
to the client region of the parent window.
wY Specifies in screen coordinates the Y origin of the control relative
to the client region of the parent window.
wCx Specifies the current width of the control in screen coordinates.
wCy Specifies the current height of the control in screen coordinates.
wId Specifies the current ID number of the control. Although you can
allow the user to change this value, in most situations changes
should be made in conjunction with the string-to-ID and ID-to-string
conversion functions provided as parameters (see below).
dwStyle Specifies the current style of the control. The low-order word
contains the control-specific flags; the high-order word
contains the Windows-specific flags. You should let the user change these
flags to any values supported by your control library.
szClass Specifies a NULL-terminated string representing the name of the
current control class. Don't allow the user to edit this field-it is
provided for informational purposes only.
szTitle Specifies the text associated with the control with a
NULL-terminated string. This text is usually displayed inside the
control or may be used to store other associated information required by the
control.
The lpfnStrToId and lpfnIdToStr functions require a little further
explanation. The primary purpose of the XxxxStyle function is to display a
dialog box through which the user can edit the various parameters associated
with the control. As you know, each control is associated with an ID value.
This value is used by the parent window or dialog box to keep track of each
of its child windows.
/* ID to string translation function prototypes */
typedef WORD (FAR PASCAL *LPFNIDTOSTR)( WORD, LPSTR, WORD );
typedef DWORD (FAR PASCAL *LPFNSTRTOID)( LPSTR );
The lpfnIdToStr function, an optional parameter provided by the calling
application, enables you to translate the ID from the CTLSTYLE data
structure to a text string. This text string can then be displayed in place
of the numeric value. The first parameter is the control ID; the second, a
long pointer to an empty text string; the third parameter is the maximum
length of this string. The value returned by this function is the number of
characters copied to the string. If a value of zero is returned, it can be
assumed that the function call failed.
The lpfnStrToId function operates exactly in reverse. This function takes a
string containing an ID value as a parameter and returns a doubleword
result. If the LOWORD of the result is nonzero, then the HIWORD contains the
new control ID value (which you can use to update the CTLSTYLE structure).
If the LOWORD of the result is zero, the ID string was undefined.
The calling application (in this case, usually the Dialog Editor) keeps
track of the ID strings and their corresponding values. The control style
function accesses this information using the lpfnStrToId and lpfnIdToStr
functions. The calling application can produce a header file containing
these ID strings and values for use by the application under development.
WORD FAR PASCAL XxxxFlags( wStyle, lpszStyle, wMaxString )
The XxxxFlags function translates the control style flags provided into a
corresponding text string for output to a text or RC file. Note that you
should return the same control style definitions specified in the #include
file that accompanies your control library.
Parameter Type Description
wStyle WORD Control style
lpszStyle LPSTR Style flags string
wMaxString WORD Maximum string length
The return value is the length of the resulting lpszStyle string. If an
error occurs, zero is returned.
LONG FAR PASCAL XxxxWndFn( hwnd, wMsg, wParam, lParam )
The XxxxWndFn function is the conventional window procedure responsible for
processing all the messages sent to the control. The messages that this
function should be capable of handling were described earlier.
Parameter Type Description
hwnd HWND Handle to the control window
wMsg WORD Message identifier
wParam WORD Additional message data
lParam LONG Additional message data
The return value indicates the result of message processing. The possible
return values depend on the actual message sent.
BOOL FAR PASCAL XxxxDlgFn( hDlg, wMsg, wParam, lParam )
The XxxxDlgFn function is a conventional window procedure that processes all
the messages sent to the control style dialog box. This dialog box should
display the control ID, title, and other related information. The user
should be able to define different control styles and change the associated
ID and title with this dialog box.
Parameter Type Description
hDlg HWND Handle to the style dialog box
wMsg WORD Message identifier
wParam WORD Additional message data
lParam LONG Additional message data
The return value indicates the result of message processing. If a nonzero
value is returned, the dialog box manager will assume that the dialog
function has processed the message. If zero is returned, the dialog box
manager will process the message.
Building the Control Library After you have defined the control library
components, you can create the installable control much as you would any
other Windows application. Make sure you use the appropriate compilation
flags, verifying especially that SS != DS.
To be consistent, consider structuring the various modules and components of
your library in the following manner:
Name Description
Xxxx Make file
Xxxx.H Public header file
Xxxx.D Private header file
Xxxx.DEF Module definition file
Xxxx.RC Resource file
Xxxx0.ASM DLL entry module
Xxxx1.C Initialization module
Xxxx2.C Message processing module
Xxxx3.C Style dialog box module
Xxxx4.C Information module
Xxxx5.C Style flags module
Sample Control
To put all this in perspective, let's work through a sample control. The
Page selection control is a relatively simple control that facilitates the
selection of a numbered page within a small range of pages. You can select a
particular page by clicking the mouse inside one of the control's page
symbols or by using the cursor when the control has the keyboard input focus
(see Figure 2). You might use this control to support the selection of a
particular page in a document, specify a print range, or perhaps enable the
user to select a particular page of a multipage data entry form.
The library PAGE.DLL completely defines the installable Page control. When
an instance is created, the Page control responds to the following set of
messages, as defined in the public header file, PAGE.H (see Figure 3).
PM_SETRANGE
Used to set the number of pages displayed inside the control (numbered from
1 to n). Note that the currently selected page is not reset. The default
range is set to the maximum number of pages that can be displayed.
Parameter Description
wParam The new number of pages
lParam (not used)
The return value is nonzero if the new range was successfully defined. If an
error occurs, zero is returned.
PM_GETRANGE
Used to retrieve the current number of pages displayed inside the control.
Parameter Description
wParam (not used)
lParam (not used)
The return value indicates the number of pages displayed.
PM_SETSELECT
Used to define the currently selected page.
Parameter Description
wParam An index for the newly selected page in the range of 0 to n1,
where n is the current page range
lParam (not used)
The return value is the index of the newly selected page; in most cases this
is identical to the wParam value.
PM_GETSELECT
Used to retrieve the index to the currently selected page.
Parameter Description
wParam (not used)
lParam (not used)
The return value is the index to the currently selected page (from 0 to n1).
PN_SELCHANGE
Whenever the user selects a particular page using the control, this
notification message is sent to the parent window, indicating that the
selected page has changed. Notification messages are used to inform the
control's parent window of the control of changes that the parent may be
interested in. As with all control notification messages, the selection
change notification message is sent to the parent window in the following
format:
Parameter Description
wMsg WM_COMMAND
wParam Page control ID
lParam Low-order word is the window handle
High-order word is the notification code (PN_SELCHANGE)
Page Control Components
The Page control library is constructed from the following files.
File Description Figure Number
PAGE Make file 4
PAGE.H Public header file 3
PAGE.D Private header file 5
PAGE.DEF Module definition file 6
PAGE.RC Resource file 7
PAGE0.ASM DLL entry module 8
PAGE1.C Initialization module 9
PAGE2.C Message processing module 10
PAGE3.C Style dialog box module 12
PAGE4.C Information module 13
PAGE5.C Style flags module 14
The make file, PAGE (see Figure 4), is almost identical to that used by any
Windows application except that it uses the Aw and D_WINDLL compile flags.
These flags instruct the C compiler to generate code in a DLL format with SS
!= DS.
The next file, PAGE.H (see Figure 3), is the public header file associated
with the control library. Because this file is designed to be used by
applications using the control, it contains only the control commands,
notification codes, and function prototypes.
The private header file PAGE.D (see Figure 5) is associated with PAGE.H.
This file defines the values used throughout the control library that do not
need to be made public. For example, the following fragment from PAGE.D
defines various constants that are used when registering the Page window
class and when returning information about the control:
#define PAGE_CLASSEXTRA 0
#define PAGE_WNDEXTRA 18
#define PAGE_VERSION 100
#define PAGE_NAME "Page"
#define PAGE_COLOR NULL
#define PAGE_WNDSTYLE WS_CHILD|WS_VISIBLE|WS_TABSTOP
#define PAGE_CLASSSTYLE CS_HREDRAW|CS_VREDRAW|CS_DBLCLKS|CS_GLOBALCLASS
Note the specification of the CS_DBLCLKS and CS_GLOBALCLASS class style
flags-they are extremely important. If you don't specify CS_DBLCLKS, you
will not be able to bring up the control style dialog box inside the dialog
editor by double-clicking inside the control. The CS_GLOBALCLASS flag is
also necessary, as the control class must be made publicly available.
PAGE.DEF (see Figure 6), the module definition file, is almost identical to
that described previously in the article, except that obviously Xxxx is
replaced with the class name Page.
The library resource file, PAGE.RC (see Figure 7), defines the PageStyle
dialog box used by the PageDlgFn function. This dialog box displays the
information passed to the PageStyle function, allowing the user to define
the control identifier when used in conjunction with the dialog editor.
PAGE0.ASM (see Figure 8) is the assembly language entry point to the DLL.
Remember that in order to create DLLs, an assembly language entry point must
call the control initialization function PageInit whenever the control is
loaded.
The PAGE1.C module (see Figure 9) contains the PageInit function. If this is
the first attempt to initialize the control, this function defines the Page
control class and registers it. If the registration process succeeds, the
library instance handle is saved and a value of TRUE is returned. The
instance handle is saved because it will be used throughout the other
modules in the library.
The next module, PAGE2.C (see Figure 10), is the most complex. It contains
the window message processing function PageWndFn. Most of this function (and
its associated helper functions) is quite typical, but two sections might
need further explanation.
The first section requiring explanation is the update of the Page control
window, which is performed when a WM_PAINT message is received or when the
user selects a different page number. Although most of this code consists of
straightforward Graphics Device Interface (GDI) calls, you should know how
the update colors are defined.
The update colors involve the use of a new pen and background brush. The pen
is created using the current system window frame color and selected into the
display device context. The background brush is obtained from the parent
window, enabling it to define the background color of the control, just as
Windows does with the system buttons.
/* define appropriate pen */
hNewPen = CreatePen( PS_SOLID, 1, GetSysColor(COLOR_WINDOWFRAME) );
hOldPen = (hNewPen) ? SelectObject(Ps.hdc,hNewPen) : NULL;
/* define appropriate brush and text colors*/
hNewBrush = (HBRUSH)SendMessage(
PARENT (hwnd),
WM_CTLCOLOR,
Ps.hdc,
MAKELONG(hwnd,CTLCOLOR_BTN)
);
hOldBrush = hNewBrush ? SelectObject(Ps.hdc,hNewBrush) : NULL;
Creating the three-dimensional look of each page icon inside the control
(see Figure 11), also requires some explanation. Unless this is done
carefully, the result may not look right when viewed with different system
colors and display drivers. Generally speaking, three-dimensional objects in
Windows 3.0 are filled using a solid brush based on the COLOR_BTNFACE system
color. The top and left edges are colored with a solid brush based on the
COLOR_WINDOW system color, while the bottom and right edges are colored with
a brush based on the COLOR_BTNSHADOW system color. These values must be used
for the three-dimensional look to appear correctly on a monochrome display.
The DrawPage function uses these system colors when drawing each page icon
in the following manner:
/* draw page outline */
hOldBrush = SelectObject( hDC, CreateSolidBrush(GetSysColor(COLOR_BTNFACE))
);
Polygon( hDC, ptPage, 7 );
DeleteObject( SelectObject(hDC,hOldBrush) );
/* draw white border */
hOldPen = SelectObject( hDC, CreatePen(PS_SOLID,1,GetSysColor(COLOR_WINDOW))
);
MoveTo( hDC, ptPage[5].x + 1, ptPage[5].y - 1 );
LineTo( hDC, ptPage[5].x + 1, ptPage[6].y + 1 );
LineTo( hDC, ptPage[1].x, ptPage[1].y + 1 );
DeleteObject( SelectObject(hDC,hOldPen) );
/* draw shadow */
hOldPen = SelectObject( hDC,
CreatePen(PS_SOLID,1,GetSysColor(COLOR_BTNSHADOW)) );
MoveTo( hDC, ptPage[5].x + 1, ptPage[5].y - 1 );
LineTo( hDC, ptPage[4].x - 1, ptPage[4].y - 1 );
LineTo( hDC, ptPage[3].x - 1, ptPage[3].y );
MoveTo( hDC, ptPage[5].x + 2, ptPage[5].y - 2 );
LineTo( hDC, ptPage[4].x - 2, ptPage[4].y - 2 );
LineTo( hDC, ptPage[3].x - 2, ptPage[3].y );
DeleteObject( SelectObject(hDC,hOldPen) );
The control style dialog box module, PAGE3.C (see Figure 12), contains the
PageStyle and PageDlgFn functions. The PageStyle function is relatively
simple; it sets a few temporary global variables and brings up the control
style dialog box.
The PageDlgFn function is also quite simple, but further explanation might
be necessary in two areas. The first involves the initial display of the
control ID value. If the lpfnIdToStr and lpfnStrToId parameters have been
defined, the control ID is passed to the lpfnIdToStr function and converted
into a string. The resulting text string is then displayed in place of the
numeric value.
/* define page id field */
if ( lpfnIdToStrTemp && lpfnStrToIdTemp ) {
(*lpfnIdToStrTemp)( lpCtlStyle->wId,
(LPSTR)szId, sizeof(szId) );
SetDlgItemText( hDlg, ID_IDENTIFER, szId );
}
When the user closes the style dialog box, the updated text string is
retrieved and translated back into an identifier using the lpfnStrToId
function. If the LOWORD of the returned double word is nonzero, then the
HIWORD contains the new control ID.
/* retrieve identifier string */
wSize = GetDlgItemText( hDlg, ID_IDENTIFIER, szId,
sizeof(szId) );
szId[wSize] = NULL;
/* translate identifier */
dwResult = (*lpfnStrToId)( (LPSTR)szId );
if ( LOWORD( dwResult ) )
lpCtlStyle->wId = HIWORD( dwResult );
The PAGE4.C module (see Figure 13) contains the PageInfo function that is
responsible for allocating and defining a CTLINFO data structure. Once this
structure has been defined, a handle is returned to the calling application.
It then becomes the calling application's responsibility to release the
memory associated with this handle when it is no longer needed.
The last file, PAGE5.C (see Figure 14), contains the PageFlags function. As
mentioned previously, this function is responsible for converting control
style flags into their text string equivalents. In situations where multiple
flags are appropriate, they can be combined with the bitwise OR operator. In
this case, since the Page control does not define any private style flags,
no action is taken.
To build the Page control library, you need the Windows 3.0 SDK, Microsoft C
5.1 or 6.0, and Microsoft Macro Assembler. The result will be PAGE.DLL, a
DLL that you can experiment with using the Windows 3.0 Dialog editor or
reference in your own application using the associated import library.
Using Installable Controls in Your Applications
Before you can use an installable control in an application, you must
include the appropriate header file and register the control with the
system. If your control is defined in a module linked into your application,
you must ensure that the initialization function is called before the
control class is used in a window or dialog box. If your control is
incorporated into a library with the mechanisms described previously, you
can load the library (and thus register the class) in one of three ways.
The first method is referencing the library initialization function
explicitly in the import section of the module definition file (DEF)
associated with your application. If you do this, Windows will automatically
load all the referenced libraries when it loads your application. If you
wanted to use the Page control in your application, for example, the
following statement in your DEF file would cause the library to be loaded:
IMPORTS
Page.PageInit
The second method involves an explicit reference to one of the control
library functions inside your application. For example, in certain
situations you might want to call the control information or style
functions. If you create an import library for your installable control
(using the IMPLIB facility) and include it with the list of object libraries
searched during the linking of your application, Windows will automatically
load the control library when resolving these external references. In the
process of loading the library, the control initialization function will be
called, and your control will be registered with the system.
The third approach is a manual one in which you explicitly load and free the
desired control library. The advantage of this approach is that you
completely control how the library is used, even to the extent of enabling
the user to determine which library is used (much as is done by the Dialog
Editor).
The following code fragment demonstrates this:
/* definitions */
HANDLE hCtlLibrary;
/* load the library */
hCtlLibrary = LoadLibrary( "PAGE.DLL" );
if ( hCtlLibrary ) {
/* USE THE CONTROL HERE */
/* release the library */
FreeLibrary( hCtlLibrary );
}
Using the Control
Once you have made sure that your library will be loaded successfully, you
can use the new control throughout your application, even as a child of an
existing window. If you wish, you can create child windows based on the
control class using an explicit CreateWindow function call. The following
code fragment shows how an instance of the Page control can be created:
hWnd = CreateWindow(
"Page",
"",
WS_CHILD | WS_TABSTOP,
8,
8,
64,
64,
hWndParent,
ID_PAGE,
hInstance,
NULL
);
Note the specification of the control class ("Page"), the use of appropriate
style flags, and the definition of a unique ID value that will be associated
with the control.
Once the control has been created, the parent window will be responsible for
managing many of its activities, such as displaying or hiding the control,
repositioning it whenever the parent window size changes, and processing all
notification messages generated by the control.
Using installable controls in dialog boxes is easier than creating them
yourself, because Windows performs many of the routine tasks for you.
Windows creates dialog boxes from templates defined in your resource file.
Once your control library has been loaded, all you need to do to use the
control is reference the public header file associated with it in your
resource file, and use the following control placement statement inside each
dialog box template:
CONTROL <text>, <id>, <class>, <flags>,
<x>, <y>, <width>, <height>
For example, the following statements define two instances of the Page
control:
CONTROL "", ID_PAGE1, "Page",
WS_CHILD|WS_TABSTOP, 8, 8, 64, 64
CONTROL "", ID_PAGE2, "Page",
WS_CHILD, 24, 24, 64, 64
Using Installable Controls with the Dialog Editor
Another way to use installable controls in your applications is with the new
Windows 3.0 Dialog Editor. Before you can use an installable control in the
Dialog Editor, you must formally make it part of the Windows environment.
One way to do this is to edit your WIN.INI file manually, creating a section
that specifies the association between each control class name and the DLL
responsible for it. To add the Page control to Windows this way, you would
add a statement such as the following to your WIN.INI file:
PAGE=C:\WINDOWS\SYSTEM\PAGE.DLL
The second and perhaps easier method is to load the Dialog Editor and select
Add Custom Control under the File menu. When you do this, the dialog box in
Figure 15 will be displayed. Using this dialog box you can specify
installable controls with either of two options. The first option specifies
controls permanently and requires the use of a DLL. First, copy the DLL to
your SYSTEM subdirectory. Then select the first option in the dialog box
and enter the full pathname of the library supporting the control. This
option causes an entry to be made automatically in your WIN.INI file
associating the control class with this DLL (as previously described). From
then on, this control can be used in the Dialog Editor in the same manner as
any predefined system control. The advantage of this process is that the
control is represented accurately on the screen and you can select the
control styles defined by the library.
The second option is temporary and does not require the use of a DLL to
define the control. If you choose this option, simply enter the name of the
control class in the edit field provided. The Dialog Editor will then let
you work with this control class. It will be represented on the screen
simply as a rectangle with the class name centered inside (see Figure 16).
Temporary control classes must be redefined each time you run the Dialog
Editor.
Once you have defined an installable control, you can start using it inside
your dialog boxes. To place a custom control in the new dialog box, select
the Custom option under the Control menu. This will display the dialog box
shown in Figure 17. Select a control class from this dialog box. A preview
of the control you have selected is displayed on the right side of the
dialog box. You will then be returned to the editor to place the control in
the same manner used for predefined system controls. Or you can simply use
the Dialog Editor toolbox. Make the toolbox visible by selecting the Toolbox
option under the Options menu. Select the tool in the lower right corner to
display the control selection dialog box (see Figure 17).
You can change the style of a custom control after it has been placed.
Select the control, then either double-click inside the control or select
the Styles option under the Edit menu. A dialog box, defined by the
control's DLL itself, will be displayed. For example, when the Page control
is selected, the dialog box in Figure 18 is displayed. Using this dialog box
you can define the identifier associated with the control. For more
sophisticated controls you can experiment with the various styles supported
by the control class. In some cases, this dialog box will also display a
small sample of the current control style in the lower right corner. When
you have specified the new control style, click OK. The control in your
dialog box will be updated automatically to reflect the changes you have
specified.
You can also use the Dialog Editor to remove temporary or permanent control
classes. Select the Remove Custom Control option under the File menu; a
dialog box that lets you select the control class to be removed will be
displayed. If the control was permanent, the Dialog Editor will release the
associated DLL and update your WIN.INI file. However, the library may remain
in memory if other applications are still using this control class. Also,
you cannot remove a class (either permanent or temporary) in the dialog box
currently being edited. When you remove custom controls from the Dialog
Editor, the DLL associated with the removed control is not deleted from your
hard disk.
Conclusion
Although the process of designing, creating, and using an installable
control can be quite complicated, the end result is well worth the trouble.
With a little effort, you should be able to create a custom collection of
installable controls that you can use over and over again when building
Windows applications.
1 For ease of reading, "Windows" refers to the Microsoft Windows graphical
environment. "Windows" refers only to this Microsoft product and is not
intended to refer to such products generally.
A New Multiple Document Interface API Simplifies MDI Application Development
Charles Petzold
The Multiple Document Interface (MDI) is a specification for applications
written for the Microsoft Windows[tm] environment or OS/2 Presentation
Manager programs. The specification describes a window structure and user
interface that allow the user to work with multiple documents in a single
application (such as text documents in a word processing program or
spreadsheets in a spreadsheet program). Just as Windows1 maintains multiple
application windows within a single screen, an MDI application maintains
multiple document windows within a single client area.
The MDI specification dates back to Windows Version 2.0 and was first used
in Microsoft Excel for Windows. Windows programmers who wrote MDI
applications before the release of Windows Version 3.0 must be commended for
their bravery and stamina. I am certainly not one of them. When I first saw
a beta version of Microsoft Excel, I was intrigued by several aspects of the
program that appeared to exhibit nonstandard behavior for a Windows
application. Using SYMDEB, I spent most of a day tracing through the
Microsoft Excel code attempting to determine what magic its programmers had
performed. It turned out not to be magic at all, but simply an enormous
amount of work.
With Windows 3.0, however, much of that work has already been done for the
applications developer. Windows 3.0 includes one new window class, four new
function calls, two new data structures, and eleven new messages
specifically designed to simplify MDI applications.
The Elements of MDI
The Multiple Document Interface is described in the Systems Application
Architecture, Common User Access: Advanced Interface Design Guide (IBM,
1989). The main application window of an MDI program is conventional: it has
a title bar, a menu, a sizing border, a system menu icon, and
minimize/maximize icons. However, the client area (called a workspace) is
not directly used to display program output. This workspace contains zero or
more child windows, each of which displays a document.
These document child windows look like normal application windows, except
they do not have a menu. The menu on the main application window applies to
the document windows.
Only one document window is active at any time. The active window appears in
front of all the other document windows and is indicated by a highlighted
title bar. All the document child windows are clipped to the workspace area
and never appear outside the application window. Figure 1 shows several
documents in Microsoft Excel.
At first, MDI seems a fairly straightforward job for the Windows programmer.
All you need to do is create a WS_CHILD or WS_POPUP window for each
document, making the program's main application window the parent of the
document window. But with a little exploration of an MDI application such as
Microsoft Excel, you'll find some complications that require some difficult
code.
The first intricacy is that an MDI document window can be maximized. The
title bar of the document window (normally used to show the filename of the
document in the window) disappears, and the filename appears appended to the
application name in the application window's title bar. The system menu icon
of the document window becomes the first item in the top-level menu of the
application window. The icon to restore the size of the document window
becomes the last item in the top-level menu and appears to the far right.
Second, although some MDI programs don't allow this, a document window
should also be minimizable. Its icon should appear at the bottom of the
workspace. (Generally an MDI application will use different icons for the
main application window and each type of document.)
The third complication is that the system keyboard accelerator to close a
document window is the same as that used to close the main window, but it
uses the Ctrl key rather than Alt. That is, Alt-F4 closes the application
window while Ctrl-F4 closes the document window. In addition, Ctrl-F6
switches among the child document windows within the active MDI application.
Alt-Spacebar invokes the system menu of the main window, as usual. Alt-Minus
invokes the system menu of the active child document window.
Fourth, when using the cursor keys to move among items on the menu in an MDI
application, control passes from the application system menu, to the
currently active document system menu, then back to the first item on the
menu bar. (Standard windows pass control from the system menu directly to
the first item on the menu bar.)
Fifth, if the application is capable of supporting several different types
of child windows (for example, the sheet and chart documents in Microsoft
Excel), the menu should reflect the operations associated with that type of
document. This requires that the program change the menu when a different
document window becomes active. In addition, when no document window exists,
the menu should be stripped down to only those operations involved in
opening a new document.
Finally, the top-level menu bar has an item called "Window." By convention,
this is the last item on the top-level menu except for Help. The "Window"
submenu generally has options to arrange the document windows within the
workspace. Document windows can be cascaded from the upper left or tiled so
that each document window is fully visible. This submenu also has a list of
all the document windows. Selecting one moves that document window to the
foreground.
All these aspects of MDI are supported in Windows 3.0. Some overhead is
required, of course (as will be shown in a sample program), but it's not
anything like the code you'd have to write to support all these features
directly.
Windows Version 3.0 and MDI
Some new terminology is necessary when discussing Windows 3.0 MDI support.
(I'm afraid this terminology will be a little confusing to OS/2 Presentation
Manager programmers. The use of the terms frame window and client window in
a Windows MDI program isn't the same as in Presentation Manager.)
The main application window is called the frame window. Just as in a
conventional Windows program, this is a window in the WS_OVERLAPPEDWINDOW
style.
An MDI application also creates a client window based on the predefined
window class MDICLIENT. The client window is created by a call to
CreateWindow using the WS_CHILD style. The last parameter to CreateWindow is
a pointer to a small structure of type CLIENTCREATESTRUCT (see Figure 2).
This client window covers the client area of the frame window and is
responsible for much of the MDI support. The color of this client window is
the system color COLOR_APPWORKSPACE.
The document windows, called child windows, are created by initializing a
structure of type MDICREATESTRUCT (see Figure 2) and sending the client
window a WM_MDICREATE message with a pointer to this structure. The document
windows are children of the client window, which in turn is a child of the
frame window. The parent-child hierarchy is shown in Figure 3.
You need a window class (and window procedure) for the frame window and each
type of child window supported by the application. You don't need a window
procedure for the client window because the window class is preregistered.
Sample Program
The Windows 3.0 Software Development Kit (SDK) includes a sample program
called MULTIPAD that demonstrates how to write an MDI program. However,
MULTIPAD contains quite a bit of code that has nothing to do with MDI. It
might be easier for you to get a better feel for MDI programming by
examining a smaller program that does little except demonstrate the MDI
features. The components of this program, called MDIDEMO, are shown in
Figure 4. To compile MDIDEMO, you need Microsoft C Version 6.0 and the
Windows 3.0 SDK.
MDIDEMO (see Figure 5) supports two types of extremely simple document
windows: one displays "Hello, World!" in the center of its client area and
the other displays a series of random rectangles. (In the source code
listings and identifier names, these are referred to as the Hello document
and the Rect document.) Different menus are associated with these two types
of document windows (compare Figures 5 and 6). The document window that
displays "Hello, World!" has a menu that allows changing the color of the
text.
Three Menus
Let's turn first to the MDIDEMO.RC resource script (see Figure 4), which
defines three menu templates used by the program. The program displays the
MdiMenuInit menu when no document windows are present. This menu simply
allows the creation of a new document or exit from the program.
The MdiMenuHello menu is associated with the document window that displays
"Hello, World!" The File submenu allows the opening of a new document of
either type, the closing of the active document, and exiting from the
program. The Color submenu lets you set the text color. The Window submenu
has options to arrange the document windows in cascade or tile fashion, to
arrange the document icons, and to close all the windows. This submenu will
also list all the document windows as they are created.
The MdiMenuRect menu is associated with the random rectangle document. This
menu is the same as the MdiMenuHello menu except that it does not include
the Color submenu.
The MDIDEMO.H header file (see Figure 4) defines all the menu identifiers as
well as three constants:
#define INIT_MENU_POS 0
#define HELLO_MENU_POS 2
#define RECT_MENU_POS 1
These identifiers indicate the position of the "Window" submenu in each of
the three menu templates. This information is needed by the program to
inform the client window where the document list is to appear. Of course,
the MdiMenuInit menu doesn't have a "Window" submenu, so I've indicated that
the list should be appended to the first submenu (position 0). The list will
never actually be viewed there, however. You'll see why when I discuss the
program.
The IDM_FIRSTCHILD identifier doesn't correspond to a menu item. This is the
identifier that will be associated with the first document window in the
list that will appear in the "Window" submenu. You should choose this
identifier to be greater than all the other menu IDs.
Program Initialization
In MDIDEMO.C (see Figure 4), WinMain begins by registering window classes
for the frame window and the two child windows. The window procedures are
called FrameWndProc, HelloWndProc, and RectWndProc. Normally, different
icons should be associated with these window classes. For simplicity, I've
used the standard IDI_APPLICATION icon and no icon for the Rect window.
(Note that I've defined the hbrBackground field of the WNDCLASS structure
for the frame window class to be the COLOR_APPWORKSPACE system color. This
is not entirely necessary because the client area of the frame window is
covered up by the client window, and the client window is this color anyway.
However, using this color looks a little better when the window is first
displayed.) The lpszMenuName field is set to NULL for each of these three
window classes. For the Hello and Rect child window classes, this is normal.
For the frame window class, I have chosen to indicate the menu handle in the
CreateWindow function when creating the frame window.
The window classes for the Hello and Rect child windows allocate extra space
for each window using a nonzero value as the cbWndExtra field of the
WNDCLASS structure. This space will contain a local memory handle that will
reference a block of memory used to store information unique to each
document window. This block of memory is the size of the HELLODATA or
RECTDATA structures defined near the top of MDIDEMO.C.
Next, WinMain uses LoadMenu to load the three menus and saves their handles
in global variables. Three calls to the GetSubMenu function obtain handles
to the "Window" submenu on which the document list will be appended. These
handles are also saved in global variables.
A call to CreateWindow in WinMain creates the frame window. During the
processing of WM_CREATE in FrameWndProc, the frame window creates the client
window. This involves another call to CreateWindow. The window class is set
to MDICLIENT, which is the preregistered class for MDI client windows. The
last parameter to CreateWindow must be set to a pointer to a structure of
type CLIENTCREATESTRUCT. This structure has two fields, hWindowMenu and
idFirstChild. The first field, hWindowMenu, is the handle of the submenu on
which the document list will be appended. In MDIDEMO, this is
hMenuInitWindow, which was obtained during WinMain. You'll see later how the
menu is changed. The second field, idFirstChild, is the menu ID to be
associated with the first document window in the document list. This is
simply IDM_FIRSTCHILD.
Back in WinMain, MDIDEMO displays the newly created frame window and enters
the message loop. The message loop is a little different from usual-after
obtaining the message from the message queue with a call to GetMessage, an
MDI program passes the message to TranslateMDISysAccel. This function
translates any keystroke that may correspond to the special MDI accelerators
(such as Ctrl-F6) into a WM_SYSCOMMAND message. If the function
TranslateMDISysAccel returns TRUE (indicating that a message was
translated), TranslateMessage and DispatchMessage are not called. MDIDEMO
uses its own accelerators also, so the message loop should call the
TranslateMessage and DispatchMessage functions only if neither
TranslateAccelerator nor TranslateMDISysAccel returns TRUE.
Creating the Child Windows
The bulk of FrameWndProc is devoted to processing WM_COMMAND messages that
signal menu selections. As usual, the wParam parameter to FrameWndProc
contains the menu ID number.
For wParam values of IDM_NEWHELLO and IDM_NEWRECT, FrameWndProc must create
a new document window. This is done by initializing the fields of an
MDICREATESTRUCT structure (most of which correspond to CreateWindow
parameters) and sending the client window a WM_MDICREATE message with lParam
set to a pointer to this structure. The client window then creates the child
document window.
Normally, the szTitle field of the MDICREATESTRUCT structure would be the
file name corresponding to the document. The style field can be set to the
window styles WS_HSCROLL or WS_VSCROLL or both to include scroll bars in the
document window. The style field can also include WS_MINIMIZE or WS_MAXIMIZE
to display the document window initially in a minimized or maximized state.
The lParam field of the MDICREATESTRUCT structure provides a way for the
frame window and the child window to share some variables. This could be set
to a local or global memory handle that references a block of memory
containing a structure. During the WM_CREATE message in the child document
window, lParam is a pointer to a CREATESTRUCT structure; the lpCreateParams
field of this structure is a pointer to the MDICREATESTRUCT structure used
to create the window.
On receipt of the WM_MDICREATE message, the client window creates the child
document window and adds the title of the window to the bottom of the
submenu specified in the MDICLIENTSTRUCT structure. When the MDIDEMO program
creates its first document window, the submenu used is the File submenu of
the MdiMenuInit menu. We'll see later how this document list gets moved to
the Window submenu of the MdiMenuHello and MdiMenuRect menus.
The menus lists as many as nine documents, each preceded by an underlined
number (1 to 9). If more than nine document windows are created, this list
is followed by a "More windows..." item on the menu. This item invokes a
dialog box containing a list box that lists all the document windows (see
Figure 7). The automatic maintenance of this document list is one of the
nicest features of the Windows MDI support.
More Frame Window Message Processing
Let's continue with FrameWndProc message processing before turning to the
child document windows. When you select Close from the File menu, MDIDEMO
closes the active child window, and it obtains the handle to the active
child window by sending the client window a WM_MDIGETACTIVE message. If the
child window responds affirmatively to a WM_QUERYENDSESSION message, then
MDIDEMO sends the client window a WM_MDIDESTROY message to close the child
window.
Processing the Exit option from the File menu requires only that the frame
window procedure send itself a WM_CLOSE message. Processing the Tile,
Cascade, and Arrange Icons options from the Window submenu is a snap,
requiring only that the WM_MDITILE, WM_MDICASCADE, and WM_MDIICONARRANGE
messages be sent to the client window.
The Close All option is more complex. FrameWndProc calls EnumChildWindows to
enumerate the child windows through the callback function CloseEnumProc.
This function sends the child a WM_MDIRESTORE message and then
WM_QUERYENDSESSION. If the child window responds affirmatively to a
WM_QUERYENDSESSION message, CloseEnumProc closes the child window by sending
the WM_MDIDESTROY message to the client window. However, the icon title
windows (the text beneath each document icon) must not be destroyed. You can
check for these windows by a non-NULL return from GetWindow with a GW_OWNER
parameter.
You'll notice that FrameWndProc does not process any of the WM_COMMAND
messages signaling that one of the colors has been selected from the Color
menu. These messages are really the responsibility of the document window.
For this reason, FrameWndProc sends all unprocessed WM_COMMAND messages to
the active child window so that the child window can process those messages
that pertain to its window.
All the messages that the frame window procedure chooses not to process must
be passed to DefFrameProc. This is one of the new MDI functions in Windows
3.0. It replaces DefWindowProc in the frame window procedure. Even if a
frame window procedure traps the WM_MENUCHAR, WM_NEXTMENU, WM_SIZE, or
WM_SETFOCUS messages, they must also be passed to DefFrameProc.
Unprocessed WM_COMMAND messages must also be passed to DefFrameProc. In
particular, FrameWndProc does not process any of the WM_COMMAND messages
resulting from the user selecting one of the documents from the list in the
Window submenu. (The wParam values for these options begin with
IDM_FIRSTCHILD.) These messages are passed to DefFrameProc and processed
there.
Notice that the frame window does not need to maintain a list of window
handles of all the document windows it creates. If these handles are ever
needed (such as when processing the Close All option from the menu), they
can be obtained using GetWindow.
Child Document Windows
Now let's look at HelloWndProc, which is the window procedure used for the
child document windows that display "Hello, World!"
As with any window class used for more than one window, static variables
defined in the window procedure (or any function called from the window
procedure) are shared by all windows based on that window class.
Data that is unique to each window must be stored using a method other than
static variables. One such technique uses window properties. Another
approach (the one I employed) uses memory space that is reserved by defining
a nonzero value in the cbWndExtra field of the WNDCLASS structure used to
register the window class.
In MDIDEMO, I use this space to store a local memory handle that references
a block of memory the size of the HELLODATA structure. HelloWndProc
allocates this memory during the WM_CREATE message, locks it, initializes
the two fields (which indicate the currently checked menu item and the text
color), unlocks the block, and stores the local memory handle using
SetWindowWord.
When processing a WM_COMMAND message for changing the text colors (recall
that these messages originate in the frame window procedure), HelloWndProc
uses GetWindowWord to obtain a handle to the memory block containing the
HELLODATA structure. Using this structure, HelloWndProc unchecks the checked
menu item, checks the selected menu item, and saves the new color.
A document window procedure receives the WM_MDIACTIVATE message whenever the
window becomes active or inactive (indicated by a TRUE or FALSE value in
wParam). You'll recall that MDIDEMO has three different menus: MdiMenuInit,
used when no documents are present; MdiMenuHello, used when a Hello document
window is active; and MdiMenuRect, used when a Rect document window is
active.
The WM_MDIACTIVATE message provides an opportunity for the document window
to change the menu. If wParam is TRUE (meaning the window is becoming
active), HelloWndProc changes the menu to MdiMenuHello. If wParam is FALSE,
HelloWndProc changes the menu to MdiMenuInit.
HelloWndProc changes the menu by sending a WM_MDISETMENU message to the
client window. The client window processes this message by removing the
document list from the current menu and appending it to the new menu. This
is how the document list is transferred from the MdiMenuInit menu (which was
in effect when the first document is created) to the MdiMenuHello menu. The
SetMenu function should not be used to change a menu in an MDI application.
Another little chore involves the checkmarks on the Color submenu. Program
options such as these should be unique to each document. For example, you
should be able to set blue text in one window and red text in another. The
menu checkmarks should reflect the option chosen in the active window (see
Figure 8). For this reason, HelloWndProc unchecks the selected menu item
when the window is becoming inactive and checks the appropriate item when
the window is becoming active.
The window procedure gets the first WM_MDIACTIVATE message with wParam set
to TRUE when the window is first created, and the last (with wParam set to
FALSE) when the window is destroyed. When the user switches from one
document to another, the first document window receives a WM_MDIACTIVATE
message with wParam set to FALSE (at which time it sets the menu to
MdiMenuInit) and the second document window receives a WM_MDIACTIVATE
message with wParam set to TRUE (at which time it sets the menu to
MdiMenuHello or MdiMenuRect as appropriate). If all the windows are closed,
the menu is left as MdiMenuInit.
You'll recall that FrameWndProc sends the child window a WM_QUERYENDSESSION
when the user selects Close or Close All from the menu. HelloWndProc
processes the WM_QUERYENDSESSION and WM_CLOSE messages by displaying a
message box and asking the user if the window can be closed. (In a real
program, this message box would ask if a file needed to be saved.) If the
user indicates that the window should not be closed, the window procedure
returns zero. During the WM_DESTROY message, HelloWndProc frees the local
memory block allocated during the WM_CREATE message.
All unprocessed messages must be passed on to DefMDIChildProc (not
DefWindowProc) for default processing. The messages shown below must be
passed to DefMDIChildProc whether the child window procedure does something
with them or not.
WM_CHILDACTIVATE
WM_GETMINMAXINFO
WM_MENUCHAR
WM_MOVE
WM_NEXTMENU
WM_SETFOCUS
WM_SIZE
WM_SYSCOMMAND
RectWndProc is fairly similar to HelloWndProc, but it's a little simpler (no
menu options are involved and the window does not verify with the user if it
can be closed), so I needn't discuss it. But note that RectWndProc breaks
after processing WM_SIZE so it is passed to DefMDIChildProc.
Finishing Up
I mentioned earlier that the MDI support in Windows 3.0 involves one new
window class, two new data structures, four new function calls, and eleven
new messages. The window class, MDICLIENT, is used for creating the client
window. The two new data structures, CLIENTCREATESTRUCT and MDICREATESTRUCT,
are used for creating the client window and the child document windows.
I discussed three of the four new function calls: TranslateMDISysAccel,
DefFrameProc, and DefMDIChildProc. The fourth new function call is
ArrangeIconicWindows, which performs the same function in an MDI application
as the WM_MDIICONARRANGE message.
MDIDEMO demonstrates nine of the eleven new messages. These messages are:
WM_MDIACTIVATE
WM_MDICASCADE
WM_MDICREATE
WM_MDIDESTROY
WM_MDIGETACTIVE
WM_MDIICONARRANGE
WM_MDIRESTORE
WM_MDISETMENU
WM_MDITILE
The other two messages are WM_MDIMAXIMIZE and WM_MDINEXT. But because
Windows takes care of all the maximizing and switching of child windows,
applications usually don't have to use these messages.
One aspect of the Multiple Document Interface described in the IBM book
about Common User Access (CUA), and used in Microsoft Excel, is splitting
windows in half or into quadrants using small controls to the top and left
of the scroll bars. This is not part of the MDI support in Windows 3.0.
However, all other features of MDI described in CUA are provided. The new
features described here make MDI accessible to Windows programmers and are
implemented in a way that illustrates the power of the object-oriented
architecture of Windows.
Providing DDE Facilities to OS/2 Kernel Applications
Richard Hale Shaw
In previous articles, I've discussed how Dynamic Data Exchange (DDE) can be
used to transfer data among Presentation Manager (hereafter "PM")
applications. DDE is basically a protocol that uses PM message queues to
regulate the passing of data. But since DDE can be used only by programs
that open PM message queues, OS/2 kernel applications cannot access DDE
directly. If a kernel application does open a PM message queue, all I/O has
to come through Presentation Manager, which means that the application will
not be able to access the OS/2 subsystems (Vio, Kbd, Mou) from its screen
group.
When I wanted to provide Clipboard services to OS/2 kernel applications, I
wrote a PM program, PMServer (see "Accessing Presentation Manager Facilities
from Within OS/2 Kernel Applications," MSJ, Vol. 5, No. 1). This month, I'm
expanding PMServer to provide DDE services to kernel applications. As I
discussed in the first PMServer article, the natural vehicle for this
expansion is a Presentation Manager object window-specifically, an object
window for each kernel client that PMServer is servicing. Once I've added
object window facilities to PMServer, I'll expand the sample program,
PMAccess, that was originally presented with PMServer (see Figure 1).
CLIENT.C
PMServer's DDE processing code is contained in CLIENT.C (see Figure 2). This
module consists of three functions: CreateClient, the object window thread
function; DdeClientProc, the window procedure function for the object
window; and MakeDDESeg, a support function that creates data packets
suitable for transfer via DDE.
Since the purpose of CreateClient is to create a window and message queue
(even though the queue is in a separate thread of execution), it's not
unlike the main function of PMServer. It takes one parameter, a pointer to a
CLIENT data type:
typedef struct _client
{
USHORT isddeclient;
HQUEUE qhandle;
PID kclient_pid;
HWND hwnd;
HWND hwndpmserver;
PVOID threadstack;
CHAR appname[MAX_APPLICATION_LEN];
CHAR topicname[MAX_TOPIC_LEN];
} CLIENT;
typedef CLIENT *PCLIENT;
This definition is in PMSVMSGS.H (see Figure 2). The header file contains
all the message definitions and structures common to PMSERVER.C and
CLIENT.C.
The CLIENT data type is an expanded version of that used in the earlier form
of PMServer. Originally, it contained only a queue handle and a process ID
for each kernel client that registered itself with PMServer. Now it has been
expanded to include several additional items: a Boolean switch that
indicates whether a client is engaged in a DDE conversation, the handle of
the object window managing the conversation, PMServer's window handle, a
pointer to the object window's stack, and the application and topic names
that identify a DDE conversation.
PMServer initializes all these items except for the object window handle,
which won't be initialized until the object window has been created by
CreateClient. CreateClient inserts the handle into the CLIENT structure when
it creates the object window. As a result, the CLIENT structure provides
information that can be used by any thread (the main thread, the Queue
Manager thread, or the object window thread) or by either window (the main
window or the object window). CreateClient begins by saving the CLIENT
pointer passed to it from _beginthread. Then it calls WinInitialize, opens
the new message queue, and registers the new DdeClient window class.
The CLIENT pointer, anchor block handle, and window handle are all stored in
a CDATA structure:
typedef struct clientdata
{
HAB hab;
HWND hwndServer;
PCLIENT pclient;
} CDATA;
typedef CDATA *PCDATA;
Using WinCreateWindow, the address of this structure is passed to the newly
created object window as the first message parameter accompanying WM_CREATE.
This makes all three components of CDATA and the elements of the CLIENT
structure available to the new window. Note that this was accomplished in a
single call; there is no need for additional messaging and interprocess
communication.
What happens when DdeClientProc, the object window's window procedure,
receives the WM_CREATE message? First, the CDATA parameter is saved from
mp1, and a local CLIENT pointer is initialized from the one stored in the
structure. Then WinSetWindowPtr is called to save the CDATA pointer in the
reserved area of a window, allowing subsequent calls to WinQueryWindowPtr
(at the beginning of the function) to reinitialize the pointer upon each
call to the window procedure.
Next, the pointer's hwndServer member is initialized to NULL. The value of
this handle will be used to determine whether a PM application has joined
the proposed DDE conversation as a server. Finally, the function calls
WinDdeInitiate to propose the conversation (by broadcasting WM_DDE_INITIATE
to every PM application).
Note that the call to WinDdeInitiate is followed by a call to WinStartTimer.
This function starts a timer that will generate WM_TIMER messages at
specified intervals, in this case every tenth of a second. When the object
window receives a WM_TIMER message, it looks to see if the hwndServer handle
is NULL. If so, it assumes that no Presentation Manager program has posted a
WM_DDE_INITIATEACK to respond positively to the WM_DDE_INITIATE and posts a
PMS_DDE_INITNAK to the kernel client and a WM_QUIT to itself. (The
consequences of this message are described below.)
If a WM_DDE_INITIATEACK is received, the handle of the responding server
application (the DDE server) is saved in the hwndServer of the CDATA
pointer and a PMS_DDE_INITACK message is sent (via MsgQSend) to the kernel
client, signaling the start of the DDE conversation.
Managing the DDE Conversation
Once the object window is in place and the DDE conversation has been
initiated, the kernel client can begin to make data requests and receive
data. A kernel client can post a PMS_DDE_REQUEST or a PMS_DDE_ADVISE message
to the Queue Manager to initiate a data request or receive data updates.
These messages should be accompanied by the data item name. The Queue
Manager will comply by sending a PMSV_REQUEST or PMSV_ADVISE message to the
object window handle.
Upon receiving either of these messages, the object window will create a
DDESTRUCT data block (via a call to MakeDDESeg) and post a WM_DDE_REQUEST
or a WM_DDE_ADVISE via WinDdePostMsg. If no data is available, the DDE
server will set the appropriate bits and reply with a WM_DDE_ACK. The object
window will then post a PMS_DDE_NODATA message to the kernel client.
However, if data is received from the DDE server, it will arrive as a
DDESTRUCT data block in a WM_DDE_DATA message. Upon receiving this message,
the object window will allocate a piece of memory and reference it with a
pointer to the ITEMREQ structure:
typedef struct itemreq
{
CHAR item[MAX_ITEM_LEN];
CHAR value[1];
} ITEMREQ;
typedef ITEMREQ *PITEMREQ;
The item component is then copied as a NULL-terminated string from the data
block into the ITEMREQ structure and is followed by the data component.
Since PMServer supports only the transfer of text (CF_TEXT) data via DDE,
the data is assumed to be text and therefore a NULL-terminator is placed
after it in the ITEMREQ structure. Because the ITEMREQ space is big enough
for an ITEMREQ structure plus the data component, there is plenty of room
for both in the new space. After the data is copied into the ITEMREQ
structure, the new data package is passed to the kernel client as part of a
PMS_DDE_DATA message. Then the object window frees the DDESTRUCT segment and
the ITEMREQ pointer.
Note that the code for handling WM_DDE_DATA specifically checks to see if
the application name is Microsoft Excel. The documentation for DDE specifies
that the data size component of a DDESTRUCT (the cbData member) contains the
size of the entire data block. However, Microsoft Excel sets the size of the
cbData member as the size of the data component only, so an exception has to
be made when interpreting a DDESTRUCT passed from this application.
Closing the Object Window
When the DDE server terminates the conversation, it sends the object window
a WM_DDE_TERMINATE message. The object window will respond with a
WM_DDE_TERMINATE of its own, then it posts a PMS_DDE_TERMINATE message to
the kernel client and a WM_QUIT message to itself. The same thing occurs if
PMServer needs to terminate the object window or if the object window
receives a WM_CLOSE.
Upon receiving a WM_QUIT, the object window thread breaks out of the message
processing loop in CreateClient. It calls WinDestroyWindow and
WinDestroyMsgQueue to remove the window and the message queue. It then posts
a PMSV_THREAD_TERMINATE to PMServer, calls WinTerminate to discard the PM
resources used by the thread, and finally calls DosExit to terminate the
thread.
Since receipt of PMSV_THREAD_TERMINATE from an object window thread will
cause PMServer to deallocate the thread's stack, it's essential that it not
do so until the thread executing CreateClient has terminated. Thus, a call
to DosEnterCritSec has been inserted before the point where the
PMSV_THREAD_TERMINATE message is posted. This temporarily freezes all the
other threads in the process and allows CreateClient to complete execution.
Once the CreateClient thread has terminated, the other threads are unfrozen
as if DosEndCritSec had been called. PMServer can then safely process the
termination message from the object window.
Note that the object window is capable of generating WM_DDE_EXECUTE and
WM_DDE_POKE messages, although PMAccess does not take specific advantage of
it. These messages are used for submitting commands and unsolicited data to
the DDE server. The object window can also process two other commands from
the kernel client: one command lets it receive notification of data changes
without an update (a variation of WM_DDE_ADVISE), and another command ceases
the notifications (WM_DDE_UNADVISE).
PMAccess
With the DDE object window code in place in PMServer, an OS/2 kernel
application can use it to receive data from PM applications via DDE. The
sample program that demonstrated PMServer's Clipboard-handling facilities,
PMAccess, will now be expanded to display its new DDE capabilities (see
Figure 3). The original PMAccess is a message-based Vio program that has
five buttons at the bottom of its window. It enables the user to perform
cut, copy, and paste operations on text.
You may recall that PMAccess uses OS/2 queues to let its threads communicate
with each other. Architecturally this means that PMAccess is largely a
reactive program not unlike a Presentation Manager application, rather than
a proactive one as a DOS program usually is. For instance, the keyboard and
mouse threads pass messages to a queue where they are received by PMAccess'
main thread. This thread can then interpret the messages and perform the
appropriate actions (such as moving the cursor, clearing the screen, or
interacting with PMServer to copy or paste data from the clipboard). This
method of message handling also allows PMAccess to react to screen buttons,
so it can respond when a message from the mouse thread indicates that a
button has been pressed.
Thus, when the user presses a screen button (by clicking it with the mouse
or pressing the button's accelerator key), the main thread receives a
message associated with the button. For example, if the user presses the
Copy button, PMAccess will receive the MSG_COPY message and pass the
selected screen text to PMServer as part of a PMS_CLPBRD_COPY message.
PMServer will then put the text in the Clipboard. If PMAccess receives a
PMS_CLPBRD_DATA message, PMServer is indicating that Clipboard data is
available in response to a previously generated PMS_CLPBRD_QUERY-probably
sent by PMAccess' Request thread, which periodically queries PMServer about
the state of the Clipboard.
The most natural way, therefore, to expand PMAccess to take advantage of
PMServer's DDE facilities is by adding support for the additional set of
"PMS_" messages (now defined in PMSERVER.H, Figure 4). To initiate a DDE
conversation, PMAccess will have to send a PMS_DDE_INIT to PMServer,
accompanied by the application and topic names. Upon receiving a
PMS_DDE_INITACK from PMServer (indicating that a DDE server is available),
PMAccess can send PMS_DDE_REQUEST or PMS_DDE_ADVISE messages to request data
or receive updates when the data changes. These messages must be accompanied
by the data item name. PMServer will respond to these with either
PMS_DDE_DATA (if data was received) or PMS_DDE_NODATA. Finally, PMAccess can
send PMS_DDE_TERMINATE if it wishes to end the DDE conversation.
Along with internal support for these messages, PMAccess required additions
to its user interface. Provisions had to be made to allow a user to initiate
a DDE conversation, request data or updates, and terminate the conversation.
Thus four new buttons (with associated messages) were added to the user
interface: Initiate, Request, Advise, and Terminate (see Figure 5). When the
user presses Initiate, for example, a MSG_INIT message is produced, and
PMAccess executes the code to generate a PMS_DDE_INIT to PMServer.
Three entry buttons were added to allow a user to specify the Application,
Topic, and Item Name parameters of a DDE conversation. When the user clicks
one of the entry buttons with the mouse, the cursor is moved to that button
so that he or she enters the appropriate DDE parameter from the keyboard.
(To do this, a change had to be made in the handling of the MSG_B1DOWN
message, which processes left mouse button presses. This handler now resets
the cursor to the position of the mouse pointer when clicked in the text
display/entry area above the buttons. This gives the user a means of getting
out of the entry buttons.)
When a user presses the Initiate button, PMAccess' main thread will read the
characters contained in the Application and Topic name button fields from
their positions on the screen. This is facilitated by a new function,
ButtonRead in BUTTON.C (see Figure 3), which uses VioReadCharStr to read the
screen. For instance, to communicate with Microsoft Excel (after it has been
started), the Application Name button should contain "excel." The Topic Name
button should contain the name of a worksheet, such as the sample worksheet,
"east.xls," that comes with Microsoft Excel (see Figure 6). The thread then
sets up a packet for PMServer containing its own process ID and the
application and topic name retrieved from the entry buttons. The packet is
then sent as part of a PMS_DDE_INIT message via MsgQSend.
Once PMServer processes the PMS_DDE_INIT, starts the new object window, and
the object window receives a WM_DDE_INITIATEACK from a PM application, the
object window posts a PMS_DDE_INITACK back to PMAccess. When PMAccess
receives this message, it will highlight the Initiate button, indicating
that a DDE conversation has been established. The user can then fill in the
item button with the name of the data item to be requested. Citing the
earlier example, the user can request rows 15 and columns 23 by entering
"r1c2:r5c3" in the Item button. The user should then click the Request or
Advise buttons (see Figure 7).
When a user selects Request or Advise, PMAccess will again set up a message
packet to be sent to PMServer. The message, either PMS_DDE_REQUEST or
PMS_DDE_ADVISE, will be accompanied by PMAccess' process ID and the data
item name (again, read from the Item Name button). Once PMAccess receives a
PMS_DDE_DATA message in response, it will display the data received at the
current cursor position in the upper part of the display. This is the same
code used to process a PMS_CLPBRD_DATA message, except that the data item
name is checked against the one most recently read from the Item entry
button.
The user can terminate the DDE conversation by clicking the Terminate
button, which causes PMAccess to send a PMS_DDE_TERMINATE message to
PMServer. The latter will then terminate the DDE conversation with the DDE
server and shut down the associated object window.
An interesting consequence of managing the new buttons was the decision to
have PMAccess' main message-processing thread generate messages to itself,
as is done in Presentation Manager programs. Instead of duplicating the code
necessary to turn the Paste and Initiate buttons on and off if the user
pressed the Clear button, PMAccess generates a new message to itself to
reset the buttons accordingly. You can find this in the processing of the
MSG_CLR and PMS_CLPBRD messages, which generate MSG_RESETPASTEBUTTON or
MSG_RESETINITBUTTON (see PMACCESS.C in Figure 3).
Conclusion
Adding DDE and Clipboard facilities to PMAccess was an interesting
experience, but not as interesting as writing PMServer. With its use of
multiple threads, IPC, and access to PM facilities, PMServer is hopefully a
good model for your own multithreaded PM programs, as well as a means of
letting your Vio applications make use of PM's Clipboard and DDE facilities.
u
Figure 1
Initiate
1. After filling in the Application, Topic, and Item Name fields, the user
clicks the Initiate button on PMAccess' interface.
2. PMAccess' mouse thread reads button event and generates a MSG_INIT to
PMAccess' main thread.
3. Upon receiving MSG_INIT, the main thread sends PMS_DDE_INIT, with
Application and Topic names, through queue to Queue Manager thread of
PMServer.
4. Queue Manager reacts to PMS_DDE_INIT, sets up CLIENT data structure,
allocates stack space for an object window thread, and starts a new thread
with a call to _beginthread.
5. New thread begins execution of CreateClient, opens new (Object) window,
enters message loop with DdeClientProc as its window procedure.
6. Object window gets WM_CREATE from PM and calls WinDdeInitiate to
broadcast WM_DDE_INITATE.
7. Microsoft Excel receives WM_DDE_INITIATE, sees that it can support DDE
conversations on requested Topic (worksheet name), calls WinDdeRespond to
post WM_DDE_INITIATEACK.
8. Object window receives WM_DDE_INITIATEACK from Microsoft Excel, and sends
PMS_DDE_INITACK to PMAccess through the latter's queue.
9. PMAccess receives PMS_DDE_INITACK, highlights Initiate button for user,
and beeps.
Request
10. User clicks Request button.
11. Mouse thread generates MSG_REQUEST to main thread.
12. Main thread sends PMS_DDE_REQUEST, with Item Name (worksheet range of
cells), through queue to Queue Manager.
13. Queue Manager posts PMSV_REQUEST to Object window.
14. Object window posts WM_DDE_REQUEST to Microsoft Excel via WinPostDdeMsg.
15. Microsoft Excel, seeing that the requested range of cells is available,
posts them back to Object window as part of WM_DDE_DATA message.
16. Object window sends data with PMS_DDE_DATA message to PMAccess through
the latter's queue.
17. PMAccess, upon receiving PMS_DDE_DATA, writes the accompanying data to
the display.
Terminate
18. User clicks Terminate button.
19. Mouse thread generates MSG_TERM to main thread.
20. PMAccess' main thread sends PMS_DDE_TERMINATE through queue to Queue
Manager thread.
21. Queue Manager posts PMSV_TERMINATE to Object window.
22. Object window posts WM_DDE_TERMINATE to Microsoft Excel to terminate DDE
conversation.
23. It follows with a PMS_DDE_TERMINATE to PMAccess...
24. ...and a WM_QUIT to itself.
25. Object window thread breaks out of message loop when WM_QUIT is
received, then calls DosEnterCritSec, temporarily freezing the other
threads. Then it posts PMSV_THREAD_TERMINATE to PMServer, and calls
WinTerminate and DosExit to terminate itself, thereby unfreezing the other
threads. PMServer's window procedure deallocates thread stack upon receiving
PMSV_THREAD_TERMINATE.
26. PMAccess receives PMS_DDE_TERMINATE; the user sees it clear the fields
as it readies itself for additional DDE instructions from the user.
Learning Windows Part I: The Message-based Paradigm
Marc Adler
The Microsoft Windows[tm] graphical environment has been getting a lot of
attention lately. Perhaps you're interested in programming for Windows1 but
don't know how or where to start, or you're unconvinced that Windows is the
way to go. MSJ will introduce you to Windows programming with a series of
articles starting in this issue, featuring brand-new Windows Version 3.0.
This article will develop a small but useful Windows application, examining
each component as it is added.
Why is Windows in the spotlight so much these days? In the past, graphical
user interfaces (GUIs) such as Windows were the domain of expensive computer
systems with more CPU power than the average person could afford. GUIs were
certainly out of the question on the original 4.77 MHz PC. The introduction
of the IBM PC/AT in 1984 and the EGA video standard made graphical systems
more feasible, but the screen drawing speed was still sluggish enough to
dissuade people from using GUIs. Today, a 386[tm] running at 16 MHz with a
VGA card, color monitor, and a mouse seems to be standard in much of the
corporate world as well as among many developers. As hardware prices fall,
it seems likely that graphical 386/486[tm] systems will become even more
widespread.
As affordable computers capable of handling this type of system proliferate,
many predict that GUIs will become the standard user interface. The
intuitive, object-oriented, icon-based method will entice more and more
computer-wary people into using the computer as an everyday tool.
Most major computer companies realize this and are developing their products
accordingly. IBM has created a blueprint for all of its future applications
called Systems Application Architecture[tm] (SAA[tm]). The part of SAA
governing how the user interacts with the program is called the Common User
Access (CUA). Adherence to CUA standards ensures that operating methods
remain consistent among a wide variety of products, thus lessening the
amount of time it takes a new user to become proficient with an application.
For example, pressing F1 in a CUA-compliant application invokes
context-sensitive help regardless of the user's location in the application.
The user also knows that help screens will be in a certain format and that
they will lead to an index of commands and a table of command keystrokes.
Consistency among applications reduces the substantial cost of training
employees on new software.
Windows is fully compliant with CUA and will continue to be, which is one
attractive reason to program in it. Another reason is device-independence.
Properly written Windows applications run on a wide variety of hardware,
from monochrome monitors to Super-VGA adapters hooked up to 19" displays.
The device-independence even extends as far as printing. As long as Windows
supports your printer (and it supports a lot of them), you will be able to
print a document with embedded graphics without having to worry about
printer-dependent code. Other reasons to program in Windows include its
ability to multitask applications; its sophisticated memory management, with
multitasking among large applications; and its ability to take advantage of
protected mode automatically on a 386-based system.
Sample Application
The sample Windows application that will be built is a small stock portfolio
analysis program. Not only will it demonstrate each of the elements of a
Windows program, it's something that might be of use to you. You'll learn
how to design and create the application's pull-down, multilevel menu
system. Several dialog boxes with different kinds of control windows will
collect data from the user regarding the stocks as well as the daily trading
information for each stock. You will perform subclassing of existing windows
classes when you implement the dialog boxes and eventually may experiment
with the new custom control facility of Windows 3.0.
The sample application will be able to display several different types of
charts, including a line graph of a stock's price over a period of time and
a bar chart of the stock's daily volume. Since the user will probably want
hardcopy of the graphs, you'll also see how printing is done under Windows.
Not only does the stock application permit exploration of important features
of Windows, it also provides a base for enhancements. For example, the
ability to download stock data at the end of the day from a commercial
online database, such as Dow Jones or CompuServe, would be a useful feature.
This would entail exploring the communications routines in Windows.
If you think of each stock as being a separate document, you can experiment
with the Windows Multiple Document Interface (MDI). Windows now has a
built-in API for creating an MDI-based application. (For more on MDI, see "A
New Multiple Document Interface API Simplifies MDI Application Development"
p. 53-Ed.) Finally, you will set up a Dynamic Data Exchange (DDE)
conversation with a Microsoft Excel spreadsheet.
Windows Programming Paradigm
Let's begin by discussing the Windows programming model. The single most
important adjustment you have to make to go from standard C programming to
Windows programming is the realization that all Windows applications are
based on the concept of message passing. Every Windows program requires a
message loop and a message handler for each window in the application.
Messages are generated by Windows whenever an event occurs or when an action
needs to be taken. When you move the mouse, Windows generates a message that
tells your application that the mouse has moved; embedded in the message are
the coordinates of the point the mouse has moved to. When you choose
something from a menu, Windows informs your application that an item was
selected via a message. Windows will frequently send your application
messages when "interesting" things happen, such as when a window needs to be
redrawn. The essence of programming a Windows application is to fetch a
message, direct that message to the window the message is meant for, act as
quickly as you can to process that message, and go back and read another
message.
Why have a message loop? Why send messages at all? Windows is a multitasking
system, so multiple programs will be running at the same time. Because each
program requires CPU time, no one program should be able to hog the CPU for
a substantial period of time. If a program uses the CPU for a long period,
other time-critical applications running simultaneously may fail to work
properly. Preemptive multitasking systems such as UNIX will interrupt an
application when its allocated time slice expires and pass control of the
CPU to another application, even if the first application is in the middle
of an important operation. Windows multitasking is nonpreemptive. An
application must explicitly yield control to another application-Windows
will not preempt a task on its own. Windows does its multitasking whenever
an application examines the message queue. If there are no pending messages
for an application, Windows will check for messages meant for other running
applications. If another application has a message pending, Windows will
give control to that application. That application in turn will fetch its
message, act on it, then try to read another message. At this point, Windows
resumes the cycle.
You must structure your Windows application to cooperate with this
"pass-the-baton" method of operation. If you don't, your application might
still run correctly, but other applications executing at the same time
probably will not run. In other words, you can't use getc's any more!
Messages
Let's examine a message. In Windows, all messages are in a fixed, compact
format that facilitates rapid message processing. If you look at the huge
WINDOWS.H include file that comes with the Windows Software Development Kit
(SDK), you can find the exact structure of a message (see Figure 1).
The first member of the structure, hwnd, tells which window the message is
targeted for. Each window has a number associated with it, known as a window
handle, that uniquely identifies the window in a Windows session.
The second member, message, is the message identifier. It tells the
application exactly what event has occurred. A list of all of the messages
that Windows knows about can also be found in WINDOWS.H (see Figure 2). All
message names start with WM_ followed by a descriptive name such as
WM_PAINT, WM_CHAR, or WM_COMMAND.
The third and fourth members, wParam and lParam, contain additional
information about the message. Their contents are dependent on the message.
A complete listing of the messages and their parameters can be found in the
SDK Programmer's Reference.
The last two members of the message structure, while always present, are not
used as frequently as the other members. The first of these, "time," is the
time at which the message was placed into the message queue; "pt" contains
the coordinates of the mouse at the time that the message was placed into
the queue.
When a message is generated by an input event, Windows places it in the
system message queue. Windows maintains a single system message queue that
is common to all applications. The system message queue is used to store all
events generated by the keyboard and the mouse (see Figure 3).
Each application also has its own message queue. Windows places
application-specific messages directly into the application queue, entirely
bypassing the system queue.
Writing the Message Loop
A sample Windows message loop, as it appears in many Windows applications,
is shown below.
MSG msg;
o
o
o
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
Windows handles messages in the following sequence: when GetMessage is
called, Windows looks first in the application's message queue. If it finds
no messages, it scans the system queue for keyboard and mouse events. If it
still finds no messages, it checks for timer events. If there are none, the
application is put to sleep.
The GetMessage function suspends the application until a message arrives in
its individual message queue, at which point it removes the message from the
head of the queue and resumes the application. As mentioned previously,
while the application is suspended, Windows gives other applications their
turn with the CPU.
GetMessage gives an application the option to retrieve all messages, a
message meant for a specific window within the application, or a message
that falls within a particular range of messages. Most of the time, you want
to retrieve any message that is meant for your application. I'll cover the
details of GetMessage's parameters later.
GetMessage returns FALSE if the message received is a WM_QUIT message. When
your application receives the WM_QUIT message, it means that your
application wants to exit. A return value of FALSE should break the
application out of the message loop.
The second function in the message loop is TranslateMessage. When the user
presses a key, Windows dissects the keypress into key-up and key-down
events. Windows then translates these two events into a message that
represents a single keypress event. TranslateMessage operates only on
keyboard messages; it translates the up/down events into one of four
messages: WM_CHAR, WM_DEADCHAR, WM_SYSCHAR, or WM_SYSDEADCHAR.
Most applications can ignore the deadkey messages. The system character
messages are generated when the user presses Alt with another key. Alt-key
combinations usually signal Windows to take some sort of action, such as
activating a menu or switching the active window to another application. All
other keys will generate a WM_CHAR message.
The last function used in the message loop of most Windows applications is
DispatchMessage. This function merely sends the message to the window the
message was meant for. The message is actually sent to the message handler
for that window, which is known as the window procedure. (Experienced
programmers will notice that the TranslateAccelerator function is omitted.
This will come up in a later article when the accelerator keys are added to
the application.)
Posting versus Sending Messages
Windows is not the only place where messages can originate. Any application
can source a message,which it can send to itself or to any other application
currently running. Two very different methods are used to pass messages to a
window: posting and sending.
When you post a message to a window, you are placing that message in the
application queue, with the hwnd field of the message structure set to the
target window's handle. If there are other messages already in the
application queue, the window will not receive that message until the
application processes the messages ahead of it. Yes, the window will
eventually receive that message, but it could be a long time.
When you send a message to a window, you are in essence calling its window
procedure directly. The message completely bypasses the application queue.
You send a message to a window when you want that window to act on something
immediately.
The two functions used to pass messages to a window are PostMessage and
SendMessage. Their parameters are listed below.
PostMessage(HWND hWnd, WORD message, WORD wParam,
DWORD lParam);
SendMessage(HWND hWnd, WORD message, WORD wParam,
DWORD lParam);
Note that the functions' parameters are identical to the first four members
of the MSG structure (see Figure 1).You can pass Windows-defined messages or
you can create your own messages to send. If you examine the list of
messages defined in the WINDOWS.H file, you'll notice a symbol called
WM_USER. Windows reserves all values under WM_USER for its own use; all
values above WM_USER can be used by your application. You can define a new
message thus:
#define WM_MYMSG (WM_USER + 0x10)
Now that you know the basics of messaging, the main object in Windows can be
discussed-the window!
Windows and WinProcs
Every window that you create under Microsoft Windows has a message handler
associated with it. This message handler is referred to as a window
procedure or WinProc. When a message is sent to a window using SendMessage,
or when DispatchMessage passes a message to a window, the WinProc of a
window is actually being called. A WinProc must be declared like this:
LONG FAR PASCAL WindowProc(hWnd, msg, wParam, lParam)
HWND hWnd;
WORD msg;
WORD wParam;
LONG lParam;
Again, the four parameters of a window procedure correspond to the first
four fields of the MSG structure.
The Pascal calling convention used in this declaration is a relatively
recent Microsoft innovation that makes the size of the compiled code
slightly smaller. Two things distinguish a function using this calling
convention from a function using the C calling convention: first, the
arguments are placed on the stack from left to right, as opposed to right to
left. Second, all stack maintenance is done by the function being called,
not by the caller of the function as in the C calling convention (see Figure
4). If a function is called many times with the C calling convention, the
same stack maintenance code must appear in the caller's code every time this
function is called. This can add up to quite a bit of overhead. If the stack
maintenance is done once and only once in the callee's code, the compiler no
longer has to generate the stack maintenance code for each invocation of the
function, saving code space and execution time. Microsoft estimates that
using the Pascal calling convention reduced the code size of Windows by
about 7 percent.
The WinProc receives every message sent to a window. You can choose to
handle a message by writing some code to react to that message, or you can
pass it on to the default window procedure.
The default window procedure, DefWindowProc, can handle all of the messages
that can possibly be sent to a window. This WinProc has default processing
for every Windows message. The Windows SDK even contains a file called
DEFWND.C that lists the source code for DefWindowProc. (Note: DefWindowProc
does not handle the Windows-defined control classes, each of which has its
own internal WinProc.) If your WinProc does not handle a message, it must
pass the message on to DefWindowProc.
A skeleton of a typical WinProc is shown in Figure 5. As you can see, this
WinProc handles the WM_PAINT and the WM_DESTROY messages that are sent to
the window and passes all other messages on to the default window procedure
for default processing. If you forget to pass these messages on, your window
would certainly not behave correctly.
Window Classes
Every window you create in a Windows program must belong to a window class.
Before you create a window, you must make sure that its class is registered.
Informally, a window class defines a window's behavior and its appearance.
Windows belonging to the scroll bar class look and behave in a certain way,
while windows of the button class behave entirely differently.
Formally, a window class is defined by the contents of a WNDCLASS data
structure (see Figure 6). The class structure contains, among other
elements, the name of the class, the name of the menu that will be attached
to each window of that class, the number of extra bytes that will be
allocated to each window of that class, the style of each window, and most
importantly, the WinProc for each window of that class. By processing
certain messages sent to the WinProc of a given class, you can define the
behavior of each window created of that class.
When you register a class, you fill a WNDCLASS structure with the desired
information and call the RegisterClass function. Once an application
registers a window class, the class is available to any copy of the same
application that is running. In previous versions of Windows, when you
registered a window class, it was available to any application that ran
during that Windows session.
Initialization Code
The message-passing loop, window classes, and window procedures have been
discussed. Now it's time to begin writing the initialization code for the
application.
Every Windows program must have a function called WinMain that serves as the
entry point to the application. This is analogous to the main routine of a
non-Windows program. However, the arguments to WinMain are very different.
WinMain looks like this :
int PASCAL WinMain(hInstance, hPrevInstance,
lpCmdLine, nCmdShow)
HANDLE hInstance;
HANDLE hPrevInstance;
LPSTR lpCmdLine;
int nCmdShow;
The instance identifier is hInstance. Under Windows, you can have multiple
copies (called instances) of an application running at the same time. This
does not mean, however, that you have multiple copies of the executable file
in memory. Because Windows does not allow you to write into a code segment,
the same code image remains unchanged for all copies of a single
application. If there are multiple copies of an application running, Windows
will keep only one copy of the code segment in memory. However, Windows will
give each copy its own data and stack segments. The instance handle serves
as a unique program identifier, so you can identify which copy of an
application is running by examining the instance handle.
hPrevInstance is the instance handle of the previously invoked copy of an
application. The first time you run an application, hPrevInstance will be
NULL. You only need the handle of the previous instance to see if you need
to do application-specific initialization. You need to register your
application's window classes only once, no matter how many instances of the
application are invoked. (Remember that when you register a window class, it
is available to all instances of that application, so it's redundant to
register that class again.)
lpCmdLine is a null-terminated string representing the command line with
which the user invoked the application. In non-Windows C programs, the
command line is parsed by the C start-up code; a pointer to each token is
placed in the argv array for you. Windows does not pass the argv array to
the WinMain, but you can use the following variables if you are programming
in C:
extern int _argc; /* argument count */
extern char **_argv; /* array of arguments */
extern char **_envp; /* environment strings */
The final parameter, nCmdShow, specifies how the application will be
initially displayed; that is, as a running, visible window or as an icon. It
should be used only once in your WinMain function, as the second argument to
the ShowWindow function. But more on this later.
At this point, you can write code to initialize the application (see Figure
7). Every window that is created using class MainWindowClass will use
MainWindowProc as its window procedure. I have also specified the icon to
use to represent the application when it is in the minimized state, the kind
of mouse pointer or cursor desired, the brush that Windows will use to paint
the window's background, the name of the menu that will be attached to the
window, and the number of bytes in the per-window and per-class data
structures.
A brush is simply a bitmap of a pattern that Windows uses to paint with.
Windows has several predefined stock brushes; I will use a solid white
"pattern" to paint the background of the main window.
Now that you have registered the main window class, create the main window.
You must use the CreateWindow function to create any kind of window (see
Figure 8).
Among the arguments CreateWindow expects is the name of the class the window
belongs to, the string that will be used as the window's title, the style of
the window, the location of the upper-left corner of the window, the
dimensions of the window, and a handle to the parent of the window, if any.
I use a strange value for the upper-left corner coordinates, the width, and
the height of the window. By specifying CW_USEDEFAULT, the Windows desktop
manager decides where to place the main window and how big it should be.
This is another example of device-independence in Windows; you don't have to
specify the actual physical coordinates of the corners of the window.
The main window has been created, and will now be displayed. When you create
a window using CreateWindow, the window will not be initially displayed. You
must call ShowWindow to make the window appear.
ShowWindow(hWndMain, nCmdShow);
UpdateWindow(hWndMain);
For the very first invocation of ShowWindow (that is, the call to display
your main window), you must give nCmdShow as the second parameter. The
UpdateWindow function sends a WM_PAINT message to your main window, telling
it that it must redraw the contents of its client area. In the sample
WinProc (see Figure 9), you ignore the WM_PAINT message, letting the default
WinProc handle the drawing.
The last thing to do in the main routine is enter the famous message loop.
This loop will fetch messages from the application's message queue and send
the messages to the WinProc for the main window. The loop will be exited
when a WM_QUIT message is received, which is usually the result of the user
closing the main window.
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
/* the return code from PostQuitMessage... */
}
Finally, a simple WinProc is added to the code (Figure 9). All this WinProc
does is post a WM_QUIT message when it detects that the main window is being
destroyed. This is done by calling the PostQuitMessage function. All other
messages are passed on to the default window procedure. This code merely
displays the main window on the screen and waits for the user to close the
window.
Summary
So far I've introduced the message-passing paradigm that your applications
must follow, and discussed window classes and window procedures. I began the
application by showing the initialization code, which entailed registering
the window classes, creating the initial window, and entering the standard
Windows message loop. In the next installment, I will begin to fill in some
of the information that goes into the resource file, such as the definition
of the icon, and show you how to compile and link a Windows program. I will
also discuss menus and set up the menu system for our application.
────────────────────────────────────────────────────────────────────────────
Volume 5 - Number 5
────────────────────────────────────────────────────────────────────────────
Creating a Multipage Data-Entry and Reporting Library for Windows[tm]
Version 3.0
Kevin Welch
Many traditional character-based applications that are ported to the
Microsoft Windows[tm] graphical environment require validated data entry and
reporting more complex than what Windows provides. These data-handling
tasks, which are commonplace in corporate computing, are time consuming and
tedious to implement in Windows1, typically requiring a great deal of
programming.
This article, the first of two parts, addresses the problem by presenting a
tool that can be easily adapted to a variety of data-entry and reporting
situations. It shows how to create a dynamic-link library, FORM.DLL, that
supports multipage data entry and reporting using a series of linked dialog
box and report templates specified in a resource file. Using this DLL, you
can create complex multipage data-entry forms and reports without a single
line of C programming.
Design
The initial design philosophy behind the library is to encapsulate a single,
multipage data-entry form and report into one DLL. Then, any application
desiring to use a specific form or report only needs to load a particular
library and call the appropriate editing or reporting function. An
additional advantage of this architecture is that applications could
dynamically link to specific forms and reports based on user input.
The second design philosophy is to separate the supporting code from form
and report definitions. This process is absolutely essential if new forms
and reports are to be created quickly and easily. As a result of this design
goal, all data-entry form and report specifications need to be extracted by
the program from a data file associated with the library. For ease of
implementation, this could best be accomplished by storing the form and
report specifications in the resource file that is associated with the DLL.
The third design philosophy is to make using the library as simple as
possible for the host application. Based on this goal, the library needs to
contain only two entry points-one for data entry and one for generating
formatted reports.
Now that you know the design philosophies behind the library, you can focus
on the two entry points. The first entry point in the library, FormEdit,
provides formatted data entry; it is defined as follows:
HANDLE FormEdit( hWndParent, hData );
The parameters and return values of this function are defined below.
hWndParent A handle to the parent window of the data-entry dialog box. This
dialog box would be automatically centered inside the parent window using
this handle.
hData A handle to a global block of memory, which contains formatted text
that defines the data to be associated with each entry field. Each entry
should be terminated with a single NULL character, and the last entry by an
extra NULL character. The data block should be formatted in the following
manner:
FIELDNAME:DATAVALUE <NULL>
o
o
o
FIELDNAME:DATAVALUE <NULL>
<NULL>
In cases where a new form is being entered, this data block should contain
default values for all required fields. If no fields are required, then the
handle provided should reference a data block consisting of a single NULL
character. Not all fields need to be specified; the system will
automatically assume that the unspecified fields are not defined.
The actual association of data values to specific data-entry fields in each
form is defined by cross referencing the field name to a specific control
ID. This cross referencing is defined by a table defined in the resource
file associated with the library. There is more on this later in the
article.
<return value> The value returned by this function should be a handle to the
updated data block. If a NULL handle is returned, then it can be assumed
that the user canceled data entry.
Calling FormEdit causes a dialog box (see Figure 1) to be displayed. This
dialog box contains a data-entry area (inside which the actual data-entry
form is displayed), a field comment area, push buttons that manage the
data-entry process, and a page-selection area to use with multipage forms.
When this dialog box is initially displayed, the caption automatically
indicates the data-entry form in use and the currently selected page. The
data-entry area contains various fields and data values associated with the
first page of the multipage form. When developing each form, the
specification and arrangement of the various controls used to display data
inside each page is performed using the dialog editor provided with the
Windows Software Development Kit. Each dialog box you define should be of
identical dimensions. It can contain any combination of Windows controls you
desire. When you have specified each dialog box page (remembering that what
you define will fit in the data entry area), you can integrate the template
into the resource file associated with the library.
To assist the user in selecting a particular data-entry page, the currently
selected page number is displayed in the lower-right corner of the main
dialog box. Rather than developing a new control for this purpose, the
installable Page control described in "Extending the Windows[tm] 3.0
Interface With Installable Custom Controls," MSJ (Vol. 5 No. 4) will be
used. With this control, the currently selected data-entry page is indicated
by a reverse-video page icon. If desired, the user can select a different
data-entry page by clicking the mouse inside another page icon or by using
the keyboard cursor keys to move the selector to a different page.
When the user enters data inside the form, the field comment area in the
lower-left corner of the main dialog box contains a short description of the
currently active data field. Each time the user presses the Tab key, the
input focus moves from field to field and a new comment is displayed in the
field comment area. If the user presses Tab from the last field in the
form, the next page is automatically displayed. Similarly, pressing
Shift-Tab in the first field of the form displays the previous page.
Each control in the form is subclassed to facilitate the use of the Page
Down and Page Up keys for moving to the next and previous data entry forms.
This assists the user in rapid data entry. Now that the data-entry
functionality has been defined, the reporting portion of the library can be
specified.
The second function in the library will create a formatted report using the
data provided:
HANDLE FormPrint( hWndParent, hData );
The parameters and return values of this function are almost identical to
that of the FormEdit function. The only difference is that if a NULL handle
is returned, you can assume that the user canceled the report-generation
process.
Calling this function generates a formatted report on the default output
device using the data provided. The report will be formatted according to
the report specifications defined in the resource file associated with the
library. While the report is being printed, a dialog box is displayed.
Besides indicating that a report is being generated, this dialog box allows
the user to cancel the report.
Now that the entry points in the library have been defined, you can examine
the mechanisms used to specify each form and report. As already mentioned,
to facilitate the creation of new multipage data-entry forms and reports,
the dialog box and report specifications are defined in the resource file
associated with the library. Because of this, a new form or report can be
created merely by changing the RC file and recompiling it with the library,
using a different name for the new DLL.
The specification of the resource file associated with the library assumes
the creation of an include file (FORM.H) that defines the identifiers used
throughout the RC file. These identifiers are specified so that each field
name in the form or report is assigned a unique number starting at 0100H,
regardless of which data-entry page they appear in. The identifiers in the
range 0000H to 00FFH are reserved by the library for internal use.
An example of the RC file required by FORM.DLL is shown in Figure 2. The
library used this file to produce the three page data-entry form in Figure
1. (Assume all the IDn_FIELD values are defined in an associated FORM.H.)
The FORM RCDATA portion of the RC file is used to specify the number of
pages in the data-entry form and the main title of the form. For example, if
a value of 3 is specified for the number of pages, the library assumes that
three dialog boxes of identical size will be defined using the names Page1,
Page2, and Page3.
The REPORT RCDATA portion of the RC file is used to specify the report to be
associated with the entire data-entry form. The specified data consists of a
series of NULL-terminated text lines. Embedded inside each line can be
C-like formatting statements in the following syntax:
%FMTSPEC = % [alignflag] [ [width] [.precision]]FIELDNAME
alignflag text alignment flags (in this implementation, limited to left
and right justification)
width minimum number of characters to output
precision maximum number of characters to output
(Note: in this implementation, you are limited to string data).
Using these embedded formatting statements, the library will create the
report using the field names specified when defining specific data values.
Additionally, linefeed and carriage return characters will be automatically
appended, and page breaks will be inserted as required.
To provide additional flexibility when creating reports, several predefined
field names will be provided. In this implementation, it is useful to have
the following field names:
LINENUMBER = current line number
PAGENUMBER = current page number
Finally, to provide greater reporting flexibility, it is useful to be able
to specify page breaks and simple headers or footers. This will be
accomplished using meta commands located in the first column:
%FORMFEED
%HEADER [header text including formatted data fields]
%FOOTER [footer text including formatted data fields]
The STRINGTABLE portion of the RC file is used to specify the relationship
between each field name, the internal identifier, and the associated field
comment text. Whenever a particular data entry form is displayed, the string
associated with each internal dialog box identifier is retrieved and the
corresponding field name is defined. The data block that is provided during
the FormEdit call is then searched for a matching entry. If one is found,
the associated data value is then displayed inside the related control (see
Figure 3).
Additionally, when the user enters data into a form, a comment is always
displayed at the bottom of the main dialog box that describes the field that
has the input focus. This comment, defined in the string table, can be used
to indicate whether the field is required, to indicate what data format is
expected, or to elaborate on the exact meaning of the information being
entered.
The remainder of the RC file contains dialog box definitions relating to
each page of the multipage data entry form. Each definition is named Page1,
Page2, and so on, up to the number of pages specified in the FORM RCDATA
portion of the RC file. The caption associated with each dialog box is
appended to the title of the main dialog box. This lets you define a title
for the entire form and for each data-entry page.
When creating each data-entry dialog box page with the dialog editor, the
only limitations are that each page have the same dimensions and that each
control use an identifier that is defined in the FORM.H include file and
referenced in the string table portion of the RC file.
Form Library
Now that the structure and contents of the form library have been defined,
you can investigate the implementation. In this article you will implement
the basic library with all associated entry points, but the actual
mechanics will be left for the next installment.
Like most other DLLs, the form library is constructed from the following
kinds of files (see Figure 4):
■ FORM
make file
■ FORM.DEF
module definition file
■ FORM.H
include file
■ FORM.RC
resource file
■ FORM0.ASM
entry point module
■ FORM1.C
initialization module
■ FORM2.C
form edit module
■ FORM3.C
form print module
The make file, FORM, is almost identical to that used by any Windows
application except that it uses the -Aw and -D_WINDLL compile flags. These
flags instruct the C compiler to generate code in a DLL format with SS !=
DS.
FORM.DEF is the module definition file. This file defines the module as a
library, imports the page selection control initialization function, and
exports the various form library entry points. Because the form library uses
the page selection control, you need to have the page control DLL with its
associated import library present.
The FORM.H file used for the first version of FORM.DLL does not contain
field identifier definitions, but it does have prototypes for the two
library entry points, FormEdit and FormPrint.
In this implementation, FORM.RC serves as a placeholder. In the next article
the contents of this file will be expanded to contain the form, report,
string table, and dialog box definitions.
FORM0.ASM is the assembly language entry point to the DLL. This module calls
the FormInit function contained in FORM1.C, which initializes the library.
The FORM2.C module contains the FormEdit function. For now, this function
displays a message box that indicates the form is being edited, and returns
a handle to the formatted data block provided.
The last module, FORM3.C, contains the FormPrint function. Almost identical
to the FormEdit function, it also displays a message box to indicate a
report is being generated.
Form Viewer
In order for this form library to be tested, you need to construct an
application that lets you load an arbitrary library and test it with
manually entered data.
The form viewer is such an application (see Figure 5). With it you can
manually load a form library, enter a block of formatted data, and send the
formatted data to either the FormEdit or FormPrint functions. Figure 6 shows
the relationship between the Viewer and a library being tested.
The form viewer is constructed from the following files (see Figure 7):
■ VIEWER
make file
■ VIEWER.DEF
module definition file
■ VIEWER.H
include file
■ VIEWER.RC
resource file
■ VIEWER.C
initialization module
As with most other include files, VIEWER.H contains the definitions
associated with the application. Of these definitions, note the
specification of two property list values used by the Viewer window to store
a child window handle and the handle to the active form library:
#define VIEWER_HWNDEDIT MAKEINTRESOURCE(100)
#define VIEWER_HFORMLIB MAKEINTRESOURCE(101)
Also note the definition of the ordinal numbers used to access the various
form library entry points:
#define FORM_INIT MAKEINTRESOURCE(2)
#define FORM_EDIT MAKEINTRESOURCE(3)
#define FORM_PRINT MAKEINTRESOURCE(4)
Finally, note the form FormEdit and FormPrint function prototypes. Since
these two functions are indirectly referenced by the Viewer application, the
two prototypes are defined using function pointers:
typedef HANDLE (FAR PASCAL * LPEDITFN) ( HWND, HANDLE );
typedef HANDLE (FAR PASCAL * LPPRINTFN) ( HWND, HANDLE );
VIEWER.C contains the code associated with the application. Of this, three
functions need mentioning. The WinMain function is responsible for the
definition and registration of the Viewer window class. Note that the
background brush is NULL. Because the ViewerWndFn creates a child edit
window inside its client area, the window background does not need to be
updated.
Once the Viewer window class has been registered, the WinMain function will
create the Viewer window and retrieve/dispatch all associated messages. This
continues until the Viewer window is destroyed.
The ViewerWndFn function is responsible for a number of tasks. When the
window is initially created, it defines the window property list and creates
a child edit window inside its client area. The resulting child window
handle is saved using one of the property list values and is used when the
window is resized or a formatted data block is retrieved.
When the user selects the Form Open... menu option, ViewerWndFn calls
OpenDlgFn. This brings up a dialog box in which the user can specify which
form library to use.
When the user selects the Form Edit... or Form Print... menu option, the
ViewerWndFn is responsible for dynamically calling the appropriate form
library function. The process of dynamically calling a library function is
accomplished as follows:
hForm = GetProp(hWnd,
VIEWER_HFORMLIB);
if ( hForm )
{
lpEditFn =
GetProcAddress(hForm,
FORM_EDIT);
(*lpEditFn)(hWnd, hData);
}
Finally, when the viewer window is destroyed, the ViewerWndFn is responsible
for releasing the current form library (if present) and removing all
associated window properties.
The OpenDlgFn function centers the dialog box when displayed and loads the
library specified by the user. If the load library operation is successful,
the previous library (if present) is released and the viewer window caption
is updated.
Once you have created all the Viewer files and compiled them, you can start
using the Viewer with the Form library described in the previous section.
Although you won't be able to start editing data or generating reports in
this installment, you will at least be able to load the library. With the
code that accompanies the next installment, the Viewer will be fully
operable.
Conclusion
Now that the basic framework has been defined for the Form Library and the
Viewer application is in place, you can start implementing the required
internals. The next installment will focus on these internals, creating a
complete library that you can easily adapt to your needs. u
Spawn Programs from Within Your DOS Application Using Almost No Additional
Memory
Marc Adler
Many PC applications have a command that permits you to escape to the
operating system without having to terminate the application. While working
on a document with a word processor, you might suddenly realize that one of
the files you need to import into your document is archived. This means you
have to go to the operating system, dearchive the file, and return to the
point where you left off in the word processor. For another example, many
program editors allow you to compile the file that you are editing. To do
this, the editor must load the compiler into memory, run the compiler, and
return you to the point in your file where you were editing.
The Microsoft C run-time library provides functions that allow you to
suspend your program temporarily, load another program into the memory area
that the current application does not occupy, and run the new program. To
run a new program, simply make the following call:
system("myapp");
The system call will load a copy of COMMAND.COM above your application. The
system will pass its string argument to COMMAND.COM to be used as the
command line. COMMAND.COM will then attempt to execute this command.
However, if myapp is a large program, then COMMAND.COM will probably be
unable to execute it. This is usually because there is not enough contiguous
unallocated memory left in your system to load the new program. Resident in
low memory are the ROM BIOS, DOS device drivers and buffers, and the
resident portion of COMMAND.COM, which can be from 20Kb to 60Kb in size
depending on which version of DOS you are running. Any
Terminate-and-Stay-Resident (TSR) programs will also take up memory. Last is
the code and data space taken up by the current application. Most
applications allocate and free chunks of memory while running, causing a
problem known as fragmentation. (For more on memory management and
fragmentation, see "Creating a Virtual Memory Manager to Handle More Data in
Your Applications," MSJ Vol. 4 No. 3.) Memory is fragmented when free blocks
of memory are scattered among allocated blocks. Unfortunately, DOS1 does not
perform garbage collection and compaction on its own, and neither do most
applications. The result is that DOS is sometimes unable to load a program
not because of a lack of free memory, but because of a lack of contiguous
free memory.
One solution is to take a complete snapshot of the application's memory
image, store it somewhere, free all of the memory taken up by the
application, and run the new program. When the new program is finished, the
memory snapshot is loaded back into RAM. Then the old application should be
exactly as it was before it was swapped out-byte for byte.
This approach has been implemented in software developed by Thomas Wagner of
Ferrari Electronic GmbH in Berlin, West Germany. (The code can be downloaded
from any MSJ bulletin board.) One of the nice features of this code is that
it permits you to swap your program to either a hard disk or expanded
memory. If you have plenty of expanded memory free, the swapping is very
fast, and the user perceives almost no delay. When a program is swapped out,
it leaves only about 1Kb of code in memory.
Swap API
Applications must make a single call to swap a program out and load another
one in. The function is defined as follows:
int pascal do_exec(char *pszCommand,
char *pszArgs,
int iMode,
unsigned uMemNeeded,
char **pEnviron)
The name of the file to execute is pszCommand. For instance, using the
string "cl" would invoke the C compiler. If an absolute pathname is not
given, this function will search for the program in the current directory,
and then search the directories specified in the DOS PATH environment
variable. Do_exec will try to locate a file with a COM extension first and
then a file with the EXE extension. The function will not attempt to execute
batch files.
If an empty string (that is, a string with \0 in the first byte) is passed
as the first argument to do_exec, a copy of COMMAND.COM (or whatever command
interpreter is specified in the COMSPEC environment variable) will be
executed.
The second argument to do_exec, pszArgs, is a string containing the
arguments to the program. If you're invoking the C compiler, pszArgs might
be a string like this:
/c /AL foo.c
The third argument, iMode, tells do_exec how to swap out the old
application. If iMode is 0, you will be returned to the DOS level instead of
the old application when the new application is terminated. If iMode is 1,
EMS will not be used for swapping. If iMode is 1 (which it should be in most
cases), the function will return to the old application after the spawned
program terminates.
The fourth argument, uMemNeeded, specifies the maximum amount of free memory
required by the new application. If this amount is greater than the amount
of contiguous free memory in the system at the time of the swapping, the old
application will be swapped out to disk or to EMS. However, if there is
enough free memory, the old application will not be swapped. This means
swapping will never take place for a uMemNeeded of 0, while a value of FFFFH
for uMemNeeded will always cause swapping to take place, regardless of the
amount of free memory available.
The last argument, pEnviron, specifies the environment to pass to the new
program. If this argument is NULL, a copy of the parent application's
environment will be passed to the new program. Otherwise, pEnviron must
point to an array of pointers to environment strings, with the final member
being NULL.
The sample call to do_exec, shown below, swaps out the current application
and spawns a copy of the C compiler:
int rc;
rc = do_exec("cl", "/c /AL foo.c", 1, 0xFFFF, NULL);
Now let's examine the C function do_exec (see Figure 1) and the lower level
assembly language routine do_spawn (see Figure 2). For the most part, these
routines prepare data structures for the DOS EXEC function, which is
interrupt 21H, service 4BH. (For more on this function, see "Everything You
Always Wanted to Know about the MS-DOS EXEC Function. . . ," MSJ, Vol. 4 No.
1.)
The first thing that do_exec does is copy the argument string into a
separate buffer and prefix the buffer with a 1-byte length indicator. The
DOS EXEC service expects to see the parameter list in this form.
Unfortunately, DOS puts a 128-byte restriction on the length of the
parameter string. In addition, a length specifier is placed before the name
of the command to execute.
If the calling program specifies a custom environment to pass to the new
program, you must allocate a buffer large enough to hold all the environment
strings. Each NULL-terminated environment string is copied in sequence into
the buffer; the final environment string is terminated with two NULL bytes.
The amount of free memory available can be determined by calling the DOS
memory allocation function interrupt 21H, service 48H. If you pass the value
FFFFH as the number of memory paragraphs to allocate, this function will
always fail (since FFFFH x 16 bytes = 1Mb of conventional memory). However,
as a result of this, the function instead returns the amount of memory
available in the system. If this amount is less than what the new program
needs, the old program will need to be swapped out.
While swapping the old program to disk, a temporary file with the unlikely
name of $$AAAAAA.AAA will be created. The DOS environment is searched for a
variable named TMP or TEMP; if found, the swap file is created there. These
environment variables usually specify a directory that applications can use
to store temporary information in (such as virtual memory swap files or
spooler output) while they are running. If these environment variables are
not found, the swap file is created in the current directory.
Next the assembly language routine do_spawn is called to perform the actual
swapping. The do_spawn routine is called with the iMode argument, the name
of the swap file, the name of the command (with the length-specifier byte),
the parameter string (also with the length-specifier), a copy of the
environment, and the length of the environment.
Here is an overview of what do_spawn does. First, do_spawn traverses the
memory control blocks allocated to the current program, adding up the size
of all the blocks in the process. If swapping to expanded memory, the
routine determines how many 1Kb blocks of expanded memory to allocate and
allocates the expanded memory pages. If instead you're swapping to disk,
do_spawn creates the temporary disk file. Next, the routine traverses the
memory control block chain, writing the contents of the memory allocated to
the control block to disk or to expanded memory. The routine then moves its
reload code to low memory and executes the new program. When control returns
from the executed program, do_spawn reads the memory image back in from disk
or from expanded memory and resumes execution of the original application.
Memory Control Blocks
To understand how do_spawn works, you must be familiar with memory control
blocks. When a program asks DOS for some memory, DOS allocates the memory,
then returns the address of the free block to the program. DOS places a
16-byte data structure, called a memory control block (MCB), before the
allocated memory area. The MCB is used to record information about the
allocated block. The structure of an MCB is shown in Figure 3.
The first byte in the MCB indicates whether the block is the last block in
the MCB chain: if it is not the last block in the chain, it's the letter M;
if it is the last block, it's the letter Z.
The next two bytes contain the program identifier of the program requesting
the memory allocation. Every program DOS loads is given a 256-byte header
called a Program Segment Prefix (PSP). The PSP contains information such as
the length of the program, a copy of the program's command string, and so on
(see Figure 4). The PSP is always located on a paragraph boundary. Since
there is a unique PSP for each program that is running, it's convenient to
use the segment where the PSP is located to identify a program.
The next two bytes of the MCB contain the number of 16-byte paragraphs
allocated to this block. The MCB's remaining 11 bytes are not used by DOS.
The do_spawn routine takes advantage of this, using the last 8 bytes of the
MCB to store some of its own information. It is always dangerous to rely on
the undocumented features and data structures of DOS, but do_spawn has been
used successfully under DOS 2.1 through DOS 4.01.
As mentioned, do_spawn will move its reload code down to low memory,
starting at the 92nd byte of the swapped program's PSP. Do_spawn can do this
because the information that had been at PSP+92 is not needed while the
spawned program is running. Therefore, do_spawn saves it in a special area.
Then the reload code and data from the swapped program can be stored in
PSP+92 while the other program is running.
Do_spawn traverses the program's MCB chain, gathering information about each
memory block. As mentioned above, the last eight bytes of each MCB are used
to store some of do_spawn's information. The revised structure of the MCB is
shown in Figure 5.
The do_spawn routine starts at the first MCB attached to the program,
located 16 bytes before the PSP. It then marches down the MCB chain, and for
every MCB that has been allocated to this program, it records information
about the MCB in the last eight bytes. It also determines the number of 1Kb
blocks that must be allocated from expanded memory to store the old program
in.
You might ask why do_spawn needs to create its own chaining information,
given that DOS maintains the regular MCB chain. Assume that a TSR program is
active in your system while this program is running. Since a TSR can ask DOS
for a block of memory at any given moment, another program's memory blocks
might be found in this chain. DOS could easily allocate a block of memory
from your program's memory space and give it to the TSR. To ensure that your
program swaps out memory that belongs to it and not to another program,
do_spawn must create a separate chain of MCBs instead of using the full DOS
chain of MCBs.
Once the MCBs are scanned, the swapping is performed (presuming that
swapping was specified in the call to do_spawn). If do_spawn wants to use
expanded memory to hold the swapped memory, it must see if an expanded
memory manager is installed and ensure that there are enough expanded memory
pages to hold the program's memory image. Otherwise, it has to create a
temporary file on disk and swap the program image to that file.
EXEC Function
Now that the entire memory image of the program has been swapped to either
expanded memory or to disk, you must prepare a data structure called a
parameter block for the DOS EXEC function. This parameter block (see Figure
6) is passed as an argument to the EXEC function.
If you specified that the spawned program should use the first environment,
segEnvironment will be set to 0. If not, do_spawn puts the segment of the
buffer where the user-defined environment string is stored into
segEnvironment and adds the size of the environment string to the number of
bytes that will stay resident when the second program is spawned.
The do_spawn routine moves the argument string to a storage area in low
memory and assigns the address to pszArguments. It also relocates the
command string there. Then do_spawn fills the two dummy File Control Block
(FCB) structures in the parameter block with zeros, parses the argument
string into both FCBs, and assigns their addresses to pFCB1 and pFCB2.
Finally, We Swap
The memory has been swapped and the EXEC parameter block has been prepared.
All that is left to do before the EXEC is to reduce the program to the
smallest memory image possible. Things that must be preserved in memory
include a small data area for do_spawn that contains information about the
program that will be swapped, such as the EMS memory handle or the name of
the temporary file, the saved code from PSP+92, and so on. The code that
performs the actual EXEC must also be saved as well as the code that reloads
the memory image. If the latter two items are not preserved, when the
spawned program terminates, control will be returned possibly to garbage
code. This information is stored starting at PSP+133; it takes up about 1Kb.
(You can play with the PSP like this because the first thing that do_spawn
did was save the contents of PSP+92 in a storage area.)
All systems are go for DOS to EXEC the new program. Do_spawn merely passes
the EXEC function the name of the command to execute and a pointer to the
EXEC parameter block, and off we go. We can only hope that some TSR has not
fragmented memory too much and that the new program has enough contiguous
memory to load.
When the spawned program terminates, it returns control to do_spawn's
reloading code, which was previously placed in low memory. The reloading
code determines whether the original program was swapped to disk or to
expanded memory, and then opens the appropriate device. It reloads the
memory image, retrieves the return code from the EXEC function, deletes the
swap file, and returns control to the point in do_exec where do_spawn was
called.
The possible return codes from do_spawn and do_exec are listed in Figure 7.
If your application gets an error code from do_exec, you might want to call
do_exec again, this time asking do_exec to load a copy of COMMAND.COM and
letting COMMAND.COM try to execute the command. You would do this if you
were trying to execute a BAT file, or if your command specified some input
or output redirection or piping. COMMAND.COM must be used to interpret and
execute the commands in a batch file or to handle redirection, which the DOS
EXEC function does not recognize. Sample code to do this is shown in Figure
8.
Enhancements
The code presented here permits applications to load and run specialized
programs, making the overall application environment more powerful. These
routines could be the kernel of a more sophisticated task-switching system
for specialized applications. You'd only have to add data structures that
will keep track of program information associated with each swapped program
and a user-interface mechanism to allow the user to switch between the
various tasks easily.
Based Pointers: Combining Far Pointer Addressability and the Small Size of
Near Pointers
Richard Hale Shaw
Based pointers, a new data type introduced in Microsoft C Version 6.0, are a
practical alternative to traditional pointers for some applications. Based
pointers combine the size of near pointers with the ability of far pointers
to address objects in segments outside your program's default data segment;
that is, far segments. Their name comes from the way they are used-you
specify the segment on which they are based. When you use a based pointer,
the compiler usually generates code to load the ES register with a specific
base (the segment upon which the pointer is based). You probably won't use
based pointers to address objects in your program's default data segment,
but they are useful when working on objects in parallel. They offer some
distinct advantages over far pointers, including a flexibility that far
pointers lack. A brief review of far, near, and huge pointers follows.
Near Pointers
Near pointers are native to memory models that use a single 64Kb segment for
code or data. For data pointers, this includes the tiny, small, and medium
memory models, in which data segments don't exceed 64Kb. Near code pointers
(function pointers) are found in models that limit code to 64Kb, like the
tiny, small, and compact models. For the purposes of this article, however,
further discussion focuses on pointers to data.
Near pointers are simple 16-bit pointers that contain an offset to an object
in a program's data segment. The compiler combines the program's data
segment with the offset to create a 32-bit address from the near pointer.
Since the DS register contains this segment value most of the time, there is
usually no need to reload this register. Besides, a number of assembly
language operations assume that DS contains a data object's segment address.
While near pointers are the smallest and fastest pointers to use, they are
also the most limited. You can access only 64Kb of data with them, the 64Kb
in the program's data segment. If you compile a small model program and
attempt to increment a near pointer past the 65,536th byte, the pointer will
be reset to 0. For example, the following code leaves the pointer set to the
base of the data segment, byte 0.
// set pointer to last byte
char _near *p = (char _near *)0xffff;
p++; // increment pointer
(Note that the _near keyword is unnecessary if you are compiling with the
small or tiny model, just as _far can be omitted when compiling with the
large model.)
Since near pointers don't usually require segment register reloads and use
only 2 bytes, they have an advantage of size and speed over far pointers.
However, their ability to address only the current data segment's 64Kb
limits their usefulness in large or complex applications.
Far Pointers
A far pointer is a variation of a near pointer. Rather than let the compiler
use the program's data segment as the segment portion of the address (as
near pointers do), the segment value is stored in the pointer. Thus, far
pointers use 4 bytes to store a complete 32-bit address. Since this address
contains both the segment and offset of the object being addressed, far
pointers can be used by any memory model. However, far data pointers are
native to the compact and large models.
Because far pointers contain both a segment and an offset, the segment
portion of a far pointer must be reloaded when it is used. Thus, they are
slower than near pointers. On the other hand, far pointers can reference any
data object, anywhere in memory.
Far pointers cannot address more than 64Kb at a time, or access all of a
data object more than 64Kb in size. Far pointers behave like near pointers
as far as address arithmetic is concerned. When you increment a far pointer,
only the offset is affected: if you increment it past the end of the
segment, the offset will wraparound and begin incrementing from 0 while the
segment portion remains the same:
// set to B800:FFFFH
char _far *fp = (char _far *)0xb800ffff;
fp++; // increment to B800:0000H
In this example, the far pointer is set to the last byte of the segment at
B8000H. Once the pointer is incremented, the pointer is set to the first
byte of the segment, at B800:0000H. Thus, like near pointers, far pointers
can only contain offset values from 0 to 65535. While they occupy more space
and are slower to manipulate, they can reference any object in any segment,
as long as the object itself does not extend beyond the end of the specified
segment.
Huge Pointers
Huge pointers are the only true pointers in the K&R sense of the word. While
physically the same as far pointers, storing a 32-bit address in a 4-byte
space, huge pointers behave differently. When you increment a huge pointer
beyond the end of a segment, it increments into the next segment. Therefore,
huge pointers can address any object of any size-even objects that extend
over several segments. For instance, the following code causes the huge
pointer to address the next physical byte, from B800:FFFFH to C800:0000H.
// set to B800:FFFFH
char _huge *hp = (char _huge *)0xb800ffff;
hp++; // increment to C800:0000H
It does not wraparound like near and far pointers.
Using huge pointers, you can write a program to access every memory location
in your machine.
// define 1 megabyte macro
#define MAXBYTES (1024L*1024L)
main(void)
{
long l;
unsigned char _huge *uch;
FP_SEG(uch) = 0;
FP_OFF(uch) = 0;
for(l = 0L; l < MAXBYTES; l++, uch++)
*uch = \0';
}
While this is a nonsensical program (nonsensical in that it would attempt to
overwrite every writable memory location in your PC, including itself and
DOS), it illustrates the addressing ability of huge pointers. This is why
huge pointers are the only pointers supported by Microsoft C that behave
like pointers in a flat-addressing scheme. Huge pointers are always slower,
since the compiler must generate code that can perform pointer arithmetic on
the entire 32-bit value, unlike near and far pointers where address
arithmetic is only performed on the offset component. Near and far pointers
are strictly products of the segmented architecture of the 80x86 CPU family
on which DOS PCs are based1.
Clearly, pointers in Microsoft C have specific attributes. Huge pointers can
address any object of any size; near and far pointers cannot address any
object outside of their designated segments. Far and huge pointers contain a
32-bit address and occupy 4 bytes; near pointers occupy 2 bytes and contain
a 16-bit address.
Based Pointers
As mentioned, based pointers combine the intersegment addressing features of
far pointers with the size and possibly the speed of near pointers. Like
near pointers, based pointers occupy only 2 bytes to create a 16-bit offset;
like far pointers, they can address up to 64Kb of any segment. However, far
addressing still requires a 32-bit address, while based pointers contain
only the 16-bit offset.
The base segment of near pointers is implicitly the program's data segment.
The "base" of far and huge pointers is contained in the segment portion of
the pointer. When using based pointers, you specify the base, but it is not
stored in the pointer itself.
Where does the segment portion of a based pointer come from? It's actually a
compiler trick. Using the new based pointer notation of C 6.0, you can
declare a based pointer and specify its segment base. Then, depending on
what type of based pointer you use, the compiler "knows" from the
declaration which segment the based pointer is based on. There are six types
of based pointers: segment based pointers, variable based pointers, void
based pointers, named segment based pointers, pointer based pointers, and
pointers based on self.
When it encounters a based pointer in use, the compiler generates code to
load a segment register (usually ES) with the pointer's associated segment
base. Then the compiler proceeds with the operation entailing the based
pointer. Of course, the based pointer itself supplies the offset portion of
the address.
Segment Based Pointers
Suppose you wanted to base a pointer on the segment associated with the
video display of an EGA or VGA adapter:
_segment videoseg = 0xb800;
unsigned _based(videoseg) *bpvid = 0;
This small piece of code does several things. It creates videoseg, which is
a _segment data type. The _segment data type, new in C 6.0, is similar to an
unsigned integer. It is used to store a segment value for pointers based on
it. The next line of code uses the _based keyword to create a based pointer,
bpvid, whose base is the segment stored in videoseg. The _based keyword must
be followed by a valid base expression in parentheses. However, you cannot
use an unsigned integer constant as below:
// WRONG!
unsigned _based((_segment)0xb800) *bpvid;
For illustrative purposes, here's how to use a far pointer to address the
same location:
unsigned _far *fpvid = (unsigned _far *)0xb8000000;
While this far pointer is set to the same location, it's not the same as the
based pointer. To use fpvid on another segment, you have to reset the entire
pointer (or at least use the FP_SEG macro to reset the segment portion of
the pointer). To use bpvid on another segment, you only have to change
videoseg. The big advantage here, of course, is that you can create an
entire set of pointers based on the same _segment variable, and change their
base by making a single change to the _segment variable.
In the following code, pspptr is a pointer to the program segment prefix
(PSP) structure, but it's based on the segment value stored in psp.
#include<dos.h>
_segment psp;
typedef struct _psp
{
unsigned int20;
unsigned allocblockseg;
char reserved0;
unsigned char dosfunctdispatch[5];
unsigned long int22;
unsigned long int23;
unsigned long int24h;
char reserved1[22];
unsigned envseg;
} PSP;
PSP _based(psp) *pspptr = 0;
If an application knows the segment address of another application's PSP, it
can access it by assigning that segment address to psp. Then pspptr can
reference members of the other application's PSP. Thus, pspptr>envseg
provides the segment address of a program's copy of the environment. For a
more explicit example, see the listings of ENVIRON.C (see Figure 1). You can
compile this program using EN.BAT (also in Figure 1). ENVIRON.C uses a based
pointer to access its own PSP and another based pointer (whose base is a
segment variable set to the environment set address in the PSP) to access
the program's copy of the environment strings.
Variable Based Pointers
You can also use _segment as a cast to extract the segment address of an
object. Thus, a based pointer can be based on the storage segment of another
object. In the following code, cbpsv is based on the segment in which count
is stored, and cbpsp is based on the segment that contains cp.
unsigned count;
char *cp;
char _based((_segment)&count) *cbpsv;
char _based((_segment)cp) *cbpsp;
The _segment cast is read as "segment of" in this context. This usage
differs from pointers based on other pointers, which is discussed below.
Using explicit segment addresses is only one way to declare a based
pointer's base. You can use the segment's name or derive the segment from
the address pointed to by a pointer. Based pointers that are members of a
structure can be based on the segment in which the structure resides. Or, as
shown in the next section, you can omit the segment declaration altogether.
Void Based Pointers
You don't always have to include a based pointer's segment with the pointer
declaration.
unsigned _based(void) *ubpv = 0;
Here, ubpv is a based pointer whose base segment is omitted from the
declaration. But a based pointer must be based on something! This method
lets you defer naming the segment only until the based pointer is used.
Whenever you reference a void based pointer, you must specify a segment
value.
_segment videoseg = 0xb800;
unsigned _based(void) *ubpv = 0;
*(videoseg:>ubpv) = (0x70 << 8) + H';
This piece of code uses videoseg (declared earlier) to provide a base for
ubpv. The code displays a reverse-video H at the upper-left corner of an EGA
or VGA screen. The new ":>" base operator lets you combine the segment
address stored in videoseg with the offset in ubpv. You can only use the
base operator on pointers based on void, however. If you attempt to use it
with a based pointer not based on void, the code wilol not compile.
The big advantage of void based pointers is that you can set them to a
particular offset and then use them with different base addresses. You can
also access the same data in different segments using the same pointer:
simply reference a different base each time. (Rather than changing a
_segment variable, you can change the base whenever you use the pointer.)
Most of the time you'll want to base a based pointer on a specific segment
address, as discussed above. But you may instead want to base it on the
address of a named segment.
Named Segment Based Pointers
Programmers who are writing large or medium model applications often make
each program module a named segment, with near calls and near data inside
the module. While this is a useful trick, it's nearly impossible to
reference the named segment outside of the module. Modules such as these are
usually named by the compiler via a compile line switch.
The based pointer support in C 6.0 lets you create new segments on-the-fly.
Additionally, it lets you designate which objects and pointers are based in
these segments. For instance, the following code creates a pointer whose
base is the program's code segment:
void _based(_segname("_CODE")) *vbpc;
The next line of code creates a pointer based on the program's data segment:
void _based(_segname("_DATA")) *vbpd;
This isn't really that useful, since it's the functional equivalent of a
near data pointer:
void *vptr;
However, the ability to base pointers on specific named segments can be
extremely useful. The _segname operator must be followed by a string that
names a segment in parentheses. This can be any of the predefined segment
names used in Microsoft C such as _CODE, _DATA, _CONST, or _STACK.
If you use a segment name that differs from these, the compiler will create
a new segment. For instance the following creates a pointer based on a new
segment, _NEWSEG:
void _based(_segname("_NEWSEG")) *vbpm;
The compiler, upon encountering this line of code, will create the new
segment. But because it doesn't do you any good to base a pointer on a
segment that doesn't contain anything, you can also embed objects in the new
segment. For instance, the following will create a string array that is
stored in _NEWSEG and referenced via newseg_message.
char _based(_segname("_NEWSEG")) newseg_message[] =
"This string is stored in the segment, _NEWSEG";
Note that if you make newseg_message a pointer instead of an array, you'll
create a based pointer to a near string:
char _based(_segname("_NEWSEG")) *newseg_message =
(char _based(_segname("_NEWSEG")) *)
"This string is stored in the segment, _NEWSEG";
The cast is required to eliminate a compiler warning.
If you've nearly exhausted data space in a small model program, you might be
able to avoid moving to the compact model by placing some of the data in
another segment. For instance, if your code segment is still well below
64Kb, you can store some of the program's data there. This code stores the
string at codeseg_message in the _CODE segment.
char _based(_segname("_CODE")) codeseg_message[] =
"This string is stored in the _CODE segment";
Keep in mind that any data referenced from another segment may require that
a segment register be reloaded, so access to this data will be slower than
that in the default data segment.
Pointers Based on Pointers
Suppose you want to base a pointer on the address another pointer points to.
unsigned _near *ip;
unsigned _far *fip;
unsigned _based(ip) *ibpip;
unsigned _based(fip) *ibpfip;
Here are two pointers and two based pointers. Each of the based pointers is
based on the address that one of the "regular" pointers points to.
Therefore, if the far pointer, fip, points to B800:0000H and ibpfip is set
to 5, then *ibpfip addresses B800:0005H. Any change to fip will affect
ibpfip: ibpfip will always be set to an address calculated by adding its
value to the address of fip.
This construction can be used in many ways. Suppose an application uses a
multidimensional array and makes changes to more than one dimension of the
same element. You can use one pointer to access the array's first dimension
and based pointers (based on the address the first pointer points to) to
access the other dimensions. If the based pointers are each set one
dimension apart, you can easily access the other dimensions in parallel.
Alternatively, you could allocate a far segment, and create a far pointer to
reference the segment base. Then you could use a based pointer (based on the
far pointer) to access parts of the same segment. You might be thinking that
this would be useful to create a linked list in a far segment. But another
type of based pointer would be more useful.
Pointers Based on Self
The remaining type of based pointer is self based; that is, its base is the
segment in which it is stored. Functionally speaking, near pointers are self
based. But you cannot use a near pointer in a far segment, nor is a far
pointer based on the segment in which it is stored.
For instance, consider the following linked list code:
typedef struct _list LIST;
struct _list
{
void *item;
LIST _based((_segment)_self) *left;
LIST _based((_segment)_self) *right;
};
void main(void)
{
LIST _based(_segname("LISTSEG")) list;
.
.
.
}
The LIST object type is defined, including two self based pointers, using
the new _self keyword. These pointers will be based on the segment in which
the LIST is created. As you can see, list is declared as being stored in the
LISTSEG segment. Therefore, the left and right pointers will point to
offsets in the LISTSEG segment, since they are self based.
Self based pointers are specifically designed to be used in far segments.
They make it easy to define complex data structures, as well as making them
portable and easy to declare. Like pointers based on void, self based
pointers defer choosing a base. (In a data structure, a self based pointer
will be based on the segment in which the data structure is stored.) That
is, its segment must be statically known at compile time. That way it will
point to an offset within the segment.
Linked List Manager
To illustrate the features, properties and benefits of based pointers
further, I've written a linked list manager (see the sidebar "The List
Manager API" and its accompanying code). As most programmers know, linked
lists are incredibly useful tools. They are generally simpler and faster to
use than disk files. List items can be added, deleted, and sorted quickly
and with little overhead. In single-linked lists, each node contains a
pointer to the next node in the list, allowing a search from any node
forward. In the List Manager, each node contains pointers to the nodes that
precede and follow it. You can use these double-linked lists to search in
either direction.
I believe that linked lists would be used more frequently if not for the
limitations inherent in most implementations. Lists are notorious for
fragmenting and exhausting a program's heap through the constant addition
and deletion of nodes. It's also quite a chore to save list items to disk:
you have to write each item to the file one at a time. It's even more
complex to restore the list from the disk file. In addition to reading the
data, the list linkages have to be restored.
The functions in the List Manager presented here use based pointers to
alleviate these problems. Instead of maintaining the list in the calling
program's data segment, the list is stored in a new segment. The List
Manager allocates this segment outside the program's data space and uses
based pointers to manage suballocation within the segment. Since a separate
segment is associated with each list, you can use and maintain multiple
lists of up to 64Kb each. Even if you're compiling with the small or tiny
memory model, the list will occupy no more than 4 bytes (for a handle) in
your program's default data segment. Also, the lists won't wreak havoc on
your program by fragmenting the heap.
Placing the list in a single separate segment also facilitates its storage.
The entire segment can be written as a single unit, making it a snap to save
it and restore it from disk. Since the linkages are retained inside the
segment, you can read them back from the disk file without having to rebuild
them. You can also extend the List Manager to use EMS memory if you wish.
Limiting a list to a single 64Kb segment might seem constraining, especially
if you're using it to juggle large objects such as graphic images. Each node
therefore contains a far pointer that can be used to track objects stored
outside the list's segment. Using a far pointer, you can track objects up to
64Kb each in size, and even store them in EMS. Finally, each node offers a
pointer to the node's name. This can also be used to store string data if
the object pointer isn't needed.
Based pointers make all of this possible. While your application uses a far
pointer as a list handle, the nodes and linkages are connected via based
pointers. Since allocation of the based pointers has to take place inside a
far segment, the new _bmalloc functions are used. These functions, discussed
in the sidebar, allow an application to suballocate a previously allocated
far segment. Therefore, the list functions call _bheapseg to allocate a new
far segment, _bmalloc to suballocate a new node within the segment, and
_bfree to free the node. Other functions (discussed in the sidebar) are
available although not used by the list functions.
How the List Manager Works
Since the List Manager has to be able to create new lists dynamically, it
uses a far memory segment that is allocated while an application is running.
Because of this, there is no previously declared base for pointers based on
this segment. The data structures for the list header and nodes (items)
therefore cannot use self based pointers, since a self based object requires
a specific base expression when an instance of it is created. Instead, the
List Manager maintains a static segment variable, _tempseg, which is used as
the base for the List Manager's pointers. Since the List Manager allows the
use of multiple lists, _tempseg must always contain the segment of the list
being manipulated by a List Manager function. Thus, each function begins
with the _listinit macro, which initializes _tempseg with the address of the
segment in which the list is stored.
The List Manager makes extensive use of two data structures, both of which
are stored in the list's far segment. The first of these is a control block
for the entire list:
typedef struct _list LIST;
typedef struct _litem LISTITEM;
struct _list
{
_segment seg; // list segment
unsigned num; // number of nodes
unsigned segsize; // size of segment
char _based(_tempseg) *name // name of list
LISTITEM _based(_tempseg) *item;
// first node or item
};
The LIST data type contains the list's segment address, segment size, and
the number of nodes. It also contains pointers to the list's name and the
first node or item in the list. Each list item is maintained in the LISTITEM
structure as follows:
struct _litem
{
void far *object; // object pointer
unsigned objectsize; // object's size
char _based(_tempseg) *name; // item name
LISTITEM _based(_tempseg) *prev;
// previous item in list
LISTITEM _based(_tempseg) *next;
// next item in list
};
An instance of this structure is allocated for each item in the list. It
lets the List Manager maintain pointers to the previous and next list items,
as well as a pointer to a far object. The object pointer can point to any
object anywhere in memory, allowing maximum flexibility. You can set this
pointer to an object maintained by your application, or it can point to a
large object stored in its own segment (for an example, see the animation
routines discussed below). The name pointer allows further flexibility. If
you want to name a list item or the object it points to, you can use this
pointer. Or, you can use it instead of the object pointer to store the
actual data item to which the node refers.
All the pointers (with the exception of the object pointer) are based on the
_tempseg variable, which is always loaded with a particular list's segment.
Every component of a list that is allocated by the List Manager is stored in
that list's segment, making it easy to maintain a list, as well as save and
restore it to and from a disk file.
List Manager API
The List Manager can be placed in a library, or linked into an application
as an object file. In either case, the calling application does not need to
know anything about the inner workings of the List Manager. A small API is
available to use instead. An application calls ListInit to create a list,
and it returns a handle (a void far pointer) to the new list. ListInit
requires the list name and the suggested number of nodes that will be used.
ListInit then uses this number to "guess" the size of the far segment to be
allocated.
Internally, ListInit calls _ListInit to create the new segment and return a
LIST pointer to it. _ListInit uses _bheapseg to create the new far segment,
and then calls _bmalloc to suballocate space for the LIST header structure.
Since _bmalloc returns _NULLOFF instead of NULL upon failure, any
uninitialized pointers in the List Manager structures are set to _NULLOFF.
After a program creates a LIST and receives a far pointer to it, the program
can call other List Manager routines to build and manipulate the list. For
example, ListAdd calls _bmalloc to suballocate a new list item and add the
item to the tail of the list. ListDelete will remove an item from the list;
if the item has a related object (via the object pointer), the pointer is
returned to the calling application to free it. The API also contains two
functions, ListDump and ListDumpItem, for debugging list nodes and pointers.
Other functions are briefly described in the sidebar "The List Manager API."
Note the two functions, ListSave and ListRestore. ListSave lets you save a
list to a specified disk file. It does this by first writing a structure
that includes the segment size and the offset portion of the list handle
pointer. Next, ListSave writes the entire list segment to the disk file, and
then writes each item's object (if it has one) to the file.
ListRestore can restore an entire list from the disk. The function first
opens the disk file and reads the structure written at the beginning of the
file. It uses the segment size to allocate a segment of the proper size
without having to read the list segment from the disk. While ListRestore
calls _ListInit to create the new segment, the LIST pointer it returns may
not correspond to the one originally saved in the file. Therefore, the
offset portion of the LIST handle is set to the offset portion of the list
handle pointer that was previously stored at the beginning of the file.
After this, ListRestore reads the entire segment into the newly allocated
segment, and reads each item's objects into far memory segments that it
creates.
Sample Programs
Several sample programs are presented with this article. TESTLIST.C (see
Figure 2) creates a list, adds several items to it, then calls ListDump to
display the list items and connections. MAKANIM.C (see Figure 3) and
ANIMATE.C (see Figure 4) display a brief, repetitive animation sequence
using a List Manager's linked list. I used a screen capture program to
create a series of screen shots in which a box is drawn and bounces between
the top and bottom of the screen. MAKANIM should be compiled and run first:
it creates the list by reading the screen shot files (which are available on
all MSJ bulletin boards) and making each an object related to a list node.
Since the objects are designed for display on an EGA or VGA monitor, only
the first 4Kb of each screen shot file (.ACP) is used. The program then
saves the list in a file, ANIMATE.LST. ANIMATE.EXE restores the list from
ANIMATE.LST, displaying the screen shots over and over in a loop. A key
press terminates the loop.
Once you've compiled MAKANIM.C and ANIMATE.C, run MAKANIM, followed by
ANIMATE.
MAKANIM
ANIMATE ANIMATE.LST [/b]
The optional /b switch will cause ANIMATE to call a based pointer screen
display routine. You'll find that the difference in speed between a based
pointer and a far pointer is almost unnoticeable.
Conclusion
Based pointers are a product of segmented architecture. They further blur
the boundaries between code and data by allowing you to store data in a code
segment. They offer an alternative that is similar to near pointers, while
allowing access to data typically reached via a far pointer. They also make
it easier to transfer complex data structures between disk, EMS, and
conventional memory. Based pointers are not without limitations. The
compiler is sometimes too strict about operations such as assigning one
based pointer to another. If both pointers use different base names that
refer to the same physical base, the compiler generates a warning. You'll
have to use a cast to eliminate this. Also, since the compiler generates
code to load the ES register when a based pointer is used, repeated reloads
of that register can generate slow-performing code. This can be a problem
especially when using a variety of based pointers with different bases. On
the other hand, the generated code can be very fast if you're using several
pointers with the same base. u
CHECKERS for PM Part IV: Adding the Game-Playing Strategy
Charles Petzold
This is turning out to be a lot harder than I thought it would be."
That's not an uncommon reaction after getting into a new programming
project. If everything were as easy as it first seemed, deadlines would be
met with no problem. But missing deadlines is a chronic problem in the
software industry. The bigger the program, the worse the problem.
Most programs published in books and magazines are very small. The code is
intended to show programming techniques in isolation from the
often-considerable requirements of real applications. I believe this is
perfectly legitimate, because the smaller the code sample is, the more
inclined readers are to look at it and learn something from it. Those of us
who write these small programs for books and magazines sometimes think that
writing a real application is simply a matter of putting all these little
techniques together. This is not so.
When the MSJ editors and I began to discuss writing a checkers program for
OS/2 Presentation Manager (hereafter "PM"), one of our goals was to show the
proper way to assemble a fairly large and complex PM program.
I believe now that that idea is fundamentally wrong. It is one thing to show
how to set up parameters for a call to GpiBitBlt-where there can be very
little variation in the code-and quite another to show the overall structure
of a large program. There is no one right way to write a large program. The
lessons to be learned are in the concepts and methodology rather than the
execution.
As I'll describe, I have shown one possible solution to structuring
CHECKERS, but it is not the only one. Every programmer who might design such
a program would probably do it differently, whereas everyone's code to move
a piece across the board using GpiBitBlt would probably look pretty much the
same.
The New Installment
This installment of CHECKERS (which may be the last since I'm not sure if I
am going to add additional functionality) has a new module, CKRSTRAT.C. It
contains a checkers-playing strategy based on straightforward look-ahead
techniques. This allows you to play a game with the program.
The source code for CHECKERS is now more than fifty pages long, so it's
impractical to reprint it here. You can download the source code and
CHECKERS.EXE from one of the MSJ bulletin boards.
Overall Structure
I decided to structure CHECKERS around the windowing and messaging mechanism
built into PM. Each major component of the program uses a window procedure
to send messages to (and receive messages from) the other components.
Figure 1 shows the six source code modules of the program and their
relationships. Four modules contain window procedures. These modules
communicate with each other using user-defined messages, which are messages
that begin with WM_USER (the value 1000H). The other two modules contain
support functions.
CHECKERS.C contains the program's main function and creates a standard
window using ClientWndProc as the window procedure for the Client window.
While processing the WM_CREATE message, ClientWndProc creates the program's
other three windows. ClientWndProc also processes menu commands.
BoardWndProc (contained in CKRBOARD.C) is the window procedure for a child
window that is the same size as the Client window and completely covers it.
BoardWndProc is responsible for drawing the board (using functions in
CKRDRAW.C), for processing keyboard and mouse messages to allow the user to
move a piece on the board, and for moving a piece based on the result of the
checkers-playing strategy.
Both JudgeWndProc (contained in CKRJUDGE.C) and StratWndProc (contained in
CKRSTRAT.C) are window procedures for object windows. Object windows are not
displayed on the screen but they can send and receive messages just like
normal windows. JudgeWndProc controls the game. It instructs BoardWndProc
when to allow the user to move a piece, it tells StratWndProc when to
calculate the best move in response to the user's move, and it tells
BoardWndProc to move a piece based on the decision made by StratWndProc.
JudgeWndProc can also service requests from BoardWndProc to determine legal
moves. In effect, when the user attempts to pick up a piece with the mouse,
BoardWndProc sends a message to JudgeWndProc asking if it's legal to pick up
that piece. Based on routines contained in CKRMOVES.C, JudgeWndProc responds
affirmatively or negatively.
I will refer to these four windows as the Client, the Board, the Judge, and
the Strategy.
User-Defined Messages
The user-defined messages that the window procedures use for communication
are defined in the CHECKERS.H header file. First, let's examine the messages
that ClientWndProc sends to (and receives from) JudgeWndProc and
BoardWndProc.
After ClientWndProc creates the other three windows, it sets up a NEWGAME
structure (defined in CHECKERS.H) and sends a WM_NEW_GAME message to
JudgeWndProc. This message initiates a new game. The message can also occur
when the user selects "New Game" from the program's menu. In this case,
ClientWndProc displays a dialog box that allows the user to select black or
white pieces and a level of difficulty.
This NEWGAME structure is defined in CHECKERS.H and contains eight fields
(see Figure 2).
The first two fields are the window handles of the two players in the game.
(I will refer to these as the Player windows.) The next three fields are the
handles of the Board, Judge, and Client windows. In the current version of
the program, either hwndBlack or hwndWhite will be the same as hwndBoard,
depending on whether the user wants to play the black or white pieces. The
other is set to the handle of the Strategy window.
Conceivably, in a future version of the program hwndBlack and hwndWhite
could refer to two windows that communicate with two different external
dynamic-link libraries that also contain checkers-playing strategies. In
this case neither hwndBlack nor hwndWhite would be the same as hwndBoard.
The Board window would be used solely to show the user how the game is
progressing.
The sixth field of the NEWGAME structure is a BOARD structure, which is also
defined in CHECKERS.H:
typedef struct
{
ULONG ulBlack ;
ULONG ulWhite ;
ULONG ulKing ;
}
BOARD ;
As I've discussed in previous installments, these three 32-bit integers
define the layout of the board, with each bit position corresponding to one
of the 32 squares on which a piece may reside. A game begins with ulBlack
equal to 00000FFFH, ulWhite equal to FFF00000H, and ulKing equal to zero.
However, a special test mode (which I'll discuss toward the end of this
article) allows overriding of this layout.
After receiving the WM_NEW_GAME message, the Judge sends the message on to
the two Player windows and, if neither of the two Player windows is the same
as the Board window, to the Board window also. Each window that receives the
WM_NEW_GAME message (the Judge, the two Players, and the Board) extracts the
fields important to it and saves them in static variables.
For example, the Judge saves the two Player window handles because it must
instruct the Players when to make a move. It also needs the Board window
handle to tell the Board when to move a piece based on the response of the
Strategy window, and it needs the Client handle for notifying the Client
that the game is over (as I'll describe shortly).
The Board window saves the Judge window handle because it will need to ask
the Judge (through messages) whether a user is attempting to make a valid
move. The Board window also saves the sBottom field, which indicates which
color should be at the "bottom" of the board (the part of the board that
seems closer to the user). Currently, this depends on whether the user is
playing black or white pieces.
The Strategy window saves the Judge window handle because it will need to
inform the Judge of the move it has decided is best. It also saves the
sLevel field, which can range from 0 to 3 and indicates the level of
difficulty used in the checkers-playing strategy.
When JudgeWndProc determines that the game is over, it sends a
WM_TELL_CLIENT_GAME_OVER message back to the Client window, indicating which
color (black or white) has won. In this version of the program,
ClientWndProc simply displays a message box indicating the winner. This
message and the WM_NEW_GAME message are the only two messages sent between
the Client and the Judge.
When the user selects a menu option to change the color of the board,
ClientWndProc sends a WM_TELL_BOARD_COLOR_DIALOG or a
WM_TELL_BOARD_STANDARD_COLORS message to BoardWndProc. These messages
instruct BoardWndProc to display a dialog box (whose dialog box procedure is
contained in CKRDRAW.C) allowing the user to choose new colors, or to set
the standard colors. These are the only two messages the Client sends to the
Board. The Board does not send any messages to the Client.
Making Moves
The Judge window notifies the two Player windows when they should make a
move by sending them WM_JUDGE_SAYS_MAKE_MOVE messages. It sends the first
make-move message to hwndBlack and waits for a response. The second
make-move message is then sent to hwndWhite. This continues until the game
is over (which occurs when a player cannot make a move) or until the user
chooses to begin a new game or ends the program. When sending this message,
the Judge really doesn't care whether the message is going to the Board
window or the Strategy window.
When the Strategy window calculates a best move in response to the make-move
message, it responds by posting the Judge a WM_TELL_JUDGE_STRAT_MOVE_ENDED
message with the move. This and the make-move message are the only two
messages between the Judge window and the Strategy window.
The messages between the Judge window and the Board window are more complex.
When the Board window receives a WM_JUDGE_SAYS_MAKE_MOVE message, it
displays a mouse pointer that looks like a little hand and allows the user
to pick up a piece with the mouse and move it. During this time, the Board
sends several messages to the Judge to determine if the move is legal. These
messages include:
WM_QUERY_JUDGE_PICKUP_PIECE
WM_QUERY_JUDGE_PUTDOWN_PIECE
WM_QUERY_JUDGE_CONTINUE_MOVE
When the user has completed a move, the Board sends the Judge a
WM_TELL_JUDGE_BOARD_MOVE_ENDED message. At this time the Judge sends the
Strategy window a WM_JUDGE_SAYS_MAKE_MOVE message.
When the Judge receives a WM_TELL_JUDGE_STRAT_MOVE_ENDED message from the
Strategy window, the Judge must inform the Board window to move the piece.
It does this using three messages:
WM_JUDGE_SAYS_MOVE_PIECE
WM_JUDGE_SAYS_KING_PIECE
WM_JUDGE_SAYS_REMOVE_PIECE
On receipt of the WM_JUDGE_SAYS_MOVE_PIECE message, the Board window starts
a timer by calling the WinStartTimer function. When it receives the WM_TIMER
messages, the Board window progressively moves the piece from its origin
square to its destination square. It does this by calling four new functions
I've added to CKRDRAW.C specifically for this purpose:
CkdExternalSave
CkdExternalShow
CkdExternalRestore
CkdExternalMove
These functions are similar to those discussed in Part III of this series
that move a piece based on the mouse pointer.
The Judge sends the Board a WM_JUDGE_SAYS_KING_PIECE to indicate to the
Board that a piece has reached the end and must be kinged, and a
WM_JUDGE_SAYS_REMOVE_PIECE to indicate to the Board that a piece has been
jumped and must be removed from the board.
When the Board has finished moving a piece in response to a
WM_JUDGE_SAYS_MOVE_PIECE message, the Board sends the Judge a
WM_TELL_JUDGE_PIECE_MOVED message. At this point, the Judge instructs the
Board to allow the user to move a piece by sending the Board a
WM_JUDGE_SAYS_MAKE_MOVE message.
The Judge and the Strategy
As I mentioned, the Judge window informs the Strategy window when to
calculate a best move by sending it a WM_JUDGE_SAYS_MAKE_MOVE message. The
mp1 message parameter that accompanies this message is a pointer to a
structure of type MOVE, defined in CKRSTRAT.H as follows:
typedef struct
{
SHORT sColor ;
SHORT sKing ;
SHORT cSubMoves ;
BOOL fNewKing ;
SUBMOVE asubmove [10] ;
}
MOVE ;
The Judge sets the sColor field to 0 for Black or 1 for White, depending on
whether it wants the Strategy window to calculate the best Black move or
best White move. The next three fields are initialized to zero.
The last field of the structure requires a bit of an explanation. A checkers
program is complicated somewhat by the rules governing jumps. If a player's
piece can jump an opponent's piece, then it must jump the piece. If after
jumping that piece, another jump is available, it must make that jump also.
This must continue until no more jumps are available. I refer to each of
these jumps in a single move as a submove.
It is possible to set up a checkerboard so that a piece can jump nine pieces
in a single move. This is highly unlikely, of course, and perhaps even
impossible. Regardless, I've allowed for nine submoves by defining an array
of ten SUBMOVE structures in the MOVE structure. (I'll discuss shortly why
the extra array element is required.)
The SUBMOVE structure is defined in CKRSTRAT.H as follows:
typedef struct
{
BOARD brd ;
SHORT iBeg, iEnd, iJmp ;
}
SUBMOVE ;
Before passing a pointer to the MOVE structure to the Strategy window, the
Judge initializes the BOARD structure in the first element of the SUBMOVE
array. (I discussed the BOARD structure earlier.) This contains the current
layout of the board.
When the Strategy window receives a WM_JUDGE_SAYS_MAKE_MOVE message, it can
save the value of mp1:
pmove = PVOIDFROMP (mp1) ;
where pmove is defined as a pointer to a MOVE structure. The Strategy window
knows what color piece to move by the value of:
pmove->sColor
It knows the current layout of the board by the BOARD structure obtained as
follows:
pmove->asubmove[0].brd
The Strategy window already knows the difficulty level because of the sLevel
field passed in the NEWGAME structure. This is the bare minimum of
information the Strategy window needs to calculate a best move.
When the Strategy window has finished calculating the best move, it alters
the MOVE structure in ways that I will describe below, and then posts a
WM_TELL_JUDGE_STRAT_MOVE_ENDED message to the Judge window. The Judge can
then determine which move was made by analyzing the MOVE structure whose
pointer it passed to the Strategy window.
The Strategy window is responsible for setting the sKing field of the MOVE
structure to 1 if the piece it moved is a king. The fNewKing field is set to
TRUE if the piece becomes a king at the end of the move. The cSubMoves field
is set to the number of submoves, which can range from zero (if no move was
available) to nine (indicating it jumped nine of the opponent's pieces).
For each submove, the Strategy window is also required to set the fields of
the SUBMOVE structures to define the move. For example, if cSubMoves is set
to 1, which means that the move consisted of a non-jump or a single jump,
then the following still represents the original layout of the board.
pmove->asubmove[0].brd
The iBeg and iEnd fields of the asubmove[0] structure are the board
positions (ranging from 0 to 31) of the origin square and destination square
of the piece that moved. The iJmp field is the board position of the
opponent's piece that was jumped, or 1 for a non-jump move. The following is
the layout of the board after the move.
pmove->asubmove[1].brd
If the move consisted of nine submoves, each submove is indicated in the
elements of the SUBMOVE array, with the following indicating the final
layout of the board.
pmove->asubmove[9].brd
In general, the layout of the board following the move can be obtained from:
pmove->asubmove[pmove->cSubMoves].brd
The Judge window uses the information in the MOVE structure to pass messages
to the Board window to carry out the move on the board.
More Structures
The Strategy window uses a straightforward look-ahead system to determine
the best move. Let's suppose that the Strategy window is playing the White
pieces. Based on the current layout of the board (obtained from the MOVE
structure as described above), the Strategy window can determine all legal
White moves based on that board layout, all Black moves in response to all
those White moves, all White moves in response to all those Black moves, and
so forth.
Keeping track of all these possible moves requires a tree of
self-referential structures. The Strategy window first uses a structure
called MOVEBLOCK to store all the legal moves available in a particular
board layout (see Figure 3).
The first field indicates the number of possible moves. The Strategy window
must allocate memory for a MOVEBLOCK structure of sufficient size for cMoves
MOVEP structures.
The MOVEP structure is similar to the MOVE structure except that it contains
an additional field, a pointer to a MOVEBLOCK structure that contains all
the moves in response to this move (see Figure 4). This pointer is defined
as a VOID pointer because the structure is defined in the source code before
the MOVEBLOCK structure is defined.
These two structures are used for building the tree of possible moves.
Suppose the user is playing Black and the Strategy window is playing White
and that there are five possible White moves. The Strategy window allocates
a memory block sufficient to hold a MOVEBLOCK structure plus four additional
MOVEP structures. The cMoves field is set to five and the five elements of
the amovep array are set to the five possible moves.
For each of those five possible moves, there may be several countermoves by
Black. Five additional blocks of memory (for each of these five sets of
countermoves) are allocated for five MOVEBLOCK structures. Pointers to these
five MOVEBLOCK structures are assigned to the pmoveblk fields of the five
MOVEP structures in the original MOVEBLOCK structure.
In practice, however, it is usually impossible to determine the number of
possible moves at the outset. The problem, as is typical with checkers
programs, is in working with multiple jump moves. There may be a jump
available, and after that jump there may be two possible second jumps. This
requires that the MOVEBLOCK structure be reallocated as the moves are being
determined.
Recursion
As you may have guessed by now, the checkers-playing strategy cries out for
recursive functions (functions that call themselves). The CKRSTRAT.C module
has several recursive functions.
The most obvious need for a recursive function is in determining moves and
countermoves and counter-countermoves and so forth. This is handled by the
CksGetCounterMoves function in CKRSTRAT.C.
Another recursive function is useful for analyzing multiple jumps. After the
program has determined that a jump is available, it must search for
additional jumps. This is handled by the recursive CksGetAllJumps function.
After all the MOVEBLOCK structures in this tree have been allocated and
analyzed, it is necessary to free all the memory. A third recursive
function, call CksFreeMoveBlock, handles this chore.
Checkers-Playing Strategy
This version of CHECKERS supports four different levels of strategy, Simple,
Beginner, Intermediate, and Advanced. In this discussion I'll assume that
the user is playing Black and the program determines the best White move in
response to that.
The Simple level is indeed quite simple. The checkers-playing strategy makes
random (but legal) moves. This is not quite as perverse as you might think:
in the early stages of the game, it's sometimes difficult to determine
whether you're playing a computer or a small child.
The playing level I call Beginner goes one level deep in the look-ahead. It
surveys all the possible White moves and picks the one with the best score.
The result of this is not very significant: if White has several possible
jump moves, it will pick the one that jumps the most pieces.
The Intermediate playing level looks three levels deep. It looks at all the
possible White moves, all the possible Black moves in response to those, and
the White moves in response to those Black moves. When considering how to
use this information, your immediate instinct, as programmer of this
algorithm, might be to find the best White move in this third level, and
take the original White move that would result in this. But that's wrong
because it assumes that the user (playing Black) is stupid.
What you have to assume is that the user is almost as smart as the program,
but not quite. For each of the first White moves, the program must find the
best Black move in response to that move, and then the best White move in
response to those best Black moves. The best White move then determines
which original move to make.
For example, Figure 5 shows a simple three-level tree, assuming that there
are three possible White moves, three Black moves in response to those, and
three White moves in response to each Black move. For some of these moves,
the player's board advantage (which I'll discuss shortly) is shown in a
circle. For the White moves, this is the White advantage, and for the Black
moves, it's the Black advantage. (Advantages don't need to be calculated for
the moves with empty circles.)
Look at the Black moves first. For each of the original White moves, Figure
5 indicates the best Black countermove with a heavy circle. Now look at the
White responses to those best Black moves. Again for each set, the best
White move in each set is indicated with a heavy circle. Overall, the best
White advantage is 300, which results from the best White move to the best
Black move to the second original White move. So the program picks the
second White move.
The Advanced level goes five levels deep. It picks the best Black move on
the fourth level, the best White moves on the fifth level in response to
those, and based on the best White move on the fifth level, determines the
best original White move.
When determining the best moves, it may be that several moves result in the
same advantage. In this case, the program chooses randomly among these best
moves.
Calculating the Advantage
This version of CHECKERS calculates an advantage based on a simple analysis
of the layout of the board. I'll discuss this based on the Black advantage;
simply taking the negative of the number converts to a White advantage.
If there are no White pieces left, or if there are no legal moves or jumps
that a White piece can make, then White has lost and the advantage is set at
10,000.
Similarly if there are no Black pieces or if Black cannot make a legal move
or jump, then Black has lost and the advantage is set at 10,000.
Otherwise, the advantage is calculated by subtracting the number of White
pieces from the number of Black pieces, where a non-king piece is counted as
100 and a king is counted as 175. (Thus, it is assumed that a king is worth
75 percent more than a non-king.)
This is perhaps the simplest approach to calculating the advantage. More
sophisticated techniques involve searching for various advantageous patterns
on the board and weighting each of them. The program could then change these
weights based on experience. If you are interested in this, a good place to
begin research are two papers by Arthur L. Samuel titled "Machine Learning
Using the Game of Checkers" reprinted in Computer Games I (Springer-Verlag,
1988), edited by David N.L. Levy.
Testing the Code
Testing a program such as this can be difficult. For a while, I simply
played games with the program and tried to make sure it wasn't making dumb
moves. But this was obviously unsatisfactory.
The solution I chose was to include a special "Setup" option on the New Game
dialog box. This creates another dialog box that allows you to interactively
define the initial layout of the board. Restricting the board to just a few
pieces allowed me to determine more easily if the program was performing
correctly.
Of course, a programmer working alone is rarely able to debug any but the
simplest programs entirely. Fortunately, I can call upon perhaps the finest
group of beta-testers in the world for help-the readers of MSJ!
Learning Windows[tm] Part II: Resources and the Menuing System
Marc Adler
The first article in this series introduced the concepts behind programming
an application for the Microsoft Windows[tm] graphical environment. It
demonstrated the message-based architecture of Windows1 and showed how
Windows uses a message loop to maintain cooperative processing among the
various tasks running in a session. I defined a window as not only a visual
object in which data is displayed, but also as an object that receives and
processes messages. Each window has a message handler, called a window
procedure, that processes the messages sent to it.
While the first article contained some initialization code, I really did not
begin writing the sample stock- charting application. In this issue, I will
create the main window and add a menu system to it. The user will choose a
command from the menu, the command will be sent to the main window, and the
main window will respond in some way.
All that you need to do to create a complete pull-down menu system is define
the menu in the application's resource file. Resource files are therefore
the first items I'll discuss. Next, menuing systems and the API functions
and messages associated with them are described. Finally, I'll discuss
shortcut keys, called accelerators, that permit the user to invoke any
command in your application.
Resources
A resource is a binary description of a Windows object. Windows resources
include dialog boxes, menus, strings, accelerators, fonts, icons, bitmaps,
cursors, and user-defined binary data.
A resource file is an ASCII text file that contains an English-like
description of the resources used by an application. This file contains the
actual definitions of some of the resources such as menus, strings,
accelerators, and dialog boxes. Other resources, such as icons, fonts, and
bitmaps, cannot be defined in ASCII. Therefore, the ASCII resource file
contains the names of files in which these resources are stored in binary
form.
The non-ASCII resources must be created with special tools. For a bitmap
image, for example, you can use the SDKPAINT program in the Windows 3.0
Software Development Kit (SDK). After you have created the bitmap image, you
can use a good old text editor to modify the resource file yourself, adding
a reference to the bitmap file you've just created. The resource file
usually has the same name as the application but has an RC extension.
After an RC file is made, it must be "compiled" into a binary resource file.
Binary images of resources are much faster to load into an application than
ASCII resource descriptions because the application doesn't have to
interpret them. The resource compiler generates binary images from the
resources defined in ASCII format and combines these with the binary images
stored in external files. The result is a file with a RES extension. To
compile the RC file into a RES, invoke the Windows Resource Compiler
(RC.EXE) on the resource file using the -r option.
rc -r app.rc
The last step in compiling and linking any Windows application is merging
together the binary resource file (RES) and the application's EXE file. To
tack the RES file onto the EXE file, invoke the resource compiler a second
time without the -r option:
rc app
This is the last command you invoke when building a Windows program.
There are several good reasons to use resources. First, a resource is not
initially loaded into memory when its application is. A resource can be
loaded explicitly at the request of the application programmer, or loaded
implicitly by Windows when Windows needs the resource. This means that the
memory taken up by the program is less than it would be if the resource were
stored statically within the data segment.
Second, compiling a resource file is extremely fast. If you need to change,
for example, the the layout of controls in a dialog box, all you need to do
is change the RC file, recompile it with the resource compiler, tack it on
to the application's EXE file, and rerun the application. There is no need
for you to change your program's source code and then have to recompile and
relink the program.
Third, by isolating all of the text strings your program uses in a resource
file, your program is much easier to adapt to a foreign language. For
instance, if you wanted to create a Swedish version of an application, all
you would have to do is to edit the strings, menus, and dialog boxes in the
resource file and recompile the resource file.
Now that you know what a resource file is, I will create one from scratch
and define the pull-down menu system in it.
Menus
Most Windows applications rely on a pull-down menu system for their
top-level user interface. Windows 3.0 improves on the standard pull-down
menus. One addition is hierarchical pull-down menus. Using this menu style,
individual pull-down menus can be attached not only to items on the menu
bar, but also to items on other menus (see Figure 1). This facility permits
an application to granularize its command set better and lets the user find
the proper command more easily. It also eases the command overcrowding that
characterizes the pull-down menus of large, complex applications.
Another nice addition to the pull-down menu system is the floating pop-up
menu. These menus are not attached to a menu bar. Windows gives you the
ability to create or load a predefined pop-up menu and place it anywhere
within a window. For example, the user can click on a button and have a menu
of choices appear over that button.
A menu can be activated in a variety of ways. You can click a menu bar item
with the mouse; you can press the Alt key with the key that corresponds to
the underlined letter of a menu bar item. This combination is called a
hotkey. Or you can press Alt without pressing any other keys. This activates
the menu bar, highlighting the first entry without activating an attached
pull-down menu. You can use the cursor keys to move along the menu bar,
pressing Enter when you reach an item whose pull-down menu you want to
invoke.
The other type of menu supported by Windows is the system menu, which is
usually attached to each top-level or pop-up window. It is represented by an
icon in the upper-left corner of the window. Certain predefined items appear
on the system menu across each class of windows. For example, every
top-level application window has a system menu with the entries Restore,
Move, Size, Minimize, Maximize, Close, and Switch To. Almost all dialog
boxes have a system menu with only two entries, Move and Close. Most Windows
applications do not alter the system menu, although the Windows API provides
that option. Users expect to find certain entries in the system menu, so
it's best to keep these standard throughout Windows applications.
Menus and Resource Files
The first step in defining a menu system in the sample stock-charting
application is to create the resource file. I used a text editor to create
a file called STOCK.RC (see Figure 2). The first line in any resource file
should be the following:
#include <windows.h>
WINDOWS.H, the main include file used by Microsoft Windows programs, is a
large file that defines most of the constants, messages, data structures,
and function prototypes in Windows. As far as resource files are concerned,
WINDOWS.H contains the #defines of the keys used in the definitions of the
accelerator keys and the constants for the style bits used by control
windows.
Now it is time to define the application's menu system. The syntax of a menu
definition is shown in Figure 3.
The POPUP keyword defines a pop-up menu that is activated when the user
selects the associated string from a lower-level menu. The string following
the POPUP keyword is actually placed on the pop-up menu's parent menu. In
fact, most menu bars are defined as a series of pop-up menus, with each
string associated with the pop-up menu placed in sequence in the menu bar.
The MENUITEM keyword defines an atomic menu item, meaning that selecting
this item does not invoke another pop-up. The MENUITEM definition contains
the text string associated with that item, a command identifier used to
distinguish a command from others in the application, and a list of menu
attributes. A menu item can have several attributes associated with it that
serve as visual cues for the end user.
An ampersand in a menu item string indicates to Windows that the character
following the ampersand will be the hotkey for that item at run time. The
ampersand itself is invisible. On a menu bar, you invoke the menu by
pressing Alt-character. In a pull-down menu, pressing the hotkey will move
the selection bar to that menu item.
One thing to note in Figure 2 is a multilevel pop-up menu. The Graph
pull-down menu has an entry called "&Grid." Pressing Enter at this menu item
reveals a higher-level pull-down menu that prompts the user to draw the
horizontal or vertical grid lines. Windows 2.x programmers had to add two
menu items to the Graph menu, "Horizontal Grid" and "Vertical Grid." In
Windows 3.0, you need only one generalized entry for the grid, simply adding
a lower-level submenu.
Also note that the "Help" menu bar item has the characters "\a" prefixed to
the string. A menu item with this prefix will be right-justified in the menu
bar. This style is usually reserved for the menu that invokes the
application's help facility.
After the menu system is defined, you can run it through the resource
compiler with the following command:
rc -r stock
Loading Menus Into an Application
When an application starts up, a binary description of the menu system sits
within the executable file on disk. One way to get it into memory and attach
it to a window is to associate the menu with a window class. In the
initialization code presented in the previous article, you saw how to
register a window class by filling it with the elements of a WNDCLASS
structure and calling the RegisterClass function. One of the members of the
WNDCLASS structure is:
LPSTR lpszMenuName;
By assigning the name of the menu to lpszMenuName before calling
RegisterClass, you tell Windows that every time a window of that class is
created, the specified menu should be loaded into memory and attached to
that window automatically. Recall that one of the arguments in the
CreateWindow function was the handle of the menu that should be attached to
the created window. If you specify NULL for the hMenu parameter when calling
CreateWindow, the class menu (if any) will be attached to that window (see
Figure 4).
Another way to load a menu from a resource file is to call LoadMenu. The
syntax for LoadMenu is:
HMENU LoadMenu
(HINST hInstance,
LPSTR lpszMenuName)
The first argument to LoadMenu is the instance handle of the instance of
your Windows application; the second parameter is the name of the menu in
the resource file. The name of the menu, or any resource, can be either a
text name or a numeric identifier. If it is a numeric value, you must use
the MAKEINTRESOURCE macro in order to cast the numeric value to an LPSTR.
For example,
hMenu = LoadMenu(hInst, "MyMenu");
or
hMenu = LoadMenu(hInst, MAKEINTRESOURCE(ID_MYMENU));
LoadMenu returns the handle of the loaded menu, which can then be used as
the hMenu parameter in the CreateWindow call. This is how you would attach a
menu other than the class menu to a window.
You can also create a menu on the fly using CreateMenu. This function
creates an empty menu and returns its handle to the application. You can
then use InsertMenu or AppendMenu to add both atomic items and pop-ups to
this menu.
HMENU hMenu;
hMenu = CreateMenu( );
AppendMenu(hMenu, MF_STRING, ID_OPEN, "&Open...");
AppendMenu(hMenu, MF_STRING, ID_CLOSE, "&Close");
The WM_COMMAND Message
When defining a menu system in a resource file, you saw that each MENUITEM
had a command identifier associated with it. For instance, in the menu item
defined by the following line, the constant ID_GRAPH_VOLUME is the command
identifier associated with the "Volume" menu item.
MENUITEM "&Volume", ID_GRAPH_VOLUME
This command identifier should be a unique number that distinguishes the
command from any other command in the application.
Fortunately, Windows makes menu handling automatic, so the programmer
doesn't have to worry about polling the menu system to determine whether the
user has selected an item from a menu. When the user selects a menu item,
Windows simply posts a WM_COMMAND message to the window procedure of the
window that owns the menu. If the user selects an item from a window's
system menu, Windows posts the similar WM_SYSCOMMAND message to that window.
The previous article stated that when a message is sent to a window
procedure, two other data elements are also passed to the window procedure.
These two pieces of data, wParam and lParam, provide the window procedure
with additional information concerning the message.
When the WM_COMMAND message is sent to a window, the wParam field contains
the command identifier of the command invoked. This command identifier is
the same value specified for a menu item in the resource file. By examining
the wParam field, you can tell which menu item the user selected. And,
because you know which command was sent, you can call one of your own
routines to handle that command. A piece of skeleton code for handling
WM_COMMAND messages is shown in Figure 5.
WM_SYSCOMMAND Message
The WM_SYSCOMMAND message is sent to a window when the user selects an item
from that window's system menu. Windows uses the low-order 4 bits of wParam
internally, so to determine which command was chosen, you need to
logical-AND wParam with FFF0H to mask off the low-order nibble. Some of the
messages and operations the system menu allows for a standard window are
shown in Figure 6.
Most of the time, you would let the DefWindowProc handle the processing of
the WM_SYSCOMMAND message. However, there might be special cases in which
you want to prevent the user from maximizing or minimizing a window. You can
do this by handling the WM_SYSCOMMAND message yourself, not passing it on to
the DefWindowProc.
Other Menu Messages
When the user selects an item from the top-level menu bar, the WM_INITMENU
message is sent to the window that owns the menu. This permits the
application to alter the appearance of the menu bar if necessary, but most
of the time this message will be simply passed on to DefWindowProc.
Windows convention dictates that the appearance of the menu bar should
remain constant, and that you should only change the appearance of the
lower-level items appearing on the pop-up menus. To this end, Windows sends
the WM_INITMENUPOPUP message to the window that owns the menu bar just
before a pop-up is displayed. This gives you the opportunity to change the
appearance of one or more of the pop-up's menu items. You can disable,
enable, check, or uncheck certain items at this point. For example, many
Windows applications with an Edit pop-up menu use the WM_INITMENUPOPUP
message to disable the "Paste" menu item if the clipboard is empty.
WM_INITMENUPOPUP contains the handle of the pop-up menu in wParam and the
index of the pop-up item in the low word of lParam. To use this message to
handle the "Paste" menu item, the following code could be used:
case WM_INITMENUPOPUP :
bTextToPaste = (AppClipboard.iLength > 0);
EnableMenuItem(wParam, ID_PASTE, bTextToPaste);
break;
When the user moves the selection bar over a menu item, whether it's in the
menu bar or in a pop-up list, Windows sends the WM_MENUSELECT message to the
window that owns the menu bar. For atomic menu items, wParam contains the
command identifier associated with that menu item. If a pop-up menu is
attached to that menu item, wParam contains the handle of the menu. In both
cases, lParam contains the current state of the menu item. Its state can be
a combination of MF_BITMAP, MF_CHECKED, MF_DISABLED, MF_GRAYED, MF_HELP,
MF_MOUSESELECT, MF_POPUP, and MF_SYSMENU. These state flags are discussed
later.
If you want to display some help text in a status window when the
highlighted selection bar moves across a menu, use the WM_MENUSELECT
message. An example is shown in Figure 7. People wonder how certain Windows
applications, such as Microsoft Excel, manage to print the help text at the
bottom of the main window as you move the highlight bar over a new menu
selection. It is done in the following way. The WM_MENUSELECT message is
sent to the window procedure of the main window (or to the window that owns
the menu bar), with wParam set to the identifier of the currently
highlighted menu item. You define a structure that maps each menu identifier
to a string of help text and implement a small routine to scan the list of
identifiers sequentially upon receipt of the WM_MENUSELECT message. The
returned string is then displayed in a hypothetical status window, which is
probably located at the bottom of the main window.
The last menu message that you need to know about is the WM_MENUCHAR
message. This message is sent by Windows to the owner of the menu bar when
the user presses a hotkey character that does not correspond to an item on
the menu. If the menu is composed solely of text strings, this message can
be ignored and passed onto DefWindowProc. This message is more useful when a
menu is composed of bitmaps or user-drawn items. Using the WM_MENUCHAR
message, you can associate a hotkey with a bitmap on the menu. For example,
assume that you have a pull-down menu in which each menu item is a color
strip; you can associate each color with a hotkey corresponding to the first
letter of the name of that color. If the user types "B," the menu selection
would be the Blue color strip (see Figure 8).
The Menu API
The Windows menu system has a complete API that allows you to manipulate a
menu in many ways. Functions exist that set and retrieve the text or state
of a menu item; add, insert, remove, and change the text or state of an
item; and create, destroy, and draw a menu. For a demonstration of these
capabilities, examine the MENU program included with the Windows 3.0 SDK.
Besides loading a menu from a resource file, you can also create a complete
menu system on the fly. CreateMenu creates an empty menu bar, returning the
handle to your application. You can then append items to this menu bar
handle. A menu can be destroyed by calling the DestroyMenu function.
DestroyMenu will free the memory occupied by the menu.
If you programmed earlier versions of Windows, you probably relied on the
ChangeMenu function as an all-purpose function to manipulate menus. Windows
3.0 augments the ChangeMenu function with several functions, each of which
performs one of the tasks that ChangeMenu used to perform (see Figure 9).
For the functions that have the nPosition parameter, you can refer to a menu
item by either its zero-based position within the menu or its associated
command identifier. You would logically OR the value MF_BYPOSITION with
the other flags in the wFlags argument if you wanted to refer to the menu
item by its position, and you would use the MF_BYCOMMAND flag if you wanted
to refer to an item by its command identifier.
/*
Get rid of the first menu item
*/
DeleteMenu(hMenu, 1, MF_BYPOSITION);
/*
Attach a new pop-up menu to the ID_EDIT menu item
*/
InsertMenu(hMenu, ID_EDIT, MF_BYCOMMAND | MF_POPUP,
hSubMenu, 0);
Figure 10 lists the possible values for the wFlags argument.
You can enable or disable a menu item by calling EnableMenuItem. A disabled
item does not generate a WM_COMMAND message when selected.
You can place or remove a check mark next to a menu item by calling
CheckMenuItem. Check marks should be placed next to items whose states can
be toggled.
Windows 3.0 permits you to define a custom bitmap to check or uncheck a menu
item. To create these bitmaps, you must first determine what its size should
be to fit on a row of the menu. Starting with Windows 3.0, the
GetMenuCheckMarkDimensions function is provided, which returns the height
and width in pixels that the bitmap should be. StretchBlt can be used to
shrink or expand a bitmap to a desired size, but more on this in a later
article.
Once you have the handles for the two bitmaps, call SetMenuItemBitmaps to
associate them with the menu item.
SetMenuitemBitmaps(hMenu, nPosition, wFlags,
hBitMapCheckOff, hBitMapCheckOn);
If either bitmap is NULL, nothing will be displayed next to the menu item
when the item has a corresponding check state. For instance, if
hBitMapCheckOff is NULL, nothing will be displayed next to the item when it
is not checked.
Owner Draw Items
One nice feature of Windows 3.0 is the ability to draw custom menu items.
For instance, imagine a Change Font pop-up menu in a word processor in which
each item in the menu is drawn in a different font. Windows 3.0 lets you do
this.
One of the state flags associated with a menu item can be MF_OWNERDRAW. This
flag tells Windows that the application will take care of drawing the menu
item. When you create an owner-draw menu item, you can attach a 32-bit value
to the item, which will assist you in the drawing of the item. For example,
in the following call, the last parameter is the special 32-bit value.
AppendMenu(hMenu, MF_OWNERDRAW | MF_BYCOMMAND,
ID_HELVETICA, (LPSTR) ul32bitValue);
This will take the place of the menu item's text string.
When the owner-draw item needs to be drawn, Windows sends two messages to
the window procedure of the window that owns the menu bar. The first
message is WM_MEASUREITEM. The wParam is not used in this message; the
lParam points to a MEASUREITEMSTRUCT data structure (see Figure 11). The
window procedure must fill in both the itemWidth and the itemHeight fields
of this structure in response to the WM_MEASUREITEM message.
The second message sent is WM_DRAWITEM. In this message, the lParam field
points to a DRAWITEMSTRUCT data structure (see Figure 12). This menu item
can be drawn using any Windows graphics function.
Floating Pop-up Menus
As mentioned, Windows 3.0 allows you to create floating pop-up menus. A new
function, CreatePopupMenu, has been added so you can create an empty pop-up
menu. This function is analogous to the CreateMenu function that lets you
create an empty menu bar. CreatePopupMenu returns the handle of an empty
pop-up. Once you have this handle, you can then add menu items by calling
AppendMenu or InsertMenu. Any function that operates on the menu bar will
also operate on a pop-up menu.
Once a pop-up menu has been created, you can position it anywhere on the
screen. To do this, Windows 3.0 adds a new function called TrackPopupMenu.
TrackPopupMenu(HWND hMenu, WORD wFlags, int x, int y,
int cx, HWND hWnd, LPRECT lprect)
The upper-left corner of the menu will be displayed at screen coordinates
x,y. If you want to position the menu in a window at a certain point, you
must use the ClientToScreen function to translate the window coordinates
into screen coordinates. hWnd is the handle of the window that the menu
notification messages will be sent to.
You can create and use a floating pop-up menu with the following code
fragment.
hMenu = CreatePopup;
AppendMenu(hMenu, ....); /* add menu items here */
o
o
o
ClientToScreen(hWnd, (LPPOINT) &pt);
TrackPopupMenu(hMenu, 0, pt.x, pt.y, 0, hWnd, NULL);
DestroyMenu(hMenu);
CreatePopup is called to create an empty pop-up menu, and then AppendMenu is
called repeatedly to add menu items to it. Once all of the items have been
added to the menu, you need to determine the absolute screen coordinates
where the pop-up should be displayed. Suppose you want to have the
upper-left corner of the menu 10 units from the top of the window hWnd and
20 units from the left border. Call ClientToScreen to derive the absolute
screen coordinates from a set of window coordinates.
Then you call TrackPopupMenu to display the menu. At this point, the
second, fifth, and seventh parameters of TrackPopupMenu are reserved by
Windows and must be set to zero. After the user makes a selection from the
menu, you call DestroyMenu to get rid of the pop-up.
Accelerators
Another way for the end user to choose a command, besides picking an item
from a menu, is to press a key combination known as an accelerator. An
accelerator is a pairing of a keystroke and a command identifier. When the
user presses an accelerator key, Windows sends a WM_COMMAND message to a
window.
You can define one or more tables of accelerators in your resource file. The
syntax of an accelerator table definition is shown below.
AccelName ACCELERATORS
BEGIN
event, idvalue, [type] [NOINVERT] [ALT] [SHIFT] [CONTROL]
END
The event field can be one of the following :
o A single ASCII character enclosed in double quotes. If the character is
preceded by a caret (^), the character is assumed to be a control character.
o An integer representing an ASCII character.
o An integer or double-quoted single character representing a virtual
key. The type field must be VIRTKEY. A list of virtual key codes can be
found in the WINDOWS.H file. Virtual key codes usually begin with the prefix
VK_; for example, the virtual key code for the F1 key is VK_F1.
The idvalue field contains the unique command identifier that will be sent
as part of the WM_COMMAND message. The type field can either be VIRTKEY if
you are using a virtual key combination, or ASCII if you are using an ASCII
key.
As you can see, the keystroke can contain any combination of the Control,
Alt, or Shift shift states.
To use accelerators, you must load the accelerator table from the resource
file. This can be done by using the LoadAccelerators function:
HANDLE LoadAccelerators(HINST hInstance,
LPSTR lpszAccelerator)
The first parameter is the instance handle for this instance of your
application. The second parameter is the name of the accelerator table as
defined in the resource file. LoadAccelerators returns a handle to the
loaded accelerator table.
Once the accelerator table is loaded, you must tell Windows to check the
keyboard events to determine whether the user pressed an accelerator key. To
do this, you must add the TranslateAccelerator function to the basic Windows
message loop. The TranslateAccelerator function tries to translate
WM_KEYDOWN and WM_KEYUP events into accelerators. The message loop now looks
like this:
while (GetMessage(&msg, hWnd, 0, 0))
{
if (!TranslateAccelerator(hWnd, hAccel, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
If the msg structure contains a keyboard event, TranslateAccelerator will
search all of the accelerators in the accelerator table hAccel. If the
keystroke corresponds to an accelerator, TranslateAccelerator puts the
window handle hWnd into the msg.hWnd and sends a WM_COMMAND message to
window hWnd. Since you do not have to call TranslateMessage and
DispatchMessage (it has already been done), the program loops back to read
another message.
If you need to find out whether a WM_COMMAND message was sent by an
accelerator key rather than a menu selection, you can examine the high word
of lParam. If it is 1, the WM_COMMAND message was generated by an
accelerator key; if 0, it was generated by a menu item.
Putting It All Together
Now that you have seen code to put up a window, created a resource file with
menu and accelerator definitions, and created an icon for the application to
use when a window is minimized, let's see how to combine everything into one
glorious executable file and run the program. Included with the source code
is a make file that you can submit to the Microsoft MAKE utility to automate
the compile and link cycle.
The files that are needed to develop a Windows application are listed in
Figure 13, and the processes that each file must go through to create the
final executable file are shown in Figure 14. First, the icon is created
using SDKPAINT and written into a binary icon file. This icon file is
referred to in the ASCII resource file. The resource file is compiled with
the "-r" option to create a binary resource file.
The C source code is then compiled into an object file. To compile an
application for Windows, you must include the following switches :
cl -c -Gsw -Zp -AS fileopen.c
The -c switch tells the compiler to compile the code without linking it. The
-Gsw switch disables stack checking and tells the compiler to emit special
Windows code before and after each function that you have declared as FAR.
The -Zp switch packs all structures. Since all structures in Windows are
packed, you must use this switch when writing any Windows application. The
-AS switch indicates that the small memory model is to be used.
Now that the object file has been created, link it with the Windows run-time
libraries to create an executable file. In the past, you had to use a
special linker called LINK4, but if you use Microsoft C Version 6.0, you use
the linker that comes with the C compiler. The Windows 3.0 SDK includes
libraries that completely replace the C run-time libraries that came with
your C compiler. These new libraries provide Windows-aware versions of every
C function.
A special user-written file called a definition file provides additional
information about the linking process to the linker. The DEF file is
required for every Windows application; in it is the name of your
application and a description of it. It may also contain the name of a stub
file, which is a normal DOS executable program that will be run if the user
tries to run the Windows program from a DOS prompt. The SDK includes a
sample stub program called WINSTUB.EXE, which merely displays the message
"This program requires Microsoft Windows." You can also create your own stub
file to do something more elaborate. The linker puts the stub file into the
executable file, so there are really two programs contained in one EXE
file-a Windows version and a non-Windows version. Another requirement of a
DEF file is the specification of the executable header type. For Windows
programs, you must specify "WINDOWS." So far, the definition file looks
like this:
NAME STOCK
DESCRIPTION 'MSJ Stock Application'
EXETYPE WINDOWS
STUB 'WINSTUB.EXE'
You can specify the size of the application's local heap and stack by using
the HEAPSIZE and STACKSIZE statements. Windows gives your application a
default heap size of zero, so if you need to do any kind of local memory
allocation, you should specify some size for the heap. Add the following two
lines to the DEF file:
HEAPSIZE 2048
STACKSIZE 8192
Now you need to tell Windows about the code and data segments in your
application. If memory space is at a premium in your system, Windows will
try to move code and data segments around in memory to reduce memory
fragmentation, and if necessary, discard code and data segments to free up
memory. If Windows discards a segment, it will try to reread it from an
application's EXE file on disk when it needs the segment again. Careful
planning can reduce Windows swapping. Below is what you should use in this
version of the application.
CODE MOVEABLE DISCARDABLE PRELOAD
DATA MOVEABLE MULTIPLE
This tells the linker that the standard code segment is movable in memory
and can be discarded if Windows runs low on memory. The code segment is
loaded when the application is loaded, instead of being loaded on demand.
You also tell the linker that each instance of the application has its own
data segment, and that this data segment can be relocated in memory by
Windows.
You can also tailor the characteristics of each segment in the program by
using the following statement:
SEGMENTS segment name [CLASS classname] [minalloc] [FIXED|MOVEABLE]
[DISCARDABLE] [SHARED|NONSHARED] [PRELOAD|LOADONCALL]
In the last part of the DEF file, you usually define the functions that you
want to export to Windows and the functions that you want to import from
dynamic-link libraries. You generally need to export any functions that will
be called directly by Windows, including the window procedures and callback
functions for dialog boxes and timer messages. This is the export section
for the stock application:
EXPORTS MainWndProc
StatusWndProc
The linker then uses the DEF file to create an EXE file. Finally, use the
resource compiler again to combine the binary resource file with the EXE
file to form a new executable file with the binary resources tacked on. Once
this is done, you have an executable file. To run the program, just enter
the following command:
WIN STOCK
Finishing Up
There are no fancy features in this stock-charting application yet, but at
least I created some generic skeleton code that you can use in all of your
future Windows programming. This version of the program doesn't really do
very much except display menu help items in response to a command being
chosen.
You should now understand resource files, menus, and accelerators. You saw
how menus and accelerators generate WM_COMMAND messages, which inform the
application that the user chose a command. The next article will discuss
child windows and control windows.
ToolBook, an Interactive Development Environment for Windows
Richard Hale Shaw
Programming the Microsoft Windows[tm] graphical environment presents a
formidable challenge to the applications developer. You must be proficient
in C, ready to abandon the traditional, procedural programming you are
accustomed to in favor of the Windows1 message-based architecture, and you
need to master the Windows Application Programming Interface.
ToolBook, by Asymetrix, is one alternative to programming Windows directly.
It allows you to create Windows applications more quickly and easily. It
doesn't require knowledge of C, and you won't have to fathom the depths of
Windows programming to master its interactive development environment
(sophisticated nonprogrammers should find it usable).
Similar to traditional object-oriented programming environments in which
objects encapsulate data and methods, ToolBook includes predefined objects
such as buttons, fields and hypertext hotwords. You can also create your own
objects by drawing them, or by importing bitmapped images. Using ToolBook's
English-like, event-based programming language, OpenScript, you can
associate blocks of code, called scripts, with objects such as push buttons.
Authors and Readers
A ToolBook application is composed of one or more files called books. Books
have one or more pages, which in turn contain the other objects (fields,
buttons, graphics, and so on) that compose a Toolbook application. In this
vein, ToolBook application developers are referred to as Authors, and users
of ToolBook applications are called Readers.
ToolBook has two modes of operation: Reader level and Author level. When a
book is read by a user, ToolBook is at Reader level. At Author level, you
can modify books and objects. The authoring version of ToolBook comes with
BookShelf, a cataloglike book that displays an icon for each book in the
TOOLBOOK subdirectory (see Figure 1). (ToolBook book files always have a TBK
extension.) If you double-click a book's icon, BookShelf will run the
selected book by starting a new instance of ToolBook, which will open the
new book and display the first page. The original instance of ToolBook will
continue to run BookShelf.
A run-time version of ToolBook, which allows you to work only at Reader
level, is included with Windows Version 3.0; this version can be used to run
the DEMO application presented with this article. The sample books
"DayBook[tm]," a calendar and personal organizer, and "Introducing
ToolBook," come with the run-time version.
Readers have access to a subset of the commands made available for Authors;
ToolBook's menu bar changes accordingly. Readers can add pages, search for
text, and save a book with a new name. They can print one of the book's
pages to a sheet of paper or print several pages per sheet; sheets may be
previewed before being printed. Readers may also format text and access the
Windows Clipboard.
The complete authoring package includes an animation primer, books of script
and page ideas, clip art, reference "books" for using Windows dynamic-link
libraries (DLLs) with ToolBook, a hypermedia tutorial, a calculator, and the
BookShelf.
Authors can test their books by bringing up the Command Window and entering
commands manually. This avoids having to go back to Reader level for testing
(see Figure 2).
Authors can use tools and commands to build books, and have access to the
color drawing tools and palettes (see Figure 3). At Author level, ToolBook
displays a status box to show what page you're on and the number of pages in
the book. You can test the modifications you make to a book by switching
back to Reader level. When the application is complete, you can assign a
password to a book to prevent unauthorized access to Author level and avoid
unauthorized opening or saving of the book.
Pages
Every book has at least one page and one background. Each page has a unique
foreground, but can share its background with any other page in the same
book. Backgrounds are like templates. They make it easy to put the same
objects on several pages, since objects displayed on a background have the
same size, style, and position on every page that shares the background.
Obviously, when you change an object in a background, the change is
reflected in every page that uses the background.
Within the foreground and background, ToolBook objects lie in layers with
one object in each layer, similar to a stack of transparencies. You can
manipulate the order of the layers in a foreground or background to overlap
objects.
Objects
ToolBook objects include buttons; fields; record fields; hotwords; imported
or Author-drawn graphics; and group objects, which are several objects
grouped together to form a complex visual image. Pages, backgrounds, and
books are objects as well.
Each ToolBook object has a specified appearance and behavior. When an object
is created, its properties are preset to default values depending on its
type (button, field, or hotword, and so on). For example, all buttons appear
as rounded rectangles with the text label "Button" inside. The values of an
object's properties determine its behavior and appearance. Since the
object's behavior is predefined, all you have to do is determine which
modifications or additions to the default behavior you need for your
application. You can modify an object's behavior by changing the value of a
property, although you don't need to know how an object changes its
appearance or behavior: ToolBook encapsulates this into the object for you.
For example, all ToolBook buttons have a borderStyle property, which
indicates whether buttons are normal, rounded, or shadowed rectangles; radio
buttons; or check boxes (see Figure 4). If you change a button's borderStyle
property to radiobutton, ToolBook will change the object to a radio button.
Text can be added to a page using borderless fields for labels or shadowed
borders for titles; field text can word-wrap and be set to a variety of
fonts and type sizes. Bordered fields are also available for data entry. You
can create push buttons, radio buttons or check boxes and use them as
labels.
Hotwords provide hypertext links to other objects and pages-like push
buttons embedded in a paragraph of text. After you designate a string of
characters as the hotword, ToolBook will move to another page or trigger an
action if the Reader clicks it. Hotwords let you build dynamic text screens,
in which extra information can be given to Readers who need it, without
cluttering the display for those who don't.
Using record fields, you can easily create simple tables of information as
well as data entry applications. Record fields themselves are used only in
backgrounds, whereas contents of fields are stored in the foreground. This
setup allows multiple foreground pages to contain different text for the
same record field. Thus, you can establish a background composed of record
fields that correspond to a table structure, using a different page to
represent each record.
Creating Objects
You can use the built-in drawing tools to create graphical objects called
draw objects, or import them from other programs to create objects called
paint objects (see Figure 3). Palettes are used to create objects within
ToolBook. Objects are chosen from the Tools palette. The Line Palette lets
you control line thicknesses; the Pattern palette offers four pages of color
patterns. A Polygon palette lets you set the number of sides in a polygon.
Two other palettes, the Color Tray and the Spectrum Palette, are used to
color backgrounds, draw objects, buttons, or fields. These palettes include
color icons that display the currently selected stroke and fill colors and a
color swatch that shows how text will look. The Color Tray contains 64
custom colors that you can customize with the Spectrum Palette, with its
more than 16 million shades and tints.
To create an object, you choose an icon in the Tools palette. You can select
buttons, fields, record fields, and hotwords and position them in the
foreground or background. The one exception is record fields, which can only
be positioned on a background. Unfortunately, it's not at all obvious what
some of the icons in the Tools palette represent. You can select a drawing
tool to place lines, arcs, angled lines, curves, regular and irregular
polygons, normal and rounded rectangles, and ellipses on a foreground or
background. When you select a tool, an indicator at the top of the palette
shows you what type of object is currently selected (the indicator will be
set to a pointer arrow if you haven't selected a tool). A zoom tool lets you
magnify the page with four levels of magnification, up to 16x (see Figure
5).
Once you've placed an object, select it to make it the size, style, and
color you want. Size the object by manipulating the square handles at its
corners.
To bring clip art and other Windows bitmap images into ToolBook, all you
have to do is display them with any program that lets you transfer them to
the Windows Clipboard, such as the PaintBrush program included with Windows
3.0. ToolBook, however, limits the size of imported bitmaps to 64Kb. And
once these images have been imported, you cannot resize or modify them as
you can in paint programs.
New pages can be added to a book via a pull-down menu option or Ctrl-N. The
new page is inserted after the current page; both will share the same
background. You can add a new background for the current page via Ctrl-V.
OpenScript
All ToolBook objects have a script property. By default, this property has
no value; that is, objects do not come with scripts already attached to
them. ToolBook scripts are composed in OpenScript. You don't write programs
in OpenScript as you would in C or Pascal, you create a script for any
object whose behavior or appearance needs modification. For instance, to
change a button named "COM1" to a radio button, you could use the following
line:
set borderStyle of button "COM1" to radiobutton
Scripts are the power behind a ToolBook application. Each script contains
instructions that define how an object should respond to a specific event,
such as what to do when a user selects the object with the mouse. You can
create scripts by recording actions you perform with the built-in script
recorder or by writing them with the ToolBook Script Editor. Scripts are
similar to macros, but considerably more powerful and sophisticated. For
instance, besides changing the appearance of an object, scripts can modify
the ToolBook graphical user interface itself, by adding menu items:
add menu "&DDE" at reader
add menuItem "&Retrieve Data" to menu "DDE" at reader
add menuItem "&Send Data" to menu "DDE" at reader
These OpenScript commands add a new menu item, "DDE," to the ToolBook menu
bar, which can be activated via Alt-D. The commands install "Retrieve Data"
and "Send Data" as items in the menu; these items can be selected by
following the Alt-D with R or S. You can hide, gray-out, and check or
uncheck these menu items just as you would in a C Windows program.
An Author can modify a ToolBook object from a script or manually via its
Properties dialog box. An object's properties include its name, visual
style, and optionally, a script. (Buttons and hotwords almost always have
scripts.) Some objects have properties specific to their object type. For
instance, a page includes a page number property; a book includes a caption
or title, the page size and orientation, and passwords; a background
includes color and pattern properties; other objects' properties include
their layer number, size, position, colors, and vertices (see Figure 6).
Some properties, such as a book's password, cannot be set from OpenScript.
The Properties dialog box for buttons and hotwords includes options for
creating links between pages in the same book or different books (see Figure
7). For example, the Link To option lets you chain to another page when a
button is pressed. The Link With option is the same as Link To, except it
automatically creates a button on the destination page to return you to the
original page. After choosing one of these linking options, select the other
page (in the book you're in or another) and ToolBook will automatically link
the objects.
Messages and Handlers
Like Windows, ToolBook is a message-based environment. ToolBook sends
objects messages whenever a particular event occurs, such as when a Reader
double-clicks an object. When it gets a message, the object's script
interprets the message and reacts to it. If a script contains instructions
for handling a particular message, ToolBook executes them.
The object may have built-in behavior that is triggered when that message is
received. For example, if a Reader navigates to a particular page of a book,
ToolBook will send the page an enterPage message. If you click an object on
a page, ToolBook sends a buttonDown message to that object when you press
the mouse button, and buttonUp when you release it. Upon receiving either
message, ToolBook checks to see if the object has a script that prescribes
the object's behavior for that message. If the object doesn't have the
instructions for handling the message (because it has no script, or its
script lacks the instructions for handling that message), the message is
passed up to the next object in the object hierarchy (see Figure 8).
ToolBook processes any messages passed to it that are not handled by any
object in the hierarchy.
Each script is divided into sections called handlers. A handler contains
OpenScript statements that tell ToolBook what actions to perform upon
receipt of a given message. Thus, each handler defines how an object should
respond to a particular event. All statements in a script (except comments)
must be inside a handler. Variables are declared within handlers,
user-defined functions and properties are defined within handlers, and all
other instructions in scripts must be within handlers. There are three types
of handlers: message handlers, "to set" handlers, and "to get" handlers.
Message handlers tell ToolBook what actions to perform when the object whose
script contains the handler receives a particular message. They are the most
commonly used handlers. "To set" handlers define what happens when
statements set a user-defined property. "To get" handlers define new
functions or how to get the value of a user-defined property.
The first statement of a handler begins with a phrase determined by the type
of handler you're writing. A message handler starts with "to handle,"
followed by the message the handler responds to and any parameters. A "to
set" handler begins with "to set," followed by the name of the property the
handler defines, plus any parameters. "To get" handlers start with "to get,"
followed by the property that the handler returns the value of or the name
of the function defined by the handler and its parameters.
Handlers end with either "end" or "end" followed by the message, function,
or property named in the handler's first statement.
The key to mastering OpenScript is learning to use message handlers. This
includes knowing when to use handlers for built-in messages, when to write
handlers for your own messages, and where to place these handlers in the
message hierarchy. You should also learn when certain objects' scripts
should generate these messages themselves.
If a message is received, and an object's script has no handler for it (or
if the object doesn't have a script at all), ToolBook passes the message
from object to object searching for a matching handler. An object hierarchy
dictates the order in which messages are passed among objects. If the
message is not processed by a handler among the objects in the hierarchy,
the message is passed to ToolBook itself.
The object hierarchy is how ToolBook implements object inheritance: the
objects become progressively more abstract as you move up the hierarchy, so
that messages are passed from lower-level objects to higher-level ones.
Adding message handlers not only expands the capability of a book, it allows
you to condense code. If the code in a handler is going to be repeated in
the script of another object, you can write a single handler that will do
the work for both objects. The trick is to place the handler high enough in
the hierarchy to catch the messages each object receives that need
servicing. If the handler is to be used throughout the book, place the
handler in the script of the book itself. A special facility called the
system book lets you trap widely used messages and functions. This is a
special book whose script is available whenever another book or application
is running. Using a system book, you can create global handlers for commonly
generated messages. If you place a common handler in the script of the
system book, you can avoid repeating the same handler in the scripts of
several objects. When the corresponding message is generated, it will be
passed up to the system book and handled there.
OpenScript has a number of built-in messages. These messages include
buttonUp (sent to the object whose area contains the mouse pointer when the
left mouse button is released), enterBook (sent to a book immediately after
it is opened), make (sent to an object just after it is created; this
message is comparable to the Windows WM_CREATE message) and destroy (sent to
an object just before it is deleted; this message is not unlike Windows
WM_CLOSE). Messages that you define must be processed by your own
handlers-if they are passed all the way up the object hierarchy to ToolBook,
an error will occur.
Creating Scripts
OpenScript statements are easy enough to hand-write. OpenScript has no
explicit data types as do C, Pascal, and BASIC. It's similar to the dBASE
language in that data types are implied by the context of the command,
function, or operator being used. However, dBASE commands only work with
certain data types; in OpenScript you do not have to designate a value's
type or what type of data variable it may contain. Also, data variables have
only two forms of scope: locals extend for the life of the handler and
system variables extend for the life of the application.
For example, to initialize a local variable called x in the handler for
buttonUp:
to handle buttonUp
set x to 0
o
o
o
end
OpenScript control structures (see Figure 9) include conditions/where
(similar to the dBASE "do case") and step (like FOR in BASIC or C). Almost
50 functions exist (see Figure 10), primarily to perform mathematical
functions and manipulate strings. More than 60 commands (see Figure 11)
allow you to manipulate data files, navigate a book, control scripts and
messages, modify the user interface and menus, print, and engage in dynamic
data exchange (DDE) conversations with other applications.
Although the OpenScript documentation says that you can create your own
OpenScript functions, these "functions" are really user-defined message
handlers that return a value. Whenever a handler in one object's script
needs to have something done by another object, all you have to do is send
the object a message. You can define your own messages and write handlers
for them in the objects that will receive them, or you can place them in a
system book.
You can also extend ToolBook by accessing Windows DLLs. Any DLL that
contains a published API can be called from ToolBook, including DLLs from
third-party vendors as well as those you write yourself. As an example, a
dBASE file-handling interface DLL is included with ToolBook.
You can create OpenScript scripts with an editor, copy them to the Windows
Clipboard, and paste them into an object's script. Or you can open the
ToolBook Script window and write a script there. The Script window,
available via the Properties dialog box, will check the syntax and compile
an OpenScript script when you exit it. If a syntax error is found, ToolBook
returns you to the Script window and places the highlight on the offending
statement (see Figure 12). You can also test scripts a line at a time using
the ToolBook Command window. In this way, you can enter single OpenScript
commands, or test entire handlers by sending messages to them. In either
case, if an error occurs, ToolBook invokes the Debug window, which shows you
the statement that caused the error. You can also look at the values of data
variables and invoke the Script window to correct the erroneous statement.
You don't have to write every OpenScript script from scratch. As mentioned,
when you use the Link To or Link With options in the Properties dialog box
of a button or hotword, ToolBook creates a script that causes it to jump to
the destination page when you click on the button or hotword. You can also
create scripts by copying a script from one object to another or by using
the Script Recorder.
Script Recording
The Script Recorder translates an Author's actions into OpenScript. Actions
that can be recorded include choosing commands; filling in dialog boxes;
creating, changing, and moving objects; navigating pages; opening books; and
switching to Reader level. When you turn the Recorder off, ToolBook places
the script in a private ToolBook clipboard. To assign it to an object, all
you have to do is paste the clipboard's contents into the object's Script
window. Then test the script by switching to Reader level and clicking the
object. Incidentally, studying a recorded script is a good way of learning
how OpenScript works.
The Script Recorder makes it easy to animate an object. You can make a
script recording while moving the object; the resulting script will move the
object in precisely the same way (see Figure 13). The object's position is
recorded only when it receives a buttonUp. At that point, the Script
Recorder records an instruction to move the object to that absolute
position. As you can see, you'll need to intersperse mouse clicks among the
movements; if you don't, ToolBook will record only the first and last
positions.
This can be demonstrated if you click on the moving fish in the opening
screen of the DEMO book. The fish's script includes a handler for buttonUp.
When you click on the fish, it will loop around the large fish on that
screen, then resume its regular motion from where it left off. The script
for this fish was generated by the Script Recorder.
Visual Effects
A number of ToolBook commands can create interesting visual effects. You can
use the fxDissolve command to dissolve one page into another, or fxWipe to
wipe the screen from one side to another. The fxZoom command lets you zoom
from one page to another; the hide and show commands hide or display any
object. Simple application commands like ask and request let you query the
user with pop-up windows, using a derivative of the WinMessageBox function
in the Windows API. (Of course, you can also call this function directly
since it's stored in a Windows DLL).
You can also draw an object directly from a script. For example, the
following line of code will draw a rectangle from those coordinates on the
current page.
draw rectangle from 1000,1000 to 5000,5000
DEMO Book
I've written a small DEMO book that demonstrates some ToolBook features of
interest to developers. You can download this book from any MSJ bulletin
board. It's a good idea to run the book while reading the article, to see
exactly how some of the techniques work.
After loading the DEMO book, ToolBook sends it an enterBook message,
notifying the book that execution has begun. The handler for this message,
found in the script of the book (see Figure 14), begins by changing the
menu: the menus found at Reader level (File, Edit, Page and Text) are
removed, and a new menu is installed. If the book is run with the run-time
version of ToolBook there will be a new File menu with a single menu item,
Exit. If you are using the authoring version of ToolBook, the level menu
will appear and the Author item will be available.
Changing the menu requires checking the sysRuntime property of the book,
which is set to TRUE if the book is running under the run-time version of
ToolBook (TBOOK.EXE) or FALSE for the authoring version. The sysLockScreen
property is set to TRUE during the menu changes, so the screen changes will
not be visible until they are complete. Note that a handler for the
leaveBook message (which follows the enterBook handler) restores the menu
bar at the Reader level. If you run another book that doesn't restore the
menus and didn't have this handler in DEMO, the Reader menu for the DEMO
book would appear: ToolBook's properties remain set from book to book until
you reset them or close ToolBook and restart it.
Once the menu changes have been made, the enterBook handler alters the
ToolBook main window size to fit the four corners of a VGA screen. This is
done by modifying the bounds property of the main window (addressed as
bounds of mainWindow in OpenScript). When a ToolBook property such as bounds
includes more than one component, each must be addressed individually. Thus,
the following statement sets the horizontal value of the bottom-right corner
of the window to pixel 643.
set item 3 of bounds of mainWindow to 643
The bounds property contains four items, so each must be addressed. You'll
see another example of this in the script for the first page of the DEMO
book, which alters the components of the background's color property.
One other note on the book script: a handler for the user-defined movefish
message controls the motion of the small fish ("MSJFish") that swims around
the screen. This handler defines a system variable, pos, used only by the
handler. The movefish handler uses the pos variable to select a screen
position for the MSJFish. The handler's control structure is the OpenScript
equivalent to a CASE construction in C or Pascal. There's a case for each
value of pos from 0 to 40; when pos reaches 41, it's reset to 0.
The First Page
The first page (see Figure 15) has a script that contains two handlers: one
handles the system-defined IDLE message, and the other handles a
user-defined message, colorchange (see Figure 16). The IDLE message is sent
by ToolBook to the current page when no other actions are occurring. When
the IDLE handler receives an IDLE message, it generates a movefish message
for the handler found in the book script; it sends a colorchange message for
every eight IDLE messages. It does this by counting the IDLE messages in a
system variable, colorcount; when colorcount is 8, the handler resets it to
0 and sends the colorchange message.
When activated, the colorchange handler alters the color of the background
using three system variables; hue, lightness, and intensity. Since ToolBook
initially sets these to null, the handler initializes them for the color red
(where hue is 0, and the lightness and intensity percentages are 10 percent
and 100 percent, respectively). Hue values can range from 0 to 360 with each
color represented in increments of 60; the other two components range from 0
to 100. When a colorchange message is received, the hue is incremented to
the next color, lightness (whiteness) is increased and intensity is
decreased. As these values reach their limits during subsequent calls to
this handler, they are reset. The background color is altered with the
following OpenScript statement, which changes the fillcolor property of the
current background.
set fillcolor of this background to hue, lightness,
intensity
A Reader moves to the next page in the book by clicking on the button at the
bottom center of the page. The script of this button contains a handler for
the buttonUp message, executing the following statement:
fxDissolve fast to page "Changing Properties"
The fxDissolve command allows you to dissolve the current page to black,
white, gray, or another page. The speed parameter (slow, normal, or fast)
controls the rate of the dissolve action. This statement makes the entire
page appear to fade away exposing the next page, which seems to be
underneath.
The Second Page: Changing Properties
The Changing Properties page demonstrates ToolBook's built-in objects;
specifically, what happens when you change an object's properties (see
Figure 17).
A Reader can change the color of the rectangle in the upper part of the
screen by adjusting any of the three scroll bars, then clicking the Apply
button or the rectangle itself. When the Apply button or the rectangle
receives a buttonUp message, they generate a changeboxcolor message, handled
in the script of the current page. (The page sends itself a changeboxcolor
message when it receives an enterPage message from ToolBook.) Upon receiving
this message, the handler (see Figure 18) sets three variables to the
appropriate hue, lightness, and intensity values and sets the fillcolor
property of the rectangle.
The color values are derived from the scroll property of each of the scroll
bars displayed on the page. These are not true scroll bars, because
unfortunately ToolBook does not offer a scroll bar object. If you want a
scroll bar, you have to use a field with scroll bars, contracting the width
of the field until only the scroll bar remains. Unfortunately, while fields
can receive buttonUp messages, their scroll bars will not pass them on.
Therefore, a scroll bar will respond to mouse messages by adjusting itself,
but since it does not pass on a buttonUp message, a script cannot detect
when the scroll bar has been clicked. This is why you cannot see dynamic
color shifts taking place on the rectangle as you change the scroll values
and why you have to click the Apply button for the color change to take
effect. The lack of a scroll bar object is a serious deficiency in ToolBook.
Another object of interest on this page is the ChangeButton, which
dynamically changes its borderstyle property and text when a Reader clicks
on it. It will alternate rounded, shadowed, radio button, rectangle and
check box styles and adjust its text from Rounded Button to Shadowed Button.
The button's script (see Figure 19) uses a system variable and a conditions
structure to increment the variable and reset it in order to alternate
button styles.
There are several hotwords on the page that can be identified by their
italic text style and by changes in the mouse pointer's shape as it passes
over them. When clicked, these hotwords trigger message boxes that give you
more information about how this page works.
Three buttons at the bottom of the page allow you to navigate between pages.
The first lets you dissolve back to the opening page. The second uses the
fxWipe command to make it appear as if the page is being turned (from right
to left) to the database demo page. This is accomplished with the following
statement in this button's buttonUp handler:
fxWipe left fast to first page of background "Data Entry"
The third button uses the fxZoom command to zoom to the animation demo:
fxZoom fast to page id 13
This command makes the target page appear to explode from the center of the
current page outward.
The Database Demo
You click the second button, Database Demo, to move to the Data Entry screen
(see Figure 20). This screen is a background with several record fields and
buttons and one or more pages, sorted by the contents of the last name
field. You enter name and address information into the current page,
according to the fields in the background. The buttons are used to add a new
blank record, delete the current record, and navigate to the previous and
next records. Here, a record is synonymous with a page: to add a blank
record, you only have to add a new page over this background. Similarly,
deleting the current records destroys the current page. The background also
maintains a record number field.
If you look at the background's script, you can see how it works (see Figure
21). The background receives an enterBackground message (see its
enterBackground handler) when records are added or deleted, or when a Reader
navigates from record to record. Then it sends an updaterecnum message to
update the record number field. The value displayed in the record number
field is the value of the current page's pageNumber property minus three.
(There are three pages in front of the first page of this background.) Since
the Data Entry background is at the end of the book (it follows the
background for the animation demo discussed below), you can add new pages to
this background.
If you study the updaterecnum handler, you'll see that it retrieves the
current page's pageNumber property into a variable called "it." In many
object-oriented languages, "it" is a variable that is globally available at
all times: you don't have to declare "it." In the script, "it" is used to
place the page number (minus 3) into the text of the record number field.
The buttons and icons at the bottom of the background have relatively simple
scripts in order to avoid unnecessary repetition of code used by each
button. Each button-icon pair has a buttonUp handler that sends a message
indicating what type of operation must take place: Add, Delete, Previous,
and Next. For example, the Delete button and the Trash Can icon have the
same script:
to handle buttonUp send deleterecord end
Each button-icon pair have the same script, but each pair sends a unique
message.
The handling for Add, Previous, and Next is relatively simple. To add a new
record (or page), the addrecord handler in the background script sends a
newPage message to the page, causing ToolBook to create a new blank page,
insert it after the current page, and navigate to the new page. When this
happens the recordfields on the background remain, but the data they contain
is blank (since the page is blank). The handler follows by sending an
updaterecnum message to update the record field.
As for Previous and Next, the handlers for previousrecord and nextrecord use
the go command, as shown below.
go to next page of this background
This is the key statement in the handler for nextrecord. The previousrecord
handler contains the word previous in place of next. These handlers finish
by sending an updaterecnum message to the background.
The most complex handler in the background's script is the deleterecord
handler. It begins by determining if only one page remains on the data entry
background. If so, it sends a newPage message to the current page. This
places the new page after the current one, so it navigates back to the
original page. From there the delete operation can proceed uniformly: the
page is selected and sent a clear message, making the page delete itself.
Then the script navigates back to the new page (now page 1 of the
background) and an updaterecnum message is sent. Note that the Reader sees
only the results of the operation, since sysLockScreen is set to TRUE at the
beginning of the operation and reset to FALSE afterwards.
You can leave the database demo by pressing the appropriate button, which
uses fxWipe to turn the page back to the right to return to the Changing
Properties page.
Animation Demo
The animated fish tank is controlled by the script of Page Three (see Figure
22). The script uses the IDLE message to move the objects continually, and
sends a message for each object that's to be moved (see Figure 23).
All the fish move along a calculated track in the direction they are
pointing. After they move offscreen, they reenter the screen from the
opposite side. The seaweed moves in sequence between two positions, so that
it looks like it's waving in the current. The starfish crawls between four
positions.
Creating the Images
The bitmapped images used in this book were imported from bitmap drawings.
The only exceptions are the backgrounds used in the animation demo (which
were drawn with ToolBook) and the icons used in the data entry demo, which
came from a variety of sources. The Trash Can icon was lifted via the
Clipboard from an old Windows 2.0 application.
Importing these images and having them display properly in ToolBook wasn't
easy. The images were originally drawn transparently on a white (empty)
background in a paint program. Once imported, colors in the background or
colors in other objects would "bleed" through the white areas of the
imported image.
The solution was to create a white background for each image by tracing an
irregular polygon around it (see Figure 24). If you try this with the
authoring version of ToolBook, first draw the polygon transparently so you
can see what you're tracing. Then the polygon's transparent property is
turned off (via the Draw pull-down), and filled with white. The new polygon
is placed behind the bitmap. Because every new object that you add to a page
or a background is inserted in a layer above the topmost object, the easiest
way to do this is to cut the image itself (not the white backdrop) to the
Clipboard, and paste it back onto the page over the polygon backdrop.
Creating an object group from the two objects fuses them together. The group
can subsequently be moved or superimposed over other objects and backgrounds
without losing the image's integrity.
Pasting images seems to fragment ToolBook's heap, so that it may generate a
message indicating that the page or background is full. Therefore, make sure
that you do not have your only copy of an important bitmap in the Clipboard.
Save the book to disk and then paste the image. If, after this, you still
get the "full" message, you may have to omit some objects from your
background or page.
Some of the images planned for the animation demo could not be pasted into
ToolBook. The large MSJ fish on the first page would simply not fit:
ToolBook currently limits the size of imported bitmaps to 64Kb each. This
image had to be bisected and pasted in two pieces. One piece is in the
background and the other is lined up next to it on the foreground. If you
have the authoring version of ToolBook, you can observe this by switching
from foreground to background while in Author mode.
Conclusion
To run ToolBook, you'll need the minimum requirements of Windows 3.0 in
protected mode, namely: an 80286 or 80386-based PC, DOS Version 3.1 or
later, 640Kb of RAM plus 256Kb extended memory, a hard disk with 2 to 8Mb
available, a mouse, a VGA or EGA monitor, and of course, Windows 3.0. As an
Author, you'll be better off throwing as much firepower at ToolBook as
possible: at least a 16MHz 386 with 2Mb or more of RAM. ToolBook is not
terribly slow, but it's not terribly fast, either. To increase its speed,
you might not want to force ToolBook to do some types of graphics redisplays
in your books. (Unfortunately, some people have dismissed ToolBook's speed
because of DayBook, which makes heavy use of graphics and is probably more
complex than many ToolBook applications will be.)
ToolBook is currently most suitable for prototyping Windows programs,
writing on-line help systems and training facilities, creating
presentations, and integrating text and graphics. ToolBook does suffer from
problems with speed and it cannot manipulate complex graphics images easily
(at least, not without a few workarounds). ToolBook lends itself well to
designing training packages and courseware: information windows and panels
can be tied to objects that cause a window to appear when the Reader clicks
on them. This would be ideal for programmed learning systems. You might
create books that teach or supply specialized information, or create
specialized financial tools, in which bar charts change as data is received
through DDE. ToolBook should also be considered to create front-ends for
popular database systems. Even a process control application would be
impressive under ToolBook: users who don't even know how to use a computer
could control an assembly line using the mouse and animated graphics. It
points the way to entirely new kinds of applications and application
interfaces.
1 For ease of reading, Windows refers solely to the Microsoft Windows
graphical environment and is not intended to refer to such products
generally.
Adapting the Parallel Port for Bidirectional Communication
Ross M. Greenberg
You probably don't consider the lowly parallel port as versatile as its
cousin, the serial port. It has been relegated to handling only the most
mundane of tasks-printing, plotting, perhaps an analog control function now
and then, sitting lonely the rest of the time. The port is powerful, but
there's only so much you can do with a unidirectional port, right?
Not once you realize that the standard parallel port is actually
bidirectional. In fact, if you want to, you can input or output on as many
as twelve lines at once. Compare that to the serial card, which can only
input or output on one line, one bit at a time. A parallel port can output a
byte and a half in the time it takes a serial port to determine whether it's
time to cause a singular transition.
Of course, the RS-232 serial port is the standard when it comes to
communicating with modems, WANs, and so forth, and a large variety of
software that supports serial communication is available. Why, then, try to
use the parallel port for the sort of communication usually associated with
the serial port, other than the programming challenge?
First, communication doesn't always need to be performed in ASCII-sometimes
it's as easy as turning on or off a wire to a relay, or reading the on/off
condition of a sensor. Using serial I/O would be overkill in these cases:
it's sometimes easier to connect the real world to the parallel port.
Second, since the parallel port is able to get multiple bits in or out
simultaneously, it should be able to provide faster I/O than a serial port.
Special code will be needed to take advantage of this bidirectional
capability. First, however, you have to understand the hardware.
Hardware
The parallel port is one of the simplest components of your computer (see
Figure 1). There are four distinct parts. The address decoding portion is
activated when a specific address is on the bus. The write logic takes the
data on the bus and gives it to the third part, the physical input/output
portion. The fourth part makes the data from the input/output portion
available to the bus when it is requested.
Four addresses enable the parallel port, together with the or bus lines.
These lines indicate that a port input or output operation is taking place
on the bus. Normal memory reads and writes do not set these pins, so the
card ignores those types of operations.
The various combinations of wires allow for reading and writing the data and
status lines on the DB25 connector. The characteristics of the output chips,
the 374 for the data output and the 174 for the control signal output, cause
the last value output to them to be destructively OR'ed with the data being
read. This means that before data is read in from the corresponding 244 and
240 chips, a logical zero must be sent out the data and control ports.
The diagram in Figure 1 shows that as many as 12 outputs can be made
available to the DB25 at a given time, and that all 17 active pins (the
other 8 are grounded) can be used for input of one form or another. Clearly,
there is a shortage of output, not input, on the parallel adapter-not what
you were expecting!
Let's examine what happens if you attempt to connect the parallel ports of
two PCs. Not all parallel adapters were created equal. When two adapters are
fighting for simultaneous control of those data lines (Pins 29) often one of
them will win out over the other, making the data read on those lines
unreliable. The solution seems to be to wire the data output lines from one
PC to the control input lines of the other, utilizing eight of the nine
wires connected to Pins 1 and 1017. But deciding which wire to connect to
each pin is a problem. Whereas the serial port is synchronized on a
character-by-character basis with start and stop bits, the parallel port is
not synchronized with anything, unless you explicitly synchronize it.
Whatever those eight data lines are attached to, the data made available on
them will be available at slightly different times. Also, the receiving
machine must be told at some point that data is ready on those lines. That
seems easy enough to do: you simply raise a signal on some line to tweak the
remote machine when data is available. Since the parallel port has its own
interrupt, IRQ7, using it for this purpose seems to be the logical way of
doing things. You simply trigger an interrupt when data is available, and
let the remote machine's interrupt service routine handle the rest.
Unfortunately, this method would need two pins: the output line that you'd
tweak would be connected to the ACK pin in the other machine that causes an
interrupt to occur. If you exclude the data output pins from the potential
pool of pins available for outputting the data ready signal (since they're
already in use for the data), that leaves us with nine pins. Eight of these
will be used for input from the remote, leaving only one free pin. You're
one pin short of what is needed. If only the original design allowed for the
ACK control bit to be a bidirectional one!
If polled I/O is used instead of interrupt-driven I/O there isn't any
problem: the full eight bits are transferred across at once, and strobing of
a "data ready" pin can be accomplished through one of the bidirectional
wires, such as Pin 1. Consider, though, how the data is actually
transmitted: a cable allowing for bidirectional 8-bit transfer requires
eight wires in each direction for the data, a common ground wire, and a
means of communicating the "data ready" condition distinct from the "data
received" condition. Once again, a problem can arise because one parallel
printer adapter is a little stronger electrically than another. Not all Pin
1's were created equal; and there is no guarantee that a bidirectional port
can be simply hooked up with a straight wire to another one. Eight-bit data
transfer might be out of the question.
What's a programmer to do? The answer is, split the byte in half and send
one nibble at a time. If you take a look at the two input registers, the
status and input ports 3BDH & 3BEH on a monochrome display/printer adapter
card (379H and 37AH on a simple parallel printer adapter), the answer starts
to become obvious (see Figure 2). A read on 3BEH/37AH allows for 4 bits of
input directly from the pins (Pins 1, 14, 16 and 17) and an additional bit
showing the interrupt enable state: not enough to have a control bit, alas.
The status port, however, has 4 bits of data plus the ability to raise or
lower a control line. Looks like the right port for the job.
Now, however, another problem arises: each byte is going to be sent in two
pieces, but each piece must still be synchronized. There is no guarantee
that what appears to be a simultaneous output on five lines will be received
as such on the remote site. A protocol must be established that permits a
machine, regardless of clock speed, to determine when the data on its data
lines is legitimate. Without timing loops, it seems that transition of the
control pin from high to low and low to high can be used for this purpose.
To demonstrate this technique, as well as the parallel port's capability for
rapid transfer, SENDFILE.C was created (see Figure 3). All of the actual
send/receive routines can be found in PAR_POLL.ASM (see Figure 4). On a 386
at 16MHz, a sustained transfer rate of over 35K bytes per second was
measured. SENDFILE has successfully transferred files between AT- and
MCA-style parallel ports.
This is the algorithm used in PAR_POLL.ASM:
Sender
1. Send the high nibble, after setting the high fifth bit, the control bit.
Receiver
1. Stay in a tight polling loop until the high bit, or control bit, on the
input control port is on.
2. Acknowledge that nibble by setting the high bit appropriately.
Sender
2. Wait for the remote site to acknowledge the nibble by raising its high
bit on the input control port, BASEPORT + 1. A tight polling loop is best
for this.
3. Output the second nibble of the byte after turning off the high bit.
Receiver
3. Stay in a tight polling loop while waiting for the control bit to go low,
indicating the second nibble is ready.
4. Acknowledge the last nibble of the byte by turning off the high bit and
outputting a dummy byte to the remote system.
5. Process the character just received.
Sender
4. Wait for the remote site to acknowledge receipt of the second half of the
byte by dropping the high bit read from the sender's input control port.
5. Repeat the process.
This scheme has a number of advantages. First, because 3BDH/379H is used for
both data and the control bit, the number of I/Os needed to accomplish a
single byte's transfer is reduced. Only two inputs and two outputs are
required of 3BDH/379H to get the two nibbles from the receiver's viewpoint,
and only four I/Os are needed to get the nibbles and control information out
from the sender's viewpoint.
If the control pin used is Bit 6 of 3BDH/379H (Pin 10), interrupt processing
can be used. This allows a TSR to read the initial nibble regardless of
other processing in the foreground and potentially emulate BIOS service 14H,
which is used for serial communications. A disadvantage is that having a
control bit in the middle of a nibble makes processing two nibbles into a
byte slightly more difficult.
If a buffered I/O routine is needed, one that can store data as it arrives
and return it only when requested, it might be implemented as a TSR.
Interrupt processing is perfect as a background task for TSRs. The code must
be changed substantially, however, and making it into a virtual BIOS service
replacement for interrupt 14H is beyond the scope of this article.
Figure 5 shows PAR_INT.ASM, an interrupt-driven parallel routine. The logic
of an interrupt-driven routine differs greatly from the logic of a polled
system. Interestingly enough, due to the high overhead incurred by the
interrupt-driven routine, the throughput is substantially lower. Each
character transmitted requires an interrupt to be generated on each system (
both the sender and the receiver) and some polling must take place, as well.
(A hybrid system that generates only a single interrupt on the receiving
system is possible, too, but would be less fun to write.) As an example of
interrupt-driven I/O, PAR_INT.ASM demonstrates simple keyboard buffering
techniques. It stores data in a buffer, then sends it when told to do so
simply by initiating the first nibble, and letting interrupts handle the
rest. This is the logic for the interrupt routine.
Sender
(send_byte subroutine)
1. Remove a character from the transmit circular buffer.
2. Take the low nibble of the character, and insert a logic one between the
third and fourth bits of the nibble, left-shifting the fourth bit into the
fifth bit position. Because the hardware will reverse the topmost bit,
reverse it now. Send the byte out the port. Return to the calling
subroutine.
Receiver
1. Upon receiving the interrupt caused by the fourth bit being held high,
get back the original low order nibble.
2. Generate an interrupt on the Sender side of the wire by outputting a high
fourth bit.
Sender
3. Upon receiving the interrupt, if in the middle of a send, vector off the
main interrupt routine.
(second_nibble_routine)
4. Send the final nibble of the character. This time, though, insert a zero
bit in the fourth bit position to signal the remote system that it may
process the second nibble.
Receiver
3. Wait for the fourth bit position to clear on the port.
4. When clear, build the final nibble for the character, build the character
and then stuff it to the input circular buffer.
5. Return from interrupt.
Sender
5. If there are more characters in the transmit circular buffer, get the
next one and call the send routine.
6. IRET back after clearing interrupts.
A simple transfer program could be grafted on to PAR_INT.ASM, but be
careful! The timing on this system bodes ill for bidirectional
interrupt-driven polling I/O.
Summary
The standard PC parallel port could have been designed a little more
carefully. If it had been, you could have gotten true 8-bit I/O, and
potentially simultaneous bidirectional I/O, at an extraordinary throughput.
Ideally, the output of a byte would require two port output and one port
input instructions (output the data, strobe the port, read the ACK), and the
receive would require one input and one output instruction (interrupt
generated on the strobe, one read of the data, one output to acknowledge the
data). However, the routines presented here are not substantially more
resource-intensive, requiring a few more IN and OUT instructions and a
carefully constructed low-level protocol.
In my environment, where all of my serial ports are used up by modems,
serial printers, and other devices, it's nice to be able to transfer files
quickly through my unused parallel ports. Adding a few lines of code, you
should be able to transfer files in the background on each machine, access
remote devices transparently (including remote printing and printer
sharing), and perhaps even use a remote machine's hard disk as a hot back-up
disk.
Questions & Answers - Windows
Q:
I'm doing a series of grouped CreateCompatibleDC, CreateBitmap and
SelectObject calls to create many relatively large, concurrent bitmaps,
followed by the proper symmetric series of SelectObject, DeleteObject and
DeleteDC calls. Everything works fine in standard and 386[tm] enhanced
modes, but in real mode the behavior is erratic. Sometimes the CreateBitmap
calls succeed and other times they fail, without any apparent pattern. I
know it's a memory management problem, but I do not know specifically what
it is and how to solve it.
A:
The sequencing of your calls sometimes causes problems. Your sequence, in
abbreviated form without error checking, is:
auto HDC ahDC1, ahDC2;
auto HBITMAP ahBM1, ahBM2;
ahDC1 = CreateCompatibleDC(...);
ahBM1 = CreateBitmap(...);
ahBM1 = SelectObject(ahDC1, ahBM1);
ahDC2 = CreateCompatibleDC(...);
ahBM2 = CreateBitmap(...);
ahBM2 = SelectObject(ahDC2, ahBM2);
o
o
o
ahBM2 = SelectObject(ahDC2, ahBM2);
DeleteObject(ahBM2);
DeleteDC(ahDC2);
ahBM1 = SelectObject(ahDC1, ahBM1);
DeleteObject(ahBM1);
DeleteDC(ahDC1);
Whenever a large bitmap is created, it is a GMEM_MOVEABLE global memory
object. Once that bitmap is selected into a DC, it becomes locked. Your
problem is that you are "sandbarring," or fragmenting memory in real mode by
selecting your bitmaps into DCs, thereby locking them, before you really
need to. This makes it increasingly unlikely that there will be chunks of
contiguous free memory large enough for creating subsequent large bitmaps.
Because of the somewhat random state that your global heap is in at the
time, the behavior is erratic.
Rearrange your code to read as follows:
ahDC1 = CreateCompatibleDC(...);
ahDC2 = CreateCompatibleDC(...);
ahBM1 = CreateBitmap(...);
ahBM2 = CreateBitmap(...);
ahBM1 = SelectObject(ahDC1, ahBM1);
ahBM2 = SelectObject(ahDC2, ahBM2);
o
o
o
ahBM2 = SelectObject(ahDC2, ahBM2);
ahBM1 = SelectObject(ahDC1, ahBM1);
DeleteObject(ahBM2);
DeleteObject(ahBM1);
DeleteDC(ahDC2);
DeleteDC(ahDC1);
You should probably also be doing a GlobalCompact((DWORD)-1L) before these
calls to maximize the amount of memory in the global heap and allow
Windows[tm] to clean it up. It is best to create your bitmaps (or any global
memory object, for that matter) in order from largest to smallest memory
requirement to increase the likelihood of getting all the global memory you
need.
────────────────────────────────────────────────────────────────────────────
Q:
My company has quite a few old 8MHz IBM PC/ATs, on which we run
Windows 3.0 in standard mode. We run a lot of DOS applications concurrently
with Windows and then access them through the Alt+Tab key combination.
Because of these DOS applications, we have set up large RAM disks for the
temporary files. Our problem is that when we Alt+Tab to a DOS application,
the first few keys subsequently hit get lost (that is, the application does
not receive them). Is this a bug in Windows?
A:
This effect is due to the way that Windows displays the standard mode DOS
applications to the user, and it is not a bug.
In standard mode, whenever you Alt+Tab to a DOS application from the Windows
"screen group," Windows needs to save the contents of low memory (memory
below 640Kb) to a hidden read-only file. This file, called ~WOA0000.TMP, is
in your temporary subdirectory. While Windows is saving the contents of
memory, the screen blanks out. After the memory is saved, Windows can then
reload low memory with the DOS application. It does this from another hidden
read-only file named ~WOAXXXX.TMP, where each X is a random hexadecimal
digit. This file is basically a memory image of the DOS application and its
video buffer as it existed when you last left DOS.
Windows next reloads the video buffer as it existed for the DOS application,
then reloads the roughly 500Kb of low DOS memory for the application. Next
Windows resets the current disk and directory for the DOS application.
Finally, Windows resets all the interrupt vectors set by the DOS application
and any TSRs that exist. As a result, you see the DOS application's screen
before the application itself is loaded and able to react to any keyboard
interrupts.
This is more pleasant visually, but it makes you think that something is
wrong when the application does not immediately react to any keystrokes. All
you can do is wait until the application is completely loaded and the
interrupt vectors are reset.
When you Alt+Tab out of a DOS application, the reverse happens. There is a
slight delay as the video buffer is saved. Windows then displays the
"Switching..." message while it saves the low memory containing the DOS
application. Then it reloads the next application in the Alt+Tab chain.
If you are running many DOS applications and Alt+Tab'ing between them, you
can hold the Alt key down during the "Switching..." message, press Tab until
you get to the DOS application name you want, then release the Alt key. This
saves you from having to go back to the Program Manager or Task Manager to
get to the next DOS application you need.
Q:
I'm writing an MDI-style application, where the focus may be on
one of many windows. In a multiple-window application, what's the easiest
way to check for messages across all windows without having to add code to
each WndProc? I want to monitor and display the state of the Num Lock, Caps
Lock and Scroll Lock keys, as is done in Microsoft Excel and Microsoft Word
for Windows. I'd prefer to use an object-oriented method, so that I don't
have similar code (or function calls) in numerous WndProcs. Microsoft Excel
and WinWord seem to monitor both WM_MOUSEMOVEs and WM_KEYDOWN/WM_KEYUPs, but
don't always show a changed key status unless they are the active
application.
A:
The easiest way to check all your messages, no matter which MDI window has
the focus, is to use a modified main GetMessage loop, as shown in the
following example.
auto MSG aMsg;
while (GetMessage(&aMsg, 0, WM_NULL, WM_NULL))
{
switch (aMsg.message)
{
case WM_XXXX:
o
o
o
continue;
// do not Translate and Dispatch
case WM_XXXX:
o
o
o
break;
// do normal Translate and Dispatch
}
TranslateMessage(&aMsg);
DispatchMessage(&aMsg);
}
Although this will allow you to monitor all the messages that go through the
normal application message queue, you will not see messages here that are
sent directly to your various WndProcs. Many common Windows messages are
sent directly to the WndProcs. As an example, a WM_ERASEBKGND is sent
directly as a result of a WM_PAINT BeginPaint call, so you could not
successfully switch on it in the above code.
For monitoring the Lock keys, consider setting a systemwide hook with
SetWindowsHook. Unfortunately, this must be done in a DLL and is
complicated. This is probably why Microsoft Excel and WinWord appear to
monitor only those WM_MOUSEMOVEs and WM_KEYDOWN/WM_KEYUPs that come to them,
and therefore do not accurately reflect the state of the Lock keys.
Incidentally, when you study how these two applications operate, the Lock
keys code they use appears to be very different.
The best way to monitor the Lock keys is to simply do a SetTimer for your
main WndProc for, say, every 125 milliseconds. This will allow you to
represent the state accurately, whether or not you are the active
application. On the WM_TIMER message, see if the state of each key
(VK_NUMLOCK, VK_CAPITAL or VK_SCROLLLOCK) has toggled with:
(GetKeyState & 0x00FF).
If it has, redisplay the state appropriately. (Note: VK_SCROLLLOCK should be
defined as 91H, because that key code is currently undocumented and not in
WINDOWS.H.)
Q:
I can easily prevent multiple copies of a Windows application
from running by coding the application so that it checks the WinMain
PrevInstance argument. How do you prevent multiple copies of a regular DOS
application from running concurrently in Windows? My goal is to protect a
dedicated file from being corrupted by accidentally running two instances of
the DOS application.
A:
There is no direct way of doing this within Windows. You can, however,
accomplish this indirectly by using a BAT file to test for the existence of
a "semaphore file" before running your DOS application under Windows. A
semaphore is a signaling apparatus; in other words, a fancy name for a flag.
Assume your DOS application is named DOSAPP.EXE and that it resides in your
C:\DOSAPP subdirectory. Create a C:\DOSAPP\APP.BAT file with either Notepad
or your editor as follows:
@echo off
c:
cd \dosapp
if exist %temp%\dosapp.sem goto sem
echo %0 >%temp%\dosapp.sem
dosapp %1 %2 %3 %4 %5 %6 %7 %8 %9
erase %temp%\dosapp.sem
goto end
:sem
echo DOSAPP is already running!
pause
:end
In the Program Manager, select the group that the application should be in
or is in, choosing File New for the Program Group or File Properties on the
Program Item. Instead of specifying C:\DOSAPP\DOSAPP.EXE in the Program Item
Properties dialog box, set the Command Line to C:\DOSAPP\APP.BAT. In this
example, you could also specify up to nine arguments (%1 to %9 in the BAT
file) on the same Command Line.
The above BAT file assumes that you have set an environment variable in your
AUTOEXEC.BAT named TEMP indicating a subdirectory where temporary files
should go, although it will still work without the TEMP variable. Ideally
the subdirectory would be on a RAM disk, created with the RAMDRIVE.SYS
program that comes with Windows. If TEMP points to a subdirectory on your
hard disk (say C:\TEMP), you should also include the following line after
the SET TEMP=C:\TEMP as a "reboot" handler in AUTOEXEC.BAT:
if exist %temp%\*.sem erase %temp%\*.sem
The two lines in the BAT file that move you into C:\DOSAPP (c: and
cd\dosapp) are necessary only if you do not put the BAT file in C:\DOSAPP.
They ensure that the current subdirectory is set properly before DOSAPP.EXE
is executed, just as Windows does if you had specified C:\DOSAPP\DOSAPP.EXE
on the Command Line. When a path is specified on the Command Line, Windows
changes into that subdirectory before executing the EXE or BAT file. If the
BAT file is in the same subdirectory as the EXE file, it should not have the
same filename since EXE files are executed before BAT files.
It would be more elegant to be able to use an environment variable as a
semaphore instead of the file to block the second instance, but this will
not work. Each EXE (Windows or DOS) or BAT file inherits the environment
that exists at the time Windows is started. Setting an environment variable
in the BAT file only applies to that specific invocation of the BAT file, so
a second execution of the same BAT file would not "see" the environment
variable set in the previous one. Unfortunately, the only common changeable
"environment" in Windows is WIN.INI, and DOS sessions cannot access it.
Another way to protect your files from multiple updates, although not
foolproof, is to execute the DOS SHARE.EXE program in AUTOEXEC.BAT. This
recommendation is documented in one of the "read me" files that come with
Windows, and is often overlooked.
Q:
What are the recommended default sizes for the predefined
window/control classes (Button, ComboBox, Edit, ScrollBar and Static)? I
want to make sure my controls adhere to the standard. Also, how would these
sizes be determined at run time?
A:
The width and height of the predefined window control classes is based on an
average character being 4 logical units wide and 8 logical units high. The
actual values for a unit then depend on the resolution and aspect ratio of
the display, as evidenced by the size of the default system font. Based on
this relative concept and these values, use the following table:
■ Class
Style
Width
Height
■ Button
BS_CHECKBOX
12
■
BS_AUTOCHECKBOX
12
■
BS_3STATE
12
■
BS_AUTO3STATE
12
■
BS_PUSHBOX
14
■
BS_PUSHBUTTON
14
■
BS_DEFPUSHBUTTON
14
■
BS_RADIOBUTTON
12
■
BS_AUTORADIOBUTTON
12
■ ComboBox
CBS_SIMPLE
12
■
CBS_DROPDOWN
12
■
CBS_DROPDOWNLIST
12
■
CBS_OWNERDRAWFIXED
17
■
CBS_OWNERDRAWVARIABLE
17
■ Edit
all
12
■ ScrollBar
SBS_HORZ
9
■
SBS_VERT
9
■ Static
SS_LEFT
8
■
SS_CENTER
8
■
SS_RIGHT
8
■
SS_SIMPLE
8
■
SS_LEFTNOWORDWRAP
8
These values can be used directly in the Dialog Editor. If you need to
convert these logical values to pixels on a particular display at run time,
use the following code:
lDlgBaseUnits = GetDialogBaseUnits();
AveHeight = HIWORD(lDlgBaseUnits);
AveWidth = LOWORD(lDlgBaseUnits);
You can then use the AveWidth and AveHeight to calculate the actual pixel
values for a standard window/control size by multiplying them by the logical
values from the table and dividing by the logical units of the width and
height.
aiButtonCheckboxDefH = AveHeight * 12 / 8;
aiButtonPushboxDefH = AveHeight * 14 / 8;
aiButtonPushbuttonDefH = AveHeight * 14 / 8;
aiButtonRadiobuttonDefH = AveHeight * 12 / 8;
aiComboboxNormalDefH = AveHeight * 12 / 8;
aiComboboxOwnerdrawDefH = AveHeight * 17 / 8;
aiEditDefH = AveHeight * 12 / 8;
aiScrollbarHorzDefH = AveHeight * 9 / 8;
aiScrollbarVertDefW = AveWidth * 9 / 4;
aiStaticTextDefH = AveHeight * 8 / 8;
────────────────────────────────────────────────────────────────────────────
Volume 5 - Number 6
────────────────────────────────────────────────────────────────────────────
Port Windows[tm] Applications to OS/2 (Almost) Painlessly with the Software
Migration Kit
Eric Fogelin, David Wood, and Noel Bergman
Porting applications from the Microsoft Windows[tm] graphical environment to
OS/2 Presentation Manager (hereafter "PM") has just become much simpler.
Microsoft has created a set of software tools, called the Microsoft Windows
to OS/2 Software Migration Kit (SMK), which acts as an extension to the
Microsoft Windows Software Development Kit Version 3.0 (SDK). The SMK
reduces the conversion time for a typical large application from months to
days, by supplying a mapping layer that translates Windows1 function calls
into OS/2 function calls.
This code layer, implemented as a set of OS/2 dynamic-link libraries (DLLs),
sits between PM and the Windows application, translating calls to the
Windows API at run time. When the Windows application calls an API function
to draw a line, for example, the mapping layer accepts the Windows API call,
interprets it, reorders and converts the parameters, and calls the
corresponding PM API (see Figure 1). Converted SMK applications run as
16-bit OS/2 executables on Versions 1.2 and higher, and 2.x.
Developers convert their Windows applications by relinking with a new set of
libraries. Although to the user converted SMK applications appear as PM
applications (see Figure 2), they behave internally as if they were running
under Windows. SMK applications are OS/2 processes, so they receive many of
the benefits of PM, including the ability to exchange data with other PM
applications via the Clipboard, dynamic data exchange (DDE), or interprocess
communication (IPC). You can enhance SMK applications with additional code
that takes advantage of specific OS/2 functionality, such as multiple
threads, and High Performance File System (HPFS) features, such as long
filenames and extended attributes (EAs).
A beta release of the SMK should be available to qualified developers
shortly after press time, with a final release to follow several months
later.
SMK Contents and Installation
The SMK includes several conversion utilities, import and C run-time
libraries for four memory models (small, medium, compact, and large), debug
and retail versions of the mapping DLLs, PM-compatible Windows fonts, an SMK
version of Windows Help, on-line documentation, and sample source code.
Figure 3 lists the SMK contents.
The SMK SETUP and SMK tools are all bound applications so they can be run
under DOS2 or OS/2 development environments. But since the migrated
applications run only under OS/23, testing and debugging must be performed
in OS/2 Version 1.2 or later using a protected-mode debugger. In addition to
OS/2, you will need the Windows SDK 3.0 and Microsoft C Version 5.1 or 6.0.
The Microsoft OS/2 Presentation Manager Toolkit Version 1.2 is not necessary
for the conversion, unless you plan to enhance your SMK application with
OS/2-specific features.
SETUP is similar to the Windows SDK installer. It copies the SMK tools and
samples, and builds only the memory model C run-time libraries you request,
thus saving disk space. If you need more memory model libraries later, you
can rerun SETUP without reinstalling the entire SMK.
After you install the SMK, you must update the system environment variables
used to build the Windows
application (such as LIB and PATH) so that both the SMK and SDK are
referenced. The SMK relies on SDK tools such as the resource compiler. You
should rebuild the SMK samples to verify that the SMK and environment are
correctly set up.
If applications that are converted and tested under the beta SMK meet your
standards of performance and reliability, you can ship your converted
product accompanied with the beta version of the SMK DLLs. To ensure that
the final release of the SMK DLLs does not affect ISVs that have shipped
using the beta SMK, the DLLs have release-specific names. The SMK's beta
DLLs have names with a _B suffix, such as: GDI_B.DLL, USER_B.DLL, and
KERNEL_B.DLL. DLLs in the final release will have suffixes other than _B. It
is recommended that all SMK DLLs be installed in the C:\OS2\DLL directory of
OS/2. That way multiple SMK applications share a single set of the DLLs.
This is not a requirement; the only requirement is that the DLLs are in a
directory pointed to by the OS/2 LIBPATH environment variable.
SMK Conversion Process
Successful conversion of a Windows application using the SMK requires that
the application is Windows 3.0-compliant and runs in protected mode. As in
the Windows SDK, there are memory-model-dependent C run-time libraries but
only one memory-model-independent import library, LIBMK_B.LIB. This import
library contains all the Windows API imports to the SMK mapping layer DLLs.
SMK-specific library names are used so as not to conflict with existing
Windows libraries. To convert library names in your make files, the
following SMK naming convention is used:
■ Windows
SMK
■ LIBW.LIB
LIBMK_B.LIB
■ xLIBCyW.LIB
xLIBCyMK.LIB
■ xDLLCyW.LIB
xDLLCyMK.LIB
■ x is the memory model (S, M, C or L) and y represents emulator or
alternate math support (E or A)
In addition to the SMK libraries, the OS/2 import library, OS2.LIB, is
required since an OS/2 application is being created. OS2.LIB is shipped with
the SMK.
One of the most time-consuming tasks when performing a traditional port of a
Windows application to OS/2 is converting the resource files, containing
dialog boxes, menus, bitmaps, and so on, because Windows and OS/2 have
incompatible resource definitions. SMK applications retain their Windows
resources and the Windows resource compiler is still used to bind these
resources to the SMK executable. The SMK mapping layer loads and manages the
Windows resources.
A subtle problem occurs when the SMK applications are displayed by the OS/2
Program Manager: it searches for an icon in the executable to display but
recognizes only the PM icon format. Because the converted application's icon
is still in Windows format, it always uses the unexciting default PM icon.
The SMK comes with three utilities to resolve this problem. CONVICON
converts a Windows 3.0 icon to
OS/2 format. RCSMK spawns an SMK version of the
OS/2 resource compiler (RCPM) to bind the PM icon to the SMK executable
before the Windows resource compiler binds the Windows resources. The OS/2
Program Manager thus finds a PM icon to display and the SMK mapping layer
knows to skip the PM icon when accessing the Windows resources. Figure 4
shows a series of Windows 3.0 applications displayed by the Windows 3.0
Program Manager and the SMK versions with converted PM icons displayed by
the OS/2 Program Manager.
Windows and OS/2 font file format differences make a font converter
necessary. CONVFONT.EXE converts Windows font files into OS/2-compatible
font files. The SMK also provides several fonts used in Windows that are not
present in OS/2. WINMONO.FON, WINPRO.FON, and SYMBOL.FON must be installed
via the OS/2 Control Panel as are any additional ISV fonts converted using
CONVFONT.EXE.
The SMK includes a version of the Windows Help Manager that has itself been
converted via the SMK to operate under OS/2. As long as the Help Manager is
in the path, spawning and interaction occur via the normal Windows help
functions. To use WinHelp, you must ship a copy of WINHELP.EXE with your
product. Your setup program should install it along with your application.
Sample Conversion
To prove how easy it is to convert Windows applications using the SMK,
Windows code from a recent MSJ article ("A New Multiple Document Interface
API Simplifies MDI Application Development," Vol. 5, No. 4) was converted.
MDIDEMO was written to illustrate the Windows 3.0 MDI support. A native port
of this application would have required significant source code changes.
Using the SMK, however, requires no source code changes, only minor changes
to the MDIDEMO.MAK make file to reference the SMK libraries (see Figure 5).
As you can see, only the library names have changed. You must use the /nod
linker switch when specifying the SMK libraries. If you don't, the linker
will do a default library search and find the Windows libraries first. The
compiler command-line switches are the same. Because MDIDEMO uses Windows
resources, the Windows resource compiler is used to compile and bind
MDIDEMO.RC to the executable.
MDIDEMO did not originally have an icon. For the sake of illustration, a
Windows icon was created to demonstrate the SMK utilities CONVICON and
RCSMK. The make file in Figure 6 runs these utilities to convert the Window
icon to PM format and bind this PM icon resource to the executable, so that
the icon will appear in an OS/2 Program Manager group. (This and other
sample SMK-converted applications and the DLLs needed to run them under OS/2
may be downloaded from any MSJ bulletin board.)
Figure 7 shows MDIDEMO running on Windows 3.0 and OS/2 1.2. Without any
source code changes, the Windows program successfully runs on OS/2 with
equivalent functionality and MDI support. (Differences in the contents of
the Rectangles child windows is due to MDIDEMO itself, which repeatedly
creates random rectangles.)
Architecture
Because an SMK-converted application operates as if it's running under
Windows, while looking like a PM application, it might seem that the SMK
application has limited participation in the PM environment. However, the
mapping layer allows converted applications to interact with other SMK and
PM applications automatically. Converted applications and native PM
applications have full Clipboard data transfer capability, including two-way
metafile conversion between the Windows and PM metafile formats. The DDE
message protocol, which enables dynamic communication with other PM
applications, is also supported. The mapping layer handles code page
translation of DDE and Clipboard text. Unfortunately, Windows has additional
Clipboard formats not present in OS/2. The mapping layer cannot translate
private formats or formats unsupported in OS/2.
The mapping layer also resolves differences between Windows and OS/2 profile
information storage. OS/2 uses the binary file OS2.INI for system
configuration storage, while Windows uses a file named WIN.INI, which is in
ASCII. Likewise, while both systems support private initialization files,
OS/2 uses a binary format while Windows uses ASCII. Windows applications
typically read and write configuration information with the functions
GetProfileString, WriteProfileString, and GetProfileInt. The SMK intercepts
these functions and maps system values (system colors, international country
settings, fonts, and so on) to OS2.INI. All other values are assumed to be
application-specific and are mapped to WIN.INI. The SMK searches the path
for the first occurrence of WIN.INI. If WIN.INI does not exist along the
path, the SMK creates it in the current directory.
Windows and OS/2 share architectural characteristics such as dynamic
linking, resource management, and windowing that allow a converted
application to run as a true OS/2 process rather than requiring
virtualization. The mapping layer does not rely on support from the DOS
compatibility box or the virtual 8086 mode of the 80386 processor. Because
Windows and OS/2 use an EXE format known as the New Executable Format (new
compared to DOS), the OS/2 loader knows how to handle code preloading and
dynamic linking. Therefore, the mapping layer handles Windows function
imports via standard OS/2 dynamic linking and translates the Windows
functions into OS/2 equivalent operations at run time.
Many simple Windows functions are mapped directly to their OS/2 equivalents.
A few that have no equivalents, such as the Windows sound functions, are
simply stubbed out (see Figure 8). For most functions, the mapping layer
retains context information across multiple Windows function calls. Window
handles, window device contexts, and resources are managed by the mapping
layer such that the relinked application behaves as if running under
Windows. An interesting aspect of the mapping layer is that it creates and
handles proxy window handles for all PM windows that an SMK application
interacts with. For example, if an SMK application calls GetFocus while a PM
window currently has the focus, a unique non-PM window handle that
represents the PM window is returned to the SMK application.
Handles to objects such as windows, bitmaps, and device contexts are not
interchangeable between PM and SMK applications. These handles must be
converted by the mapping layer during calls from the SMK application to the
Windows API. Messages between SMK and PM applications are similarly
manipulated by the mapping layer.
The SMK must also contend with differences between Windows and OS/2
multitasking. OS/2 offers preemptive multitasking, in contrast to the
cooperative, nonpreemptive, message-driven multitasking of Windows. Windows
applications relinquish control of the CPU only when they call GetMessage or
PeekMessage. This application-controlled "baton-passing" allows Windows
applications to ignore certain synchronization and resource conflicts that
can occur in a preemptive multitasking system such as OS/2.
Resource manipulation in shared DLLs illustrate the problems of bringing
Windows applications to preemptive OS/2. If a routine in a shared DLL
manipulates a resource but reaches the end of its time slice before
completing the operation, another process may execute the same DLL routine
and stumble upon a corrupt or partially manipulated resource. Windows DLLs
do not guard against this scenario because under Windows, all DLL operations
are atomic until a message queue is checked.
While a Windows DLL could be rewritten to use semaphores, the SMK mapping
layer simplifies the process. The mapping layer duplicates Windows
behavior-it will preempt an SMK application to schedule another SMK
application only when the applications check their message queues via calls
to GetMessage or PeekMessage. This synchronization of SMK threads does not
affect native PM and OS/2 kernel threads.
The SMK mapping layer cannot support some features of OS/2 due to
architectural differences between Windows and OS/2 and how they exploit the
80x86 architecture. Under Windows protected and 386 enhanced mode, all
Windows applications share one Local Descriptor Table (LDT). Each OS/2
process has a private LDT. Though it has been discouraged, some Windows
applications may take advantage of the current LDT usage of Windows to
support direct manipulation of another application's data.
As an example, a Windows application that is designed to subclass another
application's windows operates as intended under Windows 3.0, but must be
redesigned if converted via the SMK to run on OS/2. Subclassing another
application's windows is generally not supported under OS/2 and providing
this functionality is outside the scope of this article. Windows hooks are
supported under the SMK, though there are similar issues to those raised in
subclassing. Hook procedures must reside in DLLs and the system will do an
implicit LoadModule when the hook must be called from a different process.
The SMK supports the Windows global shared memory mechanism, GMEM_DDESHARE.
The mapping layer translates global memory requests to DosAllocSeg or
DosAllocHuge transparently, granting access to this shared memory to other
processes via DosGiveSeg and DosGetSeg.
In addition to relying on different LDT designs, Windows and OS/2 also
implement different ring architectures. All Windows applications and the
Windows system run in Ring 1. OS/2 applications (including SMK applications)
run at Ring 3, with the ability to create Ring 2 segments for executing port
I/O. This causes problems when porting device drivers that cannot be
addressed by the SMK. This is discussed further in the next section.
Windows applications often use their own DLLs that are shipped with the
product. Obviously, these DLLs must be converted; the SMK provides
DLL-specific C run-time libraries to do so. OS/2 loads these SMK DLLs just
like any other OS/2 DLL, creating a problem that cannot be addressed
directly by the mapping layer. Windows and OS/2 DLLs typically rely on an
assembler start-up module, which contains its entry point. LIBENTRY.ASM,
included in the Windows 3.0 SDK, is used by most Windows developers. The
Windows and OS/2 loaders call the DLL entry points with a different set of
initialized registers. For example, in Windows the CX register contains the
DLL heap size specified in the DEF file; in OS/2, the SI register contains
this information. Other registers are similarly affected.
An SMK tool, CONVDLL.EXE, modifies the DLL start-up code such that the
standard Windows register set is restored. CONVDLL relies on a small source
code change that must be inserted manually into LIBENTRY.ASM. CONVDLL.INC is
a small include file containing the necessary signature information. The
following illustrates the necessary code change to LIBENTRY.ASM.
cProc LibEntry, <PUBLIC,FAR>; entry point into DLL
INCLUDE CONVDLL.INC
cBegin
push di ; handle of the module instance
push ds ; library data segment
push cx ; heap size
push es ; command line segment
push si ; command line offset
o
o
o
The CONVDLL changes also assure that the SMK DLLs are properly initialized.
In this way, Windows APIs may be called in LIBENTRY.ASM. Without CONVDLL,
you could not be sure that the SMK DLLs are initialized during LIBENTRY
execution. The SMK includes sample code illustrating the use of CONVDLL.
SMK Limitations
Most Windows API functions are supported in the SMK mapping layer, with the
exception of sound, 32-bit memory management, some Graphic Device Interface
(GDI) functions, and a few other functions (see Figure 8). These functions
must be converted differently. Applications that call DOS system interrupts
directly (such as INT 21H) should use the Windows 3.0 Dos3Call function for
equivalent interrupt-level support. A list of supported DOS system
interrupts can be found in the SMK
documentation.
Windows supports several types of device drivers (DOS, Windows, GDI) that
are not supported by the SMK mapping layer. A Windows application that
relies on a DOS device driver loaded via CONFIG.SYS or a TSR must use an
OS/2 equivalent (many such drivers already exist). If one doesn't exist, it
must be developed using the OS/2 Device Driver Kit.
The SMK uses PM display and printer drivers for output. The mapping layer
compensates for differences in the Windows and PM drivers, especially in
printer escape support, but relies on PM drivers for actual output. PM does
not support as many devices as Windows, but does cover the most popular
displays (CGA, EGA, VGA, 8514/A, and so on) and printers (PostScript,
Hewlett-Packard LaserJet series, Epson, and so on). The SMK handles the
proper implicit mapping between these standard device drivers so that they
function as if they were the actual Windows drivers. The mapping occurs as a
result of normal API calls and does not include direct communication with
devices via port I/O, interrupts, or memory mapped I/O.
The differences between Windows and OS/2 ring architecture make it
impossible to perform direct port I/O from a Windows application. Port I/O
can be implemented in an OS/2 Ring 2 IOPL segment, but this requires source
code modification and segregation of I/O calls to a separate code segment.
It is also impossible to place interrupt service routines within DLLs as in
Windows. Interrupts can be handled only at Ring 0 in a device driver under
OS/2.
Converted applications run 5 to 10 percent slower than native PM
applications, because of the time required for the mapping layer to execute.
In general, they also perform 5 to 15 percent slower than the original
applications running in native Windows protected mode, depending upon the
actions performed. SMK application load time and dialog box performance may
appear slower at times due to the mapping overhead. The mapping layer must
be loaded when an SMK application is first started, so total load time is
longer. Intensely repetitive graphic operations involving creating and
freeing Windows Device Contexts (DCs) also slow performance. On the other
hand, running in OS/2 with HPFS greatly enhances file I/O operations.
Advanced SMK Use
Because an SMK application is an OS/2 process, it can take advantage of OS/2
features not found in Windows such as threads, long filenames, EAs, and IPC.
Adding OS/2 features creates a hybrid application that may be difficult to
support in future OS/2 releases. You should add
OS/2 features only where
appropriate.
Threads are particularly useful when long calculations are involved, such as
recalculating a spreadsheet; where there is a need to block for an
indefinite period of time for a specific event, like blocking on a semaphore
used for IPC; or when large amounts of information must be transferred, such
as during large file manipulation or serial data exchange.
Threading is affected by the mapping layer's synchronization of SMK threads.
Only one SMK thread making Windows API calls can be called at a time.
Therefore, no Windows APIs can be called in secondary threads; only
OS/2 API functions such as DosCreateThread, DosOpen, DosRead, and
DosAllocSeg can be called. The current SMK libraries cannot support a
multiple-threaded C run-time. Secondary threads calling C run-time functions
could corrupt buffers relied on by other application threads calling related
C run-time functions. Applications that require full multithreaded C
run-time support should use a separate OS/2 DLL (created via LLIBCDLL.LIB)
or an OS/2 daemon process. If a daemon process is used, shared memory,
pipes, or other IPC mechanisms can be used to communicate between the worker
(daemon) process and the user interface (SMK application).
There is one exception to the rule that Windows API calls cannot be made
from secondary threads. PostMessage may be used in secondary threads as an
asynchronous notification mechanism for the background thread to inform the
primary SMK thread that an operation has completed. The main SMK thread can
also poll background threads for status information. If global variables or
shared memory or both are used to communicate between the SMK thread and
background threads, critical sections and semaphores should be employed. The
primary SMK thread should not block on a semaphore indefinitely or fail to
check its message queue often. If it does, other SMK applications will not
be scheduled and will receive no CPU time. This is as important for SMK
applications as it is for native Windows applications.
You may want your converted application to handle long filenames. A common
problem in Windows is allocating filename buffers just large enough to hold
FAT 8.3 filenames and path buffers just large enough to hold the maximum
64-character path length. When using HPFS, however, you can use the OS/2
function DosQSysInfo to determine the maximum length of a pathname in the
current system and allocate your file and path buffers accordingly.
If your application includes filename parsing code that depends on the 8.3
format, you should modify the code so that it can handle long
filenames-specifically, filenames that contain embedded spaces and multiple
dot delimiters. The Microsoft OS/2 Presentation Manager Toolkit fully
documents parsing rules as well as other HPFS issues. Once the necessary
buffer and parsing changes have been made, simply mark your application as
being long-filename-aware by adding the LONGNAMES descriptor to the DEF
file. The SMK mapping layer is already long-filename-aware. Once marked,
OS/2 will pass long filenames to your application. For example, DlgDirList
will now display full 256-character filenames in its list box when the SMK
application is marked and run under
OS/2. Sample code included with the SMK illustrates long filename and
extended attribute manipulation in a modified Windows application.
Conclusion
The SMK enables Windows software developers to address the OS/2 platform by
leveraging their existing Windows development efforts. The SMK conversion
process is faster and easier than a native port, and is often as simple as a
relink with the SMK libraries.
No license fees are required to ship converted applications. You simply
include the SMK DLLs with your application. The Windows Help engine
(WINHELP.EXE) may also be shipped along with the SMK application, if
required. Windows applications converted to OS/2 using the SMK should
continue to run on OS/2 2.0 and above, because 2.0 is planned to be
compatible with previous
OS/2 releases.
1For ease of reading, "Windows" refers to the Microsoft Windows graphical
environment. "Windows" refers only to this Microsoft product and is not
intended to refer to such products generally.
2As used herein, "DOS" refers to the MS-DOS and PC-DOS operating systems.
3As used herein, "OS/2" refers to the OS/2 operating system jointly
developed by IBM and Microsoft.
Figure 3. Beta SMK Contents
■
■ SETUP.CMD
Setup (OS/2)
■ SETUP.BAT
Setup (DOS)
■ SETUP.DAT
Setup (data file)
■ README.TXT
General information, release notes
■ PACKING.LST
Disk contents description
■ SMK.HLP
SMK manual in on-line Advisor format
■
■
■
■ Conversion Tools
■
■
■ CONVFONT.EXE
Windows to PM font file converter
■ CONVICON.EXE
Windows to PM icon (.ICO) converter
■ CONVDLL.EXE
Windows DLL conversion utility
■ CONVDLL.INC
Include file for Windows DLL conversion
■ RCSMK.EXE
SMK-specific resource compiler
■ RCPM.EXE
OS/2 PM 1.2 resource compiler
■ WINHELP.EXE
SMK Windows help engine (English)
■
■
■
■ Converted Windows Fonts
■
■
■ WINMONO.FON
Windows fixed pitch font
■ WINPRO.FON
Windows dialog box system font (small)
■ SYMBOL.FON
Windows symbol font
■
■
■
■ Substitute Windows Libraries
■
■
■ BUILDLIB.CMD
SMK library create tool (OS/2)
■ BUILDLIB.BAT
SMK library create tool (DOS)
■ LIBMK_B.LIB
General import LIB (replaces LIBW.LIB)
■ OS2.LIB
OS/2 import LIB
■
■ NOTE: The following files are created during the SMK install process.
■
■ xLIBCyMK.LIB
Replaces xLIBCyW.LIB
■ xDLLCyMK.LIB
Replaces xDLLCyW.LIB
■
■
■ NOTE:
x = model (S, M, L, C)
■
y = math package (E or A)
■
■
■
■ SMK Mapping Layer DLLs (debug and retail)
■
■
■ USER_B.DLL
USER
■ KERNEL_B.DLL
KERNEL
■ GDI_B.DLL
GDI
■ PMSMK_B.DLL
(SMK only)
Windows-equivalent
DLLs
■ PMMETA_B.DLL
(SMK only)
■ SOUND_B.DLL
SOUND
■ KEYB_B.DLL
KEYBOARD
■ W87EM_B.DLL
WIN87EM
■ SYSTEM_B.DLL
SYSTEM
■
■ *.SYM
Symbol files for debug versions of all DLL
files
■
■
■
■ Converted Windows Sample Applications
■
■
■ MAKEALL.CMD
Batch file to build all samples (OS/2)
■ MAKEALL.BAT
Batch file to build all samples (DOS)
■
■ \GENERIC
Basic Win 3.0 example
■ \REVERSI
Advanced Win 3.0 example
■
(contains sample help file: REVERSI.HLP)
■ \WINDLL
How to convert a Win DLL
■ \OS2DLL
How to add an OS/2 DLL
■ \EDITFILE
Long filenames and EAs
■ \CLOCK
Dos3Call example
■
■
────────────────────────────────────────────────────────────────────────────
Figure 5. Make File Conversion
Original MDIDEMO.MAK
#-----------------------
# MDIDEMO.MAK make file
#-----------------------
mdidemo.exe : mdidemo.obj mdidemo.def mdidemo.res
link mdidemo, /align:16, NUL, /nod slibcew libw, mdidemo
rc mdidemo.res
mdidemo.obj : mdidemo.c mdidemo.h
cl -c -Gsw -Ow -W2 -Zp mdidemo.c
mdidemo.res : mdidemo.rc mdidemo.h
rc -r mdidemo.rc
The SMK version of MDIDEMO.MAK
#-------------------------------------
# SMK version of MDIDEMO.MAK make file
#-------------------------------------
mdidemo.exe : mdidemo.obj mdidemo.def mdidemo.res
link mdidemo, /align:16, NUL, /nod slibcemk libmk_b os2, mdidemo
rc mdidemo.res
mdidemo.obj : mdidemo.c mdidemo.h
cl -c -Gsw -Ow -W2 -Zp mdidemo.c
mdidemo.res : mdidemo.rc mdidemo.h
rc -r mdidemo.rc
Figure 6. Make File with Icon Conversion
#---------------------------------------
# RCSMK version of MDIDEMO.MAK make file
#---------------------------------------
mdidemo.exe : mdidemo.obj mdidemo.def mdidemo.res mdiicon.res
link mdidemo, /align:16, NUL, /nod slibcemk libmk_b os2, mdidemo
rcsmk -PM mdiicon.res mdidemo.res
mdiicon.res: mdidemo.ico
convicon mdidemo.ico mdiicon
mdidemo.obj : mdidemo.c mdidemo.h
cl -c -Gsw -Ow -W2 -Zp mdidemo.c
mdidemo.res : mdidemo.rc mdidemo.h
rc -r mdidemo.rc
Figure 8. Windows Functions Unsupported in SMK
■
■ User
■
■
■ GetKeyboardState
■ IconSize
■ MenuItemState
■ SetKeyboardState
■ SetMenuItemBitmaps
■ TrackPopupMenu
■
■ Kernel
■
■
■ AllocSelector
■ FreeSelector
■ GetCodeHandle
■ GetCodeInfo
■ GetCurrentPDB
■ ValidateFreeSpaces
■ LimitEmsPages
■
■ 32-bit Memory Management
■
■
■ GetWinMem32Version
■ Global16PointerAlloc
■ Global16PointerFree
■ Global32Alloc
■ Global32CodeAlias
■ Global32CodeAliasFree
■ Global32Free
■ Global32Realloc
■
■ GDI
■
■
■
■ Palette Management
■ AnimatePalette
■ CreatePalette
■ GetPaletteEntries
■ GetNearestPaletteIndex
■ RealizePalette
■ ResizePalette
■ SelectPalette
■ SetPaletteEntries
■ UpdateColors
■ Region
■ CreateEllipticRgn
■ CreateEllipticRgnIndirect
■ CreatePolyPolygonRgn
■ CreatePolygonRgn
■ CreateRoundRectRgn
■ Bitmap
■ ExtFloodFill
■ FloodFill
■ Text
■ GetTextCharacterExtra
■ SetTextCharacterExtra
■ SetTextJustification
■
■
■ Keyboard (Limited SMK support)
■
■
■
■ GetKBCodePage
■ GetKeyboardType
■ GetKeyNameText
■ MapVirtualKey
■ OEMKeyScan
■ SetSpeed
■ ToAscii
■ VkKeyScan
■
■ All Sound APIs
■
Windows[tm] 3.0 Memory Management: Supporting Disparate 80x86 Architectures
Paul Yao
The latest version of the Microsoft Windows[tm] graphical environment,
Version 3.0, has three different memory managers. From a memory management
point of view, then, Windows1 is actually three products: real mode Windows,
standard mode Windows, and 386[tm] enhanced mode Windows. Each exploits
different Intel 80x86 processors, yet a common architecture allows a program
to run unchanged under all modes.
In Windows, you can determine the current operating mode by looking at the
Program Manager's About box. A program can learn the current mode by calling
the GetWinFlags routine. This Windows library routine provides a 32-bit
value that contains a set of 1-bit flags. A code fragment showing one way to
use these flags can be found in Figure 1.
All three memory managers are accessed by running the start-up stub,
WIN.COM. If you don't choose a mode, Windows picks one for you. On 8086/8088
machines, real mode is selected. Standard mode is selected on 80286 machines
or 80386 machines with less than 2Mb of memory. On 80386 systems with at
least 2Mb of RAM, Windows chooses 386 enhanced mode. Alternatively, you can
force Windows into a specific mode from the command line. Real mode is
started by specifying:
win /r
On 80286 and higher CPUs, standard mode is requested with:
win /s
And on computers with 80386 and higher CPUs, 386 enhanced mode is requested
with:
win /3
Each mode has unique characteristics, which I will describe. I'll start by
reviewing how the 80x86 segmented address scheme works.
Segmented Addressing
Windows runs on top of DOS2, which means that Windows runs on members of
the Intel-86 family of processors. This family includes the 8086, 8088,
80188, 80186, 80286, 80386SX, 80386, and i486[tm] microprocessors. Each
processor addresses memory in segments. Under real mode Windows, segment
values are converted directly into physical addresses. Standard and enhanced
mode Windows run the processors in protected mode to gain the memory
management support built into the higher-end CPUs.
While there are important differences between real mode and protected mode
operation, both access memory in segments. Windows and its programs inherit
this segmented orientation, so that segments are the organizing units of
memory used by Windows.
There are basically four types of segments available to a Windows program:
code segments, a single default data segment, dynamically allocated
segments, and resource segments. Code segments are managed by the Windows
dynamic linking mechanism, so that code can be demand-loaded from disk when
needed and discarded when it is not.
The default data segment contains a program's static data, stack, and local
heap. Since Windows supports only a single default data segment, programs
must be built using the small or medium compiler model. While a single data
segment seems to limit a Windows program to a maximum of 64Kb of data, this
apparent limitation is easily overcome through the use of dynamically
allocated segments. This third category of memory segment
is allocated by calling GlobalAlloc.
The fourth type of segment, resources, is created for read-only data.
Resource segments are used to store various user interface objects, such as
menus, dialog box templates, and cursors. Resource segments are also used
for GDI objects like fonts and bitmaps. A resource is often in a discardable
segment and can be purged from system memory when not needed. This helps
Windows conserve memory.
Addressing Memory
All of the Intel processors mentioned address memory with a 16-bit segment
identifier paired with a 16-bit offset value. (The 80386 and higher CPUs can
use a 32-bit offset, but the Windows Version 3.0 API only supports a 16-bit
offset.) The segment identifier describes the segment being addressed, while
the offset value describes the byte in that segment.
This two-part memory address is sometimes referred to as a logical address.
Software must access memory using a two-part logical address. Together the
CPU and the operating system translate a logical address into a physical
address. The physical address is a 20-bit, 24-bit, or 32-bit address
(depending on the CPU) used to reference the physical memory installed in
the system.
In enhanced mode, a logical address is first translated into a 32-bit
virtual address. The paging hardware built into the 80386 and 80486 CPUs
then resolves this reference into a physical address in RAM.
A diagram of the translation process for all three of the Windows operating
modes is shown in Figure 2.
To calculate the maximum amount of physical memory a CPU can address, count
the number of address lines. That number is the power of two for the maximum
addressable memory. With twenty address lines, the 8088 can address 220 or
one megabyte of memory. The maximum addressable memory for each member of
the
Intel-86 family is shown in Figure 3.
Although the 80386 and 80486 processors have 32 address lines, only 24 are
available on the standard AT-style bus. For this reason, Windows limits
itself to 16Mb of physical memory when running on these systems.
The segmented address scheme is the reason that Windows programmers use the
NEAR and FAR keywords. At any point in time, the current code and data
segments are referenced using the code segment register (CS) and the data
segment register (DS). To reference code or data outside the default
segments requires FAR addressing. A FAR address requires more work, and is
therefore slightly less efficient, than NEAR addressing. FAR addressing
requires both the segment and the offset value. When a program references
code or data inside one of the current default segments, the simpler NEAR
addressing can be used. It is more efficient because only one value-the
offset-must be specified.
Windows Segment Attributes
In Windows, a memory segment is managed according to an attribute selected
when the segment is allocated. Figure 4 lists the basic segment attributes.
New Windows programmers may find it hard to imagine a discardable memory
object, but it is at the heart of Windows' dynamic-linking mechanism. You
can think of dynamic linking as a sophisticated overlay manager, which
permits any code segment to be purged from system memory. When code segments
are needed again, they are loaded from disk into memory in a completely
transparent manner. In this respect, dynamic linking is far superior to
traditional overlay management software. Discardable segments are also used
for segments containing resources, which can be purged from memory when not
needed.
The presence of moveable memory is not really new in the world of operating
systems. It is, however, somewhat unusual for the movement of memory to be
detectable from within a program. Real mode operation requires the presence
of moveable memory.
When a moveable or discardable segment is allocated, Windows issues a memory
handle to identify the segment. To read or write in such a segment, the
program calls the GlobalLock routine to retrieve the object's address. When
a segment is not locked, the memory manager is free to move the segment to
minimize the fragmentation of the global heap. GlobalLock also increments an
object's lock count to prevent the object from being moved or discarded. (An
important exception in protected mode is that GlobalLock does not increment
the lock count of moveable objects. As you'll see, the protected mode
hardware allows a moveable object to maintain its logical address even when
it has been moved.)
A fixed memory segment can be allocated for situations in which the first
two types of memory cannot be used. This creates the most restrictions for
the Windows memory manager, so it should be reserved for special
circumstances, such as interrupt service routines in device drivers. Fixed
memory objects restrict the memory manager's ability to reorganize system
memory, which it must be able to do to recover the memory that would
otherwise be lost due to fragmentation of the global heap.
These three types of segments operate in a relatively straightforward manner
in real mode Windows. In the other two modes, things get a little fuzzy. The
built-in memory management of protected mode in the higher end CPUs takes
over some of the tasks that Windows itself must do in real mode. As I
discuss the Windows operating modes, I'll describe the unique twist put on
these three types of segments by each mode. However, a description of the
segment-locking process is needed first.
Segment Locking
As I've mentioned, when you work with moveable or discardable memory
segments, you must first call GlobalLock, which asks Windows to tie down a
segment. If you are an experienced Windows programmer, you can continue
working with dynamically allocated segments in the ways
you are familiar with. However, Version 3.0 makes some new things available.
There are now four methods for locking a segment, as listed in Figure 5. The
first method, a simple lock, is the most common way to fix segments. A
simple lock is created with a call to GlobalLock, which returns a far
pointer. The pointer is guaranteed to be valid until the lock is disabled
with a call to GlobalUnlock. In real mode, when you access dynamically
allocated segments, you'll lock the segment for a very short period of
time-only as long as it takes to process a single message. Otherwise, you
risk creating sandbars in the global heap that prevent the memory manager
from reorganizing memory when it needs to.
In protected mode, the situation is a little different. As you'll see, the
Windows memory manager has help from the hardware. In protected mode, you
can lock a segment once, and leave it locked until you are finished using
the memory. Windows protected mode memory managers can still move memory
around, and even swap memory to disk in enhanced mode.
The other three types of locks are for special circumstances (see Figure 5).
For example, in real mode, to lock a segment for a relatively long period of
time, you should wire the segment by calling GlobalWire instead of
GlobalLock. A wired segment is moved first so that it doesn't obstruct the
movement of other memory objects.
The third type of segment lock is for protected mode, to fix a segment at a
given physical address. When a segment has a fixed physical address, a
low-level lock prevents it from moving in the linear address space of
protected mode Windows. Ordinarily, segments can be moved in protected mode
Windows, but the hardware memory management fools you because the segment's
logical address doesn't change when the segment moves. A fixed physical
address lock prevents such movement. This is the most secure way to tie down
a segment in standard mode but is overkill in most applications. It is
required mainly for device drivers that need to make sure that the physical
address of a dynamically allocated segment doesn't change.
The fourth type of segment lock prevents a segment from being swapped to
disk in 386 enhanced mode. The virtual memory system of this mode swaps
memory pages to disk in a manner that is entirely transparent to your
program. However, when a segment is page-locked, it cannot be swapped to
disk. Page-locking also fixes the physical address. This is the most
restrictive type of lock in Windows enhanced mode. Like the fixed physical
address lock of standard mode, a page-locked segment is overkill for most
applications but is useful for device drivers that need to be present in
memory at all times. Consider the delay that might be caused if critical
parts of the display or keyboard drivers were swapped to disk. Or imagine
the problems that would be caused if the kernel's swapping code itself were
swapped to disk.
Inside the module definition file (DEF) of Windows programs and dynamic-link
libraries (DLLs), there are entries that describe the memory disposition of
code and data segments. For example, an entry like the following makes code
moveable and discardable by default:
CODE MOVEABLE DISCARDABLE
In general, segments in a DLL that are FIXED inherit the most secure locking
of each of the operating modes in Windows. For example, if a DLL's default
data segment were declared fixed in real mode and standard mode, it would be
fixed in the physical address space:
DATA FIXED
In enhanced mode, it would be page-locked to prevent it from being swapped.
The rationale is that device drivers are implemented in DLLs, and therefore
they should get this service automatically.
In a Windows program, however, the FIXED attribute is treated a little
differently. For example, assume an application has the following in its DEF
file:
CODE FIXED
DATA FIXED
In real mode, both code and data segments would be fixed. But in protected
mode, the Windows memory manager overrides these flags and does not make
them fixed or page-locked. The reason is simple: the Intel protected mode
mechanism already gives fixed logical addresses to code and data segments
without invoking the more restrictive types of memory locks.
As this article goes to press, there is a known problem in Windows 3.0
running in protected mode. It involves the use of the GMEM_FIXED flag when
dynamically allocating a segment. If you allocate a segment like the
following from an application or a DLL, the segment will be allocated as
fixed and page-locked.
hmem = GlobalAlloc (GMEM_FIXED, dwSize);
This is a problem because these higher segment- locking attributes should
only apply when such a segment is allocated from a DLL. From an application,
it should be sufficient to give it a fixed logical address and still allow
the physical address to change. This will be altered in a future version of
Windows.
Real Mode
Experienced DOS programmers will find much that is familiar in Windows real
mode. Also, if you started programming for Windows with Version 1.x or
Version 2.x, everything is pretty much the same in real mode under Windows
Version 3.0. For those previous versions, Windows operated only in real
mode.
All 80x86 chips can run in real mode. In real mode, the size of the physical
address space is 1Mb. For the sake of compatibility, the higher-end members
of this family inherit this limitation when running in real mode. This is
true even for chips that have more address lines than the 8088/8086 CPUs.
Real mode gets its name from the fact that memory is accessed using real,
physical addresses. As depicted in Figure 6, the logical-to-physical address
translation process involves multiplying the segment identifier by 16
(shifting it left by 4 bits) to create a 20-bit base address. The offset is
added to this to access a particular byte or set of bytes in the memory
segment.
The three Windows segment attributes-fixed, moveable, and discardable-were
created with real mode in mind. They allow the Windows memory manager, in
cooperation with Windows programs, to manage memory effectively.
Even though a segment's logical address is identical to its physical
address, memory in real mode can be moved because Windows does not provide a
segment's address at segment allocation time. Instead, a memory handle is
issued. As long as a segment isn't locked, the memory manager is free to
move or discard segments as needed. To retrieve the address of a segment, a
program locks the segment. The address is guaranteed to be valid until the
segment is unlocked.
The segment allocation and segment-locking mechanism work in an identical
manner in real and protected mode. Thus, it is a simple matter to create
programs that work the same way in all operating modes.
But, as you'll see when I discuss protected mode operation, there are
shortcuts that can be taken by a Windows program in protected mode. Before
looking at the two protected modes in Windows, there is one more real mode
topic to explore: expanded memory.
Real Mode and Expanded Memory
Expanded memory is memory that is made available according to the Expanded
Memory Specification (EMS). It describes a bank switching mechanism for
using memory that is temporarily mapped into unused portions of the real
mode address space.
From the beginning, Windows made use of EMS memory. In version 1.x, the use
of EMS memory was limited to support for DOS applications running under
Windows. When EMS was available, Windows 1.x swapped idle DOS applications
to EMS memory instead of swapping them to disk.
In Version 2.0, introduced in the fall of 1987, Windows started using EMS
memory for Windows applications themselves (see "EMS Support Improves
Microsoft Windows 2.0 Application Performance," MSJ, Vol 3., No. 1). The
support under real mode Version 3.0 is identical to Windows 2.x EMS support.
In Version 3.0, real mode Windows uses EMS memory, when available, to
increase the size of the address space for each Windows program. An EMS bank
is automatically allocated for every Windows program (see Figure 7). As a
program runs, the Windows memory manager treats the EMS memory as an
extension of the global heap. In fact, since each program has its own EMS
bank, each program has its own private extension to system memory that does
not have to be shared with other programs. EMS memory is largely transparent
to programs, since Windows works behind the scenes to switch the correct EMS
memory into view. The result is less competition for system memory and
better overall system performance.
When running in real mode, Windows uses EMS memory if it finds an EMS driver
in the system. While EMS memory helped relieve the memory shortage in
Version 2.x, it no longer plays a crucial role in Windows memory management.
Banked EMS memory is abandoned by the other two operating modes, because
they have access to extended memory. Extended memory is not banked; instead
all extended memory is simultaneously accessible. Extended memory provides a
more flexible approach to breaking the 1Mb boundary that has hounded Windows
and DOS programmers for so long.
Standard Mode
From the beginning, it was intended that Windows would eventually run in
protected mode, as it does in Windows Version 3.0. The most obvious
difference between protected mode and real mode operation is that a larger
address space is available: up to 16Mb of RAM can be addressed.
Standard mode is sometimes described as the way Windows takes advantage of
the protected mode of the 80286 processor. In fact, standard mode can run on
the 80386 and higher processors as well. Even though standard mode is
associated with the less powerful 80286 CPU, you might prefer it over
enhanced mode. Standard mode actually runs a little faster than enhanced
mode because it doesn't rely on the virtual device drivers. These drivers
provide some nice features, such as allowing DOS programs to run in a
window. But they slow things down a bit, particularly for those programs
that access the disk heavily.
Understanding how the protected modes of the Intel processors access memory
will help you understand exactly how Windows uses these modes. As in real
mode, protected mode uses a segmented addressing scheme. But the segment
portion of the address does not refer to a physical memory location.
Instead, it contains an index into a segment lookup table. In protected
mode, there are two types of lookup tables: the global descriptor table
(GDT) and the local descriptor table (LDT).
Intel designed its protected modes flexibly, enabling different operating
systems to use these two types of tables in several different
configurations. For example, a single GDT could be used to reference all of
an operating system's shared memory. Each program could then have a private
LDT with which to access private segments.
If you have programmed in OS/2 systems, you are probably aware that
OS/23 uses the GDT itself and gives each process its own LDT. This minimizes
the possibility of one program adversely affecting the operation of other
programs. A program can only access data through its own LDT.
While this helps protect programs from each other, it requires a little more
work to share data. Data sharing in OS/2 requires the duplication of
selector information. Each process that needs access to a shared segment
must have an entry in its LDT. To simplify things, if a segment is allocated
as shareable, its segment selector is the same for all processes in the
system. A single segment is shared, then, by creating an entry in the LDT of
every process that wishes to access a segment.
Windows 3.0 sets up protected mode addressing using another configuration.
The GDT is reserved for the protected mode memory manager, and a single LDT
is created for all Windows libraries and applications. Every segment
allocated for Windows programs and every segment allocated for DLLs is
referenced from one systemwide LDT. A single LDT means that all programs
share a common address space, which makes it easy to share segments between
programs. But watch out!
In a future version of Windows, it is likely that every program will be
allocated a private LDT, just as is done in OS/2. When this happens, each
Windows program will have a private address space. At that time, sharing
data will require one of three methods: the Clipboard, Dynamic Data
Exchange, or DLLs.
Figure 8 depicts how a logical address is translated into a physical address
using the LDT. The segment identifier, which is called a segment selector in
protected mode, contains a descriptor table index. When a segment is
referenced, its descriptor table information is loaded into a set of
registers known as the shadow registers. The name comes from the fact that
these registers are entirely invisible to application software.
Among the values loaded into the shadow registers are a segment's base
address, its size (also known as its limit), and various flags. Translating
a logical address into a physical address involves adding the descriptor
table's base address to the offset. Since the location of a segment is not
part of the segment address itself, but instead is kept in the descriptor
table, segments can be moved freely in protected mode without changing their
logical address. This represents an important improvement over real mode
addressing, in which the segment's logical address is the same as its
physical address.
Another improvement over real mode involves automatic error checking.
Several checks are performed to make sure that the contents of memory are
protected from unauthorized access. For one thing, when a program addresses
memory, the CPU automatically checks for a valid segment selector. A valid
segment selector is one that has a valid entry in one of the current
descriptor tables (one LDT and one GDT table is current at any time). If a
valid entry is not found, a protection exception occurs.
At the hardware level, a protection exception creates a hardware interrupt
13H General Protection Fault (also known as a GP fault). Windows responds to
this interrupt by displaying a message box with the fatal error message:
"Unexpected Application Error," sometimes known as a "UAE Error."
Other errors can cause a GP fault. For example, if a program tries to
reference memory beyond the limit of a segment, a GP fault occurs. This
prevents a program from overrunning the boundaries of a segment and possibly
overwriting another program's data.
Another cause of GP faults is attempted access to a protected segment, a
violation of the privilege levels built into protected mode addressing.
Associated with every segment is a 2-bit privilege flag that defines
four privilege levels: 0
through 3.
The highest privilege level, 0, is given to the most trusted software. OS/2
programmers know that the OS/2 kernel runs at privilege level 0, also known
as Ring 0. OS/2 programs, and a large part of Presentation Manager, run at
Ring 3. The rest of Presentation Manager, and all display and printer device
drivers, run at Ring 2.
In Windows 3.0, the protected mode control programs (DOSX.EXE in standard
mode and WIN386.EXE in enhanced mode) run at Ring 0. Windows programs, and
the Windows DLLs, run at Ring 1. If a future version of Windows runs at Ring
3, Windows will have room to grow. It will also minimize the differences
between the protected mode environment of OS/2 and the protected mode of
Windows and ease porting Windows programs to
OS/2.
386 Enhanced Mode
Windows 386 enhanced mode takes advantage of the paging hardware on 80386
and above CPUs to implement virtual memory (see Figure 9). When running in
this mode, two memory managers are present: the 386 control program,
WIN386.EXE, and the Windows kernel, KRNL386.EXE.
The enhanced mode kernel uses the protected mode LDT to manage system memory
in the same way as the standard mode kernel. But instead of referencing
physical memory, the enhanced mode LDT references into a virtual address
space that can be up to four times as large as the available RAM. The kernel
treats the virtual address space as the global heap that it manages.
The second memory manager, WIN386, is responsible for supporting this
virtual address space. It does so by enabling the paging hardware built into
the 80386 chip. The paging hardware makes use of memory tables that are
similar to the LDT, but which operate at a lower level: the paging tables.
The paging tables translate a 32-bit linear address into the actual physical
addresses for a page of memory. When a page is accessed that is not present,
a page fault occurs. At that time, the virtual memory manager runs to disk
to find the needed page. It loads it into memory, fixes up the page tables
to reflect the changed system configuration, and restarts the machine
instruction that actually caused the page fault to occur. This way, virtual
memory is entirely transparent to the software.
Like virtual memory systems built on mainframe computers, the virtual memory
of the 386 control program simulates a large linear address space by
shuttling 4Kb pages between disk and RAM. The amount of virtual memory that
can be created is limited by the disk space that is available for swapping,
and by a limit built into the 386 control program that supports a virtual
address space that is up to four times the available physical RAM.
Therefore, with 5Mb of RAM and enough swap space on disk, a 20Mb virtual
address space can be created.
One feature of Windows 3.0 is that the enhanced mode kernel does not start
to discard objects until it has reached the limit of the virtual address
space. Although this means that the 386 control program may swap discardable
objects, it is faster to page out discardable data than it is to reload and
relocate an entire segment.
Protected Mode and Memory Locking
When running in real mode, programs must continually lock and unlock
dynamically allocated segments. The reason, as I mentioned earlier, is that
segments cannot be moved when locked. Since moving segments is crucial to
the health and well-being of system memory, programs must not lock segments
any longer than they have to.
The situation is different in protected mode. Once a segment is allocated,
it will retain the same logical address even though it may change physical
locations in system memory. The logical address, after all, contains an
index into a descriptor table. A memory object can be moved in protected
mode by simply updating the corresponding entry in the descriptor table.
Thus, the Windows memory manager can move memory in a manner that is
completely transparent to applications.
Windows programs running in protected mode can do something that is
inconceivable in real mode: keep data segments continually locked. This
means you can lock a segment immediately after you allocate it to retrieve
the segment's logical address. You can keep the segment locked as long as
you need to and not bother to unlock the segment until you are ready to free
the memory object. Since this will not interfere with the management of
system memory, you are free to use this approach without fear that you will
somehow disrupt the system. Of course, to do this safely, you must first
check the GetWinFlags routine to make sure that you are running in protected
mode. Also, keep in mind that such a lock will keep discardable segments
from being discarded. Therefore, you'll probably want to limit your use of
this technique to moveable segments.
In my next article, I'm going to discuss memory use by applications in
Windows Version 3.0. I'll describe every available place for storing a byte
of data and describe the impact of each on the system.
1For ease of reading, "Windows" refers to the Microsoft Windows graphical
environment. "Windows" refers only to this Microsoft product and is not
intended to refer to such products generally.
2As used herein, "DOS" refers to the MS-DOS and PC-DOS operating systems.
3As used herein, "OS/2" refers to the OS/2 operating system jointly
developed by IBM and Microsoft.
Figure 1. Call GetWinFlags to determine the current operating mode.
DWORD dwFlags;
dwFlags = GetWinFlags ();
if (dwFlags & WF_PMODE)
{
if (dwFlags & WF_STANDARD)
{
/* STANDARD MODE */
}
else
{
/* ENHANCED MODE */
}
}
else
{
/* REAL MODE */
}
Figure 3. 80x86 Processor's Physical Address Space
■
■ Processor
Address Lines
Physical Address Space
■
■
■
■ 8088
20
1Mb
■ 8086
20
1Mb
■ 80186
20
1Mb
■ 80188
20
1Mb
■ 80286
24
16Mb
■ 80386SX
24
16Mb
■ 80386
32
4Gb
■ i486
32
4Gb
■
Figure 4. Windows Memory Segment Attributes
■
■ Attribute
Description
■
■
■
■ Fixed
Address of segment is fixed
■ Moveable
Address of segment is not fixed, unless object
■
has been locked
■ Discardable
Segment can be purged from memory as needed
■
Figure 5. Four Types of Windows Memory Locks
Type of Lock
Description
Simple lock
A segment is locked with a call to GlobalLock. This routine fixes a
segment in place, and returns a far pointer to the data.
Wired
A segment is wired with a call to GlobalWire. When a segment must be
fixed for a period of time, this routine first moves the segment to a
lower part of memory before fixing a segment and returning a far
pointer to the data.
Fixed physical address
A segment is fixed with a call to GlobalFix. In real mode, this is the
same as a locked segment. In standard and 386 enhanced mode, this fixes
a segment's physical address.
Page-lock
A segment is page-locked through a call to GlobalPageLock. In real
mode and standard mode, this has the same effect as locking a segment.
In enhanced mode, it prevents the virtual memory pages of a segment
from being swapped to disk.
Measure Your Machine's Activity and Learn How to Use OS/2 Threads with
CPUMETER
Bob Chiverton
It's a miracle my first sports car didn't explode. It had only two
speeds-legal and illegal. But I was 19, it was summer, and the car wasn't
really mine. I borrowed it from my friend, Roman. And if Roman knew I pushed
the tachometer into the red zone in every gear including reverse, you
wouldn't be reading this now.
I don't redline tachometers anymore, now that I pay for my auto repairs, but
I still enjoy speed-especially in my computers. That's why I wrote CPUMETER,
an OS/21 Presentation Manager program that shows when I'm redlining the CPU.
It also taught me about using threads.
CPUMETER is a tachometer for your computer. "Tachometer" is not quite right,
though. CPUMETER is really a CPU activity meter. It shows you the percentage
of time the CPU is busy running threads. In this context, the CPU is active
(that is, busy) when it is executing a thread and inactive when all threads
are blocked or frozen (that is, not runnable). When iconized, CPUMETER uses
a needle gauge that goes from 0 to 100 percent in increments of 10 percent.
When your computer is idle, the needle is at 0. At full load, the needle
points to 100. The client window is somewhat more informative because it
keeps track of where the needle has been and displays a constantly updated
performance histogram.
Does it surprise you to learn that sometimes no threads are running? It
surprised me. The CPU doesn't pack up and leave town when all threads are
blocked. It executes code inside the scheduler. The CPU loops, waiting for a
thread to become ready. Some CPU cycles are spent servicing
I/O time-outs, semaphore time-outs, and so on, but otherwise the CPU is
polling for ready threads. So it's not that there are no runnable threads,
it's that there are no runnable threads except for the OS/2 scheduler
thread.
CPUMET
Before I discuss CPUMETER, I'd like to present two simpler versions, CPUMET
and CPUMETE. CPUMET's main function creates a standard Presentation Manager
(hereafter "PM") window (see Figure 1). After calling WinSetWindowPos to
size and position the window on the screen, it calls _beginthread twice to
create two additional threads of execution before dropping into the familiar
WinGetMsg/WinDispatchMsg loop.
There are two peculiar things about CPUMET. It looks odd, and it uses three
threads. CPUMET has no client window-just a title bar window and a system
menu (see Figure 2). CPUMET displays its output in the title bar. This is
the easiest way I know to display text in a PM application: just call
WinSetWindowText when you have a string of text to output. There are no
presentation spaces, or GPI functions, which makes everything nice and
simple.
Using the title bar this way is also helpful during debugging. One
application I wrote used two object windows to manage two separate DDE
conversations. Object windows, by definition, are not visible, and mine had
no need to be visible since they never interacted with the user. But when I
debugged them, I made their title bars visible and displayed text in them
that revealed their internal states (for example, "Waiting for Reply").
The CPUMET client window does exist, it's just hidden. To hide the client
window, CPUMET creates the standard window by calling WinCreateStdWindow.
hwndFrame = WinCreateStdWindow (HWND_DESKTOP, WS_VISIBLE,
&flFrameFlags,
szClientClass,
NULL, 0L, NULL, NULL,
&hwndClient) ;
The frame creation flags that affect the client window (such as
FCF_SIZEBORDER, FCF_MINMAX, FCF_HORZSCROLL) are not used. Since the client
window isn't used, there's no need to maximize it or change its size. CPUMET
doesn't use menus or an icon either, so the FCF_MENU and FCF_ICON flags
aren't used, and the hIcon and hMenu parameters are NULL.
CPUMET next calls WinSetWindowPos.
WinSetWindowPos (hwndFrame, HWND_TOP, 0, 0,
WinQuerySysValue (HWND_DESKTOP,SV_CXSCREEN)/3,
WinQuerySysValue (HWND_DESKTOP,SV_CYTITLEBAR),
SWP_SHOW | SWP_SIZE | SWP_MOVE);
This positions the window in the lower left of the screen. Using a width of
SV_CXSCREEN/3 and a height of SV_CYTITLEBAR, the frame window is one third
the width of the screen and is just high enough to show the title bar.
CPUMET uses three threads, which is unusual for such a tiny program. But
because of the way the OS/2 scheduler works, you'd have difficulties using
fewer.
The OS/2 Scheduler and Threads
The OS/2 scheduler starves threads. A priority-based scheduler, it always
assigns the CPU to the highest-priority runnable thread.
A thread is classified and runs in one of three priority classes: high
priority, general priority, or low priority. These are also known as
time-critical, normal, and idle-time, respectively. The special properties
associated with them make CPU load calculations possible.
The scheduler runs the highest priority thread for as long as it needs. It
doesn't matter how many other threads are waiting. They will not get the
CPU-they will starve-if a higher priority thread is running. (In
multiple-CPU systems, the scheduler may execute the n highest priority
threads.)
The scheduler is rude, too. It will cut right into the middle of a thread's
time slice and yank the CPU away if another thread with a higher priority
becomes ready.
I'd like to clear up some misconceptions about time slices. There are two
kinds in OS/2: those you specify with the TIMESLICE directive in the
CONFIG.SYS file and real time slices. I'll distinguish between them by
calling the first a "TIMESLICE" and the second a "time slice."
Real time slices are 31.25 milliseconds (msec) long. This is the length of
time between two consecutive ticks of the system clock. With every tick, the
scheduler sees if another thread with a higher priority is ready. If one is,
the scheduler will preempt the current thread in favor of the higher
priority one. When a thread gets a time slice, it's getting one of 31.25
msec duration. A thread can be preempted in the middle of a time slice, too,
if it calls an API function and a higher priority thread has become ready to
run since the last clock tick.
TIMESLICES become important only when several threads of equal priority are
all ready to run and are all CPU-bound. Then each thread will run for a full
TIMESLICE before control is passed to the next thread of the same priority.
Note that the value of TIMESLICE must be an integral number of 31.25 msec
time slices.
Calculating the Load
In OS/2, an idle-time thread gets CPU cycles only if higher priority threads
aren't available. This is the key to calculating the load. CPUMET simply
uses an idle-time thread to soak up CPU cycles when no other threads are
running. The busier the CPU is (with other threads), the fewer CPU cycles
the idle-time thread will get.
For example, suppose the idle-time thread is getting 10 percent of the total
CPU cycles every second and the rest of the time the scheduler runs other
threads. By this definition, the CPU load is 90 percent. The trick is to
record the CPU cycles/second the idle-time thread gets somehow. CPUMET does
this indirectly, using the global variable lCount and two threads.
One thread that I'll call the counting thread runs idle-time and continually
increments lCount. The other thread, the timing thread, runs time-critical
and resets lCount to zero once every second. The idle-time thread function
is CountingThread, shown below.
VOID CountingThread ()
{
DosSetPrty (PRTYS_THREAD, PRTYC_IDLETIME, 0,
tidCounting);
while (TRUE) lCount++;
}
This thread is created when the message queue thread calls _beginthread in
the main function.
tidCounting = _beginthread (CountingThread,
iCountingThreadStack, THREADSTACKSIZE,
NULL);
First, the counting thread calls DosSetPrty to change its priority to
idle-time.
By default, a thread inherits the priority of its creator thread, which in
this case is the message queue thread. The message queue thread, in turn,
inherited its general-priority class from the thread in the parent process
(the PM shell) that called DosExecPgm to launch CPUMET.
Next, the counting thread drops into an infinite loop.
while (TRUE) lCount++;
Any CPU cycles this thread now receives will be used to increment lCount.
This is the way to measure (indirectly) the CPU cycles/second the
idle-thread gets.
To get the "per second," you need the timing thread. The timing thread
resets the value of lCount once per second. It is created by the message
queue thread with the following call.
tidTiming = _beginthread (TimingThread,
iTimingThreadStack
THREADSTACKSIZE, NULL);
The first thing the timing thread does is change its priority to
time-critical. The reason for this will be discussed in a moment. Next, it
drops into an infinite loop.
while (TRUE)
{
DosSleep (1000L);
WinPostMsg (hwndClient, WM_SEM1,
MPFROMLONG(lCount), NULL);
lCount = 0L;
}
Once per second (every 1000 msec), the timing thread wakes up, posts the
message queue thread a WM_SEM1 message with lCount in the mp1 parameter,
resets lCount to zero, then goes back to sleep. This continues until CPUMET
terminates.
The timing thread runs time-critical to guarantee it will get the CPU when
it wakes up (unless another time-critical thread is running). The thread has
to be put to sleep with DosSleep until then so it doesn't hog the CPU.
DosSleep is one of those API calls that allows the scheduler to preempt the
running thread immediately (in this case the timing thread) and dispatch the
next highest priority runnable thread (see Figure 3).
Usually when you have a global variable like lCount that is modified by two
or more threads, you protect the variable with a semaphore. But in this case
no damage results if, in the highly unlikely event, the counting thread
increments lCount between two DosSleep calls in the timing thread.
Note that WinPostMsg uses the WM_SEM1 message. WM_SEM1 messages aren't
stored in the message queue; instead, they are handled like WM_TIMER
messages. Multiple WM_SEM1 messages are consolidated into one, ensuring that
only the most recent lCount is retrieved and displayed.
The message queue thread extracts lCount from the WM_SEM1 message and calls
WinSetWindowText to display it in the title bar. You can tell how busy the
CPU is by looking at the value in the title bar. The lower the number, the
busier the CPU.
Too Much of a Good Thing
Only one instance of CPUMET must run at a time, because extra instances will
load the CPU just as any other app would. Any CPU cycles that a single
instance of CPUMET would have soaked up would then be divided equally among
the instances. Because each counting thread has the same priority, the
scheduler runs them on a round-robin basis (this is where TIMESLICE comes
into
play). For n instances, each counting thread will compute
lCount/n counts per second.
If CPUMET were a Windows application, you could use the hPrevInstance
parameter in WinMain to detect multiple instances. Unfortunately, PM has no
equivalent. But it has something Windows lacks-system semaphores.
Upon entry to main, before doing anything else, CPUMET tests for the
existence of another instance with the following statement:
if ( DosCreateSem(CSEM_PUBLIC, &hSem, "\\sem\\cpumeter.sem") )
DosExit (EXIT_PROCESS, 0) ;
The function DosCreateSem will attempt to create the system semaphore
"\sem\cpumeter.sem". The first instance will succeed, but subsequent
instances will fail and exit via DosExit.
This is a somewhat untraditional role for a semaphore. It matters not one
iota if the semaphore is set or clear, only that it exists. The existence of
the semaphore represents the existence of a single instance of CPUMET.
Of course, we have to destroy the semaphore when CPUMET terminates, which is
why we need to store the handle in the global variable HSYSSEM hSem.
The semaphore is destroyed in the ClientWndProc with the following code:
case WM_DESTROY:
{
DosCloseSem (hSem) ;
return 0 ;
}
This allows the next instance to recreate the semaphore and run.
CPUMETE
CPUMET calculates the CPU load. However, there is a problem with using
lCount to represent it, due to the inverse relationship between lCount and
the CPU load. The lower the value of lCount, the higher the load. It would
be better to display an amount that varies in direct proportion to the load.
Another problem is that lCount's value is dependent on a computer's CPU
speed. A faster computer will calculate a higher value for lCount than a
slower one, everything else being equal.
I solved both problems in CPUMETE.EXE by introducing the variable lCountMax,
which represents the maximum value lCount can attain, and a new thread to
calculate lCountMax. This allows you to display the CPU load as a percentage
of maximum load. Let's see how CPUMETE accomplishes this (see Figure 4).
Adding a Calibration Thread
If you know the maximum value lCount can be for a given computer, you can
calculate CPU usage as a percentage of this maximum with the following
simple equation.
load = [1 - (lCount/lCountMax)] * 100%
The trick is to calculate lCountMax, which is done by the calibration
thread. The calibration thread function looks almost identical to the
counting thread function.
VOID CalibrationThread ()
{
DosSetPrty (PRTYS_THREAD, PRTYC_TIMECRITICAL, 30,
tidCalibration);
while (TRUE) lCountMax++;
}
The differences are that it uses lCountMax instead of lCount, and it runs
time-critical. Running time-critical guarantees the thread will get the
maximum CPU cycles possible (assuming there are no other time-critical
threads competing for the CPU). Notice that the DosSetPrty call uses 30 for
the sChange parameter.
Within a priority class, a thread is further distinguished by its relative
priority level, ranging from 0 to 31. If two threads belong to the same
priority class, the one with the higher level will get the CPU before the
other, all else being equal. The calibration thread has a priority level of
30, one fewer than the maximum. You need to reserve level 31 for a higher
priority thread so it can preempt the calibration thread after it runs for
one second. The thread doing the interrupting is the message queue thread.
Following is the sequence of code added to the main function for this
purpose.
DosSetPrty (PRTYS_THREAD, PRTYC_TIMECRITICAL, 31, 1);
tidCalibration=_beginthread(CalibrationThread,
iCalibrationThreadStack,
THREADSTACKSIZE, NULL) ;
DosSleep (1000L);
DosSuspendThread (tidCalibration);
DosSetPrty (PRTYS_THREAD, PRTYC_REGULAR, 0, 1);
You identify the message queue thread with an ID of 1 in the DosSetPrty
call. Thread IDs come on a first-come, first-serve basis, starting with 1.
Don't expect your other n threads to follow in sequential order as 2, 3, . .
., n1. Some API calls generate captive threads-that is, captive within a
DLL. Although you didn't explicitly create these threads, your process will
assign thread IDs to them. The rule here is to not assume thread ID values;
use the thread ID generated from a _beginthread or DosCreateThread call.
After setting the message queue thread's priority, CPUMETE creates the
calibration thread. Although it runs time-critical, the calibration thread
will not run yet because the message queue thread has a higher priority
level of 31. But then DosSleep is called, knocking out the message queue
thread for 1000 msec. During this time, the calibration thread becomes the
highest priority runnable thread in the system and gets all the CPU cycles.
The calibration thread gets preempted after one second, when the message
queue wakes up. By this time, lCountMax is calculated. The calibration
thread is no longer needed and is put to sleep by DosSuspendThread. This
freezes the thread; the scheduler will ignore it until it is unfrozen.
In general, one thread cannot kill another thread. A thread can kill only
itself or the entire process, by calling DosExit:
DosExit (fTerminate, usExitCode) ;
and using EXIT_THREAD or EXIT_PROCESS for the fTerminate parameter. Since
the calibration thread doesn't call DosExit, the best you can do is freeze
it from another thread. Finally, the message queue thread resets its
priority so it doesn't hog the CPU.
CPUMETE's title bar is more informative than CPUMET's (see Figure 5).
Instead of just displaying lCount like CPUMET did, it displays three values
in the following format:
x / y -> z%
where x represents lCount, y represents lCountMax, and z represents the
load. Occasionally, x will exceed y. This does not represent a serious
error, but rather inaccuracies associated with the 31.25 msec resolution of
the system clock.
Adding the Tachometer
CPUMETE uses a set of eleven icons to display the CPU load when the app is
iconized.
HPOINTER hIcon [11] ;
I created these icons with the Icon Editor. The hIcon array is initialized
in the WM_CREATE code,
case WM_CREATE:
{
for (i=0; i<=10; i++)
hIcon [i] = WinLoadPointer (HWND_DESKTOP,
NULL, (100+i));
return 0;
}
and the icons are released in the WM_DESTROY code.
case WM_DESTROY:
{
for (i=0; i<=10; i++) WinDestroyPointer(hIcon[i]);
DosCloseSem(hSem);
return 0;
}
CPUMETE uses hIcon[0] when the CPU is at 0 percent, hIcon[1] at 10 percent,
and so on, up to hIcon[10] at 100 percent. When the message queue thread
receives a WM_SEM1 message, it extracts lCount from the mp1 parameter and
calls the nearest_10_percent function.
i = nearest_10_percent (LONGFROMMP (mp1));
This function uses the load equation presented earlier and returns an
integer from 0 to 10, in increments of 1, corresponding to a CPU load of 0
percent to 100 percent. The return value i is used in the WM_SETICON message
sent to the frame window to change the icon.
WinSendMsg (hwndFrame, WM_SETICON, hIcon [i], NULL) ;
To add icons, I needed to add the FCF_MINBUTTON and FCF_ICON frameflags to
the WinCreateStdWindow call in main. I also changed the HICON parameter to
ICON00 to represent an initial CPU load of 0 percent. Finally, I had to
change the make file to accommodate the CPUMETE.RC resource file.
CPUMETER
CPUMETE's icons are useful for showing the instantaneous CPU load, but it
would be better to see the load over a period of time. That would give us a
clearer picture of the demands an application places on the CPU. What
CPUMETE needs is a histogram.
CPUMETER (see Figure 6), the final version of the program, displays a
histogram of the CPU load in its client window (see Figure 7). The histogram
is a simple bar graph, consisting of black vertical bars on a white
background. The graph is updated each second. Each bar is one pixel wide,
which means a client window 400 pixels wide can display up to 400 bars. On a
VGA display, about 11 minutes of load history can be displayed in a
maximized window. Each bar's height is directly proportional to the CPU load
it represents and is scaled to the client window height. For example, given
a client window that is 376 pixels high, a bar representing a 100 percent
load will be 376 pixels high, a 50 percent load will be 188 pixels high, and
so on. When the client window is resized vertically, the bars are resized
vertically too.
The histogram logic is contained in the file HISTGRAM.C (see Figure 8). This
file contains just five functions, corresponding to the five messages
explicitly processed by ClientWndProc: create_histogram (WM_CREATE),
size_histogram (WM_SIZE), paint_histogram (WM_PAINT), update_histogram
(WM_SEM1), and destroy_histogram (WM_DESTROY).
The histogram is created when ClientWndProc processes the WM_CREATE message.
WM_CREATE :
o
o
o
create_histogram (hab, hwnd) ;
The hab and hwnd variables are used by create_histogram to get a handle,
hPS, to a micro presentation space (micro PS) by calling WinOpenWindowDC and
GpiCreatePS.
hdc = WinOpenWindowDC (hwndClient) ;
hPS = GpiCreatePS (hab, hdc, ...) ;
Then hPS is used to set the foreground and background colors and the line
type for the PS:
GpiSetBackColor (hPS,...);
GpiSetColor (hPS,...);
GpiSetLineType (hPS, ...);
A micro PS is a good choice for this application. With a cached micro PS,
CPUMETER would have to reset the colors and line type each time it
reacquired a handle to the PS (via GpiBeginPaint or GpiGetPS). Using a micro
PS is more efficient because it retains its state information between
GpiBeginPaint calls. Using a normal PS would be overkill because CPUMETER is
only outputting to the screen; it doesn't need a normal PS's capability to
reassociate another display context, such as a printer DC, with itself.
Also, there's no need for the normal PS segments.
The histogram's main data structure is an array of ints. Each element in the
array corresponds to one bar in the histogram. There must be enough elements
to accommodate a maximized client window. This is determined by calling
WinQuerySysValue.
iArraySize = WinQuerySysValue (HWND_DESKTOP,
SV_CXSCREEN) ;
What CPUMETER needs is an array like the following.
INT anIntegerArray [iArraySize] ;
But because iArraySize is determined at run time, the array is allocated
dynamically, rather than statically. This is done by using the calls to
WinCreateHeap and WinAllocMem shown below.
hHeap = WinCreateHeap (0,0,(sizeof(INT)*iArraySize),
0,0,0) ;
npArray = (PINT) WinAllocMem (hHeap,(sizeof(INT)*iArraySize));
WinCreateHeap sets up the heap in the automatic data segment. This is one
difference between PM and the Microsoft Windows[tm] graphical environment.
In Windows2, a local heap "comes standard" with your application, but in PM
it's an option your application has to order.
The variable npArray points to the first element in the array. If you feel
uncomfortable with dynamic arrays, you can still use the familiar notation
npArray[i] to reference element i. Each array element stores a CPU load,
ranging in value from 0 to 100. When the client window receives a WM_PAINT
message and calls paint_histogram, the histogram bars are redrawn. Each bar
is scaled to the client window's height by scaling the array element value.
ptl.y = (npArray[i] / 100.) * yClientHeight ;
When CPUMETER terminates, the destroy_histogram function releases the micro
PS and frees the heap. The heap, being on the automatic data segment, would
be destroyed by OS/2 if CPUMETER didn't do it. But it's a good habit to
clean up after yourself.
To accommodate the histogram, the FCF_MINMAX and FCF_SHELLPOSITION flags
were added to the flFrameFlags variable in main. The FCF_SHELLPOSITION flag
tells PM to give our client window an initial size and position, so I
removed the call to WinSetWindowPos that followed WinCreateStdWindow in
CPUMET and CPUMETE.
Finally, you need the new function nearest_1_percent to compute the CPU load
to the nearest 1 percent. This is used in the code that processes WM_SEM1
messages in ClientWndProc to update the histogram.
i = nearest_1_percent (...);
o
o
o
update_histogram (i) ;
Conclusion
CPUMETER is fun to watch and instructive. If you have access to the sample
programs in Programming the OS/2 Presentation Manager (Microsoft Press,
1989), try running CPUMETER with the samples from Chapter 17. You'll
discover that multithreaded programs make better use of the CPU than their
single-threaded counterparts. That is, the multithreaded applications load
the CPU more heavily than their single-threaded versions do. CPUMETER shows
this quite clearly.
On a warm summer night, start up CPUMETER, then bring up your favorite
program and take it for a spin. And if you get the urge. . .redline it!
1As used herein, "OS/2" refers to the OS/2 operating system jointly
developed by IBM and Microsoft.
2For ease of reading, "Windows" refers to the Microsoft Windows graphical
environment. "Windows" refers solely to this Microsoft product and is not
intended to refer to such products generally.
Learning Windows[tm] Part III: Control Windows and MDI Support
Marc Adler
Learning Windows is a series of articles that explores programming the
Microsoft Windows[tm] graphical environment Version 3.0. In the previous two
articles I discussed the event-driven, message-passing nature of Windows1,
initialization of an application, resource files, menu systems, and
accelerators. In addition, I designed the skeleton of the sample
stock-charting application. This article explains the types of control
windows that Windows supports. I will also continue developing the
stock-charting application by adding Multiple Document Interface (MDI)
support.
Let's begin by reviewing the term window. A window is a data object that has
certain properties associated with it. It may occupy an area on the screen,
but it doesn't have to. It can be invisible during the entire course of an
application. (One use for a hidden window is to act like a "traffic cop,"
monitoring the activity in the application and processing messages. In OS/2
Presentation Manager, these windows are known as object windows.)
A window's appearance and behavior is determined mostly by its window
procedure (WinProc). Windows sends messages to a WinProc whenever something
"interesting" happens to the window it is associated with. A default window
procedure supplied by Windows, DefWindowProc, has a default action for every
message sent to a window. DefWindowProc defines the behavior of a generic
window. To give a different behavior or appearance to a window or an object
within it, some or all of these messages must be handled by code in your
WinProc, instead of DefWindowProc.
For instance, you can process mouse messages to tell an on-screen control
how to react when the control is clicked. Imagine implementing a custom
light switch control, where a switch is toggled up and down when clicked on.
When the mouse is clicked, the program should determine whether the switch
is in the up or down position. Then the program should redraw the switch in
the opposite position. This means intercepting the WM_LBUTTONDOWN message,
negating the value of the state variable that is internal to this control,
and redrawing the control, possibly using a different bitmap to represent
the on and off positions. By writing varying amounts of message-processing
code yourself, you can make a window act in any conceivable way.
Controls
Control windows are objects that assist a user in interacting with an
application. Predefined Windows controls include scroll bars, radio buttons,
check boxes, push buttons, list boxes, combo boxes, edit fields, and static
text objects. You can design your own custom control windows, or purchase
others from several third-party manufacturers. As Windows becomes more
popular, you will see more custom control libraries appear on the
marketplace, especially because Windows 3.0 introduces owner-draw controls.
(For more about custom controls, see "Extending the Windows[tm] 3.0
Interface with Installable Custom Controls," MSJ, Vol. 5, No. 4.)
There are two ways to create controls. You can use the CreateWindow function
(and, in Windows Version 3.0, the function CreateWindowEx) or you can use
the dialog editor and define controls as part of a dialog box. Although the
latter method is more common, let's begin by discussing CreateWindow.
The first article ("Learning Windows[tm] Part I: The Message-based
Paradigm," MSJ, Vol. 5, No. 4) showed you how to use CreateWindow to create
the stock-charting application's main window. Two parameters of the
CreateWindow function are of particular interest: the name of the window
class and the handle to the parent window.
In the Windows environment, windows can have a parent-child relationship.
Child windows appear inside the window that owns them, known as the parent.
Child windows are clipped to the parent window's client area (the main,
initially empty space of the window-the part that is not the caption bar,
window border, menu bar, and scroll bars), meaning that any part of the
child window extending past its parent window's borders will not be
displayed. Certain behavioral aspects are inherited by the child from the
parent. If a parent window is hidden or destroyed, its children are hidden
or destroyed as well. The same thing goes for the children of the child
windows, and so on. When a window is moved, any children are moved along
with it. For control windows, the most important aspect of the parent-child
relationship is that when certain events happen to a control window, it
notifies its parent window of the event by sending it a WM_COMMAND message.
(A scroll bar control sends a WM_VSCROLL or WM_HSCROLL message.)
In the stock-charting application, the main window did not have a parent.
You can see this by examining the hParent parameter of the CreateWindow
function that created it. If the hParent parameter is NULL, the window has
no parent. A window without a parent is called a top-level window.
(Actually, the top-level window is a child of the desktop window, but this
does not ever affect the Windows programmer.)
Control windows are almost always children of another window. So, when you
use CreateWindow to create a control, you must specify the handle of a
window as its parent.
As far as the class name of a control goes, Windows predefines a number of
control window classes. A list of the control class names and the types of
controls that you can create in each class is found in Figure 1.
Using CreateWindow
When you use CreateWindow to create a control, you must give one of the
strings in Figure 1 as the class name if you are using a predefined control
class. Often you must specify one or more flags for the third argument of
the CreateWindow call. The following call creates a push button control.
hButton = CreateWindow("Button",
"&OK",
BS_PUSHBUTTON | WS_CHILD | WS_TABSTOP,
100, 75, 32, 16,
hWndMain,
IDOK,
hInstance,
(LPSTR) NULL);
This call has several interesting features. First, note the ampersand in the
button's text, which indicates an accelerator. Second, the BS_PUSHBUTTON
flag is one of of the style bits, which informs Windows that you wish to
create a push button. Figure 2 contains a complete list of the flags you can
use for each window class. The WS_CHILD flag tells Windows that this is a
child window and to use hWndMain as the parent.
Finally, note that the value of the hMenu parameter is IDOK. IDOK, a value
defined in WINDOWS.H, is usually used for OK buttons. When you create a
child window, the value in the hMenu parameter must be the control
identifier of the window, not a handle to a menu. Windows overloads the menu
handle and the control identifier fields, so that when you create a
top-level window, the value of this parameter is considered the menu handle;
when you create a child window, this value is considered the control
identifier.
The control identifier is a number that differentiates a child/control
window from any other at that moment in your application. Because it is used
in many of the messages passed between the parent and the control and in
some Windows functions (such as CheckRadioButton), a control identifier
should be unique.
Dialog Boxes and Controls
Dialog boxes will really be covered in the next article. However, control
windows and dialog boxes are inextricable, so I am forced to digress a
little.
A dialog box is a window containing a number of control windows. It can be
created on-the-fly, or, more commonly, defined in a resource file and loaded
as needed. A dialog box can be modal or modeless. When a modal dialog box is
displayed, the user cannot switch to another window in the same program. He
or she must respond to the dialog box in some way, often by clicking an OK
or Cancel push button. The user can switch to another program, unless the
dialog box is a system modal dialog box, which must be responded to before
the user does anything else in Windows. When the user brings up a modal
dialog box, its parent window is disabled and the dialog box manager (the
Windows code that creates the dialog box window and processes its messages)
goes into its own internal message loop. The user must fill in the controls
and accept or cancel the dialog box before the application continues with
its own message loop. Modeless dialog boxes impose no restrictions on the
user, who can move the
focus on and off the modeless dialog box freely before responding to it.
Two of the more important functions of the dialog box manager are processing
keystrokes and assigning the input focus to the appropriate control. For
example, when you press Tab or Backspace, the input focus should move to the
next or previous control. When you press Enter, the default push button
control should be activated. When you press Alt with another key, the dialog
box manager must see if that keystroke corresponds to the accelerator
associated with a particular control.
If you use CreateWindow to create control windows instead of going through
the dialog box manager, your application must handle keystrokes itself and
decide if they cause a change of focus. This is a non-trivial task better
left to the dialog box manager.
Static Controls
In this article, assume that all control windows (except scroll bars) are
associated with dialog boxes.
A static control differs from other types of controls in two respects. A
static control cannot have the input focus, so you cannot give it keyboard
or mouse input. Also, a static control does not notify its parent when an
interesting event occurs.
Static controls are used mainly to display text strings and rectangular
frames. For typical static controls, see any dialog box containing an edit
control: the text labeling the edit control is a static control window. It
may seem strange to have an entire window structure (and therefore, a
WinProc) behind simple text strings, but Windows offers maximum flexibility
and extensibility for even the simplest objects.
A style flag is associated with each type of static control. For hollow
frames, you can specify SS_BLACKFRAME, SS_GRAYFRAME, or SS_WHITEFRAME. For
solid rectangles, you can specify SS_BLACKRECT, SS_GRAYRECT, or
SS_WHITERECT. Text can be displayed left-, center-, or right-justified by
specifying SS_LEFT, SS_CENTER, or SS_RIGHT. The SS_SIMPLE style is used to
display a simple rectangle with a single line of left-justified text. To
disable the ampersand processing that associates an accelerator with a
static control, specify the SS_NOPREFIX style. Finally, you can draw your
own static controls using the SS_USERITEM style.
Edit Controls
Windows supports both single-line and multiline data entry controls.
Actually, you can fashion a simple text editor just by creating a standard
Windows multiline edit control-it's that powerful. The default capabilities
give you rudimentary cursor motion (left, right, up, down, home/end of line,
home/end of buffer, next/previous word) and cutting and pasting. In Windows
3.0, edit controls interpret tab stops correctly. Of course, the mouse is
fully supported. A multiline edit control will also automatically perform
wordwrapping.
A multiline edit control has the ES_MULTILINE style. You can attach vertical
and/or horizontal scroll bars
by specifying the ES_AUTOVSCROLL and ES_AUTOHSCROLL styles. If you attach
scroll bars to an edit control, the edit control processes its own scroll
bar messages.
In Windows 3.0, the ES_LEFT, ES_CENTER, and ES_RIGHT styles function
properly. These flags tell the edit control to left-, center-, or
right-justify the text within the edit window (see Figure 3).
Windows is still weak in its data verification capabilities. Subclassing
edit controls is a popular pastime among developers who need this kind of
power. Windows 3.0 implements three new edit styles that ameliorate this
somewhat. The ES_LOWERCASE and ES_UPPERCASE styles transform keyboard input
into all lowercase or all uppercase characters. The ES_PASSWORD style
directs an edit control to display a user-defined masking character instead
of the normal text. The default masking character is an asterisk, but you
can change this by using the EM_SETPASSWORDCHAR message. Hopefully the need
for formatted data entry fields will be addressed in future versions of
Windows.
Edit Control Messages
The edit control supports a great number of messages. Some of these messages
direct the edit control to perform an editing operation on the text, while
others query the state of the control.
Most edit control messages begin with the prefix EN_. The four messages
without this prefix direct an edit control to perform a cut-and-paste
action. All text cut, copied, or pasted from an edit control is sent to the
Windows Clipboard.
The WM_CUT message copies the highlighted text to the clipboard and then
deletes the text from the edit control. The WM_CLEAR message simple deletes
the text. The WM_PASTE message inserts the text in the clipboard at the
current editing position. The WM_UNDO control reverses the last editing
operation. An edit control is limited to only one undo. In these messages,
the wParam and lParam are not used, so you can use 0 as their values.
Two of the more frequently used edit control messages deal with querying and
setting the selected area of text. The EM_GETSEL message returns the
starting position of the selected text in the LOWORD of the return value and
the position of the end of the selection plus 1 in the HIWORD of the return
value. If no text is selected, the current cursor is returned in the LOWORD.
DWORD ulRet = SendMessage(hEdit, EM_GETSEL, 0, 0L);
WORD iStart = LOWORD(ulRet);
WORD iEnd = HIWORD(ulRet);
The EM_SETSEL message sets the area of the selected text. The starting and
ending positions of the selection are contained in lParam. If the LOWORD of
lParam is 0 and the HIWORD is 7FFFH, the entire contents of the edit field
will be selected.
SendMessage(hEdit, EM_SETSEL, 0, MAKELONG(0, 0x7FFF));
You can replace the selected text with another piece of text using the
EM_REPLACESEL message. In this message, lParam is a far pointer to the text
string that will be inserted in place of the selected text.
If the edit control is in a dialog box, you can use two functions to
retrieve and set the contents of an edit control. All you need to know is
the handle of the dialog box and the control identifier of the edit control.
GetDlgItemText copies a specified number of characters from the edit control
into a user-defined buffer. SetDlgItemText sets the contents of an edit
control a certain string. Here is a function that changes the contents of an
edit control to uppercase:
GetDlgItemText(hDlg, ID_EDIT,(LPSTR) szBuf,
sizeof(szBuf)-1);
strupr(szBuf);
SetDlgItemText(hDlg, ID_EDIT,szBuf);
There are many more edit messages that may be of use with a multiline edit
control. Refer to the Microsoft Windows Software Development Kit (SDK) for a
complete description.
The notification messages in Figure 4 are generated by an edit control when
certain events occur and are sent to the edit control's parent window.
Button Controls
A button is a generic term for a control that generates a message and
possibly changes its appearance when a user clicks on it (see Figure 5).
Button controls include check boxes, radio buttons, push buttons, and group
boxes. A group box doesn't respond when you click on it; it is used only to
contain a group of one of the other kinds of buttons.
When a button is clicked, it sends one or more WM_COMMAND messages to its
parent window. The WM_COMMAND message notifies the parent that an
interesting event has happened involving a button. The wParam of the message
is set to the control identifier of the window. The LOWORD of lParam is set
to the window handle of the control, and the HIWORD is set to the
notification code. The notification code tells exactly which event occurred,
and depending on this information, you might have to dismiss a dialog box or
simply tell Windows to alter the state of a button.
In addition, Windows supports owner-draw buttons. These are buttons the
programmer draws. Windows sends owner-draw buttons a message when it needs
to be drawn in both its pressed and unpressed states. You can have a lot of
fun drawing buttons that resemble real objects. For instance, a musical
Windows application could implement buttons shaped like the ones on stereo
equipment.
Push Buttons
Push buttons are rectangular objects containing descriptive text in the
center. Unlike check boxes and radio buttons, which are mainly used to show
the state of an object visually, push buttons invoke action. When the user
clicks a push button, it usually means that the user wants to perform an
activity, such as dismissing a dialog box or activating a help window.
There is only one style of push button, indicated by the BS_PUSHBUTTON flag.
A push button can have a special property. If a user presses Enter in a
dialog box, the dialog box manager will determine if there is a default push
button control in the dialog box. If there is, the dialog box manager will
simulate its pressing; in other words, the dialog box manager will generate
a WM_COMMAND message with the identifier of the default push button in the
wParam.
Two Windows messages deal with default push buttons. The DM_GETDEFID message
asks a dialog box if there is a default push button in it. The DM_SETDEFID
message lets you make any push button control the default. The following
code determines if the OK button is the current default button in the dialog
box. If it is, it makes the CANCEL button the default push button.
DWORD ulRet = SendMessage(hDlg, DM_GETDEFID, 0, 0L);
if (HIWORD(ulRet) = = DC_HASDEFID && LOWORD(ulRet) = = IDOK)
SendMessage(hDlg, DM_SETDEFID, IDCANCEL, 0L);
Check Boxes
A check box is a button control with two parts: a small rectangular frame
that is either empty or contains a check mark, and a text string describing
the button positioned on the right side of the frame. Check boxes indicate
properties that are either on or off. For example, in a word processing
program, a check box allows the user to turn wordwrap on or off.
A standard check box has either the BS_CHECKBOX or the BS_AUTOCHECKBOX
style. Windows automatically toggles the check state of a check box with the
auto style when the user clicks on it. If it does not have the auto style,
Windows sends a WM_COMMAND message to the check box's parent when the check
box is clicked. Then CheckDlgButton must be called to check or uncheck the
check box.
A variation of the standard check box control is the three-state check box.
In addition to a checked/unchecked state, a three-state check box has a
grayed state when disabled. A three-state box has either the BS_3STATE or
the BS_AUTO3STATE style. Other than that, they are the same as standard
check boxes.
Two API functions exist for setting and querying the state of a check box.
CheckDlgButton sets the state of a standard check box or a three-state check
box. Its format is shown below.
CheckDlgButton(HWND hDlg, int idButton, WORD wCheck);
The handle of the dialog box that contains this check box is hDlg, and
idButton is the control identifier of the check box. wCheck is 0 if the
button is unchecked, 1 if the button is checked, and for three-state
buttons, 2 if it is grayed. The CheckDlgButton function simply determines
the window handle of the check box and sends a BM_SETCHECK message to the
check box's WinProc.
The state of a check box can be determined by using the IsDlgButtonChecked
function.
IsDlgButtonChecked(HWND hDlg, int idButton);
This function returns 0 if the check box is not checked, 1 if it is, and 2
for grayed three-state buttons.
Radio Buttons
Picture a car radio from the 1970s, before they went digital. There were
usually five button presets. When you pressed one button, all the others
would pop out. The Windows radio button class works the same way. Radio
buttons always come in groups of two or more. When the user clicks one, the
button is on, and all the other radio buttons in that group are off.
A standard radio button is defined with the BS_RADIOBUTTON style. Like check
boxes, there is a BS_AUTORADIOBUTTON style where Windows takes care of the
checking and unchecking for you.
The CheckDlgButton and the IsDlgButtonChecked functions set and test the
state of a radio button. You can also use the CheckRadioButton function to
check one button in the group.
CheckRadioButton(HWND hDlg, int idFirst, int idLast,
int idCheckButton);
The second and third arguments of this function specify the control
identifiers of the first and last radio buttons in a group. The fourth
argument gives the control identifier of the radio button that you want to
check. For this function to work effectively, all the radio buttons in the
group should be numbered consecutively from idFirst to idLast.
CheckRadioButton enumerates all the controls with identifiers in the range
of idFirst to idLast, and sends BM_SETCHECK messages to them with wParam set
to 0. Then it sends a BM_SETCHECK message with wParam set to 1 to the radio
button passed in the last argument.
Group Boxes
Group boxes aren't really buttons, as I said previously. Simple rectangles
used to enclose a group of buttons, group boxes are more like static text
frames than buttons. They cannot accept input, nor can they generate
WM_COMMAND messages, so they are used mainly for visual aesthetics or as a
place marker for the end of a button group.
List Boxes
A list box is a control window containing a list of strings (in Windows 3.0,
it can be a list of anything able to be displayed, but I will concentrate on
strings for the time being). The user selects one or more strings from the
list box by highlighting a string or by clicking on it. If all the strings
cannot fit in the window, vertical or horizontal scroll bars can be used to
scroll through the strings.
Windows 3.0 beefed up the capabilities of the list box control. A list box
can now grow horizontally instead of vertically so it can have more than one
column. Any kind of item can be displayed in a list box, not just strings.
List boxes now can be tabbed to align columns. An extended list box can be
created, in which certain key combinations select multiple items. In fact,
the Windows SDK contains a program called OWNCOMBO that demonstrates some of
these new capabilities. You should take a few minutes to compile and run it.
The available list box styles are shown in Figure 6.
By default, a list box supports a single string selection and the strings
are unsorted. To sort the strings, you must specify the LBS_SORT style. You
should also specify the LBS_NOTIFY style if you want the list box to send
WM_COMMAND messages to its parent when something interesting happens. The
WINDOWS.H file defines the LBS_STANDARD style; it's simply a combination of
LBS_SORT and LBS_NOTIFY.
Now that I have defined a list box, I want to be able to add strings to it.
To add a string, send a message to the list box with the lParam set to a far
pointer to the string. There are two messages used to add strings,
LB_ADDSTRING and LB_INSERTSTRING. LB_ADDSTRING appends a string to the end
of the list box. If the list box is sorted, LB_ADDSTRING inserts the string
in the sorted position.
SendMessage(hListBox, LB_ADDSTRING, 0,
(DWORD) (LPSTR) "Hello");
LB_INSERTSTRING inserts a string at any specified position in the list box.
The zero-based position passed in the wParam will be adhered to, even if the
list box is sorted. If you want to put the string at the end of the list
box, you must pass 1 in wParam.
SendMessage(hListBox, LB_INSERTSTRING, 12,
(DWORD) (LPSTR) "Hello");
Windows allocates a 64Kb global memory block for each list box. This means
that the sum of the lengths of all
of the strings in a list box cannot be greater than 64Kb.
If you exceed this limit, the LB_INSERTSTRING or LB_ADDSTRING message
returns the value of LB_ERRSPACE to your application.
To delete a string from the list box, use the LB_DELETESTRING message and
pass the zero-based index of the string in wParam.
SendMessage(hListBox, LB_DELETESTRING, 5, 0L);
Unfortunately, no single message in Windows allows you to change the
contents of a string. You must delete the string and then reinsert it.
If you append or insert strings in a visible list box, an annoying
phenomenon will take place. The list box refreshes itself after each string
is added-not an aesthetically pleasing sight for the user. To avoid this,
you have to tell the list box not to refresh itself until all of the strings
have been added. The WM_SETREDRAW message sets or clears the list box's
auto-refresh flag. Before you start inserting strings, call SendMessage with
the third parameter set to FALSE.
SendMessage(hListBox, WM_SETREDRAW, FALSE, 0L);
It is now safe to insert strings. After the last string
has been added, reenable the refresh flag by calling SendMessage with the
third parameter TRUE.
SendMessage(hListBox, WM_SETREDRAW, TRUE, 0L);
Once the strings are in the list box and the user interacts with it, a rich
set of messages is available to query the current state of the list box. The
LB_GETCURSEL message returns the index of the currently selected item in a
single-selection list box. If you would like to retrieve the text associated
with that item, use the LB_GETTEXT message.
int iSel;
char szBuf[128];
if ((iSel = SendMessage(hListBox, LB_GETCURSEL, 0,
0L)) != LB_ERR)
SendMessage(hListBox, LB_GETTEXT, iSel, (DWORD)
(LPSTR) szBuf);
To change the current selection to a different string, send the LB_SETCURSEL
message with wParam set to the zero-based index of the string to select. To
search for
a string containing a certain prefix, you can use the LB_FINDSTRING message.
To do both of these things, use LB_SELECTSTRING, which finds a string with a
given prefix and makes it the current selection.
Multiple-Selection List Boxes
In a multiple-selection list box, more than one string can be selected at a
time. Suppose you want to present the user with a list of files to delete.
It would be a hassle to select one file, press delete, select another, press
delete again, and so on. Instead, using a multiple-selection list box, the
user could mark several files, and press delete once.
Although most of the list box messages can be used for both single- and
multiple-selection list boxes, several do not overlap. The LB_GETCURSEL and
LB_SETCURSEL messages can only be used with single-selection list boxes,
since there cannot be a single currently selected item in a
multiple-selection list box. Instead, you use the LB_GETSEL and LB_SETSEL
messages to query and set the selection state of an item.
To use these messages effectively, you may need to know how many items are
in the list box. You can determine the number of entries in the list box by
sending the LB_GETCOUNT message. Once you have the number of items, you can
march down the list box and query or set the selection state of each item.
The following fragment of code reverses the selection state of every item in
the list box.
int nItems, i;
BOOL bisSelected;
nItems = (WORD) SendMessage(hListBox, LB_GETCOUNT, 0,
0L);
for (i = 0; i < nItems; i++)
{
bIsSelected = (BOOL) SendMessage(hListBox,
LB_GETSEL, i, 0L);
SendMessage(hListBox, LB_SETSEL, !bIsSelected,
(DWORD) i);
}
Two new Windows 3.0 messages help in the programming of multiple-selection
list boxes. LB_GETSELCOUNT returns the number of items selected within a
multiple-selection list box. LB_GETSELITEMS fills a buffer with an array of
integers that are the indices of all of the selected items. The next
fragment of code demonstrates these two messages.
int nItems;
nItems = (WORD) SendMessage(hListBox, LB_GETSELCOUNT,
0, 0L);
if (nItems != LB_ERR)
{
HANDLE haItems = LocalAlloc(LMEM_MOVEABLE,
nItems * sizeof(int));
LPINT lpaItems = (LPINT) LocalLock(haItems);
SendMessage(hListBox, LB_GETSELITEMS, nItems,
(DWORD) lpaItems);
o
o
o
}
The new proportionally spaced system font in Windows 3.0 may disarray
Windows 2.x applications that relied on the old monospace font to line up
the contents of a list box. However, the new tab stop support can be used to
line up columnar data. To do so, you must give the list box the
LBS_USETABSTOPS style. The default position for tab stops is one every 32
horizontal dialog box units. (A horizontal dialog box unit is 1/4 of the
dialog base width unit, a figure based on the width of the current system
font.) To set your own tabs, you can use the new LB_SETTABSTOPS message.
Multiple-Column List boxes
Windows 3.0 supports list boxes that scroll horizontally or vertically, but
not both. A list box that scrolls horizontally is called a multiple-column
list box and is defined using the LBS_MULTICOLUMN style. Each column in the
list box is given a default width of 15 characters. To change the column
width, use the LB_SETCOLUMNWIDTH message. The new width is passed in wParam.
Unfortunately, you cannot have variable width columns-but then again, if you
did, there would be nothing to wish for in future versions of Windows.
List Box Notification Codes
Like any control, a list box notifies its parent window when certain things
happen to it. Remember that the notification process occurs only if the list
box has the LBS_NOTIFY or LBS_STANDARD styles. A list of these notification
codes is found in Figure 7.
The LBN_SETFOCUS and LBN_KILLFOCUS messages, new to Windows 3.0, inform the
parent when the focus is gained or lost by a list box. The LBN_SETFOCUS
message can be used, say, to highlight a default selection when the user
tabs into a list box.
The LBN_SELCHANGE message is used frequently when another control tracks the
selected text in a list box. For example, almost all Windows file-directory
dialog boxes have both a list box containing the file, directory, and drive
names, and an edit control in which you enter the name of a file. In most
cases, if the focus is set to the list box, the text of the currently
selected item is displayed in the edit control. To do this, you need to know
when the current selection in the list box changes. The LBN_SELCHANGE
message is just the thing you need.
Each time the list box selection changes, a WM_COMMAND message is sent to
the dialog box with the LBN_SELCHANGE notification code in the HIWORD of the
lParam. You can then retrieve the text of the currently selected item using
the code shown earlier, and set the contents of the edit control to that
text with the SetDlgItemText function.
The LBN_DBLCLK message is generated when the user double clicks on a list
box entry. Double-clicking usually means that the user wants to select a
string and terminate the dialog box.
Owner-Draw List Boxes
The previous article discussed owner-draw menu items, in which Windows gives
total responsibility to the application for displaying a menu item. This
concept is carried over to list box controls, so that the user can choose
anything in a list box control, not just alphanumeric text strings. This
enables more powerful Windows user-interfaces-you can have a picture of an
object rather than a descriptive text string. If the stock-charting
application supported different types of graphs, it could prompt the user
for the type of graph to display by showing a picture of the type of graph
within a list box, not just a text string describing the graph. A miniature
picture of a scatter graph would be much more useful than the words "Scatter
Graph."
To create an owner-draw list box, you must give your
list box the LBS_OWNERDRAWFIXED or the LBS_OWNERDRAWVARIABLE styles. A list
box created with the LBS_OWNERDRAWFIXED style assumes that all items in the
list box will be of uniform height, while a list box created with the
LBS_OWNERDRAWVARIABLE must know the height of every item. If you wanted to
fill a list box with strings, with each string drawn in a different font
(maybe to query the user for a font style selection), you would give the
list box the LBS_HASSTRINGS style in addition to one of the two styles
mentioned above.
Assume that you created an owner-draw list box and want to add items to it.
You can use the regular LB_ADDSTRING or LB_INSERTSTRING functions. However,
the lParam does not have to be a far pointer to a string, unless the list
box has the LBS_HASSTRINGS style. It can be any user-defined 32-bit value.
This value can be a handle to a bitmap, an index into an array, or anything
that can be used to map a list box item to a graphical object.
Consider the stock-charting application. I would like to give the user a
choice of grid styles to use when drawing the grid lines of a graph. To draw
the grid lines, I use lines created with different Windows pens. (A pen is a
bitmap that is used to draw a line.) Windows has a solid pen and several
dashed pens. An owner-draw list box answers my needs perfectly. Instead of
trying to describe the pen styles with a text string, I can create a list
box in which each item is a line drawn with a different pen.
The first thing I do is create a list box using the LBS_OWNERDRAWFIXED
style, since the size of each line will be the same. Then I add the items to
it. Each item has a user-defined value associated with it, which are the
Windows-defined values for each pen style.
/* Fill the list box with pen IDs */
int iPen;
for (iPen = 0; iPen < PS_NULL; iPen++)
SendMessage(hPenListBox, LB_ADDSTRING, 0, (LONG) iPen);
What if an owner-draw list box had to be in some kind of order (that is, it
has the LBS_SORT style)? The list box control does not know how to sort
graphical objects. Fortunately, Windows 3.0 has a new message,
WM_COMPAREITEM, which is sent to the parent window of the list box whenever
the list
box needs to sort two objects. When this message is
sent, the value of lParam is a far pointer to a COMPAREITEMSTRUCT data
structure. Among the elements of this structure are the indices and the
user-defined values of the two items being compared. One of three possible
values will be returned: 0 if the objects are equal, 1 if item 1 sorts
before item 2, and 1 if item 1 sorts after item 2.
Windows needs to know the size of each item in an owner-draw list box. To
determine this, the WM_MEASUREITEM message is sent to the parent of the list
box. If a list box has the LBS_OWNERDRAWFIXED style, this message is sent
only once, since all items
will be the same size. However, if it has the LBS_OWNERDRAWVARIABLE style,
this message is sent once for each item in the list box. The value of lParam
for the WM_MEASUREITEM message is a far pointer to a MEASUREITEMSTRUCT data
structure. The previous article showed the details of the MEASUREITEMSTRUCT
data structure, so I won't go into it here.
Assume that each item in the pen-style list box has a width of 42 pixels and
a height of 20 pixels. The following code processes the WM_MEASUREITEM
message.
case WM_MEASUREITEM :
{
LPMEASUREITEMSTRUCT lpMI = (LPMEASUREITEMSTRUCT) (LPSTR) lParam;
lpMI->itemWidth = 42;
lpMI->itemHeight = 20;
break;
}
Finally, to draw each item, Windows sends the list box's parent the
WM_DRAWITEM message. I'm borrowing a little code from the OWNCOMBO SDK demo
program to show how to process this message (see Figure 8). I need to
be able to draw a list box item in its normal, unselected state. I also have
to draw an item in its selected state, and if the item is in a
multiple-selection list box, I need to draw the item in its focus state.
(When a standard list box text string has the focus, Windows draws a dashed
frame around the string.)
When an item is deleted from an owner-draw list
box, Windows sends the parent window the WM_DELETEITEM message. In this
message, lParam points to a DELETEITEMSTRUCT data structure. This structure
contains the index and the user-defined value corresponding to that item.
WM_DELETEITEM gives the user a chance to free resources associated with that
item (such as a bitmap or a brush).
Stock-ChartingApplication
The stock-charting application's controls will really come into play once I
have covered dialog boxes in the next installment. But since I do want to
add functionality to the application in this article, and since "A New
Multiple Document Interface API Simplifies MDI Application Development," MSJ
(Vol. 5, No. 4) is probably still fresh in your mind, I put an MDI shell
over the application. MDI really suits this application well. Each stock
graph can be considered a separate MDI document, and the application as a
whole seems to fall nicely into the user-interface scheme supported by the
MDI specification (see Figure 9).
I tied the File New menu item to the MDI child creation routine. When this
menu item is chosen, a new MDI child window is created and placed at a
default position within the client window. There is nothing displayed in the
child windows yet; as I develop the application further, I will fill these
windows with the stock graphs.
The Window pull-down menu allows you to position the MDI children in certain
ways on the screen (see Figure 10). You can automatically tile or cascade
the child windows by choosing the Window Tile and the Window Cascade menu
selections. Any window can be iconized; the icons are automatically arranged
nicely at the bottom of the MDI client window. You can close all the
children at once by choosing the Window Close All item.
Some of the MDI code was shamelessly borrowed from the MULTIPAD demo program
that comes with the SDK. There are many good demos and programming
techniques available for examination in the SDK. I strongly suggest that if
you are new to Windows, or if you want to catch up on some of the new
features in Windows 3.0, you study some of this code.
The first change in the new version of the stock-
charting application is new logic in the main loop to recognize the MDI
accelerator keys. So, before you call
the TranslateAccelerator function, you need to call
TranslateMDISysAccel. If any of these functions return a nonzero value, the
keystroke was translated.
if (!TranslateMDISysAccel(hwndMDIClient, &msg) &&
!TranslateAccelerator(hWndMain, hAccelTable,
&msg))
The second change was registering a window class for the MDI child windows.
Nothing in this code reveals that these child windows will be MDI children.
/* Register MDI chld cls */
wc.lpfnWndProc = GraphWndProc;
wc.hIcon = LoadIcon(
hThisInstance,
MAKEINTRESOURCE(ID_GRAPH));
wc.lpszMenuName = NULL;
wc.cbWndExtra = CBWNDEXTRA;
wc.lpszClassName = "GraphWindow";
if (!RegisterClass(&wc))
{
return FALSE;
}
The third task was creating the MDI client window. This needs to be done
when the main window is created. An easy way to do this is to create the MDI
client window in response to the WM_CREATE message sent to the main window.
The WM_CREATE message informs the WinProc that a window of a particular
class was just created and informs the programmer that initialization of
that window should be done. You fill in the two elements that comprise the
MDI CLIENTCREATESTRUCT data structure and call CreateWindow to create the
MDI client.
case WM_CREATE:
{
CLIENTCREATESTRUCT ccs;
/* Find window menu where children will be listed */
ccs.hWindowMenu = GetSubMenu(GetMenu(hWnd), 3);
ccs.idFirstChild = ID_WINDOW_CHILDREN;
/* Create the MDI client filling the client area */
hwndMDIClient = CreateWindow("mdiclient",
NULL,
WS_CHILD |
WS_CLIPCHILDREN |
WS_VSCROLL|WS_HSCROLL,
0,0,0,0,
hWnd,
0xCAC,
hThisInstance,
(LPSTR) &ccs);
ShowWindow(hwndMDIClient, SW_SHOW);
You also need to add logic to the main window's WinProc to process some
commands that deal with MDI. This code tells the MDI client to tile,
cascade, and close the MDI children and to arrange the iconized windows.
Be aware that the default WinProc for a window containing an MDI client
window is not DefWindowProc. Instead you must call DefFrameProc to handle
all of the default message processing.
default :
/*
We might have chosen to change the focus to
one of the MDI children.
*/
return DefFrameProc(hWnd, hwndMDIClient, msg,
wParam, lParam);
The final change needed for MDI support is to create an MDI child window in
response to the user choosing the File New menu item. Process the WM_COMMAND
message in the WinProc for the main window; if wParam is the identifier
ID_NEW, call GraphCreateWindow.
All that GraphCreateWindow does is fill out an MDICREATESTRUCT data
structure and send a WM_MDICREATE message to the MDI client window (see
Figure 11). The MDICREATESTRUCT contains information about the MDI child's
class, caption title, dimensions, instance handle, and style.
The WinProc for an MDI child window has no special processing associated
with it, except that you must call DefMDIChildProc instead of DefWindowProc
when you do not want it to process a message.
default:
/* Again, since the MDI default behavior is a
* little different,call DefMDIChildProc instead
* of DefWindowProc()
*/
return DefMDIChildProc(hWnd, msg, wParam, lParam);
That's all there is to it! You just added MDI to the stock-charting
application with a few statements. In previous versions of Windows, MDI was
extremely complicated, but Windows 3.0 makes it much easier.
The next article concludes the discussion of control windows, exploring
combo boxes and scroll bars. The main topic will be the use of dialog boxes,
with a brief discussion of some of the third-party tools that can assist you
in creating a dialog box. I will integrate our knowledge of controls with
dialog boxes and add these to the stock-charting application.
1For ease of reading, "Windows" refers to the Microsoft Windows graphical
environment. "Windows" refers only to this Microsoft product and is not
intended to refer to such products generally.
Figure 1. Windows Control Styles
■
■ Scrollbar
Horizontal or vertical scroll bar
■ Edit
Single- or multiline edit field
■ Listbox
Single- or multiple-selection list box
■ Combobox
Simple, drop-down, or drop-down list combo box
■ Static
Left-, centered, or right-justified text strings, frames
■ Button
Push button, radio button, check box, group box
■ MDIClient
Multiple Document Interface client window (this is not a control
window)
■
────────────────────────────────────────────────────────────────────────────
Figure 2. Window Styles
Style
Meaning
DS_LOCALEDIT
Specifies that edit controls in the dialog box will use memory in the
application's data segment. By default, all edit controls in dialog
boxes use memory outside the application's data segment. This feature
may be suppressed by adding the DS_LOCALEDIT flag to the STYLE command
for the dialog box. If this flag is not used, EM_GETHANDLE and EM_
SETHANDLE messages must not be used since the storage for the control
is not in the application's data segment. This feature does not affect
edit controls created outside of dialog boxes.
DS_MODALFRAME
Creates a dialog box with a modal dialog-box frame that can be combined
with a title bar and system menu by specifying the WS_CAPTION and
WS_SYSMENU styles.
DS_NOIDLEMSG
Suppresses WM_ENTERIDLE messages that Windows would otherwise send to
the owner of the dialog box while the dialog box is displayed.
DS_SYSMODAL
Creates a system-modal dialog box.
WS_BORDER
Creates a window that has a border.
WS_CAPTION
Creates a window that has a title bar (implies the WS_BORDER style).
This style cannot be used with the WS_DLGFRAME style.
WS_CHILD
Creates a child window. Cannot be used with the WS_POPUP style.
WS_CHILDWINDOW
Creates a child window that has the WS_CHILD style.
WS_CLIPCHILDREN
Excludes the area occupied by child windows when drawing within the
parent window. Used when creating the parent window.
WS_CLIPSIBLINGS
Clips child windows relative to each other; that is, when a particular
child window receives a paint message, the WS_CLIPSIBLINGS style clips
all other overlapped child windows out of the region of the child
window to be updated. (If WS_CLIPSIBLINGS is not given and child
windows overlap, it is possible, when drawing within the client area of
a child window, to draw within the client area of a neighboring child
window.) For use with the WS_CHILD style only.
WS_DISABLED
Creates a window that is initially disabled.
WS_DLGFRAME
Creates a window with a double border but no title.
WS_GROUP
Specifies the first control of a group of controls in which the user
can move from one control to the next by using the arrow keys. All
controls defined with the WS_GROUP style after the first control belong
to the same group. The next control with the WS_GROUP style ends the
style group and starts the next group (that is, one group ends where
the next begins). Only dialog boxes use this style.
WS_HSCROLL
Creates a window that has a horizontal scroll bar.
WS_ICONIC
Creates a window that is initially iconic. For use with the
WS_OVERLAPPED style only.
WS_MAXIMIZE
Creates a window of maximum size.
WS_MAXIMIZEBOX
Creates a window that has a maximize box.
WS_MINIMIZE
Creates a window of minimum size.
WS_MINIMIZEBOX
Creates a window that has a minimize box.
WS_OVERLAPPED
Creates an overlapped window. An overlapped window has a caption and a
border.
WS_OVERLAPPEDWINDOW
Creates an overlapped window having the WS_OVERLAPPED, WS_CAPTION,
WS_SYSMENU, WS_THICKFRAME, WS_MINIMIZEBOX, and WS_MAXIMIZEBOX styles.
WS_POPUP
Creates a pop-up window. Cannot be used with the WS_ CHILD style.
WS_POPUPWINDOW
Creates a pop-up window that has the WS_BORDER, WS_POPUP, and
WS_SYSMENU styles. The WS_CAPTION style must be combined with the
WS_POPUPWINDOW style to make the system menu visible.
WS_SYSMENU
Creates a window that has a system-menu box in its title bar. Used only
for windows with title bars.
WS_TABSTOP
Specifies one of any number of controls through which the user can move
by using the Tab key. The Tab key moves the user to the next control
specified by the WS_TABSTOP style. Only dialog boxes use this style.
WS_THICKFRAME
Creates a window with a thick frame that can be used to size the
window.
WS_VISIBLE
Creates a window that is initially visible. This applies to overlapped
and pop-up windows. For overlapped windows, the Y parameter is used as
a ShowWindow function parameter.
WS_VSCROLL
Creates a window that has a vertical scroll bar.
Figure 4. Edit Notification Messages
EN_CHANGE
Indicates that the user has taken some action that may have changed the
content of the text.
EN_ERRSPACE
Indicates that the edit control is out of space.
EN_HSCROLL
Indicates that the user has clicked the edit control's horizontal
scroll bar with the mouse; the parent window is notified before the
screen is updated.
EN_KILLFOCUS
Indicates that the edit control has lost the input focus.
EN_MAXTEXT
Specifies that the current insertion has exceeded a specified number of
characters for the edit control.
EN_SETFOCUS
Indicates that the edit control has obtained the input focus.
EN_UPDATE
Specifies that the edit control will display altered text.
EN_VSCROLL
Indicates that the user has clicked the edit control's vertical scroll
bar with the mouse; the parent window is notified before the screen is
updated.
Figure 7. List Box Notification Codes
LBN_DBLCLK
Sent when the user double-clicks a string with the mouse
LBN_ERRSPACE
Sent when the system is out of memory
LBN_KILLFOCUS
Indicates that a list box has lost input focus
LBN_SELCHANGE
Sent when the selection has been changed
LBN_SETFOCUS
Indicates that the list box has received input focus
Figure 8. The WM_DRAWITEM Message
case WM_DRAWITEM :
{
LPDRAWITEMSTRUCT lpdis = (LPDRAWITEMSTRUCT) (LPSTR) lParam;
switch (lpdis->itemAction)
{
case ODA_DRAWENTIRE:
DrawEntireItem(lpdis, -4);
break;
case ODA_SELECT:
HandleSelectionState(lpdis, 0);
break;
case ODA_FOCUS:
HandleFocusState(lpdis, -2);
break;
}
break;
}
/********************************************************************
*
* FUNCTION: HandleSelectionState(LPDRAWITEMSTRUCT, int)
*
* PURPOSE: Handles a change in an item selection state. If an item
* is selected, a black rectangular frame is drawn around
* that item; if an item is de-selected, the frame is
* removed.
*
* COMMENT: The black selection frame is slightly larger than the
* gray focus frame so they won't paint over each other.
*
*******************************************************************/
void FAR PASCAL HandleSelectionState(lpdis, inflate)
LPDRAWITEMSTRUCT lpdis;
int inflate;
{
RECT rc;
HBRUSH hbr;
/* Resize rectangle to place selection frame outside of the focus
* frame and the item.
*/
CopyRect ((LPRECT)&rc, (LPRECT)&lpdis->rcItem);
InflateRect ((LPRECT)&rc, inflate, inflate);
if (lpdis->itemState & ODS_SELECTED)
{
/* selecting item -- paint a black frame */
hbr = GetStockObject(BLACK_BRUSH);
}
else
{
/* de-selecting item -- remove frame */
hbr = CreateSolidBrush(GetSysColor(COLOR_WINDOW));
}
FrameRect(lpdis->hDC, (LPRECT)&rc, hbr);
DeleteObject (hbr);
}
/*******************************************************************
*
* FUNCTION: HandleFocusState(LPDRAWITEMSTRUCT, int)
*
* PURPOSE: Handle a change in item focus state. If an item gains
* the input focus, a gray rectangular frame is drawn
* around that item; if an item loses the input focus,
* the gray frame is removed.
*
* COMMENT: The gray focus frame is slightly smaller than the black
* selection frame so they won't paint over each other.
*
*******************************************************************/
void FAR PASCAL HandleFocusState(lpdis, inflate)
LPDRAWITEMSTRUCT lpdis;
int inflate;
{
RECT rc;
HBRUSH hbr;
/* Resize rectangle to place focus frame between the selection
* frame and the item.
*/
CopyRect ((LPRECT)&rc, (LPRECT)&lpdis->rcItem);
InflateRect ((LPRECT)&rc, inflate, inflate);
if (lpdis->itemState & ODS_FOCUS)
{
/* gaining input focus -- paint a gray frame */
hbr = GetStockObject(GRAY_BRUSH);
}
else
{
/* losing input focus -- remove (paint over) frame */
hbr = CreateSolidBrush(GetSysColor(COLOR_WINDOW));
}
FrameRect(lpdis->hDC, (LPRECT)&rc, hbr);
DeleteObject (hbr);
}
/********************************************************************
*
* FUNCTION : DrawEntireItem(LPDRAWITEMSTRUCT, int)
*
* PURPOSE : Draws an item and frames it with a selection frame
* and/or a focus frame when appropriate.
*
*******************************************************************/
void FAR PASCAL DrawEntireItem(lpdis, inflate)
LPDRAWITEMSTRUCT lpdis;
int inflate;
{
RECT rc;
HANDLE hOldPen;
HPEN hPen;
/* Resize rectangle to leave space for frames */
CopyRect ((LPRECT)&rc, (LPRECT)&lpdis->rcItem);
InflateRect ((LPRECT)&rc, inflate, inflate);
hPen = CreatePen((int) lpdis->itemData, 1, RGB(0, 0, 0));
hOldPen = SelectObject(lpdis->hDC, hPen);
MoveTo(lpdis->hDC, rc.left, rc.top + (rc.bottom - rc.top) / 2);
LineTo(lpdis->hDC, rc.right, rc.top + (rc.bottom - rc.top) / 2);
SelectObject(lpdis->hDC, hOldPen);
DeleteObject(hPen);
/* Draw or erase appropriate frames */
HandleSelectionState(lpdis, inflate + 4);
HandleFocusState(lpdis, inflate + 2);
}
Figure 11. MIDICREATESTRUCT
MDICREATESTRUCT mcs;
static int nChildren = 1;
if (!lpName)
{
/* If the lpName parameter is NULL, load the "Untitled" string
* from STRINGTABLE and set the title field of the MDI
* CreateStruct.
*/
sprintf(sz, "(Untitled - %d)", nChildren++);
mcs.szTitle = (LPSTR) sz;
}
else
{
/* Title the window with the supplied filename */
AnsiUpper(lpName);
mcs.szTitle = lpName;
}
mcs.szClass = "GraphWindow";
mcs.hOwner = hThisInstance;
/* Use the default size for the window */
mcs.x = mcs.cx = CW_USEDEFAULT;
mcs.y = mcs.cy = CW_USEDEFAULT;
/* Set the style DWORD of the window to default */
mcs.style = 0L;
/* tell the MDI Client to create the child */
hWnd = (WORD) SendMessage(hwndMDIClient,
WM_MDICREATE,
0,
(LONG) (LPMDICREATESTRUCT) &mcs);
return hWnd;
Extending Off-the-Shelf Windows Applications with Macros and DLLs
Bill Eisner
A new generation of software designed for sophisticated computing
environments with graphical user interfaces offers a remarkable degree of
customization. Products such as Microsoft Excel, Samna Ami Professional[tm],
and Microsoft Word for Windows allow you to develop personalized shortcuts,
alter menus, and change default characteristics.
In-house developers and consultants can often satisfy the needs of end-users
by tailoring and extending off-the-shelf products such as these, rather than
developing a software solution from scratch. Even when custom software is
needed, a high degree of integration between it and off-the-shelf products
is often required. A new generation of highly alterable and extendable
software simplifies
this task.
Although the techniques discussed here are illustrated using Microsoft Word
for Windows (hereafter "Word"), its BASIC macro language, and DLL support,
you can use similar facilities in a number of off-the-shelf Windows and OS/2
Presentation Manager environment-based applications. If you have a copy of
Word, you can type the examples here or download them from any MSJ bulletin
board.
Word supports alteration of its entire menu structure, modification of the
behavior of its keys, and "hot spots" that result in programmed actions when
clicked with a mouse. A variety of tools support these modifications,
including a document template facility; a dialog description language; a
generalized field capability; table structures; dynamic data exchange;
WordBASIC, a powerful macro language; and the ability to call dynamic-link
libraries (DLLs).
This article addresses the latter two features, WordBASIC and DLLs.
WordBASIC is a fairly comprehensive programming facility embedded in Word.
The development process for WordBASIC macros is facilitated by a macro
recorder, which creates a WordBASIC program directly from keyboard actions,
and an integrated debugging system that permits step-wise execution.
DLLs in particular offer an extraordinary degree of flexibility. A simple
DLL can be used to extend Word seamlessly or even replace existing
capabilities of software packages supporting DLL extensions.
DLLs
DLLs are familiar to Windows1 and OS/2 programmers. The applications
programming interface (API) and, in fact, most of the system code of these
environments is implemented as a collection of DLLs accessed by systems and
application software.
Conventional C programs contain statements in one source program that call
functions in other source programs or libraries of compiled programs.
Usually, a linker connects external calls from compiled objects to entry
points in other compiled objects or library routines. In a traditional
"static" environment, linking a compiled object to a library routine results
in compiled code from the library being added to the execution module by the
linker.
The Microsoft Windows Software Development Kit (SDK) introduced a new form
of linking that delays the resolution of some library linkages until run
time. This dynamic technique allows these library execution routines to be
shared, since the object code is not bound with individual software
packages.
By inventing DLLs, Windows developers created a new form of execution object
that resembles a conventionally linked program (so closely that DLLs used to
be identified with the EXE extension), but that has additional entry point
information so that individual functions in the library can be found.
DLLs are supported by a new type of library called an import library
(IMPLIB). An IMPLIB is used like a conventional library, except that when
working with an import library, the linker merely adds a reference to an
entry point in a DLL, instead of adding compiled code and data to the
executable.
The final linkage between the calling and the called objects within DLLs
typically takes place as the program is loaded. However, it is also possible
for a running
program to load a DLL and call one of its functions. Word exploits this to
allow WordBASIC programs to invoke DLL routines.
Application of DLLs
There are three types of DLLs that can be used to extend the capabilities of
a package such as Word: native operating environment DLLs (used via the
environment's standard API), commercial DLLs, and custom-built DLLs. Use of
the first type of DLL is illustrated in Figure 1, a simple WordBASIC
program. To execute this program, edit a new macro using the Macro Edit
command. Enter the short program in Figure 1, then press the Start button.
The Word title bar will be modified, demonstrating the power of the word
processor and Windows itself. A large majority of the API functions in the
Windows operating environment DLLs can be called directly from WordBASIC.
A more complex use of the Windows API functions is shown in Figure 2. This
macro invokes the Windows sound library to play music. If the character
string "cdeccdecef+
g-ef+g/gAgf-ec/gAgf-ec-cG+c-cG+c" is highlighted when the macro is invoked,
the Windows sound functions play "Frere Jacques."
You can call functions other than the Windows functions directly from
WordBASIC as well. Other APIs, such as the NETAPI.DLL functions that provide
the interface to Microsoft LAN Manager, can be called directly except when
the calls require data types not supported by
WordBASIC. The highly interactive nature of WordBASIC makes it useful for
exploring even complex API functionality.
Commercial DLLs, the second type of DLLs, are more "promise" than
"practice." Just as the wide acceptance of the C language has resulted in
the proliferation of a variety of static C libraries, it is highly likely
that the popularity of Windows will spawn commercial DLLs to expand its
capabilities. Although a few vendors use DLL facilities to support expanded
graphics, spell-checking, and other functionality, this commercial
opportunity has yet to be significantly exploited.
The opportunities for the third type, user-built DLLs, lie primarily in
environments where a large number of users are provided with an integrated
set of facilities to perform predefined tasks. Computing environments for
corporate PC users are often enhanced with company-built templates and
macros to simplify operation or to enhance functionality. While WordBASIC
provides a great deal of flexibility for customization, a custom-written DLL
provides even more, since all the features of the Windows environment can be
exploited from inside it.
Macros and DLLs
Since DLL coding requires greater programming expertise and represents a
substantial increase in programming investment over WordBASIC, it is
important to balance the use of these two tools. Because programs can be
developed and debugged more quickly in a macro language such as WordBASIC,
it is often the tool of choice. Another reason is WordBASIC's comprehensive
string handling, which can greatly simplify many typical operations even
though it is limited to 32Kb strings. Also, WordBASIC functions can access
the internal properties of Word and the documents in it directly. Until
call-back functions exist that let DLLs retrieve information from the
internals of Word, WordBASIC is the only reasonable means for accessing
selected data, bookmarks, glossary information, and other
word-processing-specific information. Therefore, even when DLLs are used,
they must cooperate with WordBASIC calling routines that can move data
between Word and the DLL.
DLLs are superior to macros in three areas: speed, storage, and control.
WordBASIC is interpreted, and executes even simple loops slowly. DLLs
written in C and compiled in machine language speed up routines that scan
and manipulate characters or perform complex mathematical calculations. The
performance enhancement is offset somewhat by the extra time required to
initiate a DLL routine, but long routines are substantially improved by
substituting a compiled language.
As for storage, the implementation of many programs' macro language limits
both program and data space. DLLs relieve these constraints through the
availability of medium model (multiple code segment) programming and access
to the global allocation routines of Windows.
Direct access to storage provides a great deal of flexibility for DLL
routines. For example, while WordBASIC supports only two fundamental data
types, real numbers and strings, DLLs coded in C can take advantage of a
variety of types, structures, and unions. Furthermore, storage allocated by
DLLs can be retained beyond the life of a single WordBASIC routine. One
simple way to maintain storage after exiting a DLL relies on the fact that
Windows will not release the storage of a DLL in use. If an initialization
routine within an AutoNew or AutoOpen WordBASIC macro invokes the Windows
LoadModule function with the name of the library, the data storage for the
library will be kept. A termination routine can clean up storage by calling
the FreeLibrary function.
Perhaps the greatest benefit from writing DLLs is the degree of control
available to the programmer. Dialog boxes are a good illustration. The
WordBASIC dialog facility is quite flexible, but it supports only a limited
user interface. It is not possible to change the contents of a combo box or
list box based on the selection of a radio button or a list item. The
NovaNew library routine (see Figure 3) demonstrates the adaptability of
DLLs. The
main function, NovaNew, replaces the standard FileNew
function of Word, which confronts the user with a
cryptic list of up-to-eight-character template names (see Figure 4).
NovaNew's dialog box groups the template names into logical categories such
as correspondence, legal,
labels, and so on (see Figure 5). If you select a category in the left list
box, NovaNew describes the document templates in another list.
The NovaNew function relies on description files with an LB extension that
are used as data for the list boxes (see Figure 6). This approach is quite
appropriate to the common corporate use of Word in which standardized
templates are provided for different types of documents by a central staff.
The creation of a new template requires the updating of these lists,
although you could write a simple WordBASIC macro to simplify that
operation.
The NovaNew Files
The NovaNew DLL consists of the usual Windows files: make, source, header,
link, module definition, resource, and dialog files. An additional file
needed for libraries, LIBENTRY.ASM, can be found in the Windows SDK. After
you install the Windows SDK, a C compiler, and a macro assembler, create the
library NOVANEW.DLL with the command:
nmake novanew
The library should be placed with the other DLLs in the Windows SYSTEM
directory.
Replacing FileNew with NovaNew
The FileNew routine is used to call the DLL routine. It relies on the fact
that each Word command is initiated by a WordBASIC program. Often, these
very simple programs merely call Word's internal functionality and can be
replaced by a user-built function. The FileNew program is installed in Word
by editing and saving the global macro "FileNew" resulting in a modification
to the file NORMAL.DOT. In this manner, any of Word's high-level functions
can be seamlessly replaced by WordBASIC routines, which can call DLLs.
To add the new FileNew, use the Macro Edit command to specify "FileNew" in
the Edit Macro Name dialog box. Replace the entire contents with the text
shown in Figure 7. Then use the File SaveAll command to save the new macro.
(Note that the substituted WordBASIC FileNew routine maintains the original
contents in a subroutine named OLD, so that you can regain the original
function while debugging. To revert back, simply change MAIN to NEW and OLD
to MAIN.)
The substituted FileNew routine declares the library's main function with
the following statement.
Declare Function NovaNew Lib "NovaNew.DLL"(Name$) As Integer
This statement identifies the function name, NovaNew; the library name,
NOVANEW.DLL; the function's argument type (in WordBASIC, a variable ending
in $ signifies a string); and the return type, Integer. Once declared, DLL
functions can be called using normal WordBASIC syntax, as shown below.
If(NovaNew(A$) = 1) Then ...
The NovaNew library function returns a 1 if successful and a 2 if the user
has pressed Cancel. Conventional Windows dialog boxes indicate success by
returning IDOK or IDCANCEL, which are defined in the Windows SDK as 1 and 2,
respectively. If the routine is successful, the string variable, A$, has
been filled by NovaNew with the name of the selected document template.
Before being used as an argument, the length of A$ must be established using
a BASIC statement such as the following.
A$ = String$(50," ")
Failure to specify a large enough string as an argument to a DLL will
typically result in a general protection fault.
NOVANEW.C
The main entry point in NOVANEW.DLL is the function NovaNew. NovaNew uses
the Windows function DialogBox to invoke the GET_TEMPLATE dialog and return
its results to WordBASIC. NovaNew relies on a small amount of trickery in
the use of the GetActiveWindow function. Typically, library routines that
manipulate windows will have a calling routine pass a window handle. Since
WordBASIC does not have such a window handle, you must assume that
GetActiveWindow returns the window handle of Word itself.
The heart of NOVANEW.C is the dialog function, fnNew. This function responds
to calls from the Windows dialog manager that indicate user interaction with
the dialog box. During initialization, a generalized function, FillListBox,
is called to fill the left list box with the contents of TEMPLATE.LB.
Whenever a left list box item is selected, the right list box is filled with
the contents of a file with the LB extension. The names of these files are
actually in the list boxes, hidden beyond the right edge of the window
frame: there are simply plenty of blanks between the visible entry and the
filename. The LastWord function extracts a filename from the list box entry
text. The ExtractTemplate function creates a template filename from the list
box entry and verifies that the template file exists whenever a right list
box item is double-clicked or the OK button is pressed.
The first two functions in NOVANEW.C, LibMain and WEP, come with the Windows
SDK. Both are specific to the creation of DLLs because they use a unique
initialization mechanism. LibMain is called by LIBENTRY.ASM when the library
is first invoked by the WordBASIC Declare statement. In NOVANEW.C, LibMain
retains the library's instance handle in a global variable so that it can be
used for Windows functions that require an hInstance. WEP is called when the
library is being removed from memory. Although it serves no useful function
in this library, it could be used to perform exit processing, such as the
freeing of allocated storage. Both functions return TRUE to indicate normal
execution.
SS != DS
Although NOVANEW.C looks like a normal Windows routine, it uses a unique
memory convention that requires special care when building a library or
converting existing Windows routines. In normal Windows programs, the stack
segment (SS) and the data segment (DS) are the same. When a DLL is called,
the DLL's data segment is used, but the stack is within the caller's data
segment. The implication of this convention is that you cannot legitimately
pass a short pointer to an object on the stack to a function. You must
either use functions that use long pointers or you must place the objects of
pointers in static or global storage. Usually, this convention interferes
only with the conventional use of C string functions such as strcpy.
Microsoft provides special versions of string functions in the SDK such as
lstrlen, lstrcpy, and lstrcat that use long pointers. The Microsoft C
compiler also has a switch, -Aw, that is used when compiling a DLL to verify
that the SS != DS convention has been followed.
All of the short pointer C functions can be used against static or global
objects, since they are located in the library's data segment. Consequently,
NOVANEW.C's string buffers are defined as global, allowing functions such as
strcpy and strcat to be used without concern. Using global arrays for large
objects is actually preferable when writing Word DLLs. Although Word
supports DLLs, it does not guarantee any specific amount of stack space for
library routine execution. Thus, it is possible to overflow the stack with
large automatic arrays or highly recursive routines. Allocating large arrays
in the data segment avoids this problem and has few adverse consequences in
the Windows environment Version 3.0.
NOVANEW.LNK and NOVANEW.DEF
The link control and module definition files used in constructing DLLs are
very similar to those used in making Windows programs with some notable
exceptions. The Windows SDK includes a set of libraries, such as SDLLCEW.LIB
and MDLLCEW.LIB, that are specific to the creation of DLLs. The LIBRARY
entry in the module definition file is also necessary to identify the type
of module properly. The EXPORTS section of the definition file identifies
entry points in the library. Any exported function in a DLL can be called
from WordBASIC.
Debugging a Word DLL
WordBASIC and Microsoft CodeView debugger for Windows make a very strong
debugging environment for DLLs. It is relatively simple to generate and
quickly modify a range of test cases for a DLL using WordBASIC statements.
Computed results can be presented using the Print statement, the MsgBox
WordBASIC function, or even using Insert text$, which will place the results
in the current file being edited. A bug in a DLL routine usually causes Word
(if not Windows itself) to fail, so it is essential that WordBASIC test
programs be saved frequently.
A misbehaving DLL routine can be best diagnosed using CodeView2. To use
CodeView requires that the DLL be compiled with the -Zi switch and linked
with the /CO option. CodeView also requires a machine with two monitors,
such as a VGA monitor combined with a monochrome monitor or an IBM 8514/A.
With that setup, it is possible to watch the source code and variables on
one monitor while running the program on another.
When debugging DLLs with Word, rather than specifying WINWORD.EXE as the
debugged object, it is usually more convenient to specify a simple program,
such as NOTEPAD.EXE, along with your DLL name. This can be done using the
Program Manager's Properties dialog box and an entry such as the following,
which specifies running CodeView on Notepad with NOVANEW as a DLL.
c:\windev\dbgtools\cvw.EXE /l e:system\novanew c:\windows\notepad.exe
With a setup such as this, CodeView can be started to debug the DLL at any
time, even after the DLL has been invoked.
Conclusion
Through the use of embedded macro languages and DLLs, systems developers and
integrators can add value to off-the-shelf software. A total end-user
solution can be constructed by combining canned applications and custom
programming. Often, a quality hybrid system can be implemented with less
effort than coding entirely new applications.
1 For ease of reading, "Windows" refers to the Microsoft Windows graphical
environment. Windows refers only to this Microsoft product and is not
intended to refer to such products generally.
2For ease of reading, "CodeView' refers to the Microsoft CodeView debugger
for Windows. CodeView refers only to this Microsoft product and is not
intended to refer to such products generally.
Figure 2. A Program Using the Windows Sound DLL
Declare Sub SetVoiceQueueSize Lib "sound"(voice As Integer, n As Integer)
Declare Sub OpenSound Lib "sound"(foo As Integer)
Declare Sub CloseSound Lib "sound"(foo As Integer)
Declare Sub StartSound Lib "sound"(foo As Integer)
Declare Sub StopSound Lib "sound"(foo As Integer)
Declare Sub SetVoiceNote Lib "sound"(nVoice As Integer, nValue As Integer,
nlength As Integer, nCdots As Integer)
Declare Sub SetVoiceSound Lib "sound"(nVoice As Integer, nFreq As Integer,
nDuration As Integer)
Declare Sub SetVoiceAccent Lib "sound"(nVoice As Integer, nTempo As Integer,
nVolume As Integer, nMode As Integer, nPitch As Integer)
Sub MAIN
Notes$ = "G1a2bc3d4ef5g6A7BC8D9EF0"
Song$ = Selection$()
voice = 1
OpenSound(1)
StopSound(1)
SetVoiceQueueSize(voice, 10 +(Len(song$) * 6))
Duration = 8
For i = 1 To Len(song$)
If Mid$(Song$, i, 1) = "+" Then
Duration = 4
ElseIf(Mid$(song$, i, 1)) = "-" Then
Duration = 8
ElseIf(Mid$(song$, i, 1)) = "/" Then
duration = 16
ElseIf InStr(Notes$, Mid$(Song$, i, 1)) <> 0 Then
SetVoiceNote(voice, InStr(Notes$, Mid$(Song$, i, 1)) + 19,
Duration, 0)
End If
Next i
StartSound(1)
REM song1 -- cdeccdecef+g-ef+g/gAgf-ec/gAgf-ec-cG+c-cG+c
REM song 2 - +cc-cd+e-edef+gg/CCCgggeee+c-gfed+c
REM song 3 -
End Sub
Figure 6. Template Description Files
■
■ TEMPLATE.LB
■
■
■ CORRESPONDENCE
CORRESPM
■ LEGAL
LEGAL
■ LABELS
LABELS
■ MISCELLANEOUS
MISCELLA
■ EXAMPLES
EXAMPLES
■ OTHER WINWORD
WINWORD
■
■ CORRESPM.LB
■
■
■ Normal Document
NORMAL
■ Plain Letter
PLNLETER
■ Letter
LETTER
■ Memorandum
MEMO
■ Envelope
ENVELOPE
■
■ LEGAL.LB
■
■
■ Legal Contract
CONTRACT
■ Legal Brief
BRIEF
■
■ LABELS.LB
■
■
■ 1-inch labels in one column, continuous
LBL1COLT
■ 1-inch labels in two columns, sheet feed
LBL2COL
■ 1-inch labels in two columns, continuous
LBL2COLT
■ 1-inch labels in three columns, sheet feed
LBL3COL
■ 1-inch labels in three columns, continuous
LBL3COLT
■
■ EXAMPLES.LB
■
■
■ Article for a Periodical
ARTICLE
■ Basic Template
BASIC
■ Technical Paper
PAPER
■ Report
REPORT
■ Brochure
BROCHURE
■ Data for a Brochure
BROCHDAT
■ Brochure Merge
BROCHMRG
■ Form Letter
FRMLTR
■ Template for Data Entry
DATADOC
■ Example Document
EXAMPLES
■
■ MISCELLA.LB
■
■
■ Independent Systems Vendor
ISV
■ Display of Keycaps
KEYCAPS
■
■ WINWORD.LB
■
■
■ PCWORD
PCWORD
■ SETUP
SETUP
■ PRICMEMO
PRICMEMO
■
Figure 7. The FileNew WordBASIC Routine
Declare Function NovaNew
Lib "NOVANEW.DLL"(Name$) As IntegerSub MAIN A$ = String$(50," ")
If(NovaNew(A$) = 1) Then A$ = Left$(A$, InStr(A$, ".") - 1) Super
FileNew .NewTemplate = 0, .Template = A$ End IfEnd SubSub OLDDim dlg As
FileNewGetCurValues dlgDialog dlgSuper FileNew dlgEnd Sub
Creating a Data-Entry and Reporting Library for Windows[tm] 3.0: Part II
Kevin P. Welch
The first article in this two-part series framed a multipage data-entry and
reporting library for the Microsoft Windows[tm] graphical environment. This
installment explores the actual form library, creates a sample form, and
demonstrates how it can be used with the form viewer.
The form library was written so that multipage data-entry forms and reports
could be created without C programming. A single multipage data-entry form
and report was encapsulated into one dynamic-link library (DLL). Then any
application using this form simply loads the library and calls the
appropri--ate editing or reporting API function. Since the actual
specification of the data-entry form and report is completely contained in
the resource file (RC) associated with the library, no programming is
required when you create a new data-entry form or report.
A form viewer application is also presented. This application enables you to
load a form library and experiment with data-entry or reporting functions.
Sample Library
To build the combined form viewer and data-entry library, you need to
compile the list of files in Figure 1. Then you can experiment. When you run
the form viewer application (VIEWER.EXE) using either the Program Manager or
the MS-DOS Executive, the window in Figure 2 is displayed.
The client area of the form viewer window is initially empty. Before you can
start entering data you need to load a form library (in this case FORM.DLL).
To do this, select the Form Open menu option, and the dialog box shown in
Figure 3 is displayed. In this dialog box you enter the name of the form
library you wish to use. When you press the OK button, the library is
automatically loaded, the caption of the viewer window is updated to
indicate the currently active library, and the input focus is set to the
client area of the viewer window.
The client area is actually a multiline edit control into which you can type
information to be used by the form library. As explained in the first
installment, each line in the data block should be in the following format:
<FIELDNAME> : <DATAVALUE>
The form library example presented in this article defines a product order
data-entry form (something that many readers might need). This library
supports a three-page data-entry form and simple report that contains the
field names shown in Figure 4.
Based on this list, you could enter information into the form viewer like
this:
NAME: Audrey Hepburn
COMPANY: Tiffany's
PRODUCT: Diamonds
When you have entered a few field names and data values, the client area of
the form viewer should look something like that shown in Figure 5.
Now that you have entered some initial information, bring up the first page
of the data-entry form by selecting Edit under the Form menu. Page1 of the
product order form is then created with the information you entered
displayed in the appropriate fields. The cursor is automatically positioned
inside the first data entry field and the field is described in the
lower-left corner of the dialog box.
You can move from field to field by pressing Tab (note the change in the
field description) while entering data. You can select a different page by
pressing the Page Down key, or by clicking the mouse inside one of the page
selection icons located in the lower-right corner of the dialog box (see
Figure 6).
When you are finished editing, save the information by pressing the Save
button. This creates an updated data block; the resulting changes will be
displayed inside the form viewer client area.
Similarly, you can produce a report based on the information inside the form
viewer by selecting Print under the Form menu. A print dialog box is
displayed while a report is sent to the default output device. To abort the
printing process, press the Cancel button inside the print dialog box (see
Figure 7).
Form Library
Now that you understand the operation of the form viewer and a sample
data-entry library, you can take a more detailed look at the way a form
library operates.
As described in the previous article, the specification of the form and
report is defined in the RC file associated with the library using two entry
points; FormEdit is used to edit data, and FormPrint is used to generate
reports.
HANDLE FormEdit( hWndParent, hData );
HANDLE FormPrint( hWndParent, hData );
The code that supports these functions is divided into five modules. The
first, FORM0.ASM, defines the assembly-language entry point to the library.
FORM1.C contains functions that are called when the library is loaded (see
Figure 8). FORM2.C contains the FormEdit function and most of the code to
display and manage a data-entry form. The next module, FORM3.C, contains the
FormPrint function and all the associated code to create a report. The last
module, FORM4.C, defines utility and support functions that are used
throughout the library.
Although these modules contain a great deal of source code, they can be
easily understood by tracing the flow of operations when the library is
first loaded, when a form is edited, and finally when the data is printed.
Figure 9 shows the hierarchy of functions in the forms library; Figure 10
lists all the functions on a file-by-file basis.
Initialization
The first time the library is loaded, the LibEntry function defined in
FORM0.ASM is called. LibEntry calls the FormInit function (defined in
FORM1.C) with the current library instance handle as the only parameter.
If this is the first instance of the library (determined by the current and
previous library instance handles), the FormInit function will initialize
the global FormInfo data structure. This data structure contains the form
name, the number of data-entry pages, the library instance handle, and other
global data. Some of this information is extracted from the FORM RCDATA
section of the resource file associated with the library. This is done by
using a sequence of FindResource, LoadResource, and LockResource function
calls. As is the case throughout the rest of the library, whenever an error
situation is encountered, a warning message is displayed listing the module
and line number where the error occurred.
Data Entry
The most complex operation supported by the form library is data entry. When
the FormEdit function in FORM2.C is called, it extracts the dialog box
template named Page1 from the resource file. From this template the
dimensions of the main data-entry dialog box are calculated. Then the
dimensions are passed to the system as part of a new, dynamically defined
dialog box template via a DialogBoxIndirect call. Before DialogBoxIndirect
is called, a system keyboard hook function is installed. This hook function
intercepts the Page Down and Page Up keystrokes that move the user to the
next or previous data entry pages.
All of the messages relating to the dialog box
that are created during the DialogBoxIndirect call are handled by
FormEditDlgFn. This function simply dispatches those messages of interest to
a series of utility functions. A WM_INITDIALOG message is received by
FormEditDlgFn when the dialog box is first created, resulting in a call to
the EditInit function. EditInit retrieves the dimensions of the data-entry
dialog box and creates the supporting controls that surround the data-entry
form pages defined in the resource file. The actual size and location of
these child windows is calculated using the base dialog units value, which
is found using the GetDialogBaseUnits function.
Once the supporting controls have been dynamically created, the EditInit
function extracts each dialog box and control template by successive calls
to the GetDlg and GetCtl functions. In the process, each control is created
(based on the extracted template), immediately hidden, its window handle
appended to the dialog box property list, and the initial data value
defined.
The initial data value is defined in the following manner. First the field
name associated with the control is retrieved from the string table via the
GetCtlName function and the control ID. If the control or field name is
successfully retrieved, the data associated with the field extracted (by the
GetCltData function) from the global formatted data block is passed to the
FormEdit function. If data exists, it is passed to the SetCtlData function,
which performs the actions necessary to initialize the control. Although in
this implementation the SetCtlData function supports only Button and Edit
control classes, it could be easily extended to support other classes.
All dialog box controls are hidden while the data entry fields are created
and defined. Only when the entire process is completed does the EditInit
function display the contents of the first page. The display is accomplished
by calling the EditDisplay function, which hides all controls associated
with the previous data entry page (initially, none), and displays all
controls associated with the new data entry page.
The next action performed by the FormEditDlgFn occurs when the WM_CTLCOLOR
and WM_USER messages are received. After the messages are received, the
EditControl function is called. This function defines the appropriate
control color (ignored in this implementation) and updates the comment field
associated with each control by using the GetCtlComment function.
Next, FormEditDlgFn calls the EditCommand function whenever a WM_COMMAND
message is received. This function either saves the dialog box contents
(using the GetDlgData function), prints the current form (using the
GetDlgData and FormPrint functions), cancels data entry, or displays a new
dialog box page.
The GetDlgData function (defined in FORM4.C) requires further explanation
because it processes each dialog box item, retrieving the actual data
associated with each control by calling the GetDlgCtlData function. Like the
SetCltData function, in this implementation the GetDlgCtlData function
supports only the Button and Edit control classes. Apart from this, the most
complex thing the GetDlgCtlData function does is the dynamic reallocation of
the resulting global data block into 32-byte chunks as data is retrieved
from each control.
When the WM_DESTROY message is received, FormEditDlgFn calls the EditDestroy
function, which cleans up the dialog box property lists and releases all
associated global data blocks.
Report Generation
Report generation from within a data-entry form or from an application is
performed when the FormPrint function (contained in FORM3.C) is called. This
function is interesting because it generates the desired report while
checking to see if the user wishes to cancel the entire process.
Prior to printing, the FormPrint function extracts the report template from
the library resource file and dynamically creates the report status dialog
box. As long as this status dialog box remains visible, the FormPrint
function assumes that the user wishes to continue printing. If the user
presses the Cancel button, the status dialog box is destroyed. Periodically
during the printing process the FormPrint function checks to see if the
status dialog box is still visible. If it has been destroyed, the printing
process is terminated.
The actual output of each report line is accomplished by successive calls to
the PrintOutput function. Using the GetCtlData function, this function
parses the output specification string to extract the desired field data
values from the global data block provided. If a requested field is
undefined, a NULL value is automatically substituted.
Conclusion
Using a text editor and the Dialog Editor in the Microsoft Windows Software
Development Kit, you can use the basic library structure presented here to
alter FORM.H, FORM.RC, and create new forms and reports quickly and easily.
Enhancements you might consider include the support of extended data types
such as dates and numbers; additional controls such as combo boxes, list
boxes, and scroll bars; enhanced reporting and data formatting; and on-line
help within each form library using the Windows help system. The most
ambitious upgrade would probably be to separate the resource portion of the
library from the actual code by parsing a RES file. Whatever your
refinements, a little customization should make FORM.DLL an important part
of your Windows toolbox.
────────────────────────────────────────────────────────────────────────────
Form Viewer (described in MSJ, Vol. 5, No. 5)
■ VIEWER
Make file
■ VIEWER.DEF
Module definition file
■ VIEWER.H
Header file
■ VIEWER.RC
Resource file
■ VIEWER.ICO
Icon
■ VIEWER.C
Source code
■
■ Page Selection Control (described in MSJ, Vol. 5, No. 4)
■
■ CONTROL.H
Installable control header file
■
■ PAGE
Make file
■ PAGE.DEF
Module definition file
■ PAGE.H
Public header file
■ PAGE.D
Private header file
■ PAGE.RC
Resource file
■ PAGE0.ASM
Entry point module
■ PAGE1.C
Intialization module
■ PAGE2.C
Message processing module
■ PAGE3.C
Style dialog box module
■ PAGE4.C
Information module
■ PAGE5.C
Style flags module
■
■ Form Library
■
■
■ FORM
Make file
■ FORM.DEF
Module definition file
■ FORM.H
Public header file*
■ FORM.D
Private header file
■ FORM.RC
Resource file*
■ FORM0.ASM
Entry point module
■ FORM1.C
Initialization module
■ FORM2.C
Form editing module
■ FORM3.C
Form printing module
■ FORM4.C
Misc. utility functions
■
■ *Only these files need changing when creating a new form library.
■
■
■ You will also need the following software development tools:
■
■ Microsoft C 5.1 or 6.0
■ Microsoft Macro Assembler
■ Microsoft Windows Software Development Kit 3.0
■ You can then create the form viewer and data-entry library by entering
the following commands:
■
■ MAKE VIEWER
■ MAKE PAGE
■ MAKE FORM
■
■ The result will be the following executables:
■
■ VIEWER.EXE
Form viewer application
■ FORM.DLL
Sample form library
■ PAGE.DLL
Page selection control (must be in your path)
■
Figure 4. Form Library Field Names
■ NAME
Customer name {first,initial,last}
■ COMPANY
Company name {optional}
■ ADDRESS
Company or customer street address
■ CITY
Company or customer city
■ STATE
Company or customer state
■ ZIPCODE
Company or customer postal code
■
■ PRODUCT
Name and version of product
■ SHIPVIA
Product shipment method
■ PAYMETHOD
Product payment method
■ CARDNUM
Credit card number {if used}
■ EXPDATE
Credit card expiration date {if used}
■
■ OPTION1
5.25 inch diskette option {TRUE or FALSE}
■ OPTION2
3.5 inch diskette option {TRUE or FALSE}
■ FRIEND
Customer heard about product from a friend
{TRUE or FALSE}
■ AD
Customer heard about product from an ad
■
{TRUE or FALSE}
■ SHOW
Customer heard about product at a tradeshow
{TRUE or FALSE}
■ DEALER
Customer heard about product from a dealer
■
{TRUE or FALSE}
■ REVIEW
Customer heard about product in a review
■
{TRUE or FALSE}
■ ARTICLE
Customer heard about product in an article
■
{TRUE or FALSE}
■ COMMENT
Misc. comments regarding customer order
■
Figure 10. Catalog of Form Library Functions ■
■ FORM0.ASM: Entry point module
■
LibEntry
■ FORM1.C: Initialization module
■
■
FormInit( hInstance ) : HANDLE
■
WEP( bExit ) : VOID
■
■ FORM2.C: Form editing module
■
■
FormEdit( hWndParent, hData ) : HANDLE
■
FormEditDlgFn( hDlg, wMessage, wParam, lParam ) : BOOL
■
■
EditInit( hDlg, wParam, lParam ) : BOOL
■
EditControl( hDlg, wParam, lParam ) : BOOL
■
EditCommand( hDlg, wParam, lParam ) : BOOL
■
EditDisplay( hDlg, wNewPage ) : VOID
■
EditDestroy( hDlg, wParam, lParam ) : BOOL
■
■ FORM3.C: Form printing module
■
■
FormPrint( hWndParent, hData ) : HANDLE
■
FormPrintDlgFn( hDlg, wMessage, wParam, lParam ) : BOOL
■
■
PrintInit( hDlg, wParam, lParam ) : BOOL
■
PrintCommand( hDlg, wParam, lParam ) : BOOL
■
PrintOutput( hPrnDC, hData, lpszPattern, wPage, wLine, wPixels ): BOOL
■
PrintDestroy( hDlg, wParam, lParam ) : BOOL
■
■ FORM4.C: Misc. utility functions
■
■
GetDlg( lpResData, lpDlgBox ) : BOOL
■
GetDlgData( hDlg ) : HANDLE
■
GetDlgCtlData( hDlg, hWndCtl, lpszData, wMaxData ) : BOOL
■
■
GetCtl( lpDlgData, wControl, lpDlgCtl ) : BOOL
■
GetCtlName( hInstance, wCtlID, lpszCtlName, wMaxCtlName ) : BOOL
■
GetCtlData( hData, lpszCtlName, lpszData, wMaxData ) : BOOL
■
GetCtlComment( hInstance, wCtlID, lpszComment, wMaxComment ) : BOOL
■
SetCtlData( hDlg, hWndCtl, lpDlgCtl, lpszData ) : BOOL
■
■
StringMatch( lpszString, lpszPattern ) : BOOL
■
StringCat( lpszDest, lpszSrce, wMaxDest ) : VOID
■
StringCopy( lpszDest, lpszSrce, wMaxDest ) : VOID
■
StringJoin( lpszDest, lpszSrce, wMaxDest ) : VOID
■
■
GetPrinterDC() : HDC
■
CenterPopup( hWnd, hWndParent ) : VOID
■
FormMsgHookFn( wContext, wCode, lData ) : WORD
■
Warning( hWndParent, lpszMessage, lpszFile, wLine ) : VOID
■
Questions & Answers - Windows
Q:
I'm writing a Windows application that I want to run single-instance-only,
and I'm having two problems. First, it appears that a user can circumvent
the WinMain
hPrevInstance argument mechanism (by which Windows informs an application
that a previous instance is already running) by simply renaming the
appplication's EXE file. I would like the user to be able to rename the EXE
to whatever he or she wants, but running an accidental second instance would
corrupt a dedicated set of files that the application uses. If I can't count
on hPrevInstance, what can I do?
Second, if I can successfully retrieve a previous instance's hPrevInstance,
how can I use hPrevInstance to make the previous instance of the application
the active application instead of starting a second instance? I've noticed
that the Windows 3.0 Control Panel does this under normal circumstances, and
it's a nice effect.
Bill Dow
Alameda, CA
A:
Under the Windows[tm] graphical environment versions 1.x and 2.x, it was
indeed possible to circumvent the hPrevInstance mechanism by renaming the
EXE file. In Windows1 3.0, simply renaming the EXE file will no longer work,
but it is still possible to trick Windows (intentionally or accidentally) by
another method.
All that Windows compares to detect previous instances are the
up-to-eight-character external name (the EXE filename, ignoring any path)
and the up-to-eight-
character internal name (the NAME in the application's DEF file, which
becomes embedded in the EXE file). Obviously, they may differ. For the
external names, it is important to note that the path leading to the EXE
file is totally ignored in previous-instance comparisons, since there is no
path associated with the internal name that could be used in the comparison.
To see a list of internal names, run the Windows Software Development Kit
HeapWalker program and scan the OWNER-NAME
column.
Earlier versions of Windows compared only the external name of the
application to be loaded with the internal name of all the applications
already loaded. If a match was found, Windows assumed that a second instance
of the first application was to be loaded. Windows did not need to reopen
the EXE file for the second instance and look for its internal name, because
it was already opened. If no match was found, Windows assumed that the
application being loaded was a first instance, with the result being an
hPrevInstance equal to 0. This is why renaming the application's EXE file
would trick earlier versions of Windows into loading duplicate code segments
and would fool the second instance of "single-instance" applications into
always thinking it was the first instance.
In Windows 3.0, renaming the EXE file will no longer cause that problem,
because Windows now remembers the external names along with the internal
names of all running applications. Renaming an EXE no longer matters; the
second instance of your application will be able to use hPrevInstance to
detect that it is the second instance. This new method does lead to an
interesting side effect. Copy NOTEPAD.EXE to your temporary subdirectory and
rename it CONTROL.EXE. Run it explicitly, that is, run C:\TMP\CONTROL.EXE.
Now try to run Control Panel from the Program Manager. You'll get a second
instance of Notepad!
So how do you trick Windows 3.0 into running two copies of a single-instance
application? Simply make a second copy of the single-instance application
and give it a new name. Then rename the original application to a third
name. The application then has two different external names that are both
different from the internal name of the application. You can then run both
EXE files as if they were different applications, even though they are not.
You should ignore the WinMain hPrevInstance argument entirely, and go
directly for the hWnd of the first instance. The first few lines of your
WinMain function should look like this (assuming that your main window's
class is named KillerApp).
auto HWND ahWndPrev;
if (ahWndPrev = FindWindow("KillerApp", NULL))
{
BringWindowToTop(ahWndPrev);
return 0;
}
All single-instance applications should do this! Not only does it ensure
that only one instance of the application runs, it is much friendlier to the
user than simply terminating or putting up a rude message box that the
application is already running. This elegantly takes care of the situation
where the first instance's window may be completely covered by another
window.
────────────────────────────────────────────────────────────────────────────
Q:
I have an application in which I need to display a number of
different bitmaps both in the client area and on paper. I need to display
them isotropically; in other words, the aspect ratio of the bitmaps needs to
be maintained. My problem is that the bitmaps are of various sizes, known
only at run time when the bitmaps are read, and are generated on devices
with different resolutions and aspect ratios, also known only at run time.
Windows offers the isotropic mapping mode, but I can't figure out a way to
fit the bitmaps into that mapping model all at the same time.
Rick Mosebach
New York, NY
A:
I've written a short function named MakeIsotropic that will perform the
isotropic calculations for you (see Figure 1). Do the isotropic calculations
manually with MakeIsotropic, and then use BitBlt to draw the bitmaps with
the width and height values generated by the function. You should do all
this in the default MM_TEXT mapping mode, so that you have complete control
over the coordinate system.
The problem you describe involves eight variables, which result from the
permutations for each bitmap of the source/destination, the
pixels/pixels-per-inch in each direction, and the horizontal/vertical
width/height:
■
■
DPX:
destination
pixels
horizontal
■
DPY:
destination
pixels
vertical
■
DIX:
destination
pixels per inch
horizontal
■
DIY:
destination
pixels per inch
vertical
■
SPX:
source
pixels
horizontal
■
SPY:
source
pixels
vertical
■
SIX:
source
pixels per inch
horizontal
■
SIY:
source
pixels per inch
vertical
Given these variables, the following formula will make them perfectly
isotropic.
DPX DIY SPX SIY
-- x -- = -- x --
DPY DIX SPY SIX
In your case, though, and in Windows in general, the DPX and DPY are a
function of how the user sizes the destination. You have to convert these
formulas so that you can calculate a new DPX or DPY based on the requested
size of the target. You can calculate either DPX or DPY using the following
formulas, which are essentially how the isotropic calculations are
performed.
DIX SPX SIY
DPX = DPY x -- x -- x --
DIY SPY SIX
DIY SPY SIX
DPY = DPX x -- x -- x --
DIX SPX SIY
MakeIsotropic works as follows. The developer must specify pointers to DPX
and DPY and the values of the other six variables. Either DPX or DPY is
decreased so that the resultant isotropic rectangle can fit in the original
rectangle specified by these two variables.
The while tests solve the potential overflow problem when calculating the
numerator and denominator. For each component of the numerator and
denominator, they check to see if the variables are not odd (therefore,
even); while both are even, each is divided by two. Theoretically you could
perform nine while tests for each numerator/denominator-component
permutation, but these five should be sufficient.
The numerator and denominator are then calculated, and another while loop is
executed to reduce both of them to prevent overflow. Once this is done, a
temporary value for DPY is calculated and compared to the original DPY. If
the original value of DPY is less than the new isotropic value, DPX must be
recalculated. Otherwise, the new lower value of DPY replaces the original
value. DPX and DPY are now an isotropic rectangle within the original DPX
and DPY.
Q:
How does SMARTDRV.SYS operate at a low level, and how can it be best used?
T. J. Kelly
Atlanta, GA
A:
SMARTDRV.SYS is a disk-caching device driver that operates with both DOS and
Windows. It minimizes the number of times that your physical disks must be
accessed as DOS/Windows is running, since disks are relatively slow.
Generally, there are four ways in which disks can be cached: by sector, by
cluster, by track, or by cylinder. SMARTDRV operates as a track cacher.
Because it's a track cacher, SMARTDRV deals with tracks when it talks to the
disk controller. When an application asks DOS or Windows to read a sector,
SMARTDRV intervenes. It asks the disk controller for the entire track that
the sector resides on, and passes the sector back to the application. This
way, the entire track that the disk's read/write heads are over is obtained
while the heads are there, minimizing head movement. (Head movement is
fairly slow.) Then when the application asks for the next sector, SMARTDRV
already has it in its memory buffers and returns it immediately.
There are two considerations when using SMARTDRV (or any track cacher, for
that matter) that users generally do not appreciate. The first is that not
all disks are interleaved optimally, so reading an entire track as the disk
spins may take longer than is actually possible. The second is that a high
degree of file fragmentation destroys the benefits of a track cacher, since
a particular file may be broken up inordinately across many tracks.
Interleaving is the low-level way to refer to specific sectors on a hard
disk. Standard IBM PC hard disks have 17 sectors per track. COMPAQ PCs are a
little more varied, with sectors per track at the following points: 17, 25,
26, 33 and 34. A 1:1 interleave means that, on each track, sector 2
physically follows sector 1, sector 3 physically follows sector 2, and so
on.
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,...
A 2:1 interleave on a 17-sectors-per-track disk would physically be laid out
like the following:
1, 10, 2, 11, 3, 12, 4, 13, 5, 14, 6,...
And a 3:1 interleave on a 17-sectors-per-track disk would physically be laid
out like this:
1, 7, 13, 2, 8, 14, 3, 9, 15, 4,...
The result of all this is that a 2:1 interleaved disk takes twice as long to
read as a 1:1 interleaved disk, because the disk has to rotate twice for
each complete track to be read sequentially by sector, a 3:1 interleave
takes three times as long, and so on. The reason that disks are interleaved
at ratios other than 1:1 is that not all disk controllers in combination
with the BIOS are fast enough to read 1:1 interleaved disks. Sector 2 on a
1:1 interleaved disk may already be passing by a slow controller when the
controller needs to retrieve it after sector 1, thereby having to wait an
entire disk revolution for it; a 2:1 interleave would be a better choice for
that disk using that controller.
Unfortunately, not all manufacturers calibrate the disks with the installed
controllers. Many simply choose a safe 3:1 interleave. Programs such as
Gibson Research SpinRite II can perform a nondestructive low-level format
that takes all of this into account. On one of my computers, I went from a
3:1 interleave to a 1:1 interleave because of a fast new $125 Western
Digital[tm] WD1006V-MM2 controller I bought-and Windows performance doubled!
The disk light stayed on for a third of the time it used to each time
SMARTDRV retrieved a track.
The second consideration is that a lot of file fragmentation destroys the
benefits of SMARTDRV, because files may be broken up inordinately across
many tracks. If you specify a 512Kb-minimum SMARTDRV on a
17-sectors-per-track (8.5Kb-per-track) disk, SMARTDRV can cache 60 tracks
(512Kb / 8.5Kb = 60). An 8Kb contiguous-sector file may reside entirely on a
track, at best; at worst, if the file was highly fragmented, it may be
spread across 4 tracks (on a 2Kb-per-cluster disk). In the worst case, an
8Kb file would be using up 34Kb (6.7 percent) of SMARTDRV's cache memory.
Since Windows does a lot of object (code, icon, cursor, etc.) discarding and
reloading, you must minimize file fragmentation to receive the benefits of
SMARTDRV. To see how much your hard disks are fragmented, you can download
CHKFRG.ZIP (which contains CHKFRAG.EXE) from the PC MagNet Utilities forum
on CompuServe (GO PCMAGNET). Programs such as PC Tools can periodically
unfragment your hard disks and improve the performance of SMARTDRV, DOS2,
and Windows.
Q:
Some aspects of dialog boxes confuse me. Why are DialogBox dialog boxes
modal and CreateDialog dialog boxes modeless? What is the difference between
the return value of a DlgProc and the value specified in EndDialog, and why
is the separate function necessary?
Alan Cooper
Menlo Park, CA
A:
When you call DialogBox, it first creates a dialog pop-up window using its
own internal DlgProc (not to be confused with the DlgProc you supply), then
creates all the control child windows using the standard internal CtlProcs
(such as Button, Edit, Static). Next, DialogBox calls the DlgProc you
furnished with a WM_INITDIALOG message, displays the dialog box window, and
finally goes into a GetMessage / TranslateMessage / DispatchMessage loop.
This loop is essentially in the dialog manager, and it is in control as far
as you are concerned. By disabling the owner of the dialog box and going
into its own loop, the dialog that appears is modal, since only a few
messages (WM_PAINT, for example) get dispatched by the dialog manager to
your WndProcs.
When you call CreateDialog, it follows the same procedure as above, but does
not go into a similar GetMessage / TranslateMessage / DispatchMessage loop.
Instead, it returns control immediately to the routine containing the
CreateDialog call. Because it doesn't go into the loop, the dialog box that
appears is modeless and all messages are dispatched by your main GetMessage
/ TranslateMessage / DispatchMessage loop as they would normally. This is
why you have to add IsDialogMessage calls to you main loop for modeless
dialog boxes. When you make an IsDialogMessage call, the dialog manager
manages the dialog controls just as it does in its internal loop in the
DialogBox code. The dialog manager essentially becomes a stub off your main
loop.
In both cases, your DlgProc becomes a stub off the dialog manager code. The
dialog manager handles most activities associated with the dialog using its
own internal DlgProc, but calls your DlgProc in cases where you need to take
some action. This is why you do not call DefWindowProc in your DlgProc; the
DefWindowProc call is done inside the dialog manager's internal DlgProc.
The return value of your DlgProc simply communicates information back to the
dialog manager. In the case of DialogBox, you need to communicate explicitly
to the dialog manager's message-processing loop when to terminate; this is
done by the EndDialog function. Since the dialog manager's loop is not in
control of dialog boxes created with CreateDialog, you do the DestroyWindow
call yourself instead of calling EndDialog.
1 For ease of reading, "Windows" refers to the Microsoft Windows graphical
environment. "Windows" refers only to this Microsoft product and is not
intended to refer to such products generally.
2 As used herein, "DOS" refers to the MS-DOS and PC-DOS operating systems.
Figure 1. MakeIsotropic
extern int FAR PASCAL MakeIsotropic(
WORD FAR * afpwDPX, // dst horz pixels
WORD FAR * afpwDPY, // dst vert pixels
WORD awDIX, // dst horz pixels per inch
WORD awDIY, // dst vert pixels per inch
WORD awSPX, // src horz pixels
WORD awSPY, // src vert pixels
WORD awSIX, // src horz pixels per inch
WORD awSIY) // src vert pixels per inch
{
auto DWORD adwNum, // DIY SPY SIX
adwDen; // DPY = DPX x --- x --- x ---
auto WORD awTmp; // DIX SPX SIY
if (*afpwDPX == 0 || awDIX == 0 || awSPX == 0 || awSIX == 0||
*afpwDPY == 0 || awDIY == 0 || awSPY == 0 || awSIY == 0)
return -1;
while (!(awDIX & 1) && !(awDIY & 1)) { awDIX /= 2; awDIY /= 2; }
while (!(awSPX & 1) && !(awSPY & 1)) { awSPX /= 2; awSPY /= 2; }
while (!(awSIX & 1) && !(awSIY & 1)) { awSIX /= 2; awSIY /= 2; }
while (!(awDIX & 1) && !(awSIX & 1)) { awDIX /= 2; awSIX /= 2; }
while (!(awDIY & 1) && !(awSIY & 1)) { awDIY /= 2; awSIY /= 2; }
adwNum = (DWORD)awDIY * awSPY * awSIX;
adwDen = (DWORD)awDIX * awSPX * awSIY;
while (!((WORD)adwNum & 1) && !((WORD)adwDen & 1))
{ adwNum /= 2; adwDen /= 2; }
awTmp = (WORD)((DWORD)*afpwDPX * adwNum / adwDen);
if (*afpwDPY < awTmp)
*afpwDPX = (WORD)((DWORD)*afpwDPY * adwDen / adwNum);
else
*afpwDPY = awTmp;
return 0;
}