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 6
────────────────────────────────────────────────────────────────────────────
Volume 6 - Number 1
────────────────────────────────────────────────────────────────────────────
Adapting Extended Processes to the Cooperative Multitasking of Microsoft
Windows
William S. Hall
Programming a lengthy process in the Microsoft Windows graphical environment
requires unique considerations. Unlike single-tasking or preemptive
multitasking operating environments, Windows1 uses message-driven,
cooperative multitasking to perform tasks. Windows programs do not execute
until a message is received; once received, the message must be processed
quickly so that control of the CPU can be relinquished to permit other
Windows programs to run. Performing a time-consuming process by coding a
single uninterrupted thread of execution is completely unsatisfactory
because Windows will never run any other task and will appear frozen to the
user during that time.
Fortunately, large tasks can often be broken down into smaller ones, each of
which can be quickly executed whenever the program is allowed to run. If
some means can be found for gaining control at appropriate intervals and
dispatching each task in the proper sequence, then it should be possible to
implement an extended process smoothly in Windows.
Asynchronous file transfer between two computers serves as a good example of
an extended process, because it can take hours over conventional
communication lines. This article demonstrates how to coordinate an extended
process into Windows using the popular Kermit file transfer protocol. One
common approach to writing a Kermit program involves breaking the protocol
into a sequence of tasks controlled by a finite state machine. I'll describe
modifications of this approach that fit the requirements of Windows and
illustrate how the program can be made to schedule its next task without the
use of a timing mechanism.
An implementation of Kermit that can be easily integrated into a Windows
terminal emulator is provided, as well as a sample terminal program that
uses this implementation. The program is capable of sending and receiving
files in text and binary formats over 7- and 8-bit wide data paths while
employing any of three methods of error detection and run-length encoding
for efficiency. Wildcards can be used to transfer groups of files in a
single operation. Although server mode and extensions such as long and
attribute packets are not included, the program is quite complete.
A total of 40 files are used to build Kermit and the two terminal emulators
described here. Partial listings taken from the Kermit and terminal programs
are included with this article. The complete source code needed to build
Kermit, the simple Windows terminal emulator, and the same emulator with
embedded Kermit is available on any MSJ bulletin board.
Realizing Kermit's Session Layer in Windows
In a Kermit session, information is exchanged by encapsulating blocks of
data in various types of packets that are normally less than 100 bytes in
length, unless an extended packet type is being used (see Figure 1).
Although Kermit is not a layered protocol as specified in the ISO open
system standards, most versions of Kermit are written with these layers in
mind to isolate functionality and make it easy to extend and maintain.
The Kermit session layer acts as the basic controlling mechanism for the
protocol. When sending or receiving a file, the session layer is driven by
the packet type received from the remote Kermit. This session layer can be
thought of as a finite state machine.
A public-domain tool called Wart has been developed for Kermit that allows a
programmer to describe such state machines very elegantly (see the sidebar
"Building a Finite State Machine with Wart" ). The Wart tool converts the
protocol description into a table-driven case statement, which is entered at
run time by calling the wart() function. The current state is maintained in
a static variable; thepacket type is obtained by the wart() function's call
to the transport layer function input() (the Kermit code implements its
protocol description using wart() in the file WNKERM.W, as shown in Figure
2). In turn, input() calls a data link layer function, rpack (see Figure 3),
to obtain a complete packet. Figure 4 diagrams the various states of the
Kermit protocol implemented in WNKERM.W.
In Kermit implementations written for single-tasking or preemptive
multitasking operating systems, once wart() is entered it never returns
until the entire session is complete or has been aborted. Likewise, input()
does not return to wart() until a properly formed packet with the correct
sequence number has been found. In turn, rpack does not return to input()
until a complete packet with a correct checksum has been obtained or a
time-out (usually of several seconds) has elapsed.
Of course, none of this waiting around is acceptable in Windows. Suppose
wart() is called from a Windows program as the result of a message. If
wart() runs as above, no other Windows program can run until the file
transfer has completed. Even if wart() returns after the action associated
with each state has been executed, long delays could still be experienced
while rpack tries to read in a complete packet from the communications line.
In fact, a complete packet might never arrive.
On the other hand, the actions associated with each state complete without
significant delay because they involve reading or writing small chunks of
data from a file, encoding or decoding this data, forming it into a packet,
and writing it out to the communications buffer. The solution is to violate
the layering principle and allow the session layer to recognize an
incomplete packet whose associated action is simply to return from wart().
This simple addition to the packet types plus a mechanism to build a packet
incrementally from successive reads of the communications line allows
Windows to release the CPU to other programs in a timely fashion.
This is how it works. Periodically the Windows procedure is entered with a
programmer-defined message that calls wart(), which then calls input(). If a
packet is ready, the action for that state is executed and another call to
input() is made. This time input() most likely returns an incomplete packet
and wart() returns control to Windows. Other tasks can then run; the result
is a nearly seamless multitasking functionality that handles the file
transfer almost transparently.
Of course, the way wart() is scheduled to run has not been explained. But
first, let's look at how the data link layer is handled.
The Data Link Layer
After the input() function calls rpack in the data link layer to get the
next packet, rpack calls getpacket, which parses the input stream into the
packet fields (see Figure 3). The getpacket function frames a packet by
detecting the start-of-packet character; the length of the data field is
used to determine when a complete packet has arrived. On any single call to
getpacket, the input stream may not contain enough data to build a complete
packet, and it may take several calls to this function before a packet can
be delivered.
This means that getpacket must be designed so that when it exhausts the
current data stream, it remembers how far along it has progressed and the
point from which it must continue when called again with more data. A state
machine implementation is again implied; it is easily realized by a case
statement because of the simplicity of getpacket. The state variable itself
is maintained in a globally visible structure that also contains the other
fields of the current packet. Each time getpacket is called, the case
statement is executed from the current state and returns when the buffer is
exhausted. This way, getpacket and its caller rpack do not need to wait for
more data to arrive from the communications line, and input can return
immediately to wart() with an incomplete packet type.
Waking Up the Session Layer
The missing piece in the above scenario is the scheduling of the session
layer. An obvious solution is to use a timer and place the call to wart() on
this thread. A timer, however, does not adjust to changing load conditions
and extra effort must be made to set the timing interval according to the
current baud rate.
A more satisfactory method is to replace the conventional GetMessage call in
the Windows message loop with PeekMessage. Whereas GetMessage does not
return to the program until a message has been placed into its queue,
PeekMessage always returns whether there is a message or not.
To use PeekMessage, your program processes any Windows messages that can be
pulled from the queue, or, if there are none, it does something else for a
short period. In the design used here, the communications port is polled
during this time, and any pending data is loaded into a local buffer. Then a
programmer-defined message is posted back to the window. The next call to
PeekMessage retrieves this message and the associated action is to call
wart(). Getpacket is entered via calls to input and rpack with the buffer
that was filled when the communications port was polled by PeekMessage. If a
complete packet is built, the next action in the session layer state machine
is executed. Otherwise, wart() sees an incomplete packet, and returns to
Windows. Figure 5 shows the message loop and excerpts of the WinProc used in
the sample terminal emulator.
Sample Application
The complete source code for a Windows-based terminal emulator with embedded
Kermit support (see Figure 6) can be found on any MSJ bulletin board. You'll
need the Microsoft Windows Software Development Kit (SDK) Version 3.0 and
Microsoft C Version 5.1 or 6.0 to build the application. A typical Windows
program, the terminal program consists of three parts: source, resources,
and definitions. Of course, the additions needed to add Kermit functionality
affect each of these three components.
For the most part, the Kermit module is independent of the terminal source
code. This module is a library to be added at link time. Of course the
terminal program cannot be entirely ignorant of certain Kermit functions,
and Kermit itself needs to be aware of certain variables such as the main
window and instance handles, the current communications channel, and the
location of the buffer used for reading data from the port. However, all of
these details can be handled fairly easily in the terminal source. By using
conditional compilation, the changes to the original code become clearly
marked, making adaptation to other terminal programs fairly straightforward.
Trying to maintain this same degree of isolation with respect to resources
is another matter. Kermit requires a user interface, so a string table, menu
item, and several dialog boxes have been added. Although the resource
compiler supplied with Version 3.0 of the SDK is a definite improvement over
its 2.x predecessor, it is still difficult to maintain multiple compilations
with a single source. In the end, it is necessary to build one RC source
file for the plain terminal emulator (without Kermit), another for the
emulator with Kermit, and then #include other resource files as needed.
Of course, the additional Kermit code and resources also affect the DEF file
because they require more function exports and segment names. But since it
is possible to have more than one SEGMENTS and EXPORTS section in a single
DEF file, it is quite simple to create a combined DEF file by concatenation.
Of course, segment names as well as function export numbers should be
unique.
There are three make files that control the sources. The first, WNTERM,
simply builds the original terminal program, WNTERM.EXE. It is provided so
you can see clearly what has to change when the Kermit code is added. The
second, WNKERM, makes the Kermit library and compiles the Kermit resources.
The third make file, WNKTERM, builds the terminal with embedded Kermit
support, WNKTERM.EXE.
The Kermit Header File
Each of the Kermit modules as well as the terminal code that references
Kermit variables or constants must include the header WNKERM.H (see Figure
7). It consists of Kermit function prototypes, a menu, resource strings,
general manifests, and a number of structures associated with dialog boxes
or the protocol. All Kermit variables are defined in this header file. At
least one module in the terminal code should include WNKERM.H to declare the
data. To avoid possible conflicts with the terminal code, all variables and
functions begin with the prefix krm. All manifests include the string KRM_.
Thus, IDS_KRM_XXX names a string stored in the resource file, IDM_KRM_XXX, a
menu item, and IDD_KRM_XXX, a dialog box control. Strings use a reference
value, and other strings' manifests base themselves from this reference. The
base value is set to 10,000, but can be changed to avoid possible conflict
with the terminal code. The same is true of menus.
The Kermit code expects certain variables to be available from the terminal.
These are copied to Kermit variables at initialization. One of these, the
handle of the main window, is good for the lifetime of the program. Other
values may change periodically. For example, the communications port
identifier (cid) may vary if the user selects a different port. Because of
this, the Kermit code uses a pointer to reference the cid. Kermit must also
be able to reference a linear character buffer along with its current count,
so the count and the start of the buffer are kept as pointers by Kermit.
Kermit also expects that there are routines in the terminal to fill this
buffer by reading the com port and delivering the input to Kermit via a call
to the state switcher wart().
Kermit and the Terminal's Main WinProc
Of course, the terminal's main WinProc is also affected by the addition of
Kermit support. When the main terminal window is created, a call is made to
Kermit at WM_CREATE time to pass along information such as the window
handle, buffer information, and the location of the communications port
identifier. Except for the handle, this information changes dynamically, so
pointer variables are needed. During the initialization, WIN.INI is also
read under the subhead [Kermit] to provide user settings pertaining to the
protocol and inbound and outbound packets. The Kermit menu is also added to
the end of the terminal's main menu. Finally, if the port to be used is not
yet known, the Kermit menu should be initially disabled.
In the event either Windows or the program is closed, any global resources
used by Kermit must be freed. If termination is attempted in the middle of a
transfer, the user must have a chance to change his or her mind. In the
Kermit exit code, a message box appears if a transfer is still in progress.
If the reply is to shut down (IDYES), an error packet is sent to the remote
Kermit, all files are closed (incompletely received files are deleted if
this option has been set), and the response to WM_CLOSE or
WM_QUERYENDSESSION proceeds as usual. Otherwise, the session continues as if
nothing happened.
The Kermit menu allows the user to send a group of files (see Figure 8),
receive a group of files, cancel a transfer in one of several ways, and
select default values for several protocol and packet parameters (see
Figures 9 and 10). The Kermit menu processor returns TRUE if a menu item was
recognized and handled. Otherwise it returns FALSE; this is a signal for the
terminal to execute its own handler for WM_COMMAND.
When Kermit is in action, any child windows on the screen are hidden and a
modeless dialog box is posted to show the state of the transfer (see Figure
11). Of course, the transfer continues even if the terminal program is
minimized. If the terminal program draws its own icon rather than using a
class icon, Kermit will show the number of packets exchanged like an
odometer (with rollover every 10,000 packets) in the icon window. This lets
the user monitor progress even when he or she is performing other tasks and
the terminal is running in the background (see Figure 12). This display is
managed in response to the WM_PAINT message.
Finally, as already noted, Kermit calls the state machine in response to a
programmer-defined message. You have already seen how such a message is
generated by PeekMessage.
Conclusion
By breaking up a long process into naturally occurring shorter ones, it is
possible to preserve the cooperative multitasking feature of Windows and
still get the job done. Although Kermit has been used to illustrate specific
techniques, the principles presented here can be applied to other
communication protocols, as well as complex, time-consuming tasks that can
be described with a finite state machine approach.
1 For ease of reading, "Windows" refers to the Microsoft Windows graphical
environment. "Windows" refers to this Microsoft product only 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.
The Kermit File Transfer Protocol
Like its famous puppet namesake, the Kermit file transfer protocol is
internationally known. Since its introduction in 1980, Kermit has become a
mainstay of academic and business computing, supported on machines from the
largest mainframes to the tiniest microcomputers. Operating systems
supporting this protocol include VM/CMS, DOS2, OS/2, CP/M, UNIX, and even
the USCD P-System. Kermit has been written in Algol, Pascal, C, FORTRAN,
BASIC, BCPL, CROSS, Compass, FORTH, PAL-8, MUMPS, BLISS, PL/1, Ratfor, PL/M,
Modula-2, LISP, and quite a few assembly languages. Kermit's importance is
rivaled only by the XMODEM protocol. There is even an IBM 370 version of
Kermit in Russian.
Kermit was written to answer the need for a reliable means of transferring
files from micros to mainframes. Simply uploading and downloading files is
often unreliable. Unusual file formats, noisy lines, or busy mainframes
expecting input at human typing speed frequently result in a trashed
transfer. To address this problem, Frank da Cruz and his associates at
Columbia University developed a protocol that supported operations between
DECSYSTEM-20 or IBM 370 systems and microcomputers running CP/M and DOS.
These early programs were made available to users along with the source
code. Today, Kermit remains free and is widely available at no more than the
cost of distribution. This policy has not only broadened the support base
for Kermit, it has also contributed to its astounding acceptance by the
computing community.
At first, Kermit was only capable of transferring text files in packets
containing at most 94 characters, using a simple 1-byte checksum. Since
then, Kermit has been extended to include binary transfers over 7- and 8-bit
wide paths, run-length encoding, CRC checksums, long packets, transfers over
networks, server modes, and more recently, sliding windows. Today, you can
not only send your file, you can preserve its attributes including its date
and time of creation. Kermit can even translate between varying character
sets such as ASCII, ISO Latin I, ISO Latin/Cyrillic, ISO Latin/Hebrew, and
Japanese JIS X 0208. Kermit has special features for users with visual,
hearing, or motor impairments, including speech synthesis, a simple
command-line interface, and special program documentation.
Most Kermit programs offer sophisticated terminal emulation as well as file
transfer. For example, Version 3.0 for DOS, written by Joe Doupnik of Utah
State University, emulates DEC VT320 terminals and supports Tektronix
graphics. Other features include screen rollback, Windows compatibility, a
visual bell for deaf users, right-to-left screen display for Hebrew and
Arabic, and a complete scripting language. Kermit rivals commercially
available products, but costs at most $20.00.
Building a Finite State Machine with Wart
Wart, written by Jeff Damens of Columbia University, is a tool that builds
state table switchers. Wart contains a small subset of the UNIX lexical
analyzer generator, lex, and may be freely distributed (lex, which means
"word" in Latin, translates to wort in German, which sounds like wart on a
frog).
Wart accepts as input a C program in the following format.
lines to be copied | %state <state names...>
%%
<state> | <state,state,...> CHAR { actions }
.
.
.
%%
The %state directive declares the program's states. The section enclosed
between the %% delimiters is the state table. A typical entry has the form
<state>X {action}
which is read as
"if in state <state> with input X, perform {action}".
The optional <state> field names the states the program must be in to
perform the related action. If no state is specified, the action is carried
out regardless of the current state. If more than one state is specified,
the action is performed in any of the listed states. Multiple states are
separated by commas.
The required input field consists of a single literal character. In a given
state, if the input is the specified character, the associated action is
performed. The character . matches any input character. No pattern matching
or range notation is provided. The input character itself is obtained from a
function called input(), which the user must define. Input should return an
alphanumeric character or one of the following characters.
% - _ $ @ .
The action statement is a series of zero or more C statements enclosed in
curly braces. Wart also provides the BEGIN macro, which is defined as state
= , as it is in lex. Wart is invoked at the command line as follows:
wart file.w file.c
In this example, Wart reads FILE.W and produces FILE.C, which can then be
compiled as an ordinary C program. Wart's output contains the function
called wart(), whose form depends on the state declarations and the state
transition table. Wart loops through calls to input() and uses the result to
index into a case statement created from the state table.
The following program demonstrates some of the capabilities and limitations
of Wart. BINTODEC.W accepts a binary number from the command line, preceded
by an optional minus sign and possibly containing a fractional part, and
prints the decimal equivalent.
BINTODEC.W
#include <stdio.h>
int state, s = 1, m = 0, d;
float f;
char *b;
/* Declare wart states */
%states sign mantissa fraction
/* Begin state table */
%%
<sign>- { s = -1; BEGIN mantissa; } /* Look for sign */
<sign>0 { m = 0; BEGIN mantissa; } /*Got digit,start mantissa */
<sign>1 { m = 1; BEGIN mantissa; }
<sign>. { fatal("bad input"); } /* Detect bad format */
<mantissa>0 { m *= 2; } /* Accumulate mantissa */
<mantissa>1 { m = 2 * m + 1; }
<mantissa>$ { printf("%d\n", s * m); return; }
<mantissa>. { f=0.0; d = 1; BEGIN fraction; } /* Start fraction */
<fraction>0 { d *= 2; } /* Accumulate fraction */
<fraction>1 { d *= 2; f += 1.0 / d; }
<fraction>$ { printf("%f\n", s * (m + f) ); return; }
<fraction>. { fatal("bad input\n"); }
%%
input(void) { /* Define input() function */
int x;
return(((x = *b++) = = '\0') ? '$' : x );
}
fatal(char *s) { /* Error exit */
fprintf(stderr,"fatal - %s\n",s);
exit(1);
}
main(int argc,char **argv) { /* Main program */
if (argc < 2) {
fprintf(stderr, "Not enough input\n");
exit(1);
}
b = *++argv;
state = sign; /* Initialize state */
wart(); /* Invoke state switcher */
exit(0); /* Done */
}
The following code is generated by processing BINTODEC.W with WART.EXE:
BINTODEC.C
/* WARNING --This C source program generated by Wart preprocessor.*/
/* Don't edit this C file; edit the Wart-format .W file instead, */
/* and then run it through Wart to produce a new C source file. */
/* Wart Version Info: */
char *wartv = "Wart Version 1A(006) Jan 1989";
#include <stdio.h>
int state, s = 1, m = 0, d;
float f;
char *b;
/* Declare wart states */
#define sign 1
#define mantissa 2
#define fraction 3
/* Begin state table */
#define BEGIN state =
int state = 0;
wart()
{
int c,actno;
extern short tbl[];
while (1) {
c = input();
if ((actno = tbl[c + state*128]) != -1)
switch(actno) {
case 1:
{ s = -1; BEGIN mantissa; }
break;
case 2:
{ m = 0; BEGIN mantissa; }
break;
case 3:
{ m = 1; BEGIN mantissa; }
break;
case 4:
{ fatal("bad input"); }
break;
case 5:
{ m *= 2; }
break;
case 6:
{ m = 2 * m + 1; }
break;
case 7:
{ printf("%d\n", s * m); return; }
break;
case 8:
{ f = 0.0; d = 1; BEGIN fraction; }
break;
case 9:
{ d *= 2; }
break;
case 10:
{ d *= 2; f += 1.0 / d; }
break;
case 11:
{ printf("%f\n", s * (m + f) ); return; }
break;
case 12:
{ fatal("bad input\n"); }
break;
}
}
}
short tbl[] = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 1, 4, 4,
2, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
-1, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
5, 6, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
0, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
12, 12, 12, 12, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
9, 10, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
};
input(void) { /* Define input() function */
int x;
return(((x = *b++) = = '\0') ? '$' : x );
}
fatal(char *s) { /* Error exit */
fprintf(stderr,"fatal - %s\n",s);
exit(1);
}
main(int argc,char **argv) { /* Main program */
if (argc < 2) {
fprintf(stderr, "Not enough input\n");
exit(1);
}
b = *++argv;
state = sign; /* Initialize state */
wart(); /* Invoke state switcher */
exit(0); /* Done */
}
Creating a Network Service Using the Client-Server Model and LAN Manager 2.0
Brendan W. Dixon
As the PC industry has matured, concepts and techniques pioneered on larger
systems have been adapted for PC users. Many of these, such as preemptive
multitasking, virtual memory and paging, are little changed on PCs. Others
have been combined with features unique to PCs, producing new uses for
computers. For example, when the graphical capability of the PC was combined
with the large-system approach to document preparation, a whole new
industry, desktop publishing, was created. The client-server model combines
the large-system concept of centralized resources with the PC's peer-to-peer
approach to networking, and lays the groundwork for distributed processing.
The client-server model is an application design approach that distributes
resources and processing between systems in a network. Unlike the standard
network model, where each client participates in resource management, the
client-server model divides responsibilities and intelligence. Servers
contain all of the intelligence necessary to own and manage the resource
(for example, a database server, as in Figure 1). Clients contain only
nominal information about the resource (that it is a database, for instance)
and do not assist in its management; they pass requests to the server, which
the server decides to accept or reject. Conversely, servers are unaware of
the client's intentions. A server only responds to requests for the resource
it is managing (say, a database transaction). Centralizing resource
management onto the server simplifies error handling and reduces the network
load. Removing resource management tasks from the clients reduces processing
requirements on workstation hardware and allows them to focus on their
primary responsibilities such as interfacing with the end user.
Microsoft LAN Manager is designed to support client-server programming.
While LAN Manager provides all of the facilities of standard PC networks
(that is, file and printer sharing), it also provides a platform that
simplifies the construction of client-server-based applications and network
communication.
This article examines the building of a LAN Manager service. Creating and
managing a group of remote named pipes will be discussed in a future
article, as well as the construction of a LAN Manager client under the
Microsoft Windows and OS/2 Presentation Manager graphical environments.
The LAN Manager Application Programming Interface (API) gives the programmer
a robust set of function calls to communicate with LAN Manager and issue
network requests. Almost any network task that can be performed from the
command line or by using LAN Manager's full-screen interface can also be
done through in-line code. There are nearly 130 API calls (see Figure 2),
and many take modal parameters that can be used to tune the function.
Most important for client-server programmers is LAN Manager's support of
remote named pipes. Named pipes are one-way or bidirectional facilities that
allow processes to communicate. They offer guaranteed delivery and built-in
buffering, simplified error detection, and access security.
Another feature of LAN Manager is nonconnection oriented messaging through
mailslots. Mailslots are useful for peer-to-peer communication, message
broadcasting, and client-server communication that does not require all the
facilities of a named pipe. Mailslots are either first-class (with
guaranteed delivery) or second-class (without guaranteed delivery). Remote
first-class mailslots, which can receive messages from remote workstations,
must be created on a LAN Manager server. But any workstation may send or
receive remote messages on a second-class mailslot.
Named pipes and mailslots enable the programmer to build applications that
utilize the network without being network-aware. LAN Manager also makes it
easy for the network administrator to control access to named pipes and
mailslots, and therefore the network. These facilities lead to easily built,
easily maintained, and easily controlled client-server applications.
Sample LAN Manager Service
In order to provide a consistent method of control for the network
administrator, LAN Manager was designed as an extensible system, implemented
as a group of services. A LAN Manager service is a detached OS/2 process
(that is, no console I/O) running as an extension of LAN Manager. Such a
service can be controlled (that is, started, paused, continued, or stopped)
through LAN Manager. Developing a LAN Manager service requires only that the
service be listed in the LANMAN.INI file, that it register itself with LAN
Manager as part of its start-up processing, and that it respond to LAN
Manager requests, which are sent to the service through the OS/2 signal
mechanism. Additionally, a LAN Manager service may take tuning parameters;
these are also normally specified in the LANMAN.INI file.
In the client-server model, the server makes resources available to the
client and manages those resources for the client. One server resource,
normally unavailable to clients, is the ownership of remote named pipes.
While a client may open and communicate on a remote named pipe, it cannot
create the pipe; remote named pipes are a resource provided by a LAN Manager
server and must be created on the server.
A sample LAN Manager service designed for this article, PBX, can serve as
the foundation for real-world client-server applications. PBX gives clients
the ability to establish peer-to-peer communications using named pipes
through a transparent routing mechanism. Clients register with PBX and
request connections with other clients. Once a connection is established
between two clients, PBX maintains the image of a peer-to-peer named pipe
connection for each client until the connection is broken. PBX creates at
least three threads in addition to the main thread created when PBX is given
control. Each thread is named after its C routine (see Figure 3). This
article examines the main thread, which initializes PBX and communicates
with LAN Manager. A future article will examine the remaining threads, which
create and maintain the virtual named pipe connections (see Figure 4 for the
source code files used to build PBXSRV.EXE).
To build a LAN Manager application, you will need either the Microsoft LAN
Manager Programmer's Toolkit or the Microsoft LAN Manager Developer's
Toolkit. These toolkits include the C header files and libraries utilized
in a LAN Manager application.
To support a LAN Manager API category in your program, you add one or more
#define statements and a #include for LAN.H in your C source file. Figure 2
lists the C preprocessor constants for each LAN Manager API category. For
example, to include support for mailslots, you would add the following to
your program file:
#define INCL_NETMAILSLOT // Include mailslots
#define INCL_NETERRORS // Include error constants
#include <lan.h>
As for linking your application, most LAN Manager applications need to link
only with LAN.LIB, so you simply add this library to those you would
normally use (see Figure 5).
To build a LAN Manager service you will need to include support for the
service APIs by including a #define statement for INCL_NETSERVICE before you
#include LAN.H. Your program must also contain an OS/2 signal handler and
use the service APIs to maintain a simple communication protocol with LAN
Manager. These points will be covered in more detail further on.
Installing a LAN Manager Service
A service is started from the command line using the LAN Manager NET command
or using the LAN Manager full-screen interface. The PBX service could be
started from an OS/2 command prompt by typing:
NET START PBX
Services can also be started and stopped automatically when the LAN Manager
Workstation or Server service starts or stops by adding the service name to
either the wrkservices or srvservices line in LANMAN.INI (see the Microsoft
LAN Manager Administrator's Reference for details on LANMAN.INI).
When requested to start a service, LAN Manager scans the services section of
LANMAN.INI for an entry describing where to find the EXE to execute. Each
entry under the services section names a service and supplies the location
and name of the EXE file to be executed to start the service.
[services]
.
.
.
service_name = service_exe_path
The service_name is the name the service is known by to LAN Manager and the
user; with our sample, it's PBX. The service_exe_path describes the path of
the EXE file to execute. If the service EXE is in the LAN Manager directory,
the pathname can be relative. This is the case for the services shipped with
LAN Manager. If the EXE is outside the LAN Manager directory, the full
pathname and drive must be supplied.
Optionally, a service may take configuration parameters. These are also
specified in LANMAN.INI (though the user may override them when starting the
service),
and should be placed under a section whose heading name is the same as the
service. PBX has five configuration parameters; they would be listed in
LANMAN.INI as shown in Figure 6.
Start-up of a LAN Manager Service
A LAN Manager service receives control at the C main function like any other
C program. Immediately upon receiving control, a service obtains its own
process identifier (PID) and registers with LAN Manager by calling the
NetServiceStatus API:
PIDINFO pidInfo; // OS/2 PID info
struct service_status ssStatus; // LM Service info
// Obtain the process ID of this process
usRetCode = DosGetPID(&pidInfo);
.
.
.
// Inform LAN Manager that this service is in the
// process of installation
// Because the signal handler is not yet installed,
// the service cannot be uninstalled or paused
ssStatus.svcs_pid = pidInfo.pid;
ssStatus.svcs_status = SERVICE_INSTALL_PENDING |
SERVICE_NOT_UNINSTALLABLE |
SERVICE_NOT_PAUSABLE;
ssStatus.svcs_code=SERVICE_CCP_CODE (PBXINSTALLTIME,
0);
usRetCode = NetServiceStatus((char _far *)&ssStatus,
sizeof(ssStatus));
The NetServiceStatus API call takes as its primary argument a pointer to a
buffer that must contain a service_status structure (see Figure 7). LAN
Manager needs the PID of the service to pass control requests to the
service. When a request is made to control a service (for example, NET PAUSE
PBX), LAN Manager signals the service through the OS/2 signal mechanism,
passing a request code in the first byte of the signal argument.
NetServiceStatus is also used by the service to pass status or error
information to LAN Manager. For example, if your service takes more than a
few seconds to install or respond to any LAN Manager request, it should
periodically inform LAN Manager that the request is still pending by calling
NetServiceStatus. Status and error information is passed by setting the
svcs_status and svcs_code fields in the service_status structure. Your
service should set the svcs_status field to the bitwise OR of the
appropriate values (see Figure 8). The service uses the SERVICE_CCP_CODE
macro to set the svcs_code field during installation and while responding to
requests from LAN Manager. The first macro argument is the approximate time
the service will take to install or complete the request in tenths of a
second. The second argument is a checkpoint counter that should be
incremented each time the service informs LAN Manager a request is still
pending.
If an error occurs and your service cannot install, set the svcs_status
field to SERVICE_UNINSTALLED and use the SERVICE_UIC_CODE macro to set the
svcs_code with the uninstall information code (UIC). Some UICs allow an
additional text string to be passed in the svcs_text field. The first
argument to the macro is the code and the second is an optional modifier
(see Figure 9). When the service is uninstalled, use the SERVICE_UIC_CODE
macro to supply LAN Manager with a reason code. For instance,
SERVICE_UIC_NORMAL is the proper UIC during a normal uninstall.
After registering with LAN Manager, the service should next establish a
signal handler for communication with LAN Manager. As mentioned, LAN Manager
communicates with a service by sending OS/2 signals. It does this using the
OS/2 DosFlagProcess API. OS/2 defines three process flags (that is,
signals): PFLG_A, PFLG_B, and PFLG_C. LAN Manager sends requests to a
service using the process flag A (PFLG_A) signal. LAN Manager also defines a
special constant, SERVICE_RCV_SIG_FLAG, which equates to PFLG_A, that can be
used when registering a signal handler to inform OS/2 that the service
should receive signals from LAN Manager. After establishing your signal
handler, inform OS/2 that your handler will ignore CTRL-C, break, and kill
process signals. You want your service to shut down only if requested to by
LAN Manager. Additionally, unless they are used by your service for
communication, process flags B (PFLG_B) and C (PFLG_C) should be regarded as
errors. The InstallSignals function in SIGNALS.C (see Figure 4) demonstrates
the proper technique for establishing a signal handler to communicate with
LAN Manager. Once your signal handler is in place, your service should call
NetServiceStatus again and update LAN Manager with its new status. For
instance, once the signal handler is installed, PBX can be uninstalled,
though it cannot be paused until installation is complete.
Communicating with LAN Manager
LAN Manager control requests are passed to your signal handler in the low
byte of the signal argument, the first argument passed to an OS/2 signal
handler. Again, your handler should respond to these requests quickly or
give LAN Manager status hints as your service is responding. Currently, LAN
Manager passes one of four arguments (see Figure 10). Values 0 through
127 are reserved, leaving 128 through 255 available for your internal
communication. The simplest way to build your signal handler is to use a
switch statement with a case for each possible argument. The PBX signal
handler, SignalHandler in SIGNALS.C, follows the format shown in Figure 11.
The requests SERVICE_CTRL_UNINSTALL and SERVICE_CTRL_INTERROGATE must be
supported by your service. When a SERVICE_CTRL_UNINSTALL is received, your
service should take whatever steps are appropriate for clean up and exit.
Prior to exiting, call NetServiceStatus one last time with the appropriate
UIC code to inform LAN Manager that your service has successfully
uninstalled. The ExitHandler routine in SIGNALS.C handles all exit requests,
error or otherwise, for PBX.
In response to a SERVICE_CTRL_INTERROGATE request, your service should call
NetServiceStatus with the current state of the service. PBX maintains a
global service_status structure that always reflects the current
state of PBX and is returned in response to SERVICE_CTRL_INTERROGATE
requests.
If you allow pausing of your service (as PBX does), you need to process
SERVICE_CTRL_PAUSE and SERVICE_CTRL_CONTINUE requests. A simple and
efficient method is to use OS/2 semaphores to control child threads. If the
semaphore is set, the child thread waits until it is cleared.
Initializing a LAN Manager Service
Once your service is registered with LAN Manager and your signal handler is
in place, you can proceed with initialization. All services should redirect
file handles zero, one, and two (corresponding to standard input, standard
output, and standard error) to the NUL device. The technique used by the
RedirectFiles routine in PBXSRV.C (see Figure 4) is the easiest: it opens
the NUL device for I/O and redirects handles zero through two by calling
DosDupHandle.
After disabling the standard file handles, your service can perform any
specific initialization it has. At this point, most services process the
configurable parameters that were specified either in LANMAN.INI or by the
user when the service was started. LAN Manager passes these parameters to
your service as a standard OS/2 argument string. Values explicitly specified
by the user when the service was started are passed first, followed by any
values from your service's section in LANMAN.INI that were not overridden
when starting the service. These values arrive as the standard C argc-argv
pair. For example, if PBX was started from the command line as below,
NET START PBX /Lines:200 /Connsperthread:10
and LANMAN.INI contained the values listed in Figure 6, PBX would receive
the following in argv:
argv[0] = Pathname of PBX .EXE file
argv[1] = "Lines=200"
argv[2] = "Connsperthread=10"
argv[3] = "LINEBUFSIZE=1024"
argv[4] = "OPENLINES=20"
argv[5] = "AUDITING=NO"
As you can see, the keywords are not in any specific order or case. LAN
Manager preprocesses the keywords to remove blanks and converts the
separating colon to an equal-sign (the user may specify either a colon or an
equal-sign when entering the keyword and its value). Any text following the
equal-sign or colon is treated as the keyword value, including comments, and
is passed to your service. If while overriding a keyword in LANMAN.INI the
user enters only a part of the keyword name, LAN Manager still passes the
full keyword from LANMAN.INI (if it exists). Since explicitly specified
keywords always come first, you may want to ignore second and subsequent
occurrences of a keyword; this allows the user to override values in
LANMAN.INI using short-cuts when starting the service.
Making a Service Known
Some services, such as PBX, offer a resource that will be used
intermittently by clients. In such situations, it benefits the client
application if the service can be automatically detected on the network. PBX
informs clients of its existence by waiting for broadcasts on a mailslot and
then responding with a message containing the name of the computer on which
it is executing.
A mailslot name looks like a pathname without a drive specification whose
root directory is named MAILSLOT. Remote mailslot names are preceded with
either the name of an individual computer (both first-class and second-class
mailslots) or a logical group of computers called a domain, which is used
with second-class mailslots only. A second class message sent to a mailslot
with the following name
\\*\MAILSLOT\mailslotname
sends the message to all machines in the same domain as the sending LAN
Manager workstation. It is recommended that the first qualifier in a
mailslot name, after the keyword MAILSLOT, be a corporate or product
identifier to avoid conflicts with other applications. PBX creates a
mailslot with the following name, using MSJ as the qualifier:
\mailslot\msj\pbx
When a message arrives on its mailslot, PBX responds by sending the name of
the computer on which it is executing prefixed with two backslashes, as in
the start of a remote name. As a message from the client, PBX expects the
computer name on which the client application is executing prefixed with two
backslashes. PBX calls the NetWkstaGetInfo function to get the computer name
on which it is executing. The call is issued twice, the first time with a
zero-length buffer and the second time with the actual buffer size needed.
Any LAN Manager call that returns a structure containing pointers to strings
or other data (instead of a fixed-length array) returns the structure and
the data addressed by the structure in the supplied buffer. Since the buffer
size cannot be statically predetermined, you should make the LAN Manager API
call twice. On the first call your service would pass a zero-length buffer.
LAN Manager will return NERR_BufTooSmall for a return code and the actual
number of bytes needed in the appropriate argument (the last argument on the
NetWkstaGetInfo call). Next, you allocate a buffer of the required size and
make the call again to retrieve the data.
If it is necessary to ensure that only a single instance of your service
exists in a particular LAN Manager domain, a similar approach may be used.
Your service would create a mailslot and wait for messages to arrive, as PBX
does; if a message is received, your service would send an appropriate
response. To determine if an instance of your service is already executing,
your service should send a broadcast message on the mailslot while it is
initializing, and briefly wait for a response. If no response is received,
your service could assume that it is running alone.
Multiple Threads in a LAN Manager Service
When writing a service it's best to use a multithreaded design. Since LAN
Manager communicates with a service through the OS/2 signal mechanism, your
service's main thread will be used to process LAN Manager signals. With
simpler services, it won't be a problem if one thread is used for all
processing. But in more complex programs, the sudden interruption of
processing may leave your service in an unstable state. After initialization
is completed, your main application thread should sleep. This is done most
efficiently by waiting on a dummy OS/2 semaphore.
When its initialization is completed, PBX uses its main thread for
processing signals and responding to broadcasts arriving on its mailslot.
// Run until PBX is shutdown
do {
// Wait until a signal is received or a message
// arrives on the mailslot (which means a client is /// looking for the PBX)
usRetCode = DosReadMailslot(
pbPBXMem->hMailslot, // Handle
pbPBXMem->pbMSlotBuf, // Buffer
&usByteCnt, // Bytes read
&usNextCnt, // Next msg size
&usNextPriority, // Next msg
priority
MAILSLOT_NO_TIMEOUT); // Wait forever
// If control has been returned because a client
// sent a message,return to the client the computer
// name of the machine on which PBX is executing
if (usRetCode == NERR_Success &&
usByteCnt != 0 ) {
strcat(pbPBXMem->pbMSlotBuf, ANNOUNCELINE);
usRetCode = DosWriteMailslot(
pbPBXMem->pbMSlotBuf, // Mailslot
pbPBXMem->pszPBXMsg, // Message
pbPBXMem->usPBXMsgSize, // Message
size
9, // Priority
2, // 2nd class
0L); // No wait
}
} while (usRetCode == ERROR_INTERRUPT ||
usRetCode == ERROR_SEM_TIMEOUT );
// Control never arrives here; the ExitHandler
// always exits PBX
return;
}
Logging Events with LAN Manager
To assist the network administrator, your service should log significant
events with LAN Manager. LAN Manager provides API support for two event
logs, an error event log and a non-error (or audit) log. At the very least,
errors that cause your service to shut down should be reported to LAN
Manager in the svcs_code field of the service_status structure (as noted
above). However, for these and other errors, your service should also log
the event with LAN Manager.
Whenever a significant error is encountered, PBX writes a message to the LAN
Manager error log using NetErrorLogWrite. The second argument passed to this
function is the error number. There are more than 120 error numbers. Most
are specific to services shipped with LAN Manager, some may be used by your
service, and one, NELOG_OEM_Code, was designed for third-party services.
Your service can pass NULL-separated substitution strings for the message
and the number of substitution strings as the sixth and seventh parameters
to NetErrorLogWrite. The NELOG_OEM_Code error message is defined as nine
substitutable strings (with no predefined text). It is recommended that the
first four strings be the company name, service name, error severity (such
as error versus warning), and subidentifier code. The remaining five strings
should contain additional data or be initialized to empty.
PBX writes all of its error messages to the error log via the ErrorRpt
routine in PERROR.C (see Figure 4) using the NELOG_OEM_Code error code. In
addition to the recommended data, PBX supplies the filename and line number
of the place where the error occurred as part of the message. While this may
be unnecessary after you ship your service, it is extremely helpful during
debugging when similar errors may occur along different code paths.
In addition to logging errors, PBX logs significant, non-error events in the
LAN Manager audit log by calling the NetAuditWrite function at the
appropriate times. Audit logging is enabled/disabled in PBX by the AUDITING
keyword, which defaults to YES. Each record in the LAN Manager audit log is
variable length and contains a fixed header, followed by a variable amount
of event-specific data, and a 2-byte field. The fixed header contains the
total length of the record (the length value at the end of the record is
redundant), the type of the record, and the time the record was written.
Your service does not build either the fixed header nor the length field at
the end of the record. You supply only the type of the audit event and the
event-specific data; everything else is built and supplied by the
NetAuditWrite function. Microsoft reserves audit event types 0 through
32,767; audit event types in the range 32,768 through 65,535 may be used by
third-party applications.
To record an audited event, your service calls NetAuditWrite, passing the
appropriate audit event type, a pointer to the event specific data, and the
length of the data. To simplify the logic of your program, NetAuditWrite
returns NERR_Success even if LAN Manager is not currently maintaining an
audit log. The user can disable all auditing when starting the server
service.
If your service does write audit records, you should also supply either a
facility to examine the records in the audit log or at least the necessary
constants and structures to interpret the records written by your service.
The audit event type used by PBX and the structure that maps the variable
portion of the PBX audit record is contained in PBXSRV.H (see Figure 4).
Debugging a LAN Manager Service
Although messages written to the LAN Manager error log can be helpful during
development to track problems, it may be even more useful to build a debug
version of your service that writes more than just error messages to the
error log. Informational messages could be written at critical checkpoints
to track flow of control and processing of requests handled by your service.
Since a service is a detached OS/2 process, your service cannot write to
standard output or standard error to report problems. It also makes it
difficult to use tools such as the Microsoft CodeView debugger. Therefore,
as you develop your service you may want to consider writing and debugging
the bulk of the application prior to converting it to a LAN Manager service.
Also, once your application has been converted to a service, instead of
redirecting standard output to the NUL device, you could direct it to a disk
file and use printf or fprintf to record debugging information. This
technique was used very successfully while developing PBX.
The next article will concentrate on the portions of PBX that create and
manage the named pipes and demonstrate how to build a sophisticated named
pipe server. It will explain multiple threads, managing multiple instances
of a pipe, client-server race conditions, and offer a brief discussion on
the two types of pipes supported by OS/2 and LAN Manager, byte and message
mode pipes.
Figure 2
Category: Access Permissions
Description: Functions to examine or modify resource access
permissions
C Define: INCL_NETACCESS
Category: Alert
Description: Functions to notify services and applications of
network events
C Define: INCL_NETALERT
Category: Auditing
Description: Functions to access and control the LAN Manager
audit log
C Define: INCL_NETAUDIT
Category: Character Device
Description: Functions to control shared communication
devices (such as COM ports) and their queues
C Define: INCL_NETCHARDEV
Category: Configuration
Description: Functions to read the LANMAN.INI file
C Define: INCL_NETCONFIG
Category: Connection
Description: Functions to list connections on a LAN Manager server
C Define: INCL_NETCONNECTION
Category: Domain
Description: Functions to retrieve domain information
C Define: INCL_NETDOMAIN
Category: Error Logging
Description: Functions to access and control the LAN Manager
error log
C Define: INCL_NETERRORLOG
Category: File
Description: Functions to monitor and close file, device, and
pipe resources on a server
C Define: INCL_NETFILE
Category: Group
Description: Functions to control groups of user (part of the
LAN Manager user account subsystem)
C Define: INCL_NETGROUP
Category: Handle
Description: Functions to retrieve or set information for
character device or named pipe specified by a handle
C Define: INCL_NETHANDLE
Category: Mailslot
Description: Functions supporting LAN Manager mailslots
C Define: INCL_NETMAILSLOT
Category: Message
Descripton: Functions to send and receive messages, and
manipulate message aliases
C Define: INCL_NETMESSAGE
Category: Print Destination
Description: Functions to control printers that receive
spooled jobs
C Define: Uses PMSPL.H or DOSPMSPL.H
Category: Print Job
Description: Functions to control jobs in a printer queue
C Define: Uses PMSPL.H or DOSPMSPL.H
Category: Printer Queue
Description: Functions to control printer queues
C Define: Uses PMSPL.H or DOSPMSPL.H
Category: Remote Utility
Description: Functions to support remote file copy and move, the
execution of remote programs, and accessing time-of-day
on a remote server
C Define: INCL_NETREMUTIL
Category: Server
Description: Functions to perform adminstrative tasks on a server
C Define: INCL_NETSERVER
Category: Service
Description: Functions to control LAN Manager services
C Define: INCL_NETSERVICE
Category: Session
Description: Functions to control sessions between workstations
and a server
C Define: INCL_NETSESSION
Category: Share
Description: Functions to control shared resources
C Define: INCL_NETSHARE
Category: Statistics
Description: Functions to retrieve or clear statistics for a
server or workstation
C Define: INCL_NETSTATS
Category: Use
Description: Functions to examine or control uses between a
workstation and a server
C Define: INCL_NETUSE
Category: User
Description: Functions to control a user's account in the user
account subsystem
C Define: INCL_NETUSER
Category: Workstation
Description: Functions to control the operation of a workstation
C Define: INCL_NETWKSTA
Figure 5
■ OS/2 1.2 PMSPL.LIB Print functions (DosPrintxxx)
LAN.LIB All other functions
■ OS/2 1.1 NETSPOOL.LIB Print functions
LAN.LIB All other functions
■ DOS 3.1 and up DOSLAN.LIB All functions
■ Windows 3.0 PMSPL.LIB Print functions
LAN.LIB All other functions
■ Windows 2.x PMSPL.LIB Print functions
LAN.LIB All other functions
Figure 6
[pbx]
lines = 100
linebufsize = 1024
openlines = 20
connsperthread = 5
auditing = no
Figure 7
struct service_status {
unsigned short svcs_status; /* Current service state */
unsigned long svcs_code; /* Install/Uninstall code */
unsigned short svcs_pid; /* Process identifier of service */
char svcs_text[64];/* Additional text buffer */
};
Figure 8
■ General status SERVICE_UNINSTALLED
SERVICE_INSTALL_PENDING
SERVICE_UNINSTALL_PENDING
SERVICE_INSTALLED
■ Paused/Active status SERVICE_ACTIVE
SERVICE_CONTINUE_PENDING
SERVICE_PAUSE_PENDING
SERVICE_PAUSED
■ Uninstallable indication SERVICE_NOT_UNINSTALLABLE
SERVICE_UNINSTALLABLE
■ Pausable status SERVICE_NOT_PAUSABLE
SERVICE_PAUSABLE
Figure 11
// Extract signal argument
fSigArg = (UCHAR)(usSigArg & 0x00FF);
// And take the appropriate action
switch (fSigArg) {
// Uninstall PBX
case SERVICE_CTRL_UNINSTALL:
// Pause PBX
case SERVICE_CTRL_PAUSE:
// Continue (a paused) PBX
case SERVICE_CTRL_CONTINUE:
// Return service information
// Unrecognized arguments should be treated as
// interrogate requests
case SERVICE_CTRL_INTERROGATE:
default:
}
// Acknowledge with OS/2 when signal processing is
// complete
DosSetSigHandler(0, 0, 0, SIGA_ACKNOWLEDGE, usSigNum);
Improve Windows Application Memory Use with Subsegment Allocation and Custom
Resources
Paul Yao
Improved memory usage is one of the most important enhancements in the
Microsoft Windows graphical environment Version 3.0. Protected mode Windows1
lets you access up to 16Mb of RAM, and in 386 enhanced mode, a virtual
address space as large as four times the available physical RAM can be
created. On a machine with 16Mb of RAM and sufficient room on the swap disk,
a 64Mb virtual address space is possible. This is quite an improvement over
the 640Kb limitation that plagued earlier versions.
Windows lets you choose among several types of memory. This article explores
application memory use in Windows and identifies every place that you can
store a byte of data, concluding with sample programs that demonstrate
subsegment allocation and the creation of custom resources.
Subsegment allocation uses the Windows local heap management routines, such
as LocalAlloc and LocalFree, to manage a dynamically allocated segment.
Programmers working in OS/2 systems will recognize this as something
performed by the DosSubAlloc routine, and OS/2 Presentation Manager
programmers may recall that the WinAllocMem routine provides this service.
In the second half of this article, I'm going to show how to use a little
assembly language programming to access this service in a Windows program.
Windows has built-in support for several types of resources: dialog box
templates, menu templates, icons, cursors, and so on, as well as
custom-written resources. Resources are quite memory-efficient because
they are read-only data objects that can be demand-loaded at any time, and
can be discarded (purged) from memory when system memory is low. When a
resource is needed again, it can be reloaded from disk. I'll demonstrate
creating and using custom resources with a sample program.
Memory Management Factors
Four factors are important when dealing with memory: allocation, visibility,
lifetime, and overhead.
"Allocation" refers to who sets aside a piece of memory (see Figure 1). In
some cases, the compiler allocates memory in response to the declaration of
variables. In other cases, memory is allocated in response to calls made to
dynamic allocation routines. Windows has two distinct dynamic allocation
packages: one to allocate segments, and one to partition segments into
smaller pieces. Memory allocation can also occur as a side effect of
creating graphical and user interface objects. For example, when you create
a pen in the Graphics Device Interface, space is set aside in GDI's local
heap. When you create a window or a menu, the Windows user interface manager
allocates memory in its own local heap.
"Visibility" describes who can access memory. Some objects have a very
limited visibility, such
as automatic variables declared inside a function. Other objects, such as
dynamically allocated segments, are visible systemwide. These objects are
the most suitable for sharing data among programs, and are central to the
implementation of the Windows Clipboard and Dynamic Data Exchange (DDE)
mechanisms.
"Lifetime" pertains to the way memory is reclaimed. Generally, when a
program terminates, Windows frees the memory the program allocated. Proper
reclamation of memory is essential to the health and well-being of Windows
as a whole. A few pitfalls require extra care when programming. For example,
programs must explicitly free GDI objects and user interface objects such as
menus before ending. A future version of Windows will hopefully reclaim
memory allocated for these types of objects as well.
"Overhead" refers to the extra cost of using a specific type of memory over
and above the actual bytes used. It is a factor primarily in the context of
dynamically allocated memory. For example, every global memory object has a
minimum overhead of 24 bytes. If you are used to allocating many small,
dynamic memory objects, you need to rethink your use of dynamic memory. In
most cases, arrays of data structures are a more efficient means of storing
data in Windows than linked lists, which are popular with C programmers.
Since Windows is built on top of the segmented architecture of the Intel-86
family of processors, the following discussion is organized in terms of the
segments that are present in the Windows global heap.
Default Data Segment
Windows uses two types of executable files: application programs and
dynamic-link libraries (DLLs). Applications are the active "clients" that
directly interact with users. DLLs are the passive "servers" that provide
code, data, and device support to applications. Both are referred to as
modules.
In Windows, every module can have a private data segment, which is sometimes
referred to as a default data segment. The C compiler, the linker, and the
Windows multitasking switcher together ensure that every module has access
to its correct data segment. From an architectural point of view, this means
that the processor's data segment (DS) register is set up every time a
module boundary is crossed. From a programming point of view, module
boundaries can only
be crossed by calling exported functions. Exported functions are listed in
the EXPORTS section of a module definition file or defined with the _exports
pragma.
Because of the manner in which Windows maintains the DS register for
different modules, you must use either the small or medium compiler model.
These memory models, after all, are built for a single data segment. Other
compiler models such as compact, large, and huge can be used, but they
impose certain restrictions on programs. One reason to use these models is
that they support multiple data segments. However, Windows allows only one
instance of a program to run if it has multiple data segments. Furthermore,
in real mode multiple data segments must be declared FIXED in the program's
DEF file. Unfortunately, this causes problems using the local heap, since a
fixed heap cannot grow beyond its initial size. This is why most Windows
programmers have concluded that these compiler models aren't worth the
trouble.
In Figure 2, HeapWalker is shown displaying the segments belonging to the
Windows Calculator. The highlighted segment is CALC's default data segment.
In many respects, a program's default data segment is like any other segment
in the global heap: it is allocated with a call to GlobalAlloc and has a
maximum size of 64Kb.
In other ways, a module's default data segment is different from other
segments. For one, it is automatically locked whenever a program receives a
message. Unlike most data segments, a program's default data segment is
automatically locked, so you don't have to lock it explicitly.
Another way in which the default data segment differs from other segments is
that it is accessible with near data pointers. That's because the DS
register points to the default data segment. When a Windows program calls a
DLL routine (such as TextOut), the processor's DS register is set up to
point to the library's default data segment. The same thing happens when
Windows calls a program's WinMain, or when it calls a WinProc. Handshaking
ensures that the DS register is assigned so that it references the module's
default data segment.
A typical default data segment consists of a header, a static data area, a
stack, a local heap, and an optional atom table (see Figure 3).
The header is a 16-byte data area containing pointers that Windows uses to
manage a default data segment. A Windows program must leave this area alone
since it is automatically allocated at compile/link time and managed by
Windows at run time.
The header contains five pointers. Perhaps the most important is pLocalHeap,
which points to the beginning of the local heap in the default data segment.
The local heap management routines use this pointer to find the heap. A
Windows program should not use it directly to manipulate the heap but
instead should call the Windows library routines.
Three of the pointers reference the stack: pStackBot, pStackMin, and
pStackTop. When running the debug version of Windows, these pointers are
used to check for stack overflow. It is a good idea to test your programs in
this special version to detect stack overflow and other error conditions.
The fifth pointer, pAtomTable, points to a local atom table if the program
has created one. An atom table stores variable length strings in a common
hash table. An atom is identified by a 2-byte handle, which allows variable
length strings to be referenced using a fixed-length 2-byte value. The atom
table is itself part of the default data segment's local heap.
The header does not contain any pointers to the static data area. The
compiler defines the static data area and generates code that correctly
accesses it.
The Static Data Area
The static data area contains the static variables, which C programmers
sometimes refer to as global variables. The static variables are those
declared outside a function boundary, and all variables declared with the
static keyword. In addition, all literal strings are stored as static data
objects.
In the following code fragment, four objects are allocated in the static
data area:
char *pchFile;
long lLength;
long FAR PASCAL WndProc (HWND hwnd, WORD wMsg,
WORD wParam, LONG lParam)
{
static int iCount;
PAINTSTRUCT ps;
switch (wMsg)
{
case WM_PAINT:
BeginPaint (hwnd, &ps);
TextOut (ps.hdc, 10, 10, "Windows", 7);
EndPaint (hwnd, &ps);
The two variables defined outside the procedure, pchFile and lLength, are
allocated in the static data area, as you would expect. So is iCount, which
uses the static keyword. This keyword makes iCount visible only within this
routine. But as a static object, it has a lifetime as long as the program
itself and therefore it resides in the static data area.
The fourth object that is allocated in the static data area is the string
"Windows". Every literal string gets its own data area even if two strings
are the same. If a string is going to be used in several places, it is a
good idea to define it once as an array and then reference the array by
name. Even better, use a string table to minimize the impact of literal
strings on the size of the static data area. A string table is a resource
that keeps strings on disk until they are needed. When needed, a string is
read from disk into its own discardable data segment. This way, when your
program is finished using them, string resources can be purged from system
memory.
The Stack
The stack is a dynamic data area that is managed by high-level languages
like C. The stack is so important that 80x86 processors have a set of
registers dedicated to its support and maintenance: the SS (stack segment)
register, the BP (base pointer) register, and the SP (stack pointer)
register.
When the call machine instruction is executed, a return address is
automatically pushed onto the stack by the CPU. When the return (ret)
instruction is executed, the return address is popped from the stack to
determine the instruction to which control is to be passed.
The C compiler and the CPU store three things on the stack: automatic or
"local" variables, arguments to called functions, and return addresses. The
STACKSIZE statement in a program's DEF file sets the size of a stack, with a
minimum stack of 5Kb allocated by the Windows loader.
Automatic variables are allocated on entry into a function and freed upon
exit. All variables declared inside the boundaries of a function without the
static keyword are automatic variables. In the code fragment shown earlier,
the PAINTSTRUCT variable ps is an automatic variable. If a program uses a
lot of automatic variables, the STACKSIZE may need to be increased to
reflect this.
Figure 4 shows a function being called in C, the corresponding assembly
language instructions that are generated, and the arrangement of the stack
after a function has been called and the stack set up. Each push instruction
copies two bytes to the stack. In this case, the called function, y, takes
three integer parameters, so three push instructions are generated to put
these parameters in place. The call instruction places a return address on
the stack, then passes control to the called function.
Inside the called function, the compiler creates code to adjust the BP
register to establish the stack frame. The base pointer then serves as the
anchor point from which parameters can be referenced as positive offsets
from the base pointer. For example, this assembly language instruction
places the third parameter into AX.
mov ax, [bp+04]
If there are local variables, the compiler creates code to adjust the SP
register to reserve space on the stack for them. This is done by the
following assembly language instruction, which is found in Figure 4:
sub sp,6
Local variables are accessed as negative offsets from the base pointer. For
example, this instruction copies the third local variable in the example
into the AX register.
mov ax, [bp-06]
Though the workings of the stack are somewhat esoteric and complex, the C
compiler usually insulates you from needing to understand every detail of
its operation. Hopefully this brief explanation has clarified the role of
the stack, and therefore the rationale behind the size that you specify in
the STACKSIZE statement.
The Local Heap
The local heap provides a private area in the default data segment from
which memory can be dynamically allocated. Every program's default data
segment has a local heap, which is automatically set up by the program
loader. A local heap is set up by calling the LocalInit routine. Allocating
and using memory in a local heap involves making calls to the local heap
management routines (see Figure 5).
Windows local heap management routines support three types of memory
objects: fixed, moveable, and discardable. These are the same types that can
be allocated on the global heap. Windows local heap routines are much more
sophisticated than the C malloc routine, which allocates only fixed objects.
To support moveable and discardable objects, the local heap manager uses a
handle-based memory allocation scheme. In other words, when a call is made
to the local heap allocation routine LocalAlloc, it returns a memory handle,
not a pointer. A memory handle is an identifier. By hiding the location of
the memory object, the local heap manager can move or discard objects when
necessary to satisfy allocation requests.
To access a local memory object, you lock the object by calling LocalLock.
This routine does two things: it increments the lock count of an object and
it returns a pointer to the object. You can keep an object locked for as
long as you need to access the object. However, since locking prevents the
local heap from being reorganized, it's often a good idea to keep a lock on
for a very short time, perhaps only for the duration of a single message.
Remove a lock by calling LocalUnlock.
To free a memory object that has been allocated on the local heap, you must
first unlock it, and then call LocalFree. The following code fragment
demonstrates allocating a memory object and copying a string into the
object.
HANDLE hMem;
PSTR pstr;
/* Allocate a 15-byte moveable object. */
hMem = LocalAlloc (LMEM_MOVEABLE, 15);
/* Lock the object, getting a pointer. */
pstr = LocalLock (hMem);
if (!pstr)
return (ERROR);
lstrcpy (pstr, "Hello World");
/* Unlock the object. */
LocalUnlock (hMem);
As you can see, error checking is important when you allocate memory. You
have no guarantee that the system will be able to satisfy your allocation
request. And if you write to the NULL pointer returned by LocalLock when it
fails, you overwrite the bytes at the bottom of the data segment. Remember,
the segment header is located at the base of your default data segment.
Overwriting it could be disastrous.
Local Atom Table
A program can create an atom table in its default data segment with a call
to InitAtomTable. This routine creates an atom table in the local heap. An
atom table efficiently stores variable-length strings. Once stored, a 2-byte
atom is returned, which is in some ways analogous to a handle. When a
duplicate string is added to an atom table, the same atom value is returned,
minimizing the duplication of variable-length strings.
In addition to the local atom table, Windows provides a global atom table.
DDE uses the global atom table to pass the names of data topics between
programs.
Dynamically Allocated Segments
Dynamically allocated segments are the most flexible type of read/write
memory for a program. Up to the limit of available system memory, a program
can have as many dynamically allocated segments as it wants, and each
allocated segment can be as large as 64Kb. In fact, you can allocate
segments that are larger than 64Kb, although that is beyond the scope of
this article.
Windows 3.0 has a systemwide limit of 8192 segments in real mode and in 386
enhanced mode. In standard mode, the limit is 4096 segments. The standard
mode limitation reflects the fact that two entries in the system handle
table, also known as the Local Descriptor Table (LDT), are required for each
segment. One entry is for the allocated segment and the other is for a
16-byte header that is attached to every allocated segment. Because real
mode and 386 enhanced mode do not require this extra handle table entry,
they are able to support twice the number of segments as standard mode.
In a future version of Windows, the limit to standard mode segments should
eventually be raised to 8192. This limit will change from a systemwide limit
to a per-task limit for both standard mode and 386 enhanced mode. Windows
will then have one LDT per task instead of the current implementation of one
LDT for the entire system in protected mode.
Programs allocate and manage dynamic segments using Windows global heap
management routines (see Figure 6). Where applicable, routines in Figure 6
are paired as a "top slice" and a "bottom slice" in a construction I call
the Windows sandwich.
My Windows sandwich has three parts: two outside pieces of bread that hold
the third part, the filling (see Figure 7). The first piece of bread
represents the code that borrows a system resource. The second piece of
bread represents the code that relinquishes the resource. The filling
represents code with which a program uses the resource. The idea is that the
filling is always used inside the sandwich, never by itself. The most common
type of sandwich is probably the following, a standard response to a
WM_PAINT message:
case WM_PAINT:
{
PAINTSTRUCT ps;
BeginPaint (hwnd, &ps); // top slice
TextOut (ps.hdc, x, y, "Hi", 2); // filling
EndPaint (hwnd, &ps); // bottom slice
.
.
.
Segment allocation mirrors local heap allocation in that both use a
handle-based memory management scheme. A call to GlobalAlloc returns a
handle, which identifies the memory but does not reveal its location.
Segments can be allocated as fixed, moveable, or discardable. To determine
the attributes of a specific segment, you can run HeapWalker and look for
objects that are identified as type Task. Using Figure 8 as a guide, the
three types of memory attributes can be easily identified. Note that a
discardable segment is also a moveable segment.
To access a global memory object, a call must be made to GlobalLock. Like
its local heap counterpart, LocalLock, GlobalLock increments a memory
object's lock count and returns a pointer to the object. Again, it is a
good idea to keep a lock only as long as an object is being used, which in
most cases means for the duration of a message. Unlocking an object involves
making a call to GlobalUnlock, which decrements the lock count on an object.
The following code fragment allocates a global memory object and copies a
string into the allocated memory.
HANDLE hMem;
LPSTR lpstr;
/* Allocate a 15-byte moveable object. */
hMem = GlobalAlloc (GMEM_MOVEABLE, 15);
/* Lock the object, getting a pointer. */
lpstr = GlobalLock (hMem);
if (!lpstr)
return (ERROR);
lstrcpy (lpstr, "Hello World");
/* Unlock the object. */
GlobalUnlock (hMem);
Error checking is just as important when using global heap objects. There is
no guarantee that Windows will be able to satisfy all of your requests for
memory, so you should plan accordingly. The above code fragment checks the
return value of GlobalLock; many Windows programmers also check for a valid
(nonzero) return value from GlobalAlloc.
For a program to run properly in all operating modes, it's a good idea to
keep global memory objects only for the duration of a single message.
However, if a program is to run solely in protected mode, it can take a
shortcut that is unthinkable in real mode. It can lock global memory objects
once, upon allocation, and keep them locked for as long as the object needs
to be in memory.
This can be fatal in real mode because global memory objects are locked in
the physical address space and quickly create memory sandbars. In protected
mode, however, such objects are not locked in the physical address space.
These objects can be moved unbeknownst to the application program.
Resources
A resource is a read-only data object that has been merged into a module's
executable (DLL, DRV, FON, or EXE) file by the resource compiler. When the
data is needed, it can be read into a discardable segment. When the memory
manager needs to use that memory for other purposes, it can discard or purge
the segment containing the resource.
Locating a resource with HeapWalker is easy, since resources are labeled
clearly. HeapWalker knows if a particular resource is a menu template,
dialog box template, or whatever. HeapWalker can even show the bitmap
associated with a particular resource (see Figure 9). Windows has built-in
support for accelerator tables, bitmaps, cursors, dialog box templates,
fonts, icons, menu templates, and string tables. These resources are used to
support several Windows user-interface objects: menus and dialog boxes, to
name two. In addition, several GDI objects are stored as resources: fonts
and bitmaps, for example.
Each resource resides in a separate segment. A program with one menu
template, three dialog box templates, and two cursors has six segments'
worth of resources. Each resource is put in its own segment so that each can
be loaded and discarded individually.
If you are writing a Windows program that has a fairly large block of
read-only data, you should consider putting the data into a resource. If
your data does not fit easily
into one of the predefined resource types, it's simple to create a custom
resource. I will demonstrate this in the CUSTRES program.
GDI Data Segment
Windows programs can call GDI routines that cause objects to be allocated in
GDI's default data segment. Whenever a call is made to a GDI routine that
contains the word Create (such as CreateDC, CreatePen, CreateFontIndirect),
you know that the routine allocates memory in GDI's local heap (see Figure
10). Fonts and bitmaps also cause memory to be allocated from the global
heap.
In general, if a program creates an object, it should make sure that it
deletes the object. In all versions of Windows, GDI expects every program to
clean up after itself. If a program forgets to delete an object, the
allocated memory is lost to the system. GDI was designed this way to allow
programs and instances of programs to share GDI drawing objects. For
example, the first instance of a program could create the GDI drawing
objects to be used by all instances. When subsequent instances start up,
they access the existing drawing objects either by sending messages or
calling the GetInstanceData routine, which copies the value of static data
objects from one instance's data segment to another instance's data segment.
Unfortunately, this design has resulted in a problem. If a program does not
free a GDI object when it terminates, the memory is lost forever. For this
reason, in a future version of Windows, Microsoft should consider changing
GDI so that, upon termination of a program, all GDI objects created by the
program are automatically destroyed. Until that time, you should ensure that
your program explicitly deletes all GDI objects that it has created (and
that it has not passed to other programs on the Clipboard).
USER Data Segment
The Windows USER module supports Windows user-interface objects, such as
windows, menus, dialog boxes, cursors, carets, and accelerator tables.
Unlike GDI objects, USER objects are not designed to be shared between
programs. For the most part, when a program terminates, USER frees the
memory that has been allocated for the user interface objects. Nevertheless,
you should be aware of the ways that a Windows program can cause memory to
be allocated in USER's local heap, because it is a limited resource that is
shared by all applications.
Quite a few user interface objects are stored in their own segments. In
fact, this is true for all user interface objects that are created as
resources. Others are allocated in USER's local heap, as shown in Figure 11.
While these objects are small, there are a few "gotchas" to avoid.
If you create several different menus and attach some of them to a window
using SetMenu, you have to be careful when your program terminates. Menus
that are connected to a window are automatically freed. Menus that are not
connected to a window, however, must be explicitly freed by your program
when it terminates.
A second "gotcha" occurs when you register a window class. As you may
recall, class registration involves filling in the values of a structure of
type WNDCLASS and passing a pointer to the RegisterClass routine. Make sure
that you fill in all elements of the WNDCLASS structure. If you don't fill
in the values of two fields, cbWndExtra and cbClassExtra, you may be in for
a surprise. These values define the amount of extra bytes to be allocated in
USER's data segment: cbWndExtra defines the extra bytes to be allocated for
each window, and cbClassExtra defines the extra bytes to be allocated for
the class. If you forget to initialize these fields, the USER module
allocates extra bytes using whatever values it finds in the data structure.
Of course, you may wish to use the often-overlooked window or class extra
bytes. They may be accessed via the functions found in Figure 12. They can
be used to connect a window efficiently to its data. For example, two window
extra bytes might be allocated to hold a memory handle for the data to be
displayed in a window. Or two class extra bytes could be allocated for GDI
drawing objects shared by the windows in a class.
Window extra bytes can be very useful when creating custom dialog box
controls, or for creating MDI windows. In general, if you are creating a
window class that will support multiple windows in a single application,
window extra bytes provide an easy way to connect a window's data to the
window itself.
Subsegment Allocation
As already discussed, there are two dynamic memory allocation packages in
Windows: local heap allocation and global memory allocation. By default,
every Windows program has a local heap. The heap is created by a routine
called InitApp, which is undocumented but is part of the standard start-up
sequence of every program. One advantage of the local heap is that the
overhead for objects is low (4 to 6 bytes), and with a granularity of 4
bytes, waste is kept to a minimum. The only problem with the local heap is
that it is too small for many uses. Depending on the size of your stack and
the static data, the room remaining for the largest default local heap might
be 30Kb--50Kb.
This problem can be solved by using the global heap. On 386 systems, this
implies taking advantage of disk swap space in addition to physical RAM.
With the global heap, however, the overhead per object is high. Also, at a
minimum of 32 bytes per segment, the granularity of segments is too high to
be used for very small objects. Only large objects or arrays of small
objects are suitable for storage in objects allocated from the global heap.
To get the benefits of both local and global heap management, it's possible
to create a local heap in a dynamically allocated global segment. You can
allocate small objects from this heap, all managed by the local heap
manager, that share the segment efficiently with other objects.
To do this, keep in mind that the first 16 bytes of a segment are reserved
for the use of the local heap manager. At offset 06H, it stores a pointer to
the local heap. This allows the local heap to sit at the end of an
application's default data segment, after its static data and stack.
A second concern involves local heap initialization, which is accomplished
by calling LocalInit. This code fragment allocates a segment and initializes
a local heap in it.
HANDLE hMem;
int pStart, pEnd;
LPSTR lp;
WORD wSeg;
hMem = GlobalAlloc (GMEM_MOVEABLE, 4096L);
if (!hMem)
goto ErrorOut;
lp = GlobalLock (hMem);
wSeg = HIWORD (lp);
pStart = 16;
pEnd = (int)GlobalSize (hMem)-1;
LocalInit (wSeg, pStart, pEnd);
GlobalUnlock(hMem);
GlobalUnlock(hMem);
Notice the two calls to GlobalUnlock. The first counteracts the call to
GlobalLock; the second is needed because LocalInit leaves a segment locked.
Without the second call, the data segment would still be locked. In
protected mode, this doesn't present a problem, but segments that are
unnecessarily locked can create memory sandbars in real mode.
As always, GlobalAlloc's return value should be checked to determine whether
the requested memory is available. Even though you asked for a 4096-byte
segment, because different operating modes align on different segment
boundaries, call GlobalSize to make sure you know the exact size of the
segment. To make room for the header, pStart is set to 16. Set pEnd to the
offset of the last byte in the segment, which is the segment size minus one.
Another way to do this is to set pStart to zero, and set pEnd to the actual
size of the local heap.
pEnd = (int)GlobalSize(hMem)-16;
LocalInit(wSeg, 0, pEnd);
You subtract 16 from the size of the segment to set aside space for the
segment's header.
Accessing the local heap requires some programming in assembly language
because the segment selector of the heap must be placed in DS. Any of the
local heap management routines may then be called to operate on the local
heap.
When the DS register is pointing at a heap segment, don't try referencing
any static variables. You won't be able to access them, but you will
overwrite some of the heap segment instead.
If you are using Microsoft C Version 6.0, you can use the _asm pragma to
embed assembly language in your C code. The following saves DS on the stack,
sets up the DS register to point to the heap segment, allocates memory from
the heap, and restores DS to point to the default data segment.
LPSTR lp;
HANDLE hmem;
WORD wHeapDS; /* Must be a stack variable! */
lp = GlobalLock (hmem); /* Where local heap lives */
wHeapDS = HIWORD (lp);
_asm{
push DS
mov AX, wHeapDS
mov DS, AX
}
hmem = LocalAlloc (LMEM_MOVEABLE, 16);
_asm{
pop DS
}
GlobalUnlock (hmem);
Using a local heap in a dynamically allocated segment requires calls to two
routines, one for the segment and one for the local heap object. To obtain a
pointer to the allocated memory, calls to GlobalLock and LocalLock are
required. And to prevent fragmentation in the global heap or the local heap,
you'll probably want to unlock at both levels.
There are other methods, of course. A program can make all local heap
objects fixed, removing the need to perform the second lock. To be most
effective, it makes sense to build a small subroutine library to manage the
two-level allocation scheme. This might be as simple as creating your own
32-bit handles, using half for the local handle and half for the global
handle. This is the approach used in the sample program, SUBSEG (see Figure
13). Another alternative is for a subroutine package to issue its own
private 16-bit handles that reference a table of segments and local memory
objects.
SUBSEG
The SUBSEG program demonstrates subsegment allocation in a dynamically
allocated segment. I borrow the term subsegment allocation from OS/2, since
it describes this procedure better than the term local allocation. This
program contains a set of subsegment allocation routines that mirror the
Windows standard memory allocation routines. A routine called SubAlloc takes
the same parameters as LocalAlloc. Four other routines provide the basic
allocation services. Another function, SubInitialize, allocates and
initializes the dynamically allocated segment.
SUBSEG displays information about the allocated data objects (see Figure
14). To convince you that it works as advertised, it reads this information
from the object itself. As you can see, the actual size of objects is a
little larger than requested, reflecting the 4-byte granularity of the local
heap manager.
Unlike the handles generated by the local and global heap managers, the
handles issued by the memory management routines in SUBMEM.C are 32-bit. The
HIWORD contains a global memory handle, and the LOWORD contains a local
memory handle. You may want to devise your own scheme for identifying memory
objects, but for many purposes this is fast and simple enough.
If you've examined the code, you may have noticed that the segment allocated
by SUBSEG is never freed. Well, do as I say, not as I do. In other words,
please be sure to free any memory you allocate, unlock any memory you lock,
and in general undo whatever needs undoing to free any resource you use.
Custom Resources
Custom resources, as I said earlier, let you exploit the built-in memory
management features of resources with a minimum of effort. The best custom
resources are data objects that won't change. The sample resource I'll
demonstrate contains a table for determining sine and cosine values. This
look-up table supplies an integer sine value, which is simply a sine value
multiplied by 10,000. Look-up tables are commonly used because they are
often faster than on-the-fly calculations. Also, because 80386 and earlier
Intel processors do not have built-in floating point support, you'll get
faster performance if you limit your calculations to integer arithmetic. (By
the way, this fact influenced Microsoft enough to build Windows without
using any floating point arithmetic.)
CUSTRES contains two routines that calculate sine and cosine values for an
angle in measured degrees. With two functions and 360 degrees, there are 720
different values required for the look-up table. By taking advantage of the
symmetry of sines and cosines and doing a little folding and rotating, I
produce the same results with a single table of 91 sine values.
I obtained the table by writing a C program that calculates sine values and
writes them as ASCII text to a data file. Why ASCII text? I'm going to show
you a trick that lets you build complex binary data objects from ASCII text
files. The only tools required are a macro assembler, the linker, and a
special converter called EXE2BIN.EXE that comes with DOS. Figure 15 contains
the program files that create the custom resource.
The data file created by this program is an assembly language file
containing data definitions but no machine code. Instead, I use the macro
assembler to convert the data definitions into a binary format. The assembly
language file that SINE.EXE creates is SINEDATA.ASM (see Figure 16).
After the data file SINEDATA.ASM has been run through the macro assembler
and the linker, you have a "ready-to-run" EXE file that contains no code.
Next, EXE2BIN is used to isolate the data into a pure binary object; this
program is ordinarily used to create COM files from EXE files. A COM file is
simply a memory image that can be loaded and run as is. Since that's exactly
what you want--a pure, binary image--EXE2BIN does the trick to create the
sine table resource.
To test the sine and cosine functions, CUSTRES connects 360 points to draw a
circle with a radius of 100 pixels (see Figure 17). While this is slower and
rougher than you would expect from a call to GDI's Ellipse routine, it does
show that the generated sine and cosine values at least look right in the
range 0 to 360 degrees.
CUSTRES (see Figure 18) does all its work during the WM_CREATE, WM_PAINT,
and WM_DESTROY messages. All of the sine and cosine information is contained
in two routines, intSin and intCos. The second function actually cheats;
since a cosine is always 90 degrees out of phase with a sine, the intCos
function subtracts 90 degrees from the actual angle and calls the intSin
function.
Using a custom resource requires the FindResource, LoadResource, and
LockResource routines. The first two are called in response to the WM_CREATE
message. The result is a memory handle stored in hresSinData. FindResource
searches for the reference to a resource in the module database, which is
simply an abbreviated memory image of the module's file header. FindResource
takes three parameters:
FindResource (hInstance, lpName, lpType)
The first, hInstance, is an instance handle. The second, lpName, is a long
pointer to a character string with the resource name. The third, lpType, is
a long pointer to a character string with the resource type.
Even though lpName and lpType are pointers to character strings, this is not
the most efficient way to identify a resource, since a string comparison is
more "expensive" than an integer comparison. Because of this, I use a macro,
MAKEINTRESOURCE, which lets me define integers and use them in place of a
character string. Following are the two integers defined in CUSTRES:
#define TABLE 100 /* Custom resource type. */
#define SINE 100 /* ID of sine table. */
These integers are used in the call to FindResource, as follows:
hRes = FindResource (hInst,
MAKEINTRESOURCE(SIN), /* Name. */
MAKEINTRESOURCE(TABLE)); /* Type. */
The MAKEINTRESOURCE macro creates a pseudo-pointer, with 0 for a segment
identifier and the integer value for the offset value. It casts this value
as an LPSTR. When the FindResource routine sees this value, it does not
treat it as a pointer (this would be a fatal error). Instead it uses the
2-byte integer value to find the resource definition, using the following
line in the resource file.
SIN TABLE sinedata.bin DISCARDABLE
This line causes the data in the resource file, SINEDATA.BIN, to be copied
entirely into CUSTRES.EXE at compile/link time. This means that CUSTRES is a
standalone program and doesn't need the original resource data file to be
present at run time.
Once FindResource has provided a resource identifier, a call to LoadResource
provides a global memory handle for the resource itself. LoadResource, the
next routine called, is defined as follows:
LoadResource (hInstance, hresInfo)
Its first parameter, hInstance, is the instance handle; the second
parameter, hresInfo, is the handle returned by the FindResource routine.
In spite of its name, LoadResource does not load a resource. It allocates a
global memory object with a size of 0. No memory is set aside; only a global
memory handle is assigned. CUSTRES stores this value in hresSinData.
The routine that causes a resource to be loaded into memory is LockResource.
CUSTRES calls this routine only when it needs to access the sine table data.
By postponing the loading of such a memory object, CUSTRES helps minimize
the demands it makes on system memory. LockResource itself performs several
tasks: it loads the resource into memory, locks it in place, and returns a
pointer to the data. LockResource is defined as follows.
LPSTR LockResource (hResData)
hResData is the handle returned by the LoadResource function. LockResource
returns a long pointer to a string. CUSTRES casts this return value to a
long pointer to integers.
int FAR * fpSin;
fpSin = (int FAR *)LockResource (hresSinData);
if (fpSin == NULL)
return (0);
Casting prevents the compiler from complaining about a type-mismatch error.
Notice that a check is made on the return value from LockResource, in case
something (like insufficient memory) prevented the resource from being
loaded.
The LockResource routine should always be used with UnlockResource (remember
the sandwich?). This construction was discussed earlier as a way to organize
the use of a shared resource, in this case memory. LockResource loads a
resource and ties it down in memory. UnlockResource unties the resource to
allow it to move, or to be discarded. In CUSTRES, the intSin function uses
these two routines to bracket its use of the sine data, creating a Windows
sandwich that ensures that the object is locked when you need it, and
unlocked when not. UnlockResource is defined as follows. hResData is the
handle returned by the LoadResource function.
BOOL UnlockResource (hResData)
The final routine involved with handling custom resources is FreeResource,
which frees the memory associated with a custom resource. Again, hResData is
the handle returned by the LoadResource function.
FreeResource (hResData)
CUSTRES calls FreeResource in response to the WM_DESTROY message to
deallocate the sine resource. This program doesn't actually need to call
FreeResource, since the resource will be freed when CUSTRES terminates, but
good programmers clean up after themselves.
This program draws using the default MM_TEXT mapping mode. The circle in
Figure 17 is perfectly round only if the program is displayed on a VGA
monitor with a 1:1 aspect ratio. CUSTRES moves the origin of the logical
coordinate system to the middle of the window with the following code:
GetClientRect (hwnd, &r);
SetViewportOrg (ps.hdc, r.right/2, r.bottom/2);
Conclusion
As a Windows programmer, you have many options for allocating and working
with memory. Knowing how to exploit the different ways of packaging data in
Windows should help you write programs that take better advantage of what
the system has to offer.
1 For ease of reading, "Windows" refers to the Microsoft Windows graphical
environment. "Windows" refers to this Microsoft product and is not intended
to refer to such products generally.
Figure 5. Local Heap Management Routines
■ LocalAlloc
Allocates memory from a local heap.
■ LocalCompact
Reorganizes a local heap.
■ LocalDiscard
Discards an unlocked, discardable object.
■ LocalFlags
Provides information about a specific memory object.
■ LocalFree
Frees a local memory object.
■ LocalHandle
Provides the handle of a local memory object
associated with a given memory address.
■ LocalInit
Initializes a local heap.
■ LocalLock
Increments the lock count on a local memory
object and returns its address.
■ LocalReAlloc
Changes the size of a local memory object.
■ LocalShrink
Reorganizes a local heap and reduces the size of
the heap (if possible) to the initial, starting size.
If this routine is successful, it reduces the size of the data segment
that contains the heap, so that
the memory can be reclaimed by the global heap.
■ LocalSize
Returns the current size of a local memory object.
■ LocalUnlock
Decrements the lock count on a local memory object.
Figure 10. Memory Used in GDI's Local Heap
Object Size
■ Bitmap 28--32 bytes
■ Brush 32 bytes
■ Device context 100 bytes per device for fixed overhead +
200 bytes per DC allocated
■ Font 40--44 bytes
■ Palette 28 bytes
■ Pen 28 bytes
■ Region 28--104 bytes
Figure 11. Memory Used in USER's Local Heap
Object Size
■ Menu 20 bytes per menu +20 bytes per menu item
■ Window class 40--50 bytes
■ Window 6070 bytes
Figure 12. Memory Used in USER's Local Heap
Window Extra Bytes Routines Description
■ SetWindowWord Copies two bytes into window extra bytes
■ SetWindowLong Copies four bytes into window extra bytes
■ GetWindowWord Retrieves two bytes from window extra bytes
■ GetWindowLong Retrieves four bytes from window extra bytes
Class Extra Bytes Routine Description
■ SetClassWord Copies two bytes into class extra bytes
■ SetClassLong Copies four bytes into class extra bytes
■ GetClassWord Retrieves two bytes from class extra bytes
■ GetClassLong Retrieves four bytes from class extra bytes
Figure 15. SINE
SINE.MAK
sine.obj: sine.c
cl -c sine.c
sine.exe: sine.obj
link sine;
sinedata.asm: sine.exe
sine
sinedata.bin: sinedata.asm
masm sinedata.asm, sinedata.obj;
link sinedata, sinedata.exe;
exe2bin sinedata.exe
SINE.C
/*-------------------------------------------------------------*\
| SINE.C - Creates an .ASM data file containing sine values |
| from 0 to 90 degrees. This file is suitable |
| for creating a custom Windows resource. |
\*-------------------------------------------------------------*/
#include "stdio.h"
#include "math.h"
char achFileHeader[] =
";\n"
"; Sine/Cosine Data Table\n"
";\n"
";\n"
"; Table of Sine values from 0 to 90 degrees\n"
";\n"
"SINDATA segment public\n";
char achFileFooter[] =
"\n"
"SINDATA ends\n"
"END\n";
main()
{
double dbPI = 3.1415926536;
double dbRad;
FILE * fp;
int iAngle;
int iSin;
if (!(fp = fopen("sinedata.asm", "w")))
{
printf("Can't create sinedata.asm.\n");
exit(1);
}
fprintf (fp, achFileHeader);
fprintf (fp, "DW ");
for (iAngle = 0; iAngle <= 90; iAngle++)
{
dbRad = (((double)iAngle) * dbPI) / 180.0;
iSin = sin(dbRad) * 10000.0 + 0.5;
fprintf(fp, " %5d", iSin);
if (iAngle % 8 == 7)
fprintf (fp, "\nDW ");
else if (iAngle != 90)
fprintf (fp, ",");
}
fprintf(fp, achFileFooter);
fclose(fp);
}
Figure 16. SINEDATA.ASM, Generated by SINE.EXE
;
; Sine/Cosine Data Table
;
;
; Table of Sine values from 0 to 90 degrees
;
SINDATA segment public
DW 0, 175, 349, 523, 698, 872, 1045, 1219
DW 1392, 1564, 1736, 1908, 2079, 2250, 2419, 2588
DW 2756, 2924, 3090, 3256, 3420, 3584, 3746, 3907
DW 4067, 4226, 4384, 4540, 4695, 4848, 5000, 5150
DW 5299, 5446, 5592, 5736, 5878, 6018, 6157, 6293
DW 6428, 6561, 6691, 6820, 6947, 7071, 7193, 7314
DW 7431, 7547, 7660, 7771, 7880, 7986, 8090, 8192
DW 8290, 8387, 8480, 8572, 8660, 8746, 8829, 8910
DW 8988, 9063, 9135, 9205, 9272, 9336, 9397, 9455
DW 9511, 9563, 9613, 9659, 9703, 9744, 9781, 9816
DW 9848, 9877, 9903, 9925, 9945, 9962, 9976, 9986
DW 9994, 9998, 10000
SINDATA ends
END
Learning Windows Part IV: Integrating Controls and Dialog Boxes
Marc Adler
Dialog boxes and the control windows within them are an integral part of the
Microsoft Windows graphical environment. The topic is broad enough to devote
two articles, both the previous article in this "Learning Windows" series as
well as this one, entirely to it. First, I conclude the discussion of
controls with a look at combo boxes, scroll bars, and user-defined controls,
then I place the controls into their proper setting: a dialog box. Finally,
adding dialog boxes to the sample stock quoting application gives it most of
its functionality.
Combo Boxes
The combo box, which was introduced in Windows1 Version 3.0, combines a
single-line edit control and a list box into one control window. It can be
used to enter a value in an edit field or to choose one string from a
predefined list of text strings. A combo box can replace a series of radio
buttons in a dialog box, taking up less space. In a word processing
application, you might want to enter the type size, in points, of the font
you will be using. A combo box could display a list of existing point sizes
and permit you to type in another number. A radio button group could be used
for this task, but if there were many point sizes, the radio button group
would probably take too much room in the dialog box. In addition, the number
of choices in a combo box can vary throughout the lifetime of an
application, but a radio button group generally has a fixed number of
choices. To add another choice to a combo box control, all the developer has
to do is append a text string to a combo box's list box; adding another
option to a radio group probably means creating a new radio button and
rearranging the entire group.
The first element of a combo box is a single-line edit control. A button
control to the right of the edit field is the second element, and the third
element is a list box control just below the edit control. As you scroll
through the list box, the current selection is displayed in the edit
control. A list box control in a combo box has the standard Windows 3.0 list
box features, including possible owner-draw items.
Windows 3.0 supports three styles of combo boxes. The simple combo box
style, which is the least used, has the CBS_SIMPLE style flag. In a simple
combo box, the list box is always visible. This kind of combo box has no
advantage over defining a separate edit control and list box control, except
that you don't have to code the list-box-to-edit tracking logic.
The other two combo box styles are drop-down (see Figure 1) and drop-down
list, defined by the CBS_DROPDOWN and the CBS_DROPDOWNLIST styles. The only
difference between them is that the edit control is disabled in the
drop-down list combo box, so users cannot enter their own values in the edit
field. This is useful if you want to make users choose from a list of
predefined values only.
The nice thing about the drop-down and drop-down list styles is that the
list box is hidden until you pull it down. You can pull it down either by
clicking on the control's button or by pressing the Alt-Down key
combination. You can use the Up, Down, Page Up and Page Down keys while the
focus is on the control to scroll through the list box items.
The messages and notification codes that are processed by the combo box
message interface are similar to those used by the standard list box and
edit controls. The only difference is that the prefixes CB_ or CBN_ are
used instead of the EM_, EN_, LB_, and
LBN_ prefixes. The single unique message, CB_SHOWDROPDOWN, is used to
display or hide the list box portion of a drop-down or drop-down list combo
box. Just before the list box portion is made visible, the CBN_DROPDOWN
notification code is sent to the parent of the combo box. This gives the
programmer a chance to modify the contents of the list box before it is
shown to the user.
Scroll Bars
Many Windows applications use a window as a "viewport" into some sort of
large data set. A list box window is a viewport into a series of strings, a
word processing document window is a viewport into a portion of the data
file, and so on.
Scroll bars are the primary means for the user to inform the application
that he or she wants to move the viewport to a new position in the data set.
A scroll bar control can be either vertical or horizontal. It has two
primary attributes: the range of values that the length of the scroll bar
represents, and the current position in the range. The current position is
indicated with a square icon called a thumb.
Scroll bars are usually attached to a window when it is created. To attach a
vertical scroll bar to a window, you simply give the window the WS_VSCROLL
style during creation. Similarly, the WS_HSCROLL style attaches a horizontal
scroll bar to the bottom of the window.
Scroll bars can also be used as standalone control windows. A good example
can be seen in the Edit Colors dialog box of Windows Paintbrush (see Figure
2).
A standalone scroll bar control is created in the same way as any other kind
of control window--using CreateWindow or defining the control as part of a
dialog box in a resource file. Vertical control scroll bars have the
SBS_VERT style, and horizontal scroll bars have the SBS_HORZ style.
Windows API functions allow you to query and set the current position and
the range of the scroll bar. The five scroll bar functions are as follows:
int GetScrollPos(hWnd, nBar)
void GetScrollRange(hWnd, nBar, lpMin, lpMax)
int SetScrollPos(hWnd, nBar, iPos, bRedraw)
void SetScrollRange(hWnd, nBar, nMin, mMax, bRedraw)
void ShowScroll bar(hWnd, nBar, bShow)
The first parameter, hWnd, can be either the handle of a scroll bar (when
dealing with a control scroll bar), or the handle of the window that
contains the scroll bar. The second parameter, nBar, details which scroll
bar you want. If nBar is SB_CTL, the hWnd parameter is the handle of a
control scroll bar. If nBar is SB_VERT, hWnd is the handle of a window
containing the vertical scroll bar; if nBar is SB_HORZ, hWnd is the handle
of a window with a horizontal scroll bar.
Scroll bars, unlike other control windows, do not generate WM_COMMAND
messages when the parent window needs to be notified of some event. Instead,
they generate WM_VSCROLL and WM_HSCROLL messages. WM_VSCROLL messages are
sent by vertical scroll bars, and WM_HSCROLL messages are sent by horizontal
scroll bars. The notification codes for these messages are the same, and are
passed in wParam (see Figure 3). If the message was generated by a control
scroll bar, the handle of the scroll bar is passed in the HIWORD of the
lParam (otherwise, the HIWORD of lParam is 0). The LOWORD of the lParam
contains the current thumb position for two of these notification messages.
When the top arrow of a vertical scroll bar or the left arrow of a
horizontal scroll bar is clicked, the SB_LINEUP notification code is sent to
the parent window. Clicking on the down arrow or the right arrow generates
an SB_LINEDOWN code. Clicking in the area between the top or left side of
the scroll bar and the thumb causes the SB_PAGEUP code to be sent. And if
you click between
the bottom or right side of the scroll bar and the thumb, the SB_PAGEDOWN
message is generated. As you drag the thumb, the scroll bar generates a
series of SB_THUMBTRACK notification codes with the current position of the
thumb in the LOWORD of lParam. The SB_THUMBTRACK notification codes allow
the developer to vary the contents of the window continuously as the thumb
is dragged. However, it is not always possible to update the contents of a
window dynamically as the user is scrolling (this is often the case if the
window's contents are too complex). When dragging stops, the scroll bar
generates the SB_THUMBPOSITION message, which contains the final position of
the thumb in the LOWORD of lParam. If dynamic scrolling of the window is not
feasible, the window can be updated once at this point to reflect the new
position of the viewport.
When these notification codes are sent to the WinProc of the scroll bar's
parent, the WinProc must update the scroll bar thumb to reflect the new
position. The WinProc must scroll the data in the window as well. Windows
automatically handles scrolling for list boxes, combo boxes, and edit
controls that have scroll bars.
Sample Scroll Bar Use
Assume that you have written a simple Windows application that constantly
generates a tone (like a program that aids guitarists in tuning their
instruments). You attach a vertical scroll bar to the main window of the
program; the range of the scroll bar represents eight octaves (there are 12
notes in an octave, for a total of 96 notes), and the current position of
the thumb represents the note that is currently sounded. (You have a
function called SoundTone that accepts a note value and generates the
corresponding tone.)
First, create a window with a scroll bar and set the range of the scroll bar
from 1 to 96. Also, the initial tone is set to an A-440Hz, the 22nd note in
your range (see Figure 4).
To get to the next or previous note in the range, the user must click on the
down or up arrow of the scroll bar. To increase or decrease the octave, set
up the scroll bar so that the user can click between the thumb and the down
arrow, or the thumb and the up arrow. Dragging the thumb produces a sliding
glissando sound (see Figure 5). The WM_VSCROLL messages that are sent to the
main window must be processed and the tone adjusted accordingly. You must
also reposition the thumb so that it ends up at the correct location in the
scroll bar.
Dialog Boxes
Dialog boxes can be thought of as the glue that binds groups of control
windows together. A dialog box is simply a pop-up window containing one or
more child windows. These child windows can be in one of the previously
discussed control classes, or they can be custom developed. Two
characteristics distinguish a dialog box from an ordinary pop-up window.
First, a dialog box can be defined in an external ASCII resource file.
Second, a dialog box manager in Windows handles the interaction between the
user and the dialog box.
The dialog box manager processes the keystrokes that occur in a dialog box
and performs the actions indicated by the keystroke. If the user presses
Tab, the dialog box manager must search forward, starting after the control
that currently has the focus, for the next control on which the focus can be
set (that is, a control with the WS_TABSTOP style). If the user presses Esc,
the dialog box manager generates a WM_COMMAND message with the IDCANCEL
value and sends it to the user-defined dialog box procedure.
Several steps are involved in using a dialog box in your Windows
application. First, you must define a dialog box in the resource file (RC),
compile it using the resource compiler, and attach it to the EXE file of
your application. Second, your application must define a dialog box
procedure to handle the messages sent to the dialog box. Third, your
application must load the dialog box from the resource file and call the
dialog box manager to display it so it can interact with the user.
There are two types of dialog boxes, modal and modeless. Most applications
use modal dialog boxes to gather information from the user. A modal dialog
box does not permit you to interact with or switch to any other window in
your application until you are finished using the dialog box. In fact, when
you invoke a modal dialog box, the dialog box manager goes into its own
message loop and dispatches only those messages meant for the dialog box or
one of its controls. Usually a user gets out of a dialog box by clicking on
an OK or Cancel push button or pressing Esc or Enter.
Modeless dialog boxes allow you to switch the focus from them to the rest of
the application. You switch the focus by clicking the mouse on another
window. The Windows PIF Editor serves as a good example. In the PIF Editor,
you can activate the pull-down menu while the focus is set to one of the
modeless dialog box controls. Since the methods for loading and using
modeless and modal dialog boxes are quite different, both will be discussed.
Defining a Dialog Box in a Resource File
Defining a dialog box and its controls by hand in a resource file is
something that an experienced Windows programmer avoids, because it's much
simpler and easier to use the Windows Dialog Editor (see Figure 6) supplied
in the Microsoft Windows Software Development Kit (SDK). Dialog boxes and
control windows can have many different options and styles. The Dialog
Editor allows you to draw and edit a dialog box interactively and generate
the necessary resource file definition.
Figure 7 shows the syntax for a sample dialog box definition. The first line
specifies the name, load options, memory options, and coordinates of the
dialog box. The default load and memory options are LOADONCALL
and MOVEABLE. A LOADONCALL resource is not loaded into memory until the
application needs it. This is one of the primary advantages of resource
files.
The coordinates specified in dialog box definitions work in a special way.
Because Windows applications should be as device-independent as possible,
your dialog boxes should look the same no matter what sort of video display
adapter is used. If a dialog box is meant to encompass the entire display,
it should do so whether the video adapter is at 640 x 350 or 1024 x 768
resolution. To ensure this, the dialog box coordinate system is based upon
the dialog base unit. This is a unit of measurement that is calculated based
on the current system font. In a dialog box definition, the x coordinate and
the width are measured in 1/8 of a dialog base unit. The y coordinate and
the height are 1/4 of a dialog base unit. To determine the exact number of
pixels in the horizontal and vertical dialog base units, use the new Windows
3.0 function GetDialogBaseUnits.
After you define the name, load options, memory options, and coordinates of
the dialog box, you can define other dialog box options. The STYLE statement
can be used to give the dialog box certain style attributes, the same kind
that are used in the CreateWindow function. Four styles are specific to
dialog boxes. These styles are DS_LOCALEDIT, DS_MODALFRAME,
DS_NOIDLEMSG, and DS_SYSMODAL. The DS_SYSMODAL style creates a system
modal dialog box, in which all windows in the system are disabled until you
exit the dialog box. This style of dialog box could be used, for example, to
signal a fatal error that would affect the performance of the Windows
session.
You can give the dialog box a title with the CAPTION statement. Other
options are the MENU statement, the CLASS statement, and the FONT statement.
The FONT statement, new to Windows 3.0, allows you to specify the type style
and point size of the text inside the dialog box.
Once this header information has been defined in the resource definition,
all you need to do is define a list of the control windows between the BEGIN
and END statements. However, the Windows SDK Dialog Editor does all of this
work for you, allowing you to assemble controls graphically, while it builds
the actual RC file of definitions.
The definition of the Add Tick dialog box (see Figure 18) for the stock
charting application is shown in Figure 7. This definition was generated by
the Dialog Editor. You can usually tell if a dialog box definition was
generated by the Dialog Editor because it generates the longer CONTROL-style
statements to define controls as opposed to the handwritten format.
Tab Stops and Groups
I previously stated that the dialog box manager handles keystrokes within a
dialog box, and that when Tab is pressed, the dialog box manager moves the
input focus to the next control. How does it know which control to move the
input focus to?
The WS_TABSTOP style bit must be set for every control window to which you
can move by pressing Tab or BackTab. This is especially important for
programmer-defined controls, since the dialog box manager has no other way
of knowing if a custom control is used for output only or can interact with
the user. (If a static control has the WS_TABSTOP style set, the dialog box
manager must search for the next nonstatic control after the static control
in order to set the input focus to a valid control.)
The dialog box manager also supports something called a control group. This
is a group of controls organized so that you can move among them by pressing
the arrow keys instead of Tab and BackTab. The most common example is a
group box that contains radio buttons. If the first radio button in the
group box has the WS_GROUP style and the first control defined outside of
the group also has the WS_GROUP style, you will be able to move among the
radio buttons by using the arrow keys. If you want to move to a control that
resides outside of the radio group, you must use Tab. In general, to create
a control group, give the first control in the group the WS_GROUP style and
give the first control defined outside of the group the WS_GROUP style too.
If you want to find the window handle of the next item in a control group,
you can use the GetNextDlgGroupItem function. This function is useful to set
up custom keyboard handling in a dialog box. For example, if you have a
Windows application that offers customized keyboard mapping, the user might
define a key combination to be the same as the down arrow key (for instance,
Ctrl-X in WordStar). When the user presses Ctrl-X in a dialog box, you may
want to set the input focus to the next member in a control group. You can
use the GetNextDlgGroupItem function to get the window handle of the next
control in the group and set the focus to it. A similar function,
GetNextDlgTabItem, is used for controls with the WS_TABSTOP style.
Loading a Dialog Box
Now that the dialog box has been defined, compiled, and attached to the EXE
file, you want to load it into your application. Four functions allow you to
load or create a modal dialog box and have it processed by the dialog manager
of the functions, DialogBox and DialogBoxParam, load the dialog box from the
RES file that is attached to the EXE. The other functions, DialogBoxIndirect
and DialogBoxIndirectParam, create dialog boxes from a description stored in
a template in your application. Since this method is uncommon, I will not
discuss it.
The function used to load and invoke a dialog box is DialogBox.
FARPROC lpfn;
lpfn = MakeProcInstance((FARPROC) OpenDlg,
hThisInstance);
DialogBox(hThisInstance, "Open", hWnd, lpfn);
FreeProcInstance(lpfn);
The MakeProcInstance call takes the address of the programmer-defined dialog
box procedure and creates a small piece of code known as a thunk. A thunk
first binds the data specified by hThisInstance to the function pointed to
by OpenDlg, and then branches to that function. MakeProcInstance returns a
far address to the thunk. When finished with the dialog box invocation, free
the thunk by calling FreeProcInstance. (Thunks are a complex mechanism and
are described in greater detail in Programming Windows, Microsoft Press,
1990)
The DialogBox function takes four arguments; the handle of the application's
instance (remember, this is a unique program identifier so that Windows can
distinguish multiple copies of the same application that are running
simultaneously); the name of the dialog box to load; the handle of the owner
window; and the pointer to the instance thunk. The value returned by
DialogBox is the value that the programmer-defined dialog box procedure
returns. (Actually, the return value is the value that is passed as the
second argument to the EndDialog function.)
Dialog Box Procedure
Every dialog box must have a programmer-defined dialog box procedure
associated with it. The dialog box procedure is similar to a standard
WinProc, but dialog box procedures and WinProcs differ slightly. First, a
dialog box procedure returns a Boolean value, TRUE or FALSE.
Unlike a standard WinProc, your dialog box procedure must return TRUE if
it processes a message and FALSE if it does not.
When you invoke a modal dialog box, the dialog box manager goes into its own
internal message loop. If it receives a message intended for the dialog box,
it passes the message to your dialog box procedure first. If the dialog box
procedure returns FALSE, the dialog box manager passes the message to its
own internal default dialog box procedure. If your dialog box procedure
returns TRUE, the dialog box manager does nothing further with the message
and reads the next message. Windows 3.0 allows the advanced programmer to
access the default dialog box procedure, DefDlgProc. Microsoft distributes
the source code to DefDlgProc with the Windows 3.0 SDK.
The dialog box procedure should be defined as follows:
BOOL FAR PASCAL MyDialogProc(hDlg, message,
wParam, lParam)
HWND hDlg;
WORD message;
WORD wParam;
DWORD lParam;
This header looks exactly like a WinProc, except for the BOOL return value.
And, just like a WinProc, the dialog box procedure must be exported in your
application's DEF file.
EXPORTS
.
.
.
MYDialogProc
If a dialog box does not seem to be behaving correctly, check that you have
exported the dialog box function. This is a common error among novice
Windows programmers (and experienced ones too).
Most simple dialog box procedures process only the WM_INITDIALOG and
WM_COMMAND messages. WM_INITDIALOG, similar to the WM_CREATE message, is
sent by the dialog box manager just before the dialog box is displayed for
the first time. This message gives the dialog box procedure the opportunity
to initialize the contents of the controls in the dialog box. The dialog box
procedure, for example, could fill in the contents of edit fields with
default values, fill list boxes with strings, and check certain radio
buttons and check boxes. If you do not want the dialog box manager to set
the initial focus to the first control with the WS_TABSTOP style
automatically, you can set the focus to another control at this point. If
you do change the focus, the dialog box procedure must return FALSE after
processing the WM_INITDIALOG message. If it returns TRUE, the dialog box
manager will set the focus automatically. (This message is the exception to
the rule stated earlier about the Boolean values returned by your dialog box
procedure.)
The lParam of the WM_INITDIALOG message was not used in previous versions of
Windows. Windows 3.0 allows you to pass the dialog box manager a long value
that the dialog box manager will in turn pass to your dialog box procedure
as the lParam value of the WM_INITDIALOG message. To do this, use the
DialogBoxParam function. This function is identical to DialogBox, except
that a fifth argument is added, the long value to pass in the WM_INITDIALOG
message.
Other than processing the WM_INITDIALOG message, your dialog box procedure
mainly handles the WM_COMMAND messages generated when the user manipulates a
control window. These WM_COMMAND messages usually come in two varieties: the
ones generated when you click on a button control, and the ones sent with
notification codes in the HIWORD of lParam.
When the user clicks the OK or Cancel button, it usually means the user
wants to dismiss the dialog box. A WM_COMMAND message is sent to the dialog
box procedure with wParam set to the control identifier of the push
button. The dialog box manager is informed that the user is through with the
dialog box by a call to EndDialog.
EndDialog takes two arguments, the handle of the dialog box, and a
word-length value. This value will be used as the return value from the call
to DialogBox.
switch (message)
{
case WM_COMMAND :
switch (wParam)
{
case IDOK :
EndDialog(hDlg, TRUE);
break;
case IDCANCEL :
EndDialog(hDlg, FALSE);
break;
If the user pressed OK, EndDialog is called to tell the dialog box manager
that the dialog box should be dismissed. The value TRUE will be returned by
the dialog box manager to the application. In the following statement the
value TRUE would be returned and assigned to the variable rc.
rc = DialogBox(hInstance, "AddTick", hWndMain,
lpfnDialogProc);
Similarly, if the user clicked on the Cancel push button, the value False
would be returned to the application.
EndDialog does not dismiss the dialog box immediately. It sets a bit in a
private data structure that the dialog box manager allocates for each dialog
box. (It also copies the value of the second argument into this structure.)
When your dialog box procedure returns control to the dialog box manager,
the manager examines this bit, and if it is set, the manager breaks out of
its message loop.
Message Boxes
Windows programs commonly inform the user of a potential or existing error
condition. For instance, if the user attempts to exit a word processing
application without first saving the document, the application should bring
up a dialog box that contains a warning message and a choice of actions. The
dialog box should have a message such as "Save the document before exiting?"
as well as three push buttons labeled Yes, No, and Cancel.
It would be extremely tedious if the Windows programmer had to design dialog
boxes for each possible error or warning condition. Fortunately, you can
call a single function to display some text in a dialog box and attach one
or more predefined buttons to it. This kind of dialog box is called a
message box; not surprisingly, the function that creates and displays one is
called MessageBox.
The nice thing about using message boxes is that Windows itself contains the
dialog box procedure for all message boxes and handles all the WM_COMMAND
messages. You don't have to create your own dialog box procedure, export it,
and load the dialog box. All you have to do is process the return code from
MessageBox.
The syntax of the MessageBox function is shown below.
int MessageBox(HWND hWndParent, LPSTR lpText,
LPSTR lpCaption, WORD wType)
The first parameter is the handle of the owner window. The second parameter
is the static text that will appear inside the message box. If it is too
long to fit on one line, Windows wraps the text to the next line and
increases the height of the message box accordingly. The third parameter is
an optional title that will appear in the caption of the message box. The
fourth parameter consists of bit flags that tell Windows what kinds of
buttons and icons you want put into the message box. If you want a push
button other than the predefined push buttons, you must define your own
dialog box to be used as a message box. Windows gives you Yes, No, Cancel,
Abort, Ignore, and Retry push buttons. It also allows you to place one of
several predefined system icons on the left side of the message box (see
Figure 8).
The value returned from MessageBox is the control identifier of the push
button the user selected. (If the user presses OK, the IDOK value is
returned; if the user presses No, the IDNO value is returned, and so on.)
The complete list of identifiers of these buttons can be found in WINDOWS.H
(see Figure 9).
A typical use of a message box is:
rc=MessageBox(hWndMain,
"Do you want to exit the application?",
"Exit",MB_YESNO | MB_ICONQUESTION);
if (rc == IDYES)
PostQuitMessage(0);
Modeless Dialog Boxes Revisited
Modeless dialog boxes can be thought of as a hybrid of normal overlapped
windows and modal dialog boxes. To create a modeless dialog box, use the
CreateDialog function. The syntax of CreateDialog is exactly the same as the
DialogBox function. However, when you create a modeless dialog box, the
CreateDialog function returns immediately with the handle of the dialog box;
Windows does not invoke the internal dialog box manager. A typical call to
create a modeless dialog box is shown below.
hDlgModeless=CreateDialog(hInstance,"MyModelessDlg",
hWndMain, lpfnProc);
Since a modeless dialog box is simply a standard overlapped window with
child windows, messages get sent to it as they do to any other window in
your application. All you need is your application's normal message loop:
while (GetMessage(&msg, 0, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
But there is something missing. Suppose the input focus is set to one of the
controls in the modeless dialog box and you press Tab. Nothing happens,
because there is no dialog box manager that processes the keystrokes and
gives a special significance to the Tab key. Of course, you could put code
in your application to handle Tab, but it would be a lot easier to "call the
dialog box manager" temporarily.
Windows allows you to do this. The function
IsDialogMessage(hDlg, &msg)
tests to see if the message contained in the passed MSG structure is meant
for the dialog box whose handle is passed in the first argument. If so,
IsDialogMessage performs all of the necessary keystroke translations and
dispatches the message itself to the dialog box. It takes care of all of the
keystroke interpretations. It then returns TRUE if the message has been
processed, and FALSE if not. The standard message loop modified to include
the use of IsDialogMessage looks like this:
while (GetMessage(&msg, 0, 0, 0))
{
if (!hDlgModeless ||
!IsDialogMessage(hDlgModeless, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
After obtaining the message, the message is passed to IsDialogMessage. If it
returns TRUE, you know it processed the message fully and there is no reason
to call TranslateMessage and DispatchMessage.
The only other concern with modeless dialog boxes is that since the dialog
manager is not used with modeless dialog boxes, the EndDialog function
cannot be used to terminate dialog box processing and destroy the dialog
box. Instead, you must use the standard DestroyWindow call. You also have to
invalidate the dialog box handle so that the IsDialogMessage function won't
be called in your message loop.
DestroyWindow(hDlg);
hDlgModeless = 0;
Sample Application
At the end of the previous article in this series, the stock charting
application had a complete pull-down menu system and MDI capabilities.
Every time you chose the File/New menu item, a new MDI child window was
created. Items on the Window pull-down menu automatically tiled and cascaded
the child windows, and arranged minimized windows nicely. But that was all
the application did; there was no code actually relating to stocks.
Now that you understand dialog boxes, an entry form will be implemented that
prompts the user for information about the stocks. You'll need to decide
what kind of information should be associated with each stock, and create a
data structure describing a stock object. The stock information will also
need to be stored for easy access. Since this application is not designed to
be a commercial application (and since this application is designed to teach
Windows programming, not data structures), ease of implementation and
comprehension will be favored over performance. When applicable, I will
discuss alternatives you can use to obtain more palatable results.
The modules to build the version of STOCK.EXE discussed here are shown in
Figure 10. A real-time stock charting system is not being created, so the
application will not continuously monitor a stock's performance as it is
traded throughout the business day. The only concern is the stock's closing
price and perhaps its volume. Other types of analyses might involve the
average price during the day or the high and low prices, but that is left as
an exercise to the reader.
You may also want to keep track of each date. Here's a structure to hold the
date:
typedef struct tagDate
{
BYTE chMonth;
BYTE chDay;
BYTE chYear;
} DATE;
Six bytes are used for each date; however, I could probably get away with
two bytes if I stored the date as an offset from January 1, 1900 and put it
in an unsigned int. Some analysis programs do not store the date along with
every tick; they are just concerned with a series of price and volume data.
To avoid the overhead of floating point arithmetic, you can store the price
as a four-byte value using the following formula.
price = (dollar amount) X (fraction denominator) + (fractional amount)
For instance, a price of 16 7/8 would be stored as (16 * 8) + 7 = 135.
Now you can define a structure that holds the daily trade information:
typedef DWORD PRICE;
typedef DWORD VOLUME;
typedef struct tagTick
{
PRICE price;
VOLUME dwVolume;
DATE date;
} TICK, FAR *LPTICK;
You also need a data structure that describes the properties of a stock
graph (see Figure 11). A future article will delve further into the logic of
creating graphs. This data structure will be discussed more fully at that
time.
All this information must be transferable to and from disk, or the program
would be pretty useless. Besides the ticker information, you want to save
the name of the stock (an up-to-five letter symbol), a description of the
stock, the graphing parameters, and the number of ticks. You also want to
store a signature at the beginning of the file for data integrity to ensure
that the file read from disk really is one of the stock files (see Figure
12).
Finally, a structure is needed to keep track of all of the stock information
while the application is running. In addition to all of the information that
resides in the stock file, you need to store the filename of the stock file,
the handle of the MDI child window in which the stock information is
displayed, a handle to the memory buffer that holds the tick data, and some
state flags that record the current status of the stock. All the stocks are
linked together in a single-linked list; a handle to the next stock
information data structure is kept in each structure (see Figure 13).
Each stock is stored in its own separate file whose name is comprised of the
stock symbol and an STO extension. For a commercial application, you would
most likely use a Windows-compatible commercial database access library.
Using Dialog Boxes
To demonstrate dialog boxes, examine a dialog box in the stock-charting
application. When the user chooses the File Open menu item, a dialog box
should be displayed that lists the files with the STO extension found in the
current directory. The user should be allowed to change directories and disk
drives to search for other STO files. Almost all Windows applications have a
File Open dialog box. Unfortunately, Windows provides no standard File Open
dialog box, so each vendor implements his or her own in a slightly different
way. The stock-charting application makes use of some sample code that comes
with the Windows SDK for a typical File Open dialog box.
You must first define the dialog box in the resource file. The resource
definition is shown in Figure 14.
The dialog box's name is Open; it has a caption and a system menu (see
Figure 15). An edit field lets the user input a filename or specification
other than the default *.STO. The edit field also tracks the current
selection in the file list box, which contains the names of all the files in
the current directory matching the file specification. It also contains a
list of subdirectories in the current directory, and a list of all disk
drives on the system. Finally, there is an OK push button that the user can
click to open the file, and a CANCEL button that aborts the Open operation.
The OK push button is the default, so if the user presses Enter, a
WM_COMMAND message with IDOK in wParam is sent to the user-defined dialog
box procedure.
The WinProc of the main window in the application processes the WM_COMMAND
message, and when wParam is set to ID_OPEN, invokes the File Open dialog
box.
case ID_OPEN :
lpfn = MakeProcInstance((FARPROC) OpenDlg,
hThisInstance);
DialogBox(hThisInstance, "Open", hWnd, lpfn);
FreeProcInstance(lpfn);
break;
You want the dialog box procedure to return a handle to the opened stock
file rather than a TRUE/FALSE value or a control window identifier. A file
handle returned by the Windows OpenFile function is a WORD value, so it can
be returned to the application by passing it as the second argument to
EndDialog.
Here is the heading of the dialog box procedure:
BOOL FAR PASCAL OpenDlg(hDlg, message,
wParam, lParam)
HWND hDlg;
unsigned message;
WORD wParam;
LONG lParam;
{
WORD index;
PSTR pTptr;
HANDLE hFile;
switch (message)
{
.
.
.
The only two messages that you need to process in the dialog box procedure
are the WM_INITDIALOG and the WM_COMMAND messages. First, look at the small
function called UpdateListBox below. This function forms a complete pathname
with a file specification at the end by concatenating two string variables,
one holding the current path and the other holding the current filespec. It
then calls a Windows function called DlgDirList.
DlgDirList takes five arguments: a handle to a dialog
box, the identifier of a file list box within that dialog box, the
identifier of a static text field also within that dialog box, a string
containing a path and file specification, and a value representing file
attributes. It then searches the path for all files matching the file
specification and the desired attributes. The name of each matching file is
placed into the list box, and the full pathname is placed into the static
text field. DlgDirList also places the subdirectory and drive names in the
list box automatically.
Finally, UpdateListBox sets the contents of the dialog box's edit field to
the default file specification by calling SetDlgItemText. This function
takes the identifier of a control window in a dialog box, determines the
window handle of the control, and sends a WM_SETTEXT message to it.
void UpdateListBox(hDlg)
HWND hDlg;
{
strcpy(str, DefPath);
strcat(str, DefSpec);
DlgDirList(hDlg, str, IDC_LISTBOX, IDC_PATH, 0x4010);
SetDlgItemText(hDlg, IDC_EDIT, DefSpec);
}
Next the WM_INITDIALOG message is processed. A call to UpdateListBox fills
the list box with the names of the STO files and sets the contents of the
file specification edit control to *.STO. The EM_SETSEL message is then sent
to the edit control to highlight the entire field. If you want to send a
message to any control within a dialog box, and you only know the identifier
of the control, you can use SendDlgItemMessage. This is equivalent to the
following statement.
SendMessage(GetDlgItem(hDlg, ID_CONTROL), msg,
wParam, lParam);
You also set the initial focus to the edit control.
case WM_INITDIALOG: /* message: initialize */
UpdateListBox(hDlg);
SendDlgItemMessage(hDlg,/* dialog handle */
IDC_EDIT, /* where to send message */
EM_SETSEL, /* select characters */
NULL, /* additional information */
MAKELONG(0, 0x7fff)); /* entire contents */
SetFocus(GetDlgItem(hDlg, IDC_EDIT));
return (FALSE);
/* Indicates the focus is set to a control */
Now it's time to process the WM_COMMAND message. You want the edit control
to track the currently selected item in the list box. When the current
selection of a list box changes, the list box notifies the dialog box by
sending a WM_COMMAND message with the identifier of the list box in wParam
and the LBN_SELCHANGE notification code in the HIWORD of lParam (see Figure
16). An easy way to get the text of the currently selected item is to call
the built-in Windows function DlgDirSelect.
Why aren't LB_GETCURSEL and LB_GETTEXT messages sent to the list box? Other
than the fact that I have to write more code to do this, the DlgDirSelect
function removes the square brackets surrounding the name of a directory and
the square brackets and hyphens surrounding the names of disk drives in the
list box. It returns zero if the selected item was a filename and nonzero if
it was a directory name. In this case, if the currently selected item is a
filename, you fill the edit control with the name of that file, and if it is
a directory name, you refresh the list box.
Double-clicking a list box selection has the same result as single-clicking
a list box item and then clicking OK. When the LBN_DLBCLK notification code
for the list box is received, the program simply jumps to the code that is
responsible for opening the file (see Figure 17).
Finally, you must process the two push buttons in the dialog box. Remember,
when a button is pressed, a WM_COMMAND message is sent to the parent window
with wParam set to the control identifier of the button. If the user clicks
Cancel, you simply end the dialog box processing and return NULL for the
value of the file handle. If the user clicks OK, you have a lot more work to
do. First, examine the contents of the edit field and see if the filename
contains a wildcard. If it does, assume that the user wants to search for a
new filespec. You get the file specification, separate it into a pathname
and a filespec, and refill the file directory list box with files matching
the new spec. If the edit field is empty, put up a message box warning the
user and return control to the dialog box manager. If neither is the case,
you have a valid filename in the edit field: open the file and return the
file handle.
There you have it--your first dialog box. Once you code a few dialog boxes,
you will find yourself churning out dialog box code by rote.
Adding a Tick
After a day of trading activity, you need to input the closing price and the
total trading volume for each stock in the database. To do this, select the
Add Tick item from the Edit menu. The Add Tick dialog box is displayed
forthe current stock (see Figure 18); you can fill in the various fields to
append a single tick to the list of tickers for that stock. The Add Tick
dialog has three fields; the trading date, the volume, and the final price
(the final price must be an integer for now). At this intermediate stage of
the application, the date and the volume fields are basically ignored in the
code--only the final price is important. Also, at this time, a tick cannot
be inserted in the middle of the ticker list. This will be remedied in a
future version of the code.
Options Dialog Box
The characteristics of a stock and how its graph is displayed can be
controlled through the Options dialog box (see Figure 19). Actually, the
same dialog box is displayed when you add a new stock to the database and
when you change the properties of an existing stock. The only difference is
that in the latter case, the field that contains the name of the stock is
disabled; this is the only piece of information that cannot be altered in an
existing stock. The following lines of code disable the symbol control if we
are altering the characteristics of an existing stock.
if (lpStockInfo->hTicks != NULL)
.
.
.
EnableWindow(GetDlgItem(hDlg, ID_SYMBOL), FALSE);
As mentioned above, at this stage of the program, some of the fields in this
dialog box are ignored by the code. The minimum and maximum prices define
the range of the y-axis of the stock graph. The scale factor is a number
that all of the numeric data is divided by before the number is plotted on
the graph. If the price range of a stock is from 100 to 300, a ticker price
can be 201 different values (assuming that each price is a whole number).
Instead of having the y-axis of the graph divided into 201 points, you can
have the y-axis represent 20 different points if you divide each price by a
scale factor of 10. The tick interval is the interval between two successive
tick marks on the y-axis. The price denominator field is ignored in this
release. It represents the denominator in the fractional part of the stock
price. (Most stocks are traded in either eighths or sixteenths.) The final
component of the Options dialog box is the owner-draw combo box that
contains the styles for the various pens that draw the horizontal and
vertical grids of the graph.
Trying It Out
The function StockFileRead reads a stock file into the application. You can
test this application using the stock file included with the application.
StockFileRead first allocates space for the stock header and then reads the
header. The number field of the header is examined to ensure that it is a
valid stock file. You allocate memory to hold all of the ticker information
and then read the tickers. Then, call GraphCreateWindow to create an MDI
child window for this stock. The handle of the MDI child is returned and
stored in the stock information structure. Likewise, the memory handle of
the stock information structure is stored in the "extra-bytes" area in the
window by using the SetWindowWord function. This makes it possible to
determine stock information associated with a given window very quickly.
When a stock window is displayed, some text about the stock is also
displayed. A future installment will discuss graphics in Windows. I will
then expand this application to show graphical information about each stock.
This article focused on dialog boxes, which are often the most important
objects in a Windows application. You saw how to define one in a resource
file, load it into an application, and create a user-defined dialog box
procedure to handle the messages. The next article finishes the discussion
of dialog boxes, and discusses some of the graphical capabilities of
Windows.
1 As used herein, "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 7. Definition of the Add Tick Dialog Box
ADDTICK DIALOG LOADONCALL MOVEABLE DISCARDABLE 112, 31, 106, 86
CAPTION "Add a Tick"
STYLE WS_BORDER | WS_CAPTION | WS_DLGFRAME | WS_POPUP
BEGIN
CONTROL "Date:", -1, "static", SS_LEFT | WS_CHILD, 2, 7, 22, 8
CONTROL "", ID_TICK_DATE, "edit", ES_LEFT | WS_BORDER | WS_TABSTOP |
WS_CHILD, 53, 5, 48, 12
CONTROL "Closing price:", -1, "static", SS_LEFT | WS_CHILD, 2, 26, 55, 11
CONTROL "", ID_TICK_PRICE, "edit", ES_LEFT | WS_BORDER | WS_TABSTOP |
WS_CHILD, 57, 25, 44, 12
CONTROL "Volume:", -1, "static", SS_LEFT | WS_CHILD, 2, 44, 32, 8
CONTROL "", ID_TICK_VOLUME, "edit", ES_LEFT | WS_BORDER | WS_TABSTOP |
WS_CHILD, 38, 43, 63, 12
CONTROL "OK", 1, "button", BS_DEFPUSHBUTTON | WS_TABSTOP | WS_CHILD, 9,
66, 28, 14
CONTROL "Cancel", 2, "button", BS_PUSHBUTTON | WS_TABSTOP | WS_CHILD, 63,
66, 32, 14
END
Figure 11. Graph Data Structure
/*
Data structure describing how we draw the graph
*/
typedef struct tagGraphInfo
{
PRICE dwMinPrice;
PRICE dwMaxPrice;
DWORD dwScaleFactor;
DWORD dwTickInterval;
WORD iDenominator; /* the fractional amount used for this stock */
WORD iGridPen;
} GRAPHINFO, FAR *LPGRAPHINFO;
Figure 12. Tagging a Stock File
#define MAXSTOCKNAME 5
#define MAXDESCRIPTION 32
#define MAXFILENAME 13
typedef struct tagStockFile
{
DWORD dwMagic;
#define MAGIC_COOKIE 66666666L
char szStock[MAXSTOCKNAME];
char szDescription[MAXDESCRIPTION];
GRAPHINFO graphinfo;
WORD nTicks;
/*
TICK aTicks[1];
*/
} STOCKFILE;
Figure 13. Structure to Track Stock Information
typedef struct tagInCoreStockInfo
{
char szFileName[MAXFILENAME]; /* file name where the stock data is
kept */
STOCKFILE StockFile; /* a copy of the stock file header */
HANDLE hTicks;
HWND hWnd; /* window in which stock is shown */
DWORD dwFlags; /* any kind of status bits we need
to keep */
#define STATE_HAS_VGRID 1L
#define STATE_HAS_HGRID 2L
HANDLE hNextStockInfo; /* link to next stock info struct */
} STOCKINFO, FAR *LPSTOCKINFO;
Figure 14. Open Dialog Box Definition
Open DIALOG 10, 10, 148, 112
STYLE DS_MODALFRAME | WS_CAPTION | WS_SYSMENU
CAPTION "Open "
BEGIN
LTEXT "Open File &Name:", IDC_FILENAME, 4, 4, 60, 10
EDITTEXT IDC_EDIT, 4, 16, 100, 12, ES_AUTOHSCROLL
LTEXT "&Files in", IDC_FILES, 4, 40, 32, 10
LISTBOX, IDC_LISTBOX, 4, 52, 70, 56, WS_TABSTOP
LTEXT "", IDC_PATH, 40, 40, 100, 10
DEFPUSHBUTTON "&Open" , IDOK, 87, 60, 50, 14
PUSHBUTTON "Cancel", IDCANCEL, 87, 80, 50, 14
END
Figure 16. Processing the WM_COMMAND Message
case WM_COMMAND:
switch (wParam)
{
case IDC_LISTBOX:
switch (HIWORD(lParam))
{
case LBN_SELCHANGE:
if (!DlgDirSelect(hDlg, str, IDC_LISTBOX))
{
SetDlgItemText(hDlg, IDC_EDIT, str);
SendDlgItemMessage(hDlg,
IDC_EDIT,
EM_SETSEL,
NULL,
MAKELONG(0, 0x7fff));
}
else
{
strcat(str, DefSpec);
DlgDirList(hDlg, str, IDC_LISTBOX, IDC_PATH, 0x4010);
}
break;
case LBN_DBLCLK:
goto openfile;
}
return TRUE;
Figure 17. Opening a File When OK Is Pressed
case IDOK:
openfile:
GetDlgItemText(hDlg, IDC_EDIT, OpenName, 128);
if (strchr(OpenName, '*') || strchr(OpenName, '?'))
{
SeparateFile(hDlg, (LPSTR) str, (LPSTR) DefSpec,
(LPSTR) OpenName);
if (str[0])
strcpy(DefPath, str);
ChangeDefExt(DefExt, DefSpec);
UpdateListBox(hDlg);
return TRUE;
}
if (!OpenName[0])
{
MessageBox(hDlg, "No filename specified.", NULL,
MB_OK | MB_ICONHAND);
return TRUE;
}
AddExt(OpenName, DefExt);
/* The routine to open the file would go here, and the */
/* handle would be returned instead of NULL. */
StockFileRead((LPSTR) OpenName);
EndDialog(hDlg, hFile);
return (TRUE);
case IDCANCEL:
EndDialog(hDlg, NULL);
return (TRUE);
}
break;
}
return FALSE;
}
Questions & Answers - Windows
Q:
I'm curious about what a window handle (hWnd) object really is physically.
Is it a global memory handle? A segment or selector? An array index?
Josh Trupin
Stamford, CT
A:
When you call CreateWindow or CreateWindowEx, you are calling functions in
the Windows USER.EXE dynamic-link library (DLL). These functions perform the
following call:
LocalAlloc(LMEM_FIXED | LMEM_ZEROINIT, 64)
and then a LocalLock, resulting in a NEAR pointer (PSTR) to 64 bytes inside
of USER's data segment (DGROUP). This pointer is returned as a window handle
(HWND) after USER fills out the 64 bytes that it refers to.
To fill out this 64-byte "window object," USER actually employs an internal
structure similar to the following.
typedef struct tagWND // bytes
// from
{ //length end GetWindow...
//------ ----- -------------
WORD Undocumented[22]; // 44 -64
DWORD ExStyle; // 4 -20 GWL_EXSTYLE
DWORD Style; // 4 -16 GWL_STYLE
WORD ID; // 2 -12 GWW_ID
WORD hWndText; // 2 -10 GWW_HWNDTEXT
HWND hWndParent; // 2 -8 GWW_HWNDPARENT
HANDLE hInstance; // 2 -6 GWW_HINSTANCE
FARPROC WndProc; // 4 -4 GWL_WNDPROC
} WND; // 64
typedef WND NEAR *PWND;
The elegance in this design is that when you make USER calls (functions
whose first argument is usually an HWND), hWnd is cast inside of USER as a
PWND so that the above WND structure elements can be addressed directly;
that is, the following refers to the window's parent.
((PWND)hWnd)->hWndParent
Code that refers to the window structures is therefore
very efficient.
Some of the arguments in the CreateWindow or CreateWindowEx call are placed
directly in the structure; others are determined by USER.
((PWND)hWnd)->ExStyle =dwExStyle;
((PWND)hWnd)->WndProc =<WndProc from WNDCLASS of ClassName>;
((PWND)hWnd)->hWndText =<another local pointer to WindowName> ;
((PWND)hWnd)->Style =dwStyle;
((PWND)hWnd)->hWndParent =hWndParent;
((PWND)hWnd)->ID =hMenu; //if child window,=child ID
((PWND)hWnd)->hInstance =hInstance;
If your window class has extra bytes requested (specified by cbWndExtra in
the WNDCLASS structure of the RegisterClass call), then additional bytes are
allocated at the end of the 64-byte WND structure in the LocalAlloc call. In
other words, the LocalAlloc call that USER performs is really as follows.
LocalAlloc(LMEM_FIXED | LMEM_ZEROINIT, 64 +
<cbWndExtra from WNDCLASS of ClassName> )
This allows you to attach (with SetWindowWord and SetWindowLong)
window-specific information to the window in an object-oriented way in your
application.
From all of the above you can see how SetWindowWord, SetWindowLong,
GetWindowWord, and GetWindowLong work. When you make these calls, USER adds
64 bytes to the hWnd pointer and then adds the nIndex.
(PSTR)hWnd + sizeof(WND) + nIndex
The resulting local offset refers to the value you are setting or getting
with SetWindowWord and SetWindowLong and GetWindowWord and GetWindowLong.
This is how application-specific extra window information is referred to
with a base index of 0, and why the GWW_XXX and GWL_XXX indexes have
negative offsets.
There is little to no protection when using window handles, because no
protected-mode selectors are being used. It is very easy to corrupt USER's
DGROUP by using invalid window handles. A bad window handle could cause USER
to corrupt its DGROUP without a protection violation occurring. Also, it is
very easy to see how you could use another application's valid window
handles to modify its window structures.
Finally, you can see that window handles are reused. Since they are just
offsets to structures that are created and destroyed, it is possible that
another CreateWindow or CreateWindowEx call could return the same "handle"
of a previously destroyed window, if the LocalAlloc resulted in the same
physical offset inside USER's DGROUP.
Therefore, a window handle is really an offset into USER's DGROUP at which a
structure exists that defines a window. If you're still curious, you can
look at these structures using HeapWalker. Just perform Object LocalWalk and
Object Show operations on USER DATA and look for 68-byte memory objects.
They're shown by HeapWalker as 68 bytes because of the additional 4 bytes at
the beginning of every local memory object that KERNEL uses to manage local
heaps.
Q:
I've written an application that uses more than 100 child windows. The
windows are created when the application starts, but only some of them are
visible at the same time. I have two problems.
First, I can only run a few instances of my application. When Free System
Resources (as reported in the Program Manager's About box, for example) gets
close to 0% I get failures trying to create new windows. I have tons of
memory, so I don't understand why this is happening.
Second, it seems as if my application is very slow to initialize. I'm in the
dark on this, since I can't find any documentation on what would cause it to
be slow.
Michael J. Paschal
Waterbury, CT
A:
Windows maintains all windows inside the USER.EXE DLL. USER's data segment
(DGROUP), like all C medium-model DGROUPs, is limited to 64Kb in size. All
windows that are created consume space inside this DGROUP, as do menus,
title-bar names, and so on, so you are bumping up against the 64Kb
segment-size limit. The Free System Resources percentage reflects this
constraint. The amount of global memory you have is irrelevant; the DGROUP
can only grow to 64Kb.
If you want to be able to start many instances of your application, you will
have to change your window creation strategy. If you can get by creating
your windows only as needed (Program Manager does this for its icon and
application-name windows), you should operate that way. If you can destroy
windows when they are not displayed, you should do that (although Program
Manager doesn't). Hiding windows does not reduce their USER memory
requirements.
Your ability to do these things depends on your application. If you are
creating all your windows in advance because you need to be positive you
have them before you start, you are stuck with your current situation. If
you can deal with a possible lack of windows further into run time, change
your strategy.
Each window you create requires a minimum of 68 bytes inside USER's 64Kb
maximum DGROUP. In theory you could therefore have a maximum of about 960
windows. But that estimation ignores the amount of static memory required by
USER, any window text, any menus, and so on. I wrote a small program to test
the maximum number of windows I could create. It turned out that the maximum
number of really bare-bones windows I could create was around 700. Assuming
you would more likely have some trimmings (menus, title-bar text), a more
realistic estimate is 350.
Your application is slow to start up because of KERNEL.EXE's local heap
memory manager. Since local heaps start off being relatively small, KERNEL
has to expand them as more local memory is requested; this takes a small
amount of time. What is more time consuming is that LMEM_MOVEABLE objects
inside USER's DGROUP have to be moved higher so that the LMEM_FIXED window
structures can be created low. Your initialization is slow because KERNEL
has to move the entire upper part (where LMEM_MOVEABLE objects are kept) of
your USER DGROUP and adjust all the local handle table entries, doing this
more than 100 times at start-up in your case.
Q:
I am creating a few edit class windows in my application by using
CreateWindow. They are displayed in my main client area as children of my
main window. The problem I am having is that the text that gets put in or
created in these windows ends up being stored in my application's local
heap, and space there has gotten very tight. Is there any way that edit
class windows can be coerced into having their text stored in the global
heap?
B.D. Beykpour
San Francisco, CA
A:
In edit controls in dialog boxes created with calls such as DialogBox and
CreateDialog, edit control text is stored in the global heap by default. To
have this text stored in the local heap, you must specify the DS_LOCALEDIT
style.
For edit class windows outside a dialog box that are created with
CreateWindow, the reverse is true. By default, edit class window text is
stored in the application's local heap. To force it to be stored in the
global heap, use the following trick. When you call CreateWindow, replace
the hInstance argument with a global handle to a small block of zeroed
memory. Specifically, create the global memory object as follows.
auto GLOBALHANDLE aGhEditBuffer;
aGhEditBuffer=GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT,
256L);
Then call CreateWindow with aGhEditBuffer as the hInstance argument. If you
specify some initial text with the lpWindowName argument, it appears as the
initial text in the edit window. The edit window operates as normal, but
using the global heap for the edit text. The global memory block grows
automatically as the text gets longer. When you destroy the edit window, the
global memory buffer is automatically freed in the same way a local memory
buffer is freed.
Q:
In WINDOWS.H a section labeled OEMRESOURCE appears to refer to the
user-interface bitmaps, but I can't find much documentation on them. What
are all these bitmaps and can I access them?
Alex Polozoff
Austin, TX
A:
Figure 1 identifies these bitmaps and shows which version of Windows they
were developed for. The resolution of each bitmap depends on the display
driver you have installed. In the Windows environment, Versions 1.x and 2.x,
these were monochrome bitmaps; in Windows1 3.0 many of them are full-color
format bitmaps, although only the colors black, white, and gray are actually
used. In some cases (the Close bitmaps, for example) the bitmap really
contains two or more equally-sized bitmaps arranged in rows; these
"sub-bitmaps" would have to be extracted if you wanted to use them.
The following code, which loads OBM_ZOOM, is an example of how to access
these bitmaps:
auto HDC ahDc;
auto HBITMAP ahBm;
auto BITMAP aBm;
ahDc = CreateCompatibleDC(0);
ahBm = LoadBitmap(0, MAKEINTRESOURCE(OBM_ZOOM));
GetObject(ahBm, sizeof(aBm), (LPSTR)&aBm);
/* aBm now has the bmWidth and bmHeight for
BitBlt'ing */
ahBm = SelectObject(ahDc, ahBm);
/* the OBM_ZOOM bitmap can now be BitBlt'ed
here from ahDc using the bmWidth and bmHeight
in aBm */
ahBm = SelectObject(ahDc, ahBm);
DeleteObject(ahBm);
DeleteDC(ahDc);
Q:
I have a child window that has a title bar in my client area. The child
window looks similar to the PageMaker Toolbox window. The title bar is meant
simply to label the window, though; I do not want the user to be able to
move the child window by grabbing the title bar with the mouse. How do I
lock down a child window that has a title bar?
Frank Mena
San Jose, CA
A:
What you want to do is prevent Windows from processing the move system
command for the child. The following code fragment, which would be placed at
the end of the child's WndProc message switch, shows how to do this.
switch (awMsg)
{
.
.
.
case WM_SYSCOMMAND:
if ((awParam & 0xFFF0) == SC_MOVE)
break;
default:
return DefWindowProc(ahWnd, awMsg, awParam,
alParam);
}
return 0L;
As you can see in this code, all system commands except SC_MOVE are
processed by DefWindowProc. By not
sending the WM_SYSCOMMAND/SC_MOVE message to DefWindowProc, you will prevent
the default processing of this message, which is for Windows to move the
child window. This technique can be used to disable any WM_SYSCOMMAND
subcommands.
Reader Feedback
Regarding the fourth question in the September Windows Q&A, MSJ (Vol. 5, No.
5), Dean White of Dallas, Texas, notes that the following
rem >%temp%\dosapp.sem
accomplishes the same end and is better than
echo %0 >%temp%\dosapp.sem
He's right. Not only is no disk sector space wasted with his method, it is
faster. A file entry with a length of zero is all that is created as a
semaphore in the temporary files subdirectory.
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.
child window. This technique can be used to disable any WM_SYSCOMMAND
subcommands.