PCjs Machines

Home of the original IBM PC emulator for browsers.

Logo

Programming MS Windows

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

Programming Windows





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

                              Programming Windows

         The Microsoft(R) guide to writing applications for Windows 3

                              by Charles Petzold

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







Published by
Microsoft Press
A Division of Microsoft Corporation
One Microsoft Way
Redmond, Washington 98052-5399

Copyright(c) 1990 by Charles Petzold

All rights reserved. No part of the contents of this book may be reproduced
or transmitted in any form or by any means without the written permission
of the publisher.

Library of Congress Cataloging-in-Publication Data

Petzold, Charles, 1953-
    Programming Windows : the Microsoft guide to writing applications
  for Windows 3 / Charles Petzold. -- 2nd ed.

       p.     cm.
    ISBN 1-55615-264-7
    1. IBM Personal computer--Programming.  2. Microsoft Windows
  (Computer programs)  I. Title
  QA76.8.I2594P474  1990
  005.4'3--dc20                                          90-35467
                                                            CIP

Printed and bound in the United States of America.

2 3 4 5 6 7 8 9  MLML  4 3 2 1 0

Distributed to the book trade in Canada by General Publishing Company, Ltd.

Distributed to the book trade outside the United States and Canada by
Penguin Books Ltd.

Penguin Books Ltd., Harmondsworth, Middlesex, England
Penguin Books Australia Ltd., Ringwood, Victoria, Australia
Penguin Books N.Z. Ltd., 182-190 Wairau Road, Auckland 10, New Zealand

British Cataloging-in-Publication Data available


Apple(R) and LaserWriter(R) are registered trademarks of Apple Computer,
Inc. IBM(R) is a registered trademark of International Business Machines
Corporation. Microsoft(R), MS-DOS(R), and MultiPlan(R) are registered
trademarks and Windows(TM) is a trademark of Microsoft Corporation.


Project Editor: Jack Litewka
Technical Editor: Wm. Jeff Carey
Acquisitions Editor: Dean Holmes



Table of Contents
────────────────────────────────────────────────────────────────────────────



     Preface


PART I  GETTING STARTED
────────────────────────────────────────────────────────────────────────────


Chapter 1  Hello, Windows

     A BRIEF HISTORY OF WINDOWS
     THE USER'S PERSPECTIVE
            The Graphical User Interface (GUI)
            GUI Concepts and Rationale
            The Consistent User Interface
            The Multitasking Advantage
            Memory Management
            The Device-Independent Graphics Interface
            MS-DOS Applications
     THE PROGRAMMER'S PERSPECTIVE
            Windows and MS-DOS
            The Windows Commitment
            The Function Calls
            Dynamic Linking
            Object-Oriented Programming
            Message-Driven Architecture
            The Window Procedure
     YOUR FIRST WINDOWS PROGRAM
            What's Wrong with this Program?
            The HELLOWIN Files
            The Make File
            The C Source Code File
            The Windows Function Calls
            Uppercase Identifiers
            New Data Types
            Getting a Handle on Handles
            Hungarian Notation
            The Program Entry Point
            Registering the Window Class
            Creating the Window
            Displaying the Window
            The Message Loop
            The Window Procedure
            Processing the Messages
            The WM_PAINT Message
            The WM_DESTROY Message
            The Module Definition File
     THE WINDOWS PROGRAMMING HURDLES
            Don't Call Me, I'll Call You
            Queued and Nonqueued Messages
            Nonpreemptive Multitasking
            The Learning Curve

Chapter 2  Painting with Text

     PAINTING AND REPAINTING
            The WM_PAINT Message
            Valid and Invalid Rectangles
     AN INTRODUCTION TO GDI
            The Device Context
            Getting a Device Context Handle: Method One
            The Paint Information Structure
            Getting a Device Context Handle: Method Two
            TextOut: The Details
            The System Font
            The Size of a Character
            Text Metrics: The Details
            Formatting Text
            Putting It All Together
            The SYSMETS1.C Window Procedure
            Not Enough Room!
            The Size of the Client Area
     SCROLL BARS
            Scroll Bar Range and Position
            Scroll Bar Messages
            Scrolling SYSMETS
            Structuring Your Program for Painting
            Building a Better Scroll
            But I Don't Like to Use the Mouse


PART II  READING INPUT
────────────────────────────────────────────────────────────────────────────


Chapter 3  The Keyboard

     KEYBOARD BASICS
            The Keyboard Driver
            Ignoring the Keyboard
            Focus, Focus, Who's Got the Focus?
            Keystrokes and Characters
     KEYSTROKE MESSAGES
            System and Nonsystem Keystrokes
            The lParam Variable
            Virtual Key Codes
            Shift States
            Using Keystroke Messages
     ENHANCING SYSMETS:
            Adding WM_KEYDOWN Logic
            Sending Messages
     CHARACTER MESSAGES
            WM_CHAR Messages
            Dead-Character Messages
     LOOKING AT KEYBOARD MESSAGES
     THE CARET (NOT THE CURSOR)
            The Caret Functions
            The TYPE Program
     THE WINDOWS CHARACTER SETS
            The OEM Character Set
            The ANSI Character Set
            OEM, ANSI, and Fonts
     INTERNATIONALIZATION CONCERNS
            Working with the Character Set
            Talking with MS-DOS
            Using the Numeric Keypad

Chapter 4  The Mouse

     MOUSE BASICS
            Some Quick Definitions
     CLIENT-AREA MOUSE MESSAGES
            Simple Mouse Processing: An Example
            POINT, RECT, and lParam
            Processing Shift Keys
            Mouse Double-Clicks
     NONCLIENT-AREA MOUSE MESSAGES
            The Hit-Test Message
            Messages Beget Messages
     HIT-TESTING IN YOUR PROGRAMS
            A Hypothetical Example
            A Sample Program
            Emulating the Mouse with the Keyboard
            Adding a Keyboard Interface to CHECKER
            Using Child Windows for Hit-Testing
            Child Windows in CHECKER
     CAPTURING THE MOUSE
            The BLOWUP1 Program
            Changing the Mouse Cursor Shape
            The StretchBlt Call
            Drawing the Capture Block

Chapter 5  The Timer

     TIMER BASICS
            SYSTEM.DRV and the Windows Timer
            Timer Messages Are Not Asynchronous
     USING THE TIMER: THREE METHODS
            Method One
            Method Two
            Method Three
     USING THE TIMER FOR A STATUS REPORT
            Creative Use of Icons
            Forcing the Icon
            Keeping the Icon an Icon
            Calculating Free Memory
            Using Floating-Point Math
     USING THE TIMER FOR A CLOCK
            Positioning and Sizing the Popup
            Getting the Date and Time
            Going International
     WINDOWS STANDARD TIME

Chapter 6  Child Window Controls

     THE BUTTON CLASS
            Creating the Child Windows
            The Child Talks to Its Parent
            The Parent Talks to Its Child
            Push Buttons
            Check Boxes
            Radio Buttons
            Group Boxes
            User-Defined Buttons
            Changing the Button Text
            Visible and Enabled Buttons
            Buttons and Input Focus
     CONTROLS AND COLORS
            System Colors
            The Button Colors
            The WM_CTLCOLOR Messages
     THE STATIC CLASS
     THE SCROLLBAR CLASS
            The COLORS1 Program
            The Automatic Keyboard Interface
            Window Subclassing
            Coloring the Background
            Coloring the Scroll Bars
            Dealing with Multiple Instances
            COLORS1 as an Icon
     THE EDIT CLASS
            The Edit Class Styles
            Edit Control Notification
            Using the Edit Controls
            Messages to an Edit Control
     THE LISTBOX CLASS
            List Box Styles
            Putting Strings in the List Box
            Selecting and Extracting Entries
            Receiving Messages from List Boxes
            A Simple List Box Application
            Listing Files
            A head for Windows
            2 KB of Wasted Space


PART III  USING RESOURCES
────────────────────────────────────────────────────────────────────────────


Chapter 7  Memory Management

     SEGMENTED MEMORY, INTEL STYLE
     MEMORY ORGANIZATION IN WINDOWS
            Fixed and Moveable Segments
            Discardable Memory
            The Global Memory Layout
            Local Memory
     CODE AND DATA SEGMENTS
            Memory Models: Small, Medium, Compact, Large, and Huge
            Multiple Code Segments
            What About the Compact and Large Models?
            Avoiding Movement Problems
            Program Segment Attributes
     HOW WINDOWS MOVES AND
            Special Treatment of Far Functions
            When Windows Runs the Program
            What MakeProcInstance Does
            The Difference for Dynamic Libraries
            Walking the Stack
            Expanded Memory
            Protected Mode
     ALLOCATING MEMORY WITHIN A PROGRAM
            Lock Your Blocks
            A Quick Example
            Global Memory Functions
            More Global Memory Functions
            Using Discardable Global Memory
            Huge Global Memory Blocks
            Allocating Local Memory
            Other Local Memory Functions
            Locking Your Own Data Segment
            Memory Allocation Shortcuts
            Using C Memory Allocation Functions
            If You Know You're Running in Protected Mode

Chapter 8  Icons, Cursors, Bitmaps, and Strings

     COMPILING RESOURCES
     ICONS AND CURSORS
            The SDKPAINT Tool
            Getting a Handle on Icons
            Using Icons in Your Program
            Using Alternate Cursors
     RESOURCES AND MEMORY
            Bitmaps: Pictures in Pixels
            Using Bitmaps and Brushes
     CHARACTER STRINGS
            Using Character String Resources
            Using Strings with MessageBox
            Character Strings and Memory Space
     USER-DEFINED RESOURCES

Chapter 9  Menus and Accelerators

     MENUS
            Menu Structure
            The Menu Template
            Referencing the Menu in Your Program
            Menus and Messages
            A Sample Program
            Menu Etiquette
            Defining a Menu the Hard Way
            A Third Approach to Defining Menus
            Floating Popup Menus
            Using the System Menu
            Changing the Menu
            Other Menu Commands
            An Unorthodox Approach to Menus
     USING BITMAPS IN MENUS
            Two Methods of Creating Bitmaps for Menus
            The Memory Device Context
            Creating a Bitmap with Text
            Scaling Bitmaps
            Putting the Menu Together
            Adding a Keyboard Interface
     KEYBOARD ACCELERATORS
            Why You Should Use Keyboard Accelerators
            Some Rules on Assigning Accelerators
            The Accelerator Table
            Loading the Accelerator Table
            Translating the Keystrokes
            Receiving the Accelerator Messages
            POPPAD with a Menu and Accelerators
            Enabling Menu Items
            Processing the Menu Options

Chapter 10  Dialog Boxes

     MODAL DIALOG BOXES
            Creating an "About" Dialog Box
            The Dialog Box Template
            The Dialog Box Procedure
            Exporting the Dialog Box Procedure
            Invoking the Dialog Box
            More on the Dialog Box Style
            More on Defining Controls
            A More Complex Dialog Box
            Working with Dialog Box Controls
            The OK and Cancel Buttons
            Tab Stops and Groups
            Painting on the Dialog Box
            Using Other Functions with Dialog Boxes
            Defining Your Own Controls
     THE MESSAGE BOX
            The Assertion Message Box
            Popup Information
     WORKING WITH FILES: POPPAD REVISITED
            The OpenFile Function Call
            Two Methods of File I/O
            Dialog Boxes for Open and Save
            The DlgDirList and DlgDirSelect Functions
            Getting Valid Filenames
            The New Version of POPPAD
     MODELESS DIALOG BOXES
            Differences Between Modal and Modeless Dialog Boxes
            The New COLORS Program
            HEXCALC: Window or Dialog Box?
            Creatively Using Control IDs
     USING THE DIALOG UTILITY


PART IV  THE GRAPHICS DEVICE INTERFACE
────────────────────────────────────────────────────────────────────────────


Chapter 11  An Introduction to GDI

     THE GDI PHILOSOPHY
     THE DEVICE CONTEXT
            Getting the Handle to the Device Context
            Getting Device Context Information
            The DEVCAPS1 Program
            The Size of the Device
            Finding Out About Color
            The Device Context Attributes
            Saving Device Contexts
     THE MAPPING MODE
            Device Coordinates and Logical Coordinates
            The Device Coordinate Systems
            The Viewport and the Window
            Working with MM_TEXT
            The "Metric" Mapping Modes
            The "Roll Your Own" Mapping Modes
            The WHATSIZE Program

Chapter 12  Drawing Graphics

     DRAWING POINTS
     DRAWING LINES
            Using Stock Pens
            Creating, Selecting, and Deleting Pens
            Avoiding Device Dependencies
            Filling In the Gaps
            Drawing Modes
            The ROP2LOOK Program
            ROP2 and Color
     DRAWING FILLED AREAS
            The Bounding Box
            The ARCS Program
            The Trigonometry of Pie Charts
            The Polygon Function and the Polygon Filling Mode
            Brushing the Interior
            Brushes and Bitmaps
            Creating and Using Bitmap Brushes
            Brush Alignment
     RECTANGLES, REGIONS, AND CLIPPING
            Working with Rectangles
            Creating and Painting Regions
            Clipping with Rectangles and Regions
            The CLOVER Program
     SOME MISCELLANEOUS GDI FUNCTIONS
     PROGRAMS THAT DRAW FOREVER

Chapter 13  Bits, Blts, and Metafiles

     THE OLD BITMAP FORMAT
            Creating Bitmaps in a Program
            The Monochrome Bitmap Format
            The Color Bitmap Format
            The Dimensions of a Bitmap
     THE DEVICE-INDEPENDENT BITMAP (DIB)
            The DIB File
            Creating a DIB
     THE MEMORY DEVICE CONTEXT
     THE MIGHTY BLT
            The PatBlt Function
            Blt Coordinates
            Transferring Bits with BitBlt
            The DrawBitmap Function
            Using Different ROP Codes
            More Fun with Memory Device Contexts
            Color Conversions
            Mapping Mode Conversions
            Stretching Bitmaps with StretchBlt
            Animation
     METAFILES
            Simple Use of Memory Metafiles
            Storing Metafiles on Disk
            Using Preexisting Metafiles
            Using Metafiles as Resources
            Looking at Metafiles
            Metafile Dos and Don'ts

Chapter 14  Text and Fonts

     SIMPLE TEXT OUTPUT
            The Text Drawing Functions
            Device Context Attributes for Text
            Using Stock Fonts
            Graying Character Strings
            The Easy Use of GrayString
            Gray Strings Without GrayString
     BACKGROUND ON FONTS
            The Types of Fonts
            Type Talk I: Families and Faces
            The Font Resource Files
            Type Talk II: Getting the Point
            Why Logical Inches?
            Type Talk III: Leading and Spacing
            The "Logical Twips" Mapping Mode
     CREATING, SELECTING,AND DELETING LOGICAL FONTS
            The PICKFONT Program
            The Logical Font Structure
            The Font-Mapping Algorithm
            Finding Out About the Font
     ENUMERATING THE FONTS
     FORMATTING TEXT
            One-Line Text Alignment
            Working with Paragraphs

Chapter 15  Using the Printer

     PRINTING, SPOOLING, AND ESCAPE
     THE PRINTER DEVICE CONTEXT
            Getting the CreateDC Parameters
            The Revised DEVCAPS Program
            The DeviceMode Call
            Checking for BitBlt Capability
     PRINTING FUNDAMENTALS
            The Escape Function
            The FORMFEED Program
     PRINTING GRAPHICS AND TEXT
            Bare-Bones Printing
            Setting an Abort Procedure
            How Windows Uses AbortProc
            Implementing an Abort Procedure
            Adding a Printing Dialog Box
            Adding Printing to POPPAD
            Handling Error Codes
     THE TECHNIQUE OF BANDING
            Strike Up the Bands
            A Different Use of the Abort Procedure
     THE PRINTER AND FONTS


PART V  DATA EXCHANGE AND LINKS
────────────────────────────────────────────────────────────────────────────


Chapter 16  The Clipboard

     SIMPLE USE OF THE CLIPBOARD
            The Standard Clipboard Data Formats
            Transferring Text to the Clipboard
            Getting Text from the Clipboard
            What the Clipboard Does
            Opening and Closing the Clipboard
            Using the Clipboard with Bitmaps
            The Revised BLOWUP Program
            The Metafile and the Metafile Picture
     BEYOND SIMPLE CLIPBOARD USE
            Using Multiple Data Items
            Delayed Rendering
            Private Data Formats
     BECOMING A CLIPBOARD VIEWER
            The Clipboard Viewer Chain
            Clipboard Viewer Functions and Messages
            A Simple Clipboard Viewer

Chapter 17  Dynamic Data Exchange (DDE)

     BASIC CONCEPTS
            Application, Topic, and Item
            The Types of Conversations
            Character Strings and Atoms
     A DDE SERVER PROGRAM
            The DDEPOP Program
            The WM_DDE_INITIATE Message
            The ServerProc Window Procedure
            The WM_DDE_REQUEST Message
            DDEPOP's PostDataMessage Function
            The WM_DDE_ADVISE Message
            Updating the Items
            The WM_DDE_UNADVISE Message
            The WM_DDE_TERMINATE Message
     A DDE CLIENT PROGRAM
            Initiating the DDE Conversation
            The WM_DDE_DATA Message
            The WM_DDE_TERMINATE Message
     WHEN THINGS GO WRONG

Chapter 18  The Multiple Document Interface (MDI)

     THE ELEMENTS OF MDI
     WINDOWS 3 AND MDI
     THE SAMPLE PROGRAM
            Three Menus
            Program Initialization
     CREATING THE CHILDREN
     MORE FRAME WINDOW MESSAGE PROCESSING
     THE CHILD DOCUMENT WINDOWS
     THE POWER OF WINDOW PROCEDURES

Chapter 19  Dynamic Link Libraries

     LIBRARY BASICS
            Library: One Word, Many Meanings
            Examining Libraries with EXEHDR
     STRPROG AND STRLIB
            The STRLIB Library
            Make File Differences
            The Library Entry Point
            The STRLIB Functions
            The Library Module Definition File
            The STRPROG Program
            Running STRPROG
            Far Function Prologs
            The Use of Call-Back Functions
     THE DS != SS ISSUE
     OTHER LIBRARY RESTRICTIONS
     DIFFERENT METHODS FOR SPECIFYING LINKS
     USING IMPORT LIBRARIES
     INTERCEPTING WINDOWS FUNCTION CALLS
     DYNAMIC LINKING WITHOUT IMPORTS
     RESOURCE-ONLY LIBRARIES

Preface

When I began writing the first edition of PROGRAMMING WINDOWS in the early
spring of 1987, Microsoft Windows 1.0 had been released for about a year and
a half, a beta-test version of Windows 2.0 was just becoming available, and
Windows 3.0 could only be regarded as a far-fetched dream. At that time, the
eventual success of Windows in the personal computer marketplace was more a
matter of faith than a certain-to-come reality.

With the release of Windows 3, more people than ever are interested in this
operating environment. Windows 3 runs Windows programs in Intel
80286\-compatible protected mode, giving Windows and Windows programs access
to 16 megabytes of memory. This exciting enhancement to Windows--coupled
with the many Windows applications released over the past few years--has
made Windows 3 an important piece of systems software released for
IBM-compatible personal computers. Windows is the graphical interface that
many computer users will first encounter.

Since the publication of the first edition of PROGRAMMING WINDOWS in early
1988, many programmers have told me that the book has been useful in helping
them learn how to write applications for Windows. Nothing could make me
happier.

It was my intention with the first edition of PROGRAMMING WINDOWS to show
the basics of writing programs for Windows using the C programming language.
A book like this cannot delve into the complexities of a full-fledged
application program, of course, but it can show how to handle all the
various components of a Windows program. It is up to the application
programmer to merge these components into a coherent whole.

In this second edition of PROGRAMMING WINDOWS, I have updated the text where
necessary, updated the code listings for a more modern style of C
programming, tightened the early chapters (where I felt I had been more
theoretical than practical), and added two new chapters--None on Dynamic
Data Exchange (DDE) and one on the Multiple Document Interface (MDI).
Interestingly enough, the Windows application program interface has been
fairly stable over the years and very few changes had to be made to the
sample programs.

Windows has a reputation for being easy for users but tough for programmers.
Often, aspiring Windows programmers face a steep learning curve, and they
want to see lots of programming examples. To satisfy that desire, this book
contains more than 50 complete programs. Many of them are short and stripped
down to clearly illustrate various Windows programming techniques. Others
are a bit longer to show how everything fits together. Several of the
programs are useful utilities. Others are tools for exploring Windows.

What I don't do in this book is teach you how to use Windows. If you have no
experience using the environment, now is the time to install it and play
with it for awhile. Windows is very easy to learn.

PROGRAMMING WINDOWS

Nor will I teach you how to program in C. Before you even think about
programming for Windows, you should have a good working knowledge of
programming in C for a more conventional environment such as MS-DOS. If your
C is a little rusty, you may want to spend some time becoming better
acquainted with the topics of structures and pointers.

A good familiarity with the segmented architecture of the Intel 8086 family
of microprocessors will also help. If you know how 80286 addressing works
(in both real mode and protected mode) and the difference between near and
far pointers and functions, you're in good shape. If you don't, I've
included some explanations along the way.

To compile the programs in this book and to write your own programs for
Windows, you need the following software packages:

  ■   Microsoft Windows 3

  ■   Microsoft Windows Software Development Kit 3

  ■   Microsoft C Professional Development System (aka Microsoft C 6)

If you haven't yet installed Microsoft C 6, you should know that the
programs in the book require only the small-model MS-DOS libraries using the
math emulator. You may be able to use a C compiler other than Microsoft's if
the compiler is suitable for compiling Windows programs. Most other C
compilers can't be used for this purpose.

To run Windows and the Windows Software Development Kit, you need the
following hardware:

  ■   An IBM personal computer (or compatible) based on the Intel 80286 or
      80386 microprocessor with a hard disk and 640 KB of memory running
      MS-DOS 3.1 or later. An 80386-based machine with a couple megabytes of
      extended memory is ideal.

  ■   A graphics display and video board, preferably compatible with the IBM
      VGA (Video Graphics Array) or better.

  ■   A mouse. Although a mouse is generally optional for most Windows
      programs, some of the programs in this book require one.

Sometimes readers of computer books are curious about the author's own
system. When I was writing the first edition of PROGRAMMING WINDOWS, I used
an IBM PC/AT Model 339 (8 MHz) with two 30-megabyte (MB) hard disks, 512 KB
of memory on the system board, and a 1.5 MB Intel Above Board PS/AT. The
system included a Microsoft mouse, an IBM 256-KB Enhanced Graphics Adapter
(EGA), and a NEC MultiSync monitor. I wrote the book using WordStar 3.3 and
printed everything on an IBM 5152 Graphics Printer.

For the second edition, I used a 20-MHz IBM PS/2 Model 70 with a 120-MB hard
disk, 6 MB of memory, an IBM 8514/A graphics board (although I generally ran
Windows in VGA mode), a NEC MultiSync 4D monitor, and a Microsoft mouse. I
used Microsoft Word for revising the book chapters, printing on a NEC
SilentWriter LC-890 PostScript printer. (Word for Windows was not available
until I was well into the revision.)

PA book such as this could not have come about without help and
encouragement from some very special people. I offer my heartfelt thanks
with a handshake or hug (as appropriate) to the following people:

  ■   To everyone at Microsoft involved in Windows 3, for creating a system
      with fascinating depth and seemingly endless things to learn.

  ■   To all the Windows 3 developers who reviewed my chapters and offered
      comments and suggestions: Clark Cyr, David D'Souza, and particularly
      David Weise.

  ■   To the MS Online System Support people in the Windows SDK group who
      reviewed galleys of the entire book: Much gratitude to Todd Cole, who
      volunteered his group and coordinated the effort; special thanks to
      John Hagerson, Mike Thurlkill, Dennis Crain, David Long, Ed Mills,
      Steve Molstad, Richard Herrmann, Dan Boone, and Kyle J. Sparks; thanks
      also to Jeff Stone, Dan Quigley, Steve Thompson, Larry Israel, Teresa
      Posakony, Neil Sandlin, Curt Palmer, David Flenniken, Charles E.
      Kindel Jr., and Doug Laundry.

  ■   To everyone at Microsoft Press who has been involved in the first and
      second editions of PROGRAMMING WINDOWS, for behind-the-scenes work
      that makes all the difference in the world.

  ■   To my friends and editors at PC Magazine and Microsoft Systems Journal
      for their help and encouragement over the years.

  ■   To the readers of the first edition of PROGRAMMING WINDOWS who wanted
      to see a second edition. It's here and it's yours!

  ■   To my family, who thought I was crazy when I quit my job to write full
      time: to my Mom, my brother Steve and his wife Bernie and Christopher
      and Michelle, my sister Sue and her husband Rich and Erika and another
      one on the way. You're right. I was crazy.

  ■   To my friend Karen. Words cannot express....

  ■   To my friends at the "DH" (and especially Devon and Leslie) for
      enjoyable company and interesting conversation that has nothing
      whatsoever to do with computers. Completing this book gets me closer
      to writing that novel!

  ■   And most of all, as always, to Jan, who was as happy as I was when I
      called her and said, "I finally finished the chapter on DDE."

Charles Petzold      July 29, 1990




PART I  GETTING STARTED
────────────────────────────────────────────────────────────────────────────

Chapter 1  Hello, Windows
────────────────────────────────────────────────────────────────────────────

Since its introduction in November 1985, Microsoft Windows has emerged as
the most popular graphical user interface environment for MS-DOS. Several
million copies of Windows have been shipped, and hundreds of Windows
applications are currently available.

For the user, Windows provides a multitasking graphical-based windowing
environment that runs programs especially designed for Windows. Such
programs include Microsoft Excel (spreadsheet and business graphics),
Microsoft Word for Windows (word processing), Aldus's PageMaker (desktop
publishing), Samna's Ami (word processing), Micrografx's Designer (drawing),
IBM's Current (a personal information manager), Asymetrix's ToolBook (a
software construction kit), and many others. Programs written for Windows
have a consistent appearance and command structure, and are thus often
easier to learn and use than conventional MS-DOS programs. Users can easily
switch among different Windows programs and exchange data between them.
Windows also provides an easy-to-use icon-based Program Manager for running
programs as well as a File Manager and Print Manager for file maintenance
and printer-queue management.

Although Windows exists primarily to run applications especially written for
the environment, Windows can also run many programs written for MS-DOS. Of
course, these programs cannot take advantage of many Windows features, but
in some cases they can be windowed and multitasked alongside Windows
programs.

For the program developer, Windows provides a wealth of built-in routines
that allow the use of menus, dialog boxes, scroll bars, and other components
of a friendly user interface. Windows also contains an extensive graphics
programming language that includes the use of formatted text in a variety of
fonts. Programmers can treat the keyboard, mouse, video display, printer,
system timer, and RS-232 communication ports in a device-independent manner.
Windows programs run the same on a variety of hardware configurations.

The "look and feel" of Windows also shows up in the OS/2 Presentation
Manager. OS/2 is the protected mode operating system developed by
International Business Machines Corporation (IBM) and Microsoft Corporation
as a successor to MS-DOS; the graphical user interface under OS/2 is called
the Presentation Manager. While the application program interfaces of
Windows and the OS/2 Presentation Manager are not the same, they have many
similarities and common structural elements.

A BRIEF HISTORY OF WINDOWS

Windows was announced by Microsoft Corporation in November 1983 and released
two years later in November 1985. Over the next two years, Windows 1.01 (the
first released version) was followed by several updates to support the
international market and to provide drivers for additional video displays
and printers.

Windows 2.0 was released in November 1987. This version incorporated several
changes to the user interface to make it consistent with the forthcoming
OS/2 Presentation Manager (released in October 1988). The most significant
of these changes involved the use of overlapping windows rather than the
"tiled" windows found in the earlier versions of Windows. Windows 2.0 also
included enhancements to the keyboard and mouse interface, particularly for
menus and dialog boxes.

Windows/386 (released shortly after Windows 2.0) used the Virtual-86 mode of
the 386 microprocessor to window and multitask many DOS programs that
directly access hardware. For symmetry, Windows 2.1 was renamed Windows/286.

Windows 3--the subject of this book--was introduced in a spectacular product
announcement on May 22, 1990. The earlier Windows/286 and Windows/386
versions have been merged into one product with this release. The big change
in Windows 3 is the support of the protected mode operation of Intel's 80286
and 80386 microprocessors. This gives Windows and Windows applications
access to up to 16 megabytes of memory. The Windows "shell" programs (the
Program Manager, Task Manager, and File Manager) have been completely
revamped.


THE USER'S PERSPECTIVE

Windows provides considerable advantages to both users and programmers over
the conventional MS-DOS environment. The benefits to users and the benefits
to program developers are really quite similar, because the job of a program
developer is to give users what they need and want. Windows makes this
possible.

The Graphical User Interface (GUI)

Windows is a graphical user interface (GUI), sometimes also called a "visual
interface" or "graphical windowing environment." The concepts behind this
type of user interface date from the mid-1970s, with the pioneering work
done at the Xerox Palo Alto Research Center (PARC) for machines such as the
Alto and the Star and for environments such as Smalltalk.

The work done at Xerox PARC was brought into the mainstream and popularized
by Apple Computer, Inc., first in the ill-fated Lisa and then a year later
in the much more successful Macintosh, introduced in January 1984. The Apple
Macintosh remains a significant challenger to IBM's dominance in the
personal-computer business market. It is not so much the hardware of the
Macintosh but its operating system that makes the machine so appealing to
users. The Mac is simply easier to use and learn than an IBM PC running
MS-DOS.

Since the introduction of the Macintosh, graphical user interfaces have
bloomed like wildflowers throughout the personal-computer industry and the
not-so-personal computer industry as well. For IBM-compatibles running
MS-DOS, there is Windows. For IBM-compatibles running OS/2, there is the
Presentation Manager. For the Commodore Amiga, there is Intuition. For the
Atari, there is GEM. For machines running UNIX, there is the X-Window
system. For Sun Microsystems workstations, there is NeWS. For the NeXT,
there is NextStep.

It is obvious that the graphical user interface is now (in the words of
Microsoft's Charles Simonyi) the single most important "grand consensus" of
the personal-computer industry. Although the various graphical environments
differ in details, they have similar characteristics.


GUI Concepts and Rationale

All graphical user interfaces make use of graphics on a bitmapped video
display. Graphics provides better utilization of screen real estate, a
visually rich environment for conveying information, and the possibility of
a WYSIWYG (what you see is what you get) video display of graphics and
formatted text prepared for a printed document.

In earlier days, the video display was used solely to echo text that the
user typed using the keyboard. In a graphical user interface, the video
display itself becomes a source of user input. The video display shows
various graphical objects in the form of icons and input devices such as
buttons and scroll bars. Using the keyboard (or, more directly, a pointing
device such as a mouse), the user can directly manipulate these objects on
the screen. Graphics objects can be dragged, buttons can be pushed, and
scroll bars can be scrolled.

The interaction between the user and a program thus becomes more intimate.
Rather than the one-way cycle of information from the keyboard to the
program to the video display, the user directly interacts with the objects
on the display.


The Consistent User Interface

Users no longer expect to spend long periods of time learning how to use the
computer or mastering a new program. Windows helps because all Windows
programs have the same fundamental look and feel. The program occupies a
window--a rectangular area on the screen. It is identified by a caption bar.
Most program functions are initiated through the program's menu. Figure 1-1
shows a typical Windows program (in this case Write, the word processor
included in Windows) with the various window components labeled.

Some menu items invoke dialog boxes, in which the user enters additional
information. One dialog box found in almost every large Windows program
opens a file. (See Figure 1-2.) This dialog box looks the same (or very
similar) in many different Windows programs, and it is almost always invoked
from the same menu option.

Once you know how to use one Windows program, you're in a good position to
easily learn another. The menus and dialog boxes allow a user to experiment
with a new program and explore its features. Most Windows programs have both
a keyboard interface and a mouse interface. Although most functions of
Windows programs can be controlled through the keyboard, using the mouse is
often easier for many chores.

  (Figure 1-1. may be found in the printed book.)

  (Figure 1-2. may be found in the printed book.)

From the programmer's perspective, the consistent user interface results
from using the routines built into Windows for constructing menus and dialog
boxes. All menus have the same keyboard and mouse interface because Windows,
rather than the application program, handles this job.


The Multitasking Advantage

Although some people continue to question whether multitasking is really
necessary on a single-user computer, users definitely are ready for
multitasking and can benefit from it. The popularity of MS-DOS RAM-resident
programs such as Sidekick proves it. Although popups are not, strictly
speaking, multitasking programs, they do allow fast context switching. This
involves many of the same concepts as multitasking.

Under Windows, every program in effect becomes a RAM-resident popup. Several
Windows programs can be displayed and running at the same time. Each program
occupies a rectangular window on the screen, as shown in Figure 1-3 on the
following page. The user can move the windows around on the screen, change
their size, switch between different programs, and transfer data from one
program to another. Because this display looks something like a desktop (in
the days before the desk became dominated by the computer itself, of
course), Windows is sometimes said to use a "desktop metaphor" for the
display of multiple programs.

  (Figure 1-3. may be found in the printed book.)


Memory Management

An operating system cannot implement multitasking without doing something
about memory management. As new programs are started up and old ones
terminate, memory can become fragmented. The system must be able to
consolidate free memory space. This requires the system to move blocks of
code and data in memory.

Even Windows 1, running on an 8088 microprocessor, was able to perform this
type of memory management. Under real mode, this can only be regarded as an
astonishing feat of software engineering. Programs running under Windows can
overcommit memory; a program can contain more code than can fit into memory
at any one time. Windows can discard code from memory and later reload the
code from the program's .EXE file. A user can run several copies (called
"instances") of a program; all these instances share the same code in
memory. Programs running in Windows can share routines located in other .EXE
files called "dynamic link libraries." Windows includes a mechanism to link
the program with the routines in the dynamic link libraries at run time.
Windows itself is a set of dynamic link libraries.

Thus, even in Windows 1, the 640-KB memory limit of the PC's architecture
was effectively stretched without requiring any additional memory. But
Microsoft didn't stop there: Windows 2 gave the Windows applications access
to expanded memory (EMS), and Windows 3 runs in protected mode to give
Windows applications access to up to 16 megabytes (MB) of extended memory.


The Device-Independent Graphics Interface

Windows is a graphical interface, and Windows programs can make full use of
graphics and formatted text on both the video display and the printer. A
graphical interface is not only more attractive in appearance, but it can
also impart a high level of information to the user, as you can see in
Figure 1-4.

Programs written for Windows do not directly access the hardware of graphics
display devices such as the screen and printer. Instead, Windows includes a
graphics programming language (called the Graphics Device Interface, or GDI)
that allows the easy display of graphics and formatted text. Windows
virtualizes display hardware. A program written for Windows will run with
any video board or any printer for which a Windows device driver is
available. The program does not need to determine what type of device is
attached to the system.

Putting a device-independent graphics interface on the IBM PC was not an
easy job for the developers of Windows. The PC design was based on the
principle of open architecture. Third-party hardware manufacturers were
encouraged to develop peripherals for the PC and have done so in great
number. Although several standards have emerged, conventional MS-DOS
programs for the PC must individually support many different hardware
configurations. For example, it is fairly common for an MS-DOS
word-processing program to be sold with one or two disks of small files,
each one supporting a particular printer.

  (Figure 1-4. may be found in the printed book.)

Windows programs do not require these drivers because the support is part of
Windows. This benefits users because most Windows programs require very
little in the way of installation. Everything a program needs can be
included in the program's single .EXE file. The user can often copy the .EXE
file to the fixed disk, load Windows, and go.


MS-DOS Applications

Although Windows primarily exists to run new programs specifically designed
for the environment, Windows can also run many non-Windows MS-DOS programs.
The Windows User's Guide refers to these as "standard applications," but
many Windows programmers call them "old applications" or "old apps."

These MS-DOS programs can be divided into two broad categories: Well-behaved
applications (or "good old apps") are those that use the MS-DOS and PC ROM
BIOS (basic input/output system) software interrupts to read the keyboard
and write to the video display. These programs can generally run in a
window.

"Bad apps" are those that write directly to the video display, use graphics,
or take control of the hardware keyboard interrupt. The term "bad" here
refers not to the quality of the program--many of the best programs written
for the PC are bad apps when it comes to Windows--but to the way in which
the program uses the hardware of the PC. When running on a 286-based
machine, there is simply no way Windows can allow such a program to be
windowed or multitasked. However, Windows can use the "virtual 86" mode of
the 386 microprocessor to window and multitask even bad applications.



THE PROGRAMMER'S PERSPECTIVE

Windows has the reputation of being easy for users but difficult for
programmers. If you have no prior experience with programming for a
graphical user interface, you should be warned right now that you will
encounter some very strange concepts. Almost every programmer who begins
writing code for Windows must go through some mental reorientation to
assimilate these concepts.

If at first you find Windows programming to be difficult, awkward, bizarrely
convoluted, and filled with alien concepts, rest assured that this is a
normal reaction. You are not alone.

Windows and MS-DOS

You start up Windows as if it were a normal application program running
under MS-DOS. But as Windows loads, it becomes almost a full-fledged
operating system. It's not quite an operating system because it runs on top
of MS-DOS. While Windows is running, it shares responsibility with MS-DOS
for managing the hardware resources of the computer. Basically, MS-DOS
continues to manage the file system, while Windows does everything  else.
Windows commands the video display, keyboard, mouse, printer, and serial
ports and is responsible for memory management, program execution, and
scheduling.

Windows is strong where MS-DOS is weak, and weak where MS-DOS is adequate.
Windows includes almost no support of file I/O, which is one of the most
essential chores of a minimal operating system such as MS-DOS. This leads to
some amusing--or not so amusing--consequences. It is easier in a Windows
program to create a disk-based metafile containing a complex series of
graphics drawing commands than to create a simple ASCII text file. The
former is a Windows job; the latter requires that the program use MS-DOS.


The Windows Commitment

Programming for Windows is an all-or-nothing proposition. For example, you
cannot write an MS-DOS application--even a well-behaved one--and use Windows
only for some graphics. If you want to use any part of Windows, you must
make the commitment to write a full-fledged Windows program.

The reason for this will become more obvious as you learn about the
structure of a Windows program. Everything in Windows is interconnected. If
you want to draw some graphics on the video display, you need something
called a "handle to a device context." To get that, you need a "handle to a
window." To get that, you must create a window and be prepared to receive
"messages" to the window. To receive and process messages, you need a
"window procedure." And at that point you're writing a Windows program. You
can't fly unless you leave the ground.


The Function Calls

Windows 3 supports over 550 function calls that applications can use. It is
highly unlikely that you will ever memorize the syntax to all these calls.
Most Windows programmers keep the Windows Programmer's Reference manual
within easy reach.

Each of the Windows functions has a descriptive name written in mixed
uppercase and lowercase letters, such as CreateWindow. This function (as you
might guess) creates a window for your program. Another example: the
function IsClipboardFormatAvailable determines whether the clipboard is
holding data of a particular format.

All the Windows functions are declared in a header file named WINDOWS.H,
included in the Windows Software Development Kit. WINDOWS.H is an important
part of the Windows documentation. You might want to print a copy or use a
file browser for quick reference.

You use these Windows functions in your Windows program the same way you use
C library functions such as strlen. However, there are some differences
between the Windows functions and the standard C library functions.

Windows functions are always declared as far pascal functions. These are two
keywords that Microsoft has added to its version of C. The far keyword
indicates that the  Windows function is in a different code segment than the
program's code. (You'll see the reason for this shortly.)

The pascal keyword indicates that the function's calling sequence is
different than the normal C calling sequence. Normally, the C compiler
generates code that pushes parameters on the stack from right to left
beginning with the last parameter. The code calling the function is
responsible for adjusting the stack pointer after the function returns. With
the pascal calling sequence, the parameters are pushed on the stack from
left to right and the called function cleans up the stack. The pascal
calling sequence is used in Windows because it is more efficient.

With one oddball exception, any pointer passed to a Windows function must be
a far pointer. This is something you normally don't have to worry about
because the compiler will extend short pointers to long pointers based on
the function template in WINDOWS.H.


Dynamic Linking

If you've been working with MS-DOS programming for awhile, you might guess
that a Windows program interfaces with Windows through a software interrupt
such as the  MS-DOS Interrupt 0x21. You might guess that the linker adds
bindings to your Windows programs that convert the Windows function calls
into this software interrupt. But you would be wrong. A Windows program
interfaces to Windows through a process called "dynamic linking."

Like MS-DOS programs, Windows executables have the filename extension .EXE.
However, this is not the same .EXE format that is used in MS-DOS. Instead,
Windows programs use a .EXE format called the New Executable file format,
similar to that used in OS/2. Whenever a Windows program calls a Windows
function, the C compiler generates assembly-language code for a far call. A
table in the .EXE file identifies the function being called using a dynamic
link library name and either a name or a number (called the ordinal number)
of the function in that library.

Windows itself consists largely of three dynamic link libraries, called
KERNEL (responsible for memory management, loading and executing programs,
and scheduling), USER (the user interface and windowing), and GDI (the
graphics). These libraries contain the code and data for the Windows
functions. You can find these three dynamic link libraries in the SYSTEM
subdirectory of your Windows directory.

When a Windows program is loaded into memory, the far calls in the program
are resolved to point to the entry of the function in the dynamic link
library, which is also loaded into memory. This is why all Windows functions
must be defined as far: The code in the dynamic link libraries is not in the
same segment as the program's code. Also, pointers passed in Windows
functions must also be defined as far to avoid confusion with the dynamic
link library's own code and data segments.

Generally, you don't have to worry about the use of far calls and far
pointers because the functions are declared as far functions with far
pointers in WINDOWS.H: The C compiler will perform the necessary address
translations for you.

When you link a Windows program to produce an executable, you must link with
a special "import library" provided with the Windows Software Development
Kit. This import library contains the dynamic link library names and ordinal
numbers of all the Windows functions. LINK uses this information to
construct the table in the .EXE file that Windows uses to resolve calls to
Windows functions when loading the program.


Object-Oriented Programming

When programming for Windows, you're really engaged in a type of
object-oriented programming. This is most evident in the object you'll be
working with most in Windows_ the object that gives Windows its name, the
object that will soon seem to take on anthropomorphic characteristics, the
object that may even show up in your dreams, the object known as the
"window."

Windows are rectangular objects on the screen. A window receives user input
from the keyboard or mouse and displays graphical output on its surface.

An application window usually contains the program's title bar, menu, sizing
border, and perhaps some scroll bars. Dialog boxes are additional windows.
Moreover, the surface of a dialog box always contains several additional
windows called "child windows." These child windows take the form of push
buttons, radio buttons, check boxes, text entry fields, list boxes, and
scroll bars.

The user sees these windows as objects on the screen and interacts directly
with these objects by pushing a button or scrolling a scroll bar.
Interestingly enough, the programmer's perspective is analogous to the
user's perspective. The window receives this user input in the form of
"messages" to the window. A window also uses messages to communicate with
other windows.

Understanding these messages is one of the hurdles you'll have to jump in
becoming a Windows programmer.


Message-Driven Architecture

The first time I saw a graphical user interface in action, I was puzzled.
The demonstration included a rudimentary word processor running in a window.
The word processor would reformat its text when the program's window was
resized.

It was obvious to me that the operating system was handling the details of
the window-resizing logic, and that the program was capable of responding to
this system function. How did the program know when its window was resized?
What was the mechanism the operating system used to convey this information
to the window? My previous programming experience was useless in
understanding how this worked.

It turns out that the answer to this question is central to understanding
the architecture used in graphical user interfaces. In Windows, when a user
resizes a window, Windows sends a message to the program indicating the new
window size. The program can then adjust the contents of its window to
reflect the new size.

"Windows sends a message to the program." I hope you didn't read that
statement without blinking. What on earth could it mean? We're talking about
program code here, not an electronic mail system. How can an operating
system send a message to a program?

When I say that "Windows sends a message to a program," I mean that Windows
calls a function within the program. The parameters to this function
describe the particular message. This function located in your Windows
program is known as the "window procedure."


The Window Procedure

You are undoubtedly accustomed to the idea of a program making calls to the
operating system. This is how a program opens a disk file, for example. What
you may not be accustomed to is the idea of an operating system making calls
to a program. Yet this is fundamental to Windows' object-oriented
architecture.

Every window that a program creates has an associated window procedure. This
window procedure is a function that could be either in the program itself or
in a dynamic link library. Windows sends a message to a window by calling
the window procedure. The window procedure does some processing based on the
message and then returns control to Windows.

More precisely, a window is always created based on a "window class." The
window class identifies the window procedure that processes messages to the
window. The use of a window class allows multiple windows to be based on the
same window class and hence use the same window procedure. For example, all
buttons in all Windows programs are based on the same window class. This
window class is associated with a window procedure (located in the Windows
USER.EXE dynamic link library) that processes messages to all the button
windows.

In object-oriented programming, an "object" is a combination of code and
data. A window is an object. The code is the window procedure. The data is
information retained by the window procedure and information retained by
Windows for each window and window class that exists in the system.

A window procedure processes messages to the window. Very often these
messages inform a window of user input from the keyboard or mouse. This is
how a push-button window knows that it's being "pressed," for example. Other
messages tell a window when it is being resized, or when the surface of the
window needs to be repainted.

When a Windows program begins execution, Windows creates a "message queue"
for the program. This message queue stores messages to all the various
windows a program  may create. The program includes a short chunk of code
called the "message loop" to retrieve these messages from the queue and
dispatch them to the appropriate window procedure. Other messages are sent
directly to the window procedure without being placed in the message queue.

If your eyes are beginning to glaze over with this excessively abstract
description of Windows architecture, maybe it will help to see how the
window, the window class, the window procedure, the message queue, the
message loop, and the window messages all fit together in the context of a
real program.



YOUR FIRST WINDOWS PROGRAM

In their classic book The C Programming Language (2d ed., Prentice Hall,
1988), Brian Kernighan and Dennis Ritchie begin discussing C with the
now-famous "Hello, world" program:

#include <stdio.h>

main ()
     {
     printf ("Hello, world\n") ;
     }

In the remainder of this chapter, I will show you the analogous program
written for Microsoft Windows. The program is called HELLOWIN, and it
creates a window that displays the text string "Hello, Windows!"

Lest you collapse from shock when you first look at the HELLOWIN code, I'll
warn you now that there are three files involved, and that the HELLOWIN.C
source code file is over 80 lines long. Most of these 80 lines are overhead.
You'll have similar overhead in almost every Windows program you write.

Rather than ask why the "Hello, Windows" program is so long and complex,
let's ask why the traditional "Hello, world" program is so short and simple.

What's Wrong with this Program?

The output model for the "Hello, world" program and other traditional C
programs is an antique piece of hardware known as the teletype. The teletype
resembles a typewriter with a continuous roll of paper. In the not too
distant past, programmers would sit at a teletype and type in commands that
were echoed to the paper. The computer responded by printing its output on
the paper.

The teletype metaphor was extended to the video display in the early days of
computers. The video display became a "glass teletype" that simply scrolled
when text reached the bottom of the screen.

How can the traditional "Hello, world" program display text without telling
the operating system the particular output device on which the text is to
appear? Because there is only one output device--the video display used as
if it were a teletype. If the user wishes the output to go elsewhere, it
must be redirected from the command line.

How can the program display text without telling the system where on the
output device the text is to appear? Because the text always appears where
the cursor happens to be, probably on the next line after you execute the
program. Suppose you want to display "Hello, world" in the center of the
screen. You'd have to use some device-dependent control codes to first
position the cursor at the desired location.

Let's say you want to run several "Hello, world" programs at one time and
see their output on the screen. What a mess! The copies of the program would
interfere with each other. There is nothing in the teletype metaphor to
separate output from several programs running concurrently.

It's also interesting that you see the "Hello, world" output even after the
program terminates. Rather than properly cleaning up after itself, the
program is leaving remnants of its existence on the video display.

The "Hello, world" program is so simple because it is designed for a simpler
age and simpler computers and simpler output devices. It's not in the same
ballpark as what we think of today as modern software, and it's not even
playing the same game.


The HELLOWIN Files

The three files necessary to create the "Hello, Windows" program are shown
in Figure 1-5:

  ■   HELLOWIN.MAK is a "make" file.

  ■   HELLOWIN.C is the C source code file.

  ■   HELLOWIN.DEF is a module definition file.

 HELLOWIN.MAK

#------------------------
# HELLOWIN.MAK make file
#------------------------

hellowin.exe : hellowin.obj hellowin.def
     link hellowin, /align:16, NUL, /nod slibcew libw, hellowin
     rc hellowin.exe

hellowin.obj : hellowin.c
     cl -c -Gsw -Ow -W2 -Zp hellowin.c


 HELLOWIN.C

/*---------------------------------------------------------
   HELLOWIN.C -- Displays "Hello, Windows!" in client area
                 (c) Charles Petzold, 1990
  ---------------------------------------------------------*/

#include <windows.h>

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdParam, int nCmdShow)
     {
     static char szAppName[] = "HelloWin" ;
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName,         // window class name
                    "The Hello Program",     // window caption
                    WS_OVERLAPPEDWINDOW,     // window style
                    CW_USEDEFAULT,           // initial x position
                    CW_USEDEFAULT,           // initial y position
                    CW_USEDEFAULT,           // initial x size
                    CW_USEDEFAULT,           // initial y size
                    NULL,                    // parent window handle
                    NULL,                    // window menu handle
                    hInstance,               // program instance handle
                    NULL) ;                  // creation parameters

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;
     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     HDC         hdc ;
     PAINTSTRUCT ps ;
     RECT        rect ;

     switch (message)
          {
          case WM_PAINT :
               hdc = BeginPaint (hwnd, &ps) ;

               GetClientRect (hwnd, &rect) ;

               DrawText (hdc, "Hello, Windows!", -1, &rect,
                         DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;

               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }

     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 HELLOWIN.DEF

;-------------------------------------
; HELLOWIN.DEF module definition file
;-------------------------------------

NAME           HELLOWIN

DESCRIPTION    'Hello Windows Program (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

These are standard files that you'll create for every Windows program you
write. Generally when you begin a new Windows program, you'll copy the
standard files from an existing program and then make appropriate changes to
them.

Most Windows programmers do all their program development and compiling
outside of Windows under MS-DOS, and then load Windows to test the program.
You can also create and compile a Windows program in the Microsoft C 6
Programmer's WorkBench. I'll be discussing the source code files as if you
create them in a text editor of your choice and then compile the program
from the MS-DOS command line outside of Windows.

If you have Windows, the Windows Software Development Kit, and the Microsoft
C Professional Development System (the C 6 compiler) properly installed, you
can create HELLOWIN.EXE from the three files shown in Figure 1-5 by
executing:

NMAKE HELLOWIN.MAK

on the MS-DOS command line. You can then run Windows and the HELLOWIN.EXE
program by executing:

WIN HELLOWIN

The program creates a normal application window as shown in Figure 1-6 on
the following page. The window displays "Hello, Windows!" in the center of
its client area.

When you think about it, this window has an amazing amount of functionality
in its mere 80 lines of code. You can grab the title bar with the mouse
pointer and move the window around the screen. You can grab the sizing
borders and resize the window. When the window changes size, the program
will automatically reposition the "Hello, Windows!" text string in the new
center of the client area. You can press the maximize button and zoom
HELLOWIN to fill the screen. You can press the minimize button and compress
the program into an icon. You can invoke all these options from the system
menu and, in addition, close the window to terminate the program.

While you may be pleased to see that HELLOWIN has all the functionality of a
normal Windows program, you may not look so pleasant-faced when you see the
source code required to create this program. But let's be brave while I
proceed to dissect this program piece by piece and analyze it to death.

  (Figure 1-6. may be found in the printed book.)


The Make File

To ease compilation of Windows programs, you can use the NMAKE utility
included in the Microsoft C Professional Development System. Whenever you
change something in one of the HELLOWIN source files, all you need do is
run:

NMAKE HELLOWIN.MAK

to create the updated HELLOWIN.EXE executable.

A make file consists of one or more sections, each of which begins with a
left-justified line that lists a target file, followed by a colon, followed
by one or more dependent files. This line is followed by one or more
indented command lines. These commands create the target file from the
dependent files. If the last modification date and time of any of the
dependent files is later than the last modification date and time of the
target file, then NMAKE executes the indented command lines.

Normally, NMAKE will update only the target file in the first section of the
make file. However, if one of the dependent files is itself a target file in
another section of the make file, then NMAKE will update that target first.

The HELLOWIN.MAK make file contains two sections. The first runs the
LINK.EXE linker if HELLOWIN.OBJ or HELLOWIN.DEF has been altered more
recently than HELLOWIN.EXE:

hellowin.exe : hellowin.obj hellowin.def
     link hellowin, /align:16, NUL, /nod slibcew libw, hellowin
     rc hellowin.exe

The second section runs the CL.EXE C compiler if HELLOWIN.C has been changed
more recently than HELLOWIN.OBJ:

hellowin.obj : hellowin.c
     cl -c -Gsw -Ow -W2 -Zp hellowin.c

Because HELLOWIN.OBJ is a dependent file in the first section of the make
file and a target file in the second section, NMAKE will check whether
HELLOWIN.OBJ needs updating before re-creating HELLOWIN.EXE. Thus, the make
file should be analyzed from the bottom up.

Running the CL.EXE C compiler creates the HELLOWIN.OBJ object module from
the HELLOWIN.C source code file:

cl -c -Gsw -Ow -W2 -Zp hellowin.c

Several compiler switches are required (or recommended) for compiling
Windows programs:

  ■   The -c switch indicates that the program should be compiled only and
      not yet linked. The link is a separate step.

  ■   The -Gsw switch is actually two switches: -Gs and -Gw. The -Gs switch
      disables checks for stack overflow. Because stack overflow messages
      are written to standard error output (and are hence ignored by
      Windows), it's best simply to be sure that you are using a sufficient
      stack. (Four kilobytes is recommended.)

  ■   The -Gw switch is a special Windows switch that inserts special prolog
      and epilog code in all far functions in the program. This code (which
      I'll discuss in Chapter 7) aids Windows in moving code and data
      segments in memory.

  ■   The -Ow switch concerns optimization. With this switch the compiler
      will avoid some optimizations that may cause problems specifically
      with Windows programs.

  ■   The -W2 switch enables warning level 2 for displaying warning
      messages. You should make an effort to write programs that show no
      warning messages when you compile with this switch. Windows will not
      tolerate sloppy programming, which can lead to nasty bugs.

  ■   The -Zp switch packs structure fields on byte boundaries. This is
      required for some of the structures defined in WINDOWS.H that programs
      use to communicate with Windows. Windows assumes that all structures
      are packed.

The first section of the make file runs two commands if HELLOWIN.OBJ or
HELLOWIN.DEF has been altered more recently than HELLOWIN.EXE. The first
indented command runs LINK:

link hellowin, /align:16, NUL, /nod slibcew libw, hellowin

The first field indicates the HELLOWIN.OBJ object file. The .OBJ extension
is assumed. The second field would normally list the name of the executable
file, but I'm letting it default to HELLOWIN.EXE. The /align:16 switch tells
LINK to align code and data segments on 16-byte boundaries in the
HELLOWIN.EXE file for better space efficiency. (The default is 512-byte
boundaries.)

The third field is the name of an optional map file. This is set to NUL to
create no map file. The fourth field lists the libraries followed by the
/nod (no default libraries) switch. SLIBCEW.LIB is the small model Windows C
run time library created during installation of the Windows Software
Development Kit.

LIBW.LIB is an import library that contains information LINK uses to set up
a table in the .EXE file so that Windows can dynamically link the program's
calls to Windows functions with the Windows dynamic link libraries that
contain those functions.

The fifth field indicates the name of the program's module definition file,
HELLOWIN.DEF. The .DEF extension is assumed. (I'll discuss this file later
in this chapter.) It contains information that LINK uses to construct
HELLOWIN.EXE.

The second indented command runs the Windows resource compiler, RC.EXE:

rc hellowin.exe

The resource compiler sets a couple flags in the HELLOWIN.EXE file to
indicate that this is a Windows 3\-compatible application. Primarily, this
avoids a Windows 3 message that warns the user that the program may crash
because it has not been modified for protected-mode operation. Later on,
we'll use the resource compiler to add menus and dialog boxes to our Windows
programs.


The C Source Code File

The second file in Figure 1-5 is HELLOWIN.C, the C source code file. It may
take awhile before you recognize that this program is indeed written in C!

Let's first take a global look at HELLOWIN.C before getting into details.
The file contains only two functions: WinMain and WndProc. WinMain is the
entry point to the program. It is the equivalent of the standard C main
function. Every Windows program has a WinMain function.

WndProc is the window procedure for HELLOWIN's window. This function
processes messages to the window. No code in HELLOWIN.C calls WndProc
directly: WndProc is called only from Windows. However, there is a reference
to WndProc in WinMain, which is why the function is declared near the top of
the program before WinMain.


The Windows Function Calls

HELLOWIN makes calls to no less than 16 Windows functions. In the order they
occur in HELLOWIN, these functions (with a brief description) are:

  ■   LoadIcon--Loads an icon for use by a program

  ■   LoadCursor--Loads a cursor for use by a program

  ■   GetStockObject--Obtains a graphics object (in this case a brush used
      for painting the window's background)

  ■   RegisterClass--Registers a window class for the program's window

  ■   CreateWindow--Creates a window based on a window class

  ■   ShowWindow--Displays the window on the screen

  ■   UpdateWindow--Directs the window to paint itself

  ■   GetMessage--Obtains a message from the message queue

  ■   TranslateMessage--Translates some keyboard messages

  ■   DispatchMessage--Sends a message to a window procedure

  ■   BeginPaint--Initiates the beginning of window painting

  ■   GetClientRect--Obtains the dimensions of the window's client area

  ■   DrawText--Displays a text string

  ■   EndPaint--Ends window painting

  ■   PostQuitMessage--Inserts a "quit" message into the message queue

  ■   DefWindowProc--Performs default processing of messages

These functions are documented in the Windows Programmer's Reference and
declared in WINDOWS.H. I'll discuss each of them as we encounter them while
dissecting the program.


Uppercase Identifiers

You'll notice the use of quite a few uppercase identifiers in HELLOWIN.C.
These identifiers are defined in WINDOWS.H.

Several of these identifiers contain a two-letter or three-letter prefix
followed by an underscore:

────────────────────────────────────────────────────────────────────────────
CS_HREDRAW           DT_SINGLELINE  WM_DESTROY
IDI_APPLICATION      CS_VREDRAW     DT_CENTER
WS_OVERLAPPEDWINDOW  IDC_ARROW      DT_VCENTER
WM_PAINT             CW_USEDEFAULT

These are simply numeric constants. The prefix indicates a general category
to which the constant belongs, as indicated in this table:

Prefix                  Category
────────────────────────────────────────────────────────────────────────────
CS                      class style
IDI                     ID for an icon
IDC                     ID for a cursor
WS                      window style
CW                      create window
WM                      window message
DT                      draw text

You almost never need to remember numeric constants when programming for
Windows. Virtually every numeric constant used in Windows has an identifier
defined in WINDOWS.H.


New Data Types

Other identifiers used in HELLOWIN.C are new data types, also defined in
WINDOWS.H. The ones used in the program are:

Data Type        Meaning
────────────────────────────────────────────────────────────────────────────
FAR              same as far
PASCAL           same as pascal
WORD             unsigned integer (16 bits)
DWORD            unsigned long integer (32 bits)
LONG             signed long integer (32 bits)
LPSTR            far (or long) pointer to a character string

These are fairly self-explanatory. The people who originally developed
Windows thought that it would someday be ported to other microprocessors.
These new data types were defined to ease the porting of Windows
applications to other architectures. Rather than use machine-specific data
sizes (such as the size of C integer), the new data types were devised to
keep programs consistent regardless of the processor on which they run.

Of course, Windows will probably never be ported to other architectures, but
the Windows functions are still defined using these new data types, and
Windows programmers continue to use them.

HELLOWIN also uses four data structures (which I'll discuss later in this
chapter) defined in WINDOWS.H:

Structure               Meaning
────────────────────────────────────────────────────────────────────────────
MSG                     The message structure
WNDCLASS                The window class structure
PAINTSTRUCT             The paint structure
RECT                    The rectangle structure

The first two data structures are used in WinMain to define two structures
named msg and wndclass. The second two are used in WndProc to define two
structures  named ps and rect.


Getting a Handle on Handles

Finally, there are three uppercase identifiers for various types of
"handles":

Identifier             Meaning
────────────────────────────────────────────────────────────────────────────
HANDLE                 Generic handle
HWND                   Handle to a window
HDC                    Handle to a device context

Handles are used quite frequently in Windows. Before the chapter is over,
you also encounter HICON (a handle to an icon), HCURSOR (a handle to a mouse
cursor), and HBRUSH (a handle to a graphics brush).

A handle is simply a 16-bit number that refers to an object. The handles in
Windows are similar to file handles used in conventional C or MS-DOS
programming. A program almost always obtains a handle by calling a Windows
function. The program uses the handle in other Windows functions to refer to
the object. The actual value of the handle is unimportant to your program,
but the Windows module that gives your program the handle knows how to use
it to reference the object.


Hungarian Notation

You may also notice that some of the variables in HELLOWIN.C have
peculiar-looking names. One example is lpszCmdParam, passed as a parameter
to WinMain.

Many Windows programmers use a variable-naming convention known as Hungarian
notation, in honor of the legendary Microsoft programmer Charles Simonyi.
Very simply, the variable name begins with a lowercase letter or letters
that denote the data type of the variable. For example, the lpsz prefix in
lpszCmdParam stands for "long pointer to a string terminated by zero."

The h prefix in hInstance and hPrevInstance stands for "handle"; the n
prefix in nCmdShow stands for "number," and usually specifies an integer.
Two of the parameters to WndProc also use Hungarian notation: wParam is a
WORD and lParam is a LONG.

When naming structure variables, you can use the structure name (or an
abbreviation of the structure name) in lowercase as either a prefix to the
variable name or as the entire variable name. For example, in the WinMain
function in HELLOWIN.C, the msg variable is a structure of the MSG type;
wndclass is a structure of the WNDCLASS type. In the WndProc function, ps is
a PAINTSTRUCT structure and rect is a RECT structure.

Hungarian notation helps you avoid errors in your code before they turn into
bugs. Because the name of a variable describes both the use of a variable
and its data type, you are much less inclined to make coding errors
involving mismatched data types.

The variable name prefixes I'll be using in this book are shown in the
following table:

╓┌──────────┌────────────────────────────────────────────────────────────────╖
Prefix     Data Type
────────────────────────────────────────────────────────────────────────────
c          char
by         BYTE (unsigned char)
n          short or int
i          int
x, y       short (used as x-coordinate or y-coordinate)
Prefix     Data Type
────────────────────────────────────────────────────────────────────────────
x, y       short (used as x-coordinate or y-coordinate)
cx, cy     short (used as x or y length; the c stands for "count")
b          BOOL (int)
w          WORD (unsigned int)
l          LONG (long)
dw         DWORD (unsigned long)
fn         function
s          string
sz         string terminated by 0 byte



The Program Entry Point

With this global look at HELLOWIN.C out of the way, we can now begin the
line-by-line dissection of the program. The code begins with an #include
statement to include the WINDOWS.H header file:

#include <windows.h>

WINDOWS.H contains declarations of the Windows functions, the Windows
structures, the new data types, and numeric constants.

This is followed by a forward declaration of the WndProc function:

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

The declaration is required because WndProc is referenced by some code in
the WinMain function.

In a C program written for a conventional environment, the entry point is a
function called main. This is where the program begins execution. (Actually,
the main function is the entry point to the part of the program written by
the programmer. Usually the C compiler will insert some start-up code in the
executable file. The start-up code then calls main.) The entry point of a
Windows program is a function called WinMain. (As is the case with main,
WinMain is actually called from some start-up code inserted into the
executable file.) WinMain is always defined like this:

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                         LPSTR lpszCmdParam, int nCmdShow)

This function uses the PASCAL calling sequence and returns an integer to the
start-up code. The function must be named WinMain. It has four parameters.

The hInstance parameter is called the "instance handle." This is a number
that uniquely identifies the program when it is running under Windows. It
could be that the user is running multiple copies of the same program under
Windows. (For example, most Windows users at one time or another have loaded
multiple versions of the CLOCK program to see what happens.) Each copy is
called an "instance," and each has a different hInstance value. The instance
handle is comparable to a "task ID" or "process ID" number common in
mulitasking operating systems.

The hPrevInstance ("previous instance") parameter is the instance handle of
the most recent previous instance of the same program that is still active.
If no other copies of the program are currently loaded, then hPrevInstance
will be 0 or NULL.

The lpszCmdParam parameter is a long (or far) pointer to a 0-terminated
string that contains any command-line parameters passed to the program. It
is possible to run a Windows program with a command-line parameter by typing
the program name and the parameter into the Run dialog box invoked from
either the Program Manager or the File Manager.

The nCmdShow parameter is a number indicating how the window is to be
initially displayed in Windows. This number is assigned by whatever program
executes the program to run under Windows. Programs do not often need to
examine this number, but they can if they want. In most cases the number is
either a 1 or a 7. But it's best not to think of the value as a 1 or a 7.
Rather, think of the value as SW_SHOWNORMAL (defined in WINDOWS.H as 1) or
SW_SHOWMINNOACTIVE (defined as 7). The SW prefix in these identifiers stands
for "show window." This indicates whether the user launched the program to
be displayed as a normal window or to be initially minimized.


Registering the Window Class

A window is always created based on a window class. The window class
identifies the window procedure that processes messages to the window. This
is important, so I'll repeat it: A window is always created based on a
window class. The window class identifies the window procedure that
processes messages to the window.

More than one window can be created based on a single window class. For
example, all button windows in Windows are created based on the same window
class. The window class defines the window procedure and some other
characteristics of the windows that are created based on that class. When
you create a window you define additional characteristics of the window that
are unique to that window.

Before you create a window for your program, you must register a window
class by calling RegisterClass. The RegisterClass function requires a single
parameter: a pointer to a structure of type WNDCLASS. The WNDCLASS structure
is defined in WINDOWS.H like this:

typedef struct tagWNDCLASS
     {
     WORD    style ;
     LONG    (FAR PASCAL *lpfnWndProc) () ;
     int     cbClsExtra ;
     int     cbWndExtra ;
     HANDLE  hInstance ;
     HICON   hIcon ;
     HCURSOR hCursor ;
     HBRUSH  hbrBackground ;
     LPSTR   lpszMenuName ;
     LPSTR   lpszClassName ;
     }
     WNDCLASS ;

In WinMain, you must define a structure of type WNDCLASS, generally like
this:

WNDCLASS wndclass ;

You then define the 10 fields of the structure and call RegisterClass:

RegisterClass (&wndclass) ;

Only the first instance of a program needs to register the window class. The
window class then becomes available to all subsequent instances of the
program. For this reason, HELLOWIN initializes the fields of the WNDCLASS
structure and calls RegisterClass only if hPrevInstance equals NULL.

The WNDCLASS structure has 10 fields. The two most important fields are the
last and the second. The last field is the name of the window class (which
is generally the same as the name of the program). The second field
(lpfnWndProc) is the address of the window procedure used for all windows
created based on this class (which is the function WndProc in HELLOWIN.C).
All the other fields describe characteristics of all windows based on this
window class.

The statement:

wndclass.style = CS_HREDRAW | CS_VREDRAW ;

combines two "class style" identifiers with a C bitwise OR operator. In
WINDOWS.H, the various identifiers beginning with the CS prefix are defined
as 16-bit constants with one bit set. For example, CS_VREDRAW is defined as
0x0001, and CS_HREDRAW is defined as 0x0002. Identifiers defined in this way
are sometimes called "bit flags." You combine the bit-flag identifiers with
the C OR operator.

These two class-style identifiers indicate that all windows created based on
this class are to be completely repainted whenever the horizontal window
size (CS_HREDRAW) or the vertical window size (CS_VREDRAW) changes. If you
resize HELLOWIN's window, you'll see that the text string is redrawn to be
in the new center of the window. These two identifiers ensure that this
happens.

The second field of the WNDCLASS structure is initialized by the statement:

wndclass.lpfnWndProc = WndProc ;

This sets the window procedure for this window class to WndProc, which is
the second function in HELLOWIN.C. This window procedure will process all
messages to all windows created based on this window class. The lpfn prefix
in the field name is Hungarian notation for "long pointer to a function."

The next two statements:

wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;

reserve some extra space in the class structure and the window structure
that Windows maintains internally. A program can use this extra space for
its own purpose. HELLOWIN does not use this feature, so zero is specified.
The cb prefix in the field names stands for a "count of bytes."

The next field is simply the instance handle of the program (which is one of
the parameters to WinMain):

wndclass.hInstance = hInstance ;

The statement:

wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;

sets an icon for all windows created based on this window class. The icon is
a small bitmap picture that appears when the program is minimized. Later in
this book you'll learn how to create customized icons for your Windows
programs. Right now, we'll take an easy approach and use a predefined icon.

To obtain a handle to a predefined icon, you call LoadIcon with a first
parameter set to NULL. (When loading your own customized icon, this
parameter would be set to the instance handle of the program.) The second
parameter is an identifier beginning with the IDI ("ID for an icon") defined
in WINDOWS.H. The IDI_APPLICATION icon is simply a white square with a black
outline. The LoadIcon function returns a handle to this icon. We don't
really care about the value of this handle. It's simply used to set the
value of the hIcon field. The hIcon field is defined in the WNDCLASS
structure to be of type HICON, which stands for "handle to an icon."

The statement:

wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;

is very similar to the previous statement. The LoadCursor function loads a
predefined mouse cursor known as IDC_ARROW and returns a handle to the
cursor. This handle is assigned to the hCursor field of the WNDCLASS
structure. When the mouse cursor appears over the client area of a window
that is created based on this class, the cursor becomes a small arrow.

The next field specifies the background color of the client area of windows
created based on this class. The hbr prefix of the hbrBackground field name
stands for "handle to a brush." A brush is a graphics term that refers to a
colored pattern of pixels used to fill an area. Windows has several
standard, or "stock," brushes. The GetObject call shown here returns a
handle to a white brush:

wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;

This means the background of the client area of the window will be solid
white, which is a common choice.

The next field specifies the window class menu. HELLOWIN has no application
menu, so the field is set to NULL:

wndclass.lpszMenuName  = NULL ;

Finally the class must be given a name. This is the same as the name of the
program, which is the "HelloWin" string stored in the szAppName variable:

wndclass.lpszClassName = szAppName ;

When all 10 fields of the structure have been initialized, HELLOWIN
registers the window class by calling RegisterClass. The only parameter to
the function is a pointer to the WNDCLASS structure:

RegisterClass (&wndclass) ;


Creating the Window

The window class defines general characteristics of a window, thus allowing
the same window class to be used for creating many different windows. When
you actually create a window by calling CreateWindow, you specify more
detailed information about the window. Rather than using a data structure as
RegisterClass does, the CreateWindow call requires all the information to be
passed as parameters to the function. Here's the CreateWindow call in
HELLOWIN.C:

hwnd = CreateWindow (szAppName,         // window class name
               "The Hello Program",     // window caption
               WS_OVERLAPPEDWINDOW,     // window style
               CW_USEDEFAULT,           // initial x position
               CW_USEDEFAULT,           // initial y position
               CW_USEDEFAULT,           // initial x size
               CW_USEDEFAULT,           // initial y size
               NULL,                    // parent window handle
               NULL,                    // window menu handle
               hInstance,               // program instance handle
               NULL) ;                  // creation parameters

The Microsoft C compiler recognizes the // symbol for single-line comments.
The comments describe the parameters to the CreateWindow function.

Although you need to register a window class only for the first instance of
a program, you must create a window separately for each instance. Each
instance has its own window, and all the windows are based on the same
window class.

The parameter marked "window class name" is szAppName, which contains the
string "HelloWin"--the name of the window class we just registered. This is
how the window is associated with the window class.

The window created by this program is a normal overlapped window with a
caption bar, a system menu box to the left of the caption bar, minimize and
maximize icons to the right of the caption bar, and a thick window-sizing
border. That's a standard style of windows, and it has the WINDOWS.H name
WS_OVERLAPPEDWINDOW, which appears as the "window style" parameter. The
"window caption" is the text that will appear in the caption bar.

The parameters marked "initial x position" and "initial y position" specify
the initial position of the upper left corner of the window relative to the
upper left corner of the screen. By using the identifier CW_USEDEFAULT for
these parameters, we're indicating we want Windows to use the default
position for an overlapped window. (CW_USEDEFAULT  is defined as 0x8000.) By
default, Windows positions successive overlapped windows at stepped
horizontal and vertical offsets from the upper left corner of the display.

Similarly, the "initial x size" and "initial y size" parameters specify the
width and height of the window. The CW_USEDEFAULT identifier again indicates
that we want Windows to use a default size for the window. The default size
extends to the right side of the display and above the icon area at the
bottom of the screen.

The parameter marked "parent window handle" is set to NULL because this
window has no parent window. (When a parent-child relationship exists
between two windows, the child window always appears on the surface of its
parent.) The "window menu handle" is also set to NULL because the window has
no menu. The "program instance handle" is set to the instance handle passed
to the program as a parameter of WinMain. Finally, a "creation parameters"
pointer is set to NULL. You could use this pointer to access some data that
you might later want to reference in the program.

The CreateWindow call returns a handle to the created window. This handle is
saved in the variable hwnd, which is defined to be of type HWND (handle to a
window). Every window in Windows has a handle. Your program uses the handle
to refer to the window. Many Windows functions require hwnd as a parameter
so that Windows knows to which window the function applies. If a program
creates many windows, each has a different handle. The handle to a window is
one of the most important handles a Windows program (pardon the expression)
handles.


Displaying the Window

After the CreateWindow call returns, the window has been created internally
in Windows. However, the window does not yet appear on the video display.
Two more calls are needed. The first is:

ShowWindow (hwnd, nCmdShow) ;

The first parameter is the handle to the window just created by
CreateWindow. The second parameter is the nCmdShow value passed as a
parameter to WinMain. This determines how the window is to be initially
displayed on the screen. If nCmdShow is SW_SHOWNORMAL (equal to 1), the
window is displayed normally. If nCmdShow is SW_SHOWMINNOACTIVE (equal to
7), then the window is initially displayed as an icon.

The ShowWindow function puts the window (or icon) on the display. If the
second parameter to ShowWindow is SW_SHOWNORMAL, the client area of the
window is erased with the background brush specified in the window class.
The function call:

UpdateWindow (hwnd) ;

then causes the client area to be painted. It accomplishes this by sending
the window procedure (the WndProc function in HELLOWIN.C) a WM_PAINT
message. We'll examine shortly how WndProc deals with this message.


The Message Loop

After the UpdateWindow call, the window is fully visible on the video
display. The program must now make itself ready to read keyboard and mouse
input from the user. Windows maintains a "message queue" for each Windows
program currently running under Windows. When an input event occurs, Windows
translates the event into a "message" that it places in the program's
message queue.

A program retrieves these messages from the message queue by executing a
block of code known as the "message loop":

while (GetMessage (&msg, NULL, 0, 0))
     {
     TranslateMessage (&msg) ;
     DispatchMessage (&msg) ;
     }
return msg.wParam ;

The msg variable is a structure of type MSG, which is defined in WINDOWS.H
as follows:

typedef struct tagMSG
     {
     HWND  hwnd ;
     WORD  message ;
     WORD  wParam ;
     LONG  lParam ;
     DWORD time ;
     POINT pt ;
     }
     MSG ;

The POINT data type is yet another structure, defined like this:

typedef struct tagPOINT
     {
     int x ;
     int y ;
     }
     POINT ;

The GetMessage call that begins the message loop retrieves a message from
the message queue:

GetMessage (&msg, NULL, 0, 0) ;

This call passes to Windows a far pointer to the MSG structure called msg.
The second, third, and fourth parameters are set to NULL or 0 to indicate
that the program wants all messages for all windows created by the program.
Windows fills in the fields of the message structure with the next message
from the message queue. The fields of this structure are:

  ■   hwnd--the handle to the window to which the message is directed. In
      the HELLOWIN program, this is the same as the hwnd value returned from
      CreateWindow, because that's the only window this program has.

  ■   message--the message identifier. This is a number that identifies the
      message. For each message, there is a corresponding identifier defined
      in WINDOWS.H that begins with the prefix WM ("window message"). For
      example, if you position the mouse pointer over HELLOWIN's client area
      and press the left mouse button, Windows will put a message in the
      message queue with a message field equal to WM_LBUTTONDOWN, which is
      the value 0x0201.

  ■   wParam--a 16-bit "message parameter," the meaning and value of which
      depend on the particular message.

  ■   lParam--a 32-bit message parameter dependent on the message.

  ■   time--the time the message was placed in the message queue.

  ■   pt--the mouse coordinates at the time the message was placed in the
      message queue.

If the message field of the message retrieved from the message queue is
anything except WM_QUIT (which equals 0x0012), then GetMessage returns a
nonzero value. A WM_QUIT message causes the program to fall out of the
message loop. The program then terminates, returning the wParam member of
the msg structure.

The statement:

TranslateMessage (&msg) ;

passes the MSG structure back to Windows for some keyboard translation.
(I'll discuss this more in Chapter 3.) The statement:

DispatchMessage (&msg) ;

again passes the MSG structure back to Windows. Windows then sends the
message to the appropriate window procedure for processing. That window
procedure is the WndProc function in HELLOWIN. After WndProc processes the
message, it then returns to Windows, which is still servicing the
DispatchMessage call. When Windows returns to HELLOWIN following the
DispatchMessage call, the message loop continues with the next GetMessage
call.


The Window Procedure

All that I've described so far is really just overhead. The window class has
been registered, the window has been created, the window has been displayed
on the screen, and the program has entered a message loop to retrieve
messages from the message queue.

The real action occurs in the window procedure, which Windows programmers
commonly call a "window proc" (pronounced "prock"). The window procedure
determines what the window displays in its client area and how the window
responds to user input.

In HELLOWIN, the window procedure is the function called WndProc. A window
procedure can have any name. A Windows program can contain more than one
window procedure. A window procedure is always associated with a particular
window class that you register by calling RegisterClass. The CreateWindow
function creates a window based on a particular window class. More than one
window can be created based on the same window class.

A window procedure is always defined like this:

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)

Note that the four parameters to the window procedure are identical to the
first four fields of the MSG structure.

The first parameter is hwnd, the handle to the window receiving the message.
This is the same handle returned from the CreateWindow function. For a
program like HELLOWIN, which creates only one window, this is the only
window handle the program knows about. If a program creates multiple windows
based on the same window class (and hence the same window procedure), then
hwnd identifies the particular window receiving the message.

The second parameter is a number (specifically, a 16-bit unsigned integer or
WORD) that identifies the message. The last two parameters (a WORD called
wParam and a 32-bit signed long integer or LONG called lParam) provide more
information about the message. These are called "message parameters." What
these parameters contain is specific to each type of message.


Processing the Messages

Each message that a window procedure receives is identified by a number,
which is the message parameter to the window procedure. The WINDOWS.H header
file defines identifiers beginning with the prefix WM ("window message") for
each message parameter.

Generally, Windows programmers use a switch and case construction to
determine what message the window procedure is receiving and how to process
it accordingly. When a window procedure processes a message, it should
return 0 from the window procedure. All messages that a window procedure
chooses not to process must be passed to a Windows function named
DefWindowProc. The value returned from DefWindowProc must be returned from
the window procedure.

In HELLOWIN, WndProc chooses to process only two messages: WM_PAINT and
WM_DESTROY. The window procedure is structured like this:

switch (message)
     {
     case WM_PAINT :
[ process WM_PAINT message ]
          return 0 ;

     case WM_DESTROY :
[ process WM_DESTROY message ]
          return 0 ;
     }

return DefWindowProc (hwnd, message, wParam, lParam) ;

It is essential to call DefWindowProc for all messages that your window
procedure does not process.


The WM_PAINT Message

The first message that WndProc processes is WM_PAINT. This message is
extremely important in Windows programming. It informs a program when part
or all of the window's client area is "invalid" and must be repainted.

How does a client area become invalid? When the window is first created, the
entire client area is invalid because the program has not yet drawn anything
on the window. The first WM_PAINT message (which normally occurs when the
program calls UpdateWindow in WinMain) directs the window procedure to draw
something on the client area.

When you resize HELLOWIN's window, the client area also becomes invalid.
You'll recall that the style parameter of HELLOWIN's wndclass structure was
set to the flags  CS_HREDRAW and CS_VREDRAW. This directs Windows to
invalidate the whole window when the size changes. The window procedure
receives a WM_PAINT message.

When you minimize HELLOWIN to be displayed as an icon and then restore the
window again to its previous size, Windows does not save the contents of the
client area. Under a graphical environment, this would be too much data.
Instead, Windows invalidates the window. The window procedure receives a
WM_PAINT message and itself restores the contents of its window.

When you move windows around so they overlap, Windows does not save the area
of a window covered by another window. When that area of the window is later
uncovered, it is flagged as invalid. The window procedure receives a
WM_PAINT message to repaint the contents of the window.

Before sending the window procedure a WM_PAINT message, Windows erases the
background of the invalid area using the brush specified in the
hbrBackground field of the WNDCLASS structure used to register the window
class. In the case of HELLOWIN, this is a stock white brush, which means
that Windows erases the background of the window by coloring it white.

WM_PAINT processing almost always begins with a call to BeginPaint:

hdc = BeginPaint (hwnd, &ps) ;

and ends with a call to EndPaint:

EndPaint (hwnd, &ps) ;

In both cases, the first parameter is a handle to the program's window and
the second parameter is a pointer to a structure of type PAINTSTRUCT.
PAINTSTRUCT contains some information that a window procedure can use for
painting the client area. (I'll discuss the fields of this structure in the
next chapter.)

BeginPaint returns a "handle to a device context." A device context refers
to a physical output device (such as a video display) and its device driver.
You need the device context handle to display text and graphics in the
client area of a window. Using the device context handle returned from
BeginPaint, you cannot draw outside the client area, even if you try.
EndPaint releases the device context handle so that it is no longer valid.
EndPaint also validates the entire client area.

If a window procedure does not process WM_PAINT messages (which is very
rare), they must be passed on to DefWindowProc. DefWindowProc simply calls
BeginPaint and EndPaint in succession so that the client area is validated.

After WndProc calls BeginPaint, it calls GetClientRect:

GetClientRect (hwnd, &rect) ;

The first parameter is the handle to the program's window. The second
parameter is a pointer to a variable named rect defined as type RECT in
WndProc.

RECT is a "rectangle" structure defined in WINDOWS.H. It has four int fields
named left, top, right, and bottom. GetClientRect sets these four fields to
the dimensions of the client area of the window. The left and top fields are
always set to 0. The right and bottom fields are set to the width and height
of the client area in pixels.

WndProc doesn't do anything with this RECT structure except pass a pointer
to it as the fourth parameter of DrawText:

DrawText (hdc, "Hello, Windows!", -1, &rect,
          DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;

DrawText (as the name implies) draws text. Because this function draws
something, the first parameter is a handle to the device context returned
from BeginPaint. The second parameter is the text to draw, and the third
parameter is set to -1 to indicate that the text string is terminated with a
0 byte.

The last parameter is a series of bit flags defined in WINDOWS.H. The flags
indicate that the text should be displayed as a single line centered
horizontally and vertically within the rectangle specified by the fourth
parameter. This function call thus causes the string "Hello, Windows!" to be
displayed centered in the client area.

Whenever the client area becomes invalid (as it does when you change the
size of the window), Windows erases the background of the window and WndProc
receives a new WM_PAINT message. WndProc obtains the updated window size by
calling GetClientRect and again displays the text in the new center of the
window.


The WM_DESTROY Message

The WM_DESTROY message is another important message. This message indicates
that Windows is in the process of destroying a window based on a command
from the user. The message is a result of the user selecting Close from the
program's system menu or pressing Alt-F4.

HELLOWIN responds to this message in a standard way by calling:

PostQuitMessage (0) ;

This function inserts a WM_QUIT message in the program's message queue. I
mentioned earlier that GetMessage returns nonzero for any message other than
WM_QUIT that it retrieves from the message queue. When GetMessage retrieves
a WM_QUIT message, GetMessage returns 0. This causes WinMain to drop out of
the message loop and exit, terminating the program.


The Module Definition File

In addition to the C source code, another file is required for Windows
programs. It is called a "module definition file" and has the extension
.DEF. The module definition file aids the LINK linker in creating the .EXE
file by telling it the characteristics of the program's code and data
segments, the size of the program's local data heap (from which the program
can allocate memory), and the size of the program's stack. This information
becomes part of the header section of the New Executable file format. The
HELLOWIN.DEF file is shown in Figure 1-5 on page 16.

The NAME line defines HELLOWIN as a program (rather than a dynamic link
library) and gives it a module name, which is usually the name of the
program's .EXE file. The DESCRIPTION line simply inserts some text into the
.EXE file. This is an excellent place for a copyright notice or version
information. The EXETYPE line identifies the program as a Windows program.
(OS/2 programs also use module definition files and the New Executable file
format.)

The STUB is a program that is inserted into the .EXE file to be executed
when anyone attempts to run HELLOWIN.EXE from the MS-DOS command line. The
WINSTUB.EXE program included with the Windows Software Development Kit
simply displays the message "This program requires Microsoft Windows" and
terminates.

The CODE statement indicates that the program's code segment is flagged as
PRELOAD (which means that Windows will load the segment into memory
immediately) and MOVEABLE (which means that Windows can move the code
segment to another location in memory if it needs to consolidate blocks of
free memory). The DISCARDABLE option makes the code "discardable" (which
means that Windows can discard the code segment from memory and later reload
it from the .EXE file). These are the normal options for Windows programs.
If you follow proper Windows programming practice, you will not (in theory)
encounter any problems when Windows moves your code.

The DATA statement indicates that we want the data segment to be PRELOAD,
MOVEABLE, and MULTIPLE. Again, we are giving Windows permission to move the
data segment in memory if necessary. The MULTIPLE keyword requests that each
instance of the program gets its own separate data segment. This is
necessary because the data segment contains the program's stack and other
data items that must be separate for each instance. The code segment, on the
other hand, is shared by all instances of the program.

The HEAPSIZE line specifies the amount of extra local memory (memory in the
program's own data segment) that will be available for allocation. The value
depends on what the program needs. HELLOWIN doesn't need to allocate any
local memory, but we'll throw in a small value nonetheless. Windows can
expand a program's local heap if necessary.

The STACKSIZE line specifies the size of the stack. The value 8192 bytes is
a minimum recommended value. You'll want a bigger stack size if your program
has recursive functions or large non-static variables.

Finally, the EXPORTS line lists the window procedure WndProc. For reasons
I'll discuss in Chapter 7, all window procedures that a program contains
must be listed in the EXPORTS section of the module definition file.



THE WINDOWS PROGRAMMING HURDLES

Even with my explanation of HELLOWIN, the structure and workings of the
program are probably still somewhat mysterious. In a short C program written
for a conventional environment, the entire program may be contained in the
main function. In HELLOWIN, WinMain contains only program overhead necessary
to register the window class, create the window, and retrieve and dispatch
messages from the message queue.

All the real action of the program occurs in the window procedure. In
HELLOWIN, this action is not much--it simply displays a text string in its
window. But in later chapters you'll find that almost everything a Windows
program does it does in response to a message to a window procedure. This is
one of the major conceptual hurdles that you must leap to begin writing
Windows programs.

Don't Call Me, I'll Call You

As I mentioned earlier, programmers are familiar with the idea of calling on
the operating system to do something. For instance, C programmers use the
open or fopen function to open a file. The library functions provided with
the compiler have code that eventually calls the operating system to open
the file. No problem.

But Windows is different. Although Windows has more than 550 functions that
your program can call, Windows also makes calls to your program,
specifically to the window procedure we have called WndProc. The window
procedure is associated with a window class that the program registers by
calling RegisterClass. A window that is created based on this class uses
this window procedure for processing all messages to the window. Windows
sends a message to the window by calling the window procedure.

Windows calls WndProc when a window is first being created. Windows calls
WndProc when the window is later destroyed. Windows calls WndProc when the
window has been resized or moved or made into an icon. Windows calls WndProc
when an item has been selected from a menu. Windows calls WndProc when a
scroll bar is being moved or clicked with the mouse. Windows calls WndProc
to tell it when it must repaint its client area.

All these calls are in the form of messages. In most Windows programs, the
bulk of the program is dedicated to handling these messages. The 130 or so
different messages that Windows can send to a window procedure are all
identified with names that begin with the letters WM and defined in
WINDOWS.H.

Actually, the idea of a routine within a program that is called from outside
the program is not unheard of in normal programming. The signal function in
C can trap a Ctrl-Break. You may have experience with intercepting hardware
interrupts in assembly language or using one of the ON constructions in
Microsoft BASIC. The Microsoft Mouse driver has a method that non-Windows
programs can use to be notified of mouse activity.

In Windows, this concept is extended to cover everything. Everything that
happens to a window is relayed to the window procedure in the form of a
message. The window procedure then responds to this message in some way or
passes the message to DefWindowProc for default processing.

The wParam and lParam parameters to the window procedure are not used in
HELLOWIN except as parameters to DefWindowProc. These parameters give the
window additional information about the message. The meaning of the
parameters is message-dependent.

Let's look at an example. Whenever the client area of a window changes in
size, Windows calls that window's window procedure. The hwnd parameter to
the window procedure is the handle of the window changing in size. The
message parameter is WM_SIZE. The wParam parameter for a WM_SIZE message is
the value SIZENORMAL, SIZEICONIC, SIZEFULLSCREEN, SIZEZOOMSHOW, or
SIZEZOOMHIDE (defined in WINDOWS.H as the numbers 0 through 4). The wParam
parameter indicates whether the window is being  minimized, maximized, or
hidden (as a result of another window being maximized). The lParam parameter
contains the new size of the window. The new width (a 16-bit value) and the
new height (a 16-bit value) have been stuck together in the 32-byte lParam.
WINDOWS.H includes macros to help you extract these two values from lParam.
We'll do this in the next chapter.

Sometimes messages generate other messages as a result of DefWindowProc
processing. For example, suppose you run HELLOWIN and select Close from the
system menu using either the keyboard or the mouse. DefWindowProc processes
this keyboard and mouse input. When it detects that you have selected the
Close option, it sends a WM_SYSCOMMAND message to the window procedure.
WndProc passes this message to DefWindowProc. DefWindowProc responds by
sending a WM_CLOSE message to the window procedure. WndProc again passes
this message to DefWindowProc. DefWindowProc responds to the WM_CLOSE
message by calling DestroyWindow. DestroyWindow causes Windows to send a
WM_DESTROY message to the window procedure. WndProc finally responds to this
message by calling PostQuitMessage to put a WM_QUIT message in the message
queue. This message causes the message loop in WinMain to terminate and the
program to end.


Queued and Nonqueued Messages

I've talked about Windows sending messages to a window, which means that
Windows calls the window procedure. But a Windows program also has a message
loop that retrieves messages from a message queue by calling GetMessage and
dispatches them to the window procedure by calling DispatchMessage.

So, does a Windows program poll for messages (exactly as a normal program
polls for keyboard data) and then route these messages to some location? Or
does it receive messages directly from outside the program? Well, both.

Messages can be either "queued" or "nonqueued." The queued messages are
those that are placed in a program's message queue by Windows and retrieved
and dispatched in the message loop. The nonqueued messages are sent to the
window directly when Windows calls the window procedure. The result is that
the window procedure gets all the messages--both queued and nonqueued--for
the window. Structurally, Windows programs are very clean, because they have
one central point of message processing. It is said that queued messages are
posted to a message queue while nonqueued messages are sent to the window
procedure.

The queued messages are primarily those that result from user input in the
form of keystrokes (such as WM_KEYDOWN and WM_KEYUP), characters that result
from keystrokes (WM_CHAR), mouse movement (WM_MOUSEMOVE), and mouse button
clicks (WM_LBUTTONDOWN). Queued messages also include the timer message
(WM_TIMER), the repaint message (WM_PAINT), and the quit message (WM_QUIT).
The nonqueued messages are everything else. In many cases the nonqueued
messages  result from queued messages. When you pass a nonqueued message to
DefWindowProc within the window procedure, Windows often processes the
message by sending the window procedure other messages.

This process is obviously complex, but fortunately most of the complexity is
Windows' problem rather than our program's. From the perspective of the
window procedure, these messages come through in an orderly, synchronized
manner. The window procedure can do something with these messages or ignore
them. For this reason, the window procedure has been called the "ultimate
hook." Messages notify the window procedure of almost everything that
affects the window.

The nonqueued messages often result from calling certain Windows function
calls or by explicitly sending a message by calling SendMessage. (Messages
can also be placed in a message queue by calling PostMessage.)

For example, when WinMain calls CreateWindow, Windows creates the window and
in the process sends the window procedure a WM_CREATE message. When WinMain
calls ShowWindow, Windows sends the window procedure WM_SIZE and
WM_SHOWWINDOW messages. When WinMain calls UpdateWindow, Windows sends the
window procedure a WM_PAINT message.

Messages are not like hardware interrupts. While processing one message in a
window procedure the program will not be interrupted by another message.
Only when the window procedure calls a function that generates a new message
will the message procedure process the message before the function returns.

The message loop and the window procedure do not run concurrently. When the
window procedure is processing a queued message, it is the result of a call
to DispatchMessage in WinMain. DispatchMessage does not return until the
window procedure has processed the message.

But notice that the window procedure must be reentrant. That is, Windows
often calls WndProc with a new message as a result of WndProc calling
DefWindowProc with a previous message. This is one reason that a Windows
program requires a 8-KB stack, as indicated in the module definition (.DEF)
file. In most cases the reentrancy of the window procedure presents no
problem, but you should be aware of it.

In many cases, the window procedure must retain information it obtains in
one message and use it while processing another message. This information
must be saved in variables defined as static in the window procedure or in
global variables.

Of course, you'll get a much better feel for all this in later chapters as
the window procedures are expanded to process more messages.


Nonpreemptive Multitasking

The GetMessage call within the message loop is important for another reason.
Except for some device drivers that must process hardware interrupts (such
as the timer, keyboard, mouse, and serial port), Windows usually treats
HELLOWIN as if it were the only program running under the system. Windows
will not arbitrarily switch away from HELLOWIN and run some other program.
The exception is during the GetMessage call. If HELLOWIN's message queue has
no waiting messages and another program has some messages in its message
queue, then Windows switches from HELLOWIN to the other program. That makes
sense, does it not?

You can think of it this way: In most cases, when your program calls a
function in Windows, you can expect that the function will be processed and
return control to your program within a reasonable period of time. When you
call GetMessage, however, it may be some time before Windows returns with a
message if the program's message queue does not contain any messages and
another program's message queue does. Windows can take advantage of the
delay caused by an empty message queue during a GetMessage call to switch to
another program that has messages waiting. As a result, Windows has a
"jumpy" type of multitasking. Sometimes a program has a long job to do, and
all other programs running under Windows seem to stop running during this
time.

Rather than "jumpy multitasking," this characteristic is usually called
"nonpreemptive multitasking." Windows is multitasking between programs by
switching between them. But Windows is not doing this as it is done within a
traditional multitasking system, based on the tick of a hardware clock and
allocating each program a tiny time-slice to do its stuff. It's multitasking
at the point where programs check the message queue for messages.

The process is actually a little more complex than that: Windows also
switches between programs during PeekMessage and WaitMessage calls, but
these are less common than GetMessage. Furthermore, the WM_PAINT and
WM_TIMER messages are treated as low-priority messages, so Windows can
switch from a program if only WM_PAINT and WM_TIMER messages are present in
the queue.


The Learning Curve

Yes, as you've undoubtedly determined from this chapter, Windows programming
is certainly different from programming for a conventional environment like
MS-DOS. Nobody will claim that Windows programming is easy.

When I first started learning Windows programming, I decided to do what I
had always done when learning a new operating system or a new language--to
write a simple "hex dump" program to display the contents of a file. In the
conventional MS-DOS environment, such a program involves command-line
processing, rudimentary file I/O, and screen output formatting. However, my
Windows hex-dump program turned into a monster. It required that I learn
about menus, dialog boxes, scroll bars, and the like. As a first Windows
program, it was definitely a mistake, demanding that I absorb too much all
at once.

Yet when this program was finished, it was quite unlike any hex-dump program
I had written. Rather than obtain the filename from a command line, WINDUMP
(as I called it) presented a list box showing all the files in the current
directory. Rather than write its output to the screen in a simple teletype
fashion, WINDUMP had scroll bars so I could move to any part of the file. As
an extra bonus, I could even run two copies of WINDUMP to compare two files
side by side. In short, WINDUMP was the first hex-dump program I wrote that
I was actually proud of.

What you have to ask yourself is this: Do I want my programs to use a more
modern and productive user interface, one that includes menus, dialog boxes,
scroll bars, and graphics? If you answer yes, then the question becomes: Do
I want to write all this menu, dialog box, scroll bar, and graphics code
myself? Or would I rather take advantage of all the code already inside
Windows for this? In other words, is it easier to learn how to use 550
function calls or to write them yourself? Is it easier to orient your
programming mind to the message-driven architecture of Windows or struggle
with using several different sources of user input in a traditional model?

If you're going to write your own user interface logic, you had better close
this book and get to work right away. Meanwhile, the rest of us are going to
learn how to display and scroll text in a window.





Chapter 2  Painting with Text
────────────────────────────────────────────────────────────────────────────

In the previous chapter you saw a simple Windows program that displayed a
single line of text in the center of its client area. The client area
occupies all the space of the window that is not taken up by the caption
bar, the window-sizing border, the menu bar (if any), and scroll bars (if
any). The client area is the part of the window on which a program is free
to draw. You can do almost anything you want with that client
area--anything, that is, except assume that it will be a particular size or
that the size will remain constant while your program is running. If you are
accustomed to writing programs for the IBM PC, this exception may come as a
bit of a shock. You can no longer think in terms of 25 lines and 80 columns
of text. Your program shares the video display with other Windows programs.
The user controls how the programs are arranged on the screen. Your program
must accept the size it's given and do something reasonable with it. (A
program could create a window of a specific fixed size, but it isn't very
common.)

This works both ways. Just as your program may find itself with a client
area barely large enough in which to say "Hello," it may also someday be run
on a big-screen high-resolution video system and discover a client area big
enough for two entire pages of text and plenty of closet space besides.
Dealing intelligently with both these eventualities is an important part of
Windows programming.

Although Windows has extensive Graphics Device Interface (GDI) functions for
displaying graphics, in this chapter I'll stick to displaying simple lines
of text. I'll also ignore the various fonts (typefaces) and font sizes that
Windows makes available and use only Windows' default "system font." This
may seem limiting, but it really isn't. The problems  we encounter--and
solve--in this chapter apply to all Windows programming. When you display a
combination of text and graphics (as, for instance, the Windows CALENDAR,
CARDFILE, and CALCULATOR programs do), the character dimensions of Windows'
default system font often determine the dimensions of the graphics.

This chapter is ostensibly about learning how to paint, but it's really
about learning the basics of device-independent programming. Windows
programs can assume little about their environment. Instead, they must use
the facilities that Windows provides to obtain information about the
environment.

PAINTING AND REPAINTING

Under MS-DOS, a program using the display in a full-screen mode can write to
any part of the display. What the program puts on the display will stay
there and will not mysteriously disappear. The program can then discard
information needed to re-create the screen display. If another program (such
as a RAM-resident popup) overlays part of the display, then the popup is
responsible for restoring the display when it leaves.

In Windows, you can display only to the client area of your window, and you
cannot be assured that what you display to the client area will remain there
until your program specifically writes over it. For instance, the dialog box
from another application may overlay part of your client area. Although
Windows will attempt to save and restore the area of the display underneath
the dialog box, it sometimes cannot do so. When the dialog box is removed
from the screen, Windows will request that your program repaint this portion
of your client area.

Windows is a message-driven system. Windows informs applications of various
events by posting messages in the application's message queue or sending
messages to the appropriate window procedure. Windows informs a window
procedure that part of the window's client area needs updating by posting a
WM_PAINT message.

The WM_PAINT Message

Most Windows programs call the function UpdateWindow during initialization
in WinMain shortly before entering the message loop. Windows takes this
opportunity to send the window procedure its first WM_PAINT message. That
message informs your window procedure that the client area is ready to be
painted. Thereafter, that window procedure should be ready at any time to
process additional WM_PAINT messages and even repaint the entire client area
of the window if necessary. A window procedure receives a  WM_PAINT message
whenever one of the following occurs:

  ■   A previously hidden area of the window is brought into view when a
      user moves a window or uncovers a window.

  ■   The user resizes the window (if the window class style has the CS-
      _HREDRAW and CS_VREDRAW bits set).

  ■   The program uses the ScrollWindow function to scroll part of its
      client area.

  ■   The program uses the InvalidateRect or InvalidateRgn function to
      explicitly generate a WM_PAINT message.

In some cases in which part of the client area is temporarily written over,
Windows attempts to save an area of the display and restore it later. This
is not always successful. Windows may sometimes post a WM_PAINT message
when:

  ■   Windows removes a dialog box or message box that was overlaying part
      of the window.

  ■   A menu is pulled down and then released.

In a few cases, Windows always saves the area of the display it overwrites
and then restores it. This is the case whenever:

  ■   The cursor is moved across the client area.

  ■   An icon is dragged across the client area.

Dealing with WM_PAINT messages requires that you alter your thinking about
how you write to the display. Your program should be structured so that it
accumulates all the information necessary to paint the client area but
paints only "on demand"--when Windows sends the window procedure a WM_PAINT
message. If your program needs to update its client area, it can force
Windows to generate this WM_PAINT message. This may seem a roundabout method
of displaying something on the screen, but the structure of your programs
will benefit from it.


Valid and Invalid Rectangles

Although a window procedure should be prepared to update the entire client
area whenever it receives a WM_PAINT message, it often needs to update only
a smaller rectangular area. This is most obvious when part of the client
area is overlaid by a dialog box. Repainting is required only for the
rectangular area uncovered when the dialog box is removed.

That rectangular area is known as an "invalid rectangle." The presence of an
invalid rectangle in a client area is what prompts Windows to place a
WM_PAINT message in the application's message queue. Your window procedure
receives a WM_PAINT message only if part of your client area is invalid.

Windows internally maintains a "paint information structure" for each
window. This structure contains (among other information) the coordinates of
the invalid rectangle. If another rectangular area of the client area
becomes invalid before the window procedure processes the WM_PAINT message,
Windows calculates a new invalid rectangle that encompasses both areas and
stores this updated information in the paint information structure. Windows
does not place multiple WM_PAINT messages in the message queue.

A window procedure can invalidate a rectangle in its own client area by
calling InvalidateRect. If the message queue already contains a WM_PAINT
message, Windows calculates a new invalid rectangle. Otherwise, it places a
WM_PAINT message in the message queue. A window procedure can obtain the
coordinates of the invalid rectangle when it receives a WM_PAINT message (as
we'll see shortly). It can also obtain these coordinates at any other time
by calling GetUpdateRect.

After the window procedure calls EndPaint during the WM_PAINT message, the
entire client area is validated. A program can also validate any rectangular
region in the client area by calling the ValidateRect function. If this call
has the effect of validating the entire invalid area, then any WM_PAINT
message currently in the queue is deleted.



AN INTRODUCTION TO GDI

To paint the client area of your window, you use Windows' Graphics Device
Interface (GDI) functions. (A full discussion of GDI is in Chapters 11\-15.)
Windows provides five GDI functions for writing text strings to the client
area of the window. We've already encountered the DrawText function in
Chapter 1, but the most popular text output function by far is TextOut. This
function has the following format:

TextOut (hdc, x, y, lpsString, nLength) ;

TextOut writes a character string to the display. The lpsString parameter is
a long (or far) pointer to the character string, and nLength is the length
of the string. The x and y parameters define the starting position, in
"logical coordinates," of the character string in the client area. The hdc
parameter is a "handle to a device context," and it is an important part of
GDI. Virtually every GDI function requires this handle as the first
parameter to the function.

The Device Context

A handle, you'll recall, is simply a number that Windows uses for internal
reference to an object. You obtain the handle from Windows and then use the
handle in other functions. The device context handle is your window's
passport to the GDI functions. With that device context handle you are free
to paint your client area and make it as beautiful or as ugly as you like.

The device context (also called the "DC") is really a data structure
maintained by GDI. A device context is associated with a particular display
device, such as a printer, plotter, or video display. For a video display, a
device context is usually associated with a particular window on the
display.

Windows uses the values in the device context structure (also called
"attributes" of the device context) in conjunction with the GDI functions.
With TextOut, for instance, the  attributes of the device context determine
the color of the text, the color of the text background, how the
x-coordinate and y-coordinate are mapped to the client area of the window,
and what font Windows uses when displaying the text.

When a program needs to paint, it must first obtain a handle to a device
context. After it has finished painting, the program should release the
handle. When a program releases the handle, the handle is no longer valid
and must not be used. The program should obtain the handle and release the
handle during processing of a single message. Except for a device context
created with a call to CreateDC, you should not keep a device context handle
around from one message to another.

Windows applications generally use two methods for getting the handle to the
device context in preparation for painting the screen.


Getting a Device Context Handle: Method One

You use this method when you process WM_PAINT messages. Two functions are
involved: BeginPaint and EndPaint. These two functions require the handle to
the window (passed to the window procedure as a parameter) and the address
of a structure variable of type PAINTSTRUCT. Windows programmers usually
name this structure variable ps and define it within the window procedure,
like so:

PAINTSTRUCT ps ;

While processing a WM_PAINT message, a Windows function first calls
BeginPaint to fill in the fields of the ps structure. The value returned
from BeginPaint is the device context handle. This is commonly saved in a
variable named hdc. You define this variable in your window procedure like
this:

HDC hdc ;

The HDC data type is defined in WINDOWS.H as a HANDLE. The program may then
use GDI functions such as TextOut. A call to EndPaint releases the device
context handle and validates the window.

Typically, processing of the WM_PAINT message looks like this:

case WM_PAINT :
     hdc = BeginPaint (hwnd,&ps) ;
[use GDI functions]
     EndPaint (hwnd, &ps) ;
     return 0 ;

The window procedure must call BeginPaint and EndPaint as a pair while
processing the WM_PAINT message. If a window procedure does not process
WM_PAINT messages, then it must pass the WM_PAINT message to DefWindowProc
(the default window procedure) located in Windows.

DefWindowProc processes WM_PAINT messages with the following code:

case WM_PAINT :
     BeginPaint (hwnd, &ps) ;
     EndPaint (hwnd, &ps) ;
     return 0 ;

This sequence of BeginPaint and EndPaint with nothing in between simply
validates the previously invalid rectangle. But don't do this:

case WM_PAINT :
     return 0 ;   // WRONG !!!

Windows places a WM_PAINT message in the message queue because part of the
client area is invalid. Unless you call BeginPaint and EndPaint (or
ValidateRect), Windows will not validate that area. Instead, Windows will
send you another WM_PAINT message. And another, and another, and another...


The Paint Information Structure

Earlier I mentioned a "paint information structure" that Windows maintains
for each window. That's what PAINTSTRUCT is. The structure is defined in
WINDOWS.H as follows:

typedef struct tagPAINTSTRUCT
  {
    HDC       hdc ;
    BOOL      fErase ;
    RECT      rcPaint ;
    BOOL      fRestore ;
    BOOL      fIncUpdate ;
    BYTE      rgbReserved[16] ;
  } PAINTSTRUCT ;

Windows fills in the fields of this structure when your program calls
BeginPaint. Your program may use only the first three fields. The others are
used internally by Windows.

The hdc field is the handle to the device context. In a redundancy typical
of Windows, the value returned from BeginPaint is also this device context
handle.

In most cases, fErase will be flagged TRUE (nonzero), meaning that Windows
has erased the background of the invalid rectangle. Windows erases the
background using the brush specified in the hbrBackground field of the
WNDCLASS structure that you use when registering the window class during
WinMain initialization. Many Windows programs use a white brush:

wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;

However, if your program invalidates a rectangle of the client area by
calling the Windows function InvalidateRect, one of the parameters to this
function specifies whether  you want the background erased. If this
parameter is FALSE (or 0), then Windows will not erase the background, and
the fErase field will also be FALSE.

The rcPaint field of the PAINTSTRUCT structure is a structure of type RECT.
As you learned in Chapter 1, the RECT structure defines a rectangle. The
four fields are left, top, right, and bottom. The rcPaint field in the
PAINTSTRUCT structure defines the boundaries of the invalid rectangle, as
shown in Figure 2-1. The values are in units of pixels relative to the upper
left corner of the client area. The invalid rectangle is the area that you
should repaint. Although a Windows program can simply repaint the entire
client area of the window whenever it receives a WM_PAINT message,
repainting only the area of the window defined by that rectangle saves time.

  (Figure 2-1. may be found in the printed book.)

The rcPaint rectangle in PAINTSTRUCT is not only the invalid rectangle; it
is also a  "clipping" rectangle. This means that Windows restricts painting
to within the clipping rectangle. When you use the device context handle
from the PAINTSTRUCT structure, Windows will not paint outside the rcPaint
rectangle.

To paint outside this rcPaint rectangle while processing WM_PAINT messages,
you can make this call:

InvalidateRect (hWnd, NULL, TRUE) ;

before calling BeginPaint. This invalidates the entire client area and
erases the background. A FALSE value in the last parameter will not erase
the background, however. Whatever was there will stay.

In the HELLOWIN program in Chapter 1, we didn't care about invalid
rectangles or clipping rectangles when processing the WM_PAINT message. If
the area where the text was displayed happened to be within the invalid
rectangle, then DrawText restored it. If not, then at some point during
processing of the DrawText call, Windows determined it didn't have to write
anything to the display. But this determination takes time. A programmer
concerned about performance and speed will want to use the invalid-rectangle
dimensions during processing of WM_PAINT to avoid unnecessary GDI calls.


Getting a Device Context Handle: Method Two

You can also obtain a handle to a device context if you want to paint the
client area when processing messages other then WM_PAINT or if you need the
device context handle for other purposes, such as obtaining information
about the device context. Call GetDC to obtain the handle to the device
context, and ReleaseDC after you're done with it:

hdc = GetDC (hwnd) ;
[use GDI functions]
ReleaseDC (hwnd, hdc) ;

Like BeginPaint and EndPaint, the GetDC and ReleaseDC functions should be
called in pairs. When you call GetDC while processing a message, you should
call ReleaseDC before you exit the window procedure. Do not call GetDC in
response to one message and ReleaseDC in response to another.

Unlike the device context handle obtained from the PAINTSTRUCT structure,
the device context handle returned from GetDC has a clipping rectangle equal
to the entire client area. You can paint on any part of the client area, not
merely on the invalid rectangle (if indeed there is an invalid rectangle).
Unlike EndPaint, ReleaseDC does not validate any invalid rectangles.


TextOut: The Details

When you obtain the handle to the device context, Windows fills the device
context structure with default values. As you'll see in later chapters, you
can change these defaults with GDI functions. The GDI function we're
interested in right now is TextOut:

TextOut (hdc, x, y, lpsString, nLength) ;

Let's examine this function in more detail.

The first parameter is the handle to the device context--either the hdc
value returned from GetDC or the hdc value returned from BeginPaint during
processing of a WM_PAINT message.

The attributes of the device context control the characteristics of this
displayed text. For instance, one attribute of the device context specifies
the text color. The default color is black. The default device context also
defines a background color of white. When a program writes text to the
display, Windows uses this background color to fill in the space surrounding
the characters.

This text background color is not the same background you set when defining
the window class. The background in the window class is a brush--which is a
pattern that may or may not be a pure color--that Windows uses to erase the
client area. It is not part of the device context structure. When defining
the window class structure, most Windows applications use WHITE_BRUSH so
that the background color in the default device context is the same color as
the brush Windows uses to erase the background of the client area.

The lpsString parameter is a long pointer to a character string, and nLength
is the length of the string. The string should not contain any ASCII control
characters such as carriage returns, linefeeds, tabs, or backspaces. Windows
displays these control characters as solid blocks. TextOut does not
recognize a 0 as denoting the end of the string and requires the nLength
parameter for the length.

The x and y values in TextOut define the starting point of the character
string within the client area. The x value is the horizontal position; the y
value is the vertical position. The upper left corner of the first character
in the string is positioned at x and y. In the default device context, the
origin (the point where x and y both equal 0) is the upper left corner of
the client area. If you use 0 values for x and y in TextOut, the character
string starts flush against the upper left corner of the client area.

GDI coordinates are "logical coordinates." Windows has a variety of "mapping
modes" that govern how the logical coordinates specified in GDI functions
are translated to the physical pixel coordinates of the display. The mapping
mode is defined in the device context. The default mapping mode is called
MM_TEXT (using the WINDOWS.H identifier). Under the MM_TEXT mapping mode,
logical units are the same as physical units, which are pixels. Values of x
increase as you move to the right in the client area and values of y
increase as you move down in the client area. (See Figure 2-2 on the
following page.) The MM_TEXT coordinate system is identical to the
coordinate system that Windows uses to define the invalid rectangle in the
PAINTSTRUCT structure. Very convenient. (This is not the case with other
mapping modes, however.)

The device context also defines a clipping region. As you've seen, the
default clipping region is the entire client area for a device context
handle obtained from GetDC and the invalid rectangle for the device context
handle obtained from BeginPaint. Windows will not display any part of the
character string that lies outside the clipping rectangle. If a character is
partly within the clipping rectangle, Windows displays only the portion of
the character inside the rectangle. Writing outside the client area of your
window isn't easy to do, so don't worry about doing it inadvertently.

  (Figure 2-2. may be found in the printed book.)


The System Font

The device context also defines the font that Windows uses when writing text
to the client area. The default is a font called the "system font" or (using
the WINDOWS.H identifier) SYSTEM_FONT. The system font is the font that
Windows uses for text in caption bars, menus, and dialog boxes.

Under Windows 3, the system font is a variable-width font, which means that
different characters have different widths. A "W" is wider than an "i." In
earlier versions of Windows, the system font was a fixed-pitch font in which
all the characters had the same width.

The system font is a "raster font," which means that the characters are
defined as blocks of pixels. The floppy disks for the Windows installation
include several system fonts in various sizes for use with different video
display adapters.

When manufacturers of a new video board develop a new Windows display
driver, they are also responsible for developing a new system font
appropriate for the resolution of the display. Alternatively, the
manufacturer might specify that one of the system font files supplied with
the retail version of Windows be used. The system font must be designed so
that at least 25 lines of 80-character text can fit on the display. That is
the only guarantee you have about the relationship between screen size and
font size in Windows.


The Size of a Character

To display multiple lines of text using the TextOut function, you need to
determine the dimensions of font characters. You can space successive lines
of text based on the height of a character, and you can space columns of
text across the client area based on the width of a character.

You can obtain character dimensions with the GetTextMetrics call.
GetTextMetrics requires a handle to the device context because it returns
information about the font currently selected in the device context. Windows
copies the various values of text metrics into a structure of type
TEXTMETRIC. The values are in units that depend on the mapping mode selected
in the device context. In the default device context, this mapping mode is
MM_TEXT, so the dimensions are in units of pixels.

To use the GetTextMetrics function, you first need to define a structure
variable (commonly called tm):

TEXTMETRIC tm ;

Next, get a handle to the device context and call GetTextMetrics:

hdc = GetDC (hwnd) ;
GetTextMetrics (hdc, &tm) ;

After you examine the values in the text metric structure (and probably save
a few of them for future use), you release the device context:

ReleaseDC (hwnd, hdc) ;


Text Metrics: The Details

The TEXTMETRIC structure provides a wealth of information about the current
font selected in the device context. However, the vertical size of a font is
defined by only five values, as shown in Figure 2-3 on the following page.

These are fairly self-explanatory. The tmInternalLeading value is the amount
of space allowed for an accent mark above a character. If the value is set
to 0, accented capital letters are made a little shorter so that the accent
fits within the ascent of the character. The tmExternalLeading value is the
amount of space that the designer of the font is suggesting be added between
character rows. You can accept or reject the font designer's suggestion for
including external leading when spacing lines of text.

The TEXTMETRIC structure has two fields that describe character width:
tmAveCharWidth (a weighted average width of lowercase characters) and
tmMaxCharWidth (the width of the widest character in the font). For a
fixed-pitch font, these two values are the same.

The sample programs in this chapter will require another character
width--the average width of uppercase letters. This can be calculated as
150% of tmAveCharWidth.

  (Figure 2-3. may be found in the printed book.)

It's important to realize that the dimensions of the system font are
dependent on the resolution of the video display on which Windows runs.
Windows provides a device- independent graphics interface, but you have to
help. Don't write your Windows program so that it guesses at character
dimensions. Don't hard code any values. Use the GetTextMetrics function to
obtain this information.


Formatting Text

Because the dimensions of the system font do not change during a Windows
session, you need to call GetTextMetrics only once when your program
executes. A good place to make this call is while processing the WM_CREATE
message in the window procedure. The WM_CREATE message is the first message
the window procedure receives. Windows calls your window procedure with a
WM_CREATE message when you call CreateWindow in WinMain.

Suppose you're writing a Windows program that displays several lines of text
running down the client area. You'll want to obtain values for the character
width and height. Within the window procedure you can define two variables
to save the average character width (cxChar) and the total height (cyChar):

static short cxChar, cyChar ;

The prefix c added to the variable names stands for "count," and in
combination with x or y refers to a width or a height. These variables are
defined as static because they must be valid when the window procedure
processes other messages (such as WM_PAINT). If the variables are defined
outside any functions, they need not be defined as static.

Here's the WM_CREATE code:

case WM_CREATE :
     hdc = GetDC  (hwnd) ;

     GetTextMetrics (hdc, &tm) ;
     cxChar = tm.tmAveCharWidth ;
     cyChar = tm.tmHeight + tm.tmExternalLeading ;

     ReleaseDC (hwnd, hdc) ;
     return 0 ;

If you do not want to include external leading to space lines of text, you
can use:

cyChar = tm.tmHeight ;

How you use this character size to calculate display coordinates is up to
you. A simple method is to leave a cyChar margin at the top of the client
area and a cxChar margin at the left. To display several lines of
left-justified text, use the following x-coordinate values when calling the
TextOut function:

cxChar

The y-coordinate values in TextOut are:

cyChar * (1 + i)

where i is the line number starting at 0.

You'll often find it necessary to display formatted numbers as well as
simple character strings. If you were programming in MS-DOS using standard C
library functions, you would probably use printf for this formatting. You
cannot use printf in Windows, because printf writes to the standard output
device, and that concept makes no sense under Windows.

Instead, you can use sprintf. The sprintf function works just like printf
except that it puts the formatted string into a character array. You can
then use TextOut to write the string to the display. Very conveniently, the
value returned from sprintf is the length of  the string--you can pass this
value to TextOut as the nLength parameter. This code shows a typical sprintf
and TextOut combination:

short nLength ;
char  szBuffer [40] ;
[other program lines]
nLength = sprintf (szBuffer, "The sum of %d and %d is %d",
                    nA, nB, nA + nB) ;
TextOut (hdc, x, y, szBuffer, nLength) ;

For something as simple as this you could dispense with the nLength
definition and combine the two statements into one:

TextOut (hdc, x, y, szBuffer,
     sprintf (szBuffer, "The sum of %d and %d is %d",
                    nA, nB, nA + nB)) ;

It's not pretty, but it works.

If you don't need to display floating-point numbers, you can use wsprintf
rather than sprintf. The wsprintf function has the same syntax as sprintf,
but it's included in Windows, so using it won't increase the size of your
.EXE file.


Putting It All Together

Now we seem to have everything we need to write a simple program that
displays multiple lines of text on the screen. We know how to get a handle
to a device context, how to use the TextOut function, and how to space text
based on the size of a single character. The only thing left to do is to
display something interesting.

The information available in the Windows GetSystemMetrics call looks
interesting enough. This function returns information about the size of
various graphical items in Windows, such as icons, cursors, caption bars,
and scroll bars. These sizes vary with the display adapter and driver.
GetSystemMetrics requires a single parameter called an "index." This index
is 1 of 37 integer identifiers defined in WINDOWS.H. GetSystemMetrics
returns an integer, usually the size of the item specified in the parameter.

Let's write a program that displays all the information available from the
GetSystemMetrics call in a simple one-line-per-item format. Working with
this information is easier if we create a header file that defines an array
of structures containing both the WINDOWS.H identifiers for the
GetSystemMetrics index and the text we want to display for each value
returned from the call. This header file is called SYSMETS.H and is shown in
Figure 2-4.

 SYSMETS.H

/*-----------------------------------------------
   SYSMETS.H -- System metrics display structure
  -----------------------------------------------*/

#define NUMLINES (sizeof sysmetrics / sizeof sysmetrics [0])

struct
     {
     int  nIndex ;
     char *szLabel ;
     char *szDesc ;
     }
     sysmetrics [] =
     {
     SM_CXSCREEN,      "SM_CXSCREEN",      "Screen width in pixels",
     SM_CYSCREEN,      "SM_CYSCREEN",      "Screen height in pixels",
     SM_CXVSCROLL,     "SM_CXVSCROLL",     "Vertical scroll arrow width",
     SM_CYHSCROLL,     "SM_CYHSCROLL",     "Horizontal scroll arrow height",
     SM_CYCAPTION,     "SM_CYCAPTION",     "Caption bar height",
     SM_CXBORDER,      "SM_CXBORDER",      "Border width",
     SM_CYBORDER,      "SM_CYBORDER",      "Border height",
     SM_CXDLGFRAME,    "SM_CXDLGFRAME",    "Dialog window frame width",
     SM_CYDLGFRAME,    "SM_CYDLGFRAME",    "Dialog window frame height",
     SM_CYVTHUMB,      "SM_CYVTHUMB",      "Vertical scroll thumb height",
     SM_CXHTHUMB,      "SM_CXHTHUMB",      "Horizontal scroll thumb width",
     SM_CXICON,        "SM_CXICON",        "Icon width",
     SM_CYICON,        "SM_CYICON",        "Icon height",
     SM_CXCURSOR,      "SM_CXCURSOR",      "Cursor width",
     SM_CYCURSOR,      "SM_CYCURSOR",      "Cursor height",
     SM_CYMENU,        "SM_CYMENU",        "Menu bar height",
     SM_CXFULLSCREEN,  "SM_CXFULLSCREEN",  "Full-screen client window
width",
     SM_CYFULLSCREEN,  "SM_CYFULLSCREEN",  "Full-screen client window
height",
     SM_CYKANJIWINDOW, "SM_CYKANJIWINDOW", "Kanji window height",
     SM_MOUSEPRESENT,  "SM_MOUSEPRESENT",  "Mouse present flag",
     SM_CYVSCROLL,     "SM_CYVSCROLL",     "Vertical scroll arrow height",
     SM_CXHSCROLL,     "SM_CXHSCROLL",     "Horizontal scroll arrow width",
     SM_DEBUG,         "SM_DEBUG",         "Debug version flag",
     SM_SWAPBUTTON,    "SM_SWAPBUTTON",    "Mouse buttons swapped flag",
     SM_RESERVED1,     "SM_RESERVED1",     "Reserved",
     SM_RESERVED2,     "SM_RESERVED2",     "Reserved",
     SM_RESERVED3,     "SM_RESERVED3",     "Reserved",
     SM_RESERVED4,     "SM_RESERVED4",     "Reserved",
     SM_CXMIN,         "SM_CXMIN",         "Minimum window width",
     SM_CYMIN,         "SM_CYMIN",         "Minimum window height",
     SM_CXSIZE,        "SM_CXSIZE",        "Minimize/Maximize icon width",
     SM_CYSIZE,        "SM_CYSIZE",        "Minimize/Maximize icon height",



     SM_CXFRAME,       "SM_CXFRAME",       "Window frame width",
     SM_CYFRAME,       "SM_CYFRAME",       "Window frame height",
     SM_CXMINTRACK,    "SM_CXMINTRACK",    "Minimum tracking width of
window",
     SM_CYMINTRACK,    "SM_CYMINTRACK",    "Minimum tracking height of
window",
     SM_CMETRICS,      "SM_CMETRICS",      "Number of system metrics"
     } ;

The program that displays this information is called SYSMETS1. The files
required to create SYSMETS1.EXE (make file, C source code, and module
definition file) are shown in Figure 2-5. Most of the code should look
familiar by now. With the exception of the program name, the make file,
resource script, and DEF file are identical to those for HELLOWIN. In
SYSMETS1.C, WinMain is virtually identical to HELLOWIN.

 SYSMETS1.MAK

#------------------------
# SYSMETS1.MAK make file
#------------------------

sysmets1.exe : sysmets1.obj sysmets1.def
     link sysmets1, /align:16, NUL, /nod slibcew libw, sysmets1
     rc sysmets1.exe

sysmets1.obj : sysmets1.c sysmets.h
     cl -c -Gsw -Ow -W2 -Zp sysmets1.c

 SYSMETS1.C

/*----------------------------------------------------
   SYSMETS1.C -- System Metrics Display Program No. 1
                 (c) Charles Petzold, 1990
  ----------------------------------------------------*/

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

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)




     {
     static char szAppName[] = "SysMets1" ;
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "Get System Metrics No. 1",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static short cxChar, cxCaps, cyChar ;
     char         szBuffer[10] ;
     HDC          hdc ;
     short        i ;
     PAINTSTRUCT  ps ;
     TEXTMETRIC   tm ;
     switch (message)
          {
          case WM_CREATE :
               hdc = GetDC (hwnd) ;

               GetTextMetrics (hdc, &tm) ;
               cxChar = tm.tmAveCharWidth ;
               cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ;
               cyChar = tm.tmHeight + tm.tmExternalLeading ;

               ReleaseDC (hwnd, hdc) ;
               return 0 ;

          case WM_PAINT :
               hdc = BeginPaint (hwnd, &ps) ;

               for (i = 0 ; i < NUMLINES ; i++)
                    {
                    TextOut (hdc, cxChar, cyChar * (1 + i),
                             sysmetrics[i].szLabel,
                             lstrlen (sysmetrics[i].szLabel)) ;

                    TextOut (hdc, cxChar + 18 * cxCaps, cyChar * (1 + i),
                             sysmetrics[i].szDesc,
                             lstrlen (sysmetrics[i].szDesc)) ;

                    SetTextAlign (hdc, TA_RIGHT | TA_TOP) ;

                    TextOut (hdc, cxChar + 18 * cxCaps + 40 * cxChar,
                             cyChar * (1 + i), szBuffer,
                             wsprintf (szBuffer, "%5d",
                                  GetSystemMetrics (sysmetrics[i].nIndex)))
;

                    SetTextAlign (hdc, TA_LEFT | TA_TOP) ;
                    }

               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }

     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 SYSMETS1.DEF

;-------------------------------------
; SYSMETS1.DEF module definition file
;-------------------------------------

NAME           SYSMETS1

DESCRIPTION    'System Metrics Display No. 1 (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

Figure 2-6 shows SYSMETS1 running on a VGA. As you can see from the
program's window, the screen width is 640 pixels and the screen height is
480 pixels. These two values, as well as many of the other values shown by
the program, will be different for different types of video displays.

  (Figure 2-6. may be found in the printed book.)


The SYSMETS1.C Window Procedure

The WndProc window procedure in the SYSMETS1.C program processes three
messages: WM_CREATE, WM_PAINT, and WM_DESTROY. The WM_DESTROY message is
processed in the same way as the HELLOWIN program in Chapter 1.

The WM_CREATE message is the first message the window procedure receives. It
is generated by Windows when the CreateWindow function creates the window.
During the WM_CREATE message, SYSMETS1 obtains a device context for the
window by calling GetDC, and gets the text metrics for the default system
font by calling GetTextMetrics. SYSMETS1 saves the average character width
in cxChar and the total height of the characters including external leading
in cyChar.

SYSMETS1 also saves an average width of uppercase letters in the static
variable cxCaps. For a fixed-pitch font, cxCaps would equal cxChar. For a
variable-width font, cxCaps is about 150% of cxChar. The low bit of the
tmPitchAndFamily field of the TEXTMETRIC structure is 1 for a variable-width
font and 0 for a fixed-pitch font. SYSMETS1 uses this bit value to calculate
cxCaps from cxChar:

cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ;

SYSMETS1 does all window painting during the WM_PAINT message. As normal,
the window procedure first obtains a handle to the device context by calling
BeginPaint. A for statement loops through all the lines of the sysmetrics
structure defined in SYSMETS.H. The three columns of text are displayed with
three TextOut functions. In each case, the third parameter to TextOut is set
to:

cyChar * (1 + i)

This parameter indicates the pixel position of the top of the character
string relative to the top of the client area. Thus, the program leaves a
margin at the top equal to cyChar. The first line of text (when i equals 0)
begins cyChar pixels below the top of the client area.

The first TextOut statement displays the uppercase identifiers in the first
of the three columns. The second parameter to TextOut is cxChar. This leaves
a one-character margin between the left edge of the client area and the text
string. The text is obtained from the szLabel field of the sysmetrics
structure. I use the Windows function lstrlen (which is similar to strlen)
to obtain the length of the string, which is required as the last parameter
to TextOut.

The second TextOut statement displays the description of the system metrics
value. These descriptions are stored in the szDesc field of the sysmetrics
structure. In this case, the second parameter to TextOut is set to:

cxChar + 18 * cxCaps

The longest uppercase identifier displayed in the first column is 16
characters, so the second column must begin at least 16 x cxCaps to the
right of the beginning of the first column of text.

The third TextOut statement displays the numeric values obtained from the
GetSystemMetrics function. The variable-width font makes formatting a column
of right-justified numbers a little tricky. All the digits from 0 through 9
have the same width, but this width is greater than the width of a space.
Numbers can be one or more digits wide, so different numbers can begin at
different horizontal positions.

Wouldn't it be easier if we could display a column of right-justified
numbers by specifying the pixel position where the number ends rather than
where it begins? This is what the SetTextAlign function lets us do. After
SYSMETS1 calls

SetTextAlign (hdc, TA_RIGHT | TA_TOP) ;

then the coordinates passed to subsequent TextOut functions will specify the
top-right corner of the text string rather than the top-left corner.

The TextOut function to display the column of numbers has a second parameter
set to:

cxChar + 18 * cxCaps + 40 * cxChar

The 40 x cxChar value accommodates the width of the second column and the
width of the third column. Following the TextOut function, another call to
SetTextAlign sets things back to normal for the next time through the loop.


Not Enough Room!

One little nasty problem exists with the SYSMETS1 program: Unless you have a
big-screen high-resolution video adapter, you can't see the last few lines
of the system metrics list. If you make the window narrower, you can't see
even the values.

SYSMETS1 doesn't know how large its client area is. It begins the text at
the top of the window and relies on Windows to clip everything that drifts
beyond the edges of the client area. Our first job is to determine how much
of the program's output can actually fit within the client area.


The Size of the Client Area

If you experiment with existing Windows applications, you'll find that
window sizes can vary widely. At the most (assuming the window does not have
a menu or scroll bars), the window can be maximized, and the client area
will occupy the entire screen except for the caption bar. The minimum size
of the window can be quite small, sometimes almost nonexistent, eliminating
the client area.

One common method for determining the size of a window's client area is to
process the WM_SIZE message within your window procedure. Windows sends a
WM_SIZE message to a window procedure whenever the size of the window
changes. The lParam variable passed to the window procedure contains the
width of the client area in the low word and the height in the high word.
The code to process this message looks like this:

static short cxClient, cyClient ;
[other program lines]
case WM_SIZE :
     cxClient = LOWORD (lParam) ;
     cyClient = HIWORD (lParam) ;
     break ;

The LOWORD and HIWORD macros are defined in WINDOWS.H. Like cxChar and
cyChar, the cxClient and cyClient variables are defined as static inside the
window procedure because they are used later when processing other messages.

The WM_SIZE message will eventually be followed by a WM_PAINT message. Why?
Because when we define the window class, we specify that the class style is:

CS_HREDRAW | CS_VREDRAW

This class style tells Windows to force a repaint if either the horizontal
or vertical size changes.

You can calculate the number of full lines of text displayable within the
client area with the formula:

cyClient / cyChar

This may be 0 if the height of the client area is too small to display a
full character. Similarly, the approximate number of lowercase characters
you can display horizontally within the client area is equal to:

cxClient / cxChar

If you determine cxChar and cyChar during a WM_CREATE message, don't worry
about dividing by 0 in these calculations. Your window procedure receives a
WM_CREATE message when WinMain calls CreateWindow. The first WM_SIZE message
comes a little later when WinMain calls ShowWindow, at which point cxChar
and cyChar have already been assigned positive values.

Knowing the size of the window's client area is the first step in providing
a way for the user to move the text within the client area if the client
area is not large enough to hold everything. If you're familiar with other
Windows applications that have similar requirements, you probably know what
we need: This is a job for scroll bars.



SCROLL BARS

Scroll bars are one of the best features of a graphics and mouse interface.
They are easy to use and provide good visual feedback. You can use scroll
bars whenever you need to display anything--text, graphics, a spreadsheet,
database records, pictures--that requires more space than the available
client area of the window.

Scroll bars are positioned either vertically (for up and down movement) or
horizontally (for left and right movement). You can click with the mouse on
the arrows at each end of a scroll bar or on the area between the arrows. A
"scroll box," or "thumb," travels the length of the scroll bar to indicate
the approximate location of the material shown on the display in relation to
the entire document. You can also drag the thumb with the mouse to move to a
particular location. Figure 2-7 shows the recommended use of a vertical
scroll bar for text.

Programmers sometimes have problems with scrolling terminology because their
perspective is different from the user's: A user who scrolls down wants to
bring a lower part of the document into view. However, the program actually
moves the document up in relation to the display window. The Windows
documentation and the WINDOWS.H identifiers are based on the user's
perspective: Scrolling up means moving toward the beginning of the document;
scrolling down means moving toward the end.

It is very easy to include a horizontal or vertical scroll bar in your
application window. All you need to do is include the identifier WS_VSCROLL
(vertical scroll) or WS_HSCROLL (horizontal scroll) or both to the window
style in the CreateWindow statement. These scroll bars are always placed
against the right side or bottom of the window and extend for the full
length or width of the client area. The client area does not include the
space occupied by the scroll bar. The width of a vertical window scroll bar
and the height of a horizontal window scroll bar are constant for a
particular display driver. If you need these values, you can obtain them (as
you may have observed) from the GetSystemMetrics call.

Windows takes care of all mouse logic for the scroll bars. However, window
scroll bars do not have an automatic keyboard interface. If you want the
cursor keys to duplicate some of the window scroll bars' functions, you must
explicitly provide logic for that (as we'll do in the next chapter).

Scroll Bar Range and Position

Scroll bars have a "range" and a current "position." The range is defined by
minimum and maximum integer values. When the thumb is at the top (or left)
of the scroll bar, the position of the thumb is the minimum value of the
range. At the bottom (or right) of the scroll bar, the thumb position is the
maximum value of the range.

The position of the thumb is always a discrete integral value. For instance,
a scroll bar with a range from 0 through 4 has five thumb positions, as
shown in Figure 2-8. By default, the range of a scroll bar is 0 (top or
left) through 100 (bottom or right), but it's easy to change the range to
something that is more convenient for the program:

SetScrollRange (hwnd, nBar, nMin, nMax, bRedraw) ;

The nBar parameter is either SB_VERT or SB_HORZ, nMin and nMax are the
minimum and maximum positions of the range, and bRedraw is set to TRUE if
you want Windows to redraw the scroll bar based on the new range.

You can use SetScrollPos to set a new thumb position within the range:

SetScrollPos (hwnd, nBar, nPos, nRedraw) ;

The nPos parameter is the new position and must be within the range of nMin
through nMax. Windows provides similar functions (GetScrollRange and
GetScrollPos) to obtain the current range and position of a scroll bar.

When you use scroll bars within your program, you share responsibility with
Windows for maintaining the scroll bars and updating the position of the
scroll bar thumb. These are Windows' responsibilities for scroll bars:

  ■   Handle all scroll bar mouse logic

  ■   Provide a "reverse video" flash when the user clicks on the scroll bar

  ■   Display a "ghost" box when the user drags the thumb within the scroll
      bar

  ■   Send scroll bar messages to the window procedure for the window
      containing the scroll bar

These are your program's responsibilities:

  ■   Initialize the range of the scroll bar

  ■   Process the scroll bar messages

  ■   Update the position of the scroll bar thumb

        (Figure 2-8. may be found in the printed book.)


Scroll Bar Messages

Windows sends the window procedure WM_VSCROLL and WM_HSCROLL messages when
the scroll bar is clicked with the mouse or the thumb is dragged. Each mouse
action on the scroll bar generates at least two messages, one when the mouse
button is pressed and another when it is released.

The value of wParam that accompanies the WM_VSCROLL and WM_HSCROLL messages
describes what the mouse is doing to the scroll bar. These values of wParam
have WINDOWS.H identifiers that begin with SB, which stands for "scroll
bar." Although some of these identifiers use the words "UP" and "DOWN," they
apply to horizontal as well as vertical scroll bars, as you see in Figure
2-9. Your window procedure can receive multiple SB_LINEUP, SB_PAGEUP,
SB_LINEDOWN, or SB_PAGEDOWN messages if the mouse button is held down while
positioned on the scroll bar. The SB_ENDSCROLL message signals that the
mouse button has been released. You can generally ignore SB_ENDSCROLL
messages.

When wParam is SB_THUMBTRACK or SB_THUMBPOSITION, the low word of lParam is
the current position of the dragged scroll bar. This position is within the
minimum and maximum values of the scroll bar range. For other values of
wParam, the low word of lParam should be ignored. You can also ignore the
high word of lParam.

The Windows documentation indicates that the wParam value can also be SB_TOP
and SB_BOTTOM, indicating that the scroll bar has been moved to its minimum
or maximum position. However, you will never receive these values for a
scroll bar created as part of your application window.

Handling the SB_THUMBTRACK and SB_THUMBPOSITION messages is problematic. If
you set a large scroll bar range and the user quickly drags the thumb inside
the scroll bar, Windows sends your window function a barrage of
SB_THUMBTRACK messages.

  (Figure 2-9. may be found in the printed book.)

Your program may have problems keeping up with these messages. For this
reason, most Windows applications ignore these messages and take action only
on receipt of SB_THUMBPOSITION, which means that the thumb is again at rest.

However, if you can update your display quickly, you may want to include
SB_THUMBTRACK processing in your program. But be aware that users who
discover that your program scrolls as they move the scroll bar thumb will
undoubtedly try to move it as quickly as possible to see if your program can
keep up. They will get an inordinate amount of satisfaction if it cannot.


Scrolling SYSMETS

Enough explanation. It's time to put this stuff into practice. But let's
start simply. We'll begin with vertical scrolling because that's what we
desperately need. The horizontal scrolling can wait. SYSMETS2 is shown in
Figure 2-10.

The new CreateWindow call adds a vertical scroll bar to the window; the
scroll bar has this window style:

WS_OVERLAPPEDWINDOW | WS_VSCROLL

 SYSMETS2.MAK

#------------------------
# SYSMETS2.MAK make file
#------------------------

sysmets2.exe : sysmets2.obj sysmets2.def
     link sysmets2, /align:16, NUL, /nod slibcew libw, sysmets2
     rc sysmets2.exe

sysmets2.obj : sysmets2.c sysmets.h
     cl -c -Gsw -Ow -W2 -Zp sysmets2.c

 SYSMETS2.C

/*----------------------------------------------------
   SYSMETS2.C -- System Metrics Display Program No. 2
                 (c) Charles Petzold, 1990
  ----------------------------------------------------*/

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

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;



int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName[] = "SysMets2" ;
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "Get System Metrics No. 2",
                          WS_OVERLAPPEDWINDOW | WS_VSCROLL,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static short cxChar, cxCaps, cyChar, cxClient, cyClient, nVscrollPos ;
     char         szBuffer[10] ;
     HDC          hdc ;
     short        i, y ;
     PAINTSTRUCT  ps ;
     TEXTMETRIC   tm ;
     switch (message)
          {
          case WM_CREATE :
               hdc = GetDC (hwnd) ;

               GetTextMetrics (hdc, &tm) ;
               cxChar = tm.tmAveCharWidth ;
               cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ;
               cyChar = tm.tmHeight + tm.tmExternalLeading ;

               ReleaseDC (hwnd, hdc) ;

               SetScrollRange (hwnd, SB_VERT, 0, NUMLINES, FALSE) ;
               SetScrollPos   (hwnd, SB_VERT, nVscrollPos, TRUE) ;
               return 0 ;

          case WM_SIZE :
               cyClient = HIWORD (lParam) ;
               cxClient = LOWORD (lParam) ;
               return 0 ;

          case WM_VSCROLL :
               switch (wParam)
                    {
                    case SB_LINEUP :
                         nVscrollPos -= 1 ;
                         break ;

                    case SB_LINEDOWN :
                         nVscrollPos += 1 ;
                         break ;

                    case SB_PAGEUP :
                         nVscrollPos -= cyClient / cyChar ;
                         break ;

                    case SB_PAGEDOWN :
                         nVscrollPos += cyClient / cyChar ;
                         break ;

                    case SB_THUMBPOSITION :
                         nVscrollPos = LOWORD (lParam) ;
                         break ;

                    default :
                         break ;
                    }
               nVscrollPos = max (0, min (nVscrollPos, NUMLINES)) ;

               if (nVscrollPos != GetScrollPos (hwnd, SB_VERT))
                    {
                    SetScrollPos (hwnd, SB_VERT, nVscrollPos, TRUE) ;
                    InvalidateRect (hwnd, NULL, TRUE) ;
                    }
               return 0 ;

          case WM_PAINT :
               hdc = BeginPaint (hwnd, &ps) ;

               for (i = 0 ; i < NUMLINES ; i++)
                    {
                    y = cyChar * (1 - nVscrollPos + i) ;

                    TextOut (hdc, cxChar, y,
                             sysmetrics[i].szLabel,
                             lstrlen (sysmetrics[i].szLabel)) ;

                    TextOut (hdc, cxChar + 18 * cxCaps, y,
                             sysmetrics[i].szDesc,
                             lstrlen (sysmetrics[i].szDesc)) ;

                    SetTextAlign (hdc, TA_RIGHT | TA_TOP) ;

                    TextOut (hdc, cxChar + 18 * cxCaps + 40 * cxChar, y,
                             szBuffer,
                             wsprintf (szBuffer, "%5d",
                                  GetSystemMetrics (sysmetrics[i].nIndex)))
;

                    SetTextAlign (hdc, TA_LEFT | TA_TOP) ;
                    }

               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }

     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 SYSMET2.DEF

;-------------------------------------
; SYSMETS2.DEF module definition file
;-------------------------------------

NAME           SYSMETS2

DESCRIPTION    'System Metrics Display No. 2 (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

The WndProc window procedure has two additional lines to set the range and
position of the vertical scroll bar during processing of the WM_CREATE
message:

SetScrollRange (hwnd, SB_VERT, 0, NUMLINES, FALSE) ;
SetScrollPos   (hwnd, SB_VERT, nVscrollPos, TRUE) ;

The sysmetrics structure has NUMLINES lines of text, so the scroll bar range
is set from 0 through NUMLINES. Each position of the scroll bar corresponds
to a line of text displayed at the top of the client area. If the scroll bar
thumb is at position 0, a blank line is left at the top of the screen for a
margin. As you increase the position of the scroll bar by scrolling down,
the text should move up. When the scroll bar position is at the bottom, the
last line of the structure is at the top.

To help with processing of the WM_VSCROLL messages, a static variable called
nVscrollPos is defined within the WndProc window procedure. This variable is
the current position of the scroll bar thumb. For SB_LINEUP and SB_LINEDOWN,
all we need to do is adjust the scroll position by 1. For SB_PAGEUP and
SB_PAGEDOWN, we want to move the text by the contents of one screen, or
cyClient divided by cyChar. For SB_THUMBPOSITION, the new thumb position is
the low word of lParam. SB_ENDSCROLL and SB_THUMBTRACK messages are ignored.

The nVscrollPos is then adjusted using the min and max macros (defined in
WINDOWS.H) to ensure that it is between the minimum and maximum range
values. If the scroll position has changed, then it is updated using
SetScrollPos, and the entire window is invalidated by an InvalidateRect
call.

The InvalidateRect call generates a WM_PAINT message. When the original
SYSMETS1 processed WM_PAINT messages, the y-coordinate of each line was
calculated as:

cyChar * (1 + i)

In SYSMETS2, the formula is:

cyChar * (1 - nVscrollPos + i)

The loop still displays NUMLINES lines of text, but for values of
nVscrollPos of 2 and above, the loop begins displaying lines above the
client area. Windows merely ignores these lines.

I told you we'd start simply. This is rather wasteful and inefficient code.
We'll fix it shortly, but first consider how we update the client area after
a WM_VSCROLL message.


Structuring Your Program for Painting

The window procedure in SYSMETS2 does not repaint the client area after
processing a scroll bar message. Instead, it calls InvalidateRect to
invalidate the client area. This causes Windows to place a WM_PAINT message
in the message queue.

It is best to structure your Windows programs so that you do all client-area
painting in response to a WM_PAINT message. Because your program should be
able to repaint the entire client area of the window at any time on receipt
of a WM_PAINT message, you will probably duplicate code if you also paint in
other parts of the program.

At first, you may rebel at this dictum because it is so different from
normal PC programming. I won't deny that, on occasion, painting in response
to messages other than WM_PAINT is much more convenient. (The KEYLOOK
program in the next chapter is an example of such a program.) But in many
cases it's simply unnecessary, and after you master the discipline of
accumulating all the information you need to paint in response to a WM_PAINT
message, you'll be pleased with the results. However, your program will
often determine that it must repaint a particular area of the display when
processing a message other than WM_PAINT. This is where InvalidateRect comes
in handy. You can use it to invalidate specific rectangles of the client
area or the entire client area.

Simply marking areas of the window as invalid to generate WM_PAINT messages
may not be entirely satisfactory in some applications. After you make an
InvalidateRect call, Windows places a WM_PAINT message in the message queue,
and the window procedure eventually processes it. However, Windows treats
WM_PAINT messages as low priority. If your message queue contains only a
WM_PAINT message and another application has other messages waiting, Windows
switches to the other application when you make a GetMessage call.

If you prefer to update the invalid area immediately, you can call
UpdateWindow after you call InvalidateRect:

UpdateWindow (hwnd) ;

UpdateWindow causes the window procedure to be called immediately with a
WM_PAINT message if any part of the client area is invalid. (It will not
call the window procedure if the entire client area is valid.) This WM_PAINT
message bypasses the message  queue. The window procedure is called directly
from Windows. When the window procedure has finished repainting, it exits
and Windows returns control to the program at the statement following the
UpdateWindow call.

You'll note that UpdateWindow is the same function used in WinMain to
generate the first WM_PAINT message. When a window is first created, the
entire client area is invalid. UpdateWindow directs the window procedure to
paint it.


Building a Better Scroll

Because SYSMETS2 is too inefficient a model to be imitated in other
programs, let's clean it up. SYSMETS3--our final version of the SYSMETS
program in this chapter--is shown in Figure 2-11. This version adds a
horizontal scroll bar for left and right scrolling and repaints the client
area more efficiently.

 SYSMETS3.MAK

#------------------------
# SYSMETS3.MAK make file
#------------------------

sysmets3.exe : sysmets3.obj sysmets3.def
     link sysmets3, /align:16, NUL, /nod slibcew libw, sysmets3
     rc sysmets3.exe

sysmets3.obj : sysmets3.c sysmets.h
     cl -c -Gsw -Ow -W2 -Zp sysmets3.c

 SYSMETS3.C

/*----------------------------------------------------
   SYSMETS3.C -- System Metrics Display Program No. 3
                 (c) Charles Petzold, 1990
  ----------------------------------------------------*/

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

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)



     {
     static char szAppName[] = "SysMets3" ;
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "Get System Metrics No. 3",
                          WS_OVERLAPPEDWINDOW | WS_VSCROLL | WS_HSCROLL,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static short cxChar, cxCaps, cyChar, cxClient, cyClient, nMaxWidth,
                  nVscrollPos, nVscrollMax, nHscrollPos, nHscrollMax ;
     char         szBuffer[10] ;
     HDC          hdc ;
     short        i, x, y, nPaintBeg, nPaintEnd, nVscrollInc, nHscrollInc ;
     PAINTSTRUCT  ps ;
     TEXTMETRIC   tm ;

     switch (message)

          {
          case WM_CREATE :
               hdc = GetDC (hwnd) ;

               GetTextMetrics (hdc, &tm) ;
               cxChar = tm.tmAveCharWidth ;
               cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ;
               cyChar = tm.tmHeight + tm.tmExternalLeading ;

               ReleaseDC (hwnd, hdc) ;

               nMaxWidth = 40 * cxChar + 18 * cxCaps ;
               return 0 ;

          case WM_SIZE :
               cyClient = HIWORD (lParam) ;
               cxClient = LOWORD (lParam) ;

               nVscrollMax = max (0, NUMLINES + 2 - cyClient / cyChar) ;
               nVscrollPos = min (nVscrollPos, nVscrollMax) ;

               SetScrollRange (hwnd, SB_VERT, 0, nVscrollMax, FALSE) ;
               SetScrollPos   (hwnd, SB_VERT, nVscrollPos, TRUE) ;

               nHscrollMax = max (0, 2 + (nMaxWidth - cxClient) / cxChar) ;
               nHscrollPos = min (nHscrollPos, nHscrollMax) ;

               SetScrollRange (hwnd, SB_HORZ, 0, nHscrollMax, FALSE) ;
               SetScrollPos   (hwnd, SB_HORZ, nHscrollPos, TRUE) ;
               return 0 ;

          case WM_VSCROLL :
               switch (wParam)
                    {
                    case SB_TOP :
                         nVscrollInc = -nVscrollPos ;
                         break ;

                    case SB_BOTTOM :
                         nVscrollInc = nVscrollMax - nVscrollPos ;
                         break ;

                    case SB_LINEUP :
                         nVscrollInc = -1 ;
                         break ;

                    case SB_LINEDOWN :
                         nVscrollInc = 1 ;
                         break ;
                    case SB_PAGEUP :
                         nVscrollInc = min (-1, -cyClient / cyChar) ;
                         break ;

                    case SB_PAGEDOWN :
                         nVscrollInc = max (1, cyClient / cyChar) ;
                         break ;

                    case SB_THUMBTRACK :
                         nVscrollInc = LOWORD (lParam) - nVscrollPos ;
                         break ;

                    default :
                         nVscrollInc = 0 ;
                    }
               if (nVscrollInc = max (-nVscrollPos,
                         min (nVscrollInc, nVscrollMax - nVscrollPos)))
                    {
                    nVscrollPos += nVscrollInc ;
                    ScrollWindow (hwnd, 0, -cyChar * nVscrollInc, NULL,
NULL) ;
                    SetScrollPos (hwnd, SB_VERT, nVscrollPos, TRUE) ;
                    UpdateWindow (hwnd) ;
                    }
               return 0 ;

          case WM_HSCROLL :
               switch (wParam)
                    {
                    case SB_LINEUP :
                         nHscrollInc = -1 ;
                         break ;

                    case SB_LINEDOWN :
                         nHscrollInc = 1 ;
                         break ;

                    case SB_PAGEUP :
                         nHscrollInc = -8 ;
                         break ;

                    case SB_PAGEDOWN :
                         nHscrollInc = 8 ;
                         break ;

                    case SB_THUMBPOSITION :
                         nHscrollInc = LOWORD (lParam) - nHscrollPos ;
                         break ;

                    default :
                         nHscrollInc = 0 ;
                    }

               if (nHscrollInc = max (-nHscrollPos,
                         min (nHscrollInc, nHscrollMax - nHscrollPos)))
                    {
                    nHscrollPos += nHscrollInc ;
                    ScrollWindow (hwnd, -cxChar * nHscrollInc, 0, NULL,
NULL) ;
                    SetScrollPos (hwnd, SB_HORZ, nHscrollPos, TRUE) ;
                    }
               return 0 ;

          case WM_PAINT :
               hdc = BeginPaint (hwnd, &ps) ;

               nPaintBeg = max (0, nVscrollPos + ps.rcPaint.top / cyChar -
1) ;
               nPaintEnd = min (NUMLINES,
                                nVscrollPos + ps.rcPaint.bottom / cyChar) ;

               for (i = nPaintBeg ; i < nPaintEnd ; i++)
                    {
                    x = cxChar * (1 - nHscrollPos) ;
                    y = cyChar * (1 - nVscrollPos + i) ;

                    TextOut (hdc, x, y,
                             sysmetrics[i].szLabel,
                             lstrlen (sysmetrics[i].szLabel)) ;

                    TextOut (hdc, x + 18 * cxCaps, y,
                             sysmetrics[i].szDesc,
                             lstrlen (sysmetrics[i].szDesc)) ;

                    SetTextAlign (hdc, TA_RIGHT | TA_TOP) ;

                    TextOut (hdc, x + 18 * cxCaps + 40 * cxChar, y,
                             szBuffer,
                             wsprintf (szBuffer, "%5d",
                                  GetSystemMetrics (sysmetrics[i].nIndex)))
;

                    SetTextAlign (hdc, TA_LEFT | TA_TOP) ;
                    }

               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }

     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 SYSMETS3.DEF

;-------------------------------------
; SYSMETS3.DEF module definition file
;-------------------------------------

NAME           SYSMETS3

DESCRIPTION    'System Metrics Display No. 3 (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

These are the improvements in SYSMETS3 and how they are implemented in the
program:

  ■   You can no longer scroll the display so that the last line appears at
      the top of the client area. You can scroll only far enough to see the
      last line at the bottom of the client area. This requires that the
      program calculate a new scroll bar range (and possibly a new thumb
      position) when it processes a WM_SIZE message. The WM_SIZE logic
      calculates the scroll bar range based on the number of lines of text,
      the width of the text, and the size of the client area. This approach
      results in a smaller range--only that necessary to bring into view the
      text that falls outside the client area.

      This offers an interesting dividend. Suppose that the client area of
      the window is large enough to display the entire text with top and
      bottom margins. In this case, both the minimum position and maximum
      position of the scroll bar range will equal zero. What will Windows do
      with this information? It will remove the scroll bar from the window!
      It's no longer needed. Similarly, if the client area is wide enough to
      show the full 60-column width of the text, no horizontal scroll bar is
      displayed in the window.

  ■   The WM_VSCROLL and WM_HSCROLL messages are processed by first
      calculating an increment of the scroll bar position for each value of
      wParam. This value is then used to scroll the existing contents of the
      window using the Windows ScrollWindow call. This function has the
      following format:

      ScrollWindow (hwnd, xInc, yInc, lpRect, lpClipRect) ;

      The xInc and yInc values specify an amount to scroll in pixels. In
      SYSMETS3, the lpRect and lpClipRect values are set to NULL to specify
      that the entire client area should be scrolled. Windows invalidates
      the rectangle in the client area "uncovered" by the scrolling
      operation. This generates a WM_PAINT message. InvalidateRect is no
      longer needed. (Note that ScrollWindow is not a GDI procedure because
      it does not require a handle to a device context. It is one of the few
      non-GDI Windows functions that changes the appearance of the client
      area of a window.)

  ■   The WM_PAINT processing now determines which lines are within the
      invalid rectangle and rewrites only those lines. It does this by
      analyzing the top and bottom coordinates of the invalid rectangle
      stored in the PAINTSTRUCT structure. The program paints only those
      text lines within the invalid rectangle. The code is more complex, but
      it is much faster.

  ■   Because WM_PAINT was speeded up, I decided to let SYSMETS3 process
      SB_THUMBTRACK operations for WM_VSCROLL messages. Previously, the
      program would ignore SB_THUMBTRACK messages (which occur as the user
      drags the scroll bar thumb) and would act only on SB- _THUMBPOSITION
      messages, which occur when the user stops dragging the thumb. The
      WM_VSCROLL code also calls UpdateWindow to update the client area
      immediately. When you move the thumb on the vertical scroll bar,
      SYSMETS3 will continually scroll and update the client area. I'll let
      you decide whether SYSMETS3 (and Windows) is fast enough to justify
      this change.


But I Don't Like to Use the Mouse

If you don't have a mouse on your PC, you can't scroll SYSMETS3 at all.
Scroll bars created as part of your application window do not have an
automatic keyboard interface. Because Windows can be installed without a
mouse, it is highly recommended that you write programs that do not require
the mouse.

In the next chapter you'll learn how to use the keyboard and how to add a
keyboard interface to SYSMETS. You'll notice that SYSMETS3 seems to process
WM_VSCROLL messages where wParam equals SB_TOP and SB_BOTTOM. I mentioned
earlier that a window procedure doesn't receive these messages for scroll
bars, so right now this is superfluous code. When we come back to this
program in the next chapter, you'll see the reason for including this code.






PART II  READING INPUT
────────────────────────────────────────────────────────────────────────────

Chapter 3  The Keyboard
────────────────────────────────────────────────────────────────────────────

Like most interactive programs that run on personal computers, Windows
applications rely heavily on the keyboard for user input. Although Windows
also supports a mouse as an input device, you can't depend on a mouse being
present in an installed version of Windows. For this reason, program
developers should attempt to allow complete program functionality from the
keyboard. (Of course, in some cases, such as drawing programs or desktop
publishing programs, this is simply not practical and a mouse will be
required.)

The keyboard cannot be treated solely as an input device in isolation from
other program functions. For example, a program almost always echoes the
keyboard input by displaying typed characters in the client area of a
window. Handling keyboard input and displaying text must be treated
together. Sometimes the keystrokes result in a document being created that
is eventually saved in a disk file. Sometimes a program requires that the
user enter an MS-DOS filename. These apparently straightforward chores raise
issues related to the support of the ASCII extended character set (codes of
128 and above) and of international characters. For this reason, topics such
as the character sets supported by Windows and multibyte character codes are
also covered in this chapter.

KEYBOARD BASICS

As the user presses and releases keys, the keyboard driver passes the
keystrokes to Windows. Windows saves the keystrokes in the system message
queue and then transfers them to the message queue of the program with the
"input focus." These messages are processed in the program's window
procedure. In most cases, the keyboard information encoded in these messages
is probably more than your program needs. Part of the job of handling the
keyboard is knowing which messages are important and which are not.

The Keyboard Driver

Windows is shipped with several keyboard drivers for the support of various
keyboard hardware and dynamic link libraries that support international
keyboard configurations. Keyboards for European languages must include
additional characters (such as letters with diacritics) and symbols (such as
the British pound sign). When you install Windows, the SETUP program copies
the keyboard driver for the keyboard and country you request into the SYSTEM
subdirectory of your Windows directory.

KEYBOARD.DRV is a relatively small and simple driver. When Windows starts
up, it enables the keyboard driver, which responds by saving the original
interrupt vector addresses for Interrupt 09H (the hardware keyboard
interrupt) and setting this interrupt vector to routines within the driver.

Pressing or releasing a key generates an Interrupt 09H. This is sometimes
called an "asynchronous" interrupt because it can occur at any time. The
interrupt suspends the program currently running and passes control to the
Interrupt 09H keyboard handler. When the keyboard handler is finished, it
passes control back to the interrupted program. The Interrupt 09H keyboard
handler within KEYBOARD.DRV decodes the key and calls a routine within the
Windows USER module, which stores them as queued messages. The Windows
program then obtains the keyboard messages when the program calls
GetMessage.

Because a Windows program effectively polls for keyboard input by calling
GetMessage, Windows programs are not very different from PC programs that
obtain keystrokes by polling through the software Interrupts 16H and 21H.
However, the quantity of information that Windows encodes in the keyboard
messages is much greater than that available from Interrupts 16H and 21H.

Some application programs written for the IBM PC intercept Interrupt 09H and
do their own hardware keyboard processing. This allows the program to use
all possible combinations of keystrokes, not only those defined by the PC
BIOS. Windows programs are not very different from these programs either,
because the window procedure is a message handler that receives messages
about all keyboard events. The only real difference between message handling
and interrupt handling is that the Windows messages are not asynchronous. A
Windows program is never interrupted to be notified of a keystroke; the
program receives a new keyboard message only from the message queue. In
short, Windows provides programs with all the benefits of intercepting the
hardware Interrupt 09H but with none of the hassles.

When a user types on the keyboard faster than a program can process the
keys, Windows stores the extra keystrokes in a system message queue rather
than in an individual program's message queue. One of these extra keystrokes
(Alt-Tab, for instance) may have the effect of switching to another program.
The keys following Alt-Tab should then go to the other program. Windows
correctly synchronizes such keyboard messages.

Windows sends eight different messages to programs to indicate various
keyboard events. That may seem like a lot, but your program can safely
ignore many of them.


Ignoring the Keyboard

Although the keyboard is the primary source of user input to Windows
programs, your program does not need to act on every keyboard message it
receives. Windows handles many keyboard functions itself. For instance, you
can ignore keystrokes that pertain to system functions. These keystrokes
generally involve the Alt key.

A program need not monitor these keystrokes itself because Windows notifies
a program of the effect of the keystrokes. (A program can monitor the
keystrokes if it wants to, however.) For instance, if the Windows user
selects a menu item with the keyboard, Windows sends the program a message
that the menu item has been selected, regardless of whether it was selected
by using the mouse or by using the keyboard. (Menus are covered in Chapter
9.)

Some Windows programs use "keyboard accelerators" to invoke common menu
items. The accelerators generally involve the function keys, special
noncharacter keys such as Insert or Delete, or a letter in combination with
the Ctrl key. These keyboard accelerators are defined in a program's
resource script. (Chapter 9 shows how Windows translates the accelerators
into menu command messages. You don't have to do the translation yourself.)

Dialog boxes (covered in Chapter 10) also have a keyboard interface, but
programs usually do not need to monitor the keyboard when a dialog box is
active. The keyboard interface is handled by Windows, and Windows sends
messages to your program about the effects of the keystrokes. Dialog boxes
can contain "edit" controls for text input. These are generally small boxes
in which the user types a character string. Windows handles all the edit
control logic and gives your program the final contents of the edit control
when the user is done.

Even within your main window you can define child windows that function as
edit controls. An extreme example of this is the Windows NOTEPAD program,
which is little more than a large multiline edit control. NOTEPAD does
little keyboard processing on its own and relies on Windows to handle all
the dirty work. (Chapter 6 discusses how this works.)


Focus, Focus, Who's Got the Focus?

The keyboard must be shared by all applications running under Windows. Some
applications may have more than one window, and the keyboard must be shared
by these windows within the same application. When a key on the keyboard is
pressed, only one window procedure can receive a message that the key has
been pressed. The window that receives this keyboard message is the window
with the "input focus."

The concept of input focus is closely related to the concept of "active
window." The window with the input focus is either the active window or a
child window of the active window. The active window is usually easy to
identify. If the active window has a caption bar, Windows highlights the
caption bar. If the active window has a dialog frame (a form most commonly
seen in dialog boxes) instead of a caption bar, Windows highlights the
frame. If the active window is an icon, Windows highlights the window's
caption bar text below the icon.

The most common child windows are controls such as push buttons, radio
buttons, check boxes, scroll bars, edit boxes, and list boxes that usually
appear in a dialog box. Child windows are never themselves active windows.
If a child window has the input focus, then the active window is its parent.
Child window controls indicate that they have the input focus generally by
using a flashing cursor or caret.

If the active window is an icon, then no window has the input focus. Windows
continues to send keyboard messages to the icon, but these messages are in a
different form from keyboard messages sent to active windows that are not
icons.

A window procedure can determine when it has the input focus by trapping
WM_SETFOCUS and WM_KILLFOCUS messages. WM_SETFOCUS indicates that the window
is receiving the input focus, and WM_KILLFOCUS signals that the window is
losing the input focus.


Keystrokes and Characters

The messages that an application receives from Windows about keyboard events
distinguish between "keystrokes" and "characters." This is in accordance
with the two ways you can view the keyboard. First, you can think of the
keyboard as a collection of keys. The keyboard has only one A key. Pressing
that key is a keystroke. Releasing that key is a keystroke. But the keyboard
is also an input device that generates displayable characters. The A key can
generate several characters depending on the status of the Ctrl, Shift, and
Caps Lock keys. Normally, the character is a lowercase a. If the Shift key
is down or Caps Lock is toggled on, the character is an uppercase A. If Ctrl
is down, the character is a Ctrl-A. On a foreign-language keyboard, the A
keystroke may be preceded by a "dead-character key" or by Shift, Ctrl, or
Alt in various combinations. The combinations could generate a lowercase a
or an uppercase A with an accent mark.

For keystroke combinations that result in displayable characters, Windows
sends a program both keystroke messages and character messages. Some keys do
not generate characters. These include the shift keys, the function keys,
the cursor movement keys, and special keys such as Insert and Delete. For
these keys, Windows generates only keystroke messages.



KEYSTROKE MESSAGES

When you press a key, Windows places either a WM_KEYDOWN or WM_SYSKEYDOWN
message in the message queue of the window with the input focus. When you
release a key, Windows places either a WM_KEYUP or WM_SYSKEYUP message in
the message queue.


                      Key Pressed    Key Released
────────────────────────────────────────────────────────────────────────────
Nonsystem Keystroke:  WM_KEYDOWN     WM_KEYUP
System Keystroke:     WM_SYSKEYDOWN  WM_SYSKEYUP

Usually the "down" and "up" messages occur in pairs. However, if you hold
down a key so that the typematic (autorepeat) action takes over, Windows
sends the window procedure a series of WM_KEYDOWN (or WM_SYSKEYDOWN)
messages and a single WM_KEYUP (or WM_SYSKEYUP) message when the key is
finally released. Like all messages, keystroke messages are time-stamped.
You can obtain the relative time a key was pressed or released by calling
GetMessageTime.

System and Nonsystem Keystrokes

The "SYS" in WM_SYSKEYDOWN and WM_SYSKEYUP stands for "system" and refers to
keystrokes that are more important to Windows than to the Windows
application. The WM_SYSKEYDOWN and WM_SYSKEYUP messages are usually
generated for keys typed in combination with the Alt key. These keystrokes
invoke options on the program's menu or system menu, or they are used for
system functions such as switching the active window (Alt-Tab or Alt-Esc) or
for system menu accelerators (Alt in combination with a function key).
Programs usually ignore the WM_SYSKEYUP and WM_SYSKEYDOWN messages and pass
them to DefWindowProc. Because Windows takes care of all the Alt-key logic,
you really have no need to trap these messages. Your window procedure will
eventually receive other messages concerning the result of these keystrokes
(such as a menu selection). If you want to include code in your window
procedure to trap the system keystroke messages (as we will do in the
KEYLOOK program later in this chapter), pass the messages to DefWindowProc
after you process them so that Windows can still use them for their normal
purposes.

But think about this for a moment. Almost everything that affects your
program's window passes through your window procedure first. Windows does
something with the message only if you pass the message to DefWindowProc.
For instance, if you add the lines:

case WM_SYSKEYDOWN :
case WM_SYSKEYUP :
case WM_SYSCHAR :
     return 0 ;

to a window procedure, then you effectively disable all Alt-key operations
(menu commands, Alt-Tab, Alt-Esc, and so on) when your program has the input
focus. Although I doubt you would want to do this, I trust you're beginning
to sense the power in your window procedure.

The WM_KEYDOWN and WM_KEYUP messages are usually generated for keys that are
pressed and released without the Alt key. Your program may use or discard
these keystroke messages. Windows itself doesn't care about them.


The lParam Variable

For all four keystroke messages, the 32-bit lParam variable passed to the
window procedure is divided into six fields: Repeat Count, OEM Scan Code,
Extended Key Flag, Context Code, Previous Key State, and Transition State.
(See Figure 3-1.)

  (Figure 3-1. may be found in the printed book.)

Repeat Count

The Repeat Count is the number of keystrokes represented by the message. In
most cases the Repeat Count is set to 1. However, if a key is held down and
your window procedure is not fast enough to process key-down messages at the
typematic rate (approximately  a 10-character-per-second default), Windows
combines several WM_KEYDOWN or WM_SYSKEYDOWN messages into a single message
and increases Repeat Count accordingly. The Repeat Count is always 1 for a
WM_KEYUP or WM_SYSKEYUP message.

Because a Repeat Count greater than 1 indicates that typematic keystrokes
are occurring faster than your program can process them, you may want to
ignore the Repeat Count when processing the keyboard messages. Almost
everyone has had the experience of "overscrolling" a word-processing
document or spreadsheet because extra keystrokes have stacked up in the
keyboard buffer. Ignoring the Repeat Count in your program will
significantly reduce the possibilities for overscrolling. However, in other
cases you will want to use the Repeat Count. You should probably try your
programs both ways and see which approach feels the most natural.


OEM Scan Code

The OEM Scan Code is the keyboard scan code generated by the hardware of the
computer. For the IBM PC, this scan code is the same as the value passed
back to a program in register AH during a BIOS Interrupt 16H call. Windows
applications generally ignore the OEM Scan Code because there are better
ways to decode keyboard information.


Extended Key Flag

The Extended Key Flag is 1 if the keystroke results from one of the
additional keys on the IXC Enhanced Keyboard. (The IXC Enhanced Keyboard has
function keys across the top and a separate [combined] keypad for cursor
keys and number keys.) This flag is set to 1 for the Alt and Ctrl keys at
the right of the keyboard, the cursor movement keys (including Insert and
Delete) that are not part of the numeric keypad, the Slash (/) and Enter
keys on the numeric keypad, and the Num Lock key. Windows programs generally
ignore the Extended Key Flag.


Context Code

The Context Code is 1 if the Alt key is pressed. This bit will always be 1
for the WM_SYSKEYUP and WM_SYSKEYDOWN messages and 0 for the WM_KEYUP and
WM_KEYDOWN messages with two exceptions:

  ■   If the active window is an icon, it does not have the input focus. All
      keystrokes generate WM_SYSKEYUP and WM_SYSKEYDOWN messages. If the Alt
      key is not pressed, the Context Code field is set to 0. (Windows uses
      SYS keyboard messages so that the active window that is an icon
      doesn't process these keystrokes.)

  ■   On some foreign-language keyboards, certain characters are generated
      by combining Shift, Ctrl, or Alt with another key. In these cases the
      lParam variable that accompanies WM_KEYUP and WM_KEYDOWN messages has
      a 1 in the Context Code field, but the messages are not system
      keystroke messages.


Previous Key State

The Previous Key State is 0 if the key was previously up and 1 if the key
was previously down. It is always set to 1 for a WM_KEYUP or WM_SYSKEYUP
message, but it can be 0 or 1 for a WM_KEYDOWN or WM_SYSKEYDOWN message. A 1
indicates second and subsequent messages for keys that are the result of
typematic action.


Transition State

The Transition State is 0 if the key is being pressed and 1 if the key is
being released. The field is set to 0 for a WM_KEYDOWN or WM_SYSKEYDOWN
message and to 1 for a WM_KEYUP or WM_SYSKEYUP.



Virtual Key Codes

Although some information in lParam might be useful for processing WM_KEYUP,
WM_KEYDOWN, WM_SYSKEYUP, and WM_SYSKEYDOWN messages, the wParam parameter is
much more important. This parameter contains the "virtual key code" that
identifies the key that was pressed or released. The developers of Windows
have attempted to

Table   VIRTUAL KEY CODES

╓┌────────┌───────┌──────────────┌─────────┌─────────────────────────────────╖
                 WINDOWS.H
Decimal  Hex     Identifier     Required  IBM Keyboard
────────────────────────────────────────────────────────────────────────────
1        01      VK_LBUTTON

2        02      VK_RBUTTON

3        03      VK_CANCEL      *         Ctrl-Break

4        04      VK_MBUTTON

8        08      VK_BACK        *         Backspace

9        09      VK_TAB         *         Tab

12       0C      VK_CLEAR                 Numeric keypad 5 with Num Lock
                                          OFF

                 WINDOWS.H
Decimal  Hex     Identifier     Required  IBM Keyboard
────────────────────────────────────────────────────────────────────────────
1        01      VK_LBUTTON

13       0D      VK_RETURN      *         Enter

16       10      VK_SHIFT       *         Shift

17       11      VK_CONTROL     *         Ctrl

18       12      VK_MENU        *         Alt

19       13      VK_PAUSE                 Pause

20       14      VK_CAPITAL     *         Caps Lock

27       1B      VK_ESCAPE      *         Esc

32       20      VK_SPACE       *         Spacebar

                 WINDOWS.H
Decimal  Hex     Identifier     Required  IBM Keyboard
────────────────────────────────────────────────────────────────────────────
1        01      VK_LBUTTON

33       21      VK_PRIOR       *         Page Up

34       22      VK_NEXT        *         Page Down

35       23      VK_END                   End

36       24      VK_HOME        *         Home

37       25      VK_LEFT        *         Left Arrow

38       26      VK_UP          *         Up Arrow

39       27      VK_RIGHT       *         Right Arrow

40       28      VK_DOWN        *         Down Arrow

                 WINDOWS.H
Decimal  Hex     Identifier     Required  IBM Keyboard
────────────────────────────────────────────────────────────────────────────
1        01      VK_LBUTTON

41       29      VK_SELECT

42       2A      VK_PRINT

43       2B      VK_EXECUTE

44       2C      VK_SNAPSHOT              Print Screen

45       2D      VK_INSERT      *         Insert

46       2E      VK_DELETE      *         Delete

47       2F      VK_HELP

48\-57   30\-39                 *         0 through 9 on main keyboard

                 WINDOWS.H
Decimal  Hex     Identifier     Required  IBM Keyboard
────────────────────────────────────────────────────────────────────────────
1        01      VK_LBUTTON

65\-90   41\-5A                 *         A through Z

96       60      VK_NUMPAD0               Numeric keypad 0 with Num Lock
                                          ON



97       61      VK_NUMPAD1               Numeric keypad 1 with Num Lock
                                          ON

98       62      VK_NUMPAD2               Numeric keypad 2 with Num Lock
                                          ON

99       63      VK_NUMPAD3               Numeric keypad 3 with Num Lock
                                          ON

                 WINDOWS.H
Decimal  Hex     Identifier     Required  IBM Keyboard
────────────────────────────────────────────────────────────────────────────
1        01      VK_LBUTTON

100      64      VK_NUMPAD4               Numeric keypad 4 with Num Lock
                                          ON

101      65      VK_NUMPAD5               Numeric keypad 5 with Num Lock
                                          ON

102      66      VK_NUMPAD6               Numeric keypad 6 with Num Lock
                                          ON

103      67      VK_NUMPAD7               Numeric keypad 7 with Num Lock
                                          ON

104      68      VK_NUMPAD8               Numeric keypad 8 with Num Lock
                                          ON

105      69      VK_NUMPAD9               Numeric keypad 9 with Num Lock
                 WINDOWS.H
Decimal  Hex     Identifier     Required  IBM Keyboard
────────────────────────────────────────────────────────────────────────────
1        01      VK_LBUTTON
105      69      VK_NUMPAD9               Numeric keypad 9 with Num Lock
                                          ON

106      6A      VK_MULTIPY               Numeric keypad * (enhanced
                                          keyboard)

107      6B      VK_ADD                   Numeric keypad + (enhanced
                                          keyboard)

108      6C      VK_SEPARATOR

109      6D      VK_SUBTRACT              Numeric keypad - (enhanced
                                          keyboard)

110      6E      VK_DECIMAL               Numeric keypad

111      6F      VK_DIVIDE                Numeric keypad /(enhanced
                 WINDOWS.H
Decimal  Hex     Identifier     Required  IBM Keyboard
────────────────────────────────────────────────────────────────────────────
1        01      VK_LBUTTON
111      6F      VK_DIVIDE                Numeric keypad /(enhanced
                                          keyboard)

112      70      VK_F1          *         Function key F1

113      71      VK_F2          *         Function key F2

114      72      VK_F3          *         Function key F3

115      73      VK_F4          *         Function key F4

116      74      VK_F5          *         Function key F5

117      75      VK_F6          *         Function key F6

118      76      VK_F7          *         Function key F7

                 WINDOWS.H
Decimal  Hex     Identifier     Required  IBM Keyboard
────────────────────────────────────────────────────────────────────────────
1        01      VK_LBUTTON

119      77      VK_F8          *         Function key F8

120      78      VK_F9          *         Function key F9

121      79      VK_F10         *         Function key F10

122      7A      VK_F11                   Function key F11 (enhanced
                                          keyboard)

123      7B      VK_F12                   Function Key F12 (enhanced
                                          keyboard)

124      7C      VK_F13



                 WINDOWS.H
Decimal  Hex     Identifier     Required  IBM Keyboard
────────────────────────────────────────────────────────────────────────────
1        01      VK_LBUTTON

125      7D      VK_F14

126      7E      VK_F15

127      7F      VK_F16

144      90      VK_NUMLOCK               Num Lock





define virtual keys in a device-independent manner. For this reason, some
virtual key codes cannot be generated on the IBM PC and strict compatibles
but may be found on other manufacturer's keyboards.

The virtual key codes you use most often have names defined in WINDOWS.H.
The table above shows these names along with the numeric key codes and the
IBM PC key that corresponds to the virtual key. Although all keys cause
keystroke messages, the table does not include any symbol keys (such as the
key with the / and ? symbols). These keys have virtual key codes of 128 and
above, and they are often defined differently for international keyboards.
You can determine the values of these virtual key codes using the KEYLOOK
program that is shown later in this chapter, but normally you should not
process keystroke messages for these keys.

An asterisk (*) in the column labeled "Required" indicates that the key is
mandatory for any Windows implementation. Windows also requires that a
keyboard and keyboard driver allow the Shift, Ctrl, and Shift and Ctrl keys
together to be combined with all letter keys, all required cursor keys, and
all required function keys. The VK_LBUTTON, VK_MBUTTON, and VK_RBUTTON
virtual key codes refer to the left, middle, and right buttons of a mouse.
However, you will never receive keystroke messages with wParam set to these
values. The mouse generates its own messages.


Shift States

The wParam and lParam parameters that accompany WM_KEYDOWN, WM_KEYUP,
WM_SYSKEYDOWN, and WM_SYSKEYUP messages do not tell your program about the
state of the shift keys. You can obtain the current state of any virtual key
using the GetKeyState function. This function generally is used to obtain
the state of shift keys (Shift, Ctrl, and Alt) and toggle keys (Caps Lock
and Num Lock). For instance:

GetKeyState (VK_SHIFT) ;

returns a negative value (that is, the high bit is set) if the Shift key is
down. The value returned from:

GetKeyState (VK_CAPITAL) ;

has the low bit set if the Caps Lock key is toggled on. You can also obtain
the state of the mouse buttons using the virtual key codes VK_LBUTTON,
VK_RBUTTON, and VK_MBUTTON. However, most Windows programs that need to
monitor a combination of mouse buttons and keystrokes usually do it the
other way around--by checking keystrokes when they receive a mouse message.
In fact, shift-state information is included in the mouse messages (as
you'll see in the next chapter).

Be careful with GetKeyState. It is not a real-time keyboard status check.
Rather, it is a check of the keyboard status up to and including the current
message being processed. GetKeyState does not let you retrieve keyboard
information independent of normal keyboard messages. For instance, you may
want to hold up processing in your window procedure until the user presses
the F1 function key:

while (GetKeyState (VK_F1) >= 0) ;  // WRONG !!!

This statement will execute for a very long time--until you reset your
machine with Ctrl-Alt-Delete. Your program must retrieve the keyboard
message from the queue before GetKeyState can retrieve the state of the key.
This synchronization actually works to your advantage, because if you need
to know the shift state for a particular keystroke message, GetKeyState is
guaranteed to be accurate, even if you are processing the message after the
shift key has been released. If you really need the current state of the
key, you can use GetAsyncKeyState.


Using Keystroke Messages

The idea of a program getting information about every keystroke is certainly
nice, but most Windows programs ignore all but a few keystroke messages. The
WM_SYSKEYDOWN and WM_SYSKEYUP messages are for Windows system functions, and
you don't need to look at them. If you process WM_KEYDOWN messages, you can
also ignore WM_KEYUP messages.

Windows programs generally use WM_KEYDOWN messages for keystrokes that do
not generate characters. Although you may think that it's possible to use
keystroke messages in combination with shift-state information to translate
keystroke messages into character messages, don't do it. You'll have
problems with international keyboard differences. For instance, if you get a
WM_KEYDOWN message with wParam equal to 33H, you know the user pressed the 3
key. So far, so good. If you use GetKeyState and find out that the Shift key
is down, you might assume that the user is typing a pound sign (#). Not
necessarily so. A British user is typing a £. So the WM_KEYDOWN messages are
most useful for the cursor movement keys, the function keys, and special
keys such as Insert and Delete. However, Insert, Delete, and the function
keys often appear as menu accelerators. Because Windows translates menu
accelerators into menu command messages, you don't have to process the
keystrokes themselves. Some non-Windows programs for the PC use function
keys extensively in combination with the Shift, Ctrl, and Alt keys. You can
do something similar in your Windows programs, but it's not recommended. If
you want to use the function keys, they should duplicate menu commands. One
objective in Windows is to provide a user interface that doesn't require
memorizing or using complex command charts.

We've managed to eliminate everything except one final case: Most of the
time, you will process WM_KEYDOWN messages only for cursor movement keys.
When you use the cursor keys, you can check the Shift-key and Ctrl-key
states through GetKeyState. Windows functions often use the Shift key in
combination with the cursor keys to extend a selection in (for instance) a
word-processing document. The Ctrl key is often used to alter the meaning of
the cursor key. (For example, Ctrl in combination with the Right Arrow key
might mean to move the cursor one word to the right.)

The Common User Access: Advanced Interface Design Guide contains a list of
recommended keyboard definitions. (The guide, hereinafter referred to as the
CUA Advanced Interface Design Guide, is included in the Windows Software
Development Kit and is part of the IBM Systems Application Architecture
Library.) You can also examine how the keyboard is used in existing Windows
programs. If you don't like those definitions, you are free to do something
different. But keep in mind that doing so may be detrimental to a user's
ability to quickly learn your program.



ENHANCING SYSMETS:

ADDING A KEYBOARD INTERFACE

When we wrote the three versions of the SYSMETS program in Chapter 2, we
didn't know anything about the keyboard. We were able to scroll the text
only by using the mouse on the scroll bars. Now that we know how to process
keystroke messages, let's add a keyboard interface to SYSMETS. This is
obviously a job for cursor movement keys. We'll use most of the cursor
movement keys (Home, End, Page Up, Page Down, Up Arrow, and Down Arrow) for
vertical scrolling. The Left Arrow key and the Right Arrow key can take care
of the less-important horizontal scrolling.

Adding WM_KEYDOWN Logic

One obvious way to create a keyboard interface is to add some WM_KEYDOWN
logic to the window procedure that parallels the WM_VSCROLL and WM_HSCROLL
logic:

case WM_KEYDOWN :
     nVscrollInc = nHscrollInc = 0 ;

     switch (wParam)
          {
          case VK_HOME :       // same as WM_VSCROLL, SB_TOP
               nVscrollInc = -nVscrollPos ;
               break ;

          case VK_END :        // same as WM_VSCROLL, SB_BOTTOM
               nVscrollInc = nVscrollMax - nVscrollPos ;
               break ;

          case VK_UP :         // same as WM_VSCROLL, SB_LINEUP
               nVscrollInc = -1 ;
               break ;

          case VK_DOWN :       // same as WM_VSCROLL, SB_LINEDOWN
               nVscrollInc = 1 ;
               break ;

          case VK_PRIOR :      // same as WM_VSCROLL, SB_PAGEUP
               nVscrollInc = min (-1, -cyClient / cyChar) ;
               break ;

          case VK_NEXT :       // same as WM_VSCROLL, SB_PAGEDOWN
               nVscrollInc = max (1, cyClient / cyChar) ;
               break ;

          case VK_LEFT :       // same as WM_HSCROLL, SB_PAGEUP
               nHscrollInc = -8 ;
               break ;

          case VK_RIGHT :      // same as WM_HSCROLL, SB_PAGEDOWN
               nHscrollInc = 8 ;
               break ;

          default :
               break ;
          }

     if (nVscrollInc = max (-nVscrollPos,
               min (nVscrollInc, nVscrollMax - nVscrollPos)))
          {
          nVscrollPos += nVscrollInc ;
          ScrollWindow (hwnd, 0, -cyChar * nVscrollInc, NULL, NULL) ;
          SetScrollPos (hwnd, SB_VERT, nVscrollPos, TRUE) ;
          UpdateWindow (hwnd) ;
          }

     if (nHscrollInc = max (-nHscrollPos,
               min (nHscrollInc, nHscrollMax - nHscrollPos)))
          {
          nHscrollPos += nHscrollInc ;
          ScrollWindow (hwnd, -cxChar * nHscrollInc, 0, NULL, NULL) ;
          SetScrollPos (hwnd, SB_HORZ, nHscrollPos, TRUE) ;
          }

     return 0 ;

Do you dislike this code as much as I do? Simply duplicating all the scroll
bar code is unwise, because if we ever wanted to change the scroll bar
logic, we'd have to make parallel changes in WM_KEYDOWN. There has to be a
better way. And there is.


Sending Messages

Wouldn't it be better to simply translate each of these WM_KEYDOWN messages
into an equivalent WM_VSCROLL and WM_HSCROLL message and then perhaps fool
WndProc into thinking that it's getting a WM_VSCROLL or WM_HSCROLL message,
perhaps by sending a phony scroll bar message to the window procedure?
Windows lets you do this. The function is called SendMessage, and it takes
the same parameters as those passed to the window procedure:

SendMessage (hwnd, message, wParam, lParam) ;

When you call SendMessage, Windows calls the window procedure whose window
handle is hwnd, passing to it these four parameters. When the window
procedure has completed processing the message, Windows returns control to
the next statement following the SendMessage call. The window procedure to
which you send the message could be the same window procedure, another
window procedure in the same program, or a window procedure in another
application.

Here's how we might use SendMessage for processing WM_KEYDOWN codes in the
SYSMETS program:

case WM_KEYDOWN :
     switch (wParam)
          {
          case VK_HOME :
               SendMessage (hwnd, WM_VSCROLL, SB_TOP, 0L) ;
               break ;

          case VK_END :
               SendMessage (hwnd, WM_VSCROLL, SB_BOTTOM, 0L) ;
               break ;

          case VK_PRIOR :
               SendMessage (hwnd, WM_VSCROLL, SB_PAGEUP, 0L) ;
               break ;
[other program lines]

OK, you get the general idea. Our goal was to add a keyboard interface to
the scroll bars, and that's exactly what we've done. We've made the cursor
movement keys duplicate scroll bar logic by actually sending the window
procedure a scroll bar message. Now you see why I included SB_TOP and
SB_BOTTOM processing for WM_VSCROLL messages in the SYSMETS3 program. It
wasn't used then, but it's used now for processing the Home and End keys.
The final SYSMETS program, shown in Figure 3-2, incorporates these changes.
You'll also need the SYSMETS.H file from Chapter 2 (Figure 2-4) to compile
this program.

Remember: To send a message to a window procedure, use the SendMessage
function. Do not try to call the window procedure directly like this:

WndProc (hwnd, WM_VSCROLL, SB_PAGEUP, 0L) ; // WRONG !!!

This statement will cause "unpredictable results" (if you call a system
crash "unpredictable"). You may define and call other subroutines within a
Windows program, but you must not call a window procedure directly. You'll
find out why in Chapter 7.

 SYSMETS.MAK

#-----------------------
# SYSMETS.MAK make file
#-----------------------

sysmets.exe : sysmets.obj sysmets.def
     link sysmets, /align:16, NUL, /nod slibcew libw, sysmets
     rc sysmets.exe

sysmets.obj : sysmets.c sysmets.h
     cl -c -Gsw -Ow -W2 -Zp sysmets.c

 SYSMETS.C

/*-----------------------------------------------------
   SYSMETS.C -- System Metrics Display Program (Final)
                (c) Charles Petzold, 1990
  -----------------------------------------------------*/

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

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName[] = "SysMets" ;
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;



          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "System Metrics",
                          WS_OVERLAPPEDWINDOW | WS_VSCROLL | WS_HSCROLL,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static short  cxChar, cxCaps, cyChar, cxClient, cyClient, nMaxWidth,
                   nVscrollPos, nVscrollMax, nHscrollPos, nHscrollMax ;
     char          szBuffer[10] ;
     HDC           hdc ;
     short         i, x, y, nPaintBeg, nPaintEnd, nVscrollInc, nHscrollInc ;
     PAINTSTRUCT   ps ;
     TEXTMETRIC    tm ;

     switch (message)
          {
          case WM_CREATE :
               hdc = GetDC (hwnd) ;

               GetTextMetrics (hdc, &tm) ;
               cxChar = tm.tmAveCharWidth ;
               cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2 ;
               cyChar = tm.tmHeight + tm.tmExternalLeading ;

               ReleaseDC (hwnd, hdc) ;

               nMaxWidth = 40 * cxChar + 18 * cxCaps ;
               return 0 ;

          case WM_SIZE :
               cyClient = HIWORD (lParam) ;
               cxClient = LOWORD (lParam) ;
               nVscrollMax = max (0, NUMLINES + 2 - cyClient / cyChar) ;
               nVscrollPos = min (nVscrollPos, nVscrollMax) ;

               SetScrollRange (hwnd, SB_VERT, 0, nVscrollMax, FALSE) ;
               SetScrollPos   (hwnd, SB_VERT, nVscrollPos, TRUE) ;

               nHscrollMax = max (0, 2 + (nMaxWidth - cxClient) / cxChar) ;
               nHscrollPos = min (nHscrollPos, nHscrollMax) ;

               SetScrollRange (hwnd, SB_HORZ, 0, nHscrollMax, FALSE) ;
               SetScrollPos   (hwnd, SB_HORZ, nHscrollPos, TRUE) ;
               return 0 ;

          case WM_VSCROLL :
               switch (wParam)
                    {
                    case SB_TOP :
                         nVscrollInc = -nVscrollPos ;
                         break ;

                    case SB_BOTTOM :
                         nVscrollInc = nVscrollMax - nVscrollPos ;
                         break ;

                    case SB_LINEUP :
                         nVscrollInc = -1 ;
                         break ;

                    case SB_LINEDOWN :
                         nVscrollInc = 1 ;
                         break ;

                    case SB_PAGEUP :
                         nVscrollInc = min (-1, -cyClient / cyChar) ;
                         break ;

                    case SB_PAGEDOWN :
                         nVscrollInc = max (1, cyClient / cyChar) ;
                         break ;

                    case SB_THUMBTRACK :
                         nVscrollInc = LOWORD (lParam) - nVscrollPos ;
                         break ;

                    default :
                         nVscrollInc = 0 ;
                    }
               if (nVscrollInc = max (-nVscrollPos,
                         min (nVscrollInc, nVscrollMax - nVscrollPos)))
                    {
                    nVscrollPos += nVscrollInc ;
                    ScrollWindow (hwnd, 0, -cyChar * nVscrollInc, NULL,
NULL) ;
                    SetScrollPos (hwnd, SB_VERT, nVscrollPos, TRUE) ;
                    UpdateWindow (hwnd) ;
                    }
               return 0 ;

          case WM_HSCROLL :
               switch (wParam)
                    {
                    case SB_LINEUP :
                         nHscrollInc = -1 ;
                         break ;

                    case SB_LINEDOWN :
                         nHscrollInc = 1 ;
                         break ;

                    case SB_PAGEUP :
                         nHscrollInc = -8 ;
                         break ;

                    case SB_PAGEDOWN :
                         nHscrollInc = 8 ;
                         break ;

                    case SB_THUMBPOSITION :
                         nHscrollInc = LOWORD (lParam) - nHscrollPos ;
                         break ;

                    default :
                         nHscrollInc = 0 ;
                    }
               if (nHscrollInc = max (-nHscrollPos,
                         min (nHscrollInc, nHscrollMax - nHscrollPos)))
                    {
                    nHscrollPos += nHscrollInc ;
                    ScrollWindow (hwnd, -cxChar * nHscrollInc, 0, NULL,
NULL) ;
                    SetScrollPos (hwnd, SB_HORZ, nHscrollPos, TRUE) ;
                    }
               return 0 ;

          case WM_KEYDOWN :
               switch (wParam)
                    {
                    case VK_HOME :
                         SendMessage (hwnd, WM_VSCROLL, SB_TOP, 0L) ;
                         break ;
                    case VK_END :
                         SendMessage (hwnd, WM_VSCROLL, SB_BOTTOM, 0L) ;
                         break ;

                    case VK_PRIOR :
                         SendMessage (hwnd, WM_VSCROLL, SB_PAGEUP, 0L) ;
                         break ;

                    case VK_NEXT :
                         SendMessage (hwnd, WM_VSCROLL, SB_PAGEDOWN, 0L) ;
                         break ;

                    case VK_UP :
                         SendMessage (hwnd, WM_VSCROLL, SB_LINEUP, 0L) ;
                         break ;

                    case VK_DOWN :
                         SendMessage (hwnd, WM_VSCROLL, SB_LINEDOWN, 0L) ;
                         break ;

                    case VK_LEFT :
                         SendMessage (hwnd, WM_HSCROLL, SB_PAGEUP, 0L) ;
                         break ;

                    case VK_RIGHT :
                         SendMessage (hwnd, WM_HSCROLL, SB_PAGEDOWN, 0L) ;
                         break ;
                    }
               return 0 ;

          case WM_PAINT :
               hdc = BeginPaint (hwnd, &ps) ;

               nPaintBeg = max (0, nVscrollPos + ps.rcPaint.top / cyChar -
1) ;
               nPaintEnd = min (NUMLINES,
                                nVscrollPos + ps.rcPaint.bottom / cyChar) ;

               for (i = nPaintBeg ; i < nPaintEnd ; i++)
                    {
                    x = cxChar * (1 - nHscrollPos) ;
                    y = cyChar * (1 - nVscrollPos + i) ;

                    TextOut (hdc, x, y,
                             sysmetrics[i].szLabel,
                    lstrlen (sysmetrics[i].szLabel)) ;

                    TextOut (hdc, x + 18 * cxCaps, y,
                             sysmetrics[i].szDesc,
                    lstrlen (sysmetrics[i].szDesc)) ;
                    SetTextAlign (hdc, TA_RIGHT | TA_TOP) ;

                    TextOut (hdc, x + 18 * cxCaps + 40 * cxChar, y,
                             szBuffer,
                             wsprintf (szBuffer, "%5d",
                    GetSystemMetrics (sysmetrics[i].nIndex))) ;

                    SetTextAlign (hdc, TA_LEFT | TA_TOP) ;
                    }

               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }

     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 SYSMETS.DEF

;------------------------------------
; SYSMETS.DEF module definition file
;------------------------------------

NAME           SYSMETS

DESCRIPTION    'System Metrics Display (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc



CHARACTER MESSAGES

Earlier I discussed the idea of translating keystroke messages into
character messages by taking into account shift-state information, and I
warned that shift-state information is not enough: You also need to know
about country-dependent keyboard configurations. For this reason, you should
not attempt to translate keystroke messages into character codes yourself.

Windows does it for you. You've seen this code before:

while (GetMessage (&msg, NULL, 0, 0))
     {
     TranslateMessage (&msg) ;
     DispatchMessage (&msg) ;
     }

This is a typical message loop that appears in WinMain. The GetMessage
function fills in the msg structure fields with the next message from the
queue. DispatchMessage calls the appropriate window procedure with this
message.

Between these two functions is TranslateMessage, which translates keystroke
messages into character messages. If the message is WM_KEYDOWN or
WM_SYSKEYDOWN, and if the keystroke in combination with the shift states
produces a character, then TranslateMessage places a character message in
the message queue. This character message will be the next message that
GetMessage retrieves from the queue after the keystroke message.

There are four character messages:

                       Characters  Dead Characters
────────────────────────────────────────────────────────────────────────────
Nonsystem Characters:  WM_CHAR     WM_DEADCHAR
System Characters:     WM_SYSCHAR  WM_SYSDEADCHAR

The WM_CHAR and WM_DEADCHAR messages are derived from WM_KEYDOWN messages.
The WM_SYSCHAR and WM_SYSDEADCHAR messages are derived from WM_SYSKEYDOWN
messages. In most cases, your Windows program can ignore everything except
WM_CHAR messages. The lParam parameter passed to the window procedure with
the character code message is the same as the lParam parameter for the
keystroke message that generated the character code message. The wParam
parameter is the ASCII code for the character (yes, good old familiar
ASCII).

The character messages are delivered to your window procedure sandwiched
between keystroke messages. For instance, if Caps Lock is not toggled on and
you press and release the A key, the window procedure receives the following
three messages:

Message                    Key or Code
────────────────────────────────────────────────────────────────────────────
WM_KEYDOWN                 Virtual key A
WM_CHAR                    ASCII code a
WM_KEYUP                   Virtual key A

If you type an uppercase A by pressing the Shift key, pressing the A key,
releasing the A key, and then releasing the Shift key, the window procedure
receives five messages:

Message                  Key or Code
────────────────────────────────────────────────────────────────────────────
WM_KEYDOWN               Virtual key VK_SHIFT
WM_KEYDOWN               Virtual key A
WM_CHAR                  ASCII code A
WM_KEYUP                 Virtual key A
WM_KEYUP                 Virtual key VK_SHIFT

The Shift key by itself does not generate a character message.

If you hold down the A key so that the typematic action generates
keystrokes, you'll get a character message for each WM_KEYDOWN message:

Message                    Key or Code
────────────────────────────────────────────────────────────────────────────
WM_KEYDOWN                 Virtual key A
WM_CHAR                    ASCII code a
WM_KEYDOWN                 Virtual key A
WM_CHAR                    ASCII code a
WM_KEYDOWN                 Virtual key A
WM_CHAR                    ASCII code a
WM_KEYDOWN                 Virtual key A
WM_CHAR                    ASCII code a
WM_KEYUP                   Virtual key A

If some of the WM_KEYDOWN messages have a Repeat Count greater than 1, the
corresponding WM_CHAR messages will have the same Repeat Count.

The Ctrl key in combination with a letter key generates ASCII control codes
from 01H (Ctrl-A) through 1AH (Ctrl-Z). You can also use other keys to
generate these control codes. The following table shows the value of wParam
in a WM_CHAR message for keys that generate control codes:

Key         ASCII Code  Duplicated by
────────────────────────────────────────────────────────────────────────────
Backspace   08H         Ctrl-H
Tab         09H         Ctrl-I
Ctrl-Enter  0Ah         Ctrl-J
Enter       0Dh         Ctrl-M
Esc         1BH         Ctrl-[

Windows programs sometimes use the Ctrl key in combination with letter keys
for menu accelerators, in which case the letter keys are not translated into
character messages.

WM_CHAR Messages

When your Windows program needs to process characters from the keyboard (for
instance, in a word-processing or communications program), it will process
WM_CHAR messages. You'll probably want some special processing for the
Backspace, Tab, and Enter keys (and perhaps the Linefeed key), but you'll
treat all other characters the same:

case WM_CHAR :

     switch (wParam)
          {
          case '\b' :    // backspace
[other program lines]
               break ;

          case '\t' :    // tab
[other program lines]
               break ;

          case '\n' :    // linefeed
[other program lines]
               break ;

          case '\r' :    // carriage return
[other program lines]
               break ;

          default :      // character code
[other program lines]
               break ;
          }
     return 0 ;

This program fragment is virtually identical to keyboard character
processing in regular MS-DOS programs.


Dead-Character Messages

Windows programs can usually ignore WM_DEADCHAR and WM_SYSDEADCHAR messages.
On some non-U.S. keyboards, certain keys are defined to add a diacritic to a
letter. These are called "dead keys" because they don't create characters by
themselves. For instance, when the German keyboard is installed, the key
that is in the same position as the +/= key on a U.S. keyboard is a dead key
for the acute accent (\a) when unshifted and the grave accent (\g) when
shifted.

When a user presses this dead key, your window procedure receives a
WM_DEADCHAR message with wParam equal to the ASCII code for the diacritic by
itself. When the user then presses a letter key (for instance, the A key),
the window procedure receives a WM_CHAR message where wParam is the ASCII
code for the letter a with the diacritic. Thus, your program does not have
to process the WM_DEADCHAR message, because the WM_CHAR message gives the
program all the information it needs. The Windows logic even has built-in
error handling: If the dead key is followed by a letter that can't take a
diacritic (such as the letter s), then the window procedure receives two
WM_CHAR messages in a row--the first with wParam equal to the ASCII code for
the diacritic by itself (the same wParam value delivered with the
WM_DEADCHAR message) and the second with wParam equal to the ASCII code for
the letter s.



LOOKING AT KEYBOARD MESSAGES

If you'd like to see how Windows sends keyboard messages to a program,
KEYLOOK, shown in Figure 3-3, will help. This program displays in its client
area all the information that Windows sends the window procedure for the
eight different keyboard messages.

 KEYLOOK.MAK

#-----------------------
# KEYLOOK.MAK make file
#-----------------------

keylook.exe : keylook.obj keylook.def
     link keylook, /align:16, NUL, /nod slibcew libw, keylook
     rc keylook.exe

keylook.obj : keylook.c
     cl -c -Gsw -Ow -W2 -Zp keylook.c

 KEYLOOK.C

/*-------------------------------------------------------
   KEYLOOK.C -- Displays Keyboard and Character Messages
                (c) Charles Petzold, 1990
  -------------------------------------------------------*/

#include <windows.h>
#include <stdio.h>

long  FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;



RECT  rect ;
short cxChar, cyChar ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName[] = "KeyLook" ;
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "Keyboard Message Looker",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

void ShowKey (HWND hwnd, int iType, char *szMessage, WORD wParam, LONG
lParam)
     {
     static char *szFormat[2] = { "%-14s %3d    %c %6u %4d %3s %3s %4s %4s",
                                  "%-14s    %3d %c %6u %4d %3s %3s %4s %4s"
} ;
     char        szBuffer[80] ;
     HDC         hdc ;
     ScrollWindow (hwnd, 0, -cyChar, &rect, &rect) ;
     hdc = GetDC (hwnd) ;

     SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;

     TextOut (hdc, cxChar, rect.bottom - cyChar, szBuffer,
              wsprintf (szBuffer, szFormat [iType],
                        (LPSTR) szMessage, wParam,
                        (BYTE) (iType ? wParam : ' '),
                        LOWORD (lParam),
                        HIWORD (lParam) & 0xFF,
                        (LPSTR) (0x01000000 & lParam ? "Yes"  : "No"),
                        (LPSTR) (0x20000000 & lParam ? "Yes"  : "No"),
                        (LPSTR) (0x40000000 & lParam ? "Down" : "Up"),
                        (LPSTR) (0x80000000 & lParam ? "Up"   : "Down"))) ;

     ReleaseDC (hwnd, hdc) ;
     ValidateRect (hwnd, NULL) ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static char szTop[] =
                    "Message        Key Char Repeat Scan Ext ALT Prev Tran";
     static char szUnd[]=
                    "_______        ___ ____ ______ ____ ___ ___ ____ ____";
     HDC         hdc ;
     PAINTSTRUCT ps ;
     TEXTMETRIC  tm ;

     switch (message)
          {
          case WM_CREATE :
               hdc = GetDC (hwnd) ;

               SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;

               GetTextMetrics (hdc, &tm) ;
               cxChar = tm.tmAveCharWidth ;
               cyChar = tm.tmHeight ;

               ReleaseDC (hwnd, hdc) ;

               rect.top = 3 * cyChar / 2 ;
               return 0 ;

          case WM_SIZE :
               rect.right  = LOWORD (lParam) ;
               rect.bottom = HIWORD (lParam) ;
               UpdateWindow (hwnd) ;
               return 0 ;
          case WM_PAINT :
               InvalidateRect (hwnd, NULL, TRUE) ;
               hdc = BeginPaint (hwnd, &ps) ;

               SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;

               SetBkMode (hdc, TRANSPARENT) ;
               TextOut (hdc, cxChar, cyChar / 2, szTop, (sizeof szTop) - 1)
;
               TextOut (hdc, cxChar, cyChar / 2, szUnd, (sizeof szUnd) - 1)
;
               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_KEYDOWN :
               ShowKey (hwnd, 0, "WM_KEYDOWN", wParam, lParam) ;
               return 0 ;

          case WM_KEYUP :
               ShowKey (hwnd, 0, "WM_KEYUP", wParam, lParam) ;
               return 0 ;

          case WM_CHAR :
               ShowKey (hwnd, 1, "WM_CHAR", wParam, lParam) ;
               return 0 ;

          case WM_DEADCHAR :
               ShowKey (hwnd, 1, "WM_DEADCHAR", wParam, lParam) ;
               return 0 ;

          case WM_SYSKEYDOWN :
               ShowKey (hwnd, 0, "WM_SYSKEYDOWN", wParam, lParam) ;
               break ;        // i.e., call DefWindowProc

          case WM_SYSKEYUP :
               ShowKey (hwnd, 0, "WM_SYSKEYUP", wParam, lParam) ;
               break ;        // i.e., call DefWindowProc

          case WM_SYSCHAR :
               ShowKey (hwnd, 1, "WM_SYSCHAR", wParam, lParam) ;
               break ;        // i.e., call DefWindowProc

          case WM_SYSDEADCHAR :
               ShowKey (hwnd, 1, "WM_SYSDEADCHAR", wParam, lParam) ;
               break ;        // i.e., call DefWindowProc

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 KEYLOOK.DEF

;------------------------------------
; KEYLOOK.DEF module definition file
;------------------------------------

NAME           KEYLOOK

DESCRIPTION    'Key Look Program (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

KEYLOOK uses the display like an old-fashioned teletype output device. When
KEYLOOK receives a keystroke message, it calls ScrollWindow to scroll the
contents of the entire client area of the window so that the contents move
up the height of one character. TextOut is used to display the line of new
information beginning one character height from the bottom. This is about as
simple as a teletype output can get. Figure 3-4 shows what the KEYLOOK
display looks like when you type the word "Windows." The first column shows
the keyboard message, the second shows the virtual key code for keystroke
messages, the

  (Figure 3-4. may be found in the printed book.)

third shows the character code (and the character itself) for character
messages, and the other six columns show the states of the six fields in the
lParam message parameter.

Most of KEYLOOK.C uses features of Windows that have already been covered in
the various SYSMETS programs, but a few new functions are used here.

The column formatting of KEYLOOK would be difficult with the default
proportional font. The code to display each line would need to be broken
into nine sections to get everything lined up. For something like this, a
much easier approach is to simply switch to a fixed-pitch font. This
requires two functions in a single statement:

SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;

KEYLOOK calls these two functions whenever it obtains a device context. This
occurs in three places: the ShowKey function, while processing the WM_CREATE
message in WndProc, and while processing the WM_PAINT message. The
GetStockObject function obtains a handle to a "stock" graphics object, which
is a predefined graphics object that Windows makes available to programs. In
this case, GetStockObject obtains a handle to a font known as
SYSTEM_FIXED_FONT, which is the fixed-pitch font that was used in versions
of Windows prior to Windows 3. The SelectObject call places that object into
the device context. Following this call, all text that is displayed will use
the fixed-pitch font. It is possible to switch back to the default
proportional font by calling:

SelectObject (hdc, GetStockObject (SYSTEM_FONT)) ;

I'll discuss these functions in more depth in Chapter 12.

The ShowKey function calls ScrollWindow to scroll the previous lines of
keystrokes up before displaying a new line. Normally this would cause part
of the window to become invalid and hence generate a WM_PAINT message. The
ShowKey function concludes with a call to ValidateRect to prevent this.

Notice the use of the Windows wsprintf function in the ShowKey function. The
character strings must be explicitly cast to far pointers using the LPSTR
data type (defined in WINDOWS.H as a far pointer to a character string). The
wsprintf function is one of the very few functions in Windows that
explicitly requires casting of its parameters.

KEYLOOK does not save the keystrokes it receives, so on receipt of a
WM_PAINT message it cannot re-create the window. For this reason, KEYLOOK
simply displays the header at the top of the client area during the WM_PAINT
message. Before calling BeginPaint during the WM_PAINT message, KEYLOOK
invalidates the entire window. This allows the whole window to be erased
rather than just the invalid rectangle.

(That KEYLOOK does not save the keystrokes and hence cannot redraw the
window during a WM_PAINT message is certainly a flaw. The TYPE program shown
later in this chapter corrects this flaw.)

KEYLOOK draws a header at the top of the client area identifying the nine
columns. Although it's possible to create an underlined font, I took a
slightly different approach  here. I defined two character string variables
named szTop (which has the text) and szUnd (which has the underlining) and
displayed both of them at the same position at the top of the window during
the WM_PAINT message. Normally, Windows displays text in an  "opaque" mode,
meaning that Windows erases the character background area while displaying a
character. This would cause the second character string (szUnd) to erase the
first (szTop). To prevent this, switch the device context into the
"transparent" mode:

SetBkMode (hdc, TRANSPARENT) ;


THE CARET (NOT THE CURSOR)

When you type text into a program, generally a little underline or box shows
you where the next character you type will appear on the screen. You may
know this as a "cursor," but you'll have to get out of that habit when
programming for Windows. In Windows, it's called the "caret." The word
"cursor" is used for the bitmap image that represents the mouse position.

The Caret Functions

There are five essential caret functions:

  ■   CreateCaret creates a caret associated with a window.

  ■   SetCaretPos sets the position of the caret on the window.

  ■   ShowCaret shows the caret.

  ■   HideCaret hides the caret.

  ■   DestroyCaret destroys the caret.

There are also functions to get the caret position (GetCaretPos) and to get
and set the caret blink time (GetCaretBlinkTime and SetCaretBlinkTime).

The caret is customarily a horizontal line, or a box that is the size of a
character, or a vertical line. The vertical line is recommended when you use
a proportional font such as the Windows default system font. Because the
characters in a proportional font are not a fixed size, the horizontal line
and box can't be set to the size of a character.

You cannot simply create a caret during the WM_CREATE message and destroy it
during the WM_DESTROY message. The caret is what is known as a "systemwide
resource." What this means is that there is only one caret in the system. In
effect, a program "borrows" the caret from the system when it needs to
display a caret in its window.

Does this sound bizarrely restrictive? It's really not. Think about it: The
display of a caret in a window makes sense only when the window has the
input focus. This indicates to the user that he or she may enter text in the
program. Only one window has the input focus at any time so only one caret
is needed in the whole system.

A program can determine if it has the input focus by processing the
WM_SETFOCUS and WM_KILLFOCUS messages. A window procedure receives a
WM_SETFOCUS message when it receives the input focus, and a WM_KILLFOCUS
message when it loses the input focus. These messages occur in pairs: A
window procedure will always receive a WM_SETFOCUS message before it
receives a WM_KILLFOCUS message, and it always receives an equal number of
WM_SETFOCUS and WM_KILLFOCUS messages over the course of the window's
lifetime.

The main rule for using the caret is simple: A window procedure calls
CreateCaret during the WM_SETFOCUS message and DestroyCaret during the
WM_KILLFOCUS message.

There are a few other rules: The caret is created hidden. After calling
CreateCaret, the window procedure must call ShowCaret for the caret to be
visible. In addition, the window procedure must hide the caret by calling
HideCaret whenever it draws something on its window during a message other
than WM_PAINT. After it finishes drawing on the window, it calls ShowCaret
to display the caret again. The effect of HideCaret is additive: If you call
HideCaret several times without calling ShowCaret, you must call ShowCaret
the same number of times before the caret becomes visible again.


The TYPE Program

The TYPE program shown in Figure 3-5 (beginning on the following page)
brings together much of what we've learned in this chapter. You can think of
TYPE as an extremely rudimentary text editor. You can type in the window,
move the cursor (I mean caret) around with the cursor movement (or are they
caret movement?) keys, and erase the contents of the window by pressing
Escape. The contents of the window are also erased when you resize the
window. There's no scrolling, no search and replace, no way to save files,
and no spell checker, but it's a start.

To make things easy for myself, TYPE uses SYSTEM_FIXED_FONT. Writing a text
editor for a proportional font is, as you might imagine, much more
difficult. The program obtains a device context in several places: during
the WM_CREATE message, the WM_KEYDOWN message, the WM_CHAR message, and the
WM_PAINT message. Each time, calls to GetStockObject and SelectObject select
the fixed-pitch font.

During the WM_SIZE message, TYPE calculates the character width and height
of the window and saves these values in the variables cxBuffer and cyBuffer.
It then uses malloc to allocate a buffer to hold all the characters that can
be typed in the window. The xCaret and yCaret variables store the character
position of the caret.

During the WM_SETFOCUS message, TYPE calls CreateCursor to create a cursor
that is the width and height of a character, SetCaretPos to set the caret
position, and ShowCaret to make the caret visible. During the WM_KILLFOCUS
message, TYPE calls HideCaret and DestroyCaret.

 TYPE.MAK

#--------------------
# TYPE.MAK make file
#--------------------

type.exe : type.obj type.def
     link type, /align:16, NUL, /nod slibcew libw, type
     rc type.exe

type.obj : type.c
     cl -c -Gsw -Ow -W2 -Zp type.c

 TYPE.C

/*-------------------------------------
   TYPE.C -- Typing Program
             (c) Charles Petzold, 1990
  -------------------------------------*/

#include <windows.h>
#include <stdlib.h>

#define BUFFER(x, y) *(pBuffer + y * cxBuffer + x)

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName[] = "Type" ;
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;



          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "Typing Program",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static char *pBuffer = NULL ;
     static int  cxChar, cyChar, cxClient, cyClient, cxBuffer, cyBuffer,
                 xCaret, yCaret ;
     HDC         hdc ;
     int         x, y, i ;
     PAINTSTRUCT ps ;
     TEXTMETRIC  tm ;

     switch (message)
          {
          case WM_CREATE :
               hdc = GetDC (hwnd) ;

               SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;
               GetTextMetrics (hdc, &tm) ;
               cxChar = tm.tmAveCharWidth ;
               cyChar = tm.tmHeight ;

               ReleaseDC (hwnd, hdc) ;
               return 0 ;

          case WM_SIZE :
                                   // obtain window size in pixels
               cxClient = LOWORD (lParam) ;
               cyClient = HIWORD (lParam) ;

                                   // calculate window size in characters

               cxBuffer = max (1, cxClient / cxChar) ;
               cyBuffer = max (1, cyClient / cyChar) ;

                                   // allocate memory for buffer and clear
it

               if (pBuffer != NULL)
                    free (pBuffer) ;

               if ((LONG) cxBuffer * cyBuffer > 65535L ||
                         (pBuffer = malloc (cxBuffer * cyBuffer)) == NULL)

                    MessageBox (hwnd, "Window too large.  Cannot "
                                      "allocate enough memory.", "Type",
                                MB_ICONEXCLAMATION | MB_OK) ;

               else
                    for (y = 0 ; y < cyBuffer ; y++)
                         for (x = 0 ; x < cxBuffer ; x++)
                              BUFFER(x, y) = ' ' ;

                                   // set caret to upper left corner
               xCaret = 0 ;
               yCaret = 0 ;

               if (hwnd == GetFocus ())
                    SetCaretPos (xCaret * cxChar, yCaret * cyChar) ;

               return 0 ;

          case WM_SETFOCUS :
                                   // create and show the caret

               CreateCaret (hwnd, NULL, cxChar, cyChar) ;
               SetCaretPos (xCaret * cxChar, yCaret * cyChar) ;
               ShowCaret (hwnd) ;
               return 0 ;

          case WM_KILLFOCUS :
                                   // hide and destroy the caret
               HideCaret (hwnd) ;
               DestroyCaret () ;
               return 0 ;
          case WM_KEYDOWN :
               switch (wParam)
                    {
                    case VK_HOME :
                         xCaret = 0 ;
                         break ;

                    case VK_END :
                         xCaret = cxBuffer - 1 ;
                         break ;

                    case VK_PRIOR :
                         yCaret = 0 ;
                         break ;

                    case VK_NEXT :
                         yCaret = cyBuffer - 1 ;
                         break ;

                    case VK_LEFT :
                         xCaret = max (xCaret - 1, 0) ;
                         break ;

                    case VK_RIGHT :
                         xCaret = min (xCaret + 1, cxBuffer - 1) ;
                         break ;

                    case VK_UP :
                         yCaret = max (yCaret - 1, 0) ;
                         break ;

                    case VK_DOWN :
                         yCaret = min (yCaret + 1, cyBuffer - 1) ;
                         break ;

                    case VK_DELETE :
                         for (x = xCaret ; x < cxBuffer - 1 ; x++)
                              BUFFER (x, yCaret) = BUFFER (x + 1, yCaret) ;

                         BUFFER (cxBuffer - 1, yCaret) = ' ' ;

                         HideCaret (hwnd) ;
                         hdc = GetDC (hwnd) ;

                         SelectObject (hdc,
                              GetStockObject (SYSTEM_FIXED_FONT)) ;

                         TextOut (hdc, xCaret * cxChar, yCaret * cyChar,
                                  & BUFFER (xCaret, yCaret),
                                  cxBuffer - xCaret) ;
                         ShowCaret (hwnd) ;
                         ReleaseDC (hwnd, hdc) ;
                         break ;
                    }

               SetCaretPos (xCaret * cxChar, yCaret * cyChar) ;
               return 0 ;

          case WM_CHAR :
               for (i = 0 ; i < LOWORD (lParam) ; i++)
                    {
                    switch (wParam)
                         {
                         case '\b' :                    // backspace
                              if (xCaret > 0)
                                   {
                                   xCaret-- ;
                                   SendMessage (hwnd, WM_KEYDOWN,
                                                VK_DELETE, 1L) ;
                                   }
                              break ;

                         case '\t' :                    // tab
                              do
                                   {
                                   SendMessage (hwnd, WM_CHAR, ' ', 1L) ;
                                   }
                              while (xCaret % 8 != 0) ;
                              break ;

                         case '\n' :                    // linefeed
                              if (++yCaret == cyBuffer)
                                   yCaret = 0 ;
                              break ;

                         case '\r' :                    // carriage return
                              xCaret = 0 ;

                              if (++yCaret == cyBuffer)
                                   yCaret = 0 ;
                              break ;

                         case '\x1B' :                  // escape
                              for (y = 0 ; y < cyBuffer ; y++)
                                   for (x = 0 ; x < cxBuffer ; x++)
                                        BUFFER (x, y) = ' ' ;

                              xCaret = 0 ;
                              yCaret = 0 ;
                              InvalidateRect (hwnd, NULL, FALSE) ;
                              break ;

                         default :                      // character codes
                              BUFFER (xCaret, yCaret) = (char) wParam ;

                              HideCaret (hwnd) ;
                              hdc = GetDC (hwnd) ;

                              SelectObject (hdc,
                                   GetStockObject (SYSTEM_FIXED_FONT)) ;

                              TextOut (hdc, xCaret * cxChar, yCaret *
cyChar,
                                       & BUFFER (xCaret, yCaret), 1) ;

                              ShowCaret (hwnd) ;
                              ReleaseDC (hwnd, hdc) ;

                              if (++xCaret == cxBuffer)
                                   {
                                   xCaret = 0 ;

                                   if (++yCaret == cyBuffer)
                                        yCaret = 0 ;
                                   }
                              break ;
                         }
                    }

               SetCaretPos (xCaret * cxChar, yCaret * cyChar) ;
               return 0 ;

          case WM_PAINT :
               hdc = BeginPaint (hwnd, &ps) ;
               SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;

               for (y = 0 ; y < cyBuffer ; y++)
                    TextOut (hdc, 0, y * cyChar, & BUFFER(0, y), cxBuffer) ;

               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 TYPE.DEF

;---------------------------------
; TYPE.DEF module definition file
;---------------------------------

NAME           TYPE

DESCRIPTION    'Typing Program (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

The processing of the WM_KEYDOWN and WM_CHAR messages is more extensive. The
WM_KEYDOWN processing mostly involves the cursor movement keys. Home and End
send the caret to the beginning and end of a line respectively, and Page Up
and Page Down send the caret to the top and bottom of the window. The arrow
keys work as you would expect. For the Delete key, TYPE must move everything
left in the buffer from the next caret position to the end of the line and
then display a blank at the end of the line.

The WM_CHAR processing handles the Backspace, Tab, Linefeed (Ctrl-Enter),
Enter, Escape, and character keys. Notice I've used Repeat Count in lParam
when processing the WM_CHAR message (under the assumption that every
character the user types is important) but not during the WM_KEYDOWN message
(to prevent inadvertent overscrolling). The Backspace and Tab processing is
simplified somewhat by the use of the SendMessage function. Backspace is
emulated by the Delete logic, and Tab is emulated by a series of spaces.

As I mentioned earlier, you should hide the cursor when drawing on the
window during messages other then WM_PAINT. The program does this when
processing the WM_KEYDOWN message for the Delete key and the WM_CHAR message
for character keys. In both these cases, TYPE alters the contents of the
buffer and then draws the new character or characters on the window.

I use TYPE when working on speeches, as shown in Figure 3-6.

  (Figure 3-6. may be found in the printed book.)



THE WINDOWS CHARACTER SETS

I mentioned earlier that letter keys preceded by dead-character keys
generate WM_CHAR messages where wParam is the ASCII code for a character
with a diacritic. This may be a little puzzling because the ASCII character
set doesn't include any codes for characters with diacritics. What exactly
is the value of wParam in this case? The answer to this question requires
that we tackle the subject of character sets, a topic that may at first seem
more appropriate for a later discussion about character fonts. However, it
is also of vital importance in keyboard handling.

The standard 7-bit ASCII character set defines codes from 0 through 31 (1FH)
and 127 (7FH) as control characters, and it defines codes from 32 (20H)
through 126 (7EH) as displayable characters. None of these characters have
diacritics. Because personal computers use 8-bit bytes, computer
manufacturers often define character sets that use 256 codes rather than the
128 ASCII codes. The additional codes may be assigned characters with
diacritics. The resultant "extended character set" then includes the ASCII
character set and up to 128 other characters.

If Windows supported such an extended character set, displaying characters
with diacritics would be easy. But Windows doesn't support a simple extended
character set. Windows supports two extended character sets. Unfortunately,
the presence of these two character sets doesn't make things twice as easy.

The OEM Character Set

First, let's go back to the hardware that Windows runs on--the IBM PC and
compatibles. In the early 1980s the developers of the IBM PC decided to
extend the ASCII character set as shown in Figure 3-7. The codes from 20H
through 7EH are displayable characters from the ASCII character set. The
rest are nonstandard--or at least were at the time.

This character set cannot be ignored. It is encoded in millions of ROM chips
in IXC video adapters, printers, and system board BIOS's. It has been
duplicated in the hardware of numerous manufacturers of IXC-compatible
computers and peripherals. This character set is part of what is meant by
the phrase "the IXC standard." Many programs written for the IBM PC require
this extended character set because they use the block-drawing and
line-drawing characters (codes B0H through DFH) in their screen output.

  (Figure 3-7. may be found in the printed book.)

The only problem is this: The IXC extended character set is inappropriate
for Windows. First, the block-drawing and line-drawing characters commonly
used by PC programs in text-mode applications are not needed in Windows
because Windows does real graphics. If you want to draw a horizontal line in
Windows, it's easier to draw a line than to display a string of C4H
characters. Second, the Greek alphabet and mathematical symbols are less
important in Windows than are the accented letters used in most European
languages. A program that needs to display mathematical symbols can best
draw them using graphics functions.

In short, Windows supports the IXC character set, but it is relegated to
secondary importance, mostly for old applications that run in a window.
Windows applications do not normally use the IXC character set. In Windows
documentation, the IXC character set is referred to as the "OEM character
set." The OEM character set is more precisely defined as the character set
that is native to the machine currently running Windows.

International Support under DOS

There are a number of variants on the IBM PC character set, called "code
pages." The variant used in the United States and in most European countries
is called Code Page 437. Systems sold in Norway, Denmark, Portugal, and a
few other European countries use different, special, code pages, which
contain more of the special characters required by the languages of those
countries. Recently, a number of these countries began to use Code Page 850,
which contains fewer graphics symbols and more accented letters and other
special characters.

Windows 3.0 supports code pages by installing OEM fonts (used for running
DOS applications in windows and in the clipboard viewer), which correspond
to the system's code page, and by installing appropriate translation tables
for the AnsiToOem and OemToAnsi functions (discussed later). If the system
is running DOS version 3.3 or later, the Windows Setup program will use the
current DOS code page. For earlier versions of DOS, Setup will select a code
page based on the localized (national) version of Windows.



The ANSI Character Set

The extended character set that Windows and Windows programs use for most
purposes is called the "ANSI character set." When your program receives a
WM_CHAR message, the wParam parameter is the ANSI character code. The ANSI
character set is shown in Figure 3-8. As you can see, the codes from 20H
through 7EH represent the same characters that  appear in the OEM character
set and the ASCII character set. The characters displayed as solid blocks
are undefined characters. They may appear differently on other output
devices (such as a printer).

  (Figure 3-8. may be found in the printed book.)


OEM, ANSI, and Fonts

Windows has different fonts for displaying the ANSI and OEM character sets.
When you first obtain a handle to a device context, one of the attributes in
the device context is a font.

By default this is the SYSTEM_FONT or "system font," which uses the ANSI
character set. If you want to display characters from the OEM character set,
you can select the OEM_FIXED_FONT (also called the "terminal font") in the
device context by using the following code:

SelectObject (hdc, GetStockObject (OEM_FIXED_FONT)) ;

This is the only font in Windows guaranteed to support the OEM character
set.



INTERNATIONALIZATION CONCERNS

Here's why we have to talk about fonts in the middle of the keyboard
chapter. We've established that when a Windows user on a non-U.S. keyboard
types a character with a diacritic, the wParam parameter of the WM_CHAR
message is the code for that character in the ANSI character set.

So, if you need to echo that character to the display, you had better be
using a font with the ANSI character set (such as the SYSTEM_FONT or
SYSTEM_FIXED_FONT). If you instead use the OEM_FIXED_FONT, the character you
write to the display will be incorrect and will surprise the user. A few
other simple rules will allow the keyboard logic in your Windows programs to
survive intact when you convert your programs for a Euro- pean market.

Working with the Character Set

When you get a WM_CHAR message, keep in mind that wParam may legitimately
have values above 128. Don't assume that anything above 127 is an invalid
character.

You may want to convert a character to uppercase. Don't use your own
algorithm:

if (ch >= 'a' && ch <= 'z')
     ch -= 32 ;               // WRONG !!!

That's a poor practice even when writing non-Windows C. But don't use the
standard C function either:

ch = toupper (ch) ;           // WRONG !!!

Both these functions work only for the lower half of the ANSI character set.
They will not convert a C0H to an E0H.

Instead, you should use the Windows functions AnsiUpper and AnsiLower. If
str is a zero-terminated character string, you can convert it to uppercase
using AnsiUpper:

AnsiUpper (pString) ;

or using the AnsiUpperBuff function for character strings that are not
zero-terminated:

AnsiUpperBuff (pString, nLength) ;

You can also use AnsiUpper to convert a single character, but some casting
is required because the high-order word of the parameter must be zero:

ch = AnsiUpper ((LPSTR) (LONG) (BYTE) ch) ;

If ch is defined as an unsigned character, the initial BYTE cast is not
required. Windows also includes AnsiLower and AnsiLowerBuff functions for
converting to lowercase.

If you are really serious about writing Windows programs that can be easily
converted to foreign languages, you should also investigate the AnsiNext and
AnsiPrev functions. These functions facilitate handling of multibyte
character sets. The Japanese character set requires more than 256
characters, some of which use 2 bytes. If you use normal C pointer
arithmetic to scan a string (perhaps searching for a backslash character in
a directory path string), you may think you've found the character when
you've really found the second byte of a 2-byte character code. AnsiNext and
AnsiPrev take a far pointer to a character string and return a far pointer
that has been correctly incremented or decremented past 2-byte character
codes.


Talking with MS-DOS

If Windows were the only operating environment running on a machine, then
you could forget about the OEM character set and use only the ANSI character
set. However, users can create files in the MS-DOS environment and use them
in Windows; they can also create files in Windows and use them when back in
MS-DOS. Unfortunately, MS-DOS uses the OEM character set.

Here's an example of the communications problems that can occur. Suppose
that a German-speaking PC user creates a file named \UUBUNGEN.TXT ("practice
exercises") in an MS-DOS program such as EDLIN. On the IBM PC, the \UU is
part of the IXC (that is, OEM) character set and has a code of 154 or 9AH.
(When using MS-DOS with a U.S. keyboard on an IBM PC, you can also create
this letter by typing Alt-154 using the numeric keypad.) MS-DOS uses that
character code in the directory entry of the file.

If a Windows program uses MS-DOS function calls to obtain a directory of
files and then writes them directly to the display using an ANSI character
set font, the first letter of \UUBUNGEN will show up as a solid block,
because the code 154 is one of the undefined characters in the ANSI
character set. The Windows program needs to convert the IXC extended
character set code of 154 (9AH) to an ANSI character set code of 220 (or
DCH), which is the letter \UU in the ANSI character set. That's what the
Windows function  OemToAnsi does for you. It requires two far pointers to
strings. The OEM characters in the first string are converted to ANSI
characters and stored in the second string:

OemToAnsi (lpszOemStr, lpszAnsiStr) ;

Now let's take the opposite example. The German-speaking user wants your
Windows program to create a file named \UUBUNGEN.TXT. The filename entered
by the user has a 220 (DCH) as the first character. If you use an MS-DOS
function call to open the file, MS-DOS uses that character in the filename.
When the user later looks at the file under MS-DOS, the first character
shows up as a block. Before you use the MS-DOS function calls, you must
convert the filename to the OEM character set:

AnsiToOem (lpszAnsiStr, lpszOemStr) ;

This converts a 220 (DCH) to a 154 (9AH). Windows also includes two
functions named AnsiToOemBuff and OemToAnsiBuff that do not require a
zero-terminated string.

Windows has an OpenFile call that will convert this for you. If you use
OpenFile, don't do your own AnsiToOem conversion. If you use MS-DOS function
calls to obtain lists of filenames (as the Windows File Manager program
does), then these filenames should be passed through OemToAnsi before being
displayed.

Converting the contents of files is another problem that arises when files
are used in both Windows and MS-DOS. If your Windows program uses files that
you are certain have been created in an MS-DOS program, then you may need to
pass the text contents of the file through the OemToAnsi function. (For
instance, Windows WRITE does this when converting Microsoft Word files to
WRITE format.) Similarly, if your Windows program is preparing a file for
use in an MS-DOS program, you may want to use AnsiToOem to convert the text.

The OemToAnsi and AnsiToOem functions are located in the keyboard driver.
They incorporate very simple lookup tables. The OemToAnsi routine converts
an OEM code from 80H through FFH to a character code in the ANSI set that
most closely resembles the OEM character. In some cases, this conversion is
only grossly approximate. For instance, most of the line-drawing characters
in the IXC character set are translated as plus signs, dashes, and vertical
lines. Most of the OEM codes from 00H through 1FH are not translated to ANSI
codes.

The AnsiToOem routine converts ANSI codes from A0H through FFH into codes in
the OEM set. The accented characters in the ANSI character set that do not
appear in the OEM character set are translated into regular ASCII codes for
the characters without the diacritics.


Using the Numeric Keypad

As you probably know, the IBM PC keyboard and BIOS let you enter codes for
the IXC extended character set by pressing and holding down the Alt key,
typing on the numeric keypad the three-digit decimal code representing the
OEM character, and releasing the Alt key. This facility is duplicated in
Windows in two ways:

First, when you type Alt-[OEM code] on the numeric keypad, Windows gives to
you the ANSI character code in the wParam parameter of the WM_CHAR message
that most closely approximates the OEM character represented by the OEM
code. That is, Windows passes the code through the OemToAnsi function before
generating the WM_CHAR message. This facility is for the user's convenience:
If you do not have a foreign-language keyboard and you are accustomed to
typing a \UU by typing Alt-154, you can do the same thing in a Windows
program. You don't need to relearn the ANSI character codes.

Second, if you want to generate ANSI extended character codes from the U.S.
keyboard, type Alt-0[OEM code] on the numeric keypad. The wParam parameter
of the WM_CHAR message is that OEM code. Thus, Alt-0220 is also a \UU. You
can try this out in the TYPE program.





Chapter 4  The Mouse
────────────────────────────────────────────────────────────────────────────

The mouse is a pointing device with one or more buttons. Although a mouse is
considered an important part of Windows' user interface, it is an optional
accessory. You can install Windows without a mouse, and you can control most
Windows programs entirely from the keyboard. Often, the most difficult
aspect of using the mouse in your program is adding a keyboard interface to
duplicate the mouse functions.

MOUSE BASICS

Windows can support a one-button, two-button, or three-button mouse or use a
joystick or light pen to mimic a one-button mouse. The support of a second
and third mouse button is rarely exploited, however. Unless you know your
Windows program will run only on machines equipped with a two-button or
three-button mouse, you must write programs for the lowest common
denominator and use only a single button.

You can determine if a mouse is present by using the GetSystemMetrics
function:

fMouse = GetSystemMetrics (SM_MOUSEPRESENT) ;

The value of fMouse will be TRUE (nonzero) if a mouse is installed. No
method is documented for determining the number of buttons on the installed
mouse.

Some Quick Definitions

When the Windows user moves the mouse, Windows moves a small bit-mapped
picture on the display called the "mouse cursor." The mouse cursor has a
single-pixel "hot spot" that points to a precise location on the display.

The display driver contains several predefined mouse cursors that programs
may use. The most common is the slanted arrow called IDC_ARROW in WINDOWS.H.
The hot spot is the tip of the arrow. The IDC_CROSS cursor (used in the
BLOWUP1 program shown in this chapter) has a hot spot in the center of a
cross-hair pattern. The IDC_WAIT cursor is an hourglass generally used by
programs to indicate they are busy. Programmers can also design their own
cursors (as we'll do in Chapter 8). The default cursor for a particular
window is specified when defining the window class structure, for instance:

wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;

The following terms describe the actions you take with mouse buttons:

  ■   Clicking--Pressing and releasing a mouse button

  ■   Double-clicking--Pressing and releasing a mouse button twice in quick
      succession

  ■   Dragging--Moving the mouse while holding down a button

On a three-button mouse, the buttons are called the left button, middle
button, and right button. Mouse-related identifiers defined in WINDOWS.H use
the abbreviations LBUTTON, MBUTTON, and RBUTTON. A two-button mouse has only
a left button and a right button. The single button on a one-button mouse is
a left button.



CLIENT-AREA MOUSE MESSAGES

In the previous chapter you saw how Windows sends keyboard messages only to
the window with the input focus. Mouse messages are different: A window
procedure receives mouse messages whenever the mouse passes over the window
or is clicked within the window, even if the window is not active or does
not have the input focus.

Windows defines 21 messages for the mouse. However, 11 of these messages do
not relate to the client area (hereinafter, "nonclient-area" messages), and
Windows programs usually ignore them. Of the 10 "client-area" mouse
messages, 6 pertain to the right and middle buttons. Windows programs
usually ignore these messages also.

When the mouse is moved over the client area of a window, the window
procedure receives the message WM_MOUSEMOVE. When a mouse button is pressed
or released within the client area of a window, the window procedure
receives these messages:

Button  Pressed         Released      Pressed (2d Click)
────────────────────────────────────────────────────────────────────────────
Left    WM_LBUTTONDOWN  WM_LBUTTONUP  WM_LBUTTONDBLCLK
Middle  WM_MBUTTONDOWN  WM_MBUTTONUP  WM_MBUTTONDBLCLK
Right   WM_RBUTTONDOWN  WM_RBUTTONUP  WM_RBUTTONDBLCLK

Your window procedure receives "MBUTTON" messages only for a three-button
mouse and "RBUTTON" messages only for a two-button or three-button mouse.
The window procedure receives "DBLCLK" (double-click) messages only if the
window class has been defined to receive them (as described below).

For all these messages, the value of lParam contains the position of the
mouse. The low word is the x-coordinate, and the high word is the
y-coordinate relative to the upper left corner of the client area of the
window. You can extract the x-coordinate and y-coordinate from lParam using
the LOWORD and HIWORD macros defined in WINDOWS.H. The value of wParam
indicates the state of the mouse buttons and the Shift and Ctrl keys. You
can test wParam using the bit masks defined in WINDOWS.H. The MK prefix
stands for "mouse key."

────────────────────────────────────────────────────────────────────────────
MK_LBUTTON               Left button is down
MK_MBUTTON               Middle button is down
MK_RBUTTON               Right button is down
MK_SHIFT                 Shift key is down
MK_CONTROL               Ctrl key is down

As you move the mouse over the client area of a window, Windows does not
generate a WM_MOUSEMOVE message for every possible pixel position of the
mouse. The number of WM_MOUSEMOVE messages your program receives depends on
the mouse hardware and on the speed at which your window procedure can
process the mouse movement messages. You'll get a good idea of the rate of
WM_MOUSEMOVE messages when you experiment with the CONNECT program described
below.

If you click the left mouse button in the client area of an inactive window,
Windows changes the active window to the window that is being clicked and
then passes the WM_LBUTTONDOWN message to the window procedure. When your
window procedure gets a WM_LBUTTONDOWN message, your program can safely
assume the window is active. However, your window procedure can receive a
WM_LBUTTONUP message without first receiving a WM_LBUTTONDOWN message. This
can happen if the mouse button is pressed in one window, moved to your
window, and released. Similarly, the window procedure can receive a
WM_LBUTTONDOWN without a corresponding WM_LBUTTONUP message if the mouse
button is released while positioned over another window.

There are two exceptions to these rules:

  ■   A window procedure can "capture the mouse" and continue to receive
      mouse messages even when the mouse is outside the window's client
      area. You'll learn how to capture the mouse later in this chapter.

  ■   If a system-modal message box or a system-modal dialog box is on the
      display, no other program can receive mouse messages. System-modal
      message boxes and dialog boxes prohibit switching to another window or


      program while the box is active. (An example of a system-modal message
      box is the one that says "This will end your Windows session" when you
      close the Program Manager.)

Simple Mouse Processing: An Example

The CONNECT program, shown in Figure 4-1, does some simple mouse processing
to let you get a good feel for how Windows sends your program mouse
messages.

 CONNECT.MAK

#-----------------------
# CONNECT.MAK make file
#-----------------------

connect.exe : connect.obj connect.def
     link connect, /align:16, NUL, /nod slibcew libw, connect
     rc connect.exe

connect.obj : connect.c
     cl -c -Gsw -Ow -W2 -Zp connect.c

 CONNECT.C

/*--------------------------------------------------
   CONNECT.C -- Connect-the-Dots Mouse Demo Program
                (c) Charles Petzold, 1990
  --------------------------------------------------*/

#include <windows.h>
#define MAXPOINTS 1000

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName[] = "Connect" ;
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;



     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "Connect-the-Dots Mouse Demo",
                         WS_OVERLAPPEDWINDOW,
                         CW_USEDEFAULT, CW_USEDEFAULT,
                         CW_USEDEFAULT, CW_USEDEFAULT,
                         NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static POINT points[MAXPOINTS] ;
     static short nCount ;
     HDC          hdc ;
     PAINTSTRUCT  ps ;
     short        i, j ;

     switch (message)
          {
          case WM_LBUTTONDOWN :
               nCount = 0 ;
               InvalidateRect (hwnd, NULL, TRUE) ;
               return 0 ;
          case WM_MOUSEMOVE :
               if (wParam & MK_LBUTTON && nCount < MAXPOINTS)
                    {
                    points [nCount++] = MAKEPOINT (lParam) ;
                    hdc = GetDC (hwnd) ;
                    SetPixel (hdc, LOWORD (lParam), HIWORD (lParam), 0L) ;
                    ReleaseDC (hwnd, hdc) ;
                    }
               return 0 ;

          case WM_LBUTTONUP :
               InvalidateRect (hwnd, NULL, FALSE) ;
               return 0 ;

          case WM_PAINT :
               hdc = BeginPaint (hwnd, &ps) ;

               for (i = 0 ; i < nCount - 1 ; i++)
                    for (j = i ; j < nCount ; j++)
                         {
                         MoveTo (hdc, points[i].x, points[i].y) ;
                         LineTo (hdc, points[j].x, points[j].y) ;
                         }

               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 CONNECT.DEF

;------------------------------------
; CONNECT.DEF module definition file
;------------------------------------

NAME           CONNECT

DESCRIPTION    'Mouse Connect Program (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

CONNECT processes three mouse messages:

  ■   WM_LBUTTONDOWN--CONNECT clears the client area.

  ■   WM_MOUSEMOVE--If the left button is down, CONNECT draws a black dot on
      the client area at the mouse position.

  ■   WM_LBUTTONUP--CONNECT connects every dot drawn in the client area to
      every other dot. Sometimes this results in a pretty design; sometimes
      in a dense blob. (See Figure 4-2.)

To use CONNECT, bring the mouse cursor into the client area, press the left
button, move the mouse around a little, and release the left button. CONNECT
works best for a curved pattern of a few dots, which you can draw by moving
the mouse quickly while the left button is depressed. CONNECT uses several
simple Graphics Device Interface (GDI) functions. SetPixel draws a one-pixel
dot of a particular color, in this case black. (On high-resolution displays,
the pixel may be nearly invisible.) Drawing the lines requires two
functions: MoveTo marks the x-coordinate and y-coordinate of the beginning
of the line, and LineTo draws the line.

If you move the mouse cursor out of the client area before releasing the
button, CONNECT does not connect the dots, because it doesn't receive the
WM_LBUTTONUP message. If you move the mouse back into the client area and
press the left button again, CONNECT clears the client area. (If you want to
continue a design after releasing the

  (Figure 4-2. may be found in the printed book.)

button outside the client area, press the left button again while the mouse
is outside the client area and then move the mouse back inside.)

CONNECT stores a maximum of 1000 points. The number of lines it draws is
equal to:


  (P) x (P - 1)

               where P is the number of points. With 1000 points, this
involves almost 500,000 lines, which can take several minutes to draw. For
anything but a demonstration program, this is too long for a Windows program
to hog system resources.

If CONNECT is busy drawing lines, you can press the mouse button, move the
mouse around, and release the mouse button, but nothing will happen. CONNECT
does not receive these messages because it is busy and not making any
GetMessage calls. After CONNECT finishes drawing the lines, it does not
receive these messages because the mouse button has been released already.
In this respect, the mouse is not like the keyboard. Windows treats every
keystroke as if it were important. However, if a mouse button is pressed and
released in the client area while a program is busy, the mouse clicks are
discarded.

Now try this: While CONNECT is engaged in a lengthy drawing routine, hold
down the mouse button and move the mouse around. After CONNECT is finished
drawing, it will retrieve the WM_LBUTTONDOWN message from the queue (and
clear the client area) because the button is currently down. However, it
receives only the WM_MOUSEMOVE messages that occur after it receives the
WM_LBUTTONDOWN message.

Sometimes the word "tracking" is used to refer to the way that programs
process mouse movement. Tracking does not mean, however, that your program
sits in a loop in its window procedure attempting to follow the mouse's
movements on the display. The window procedure instead processes each mouse
message as it comes and then quickly exits.


POINT, RECT, and lParam

CONNECT uses an array of POINT structures for saving points. The POINT
structure is defined in WINDOWS.H and has two fields named x and y:

typedef struct tagPOINT
  {
    int x ;
    int y ;
  } POINT ;

Some Windows functions require a POINT structure (or a pointer to a POINT
structure) as a parameter. You can define a POINT structure variable (named
point, for instance) in your program with this definition:

POINT point ;

If you need to convert an lParam value--the x and y mouse coordinates--to a
POINT structure, you can use the MAKEPOINT macro:

point = MAKEPOINT (lParam) ;

In WINDOWS.H, MAKEPOINT is defined like this:

#define MAKEPOINT(l) (*((POINT *)&l))

Despite the apparent complexity of this macro, it compiles very efficiently
because all it does is store lParam at the address of point. WINDOWS.H
defines the type PPOINT as a pointer to a POINT structure, so perhaps this
statement (without using the macro) makes the conversion a little clearer:

point = * (PPOINT) &lParam ;

(Remember that standard C order-of-evaluation rules cause address,
indirection, and type cast operators to be evaluated from right to left.)

The RECT structure defines a rectangle. Here's the WINDOWS.H definition:

typedef struct tagRECT
  {
    int left ;
    int top ;
    int right ;
    int bottom ;
  } RECT ;

This structure really contains two points side by side: left and right are
x-coordinates, and top and bottom are y-coordinates. You can define a
structure variable (named rect, for instance) with the statement:

RECT rect ;

Transferring coordinates from a RECT structure to a POINT structure is also
straightforward. This statement sets point to the upper left corner of the
rectangle:

point = * (PPOINT) &rect.left ;

This does the same for the lower right corner:

point = * (PPOINT) &rect.right ;

You can also define an array of two points:

POINT points [2] ;

and transfer these two points into a RECT structure:

rect = * (PRECT) points ;

PRECT is defined in WINDOWS.H as a pointer to a RECT structure. You don't
need the & (address) operator before points because points is an array. In
C, an array name is the address of the first element of the array.


Processing Shift Keys

When CONNECT receives a WM_MOUSEMOVE message, it performs a bitwise AND
operation on the value of wParam and MK_LBUTTON to determine if the left
button is depressed. You can also use wParam to determine the state of the
Shift keys. For instance, if processing must be dependent on the status of
the Shift and Ctrl keys, you might use logic that looks like this:

if (MK_SHIFT & wParam)
     if (MK_CONTROL & wParam)
          {
[Shift and Ctrl keys are down]
          }
     else
          {
[Shift key is down]
          }
else if (MK_CONTROL & wParam)
          {
[Ctrl key is down]
          }
     else
          {
[neither Shift nor Ctrl key is down]
          }

The Windows function GetKeyState (described in Chapter 3) can also return
the state of the mouse buttons or shift keys using the virtual-key codes
VK_LBUTTON, VK_RBUTTON, VK_MBUTTON, VK_SHIFT, and VK_CONTROL. The button or
key is down if the value returned from GetKeyState is negative. Because
GetKeyState returns mouse or key states as of the message currently being
processed, the status information is properly synchronized with the
messages. But just as you cannot use GetKeyState for a key that has yet to
be pressed, so you cannot use it for a mouse button that has yet to be
pressed. Don't do this:

while (GetKeyState (VK_LBUTTON) >= 0) ;  // WRONG !!!

The GetKeyState function will report that the left button is depressed only
if the button is already depressed when you process the message during which
you call GetKeyState.


Mouse Double-Clicks

A mouse double-click is two clicks in quick succession. To qualify as a
double-click, the two clicks must occur within a specific interval called
the "double-click time." If you want your window procedure to receive
double-click mouse messages, you must include the identifier CS_DBLCLKS when
initializing the window style in the window class structure before calling
RegisterClass:

wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS ;

If you do not include CS_DBLCLKS in the window style and the user clicks the
left mouse button twice in quick succession, your window procedure receives
these messages: WM_LBUTTONDOWN, WM_LBUTTONUP, WM_LBUTTONDOWN, and
WM_LBUTTONUP. (The window procedure might also receive other messages
between these button messages.) If you want to implement your own
double-click logic, you can use the Windows function GetMessageTime to
obtain the relative times of the WM_LBUTTONDOWN messages. This function is
discussed in more detail in Chapter 5.

If you include CS_DBLCLKS in your window class, the window procedure
receives these messages for a double-click: WM_LBUTTONDOWN, WM_LBUTTONUP,
WM- _LBUTTONDBLCLK, and WM_LBUTTONUP. The WM_LBUTTONDBLCLK message simply
replaces the second WM_LBUTTONDOWN message.

Double-click messages are much easier to process if the first click of a
double-click performs the same action as a single click. The second click
(the WM_LBUTTONDBLCLK message) then does something in addition to the first
click. For example, look at how the mouse works with the file list in the
File Manager program. A single click selects the file. The File Manager
highlights the file with a reverse-video bar. A double-click performs two
actions: The first click selects the file, just as a single click does; the
second click (which is a WM_LBUTTONDBLCLK message) directs the File Manager
to run the file. That's fairly easy logic.

Mouse-handling logic could get more complex if the first click of a
double-click does not perform the same action as a single click.



NONCLIENT-AREA MOUSE MESSAGES

The 10 mouse messages discussed so far occur when the mouse is moved or
clicked within the client area of a window. If the mouse is outside a
window's client area but still within the window, Windows sends the window
procedure a "nonclient-area" mouse message. The nonclient area includes the
caption bar, the menu, and window scroll bars.

You do not usually need to process nonclient-area mouse messages. Instead,
you simply pass them on to DefWindowProc so Windows can perform system
functions. In this respect, the nonclient-area mouse messages are similar to
the system keyboard messages WM_SYSKEYDOWN, WM_SYSKEYUP, and WM_SYSCHAR.

The nonclient-area mouse messages parallel almost exactly the client-area
mouse messages. The messages include the letters "NC" to indicate
"nonclient." If the mouse is moved within a nonclient area of a window, then
the window procedure receives the message WM_NCMOUSEMOVE. The mouse buttons
generate these messages:

Button  Pressed           Released        Pressed (2d Click)
────────────────────────────────────────────────────────────────────────────
Left    WM_NCLBUTTONDOWN  WM_NCLBUTTONUP  WM_NCLBUTTONDBLCLK
Middle  WM_NCMBUTTONDOWN  WM_NCMBUTTONUP  WM_NCMBUTTONDBLCLK
Right   WM_NCRBUTTONDOWN  WM_NCRBUTTONUP  WM_NCRBUTTONDBLCLK

However, the wParam and lParam parameters for nonclient-area mouse messages
are different from those for client-area mouse messages. The wParam
parameter indicates the nonclient area where the mouse was moved or clicked.
It is set to one of the identifi- ers beginning with HT that are defined in
WINDOWS.H (such as HTCAPTION and HTSYSMENU).

The lParam variable contains an x-coordinate in the low word and a
y-coordinate in the high word. However, these are screen coordinates, not
client-area coordinates as they are for client-area mouse messages. For
screen coordinates, the upper left corner of the display area has x and y
values of 0. Values of x increase as you move to the right, and values of y
increase as you move down. (See Figure 4-3.)

  (Figure 4-3. may be found in the printed book.)

You can convert screen coordinates to client-area coordinates and vice versa
with two Windows functions:

ScreenToClient (hwnd, lpPoint) ;
ClientToScreen (hwnd, lpPoint) ;

The lpPoint parameter is a far (or long) pointer to a structure of type
POINT. These two functions convert the values stored in the structure
without preserving the old values. Note that if a screen-coordinate point is
above the window's client area, then the converted client-area y-coordinate
will be negative. Similarly, a screen coordinate to the left of a client
area is a negative x value when expressed as a client-area coordinate.

The Hit-Test Message

If you've been keeping count, you know that we've covered 20 of the 21 mouse
messages. The last message is WM_NCHITTEST, which stands for "nonclient hit
test." This message precedes all other client-area and nonclient-area mouse
messages. The lParam parameter contains the x and y screen coordinates of
the mouse position. The wParam parameter is not used.

Windows applications usually pass this message to DefWindowProc. Windows
then uses the WM_NCHITTEST message to generate all other mouse messages
based on the position of the mouse. For nonclient-area mouse messages, the
value returned from DefWindowProc when processing WM_NCHITTEST becomes the
wParam parameter in the mouse message. This value can be any of the wParam
values that accompany the nonclient-area mouse messages plus the following:

────────────────────────────────────────────────────────────────────────────
HTCLIENT             Client area
HTNOWHERE            Not on any window
HTTRANSPARENT        A window covered by another window
HTERROR              Causes DefWindowProc to produce a beep

If DefWindowProc returns HTCLIENT after it processes a WM_NCHITTEST message,
then Windows converts the screen coordinates to client-area coordinates and
generates a client-area mouse message.

If you remember how we disabled all system keyboard functions by trapping
the WM_SYSKEYDOWN message, you may wonder if you can do something similar by
trapping mouse messages. Sure. If you include the lines:

case WM_NCHITTEST :
     return (long) HTNOWHERE ;

in your window procedure, you will effectively disable all client-area and
nonclient-area mouse messages to your window. The mouse buttons will simply
not work while the mouse is anywhere within your window, including the
system menu box and size box.


Messages Beget Messages

Windows uses the WM_NCHITTEST message to generate all other mouse messages.
The idea of messages giving birth to other messages is common in Windows.
Let's take an example. As you know, if you double-click the system menu box
of a Windows program, the program will be terminated. The double-click
generates a series of WM_NCHITTEST messages. Because the mouse is positioned
over the system menu box, DefWindowProc returns a value of HTSYSMENU and
Windows puts a WM_NCLBUTTONDBLCLK message in the message queue with wParam
equal to HTSYSMENU.

The window procedure usually passes that mouse message to DefWindowProc.
When DefWindowProc receives the WM_NCLBUTTONDBLCLK message with wParam equal
to HTSYSMENU, it puts a WM_SYSCOMMAND message with wParam equal to  SC_CLOSE
in the message queue. (This WM_SYSCOMMAND message is also generated when a
user selects Close from the system menu box.) Again, the window procedure
usually passes that message to DefWindowProc. DefWindowProc processes the
message by sending a WM_CLOSE message to the window procedure.

If a program wants to require confirmation from a user before terminating,
the window procedure can trap WM_CLOSE. Otherwise, DefWindowProc processes
WM_CLOSE by calling the DestroyWindow function. Among other actions,
DestroyWindow sends a WM_DESTROY message to the window procedure. Normally,
a window procedure processes WM_DESTROY with the code:

case WM_DESTROY :
     PostQuitMessage (0) ;
     return 0 ;

The PostQuitMessage causes Windows to place a WM_QUIT message in the message
queue. This message never reaches the window procedure because it causes
GetMessage to return 0, which terminates the message loop and the program.



HIT-TESTING IN YOUR PROGRAMS

Earlier I discussed how the File Manager responded to mouse clicks and
double-clicks. Obviously, the program must determine which file the user is
pointing at with the mouse. This is called "hit-testing." Just as
DefWindowProc must do some hit-testing when processing WM_NCHITTEST
messages, very often a window procedure must do some hit-testing within the
client area. In general, hit-testing involves calculations using the x- and
y-coordinates passed to your window procedure in the lParam parameter of the
mouse message.

A Hypothetical Example

Here's an example. Your program displays several columns of alphabetically
sorted files similar to the File Manager file windows. The file list starts
at the top of the client area, which is cxClient pixels wide and cyClient
pixels high; each character is cyChar pixels high. The filenames are stored
in a sorted array of pointers to character strings called szFileNames.

Let's assume that the columns are cxColWidth pixels wide. The number of
files you can fit in each column is:

nNumInCol = cyClient / cyChar ;

You receive a mouse click message with the coordinates cxMouse and cyMouse
derived from lParam. You can determine which column of filenames the user is
pointing to by using the formula:

nColumn = cxMouse / cxColWidth ;

The position of the filename in relation to the top of the column is:

nFromTop = cyMouse / cyChar ;

Now you can calculate an index to the szFileNames array:

nIndex = nColumn * nNumInCol + nFromTop ;

Obviously, if nIndex exceeds the number of files in the array, the user is
clicking on a blank area of the display.

In many cases, hit-testing is more complex than this example suggests. For
instance, it can become very messy in a word processing program that uses
variable font sizes (such as WRITE). When you display something to the
client area, you must determine the coordinates for each item you display.
In hit-testing calculations, you must go backward from the coordinates to
the object. However, if the objects you display are strings, then going
backward involves finding the character position within the string.


A Sample Program

The CHECKER1 program, shown in Figure 4-4, demonstrates some simple
hit-testing. The program divides the client area into a 5-by-5 array of 25
rectangles. If you click the mouse on one of the rectangles, the rectangle
is filled with an X. If you click there again, the X is removed.

 CHECKER1.MAK

#------------------------
# CHECKER1.MAK make file
#------------------------

checker1.exe : checker1.obj checker1.def
     link checker1, /align:16, NUL, /nod slibcew libw, checker1
     rc checker1.exe


checker1.obj : checker1.c
     cl -c -Gsw -Ow -W2 -Zp checker1.c

 CHECKER1.C

/*-------------------------------------------------
   CHECKER1.C -- Mouse Hit-Test Demo Program No. 1
                 (c) Charles Petzold, 1990
  -------------------------------------------------*/

#include <windows.h>
#define DIVISIONS 5

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR  lpszCmdLine, int nCmdShow)
     {
     static char szAppName[] = "Checker1" ;
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = NULL ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "Checker1 Mouse Hit-Test Demo",
                         WS_OVERLAPPEDWINDOW,
                         CW_USEDEFAULT, CW_USEDEFAULT,
                         CW_USEDEFAULT, CW_USEDEFAULT,
                         NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;
     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static BOOL  fState[DIVISIONS][DIVISIONS] ;
     static short cxBlock, cyBlock ;
     HDC          hdc ;
     PAINTSTRUCT  ps ;
     RECT         rect ;
     short        x, y ;

     switch (message)
          {
          case WM_SIZE :
               cxBlock = LOWORD (lParam) / DIVISIONS ;
               cyBlock = HIWORD (lParam) / DIVISIONS ;
               return 0 ;

          case WM_LBUTTONDOWN :
               x = LOWORD (lParam) / cxBlock ;
               y = HIWORD (lParam) / cyBlock ;

               if (x < DIVISIONS && y < DIVISIONS)
                    {
                    fState [x][y] ^= 1 ;
                    rect.left   = x * cxBlock ;
                    rect.top    = y * cyBlock ;
                    rect.right  = (x + 1) * cxBlock ;
                    rect.bottom = (y + 1) * cyBlock ;

                    InvalidateRect (hwnd, &rect, FALSE) ;
                    }
               else
                    MessageBeep (0) ;
               return 0 ;

          case WM_PAINT :
               hdc = BeginPaint (hwnd, &ps) ;

               for (x = 0 ; x < DIVISIONS ; x++)
                    for (y = 0 ; y < DIVISIONS ; y++)
                         {
                         Rectangle (hdc, x * cxBlock, y * cyBlock,
                                   (x + 1) * cxBlock, (y + 1) * cyBlock) ;
                         if (fState [x][y])
                              {
                              MoveTo (hdc,  x    * cxBlock,  y    * cyBlock)
;
                              LineTo (hdc, (x+1) * cxBlock, (y+1) * cyBlock)
;
                              MoveTo (hdc,  x    * cxBlock, (y+1) * cyBlock)
;
                              LineTo (hdc, (x+1) * cxBlock,  y    * cyBlock)
;
                              }
                         }
               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 CHECKER1.DEF

;-------------------------------------
; CHECKER1.DEF module definition file
;-------------------------------------

NAME           CHECKER1

DESCRIPTION    'Mouse Hit-Test Demo Program No. 1 (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

Figure 4-5 shows the CHECKER1 display. All 25 rectangles have the same width
and height. These width and height values are stored in cxBlock and cyBlock
and are recalculated when the size of the client area changes. The
WM_LBUTTONDOWN logic uses the mouse coordinates to determine which rectangle
has been clicked. It flags the current state of the rectangle in the array
fState and invalidates the rectangle to generate a WM- _PAINT message. If
the width or height of the client area is not evenly divisible by five, a
small strip of client area at the left or bottom will not be covered by a
rectangle. For error processing, CHECKER1 responds to a mouse click in this
area by calling MessageBeep.

When CHECKER1 receives a WM_PAINT message, it repaints the entire client
area by drawing rectangles using the GDI Rectangle function. If the fState
value is set,

  (Figure 4-5. may be found in the printed book.)

CHECKER1 draws two lines using the MoveTo and LineTo functions. During
WM_PAINT processing, CHECKER1 does not check the validity of each
rectangular section before repainting it, but it could. One method for
checking validity involves building a RECT structure for each rectangular
block within the loop (using the same formulas as in the WM_LBUTTONDOWN
logic) and checking whether it intersects the invalid rectangle
(ps.rcPaint) by using the function IntersectRect. Another method is to use
PtInRect to determine if any of the four corners of the rectangular block
are within the invalid rectangle.


Emulating the Mouse with the Keyboard

CHECKER1 works only if you have a mouse. We'll be adding a keyboard
interface to the program shortly, as we did for the SYSMETS program in
Chapter 3. However, adding a keyboard interface to a program that uses the
mouse cursor for pointing purposes requires that we also must worry about
displaying and moving the mouse cursor.

Even if a mouse device is not installed, Windows can still display a mouse
cursor. Windows maintains a "display count" for this cursor. If a mouse is
installed, the display count is initially 0; if not, the display count is
initially -1. The mouse cursor is displayed only if the display count is 0
or positive. You can increment the display count by calling:

ShowCursor (TRUE) ;

and decrement it by calling:

ShowCursor (FALSE) ;

You do not need to determine if a mouse is installed before using
ShowCursor. If you want to display the mouse cursor regardless of the
presence of the mouse, simply increment the display count. After you
increment the display count once, decrementing it will hide the cursor if no
mouse is installed but leave it displayed if a mouse is present. The display
count applies to all of Windows, so you should ensure that you increment and
decrement the display count an equal number of times.

You may want to use the following simple logic in your window procedure:

WM_SETFOCUS :
     ShowCursor (TRUE) ;
     return 0 ;

WM_KILLFOCUS :
     ShowCursor (FALSE) ;
     return 0 ;

A window procedure receives the WM_SETFOCUS message when the window obtains
the keyboard input focus and WM_KILLFOCUS when it loses the input focus.
These are ideal times to display and hide the mouse cursor. First, the
WM_SETFOCUS and WM_KILLFOCUS calls are balanced--that is, the window
procedure will increment and decrement the mouse cursor display count an
equal number of times. Second, for versions of Windows installed without a
mouse, using the WM_SETFOCUS and WM_KILLFOCUS messages causes the cursor to
be visible only when the window has the input focus. That is also the only
time the user can move the cursor using the keyboard interface that you'll
design.

Windows maintains a current mouse cursor position even if a mouse is not
installed. If a mouse is not installed, and you display the mouse cursor, it
may appear in any part of the display, and will remain in that position
until you explicitly move it. You can obtain the cursor position by using:

GetCursorPos (lpPoint) ;

where lpPoint is a far pointer to a POINT structure. The function fills in
the POINT fields with the x- and y-coordinates of the mouse. You can set the
cursor position by using:

SetCursorPos (x, y) ;

In both cases, the x and y values are screen coordinates, not client-area
coordinates. (This should be evident because the functions do not require a
hwnd parameter.) As noted earlier, you can convert screen coordinates to
client-area coordinates and vice versa by using ScreenToClient and
ClientToScreen.

If you call GetCursorPos while processing a mouse message and convert to
client-area coordinates, the coordinates may still be slightly different
from those in lParam of the mouse message. The coordinates returned from
GetCursorPos indicate the current position of the mouse. The coordinates in
lParam of a mouse message are the coordinates of the mouse when the message
was generated.

You'll probably want to write keyboard logic to move the mouse cursor with
the keyboard arrow keys and simulate the mouse button with the Spacebar or
Enter key. What you don't want to do is move the mouse cursor one pixel per
keystroke. That forces a user to hold down an arrow key for more than a
minute to move the mouse cursor from one side of the display to the other.

If you need to implement a keyboard interface to the mouse cursor but still
maintain the ability to position the cursor at precise pixel locations, take
a look at Windows' PAINTBRUSH program. When you hold down an arrow key, the
mouse cursor starts moving slowly but then speeds up. You'll recall that the
lParam parameter in WM_KEYDOWN messages indicates if the keystroke messages
are the result of typematic action. This is an excellent application of that
information.


Adding a Keyboard Interface to CHECKER

The CHECKER2 program, shown in Figure 4-6, is the same as CHECKER1 except
that it includes a keyboard interface. You can use the Left, Right, Up, and
Down arrow keys to move the cursor among the 25 rectangles. The Home key
sends the cursor to the upper left rectangle; the End key drops it down to
the lower right rectangle. Both the Spacebar and Enter keys toggle the X
mark.

 CHECKER2.MAK

#------------------------
# CHECKER2.MAK make file
#------------------------

checker2.exe : checker2.obj checker2.def
     link checker2, /align:16, NUL, /nod slibcew libw, checker2
     rc checker2.exe

checker2.obj : checker2.c
     cl -c -Gsw -Ow -W2 -Zp checker2.c

 CHECKER2.C

/*-------------------------------------------------
   CHECKER2.C -- Mouse Hit-Test Demo Program No. 2
                 (c) Charles Petzold, 1990
  -------------------------------------------------*/


#include <windows.h>
#define DIVISIONS 5

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)

     {
     static char szAppName[] = "Checker2" ;
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = NULL ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "Checker2 Mouse Hit-Test Demo",
                         WS_OVERLAPPEDWINDOW,
                         CW_USEDEFAULT, CW_USEDEFAULT,
                         CW_USEDEFAULT, CW_USEDEFAULT,
                         NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static BOOL  fState[DIVISIONS][DIVISIONS] ;
     static short cxBlock, cyBlock ;
     HDC          hdc ;
     PAINTSTRUCT  ps ;
     POINT        point ;
     RECT         rect ;
     short        x, y ;

     switch (message)
          {
          case WM_SIZE :
               cxBlock = LOWORD (lParam) / DIVISIONS ;
               cyBlock = HIWORD (lParam) / DIVISIONS ;
               return 0 ;

          case WM_SETFOCUS :
               ShowCursor (TRUE) ;
               return 0 ;

          case WM_KILLFOCUS :
               ShowCursor (FALSE) ;

               return 0 ;

          case WM_KEYDOWN :
               GetCursorPos (&point) ;
               ScreenToClient (hwnd, &point) ;

               x = max (0, min (DIVISIONS - 1, point.x / cxBlock)) ;
               y = max (0, min (DIVISIONS - 1, point.y / cyBlock)) ;

               switch (wParam)
                    {
                    case VK_UP :
                         y-- ;
                         break ;

                    case VK_DOWN :
                         y++ ;
                         break ;

                    case VK_LEFT :
                         x-- ;
                         break ;

                    case VK_RIGHT :
                         x++ ;
                         break ;

                    case VK_HOME :
                         x = y = 0 ;
                         break ;
                    case VK_END :
                         x = y = DIVISIONS - 1 ;
                         break ;

                    case VK_RETURN :
                    case VK_SPACE :
                         SendMessage (hwnd, WM_LBUTTONDOWN, MK_LBUTTON,
                                   MAKELONG (x * cxBlock, y * cyBlock)) ;
                         break ;
                    }
               x = (x + DIVISIONS) % DIVISIONS ;
               y = (y + DIVISIONS) % DIVISIONS ;

               point.x = x * cxBlock + cxBlock / 2 ;
               point.y = y * cyBlock + cyBlock / 2 ;

               ClientToScreen (hwnd, &point) ;
               SetCursorPos (point.x, point.y) ;
               return 0 ;

          case WM_LBUTTONDOWN :
               x = LOWORD (lParam) / cxBlock ;
               y = HIWORD (lParam) / cyBlock ;

               if (x < DIVISIONS && y < DIVISIONS)
                    {
                    fState[x][y] ^= 1 ;

                    rect.left   = x * cxBlock ;
                    rect.top    = y * cyBlock ;
                    rect.right  = (x + 1) * cxBlock ;
                    rect.bottom = (y + 1) * cyBlock ;

                    InvalidateRect (hwnd, &rect, FALSE) ;
                    }
               else
                    MessageBeep (0) ;
               return 0 ;

          case WM_PAINT :
               hdc = BeginPaint (hwnd, &ps) ;

               for (x = 0 ; x < DIVISIONS ; x++)
                    for (y = 0 ; y < DIVISIONS ; y++)
                         {
                         Rectangle (hdc, x * cxBlock, y * cyBlock,
                                   (x + 1) * cxBlock, (y + 1) * cyBlock) ;

                         if (fState [x][y])
                              {
                              MoveTo (hdc,  x    * cxBlock,  y    * cyBlock)
;
                              LineTo (hdc, (x+1) * cxBlock, (y+1) * cyBlock)
;
                              MoveTo (hdc,  x    * cxBlock, (y+1) * cyBlock)
;
                              LineTo (hdc, (x+1) * cxBlock,  y    * cyBlock)
;
                              }
                         }
               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 CHECKER2.DEF

;-------------------------------------
; CHECKER2.DEF module definition file
;-------------------------------------

NAME           CHECKER2

DESCRIPTION    'Mouse Hit-Test Demo Program No. 2 (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

The WM_KEYDOWN logic in CHECKER2 determines the position of the cursor
(GetCursorPos), converts the screen coordinates to client-area coordinates
(ScreenToClient), and divides the coordinates by the width and height of the
rectangular block. This produces x and y values that indicate the position
of the rectangle in the 5-by-5 array. The mouse cursor may or may not be in
the client area when a key is pressed, so x and y must be passed through the
WINDOWS.H min and max macros to ensure that they range from 0 through 4.

For arrow keys, CHECKER2 increments or decrements x and y appropriately. If
the key is the Enter key (VK_RETURN) or Spacebar (VK_SPACE), CHECKER2 uses
SendMessage to send a WM_LBUTTONDOWN message to itself. This technique is
similar to the method used in the SYSMETS program in Chapter 3 to add a
keyboard interface to the window scroll bar. The WM_KEYDOWN logic finishes
by calculating client-area coordinates that point to the center of the
rectangle, converting to screen coordinates (ClientToScreen), and setting
the cursor position (SetCursorPos).


Using Child Windows for Hit-Testing

Some programs, like the Windows PAINTBRUSH program, divide the client area
into several smaller logical areas. The PAINTBRUSH program, shown in Figure
4-7, has an area at the left for its icon-based menu and an area at the
bottom for the color menu. PAINTBRUSH, when hit-testing on these two menus,
must take into account the location of the menu within the client area
before determining the menu item being selected by the user.

Or maybe not. In reality, PAINTBRUSH simplifies the menu drawing and
hit-testing through the use of "child windows." The child windows divide the
entire client area into several smaller rectangular regions. Each child
window has its own window handle, window procedure, and client area. Each
window procedure receives mouse messages that apply only to its child
window. The lParam parameter in the mouse message contains coordinates
relative to the upper left corner of the client area of the child window,
not of the parent window.

Child windows used in this way can help you structure and modularize your
programs. If the child windows use different window classes, each child
window can have its

  (Figure 4-7. may be found in the printed book.)

own window procedure. The different window classes can also define different
background colors and different default cursors. In Chapter 6, we'll look at
"child window controls"--predefined child windows that take the form of
scroll bars, buttons, and edit boxes. Right now, let's see how we can use
child windows in the CHECKER program.


Child Windows in CHECKER

Figure 4-8 shows CHECKER3. This version of the program creates 25 child
windows to process mouse clicks. It does not have a keyboard interface, but
one could be easily added.

CHECKER3 has two window procedures called WndProc and ChildWndProc. WndProc
is still the window procedure for the main (or parent) window. ChildWndProc
is the window procedure for the 25 child windows. The names of both window
procedures must appear as EXPORTS in the CHECKER3.DEF file because both
procedures are called from Windows.

Because the window procedure is defined by the window class structure that
you register with Windows using the RegisterClass call, the two window
procedures in CHECKER3.C require two window classes. The first window class
is for the main window and has the name "Checker3". The second window class
is given the name "Checker3_Child".

Most of the fields of the wndclass structure variable are simply reused when
"Checker3_Child" is registered in WinMain. The lpszClassName field is set to
"Checker3_Child"--the name of the class. The lpfnWndProc field is set to
ChildWndProc, the window procedure for this window class, and the hIcon
field is set to NULL, because icons are not used with child windows. For the
"Checker3_Child" window class, the cbWndExtra field in the wndclass
structure variable is set to 2 bytes, or more precisely, sizeof(WORD). This
field tells Windows to reserve 2 bytes of extra space in a structure that
Windows maintains for each window based on this window class. You can use
this space to store information that may be different for each window.

 CHECKER3.MAK

#------------------------
# CHECKER3.MAK make file
#------------------------

checker3.exe : checker3.obj checker3.def
     link checker3, /align:16, NUL, /nod slibcew libw, checker3
     rc checker3.exe

checker3.obj : checker3.c
     cl -c -Gsw -Ow -W2 -Zp checker3.c

gure 4-8.

The CHECKER3 program.

 CHECKER3.C

/*-------------------------------------------------
   CHECKER3.C -- Mouse Hit-Test Demo Program No. 3
                 (c) Charles Petzold, 1990
  -------------------------------------------------*/

#include <windows.h>
#define DIVISIONS 5

long FAR PASCAL WndProc      (HWND, WORD, WORD, LONG) ;
long FAR PASCAL ChildWndProc (HWND, WORD, WORD, LONG) ;

char szChildClass[] = "Checker3_Child" ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName[] = "Checker3" ;
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;

          wndclass.lpfnWndProc   = ChildWndProc ;
          wndclass.cbWndExtra    = sizeof (WORD) ;
          wndclass.hIcon         = NULL ;
          wndclass.lpszClassName = szChildClass ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "Checker3 Mouse Hit-Test Demo",
                         WS_OVERLAPPEDWINDOW,
                         CW_USEDEFAULT, CW_USEDEFAULT,
                         CW_USEDEFAULT, CW_USEDEFAULT,
                         NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static HWND hwndChild [DIVISIONS] [DIVISIONS] ;
     short       cxBlock, cyBlock, x, y ;

     switch (message)
          {
          case WM_CREATE :
               for (x = 0 ; x < DIVISIONS ; x++)
                    for (y = 0 ; y < DIVISIONS ; y++)
                         {
                         hwndChild [x][y] = CreateWindow (szChildClass,
NULL,
                              WS_CHILDWINDOW | WS_VISIBLE,
                              0, 0, 0, 0,
                              hwnd, y << 8 | x,
                              GetWindowWord (hwnd, GWW_HINSTANCE), NULL) ;
                         }
               return 0 ;

          case WM_SIZE :
               cxBlock = LOWORD (lParam) / DIVISIONS ;
               cyBlock = HIWORD (lParam) / DIVISIONS ;
               for (x = 0 ; x < DIVISIONS ; x++)
                    for (y = 0 ; y < DIVISIONS ; y++)
                         MoveWindow (hwndChild [x][y],
                              x * cxBlock, y * cyBlock,
                              cxBlock, cyBlock, TRUE) ;
               return 0 ;

          case WM_LBUTTONDOWN :
               MessageBeep (0) ;
               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

long FAR PASCAL ChildWndProc (HWND hwnd, WORD message, WORD wParam, LONG
lParam)
     {
     HDC         hdc ;
     PAINTSTRUCT ps ;
     RECT        rect ;

     switch (message)
          {
          case WM_CREATE :
               SetWindowWord (hwnd, 0, 0) ;       // on/off flag
               return 0 ;

          case WM_LBUTTONDOWN :
               SetWindowWord (hwnd, 0, 1 ^ GetWindowWord (hwnd, 0)) ;
               InvalidateRect (hwnd, NULL, FALSE) ;
               return 0 ;

          case WM_PAINT :
               hdc = BeginPaint (hwnd, &ps) ;

               GetClientRect (hwnd, &rect) ;
               Rectangle (hdc, 0, 0, rect.right, rect.bottom) ;

               if (GetWindowWord (hwnd, 0))
                    {
                    MoveTo (hdc, 0,          0) ;
                    LineTo (hdc, rect.right, rect.bottom) ;
                    MoveTo (hdc, 0,          rect.bottom) ;
                    LineTo (hdc, rect.right, 0) ;
                    }

               EndPaint (hwnd, &ps) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 CHECKER3.DEF

;-------------------------------------
; CHECKER3.DEF module definition file
;-------------------------------------
NAME           CHECKER3

DESCRIPTION    'Mouse Hit-Test Demo Program No. 3 (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc
               ChildWndProc

The CreateWindow call in WinMain creates the main window based on the
"Checker3" class. This is normal. However, when WndProc receives a WM_CREATE
message, it calls CreateWindow 25 times to create 25 child windows based on
the "Checker3_Child" window class. Here's a comparison of the parameters to
the CreateWindow call in WinMain that creates the main window and the
CreateWindow call in WndProc that creates the 25 child windows:

╓┌───────────────────────┌────────────────────┌──────────────────────────────╖
Parameter               Main Window          Child Window
────────────────────────────────────────────────────────────────────────────
window class            "Checker3"           "Checker3_Child"
window caption          "Checker3..."        NULL
window style            WS_OVERLAPPEDWINDOW  WS_CHILDWINDOW | WS_VISIBLE
horizontal position     CW_USEDEFAULT        0
vertical position       CW_USEDEFAULT        0
width                   CW_USEDEFAULT        0
height                  CW_USEDEFAULT        0
parent window handle    NULL                 hwnd
Parameter               Main Window          Child Window
────────────────────────────────────────────────────────────────────────────
parent window handle    NULL                 hwnd
menu handle / child ID  NULL                 y << 8 | x
instance handle         hInstance            GetWindowWord (hwnd,
GWW_HINSTANCE)
extra parameters        NULL                 NULL


Normally, the position, width, and height parameters are required for child
windows, but in CHECKER3 the child windows are positioned and resized later
in WndProc. The parent window handle is NULL for the main window because it
is the parent. The parent window handle is required when using the
CreateWindow call to create a child window.

The main window doesn't have a menu, so that parameter is NULL. For child
windows, the same parameter position is called a "child ID." This is a
number that uniquely identifies the child window. The child ID becomes much
more important when working with child window controls because messages to
the parent window are identified by this child ID. For CHECKER3, I've used
the child ID to identify the position in the 5-by-5 array that each child
window occupies within the main window.

The instance handle is hInstance in both cases. When the child window is
created, the hInstance value is extracted using the function GetWindowWord
from the structure that Windows maintains for the window. (Rather than use
GetWindowWord, I could have saved the value of hInstance in a global
variable and used it directly.)

Each child window has a different window handle that is stored in the
hwndChild array. When WndProc receives a WM_SIZE message, it calls
MoveWindow for each of the 25 child windows. The parameters indicate the
upper left corner of the child window relative to the parent window
client-area coordinates, the width and height of the child window, and
whether the child window needs repainting.

Now let's take a look at ChildWndProc. This window procedure processes
messages for all 25 child windows. The hwnd parameter to ChildWndProc is the
handle to the child window receiving the message. When ChildWndProc
processes a WM_CREATE message (which will happen 25 times because there are
25 child windows), it uses SetWindowWord to store a 0 in the extra area
reserved within the window structure. (Recall that we reserved this space by
using the cbWndExtra field when defining the window class structure.)
ChildWndProc uses this value to store the current state (X or no X) of the
rectangle. When the child window is clicked, the WM_LBUTTONDOWN logic simply
flips the value of this word (from 0 to 1 or from 1 to 0) and invalidates
the entire child window client area. This area is the single rectangle being
clicked. The WM_PAINT processing is trivial, because the size of the
rectangle it draws is the same size as the client window.

Because the C source code file and the .EXE file of CHECKER3 are larger than
those for CHECKER1 (to say nothing of my explanation of the programs), I
will not try to convince you that CHECKER3 is "simpler" than CHECKER1. But
note that we no longer have to do any mouse hit-testing! If a child window
in CHECKER3 gets a WM_LBUTTONDOWN message, the window has been hit, and
that's all it needs to know.

If you want to add a keyboard interface to CHECKER3, be aware that the main
window still gets keyboard messages because it has the input focus. We'll
explore child windows more in Chapter 6.



CAPTURING THE MOUSE

A window procedure normally receives mouse messages only when the mouse
cursor is positioned over the client or nonclient area of the window. A
program that needs to receive mouse messages when the mouse is outside the
window can "capture" the mouse.

Capturing the mouse is easier than baiting a mousetrap. You need only call:

SetCapture (hwnd) ;

After this function call, Windows sends all mouse messages to the window
procedure for the window whose handle is hwnd. The mouse messages are always
client-area messages, even when the mouse is in a nonclient area of the
window. The lParam parameter still indicates the position of the mouse in
client-area coordinates. These x- and y-coordinates, however,  can be
negative if the mouse is to the left of or above the client area.

During the time the mouse is captured, system keyboard functions are also
disabled. When you want to release the mouse, call:

ReleaseCapture () ;

This returns processing to normal.

The BLOWUP1 Program

The BLOWUP1 program, shown in Figure 4-9, uses SetCapture and ReleaseCapture
and a few other interesting techniques. The program lets you use the mouse
to block out any rectangular area of the screen. BLOWUP1 then copies the
contents of that rectangular area into its own client area, stretching or
compressing the image as appropriate. (See Figure 4-10 on page 169.)

 BLOWUP1.MAK

#-----------------------
# BLOWUP1.MAK make file
#-----------------------

blowup1.exe : blowup1.obj blowup1.def
     link blowup1, /align:16, NUL, /nod slibcew libw, blowup1
     rc blowup1.exe

blowup1.obj : blowup1.c
     cl -c -Gsw -Ow -W2 -Zp blowup1.c

 BLOWUP1.C

/*------------------------------------------------
   BLOWUP1.C -- Screen Capture Mouse Demo Program
               (c) Charles Petzold, 1990
  ------------------------------------------------*/

#include <windows.h>

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)


     {
     static char szAppName[] = "BlowUp1" ;
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "Blow-Up Mouse Demo",
                         WS_OVERLAPPEDWINDOW,
                         CW_USEDEFAULT, CW_USEDEFAULT,
                         CW_USEDEFAULT, CW_USEDEFAULT,
                         NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

void InvertBlock (HWND hwnd, POINT ptBeg, POINT ptEnd)
     {
     HDC hdc ;

     hdc = CreateDC ("DISPLAY", NULL, NULL, NULL) ;
     ClientToScreen (hwnd, &ptBeg) ;
     ClientToScreen (hwnd, &ptEnd) ;
     PatBlt (hdc, ptBeg.x, ptBeg.y, ptEnd.x - ptBeg.x, ptEnd.y - ptBeg.y,
             DSTINVERT) ;
     DeleteDC (hdc) ;
     }
long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static BOOL  fCapturing, fBlocking ;
     static POINT ptBeg, ptEnd ;
     HDC          hdc ;
     RECT         rect ;

     switch (message)
          {
          case WM_LBUTTONDOWN :
               if (!fCapturing)
                    {
                    fCapturing = TRUE ;
                    SetCapture (hwnd) ;
                    SetCursor (LoadCursor (NULL, IDC_CROSS)) ;
                    }
               else if (!fBlocking)

                    {
                    fBlocking = TRUE ;
                    ptBeg = MAKEPOINT (lParam) ;
                    }
               return 0 ;

          case WM_MOUSEMOVE :
               if (fBlocking)
                    {
                    ptEnd = MAKEPOINT (lParam) ;
                    InvertBlock (hwnd, ptBeg, ptEnd) ;
                    InvertBlock (hwnd, ptBeg, ptEnd) ;
                    }
               return 0 ;

          case WM_LBUTTONUP :
               if (fBlocking)
                    {
                    fCapturing = fBlocking = FALSE ;
                    ptEnd = MAKEPOINT (lParam) ;
                    SetCursor (LoadCursor (NULL, IDC_WAIT)) ;

                    hdc = GetDC (hwnd) ;
                    GetClientRect (hwnd, &rect) ;
                    StretchBlt (hdc, 0, 0, rect.right, rect.bottom,
                                hdc, ptBeg.x, ptBeg.y,
                                ptEnd.x - ptBeg.x, ptEnd.y - ptBeg.y,
                                SRCCOPY) ;

                    ReleaseDC (hwnd, hdc) ;
                    SetCursor (LoadCursor (NULL, IDC_ARROW)) ;
                    ReleaseCapture () ;
                    }
               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 BLOWUP1.DEF

;------------------------------------
; BLOWUP1.DEF module definition file
;------------------------------------

NAME           BLOWUP1

DESCRIPTION    'Blow-Up Mouse Demo Program (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

The job of stretching and compressing bitmapped images may seem complex, but
it's simplified for us by a Windows GDI function called StretchBlt. (The
abbreviation Blt is pronounced "blit." The function is related to the
Windows BitBlt function, which stands for "bit-block transfer." These
functions are discussed in more detail in Chapter 13.)

Here's how to use BLOWUP1:

  1.  Click the mouse in BLOWUP1's client area. The mouse cursor changes to
      a cross hair.

  2.  Position the mouse cursor over the upper left corner of the area of
      the screen you want to transfer.

  3.  Press the mouse button, drag the mouse to the lower right corner, and
      release the mouse button. The mouse cursor changes to an hourglass.

  4.  In a few seconds (or perhaps a little longer), the area that you
      blocked out is copied to BLOWUP1's client area, compressed or expanded
      appropriately.

If you block out a rectangle by moving from the upper right corner to the
lower left corner, BLOWUP1 displays a mirror image. If you move from the
lower left to the upper right, BLOWUP1 displays an upside-down image. And if
you move from lower right to upper left, the program combines the two
effects.

BLOWUP1 does not retain the captured image, and doesn't process the WM_PAINT
message. If you change the size of the window, the window will be erased.

  (Figure 4-10. may be found in the printed book.)


Changing the Mouse Cursor Shape

BLOWUP1 uses the SetCursor calls to change the cursor shape from an arrow to
a cross hair, then to an hourglass, and back to an arrow. All these are
stock cursors available in Windows. You obtain the handle to the cursor
using the LoadCursor function. In Chapter 8 we'll use LoadCursor to display
customized mouse cursors.

Many applications display an hourglass cursor (defined in WINDOWS.H as
IDC_WAIT) while doing processing that may take some time to complete. This
is fairly simple to implement. You can save the handle to the original
cursor by storing the return value from SetCursor and using it to reset the
cursor later. First, you'll need a variable of type HCURSOR (defined in
WINDOWS.H as a HANDLE, or 16-bit WORD) to store that value:

HCURSOR hCursor ;

Right before you start the lengthy processing, use the following two lines:

hCursor = SetCursor (LoadCursor (NULL, IDC_WAIT)) ;
ShowCursor (TRUE) ;

After you're done with the work, call:

ShowCursor (FALSE) ;
SetCursor (hCursor) ;

The two ShowCursor calls display and then hide the hourglass cursor if a
mouse is not actually present.

Normally, Windows changes the mouse cursor to the cursor included in the
window class structure whenever the window procedure receives a WM_MOUSEMOVE
message. If you use SetCursor to change the mouse cursor and then exit the
window procedure, the mouse cursor will be restored to the cursor in the
window class structure the next time the mouse is moved. This does not
happen in BLOWUP1, because the mouse is captured during the time the
cross-hair cursor (IDC_CROSS) is displayed. Windows will not change the
cursor to the window class cursor when the mouse is captured. Also, if you
need to display an hourglass cursor when doing some lengthy work (as BLOWUP1
does when it calls StretchBlt), you don't have to worry about the problem,
because you're not receiving any other messages during that time.

But if you want to use different mouse cursors for other purposes, you
should define a NULL cursor handle in your window class:

wndclass.hCursor = NULL ;

In your window function, you then call SetCursor for each WM_MOUSEMOVE
message. The SetCursor call is fast if the mouse cursor is not being
changed.


The StretchBlt Call

BLOWUP1 calls the StretchBlt function during processing of the WM_LBUTTONUP
message to transfer the blocked-out image to BLOWUP1's client area:

StretchBlt (hdc, 0, 0, rect.right, rect.bottom,
            hdc, ptBeg.x, ptBeg.y,
            ptEnd.x - ptBeg.x, ptEnd.y - ptBeg.y,
            SRCCOPY) ;

This function is discussed in more detail in Chapter 13, but let's take a
quick look at it here.

StretchBlt transfers a bitmapped image from a source to a destination. The
first five parameters are for the destination of the image, defining the
device context handle, the  x- and y-coordinates of the upper left corner,
and the width and height. The next five parameters give the same information
for the source of the image. The last parameter is the operation, which in
this case is a simple copy from source to destination.

The source of the image involves two POINT structures named ptBeg (beginning
of the block) and ptEnd (end of the block). These two points have negative
coordinate values if you block out an image to the left of or above the
client area. StretchBlt can read a bitmapped image that falls outside the
client area, but like all GDI functions it cannot write outside the client
area.


Drawing the Capture Block

But wait. When you use BLOWUP1 to block out an image outside its client
area, the program briefly displays the image in reverse video and then
restores it to normal. BLOWUP1 is apparently writing outside its client
area. Can that be so?

It certainly can. This little trick is carried off in BLOWUP1's InvertBlock
function. Rather than obtain a device context handle from GetDC, InvertBlock
uses CreateDC:

hdc = CreateDC ("DISPLAY", NULL, NULL, NULL) ;

This returns a device context handle for the entire display. Using this
device context handle, you can write outside your client area.

InvertBlock uses the GDI PatBlt function (a "pattern bit-block transfer") to
invert the blocked-out image. BLOWUP1 calls InvertBlock twice in succession.
When called the second time, the block is restored to normal. This means
that you can see the block briefly only when you move the mouse cursor.

Why do it like this? BLOWUP1 doesn't leave the block in an inverted state
because other Windows programs can receive messages between BLOWUP1's
WM_MOUSEMOVE messages. (For instance, the Windows CLOCK gets WM_TIMER
messages every second.) If BLOWUP1 left the block inverted when it exited
its window function, then the program with the altered client area could
write over the inverted block. When BLOWUP1 then reinverted the block--that
is, returned it to normal--the result would start looking like a mess. Keep
considerations like this in mind when you start working with powerful
functions like CreateDC. Windows gives you the power to do almost anything,
but your programs must share resources such as the display with other
programs. Try to exercise a little restraint.





Chapter 5  The Timer
────────────────────────────────────────────────────────────────────────────

The Windows timer is an input device that periodically notifies an
application when a specified interval of time has elapsed. Your program
tells Windows the interval, in effect saying, for example, "Give me a nudge
every 10 seconds." Windows then sends your program recurrent WM_TIMER
messages to signal the intervals.

At first, the Windows timer may seem a less important input device than the
keyboard or mouse, and certainly it is for many applications. But the timer
is more useful than you may think, and not only for programs (like the
Windows CLOCK) that keep time. The CALENDAR, CONTROL, REVERSI, SPOOLER,
TERMINAL, and WRITE programs supplied with Windows also use the timer. Here
are some uses for the Windows timer, some obvious and some perhaps not so
obvious:

  ■   Keeping time--Both the CLOCK and CONTROL programs that come with
      Windows display the current time. The timer tells the programs when to
      update the clock. The DIGCLOCK program, described later in this
      chapter, uses the timer to display a digital clock.

  ■   Maintaining an updated status report--The FREEMEM program, shown in
      this chapter, uses the timer to display available memory in Windows.
      The display is updated every second.

  ■   Waking up--The Windows CALENDAR program uses the timer to trigger a
      preset alarm.

  ■   Multitasking--Windows is a nonpreemptive multitasking environment, and
      it is important that programs return control to Windows as quickly as
      possible. If a program must do a large amount of processing, it can
      divide the job into smaller pieces and process each piece on receipt
      of a WM_TIMER message.

  ■   Implementing an "autosave" feature--The timer can prompt a Windows
      program to save a user's work to disk whenever a specified amount of
      time has elapsed.

  ■   Pacing movement--Graphical objects in a game or successive displays in
      a computer-assisted instruction program may need to proceed at a set
      rate. Using the timer eliminates the inconsistencies that might result
      from variations in microprocessor speed.

  ■   Terminating demonstration versions of programs--Some demonstration
      versions of programs are designed to terminate, say, 30 minutes after
      they begin. The timer can signal such applications when the time is
      up.

  ■   Using serial or parallel communications--Unlike most other input
      devices in Windows, serial or parallel communications ports do not
      generate messages. Rather, these programs must poll for input, and the
      timer can tell them when to do so. (An alternative to using the timer
      for polling involves a message loop built around the PeekMessage call.
      This technique is discussed in Chapter 15.)

This chapter also explores topics that extend beyond the timer to other
areas of Windows programming. Foremost among these topics is that of
"call-back" functions. To the uninitiated, these important functions might
seem to work in mysterious ways, and the timer is not the only place you
will encounter them. This chapter also discusses what to do when a program
cannot gain access to a timer--a problem that occurs because Windows
maintains only a limited number of timers. Solving this problem is
fundamental to working with the Windows timer, but the method presented here
can also be applied to error handling in other programs. Finally, the sample
programs shown here deal with such decidedly nontimer issues as Windows' use
of color, using a type of window known as a "popup," forcing an application
to be loaded as an icon, obtaining the amount of free memory available in
Windows, using floating-point mathematics in your Windows programs, and
accessing the WIN.INI file to obtain information about international time
and date formats.

TIMER BASICS

You can allocate a timer for your Windows program by calling the SetTimer
function. SetTimer includes a parameter specifying an interval that can
range (in theory) from 1 msec (millisecond) to 65,535 msec, or about 65.5
seconds. The value indicates the rate at which Windows sends your program
WM_TIMER messages. For instance, an interval of 1000 msec causes Windows to
send your program a WM_TIMER message every second.

When your program is done using the timer, it calls the KillTimer function
to stop the timer messages. You can program a "one-shot" timer by calling
KillTimer during the processing of the WM_TIMER message. The KillTimer call
purges the message queue of any pending WM_TIMER messages. Your program will
never receive a stray WM_TIMER message following a KillTimer call.

As you've undoubtedly learned from experimenting with loading multiple
instances of CLOCK, Windows allows only 16 timers to be active at one time.
If all 16 timers are already allocated, SetTimer returns NULL. Windows
programs that use a timer must include some way to deal with this problem.

SYSTEM.DRV and the Windows Timer

The Windows timer is a relatively simple extension of the timer logic built
into the IBM PC's hardware and ROM BIOS. The PC's ROM BIOS initializes an
Intel 8259 timer chip to generate the hardware Interrupt 08H. This interrupt
is sometimes called the "clock tick" or "timer tick" interrupt. An Interrupt
08H occurs every 54.925 msec, or about 18.2 times per second. Among other
purposes, the BIOS uses Interrupt 08H to update a "time-of-day" value stored
in the BIOS data area. MS-DOS uses this value to calculate the current time.

The SYSTEM.DRV driver located in the SYSTEM subdirectory of your Windows
directories handles hardware timer interrupts. SYSTEM.DRV sets a new
Interrupt 08H vector address during initialization and restores the original
vector address before Windows terminates. The Interrupt 08H routine within
SYSTEM.DRV calls the original Interrupt 08H handler before doing its own
processing so that underlying system functions that require this interrupt
will continue to work normally.

When SYSTEM.DRV receives an Interrupt 08H, it calls a routine within the
USER module of Windows that decrements counters for each timer set by
Windows applications. When a counter reaches 0, USER places a WM_TIMER
message in that application's message queue and resets the counter to the
original value.

Because a Windows application retrieves WM_TIMER messages from the normal
message queue, you never have to worry about your program being
"interrupted" by a sudden WM_TIMER message while doing other processing. In
this way, the timer is similar to the keyboard and mouse: The driver handles
the asynchronous hardware interrupt events, and Windows translates these
events into orderly, structured, serialized messages.

SYSTEM.DRV does not attempt to reprogram the 8259 timer chip in the IBM PC.
The Windows timer has the same 54.925-msec resolution as the underlying PC
timer. This fact has two important implications:

  ■   A Windows application cannot receive WM_TIMER messages at a rate
      faster than about 18.2 times per second when using a single timer.

  ■   The time interval you specify in the SetTimer call is always rounded
      down to an integral multiple of clock ticks. For instance, a 1000-msec


      interval divided by 54.925 msec is 18.207 clock ticks, which is
      rounded down to 18 clock ticks, which is really a 989-msec interval.
      For intervals less than 55 msec, each clock tick generates a single
      WM_TIMER message.

      Do not attempt to intercept the ROM BIOS timer interrupt in your
      Windows programs. Use the Windows timer instead.


Timer Messages Are Not Asynchronous

Non-Windows programs written for the IBM PC and compatibles can use the
timer tick interrupt by intercepting Interrupt 08H or Interrupt 1CH (a
software interrupt called by the BIOS Interrupt 08H handler). When the
hardware interrupt occurs, the program currently running is suspended, and
control passes to the interrupt handler. When the interrupt handler is done,
it passes control back to the interrupted program.

Like the hardware keyboard and mouse interrupts, the hardware timer tick
interrupt is sometimes called an asynchronous interrupt because it occurs
randomly with respect to the program that it interrupts. (Actually, the term
isochronous is more accurate than asynchronous for a timer interrupt because
the interrupts occur at equal intervals. But the interrupts are still
asynchronous with respect to other processing.)

Although the SYSTEM.DRV driver also handles asynchronous Interrupt 08H clock
ticks, the WM_TIMER messages that Windows sends to applications are not
asynchronous. The WM_TIMER messages are placed in the normal message queue
and ordered with all the other messages. Therefore, if you specify 1000 msec
in the SetTimer call, your program is not guaranteed to receive a WM_TIMER
message every second or even (as I mentioned above) every 989 msec. If
another application is busy for more than a second, your program will not
get any WM_TIMER messages during that time. Only when the other application
yields control to Windows (by calling GetMessage, PeekMessage, or
WaitMessage) will your program retrieve its next WM_TIMER message from the
queue.

You can easily demonstrate this to yourself with the CLOCK program included
with Windows or with the sample programs shown in this chapter. If another
program has a long paint job and does not immediately relinquish control,
CLOCK will stop. When CLOCK regains control, it will jump ahead to the
correct time. In fact, Windows handles WM_TIMER messages much like WM_PAINT
messages. Both these messages are low priority. If a program's message queue
contains only WM_PAINT or WM_TIMER messages, and another program's message
queue contains messages other than WM_PAINT or WM_TIMER, Windows will pass
control to the other application.

The WM_TIMER messages are similar to WM_PAINT messages in another respect:
Windows does not keep loading up the message queue with multiple WM_TIMER
messages. Instead, Windows combines several WM_TIMER messages in the message
queue into a single message. Therefore, the application won't get a bunch of
them all at once, although it may get two WM_TIMER messages in quick
succession. An application cannot determine the number of "missing" WM_TIMER
messages that result from this process.

When CLOCK regains control and jumps ahead to the correct time, it is not
because it gets several WM_TIMER messages in a row. CLOCK must determine the
actual time and then set itself. The WM_TIMER messages only inform CLOCK
when it should be updated. A program can't keep time itself solely by
counting WM_TIMER messages. (Later in this chapter we will write a clock
application that updates every second, and we'll see precisely how this is
accomplished.)

For convenience, I'll be talking about the timer in terms such as "getting a
WM_TIMER message every second." But keep in mind that these messages are not
precise clock tick interrupts.



USING THE TIMER: THREE METHODS

If you need a timer for the entire duration of your program, you'll probably
call SetTimer from the WinMain function or while processing the WM_CREATE
message, and KillTimer in response to a WM_DESTROY message. Setting the
timer in WinMain provides the easiest error handling if a timer is
unavailable. You can use a timer in one of three ways, depending on the
parameters to the SetTimer call.

Method One

This method, the easiest, causes Windows to send WM_TIMER messages to the
normal window procedure of the application. The SetTimer call looks like
this:

SetTimer (hwnd, 1, wMsecInterval, NULL) ;

The first parameter is a handle to the window whose window procedure will
receive the WM_TIMER messages. The second parameter is the timer ID, which
should be a nonzero number. I have arbitrarily set it to 1 in this example.
The third parameter is a WORD (16-bit unsigned integer) that specifies an
interval in milliseconds. The largest value (65535) will deliver a WM_TIMER
message about once a minute.

You can stop the WM_TIMER messages at any time (even while processing a
WM_TIMER message) by calling:

KillTimer (hwnd, 1) ;

The second parameter is the same timer ID used in the SetTimer call. You
should kill any active timers in response to a WM_DESTROY message before
your program terminates.

When your window procedure receives a WM_TIMER message, wParam is equal to
the timer ID (which in the above case is simply 1), and lParam is 0. If you
need to set more than one timer, use a different timer ID for each timer.
The value of wParam will  differentiate the WM_TIMER messages passed to your
window procedure. To make your program more readable, you may want to use
#define statements for the different timer IDs:

#define TIMER_SEC  1
#define TIMER_MIN  2

You can then set the two timers with two SetTimer calls:

SetTimer (hwnd, TIMER_SEC, 1000, NULL) ;
SetTimer (hwnd, TIMER_MIN, 60000, NULL) ;

The WM_TIMER logic might look something like this:

case WM_TIMER :
     switch (wParam)
          {
          case TIMER_SEC :
[once-per-second processing]
               break ;
          case TIMER_MIN :
[once-per-minute processing]
               break ;
          }
     return 0 ;

If you want to set an existing timer to a different elapsed time, kill the
timer and call SetTimer again. This code assumes that the timer ID is 1:

KillTimer (hwnd, 1) ;
SetTimer (hwnd, 1, wMsecInterval, NULL) ;

The wMsecInterval parameter is the new elapsed time in milliseconds. The
Windows CLOCK application uses this method to change the timer from 1000
msec to 60,000 msec when it becomes an icon. As an icon, CLOCK needs to
update the clock every minute rather than every second. When it is expanded
from an icon to a window, CLOCK changes the timer back to 1000 msec.

What to do if no timer is available

Windows allows only 16 timers to be active at any time. If no timer is
available, SetTimer returns NULL. Your program might be able to function
reasonably well without the timer, but if you need the timer (as CLOCK
certainly does), the application has no choice but to terminate if it can't
get one. If you call SetTimer in WinMain, you can terminate the program
simply by returning FALSE from WinMain.

Let's assume you want a 1000-msec timer. Following the CreateWindow call but
before the message loop, you might have a statement like this:

if (!SetTimer (hwnd, 1, 1000, NULL))
     return FALSE ;

This is the unfriendly way to terminate. The user is left wondering why the
application will not load. (Surely the 16 clocks sitting down in the icon
area have nothing to do with it!) It's much friendlier--and fairly easy--to
use a Windows message box for displaying a message. A complete discussion of
message boxes awaits you in Chapter 10, but this will get you started.

A message box is a popup window that always appears in the center of the
display. Message boxes have a caption bar but no size box. The caption bar
usually contains the name of the application. The message box encloses a
message and one, two, or three buttons (some combination of OK, Retry,
Cancel, Yes, No, and others). The message box can also contain a predefined
icon: a lowercase "i" (which stands for "information"), an exclamation
point, a question mark, or a stop sign. You have probably seen plenty of
message boxes when working with Windows.

This code creates an informatory message box that you can use when SetTimer
fails to allocate a timer:

if (!SetTimer (hwnd, 1, 1000, NULL))
     {
     MessageBox (hwnd,
          "Too many clocks or timers!",
          "Program Name",
          MB_ICONEXCLAMATION | MB_OK) ;
     return FALSE ;
     }

The message box is shown in Figure 5-1. When the user presses Enter or
clicks the OK button, WinMain terminates by returning FALSE.

  (Figure 5-1. may be found in the printed book.)

By default, message boxes are "application modal" windows. This means that a
user must respond to the message box before the application will continue.
However, the user can switch to other applications by pressing Alt-Tab or
Alt-Esc or by clicking the mouse in the window of another program.

Why not give the user the opportunity to close one of those 16 minimized
clocks at the bottom of the display and successfully load your application?
That's what this code does:

while (!SetTimer (hwnd, 1, 1000, NULL))

     if (IDCANCEL == MessageBox (hwnd,
               "Too many clocks or timers!",
               "Program Name",
               MB_ICONEXCLAMATION | MB_RETRYCANCEL))
          return FALSE ;

This message box, shown in Figure 5-2, has two buttons, labeled Retry and
Cancel. If the user selects Cancel, the MessageBox function returns a value
equal to IDCANCEL, and the program terminates. If the user selects Retry,
SetTimer is called again.

  (Figure 5-2. may be found in the printed book.)


A sample program

Figure 5-3 shows a sample program that uses the timer. This program, called
BEEPER1, sets a timer for 1-second intervals. When it receives a WM_TIMER
message, it alternates coloring the client area blue and red, and it beeps
by calling the function MessageBeep. (Although MessageBeep is documented as
a companion to MessageBox, it's really an all-purpose beep function. The
WORD parameter to MessageBeep can be any value.) BEEPER1 sets the timer in
the WinMain function and processes the WM_TIMER messages in the WndProc
window procedure. During the WM_TIMER message, BEEPER1 calls MessageBeep,
inverts the value of bFlipFlop and invalidates the window to generate a
WM_PAINT message. During the WM_PAINT message, BEEPER1 obtains a RECT
structure for the size of the window by calling GetClientRect and colors the
window by calling FillRect.

 BEEPER1.MAK

#-----------------------
# BEEPER1.MAK make file
#-----------------------

beeper1.exe : beeper1.obj beeper1.def
     link beeper1, /align:16, NUL, /nod slibcew libw, beeper1
     rc beeper1.exe

beeper1.obj : beeper1.c
     cl -c -Gsw -Ow -W2 -Zp beeper1.c

 BEEPER1.C

/*-----------------------------------------
   BEEPER1.C  -- Timer Demo Program No. 1
                 (c) Charles Petzold, 1990
  -----------------------------------------*/

#include <windows.h>
#define ID_TIMER    1

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName[] = "Beeper1" ;
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "Beeper1 Timer Demo",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;
     while (!SetTimer (hwnd, ID_TIMER, 1000, NULL))
          if (IDCANCEL == MessageBox (hwnd,
                              "Too many clocks or timers!", szAppName,
                              MB_ICONEXCLAMATION | MB_RETRYCANCEL))
               return FALSE ;
     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static BOOL fFlipFlop = FALSE ;
     HBRUSH      hBrush ;
     HDC         hdc ;
     PAINTSTRUCT ps ;
     RECT        rc ;

     switch (message)
          {
          case WM_TIMER :
               MessageBeep (0) ;

               fFlipFlop = !fFlipFlop ;
               InvalidateRect (hwnd, NULL, FALSE) ;

               return 0 ;

          case WM_PAINT :
               hdc = BeginPaint (hwnd, &ps) ;

               GetClientRect (hwnd, &rc) ;

               hBrush = CreateSolidBrush (fFlipFlop ? RGB(255,0,0) :
                                                      RGB(0,0,255)) ;
               FillRect (hdc, &rc, hBrush) ;
               EndPaint (hwnd, &ps) ;
               DeleteObject (hBrush) ;
               return 0 ;

          case WM_DESTROY :
               KillTimer (hwnd, ID_TIMER) ;
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 BEEPER1.DEF

;------------------------------------
; BEEPER1.DEF module definition file
;------------------------------------

NAME           BEEPER1

DESCRIPTION    'Timer Demo Program No. 1 (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

Because BEEPER1 audibly indicates every WM_TIMER message it receives, you
can get a good idea of the erratic nature of WM_TIMER messages by loading
BEEPER1 and performing some other actions within Windows. For instance, try
moving or resizing a window. This stops all messages, and BEEPER1 stops
beeping. When you complete the move or resize, you'll note that BEEPER1
doesn't get all the WM_TIMER messages it has missed, although the first two
messages may be less than a second apart.

This is our first encounter with a Windows program that uses color, so a
brief look at how Windows handles color is worthwhile here.


Windows' use of color

Windows uses an unsigned long (32-bit) integer value to represent a color.
The lowest three bytes specify red, green, and blue values that range from 0
through 255, as illustrated by Figure 5-4. This results in a potential 224
(or about 16 million) colors.

  (Figure 5-4. may be found in the printed book.)

This unsigned long is often referred to as an "RGB color." The WINDOWS.H
header file provides several macros for working with RGB color values. The
RGB macro in WINDOWS.H takes three arguments representing red, green, and
blue values and combines them into an unsigned long:

#define RGB(r,g,b) ((DWORD)(((BYTE)(r) | \
                        ((WORD)(g) << 8)) | \
                        (((DWORD)(BYTE)(b)) << 16)))

Thus, the value:

RGB (255, 0, 255)

is really 0x00FF00FF, an RGB color value for magenta. When all three
arguments are set to 0, the color is black; when the arguments are set to
255, the color is white. The GetRValue, GetGValue, and GetBValue macros
extract the unsigned character primary-color values from an unsigned long
RGB color value. These macros are sometimes handy when you're using Windows
functions that return RGB color values to your program.

The most common video display adapters used for Windows are the Enhanced
Graphics Adapter (EGA) and Video Graphics Array (VGA). In the display
resolutions that Windows uses, both these adapters can display 16 different
colors. (Some "Super VGA" boards can display 256 different colors under
Windows.) Windows can display additional colors by "dithering," which is
creating a pixel pattern that combines pixels of different pure colors.

Not all unique combinations of red, green, and blue bytes produce different
dithering patterns. For instance, on a color EGA or VGA, a red, green, or
blue value must generally be incremented by 4 to produce a different
dithering pattern. So for these adapters, you have 218 (or 262,144) dithered
colors.

BEEPER1 uses the FillRect function to color its client area. The first
parameter to FillRect is the device context handle, the second is a pointer
to the RECT structure, and the third is a handle to a "brush." A brush is a
graphics object that Windows uses to fill an area. Brushes can be solid
colors or composed of various hatchmarks or patterns.

BEEPER1 creates a brush of a solid color by calling CreateSolidBrush. The
only parameter is an RGB color value. Depending on the value of fFlipFlop,
BEEPER sets this parameter to RGB(255,0,0), which is red, or RGB(0,0,255),
which is blue.

A brush is a graphics object. If you create a brush, you must also delete it
when you're finished. After calling FillRect, BEEPER1 deletes the brush by
calling DeleteObject.



Method Two

The first method for setting the timer causes WM_TIMER messages to be sent
to the normal window procedure. With this second method, you can direct
Windows to send the timer messages to another function within your program.

The function that will receive these timer messages is termed a "call-back"
function. This is a function within your program that is called by Windows.
You tell Windows the address of this function (well, not really the address
of the function, but we'll get to that), and Windows later calls the
function. This should sound familiar because a program's window procedure is
really a type of call-back function. You tell Windows the address of the
function when registering the window class, and Windows calls the function
when sending messages to the program. However, call-back functions that are
not window procedures must be handled a little differently.

SetTimer is not the only Windows function that uses a call-back function.
The CreateDialog and DialogBox functions (discussed in Chapter 10) use
call-back functions to process messages in a dialog box; several Windows
functions (EnumChildWindows, EnumFonts, EnumObjects, EnumProps, and
EnumWindows) pass enumerated information to call-back functions; and several
less commonly used functions (GrayString, LineDDA, SetResourceHandler, and
SetWindowsHook) also require call-back functions. Call-back functions are
often a major hang-up for beginning Windows programmers. Some strange things
are involved. I'm first going to tell you how to use call-back functions,
and then I'll tell you the reasons for what you're doing.

Like a window procedure, a call-back function must be defined as FAR PASCAL
because it is called by Windows from outside the code segment of the
program. The parameters to the call-back function and the value returned
from the call-back function depend on the purpose of the call-back function.
In the case of the call-back function associated with the timer, the input
parameters are the same as the input parameters to a window procedure. The
timer call-back function returns a WORD value to Windows.

Let's name the call-back function TimerProc. (You can name it anything you
like.) It will process only WM_TIMER messages.

WORD FAR PASCAL TimerProc (HWND hwnd, WORD message, WORD wParam, LONG
lParam)
     {
[process WM_TIMER messages]
     return 0 ;
     }

The hwnd input parameter is the handle to the window specified when you call
SetTimer. Windows will send only WM_TIMER messages to TimerProc, so a
message will always equal WM_TIMER. The wParam value is the timer ID, and
the lParam value can be ignored. (It is set to the address of the function.)

Just as you must include your regular window procedure in the EXPORTS
section of the module definition (.DEF) file, you must also include the
names of any call-back functions within your program. When using a call-back
function named TimerProc, your module definition file contains the following
lines:

EXPORTS WndProc
        TimerProc

As I noted earlier, the first method for setting a timer requires a SetTimer
call that looks like this:

SetTimer (hwnd, 1, wMsecInterval, NULL) ;

When you use a call-back function to process WM_TIMER messages, the fourth
parameter to SetTimer is instead the far address of the call-back function.

But not really.

Now listen carefully: The far address that you must pass to Windows as the
fourth parameter of the SetTimer call is not the address of the function
within the program. It is instead a far address obtained from the Windows
function MakeProcInstance. To use MakeProcInstance, first define a variable
that is a far pointer to a function. You can use the WINDOWS.H identifier
FARPROC for this definition:

FARPROC lpfnTimerProc ;

In WinMain (or any other section of your program that is executed only once
for each instance), call MakeProcInstance. The parameters to
MakeProcInstance are the address of TimerProc and the value of hInstance.
MakeProcInstance returns a far pointer that you save in lpfnTimerProc:

lpfnTimerProc = MakeProcInstance (TimerProc, hInstance) ;

You can now use this lpfnTimerProc value when you call SetTimer:

SetTimer (hwnd, 1, wMsecInterval, lpfnTimerProc) ;

You're done. Now that's not too bad, is it?

Well, you may say, "This is so weird that I'll never use this method for
setting a timer. I'll use the first method, where I don't have to bother
with call-back functions." That's fine. But you're going to be forced to
deal with call-back functions, EXPORTS, and MakeProcInstance when we start
discussing dialog boxes. You can't do a dialog box without them. So you can
pay your dues now, or you can pay them later.

A sample program

Let's look at some sample code so you can see how this stuff fits together.
Then we'll explore MakeProcInstance some more. The BEEPER2 program, shown in
Figure 5-5, is functionally the same as BEEPER1 except that Windows sends
the timer messages to TimerProc rather than WndProc. To tell the C compiler
that TimerProc is a function so that we can use the name of the function
when calling MakeProcInstance, we declare TimerProc at the top of the
program along with WndProc. Notice that the program calls MakeProcInstance
for each instance.

I mentioned above that the lParam value passed to TimerProc is the address
of the TimerProc function. Not exactly. It is actually the value of
lpfnTimerProc returned from MakeProcInstance, if you ever need to use it.

 BEEPER2.MAK

#-----------------------
# BEEPER2.MAK make file
#-----------------------


beeper2.exe : beeper2.obj beeper2.def
     link beeper2, /align:16, NUL, /nod slibcew libw, beeper2
     rc beeper2.exe

beeper2.obj : beeper2.c
     cl -c -Gsw -Ow -W2 -Zp beeper2.c

 BEEPER2.C

/*----------------------------------------
   BEEPER2.C -- Timer Demo Program No. 2
                (c) Charles Petzold, 1990
  ----------------------------------------*/

#include <windows.h>
#define ID_TIMER    1

long FAR PASCAL WndProc   (HWND, WORD, WORD, LONG) ;
WORD FAR PASCAL TimerProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName[] = "Beeper2" ;
     FARPROC     lpfnTimerProc ;
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "Beeper2 Timer Demo",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     lpfnTimerProc = MakeProcInstance (TimerProc, hInstance) ;

     while (!SetTimer (hwnd, ID_TIMER, 1000, lpfnTimerProc))
          if (IDCANCEL == MessageBox (hwnd,
                              "Too many clocks or timers!", szAppName,
                              MB_ICONEXCLAMATION | MB_RETRYCANCEL))
               return FALSE ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     switch (message)
          {
          case WM_DESTROY :
               KillTimer (hwnd, ID_TIMER) ;
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

WORD FAR PASCAL TimerProc (HWND hwnd, WORD message, WORD wParam, LONG
lParam)
     {
     static BOOL fFlipFlop = FALSE ;
     HBRUSH      hBrush ;
     HDC         hdc ;
     RECT        rc ;
     MessageBeep (0) ;
     fFlipFlop = !fFlipFlop ;

     GetClientRect (hwnd, &rc) ;
     hdc = GetDC (hwnd) ;
     hBrush = CreateSolidBrush (fFlipFlop ? RGB(255,0,0) : RGB(0,0,255)) ;

     FillRect (hdc, &rc, hBrush) ;
     ReleaseDC (hwnd, hdc) ;
     DeleteObject (hBrush) ;

     return 0 ;
     }

 BEEPER2.DEF

;------------------------------------
; BEEPER2.DEF module definition file
;------------------------------------

NAME           BEEPER2

DESCRIPTION    'Timer Demo Program No. 2 (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc
               TimerProc


Proper handling of call-back functions

Let's summarize the three requirements for call-back functions. Any function
within your program that is called by Windows must be handled as follows:

  1.  The function must be defined as FAR PASCAL.

  2.  The function must be included in the EXPORTS section of the module
      definition (.DEF) file.

  3.  The address of the function that you give to Windows must be the
      return value from a MakeProcInstance call. (This third rule does not
      apply to window procedures that are passed to Windows as part of a
      window's class structure in a RegisterClass call. Windows itself
      handles the MakeProcInstance requirement in this case.)

      As you'll discover in Chapter 7, "Memory Management," these three
      requirements are closely related to each other. They are part of the
      overhead necessary for Windows to run several instances of the same
      program using the same code segment, where each instance must have its
      own data segment. Windows also requires this overhead to move the code
      segment and the data segments around in memory. Here are the practical
      results of the three requirements:

      When you define a function as FAR and you compile with the -Gw
      (Windows) flag, the compiler inserts special prolog and epilog code in
      the function. In assembly language, the prolog code sets the value of
      the AX register equal to the DS (data segment) register by using the
      PUSH DS and POP AX instructions. It then saves the value of DS (with
      PUSH DS) and sets DS equal to AX by using a MOV DS, AX instruction.
      The epilog code at the end of the function pops the original value of
      DS off the stack.

  4.  If the FAR function is also exported (that is, if the function is
      listed in the EXPORTS section of the module definition file), Windows
      replaces the PUSH DS and POP AX instructions at the top of the
      function prolog with NOP (no operation) instructions when the code
      segment is loaded into memory. With this change the function sets DS
      from the value of AX. But what is the value of AX on entry to the
      function? Well, if you didn't do anything else, the value of AX would
      be indeterminate. So would the operation of your program.

  5.  MakeProcInstance creates a small piece of code elsewhere in Windows
      called a "thunk." The far address returned from MakeProcInstance is
      the address of this thunk. The thunk loads the segment address of the
      data segment in AX and branches to the function. The function prolog
      then loads DS from AX. Perfect.

Note that MakeProcInstance requires hInstance as a parameter. You must use
MakeProcInstance to create a different thunk for each instance because each
instance has its own data segment. For a particular call-back function, the
thunks for each instance all branch to the same function address (because
all instances use the same code segment), but each thunk sets AX to a
different data segment--the data segment for that instance. When Windows
moves a data segment in memory, it must change the thunk for that instance
so that the thunk sets AX to the new data segment address. The thunk itself
is always in an unmoveable area of memory.

(To further complicate this matter, any reference in your program to a FAR
function--such as the address passed to the MakeProcInstance call and the
address that the thunk branches to--is not even the address of the function
within your program. Another small routine sits between the thunk and the
actual function. This routine loads the code segment into memory if it has
not yet been loaded or if it has been discarded from memory. But let's
forget about this for now and come back to it in Chapter 7. The last thing I
want to do here is make this subject sound as complex as it actually is.)

These requirements imply that you should never call exported far functions
directly from within your program. For instance, you might want to simulate
a timer message with the following statement:

TimerProc (hwnd, WM_TIMER, 1, 0L) ;  // WRONG !!!

It looks OK, but don't do it! The prolog of TimerProc will set DS equal to
AX, but the value of AX could be anything, and it very likely is not the
segment address of the program's data segment. If you need to directly call
an exported function from within your program, use the far pointer returned
from MakeProcInstance:

(*lpfnTimerProc) (hwnd, WM_TIMER, 1, 0L) ;  // RIGHT

This code calls the thunk, and the thunk sets AX equal to the correct data
segment before branching to the function.



Method Three

The third method of setting the timer is similar to the second method. It
requires a far pointer created from MakeProcInstance to a function that
processes the WM_TIMER messages. However, the hwnd parameter to SetTimer is
set to NULL, and the second parameter (normally the timer ID) is ignored.
Instead, the function returns a timer ID:

nTimerID = SetTimer (NULL, 0, wMsecInterval, lpfnTimerProc) ;

The nTimerID returned from SetTimer will be NULL if no timer is available.

The first parameter to KillTimer (usually the window handle) must also be
NULL. The timer ID must be the value returned from SetTimer:

KillTimer (NULL, nTimerID) ;

The hwnd parameter passed to the TimerProc timer function will also be NULL.
The wParam parameter is the timer ID, and lParam is lpfnTimerProc, the same
as in the second method.

This method for setting a timer is rarely used. It might come in handy if
you do a lot of SetTimer calls at different times in your program and don't
want to keep track of which timer IDs you've already used.

Now that you know how to use the Windows timer, you're ready for a couple of
useful timer programs.



USING THE TIMER FOR A STATUS REPORT

One use of a Windows timer is to periodically update a status report
displayed on the screen. The program can relinquish control until the next
WM_TIMER message and thus not hog precious processing time. The FREEMEM
program, shown in Figure 5-6 on the following pages, displays the amount of
free memory available in Windows in megabytes. The free memory value is
updated every second and is consistent with the figure shown in the Program
Manager's and File Manager's About box. FREEMEM can let you know how close
Windows is to running out of memory. While testing a new Windows program,
you may want to keep an eye on FREEMEM for a rough indication of how your
program is allocating and freeing memory.

 FREEMEM.MAK

#-----------------------
# FREEMEM.MAK make file
#-----------------------

freemem.exe : freemem.obj freemem.def
     link freemem, /align:16, NUL, /nod slibcew win87em libw, freemem
     rc freemem.exe

freemem.obj : freemem.c
     cl -c -Gsw -Ow -W2 -Zp freemem.c

 FREEMEM.C

/*------------------------------------------
   FREEMEM.C -- Free Memory Display Program
                (c) Charles Petzold, 1990
  ------------------------------------------*/

#include <windows.h>
#include <stdio.h>
#define ID_TIMER    1

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName[] = "FreeMem" ;
     HDC         hdc ;
     HWND        hwnd ;

     MSG         msg ;
     TEXTMETRIC  tm ;
     WNDCLASS    wndclass ;



     if (hPrevInstance)
          return FALSE ;

     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = NULL ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = szAppName ;

     RegisterClass (&wndclass) ;

     hwnd = CreateWindow (szAppName, "Free Memory",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     hdc = GetDC (hwnd) ;
     GetTextMetrics (hdc, &tm) ;
     ReleaseDC (hwnd, hdc) ;

     if (4 * tm.tmAveCharWidth > GetSystemMetrics (SM_CXICON) ||
               2 * tm.tmHeight > GetSystemMetrics (SM_CYICON))
          {
          MessageBox (hwnd, "Icon size too small for display!",
                      szAppName, MB_ICONEXCLAMATION | MB_OK) ;
          return FALSE ;
          }

     if (!SetTimer (hwnd, ID_TIMER, 1000, NULL))
          {
          MessageBox (hwnd, "Too many clocks or timers!",
                      szAppName, MB_ICONEXCLAMATION | MB_OK) ;
          return FALSE ;
          }

     ShowWindow (hwnd, SW_SHOWMINNOACTIVE) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static DWORD  dwFreeMem, dwPrevMem ;
     static RECT   rect ;
     char          cBuffer [20] ;
     HDC           hdc ;
     PAINTSTRUCT   ps ;

     switch (message)
          {
          case WM_TIMER :
               dwFreeMem = GetFreeSpace (0) ;

               if (dwFreeMem != dwPrevMem)
                    InvalidateRect (hwnd, NULL, TRUE) ;

               dwPrevMem = dwFreeMem ;
               return 0 ;

          case WM_SIZE :
               GetClientRect (hwnd, &rect) ;
               return 0 ;

          case WM_PAINT :
               hdc = BeginPaint (hwnd, &ps) ;

               DrawText (hdc, cBuffer,
                         sprintf (cBuffer, "%.2f megs",
                                  dwFreeMem / 1024.0 / 1024.0),
                         &rect, DT_WORDBREAK) ;

               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_QUERYOPEN :
               return 0 ;

          case WM_DESTROY :
               KillTimer (hwnd, ID_TIMER) ;
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 FREEMEM.DEF

;------------------------------------
; FREEMEM.DEF module definition file
;------------------------------------

NAME           FREEMEM

DESCRIPTION    'Free Memory Display (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

Because FREEMEM doesn't need much display space, I've written it to appear
as an icon at the bottom of the Windows screen. (See Figure 5-7.) This is
about as unobtrusive a window as you can create in Windows.

  (Figure 5-7. may be found in the printed book.)

Although FREEMEM's use of the timer is simple enough, the program
illustrates some interesting tricks that admittedly have nothing to do with
the timer.

Creative Use of Icons

Unlike most Windows programs, FREEMEM starts out life as an icon, displays
everything it needs to display within the icon, and cannot be opened into a
regular window. Most Windows applications have static pictorial icons that
you specify in the windows class structure. These icons are usually created
with the ICONEDIT utility supplied with the Windows Software Development Kit
(as you'll see in Chapter 8). So far, we've been using predefined Windows
icons in our programs. Alternatively, you can specify a NULL icon in the
window class structure with the statement:

wndclass.hIcon = NULL ;

A NULL icon means that the application is responsible for drawing the icon.
Windows sends the application WM_PAINT messages when the icon needs to be
painted. Thus, a  program can change the appearance of the icon while the
program is running. The CLOCK application included with Windows uses this
technique to display the clock even when the window is an icon. A NULL icon
is really just a tiny window that you can draw on in the same way that you
draw on the client area of a normal window. If you need to know when your
application is becoming an icon, you can get the information from the wParam
parameter of a WM_SIZE message. For instance, when CLOCK becomes an icon, it
eliminates the second hand from the clock and changes the timer from a
1-second interval to a 1-minute interval.

FREEMEM displays two lines of text within its icon. This will work fine on
the most common displays used for Windows, but it may not work for some
high-resolution boards that may use a larger system font. Because FREEMEM
cannot display correctly on these video boards, it checks the size of the
system font against the icon size and uses a message box to inform the user
if the icon size is too small.


Forcing the Icon

Shortly before entering the message loop in WinMain, most Windows
applications execute the function:

ShowWindow (hwnd, nCmdShow) ;

The nCmdShow variable is passed to the program as a parameter to WinMain.

If you run a program from the File Manager (by selecting Run from the File
menu, pressing Enter when the cursor is on the program name, or
double-clicking the program), nCmdShow is set equal to SW_SHOWNORMAL. If you
load an application as an icon (by selecting Load from the File menu or
pressing Shift-Enter with the cursor on the program name), nCmdShow is set
equal to SW_SHOWMINNOACTIVE. Your application usually doesn't have to figure
this out but simply passes this parameter to ShowWindow.

However, you aren't required to use the nCmdShow variable with ShowWindow.
Instead, FREEMEM uses the line:

ShowWindow (hwnd, SW_SHOWMINNOACTIVE) ;

This forces the window to appear as an icon regardless of the value of
nCmdShow. The active program remains active.

You can perform other tricks with this technique. If you always want a
particular application to appear first as a maximized full-screen display,
you can use:

ShowWindow (hwnd, SW_SHOWMAXIMIZED) ;

You can even force FREEMEM to occupy a particular icon position at the
bottom of the display. If you replace the existing ShowWindow call in
FREEMEM with:

ShowWindow (hwnd, (int) 0xFF8F) ;

the icon will be positioned in icon slot 15. This syntax is a little obscure
but is documented in the Programmer's Reference.


Keeping the Icon an Icon

FREEMEM does not allow itself to be opened into a regular window. Would you
believe this trick requires merely two simple lines in the WndProc function?
Here they are:

case WM_QUERYOPEN :
     return 0 ;

These two lines don't seem to be doing very much, but let's take a closer
look.

Windows sends a WM_QUERYOPEN message to a program when it wants to open an
icon into a window. Normally, WM_QUERYOPEN is passed on to the DefWindowProc
function, which returns a nonzero value; Windows then opens the icon. With
the two lines shown above, however, WndProc returns a value of 0 for a
WM_QUERYOPEN message. So when Windows asks, "Do you want to be opened?"
WndProc answers "Zero," which in this case means "No thanks."


Calculating Free Memory

When Windows sends FREEMEM a WM_TIMER message, FREEMEM must determine the
amount of free memory. Like the About box in the Program Manager and File
Manager, FREEMEM gets a free memory value by calling GetFreeSpace with a
parameter of 0. The GetFreeSpace function is new in Windows 3. Prior to
Windows 3, Windows programs used GlobalCompact with a parameter of 0 to
obtain the largest block of contiguous free memory in the system, which is
not nearly as useful as the total amount of free memory. (Windows memory
management is covered in detail in Chapter 7.)

If the free memory value has changed since the last GetFreeSpace call,
FREEMEM invalidates the client area to generate a WM_PAINT message. FREEMEM
processes WM_PAINT messages by calling DrawText, a convenient function for
simple word-wrapped text. FREEMEM converts the free memory in bytes to a
floating-point value in megabytes, and sprintf stores it formatted to two
decimal places.


Using Floating-Point Math

When a Windows program uses floating-point math (as FREEMEM does) and you
link with the floating-point emulator library (SLIBCEW.LIB for small model),
you must also include the WIN87LIB.LIB import library in the library field
of the LINK command.

Alternatively, you can compile with the -FPa switch and link with the
"alternate math library" (SLIBCAW.LIB for small library). This library does
not use the math coprocessor chip, even if one is present.



USING THE TIMER FOR A CLOCK

A clock is the most obvious application for the timer. Although digital
clocks were once in fashion, the pendulum has swung back (so to speak) to
analog clocks. But you already have an analog clock with Windows. Although
the CLOCK program has a digital-clock setting, I'm going to write a good old
digital-clock program--because it provides an interesting example of the use
of the timer. The DIGCLOCK program, shown in Figure 5-8, creates a popup
window that positions itself in the lower right corner of the display in the
icon area. The program displays the day of the week, the time, and the date.
(See Figure 5-9 on page 202.)

 DIGCLOCK.MAK

#------------------------
# DIGCLOCK.MAK make file
#------------------------

digclock.exe : digclock.obj digclock.def
     link digclock, /align:16, NUL, /nod slibcew libw, digclock
     rc digclock.exe

digclock.obj : digclock.c
     cl -c -Gsw -Ow -W2 -Zp digclock.c

 DIGCLOCK.C

/*-----------------------------------------
   DIGCLOCK.C -- Digital Clock Program
                 (c) Charles Petzold, 1990
  -----------------------------------------*/

#include <windows.h>
#include <time.h>
#define ID_TIMER    1

#define YEAR  (datetime->tm_year % 100)
#define MONTH (datetime->tm_mon  + 1)
#define MDAY  (datetime->tm_mday)
#define WDAY  (datetime->tm_wday)
#define HOUR  (datetime->tm_hour)
#define MIN   (datetime->tm_min)
#define SEC   (datetime->tm_sec)



long FAR PASCAL WndProc (HWND, WORD, WORD, LONG);
void SizeTheWindow (short *, short *, short *, short *) ;

char  sDate [2], sTime [2], sAMPM [2][5] ;
int   iDate, iTime ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName[] = "DigClock" ;
     HWND        hwnd;
     MSG         msg;
     short       xStart, yStart, xClient, yClient ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = NULL ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     SizeTheWindow (&xStart, &yStart, &xClient, &yClient) ;

     hwnd = CreateWindow (szAppName, szAppName,
                          WS_POPUP | WS_DLGFRAME | WS_SYSMENU,
                          xStart,  yStart,
                          xClient, yClient,
                          NULL, NULL, hInstance, NULL) ;

     if (!SetTimer (hwnd, ID_TIMER, 1000, NULL))
          {
          MessageBox (hwnd, "Too many clocks or timers!", szAppName,
                      MB_ICONEXCLAMATION | MB_OK) ;
          return FALSE ;
          }

     ShowWindow (hwnd, SW_SHOWNOACTIVATE) ;
     UpdateWindow (hwnd) ;
     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

void SizeTheWindow (short *pxStart,  short *pyStart,
                    short *pxClient, short *pyClient)
     {
     HDC        hdc ;
     TEXTMETRIC tm ;

     hdc = CreateIC ("DISPLAY", NULL, NULL, NULL) ;
     GetTextMetrics (hdc, &tm) ;
     DeleteDC (hdc) ;

     *pxClient = 2 * GetSystemMetrics (SM_CXDLGFRAME) + 16*tm.tmAveCharWidth
;
     *pxStart  =     GetSystemMetrics (SM_CXSCREEN)   - *pxClient ;
     *pyClient = 2 * GetSystemMetrics (SM_CYDLGFRAME) + 2*tm.tmHeight ;
     *pyStart  =     GetSystemMetrics (SM_CYSCREEN)   - *pyClient ;
     }

void SetInternational (void)
     {
     static char cName [] = "intl" ;

     iDate = GetProfileInt (cName, "iDate", 0) ;
     iTime = GetProfileInt (cName, "iTime", 0) ;

     GetProfileString (cName, "sDate",  "/", sDate,     2) ;
     GetProfileString (cName, "sTime",  ":", sTime,     2) ;
     GetProfileString (cName, "s1159", "AM", sAMPM [0], 5) ;
     GetProfileString (cName, "s2359", "PM", sAMPM [1], 5) ;
     }

void WndPaint (HWND hwnd, HDC hdc)
     {
     static char szWday[] = "Sun\0Mon\0Tue\0Wed\0Thu\0Fri\0Sat" ;
     char        cBuffer[40] ;
     long        lTime ;
     RECT        rect ;
     short       nLength ;
     struct tm   *datetime ;

     time (&lTime) ;
     datetime = localtime (&lTime) ;
     nLength = wsprintf (cBuffer, "  %s  %d%s%02d%s%02d  \r\n",
               (LPSTR) szWday + 4 * WDAY,
               iDate == 1 ? MDAY  : iDate == 2 ? YEAR  : MONTH, (LPSTR)
sDate,
               iDate == 1 ? MONTH : iDate == 2 ? MONTH : MDAY,  (LPSTR)
sDate,
               iDate == 1 ? YEAR  : iDate == 2 ? MDAY  : YEAR) ;

     if (iTime == 1)
          nLength += wsprintf (cBuffer + nLength, "  %02d%s%02d%s%02d  ",
                               HOUR, (LPSTR) sTime, MIN, (LPSTR) sTime, SEC)
;
     else
          nLength += wsprintf (cBuffer + nLength, "  %d%s%02d%s%02d %s  ",
                               (HOUR % 12) ? (HOUR % 12) : 12,
                               (LPSTR) sTime, MIN, (LPSTR) sTime, SEC,
                               (LPSTR) sAMPM [HOUR / 12]) ;

     GetClientRect (hwnd, &rect) ;
     DrawText (hdc, cBuffer, -1, &rect, DT_CENTER | DT_NOCLIP) ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     HDC         hdc ;
     PAINTSTRUCT ps ;

     switch (message)
          {
          case WM_CREATE :
               SetInternational () ;
               return 0 ;

          case WM_TIMER :
               InvalidateRect (hwnd, NULL, FALSE) ;
               return 0 ;

          case WM_PAINT :
               hdc = BeginPaint (hwnd, &ps) ;
               WndPaint (hwnd, hdc) ;
               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_WININICHANGE :
               SetInternational () ;
               InvalidateRect (hwnd, NULL, TRUE) ;
               return 0 ;

          case WM_DESTROY :
               KillTimer (hwnd, ID_TIMER) ;
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 DIGCLOCK.DEF

;-------------------------------------
; DIGCLOCK.DEF module definition file
;-------------------------------------

NAME           DIGCLOCK

DESCRIPTION    'Digital Clock (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

  (Figure 5-9. may be found in the printed book.)

All the programs shown so far have used the window style WS_OVERLAPPEDWINDOW
as the third parameter to the CreateWindow function. DIGCLOCK uses the
window style:

WS_POPUP | WS_DLGFRAME | WS_SYSMENU

This creates a style of window known as "popup," with a dialog box frame and
a system menu. The popup style is most commonly used for dialog boxes and
message boxes, and only rarely for applications. DIGCLOCK also uses yet
another variation of the ShowWindow call:

ShowWindow (hwnd, SW_SHOWNOACTIVATE) ;

Normally, a program becomes the active window when you run it.
SW_SHOWNOACTIVATE tells Windows that the program that loaded DIGCLOCK should
remain the active window. You can make DIGCLOCK active, however, by clicking
on its window with the mouse or by pressing Alt-Tab or Alt-Esc. Although
DIGCLOCK has no system menu box, you can still access the system menu when
DIGCLOCK is active by pressing Alt-Spacebar. If you select Move, you can
move the window with the keyboard.

Positioning and Sizing the Popup

The DIGCLOCK popup window is positioned at the lower right corner of the
display. The window must be large enough to accommodate two lines of text of
16 characters each. The SizeTheWindow procedure in DIGCLOCK.C determines the
correct parameters to use in the CreateWindow call. Normally, a program
cannot obtain a text size without first creating a window, because it needs
the window handle to obtain a device context handle. DIGCLOCK gets around
this problem by obtaining an information device context handle for the
screen using CreateIC. This function is similar to CreateDC (used in the
BLOWUP program in Chapter 4) but is used to obtain information from the
device context. The text size in combination with information available from
GetSystemMetrics is enough to derive an initial starting position and window
size.


Getting the Date and Time

In its WndPaint function, DIGCLOCK uses the time and localtime C functions
available in the Microsoft C Compiler library to determine the current date
and time. The localtime function puts all the information we need into a
structure; several macro definitions near the top of the program help make
the wsprintf calls more readable. (You should avoid making MS-DOS or ROM
BIOS function calls in your Windows programs; use Windows functions or the C
run time library instead.)


Going International

Windows includes international support. The WIN.INI file created during
installation of Windows contains a section headed [intl]. This lists
information concerning formats of dates, time, currency, and numbers. You
can display dates in one of three different formats: month-day-year,
year-month-day, or day-month-year. The separator between these three numbers
can be a slash, a dash, a period, or, in fact, any character you like. You
can display the time in either 12-hour or 24-hour format; a semicolon or a
period is commonly used to separate hours, minutes, and seconds.

The SetInternational function in DIGCLOCK retrieves this formatting
information from the WIN.INI file by using the Windows GetProfileInt (for
integers) and GetProfileString (for strings). These calls must include
default values if Windows cannot find the values in WIN.INI.
SetInternational stores the values in global variables that have the same
names as the text strings that identify them in WIN.INI. The WndPaint
function uses the values obtained from WIN.INI to format the date and time
displays and then calls DrawText to center the two lines of text within the
window.

As you would expect, whenever DIGCLOCK's window procedure receives a
WM_TIMER message, it invalidates the window to generate a WM_PAINT message.
But WndProc also invalidates the window when it receives a WM_WININICHANGE
message. Any application that changes WIN.INI sends the WM_WININICHANGE
message to all active Windows applications. If the [intl] section of WIN.INI
is changed, DIGCLOCK will know and will obtain the new international
information. To see how this works, load DIGCLOCK, load the CONTROL PANEL
program included with Windows, select Country Settings from the Preferences
menu, and change either the date format, the date separator, the time
format, or the time separator. Now press Enter. The Control Panel updates
the WIN.INI file, and DIGCLOCK's display reflects that change--Windows'
message magic at work.

When the window procedure receives a WM_WININICHANGE message, it invalidates
the window using:

InvalidateRect (hwnd, NULL, TRUE) ;

When DIGCLOCK receives a WM_TIMER message, it invalidates the window using:

InvalidateRect (hwnd, NULL, FALSE) ;

A value of TRUE in the last parameter tells Windows to erase the background
before drawing the window. A value of FALSE tells Windows simply to draw
over the existing background. We use FALSE when processing WM_TIMER messages
because this approach reduces flickering of the display. You may be
wondering why we need to use the TRUE value at all.

A TRUE value is necessary when processing WM_WININICHANGE messages because
the length of the displayed strings can change by several characters if you
switch the time format from 12 hours to 24 hours. However, the largest
change that occurs as a result of a WM_TIMER message is two characters--for
instance, when the date advances from 12/31/87 to 1/1/88--and the formatted
string that WndPaint uses for the display has a couple of blanks on each end
to account for this change in length and the proportional font.

We could also have DIGCLOCK process WM_TIMECHANGE messages, which notify
applications of changes to the system date or time. Because DIGCLOCK is
updated every second by WM_TIMER messages this is unnecessary. Processing
WM_TIMECHANGE messages would make more sense for a clock that was updated
every minute.



WINDOWS STANDARD TIME

If you've been scouting around the Programmer's Reference of the Windows
Software Development Kit, you may be wondering why the Windows
GetCurrentTime function is not used in DIGCLOCK. The answer is that
GetCurrentTime tells you about "Windows time" rather than real time. This is
the time (in milliseconds) since the beginning of the current Windows
session. GetCurrentTime is used mostly for calculating a difference from the
time returned from GetMessageTime. You can use these two calls while
processing a message to determine how long the message was in the message
queue before you retrieved it for processing.




Chapter 6  Child Window Controls
────────────────────────────────────────────────────────────────────────────

Chapter 4 showed programs in the CHECKER series that display a grid of
rectangles. When you click the mouse in a rectangle, the program draws an X.
When you click again, the X disappears. As you played with the program, you
may have thought that the rectangle with the X inside looked vaguely
familiar. If the rectangle were reduced in size, it would resemble a "check
box" that Windows programs use in dialog boxes to allow the selection of
options.

Although the CHECKER1 and CHECKER2 versions of this program use only one
main window, the CHECKER3 version uses a child window for each rectangle.
The rectangles are maintained by a separate window procedure called
ChildWndProc. If we wanted to, we could add a facility to ChildWndProc to
send a message to its parent window procedure (WndProc) whenever a rectangle
is checked or unchecked.

Here's how: The child window procedure can determine the window handle of
its parent by calling GetParent:

hwndParent = GetParent (hwnd) ;

where hwnd is the window handle of the child window. It can then send a
message to the parent window procedure:

SendMessage (hwndParent, message, wParam, lParam) ;

Perhaps for this message the child window could set wParam to its child
window ID. The lParam could be set to a 1 if the child window were being
checked and a 0 if it were being unchecked.

This in effect creates a "child window control." The child window processes
mouse and keyboard messages and notifies the parent window when the child
window's state has changed. In this way, the child window becomes an input
device for the parent window.

Although you can create your own child window controls, you can also take
advantage of several predefined window classes (and window procedures) that
your program can use to create child window controls. These controls take
the form of buttons, check boxes, edit boxes, list boxes, combo boxes, text
strings, and scroll bars. For instance, if you want to put a button labeled
"Recalculate" in a corner of your spreadsheet program, you can create it
with a single CreateWindow call. You don't have to worry about the mouse
logic or button painting logic or about making the button "flash" when it's
clicked. That's all done in Windows. All you have to do is trap WM_COMMAND
messages--that's how the button informs your window procedure when it has
been triggered.

Is it really that simple? Well, almost.

Child window controls are used most often in dialog boxes. As you'll see in
Chapter 10, the position and size of the child window controls are defined
in a dialog box template contained in the program's resource script.
However, you can also use predefined child window controls on the surface of
a normal overlapped window's client area. You create each child window with
a CreateWindow call and adjust the position and size of the child windows
with calls to MoveWindow. The parent window procedure sends messages to the
child window controls, and the child window controls send messages back to
the parent window procedure.

When you bring up your normal window, you first define a window class and
register it with Windows using RegisterClass. You then create the window
based on that class using CreateWindow. When you use one of the predefined
controls, however, you do not register a window class for the child window.
The class already exists within Windows and has one of these names:
"button," "static," "scrollbar," "edit," "listbox," or "combobox." You
simply use the name as the window class parameter in CreateWindow. The
window style parameter to CreateWindow defines more precisely the appearance
and functionality of the child window control. Windows contains the window
procedures that process messages to the child windows based on these
classes.

Using child window controls directly on the surface of your window involves
tasks of a lower level than are required for using child window controls in
dialog boxes, where the dialog box manager adds a layer of insulation
between your program and the controls themselves. In particular, you'll
discover that the child window controls you create on the surface of your
window have no built-in facility to move the input focus from one control to
another using the Tab or cursor movement keys. A child window control can
obtain the input focus, but once it does, it won't relinquish the input
focus back to the parent window. This is a problem we'll struggle with
throughout this chapter.

THE BUTTON CLASS

We'll begin our exploration of the button window class with a program called
BTNLOOK ("button look"), which is shown in Figure 6-1. BTNLOOK creates 11
child window button controls, one for each of the 11 styles of buttons.

 BTNLOOK.MAK

#-----------------------
# BTNLOOK.MAK make file
#-----------------------

btnlook.exe : btnlook.obj btnlook.def
     link btnlook, /align:16, NUL, /nod slibcew libw, btnlook
     rc btnlook.exe

btnlook.obj : btnlook.c
     cl -c -Gsw -Ow -W2 -Zp btnlook.c

 BTNLOOK.C

/*----------------------------------------
   BTNLOOK.C -- Button Look Program
                (c) Charles Petzold, 1990
  ----------------------------------------*/

#include <windows.h>
#include <stdio.h>

struct
     {
     long style ;
     char *text ;
     }
     button[] =
     {
     BS_PUSHBUTTON,      "PUSHBUTTON",
     BS_DEFPUSHBUTTON,   "DEFPUSHBUTTON",
     BS_CHECKBOX,        "CHECKBOX",
     BS_AUTOCHECKBOX,    "AUTOCHECKBOX",
     BS_RADIOBUTTON,     "RADIOBUTTON",
     BS_3STATE,          "3STATE",
     BS_AUTO3STATE,      "AUTO3STATE",
     BS_GROUPBOX,        "GROUPBOX",



     BS_USERBUTTON,      "USERBUTTON",
     BS_AUTORADIOBUTTON, "AUTORADIO",
     BS_PUSHBOX,         "PUSHBOX"
     } ;

#define NUM (sizeof button / sizeof button [0])

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName[] = "BtnLook" ;
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "Button Look",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }
long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static char  szPrm []    = "wParam       LOWORD(lParam)
HIWORD(lParam)",
                  szTop []    = "Control ID   Window Handle   Notification",
                  szUnd []    = "__________   _____________   ____________",
                  szFormat [] = " %5u           %4X          %5u",
                  szBuffer [50] ;
     static HWND  hwndButton [NUM] ;
     static RECT  rect ;
     static int   cxChar, cyChar ;
     HDC          hdc ;
     PAINTSTRUCT  ps ;
     int          i ;
     TEXTMETRIC   tm ;

     switch (message)
          {
          case WM_CREATE :
               hdc = GetDC (hwnd) ;
               SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;
               GetTextMetrics (hdc, &tm) ;
               cxChar = tm.tmAveCharWidth ;
               cyChar = tm.tmHeight + tm.tmExternalLeading ;
               ReleaseDC (hwnd, hdc) ;

               for (i = 0 ; i < NUM ; i++)
                    hwndButton [i] = CreateWindow ("button", button[i].text,
                              WS_CHILD | WS_VISIBLE | button[i].style,
                              cxChar, cyChar * (1 + 2 * i),
                              20 * cxChar, 7 * cyChar / 4,
                              hwnd, i,
                              ((LPCREATESTRUCT) lParam) -> hInstance, NULL)
;
               return 0 ;

          case WM_SIZE :
               rect.left   = 24 * cxChar ;
               rect.top    =  3 * cyChar ;
               rect.right  = LOWORD (lParam) ;
               rect.bottom = HIWORD (lParam) ;
               return 0 ;

          case WM_PAINT :
               InvalidateRect (hwnd, &rect, TRUE) ;

               hdc = BeginPaint (hwnd, &ps) ;
               SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;
               SetBkMode (hdc, TRANSPARENT) ;
               TextOut (hdc, 24 * cxChar, 1 * cyChar, szPrm, sizeof szPrm -
1) ;
               TextOut (hdc, 24 * cxChar, 2 * cyChar, szTop, sizeof szTop -
1) ;
               TextOut (hdc, 24 * cxChar, 2 * cyChar, szUnd, sizeof szUnd -
1) ;

               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_COMMAND :
               ScrollWindow (hwnd, 0, -cyChar, &rect, &rect) ;
               hdc = GetDC (hwnd) ;
               SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;

               TextOut (hdc, 24 * cxChar, cyChar * (rect.bottom / cyChar -
1),
                        szBuffer, sprintf (szBuffer, szFormat, wParam,
                        LOWORD (lParam), HIWORD (lParam))) ;

               ReleaseDC (hwnd, hdc) ;
               ValidateRect (hwnd, NULL) ;
               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 BTNLOOK.DEF

;------------------------------------
; BTNLOOK.DEF module definition file
;------------------------------------

NAME           BTNLOOK

DESCRIPTION    'Button Look Program (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

As you click on each button, it sends a WM_COMMAND message to the parent
window procedure, which is the familiar WndProc. BTNLOOK's WndProc displays
the wParam and lParam parameters of this message on the right half of the
client area, as shown in Figure 6-2.

  (Figure 6-2. may be found in the printed book.)

Creating the Child Windows

BTNLOOK defines a structure called button that contains button window styles
and descriptive text strings for each of the 11 types of buttons. The button
window styles all begin with the letters BS, which stand for "button style."

The 11 button child windows are created in a for loop during WM_CREATE
message processing in WndProc. The CreateWindow call uses the following
parameters:

╓┌────────────────────┌──────────────────────────────────────────────────────╖
────────────────────────────────────────────────────────────────────────────
Class name           "button"
Window text          button[i].text
Window style         WS_CHILD | WS_VISIBLE | button[i].style
x position           cxChar
y position           cyChar * (1 + 2 * i)
────────────────────────────────────────────────────────────────────────────
y position           cyChar * (1 + 2 * i)
Width                20 * xChar
Height               7 * yChar / 4
Parent window        hwnd


────────────────────────────────────────────────────────────────────────────
Child window ID        i
Instance handle        ((LPCREATESTRUCT) lParam) -> hInstance
Extra parameters       NULL

The class name parameter is the predefined name. The window style uses WS-
_CHILD, WS_VISIBLE, and one of the eleven button styles (BS_PUSHBUTTON, BS-
_DEFPUSHBUTTON, and so forth) in the button structure. The window text
parameter (which for a normal window is the text that appears in the caption
bar) is text that will be displayed with each button. I've simply used text
that identifies the button style.

The x position and y position parameters indicate the placement of the upper
left corner of the child window relative to the upper left corner of the
parent window's client area. The width and height parameters specify the
width and height of each child window.

The child window ID parameter should be unique for each child window. This
ID helps your window procedure identify the child window when processing
WM_COMMAND messages from it.

The instance handle parameter of the CreateWindow call looks a little
strange, but we're taking advantage of the fact that during a WM_CREATE
message lParam is actually a pointer to a structure of type CREATESTRUCT
("creation structure") that has a member hInstance. So we cast lParam into a
long (or far) pointer to a CREATESTRUCT structure and get hInstance out.

(Some Window programs use a global variable named hInst to give window
procedures access to the instance handle available in WinMain. In WinMain,
you need simply set:

hInst = hInstance ;

before creating the main window. In Chapter 4 we used GetWindowWord to
obtain the instance handle:

GetWindowWord (hwnd, GWW_HINSTANCE)

Any of these methods is fine.)

After the CreateWindow call, we don't have to do anything more with these
child windows. The button window procedure within Windows maintains them for
us and handles all repainting jobs. (The exception is the button with the
BS_USERBUTTON style; as I'll discuss shortly, this button style requires the
program to draw the button.) At the program's termination, Windows destroys
these child windows when the parent window is destroyed.


The Child Talks to Its Parent

When you run BTNLOOK, you see the different button types displayed on the
left side of the client area. (The BS_USERBUTTON button is not visible.) As
I mentioned earlier, when you click a button with the mouse, the child
window control sends a WM_COMMAND message to its parent window. BTNLOOK
traps the WM_COMMAND message and displays the values of wParam and lParam.
Here's what they mean:

────────────────────────────────────────────────────────────────────────────
wParam                       Child window ID
LOWORD (lParam)              Child window handle
HIWORD (lParam)              Notification code

The child window ID is the value passed to CreateWindow when the child
window is created. In BTNLOOK these IDs are 0 through 10 for the 11 buttons
displayed in the client area. The child window handle is the value that
Windows returns from the CreateWindow call.

The notification code is a submessage code that the child window uses to
tell the parent window in more detail what the message means. The possible
values of button notification codes are defined in WINDOWS.H:

Button Notification
Code Identifier                      Value
────────────────────────────────────────────────────────────────────────────
BN_CLICKED                           0

BN_PAINT                             1

BN_HILITE                            2

BN_UNHILITE                          3

BN_DISABLE                           4

BN_DOUBLECLICKED                     5


For all button styles except BS_USERBUTTON, this notification code is always
BN_CLICKED, which simply tells the parent window that the button has been
clicked. The other notification codes are used for the BS_USERBUTTON style.

You'll notice that when you click a button with the mouse, a dashed line
surrounds the text of the button. This indicates that the button has the
input focus. All keyboard input now goes to the child window button control
rather than to the main window. However, when the button control has the
input focus, it ignores all keystrokes except the Spacebar, which now has
the same effect as a mouse click.


The Parent Talks to Its Child

Although BTNLOOK does not demonstrate this fact, a window procedure can also
send messages to the child window control. Five button-specific messages are
defined in WINDOWS.H; each begins with the letters "BM," which stand for
"button message." These messages are defined in WINDOWS.H in terms of the
WM_USER identifier:

#define BM_GETCHECK  (WM_USER+0)
#define BM_SETCHECK  (WM_USER+1)
#define BM_GETSTATE  (WM_USER+2)
#define BM_SETSTATE  (WM_USER+3)
#define BM_SETSTYLE  (WM_USER+4)

The WM_USER identifier is available for programs to define their own
messages beyond the predefined messages. Each window class can have its own
separate set of messages unique to that class. The other classes of
predefined child window controls also can have their own messages defined in
terms of WM_USER.

The BM_GETCHECK and BM_SETCHECK messages are sent by a parent window to a
child window control to get and set the check mark of check boxes and radio
buttons. The BM_GETSTATE and BM_SETSTATE messages refer to the normal or
"pushed" state of a window when you click it with the mouse or press it with
the Spacebar. We'll see how these messages work when we look at each type of
button. The BM_SETSTYLE message lets you change the button style after the
button is created.


Push Buttons

The first two buttons shown in BTNLOOK are "push" buttons. A push button is
a rectangle enclosing text specified in the window text parameter of the
CreateWindow call. The rectangle takes up the full height and width of the
dimensions given in the CreateWindow or MoveWindow call. The text is
centered within the rectangle.

Push-button controls are used mostly to trigger an immediate action without
retaining any type of on/off indication. The two types of push-button
controls have window styles called BS_PUSHBUTTON and BS_DEFPUSHBUTTON. The
"DEF" in BS_DEFPUSHBUTTON stands for "default." When used to design dialog
boxes, BS_PUSHBUTTON controls and BS_DEFPUSHBUTTON controls function
differently from one another. When used as child window controls, however,
the two types of push buttons function the same way, although
BS_DEFPUSHBUTTON has a heavier outline.

A push button looks best when its height is 7/4 times the height of a
SYSTEM_FONT character, which is what BTNLOOK uses. The push button's width
must accommodate at least the width of the text plus two additional
characters.

When the mouse cursor is inside the push button, pressing the mouse button
causes the button to repaint itself using 3D-style shading to appear as if
it's been depressed. Releasing the mouse button restores the original
appearance and sends a WM_COMMAND message to the parent window with
notification code BN_CLICKED. As with the other button types, when a push
button has the input focus, a dashed line surrounds the text, and pressing
and releasing the Spacebar has the same effect as pressing and releasing the
mouse button.

You can simulate a push-button flash by sending the window a BM_SETSTATE
message. This causes the button to be depressed:

SendMessage (hwndButton, BM_SETSTATE, 1, 0L) ;

This call causes the button to return to normal:

SendMessage (hwndButton, BM_SETSTATE, 0, 0L) ;

The hwndButton window handle is the value returned from the CreateWindow
call.

You can also send a BM_GETSTATE message to a push button. The child window
control returns the current state of the button--TRUE if the button is
depressed and FALSE (or 0) if normal. Most applications do not require this
information, however. And because push buttons do not retain any on/off
information, the BM_SETCHECK and BM_GETCHECK messages are not used.

Buttons created with the BS_PUSHBOX style are displayed only when the button
has the input focus. This style of button is rarely used by Windows
applications.


Check Boxes

A check box is a square box with text; the text usually appears to the right
of the check box. (If you include the BS_LEFTTEXT style when creating the
button, the text appears to the left.) Check boxes are usually incorporated
in an application to allow a user to select options. The check box commonly
functions as a toggle switch: Clicking the box once causes an X to appear;
clicking again toggles the X off.

The two most common styles for a check box are BS_CHECKBOX and
BS_AUTOCHECKBOX. When you use the BS_CHECKBOX style, you must set the X mark
yourself by sending the control a BM_SETCHECK message. The wParam parameter
is set to 1 to create an X and to 0 to remove it. You can obtain the current
check state of the box by sending the control a BM_GETCHECK message. You
might use code like this to toggle the X mark when processing a WM_COMMAND
message from the control:

SendMessage (LOWORD (lParam), BM_SETCHECK, (WORD)
          !SendMessage (LOWORD (lParam), BM_GETCHECK, 0, 0L), 0L) ;

Note the ! operator in front of the second SendMessage call. The low word of
lParam is the child window handle passed to your window procedure in the
WM_COMMAND message. When you later need to know the state of the button,
send it another BM_GETCHECK message. Or you can retain the current check
state in a static variable in your window procedure. You can also initialize
a BS_CHECKBOX check box with an X by sending it a BM_SETCHECK message:

SendMessage (hwndButton, BM_SETCHECK, 1, 0L) ;

For the BS_AUTOCHECKBOX style, the button control itself toggles the X on
and off. Your window procedure can ignore WM_COMMAND messages. When you need
the current state of the button, send the control a BM_GETCHECK message:

nCheck = (WORD) SendMessage (hwndButton, BM_GETCHECK, 0, 0L) ;

The value of nCheck is TRUE or nonzero if the button is checked, FALSE or
zero if not.

The other two check box styles are BS_3STATE and BS_AUTO3STATE. As their
names indicate, these styles can display a third state as well--a gray color
within the check box--which occurs when you send the control a WM_SETCHECK
message with wParam equal to 2. The gray color indicates to the user that
the box cannot be checked_ that is, that it's disabled. However, the check
box control continues to send messages to the parent when the box is
clicked. Better methods for disabling a check box are described later.

The check box is aligned with the rectangle's left edge and is centered
within the top and bottom dimensions of the rectangle that were specified
during the CreateWindow call. Clicking anywhere within the rectangle causes
a WM_COMMAND message to be sent to the parent. The minimum height for a
check box is one character height. The minimum width is the number of
characters in the text plus two.


Radio Buttons

A radio button looks very much like a check box except that it is shaped
like a circle rather than a box. A heavy dot within the circle indicates
that the radio button has been checked. The radio button has the window
style BS_RADIOBUTTON or BS_AUTORADIOBUTTON, but the latter is used only in
dialog boxes.

In dialog boxes, groups of radio buttons are conventionally used to indicate
mutually exclusive options. (For instance, look at the dialog box in the
Windows Terminal program that appears when you select Communications from
the Settings menu.) Unlike check boxes, radio buttons do not work as
toggles--that is, when you click a radio button a second time, its state
remains unchanged.

When you receive a WM_COMMAND message from a radio button, you should
display its check by sending it a BM_SETCHECK message with wParam equal to
1:

SendMessage (hwndButton, BM_SETCHECK, 1, 0L) ;

For all other radio buttons in the same group, you can turn off the checks
by sending them BM_SETCHECK messages with wParam equal to 0:

SendMessage (hwndButton, BM_SETCHECK, 0, 0L) ;


Group Boxes

The group box, style BS_GROUPBOX, is an oddity in the button class. It
neither processes mouse or keyboard input nor sends WM_COMMAND messages to
its parent. The group  box is a rectangular outline with its window text at
the top. Group boxes are often used to enclose other button controls.


User-Defined Buttons

The user-defined button, which has the style BS_USERBUTTON, is the only
button that sends WM_COMMAND messages to its parent with these notification
codes:

────────────────────────────────────────────────────────────────────────────
BN_PAINT                 Button is normal
BN_HILITE                Button is being clicked
BN_UNHILITE              Clicking is finished
BN_DISABLE               Button is disabled

These notification codes indicate that the window must be painted. The
parent window is responsible for this painting. It can use the low word of
lParam to obtain the window handle of the button, GetClientRect to determine
the button's dimensions, and GetDC to get the button's device context in
preparation for painting. BTNLOOK doesn't process these notification codes,
so only a dotted outline appears when the button has the input focus.


Changing the Button Text

You can change the text in a button (or in any other window) by calling
SetWindowText:

SetWindowText (hwnd, lpszString) ;

where hwnd is a handle to the window whose text is being changed and
lpszString is a long (or far) pointer to a null-terminated string. For a
normal window, this text is the text of the caption bar. For a button
control, it's the text displayed with the button.

You can also obtain the current text of a window:

nLength = GetWindowText (hwnd, lpszBuffer, nMaxLength) ;

The nMaxLength parameter specifies the maximum number of characters to copy
into the buffer pointed to by lpszBuffer. The function returns the string
length copied. You can prepare your program for a particular text length by
first calling:

nLength = GetWindowTextLength (hwnd) ;


Visible and Enabled Buttons

To receive mouse and keyboard input, a child window must be both visible
(displayed) and enabled. When a child window is visible but not enabled,
Windows displays it in gray rather than black.

If you do not include WS_VISIBLE in the window class when creating the child
window, the child window will not be displayed until you make a call to
ShowWindow:

ShowWindow (hwndChild, SW_SHOWNORMAL) ;

If you include WS_VISIBLE in the window class, you do not need to call
ShowWindow. However, you can hide the child window by a call to ShowWindow:

ShowWindow (hwndChild, SW_HIDE) ;

You can determine if a child window is visible by a call to:

IsWindowVisible (hwndChild) ;

You can also enable and disable a child window. By default, a window is
enabled. You can disable it by calling:

EnableWindow (hwndChild, FALSE) ;

For button controls, this has the effect of graying the button text string.
The button no longer responds to mouse or keyboard input. This is the best
method for indicating that a button option is currently unavailable.

You can reenable a child window by calling:

EnableWindow (hwndChild, TRUE) ;

You can determine whether a child window is enabled by calling:

IsWindowEnabled (hwndChild) ;


Buttons and Input Focus

As I noted earlier in this chapter, push buttons, check boxes, radio
buttons, and user-defined buttons receive the input focus when they are
clicked with the mouse. The control indicates it has the input focus by a
dashed line surrounding the text. When the child window control gets the
input focus, the parent window loses it; all keyboard input then goes to the
control rather than to the parent window. However, the child window control
responds only to the Spacebar, which now functions like the mouse. This
situation presents an obvious problem: Your program has lost control of
keyboard processing. Let's see what we can do about it.

When Windows switches the input focus from one window (such as a parent) to
another (such as a child window control), it first sends a WM_KILLFOCUS
message to the window losing the input focus. The wParam parameter is the
handle of the window that is to receive the input focus. Windows then sends
a WM_SETFOCUS message to the window receiving the input focus, with wParam
the handle of the window losing the input focus. (In both cases, wParam may
be NULL, which indicates that no window has or is receiving the input
focus.)

A parent window can prevent a child window control from getting the input
focus by processing WM_KILLFOCUS messages. Assume that the array hwndChild
contains the window handles of all child windows. (These were saved in the
array during the CreateWindow calls that created the windows.) NUM is the
number of child windows:

case WM_KILLFOCUS :
     for (i = 0 ; i < NUM ; i++)
          if (hwndChild [i] == wParam)
               {
               SetFocus (hwnd) ;
               break ;
               }
     return 0 ;

In this code, when the parent window detects that it's losing the input
focus to one of its child window controls, it calls SetFocus to restore the
input focus to itself.

Here's a simpler (but less obvious) way of doing it:

case WM_KILLFOCUS :
     if (hwnd == GetParent (wParam))
          SetFocus (hwnd) ;
     return 0 ;

Both these methods have a shortcoming, however: They prevent the button from
responding to the Spacebar, because the button never gets the input focus. A
better approach would be to let the button get the input focus but also to
include the facility for the user to move from button to button using the
Tab key. At first this sounds impossible, but I'll show you how to
accomplish it with a technique called "window subclassing" in the COLORS1
program shown later in this chapter.



CONTROLS AND COLORS

I deliberately put a little "gotcha" into BTNLOOK. There is something wrong
with the program. It may not be immediately apparent, but here's how to see
it: Run BTNLOOK and bring up the Control Panel program included with
Windows. Select the Colors icon; this brings up a dialog box that lets you
change system colors. Select Color Palette and change the colors of Window
Background and Window Text, and save the new settings by clicking the OK
button: The background and text of the buttons (with the exception of the
push buttons) in BTNLOOK changes to reflect the new colors, but the
background color and text color of the rest of BTNLOOK's client area remain
the same--black text on a white background. It looks dreadful.

What happened? Simple--the button colors change because they are based on
the system colors you set in Control Panel, but BTNLOOK's client-area
background remains white because white is specified in the window class:

wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;

When BTNLOOK writes text to the display, it uses the text color and
background color defined in the default device context. These are always
black and white, regardless of the system colors set with Control Panel.

Let's fix this problem. I discussed Windows' use of color in Chapter 5, but
this problem involves Windows "system colors."

System Colors

Windows maintains 19 system colors for painting various parts of the
display. You can obtain and set these colors using GetSysColor and
SetSysColor. Identifiers defined in WINDOWS.H specify the system color.
Setting a system color with SetSysColor changes it only for the current
Windows session.

You can set system colors for future Windows sessions using the Windows
Control Panel program. You can also modify the [colors] section in the
WIN.INI file. The [colors] section uses keywords for the 19 system colors
(different from the GetSysColor and SetSysColor identifiers) followed by
red, green, and blue values that can range from 0 to 255. The following
table shows how the 19 system colors are identified using the WINDOWS.H
identifiers for GetSysColor and SetSysColor, the WIN.INI keywords, and the
Control Panel terms:

╓┌──────────────────────────┌───────────────┌────────────────────────────────╖
GetSysColor & SetSysColor  WIN.INI         Control Panel
────────────────────────────────────────────────────────────────────────────
COLOR_SCROLLBAR            Scrollbar       Scroll Bars
COLOR_BACKGROUND           Background      Desktop Background
COLOR_ACTIVECAPTION        ActiveTitle     Active Title Bar
COLOR_INACTIVECAPTION      InactiveTitle   Inactive Title Bar
COLOR_MENU                 Menu            Menu Bar
COLOR_WINDOW               Window          Window Background
COLOR_WINDOWFRAME          WindowFrame     Window Frame
COLOR_MENUTEXT             MenuText        Menu Text
COLOR_WINDOWTEXT           WindowText      Window Text
COLOR_CAPTIONTEXT          TitleText       Title Bar Text
COLOR_ACTIVEBORDER         ActiveBorder    Active Border
COLOR_INACTIVEBORDER       InactiveBorder  Inactive Border
COLOR_APPWORKSPACE         AppWorkspace    Application Workspace
COLOR_HIGHLIGHT            Highlight
COLOR_HIGHLIGHTTEXT        HighlightText
COLOR_BTNFACE              ButtonFace
COLOR_BTNSHADOW            ButtonShadow
COLOR_GRAYTEXT             GrayText
COLOR_BTNTEXT              ButtonText
GetSysColor & SetSysColor  WIN.INI         Control Panel
────────────────────────────────────────────────────────────────────────────
COLOR_BTNTEXT              ButtonText


Most of these are self-explanatory. COLOR_BACKGROUND is the color of the
desktop area behind all the windows. The COLOR_WINDOWFRAME color is the
color used for lines drawn between many of the sections of the display, such
as between a menu and a client area. The last six system colors cannot be
changed from the Control Panel: The two "Highlight" colors involve selected
options in menus and list boxes. The last four system colors determine the
colors used in push buttons.

Default values for these 19 colors are provided by the display driver.
Windows uses these default values unless they are overriden by the [colors]
section of WIN.INI.


The Button Colors

COLOR_WINDOW and COLOR_WINDOWTEXT are used by many windows to color
themselves. The button controls (with the exception of push buttons) use
COLOR_WINDOW to color the background behind the button. (For a group box,
COLOR_WINDOW is used only for the background behind the text.) The button
controls use COLOR_WINDOWTEXT for text, for the box in a check box control,
and for the round button in a radio-button control. The outline of push
buttons and group boxes is defined by using COLOR_WINDOWFRAME.

You can use one of two methods to make your main window and the child window
control consistent in their use of colors. The first method is to use system
colors for your main window. To begin, you use COLOR_WINDOW for the
background of your client area when defining the window class:

wndclass.hbrBackground = COLOR_WINDOW + 1 ;

(Windows requires that you add 1 when you use these identifiers in your
wndclass structure, but doing so has no profound purpose other than to
prevent the value from being 0.) But that causes another problem. When you
display text using TextOut, Windows uses values defined in the device
context for the text background color (which erases the background behind
the text) and the text color. The default values are white (background) and
black (text) regardless of both the system colors and the hbrBackground
field of the window class structure. So you need to use SetTextColor and
SetBkColor to change your text and text background colors to the system
colors. You do this after you obtain the handle to a device context:

SetBkColor (hdc, GetSysColor (COLOR_WINDOW)) ;
SetTextColor (hdc, GetSysColor (COLOR_WINDOWTEXT)) ;

Now the client-area background, text background, and text color are all
consistent with button colors. That's the first method.

The second method is to force the child window controls to use the colors
you want to use. This method is a little more involved; it requires
processing WM_CTLCOLOR messages.


The WM_CTLCOLOR Messages

WM_CTLCOLOR is a message that a predefined child window control sends to its
parent window procedure when the child window is about to paint its client
area. The parent window can use this opportunity to alter the colors that
the child window procedure will use for painting.

When the parent window procedure receives a WM_CTLCOLOR message, the wParam
and lParam parameters have the following meaning:

────────────────────────────────────────────────────────────────────────────
wParam                Handle to child window's device context
LOWORD (lParam)       Handle to child window
HIWORD (lParam)       Type of window

The high word of lParam can be one of the following:

HIWORD (lParam)                Type of Window
────────────────────────────────────────────────────────────────────────────
CTLCOLOR_MSGBOX                Message box
CTLCOLOR_EDIT                  Edit control
CTLCOLOR_LISTBOX               List box control
CTLCOLOR_BTN                   Button control
CTLCOLOR_DLG                   Dialog box
CTLCOLOR_SCROLLBAR             Scroll bar control
CTLCOLOR_STATIC                Static control

Right now, we're interested in CTLCOLOR_BTN, the WM_CTLCOLOR message from a
button control. When the parent window procedure gets this message, the
child window control has already obtained its device context. The handle to
this device context is in wParam. Any GDI (Graphics Device Interface) calls
you make using this device context will affect the painting that the child
window does when you pass control back to the child window.

You must perform three actions when processing a WM_CTLCOLOR message:

  ■   Set a text color using SetTextColor.

  ■   Set a background color using SetBkColor.

  ■   Return a handle to a brush to the child window.

A "brush" is a GDI object that defines a bitmapped pattern of pixels.
Windows uses brushes to fill areas with color. You can get a handle to a
brush using GetStockOb- ject, CreateSolidBrush, CreateHatchBrush, or
CreatePatternBrush. For processing the WM_CTLCOLOR message, you'll probably
use CreateSolidBrush. Before your program terminates, you must explicitly
delete any brushes you create. A good time to do this is while processing
the WM_DESTROY message.

For most child window controls, the color you set in SetBkColor should be
the same as the color of the brush you return from the WM_CTLCOLOR message.
For instance, button controls use the brush to color the background of the
entire child window client area. The text background color is used only for
the background behind the text. These two colors should be the same. To see
how this works, let's take an example of processing a WM- _CTLCOLOR message
for button controls where the window procedure simply sets the normal
default colors. During initialization (probably when processing a WM_CREATE
message), you can create a brush:

hBrush = CreateSolidBrush (GetSysColor (COLOR_WINDOW)) ;

The hBrush brush handle should be stored in a static variable. Here's what
the WM- _CTLCOLOR processing looks like:

case WM_CTLCOLOR :
     if (HIWORD (lParam) == CTLCOLOR_BTN)
          {
          SetBkColor (wParam, GetSysColor (COLOR_WINDOW)) ;
          SetTextColor (wParam, GetSysColor (COLOR_WINDOWTEXT)) ;

          UnrealizeObject (hBrush) ;
          point.x = point.y = 0 ;
          ClientToScreen (hwnd, &point) ;
          SetBrushOrg (wParam, point.x, point.y) ;

          return ((DWORD) hBrush) ;
          }
     break ;

Note that wParam is the device context handle of the button control. The
four statements that culminate in the SetBrushOrg call require some further
explanation.

As noted earlier, a brush defines a bitmapped pattern of pixels. When
Windows uses this brush to fill an area with color, the pattern of the brush
is repeated horizontally and vertically until the area is filled. The origin
of this brush--the place where Windows assumes the repeating pattern
begins--is the upper left corner of the client area associated with the
device context.

But if you color both the client area of a parent window and the client area
of a child window with this same brush, the pattern won't merge correctly at
the edge of the child window because Windows is using two different origins
for the same brush. To avoid this problem, you call UnrealizeObject. This
function causes Windows to reset the origin of the brush the next time it is
selected into a device context (which will follow the return from the
WM_CTLCOLOR processing). The origin Windows will use is the one you set with
SetBrushOrg; in this example, the function sets the brush origin to the
screen origin of the parent window. (Don't use UnrealizeObject for a stock
brush handle that you obtain from GetStockObject, and don't worry if this
sounds a bit obscure right now. We'll cover the issues in more depth in
Chapter 12.)

The brush we created in our example is based on the system color
COLOR_WINDOW. If this color changes while the program is running, the window
procedure receives a WM_SYSCOLORCHANGE message. The program deletes the
brush and creates a new one:

case WM_SYSCOLORCHANGE :
     DeleteObject (hBrush) ;
     hBrush = CreateSolidBrush (GetSysColor (COLOR_WINDOW)) ;
     return 0 ;

Finally, when the program is about to terminate, the brush should be
deleted:

case WM_DESTROY :
     DeleteObject (hBrush) ;
     PostQuitMessage (0) ;
     return 0 ;

I've shown here how you can reproduce the default processing of WM_CTLCOLOR
messages for button controls. Using your own colors is much the same. You
would not need to trap WM_SYSCOLORCHANGE messages unless you wanted to base
the brush on a system color. We'll come back to WM_CTLCOLOR messages later
in this chapter, when we use the COLORS1 program. For now, let's explore
another class of child window controls.



THE STATIC CLASS

You create a static child window control using "static" as the window class
in the CreateWindow function. These are fairly benign child windows. They do
not accept mouse or keyboard input, and they do not send WM_COMMAND messages
back to the parent window. (When you move or click the mouse over a static
child window, the child window traps the WM_NCHITTEST message and returns a
value of HTTRANSPARENT to Windows. This causes Windows to send the same
WM_NCHITTEST message to the underlying window, which is usually the parent.
The parent usually passes the message to DefWindowProc, where it is
converted into a client-area mouse message.)

The first six static window styles simply draw a rectangle or a frame in the
client area of the child window. The three "RECT" static styles (left column
below) are filled-in rectangles; the three "FRAME" styles (right column) are
rectangular outlines that are not filled in:

────────────────────────────────────────────────────────────────────────────
SS_BLACKRECT                 SS_BLACKFRAME
SS_GRAYRECT                  SS_GRAYFRAME
SS_WHITERECT                 SS_WHITEFRAME

"BLACK," "GRAY," and "WHITE" do not mean the colors are black, gray, and
white. Rather, the colors are based on system colors as shown here:

Static Control               System Color
────────────────────────────────────────────────────────────────────────────
BLACK                        COLOR_WINDOWFRAME
GRAY                         COLOR_BACKGROUND
WHITE                        COLOR_WINDOW

Most display drivers define default settings of black for COLOR_WINDOWFRAME
and white for COLOR_WINDOW. (Of course, a user can change any of these
colors using the Control Panel program in Windows.) The colors used in the
"RECT" and "FRAME" static styles cannot be changed by trapping WM_CTLCOLOR
messages. The window text field of the CreateWindow call is ignored for
these styles. The upper left corner of the rectangle begins at the x
position and y position coordinates relative to the parent window.

The static class also includes three text styles: SS_LEFT, SS_RIGHT, and
SS_CENTER. These create left-justified, right-justified, and centered text.
The text is given in the window text parameter of the CreateWindow call, and
it can be changed later using SetWindowText. When the window procedure for
static controls displays this text, it uses the DrawText function with
DT_WORDBREAK, DT_NOCLIP, and DT_EXPANDTABS parameters. The text is
wordwrapped within the rectangle of the child window. The background of
these three text-style child windows is normally COLOR_WINDOW, and the text
itself is COLOR_WINDOWTEXT. When you intercept WM_CTLCOLOR messages, you can
change the text color by calling SetTextColor and the background color by
calling SetBkColor and by returning the handle to the background brush.

Finally, the static class also includes the window styles SS_ICON and
SS_USERITEM. However, these have no meaning when used as child window
controls. We'll look at them again when discussing dialog boxes.


THE SCROLLBAR CLASS

When the subject of scroll bars first came up in Chapter 2 while I was
designing the SYSMETS series of programs, I discussed some of the
differences between "window scroll bars" and "scroll bar controls." SYSMETS
uses window scroll bars, which appear at the right and bottom of the window.
You add window scroll bars to a window by including the identifier
WS_VSCROLL or WS_HSCROLL or both in the window style when creating the
window. Now we're ready to make some scroll bar controls, which are child
windows that can appear anywhere in the client area of the parent window.
You create child window scroll bar controls by using the predefined window
class "scrollbar" and one of the two scroll bar styles SBS_VERT and
SBS_HORZ.

Unlike the button controls (and the edit and list box controls to be
discussed later), scroll bar controls do not send WM_COMMAND messages to the
parent window. Instead, they send WM_VSCROLL and WM_HSCROLL messages, just
like window scroll bars. When  processing the scroll bar messages, you can
differentiate between window scroll bars and scroll bar controls by the high
word of the lParam parameter:

Scroll Bar Type              HIWORD (lParam)
────────────────────────────────────────────────────────────────────────────
Window scroll bar            0
Scroll bar control           Window handle of control

The wParam parameter and the low word of lParam have the same meaning for
window scroll bars and scroll bar controls.

Although window scroll bars have a fixed width, Windows uses the full
rectangle dimensions given in the CreateWindow call (or later in the
MoveWindow call) to size scroll bar controls. You can make long, thin scroll
bar controls or short, pudgy scroll bar controls. If you want to create
scroll bar controls that have the same dimensions as window scroll bars, you
can use GetSystemMetrics to obtain the height of a horizontal scroll bar:

GetSystemMetrics (SM_CYHSCROLL) ;

or the width of a vertical scroll bar:

GetSystemMetrics (SM_CXVSCROLL) ;

(The scroll bar window style identifiers SBS_LEFTALIGN, SBS_RIGHTALIGN,
SBS_TOPALIGN, and SBS_BOTTOMALIGN are documented to give standard dimensions
to scroll bars. However, these styles work only for scroll bars in dialog
boxes.)

You can set the range and position of a scroll bar control with the same
calls used for window scroll bars:

SetScrollRange (hwndScroll, SB_CTL, nMin, nMax, bRedraw) ;
SetScrollPos (hwndScroll, SB_CTL, nPos, bRedraw) ;

The difference is that window scroll bars use a handle to the parent window
as the first parameter and SB_VERT or SB_HORZ as the second parameter.

The interior bar of the scroll bar is COLOR_SCROLLBAR. The thumb and arrow
colors are based on the push button colors. If you trap WM_CTLCOLOR
messages, you can return a brush from the message to override this color.
Let's do it.

The COLORS1 Program

To see some uses of scroll bars and static child windows--and also to
explore color in more depth--we'll use the COLORS1 program, shown in Figure
6-3. COLORS1 displays three scroll bars in the left half of the client area
labeled "Red," "Green," and "Blue." As you scroll the scroll bars, the right
half of the client area changes to the composite color indicated by the mix
of the three primary colors. The numeric values of the three primary colors
are displayed under the three scroll bars.

 COLORS1.MAK

#-----------------------
# COLORS1.MAK make file
#-----------------------

colors1.exe : colors1.obj colors1.def
     link colors1, /align:16, NUL, /nod slibcew libw, colors1
     rc colors1.exe

colors1.obj : colors1.c
     cl -c -Gsw -Ow -W2 -Zp colors1.c

 COLORS1.C

/*----------------------------------------
   COLORS1.C -- Colors Using Scroll Bars
                (c) Charles Petzold, 1990
  ----------------------------------------*/

#include <windows.h>
#include <stdlib.h>

long FAR PASCAL WndProc    (HWND, WORD, WORD, LONG) ;
long FAR PASCAL ScrollProc (HWND, WORD, WORD, LONG) ;

FARPROC lpfnOldScr[3] ;
HWND    hwndScrol[3], hwndLabel[3], hwndValue[3], hwndRect ;
short   color[3], nFocus ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName[] = "Colors1" ;
     static char *szColorLabel[] = { "Red", "Green", "Blue" } ;
     FARPROC     lpfnScrollProc ;
     HWND        hwnd ;
     MSG         msg ;
     short       n ;
     WNDCLASS    wndclass ;

     if (hPrevInstance)
          return FALSE ;



     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = NULL ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = CreateSolidBrush (0L) ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = szAppName ;

     RegisterClass (&wndclass) ;

     hwnd = CreateWindow (szAppName, "Color Scroll",
                          WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     hwndRect = CreateWindow ("static", NULL,
                              WS_CHILD | WS_VISIBLE | SS_WHITERECT,
                              0, 0, 0, 0,
                              hwnd, 9, hInstance, NULL) ;

     lpfnScrollProc = MakeProcInstance ((FARPROC) ScrollProc, hInstance) ;

     for (n = 0 ; n < 3 ; n++)
          {
          hwndScrol[n] = CreateWindow ("scrollbar", NULL,
                              WS_CHILD | WS_VISIBLE | WS_TABSTOP | SBS_VERT,
                              0, 0, 0, 0,
                              hwnd, n, hInstance, NULL) ;

          hwndLabel[n] = CreateWindow ("static", szColorLabel[n],
                              WS_CHILD | WS_VISIBLE | SS_CENTER,
                              0, 0, 0, 0,
                              hwnd, n + 3, hInstance, NULL) ;

          hwndValue[n] = CreateWindow ("static", "0",
                              WS_CHILD | WS_VISIBLE | SS_CENTER,
                              0, 0, 0, 0,
                              hwnd, n + 6, hInstance, NULL) ;

          lpfnOldScr[n] = (FARPROC) GetWindowLong (hwndScrol[n],
GWL_WNDPROC) ;
          SetWindowLong (hwndScrol[n], GWL_WNDPROC, (LONG) lpfnScrollProc) ;
          SetScrollRange (hwndScrol[n], SB_CTL, 0, 255, FALSE) ;
          SetScrollPos   (hwndScrol[n], SB_CTL, 0, FALSE) ;
          }

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd);

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage  (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static HBRUSH hBrush[3] ;
     char          szbuffer[10] ;
     HDC           hdc ;
     POINT         point ;
     short         n, cxClient, cyClient, cyChar ;
     TEXTMETRIC    tm ;

     switch (message)
          {
          case WM_CREATE :
               hBrush[0] = CreateSolidBrush (RGB (255, 0, 0)) ;
               hBrush[1] = CreateSolidBrush (RGB (0, 255, 0)) ;
               hBrush[2] = CreateSolidBrush (RGB (0, 0, 255)) ;
               return 0 ;

          case WM_SIZE :
               cxClient = LOWORD (lParam) ;
               cyClient = HIWORD (lParam) ;

               hdc = GetDC (hwnd) ;
               GetTextMetrics (hdc, &tm) ;
               cyChar = tm.tmHeight ;
               ReleaseDC (hwnd, hdc) ;

               MoveWindow (hwndRect, 0, 0, cxClient / 2, cyClient, TRUE) ;

               for (n = 0 ; n < 3 ; n++)
                    {
                    MoveWindow (hwndScrol[n],
                         (2 * n + 1) * cxClient / 14, 2 * cyChar,
                         cxClient / 14, cyClient - 4 * cyChar, TRUE) ;
                    MoveWindow (hwndLabel[n],
                         (4 * n + 1) * cxClient / 28, cyChar / 2,
                         cxClient / 7, cyChar, TRUE) ;

                    MoveWindow (hwndValue[n],
                         (4 * n + 1) * cxClient / 28, cyClient - 3 * cyChar
/ 2,
                         cxClient / 7, cyChar, TRUE) ;
                    }
               SetFocus (hwnd) ;
               return 0 ;

          case WM_SETFOCUS :
               SetFocus (hwndScrol[nFocus]) ;
               return 0 ;

          case WM_VSCROLL :
               n = GetWindowWord (HIWORD (lParam), GWW_ID) ;

               switch (wParam)
                    {
                    case SB_PAGEDOWN :
                         color[n] += 15 ;         /* fall through */
                    case SB_LINEDOWN :
                         color[n] = min (255, color[n] + 1) ;
                         break ;
                    case SB_PAGEUP :
                         color[n] -= 15 ;         /* fall through */
                    case SB_LINEUP :
                         color[n] = max (0, color[n] - 1) ;
                         break ;
                    case SB_TOP :
                         color[n] = 0 ;
                         break ;
                    case SB_BOTTOM :
                         color[n] = 255 ;
                         break ;
                    case SB_THUMBPOSITION :
                    case SB_THUMBTRACK :
                         color[n] = LOWORD (lParam) ;
                         break ;
                    default :
                         break ;
                    }
               SetScrollPos  (hwndScrol[n], SB_CTL, color[n], TRUE) ;
               SetWindowText (hwndValue[n], itoa (color[n], szbuffer, 10)) ;
               DeleteObject (GetClassWord (hwnd, GCW_HBRBACKGROUND)) ;
               SetClassWord (hwnd, GCW_HBRBACKGROUND,
                    CreateSolidBrush (RGB (color[0], color[1], color[2]))) ;

               InvalidateRect (hwnd, NULL, TRUE) ;
               return 0 ;

          case WM_CTLCOLOR :
               if (HIWORD (lParam) == CTLCOLOR_SCROLLBAR)
                    {
                    SetBkColor (wParam, GetSysColor (COLOR_CAPTIONTEXT)) ;
                    SetTextColor (wParam, GetSysColor (COLOR_WINDOWFRAME)) ;

                    n = GetWindowWord (LOWORD (lParam), GWW_ID) ;
                    point.x = point.y = 0 ;
                    ClientToScreen (hwnd, &point) ;
                    UnrealizeObject (hBrush[n]) ;
                    SetBrushOrg (wParam, point.x, point.y) ;
                    return ((DWORD) hBrush[n]) ;
                    }
               break ;

          case WM_DESTROY :
               DeleteObject (GetClassWord (hwnd, GCW_HBRBACKGROUND)) ;
               for (n = 0 ; n < 3 ; DeleteObject (hBrush [n++])) ;
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

long FAR PASCAL ScrollProc (HWND hwnd, WORD message, WORD wParam, LONG
lParam)
     {
     short n = GetWindowWord (hwnd, GWW_ID) ;

     switch (message)
          {
          case WM_KEYDOWN :
               if (wParam == VK_TAB)
                    SetFocus (hwndScrol[(n +
                         (GetKeyState (VK_SHIFT) < 0 ? 2 : 1)) % 3]) ;
               break ;

          case WM_SETFOCUS :
               nFocus = n ;
               break ;
          }
     return CallWindowProc (lpfnOldScr[n], hwnd, message, wParam, lParam) ;
     }

 COLORS1.DEF

;------------------------------------
; COLORS1.DEF module definition file
;------------------------------------

NAME           COLORS1

DESCRIPTION    'Colors Using Scroll Bars (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc
               ScrollProc

COLORS1 puts its children to work. The program uses 10 child window
controls: 3 scroll bars, 6 windows of static text, and 1 static rectangle.
COLORS1 traps WM_CTLCOLOR messages to color the interior sections of the
three scroll bars red, green, and blue. You can scroll the scroll bars using
either the mouse or the keyboard. You can use COLORS1 as a development tool
in experimenting with color and choosing attractive (or, if you prefer,
ugly) colors for your own Windows programs. A monochrome version of the
COLORS1 display is shown in Figure 6-4; obviously, to take advantage of the
program's manipulation of color, you'll need to use a color monitor.

COLORS1 doesn't process WM_PAINT messages, and the program obtains a device
context handle only for determining the height of a character. Most of the
work in COLORS1 is done by the child windows.

The color shown on the right half of the client area is actually the
background color of the parent window. A static child window with style
SS_WHITERECT blocks out the left half of the client area. The three scroll
bars are child window controls with the style SBS_VERT placed on top of the
SS_WHITERECT child. Six more static child windows of style SS_CENTER
(centered text) provide the labels and the color values. COLORS1 creates its
normal overlapped window and the ten child windows within the WinMain
function using CreateWindow. The SS_WHITERECT and SS_CENTER static windows
use the window class "static," and the three scroll bars use the window
class "scrollbar."

  (Figure 6-4. may be found in the printed book.)

The x position, y position, width, and height parameters of the CreateWindow
call are initially set to 0 because the position and sizing depend on the
size of the client area, which is not yet known. COLORS1's window procedure
resizes all ten child windows using MoveWindow when it receives a WM_SIZE
message. So whenever you resize the COLORS1 window, the size of the scroll
bars changes proportionally.

When the WndProc window procedure receives a WM_VSCROLL message, the high
word of the lParam parameter is the handle to the child window. We can use
GetWindowWord to get the window ID number:

n = GetWindowWord (HIWORD (lParam), GWW_ID) ;

For the three scroll bars, we have conveniently set the ID numbers to 0, 1,
and 2, so WndProc can tell which scroll bar is generating the message.

Because the handles to the child windows were saved in arrays when the
windows were created, WndProc can process the scroll bar message and set the
new value of the appropriate scroll bar using the SetScrollPos call:

SetScrollPos (hwndScrol[n], SB_CTL, color[n], TRUE) ;

WndProc also changes the text of the child window at the bottom of the
scroll bar:

SetWindowText (hwndValue[n], itoa (color[n], szbuffer, 10)) ;


The Automatic Keyboard Interface

Scroll bar controls can also process keystrokes, but only if they have the
input focus. The following table shows how keyboard cursor keys translate
into scroll bar messages:

Cursor Key             Scroll Bar Message wParam Value
────────────────────────────────────────────────────────────────────────────
Home                   SB_TOP
End                    SB_BOTTOM
Page Up                SB_PAGEUP
Page Down              SB_PAGEDOWN
Left or Up             SB_LINEUP
Right or Down          SB_LINEDOWN

In fact, the SB_TOP and SB_BOTTOM scroll bar messages can be generated only
by using the keyboard. If you want a scroll bar control to obtain the input
focus when the scroll bar is clicked with the mouse, you must include the
WS_TABSTOP identifier in the window class parameter of the CreateWindow
call. When a scroll bar has the input focus, a blinking gray block is
displayed on the scroll bar thumb.

To provide a full keyboard interface to the scroll bars, however, some more
work is necessary. First, the WndProc window procedure must specifically
give a scroll bar the input focus. It does this by processing the
WM_SETFOCUS message, which the parent window receives when it obtains the
input focus. WndProc simply sets the input focus to one of the scroll bars:

SetFocus (hwndScrol[nFocus]) ;

But you also need some way to get from one scroll bar to another by using
the keyboard, preferably by using the Tab key. This is more difficult,
because once a scroll bar has the input focus, it processes all keystrokes.
But the scroll bar cares only about the cursor keys; it ignores the Tab key.
The way out of this dilemma lies in a technique called "window subclassing."
We'll use it to add a facility to COLORS1 to jump from one scroll bar to
another using the Tab key.


Window Subclassing

The window procedure for the scroll bar controls is somewhere inside
Windows. However, you can obtain the address of this window procedure by a
call to GetWindowLong using the GWL_WNDPROC identifier as a parameter.
Moreover, you can set a new window procedure for the scroll bars by calling
SetWindowLong. This technique, called "window subclassing," is very
powerful. It lets you hook into existing window procedures, process some
messages within your own program, and pass all other messages to the old
window procedure.

The window procedure that does preliminary scroll bar message processing in
COLORS1 is called ScrollProc; it is toward the end of the COLORS1.C listing.
Because ScrollProc is a function within COLORS1 that is called by Windows,
it must be defined as FAR PASCAL and must be listed under EXPORTS in the
COLORS1.DEF module definition file.

First, to ensure that ScrollProc accesses the proper data segment, COLORS1
must obtain a far address for the function using MakeProcInstance:

lpfnScrollProc = MakeProcInstance ((FARPROC) ScrollProc, hInstance);

For each of the three scroll bars, COLORS1 uses GetWindowLong to obtain and
save the address of the existing scroll bar window procedure:

lpfnOldScr[n] = (FARPROC) GetWindowLong (hwndScrol[n], GWL_WNDPROC) ;

Next, the program sets the new scroll bar window procedure:

SetWindowLong (hwndScrol[n], GWL_WNDPROC, (LONG) lpfnScrollProc) ;

Now the function ScrollProc gets all messages that Windows sends to the
scroll bar window procedure for the three scroll bars in COLORS1 (but not,
of course, for scroll bars in other programs). The ScrollProc window
procedure simply changes the input focus to the next (or previous) scroll
bar when it receives a Tab or Shift-Tab keystroke. It calls the old scroll
bar window procedure using CallWindowProc.


Coloring the Background

When COLORS1 defines its window class, it gives the background of its client
area a solid black brush:

wndclass.hbrBackground = CreateSolidBrush (0L) ;

When you change the settings of COLORS1's scroll bars, the program must
create a new brush and put the new brush handle in the window class
structure. Just as we were able to get and set the scroll bar window
procedure using GetWindowLong and SetWindowLong, we can get and set the
handle to this brush using GetClassWord and SetClassWord.

First you must delete the existing brush:

DeleteObject (GetClassWord (hwnd, GCW_HBRBACKGROUND)) ;

Then you can create the new brush and insert the handle in the window class
structure:

SetClassWord (hwnd, GCW_HBRBACKGROUND,
     CreateSolidBrush (RGB (color[0], color[1], color[2]))) ;

The next time Windows recolors the background of the window, Windows will
use this new brush. To force Windows to erase the background, we invalidate
the entire client area:

InvalidateRect (hwnd, NULL, TRUE) ;

The TRUE (nonzero) value as the third parameter indicates that we want the
background erased before repainting.

InvalidateRect causes Windows to put a WM_PAINT message in the message queue
of the window procedure. Because WM_PAINT messages are low priority, this
message will not be processed immediately if you are still moving the scroll
bar with the mouse or the cursor keys. Alternatively, if you want the window
to be updated immediately after the color is changed, you can add the
statement:

UpdateWindow (hwnd) ;

after the InvalidateRect call. But this slows down keyboard and mouse
processing.

COLORS1's WndProc function doesn't process the WM_PAINT message but passes
it to DefWindowProc. Window's default processing of WM_PAINT messages simply
involves calling BeginPaint and EndPaint to validate the window. Because we
specified in the InvalidateRect call that the background should be erased,
the BeginPaint call causes Windows to generate a WM_ERASEBKGND (erase
background) message. WndProc ignores this message also. Windows processes it
by erasing the background of the client area using the brush specified in
the window class.

Normally, Windows would erase the entire client area using the window class
brush. Doing so would erase the 10 child windows, however, and Windows would
then have to send WM_PAINT messages to all the child windows so they could
repaint themselves_ very annoying. We avoid the problem by using the
WS_CLIPCHILDREN style value when first creating the parent window using
CreateWindow; this style prevents the parent window from painting over its
children. Take the WS_CLIPCHILDREN style out of CreateWindow, and you'll see
a big difference in how COLORS1 works.

Like all GDI objects, the brushes created by a program using
CreateSolidBrush are not automatically deleted by Windows when the program
terminates. We've been good about deleting each brush before creating a new
one, but when the program is about to terminate, one last brush in the
window class still should be discarded. Thus, during processing of the
WM_DESTROY message, DeleteObject is called once more:

DeleteObject (GetClassWord (hwnd, GCW_HBRBACKGROUND)) ;


Coloring the Scroll Bars

On a color display, the interiors of the three scroll bars in COLORS1 are
red, green, and blue. This coloring is accomplished by processing
WM_CTLCOLOR messages.

In WndProc we define a static array of three handles to brushes:

static HBRUSH hBrush [3] ;

During processing of WM_CREATE, we create the three brushes:

hBrush[0] = CreateSolidBrush (RGB (255, 0, 0)) ;
hBrush[1] = CreateSolidBrush (RGB (0, 255, 0)) ;
hBrush[2] = CreateSolidBrush (RGB (0, 0, 255)) ;

During the WM_CTLCOLOR processing, the text and text background colors are
set to the normal values for scroll bars. The brush that's returned from
this message is one of the three brushes created earlier:

case WM_CTLCOLOR :
     if (HIWORD (lParam) == CTLCOLOR_SCROLLBAR)
          {
          SetBkColor (wParam, GetSysColor (COLOR_CAPTIONTEXT)) ;
          SetTextColor (wParam, GetSysColor (COLOR_WINDOWFRAME)) ;

          n = GetWindowWord (LOWORD (lParam), GWW_ID) ;
          point.x = point.y = 0 ;
          ClientToScreen (hwnd, &point) ;
          UnrealizeObject (hBrush[n]) ;
          SetBrushOrg (wParam, point.x, point.y) ;

          return ((DWORD) hBrush[n]) ;
          }
     break ;

These brushes must be destroyed during processing of the WM_DESTROY message:

for (n = 0 ; n < 3 ; DeleteObject (hBrush [n++])) ;


Dealing with Multiple Instances

Normally, Windows programs reuse the same window class when you load
multiple instances of the program. The window class is registered only if
the previous instance is NULL:

if (!hPrevInstance)
     {
     wndclass.style = CS_HREDRAW | CS_VREDRAW ;
[more program lines]

But COLORS1 can't do this, because the background color is specified in the
window class. If all instances of COLORS1 used the same window class, then
each instance would use (and change) the same background color. We can avoid
this problem entirely by allowing only one instance of COLORS1 to run:

if (hPrevInstance)
     return FALSE ;


COLORS1 as an Icon

When you make COLORS1 into an icon, the entire surface of the icon--rather
than only the right half--is the color of the parent window's background.
Yet COLORS1 doesn't seem to have any separate icon logic.

You'll note that COLORS1 specifies a NULL icon in the window class:

wndclass.hIcon = NULL ;

This indicates that COLORS1 is responsible for painting its icon. The entire
icon appears as the background color because Windows hides child windows
when a program becomes an icon, and thus the colored background is
completely uncovered.



THE EDIT CLASS

The edit class is in some ways the simplest predefined window class and in
other ways the most complex. When you create a child window using the class
name "edit," you define a rectangle based on the x position, y position,
width, and height parameters of the CreateWindow call. This rectangle
contains editable text. When the child window control has the input focus,
you can type text, move the cursor around, select portions of text using
either the mouse or the Shift key and a cursor key, delete selected text to
the clipboard by pressing Shift-Del, or insert text from the clipboard by
pressing Shift-Ins.

One of the simplest uses of edit controls is for single-line entry fields.
For instance, the Windows PIF Editor program uses edit controls in this way
on its main window. But edit controls are not limited to single lines. For
example, the Windows Notepad program uses a multiline edit control. The file
size of the Notepad program is surprisingly small_ less than 32 KB. Most of
the editing logic is not in Notepad at all; it's in the edit control logic
within Windows.

To give you an idea of the power of edit controls, we'll write a "Notepad
clone" program called POPPAD1. We'll begin the program in this chapter and
continue it in Chapters 9 (when we'll add a menu) and 10 (when we'll use
dialog boxes to load and save files). POPPAD1 is shown in Figure 6-5.

 POPPAD1.MAK

#-----------------------
# POPPAD1.MAK make file
#-----------------------

poppad1.exe : poppad1.obj poppad1.def
     link poppad1, /align:16, NUL, /nod slibcew libw, poppad1.def
     rc poppad1.exe

poppad1.obj : poppad1.c
     cl -c -Gsw -Ow -W2 -Zp poppad1.c


 POPPAD1.C

/*-------------------------------------------------------
   POPPAD1.C -- Popup Editor Using Child Window Edit Box
                (c) Charles Petzold, 1990
  -------------------------------------------------------*/

#include <windows.h>

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG);

char szAppName[] = "PopPad1" ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     HWND     hwnd ;
     MSG      msg;
     WNDCLASS wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, szAppName,
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          GetSystemMetrics (SM_CXSCREEN) / 2,
                          GetSystemMetrics (SM_CYSCREEN) / 2,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;
     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static HWND hwndEdit ;

     switch (message)
          {
          case WM_CREATE :
               hwndEdit = CreateWindow ("edit", NULL,
                         WS_CHILD | WS_VISIBLE | WS_HSCROLL | WS_VSCROLL |
                              WS_BORDER | ES_LEFT | ES_MULTILINE |
                              ES_AUTOHSCROLL | ES_AUTOVSCROLL,
                         0, 0, 0, 0,
                         hwnd, 1,
                         ((LPCREATESTRUCT) lParam) -> hInstance, NULL) ;
               return 0 ;

          case WM_SETFOCUS :
               SetFocus (hwndEdit) ;
               return 0 ;

          case WM_SIZE :
               MoveWindow (hwndEdit, 0, 0, LOWORD (lParam),
                                           HIWORD (lParam), TRUE) ;
               return 0 ;

          case WM_COMMAND :
               if (wParam == 1 && HIWORD (lParam) == EN_ERRSPACE)
                    MessageBox (hwnd, "Edit control out of space.",
                              szAppName, MB_OK | MB_ICONSTOP) ;
               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 POPPAD1.DEF

;------------------------------------
; POPPAD1.DEF module definition file
;------------------------------------

NAME           POPPAD1

DESCRIPTION    'Popup Editor Version 1 (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

POPPAD1 is a multiline editor (without any file I/O just yet) in less than
100 lines of C. As you can see, POPPAD1 itself doesn't do very much. The
predefined edit control is doing quite a lot. In this form, the program lets
you explore what edit controls can do with- out any help from a program.

The Edit Class Styles

As noted earlier, you create an edit control using "edit" as the window
class in the CreateWindow call. The window style is WS_CHILD plus several
options. As in static child window controls, the text in edit controls can
be either left-justified, right-justified, or centered. You specify this
formatting with the window styles ES_LEFT, ES_RIGHT, and ES_CENTER.

By default, an edit control has a single line. You can create a multiline
edit control with the window style ES_MULTILINE. For a single-line edit
control, you can normally enter text only to the end of the edit control
rectangle. To create an edit control that automatically scrolls
horizontally, you use the style ES_AUTOHSCROLL. For a multiline edit
control, text wordwraps unless you use the ES_AUTOHSCROLL style, in which
case you must press the Enter key to start a new line. You can also include
vertical scrolling in a multiline edit control by using the style
ES_AUTOVSCROLL.

When you include these scrolling styles in multiline edit controls, you
might also want to add scroll bars to the edit control. You do so by using
the same window style identifiers as for nonchild windows: WS_HSCROLL and
WS_VSCROLL.

By default, an edit control has no border. You can add one by using the
style WS_BORDER.

When you select text in an edit control, Windows displays it in reverse
video. When the edit control loses the input focus, however, the selected
text is no longer highlighted. If  you want the selection to be highlighted
even when the edit control does not have the input focus, you can use the
style ES_NOHIDESEL.

When POPPAD1 creates its edit control, the style is given in the
CreateWindow call as:

WS_CHILD | WS_VISIBLE | WS_HSCROLL | WS_VSCROLL |
     WS_BORDER | ES_LEFT | ES_MULTILINE |
     ES_AUTOHSCROLL | ES_AUTOVSCROLL

In POPPAD1 the dimensions of the edit control are later defined by a call to
MoveWindow when WndProc receives a WM_SIZE message. The size of the edit
control is simply set to the size of the main window:

MoveWindow (hwndEdit, 0, 0, LOWORD (lParam),
                            HIWORD (lParam), TRUE) ;

For a single-line edit control, the height of the control must accommodate
the height of a character. If the edit control has a border (as most do),
use 1-1/2 times the height of a character (including external leading).


Edit Control Notification

Edit controls send WM_COMMAND messages to the parent window procedure. The
meanings of the wParam and lParam variables are the same as for button
controls:

Parameter                    Description
────────────────────────────────────────────────────────────────────────────
wParam                       Child window ID
LOWORD (lParam)              Child window handle
HIWORD (lParam)              Notification code

The notification codes are shown below:

╓┌──────────────┌────────────────────────────────────────────────────────────╖
────────────────────────────────────────────────────────────────────────────
EN_SETFOCUS    Edit control has gained the input focus
EN_KILLFOCUS   Edit control has lost the input focus
EN_CHANGE      Edit control's contents will change
EN_UPDATE      Edit control's contents have changed
EN_ERRSPACE    Edit control has run out of space
EN_MAXTEXT     Edit control has run out of space on insertion
EN_HSCROLL     Edit control's horizontal scroll bar has been clicked
EN_VSCROLL     Edit control's vertical scroll bar has been clicked


POPPAD1 traps only EN_ERRSPACE notification codes and displays a message
box.

The edit control stores text in the local heap of its parent window's
program. The contents of an edit control are limited to about 32 KB. You'll
note that POPPAD1 reserves only 1 KB of space for its local heap in the
module definition file. As we'll see in Chapter 7, this is not a problem.
Windows will expand the program's local heap if an edit control needs more
space.


Using the Edit Controls

If you use several single-line edit controls on the surface of your main
window (as PIFEDIT does), you'll need to use window subclassing to move the
input focus from one control to another. You can accomplish this much as
COLORS1 does, by intercepting Tab and Shift-Tab keystrokes. (Another example
of window subclassing is shown later in this chapter in the HEAD program.)
How you handle the Enter key is up to you. You can use it the same way as
the Tab key or as a signal to your program that all the edit fields are
ready.

If you want to insert text into an edit field, you can do so using
SetWindowText. Getting text out of an edit control involves
GetWindowTextLength and GetWindowText. We'll see examples of these
facilities in our later revisions to the POPPAD1 program.


Messages to an Edit Control

We won't cover all the messages you can send to an edit control using
SendMessage, because there are quite a few of them, and several will be used
in the later POPPAD1 revisions. Here's a broad overview.

These messages let you cut, copy, or clear the current selection. A user
selects the text to be acted upon by using the mouse or the Shift key and a
cursor key, thus highlighting the selected text in the edit control.

SendMessage (hwndEdit, WM_CUT, 0, 0L) ;
SendMessage (hwndEdit, WM_COPY, 0, 0L) ;
SendMessage (hwndEdit, WM_CLEAR, 0, 0L) ;

WM_CUT removes the current selection from the edit control and sends it to
the clipboard. WM_COPY copies the selection to the clipboard but leaves it
intact in the edit control. WM_CLEAR deletes the selection from the edit
control without passing it to the clipboard.

You can also insert clipboard text into the edit control at the cursor
position:

SendMessage (hwndEdit, WM_PASTE, 0, 0L) ;

You can obtain the starting and ending positions of the current selection:

lSelect = SendMessage (hwndEdit, EM_GETSEL, 0, 0L) ;

The low word of lSelect has the starting position. The high word has the end
position plus 1.

You can select text:

SendMessage (hwndEdit, EM_SETSEL, 0, MAKELONG (wBegin, wEnd)) ;

You can also replace a current selection with other text:

SendMessage (hwndEdit, EM_REPLACESEL, 0, (LONG) lpszString) ;

For multiline edit controls, you can obtain the number of lines:

nCount = SendMessage (hwndEdit, EM_GETLINECOUNT, 0, 0L) ;

For any particular line, you can obtain an offset from the beginning of the
edit buffer text:

nOffset = SendMessage (hwndEdit, EM_LINEINDEX, wLine, 0L) ;

Lines are numbered starting at 0. A wLine value of -1 returns the offset of
the line containing the cursor. You obtain the length of the line from:

nOffset = SendMessage (hwndEdit, EM_LINELENGTH, wLine, 0L) ;

and copy the line itself into a buffer using:

nLength = SendMessage (hwndEdit, EM_GETLINE, wLine, lpszBuffer) ;



THE LISTBOX CLASS

The final predefined child window control I'll discuss in this chapter is
the list box. (The combo box is a combination of a list box and an edit
field.) A list box is a collection of text strings displayed as a scrollable
columnar list within a rectangle. A program can add or remove strings in the
list by sending messages to the list box window procedure. The list box
control sends WM_COMMAND messages to its parent window when an item in the
list is selected. The parent window can then determine which item has been
selected.

List boxes are most commonly used in dialog boxes called up by selecting
Open from the File menu. The list box displays files in the current
directory and can also display other subdirectories and disk drives. List
boxes are also used in the CONTROL program for changing colors and in WRITE
for selecting fonts. A list box can be either single selection or multiple
selection. The latter allows the user to select more than one item from the
list box. When a list box has the input focus, it displays a dashed line
surrounding an item in the list box. This cursor does not indicate the
selected item in the list box. The selected item is indicated by
highlighting, which displays the item in reverse video.

In a single-selection list box, the user can select the item that the cursor
is positioned on by pressing the Spacebar. The arrow keys move both the
cursor and the current selection and can scroll contents of the list box.
The Page Up and Page Down keys also scroll the list box by moving the cursor
but not the selection. Pressing a letter key moves the cursor  and the
selection to the first (or next) item that begins with that letter. An item
can also be selected by clicking or double-clicking the mouse on the item.

In a multiple-selection list box, the Spacebar toggles the selection state
of the item where the cursor is positioned. (If the item is already
selected, it is deselected.) The arrow keys deselect all previously selected
items and move the cursor and selection just as in single-selection list
boxes. However, the Ctrl key and the arrow keys can move the cursor without
moving the selection. The Shift key and arrow keys can extend a selection.

Clicking or double-clicking an item in a multiple-selection list box
deselects all previously selected items and selects the clicked item.
However, clicking an item while pressing the Shift key toggles the selection
state of the item without changing the selection state of any other item.

List Box Styles

You create a list box child window control with CreateWindow using "listbox"
as the window class and WS_CHILD as the window style. However, this default
list box style does not send WM_COMMAND messages to its parent, meaning that
a program would have to interrogate the list box (via messages to the list
box controls) regarding the selection of items within the list box.
Therefore, list box controls almost always include the list box style
identifier LBS_NOTIFY, which allows the parent window to receive WM_COMMAND
messages from the list box. If you want the list box control to sort the
items in the list box, you can also use LBS_SORT, another common style.

By default, list boxes are single selection. Multiple-selection list boxes
are relatively rare. If you want to create one, you use the style
LBS_MULTIPLESEL.

Normally, a list box updates itself when a new item is added to the scroll
box list. You can prevent this by including the style LBS_NOREDRAW. You will
probably not want to use this style, however. Instead, you can temporarily
prevent repainting of a list box control by using the WM_SETREDRAW message
that I'll describe a little later.

By default, the list box window procedure displays only the list of items
without any border around it. You can add a border with the window style
identifier WS_BORDER. And to add a vertical scroll bar for scrolling through
the list with the mouse, you use the window style identifier WS_VSCROLL.

WINDOWS.H defines a list box style called LBS_STANDARD that includes the
most commonly used styles. It is defined as:

(LBS_NOTIFY | LBS_SORT | WS_VSCROLL | WS_BORDER)

You can also use the WS_SIZEBOX and WS_CAPTION identifiers, but these will
allow the user to resize the list box and to move it around its parent's
client area.

The width of a list box should accommodate the width of the longest string
plus the width of the scroll bar. You can get the width of the vertical
scroll bar using:

GetSystemMetrics (SM_CXVSCROLL) ;

You can calculate the height of the list box by multiplying the height of a
character by the number of items you want to appear in view. A list box does
not use tmExternalLeading when spacing lines of text.


Putting Strings in the List Box

After you've created the list box, the next step is to put text strings in
it. You do this by sending messages to the list box window procedure using
the SendMessage call. The text strings are generally referenced by an index
number that starts at 0 for the topmost item. In the examples that follow,
hwndList is the handle to the child window list box control, and wIndex is
the index value.

In cases where you pass a text string in the SendMessage call, the lParam
parameter is a far pointer to a null-terminated string. To avoid error
messages during compilation, cast this pointer to a LONG. Note that when you
cast a near pointer to a LONG, the C compiler will first cast the near
pointer to a far pointer.

In most of these examples, the SendMessage call returns LB_ERRSPACE (defined
as -2) if the window procedure runs out of available memory space to store
the contents of the list box. SendMessage returns LB_ERR (-1) if an error
occurs for other reasons and LB_OKAY (0) if the operation is successful. You
can test SendMessage for a nonzero value to detect either of the two errors.
The list box allocates global memory (outside your program's data segment)
for the list box contents.

If you use the LBS_SORT style (or if you are placing strings in the list box
in the order that you want them to appear), then the easiest way to fill up
a list box is with the LB- _ADDSTRING message:

SendMessage (hwndList, LB_ADDSTRING, 0, (LONG) szString) ;

If you do not use LBS_SORT, you can insert strings into your list box by
specifying an index value with LB_INSERTSTRING:

SendMessage (hwndList, LB_INSERTSTRING, wIndex, (LONG) szString) ;

For instance, if wIndex is equal to 4, szString becomes the new string with
an index value of 4--the fifth string from the top because counting starts
at 0. Any strings below this point are pushed down. A wIndex value of -1
adds the string to the bottom. You can use LB_INSERTSTRING with list boxes
that have the LBS_SORT style, but the list box contents will not be
re-sorted. (You can also insert strings into a list box using the LB_DIR
message, which is discussed in detail toward the end of this chapter.)

You can delete a string from the list box by specifying the index value with
the LB_DELETESTRING message:

SendMessage (hwndList, LB_DELETESTRING, wIndex, 0L) ;

You can clear out the list box using LB_RESETCONTENT:

SendMessage (hwndList, LB_RESETCONTENT, 0, 0L) ;

The list box window procedure updates the display when an item is added to
or deleted from the list box. If you have a number of strings to add or
delete, you may want to temporarily inhibit this action by turning off the
control's redraw flag:

SendMessage (hwndList, WM_SETREDRAW, FALSE, 0L) ;

After you've finished, you can turn the redraw flag back on:

SendMessage (hwndList, WM_SETREDRAW, TRUE, 0L) ;

A list box created with the LBS_NOREDRAW style begins with the redraw flag
turned off.


Selecting and Extracting Entries

The SendMessage calls that carry out the tasks shown below usually return a
value. If an error occurs, this value is set to LB_ERR (defined as -1). Note
that the return value from SendMessage is normally a signed long (LONG), but
the values are unsigned integers (WORD), so some casting is necessary.

After you've put some items into a list box, you can find out how many items
are in the list box:

nCount = (WORD) SendMessage (hwndList, LB_GETCOUNT, 0, 0L) ;

Some of the other calls are different for single-selection and
multiple-selection list boxes. Let's first look at single-selection list
boxes.

Normally, you'll let a user select from a list box. But if you want to
highlight a default selection, you can use:

SendMessage (hwndList, LB_SETCURSEL, nIndex, 0L) ;

Setting lParam to -1 in this call deselects all items.

You can also select an item based on its initial characters:

nIndex = (WORD) SendMessage (hwndList, LB_SELECTSTRING, wIndex,
                         (LONG) szSearchString) ;

The wIndex given as the wParam parameter to the SendMessage call is the
index following which the search begins for an item with initial characters
that match szSearchString. A wIndex value of -1 starts the search from the
top. SendMessage returns the index of the selected item, or LB_ERR if no
initial characters match szSearchString.

When you get a WM_COMMAND message from the list box (or at any other time),
you can determine the index of the current selection using LB_GETCURSEL:

nIndex = (WORD) SendMessage (hwndList, LB_GETCURSEL, 0, 0L) ;

The nIndex value returned from the call is LB_ERR if no item is selected.

You can determine the length of any string in the list box:

nLength = (WORD) SendMessage (hwndList, LB_GETTEXTLEN, nIndex, 0L) ;

and copy the item into the text buffer:

nLength = (WORD) SendMessage (hwndList, LB_GETTEXT, nIndex,
                          (LONG) szBuffer) ;

In both cases, the nLength value returned from the call is the length of the
string. The szBuffer array must be large enough for the length of the string
and a terminating NULL. You may want to use LB_GETTEXTLEN to first allocate
some local memory to hold the string (which you'll learn how to do in
Chapter 8).

For a multiple-selection list box, you cannot use LB_SETCURSEL,
LB_GETCURSEL, or LB_SELECTSTRING. Instead, you use LB_SETSEL to set the
selection state of a particular item without affecting other items that may
also be selected:

SendMessage (hwndList, LB_SETSEL, wParam, (LONG) wIndex) ;

The wParam parameter is nonzero to select and highlight the item and 0 to
deselect it. If the lParam parameter is -1, all items are either selected or
deselected. You can also determine the selection state of a particular item
using:

wSelect = (WORD) SendMessage (hwndList, LB_GETSEL, wIndex, 0L) ;

where wSelect is set to nonzero if the item indexed by wIndex is selected
and 0 if it is not.


Receiving Messages from List Boxes

When a user clicks on a list box with the mouse, the list box receives the
input focus. A parent window can give the input focus to a list box control
by using:

SetFocus (hwndList) ;

When a list box has the input focus, the cursor movement keys, letter keys,
and Spacebar can also be used to select items from the list box.

A list box control sends WM_COMMAND messages to its parent. The meanings of
the wParam and lParam variables are the same as for the button and edit
controls:

────────────────────────────────────────────────────────────────────────────
wParam                       Child window ID
LOWORD (lParam)              Child window handle
HIWORD (lParam)              Notification code

The notification codes and their values are as follows:

╓┌────────────────────────────────┌──────────────────────────────────────────╖
────────────────────────────────────────────────────────────────────────────
LBN_ERRSPACE                     -2
LBN_SELCHANGE                    1
LBN_DBLCLK                       2
LBN_SELCANCEL                    3
LBN_SETFOCUS                     4
LBN_KILLFOCUS                    5
────────────────────────────────────────────────────────────────────────────
LBN_KILLFOCUS                    5


The list box control sends the parent window LBN_SELCHANGE and LBN_DBLCLK
codes only if the list box window style includes LBS_NOTIFY.

The LBN_ERRSPACE code indicates that the list box control has run out of
space. The LBN_SELCHANGE code indicates that the current selection has
changed; these messages occur as the user moves the highlight through the
list box, toggles the selection state with the Spacebar, or clicks an item
with the mouse. The LBN_DBLCLK code indicates that a list box item has been
double-clicked with the mouse. (The notification code values for
LBN_SELCHANGE and LBN_DBLCLK refer to the number of mouse clicks.)

Depending on your application, you may want to use either LBN_SELCHANGE or
LBN_DBLCLK messages or both. Your program will get many LBN_SELCHANGE
messages, but LBN_DBLCLK messages occur only when the user double-clicks
with the mouse. If your program uses double-clicks, you'll need to provide a
keyboard interface that duplicates LBN_DBLCLK.


A Simple List Box Application

Now that you know how to create a list box, fill it with text items, receive
messages from the list box, and extract strings, it's time to program an
application. The ENVIRON program, shown in Figure 6-6, uses a list box in
its client area to display the name of your current MS-DOS environment
variables (such as PATH, COMSPEC, and PROMPT). As you select a variable, the
name and the environment string are displayed across the top of the client
area.

 ENVIRON.MAK

#-----------------------
# ENVIRON.MAK make file
#-----------------------

environ.exe : environ.obj environ.def
     link environ, /align:16, NUL, /nod slibcew libw, environ
     rc environ.exe

environ.obj : environ.c
     cl -c -Gsw -Ow -W2 -Zp environ.c

 ENVIRON.C

/*----------------------------------------
   ENVIRON.C -- Environment List Box
                (c) Charles Petzold, 1990
  ----------------------------------------*/



#include <windows.h>
#include <stdlib.h>
#include <string.h>
#define  MAXENV  4096

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName[] = "Environ" ;
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = COLOR_WINDOW + 1 ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "Environment List Box",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }
long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static char szBuffer [MAXENV + 1] ;
     static HWND hwndList, hwndText ;
     HDC         hdc ;
     TEXTMETRIC  tm ;
     WORD        n ;

     switch (message)
          {
          case WM_CREATE :
               hdc = GetDC (hwnd) ;
               GetTextMetrics (hdc, &tm) ;
               ReleaseDC (hwnd, hdc) ;

               hwndList = CreateWindow ("listbox", NULL,
                              WS_CHILD | WS_VISIBLE | LBS_STANDARD,
                              tm.tmAveCharWidth, tm.tmHeight * 3,
                              tm.tmAveCharWidth * 16 +
                                   GetSystemMetrics (SM_CXVSCROLL),
                              tm.tmHeight * 5,
                              hwnd, 1,
                              GetWindowWord (hwnd, GWW_HINSTANCE), NULL) ;

               hwndText = CreateWindow ("static", NULL,
                              WS_CHILD | WS_VISIBLE | SS_LEFT,
                              tm.tmAveCharWidth,          tm.tmHeight,
                              tm.tmAveCharWidth * MAXENV, tm.tmHeight,
                              hwnd, 2,
                              GetWindowWord (hwnd, GWW_HINSTANCE), NULL) ;

               for (n = 0 ; environ[n] ; n++)
                    {
                    if (strlen (environ [n]) > MAXENV)
                         continue ;
                    *strchr (strcpy (szBuffer, environ [n]), '=') = '\0' ;
                    SendMessage (hwndList, LB_ADDSTRING, 0,
                                 (LONG) (LPSTR) szBuffer) ;
                    }
               return 0 ;

          case WM_SETFOCUS :
               SetFocus (hwndList) ;
               return 0 ;
          case WM_COMMAND :
               if (wParam == 1 && HIWORD (lParam) == LBN_SELCHANGE)
                    {
                    n = (WORD) SendMessage (hwndList, LB_GETCURSEL, 0, 0L) ;
                    n = (WORD) SendMessage (hwndList, LB_GETTEXT, n,
                                            (LONG) (LPSTR) szBuffer) ;

                    strcpy (szBuffer + n + 1, getenv (szBuffer)) ;
                    *(szBuffer + n) = '=' ;

                    SetWindowText (hwndText, szBuffer) ;
                    }
               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 ENVIRON.DEF

;------------------------------------
; ENVIRON.DEF module definition file
;------------------------------------

NAME           ENVIRON

DESCRIPTION    'Environment List Box Program (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

ENVIRON creates two child windows: a list box with the style LBS_STANDARD
and a static window with the style SS_LEFT (left-justified text). ENVIRON
uses the environ variable (declared external in STDLIB.H) to obtain the list
of environment strings, and it uses the message LB_ADDSTRING to direct the
list box window procedure to place each string in the list box.

When you run ENVIRON, you can select an environment variable using the mouse
or the keyboard. Each time you change the selection, the list box sends a
WM_COMMAND message to the parent window, which is WndProc. When WndProc
receives a WM_COMMAND message, it checks to see if wParam is 1 (the child ID
of the list box) and if the high word of lParam (the notification code) is
equal to LBN_SELCHANGE. If so, it obtains the index of the selection using
the LB_GETCURSEL message and the text itself--the environment variable
name--using LB_GETTEXT. ENVIRON uses the C function getenv to obtain the
environment string corresponding to that variable and SetWindowText to pass
this string to the static child window control, which displays the text.

Note that ENVIRON cannot use the index returned from LB_GETCURSEL to index
the environ variable and obtain the environment string. Because the list box
has an LBS_SORT style (included in LBS_STANDARD), the indices no longer
match.


Listing Files

I've been saving the best for last: LB_DIR, the most powerful list box
message. This fills the list box with a file directory list, optionally
including subdirectories and valid disk drives:

SendMessage (hwndList, LB_DIR, wAttr, (LONG) lpszFileSpec) ;

Using file attribute codes

The wAttr parameter is a file attribute code. The least significant byte is
the normal file attribute code when making MS-DOS function calls:

wAttr                Attribute
────────────────────────────────────────────────────────────────────────────
0x0000               Normal file
0x0001               Read-only file
0x0002               Hidden file
0x0004               System file
0x0010               Subdirectory
0x0020               File with archive bit set

The high byte provides some additional control over the items desired:

wAttr                 Option
────────────────────────────────────────────────────────────────────────────
0x4000                Include drive letters
0x8000                Exclusive search only

When the wAttr value of the LB_DIR message is 0x0000, the list box lists
normal files, read-only files, and files with the archive bit set. This is
consistent with the logic used by MS-DOS function calls to find files. When
the value is 0x0010, the list includes child  subdirectories in addition to
these files; this list is the equivalent of that displayed by the Directory
command or by Windows' File Manager. A value of 0x4010 expands the 0x0010
list to include all valid drives; for many Windows programs, this is the
list in the dialog box called up by selecting Open from the program's File
menu. To list all files, child subdirectories, and drives, you set the wAttr
value to 0x4037.

Setting the topmost bit of wAttr lists the files with the indicated flag
while excluding normal files. For a Windows file backup program, for
instance, you might want to list only files that have been modified since
the last backup. Such files have their archive bits set, so you would use
0x8020. A value of 0x8010 lists only subdirectories; 0xC000, only valid disk
drives; and 0xC010, subdirectories and valid disk drives but no files.


Ordering file lists

The lParam parameter is a far pointer to a file specification string such as
"*.*". This file specification does not affect the subdirectories that the
list box includes.

You'll want to use the LBS_SORT message for list boxes with file lists. The
list box will first list files satisfying the file specification and then
(optionally) list valid disk drives in the form:

[-A-]

and (also optionally) subdirectory names. The first subdirectory listing
will take the form:

[..]

This "double-dot" subdirectory entry lets the user back up one level toward
the root directory. (The entry will not appear if you're listing files in
the root directory.) Finally, the specific subdirectory names are listed in
the form:

[SUBDIR]

If you do not use LBS_SORT, the filenames and subdirectory names are
intermixed and the drive letters appear at the bottom of the list box.



A head for Windows

A well-known UNIX utility called head displays the beginning lines of a
file. Let's use a list box to write a similar program for Windows. HEAD,
shown in Figure 6-7, lists all files and child subdirectories in the list
box. You can choose a file to display by double-clicking on the filename
with the mouse or by pressing the Enter key when the filename is selected.
You can also change the subdirectory using either of these methods. The
program displays up to 2 KB of the beginning of the file in the right side
of the client area of HEAD's window.

 HEAD.MAK

#--------------------
# HEAD.MAK make file
#--------------------

head.exe : head.obj head.def
     link head, /align:16, NUL, /nod slibcew libw, head
     rc head.exe

head.obj : head.c
     cl -c -Gsw -Ow -W2 -Zp head.c

 HEAD.C

/*---------------------------------------------
   HEAD.C -- Displays Beginning (Head) of File
             (c) Charles Petzold, 1990
  ---------------------------------------------*/

#include <windows.h>
#include <io.h>
#include <string.h>
#include <direct.h>

#define  MAXPATH     100
#define  MAXREAD    2048

long FAR PASCAL WndProc  (HWND, WORD, WORD, LONG) ;
long FAR PASCAL ListProc (HWND, WORD, WORD, LONG) ;

char    sReadBuffer [MAXREAD] ;
FARPROC lpfnOldList ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName [] = "Head" ;
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;


          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = COLOR_WINDOW + 1 ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "File Head",
                          WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static BOOL     bValidFile ;
     static char     szFile [16] ;
     static HWND     hwndList, hwndText ;
     static OFSTRUCT ofs ;
     static RECT     rect ;
     char            szBuffer [MAXPATH + 1] ;
     HDC             hdc ;
     int             iHandle, i, iCount ;
     PAINTSTRUCT     ps ;
     TEXTMETRIC      tm ;

     switch (message)
          {
          case WM_CREATE :
               hdc = GetDC (hwnd) ;
               SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;
               GetTextMetrics (hdc, &tm) ;
               ReleaseDC (hwnd, hdc) ;
               rect.left = 20 * tm.tmAveCharWidth ;
               rect.top  =  3 * tm.tmHeight ;

               hwndList = CreateWindow ("listbox", NULL,
                              WS_CHILDWINDOW | WS_VISIBLE | LBS_STANDARD,
                              tm.tmAveCharWidth, tm.tmHeight * 3,
                              tm.tmAveCharWidth * 13 +
                                   GetSystemMetrics (SM_CXVSCROLL),
                              tm.tmHeight * 10,
                              hwnd, 1,
                              GetWindowWord (hwnd, GWW_HINSTANCE), NULL) ;

               hwndText = CreateWindow ("static", getcwd (szBuffer,
MAXPATH),
                              WS_CHILDWINDOW | WS_VISIBLE | SS_LEFT,
                              tm.tmAveCharWidth,           tm.tmHeight,
                              tm.tmAveCharWidth * MAXPATH, tm.tmHeight,
                              hwnd, 2,
                              GetWindowWord (hwnd, GWW_HINSTANCE), NULL) ;

               lpfnOldList = (FARPROC) GetWindowLong (hwndList, GWL_WNDPROC)
;

               SetWindowLong (hwndList, GWL_WNDPROC,
                         (LONG) MakeProcInstance ((FARPROC) ListProc,
                                   GetWindowWord (hwnd, GWW_HINSTANCE))) ;

               SendMessage (hwndList, LB_DIR, 0x37, (LONG) (LPSTR) "*.*") ;
               return 0 ;

          case WM_SIZE :
               rect.right  = LOWORD (lParam) ;
               rect.bottom = HIWORD (lParam) ;
               return 0 ;

          case WM_SETFOCUS :
               SetFocus (hwndList) ;
               return 0 ;

          case WM_COMMAND :
               if (wParam == 1 && HIWORD (lParam) == LBN_DBLCLK)
                    {
                    if (LB_ERR == (i = (WORD) SendMessage (hwndList,
                                                  LB_GETCURSEL, 0, 0L)))
                         break ;

                    SendMessage (hwndList, LB_GETTEXT, i,
                                        (LONG) (char far *) szBuffer) ;
                    if (-1 != OpenFile (szBuffer, &ofs, OF_EXIST | OF_READ))
                         {
                         bValidFile = TRUE ;
                         strcpy (szFile, szBuffer) ;
                         getcwd (szBuffer, MAXPATH) ;
                         if (szBuffer [strlen (szBuffer) - 1] != '\\')
                              strcat (szBuffer, "\\") ;
                         SetWindowText (hwndText, strcat (szBuffer, szFile))
;
                         }
                    else
                         {
                         bValidFile = FALSE ;
                         szBuffer [strlen (szBuffer) - 1] = '\0' ;
                         chdir (szBuffer + 1) ;
                         getcwd (szBuffer, MAXPATH) ;
                         SetWindowText (hwndText, szBuffer) ;
                         SendMessage (hwndList, LB_RESETCONTENT, 0, 0L) ;
                         SendMessage (hwndList, LB_DIR, 0x37,
                                      (LONG) (LPSTR) "*.*") ;
                         }
                    InvalidateRect (hwnd, NULL, TRUE) ;
                    }
               return 0 ;

          case WM_PAINT :
               hdc = BeginPaint (hwnd, &ps) ;
               SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;
               SetTextColor (hdc, GetSysColor (COLOR_WINDOWTEXT)) ;
               SetBkColor   (hdc, GetSysColor (COLOR_WINDOW)) ;

               if (bValidFile && -1 != (iHandle =
                         OpenFile (szFile, &ofs, OF_REOPEN | OF_READ)))
                    {
                    i = read (iHandle, sReadBuffer, MAXREAD) ;
                    close (iHandle) ;
                    DrawText (hdc, sReadBuffer, i, &rect, DT_WORDBREAK |
                                   DT_EXPANDTABS | DT_NOCLIP | DT_NOPREFIX)
;
                    }
               else
                    bValidFile = FALSE ;

               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

long FAR PASCAL ListProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     if (message == WM_KEYDOWN && wParam == VK_RETURN)

          SendMessage (GetParent (hwnd), WM_COMMAND, 1,
                         MAKELONG (hwnd, LBN_DBLCLK)) ;

     return CallWindowProc (lpfnOldList, hwnd, message, wParam, lParam) ;
     }

 HEAD.DEF

;---------------------------------
; HEAD.DEF module definition file
;---------------------------------

NAME           HEAD

DESCRIPTION    'File Head Program (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc
               ListProc

In ENVIRON, when we selected an environment variable--either with a mouse
click or with the keyboard--the program displayed an environment string. If
we used this select-display approach in HEAD, however, the program would be
too slow because it would continually need to open and close files as you
moved the selection through the list box. Instead, HEAD requires that the
file or subdirectory be double-clicked. This presents a bit of a problem
because list box controls have no automatic keyboard interface that
corresponds to a mouse double-click. As we know, we shouldn't write Windows
programs that require a mouse.

The solution? Window subclassing, of course. The list box subclass function
in HEAD is called ListProc. It simply looks for a WM_KEYDOWN message with
wParam equal to VK_RETURN and sends a WM_COMMAND message with an LBN_DBLCLK
notification code back to the parent. The WM_COMMAND processing in WndProc
uses the Windows function OpenFile to check for the selection from the list.
If OpenFile returns an error, the  selection is not a file, so it's probably
a subdirectory. HEAD then uses chdir to change the subdirectory. It sends a
LB_RESETCONTENT message to the list box to clear out the contents and a
LB_DIR message to fill the list box with files from the new subdirectory.

The WM_PAINT message processing in WndProc opens the file using the Windows
OpenFile function. This returns an MS-DOS handle to the file that can be
passed to the normal C functions read and close. The contents of the file
are displayed using DrawText.


2 KB of Wasted Space

HEAD includes a 2-KB array called sReadBuffer that is needed only briefly,
when the contents of the file are read and passed to DrawText. But this
array remains in the program's data segment during the entire time this
program is running. Wouldn't it make more sense to allocate that memory
before the read call and free it up after DrawText?

Yes, it would. For that reason we can no longer avoid the subject of Windows
memory management. It's not an easy subject, but let's begin.






PART III  USING RESOURCES
────────────────────────────────────────────────────────────────────────────

Chapter 7  Memory Management
────────────────────────────────────────────────────────────────────────────

Multitasking without memory management is like having a party in a closet:
You may be able to accommodate some of the earlier arrivals, but once
everybody starts mingling, some toes are going to get smashed.

Memory management has always been one of the most remarkable aspects of
Windows. Even Windows 1 included a sophisticated memory management scheme
that implemented in software some of the memory management features you
might expect to find in a protected-mode operating system. Here are some
examples of Windows 1 memory management:

  ■   When Windows runs multiple instances of the same program, it uses the
      same code segments and the same resources for each instance.
      (Resources include icons, cursors, menu templates, and dialog box
      templates, all of which are covered in the next three chapters.) In
      most cases, Windows requires only that data segments be unique for
      each instance of a program.

  ■   Much of the memory allocated within Windows is moveable, including (in
      most cases) the memory allocated for a program's code segments, data
      segments, and resources.

  ■   Code segments and resources are often "demand-loaded": Windows does
      not load them into memory until a program specifically needs them.

  ■   Code segments and resources are often discardable: When Windows needs
      to free some memory, it discards the segments from memory and later
      reloads them from the program's .EXE file as the program requires.

These memory management features allowed Windows 1 to run several large
programs in a memory space that might not be large enough for even one of
the programs under a less ambitious memory management scheme. The problem is
that this memory management scheme requires that Windows often reload code
segments and resources from the hard disk, hurting program performance. For
this reason, support of the Expanded Memory Specification (EMS) 4.0 was
added to Windows 2, and protected-mode support was added to Windows 3.

Windows 3 can run in three distinct modes:

  ■   On a machine based around the Intel 8086 processor (or an 80286 or
      80386 processor with less than 1 MB of memory), Windows 3 runs in
      "real mode." This is essentially compatible with the memory
      configuration of Windows 2.1. Windows and its applications occupy an
      upper area of the 640 KB of conventional memory above MS-DOS and any
      device drivers and RAM-resident programs that may be loaded.

      In this mode, Windows can take advantage of any expanded memory under
      the Lotus-Intel-Microsoft Expanded Memory Specification 4.0 (LIM EMS
      4.0). This configuration requires an EMS memory board and an EMS 4
      device driver.

  ■   On a machine based around the Intel 80286 processor with at least 1 MB
      of memory (or an 80386 processor with less than 2 MB of memory),
      Windows 3 runs in "standard mode." This is 286-compatible protected
      mode. Windows can use up to 16 MB of conventional memory and extended
      memory.

  ■   On a machine based around the Intel 80386 processor with at least 2 MB
      of memory, Windows 3 runs in "386 enhanced mode." This is essentially
      standard mode with two additional features: Windows uses the paging
      registers of the 386 processor to implement virtual memory. The 386
      pages are 4 KB in length. Windows can swap pages to disk and reload
      them when necessary. (This is the only form of virtual memory
      supported for Windows applications.) The page swapping is something
      you normally don't have to think about when coding for Windows. The
      second feature of 386 enhanced mode uses the Virtual-86 mode of the
      386 processor to support multiple virtual DOS machines. This does not
      impact Windows programming.

      You can override the default configuration by running Windows from the
      command line with a /R (real mode), /2 (standard mode), or /3 (386
      enhanced mode) parameter. A Windows program can obtain information
      about the mode in which it is running by calling GetWinFlags.

      Much of this chapter discusses the real mode memory configuration
      because this is the least common denominator of all the modes in which
      Windows can run. (Another reason for this is that the techniques
      Windows uses to manage memory in real mode are quite interesting!) If
      you initially feel a little queasy when thinking about Windows moving
      your program around in memory in real mode, that's good. It means that
      you're already aware that this is not an easy feat. You must keep this
      fact in mind in order to write programs that run without problems. You
      must cooperate with Windows' memory management. That's what we'll look
      at in this chapter.

SEGMENTED MEMORY, INTEL STYLE

Windows organizes memory in "segments"--so before we proceed, let's quickly
review the segmented memory architecture of the Intel 8086 family of
microprocessors. This family includes the 8088 found in the PC and PC/XT,
the 8086 and the 186 found in some compatibles, the 286 found in the PC/AT,
and the 386 and 486.

When these processors run in real mode, a memory address consists of two
parts, a 16-bit segment address and a 16-bit offset address. The 16-bit
segment address is shifted 4 bits to the left and added to the offset
address. The resultant 20-bit physical address can access 1 MB of data:

16-bit offset address        xxxxxxxxxxxxxxxx

16-bit segment address + xxxxxxxxxxxxxxxx0000

20-bit physical address  xxxxxxxxxxxxxxxxxxxx

Four internal registers of the microprocessor hold segment addresses. These
segment registers are called CS (code segment), DS (data segment), SS (stack
segment), and ES (extra segment). The 386 and 486 have two additional
segment registers: FS and GS.

Software for the 8086 family runs most efficiently when the segment
addresses are held constant and all addressing is done by varying the offset
addresses. The offset addresses generated by the microprocessor include the
instruction pointer (IP), which accesses code in combination with the CS
register; the stack pointer (SP) and base pointer (BP), which access the
stack in combination with the SS register; and the BX (base), SI (source
index), and DI (destination index) registers, which access data, most often
in combination with the DS or ES register. An address that uses only the
offset address with an implied segment address (the current segment address)
is called a "near pointer" or sometimes a "short pointer." An address that
uses both the segment and offset addresses is called a "far pointer" or a
"long pointer."

For any particular segment address, the offset address can vary within a
64-KB range, from 0000H through FFFFH. A block of memory that is based on a
particular segment address is called (appropriately enough) a "segment."
People used to think of segments as 64-KB blocks of memory, but this
definition is becoming less common. Now we say that segments can be any size
up to 64 KB. Sometimes the size of a segment (or the size of an area of
memory larger than 64 KB) is given in terms of "paragraphs." A paragraph is
16 bytes. Memory allocated for a segment is often a multiple of 16 bytes,
because a segment must begin on a 16-byte boundary of physical memory. (When
the segment register is shifted left 4 bits, the bottom 4 bits are 0.)

When the 286, 386, and 486 processors run in 286-compatible protected mode,
the segment does not refer to a physical memory address. Instead, the
segment is an offset into a "descriptor table" that provides a 24-bit base
address in physical memory. The offset address is then added to this base
address to generate a 24-bit physical address that can access up to 16
megabytes of memory.

The use of segments is central to Windows' memory organization. The entire
memory space controlled by Windows is divided into segments of various
lengths. Some of these segments contain code, and others contain data.


MEMORY ORGANIZATION IN WINDOWS

The entire memory area that Windows controls is called "global memory" or
the "global heap." This area begins at the location where MS-DOS first loads
Windows into memory and ends at the top of available memory, which most
often is the top of physical memory. (In C programming, the word global
usually refers to variables or functions in one source code file that can be
referenced from functions in another source code file of the same program.
That is not the meaning of global here. In this discussion of Windows'
memory organization, the word global instead means "everything.") Every
block of memory allocated from the global heap is a segment. Global memory
not currently allocated is called "free memory."

A Windows program can have one or more code segments and one or more data
segments. (The example programs shown in this book have only one of each.)
When Windows loads a program into memory, it allocates at least one segment
from the global heap for code and one segment for data. When the program
begins to execute, the microprocessor's CS register is set to the segment
address of the code segment that contains the entry point of the program.
The DS and SS registers are set to the segment address of the program's
automatic, or default, data segment, which is the data segment that contains
the stack. (The combination of the data and the stack into one segment
referenced by both DS and SS is normal for C compilers. DS is used to
reference data declared as static; SS is used to reference data on the
stack, which includes local nonstatic data and arguments passed to
functions. This approach allows near pointers to be used for function
parameters. The function doesn't have to know whether it's dealing with
static data or stack data. Problems related to unequal DS and SS segment
registers are discussed in Chapter 19, "Dynamic Link Libraries.")

When loading a program, Windows also allocates two other segments from the
global heap for program overhead. One of these segments contains the header
portion of the program's .EXE file. This segment is used for all instances
of a program, so it is allocated only for the first instance. The other
segment contains information unique to each instance, such as the program's
command-line string and the program's current subdirectory. When a program
loads resources (such as icons, cursors, or menu templates) into memory,
each resource gets its own segment in the global heap. A program may itself
also allocate some memory from the global heap.

If a program has only one code segment, then any calls it makes to functions
within the program are compiled as near calls. The CS code segment register
remains the same. However, when a program calls a Windows function, the
Windows function is in a different code segment. This situation requires
that the program generate a far call, which is the reason that all Windows
functions (and all functions within your program that are called by Windows)
must be declared as far.

A Windows program that has one data segment can use near pointers to access
memory within that data segment. However, when a Windows program passes a
pointer to a Windows function, the pointer must be a far (or long) pointer;
otherwise, the code that contains the Windows function will use its own data
segment. The far pointer is required for the Windows function to access the
data within your program's data segment.

Fixed and Moveable Segments

Every segment in Windows' total memory space is marked with certain
attributes that tell Windows how to manage the segment. First and foremost,
segments are marked as either "fixed" or "moveable." Windows can move
moveable segments in memory if necessary to make room for other memory
allocations. When Windows moves a segment in memory, all existing near
pointers to that segment continue to be valid, because near pointers
reference an offset from the beginning of a segment. However, far pointers
become invalid when the segment they reference is moved. A fixed segment
cannot be moved in memory. Segments must be marked as fixed if Windows is
incapable of modifying an existing far pointer to the segment.

In protected mode, all program segments are moveable because Windows can
move the segment without changing the segment address. Windows need only
change the physical base address in the descriptor table.

Most segments--including the segments allocated for your program's code and
data--are moveable, but some exceptions exist. Whenever Windows gives your
program a far pointer, the pointer references a fixed data segment. For
instance, when your Windows program begins executing, Windows passes a
parameter to WinMain that we call  lpszCmdLine. This is a far pointer to an
area of memory that contains a command-line argument for the program. I
mentioned above that this command-line string is stored in a program
overhead segment that Windows creates for each instance of a program. This
program overhead segment must be fixed. If Windows moves it, the
command-line pointer passed to your program becomes invalid.

Here's one way Windows deals with moveable segments: You've seen how Windows
and your programs use numbers called "handles." In many cases, the handles
are really near pointers. Windows maintains a segment called BURGERMASTER
(named after a favorite restaurant of the early Windows developers) that
contains a master handle-to-memory table. The handle points to a small area
of memory within BURGERMASTER that contains the segment address of the item
that the handle references. When Windows moves the segment that contains the
item, it can adjust the address in BURGERMASTER without invalidating the
handle. BURGERMASTER is itself a moveable segment.

All non-Windows MS-DOS programs are assigned fixed segments when they run
under Windows. Windows cannot determine how these programs reference memory,
so Windows has no choice but to make them fixed. However, you should try
very hard to ensure that the code and data segments of your Windows programs
(as well as any additional segments your programs allocate) are moveable
segments. Fixed segments stand like brick walls in memory space and clog up
Windows' memory management. Users quickly learn which programs seem to use
little memory (because they use moveable segments) and which seem to use a
lot of memory (because they use fixed segments). Users have a name for
programs that use a lot of memory. They say, "This program is a real pig."
Your goal should be to write programs that are not pigs.


Discardable Memory

Moveable segments can also be marked as discardable. This means that when
Windows needs additional memory space, it can free up the area occupied by
the segment. Windows uses a "least recently used" (LRU) algorithm to
determine which segments to discard when attempting to free up memory.

Discardable segments are almost always those that do not change after they
are loaded. Code segments of Windows programs are discardable because (in
most cases) programs do not modify their code segments. Indeed, code
segments cannot be modified in protected mode. When Windows discards a code
segment, it can later reload the code segment by accessing the .EXE file.
Most of Windows' own code in the USER and GDI modules and various driver
libraries is also discardable. (The KERNEL module is fixed. This is the
module responsible for Windows' memory management.) Resources--such as
dialog box templates, cursors, and icons--also are often marked as
discardable. Again, Windows can simply reload the resource into memory by
accessing the .EXE file that contains the resource.

Sometimes you'll see that a disk is being accessed when you move the mouse
from the client area of one program to the client area of another. Why is
this? Windows has to send mouse movement messages to the second application.
If the program's code to process this message is not currently in memory,
Windows must reload it from the disk file. If you have several large Windows
programs loaded simultaneously, you may witness some "thrashing" (an
inordinate amount of disk activity) as you move from program to program.
Windows is reloading previously discarded segments.

Discardable segments must also be moveable segments, because discardable
segments can be reloaded in a different area of memory than the area they
occupied earlier. However, moveable segments are not always discardable
segments. This is usually the case with data segments. Windows cannot
discard a program's automatic data segment, because the segment always
contains read-write data and the stack.

Many people are under the impression that Windows also swaps memory--that
is, that when Windows needs additional memory, it saves some portion of
memory on the disk and reloads it at a later time. For Windows programs,
this is true only when Windows is running in 386 enhanced mode. Windows
swaps memory to disk only when running non-Windows programs (otherwise known
as "old applications"). When Windows loads these old applications from disk
back into memory, it must load them at the same memory address they occupied
previously.


The Global Memory Layout

As I noted before, global memory ranges from the spot where MS-DOS first
loads Windows to the top of available memory. At the bottom of global memory
(the area with the lowest memory address), Windows allocates fixed segments.
Fixed segments are allocated from the bottom up. At the top of global
memory, Windows allocates discardable code segments. (Remember that
discardable segments are also moveable segments.) Discardable segments are
allocated from the top down.

Between fixed segments and discardable segments, Windows allocates moveable
segments and nondiscardable data segments. The largest block of free memory
is usually located below the discardable segments. The memory layout looks
something like that shown in Figure 7-1 on the following page, with arrows
indicating the direction in which the areas expand. When Windows needs to
allocate a fixed segment, it starts searching from the bottom up for a
sufficiently large free block below the area of moveable segments. If it
can't find one, it starts moving moveable segments up in memory to make
room. If that doesn't work, Windows begins discarding discardable segments,
based on an LRU (least recently used) algorithm, again moving moveable
segments. To allocate moveable but nondiscardable segments, Windows searches
the free memory area below the discardable segments. If it doesn't find
enough room, Windows moves other moveable segments down in memory and
eventually starts discarding discardable segments.

FIGURE 7-1 ILLUSTRATION WITH LABELS AND ARTWORK

  (Figure 7-1. may be found in the printed book.)

Within the area of discardable memory, Windows maintains a space large
enough to accommodate the largest code segment of every currently running
program. Windows never runs out of memory space when reloading code
segments. However, Windows can run out of memory space when a program
attempts to allocate global memory or load a resource. Sometimes it can be a
little tricky to deal with this problem--you may need a text string or icon
not currently in memory to display an error message. If your program needs
to report that it is low on memory, you can use a message box. Windows keeps
in memory all the code necessary to create a message box. You'll want to use
the MB_SYSTEMMODAL flag to prevent the user from switching to another
application. MB_ICONHAND (which is supposed to accompany messages about
severe problems) is also always in memory. The text message in the message
box should either be in your default data segment or be a string resource
that has previously been copied into your data segment.


Local Memory

Every Windows program has at least one data segment called the default, or
automatic, data segment. A program's DS and SS segment registers both point
to this segment. In contrast to the "global memory" that Windows manages,
this automatic data segment is called your program's "local memory." Within
Windows' global memory organization, your program's automatic data segment
is most often a moveable but nondiscardable segment. The segment is called
DGROUP.

In both regular MS-DOS C programs and Windows programs, the memory within
DGROUP is organized into four areas, as shown in Figure 7-2. These four
areas are described below:

  ■   Initialized static data--This area contains initialized variables
      defined outside of functions, initialized static variables within
      functions, and explicit strings and floating-point numbers.

  ■   Uninitialized static data--This area has uninitialized variables that
      are defined outside of functions and uninitialized variables defined
      as static

      within functions. In accordance with C standards, all uninitialized
      static variables are initialized to 0 when the data segment is created
      in memory.

  ■   Stack--This area is used for "automatic" data items defined within
      functions (variables not defined as static), for data passed to
      functions, and for return addresses during function calls.

  ■   Local heap--This is free memory available for dynamic allocation by
      the program.

        (Figure 7-2. may be found in the printed book.)

      The module definition (.DEF) file specifies your program's stack and
      local heap size:

      HEAPSIZE  1024
      STACKSIZE 8192

      In a regular C program, you can allocate memory from the local heap
      using the malloc and calloc functions. In Windows programs, you can
      also allocate memory from the local heap, but you'll want to use the
      Windows memory allocation functions rather than the C functions. When
      you use Windows functions to allocate local memory, Windows organizes
      the local heap just like global memory, as shown in Figure 7-3.
      Although the stack is fixed in size, Windows can dynamically expand
      the local heap if you attempt to allocate more local memory than is
      specified in your module definition file. Windows can even move your
      data segment if that is necessary to expand the local heap.

        (Figure 7-3. may be found in the printed book.)



CODE AND DATA SEGMENTS

All the Windows programs shown so far have one code segment and one data
segment. Windows programs can also have multiple code and data segments. For
larger programs, using multiple code segments is highly recommended because
it helps relieve memory congestion in Windows. Using multiple data segments,
on the other hand, is a real problem. Let's take a look at this interesting
subject.

Memory Models: Small, Medium, Compact, Large, and Huge

When we speak about a program having one code or data segment or multiple
code or data segments, we're referring to "memory models." Microsoft C 6
supports five memory models that you can use for Windows programs:

Model    Code Segments  Data Segments
────────────────────────────────────────────────────────────────────────────
Small    1              1
Medium   Multiple       1
Compact  1              Multiple
Large    Multiple       Multiple
Huge     Multiple       Multiple

Command-line switches of the compiler determine the memory model you use.
The small memory model is the default, but programs that have more than 64
KB of code must contain two or more code segments, and programs that have
more than 64 KB of data must contain two or more data segments. In
medium-model and large-model programs, all functions (unless explicitly
declared as near) are far, and the compiler generates far calls for them. In
compact-model and large-model programs, all data references use far
pointers.

The small, compact, medium, and large models all have their own C libraries.
The medium-model and large-model libraries contain functions that assume
they have been called from another segment. The functions in the
compact-model and large-model libraries always assume they have been passed
long pointers to data. The huge model is essentially the same as the large
model, except that individual data items may be greater than 64 KB. The huge
model has limited library support.

Most small Windows programs are compiled as small-model programs, with one
code segment and one data segment. Strictly speaking, however, Windows
programs are really "mixed-model" programs, because they extensively use the
near and far keywords. Windows programs make far calls to Windows functions,
and Windows makes far calls to functions within a program, such as window
procedures or call-back functions. All data pointers passed between a
program and Windows (with one oddball exception--the GetInstanceData
function) are far pointers.

Windows programs can also be compiled as medium-model programs. You can try
this out on any of the programs shown so far. You need to make two changes
to the make file:

  ■   Add the -AM switch to the compile step. This compiles for the medium
      model.

  ■   Change SLIBCEW to MLIBCEW in the link step. This is the library that
      contains the Windows-specific C run time library functions.

Delete the .OBJ file and run NMAKE. You'll find that the .EXE file is
somewhat larger than before because all the functions within your
program--not only the window procedure and call-back functions--now require
far calls.


Multiple Code Segments

The medium model doesn't make sense for a small program, and it comes into
play only when you have more than one source code module. But then it starts
making a lot of sense. In the medium model, each source code module becomes
a different code segment. Each of these code segments can be moveable and
discardable. The amount of space required to fit your code into memory is
the size of the largest code segment.

For instance, the approximately 160 KB of code in Windows WRITE is
distributed among 78 separate moveable and discardable code segments. The
largest code segment in WRITE is about 8 KB. Thus, when memory is limited,
WRITE can continue to run with only an 8-KB code space. As program logic
within WRITE moves from segment to segment, the code segment currently in
memory can be discarded and a new one can be loaded in.

If you like, you can think of the medium model as a simplified overlay
manager: You split your program into multiple source code modules and
compile for the medium model. Windows does the rest. In order to work
efficiently, the medium-model approach requires some planning. The functions
in each of your source modules should be organized in functional groups.
When your program is dealing with such routine matters as processing mouse
messages, for example, it should not have to load several code segments to
get from the top of the window procedure to the bottom.

While using the medium model is certainly the easiest approach to take with
a large program, it is not the most efficient. As you'll see shortly, when
you run the Microsoft C Compiler with the -Gw switch (the Windows switch),
the compiler adds some extra prolog code to all far functions. Only those
functions actually called by Windows (such as window procedures or call-back
functions) need this prolog code, however. When all the functions in your
program are far functions (as they are in the medium model), this extra code
can add up to a significant waste of space.

There are several solutions to this problem. The first is fairly simple.
When compiling a module that does not include any functions that are called
from Windows (such as window procedures, dialog procedures, or call-back
functions), compile with the -GW switch rather than the -Gw switch. This
reduces the prolog code on far functions.

Another approach to reduce wasted space in a medium-model program is to
define all functions used only within a module as near functions. Another
solution is to use a mixed model with a small model as a base. Let's assume
you have five source code modules:

  ■   Module 1 contains WinMain, the message loop, your window procedure,
      and most message processing.

  ■   Module 2 contains one function that has all initialization code. This
      function is called by WinMain from Module 1 before entering the
      message loop.

  ■   Module 3 contains one function called from your window procedure and
      several other functions called only within this module.

  ■   Module 4 also contains a function called from your window procedure
      and several other functions called only within this module. This
      module also calls a function in Module 5.

  ■   Module 5 contains a function called from Module 4 and several other
      functions called only within this module.

You can organize this program into four segments, with Modules 4 and 5 in a
single segment. Within each module, you explicitly define as far any
function called from outside the segment. This involves one function each in
Modules 2, 3, and 4. In the modules that call these functions, the functions
must be declared as far using the function name prefaced by FAR near the top
of the program.

You compile each module for the small model, except that you assign one code
segment name to Module 2, another to Module 3, and yet another to Modules 4
and 5. These names are assigned by including the -NT ("name the text
segment") switch when compiling. Each module with the same code segment name
is in the same segment. Now you have far functions only where you need
them--for functions that are called from another segment.

As you can see, this mixed-model approach is more of a headache than the
medium-model approach. It requires that you figure out which functions must
be declared far and which can be near. It also has an additional problem:
You can call normal C library routines from only one segment--the segment
that gets the default segment name _TEXT when you compile without the -NT
switch.


What About the Compact and Large Models?

Windows programmers who require more than 64 KB of data in their programs
might be feeling a little nervous at this point. They have a right to be,
because the compact and large models are not recommended for Windows
programs. This doesn't mean they can't be used, however. The Windows
Software Development Kit allows you to install compact-model and large-model
Windows libraries, so obviously these models are legal. However,
compact-model and large-model programs are subject to a very strict penalty:
The data segments must be fixed in memory. They cannot be flagged as
moveable.

Why this restriction? There are various reasons; here's an easy example that
illustrates one of them. Suppose that somewhere within your program you
define a static pointer that looks like this:

char *pointer ;

Because you're compiling for a compact model or large model, this is a far
pointer. During your program's execution, you assign a value to that
pointer. If the pointer references a moveable data segment, then the value
of the pointer must be adjusted when Windows moves the data segment it
references. But the compiler and linker will not even generate a relocation
address for that pointer because it's not initialized.

Program developers who cry "but I need the large model" should consider the
alternatives made evident by existing Windows applications. Take a look at
some large Windows programs such as Microsoft Excel and Aldus PageMaker.
These programs use many code segments but only one data segment. And if
these programs don't need the large model, then you probably don't either.
If your program needs more than 64 KB of data, you have alternatives to
using the compact model or large model. Here they are:

  ■   If your program uses large blocks of uninitialized data, do not define
      these data as variables within the program. Instead, use the Windows
      GlobalAlloc function (discussed later in this chapter) to allocate a
      block of moveable memory outside your program.

  ■   If your program uses large blocks of initialized read-only data (the
      most obvious example is "help" text), make the data a discardable
      "user-defined resource" (discussed in Chapter 8) to be loaded from the
      disk only when necessary. This keeps the data out of memory when not
      needed.

  ■   If your program uses large blocks of initialized read-write data, put
      the initialized data in a discardable "user-defined resource" and
      transfer the data to a global memory block allocated with GlobalAlloc.


Avoiding Movement Problems

We have been creating small Windows programs for several chapters now and
have not run into problems when Windows has moved the code and data segments
in memory. Here are some general rules for continuing to avoid problems:

  ■   Use the small model or medium model.

  ■   Don't declare any variables as far. For instance, don't do something
      like this:

      int far array [10][1000] ;    // Bad!!!!!

      This code creates a second data segment in your program. Unless you
      mark this segment as fixed, Windows does not properly handle
      references to this array.

  ■   Don't store any far pointers to data except those that Windows gives
      you. The lpszCmdLine parameter passed to WinMain is OK. The far
      addresses returned from GlobalLock (discussed later in this chapter)
      and LockResource (discussed in Chapter 8) point to fixed data until
      you specifically unlock the data, so these are legitimate also. Some
      window messages (WM_CREATE, for instance) have lParam values that are
      far pointers. Use these pointers only for the duration of the message.

  ■   When you call Windows functions, you must often pass far pointers to
      data that are within your data segment. But don't declare far pointers
      and assign them far addresses to local data items and then later use
      the far pointers when calling Windows functions. Instead, cast the
      near pointers into far pointers when you call the functions, or let
      the compiler do this casting for you.

  ■   Don't store or use any far pointers to code, except for pointers to
      functions that are specifically declared as far or far pointers
      returned from MakeProcInstance. (Pointers to functions in medium-model
      programs are also OK.)

  ■   When you need to give Windows a far pointer to a function for a
      call-back function (as discussed in Chapter 5), a window subclassing
      function (as discussed in Chapter 6), or a dialog box function (as
      discussed in Chapter 10), obtain the pointer from MakeProcInstance.
      The function must be declared as FAR PASCAL and included in the
      EXPORTS section of your module definition file.

Although these rules are numerous and important, don't let them drive you to
paranoia. Keep in mind that Windows is nonpreemptive. Windows will not pull
the rug out from under you by interrupting your code. Windows will move or
discard your code segments only when you make a Windows call. Windows will
not move your data segment except when you make a few select Windows calls.
If you have a long stretch of code between two Windows calls, you can be as
carefree as you like in that code without worrying about sudden movements.


Program Segment Attributes

I have been talking about program segments that are fixed, moveable, and
discardable. These characteristics are called "segment attributes." To tell
Windows the attributes you want in your program segments, you use the CODE,
DATA, and SEGMENTS statements in the module definition file. During linking,
LINK encodes the attributes for each program segment into a 16-bit flag and
stores this flag in the segment table of the .EXE file. Windows has access
to these segment attributes when loading your code and data segments. The
sample programs presented so far contain these two lines in their module
definition files:

CODE PRELOAD MOVEABLE DISCARDABLE
DATA PRELOAD MOVEABLE MULTIPLE

We have not yet encountered the SEGMENTS statement.

The CODE statement applies to all code segments within your program that are
not explicitly listed in the SEGMENTS statement. Similarly, the DATA
statement applies to all data segments except those listed in the SEGMENTS
statement.

There are four standard attributes: "load," "memory," "discardable," and
"instance."

The "load" attribute can be either PRELOAD or LOADONCALL. This attribute
tells Windows when to load the segment. The default is PRELOAD, which means
that the code segment (or segments) should be loaded at the time the program
begins execution. A LOADONCALL segment will be loaded into memory the first
time it is needed. For a program with a single code module, it really
doesn't matter which attribute you use. For programs with multiple code
modules, you should use PRELOAD for segments that are required when the
program first starts up (for instance, those that do initialization from
WinMain) and LOADONCALL for the other code segments. To specify different
attributes for multiple code segments, you must use the SEGMENTS statement,
discussed later.

The "memory" attribute can be either FIXED or MOVEABLE. The default is
FIXED. If you're writing normal Windows programs, you should have no problem
specifying MOVEABLE. (Device drivers that must process hardware interrupts
are another matter. These usually require one fixed code segment.)

The "discardable" attribute is indicated by the DISCARDABLE keyword. This
indicates that Windows can discard the segment from memory and reload it
from the .EXE file when necessary. Code segments in normal Windows programs
should be flagged as DISCARDABLE. The default data segment cannot be flagged
as DISCARDABLE.

The "instance" attribute is relevant only for data segments. It should be
set to MULTIPLE for Windows programs, indicating that each instance gets its
own data segments. The NONE and INSTANCE options are for Windows dynamic
link libraries (discussed in Chapter 19), because Windows libraries can have
only one instance. If the Windows library has a data segment, INSTANCE (or
SINGLE) is used. If not, NONE is used.

The SEGMENTS statement lets you assign different segment attributes to other
code and data segments within your program. The general form of the SEGMENTS
statement is:

SEGMENTS segment-name [CLASS 'class-name'] [allocate] [attributes]

You'll probably use this statement most often if you create a medium-model
program or a small-model program with multiple code segments. For a
medium-model program, the C library functions and start-up code are in a
code segment named _TEXT. Each source code module is assigned a different
code segment name that by default is the filename of the source code file
followed by _TEXT. For instance, if you have three source code modules
called PROGRAM.C, MODULE1.C, and MODULE2.C, the PROGRAM.EXE file has four
code segments: _TEXT, PROGRAM_TEXT, MODULE1_TEXT, and MODULE2_TEXT.

If you do not include a SEGMENTS statement, all four code modules take on
the attributes from the CODE statements. If you do include a SEGMENTS
statement, however, your results might look something like this:

CODE      PRELOAD MOVEABLE DISCARDABLE
DATA      PRELOAD MOVEABLE MULTIPLE
SEGMENTS  MODULE1_TEXT LOADONCALL MOVEABLE DISCARDABLE
          MODULE2_TEXT LOADONCALL MOVEABLE DISCARDABLE

Now the _TEXT and PROGRAM_TEXT segments are PRELOAD and are loaded when the
program first begins execution. MODULE1_TEXT and MODULE2_TEXT are
LOADONCALL. Windows loads them only when they are needed.

If you use the SEGMENTS statement for data segments, you must include:

CLASS 'DATA'

By default, the class is CODE. You can also add a minimum allocation size to
increase the size of the data segment when it's loaded into memory.



HOW WINDOWS MOVES AND

RELOADS PROGRAM SEGMENTS

In this section, I want to give you some insights into how Windows is able
to move your program's code and data segments and (in the case of code
segments) discard and later reload the segments in real mode. If you'd
rather not know (or if the assembly language in the pages ahead looks like
Greek to you), that's fine. But you're likely to see some of this code when
debugging Windows programs, so it's better that you see it here first and
understand what's going on.

I'll be discussing the most general recommended case--a program that
contains multiple moveable and discardable code segments and a single
moveable data segment. Everything works a little differently (and more
simply) when the segments are fixed and nondiscardable.

When a program has a single data segment, neither the code nor the data have
to contain any explicit references to the segment address of the data. When
a Windows program begins running, Windows has already set the DS and SS
registers to the data segment. All pointers are near pointers. When the
program needs the data segment address (for instance, for casting near
pointers into far pointers when calling Windows functions), it simply uses
the value of DS. (In contrast to this, when you run a regular .EXE program
outside Windows, MS-DOS does not set DS to the program's data segment on
entry to the program. The program must begin by executing code like this:

MOV AX, DGROUP
MOV DS, AX

You'll never see code like this in a compiled Windows program.) If you cast
a near data pointer into a far pointer and store the far pointer in a
variable, you're storing the current value of the data segment address. If
Windows moves the data segment, that far pointer will no longer be valid.
That's why it is recommended that you not do this.

Windows moves a program's data segment only during certain Windows calls.
Most often, movement of a data segment occurs during a GetMessage or
PeekMessage call. Windows always returns the new values of DS and SS to the
program when it returns from one of these calls.

Windows programs use the same segment for data and the stack. However,
Windows libraries (including the USER, KERNEL, and GDI modules, as well as
the drivers) have their own data segments but not their own stacks. They use
the stack of the program calling the Windows function. Thus, the stack
segment address is always that of the currently running program, even if the
program is calling a Windows function. When switching from one program to
another, Windows must also switch the stack.

When Windows calls a function in your program (such as a window procedure, a
window subclassing function, a call-back function, or a dialog box
function), the stack segment address is set to your program's stack segment,
but the data segment address is still the one used by the Windows library
calling your function. For this reason, Windows must also include a facility
so that programs retrieve their own data segments during one of these calls.

All these aspects of Windows' memory management--supporting multiple
instances, moving code and data segments, and discarding and reloading code
segments--are tied together.

Special Treatment of Far Functions

When you compile a C program, the compiler inserts prolog and epilog code in
each function. This code sets up a "stack frame" for the function. The
function uses the stack frame to retrieve parameters passed to the function
on the stack and to temporarily store the function's own local variables.
For a normal MS-DOS program, the prolog and epilog can be as simple as this:

PUSH BP
MOV  BP, SP
SUB  SP, x
[other program lines]
MOV  SP, BP
POP  BP
RET  y

I'm assuming here that stack overflow checking has been disabled. The value
of x that the prolog subtracts from SP is the total size of the function's
local nonstatic data (increased to the next even number).

After the three prolog instructions have been executed, the stack is
organized as shown in Figure 7-4 (from higher addresses to lower). The
function uses the BP register to reference both data passed to the function
on the stack (which have positive offsets to BP) and local data declared
within the function (which have negative offsets to BP). If the function
uses the SI and DI registers, these registers will also be saved on the
stack because they can be used by the caller for register variables. The DS
register must also be preserved during a function call.

  (Figure 7-4. may be found in the printed book.)

For a function declared as near, the return address is only one word--the
offset address. For a far function, the return address is two words. A
function declared as near or far normally has the same prolog and epilog,
but the compiled function must take into account the size of the return
address--one word or two--when accessing the parameters passed to the
function.

At the end of the function, the value of SP is set equal to BP, and BP is
popped off the stack to restore it to its original value. The function is
now ready to return to the caller. This involves a near RET or a far RET,
depending on whether the function is near or far. If the function is also
declared as pascal, the y value in the code above is the total size of
parameters passed to the function. Otherwise, y is not used, and the caller
adjusts the stack pointer when the function returns.

That's a normal compilation. When you compile a Windows program with the -Gw
switch (the Windows switch), every far function gets special prolog and
epilog code that looks like this:

PUSH DS
POP  AX        ; set AX to DS
NOP
INC  BP
PUSH BP        ; save incremented BP on stack
MOV  BP, SP
PUSH DS        ; save DS on stack
MOV  DS, AX    ; set DS to AX
SUB  SP, x
[other program lines]
DEC  BP
DEC  BP
MOV  SP, BP    ; reset SP to point to DS on stack
POP  DS        ; get back DS
POP  BP        ; get back incremented BP
DEC  BP        ; restore BP to normal
RET  y

Functions that are declared as near get the normal prolog and epilog even
with the -Gw switch. Notice two points here: First, the prolog sets the
value of AX equal to DS, saves the DS register on the stack, and then sets
DS from AX. On exiting, DS is retrieved from the stack. That code is not
doing anything (or anything harmful) except taking up unnecessary space.
Second, the previous value of the BP register is incremented before being
saved on the stack and decremented after it is retrieved from the stack.
This certainly looks mystifying right now. Figure 7-5 shows what the stack
frame looks like for far functions compiled with the -Gw compiler switch
after the prolog code is executed.

  (Figure 7-5. may be found in the printed book.)

When LINK creates the program's .EXE file, it treats every far function in a
moveable code segment as a "moveable entry point." In the .EXE header, LINK
builds an entry table that contains 6 bytes of information for every
moveable entry point in the program. This information includes the segment
ordinal number of the function--simply a sequential numbering of the
program's segments--and the offset of the function within that segment. The
entry table also includes the 2 bytes CDH and 3FH for every moveable entry
point. These 2 bytes are actually the instruction INT 3FH. This same
software interrupt shows up in non-Windows programs, where it is used for
overlay management. In Windows, the interrupt performs a similar function in
that it loads a program's code segment from disk into memory.

A flag in the entry table indicates whether the far function was listed in
the EXPORTS section of the module definition (.DEF) file. As I've discussed,
the EXPORTS section of the .DEF file must list all far functions in your
program that are called by Windows. These include window procedures (such as
the function I've been calling WndProc), call-back functions, window
subclassing functions, and dialog box functions.

When your program's code contains references to the addresses of far
functions, LINK has to leave the instruction incomplete. For example, if you
call a far function, the compiler generates a far CALL instruction, but the
address is not yet known because LINK doesn't know where the segment
containing the function will be loaded in memory. LINK builds a relocation
table in the .EXE file at the end of each code segment. This table lists all
the references to far functions within your program as well as all
references to Windows functions.

Now that the compiler and LINK have done all these strange things to your
program, it's time for Windows to run the program and do its magic.


When Windows Runs the Program

When Windows runs the first instance of a program, it loads the data segment
and also loads one or more code segments. Windows also builds two fixed
segments for program overhead. One segment contains information unique to
each instance of the program, such as the command-line argument passed to
the program and the program's current subdirectory. The other overhead
segment, which is used for all instances of the program, contains a large
chunk of the program's .EXE file header, including the entry table. The
entry table already has 6 bytes of data for every far function in your
program. Windows expands each entry into 8 bytes. These entries, called
"reload thunks," are small pieces of code that handle segment loading.

If the segment containing the far function has not yet been loaded into
memory, the reload thunk for that far function looks like this:

SAR BY CS:[xxxx], 1
INT 3F 01 yyyy

The first statement is part of Windows' mechanism to determine whether a
segment is a candidate for discarding. The second statement calls Interrupt
3FH, the software interrupt that loads into memory the appropriate segment
containing the function.

If the segment containing the far function is present in memory, the reload
thunk looks like this:

SAR BY CS:[xxxx], 1
JMP ssss:oooo

The second instruction is a far jump instruction. The ssss:oooo segment and
offset address points to the beginning of the far function in the loaded
code segment.

The references in your program's code to far functions are listed in the
segment's relocation table. Windows must insert the addresses of the far
functions into your code. The addresses that Windows uses for this are not
the actual addresses of the far functions but rather the addresses of the
reload thunks for the far functions. Because the reload thunk is in fixed
memory, Windows doesn't need to make other changes to the code when moving
the code segment that contains the far call or moving the code segment that
the far call references. Windows needs to change only the ssss:oooo address
in the reload thunk.

When the program calls a far function, it's actually calling the reload
thunk. If the segment containing the far function is not currently in
memory, the reload thunk executes the Interrupt 3FH loader that loads the
segment into memory. Windows then alters the reload thunk to contain a JMP
instruction to the function. Windows jumps back to the reload thunk, which
then jumps to the actual function. When Windows discards a segment from
memory, it changes the reload thunk from the JMP instruction back to the INT
3FH instruction. The next time a function in that segment is needed, the
segment is reloaded.

The calls in your program to Windows functions are translated into far CALL
instructions and are also listed in the segment's relocation tables. When
Windows loads your code segment into memory, it also resolves the calls to
Windows functions. For Windows functions in fixed code segments, Windows
simply inserts the address of the Windows function into your code. Windows
functions in moveable segments have their own reload thunks, and Windows
inserts the addresses of these thunks into your code.

Windows also does something special with functions that have been listed in
the EXPORTS section of the program's module definition file. Windows
modifies the function prolog of these functions when loading the segment
into memory. It replaces the first 2 bytes of every exported far function
with NOP (no operation) instructions. The prolog now looks like this in
memory:

NOP
NOP
NOP
INC  BP
PUSH BP        ; save incremented BP on stack
MOV  BP, SP
PUSH DS        ; save DS on stack
MOV  DS, AX    ; set DS to AX
SUB  SP, x

Those two NOPs make a big difference. Now the prolog saves the original
value of DS and sets DS to AX. When this function is called, AX must already
be set to the data segment that the function must use. This change makes the
exported function unusable for normal calls to the function. This is why you
cannot call your window procedure (or any other exported function) directly
from within your program.


What MakeProcInstance Does

You've just seen that far functions listed in the EXPORTS section of your
.DEF file become unsuitable for normal use when Windows loads them into
memory. These functions require that the value of AX on entry be the
program's data segment. This data segment is different for every instance of
your program. You can do only one of two things with an exported far
function:

  ■   If the function is a window procedure, you can give Windows the
      address of the function in the window class structure when registering
      the window class with RegisterClass:

      wndclass.lpfnWndProc = WndProc ;

  ■   If the function is a call-back function, a window subclassing
      function, or a dialog box function, you must give Windows an address
      returned from MakeProcInstance. For instance, if CallBackProc is the
      name of a call-back function, you must first make a call like this:

      lpfnCallBack = MakeProcInstance (CallBackProc, hInstance)

      Because CallBackProc is a far function, the address you pass to
      MakeProcInstance is actually the address of the reload thunk. The
      lpfnCallBack address you get back from MakeProcInstance can now be
      used as a parameter to a Windows function such as SetTimer.

      MakeProcInstance and RegisterClass both deal with the exported far
      function in the same way. They create an "instance thunk" for the
      function. The address that MakeProcInstance returns is the address of
      the instance thunk. Instance thunks are in a fixed area of memory, and
      they look like this:

      MOV AX, xxxx
      JMP ssss:oooo

      The xxxx value is the data segment address for this instance of your
      program. The instance thunks are different for each instance because
      each instance uses a different data segment. The ssss:oooo address in
      the instance thunk is the segment and offset address of the reload
      thunk that reloads (or jumps to) the function. The same reload thunks
      are used for all instances of a program because they jump to the same
      shareable code.

      When Windows needs to call a function in your program (such as a
      window procedure), it actually calls the instance thunk. The instance
      thunk sets AX equal to the data segment address for that instance and
      jumps to the reload thunk. The reload thunk loads the segment if it is
      not currently present in memory and branches to the function. The
      function then saves the previous value of DS and sets DS from the
      value of AX--the data segment address for the instance. When the
      function ends, it retrieves the original value of DS from the stack
      and returns control to Windows. When Windows moves the data segment
      for a particular instance, Windows must also change the xxxx values in
      all the instance thunks for that instance.


The Difference for Dynamic Libraries

You've seen that the far function prolog inserted by the compiler is
modified by Windows if the function is exported. When far functions are in
memory, they can have one of three possible prologs. If the prolog starts
off like this:

MOV AX, DS
NOP

then the far function is called only from within the same program and is not
called by Windows. If the prolog starts off like this:

NOP
NOP
NOP

then the far function has been exported. It is not called from within the
program but instead is called only by Windows. A program's window procedure
starts off in this way.

What's the purpose of the extra NOP that shows up in both prologs? The extra
NOP disappears in the third form of the prolog:

MOV AX, xxxx

You'll find this form at the beginning of many Windows functions in the
Windows library modules (USER, KERNEL, and GDI) and drivers (MOUSE,
KEYBOARD, SYSTEM, and so forth). Unlike Windows programs, Windows libraries
cannot have multiple instances, so they do not need instance thunks. The far
function itself can contain code that sets AX equal to the segment address
of the library's data segment (the value xxxx). The rest of the prolog then
saves DS and sets DS equal to AX.

When Windows moves a library's data segment in memory, it must change the
prologs of the library functions that use that data segment. And when your
program calls a Windows function, the address it calls is either the address
of the function's reload thunk (if the function is in a moveable segment) or
the function itself (if the function is in a fixed segment).

Note that Windows library functions use their own data segments but continue
to use the stack of the caller. If the library function were to use its own
stack, it would need to copy function parameters from the caller's stack.
Windows switches the stack only when switching between tasks; calling a
library function does not constitute a task switch.


Walking the Stack

One little mystery remains. You'll recall that the far function prolog
includes the statements:

INC  BP
PUSH BP

The epilog then returns BP to normal:

POP  BP
DEC  BP

What is this for?

Think about this problem: Free memory is very low. A function in one of your
program's code segments calls a far function in another of your program's
code segments. This function then calls a Windows function. The code segment
containing this Windows function is not currently present in memory. To load
it into memory, Windows has to move your data segment and discard both your
code segments. This sounds like a serious problem, because when the Windows
function returns to your program, your program will be gone.

When Windows must discard code segments, it first goes through a little
exercise called "walking the stack." Within any function, the value of
SS:[BP] is the value of BP from the previous function. If this previous
value of BP is even, the previous function is a near function; if it's odd,
the previous function is a far function. By using successive values of BP
stored on the stack, Windows can trace through the stack until it reaches
the top, which is the stack pointer address originally given to your program
when the program began executing. Windows can determine the segment
addresses and the saved DS register values of all the functions involved in
making the current Windows function call.

If Windows has to move your program's data segment (which also requires
moving the stack), it can adjust the DS register on the stack to the new
segment address. If Windows has to move a code segment containing functions
that have been involved in the current Windows function call, it changes the
return address on the stack. If Windows has to discard a code segment, it
replaces the return address with an address that points to code, which
reloads the segment and which then branches to the appropriate return
address.

As I mentioned at the onset of this discussion, you may prefer not to think
about all this activity going on in the innards of Windows.


Expanded Memory

So far, I've been discussing how Windows manages memory in simple 640-KB
systems. Windows also supports bank-switched memory that follows the
Lotus-Intel-Microsoft Expanded Memory Specification 4.0 (LIM EMS 4.0).

In a bank-switched memory scheme such as EMS, special memory boards provide
multiple banks of memory that can occupy a single same address space. Only
one of these multiple banks occupies the address space at any time. By
manipulating registers on the memory board, the EMS device driver can switch
out one bank of memory and switch in another bank.

Expanded memory under EMS 4.0 is supported in Windows 3 in two
configurations:

In the first configuration (called "small frame"), the bank-switched memory
resides in an unused area above the 640-KB level of conventional memory and
below the 1-MB level addressable by the 8086 family running in real mode.
This 384-KB area contains the ROM BIOS, some BIOS device drivers, and video
adapter buffers, but at least 64 KB are usually available for expanded
memory.

In the second configuration (called "large frame"), the bank-switched memory
also occupies an area of the 640-KB address space containing conventional
memory. The bank-switched memory can usually occupy as much as 384 KB of
conventional memory, from the 256-KB level to the 640-KB level. The level
above which memory is bank-switched is called the "bankline."

The GetWinFlags function includes the flags WF_SMALLFRAME and WF_LARGEFRAME
if you need to determine the configuration under which your program is
running.

Windows 3 uses bank-switched memory on a per process basis. When switching
between Windows programs (which normally occurs during calls to the
GetMessage, PeekMessage, and WaitMessage functions), Windows 3 will switch
out the banks of memory associated with the first process and switch in the
banks of memory associated with the second process.

The chapter entitled "More Memory Management" in the Windows Guide to
Programming discusses which memory objects are stored below the bankline and
above the bankline in the small-frame and large-frame configurations.
Generally, this is transparent to the Windows application. The only problem
arises when two Windows programs share memory using a technique other than
the clipboard, dynamic data exchange, or dynamic link libraries. Such
sharing is not recommended because it may not work in future versions of
Windows.


Protected Mode

When running on a 286 or 386 processor with at least 1 MB of memory, Windows
runs in 286-compatible protected mode. In this mode, Windows can use the 640
KB of conventional memory and memory allocated from extended memory using
the XMS (Extended Memory) driver included in the retail release of Windows
3.

In this mode, segment addresses do not correspond to physical memory.
Instead, the segment addresses are called "selectors," which reference
physical memory through a descriptor table. Protected mode is called
"protected" because the hardware of the 286 and 386 microprocessors ensures
that programs do not load invalid segment addresses or attempt to access a
segment beyond the segment's size. The processor generates a protection
exception that the operating system (or in this case, Windows) traps.
Windows responds by terminating the offending application.

For this reason, several rules are associated with protected mode:

  ■   Do not perform segment arithmetic.

  ■   Do not load far pointers with invalid addresses.

  ■   Do not attempt to address a segment beyond its allocated length.

  ■   Do not store data in code segments.

If you follow the memory allocation guidelines I discuss below, you should
have no problem running your program in protected mode. You should do your
development work in protected mode to more easily catch bugs; if you can't,
you should definitely test your code in protected mode, because it can
reveal bugs in your code that are not so evident when running in real mode.



ALLOCATING MEMORY WITHIN A PROGRAM

Programs often need to dynamically allocate blocks of memory for internal
use. Windows programs can allocate memory either from the program's private
local heap or from Windows' global heap. Windows includes two sets of memory
allocation functions, one set for using the local heap and one for the
global heap.

There are certain trade-offs between local and global memory allocations.
Local memory allocations are generally faster and require less overhead, but
the memory in the local heap is limited to 64 KB less the combined size of
the program's initialized static variables, the program's uninitialized
static variables, and the stack. Global memory allocations are not limited
to 64 KB, either in the size of single blocks or in the total memory you can
allocate. However, pointers to global memory blocks are far pointers, which
can be awkward to work with in your program. (You cannot pass a far pointer
to small-model and medium-model C library functions, for instance.)

This chart summarizes the differences between memory allocations from the
local heap and the global heap:

                Local Heap       Global Heap
────────────────────────────────────────────────────────────────────────────
Size of block:  Less than 64 KB  Any size
Total memory:   Less than 64 KB  Free global memory
Pointer:        Near             Far

You'll probably find local heap allocations convenient for small,
short-lived blocks of memory and global heap allocations for large,
long-lived blocks.

The memory blocks you allocate can be fixed, moveable, or discardable. You
specify the attributes you want when you allocate the memory block. When you
write polite, well-mannered Windows programs, you'll probably want to use
moveable blocks when allocating global memory, but you can use either fixed
or moveable blocks for local memory. How do you deal with a moveable memory
block in your program? Very carefully.

Lock Your Blocks

At first, it seems impossible for Windows to support moveable memory blocks
in real mode. When a program allocates some local or global memory, Windows
has to give the program a pointer so that the program can access the memory.
If Windows later moves the memory block, then that pointer will be invalid.
It will no longer point to the right place.

How does Windows do it? The catch here is that the memory allocation
functions do not directly return pointers that the program may use. Instead,
these functions return--as you can probably guess by now--handles. WINDOWS.H
defines two data types called LOCALHANDLE and GLOBALHANDLE. These are
defined as type HANDLE (which is a WORD, or unsigned 16-bit short integer).
Before your program can use the allocated memory block, it must pass that
handle back to Windows in another function that locks the memory block and
returns a pointer. When a memory block is locked, Windows will not move it.
The pointer will continue to be valid until you call another function to
unlock the block. After that, Windows is free to move the memory again.

More precisely, the functions that lock a block of memory increment a "lock
count" for the memory block. The unlocking functions decrement the lock
count. When you first allocate moveable memory, the lock count is 0, meaning
that Windows can move it if necessary. When the lock count is positive,
Windows cannot move the block. (A lock count is preferable to a simple flag
that denotes whether a block is locked, because different parts of your
program can independently lock a block, use it, and unlock it. If Windows
used a flag instead of a lock count, the memory block could become unlocked
when another section of the program still needed it.)

When you use moveable memory in your Windows program, you should keep it
locked only when your program has control. You should unlock it before you
leave your window procedure. When you are entirely done with the memory
block, you can then free the block.


A Quick Example

Before going into details, let's look at a quick example to get the feel of
this process. Let's say that your program needs a 48-KB chunk of global
memory during the entire time the program is running. During initialization
(for instance, when processing the WM_CREATE message), the program uses
GlobalAlloc to allocate a 48-KB segment of moveable memory:

hGlobalMemory = GlobalAlloc (GMEM_MOVEABLE, 0xC000L) ;

The value returned from GlobalAlloc is a handle to a 48-KB global memory
segment. Store the handle in a static variable (here called hGlobalMemory).

While processing other messages, you might need to read from or write to
this memory segment. At that time you lock the block of memory using
GlobalLock and save the far pointer returned from that function:

lpGlobalMemory = GlobalLock (hGlobalMemory) ;

You can now use the lpGlobalMemory pointer as a normal far pointer. When you
are finished using the memory or processing the message, unlock the segment
so that Windows can move it around in memory again:

GlobalUnlock (hGlobalMemory) ;

When your program cleans up in preparing to terminate (probably when
processing the WM_DESTROY message), it can free up the memory segment by
using:

GlobalFree (hGlobalMemory) ;

This procedure shows how a polite, well-behaved Windows program uses global
memory. (I've ignored some details for now but will cover them shortly.)
Good Windows programmers keep in mind that their programs share resources
with other Windows programs. Good Windows programmers structure their
programs to allow Windows to move the global segments around in memory if
necessary. Good Windows programmers lock their segments only when they need
to use the memory and unlock the segments when they are done.

An impolite, bad Windows program uses initialization code like this:

lpGlobalMemory = GlobalLock (GlobalAlloc (GMEM_FIXED, 0xC000L)) ;

The GMEM_FIXED flag in GlobalAlloc specifies that the segment is fixed in
memory. Windows can't move it; therefore, the lpGlobalMemory value returned
from GlobalLock will be valid until the segment is freed up. More
convenient, yes. But don't do it.


Global Memory Functions

Now for the details. This is the general syntax of the GlobalAlloc call:

hGlobalMemory = GlobalAlloc (wFlags, dwBytes) ;

The dwBytes parameter is a double word (unsigned long). This value can be
greater than 65,536, but there are special considerations in using global
memory blocks larger than 64 KB. (These will be discussed later.)

The wFlags parameter can be a combination of several identifiers that are
combined with the C bitwise OR operator. You first have a choice of three
identifiers to define the attribute of the allocated memory block:

  ■   GMEM_FIXED--Memory is fixed. (This is the default if wFlags is 0.)

  ■   GMEM_MOVEABLE--Memory is moveable.

  ■   GMEM_DISCARDABLE--Memory is discardable. This option should be used
      only with GMEM_MOVEABLE. I'll discuss later how you can manage
      discardable global memory in your programs.

With any of the above three flags, you can use the GMEM_ZEROINIT flag for
convenience; this flag tells Windows to initialize memory contents to 0.

You can use two more flags to tell Windows what to do if not enough free
memory exists in the global heap. When Windows attempts to allocate the
block requested by GlobalAlloc, it first searches to see if a large enough
free block exists already. If not, Windows begins moving blocks of memory
that are moveable and not currently locked. If that still doesn't generate
enough space, Windows begins discarding blocks that are marked as
discardable and not currently locked, again moving moveable unlocked
segments. You can inhibit this action by using one of two flags:

  ■   GMEM_NOCOMPACT--Windows will neither compact memory nor discard memory
      when attempting to allocate the block.

  ■   GMEM_NODISCARD--Windows will not discard discardable global memory
      when attempting to allocate the block. Windows may still compact
      memory by moving moveable blocks.

If your program implements Dynamic Data Exchange (DDE), you'll need to use
the GMEM_DDESHARE flag to allocate blocks of memory that are shareable among
multiple programs. I'll discuss this in Chapter 17.

WINDOWS.H includes two shorthand flags for the most common global memory
allocations. The flag GHND (which stands for "global handle") is defined as:

GMEM_MOVEABLE | GMEM_ZEROINIT

The flag GPTR ("global pointer") is defined as:

GMEM_FIXED | GMEM_ZEROINIT

The name of this flag seems odd. Why is a fixed global block referred to as
a "global pointer"? The answer is given later in this chapter, in the
section entitled "Memory Allocation Shortcuts."

The hGlobalMemory value returned from GlobalAlloc is a handle to the global
memory block. It is NULL if GlobalAlloc could not allocate the requested
memory. You should definitely check the return value from GlobalAlloc when
allocating global memory.

The function GlobalLock locks the segment in memory by incrementing the lock
count and returns a far pointer to type char. You should have a variable
declared for this pointer:

LPSTR lpGlobalMemory ;
[other program lines]
lpGlobalMemory = GlobalLock (hGlobalMemory) ;

If hGlobalMemory is valid, GlobalLock can return NULL only if you flagged
the memory block with GMEM_DISCARDABLE. The NULL return value indicates that
the block has been discarded.

Because GlobalLock is declared as returning a far pointer to type char, you
should use casting if you need something different:

DWORD FAR *lpdwGlobalMemory ;
[other program lines]
lpdwGlobalMemory = (DWORD FAR *) GlobalLock (hGlobalMemory) ;

The far pointer returned from GlobalLock points to the beginning of a
segment. The offset address is 0. If you need to save pointers to areas
within a moveable block, do not save them as far pointers. These far
pointers may be invalid the next time you lock the segment. Instead, store
an offset from the beginning of the block. For a global block less than 64
KB, for instance, you need save only the offset address (the lower 16 bits)
of the pointer.

The GlobalUnlock function decrements the lock count for the hGlobalMemory
handle:

GlobalUnlock (hGlobalMemory) ;

Calling GlobalUnlock invalidates the lpGlobalMemory pointer returned from
GlobalLock. When the lock count is 0, Windows can move the block in memory.

GlobalLock and GlobalUnlock are fairly fast, so you don't suffer a real
performance penalty if you use the two functions liberally. You should
definitely not keep a block locked from one message to another. Remember
that Windows performs best when memory is moveable. When you make a call to
a Windows function, Windows may need to load code into memory. If you have a
locked memory block sitting around, Windows may have to discard other
segments to make room.

When you are entirely finished with the memory block, you can call:

GlobalFree (hGlobalMemory) ;

Following this call, the hGlobalMemory handle is no longer valid, and the
block is freed.


More Global Memory Functions

Although the four global memory functions shown above are the ones you'll
use most often, Windows also provides several others. Before using
GlobalAlloc, you may want to determine the amount of global memory currently
available:

dwAvailable = GlobalCompact (dwRequested) ;

GlobalCompact causes Windows to move moveable blocks and to calculate the
area of free memory that could be obtained by also discarding discardable
blocks. If the function cannot generate dwRequested bytes, it returns the
largest block of free memory available. Discarding doesn't take place until
you call GlobalAlloc using the size returned from GlobalCompact.

After you allocate a memory block, you can determine its size using:

dwBytes = GlobalSize (hGlobalMemory) ;

You can also change the size of the memory block or change its attributes
using GlobalReAlloc. This function is a little tricky, because it can be
used in one of three ways. Here's the general syntax:

hGlobalMemory = GlobalReAlloc (hGlobalMemory, dwBytes, wFlags) ;

First, you can change the size of a global memory block (either increasing
or decreasing it) using:

GlobalReAlloc (hGlobalMemory, dwNewBytes, wFlags) ;

The data already stored in the block are preserved. The function returns
NULL if it cannot increase the block to the requested size.

The wFlags parameter is used in the same way as the wFlags parameter for
GlobalAlloc: GMEM_NODISCARD and GMEM_NOCOMPACT place restrictions on what
Windows will do to satisfy the allocation request. GMEM_ZEROINIT zeroes out
additional bytes if you are expanding the block. When calling GlobalReAlloc,
you don't have to include the GMEM_FIXED, GMEM_MOVEABLE, or GMEM_DISCARDABLE
flags. Windows preserves the attribute specified when the block was
allocated. However, you may want to use the GMEM_MOVEABLE flag for
reallocating a fixed block. Doing so gives Windows permission to move the
block in memory to satisfy the allocation request. In this case,
GlobalReAlloc returns a new global memory handle to the fixed block:

hGlobalMemoryNew = GlobalReAlloc (hGlobalMemoryOld, dwNewBytes,
                                   GMEM_MOVEABLE) ;

If GlobalReAlloc returns NULL, the request for memory was refused, and the
original hGlobalMemoryOld value passed to GlobalReAlloc is still valid for
the fixed block.

The second way to use GlobalReAlloc is to change the discardable attribute
of moveable blocks. The dwNewBytes value is ignored. You can change a
moveable block to a discardable one:

GlobalReAlloc (hGlobalMemory, 0L, GMEM_MODIFY | GMEM_DISCARDABLE) ;

or change a discardable block to a moveable (but nondiscardable) one:

GlobalReAlloc (hGlobalMemory, 0L, GMEM_MODIFY | GMEM_MOVEABLE) ;

The third use of GlobalReAlloc is to discard a discardable memory block.
This requires dwNewBytes to be 0 and the wFlags parameter to be
GMEM_MOVEABLE:

GlobalReAlloc (hGlobalMemory, 0L, GMEM_MOVEABLE) ;

You can do the same thing using:

GlobalDiscard (hGlobalMemory) ;

In fact, GlobalDiscard is a macro defined in terms of GlobalReAlloc.


Using Discardable Global Memory

Discardable memory segments are generally used for data that can be easily
regenerated. For example, suppose you decide to use a separate file for
"help" text that your program displays when the user requests it. You could
allocate a moveable block, lock it, read some data from the file, display it
on the screen, unlock the block, and free it. However, this approach
requires that your program allocate a new block of memory and access this
file every time help information is requested.

Alternatively, you could keep a moveable block for this file buffer in
memory all the time. When the user requests some help information, you check
to see that the information is already in the buffer, and then you use that
information rather than access the disk again. The performance of your
program improves, but it does so at the cost of having a block of global
memory allocated for the duration of your program.

How about using a discardable block instead? This keeps the buffer in memory
but also gives Windows permission to discard it if necessary. When you lock
the block:

lpGlobalMemory = GlobalLock (hGlobalMemory) ;

the lpGlobalMemory return value will be NULL if the block has been
discarded. In that case, you use GlobalReAlloc to reallocate the segment.
Windows never discards a discardable block when the lock count is nonzero.

If you have obtained a valid far pointer from GlobalLock, that pointer is
valid until you call GlobalUnlock. Even after the block is discarded, the
handle is still valid. (This avoids problems when you pass the handle to
GlobalLock.)

You can also determine that a block is discardable or has been discarded by
using the GlobalFlags function:

wFlags = GlobalFlags (hGlobalMemory) ;

WINDOWS.H has identifiers you can use in combination with wFlags. This value
is nonzero if the block is discardable:

(GMEM_DISCARDABLE & wFlags)

This value is nonzero if the block has been discarded:

(GMEM_DISCARDED & wFlags)

Another approach is to include the GLOBAL_NOTIFY flag when allocating a
discardable segment. In this case, Windows will call a call-back function in
your program that has been registered with the GlobalNotify function when it
is about to discard a discardable segment.


Huge Global Memory Blocks

The dwSize parameter in GlobalAlloc is a 32-bit DWORD (double word), large
enough in theory to allocate a 4-gigabyte block of memory. Although you
obviously won't be able to get a block quite that large, it appears that you
can still allocate a block of memory larger than 64 KB. Yes, you can, but
you have to be careful. Beginning with version 4 of the Microsoft C
Compiler, the huge keyword was implemented for defining variables that are
larger than 64 KB. A huge pointer is 32 bits, just like a far pointer.
However, the Microsoft C Compiler assumes that a far pointer addresses only
a 64-KB range and will never run past the end of the segment. With a huge
pointer, the compiler generates code that checks for segment overrun and
does appropriate segment arithmetic on the pointer.

The phrase "segment arithmetic" should have triggered a bell in your head! I
mentioned earlier that you should not perform segment arithmetic in your
Windows programs because it violates rules of protected mode. Fortunately,
the Microsoft C 6 compiler and Windows work together to perform different
segment arithmetic depending on whether the program is running in real mode
or protected mode. In real mode, jumping from the end of one 64-KB segment
to the beginning of another segment requires adding 0x1000. In protected
mode, the selectors are allocated so that 8 is added for the segment jump.
(Note: Don't rely on this number; it may change under future versions.)

When you use GlobalAlloc to allocate memory greater than 64 KB, you must
cast the pointer returned from GlobalLock into a huge pointer and save it as
a huge pointer. For instance, this code allocates a 128-KB memory block and
locks it:

GLOBALHANDLE hGlobalMemory ;
char huge    *lpGlobalMemory ;
[other program lines]
hGlobalMemory = GlobalAlloc (GMEM_MOVEABLE, 0x20000L) ;
[other program lines]
lpGlobalMemory = (char huge *) GlobalLock (hGlobalMemory) ;

Every function that manipulates this huge pointer must be aware that the
pointer is huge. If a function that is passed a huge pointer believes that
the pointer is a simple far pointer, the Microsoft C Compiler will not
generate any segment arithmetic when you manipulate the pointer. For this
reason, you should not pass a huge pointer to most of the standard C library
functions (the C 6 manuals list functions that support huge arrays) or to
any of the Windows functions unless you know that the function will not be
referencing the pointer past the end of a segment.

That's one problem with huge pointers. Another problem is the possibility
that a single data item referenced by the pointer may straddle two segments.
With a huge pointer to character data, this is never a problem, because each
character is a byte. The offset address that GlobalLock returns is always 0,
so the huge pointer can also safely reference arrays of all the standard
data types (char, int, short, long, float, and double).

If you use a huge pointer to an array of structures, you will have no
problems if the size of the structure is a power of 2 (such as 2, 4, 8, 16,
and so forth). That guarantees that no single structure will straddle two
segments. If the size of the structure is not a power of 2, then you are
bound by two restrictions:

  ■   The data block allocated with GlobalAlloc cannot be larger than 128
      KB.

  ■   The offset address returned from GlobalLock must be adjusted so that a
      structure does not straddle two segments.

The first rule is actually implied by the second rule. If the initial offset
address is adjusted so that an element of the structure does not straddle
the first and second segments, it will straddle the second and third
segments.

This explanation requires an example. Let's say you want a huge memory block
to hold an array of 15,000 structures, where each structure requires 6
bytes. You can use typedef statements for this structure and a far pointer
to the structure:

#typedef struct
          {
          int  element1 ;
          long element2 ;
          }
          MYSTRUCT ;

#typedef MYSTRUCT huge *LPMYSTRUCT ;

In your program you can define a variable for the far pointer to the
structure:

GLOBALHANDLE   hGlobalMemory ;
LPMYSTRUCT     lpMyStruct ;
[other program lines]
hGlobalMemory = GlobalAlloc (GHND, 15001 * sizeof (MYSTRUCT)) ;

lpMyStruct = (LPMYSTRUCT) ((65536L % sizeof (MYSTRUCT)) +
                       GlobalLock (hMem)) ;

The pointer returned from GlobalLock will have an offset address of 0. You
must increase that so that a single structure does not straddle the two
segments. The adjustment value is the remainder of 65,536 divided by the
size of the structure. (In this case, the adjustment value is 4.) Because
you have a little waste here, GlobalAlloc allocates one more structure than
is really needed.


Allocating Local Memory

I've been stressing the importance of using moveable (and, if possible,
discardable) global memory blocks. With local memory you have the same
options, but the guidelines are more relaxed. Whether you use fixed or
moveable memory blocks within your local heap is up to you. Because your
entire data segment is moveable (as it will be if you use the small or
medium model), what you do inside your data segment doesn't affect other
applications.

In fact, Windows makes it easier to use local memory if the blocks are
fixed. The question to ask is: Can my local heap be smaller if I use
moveable blocks instead of fixed blocks? If you use local memory a lot, and
the life spans of the memory blocks overlap each other, then the answer to
that question may be yes. If you use local memory allocations solely for
short-lived memory, there's no reason to make the blocks moveable.

The local memory functions are similar to the global memory functions.
Instead of GlobalAlloc, GlobalLock, GlobalUnlock, and GlobalFree, you use
LocalAlloc, LocalLock, LocalUnlock, and LocalFree. Instead of identifiers
that begin with GMEM, you use identifiers that begin with LMEM. The only
real differences are these: The memory size passed to LocalAlloc is a WORD
(unsigned integer) rather than a DWORD, and the pointer returned from
LocalLock is a near pointer rather than a far pointer.

This is the syntax of LocalAlloc:

hLocalMemory = LocalAlloc (wFlags, wSize) ;

The wSize parameter is large enough to accommodate a requested size of
65,536 bytes, but you won't get a local block that large, because the data
segment also includes your program's stack and static variables.

The wFlags parameter can first specify the attributes of the block:

  ■   LMEM_FIXED--Memory is fixed. (This is the default if wFlags is 0.)

  ■   LMEM_MOVEABLE--Memory is moveable.

  ■   LMEM_DISCARDABLE--Memory is discardable. This option should be used
      only with LMEM_MOVEABLE.

The LMEM_ZEROINIT flag zeroes out the memory block.

These two flags are equivalent to the similar flags for GlobalAlloc:

  ■   LMEM_NOCOMPACT--Windows will neither compact nor discard memory in the
      local heap when attempting to allocate the block.

  ■   LMEM_NODISCARD--Windows will not discard discardable memory in the
      local heap when attempting to allocate the block. Windows may still
      compact memory by moving moveable blocks.

WINDOWS.H also includes two shorthand flags for local memory allocations.
The flag LHND (which stands for "local handle") is defined as:

LMEM_MOVEABLE | LMEM_ZEROINIT

The flag LPTR ("local pointer") is defined as:

LMEM_FIXED | LMEM_ZEROINIT

If Windows cannot find enough memory in the local heap to allocate the
block, it will attempt to expand the local heap by enlarging the size of the
entire data segment. (Remember that the local heap is always at the top of
the automatic data segment.) Windows may even move the data segment to
another location in memory if that will provide the space it needs to expand
the local heap. When LocalAlloc returns, your data segment may have been
moved. (If this makes you nervous, check the section below entitled "Locking
Your Own Data Segment.") The HEAPSIZE specification in the module definition
(.DEF) file is really a minimum value for the heap.

If, after all this, Windows still cannot find enough memory in the local
heap to allocate the memory block, the handle returned from LocalAlloc will
be NULL. If you use local memory allocation only for small, short-lived
memory blocks, you probably don't need to check the handle for a NULL value.
(Alternatively, you might want to check the value during program development
but not in the final version of the program.) If you do a lot of random
local memory allocation with blocks of various sizes and different life
spans, then you'll have to implement some kind of error processing.

LocalLock turns the local memory handle into a near pointer and then locks
the block. LocalUnlock unlocks the block and invalidates the pointer.
LocalFree frees the memory block and invalidates the handle.

Here's an example of using local memory to define the window class structure
during program initialization:

LOCALHANDLE    hLocalMemory ;
NPWNDCLASS     npwndclass ;
[other program lines]
if (!hPrevInstance)
     {
     hLocalMemory = LocalAlloc (LHND, sizeof (WNDCLASS)) ;
     npwndclass = (NPWNDCLASS) LocalLock (hLocalMemory) ;

     npwndclass->style         = CS_HREDRAW | CS_VREDRAW ;
     npwndclass->lpfnWndProc   = WndProc ;
     npwndclass->cbClsExtra    = 0 ;
     npwndclass->cbWndExtra    = 0 ;
     npwndclass->hInstance     = hInstance ;
     npwndclass->hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
     npwndclass->hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     npwndclass->hbrBackground = GetStockObject (WHITE_BRUSH) ;
     npwndclass->lpszMenuName  = NULL ;
     npwndclass->lpszClassName = szAppName ;

     RegisterClass (npwndclass) ;

     LocalUnlock (hLocalMemory) ;
     LocalFree (hLocalMemory) ;
     }

The size of the memory block passed to LocalAlloc is the size of the
WNDCLASS structure. LocalLock always returns a near pointer regardless of
the memory model, because it allocates memory from the local heap in the
program's automatic data segment. In this example, the pointer to type char
that LocalLock returns is cast into a pointer to a WNDCLASS structure. The
-> notation is used to reference the elements of a structure based on a
pointer to the structure. In the RegisterClass call, we don't use the
address (&) operator because npwndclass is already a pointer. Note also that
the use of LHND initializes the block of memory to 0. All variables in the
structure that take a 0 or NULL value need not be explicitly assigned.


Other Local Memory Functions

Some other local memory functions parallel those for global memory
allocations, except that sizes are in terms of wBytes rather than dwBytes.
You can get the current size of a local memory block by calling:

wBytes = LocalSize (hLocalMemory) ;

The function LocalReAlloc can change the size of an allocated memory block,
change a moveable block to discardable, change a discardable block to
nondiscardable, and discard a discardable block, just like GlobalReAlloc.
During a LocalReAlloc call, Windows may expand the size of the local heap by
expanding the size of the entire data segment, possibly moving it to another
location in memory. LocalCompact can determine the amount of free local
memory available in the heap, LocalDiscard discards a discardable memory
block, and LocalFlags provides the current status of discardable blocks.

Two other local memory functions do not have counterparts in the global
memory functions. You can prevent your local heap from being compacted by
calling:

LocalFreeze (0) ;

When you later want to allow compacting, you can call:

LocalMelt (0) ;


Locking Your Own Data Segment

Now that you are thoroughly paranoid about locking and unlocking memory
blocks, you may start to wonder about the automatic data segment of the
program itself. When the program begins executing, your automatic data
segment has a lock count of 1, and the data segment cannot be moved in
memory. Windows decrements that lock count when the program makes one of the
following calls: GetMessage, PeekMessage, WaitMessage, LocalAlloc, or
LocalReAlloc.

The GetMessage, PeekMessage, and WaitMessage calls can cause a switch from
your program to another program. When your program gets control again, your
data segment may have been moved. A LocalAlloc or LocalReAlloc call can
cause Windows to expand the size of your local heap, in the process moving
the data segment to another location in memory. Windows increments the lock
count when it returns from these calls to your program. So in most cases,
your program's data segment is locked when your program has control. This
means that you can construct (through casting) far pointers to data in your
data segment, and they will remain valid until you make one of these five
calls or exit the window procedure.

If you want to prevent the movement of your data segment during a LocalAlloc
or LocalReAlloc call, you can increase the lock count by 1 before calling
the function:

LockData (0) ;

Following the LockData call, the lock count of your data segment will be 2.
When Windows decrements the count during a LocalAlloc or LocalReAlloc call,
it will still be positive and your data segment will still be locked. You
can decrement the lock count by calling:

UnlockData (0) ;

If you're brave, you might also want to take the opposite approach. You
might be willing to keep your data segment unlocked while making Windows
calls. You would do this by calling UnlockData to decrement the lock count
to 0 and then LockData to increment it to 1 again before exiting the window
procedure. When your data segment is unlocked, Windows has more flexibility
in moving segments in memory because Windows can move your data segment as
well. However, when your data segment is unlocked, you should not make any
Windows calls that require a far pointer to your data segment. Depending on
what happens during that call, the pointer may become invalid by the time
Windows needs to use it.


Memory Allocation Shortcuts

We have been treating handles as abstract numbers. But sometimes handles are
actually pointers. If you use LMEM_FIXED in LocalAlloc, the handle returned
from the call is a near pointer that you can use directly. You do not need
to call LocalLock. (If you do, it simply returns the same value you pass as
a parameter--the handle that is actually a valid near pointer.) In fact,
WINDOWS.H defines a special flag for calling LocalAlloc with an LMEM_FIXED
parameter. The flag is LPTR ("local pointer") and is defined as:

LMEM_FIXED | LMEM_ZEROINIT

When you use this flag, you need only cast the return value from LocalAlloc
into a near pointer of the type you want:

npString = (char NEAR *) LocalAlloc (LPTR, wSize) ;

You free it up by casting the near pointer back to a handle and calling
LocalFree:

LocalFree ((LOCALHANDLE) npString) ;

This technique is handy for allocating small chunks of local memory. For
instance, here's the example shown above for allocating memory for the
window class structure. It now uses a fixed block of local memory:

NPWNDCLASS     npwndclass ;
[other program lines]
if (!hPrevInstance)
     {
     npwndclass = (NPWNDCLASS) LocalAlloc (LPTR, sizeof (WNDCLASS)) ;

     npwndclass->style         = CS_HREDRAW | CS_VREDRAW ;
     npwndclass->lpfnWndProc   = WndProc ;
     npwndclass->cbClsExtra    = 0 ;

     npwndclass->cbWndExtra    = 0 ;
     npwndclass->hInstance     = hInstance ;
     npwndclass->hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
     npwndclass->hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     npwndclass->hbrBackground = GetStockObject (WHITE_BRUSH) ;
     npwndclass->lpszMenuName  = NULL ;
     npwndclass->lpszClassName = szAppName ;

     RegisterClass (npwndclass) ;

     LocalFree ((LOCALHANDLE) npwndclass) ;
     }

We've eliminated two lines of code (LocalLock and LocalUnlock) and one
variable (the local memory handle). Note the casting in the LocalAlloc and
LocalFree calls.

The same technique is even applicable for GlobalAlloc when the GMEM_FIXED
(or GPTR) flag is used. The "handle" returned from GlobalAlloc is the
segment address of the segment. It's a little more clumsy to convert that
into a far pointer, but here's how to do it:

lpString = (LPSTR) MAKELONG (0, GlobalAlloc (GPTR, dwSize)) ;

The MAKELONG macro combines the segment address returned from GlobalAlloc
and an offset address of 0 to make a long integer, which is then cast into a
long pointer to type char. To free this block, you have to extract the
segment address from the pointer and pass it to GlobalFree:

GlobalFree ((GLOBALHANDLE) LOWORD ((LONG) lpString))) ;

I don't like the memory allocation shortcuts, and I don't think they should
be used. I've included them only because many sample Windows programs use
them, particularly when allocating local memory.


Using C Memory Allocation Functions

The start-up code that LINK attaches to C programs running under Windows
contains functions for many of the memory allocation functions in Microsoft
C 6, such as calloc, malloc, realloc, and free. The routines in the start-up
code convert the normal C memory allocation functions into equivalent
Windows memory allocation functions. For instance, the function:

malloc (wSize) ;

is translated into:

LocalAlloc (LMEM_FIXED | LMEM_NOCOMPACT, min (1, wSize)) ;

These functions are included in the Windows start-up code not for your
benefit, but because several other C functions from the standard library
make calls to these C memory allocation functions. These other C functions
cannot work properly without using the  Windows memory allocation calls.
Although it's not intended that you use these functions, you can use them.
Be aware, however, that in compact-memory and large-memory models (which you
shouldn't be using for Windows programs anyway), the Windows malloc returns
a far pointer to your program's automatic data segment--as opposed to the
non-Windows malloc, which returns a far pointer to a block of memory outside
the automatic data segment. Also be aware that _fmalloc and halloc are
translated into GlobalAlloc calls with a flag that is equal to (GMEM_FIXED |
GMEM_NODISCARD), and as you know, you should not use fixed global memory
blocks. Moreover, the pointer returned from the Windows halloc is not
properly aligned for an array of elements that are not multiples of 2, and
the memory is not initialized to 0. The point is clear: Unless you feel
deprived doing C programming without malloc, use the Windows functions for
all memory allocations.


If You Know You're Running in Protected Mode

In this chapter, I have tried to present guidelines for memory management
that are valid in all modes in which Windows 3 can run. However, if your
program is so large that it can (realistically) run only in protected mode,
then you can simplify your memory management. Such a program should use
GetWinFlags and terminate if it's running in real mode.

When allocating moveable global memory, you can lock the memory block
immediately to obtain the pointer. Even though the block is locked, Windows
can still move it in memory--and the pointer will remain valid. You don't
even have to save the global memory handle. To free the block, you can use
GlobalHandle to obtain the handle from the pointer. Then unlock the block
and free it as normal.

You can compile with the -G2 flag to generate 286 code.





Chapter 8  Icons, Cursors, Bitmaps, and Strings
────────────────────────────────────────────────────────────────────────────

Most Windows programs include a customized icon that Windows displays when
the program is minimized. Some programs (such as the Windows PAINTBRUSH
program) also use customized cursors to represent different operations of
the program. Most Windows programs also use menus and dialog boxes.

Icons, cursors, menus, and dialog boxes are all examples of "resources."
Resources are data and are included in a program's .EXE file, but they do
not reside in a program's normal data segment. When Windows loads a program
into memory for execution, it usually leaves the resources on disk. Only
when Windows needs a particular resource does it load the resource into
memory. (You've probably noticed dynamic loading of resources when working
with Windows programs. When you invoke a program's dialog box for the first
time, Windows usually accesses the disk to copy the dialog box resource from
the program's .EXE file into memory.)

Most resources are read-only data and are marked as discardable. When
Windows needs more memory, segments occupied by discardable resources can be
freed up. If the

resource is required again later, Windows reloads it from the .EXE file.
Just as multiple instances of the same program share the same code, multiple
instances also usually share resources. I'll be discussing these resources:

  ■   Icons

  ■   Cursors

  ■   Bitmaps

  ■   Character strings

  ■   User-defined resources

  ■   Menus

  ■   Keyboard accelerators

  ■   Dialog boxes

  ■   Fonts

The first five resources in the list are discussed in this chapter. Menus
and keyboard accelerators are covered in Chapter 9, dialog boxes in Chapter
10, and fonts in Chapter 14.

COMPILING RESOURCES

During program development, resources are defined in a "resource script,"
which is an ASCII text file with the extension .RC. The resource script can
contain ASCII representations of resources and can also reference other
files (either ASCII or binary files) that contain resources. The resource
compiler (RC.EXE) compiles the resource script into a binary form, adds the
resources to the end of the .EXE file that LINK generates, and creates a
"resource table" in the .EXE header.

You can use the resource compiler included in the Windows Software
Development Kit in one of three ways:

  ■   You can compile resources and add them to the linked .EXE file in one
      step by executing the command:

      RC filename

      where filename.RC is the name of the resource script (the .RC
      extension is assumed) and filename.EXE is the name of the linked .EXE
      file. You can also use:

      RC resource-name exe-name

      if the name of your .RC resource script and the name of your .EXE
      executable are different. (This is usually not the case.)

  ■   You can compile a .RC resource script into a binary compiled form with
      the extension .RES by executing:

      RC -r filename

      This uses the ASCII filename.RC file to create a binary file called
      filename.RES. You can then add the resources to the linked file by
      executing:

      RC filename.RES

      The .RES extension is required here to differentiate this command from
      the command shown earlier that both compiles the resource script and
      adds the resources to the .EXE file.

  ■   If your program has no resources, you should run rc.exe on the linked
      file:

      RC filename.EXE

      This flags the program as being "Windows 3 aware."

      This second method is the one most commonly used when the resource
      script contains resources. Although it requires that the RC.EXE
      resource compiler be run twice_once to compile the resource script and
      again to add the resources to the .EXE file--it actually results in a
      faster edit-make-run cycle when developing Windows programs. The
      reason is that compiling the resources generally takes much longer
      than adding them to the .EXE file. During program development, you
      will probably modify your C source code much more frequently than the
      resource script, so you have no need to recompile the resources each
      time.

      The procedure of compiling resources is reflected in a different make
      file. Up until now we have been using a make file that looks something
      like this:

      progname.exe : progname.obj progname.def
           link progname, /align:16, NUL, /nod slibcew libw, progname
           rc progname.exe

      progname.obj : progname.c
           cl -c -Gsw -Ow -W2 -Zp progname.c

      When we start using resources, we'll use an expanded make file that
      looks like this:

      progname.exe : progname.obj progname.def progname.res
           link progname, /align:16, NUL, /nod slibcew libw, progname
           rc progname.res

      progname.obj : progname.c [progname.h]
           cl -c -Gsw -Ow -W2 -Zp progname.c

      progname.res : progname.rc [progname.h] [and other files]
           rc -r progname.rc

      In the second and third sections I've indicated that a .H header file
      can be used in both the C source code and the resource script. This
      header file usually defines identifiers used by the program to
      reference resources. I've also indicated in the third section that the
      depen- dent file list possibly includes "other files." These are files
      referenced from within the resource script. Generally they are binary
      files that contain icons, cursors, or bitmaps.

      The RC.EXE resource compiler uses a preprocessor called RCPP.EXE. This
      preprocessor folds added or subtracted constants, recognizes /* and */
      as comment delimiters, and recognizes the C preprocessor directives
      #define, #undef, #ifdef, #ifndef, #include, #if, #elif, #else, and
      #endif. The #include directive works a little differently than in
      normal C programs. We'll examine this in greater detail in Chapter 10.

      In the first section of the make file, the .OBJ and .RES files are
      dependent files for the .EXE target. NMAKE checks the rest of the make
      file to determine if these dependent files must be updated. The second
      section compiles the C source code as usual. The third section
      compiles the .RC resource script into a binary .RES file.

      The first section is then executed if either the .OBJ, .DEF, or .RES
      file has changed since the last .EXE file was created. This section
      links the program as usual and runs .RC again to add the resources to
      the .EXE file. If you change only the .RC resource script file, you
      still need to relink to produce a new .EXE file without the previous
      resources. The resource compiler cannot remove old resources from a
      .EXE file when adding new ones.


ICONS AND CURSORS

Let's begin by looking at a sample program that uses two resources--an icon
and a cursor. RESOURC1, shown in Figure 8-1, displays a customized icon when
the program is minimized and uses a customized cursor when the mouse is in
RESOURC1's client area. RESOURC1 also draws its icon in several rows and
columns within the client area.

 RESOURC1.MAK

#------------------------
# RESOURC1.MAK make file
#------------------------

resourc1.exe : resourc1.obj resourc1.def resourc1.res
     link resourc1, /align:16, NUL, /nod slibcew libw, resourc1
     rc resourc1.res

resourc1.obj : resourc1.c
     cl -c -Gsw -Ow -W2 -Zp resourc1.c

resourc1.res : resourc1.rc resourc1.ico resourc1.cur
     rc -r resourc1.rc

 RESOURC1.C

/*-----------------------------------------------------------
   RESOURC1.C -- Icon and Cursor Demonstration Program No. 1
                 (c) Charles Petzold, 1990
  -----------------------------------------------------------*/

#include <windows.h>

long FAR PASCAL WndProc  (HWND, WORD, WORD, LONG) ;

char   szAppName [] = "Resourc1" ;
HANDLE hInst ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     HWND     hwnd ;
     MSG      msg ;
     WNDCLASS wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (hInstance, szAppName) ;
          wndclass.hCursor       = LoadCursor (hInstance, szAppName) ;
          wndclass.hbrBackground = COLOR_WINDOW + 1 ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hInst = hInstance ;

     hwnd = CreateWindow (szAppName, "Icon and Cursor Demo",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;
     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static HICON hIcon ;
     static short cxIcon, cyIcon, cxClient, cyClient ;
     HDC          hdc ;
     RECT         rect ;
     PAINTSTRUCT  ps ;
     short        x, y ;

     switch (message)
          {
          case WM_CREATE :
               hIcon = LoadIcon (hInst, szAppName) ;
               cxIcon = GetSystemMetrics (SM_CXICON) ;
               cyIcon = GetSystemMetrics (SM_CYICON) ;
               return 0 ;

          case WM_SIZE :
               cxClient = LOWORD (lParam) ;
               cyClient = HIWORD (lParam) ;
               return 0 ;

          case WM_PAINT :
               hdc = BeginPaint (hwnd, &ps) ;

               for (y = cyIcon ; y < cyClient ; y += 2 * cyIcon)
                    for (x = cxIcon ; x < cxClient ; x += 2 * cxIcon)
                         DrawIcon (hdc, x, y, hIcon) ;

               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 RESOURC1.RC

/*-----------------------------
   RESOURC1.RC resource script
  -----------------------------*/

resourc1  ICON    resourc1.ico
resourc1  CURSOR  resourc1.cur

 RESOURC1.ICO  -- Please refer to the book.

 RESOURC1.CUR  -- Please refer to the book.

 RESOURC1.DEF

;-------------------------------------
; RESOURC1.DEF module definition file
;-------------------------------------

NAME           RESOURC1
DESCRIPTION    'Icon and Cursor Demo Program No. 1 (c) Charles Petzold,
1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

Both the icon and the cursor were created using the SDKPAINT program
supplied with the Windows Software Development Kit. They are shown in Figure
8-1 against a light gray background. SDKPAINT is a Windows application, and
it requires a mouse. Icons are saved from SDKPAINT with a .ICO extension;
cursors have a .CUR extension. These files are referred to in the
RESOURC1.RC resource script.

The SDKPAINT Tool

SDKPAINT is one of the most important development tools in the Windows
Software Development Kit. The program allows you to create bitmaps, icons,
and cursors for use in your Windows programs. Icons and cursors are both
variations of bitmaps, so it will be helpful to examine bitmaps first.

A bitmap is an array of bits where one or more bits corresponds to each
display pixel. In a monochrome bitmap, one bit corresponds to one pixel. (In
the simplest case, a 1 bit represents white and a 0 bit represents black.
However, bitmaps are often used in logical operations rather than merely to
create simple drawings.) In a color bitmap, multiple bits correspond to each
pixel to represent color. SDKPAINT supports the creation of monochrome
bitmaps and 16-color bitmaps. In a 16-color bitmap, 4 bits are required for
each pixel.

A bitmap may have any number of rows and columns. (However, the bitmaps you
create in SDKPAINT are limited to 72 rows and 72 columns. You can create
larger bitmaps in PAINTBRUSH.) Bitmaps are stored in files with a .BMP
extension. (I'll discuss the format of the bitmap file in Chapter 13.)

You can also create icons and cursors in SDKPAINT. Icons and cursors are
very similar, and they are both variations of bitmaps.

Windows displays icons and cursors on the screen in a pixel size that
depends on the resolution of the video display. This ensures that the icons
and cursors are neither too large nor too small. A program can obtain these
pixel dimensions using the GetSystemMetrics function with parameters of
SM_CXICON, SM_CYICON, SM_CXCURSOR, and SM_CYCURSOR. On most video displays,
the dimensions of icons and cursors are identical. To keep it simple in the
following discussion, I'll refer only to icons, but keep in mind that
everything I say applies to cursors also.

On an IBM Color Graphics Adapter (CGA), the width of an icon is 32 pixels
and the height is 16 pixels. On an Enhanced Graphics Adapter (EGA), Video
Graphics Array (VGA), and the IBM 8514/A video adapter, the icons are 32
pixels wide and 32 pixels high. For higher-resolution adapters, icons could
be displayed as 64 pixels by 64 pixels.

Each .ICO file can contain multiple icon images, each one designed for
particular resolutions and color capabilities of the various video adapters
on which your Windows program can run. SDKPAINT supports four different
image formats. When you create a new icon file (by selecting New from
SDKPAINT's File menu), you select one of these four formats. After creating
an icon in this format, you can then select another of the four formats from
the New option on the Image menu. These four formats are:

  ■   32 pixels by 16 pixels with 2 colors (monochrome)

  ■   32 pixels by 32 pixels with 2 colors (monochrome)

  ■   32 pixels by 32 pixels with 8 colors

  ■   32 pixels by 32 pixels with 16 colors

The first format is for the CGA, and the second is for other video adapters
(EGA, VGA, and 8514/A) running in a monochrome mode. The third and fourth
are for non-CGA adapters running in color modes. The 8-color format is of
limited use: SDKPAINT actually uses a 16-color format internally and when
saving the image to the file, but allows you to color it with only 8 colors.
The EGA, VGA, and 8514/A are all capable of 16 colors.

You don't need to create icon images in all four formats. When a program
contains an icon resource, Windows will choose the format that most closely
approximates the size and color capabilities appropriate to the video
adapter. For example, if you create only 32-by-32-pixel icons and your
program is run on a CGA, Windows will display the icon using every other row
of pixels, effectively compressing the height of the icon.

If you create only a 32-by-32 icon with 16 colors, use color sparingly
because the colors can be approximated only with gray shades (or converted
to black or white) when running with a monochrome display. All the icons and
cursors in the programs in this chapter were created in the 32-by-32
monochrome format.

When you create an icon image in one of the four formats, SDKPAINT actually
stores it as two bitmaps--a monochrome bitmap "mask" and a monochrome or
color bitmap image. Icons are always rectangular, but this mask allows the
icon to appear to be nonrectangular. That is, part of the icon allows the
background against which the icon is displayed to be visible. The icon can
also contain areas that invert the background color.

These two options are indicated in SDKPAINT by radio buttons labeled
"Screen" and "Inverse." After selecting "Screen," anything you draw in the
icon will be transparent, and after selecting "Inverse," anything you draw
in the icon will invert the background. You can select different background
colors to see how this looks. The icons and cursor in Figure 8-1 are shown
against a light gray background. The light gray areas were colored using the
"Screen" option, and the dark gray areas were colored using the "Inverse"
option.

For a monochrome icon, the following table shows how SDKPAINT constructs the
two bitmaps that describe the icon:

Color:         Black  White  Screen  Inverse Screen
────────────────────────────────────────────────────────────────────────────
Mask Bitmap:   0      0      1       1
Image Bitmap:  0      1      0       1

When displaying the icon, Windows first uses a bitwise AND operation of the
display and the first bitmap. The display pixels corresponding to 0 bits
from the first bitmap all become 0's, which are black. The display pixels
corresponding to 1 bit remain the same. This is shown in the following logic
table.

          Display Pixel
Mask Bit  0              1
────────────────────────────────────────────────────────────────────────────
0         0              0
1         0              1

Next, Windows performs a bitwise exclusive OR operation of the image bitmap
and the display. A 0 in the second bitmap leaves the display pixel the same;
a 1 in the second bitmap inverts the display pixel. Here's the logic table:

           Display Pixel
Image Bit  0              1
────────────────────────────────────────────────────────────────────────────
0          0              1
1          1              0

Using C notation for the operations, the display is altered by the following
formula:

  Display = (Display & Mask) ^ Image

For a 16-color icon, the mask bitmap is still monochrome and constructed as
shown above. The image bitmap contains 4 bits per pixel to represent 16
colors. All four bits are set to 1 for areas of the icon that invert the
background.

Earlier I said that when talking about bitmaps, 0 does not necessarily mean
black, and 1 does not necessarily mean white. As you can see here, it
depends on how Windows uses the bitmaps. (I'll discuss this more in Chapter
13.)

In RESOURC1, I've defined the window class to make the background of the
client area be COLOR_WINDOW. You may want to bring up the Windows Control
Panel program and change the window color to see how the icon and cursor
invert colors.


Getting a Handle on Icons

A resource script references the icon file with a statement that looks like
this:

myicon ICON iconfile.ico

where ICONFILE.ICO is the name of the icon file. This statement assigns the
name  "myicon" to the icon. In your C program, you use the LoadIcon function
to obtain a handle to the icon. LoadIcon requires two parameters. The first
is the instance handle of your program, generally called hInstance in
WinMain. Windows needs this handle to determine which .EXE file contains the
icon resource. The second parameter is the icon name from the resource
script, in the form of a pointer to a null-terminated string. LoadIcon
returns a value of type HICON, which is defined in WINDOWS.H.

This diagram shows the relationship between the icon name in the resource
script and the LoadIcon statement in your C program:

────────────────────────────────────────────────────────────────────────────
Resource script:      myicon ICON iconfile.ico
Program source:       hIcon = LoadIcon (hInstance, "myicon") ;

Don't worry about uppercase and lowercase here. The resource compiler
converts the name in the resource script file to uppercase and inserts the
name in the resource table of the program's .EXE file header. The first time
you call LoadIcon, Windows converts the string from the second parameter to
uppercase and searches the resource table of the .EXE file for a matching
name.

You can speed up this search by using a number (an unsigned integer) instead
of a name. This number is called an ID number for the icon. Here's how it's
done:

────────────────────────────────────────────────────────────────────────────
Resource script:  125 ICON iconfile.ico
Program source:   hIcon = LoadIcon (hInstance, MAKEINTRESOURCE (125)) ;

MAKEINTRESOURCE ("make an integer into a resource string") is a macro
defined in WINDOWS.H that converts a number into a far pointer. The offset
address is set to the number, and the segment address is set to 0. Here's
how MAKINTRESOURCE is defined in WINDOWS.H:

#define MAKEINTRESOURCE(i) (LPSTR)((DWORD)((WORD)(i)))

Windows knows that the second parameter is a number rather than a pointer to
a character string because the segment address is 0.

Sample programs presented earlier in this book use predefined icons:

LoadIcon (NULL, IDI_APPLICATION) ;

Windows knows that this is a predefined icon because the hInstance parameter
is set to NULL. IDI_APPLICATION happens also to be defined in WINDOWS.H in
terms of MAKEINTRESOURCE:

#define IDI_APPLICATION MAKEINTRESOURCE(32512)

The predefined icons and cursors are part of the display driver file.

You can also reference the icon name using a third method that combines the
string method and the number method:

────────────────────────────────────────────────────────────────────────────
Resource script:       125 ICON iconfile.ico
Program source:        hIcon = LoadIcon (hInstance, "#125") ;

Windows recognizes the initial # character as prefacing a number in ASCII
form.

How about a fourth method? This one uses a macro definition in a header file
that is included (using the #include directive) in both the resource script
and your program:

────────────────────────────────────────────────────────────────────────────
Header file:      #define myicon 125
Resource script:  myicon ICON iconfile.ico
Program source:   hIcon = LoadIcon (hInstance, MAKEINTRESOURCE (myicon)) ;

Be careful when you use this method! Although case does not matter when the
icon name is a character string, case does make a difference for identifiers
that are generated from #define statements.

Using ID numbers rather than names for icons reduces the .EXE file size and
speeds up the LoadIcon call. Moreover, if your program uses many icons,
you'll find it easier to store the ID numbers in an array.


Using Icons in Your Program

Icons have only a couple of purposes. Most Windows programs use an icon only
for displaying the program in the icon area. This is accomplished when
defining the window class:

wndclass.hIcon = LoadIcon (hInstance, "myicon") ;

If you later want to change the program's icon, you can do so using
SetClassWord. Let's assume you had a second icon in your resource script:

anothericon ICON iconfil2.ico

You can substitute this icon for "myicon" with the statement:

SetClassWord (hwnd, GCW_HICON, LoadIcon (hInstance,"anothericon")) ;

If you save the icon handle from a LoadIcon call, you can also draw the icon
on the client area of your window:

DrawIcon (hdc, x, y, hIcon) ;

Windows itself uses the DrawIcon function when displaying your program's
icon in the icon area. Windows obtains the handle to the icon from the
window class structure. You can obtain the handle in the same way:

DrawIcon (hdc, x, y, GetClassWord (hwnd, GGW_HICON)) ;

The RESOURC1 sample program uses the same icon for the window class and for
displaying in its client area. In the resource script the icon is given the
same name as the program:

resourc1  ICON  resourc1.ico

Because the character string "Resourc1" is stored in the array szAppName and
is already used in the program for the window class name, the LoadIcon call
is simply:

LoadIcon (hInstance, szAppName) ;

You'll notice that LoadIcon is called twice in RESOURC1 for the same icon,
once when defining the window class in WinMain and again when obtaining a
handle to the icon while processing the WM_CREATE message in WndProc.
Calling LoadIcon twice presents no problem: Both calls return the same
handle. Windows actually loads the icon only once from the .EXE file and
then uses it for all instances of the RESOURC1 program.


Using Alternate Cursors

The statements that you use to specify a cursor in your resource script and
to obtain a handle to a cursor in your program are very similar to the icon
statements shown above:

────────────────────────────────────────────────────────────────────────────
Resource script:    mycursor CURSOR cursfile.cur
Program source:     hCursor = LoadCursor (hInstance, "mycursor") ;

The other methods shown for icons (using ID numbers and MAKEINTRESOURCE)
work with cursors also. WINDOWS.H includes a typedef definition for HCURSOR
that you can use for storing the cursor handle. (Both HICON and HCURSOR are
defined as HANDLE.)

You can use the cursor handle obtained from LoadCursor when setting the
hCursor member of the window class structure:

wndclass.hCursor = LoadCursor (hInstance, "mycursor") ;

This causes the mouse cursor to be displayed as your customized cursor when
the mouse is within the client area of your window.

If you use child windows, you may want the cursor to appear differently,
depending on the child window below the cursor. If your program defines the
window class for these child windows, you can use different cursors for each
class by appropriately setting the hCursor field in each window class. And
if you use predefined child window controls, you can alter the hCursor field
of the window class using:

SetClassWord (hwndChild, GCW_HCURSOR,
               LoadCursor (hInstance, "childcursor") ;

If you separate your client area into smaller logical areas without using
child windows, you can use SetCursor to change the mouse cursor:

SetCursor (hCursor) ;

You should call SetCursor during processing of the WM_MOUSEMOVE message.
Otherwise, Windows uses the cursor specified in the window class to redraw
the cursor when it is moved.

RESOURC1 uses the name of the program for the name of the cursor:

resourc1  CURSOR  resourc1.cur

When RESOURC1.C defines the window class, this szAppName variable is used
for LoadCursor:

wndclass.hCursor = LoadCursor (hInstance, szAppName) ;



RESOURCES AND MEMORY

The LoadIcon and LoadCursor functions certainly sound as if they load the
icon or cursor from the .EXE file into memory. They do not. Windows doesn't
load the icon or cursor until it needs the object for drawing. During
loading, Windows may alter the object to fit the dimensions and color
capabilities of the display.

Icons and cursors (as well as all other resources except bitmaps) are
"owned" by the program. Multiple instances of the same program share the
same cursors and icons loaded into memory. When the last instance
terminates, Windows frees up the memory occupied by the resource. And for
most resources, Windows can discard the resource from memory to generate
free space and then load it back into memory when needed.

You can override some of these characteristics, however. For all resources
except the keyboard accelerators (covered in Chapter 9), you can specify
"load" and "memory" options in the resource script file. These options are
similar to the module definition file options for code and data segments
discussed in Chapter 7. In the resource script, the load and memory options
follow the resource type. This is the generalized form of the ICON statement
in a resource script file:

iconID ICON [load-option] [memory-option] iconfile.ico

The load option can be either PRELOAD or LOADONCALL. A resource defined as
PRELOAD will be loaded into memory when the program is loaded. LOADONCALL
means that the resource will not be loaded until Windows needs it.
LOADONCALL is the default for all resources. You will probably want to use
PRELOAD only when you know that your program will need the resource
immediately after beginning to execute.

The memory options are FIXED, MOVEABLE, and DISCARDABLE. DISCARDABLE
resources must also be MOVEABLE. For the resources discussed in this
chapter, the icon, cursor, and character string resources have default
memory options of MOVEABLE and DISCARDABLE. The bitmap and user-defined
resources are MOVEABLE only. Why the difference? Icon, cursor, and character
string resources are read-only, so Windows can safely discard them from
memory. Windows allows bitmaps and user-defined resources to be modified
from within a program--and modified resources cannot be discarded.

Bitmaps: Pictures in Pixels

We've already talked about the use of bitmaps in icons and cursors. Windows
also includes a resource type called BITMAP.

Bitmaps are used for two major purposes. The first is to draw pictures on
the display. For instance, the Windows display driver files contain lots of
tiny bitmaps used for drawing the arrows in scroll bars, the check mark in
pull-down menus, the system menu box, the size box, check boxes, and radio
buttons. Programs such as PAINTBRUSH use bitmaps to display a graphics menu.

The second major use of bitmaps is to create brushes. Brushes, you'll
recall, are patterns of pixels that Windows uses to fill an area of the
display. (Chapter 9 discusses a third and less common use of bitmaps, as
selection items in menus.)


Using Bitmaps and Brushes

The RESOURC2 program, shown in Figure 8-2, is an upgraded version of
RESOURC1 that includes a monochrome bitmap resource used to create a brush
for the background of the client area. The bitmap was created in SDKPAINT
with dimensions of 8 by 8, which is the minimum size for a brush.

 RESOURC2.MAK

#------------------------
# RESOURC2.MAK make file
#------------------------

resourc2.exe : resourc2.obj resourc2.def resourc2.res
     link resourc2, /align:16, NUL, /nod slibcew libw, resourc2
     rc resourc2.res

resourc2.obj : resourc2.c
     cl -c -Gsw -Ow -W2 -Zp resourc2.c

resourc2.res : resourc2.rc resourc2.ico resourc2.cur resourc2.bmp
     rc -r resourc2.rc

 RESOURC2.C

/*-----------------------------------------------------------
   RESOURC2.C -- Icon and Cursor Demonstration Program No. 2
                 (c) Charles Petzold, 1990
  -----------------------------------------------------------*/


#include <windows.h>

long FAR PASCAL WndProc  (HWND, WORD, WORD, LONG) ;

char   szAppName[] = "Resourc2" ;
HANDLE hInst ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     HBITMAP  hBitmap ;
     HBRUSH   hBrush ;
     HWND     hwnd ;
     MSG      msg ;
     WNDCLASS wndclass ;

     hBitmap = LoadBitmap (hInstance, szAppName) ;
     hBrush = CreatePatternBrush (hBitmap) ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (hInstance, szAppName) ;
          wndclass.hCursor       = LoadCursor (hInstance, szAppName) ;
          wndclass.hbrBackground = hBrush ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hInst = hInstance ;

     hwnd = CreateWindow (szAppName, "Icon and Cursor Demo",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;
     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }

     DeleteObject (hBrush) ;       // clean up
     DeleteObject (hBitmap) ;

     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static HICON hIcon ;
     static short cxIcon, cyIcon, cxClient, cyClient ;
     HDC          hdc ;
     PAINTSTRUCT  ps ;
     RECT         rect ;
     short        x, y ;

     switch (message)
          {
          case WM_CREATE :
               hIcon = LoadIcon (hInst, szAppName) ;
               cxIcon = GetSystemMetrics (SM_CXICON) ;
               cyIcon = GetSystemMetrics (SM_CYICON) ;
               return 0 ;

          case WM_SIZE :
               cxClient = LOWORD (lParam) ;
               cyClient = HIWORD (lParam) ;
               return 0 ;

          case WM_PAINT :
               hdc = BeginPaint (hwnd, &ps) ;

               for (y = cyIcon ; y < cyClient ; y += 2 * cyIcon)
                    for (x = cxIcon ; x < cxClient ; x += 2 * cxIcon)
                         DrawIcon (hdc, x, y, hIcon) ;

               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 RESOURC2.RC

/*-----------------------------
   RESOURC2.RC resource script
  -----------------------------*/

resourc2  ICON    resourc2.ico
resourc2  CURSOR  resourc2.cur
resourc2  BITMAP  resourc2.bmp

 RESOURC2.ICO  -- Please refer to the book.

 RESOURC2.CUR  -- Please refer to the book.

 RESOURC2.BMP  -- Please refer to the book.

 RESOURC2.DEF

;-------------------------------------
; RESOURC2.DEF module definition file
;-------------------------------------

NAME           RESOURC2

DESCRIPTION    'Icon and Cursor Demo Program No. 2 (c) Charles Petzold,
1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

The bitmap resource is included in the resource script in the same format as
the icon and cursor:

resourc2 BITMAP resourc2.bmp

The LoadBitmap function used in WinMain is similar to the LoadIcon and
LoadCursor calls. It returns a handle to a bitmap:

hBitmap = LoadBitmap (hInstance, szAppName) ;

This handle is then used to create a pattern brush. The brush is based on
the bitmap:

hBrush = CreatePatternBrush (hBitmap) ;

When Windows fills an area of the display with this brush, the bitmap is
repeated horizontally and vertically every eight pixels. We want to use this
brush to color the background of the client area, which we accomplish when
defining the window class:

wndclass.hbrBackground = hBrush ;

The major difference between bitmaps and other resources is of practical
significance and can be simply stated: Bitmaps are GDI objects. They are not
shared among instances of your program, and they are not automatically
deleted from memory when your program terminates. Because bitmaps and
brushes are GDI objects, they must be deleted before the program terminates.
In RESOURC2 this is done at the end of WinMain:

DeleteObject (hBrush) ;
DeleteObject (hBitmap) ;



CHARACTER STRINGS

Having a resource for character strings may seem odd at first. Certainly we
haven't had any problem using regular old character strings defined as
variables right in our source code.

Character string resources are primarily for easing the translation of your
program to other languages. As you'll discover in the next two chapters,
menus and dialog boxes are also part of the resource script. If you use
character string resources rather than put strings directly into your source
code, then all text that your program uses will be in one file--the resource
script. If the text in this resource script is translated, all you need do
to create a foreign-language version of your program is relink the program
and add the translated resources to the .EXE file. This method is much safer
than messing around with your source code. (Of course, you could also choose
to define all your character strings as macros and store them in a header
file. This method also avoids altering source code during language
translations.)

A second reason for using character string resources is to reduce memory
space. This reason is less obvious--in fact, if you use character string
resources inefficiently, you might not reduce memory space at all. We'll
examine this problem after we get through the basics.

Using Character String Resources

The character string resources are defined in your resource script using the
keyword STRINGTABLE:

STRINGTABLE [load option] [memory option]
     {
          nID1, "character string 1"
          nID2, "character string 2"
[other string definitions]
     }

 The resource script can contain only one string table. LOADONCALL is the
default load option; MOVEABLE and DISCARDABLE are the default memory
options. Each string can be only one line long with a maximum of 255
characters. The strings cannot contain any C-style control characters except
for \t (tab). However, the strings can contain octal constants:

────────────────────────────────────────────────────────────────────────────
Tab                               \011
Linefeed                          \012
Carriage return                   \015

These control characters are recognized by the DrawText and MessageBox
functions.

Your program can use the LoadString call to copy a string resource into a
buffer in the program's data segment:

LoadString (hInstance, nID, lpszBuffer, nMaxLength) ;

The nID parameter refers to the ID number that precedes each string in the
resource script; lpszBuffer is a far (or long) pointer to a character array
that receives the character string; and nMaxLength is the maximum number of
characters to transfer into the lpszBuffer. The string ID numbers that
precede each string are generally macro identifiers defined in a header
file. Many Windows programmers use the prefix IDS_ to denote an ID number
for a string. Sometimes a filename or other information must be embedded in
the string when the string is displayed. In this case you put C formatting
characters in the string and use it as a formatting string in sprintf or
wsprintf.


Using Strings with MessageBox

Let's look at an example of a program that uses three character strings to
display three error messages in a message box. A header file that we'll call
PROGRAM.H defines three identifiers for these messages:

#define IDS_FILENOTFOUND 1
#define IDS_FILETOOBIG   2
#define IDS_FILEREADONLY 3

The resource script looks like this:

#include "program.h"
[other resource script]
STRINGTABLE
     {
          IDS_FILENOTFOUND,   "File %s not found."
          IDS_FILETOOBIG,     "File %s too large to edit."
          IDS_FILEREADONLY,   "File %s is read-only."
     }

The C source code file also includes this header file and defines a function
to display a message box. (I'm assuming that szAppName is a global variable
that contains the program name.)

#include "program.h"
[other program lines]
OkMessage (HWND hwnd, WORD wErrorNumber, char *szFileName)
     {
     char szFormat [40] ;
     char szBuffer [60] ;

     LoadString (hInst, wErrorNumber, szFormat, 40) ;

     sprintf (szBuffer, szFormat, szFilename) ;

     return MessageBox (hwnd, szBuffer, szAppName,
                            MB_OK | MB_ICONEXCLAMATION) ;
     }

To display a message box containing the "file not found" message, the
program calls:

OkMessage (hwnd, IDS_FILENOTFOUND, szFileName) ;


Character Strings and Memory Space

Character string resources usually save memory space, but the amount of
space saved depends on how efficiently they're used. When the RC.EXE
resource compiler adds strings to the .EXE file, the strings are grouped
into different segments depending on the ID numbers of the strings. Each
segment contains a maximum of 16 strings. Strings with ID numbers from 0 to
15 are stored in one segment, from 16 to 31 are in another, and so forth.
Because of this grouping into segments, your .EXE file will be shorter if
you use consecutive numbers for your string IDs.

When you use LoadString to copy a string resource into memory, however,
Windows loads the entire segment (containing up to 16 strings) into memory
as a resource. Windows then also copies the content of the string specified
in the LoadString call to a buffer in your program's data segment. So when
string resources are loaded into memory, they initially occupy less memory
if you do not use consecutive numbers for the string IDs. Probably the worst
way to use string resources is to load all the strings into separate global
static arrays during program initialization in WinMain. If you do that, you
don't use any less space than if you had included the strings in your C
source code. In fact, you'll use more space, because you'll set the size of
these arrays somewhat larger than the actual string lengths. Here are some
general rules for using string resources:

  ■   Assign string ID numbers according to logical groupings. For instance,
      if five strings are involved in one section of your program and six
      strings are involved in another section, you might use ID numbers 0 to
      4 for the first group of strings and 16 to 21 for the second group.

  ■   Whenever possible, load the strings into automatic local variables
      within functions, thus freeing up the space when the function is
      exited. (The OkMessage function shown above uses this approach.)

  ■   Alternatively, reuse a single static array for loading strings.

Here's an example of the last approach. Let's assume that your program never
requires more than one string at a time. You can define a function that
loads the string and returns a pointer to a static variable containing the
string:

char *String (WORD wID)
     {
     static szBuffer [256] ;

     LoadString (hInst, wID, szBuffer, 255) ;
     return buffer ;
     }

If you want to use DrawText to display the string with ID number 45, you use
this statement:

DrawText (hdc, String (45), -1, &rect, DT_LEFT) ;

Each call to String destroys the contents of the static buffer. If you
require access to (for example) three strings at one time, you can modify
String as follows:

char *String (WORD wID, short n)
     {
     static szBuffer [3][256] ;

     LoadString (hInst, wID, szBuffer [n], 255) ;
     return szBuffer [n] ;
     }

When you call String now, you pass an ID number and a second parameter that
is either 0, 1, or 2.



USER-DEFINED RESOURCES

The "user-defined resource" is convenient for attaching miscellaneous data
to your .EXE file and obtaining access to that data within the program. The
data can be in any format you want. The Windows functions used to access
user-defined resources return a far pointer to the data when Windows loads
the data into memory. You can do whatever you want with that data. For
instance, suppose you have a file called PROGHELP.TXT that contains "help"
text for your program. This file needn't be a pure ASCII file: It can also
contain binary data, such as pointers that would aid your program in
referencing various sections of this file. Reference this file with a
statement in your resource script that looks like this:

helptext TEXT proghelp.txt

For helptext (the name of the resource) and TEXT (the type of the resource),
you can use any names you want. I've capitalized TEXT simply to make it look
like the ICON, CURSOR, and BITMAP statements. What we're doing here is
making up our own type of resource, called TEXT.

During program initialization (for example, during processing of the
WM_CREATE message), you can obtain a handle to this resource:

hResource = LoadResource (hInstance,
            FindResource (hInstance, "TEXT", "helptext")) ;

The variable hResource is defined with type HANDLE. Despite its name,
LoadResource does not actually load the resource into memory just yet. The
LoadResource and FindResource functions used together like this are
essentially equivalent to the LoadIcon and LoadCursor functions. In fact,
LoadIcon and LoadCursor use the LoadResource and FindResource functions.

You can use numbers rather than names for the resource name and resource
type. The numbers can be converted to far pointers in the FindResource call
using MakeIntResource. The numbers used for the resource type must be
greater than 255. (Windows uses numbers between 1 and 9 when calling
FindResource for existing resource types.)

When you need access to the text, call LockResource:

lpHelpText = LockResource (hResource) ;

LockResource loads the resource into memory (if it has not already been
loaded), locks it using the GlobalLock function, and returns a far pointer
to it. When you are finished accessing the memory, unlock the segment:

UnlockResource (hResource) ;

This allows Windows to move the segment in memory. When you're finished with
the resource, you can free it from memory:

FreeResource (hResource) ;

The resource will be freed when your program terminates, even if you don't
call FreeResource.

Normally, user-defined resources are not discardable unless you include the
DISCARDABLE keyword before the filename in the resource script. But if you
use the pointer returned from LockResource to alter as well as read the
data, don't make the resource DISCARDABLE. Note also that the same resource
is shared among all instances of the program. If each instance needs its own
copy of the resource, you should make the resource discardable, use
LockResource to obtain a pointer to the resource, use GlobalAlloc to obtain
a global memory block of the same size, use GlobalLock to lock that block,
and then copy the contents of the resource into the global memory block.

Let's look at a sample program that uses three resources--an icon, a string
table, and a user-defined resource. The POEPOEM program, shown in Figure
8-3, displays the text of

Edgar Allan Poe's "Annabel Lee" in its client area. The user-defined
resource is the file POEPOEM.ASC, which contains the text of the poem. The
text file is terminated with a backslash (\).

 POEPOEM.MAK

#-----------------------
# POEPOEM.MAK make file
#-----------------------

poepoem.exe : poepoem.obj poepoem.def poepoem.res
     link poepoem, /align:16, NUL, /nod slibcew libw, poepoem
     rc poepoem.res

poepoem.obj : poepoem.c poepoem.h
     cl -c -Gsw -Ow -W2 -Zp poepoem.c

poepoem.res : poepoem.rc poepoem.ico poepoem.asc poepoem.h
     rc -r poepoem.rc

 POEPOEM.C

/*-------------------------------------------------
   POEPOEM.C -- Demonstrates User-Defined Resource
                (c) Charles Petzold, 1990
  -------------------------------------------------*/

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

long FAR PASCAL WndProc  (HWND, WORD, WORD, LONG) ;

char   szAppName [10] ;
char   szCaption [35] ;
HANDLE hInst ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     HWND     hwnd ;
     MSG      msg ;
     WNDCLASS wndclass ;

     if (!hPrevInstance)
          {
          LoadString (hInstance, IDS_APPNAME, szAppName, sizeof szAppName) ;
          LoadString (hInstance, IDS_CAPTION, szCaption, sizeof szCaption) ;


          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (hInstance, szAppName) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }
     else
          {
          GetInstanceData (hPrevInstance, szAppName, sizeof szAppName) ;
          GetInstanceData (hPrevInstance, szCaption, sizeof szCaption) ;
          }

     hInst = hInstance ;

     hwnd = CreateWindow (szAppName, szCaption,
                          WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static HANDLE hResource ;
     static HWND   hScroll ;
     static short  nPosition, cxChar, cyChar, cyClient, nNumLines, xScroll ;
     char          szPoemRes [15] ;
     char far      *lpText ;

     HDC           hdc ;
     PAINTSTRUCT   ps ;
     RECT          rect ;
     TEXTMETRIC    tm ;

     switch (message)
          {
          case WM_CREATE :
               hdc = GetDC (hwnd) ;
               GetTextMetrics (hdc, &tm) ;
               cxChar = tm.tmAveCharWidth ;
               cyChar = tm.tmHeight + tm.tmExternalLeading ;
               ReleaseDC (hwnd, hdc) ;

               xScroll = GetSystemMetrics (SM_CXVSCROLL) ;

               hScroll = CreateWindow ("scrollbar", NULL,
                              WS_CHILD | WS_VISIBLE | SBS_VERT,
                              0, 0, 0, 0,
                              hwnd, 1, hInst, NULL) ;

               LoadString (hInst, IDS_POEMRES, szPoemRes, sizeof szPoemRes)
;
               hResource = LoadResource (hInst,
                           FindResource (hInst, szPoemRes, "TEXT")) ;

               lpText = LockResource (hResource) ;

               nNumLines = 0 ;

               while (*lpText != '\\' && *lpText != '\0')
                    {
                    if (*lpText == '\n')
                         nNumLines ++ ;
                    lpText = AnsiNext (lpText) ;
                    }
               *lpText = '\0' ;

               GlobalUnlock (hResource) ;

               SetScrollRange (hScroll, SB_CTL, 0, nNumLines, FALSE) ;
               SetScrollPos   (hScroll, SB_CTL, 0, FALSE) ;
               return 0 ;

          case WM_SIZE :
               MoveWindow (hScroll, LOWORD (lParam) - xScroll, 0,
                    xScroll, cyClient = HIWORD (lParam), TRUE) ;
               SetFocus (hwnd) ;
               return 0 ;

          case WM_SETFOCUS :
               SetFocus (hScroll) ;
               return 0 ;
          case WM_VSCROLL :
               switch (wParam)
                    {
                    case SB_TOP :
                         nPosition = 0 ;
                         break ;
                    case SB_BOTTOM :
                         nPosition = nNumLines ;
                         break ;
                    case SB_LINEUP :
                         nPosition -= 1 ;
                         break ;
                    case SB_LINEDOWN :
                         nPosition += 1 ;
                         break ;
                    case SB_PAGEUP :
                         nPosition -= cyClient / cyChar ;
                         break ;
                    case SB_PAGEDOWN :
                         nPosition += cyClient / cyChar ;
                         break ;
                    case SB_THUMBPOSITION :
                         nPosition = LOWORD (lParam) ;
                         break ;
                    }
               nPosition = max (0, min (nPosition, nNumLines)) ;

               if (nPosition != GetScrollPos (hScroll, SB_CTL))
                    {
                    SetScrollPos (hScroll, SB_CTL, nPosition, TRUE) ;
                    InvalidateRect (hwnd, NULL, TRUE) ;
                    }
               return 0 ;

          case WM_PAINT :
               hdc = BeginPaint (hwnd, &ps) ;

               lpText = LockResource (hResource) ;

               GetClientRect (hwnd, &rect) ;
               rect.left += cxChar ;
               rect.top  += cyChar * (1 - nPosition) ;
               DrawText (hdc, lpText, -1, &rect, DT_EXTERNALLEADING) ;

               GlobalUnlock (hResource) ;

               EndPaint (hwnd, &ps) ;
               return 0 ;
          case WM_DESTROY :
               FreeResource (hResource) ;
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 POEPOEM.RC

/*----------------------------
   POEPOEM.RC resource script
  ----------------------------*/

#include "poepoem.h"

poepoem     ICON  poepoem.ico
AnnabelLee  TEXT  poepoem.asc

STRINGTABLE
     {
     IDS_APPNAME, "poepoem"
     IDS_CAPTION, """Annabel Lee"" by Edgar Allen Poe"
     IDS_POEMRES, "AnnabelLee"
     }

 POEPOEM.ICO  -- Please refer to the book.

 POEPOEM.H

/*-----------------------
   POEPOEM.H header file
  -----------------------*/

#define IDS_APPNAME 0
#define IDS_CAPTION 1
#define IDS_POEMRES 2

 POEPOEM.ASC

It was many and many a year ago,
   In a kingdom by the sea,
That a maiden there lived whom you may know
   By the name of Annabel Lee;
And this maiden she lived with no other thought
   Than to love and be loved by me.

I was a child and she was a child
   In this kingdom by the sea,
But we loved with a love that was more than love --
   I and my Annabel Lee --
With a love that the wing\ged seraphs of Heaven
   Coveted her and me.

And this was the reason that, long ago,
   In this kingdom by the sea,
A wind blew out of a cloud, chilling
   My beautiful Annabel Lee;
So that her highborn kinsmen came
   And bore her away from me,
To shut her up in a sepulchre
   In this kingdom by the sea.

The angels, not half so happy in Heaven,
   Went envying her and me --
Yes! that was the reason (as all men know,
   In this kingdom by the sea)
That the wind came out of the cloud by night,
   Chilling and killing my Annabel Lee.
But our love it was stronger by far than the love
   Of those who were older than we --
   Of many far wiser than we --
And neither the angels in Heaven above
   Nor the demons down under the sea
Can ever dissever my soul from the soul
   Of the beautiful Annabel Lee:

For the moon never beams, without bringing me dreams
   Of the beautiful Annabel Lee;
And the stars never rise, but I feel the bright eyes
   Of the beautiful Annabel Lee:
And so, all the night-tide, I lie down by the side
Of my darling -- my darling -- my life and my bride,
   In her sepulchre there by the sea --
   In her tomb by the sounding sea.

                                       [May 1849]
\

 POEPOEM.DEF

;------------------------------------
; POEPOEM.DEF module definition file
;------------------------------------

NAME           POEPOEM

DESCRIPTION    'Demo of User-Defined Resource (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

In the POEPOEM.RC resource script, the user-defined resource is given the
type TEXT and the name AnnabelLee:

AnnabelLee  TEXT  poepoem.asc

During WM_CREATE processing in WndProc, a handle to the resource is obtained
using FindResource and LoadResource. The resource is locked using
LockResource, and a small routine replaces the backslash (\) at the end of
the file with a 0. (This is for the benefit of the DrawText function used
later.) In most cases it's not a good idea to write on a user-defined
resource directly, because the same resource is shared among all instances
of the program. However, later instances of POEPOEM will not encounter
problems with the change we've made. The resource is then unlocked using
GlobalUnlock.

The resource is also locked and unlocked during processing of WM_PAINT to
write the text to the display using DrawText. Note the use of a child window
scroll bar control rather than a window scroll bar. The child window scroll
bar control has an automatic keyboard interface, so no WM_KEYDOWN processing
is required in POEPOEM.

POEPOEM also uses three character strings, the IDs of which are defined in
the POEPOEM.H header file. For the first instance of the program, the
IDS_APPNAME and IDS_CAPTION strings are loaded into global static variables
using LoadString:

LoadString (hInstance, IDS_APPNAME, szAppName, sizeof szAppName) ;
LoadString (hInstance, IDS_CAPTION, szCaption, sizeof szCaption) ;

However, for subsequent instances of POEPOEM, the strings are copied from
the previous instance:

GetInstanceData (hPrevInstance, szAppName, sizeof szAppName) ;
GetInstanceData (hPrevInstance, szCaption, sizeof szCaption) ;

GetInstanceData is faster than LoadString if the string resource has been
discarded from memory. The pointers (szAppName and szCaption) are near
pointers to static global variables. Windows uses these pointers in
combination with the data segment address of the previous instance and the
data segment address of the current instance to copy the contents of the
variables.

Now that we've defined all the character strings used in POEPOEM as
resources, we've made it easier for translators to convert the program into
a foreign-language version. Of course, they'd also have to translate the
text of "Annabel Lee"--which would, I suspect, be a somewhat more difficult
task.




Chapter 9  Menus and Accelerators
────────────────────────────────────────────────────────────────────────────

Menus are an important part of the consistent user interface that Windows
programs offer. Adding a menu to your program is a relatively easy part of
Windows programming: You simply define the structure of the menu in your
resource script and assign a unique ID number to each menu item. You specify
the name of the menu in the window class structure. When the user chooses a
menu item, Windows sends your program a WM_COMMAND message containing that
ID. But we won't stop with that simple example. One of the more interesting
things you can do with menus is display bitmaps in the menu rather than
character strings, so we'll take a detailed look at how that is done.

This chapter also covers "keyboard accelerators." These are key combinations
that are used primarily to duplicate menu functions.

MENUS

A window's menu bar is displayed immediately below the caption bar. This
menu bar is sometimes called a program's "main menu" or the "top-level
menu." Items listed in the top-level menu almost always invoke a drop-down
menu, which is called either a "popup menu" or a "submenu." Beginning with
Windows 3, you can define multiple nestings of popups: that is, an item on a
popup menu can invoke another popup menu. Sometimes items in popup menus
invoke a dialog box for more information. (Dialog boxes are covered in
Chapter 10.) Most parent windows have, to the far left of the caption bar, a
box containing a single line. This box invokes the system menu, which is
really another popup menu.

Menu items in popups can be "checked," which means that Windows draws a
small check mark to the left of the menu text. The use of check marks lets
the user choose different program options from the menu. These options can
be mutually exclusive, but they don't have to be. Top-level menu items
cannot be checked.

Menu items in the top-level menu or in popup menus can be "enabled,"
"disabled," or "grayed." The words "active" and "inactive" are sometimes
used synonymously with "enabled" and "disabled." Menu items flagged as
enabled or disabled look the same to the user, but a grayed menu item is
displayed in gray text.

From the perspective of the user, enabled, disabled, and grayed menu items
can all be "selected" (highlighted). That is, the user can click the mouse
on a disabled menu item, or move the reverse-video cursor bar to a disabled
menu item, or trigger the menu item using the item's key letter. However,
from the perspective of your program, enabled, disabled, and grayed menu
items function differently. Windows sends your program a WM_COMMAND message
only for enabled menu items. You use disabled and grayed menu items for
options that are not currently valid. If you want to let the user know the
option is not valid, make it grayed.

Menu Structure

When you create or change menus in a program, it's useful to think of the
top-level menu and each popup menu as being separate menus. The top-level
menu has a menu handle, each popup menu within a top-level menu has its own
menu handle, and the system menu (which is also a popup) has a menu handle.

Each item in a menu is defined by three characteristics: The first
characteristic is what appears in the menu. This is either a text string or
a bitmap. The second characteristic is either an ID number that Windows
sends to your program in a WM_COMMAND message or a popup menu that Windows
displays when the user chooses that menu item. The third characteristic
describes the attribute of the menu item, including whether the item is
disabled, grayed, or checked.


The Menu Template

You can create a menu in three different ways. The most common (and the
easiest) is to define the menu in your resource script in the form of a menu
template. This example shows all the different options you can use in this
template.

MyMenu MENU [load option] [memory option]
     {
     MENUITEM "&One",   1
     POPUP "&Two"
          {
          MENUITEM "&Ten",       10, CHECKED
          MENUITEM "&Eleven",    11
          MENUITEM SEPARATOR
          MENUITEM "T&welve",    12, INACTIVE
          MENUITEM "T&hirteen",  13
          MENUITEM "&Fourteen",  14, MENUBREAK
          MENUITEM "F&ifteen",   15,
          MENUITEM "&Sixteen",   16, MENUBARBREAK
          POPUP    "Se&venteen", 17,
               {
               MENUITEM "&Twenty",      20
               MENUITEM "T&wenty-One",  21
               MENUITEM "Tw&enty-Two",  22
               }
          MENUITEM "Ei&ghteen",  18, GRAYED
          }
     MENUITEM "Th&ree",  3
     MENUITEM "&Four",   4, INACTIVE
     MENUITEM "Fi&ve",   5
     MENUITEM "Si&x",    6, MENUBREAK
     MENUITEM "&Seven",  7,
     MENUITEM "&Eight",  8, GRAYED
     MENUITEM "\a&Help", 9, HELP
     }

This particular menu template defines a top-level menu that displays the
labels "One" through "Eight" and "Help," as shown in Figure 9-1 on the
following page. Only the second item invokes a popup menu. The popup menu
displays the labels "Ten" through "Eighteen," as shown in Figure 9-2 on the
following page. The little arrow to the right of the "Seventeen" option
indicates that it invokes yet another popup menu.

MyMenu is the name of the menu. This name performs the same function as the
names of icon, cursor, and bitmap resources discussed in Chapter 8. As with
other resources, the load option on the MENU statement can be either PRELOAD
(in which case Windows loads the resource into memory when the program is
executed) or LOADONCALL (in which case Windows loads the resource into
memory only when it is needed). The default is LOADONCALL. The memory
options are FIXED, MOVEABLE, and DISCARDABLE. The default is MOVEABLE and
DISCARDABLE. Discardable menus must also be moveable. Although we'll change
menus in some of the programs shown later, don't worry that the menu
resource is discardable. Windows makes a copy of the menu for your program
to use and change.

  (Figure 9-1. may be found in the printed book.)

  (Figure 9-2. may be found in the printed book.)

The top-level menu is enclosed in left and right brackets. (You can use
BEGIN and END statements instead if you wish.) The two types of statements
allowed within these brackets are:

MENUITEM "text", wID, options

and:

POPUP "text", options

The text displayed for each menu must be enclosed in double quotation marks.
An ampersand (&) causes the character that follows it to be underlined when
Windows displays the menu. This is also the character Windows searches for
when you select a menu item using the Alt key. If you don't include an
ampersand in the text, no underline will appear, and Windows will use
instead the first letter of the text for Alt-key searches.

The options on the MENUITEM and POPUP statements that appear in the
top-level menu list are as follows:

  ■   GRAYED--The menu item is inactive, and it does not generate a
      WM_COMMAND message. The text is grayed.

  ■   INACTIVE--The menu item is inactive, and it does not generate a
      WM_COMMAND message. The text is displayed normally.

  ■   MENUBREAK--This item and following items appear on a new line of the
      menu.

  ■   HELP--When used in combination with \a before the text, this item is
      right-justified.

Options can be combined using the C bitwise OR symbol (|), but GRAYED and
INACTIVE cannot be used together. MENUBREAK is uncommon in a top-level menu,
because Windows automatically separates a top-level menu into multiple lines
if the window is too narrow to fit the entire menu.

Following a POPUP statement in the main menu, the left and right brackets
(or the BEGIN and END keywords) block off a list of items in the popup. The
following statements are allowed in a popup definition:

MENUITEM "text", wID, options

and:

MENUITEM SEPARATOR

and:

POPUP  "text", options

MENUITEM SEPARATOR draws a horizontal line in the popup menu. This line is
often used to separate groups of related options.

For items in popup menus, you can use the columnar tab character \t in the
text string. Text following the \t is placed in a new column spaced far
enough to the right to accommodate the longest text string in the first
column of the popup. We'll see how this works when discussing keyboard
accelerators toward the end of this chapter. A \a right-justifies the text
that follows it. The options for MENUITEM in a popup are as follows:

  ■   CHECKED--A check mark appears to the left of the text.

  ■   GRAYED--The menu item is inactive and does not generate a WM_COMMAND
      message. The text is grayed.

  ■   INACTIVE--The menu item is inactive and does not generate a WM_COMMAND
      message. The text is displayed normally.

  ■   MENUBREAK--This item and the following items appear in a new column of
      the menu.

  ■   MENUBARBREAK--This item and the following items appear in a new column
      of the menu. A vertical line separates the columns.

GRAYED and INACTIVE cannot be used together. MENUBREAK and MENUBARBREAK
cannot be used together. You should use either MENUBREAK or MENUBARBREAK
when the number of items in a popup is too long to be displayed in a single
column.

The wID values in the MENUITEM statements are the numbers that Windows sends
to the window procedure in menu messages. The wID values should be unique
within a menu. Instead of using numbers, you'll probably want to use
identifiers defined in a header file. By convention, these identifiers begin
with the letters IDM ("ID for a menu").


Referencing the Menu in Your Program

Most Windows applications have only one menu in the resource script. The
program makes reference to this menu in the definition of the window class:

wndclass.lpszMenuName = "MyMenu" ;

Programmers often use the name of the program as the name of the menu so
that the same text string can also be used for the window class, the name of
the program's icon, and the name of the menu. However, you can also use a
number (or a macro identifier) for the menu rather than a name. The resource
script would look like this:

45 MENU
     {
[menu definition]
     }

In this case, the assignment statement for the lpszMenuName field of the
window class structure can be either:

wndclass.lpszMenuName = MAKEINTRESOURCE (45) ;

or:

wndclass.lpszMenuName = "#45" ;

Although specifying the menu in the window class is the most common way to
reference a menu resource, you have alternatives. A Windows application can
load a menu resource into memory with the LoadMenu function, which is
similar to the LoadIcon and LoadCursor functions described in Chapter 8. If
you use a name for the menu in the resource script, LoadMenu returns a
handle to the menu:

hMenu = LoadMenu (hInstance, "MyMenu") ;

If you use a number, the LoadMenu call takes either this form:

hMenu = LoadMenu (hInstance, MAKEINTRESOURCE (45)) ;

or this form:

hMenu = LoadMenu (hInstance, "#45") ;

You can then specify this menu handle as the ninth parameter to
CreateWindow:

hwnd = CreateWindow ("MyClass", "Window Caption",
               WS_OVERLAPPEDWINDOW,
               CW_USEDEFAULT,CW_USEDEFAULT,
               CW_USEDEFAULT,CW_USEDEFAULT,
               NULL,
               hMenu,
               hInstance,
               NULL) ;

In this case, the menu specified in the CreateWindow call overrides any menu
specified in the window class. You can think of the menu in the window class
as being a default menu for the windows based on the window class if the
ninth parameter to CreateWindow is NULL. Therefore, you can use different
menus for several windows based on the same window class.

You can also have a NULL menu in the window class and a NULL menu in the
CreateWindow call and assign a menu to a window after the window has been
created:

SetMenu (hwnd, hMenu) ;

This form lets you dynamically change a window's menu. We'll see an example
of this in the NOPOPUPS program shown later in this chapter.


Menus and Messages

Windows usually sends a window procedure several different messages when the
user selects a menu item. In most cases your program can ignore many of
these messages and simply pass them to DefWindowProc. Let's take a look at
them anyway.

The first message your program receives when the user selects a menu item
with the keyboard or mouse is a WM_SYSCOMMAND message. The values of wParam
and lParam are shown below:

           wParam  lParam
────────────────────────────────────────────────────────────────────────────
Mouse:     F09x    0
Keyboard:  F10x    0

The WINDOWS.H identifier SC_MOUSEMENU is equal to F090H; SC_KEYMENU is
F100H, but the last digit in wParam (indicated by an x in the table above)
can be anything. Use:

(wParam & 0xFFF0)

if you need to check this value. Most programs pass these messages to
DefWindowProc.

The second message your program receives is a WM_INITMENU message with the
following parameters:

wParam               LOWORD (lParam)  HIWORD (lParam)
────────────────────────────────────────────────────────────────────────────
Handle to main menu  0                0

The value of wParam is the handle to your main menu even if the user is
selecting an item from the system menu. Windows programs generally ignore
the WM_INITMENU message. Although the message exists to give you the
opportunity to change the menu before an item is chosen, I suspect any
changes to the top-level menu at this time would be very disconcerting to
the user.

The next message your program receives is WM_MENUSELECT. A program can
receive many WM_MENUSELECT messages as the user moves the cursor or mouse
among the menu items. The parameters that accompany WM_SELECT are as
follows:

wParam                       LOWORD (lParam)  HIWORD (lParam)
────────────────────────────────────────────────────────────────────────────
Selected item: Menu ID or    Selection flags  Handle to menu containing
popup menu handle                             selected item


WM_MENUSELECT is a menu-tracking message. The value of wParam tells you what
item of the menu is currently selected (highlighted). The "selection flags"
in the low word of lParam can be a combination of the following: MF_GRAYED,
MF_DISABLED, MF_CHECKED, MF_BITMAP, MF_POPUP, MF_HELP, MF_SYSMENU, and
MF_MOUSESELECT. You may want to use WM_MENUSELECT if you need to change
something in the client area of your window based on the movement of the
highlight among the menu items. Most programs pass this message to
DefWindowProc.

When Windows is ready to display a popup menu, it sends the window procedure
a WM_INITMENUPOPUP message with the following parameters:

wParam             LOWORD (lParam)  HIWORD (lParam)
────────────────────────────────────────────────────────────────────────────
Popup menu handle  Popup index      1 for system menu, 0 otherwise

This message is important if you need to enable or disable items in a popup
menu before it is displayed. For instance, suppose your program can copy
text from the clipboard using the Paste command on a popup menu. When you
receive a WM_INITMENUPOPUP message for that popup, you should determine if
the clipboard has text in it. If it doesn't, you should gray the Paste menu
item. We'll see an example of this in the revised POPPAD program shown
toward the end of this chapter.

The most important menu message is WM_COMMAND. This message indicates that
the user has chosen an enabled menu item from your window's menu. You'll
recall from Chapter 6 that WM_COMMAND messages also result from child window
controls. If you happen to use the same ID codes for menus and child window
controls, you can differentiate between them by the low word of lParam,
which will be 0 for a menu item:

          wParam      LOWORD (lParam)      HIWORD (lParam)
────────────────────────────────────────────────────────────────────────────
Menu:     Menu ID     0                    0
Control:  Control ID  Child window handle  Notification code

The WM_SYSCOMMAND message is similar to the WM_COMMAND message except that
WM_SYSCOMMAND signals that the user has chosen an enabled menu item from the
system menu:

              wParam   LOWORD (lParam)  HIWORD (lParam)
────────────────────────────────────────────────────────────────────────────
System menu:  Menu ID  0                0

The menu ID indicates which item on the system menu has been chosen. For the
predefined system menu items, the bottom four bits should be masked out. The
resultant value will be one of the following: SC_SIZE, SC_MOVE, SC_MINIMIZE,
SC_MAXIMIZE, SC_NEXTWINDOW, SC_PREVWINDOW, SC_CLOSE, SC_VSCROLL, SC_HSCROLL,
SC- _ARRANGE, SC_RESTORE, and SC_TASKLIST. In addition, wParam can be
SC_MOUSEMENU or SC_KEYMENU, as indicated earlier.

If you add menu items to the system menu, wParam will be the menu ID that
you define. To avoid conflicts with the predefined menu IDs, use values
below F000H. It is important that you pass normal WM_SYSCOMMAND messages to
DefWindowProc. If you do not, you'll effectively disable the normal system
menu commands.

The final message we'll discuss is WM_MENUCHAR, which isn't really a menu
message at all. Windows sends this message to your window procedure in one
of two circumstances: if the user presses Alt and a character key that does
not correspond to a menu item, or, when a popup is displayed, if the user
presses a character key that does not correspond to an item in the popup.
The parameters that accompany the WM_MENUCHAR message are as follows:

wParam      LOWORD (lParam)  HIWORD (lParam)
────────────────────────────────────────────────────────────────────────────
ASCII code  Selection code   Handle to menu

The selection code is:

  ■   0--No popup is displayed.

  ■   MF_POPUP--Popup is displayed.

  ■   MF_SYSMENU--System menu popup is displayed.

Windows programs usually pass this message to DefWindowProc, which normally
returns a 0 to Windows, which causes Windows to beep. We'll see a use for
the WM_MENUCHAR message in the GRAFMENU program shown later in this chapter.


A Sample Program

Let's look at a simple example. The MENUDEMO program, shown in Figure 9-3,
has five items in the main menu--File, Edit, Background, Timer, and Help.
Each of these items has a popup. MENUDEMO does the simplest and most common
type of menu processing, which involves trapping WM_COMMAND messages and
checking the value of wParam.

 MENUDEMO.MAK

#------------------------
# MENUDEMO.MAK make file
#------------------------

menudemo.exe : menudemo.obj menudemo.def menudemo.res
     link menudemo, /align:16, NUL, /nod slibcew libw, menudemo
     rc menudemo.res

menudemo.obj : menudemo.c menudemo.h
     cl -c -Gsw -Ow -W2 -Zp menudemo.c

menudemo.res : menudemo.rc menudemo.h
     rc -r menudemo.rc

 MENUDEMO.C

/*-----------------------------------------
   MENUDEMO.C -- Menu Demonstration
                 (c) Charles Petzold, 1990
  -----------------------------------------*/

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

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

char szAppName [] = "MenuDemo" ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     HWND     hwnd ;
     MSG      msg ;
     WNDCLASS wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = szAppName ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "Menu Demonstration",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;     ShowWindow
(hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static int  wColorID [5] = { WHITE_BRUSH,  LTGRAY_BRUSH, GRAY_BRUSH,
                                  DKGRAY_BRUSH, BLACK_BRUSH } ;
     static WORD wSelection = IDM_WHITE ;
     HMENU       hMenu ;

     switch (message)
          {
          case WM_COMMAND :
               hMenu = GetMenu (hwnd) ;

               switch (wParam)
                    {
                    case IDM_NEW :
                    case IDM_OPEN :
                    case IDM_SAVE :
                    case IDM_SAVEAS :
                         MessageBeep (0) ;
                         return 0 ;

                    case IDM_EXIT :
                         SendMessage (hwnd, WM_CLOSE, 0, 0L) ;
                         return 0 ;

                    case IDM_UNDO :
                    case IDM_CUT :
                    case IDM_COPY :
                    case IDM_PASTE :
                    case IDM_CLEAR :
                         MessageBeep (0) ;
                         return 0 ;

                    case IDM_WHITE :          // Note: Logic below
                    case IDM_LTGRAY :         //   assumes that IDM_WHITE
                    case IDM_GRAY :           //   through IDM_BLACK are
                    case IDM_DKGRAY :         //   consecutive numbers in
                    case IDM_BLACK :          //   the order shown here.
CheckMenuItem (hMenu, wSelection, MF_UNCHECKED) ;
                         wSelection = wParam ;
                         CheckMenuItem (hMenu, wSelection, MF_CHECKED) ;

                         SetClassWord (hwnd, GCW_HBRBACKGROUND,
                              GetStockObject (wColorID [wParam -
IDM_WHITE])) ;

                         InvalidateRect (hwnd, NULL, TRUE) ;
                         return 0 ;

                    case IDM_START :
                         if (SetTimer (hwnd, 1, 1000, NULL))
                              {
                              EnableMenuItem (hMenu, IDM_START, MF_GRAYED) ;
                              EnableMenuItem (hMenu, IDM_STOP,  MF_ENABLED)
;
                              }
                         return 0 ;

                    case IDM_STOP :
                         KillTimer (hwnd, 1) ;
                         EnableMenuItem (hMenu, IDM_START, MF_ENABLED) ;
                         EnableMenuItem (hMenu, IDM_STOP,  MF_GRAYED) ;
                         return 0 ;

                    case IDM_HELP :
                         MessageBox (hwnd, "Help not yet implemented.",
                                     szAppName, MB_ICONINFORMATION | MB_OK)
;
                         return 0 ;

                    case IDM_ABOUT :
                         MessageBox (hwnd, "Menu Demonstration Program.",
                                     szAppName, MB_ICONINFORMATION | MB_OK)
;
                         return 0 ;
                    }
               break ;

          case WM_TIMER :
               MessageBeep (0) ;
               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 MENUDEMO.RC

/*-----------------------------
   MENUDEMO.RC resource script
  -----------------------------*/

#include "menudemo.h"

MenuDemo MENU
     {
     POPUP "&File"
          {
          MENUITEM "&New",                   IDM_NEW
          MENUITEM "&Open...",               IDM_OPEN
          MENUITEM "&Save",                  IDM_SAVE
          MENUITEM "Save &As...",            IDM_SAVEAS
          MENUITEM SEPARATOR
          MENUITEM "E&xit",                  IDM_EXIT
          }
     POPUP "&Edit"
          {
          MENUITEM "&Undo",                  IDM_UNDO
          MENUITEM SEPARATOR
          MENUITEM "Cu&t",                   IDM_CUT
          MENUITEM "&Copy",                  IDM_COPY
          MENUITEM "&Paste",                 IDM_PASTE
          MENUITEM "C&lear",                 IDM_CLEAR
          }
     POPUP "&Background"
          {
          MENUITEM "&White",                 IDM_WHITE, CHECKED
          MENUITEM "&Lt Gray",               IDM_LTGRAY
          MENUITEM "&Gray",                  IDM_GRAY
          MENUITEM "&Dk Gray",               IDM_DKGRAY
          MENUITEM "&Black",                 IDM_BLACK
          }
     POPUP "&Timer"
          {
          MENUITEM "&Start"                  IDM_START
          MENUITEM "S&top"                   IDM_STOP,  GRAYED
          }
     POPUP "&Help"
          {
          MENUITEM "&Help",                  IDM_HELP
          MENUITEM "&About MenuDemo...",     IDM_ABOUT
          }
     }

;FC

 MENUDEMO.H

/*------------------------
   MENUDEMO.H header file
  ------------------------*/

#define IDM_NEW      1
#define IDM_OPEN     2
#define IDM_SAVE     3
#define IDM_SAVEAS   4
#define IDM_EXIT     5

#define IDM_UNDO    10
#define IDM_CUT     11
#define IDM_COPY    12
#define IDM_PASTE   13
#define IDM_CLEAR   14

#define IDM_WHITE   20
#define IDM_LTGRAY  21
#define IDM_GRAY    22
#define IDM_DKGRAY  23
#define IDM_BLACK   24

#define IDM_START   30
#define IDM_STOP    31

#define IDM_HELP    40
#define IDM_ABOUT   41

 MENUDEMO.DEF

;-------------------------------------
; MENUDEMO.DEF module definition file
;-------------------------------------

NAME           MENUDEMO

DESCRIPTION    'Menu Demonstration Program (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

Identifiers for all menu IDs are defined in MENUDEMO.H. This file must be
specified (using a #include statement) in both the resource script file and
the C source code file. The identifiers begin with IDM. (The ID numbers
defined for the menu items need not be consecutive. However, if you process
these IDs in your program using switch and case statements, keep in mind
that the C compiler can best optimize this code using jump tables if you use
consecutive menu ID numbers.)

The MENUDEMO program simply beeps when it receives a WM_COMMAND message for
most items in the File and Edit popups. The Background popup lists five
stock brushes that MENUDEMO can use to color the background. In the
MENUDEMO.RC resource script the White menu item (with a menu ID of
IDM_WHITE) is flagged as CHECKED, which places a check mark next to the
item. In MENUDEMO.C, the value of wSelection is initially set to IDM_WHITE.

The five brushes on the Background popup are mutually exclusive. When
MENUDEMO.C receives a WM_COMMAND message where wParam is one of these five
items on the Background popup, it must remove the check mark from the
previously chosen background color and add a check mark to the new
background color. To do this, it first gets a handle to its menu:

hMenu = GetMenu (hwnd) ;

The CheckMenuItem function is used to uncheck the currently checked item:

CheckMenuItem (hMenu, wSelection, MF_UNCHECKED) ;

The wSelection value is set to the value of wParam, and the new background
color is checked:

wSelection = wParam ;
CheckMenuItem (hMenu, wSelection, MF_CHECKED) ;

The background color in the window class is then replaced with the new
background color, and the window client area is invalidated. Windows erases
the window using the new background color.

The Timer popup lists two options--Start and Stop. Initially, the Stop
option is grayed (as indicated in the menu definition for the resource
script). When you choose the Start option, MENUDEMO tries to start a timer
and, if successful, grays the Start option and makes the Stop option active:

EnableMenuItem (hMenu, IDM_START, MF_GRAYED) ;
EnableMenuItem (hMenu, IDM_STOP,  MF_ENABLED) ;

On receipt of a WM_COMMAND message with wParam equal to IDM_STOP, MENUDEMO
kills the timer, activates the Start option, and grays the Stop option:

EnableMenuItem (hMenu, IDM_START, MF_ENABLED) ;
EnableMenuItem (hMenu, IDM_STOP,  MF_GRAYED) ;

Notice that it's impossible for MENUDEMO to receive a WM_COMMAND message
with wParam equal to IDM_START when the timer is going. Similarly, it's
impossible to receive a WM_COMMAND with wParam equal to IDM_STOP when the
timer is not going.

When MENUDEMO receives a WM_COMMAND message with the wParam parameter equal
to IDM_ABOUT or IDM_HELP, it displays a message box. (In Chapter 10 we'll
change this to a dialog box.)

When MENUDEMO receives a WM_COMMAND message with wParam equal to IDM_EXIT,
it sends itself a WM_CLOSE message. This is the same message that
DefWindowProc sends the window procedure when it receives a WM_SYSCOMMAND
message with wParam equal to SC_CLOSE. We'll examine this more in the
POPPAD2 program shown toward the end of this chapter.


Menu Etiquette

The format of the File and Edit popups in MENUDEMO follows the
recommendations of the CUA Advanced Interface Design Guide. Many Windows
programs have File and Edit popups. One of the objectives of Windows is to
provide a user with a recognizable interface that does not require
relearning basic concepts for each program. It certainly helps if the File
and Edit menus look the same in every Windows program and use the same
letters for selection with the Alt key.

Beyond the File and Edit popups, the menus of most Windows programs will be
different. When designing a menu you should look at existing Windows
programs and aim for some consistency. Of course, if you think these other
programs are wrong and you know the right way to do it, nobody's going to
stop you. Also keep in mind that revising a menu usually requires revising
only the resource script and not your program code. You can move menu items
around at a later time without many problems.

At the beginning of this chapter, I showed you a menu with nine top-level
items but with only one popup that is invoked from the top-level menu. This
menu is certainly atypical. Most often, each top-level item has a popup,
even if the popup has only one option. Top-level items without popups can be
too easily chosen by mistake.


Defining a Menu the Hard Way

Defining a menu in a program's resource script is usually the easiest way to
add a menu in your window, but it's not the only way. You can dispense with
the resource script and create a menu entirely within your program using two
functions called CreateMenu and AppendMenu. After you finish defining the
menu, you can pass the menu handle to CreateWindow or use SetMenu to set the
window's menu.

Here's how it's done. CreateMenu simply returns a handle to a new menu:

hMenu = CreateMenu () ;

The menu is initially empty. AppendMenu inserts items into the menu. You
must obtain a different menu handle for the top-level menu item and for each
popup. The popups are constructed separately; the popup menu handles are
then inserted into the top-level menu. The code shown in Figure 9-4 creates
a menu in this fashion; in fact, it is the same menu as in the MENUDEMO
program.

hMenu = CreateMenu () ;

hMenuPopup = CreateMenu () ;

AppendMenu (hMenuPopup, MF_STRING,    IDM_NEW,    "&New") ;
AppendMenu (hMenuPopup, MF_STRING,    IDM_OPEN,   "&Open...") ;
AppendMenu (hMenuPopup, MF_STRING,    IDM_SAVE,   "&Save") ;
AppendMenu (hMenuPopup, MF_STRING,    IDM_SAVEAS, "Save &As...") ;
AppendMenu (hMenuPopup, MF_SEPARATOR, 0,          NULL) ;
AppendMenu (hMenuPopup, MF_STRING,    IDM_EXIT,   "E&xit") ;

AppendMenu (hMenu, MF_POPUP, hMenuPopup, "&File") ;

hMenuPopup = CreateMenu () ;

AppendMenu (hMenuPopup, MF_STRING,    IDM_UNDO,  "&Undo") ;
AppendMenu (hMenuPopup, MF_SEPARATOR, 0,         NULL) ;
AppendMenu (hMenuPopup, MF_STRING,    IDM_CUT,   "Cu&t") ;
AppendMenu (hMenuPopup, MF_STRING,    IDM_COPY,  "&Copy") ;
AppendMenu (hMenuPopup, MF_STRING,    IDM_PASTE, "&Paste") ;
AppendMenu (hMenuPopup, MF_STRING,    IDM_CLEAR, "C&lear") ;

AppendMenu (hMenu, MF_POPUP, hMenuPopup, "&Edit") ;

hMenuPopup = CreateMenu () ;

AppendMenu (hMenuPopup, MF_STRING | MF_CHECKED, IDM_WHITE,  "&White") ;
AppendMenu (hMenuPopup, MF_STRING,              IDM_LTGRAY, "&Lt Gray") ;
AppendMenu (hMenuPopup, MF_STRING,              IDM_GRAY,   "&Gray") ;
AppendMenu (hMenuPopup, MF_STRING,              IDM_DKGRAY, "&Dk Gray") ;
AppendMenu (hMenuPopup, MF_STRING,              IDM_BLACK,  "&Black") ;

AppendMenu (hMenu, MF_POPUP, hMenuPopup, "&Background") ;

hMenuPopup = CreateMenu () ;

AppendMenu (hMenuPopup, MF_STRING,             IDM_START, "&Start") ;
AppendMenu (hMenuPopup, MF_STRING | MF_GRAYED, IDM_STOP,  "S&top") ;

AppendMenu (hMenu, MF_POPUP, hMenuPopup, "&Timer") ;

hMenuPopup = CreateMenu () ;

AppendMenu (hMenuPopup, MF_STRING, IDM_HELP,  "&Help") ;
AppendMenu (hMenuPopup, MF_STRING, IDM_ABOUT, "&About MenuDemo...") ;

AppendMenu (hMenu, MF_POPUP, hMenuPopup, "&Help") ;

I think you'll agree that the resource script menu template is easier and
clearer. I'm not recommending that you define a menu in this way, only
showing that it can be done. Certainly you can cut down on the code size
substantially by using some arrays of structures containing all the menu
item character strings, IDs, and flags. But if you do that, you might as
well take advantage of the third method Windows provides for defining a
menu.


A Third Approach to Defining Menus

The LoadMenuIndirect function accepts a pointer to a structure of type
MENUITEMTEMPLATE and returns a handle to a menu. This function is used
within Windows to construct a menu after loading the normal menu template
from a resource script. If you're brave, you can try using it yourself.

Be forewarned, however: The MENUITEMTEMPLATE structure has a field defined
as LPSTR that is set to a far pointer to a character string, a handle to a
popup, or a handle to a bitmap. But you can't simply define a
MENUITEMTEMPLATE structure in your program and initialize the field to a
character string. During compilation, the pointer to the character string is
converted to a far pointer. This violates one of the most important rules
discussed in Chapter 7: Don't store far pointers to your data segment.
Instead, immediately before calling LoadMenuIndirect, you use a series of
assignment statements to set this field to the character string pointers.
Between these assignment statements and the LoadMenuIndirect call, you can't
make any Windows calls that can result in your data segment being moved
(such as GetMessage).


Floating Popup Menus

Beginning with Windows 3, you can make use of menus without having a
top-level menu bar. You can instead cause a popup menu to appear on top of
any part of the screen. One approach is to invoke this popup menu in
response to a click of the right mouse button. However, menu items must
still be selected with the left mouse button. The POPMENU program in Figure
9-5 (beginning on the following page) shows how this is done.

 POPMENU.MAK

#-----------------------
# POPMENU.MAK make file
#-----------------------

popmenu.exe : popmenu.obj popmenu.def popmenu.res
     link popmenu, /align:16, NUL, /nod slibcew libw, popmenu
     rc popmenu.res

popmenu.obj : popmenu.c popmenu.h
     cl -c -Gsw -Ow -W2 -Zp popmenu.c

popmenu.res : popmenu.rc popmenu.h
     rc -r popmenu.rc

 POPMENU.C

/*----------------------------------------
   POPMENU.C -- Popup Menu Demonstration
                (c) Charles Petzold, 1990
  ----------------------------------------*/

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

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

char   szAppName [] = "PopMenu" ;
HANDLE hInst ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     HWND     hwnd ;
     MSG      msg ;
     WNDCLASS wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;



          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hInst = hInstance ;

     hwnd = CreateWindow (szAppName, "Popup Menu Demonstration",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static HMENU hMenu ;
     static int   wColorID [5] = { WHITE_BRUSH,  LTGRAY_BRUSH, GRAY_BRUSH,
                                   DKGRAY_BRUSH, BLACK_BRUSH } ;
     static WORD  wSelection = IDM_WHITE ;
     POINT        point ;

     switch (message)
          {
          case WM_CREATE :
               hMenu = LoadMenu (hInst, szAppName) ;
               hMenu = GetSubMenu (hMenu, 0) ;
               return 0 ;

          case WM_RBUTTONDOWN :
               point = MAKEPOINT (lParam) ;
               ClientToScreen (hwnd, &point) ;

               TrackPopupMenu (hMenu, 0, point.x, point.y, 0, hwnd, NULL) ;
               return 0 ;

          case WM_COMMAND :
               switch (wParam)
                    {                    case IDM_NEW :
                    case IDM_OPEN :
                    case IDM_SAVE :
                    case IDM_SAVEAS :
                    case IDM_UNDO :
                    case IDM_CUT :
                    case IDM_COPY :
                    case IDM_PASTE :
                    case IDM_CLEAR :
                         MessageBeep (0) ;
                         return 0 ;

                    case IDM_WHITE :          // Note: Logic below
                    case IDM_LTGRAY :         //   assumes that IDM_WHITE
                    case IDM_GRAY :           //   through IDM_BLACK are
                    case IDM_DKGRAY :         //   consecutive numbers in
                    case IDM_BLACK :          //   the order shown here.

                         CheckMenuItem (hMenu, wSelection, MF_UNCHECKED) ;
                         wSelection = wParam ;
                         CheckMenuItem (hMenu, wSelection, MF_CHECKED) ;

                         SetClassWord (hwnd, GCW_HBRBACKGROUND,
                              GetStockObject (wColorID [wParam -
IDM_WHITE])) ;

                         InvalidateRect (hwnd, NULL, TRUE) ;
                         return 0 ;

                    case IDM_ABOUT :
                         MessageBox (hwnd, "Popup Menu Demonstration
Program.",
                                     szAppName, MB_ICONINFORMATION | MB_OK)
;
                         return 0 ;

                    case IDM_EXIT :
                         SendMessage (hwnd, WM_CLOSE, 0, 0L) ;
                         return 0 ;

                    case IDM_HELP :
                         MessageBox (hwnd, "Help not yet implemented.",
                                     szAppName, MB_ICONINFORMATION | MB_OK)
;
                         return 0 ;
                    }
               break ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 POPMENU.RC

/*----------------------------
   POPMENU.RC resource script
  ----------------------------*/

#include "popmenu.h"

PopMenu MENU
     {
     POPUP ""
          {
          POPUP "&File"
               {
               MENUITEM "&New",               IDM_NEW
               MENUITEM "&Open...",           IDM_OPEN
               MENUITEM "&Save",              IDM_SAVE
               MENUITEM "Save &As...",        IDM_SAVEAS
               MENUITEM SEPARATOR
               MENUITEM "E&xit",              IDM_EXIT
               }
          POPUP "&Edit"
               {
               MENUITEM "&Undo",              IDM_UNDO
               MENUITEM SEPARATOR
               MENUITEM "Cu&t",               IDM_CUT
               MENUITEM "&Copy",              IDM_COPY
               MENUITEM "&Paste",             IDM_PASTE
               MENUITEM "C&lear",             IDM_CLEAR
               }
          POPUP "&Background"
               {
               MENUITEM "&White",             IDM_WHITE, CHECKED
               MENUITEM "&Lt Gray",           IDM_LTGRAY
               MENUITEM "&Gray",              IDM_GRAY
               MENUITEM "&Dk Gray",           IDM_DKGRAY
               MENUITEM "&Black",             IDM_BLACK
               }
          POPUP "&Help"
               {
               MENUITEM "&Help",              IDM_HELP
               MENUITEM "&About PopMenu...",  IDM_ABOUT
               }
          }
     }

 POPMENU.H

/*-----------------------
   POPMENU.H header file
  -----------------------*/

#define IDM_NEW      1
#define IDM_OPEN     2
#define IDM_SAVE     3
#define IDM_SAVEAS   4
#define IDM_EXIT     5

#define IDM_UNDO    10
#define IDM_CUT     11
#define IDM_COPY    12
#define IDM_PASTE   13
#define IDM_CLEAR   14

#define IDM_WHITE   20
#define IDM_LTGRAY  21
#define IDM_GRAY    22
#define IDM_DKGRAY  23
#define IDM_BLACK   24

#define IDM_HELP    30
#define IDM_ABOUT   31

 POPMENU.DEF

;------------------------------------
; POPMENU.DEF module definition file
;------------------------------------

NAME           POPMENU

DESCRIPTION    'Popup Menu Demonstration Program (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

The POPMENU.RC resource script defines a menu very similar to the one in
MENUDEMO.RC. The difference is that the top-level menu contains only one
item--a popup that invokes the File, Edit, Background, and Help options.

During the WM_CREATE message in WndProc, POPMENU obtains a handle to this
popup menu:

hMenu = LoadMenu (hInst, szAppName) ;
hMenu = GetSubMenu (hMenu, 0) ;

During the WM_RBUTTONDOWN message, POPMENU obtains the position of the mouse
point, coverts the position to screen coordinates, and passes the
coordinates to TrackPopupMenu:

point = MAKEPOINT (lParam) ;
ClientToScreen (hwnd, &point) ;

TrackPopupMenu (hMenu, 0, point.x, point.y, 0, hwnd, NULL) ;

Windows then displays the popup menu with the items File, Edit, Background,
and Help. Selecting any of these options causes the nested popup menus to
appear to the right. The menu functions the same as a normal menu.


Using the System Menu

Parent windows created with a style that includes WS_SYSMENU have a system
menu box at the left of the caption bar. If you like, you can modify this
menu. For instance, you can add your own menu commands to the system menu.
While this is not recommended, modifying the system menu is often a
quick-and-dirty way to add a menu to a short program without defining it in
the resource script. The only restriction is this: The ID numbers you use to
add commands to the system menu must be lower than F000H. Otherwise, they
will conflict with the IDs that Windows uses for the normal system menu
commands. And remember: When you process WM_SYSCOMMAND messages in your
window procedure for these new menu items, you must pass the other
WM_SYSCOMMAND messages to DefWindowProc. If you don't, you'll effectively
disable all normal options on the system menu.

The program POORMENU ("poor person's menu"), shown in Figure 9-6 beginning
on the following page, adds a separator bar and three commands to the system
menu. The last of these commands removes the additions.

 POORMENU.MAK

#------------------------
# POORMENU.MAK make file
#------------------------

poormenu.exe : poormenu.obj poormenu.def
     link poormenu, /align:16, NUL, /nod slibcew libw, poormenu
     rc poormenu.exe

poormenu.obj : poormenu.c
     cl -c -Gsw -Ow -W2 -Zp poormenu.c

 POORMENU.C

/*-----------------------------------------
   POORMENU.C -- The Poor Person's Menu
                 (c) Charles Petzold, 1990
  -----------------------------------------*/

#include <windows.h>

#define IDM_ABOUT   1
#define IDM_HELP    2
#define IDM_REMOVE  3

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

static char szAppName [] = "PoorMenu" ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     HMENU    hMenu ;
     HWND     hwnd ;
     MSG      msg ;
     WNDCLASS wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;


          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "The Poor Person's Menu",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     hMenu = GetSystemMenu (hwnd, FALSE) ;

     AppendMenu (hMenu, MF_SEPARATOR, 0,          NULL) ;
     AppendMenu (hMenu, MF_STRING,    IDM_ABOUT,  "About...") ;
     AppendMenu (hMenu, MF_STRING,    IDM_HELP,   "Help...") ;
     AppendMenu (hMenu, MF_STRING,    IDM_REMOVE, "Remove Additions") ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     switch (message)
          {
          case WM_SYSCOMMAND :
               switch (wParam)
                    {
                    case IDM_ABOUT :
                         MessageBox (hwnd, "The Poor Person's Menu
Program.",
                                     szAppName, MB_OK | MB_ICONEXCLAMATION)
;
                         return 0 ;

                    case IDM_HELP :
                         MessageBox (hwnd, "Help not yet implemented.",
                                     szAppName, MB_OK | MB_ICONEXCLAMATION)
;
                         return 0 ;
                    case IDM_REMOVE :
                         GetSystemMenu (hwnd, TRUE) ;
                         return 0 ;
                    }
               break ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 POORMENU.DEF

;-------------------------------------
; POORMENU.DEF module definition file
;-------------------------------------

NAME           POORMENU

DESCRIPTION    'The Poor Person's Menu (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

The three menu IDs are defined near the top of POORMENU.C:

#define IDM_ABOUT   1
#define IDM_HELP    2
#define IDM_REMOVE  3

After the program's window has been created, POORMENU obtains a handle to
the system menu:

hMenu = GetSystemMenu (hwnd, FALSE) ;

When you first call GetSystemMenu, you should set the second parameter to
FALSE in preparation for modifying the menu.

The menu is altered with four AppendMenu calls:

AppendMenu (hMenu, MF_SEPARATOR, 0,          NULL) ;
AppendMenu (hMenu, MF_STRING,    IDM_ABOUT,  "About...") ;
AppendMenu (hMenu, MF_STRING,    IDM_HELP,   "Help...") ;
AppendMenu (hMenu, MF_STRING,    IDM_REMOVE, "Remove Additions") ;

The first AppendMenu call adds the separator bar. Choosing the Remove
Additions menu item causes POORMENU to remove these additions, which it
accomplishes simply by calling GetSystemMenu again with the second parameter
set to TRUE:

GetSystemMenu (hwnd, TRUE) ;

The standard system menu has the options Restore, Move, Size, Minimize,
Maximize, Close, and Switch To. These generate WM_SYSCOMMAND messages with
wParam equal to SC_RESTORE, SC_MOVE, SC_SIZE, SC_MINIMUM, SC_MAXIMUM,
SC_CLOSE, and SC_TASKLIST. Although Windows programs do not normally do so,
you can process these messages yourself rather than pass them on to
DefWindowProc. You can also disable or remove some of these standard options
from the system menu using methods described below. The Windows
documentation also includes some standard additions to the system menu.
These use the identifiers SC_NEXTWINDOW, SC_PREVWINDOW, SC_VSCROLL,
SC_HSCROLL, and SC_ARRANGE. You might find it appropriate to add these
commands to the system menu in some applications.


Changing the Menu

We've already seen how the AppendMenu function can be used to define a menu
entirely within a program and to add menu items to the system menu. Prior to
Windows 3, you would have been forced to use the ChangeMenu function for
this job. ChangeMenu was so versatile that it was one of the most complex
functions in all of Windows. In Windows 3, ChangeMenu is still available,
but its functionality has been divided among five new functions:

  ■   AppendMenu adds a new item to the end of a menu.

  ■   DeleteMenu deletes an existing item from a menu and destroys the item.

  ■   InsertMenu inserts a new item into a menu.

  ■   ModifyMenu changes an existing menu item.

  ■   RemoveMenu removes an existing item from a menu.

The difference between DeleteMenu and RemoveMenu is important if the item is
a popup menu. DeleteMenu destroys the popup menu--but RemoveMenu does not.


Other Menu Commands

Here are some more functions useful for working with menus:

When you change a top-level menu item, the change is not shown until Windows
redraws the menu bar. You can force this redrawing by calling:

DrawMenuBar (hwnd) ;

Note that the parameter to DrawMenuBar is a handle to the window rather than
a handle to the menu.

You can obtain the handle of a popup menu using:

hMenuPopup = GetSubMenu (hMenu, nPos) ;

where nPos is the index (starting at 0) of the popup within the top-level
menu indicated by hMenu. You can then use the popup menu handle with other
functions (such as AppendMenu).

You can obtain the current number of items in a top-level or popup menu
using:

nCount = GetMenuItemCount (hMenu) ;

You can obtain the menu ID for an item in a popup menu from:

wID = GetMenuItemID (hMenuPopup, nPosition) ;

where nPosition is the position (starting at 0) of the item within the
popup.

In MENUDEMO you saw how to check or uncheck an item in a popup menu using:

CheckMenuItem (hMenu, wID, wCheck) ;

In MENUDEMO, hMenu was the handle to the top-level menu, wID was the menu
ID, and the value of wCheck was either MF_CHECKED or MF_UNCHECKED. If hMenu
is a handle to a popup menu, then the wID parameter can be a positional
index rather than a menu ID. If an index is more convenient, you include
MF_BYPOSITION in the third parameter. For instance:

CheckMenuItem (hMenu, nPosition, MF_CHECKED | MF_BYPOSITION) ;

The EnableMenuItem function works similarly to CheckMenuItem except the
third parameter is MF_ENABLED, MF_DISABLED, or MF_GRAYED. If you use
EnableMenuItem on a top-level menu item that has a popup, you must also use
the MF_BYPOSITION identifier in the third parameter because the menu item
has no menu ID. We'll see an example of EnableMenuItem in the POPPAD program
shown later in this chapter. HiliteMenuItem is similar to CheckMenuItem and
EnableMenuItem but uses MF_HILITE and MF_UNHILITE. This highlighting is the
reverse video that Windows uses when you move among menu items. You do not
normally need to use HiliteMenuItem.

What else do you need to do with your menu? Have you forgotten what
character string you used in a menu? You can refresh your memory by calling:

nByteCount = GetMenuString (hMenu, wID, lpString, nMaxCount, wFlag) ;

The wFlag is either MF_BYCOMMAND (where wID is a menu ID) or MF_BYPOSITION
(wID is a positional index). The function copies up to nMaxCount bytes of
the text string into lpString and returns the number of bytes copied.

Or perhaps you'd like to know what the current flags of a menu item are:

wFlags = GetMenuState (hMenu, wID, wFlag) ;

Again, wFlag is either MF_BYCOMMAND or MF_BYPOSITION. The wFlags parameter
is a combination of all the current flags. You can determine them by testing
against the MF_DISABLED, MF_GRAYED, MF_CHECKED, MF_MENUBREAK,
MF_MENUBARBREAK, and MF_SEPARATOR identifiers.

Or maybe by this time you're a little fed up with menus. In that case you'll
be pleased to know that if you no longer need a menu in your program, you
can destroy it:

DestroyMenu (hMenu) ;

This invalidates the menu handle.


An Unorthodox Approach to Menus

Now let's step a little off the beaten path. Instead of having drop-down
menus in your program, how about creating multiple top-level menus without
any popups and switching between the top-level menus using the SetMenu call?
The NOPOPUPS program, shown in Figure 9-7, demonstrates how to do it. This
program includes similar File and Edit items that MENUDEMO uses but displays
them as alternate top-level menus.

 NOPOPUPS.MAK

#------------------------
# NOPOPUPS.MAK make file
#------------------------

nopopups.exe : nopopups.obj nopopups.def nopopups.res
     link nopopups, /align:16, NUL, /nod slibcew libw, nopopups
     rc nopopups.res

nopopups.obj : nopopups.c nopopups.h
     cl -c -Gsw -Ow -W2 -Zp nopopups.c

nopopups.res : nopopups.rc nopopups.h
     rc -r nopopups.rc

 NOPOPUPS.C

/*-------------------------------------------------
   NOPOPUPS.C -- Demonstrates No-Popup Nested Menu
                 (c) Charles Petzold, 1990
  -------------------------------------------------*/

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

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName [] = "NoPopUps" ;
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "No-Popup Nested Menu Demonstration",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static HMENU hMenuMain, hMenuEdit, hMenuFile ;
     HANDLE       hInstance ;

     switch (message)
          {
          case WM_CREATE :
               hInstance = GetWindowWord (hwnd, GWW_HINSTANCE) ;

               hMenuMain = LoadMenu (hInstance, "MenuMain") ;
               hMenuFile = LoadMenu (hInstance, "MenuFile") ;
               hMenuEdit = LoadMenu (hInstance, "MenuEdit") ;

               SetMenu (hwnd, hMenuMain) ;
               return 0 ;

          case WM_COMMAND :
               switch (wParam)
                    {
                    case IDM_MAIN :
                         SetMenu (hwnd, hMenuMain) ;
                         return 0 ;

                    case IDM_FILE :
                         SetMenu (hwnd, hMenuFile) ;
                         return 0 ;

                    case IDM_EDIT :
                         SetMenu (hwnd, hMenuEdit) ;
                         return 0 ;

                    case IDM_NEW :
                    case IDM_OPEN :
                    case IDM_SAVE :
                    case IDM_SAVEAS :
                    case IDM_UNDO :
                    case IDM_CUT :
                    case IDM_COPY :
                    case IDM_PASTE :
                    case IDM_CLEAR :
                         MessageBeep (0) ;
                         return 0 ;
                    }
               break ;          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 NOPOPUPS.RC

/*-----------------------------
   NOPOPUPS.RC resource script
  -----------------------------*/

#include "nopopups.h"

MenuMain MENU
     {
     MENUITEM "MAIN:",         0,        INACTIVE
     MENUITEM "&File...",      IDM_FILE
     MENUITEM "&Edit...",      IDM_EDIT
     }

MenuFile MENU
     {
     MENUITEM "FILE:",         0,        INACTIVE
     MENUITEM "&New",          IDM_NEW
     MENUITEM "&Open...",      IDM_OPEN
     MENUITEM "&Save",         IDM_SAVE
     MENUITEM "Save &As...",   IDM_SAVEAS
     MENUITEM "(&Main)",       IDM_MAIN
     }

MenuEdit MENU
     {
     MENUITEM "EDIT:",         0,        INACTIVE
     MENUITEM "&Undo",         IDM_UNDO
     MENUITEM "Cu&t",          IDM_CUT
     MENUITEM "&Copy",         IDM_COPY
     MENUITEM "&Paste",        IDM_PASTE
     MENUITEM "C&lear",        IDM_CLEAR
     MENUITEM "(&Main)",       IDM_MAIN
     }

 NOPOPUPS.H

/*------------------------
   NOPOPUPS.H header file
  ------------------------*/

#define IDM_NEW      1
#define IDM_OPEN     2
#define IDM_SAVE     3
#define IDM_SAVEAS   4

#define IDM_UNDO     5
#define IDM_CUT      6
#define IDM_COPY     7
#define IDM_PASTE    8
#define IDM_CLEAR    9

#define IDM_MAIN    10
#define IDM_EDIT    11
#define IDM_FILE    12

 NOPOPUPS.DEF

;-------------------------------------
; NOPOPUPS.DEF module definition file
;-------------------------------------

NAME           NOPOPUPS

DESCRIPTION    'Demonstration of No-Popup Menu (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

The resource script has three menus rather than one. When the window
procedure processes the WM_CREATE message, Windows loads each of the menu
resources into memory:

hMenuMain = LoadMenu (hInstance, "MenuMain") ;
hMenuFile = LoadMenu (hInstance, "MenuFile") ;
hMenuEdit = LoadMenu (hInstance, "MenuEdit") ;

Initially, the program displays the main menu:

SetMenu (hwnd, hMenuMain) ;

The main menu lists the three options using the character strings "MAIN:",
"File...^^, and "Edit...^^ However, "MAIN:" is disabled, so it doesn't cause
WM_COMMAND messages to be sent to the window procedure. The File and Edit
menus begin "FILE:" and "EDIT:" to identify these as submenus. The last item
in each menu is the character string "(Main)"; this option indicates a
return to the main menu. Switching among these three menus is simple:

case WM_COMMAND :

     switch (wParam)
          {
          case IDM_MAIN :
               SetMenu (hwnd, hMenuMain) ;
               return 0 ;

          case IDM_FILE :
               SetMenu (hwnd, hMenuFile) ;
               return 0 ;

          case IDM_EDIT :
               SetMenu (hwnd, hMenuEdit) ;
               return 0 ;
[other program lines]
          }
     break ;



USING BITMAPS IN MENUS

Character strings are not the only way to display a menu item. You can also
use a bitmap. If you immediately recoiled at the thought of pictures of file
folders, paste jars, and trash cans in a menu, don't think of pictures.
Think instead of how useful menu bitmaps might be for a drawing program.
Think of using different fonts and font sizes, line widths, hatch patterns,
and colors in your menus.

The program we're going to examine is called GRAFMENU ("graphics menu"). The
top-level menu is shown in Figure 9-8. The enlarged block letters are
obtained from 40-by-16-pixel monochrome bitmap files created in SDKPAINT and
saved as .BMP files; they could be pictures instead. Choosing FONT from the
menu invokes a popup containing three options--Courier, Helvetica, and Times
Roman--each displayed in its respective font (Figure 9-9). These bitmaps
were created in the program using a technique involving a "memory device
context."

  (Figure 9-8. may be found in the printed book.)

  (Figure 9-9. may be found in the printed book.)

Finally, when you pull down the system menu, you see that you have access to
some "help" information, with the word "Help" perhaps mirroring the
desperation of a new user (Figure 9-10). This 64-by-64-pixel monochrome
bitmap was created in SDKPAINT.

  (Figure 9-10. may be found in the printed book.)

The GRAFMENU program, including the four bitmaps created in SDKPAINT, is
shown in Figure 9-11.

 GRAFMENU.MAK

#------------------------
# GRAFMENU.MAK make file
#------------------------

grafmenu.exe : grafmenu.obj grafmenu.def grafmenu.res
     link grafmenu, /align:16, NUL, /nod slibcew libw, grafmenu
     rc grafmenu.res

grafmenu.obj : grafmenu.c grafmenu.h
     cl -c -Gsw -Ow -W2 -Zp grafmenu.c

grafmenu.res : grafmenu.rc grafmenu.h \
               editlabl.bmp filelabl.bmp fontlabl.bmp bighelp.bmp
     rc -r grafmenu.rc

 GRAFMENU.C

/*----------------------------------------------
   GRAFMENU.C -- Demonstrates Bitmap Menu Items
                 (c) Charles Petzold, 1990
  ----------------------------------------------*/

#include <windows.h>
#include <string.h>
#include "grafmenu.h"

long FAR PASCAL WndProc  (HWND, WORD, WORD, LONG) ;
HBITMAP StretchBitmap (HBITMAP) ;
HBITMAP GetBitmapFont (int) ;

char szAppName [] = "GrafMenu" ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     HBITMAP  hBitmapHelp, hBitmapFile, hBitmapEdit,
              hBitmapFont, hBitmapPopFont [3] ;
     HMENU    hMenu, hMenuPopup ;
     HWND     hwnd ;
     int      i ;
     MSG      msg ;
     WNDCLASS wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hMenu = CreateMenu () ;

     hMenuPopup = LoadMenu (hInstance, "MenuFile") ;
     hBitmapFile = StretchBitmap (LoadBitmap (hInstance, "BitmapFile")) ;
     AppendMenu (hMenu, MF_BITMAP | MF_POPUP, hMenuPopup,
                 (LPSTR) (LONG) hBitmapFile) ;
     hMenuPopup = LoadMenu (hInstance, "MenuEdit") ;
     hBitmapEdit = StretchBitmap (LoadBitmap (hInstance, "BitmapEdit")) ;
     AppendMenu (hMenu, MF_BITMAP | MF_POPUP, hMenuPopup,
                 (LPSTR) (LONG) hBitmapEdit) ;

     hMenuPopup = CreateMenu () ;

     for (i = 0 ; i < 3 ; i++)
          {
          hBitmapPopFont [i] = GetBitmapFont (i) ;
          AppendMenu (hMenuPopup, MF_BITMAP, IDM_COUR + i,
                      (LPSTR) (LONG) hBitmapPopFont [i]) ;
          }

     hBitmapFont = StretchBitmap (LoadBitmap (hInstance, "BitmapFont")) ;
     AppendMenu (hMenu, MF_BITMAP | MF_POPUP, hMenuPopup,
                 (LPSTR) (LONG) hBitmapFont) ;

     hwnd = CreateWindow (szAppName, "Bitmap Menu Demonstration",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, hMenu, hInstance, NULL) ;

     hMenu = GetSystemMenu (hwnd, FALSE) ;
     hBitmapHelp = StretchBitmap (LoadBitmap (hInstance, "BitmapHelp")) ;
     AppendMenu (hMenu, MF_SEPARATOR, NULL,     NULL) ;
     AppendMenu (hMenu, MF_BITMAP,    IDM_HELP, (LPSTR) (LONG) hBitmapHelp)
;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }

     DeleteObject (hBitmapHelp) ;
     DeleteObject (hBitmapEdit) ;
     DeleteObject (hBitmapFile) ;
     DeleteObject (hBitmapFont) ;

     for (i = 0 ; i < 3 ; i++)
          DeleteObject (hBitmapPopFont [i]) ;

     return msg.wParam ;
     }

HBITMAP StretchBitmap (HBITMAP hBitmap1)
     {
     BITMAP     bm1, bm2 ;
     HBITMAP    hBitmap2 ;
     HDC        hdc, hdcMem1, hdcMem2 ;
     TEXTMETRIC tm ;

     hdc = CreateIC ("DISPLAY", NULL, NULL, NULL) ;
     GetTextMetrics (hdc, &tm) ;
     hdcMem1 = CreateCompatibleDC (hdc) ;
     hdcMem2 = CreateCompatibleDC (hdc) ;
     DeleteDC (hdc) ;

     GetObject (hBitmap1, sizeof (BITMAP), (LPSTR) &bm1) ;

     bm2 = bm1 ;
     bm2.bmWidth      = (tm.tmAveCharWidth * bm2.bmWidth)  / 4 ;
     bm2.bmHeight     = (tm.tmHeight       * bm2.bmHeight) / 8 ;
     bm2.bmWidthBytes = ((bm2.bmWidth + 15) / 16) * 2 ;

     hBitmap2 = CreateBitmapIndirect (&bm2) ;

     SelectObject (hdcMem1, hBitmap1) ;
     SelectObject (hdcMem2, hBitmap2) ;

     StretchBlt (hdcMem2, 0, 0, bm2.bmWidth, bm2.bmHeight,
                 hdcMem1, 0, 0, bm1.bmWidth, bm1.bmHeight, SRCCOPY) ;

     DeleteDC (hdcMem1) ;
     DeleteDC (hdcMem2) ;
     DeleteObject (hBitmap1) ;

     return hBitmap2 ;
     }

HBITMAP GetBitmapFont (int i)
     {
     static  struct
          {
          BYTE lfPitchAndFamily ;
          BYTE lfFaceName [LF_FACESIZE] ;
          char *szMenuText ;
          }
          lfSet [3] =
          {
          FIXED_PITCH    | FF_MODERN, "Courier",   "Courier",
          VARIABLE_PITCH | FF_SWISS,  "Helvetica", "Helvetica",
          VARIABLE_PITCH | FF_ROMAN,  "Tms Rmn",   "Times Roman"
          } ;
     DWORD   dwSize ;
     HBITMAP hBitmap ;
     HDC     hdc, hdcMem ;
     HFONT   hFont ;
     LOGFONT lf ;
     hFont = GetStockObject (SYSTEM_FONT) ;
     GetObject (hFont, sizeof (LOGFONT), (LPSTR) &lf) ;

     lf.lfHeight *= 2 ;
     lf.lfWidth  *= 2 ;
     lf.lfPitchAndFamily = lfSet[i].lfPitchAndFamily ;
     strcpy (lf.lfFaceName, lfSet[i].lfFaceName) ;

     hdc = CreateIC ("DISPLAY", NULL, NULL, NULL) ;
     hdcMem = CreateCompatibleDC (hdc) ;
     SelectObject (hdcMem, CreateFontIndirect (&lf)) ;
     dwSize = GetTextExtent (hdcMem, lfSet[i].szMenuText,
                             strlen (lfSet[i].szMenuText)) ;

     hBitmap = CreateBitmap (LOWORD (dwSize)-1, HIWORD (dwSize), 1, 1, NULL)
;
     SelectObject (hdcMem, hBitmap) ;


     TextOut (hdcMem, 0, 0, lfSet[i].szMenuText,
                            strlen (lfSet[i].szMenuText)) ;

     DeleteObject (SelectObject (hdcMem, hFont)) ;
     DeleteDC (hdcMem) ;
     DeleteDC (hdc) ;

     return hBitmap ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     HMENU  hMenu ;
     static short nCurrentFont = IDM_COUR ;

     switch (message)
          {
          case WM_CREATE :
               CheckMenuItem (GetMenu (hwnd), nCurrentFont, MF_CHECKED) ;
               return 0 ;

          case WM_SYSCOMMAND :
               switch (wParam)
                    {
                    case IDM_HELP :
                         MessageBox (hwnd, "Help not yet implemented.",
                                   szAppName, MB_OK | MB_ICONEXCLAMATION) ;
                         return 0 ;
                    }
               break ;
          case WM_COMMAND :
               switch (wParam)
                    {
                    case IDM_NEW :
                    case IDM_OPEN :
                    case IDM_SAVE :
                    case IDM_SAVEAS :
                    case IDM_UNDO :
                    case IDM_CUT :
                    case IDM_COPY :
                    case IDM_PASTE :
                    case IDM_CLEAR :
                         MessageBeep (0) ;
                         return 0 ;

                    case IDM_COUR :
                    case IDM_HELV :
                    case IDM_TMSRMN :
                         hMenu = GetMenu (hwnd) ;
                         CheckMenuItem (hMenu, nCurrentFont, MF_UNCHECKED) ;
                         nCurrentFont = wParam ;
                         CheckMenuItem (hMenu, nCurrentFont, MF_CHECKED) ;
                         return 0 ;
                    }
               break ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 GRAFMENU.RC

/*-----------------------------
   GRAFMENU.RC resource script
  -----------------------------*/

#include "grafmenu.h"

BitmapEdit BITMAP editlabl.bmp
BitmapFile BITMAP filelabl.bmp
BitmapFont BITMAP fontlabl.bmp
BitmapHelp BITMAP bighelp.bmpMenuFile MENU
     {
     MENUITEM "&New",        IDM_NEW
     MENUITEM "&Open...",    IDM_OPEN
     MENUITEM "&Save",       IDM_SAVE
     MENUITEM "Save &As...", IDM_SAVEAS
     }

MenuEdit MENU
     {
     MENUITEM "&Undo",       IDM_UNDO
     MENUITEM SEPARATOR
     MENUITEM "Cu&t",        IDM_CUT
     MENUITEM "&Copy",       IDM_COPY
     MENUITEM "&Paste",      IDM_PASTE
     MENUITEM "C&lear",      IDM_CLEAR
     }

 GRAFMENU.H

/*------------------------
   GRAFMENU.H header file
  ------------------------*/

#define IDM_NEW      1
#define IDM_OPEN     2
#define IDM_SAVE     3
#define IDM_SAVEAS   4

#define IDM_UNDO     5
#define IDM_CUT      6
#define IDM_COPY     7
#define IDM_PASTE    8
#define IDM_CLEAR    9

#define IDM_COUR    10
#define IDM_HELV    11
#define IDM_TMSRMN  12

#define IDM_HELP    13

 EDITLABL.BMP  -- Please refer to the book.

 FILELABL.BMP  -- Please refer to the book.

 FONTLABL.BMP  -- Please refer to the book.

 BIGHELP.BMP  -- Please refer to the book.

 GRAFMENU.DEF

;-------------------------------------
; GRAFMENU.DEF module definition file
;-------------------------------------

NAME           GRAFMENU

DESCRIPTION    'Demo of Bitmapped Menu Items (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

To examine the subject of bitmaps and menus in the detail it deserves, we'll
need to cross the border into GDI territory--a full exploration of which
awaits us in the next section of this book. The discussion here will serve
as a preview of topics that we'll return to again in Chapter 11.

Two Methods of Creating Bitmaps for Menus

To insert a bitmap into a menu, you use AppendMenu or InsertMenu. Where does
this bitmap come from? It can come from one of two places. First, you can
create a bitmap using SDKPAINT and include the bitmap file in your resource
script. Within the program, you can use LoadBitmap to load the bitmap
resource into memory and use AppendMenu or InsertMenu to attach it to the
menu. There's a problem with this approach, however. The bitmap will not be
suitable for all types of video resolutions and aspect ratios; you have to
stretch the loaded bitmap to account for this. Alternatively, you can create
the bitmap right in the program and attach it to the menu.

Both of these methods sound a lot more difficult than they actually are. We
don't have to mess around with the actual bits themselves. Windows provides
functions that let us manipulate bitmaps cleanly using something called the
"memory device context."


The Memory Device Context

When you use GDI calls (such as TextOut) to write on the client area of your
window, you're actually writing to a block of memory (the video display
memory) that is organized much like a giant bitmap. The width and height of
this bitmap are equal to the resolution of the video adapter. The manner in
which multiple bits define color is also defined by the video adapter.
Windows should also be able to pretend that a block of regular memory is
video display memory. It should be able to write on this memory the same way
it writes on the screen. We should then be able to use this block of memory
as a bitmap.

That's exactly what a memory device context is. It helps us fill up and
manipulate bitmaps in a Windows program. Here are the steps involved:

  1.  Create a memory device context using the CreateCompatibleDC call.
      Initially, the display surface of this memory device context contains
      one monochrome pixel. You can think of this device context as being 1
      pixel high and 1 pixel wide, with two colors (black and white).

  2.  Create an uninitialized bitmap using CreateBitmap,
      CreateBitmapIndirect, or CreateCompatibleBitmap. When you create the
      bitmap, you specify the height and width and the color organization.
      However, the pixels of the bitmap need not actually represent anything
      yet. Save the handle to the bitmap.

      3.

Select the bitmap into the memory device context using SelectObject. Now the
memory device context has a display surface that is the size of the bitmap
with the same number of colors as defined by the bitmap.

  Use GDI functions to draw on the memory device context the same way you
use GDI functions to draw on a normal device context. Anything you draw
within the display surface of the memory device context is actually drawn on
the bitmap selected into the device context.

  Delete the memory device context. You are left with a handle to a bitmap
that contains a pixel representation of what you drew on the memory device
context.


Creating a Bitmap with Text

The GetBitmapFont function in GRAFMENU takes a parameter of 0, 1, or 2 and
returns a handle to a bitmap. This bitmap contains the string "Courier,"
"Helvetica," or "Times Roman" in the appropriate font and about twice the
size of the normal system font. Let's see how GetBitmapFont does it. (The
code that follows is not the same as that in the GRAFMENU.C file. For
purposes of clarity, I've replaced references to the lfSet structure with
the values appropriate for Times Roman.)

The first step is to get a handle to the system font and use GetObject to
copy characteristics of that font into the structure lf that has type
LOGFONT ("logical font"):

hFont = GetStockObject (SYSTEM_FONT) ;
GetObject (hFont, sizeof (LOGFONT), (LPSTR) &lf) ;

Certain fields of this logical font structure must be modified to make it
describe a larger Times Roman font:

lf.lfHeight *= 2 ;
lf.lfWidth  *= 2 ;
lf.lfPitchAndFamily = VARIABLE_PITCH | FF_ROMAN ;
strcpy (lf.lfFaceName, "Tms Rmn") ;

The next step is to get a device context for the screen and create a memory
device context compatible with the screen:

hdc = CreateIC ("DISPLAY", NULL, NULL, NULL) ;
hdcMem = CreateCompatibleDC (hdc) ;

The handle to the memory device context is hdcMem. Next, we create a font
based on the modified lf structure and select that font into the memory
device context:

SelectObject (hdcMem, CreateFontIndirect (&lf)) ;

Now when we write some text to the memory device context, Windows will use
the Times Roman font selected into the device context.

But this memory device context still has a one-pixel monochrome device
surface. We have to create a bitmap large enough for the text we want to
display on it. You can obtain the dimensions of the text through
GetTextExtent and create a bitmap based on these dimensions with
CreateBitmap:

dwSize = GetTextExtent (hdcMem, "Times Roman", 11) ;
hBitmap = CreateBitmap (LOWORD (dwSize), HIWORD (dwSize), 1, 1, NULL) ;
SelectObject (hdcMem, hBitmap) ;

This device context now has a monochrome display surface exactly the size of
the text. Now all we have to do is write the text to it. You've seen this
function before:

TextOut (hdcMem, 0, 0, "Times Roman", 11) ;

We're finished except for cleaning up. To do so, we select the system font
(with handle hFont) back into the device context using SelectObject, and we
delete the previous font handle that SelectObject returns, which is the
handle to the Times Roman font:

DeleteObject (SelectObject (hdcMem, hFont)) ;

Now we can also delete the two device contexts:

DeleteDC (hdcMem) ;
DeleteDC (hdc) ;

We're left with a bitmap that has the text "Times Roman" in a Times Roman
font.


Scaling Bitmaps

The memory device context also comes to the rescue when we need to scale
fonts to a different display resolution or aspect ratio. I created the four
bitmaps used in GRAFMENU to be the correct size for a display that has a
system font height of 8 pixels and width of 4 pixels. For other system font
dimensions, the bitmap has to be stretched. This is done in GRAFMENU's
StretchBitmap function.

The first step is to get the device context for the screen, obtain the text
metrics for the system font, and create two memory device contexts:

hdc = CreateIC ("DISPLAY", NULL, NULL, NULL) ;
GetTextMetrics (hdc, &tm) ;
hdcMem1 = CreateCompatibleDC (hdc) ;
hdcMem2 = CreateCompatibleDC (hdc) ;
DeleteDC (hdc) ;

The bitmap handle passed to the function is hBitmap1. The program can obtain
the dimensions of this bitmap using GetObject:

GetObject (hBitmap1, sizeof (BITMAP), (LPSTR) &bm1) ;

This copies the dimensions into a structure bm1 of type BITMAP. The
structure bm2 is set equal to bm1, and then certain fields are modified
based on the system font dimensions:

bm2 = bm1 ;
bm2.bmWidth      = (tm.tmAveCharWidth * bm2.bmWidth)  / 4 ;
bm2.bmHeight     = (tm.tmHeight       * bm2.bmHeight) / 8 ;
bm2.bmWidthBytes = ((bm2.bmWidth + 15) / 16) * 2 ;

Then a new bitmap with handle hBitmap2 can be created based on the altered
dimensions:

hBitmap2 = CreateBitmapIndirect (&bm2) ;

You can then select these two bitmaps into the two memory display contexts:

SelectObject (hdcMem1, hBitmap1) ;
SelectObject (hdcMem2, hBitmap2) ;

We want to copy the first bitmap to the second bitmap and stretch it in the
process. This involves the StretchBlt call:

StretchBlt (hdcMem2, 0, 0, bm2.bmWidth, bm2.bmHeight,
            hdcMem1, 0, 0, bm1.bmWidth, bm1.bmHeight, SRCCOPY) ;

Now the second bitmap has the properly scaled bitmap. We'll use that one in
the menu. Cleanup is simple:

DeleteDC (hdcMem1) ;
DeleteDC (hdcMem2) ;
DeleteObject (hBitmap1) ;


Putting the Menu Together

GRAFMENU's WinMain function uses the StretchBitmap and GetBitmapFont
functions when constructing the menu. GRAFMENU has two menus already defined
in the resource script. These will become popups for the File and Edit
options.

GRAFMENU begins by obtaining a handle to an empty menu:

hMenu = CreateMenu () ;

The popup menu for File (containing the four options New, Open, Save, and
Save As) is loaded from the resource script:

hMenuPopup = LoadMenu (hInstance, "MenuFile") ;

The bitmap containing the word "FILE" is also loaded from the resource
script and stretched using StretchBitmap:

hBitmapFile = StretchBitmap (LoadBitmap (hInstance, "BitmapFile")) ;

The bitmap handle and popup menu handle become parameters in the ChangeMenu
call:

AppendMenu (hMenu, MF_BITMAP | MF_POPUP, hMenuPopup, (LPSTR) (LONG)
hBitmapFile) ;

The same procedure is followed for the Edit menu:

hMenuPopup = LoadMenu (hInstance, "MenuEdit") ;
hBitmapEdit = StretchBitmap (LoadBitmap (hInstance, "BitmapEdit")) ;
AppendMenu (hMenu, MF_BITMAP | MF_POPUP, hMenuPopup, (LPSTR) (LONG)
hBitmapEdit) ;

The popup menu for the three fonts is constructed from calls to the
GetBitmapFont function:

hMenuPopup = CreateMenu () ;
for (i = 0 ; i < 3 ; i++)
     {
     hBitmapPopFont [i] = GetBitmapFont (i) ;
     AppendMenu (hMenuPopup, MF_BITMAP,IDM_COUR + i,
                 (LPSTR) (LONG) hMenuPopupFont [i]) ;

The popup is then added to the menu:

hBitmapFont = StretchBitmap (LoadBitmap (hInstance, "BitmapFont")) ;
AppendMenu (hMenu, MF_BITMAP | MF_POPUP, hBitmapFont,
            (LONG) (LPSTR) hBitmapFont) ;

The window menu is complete. Now you can include hMenu in the CreateWindow
call:

hwnd = CreateWindow (szAppName, "Bitmap Menu Demonstration",
               WS_OVERLAPPED,
               CW_USEDEFAULT, CW_USEDEFAULT,
               CW_USEDEFAULT, CW_USEDEFAULT,
               NULL, hMenu, hInstance, NULL) ;

After hwnd is available, GRAFMENU can alter the system menu. GRAFMENU first
obtains a handle to it:

hMenu = GetSystemMenu (hwnd, FALSE) ;

This loads the "Help" bitmap and stretches it to an appropriate size:

hBitmapHelp = StretchBitmap (LoadBitmap (hInstance, "BitmapHelp")) ;

This adds a separator bar and the stretched bitmap to the system menu:

AppendMenu (hMenu, MF_SEPARATOR, 0, NULL) ;
AppendMenu (hMenu, MF_BITMAP, IDM_HELP, (LPSTR) (LONG) hBitmapHelp) ;

Remember that bitmaps are GDI objects and must be explicitly deleted before
your program terminates. You accomplish this after GRAFMENU exits from its
message loop:

DeleteObject (hBitmapHelp) ;
DeleteObject (hBitmapEdit) ;
DeleteObject (hBitmapFile) ;
DeleteObject (hBitmapFont) ;

for (i = 0 ; i < 3 ; i++)
     DeleteObject (hBitmapPopFont [i]) ;

I'll conclude this section with a couple of miscellaneous notes:

  ■   In a top-level menu, Windows adjusts the menu bar height to
      accommodate the tallest bitmap. Other bitmaps (or character strings)
      are aligned at the top of the menu bar. The size of the menu bar
      obtained from:

      GetSystemMetrics (SM_CYMENU)

      is no longer valid after you put bitmaps in a top-level window.

  ■   As you can see from playing with GRAFMENU, you can use check marks
      with bitmapped menu items in popups, but the check mark is of normal
      size. If that bothers you, you can create a customized check mark and
      use SetMenuItemBitmaps.

  ■   Another approach to using non-text (or text in a font other than the
      system font) on a menu is the "owner-draw" item. The Windows Guide to
      Programming discusses this approach.


Adding a Keyboard Interface

Now we have another problem. When the menu contains text, Windows
automatically adds a keyboard interface. You can select a menu item using
the Alt key in combination with a letter of the character string. But once
you put a bitmap in a menu, you've eliminated that keyboard interface. Even
if the bitmap says something, Windows doesn't know about it.

This is where the WM_MENUCHAR message comes in handy. Windows sends a
WM_MENUCHAR message to your window procedure when you press Alt with a
character key that does not correspond to a menu item. We need to intercept
WM_MENUCHAR messages and check the value of wParam (the ASCII character of
the pressed key). If this corresponds to a menu item, we have to return a
long integer back to Windows where the high word is set to 2 and the low
word is set to the index of the menu item we want associated with that key.
Windows does the rest.



KEYBOARD ACCELERATORS

Described as simply as possible, keyboard accelerators are key combinations
that generate WM_COMMAND (or in some cases WM_SYSCOMMAND) messages. Most
often, programs use keyboard accelerators to duplicate the action of common
menu options. (However, keyboard accelerators can also perform nonmenu
functions.) For instance, many Windows programs have an Edit menu that
includes a Cut option; these programs conventionally assign the Del key as a
keyboard accelerator for this option. The user can choose the Cut option
from the menu by pressing an Alt-key combination or can use the keyboard
accelerator by simply pressing the Del key. When the window procedure gets a
WM_COMMAND message, it does not have to determine whether the menu or the
keyboard accelerator was used.

Why You Should Use Keyboard Accelerators

You may ask: Why should I use keyboard accelerators? Why can't I simply trap
WM- _KEYDOWN or WM_CHAR messages and duplicate the menu functions myself?
What's the advantage? For a single-window application, you can certainly
trap keyboard messages, but you get certain advantages from using keyboard
accelerators: You don't need to duplicate the menu and keyboard accelerator
logic. If the keyboard accelerator duplicates a menu function, Windows
flashes the top-level item on the menu when a keyboard accelerator is used,
thus providing some visual feedback to the user.

For applications with multiple windows and multiple window procedures,
keyboard accelerators become very important. As we've seen, Windows sends
keyboard messages to the window procedure for the window that currently has
the input focus. For keyboard accelerators, however, Windows sends the
WM_COMMAND message to the window procedure whose handle is specified in the
Windows function TranslateAccelerator. Generally, this will be your main
window, the same window that has the menu, which means that the logic for
acting upon keyboard accelerators does not have to be duplicated in every
window procedure.

This advantage becomes particularly important if you use modeless dialog
boxes (discussed in Chapter 10) or child windows on your main window's
client area. If a particular keyboard accelerator is defined to move among
windows, then only one window procedure has to include this logic. The child
windows do not receive WM_COMMAND messages from the keyboard accelerators.


Some Rules on Assigning Accelerators

In theory, you can define a keyboard accelerator for any virtual key or any
character key in combination with the Shift key, the Ctrl key, or both.
However, the CUA Advanced Interface Design Guide offers several
recommendations that are intended to achieve some consistency among
applications and to avoid interfering with Windows' use of the keyboard. For
programs that have an Edit menu, the CUA Advanced Interface Design Guide
highly recommends use of the following accelerators:

Key(s)                         Function
────────────────────────────────────────────────────────────────────────────
Alt+Backspace                  Undo
Del                            Clear
Ctrl+Ins                       Copy
Shift+Ins                      Paste
Shift+Del                      Cut

You should avoid using Tab, Enter, Esc, and the Spacebar in keyboard
accelerators, because these are often used for system functions.

Although some older Windows programs use alphabetic keys in combination with
the Ctrl key for keyboard accelerators, more recent Windows programs use
function keys, sometimes in combination with the Shift key, the Ctrl key, or
both. These function-key assignments are common in some applications:

Key(s)                      Function
────────────────────────────────────────────────────────────────────────────
F1                          Help
F3                          Save
F6                          Next window
Shift+F6                    Previous window
Ctrl+F6                     Next section
Shift+Ctrl+F6               Previous section


The Accelerator Table

Keyboard accelerator tables are defined in your .RC resource script. The
general form is shown here:

MyAccelerators ACCELERATORS
     {
[accelerator definitions]
     }

This accelerator table name is MyAccelerators. The ACCELERATORS table does
not include load and memory options. You can have multiple ACCELERATORS
tables in your resource script.

Each keyboard accelerator you define requires a different line in the table.
There are four types of accelerator definitions:

"char",  wID          [,NOINVERT] [,SHIFT] [,CONTROL]

"^char", wID          [,NOINVERT] [,SHIFT] [,CONTROL]

nCode,   wID, ASCII   [,NOINVERT] [,SHIFT] [,CONTROL]

nCode,   wID, VIRTKEY [,NOINVERT] [,SHIFT] [,CONTROL]

In these examples, "char" means a single character enclosed in double
quotation marks, and "^char" is the character ^ and a single character in
double quotation marks. The wID number performs a function similar to the
menu ID in a menu definition. It is the value that Windows sends to your
window procedure in the WM_COMMAND message to identify the accelerator.
These are usually identifiers defined in a header file. When the keyboard
accelerator duplicates a menu command, use the same ID for both the menu and
the accelerator. When the keyboard accelerator does not duplicate a menu
command, use a unique ID.

Keyboard accelerators almost always select options in popup menus. Windows
automatically flashes a top-level menu item when you press an accelerator
key that duplicates an option in a popup. (For example, the Edit text
flashes if you press the Del key.) If you don't want the menu to flash,
include the option NOINVERT.

In the first type of accelerator definition, the keyboard accelerator is a
case-sensitive match of the character in double quotes:

"char",  wID          [,NOINVERT] [,SHIFT] [,CONTROL]

If you want to define a keyboard accelerator for that key in combination
with the Shift or Ctrl key or both, simply add SHIFT or CONTROL or both.

In the second type of definition, the keyboard accelerator is the character
in combination with the Ctrl key:

"^char", wID          [,NOINVERT] [,SHIFT] [,CONTROL]

This type is the same as the first type when the CONTROL keyword is used
with the character alone.

The third and fourth types use a number (nCode) rather than a character in
quotes:

nCode,   wID, ASCII   [,NOINVERT] [,SHIFT] [,CONTROL]
nCode,   wID, VIRTKEY [,NOINVERT] [,SHIFT] [,CONTROL]

This number is interpreted as either case-sensitive ASCII code or a virtual
key code, depending on the ASCII or VIRTKEY keyword.

The most common keyboard accelerators are the second and fourth types. You
use the second type for character keys in combination with Ctrl. For
example, this defines an accelerator for Ctrl-A:

"^A", wID

Use the fourth type for virtual key codes such as function keys. This
defines an accelerator for the Ctrl-F9 combination:

VK_F9, wID, VIRTKEY, CONTROL

The identifier VK_F9 is defined in WINDOWS.H as the virtual key code for the
F9 key, so you have to include the statement:

#include <windows.h>

near the top of the resource script. The resource compiler defines an
identifier named RC_INVOKED that causes much of WINDOWS.H to be ignored.

The first and third types of definition shown above are rarely used. If you
want to use them, watch out for case-sensitivity. Windows does a
case-sensitive match on the "char"  or nCode based on the character you
press. When you add the SHIFT keyword, Windows checks to see if the Shift
key is depressed. This situation sometimes causes results you may not
anticipate. For instance, if "char" is "A", the keyboard accelerator is
invoked when you press the A key with the Shift key down or Caps Lock on,
but not both. If you use "A" with SHIFT, the A key must be pressed with
Shift down, but the accelerator can't be invoked at all when Caps Lock is
on. Similarly, "a" by itself is a keyboard accelerator for the unshifted A
key or for the A key with both Shift down and Caps Lock on. But "a" with
SHIFT invokes the accelerator only when Shift is down and Caps Lock is on.

When you define keyboard accelerators for a menu item, you should include
the key combination in the menu item text. The tab (\t) character separates
the text from the accelerator so that the accelerators align in a second
column. To notate accelerator keys in a menu, the CUA Advanced Interface
Design Guide recommends the text Ctrl or Shift followed by a plus sign and
the key--for instance:

  ■   F6

  ■   Shift+F6

  ■   Ctrl+F6


Loading the Accelerator Table

Within your program, you use the LoadAccelerators function to load the
accelerator table into memory and obtain a handle to it. The
LoadAccelerators statement is very similar to the LoadIcon, LoadCursor,
LoadBitmap, and LoadMenu statements.

First, define a handle to an accelerator table as type HANDLE:

HANDLE hAccel ;

Then load the accelerator table:

hAccel = LoadAccelerators (hInstance, "MyAccelerators") ;

As with icons, cursors, bitmaps, and menus, you can use a number for the
accelerator table name and then use that number in the LoadAccelerators
statement with the MAKEINTRESOURCE macro or in quotations preceded by a #
character.


Translating the Keystrokes

We will now tamper with three lines of code that are common to all the
Windows programs that we've created so far in this book. The code is the
standard message loop:

while (GetMessage (&msg, NULL, 0, 0))
     {
     TranslateMessage (&msg) ;
     DispatchMessage (&msg) ;
     }

Here's how we change it to use the keyboard accelerator table:

while (GetMessage (&msg, NULL, 0, 0))
     {
     if (!TranslateAccelerator (hwnd, hAccel, &msg))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     }

The TranslateAccelerator function determines if the message stored in the
msg message structure is a keyboard message. If it is, the function searches
for a match in the accelerator table whose handle is hAccel. If it finds a
match, it calls the window procedure for the window whose handle is hwnd. If
the keyboard accelerator ID corresponds to a menu item in the system menu,
then the message is WM_SYSCOMMAND. Otherwise, the message is WM_COMMAND.

When TranslateAccelerator returns, the return value is nonzero if the
message has been translated (and already sent to the window procedure) and 0
if not. If TranslateAccelerator returns a nonzero value, you should not call
TranslateMessage and DispatchMessage but rather loop back to the GetMessage
call.

The hwnd parameter in TranslateMessage looks a little out of place because
it's not required in the other three functions in the message loop.
Moreover, the message structure itself (the structure variable msg) has a
member named hwnd, which is also a handle to a window.

The fields of the msg structure are filled in by the GetMessage call. When
the second parameter of GetMessage is NULL, the function retrieves messages
for all windows belonging to the application. When GetMessage returns, the
hwnd member of the msg structure is the window handle of the window that
will get the message. However, when TranslateAccelerator translates a
keyboard message into a WM_COMMAND or WM_SYSCOMMAND message, it replaces the
msg.hwnd window handle with the window handle hwnd specified as the first
parameter to the function. That is how Windows sends all keyboard
accelerator messages to the same window procedure even if another window in
the application currently has the input focus. TranslateAccelerator does not
translate keyboard messages when a modal dialog box or message box has the
input focus, because messages for these windows do not come through the
program's message loop.

In some cases in which another window in your program (such as a modeless
dialog box) has the input focus, you may not want keyboard accelerators to
be translated. You'll see how to handle this in Chapter 10.


Receiving the Accelerator Messages

When a keyboard accelerator corresponds to a menu item in the system menu,
TranslateAccelerator sends the window procedure a WM_SYSCOMMAND message. If
you need to, you can differentiate between a direct system menu selection
and a keyboard accelerator for that system menu item by the high word of
lParam:

              wParam          LOWORD (lParam)  HIWORD (lParam)
────────────────────────────────────────────────────────────────────────────
Accelerator:  Accelerator ID  0                1
Menu:         Menu ID         0                0

If the accelerator ID corresponds to a menu item (or does not correspond to
any item on the menu or system menu), TranslateAccelerator sends the window
procedure a WM_COMMAND message. The following table shows the types of
WM_COMMAND messages you can receive for keyboard accelerators, menu
commands, and child window controls:

              wParam          LOWORD (lParam)      HIWORD (lParam)
────────────────────────────────────────────────────────────────────────────
Accelerator:  Accelerator ID  0                    1
Menu:         Menu ID         0                    0
Control:      Control ID      Child window handle  Notification code

If the keyboard accelerator corresponds to a menu item, the window procedure
also receives WM_INITMENU, WM_INITMENUPOPUP, and WM_MENUSELECT messages,
just as if the menu option had been chosen. Programs usually enable and
disable items in a popup menu when processing WM_INITMENUPOPUP, so you still
have that facility when

using keyboard accelerators. If the keyboard accelerator corresponds to a
disabled or grayed menu item, however, TranslateAccelerator does not send
the window procedure a WM_COMMAND or WM_SYSCOMMAND message.

If the active window is minimized, TranslateAccelerator sends the window
procedure WM_SYSCOMMAND messages--but not WM_COMMAND messages--for keyboard
accelerators that correspond to enabled system menu items.
TranslateAccelerator also sends that window procedure WM_COMMAND messages
for accelerators that do not correspond to any menu items.


POPPAD with a Menu and Accelerators

In Chapter 6 we created a program called POPPAD1 that uses a child window
edit control to mimic some of the workings of Windows' Note Pad program. In
this chapter we'll add a File and Edit menu and call it POPPAD2. The Edit
items will all be functional; we'll finish the File functions in Chapter 10
and the Print function in Chapter 15. POPPAD2 is shown in Figure 9-12
beginning on the following page.

 POPPAD2.MAK

#-----------------------
# POPPAD2.MAK make file
#-----------------------

poppad2.exe : poppad2.obj poppad2.def poppad2.res
     link poppad2, /align:16, NUL, /nod slibcew libw, poppad2.def
     rc poppad2.res

poppad2.obj : poppad2.c poppad2.h
     cl -c -Gsw -Ow -W2 -Zp poppad2.c

poppad2.res : poppad2.rc poppad2.h poppad2.ico
     rc -r poppad2.rc

 POPPAD2.C

/*-----------------------------------------------------
   POPPAD2.C -- Popup Editor Version 2 (includes menu)
                (c) Charles Petzold, 1990
  -----------------------------------------------------*/

#include <windows.h>
#include "poppad2.h"
long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

char szAppName [] = "PopPad2" ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     HANDLE   hAccel ;
     HWND     hwnd ;
     MSG      msg ;
     WNDCLASS wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;

          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (hInstance, szAppName) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = szAppName ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, szAppName,
                          WS_OVERLAPPEDWINDOW,
                          GetSystemMetrics (SM_CXSCREEN) / 4,
                          GetSystemMetrics (SM_CYSCREEN) / 4,
                          GetSystemMetrics (SM_CXSCREEN) / 2,
                          GetSystemMetrics (SM_CYSCREEN) / 2,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     hAccel = LoadAccelerators (hInstance, szAppName) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          if (!TranslateAccelerator (hwnd, hAccel, &msg))
               {
               TranslateMessage (&msg) ;
               DispatchMessage (&msg) ;
               }
          }

     return msg.wParam ;
     }

AskConfirmation (HWND hwnd)
     {
     return MessageBox (hwnd, "Really want to close PopPad2?",
                        szAppName, MB_YESNO | MB_ICONQUESTION) ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static HWND hwndEdit ;
     LONG        lSelect ;
     WORD        wEnable ;
     switch (message)
          {
          case WM_CREATE :
               hwndEdit = CreateWindow ("edit", NULL,
                         WS_CHILD | WS_VISIBLE | WS_HSCROLL | WS_VSCROLL |
                              WS_BORDER | ES_LEFT | ES_MULTILINE |
                              ES_AUTOHSCROLL | ES_AUTOVSCROLL,
                         0, 0, 0, 0,
                         hwnd, 1, ((LPCREATESTRUCT) lParam)->hInstance,
NULL) ;
               return 0 ;

          case WM_SETFOCUS :
               SetFocus (hwndEdit) ;
               return 0 ;

          case WM_SIZE :
               MoveWindow (hwndEdit, 0, 0, LOWORD (lParam),
                                           HIWORD (lParam), TRUE) ;
               return 0 ;

          case WM_INITMENUPOPUP :
               if (lParam == 1)
                    {
                    EnableMenuItem (wParam, IDM_UNDO,
                         SendMessage (hwndEdit, EM_CANUNDO, 0, 0L) ?
                              MF_ENABLED : MF_GRAYED) ;

                    EnableMenuItem (wParam, IDM_PASTE,
                         IsClipboardFormatAvailable (CF_TEXT) ?
                              MF_ENABLED : MF_GRAYED) ;

                    lSelect = SendMessage (hwndEdit, EM_GETSEL, 0, 0L) ;

                    if (HIWORD (lSelect) == LOWORD (lSelect))
                         wEnable = MF_GRAYED ;
                    else
                         wEnable = MF_ENABLED ;

                    EnableMenuItem (wParam, IDM_CUT,   wEnable) ;
                    EnableMenuItem (wParam, IDM_COPY,  wEnable) ;
                    EnableMenuItem (wParam, IDM_CLEAR, wEnable) ;

                    return 0 ;
                    }
               break ;
          case WM_COMMAND :
               if (LOWORD (lParam))
                    {
                    if (wParam == 1 && HIWORD (lParam) == EN_ERRSPACE)
                         MessageBox (hwnd, "Edit control out of space.",
                                     szAppName, MB_OK | MB_ICONSTOP) ;

                    return 0 ;
                    }

               else switch (wParam)
                         {
                         case IDM_NEW :
                         case IDM_OPEN :
                         case IDM_SAVE :
                         case IDM_SAVEAS :
                         case IDM_PRINT :
                              MessageBeep (0) ;
                              return 0 ;

                         case IDM_EXIT :
                              SendMessage (hwnd, WM_CLOSE, 0, 0L) ;
                              return 0 ;

                         case IDM_ABOUT :
                              MessageBox (hwnd,
                                   "POPPAD2 (c) Charles Petzold, 1990",
                                   szAppName, MB_OK | MB_ICONINFORMATION) ;
                              return 0 ;

                         case IDM_UNDO :
                              SendMessage (hwndEdit, WM_UNDO, 0, 0L) ;
                              return 0 ;

                         case IDM_CUT :
                              SendMessage (hwndEdit, WM_CUT, 0, 0L) ;
                              return 0 ;

                         case IDM_COPY :
                              SendMessage (hwndEdit, WM_COPY, 0, 0L) ;
                              return 0 ;

                         case IDM_PASTE :
                              SendMessage (hwndEdit, WM_PASTE, 0, 0L) ;
                              return 0 ;

                         case IDM_CLEAR :
                              SendMessage (hwndEdit, WM_CLEAR, 0, 0L) ;
                              return 0 ;
                         case IDM_SELALL :
                              SendMessage (hwndEdit, EM_SETSEL, 0,
                                           MAKELONG (0, 32767)) ;

                              return 0 ;
                         }
               break ;

          case WM_CLOSE :
               if (IDYES == AskConfirmation (hwnd))
                    DestroyWindow (hwnd) ;
               return 0 ;

          case WM_QUERYENDSESSION :
               if (IDYES == AskConfirmation (hwnd))
                    return 1L ;
               else
                    return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 POPPAD2.RC

/*----------------------------
   POPPAD2.RC resource script
  ----------------------------*/

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

PopPad2 ICON poppad2.ico

PopPad2 MENU
     {
     POPUP "&File"
          {
          MENUITEM "&New",              IDM_NEW
          MENUITEM "&Open...",          IDM_OPEN
          MENUITEM "&Save",             IDM_SAVE
          MENUITEM "Save &As...",       IDM_SAVEAS
          MENUITEM SEPARATOR
          MENUITEM "&Print",            IDM_PRINT
          MENUITEM SEPARATOR
          MENUITEM "E&xit",             IDM_EXIT
          MENUITEM "&About PopPad2...", IDM_ABOUT
          }
     POPUP "&Edit"
          {
          MENUITEM "&Undo\tAlt+BkSp",   IDM_UNDO
          MENUITEM SEPARATOR
          MENUITEM "Cu&t\tShift+Del",   IDM_CUT
          MENUITEM "&Copy\tCtrl+Ins",   IDM_COPY
          MENUITEM "&Paste\tShift+Ins", IDM_PASTE
          MENUITEM "C&lear\tDel",       IDM_CLEAR
          MENUITEM SEPARATOR
          MENUITEM "&Select All",       IDM_SELALL
          }
     }

PopPad2 ACCELERATORS
     {
     VK_DELETE, IDM_CUT,   VIRTKEY, SHIFT
     VK_INSERT, IDM_COPY,  VIRTKEY, CONTROL
     VK_INSERT, IDM_PASTE, VIRTKEY, SHIFT
     VK_DELETE, IDM_CLEAR, VIRTKEY
     }

 POPPAD2.H

/*-----------------------
   POPPAD2.H header file
  -----------------------*/

#define IDM_NEW      1
#define IDM_OPEN     2
#define IDM_SAVE     3
#define IDM_SAVEAS   4
#define IDM_PRINT    5

#define IDM_EXIT     6
#define IDM_ABOUT    7

#define IDM_UNDO     8
#define IDM_CUT      9
#define IDM_COPY    10
#define IDM_PASTE   11
#define IDM_CLEAR   12
#define IDM_SELALL  13

 POPPAD2.ICO  -- Please refer to the book.

 POPPAD2.DEF

;------------------------------------
; POPPAD2.DEF module definition file
;------------------------------------

NAME           POPPAD2

DESCRIPTION    'Popup Editor Version 2 (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

The POPPAD2.RC resource script file contains the menu and accelerator table.
You'll notice that the accelerators are all indicated within the text
strings of the Edit popup menu following the tab (\t) character.


Enabling Menu Items

The major job in the window procedure now involves enabling and graying the
options in the Edit menu, which is done when processing the
WM_INITMENUPOPUP. First, the program checks to see if the Edit popup is
about to be displayed. Because the position index of Edit in the menu
(starting with File at 0) is 1, lParam equals 1 if the Edit popup is about
to be displayed.

To determine if the Undo option can be enabled, POPPAD2 sends an EM_CANUNDO
message to the edit control. The SendMessage call returns nonzero if the
edit control can perform an Undo action, in which case the option is
enabled; otherwise, it is grayed:

EnableMenuItem (wParam, IDM_UNDO,
     SendMessage (hwndEdit, EM_CANUNDO, 0, 0L) ?
                  MF_ENABLED : MF_GRAYED) ;

The Paste option should be enabled only if the clipboard currently contains
text. We can determine this through the IsClipboardFormatAvailable call with
the CF_TEXT identifier:

EnableMenuItem (wParam, IDM_PASTE,
     IsClipboardFormatAvailable (CF_TEXT) ?
                   MF_ENABLED : MF_GRAYED) ;

The Cut, Copy, and Clear options should be enabled only if text in the edit
control has been selected. Sending the edit control an EM_GETSEL message
returns a long integer containing this information:

lSelect = SendMessage (hwndEdit, EM_GETSEL, 0, 0L) ;

The low word of lSelect is the position of the first selected character; the
high word of lSelect is the position of the character following the
selection. If these two words are equal, no text has been selected:

if (HIWORD (lSelect) == LOWORD (lSelect))
     wEnable = MF_GRAYED ;
else
     wEnable = MF_ENABLED ;

The value of wEnable is then used for the Cut, Copy, and Clear options:

EnableMenuItem (wParam, IDM_CUT,   wEnable) ;
EnableMenuItem (wParam, IDM_COPY,  wEnable) ;
EnableMenuItem (wParam, IDM_CLEAR, wEnable) ;


Processing the Menu Options

Of course, if we were not using a child window edit control for POPPAD2, we
would now be faced with the problems involved with actually implementing the
Undo, Cut, Copy, Paste, Clear, and Select All options from the Edit menu.
But the edit control makes this process easy, because we merely send the
edit control a message for each of these options:

case IDM_UNDO :
     SendMessage (hwndEdit, WM_UNDO, 0, 0L) ;
     return 0 ;

case IDM_CUT :
     SendMessage (hwndEdit, WM_CUT, 0, 0L) ;
     return 0 ;

case IDM_COPY :
     SendMessage (hwndEdit, WM_COPY, 0, 0L) ;
     return 0 ;

case IDM_PASTE :
     SendMessage (hwndEdit, WM_PASTE, 0, 0L) ;
     return 0 ;

case IDM_CLEAR :
     SendMessage (hwndEdit, WM_CLEAR, 0, 0L) ;
     return 0 ;

case IDM_SELALL :
     SendMessage (hwndEdit, EM_SETSEL, 0,
                            MAKELONG (0, 32767)) ;
     return 0 ;

Notice that we could have simplified this even further by making the values
of IDM_UNDO, IDM_CUT, and so forth equal to the values of the corresponding
window messages WM_UNDO, WM_CUT, and so forth.

The About option for the File popup invokes a simple message box:

case IDM_ABOUT :
     MessageBox (hwnd,
          "POPPAD2 (c) Charles Petzold, 1990",
          szAppName, MB_OK | MB_ICONINFORMATION) ;
     break ;

In Chapter 10 we'll make this a dialog box.

The Exit option sends the window procedure a WM_CLOSE message:

case IDM_EXIT :
     SendMessage (hwnd, WM_CLOSE, 0, 0L) ;
     return 0 ;

That is precisely what DefWindowProc does when it receives a WM_SYSCOMMAND
message with wParam equal to SC_CLOSE.

In previous programs we have not processed the WM_CLOSE messages in our
window procedure but have simply passed them to DefWindowProc. DefWindowProc
does something very simple with WM_CLOSE: It calls the DestroyWindow
function. Rather than send WM_CLOSE messages to DefWindowProc, however,
POPPAD2 processes them. This fact is not so important now, but it will
become very important in Chapter 10 when POPPAD can actually edit files:

case WM_CLOSE :
     if (IDYES == AskConfirmation (hwnd))
          DestroyWindow (hwnd) ;
     return 0 ;

AskConfirmation is a function in POPPAD2 that displays a message box asking
for confirmation to close the program:

AskConfirmation (HWND hwnd)
     {
     return MessageBox (hwnd, "Really want to close POPPAD2?",
                       szAppName, MB_YESNO | MB_ICONQUESTION) ;
     }

The message box (as well as the AskConfirmation function) returns IDYES if
the Yes button is selected. Only then does POPPAD2 call DestroyWindow.
Otherwise, the program is not terminated.

If you want confirmation before terminating a program, you must also process
WM_QUERYENDSESSION messages. Windows begins sending every window procedure a
WM_QUERYENDSESSION message when the user chooses Close from the MS-DOS
Executive system menu. If any window procedure returns 0 from this message,
the Windows session is not terminated. Here's how we handle
WM_QUERYENDSESSION:

case WM_QUERYENDSESSION :
     if (IDYES == AskConfirmation (hwnd))
          return 1L ;
     else
          return 0 ;

The WM_CLOSE and WM_QUERYENDSESSION messages are the only two messages you
have to process if you want to ask for user confirmation before ending a
program. That's why we made the Exit menu option in POPPAD2 send the window
procedure a WM_CLOSE message--by doing so, we avoided having to ask for
confirmation at yet a third point.

If you process WM_QUERYENDSESSION messages, you may also be interested in
the WM_ENDSESSION message. Windows sends this message to every window
procedure that has previously received a WM_QUERYENDSESSION message. The
wParam parameter is 0 if the session fails to terminate because another
program has returned 0 from WM_QUERYENDSESSION. The WM_ENDSESSION message
essentially answers the question: I told Windows it was OK to terminate me,
but did I really get terminated?

Although I've included the normal New, Open, Save, and Save As options in
POPPAD2's File menu, they are currently nonfunctional. To process these
commands, we need to use dialog boxes. You're now ready to learn about them.





Chapter 10  Dialog Boxes
────────────────────────────────────────────────────────────────────────────

Dialog boxes are most often used for obtaining additional input from the
user beyond what can be easily managed through a menu. The programmer
indicates that a menu item invokes a dialog box by adding an ellipsis (...)
to the menu item.

A dialog box generally takes the form of a popup window containing various
child window controls. The size and placement of these controls are
specified in a "dialog box template" in the program's resource script file.
Windows is responsible for creating the dialog box popup window and the
child window controls and for providing a window procedure to process dialog
box messages (including all keyboard and mouse input). The code within
Windows that does all this is sometimes referred to as the "dialog box
manager."

Many of the messages that are processed by the dialog box window procedure
within Windows are also passed to a function within your own program, called
a "dialog box procedure" or "dialog procedure." This function is similar to
a normal window procedure, but with some important differences. Generally,
you will not be doing very much within the dialog procedure except
initializing the child window controls when the dialog box is created,
processing messages from the child window controls, and ending the dialog
box.

The subject of dialog boxes would normally be a big one, because it involves
the use of child window controls. However, we have already explored child
window controls in Chapter 6. When you use child window controls in dialog
boxes, the Windows dialog box manager picks up many of the responsibilities
that we assumed in Chapter 6. In particular, the problems we encountered
with passing the input focus between the scroll bars in the COLORS1 program
do not occur with dialog boxes. Windows handles all the logic necessary to
shift input focus between controls in a dialog box.

However, adding a dialog box to a program is not a trivial undertaking. It
involves changes to several files--the dialog box template goes in the
resource script file, the dialog box procedure goes in the source code file,
the name of the dialog box procedure goes in the module definition file, and
identifiers used in the dialog box often go in the program's header file.
We'll begin with a simple dialog box so that you get a feel for the
interconnections between these various pieces.

MODAL DIALOG BOXES

Dialog boxes are either "modal" or "modeless." The modal dialog box is the
most common. When your program displays a modal dialog box, the user cannot
switch between the dialog box and another window in your program. The user
must explicitly end the dialog box, usually by clicking a push button marked
either OK or Cancel. The user can, however, generally switch to another
program while the dialog box is still displayed. Some dialog boxes (called
"system modal") do not allow even this. System modal dialog boxes must be
ended before the user does anything else in Windows.

Creating an "About" Dialog Box

Even if a Windows program requires no user input, it will often have a
dialog box that is invoked by an About option on the menu. This dialog box
displays the name and icon of the program, a copyright notice, a push button
labeled OK, and perhaps other information. The first program we'll look at
does nothing except display an About dialog box. The ABOUT1 program is shown
in Figure 10-1.

 ABOUT1.MAK

#----------------------
# ABOUT1.MAK make file
#----------------------

about1.exe : about1.obj about1.def about1.res
     link about1, /align:16, NUL, /nod slibcew libw, about1
     rc about1.res

about1.obj : about1.c about1.h
     cl -c -Gsw -Ow -W2 -Zp about1.c

about1.res : about1.rc about1.h about1.ico
     rc -r about1.rc


 ABOUT1.C

/*------------------------------------------
   ABOUT1.C -- About Box Demo Program No. 1
               (c) Charles Petzold, 1990
  ------------------------------------------*/

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

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName [] = "About1" ;
     MSG         msg ;
     HWND        hwnd ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (hInstance, szAppName) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = szAppName ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "About Box Demo Program",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

BOOL FAR PASCAL AboutDlgProc (HWND hDlg, WORD message, WORD wParam, LONG
lParam)
     {
     switch (message)
          {
          case WM_INITDIALOG :
               return TRUE ;

          case WM_COMMAND :
               switch (wParam)
                    {
                    case IDOK :
                    case IDCANCEL :
                         EndDialog (hDlg, 0) ;
                         return TRUE ;
                    }
               break ;
          }
     return FALSE ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static FARPROC lpfnAboutDlgProc ;
     static HANDLE  hInstance ;

     switch (message)
          {
          case WM_CREATE :
               hInstance = ((LPCREATESTRUCT) lParam)->hInstance ;

               lpfnAboutDlgProc = MakeProcInstance (AboutDlgProc, hInstance)
;
               return 0 ;

          case WM_COMMAND :
               switch (wParam)
                    {
                    case IDM_ABOUT :
                         DialogBox (hInstance, "AboutBox", hwnd,
                                        lpfnAboutDlgProc) ;
                         return 0 ;
                    }
               break ;
          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 ABOUT1.RC

/*---------------------------
   ABOUT1.RC resource script
  ---------------------------*/

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

About1 ICON about1.ico

About1 MENU
     {
     POPUP "&Help"
          {
          MENUITEM "&About About1...",       IDM_ABOUT
          }
     }

AboutBox DIALOG  20, 20, 160, 80
     STYLE WS_POPUP | WS_DLGFRAME
     {
     CTEXT "About1"                      -1,   0, 12, 160,  8
     ICON  "About1"                      -1,   8,  8,   0,  0
     CTEXT "About Box Demo Program"      -1,   0, 36, 160,  8
     CTEXT "(c) Charles Petzold, 1990"   -1,   0, 48, 160,  8
     DEFPUSHBUTTON "OK"                IDOK,  64, 60,  32, 14, WS_GROUP
     }

 ABOUT1.H

/*----------------------
   ABOUT1.H header file
  ----------------------*/

#define IDM_ABOUT      1

 ABOUT1.ICO  -- Please refer to the book.

FIG 10-1 partial screen dump

 ABOUT1.DEF

;-----------------------------------
; ABOUT1.DEF module definition file
;-----------------------------------

NAME           ABOUT1

DESCRIPTION    'About Box Demo No. 1 (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc
               AboutDlgProc


The Dialog Box Template

The first job involved in adding a dialog box to a program is designing the
dialog box template. This template can go directly in the resource script
file, or it can be in a separate file that by convention uses the extension
.DLG (for "dialog"). If you put the template in a separate file, you include
the line:

rcinclude filename.dlg

in the resource script file.

You can create the dialog box template by hand in a text editor, or you can
use the DIALOG program included with the Windows Software Development Kit.
Because the output from DIALOG is virtually unreadable, I'll be showing
dialog box templates that look as if they were created by hand. A discussion
of DIALOG concludes this chapter.

The dialog box template for ABOUT1 looks like this:

AboutBox DIALOG  20, 20, 160, 80
     STYLE WS_POPUP | WS_DLGFRAME
     {
     CTEXT "About1"                      -1,   0, 12, 160,  8
     ICON  "About1"                      -1,   8,  8,   0,  0
     CTEXT "About Box Demo Program"      -1,   0, 36, 160,  8
     CTEXT "(c) Charles Petzold, 1990"   -1,   0, 48, 160,  8
     DEFPUSHBUTTON "OK"                IDOK,  64, 60,  32, 14, WS_GROUP
     }

The first line gives the dialog box a name (in this case, AboutBox). As is
the case for other resources, you can use a number instead. The name is
followed by the keyword DIALOG and four numbers. The first two numbers are
the x- and y-coordinates of the upper left corner of the dialog box,
relative to the client area of its parent when the dialog box is invoked by
the program. The second two numbers are the width and height of the dialog
box.

These coordinates and sizes are not in units of pixels. They are instead
based on a special coordinate system used only for dialog box templates. The
numbers are based on the size of a system font character: x-coordinates and
width are expressed in units of 1/4 of an average character width;
y-coordinates and height are expressed in units of 1/8 of a character
height. Thus for this particular dialog box, the upper left corner of the
dialog box is 5 characters from the left of the main window's client area
and 2-1/2 characters from the top. It is 40 characters wide and 10
characters high.

This coordinate system allows you to use coordinates and sizes that will
retain the general dimensions and look of the dialog box regardless of the
resolution of the video display. Because system font characters are often
approximately twice as high as they are wide, the units on both the x- and
y-axes are about the same.

The DIALOG statement can also include load options (PRELOAD and LOADONCALL)
and memory options (FIXED, MOVEABLE, and DISCARDABLE) immediately following
the word DIALOG. The defaults are LOADONCALL and MOVEABLE. The STYLE
statement in the template is similar to the style field of a CreateWindow
call. Using WS_POPUP and WS_DLGFRAME is normal for modal dialog boxes, but
we'll explore some alternatives later on.

Within the left and right brackets, you define the child window controls
that will appear in the dialog box. This dialog box uses three types of
child window controls: CTEXT (centered text), ICON (an icon), and
DEFPUSHBUTTON (a default push button). The format of these statements is:

control-type "text" nID, xPos, yPos, xWidth, yHeight, dwStyle

The dwStyle value at the end is optional; it specifies additional window
styles using identifiers defined in WINDOWS.H.

These CTEXT, ICON, and DEFPUSHBUTTON identifiers are used only in dialog
boxes. They are shorthand for a particular window class and window style.
For example, CTEXT indicates that the class of the child window control is
"static" and that the style is:

WS_CHILD | SS_CENTER | WS_VISIBLE | WS_GROUP

Although this is the first time we've encountered the WS_GROUP identifier,
we used  the WS_CHILD, SS_CENTER, and WS_VISIBLE window styles when creating
static child window text controls in the COLORS1 program in Chapter 6.

For the icon, the text field is the name of the program's icon resource,
which is also defined in the ABOUT1 resource script. For the push button,
the text field is the text that appears inside the push button. This text is
equivalent to the text specified as the second parameter to a CreateWindow
call when you create a child window control in a program.

The nID field is a value that the child window uses to identify itself when
sending messages (usually WM_COMMMAND messages) to its parent. The parent
window of these child window controls is the dialog box window itself, which
sends these messages to a window procedure in Windows. However, this window
procedure also sends these messages to the dialog box procedure that you'll
include in your program. The nID values are equivalent to the child window
IDs used in the CreateWindow function when we created child windows in
Chapter 6. Because the text and icon controls do not send messages back to
the parent window, these values are set to -1. The nID value for the push
button is IDOK, which is defined in WINDOWS.H as 1.

The next four numbers set the position of the child window control (relative
to the upper left corner of the dialog box's client area) and the size. The
position and size are expressed in units of 1/4 the average width and 1/8
the height of a system font character. The width and height values are
ignored for the ICON statement.

The DEFPUSHBUTTON statement in the dialog box template includes the window
style WS_GROUP in addition to the window style implied by the DEFPUSHBUTTON
keyword. I'll have more to say about WS_GROUP (and the related WS_TABSTOP
style) when discussing the second version of this program, ABOUT2, a bit
later.


The Dialog Box Procedure

The dialog box procedure within your program handles messages to the dialog
box. Although it looks very much like a window procedure, it is not a true
window procedure. The window procedure for the dialog box is within Windows.
That window procedure calls your dialog box procedure with many of the
messages that it receives. Here's the dialog box procedure for ABOUT1:

BOOL FAR PASCAL AboutDlgProc (HWND hDlg, WORD message,
                              WORD wParam, LONG lParam)
     {
     switch (message)
          {
          case WM_INITDIALOG :
              return TRUE ;

          case WM_COMMAND :
               switch (wParam)
                    {
                    case IDOK :
                    case IDCANCEL :
                         EndDialog (hDlg, 0) ;
                         return TRUE ;
                    }
               break ;
          }
     return FALSE ;
     }

The parameters to this function are the same as those for a normal window
procedure. (Although I've used hDlg for the handle to the dialog box window,
you can use hwnd instead if you like.) Let's note first the differences
between this function and a window procedure:

  ■   A window procedure returns a long; a dialog box procedure returns a
      BOOL (which is defined in WINDOWS.H as an int).

  ■   A window procedure calls DefWindowProc if it does not process a
      particular message; a dialog box procedure returns TRUE (nonzero) if
      it processes a message and FALSE (0) if it does not.

  ■   A dialog box procedure does not need to process WM_PAINT or WM_DESTROY
      messages. A dialog box procedure will not receive a WM_CREATE message;
      instead, the dialog box procedure performs initialization during the
      special WM_INITDIALOG message.

The WM_INITDIALOG message is the first message the dialog box procedure re-
ceives. This message is sent only to dialog box procedures. If the dialog
box procedure  returns TRUE, then Windows sets the input focus to the first
child window control in the dialog box that has a WS_TABSTOP style (which
I'll explain in the discussion of ABOUT2). In this dialog box, the first
child window control that has a WS_TABSTOP style is the push button.
Alternatively, during processing of WM_INITDIALOG the dialog box procedure
can use SetFocus to set the focus to one of the child window controls in the
dialog box and then return FALSE.

The only other message this dialog box processes is WM_COMMAND. This is the
message the push-button control sends to its parent window either when the
button is clicked with the mouse or when the Spacebar is pressed while the
button has the input focus. The ID of the control (which we set to IDOK in
the dialog box template) is in wParam. For this message, the dialog box
procedure calls EndDialog, which tells Windows to destroy the dialog box.
For all other messages, the dialog box procedure returns FALSE to tell the
dialog box window procedure within Windows that our dialog box procedure did
not process the message.

The messages for a modal dialog box don't go through your program's message
queue, so you needn't worry about the effect of keyboard accelerators within
the dialog box.


Exporting the Dialog Box Procedure

Because this dialog box procedure is called from outside the program, it
must be included in the EXPORTS section of the module definition file:

EXPORTS   WndProc
          AboutDlgProc

This is the easiest part of the job but also the easiest to forget. I forget
to export the dialog box procedure about one time in four. Often the
function will seem to work (more or less), but because it's not using the
program's data segment, it could be altering data inside Windows' data
segment. Our simple dialog box procedure doesn't reference anything in
ABOUT1's data segment, so strictly speaking, exporting the function is not
required. But get into the habit of exporting the function; I hope you
achieve a better track record than mine.


Invoking the Dialog Box

During processing of WM_CREATE, the program's instance handle is obtained
(and stored in a static variable) and MakeProcInstance is called to create
an instance thunk for the dialog procedure. The pointer to the instance
thunk is also stored in a static variable:

hInstance = ((LPCREATESTRUCT) lParam)->hInstance ;
lpfnAboutDlgProc = MakeProcInstance (AboutDlgProc, hInstance) ;

The MakeProcInstance function assures that AboutDlgProc obtains the correct
data segment address for this instance of ABOUT1.

The program checks for WM_COMMAND messages where wParam is equal to
IDM_ABOUT. When it gets one, the program calls DialogBox:

DialogBox (hInstance, "AboutBox", hwnd, lpfnAboutDlgProc) ;

This function requires the instance handle (saved during WM_CREATE), the
name of the dialog box (as defined in the resource script), the parent of
the dialog box (which is the program's main window), and the address of the
instance thunk return from MakeProcInstance. If you use a number rather than
a name for the dialog box template, you can convert it to a string using the
MAKEINTRESOURCE macro.

Selecting "About About1..." from the menu displays the dialog box, as shown
in Figure 10-2. You can end this dialog box by clicking the OK button with
the mouse, by pressing the Spacebar, or by pressing Enter. For any dialog
box that contains a default push button, Windows sends a WM_COMMAND message
to the dialog box, with wParam equal to the ID of the default push button
when Enter or the Spacebar is pressed.

  (Figure 10-2. may be found in the printed book.)

The DialogBox function you call to display the dialog box will not return
control to WndProc until the dialog box is ended. The value returned from
DialogBox is the second parameter to the EndDialog function called within
the dialog box procedure. (This value is not used in ABOUT1 but is used in
ABOUT2.) WndProc can then return control to Windows.

Even when the dialog box is displayed, WndProc can continue to receive
messages. In fact, you can send messages to WndProc from within the dialog
box procedure. ABOUT1's main window is the parent of the dialog box popup
window, so the SendMessage call in AboutDlgProc would start off like this:

SendMessage (GetParent (hDlg),  . . . ) ;

If you have a lot of dialog boxes within your program, you may not want to
create and save instance thunks for all of them. You can instead create
instance thunks as needed and free them after DialogBox returns:

lpfnDlgProc = MakeProcInstance (AboutDlgProc, hInstance) ;
DialogBox (hInstance, "AboutBox", hwnd, lpfnDlgProc) ;
FreeProcInstance (lpfnDlgProc) ;


More on the Dialog Box Style

The window style of the dialog box is specified in the STYLE line of the
dialog box template. For ABOUT1, we used a style that is most common for
modal dialog boxes:

STYLE WS_POPUP | WS_DLGFRAME

However, you can also experiment with other styles. For example, you can
try:

STYLE WS_POPUP | WS_CAPTION

This creates a dialog box with a caption bar and a normal window border. The
caption bar allows the user to move the dialog box around the display by
using the mouse. When you use WS_CAPTION, the x- and y-coordinates specified
in the DIALOG statement are the coordinates of the dialog box's client area,
relative to the upper left corner of the parent window's client area. The
caption bar will be shown above the y-coordinate.

If you have a caption bar, you can put text in it using the CAPTION
statement in the dialog box template:

CAPTION "Dialog Box Caption"

following the STYLE statement.  Or while processing the WM_INITDIALOG
message in the dialog procedure, you can use:

SetWindowText (hDlg, "Dialog Box Caption") ;

If you use the WS_CAPTION style, you can also add a system menu box with the
WS_SYSMENU style:

STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU

This style allows the user to select Move or Close from the system menu.

Adding WS_THICKFRAME to the style allows the user to resize the dialog box,
although resizing is unusual for a dialog box. If you don't mind being a
little unusual, you can also try adding WS_MAXIMIZEBOX to the STYLE
statement.

The STYLE statement is not required. If you do not include a STYLE or
CAPTION statement in the template, the default style is:

WS_POPUP | WS_BORDER

But this is rather dull looking. WS_DLGFRAME produces much more attractive
results. If you include a CAPTION statement with a STYLE statement, the
default style is:

WS_POPUP | WS_CAPTION | WS_SYSMENU

You can also add a menu to a dialog box by specifying:

MENU menu-name

in the dialog box template. The argument is either the name or number of a
menu in the resource script. Menus are highly uncommon for modal dialog
boxes. If you use one, be sure that all the ID numbers in the menu and the
dialog box controls are unique.

Although the dialog box window procedure is normally within Windows, you can
use one of your own window procedures to process dialog box messages. To do
so, you specify a window class name in the dialog box template:

CLASS "class-name"

This approach is rare, but we'll use it in the HEXCALC program shown later
in this chapter.

When you call DialogBox specifying the name of a dialog box template,
Windows has almost everything it needs to create a popup window by calling
the normal CreateWindow function. Windows obtains the coordinates and size
of the window, the window style, the caption, and the menu from the dialog
box template. Windows gets the instance handle and the parent window handle
from the parameters to DialogBox. The only other piece of information it
needs is a window class (assuming the dialog box template does not specify
one). Windows registers a special window class for dialog boxes. The window
procedure for this window class has access to the pointer to your dialog box
procedure (which you provide in the DialogBox call), so it can keep your
program informed of messages that this popup window receives. Of course, you
can create and maintain your own dialog box by creating the popup window
yourself. Using DialogBox is simply an easier approach.


More on Defining Controls

In the dialog box template in ABOUT1.RC, we used the shorthand notation
CTEXT, ICON, and DEFPUSHBUTTON to define the 3 types of child window
controls we wanted in the dialog box. There are 10 others you can use. Each
type implies a particular predefined window class and a window style. The
following table shows the equivalent window class and window style for each
of the 13 control types:

╓┌──────────────┌─────────────┌──────────────────────────────────────────────╖
Control Type   Window Class  Window Style
────────────────────────────────────────────────────────────────────────────
PUSHBUTTON     button        BS_PUSHBUTTON | WS_TABSTOP
DEFPUSHBUTTON  button        BS_DEFPUSHBUTTON | WS_TABSTOP
CHECKBOX       button        BS_CHECKBOX | WS_TABSTOP
RADIOBUTTON    button        BS_RADIOBUTTON | WS_TABSTOP
GROUPBOX       button        BS_GROUPBOX | WS_TABSTOP
LTEXT          static        SS_LEFT | WS_GROUP
CTEXT          static        SS_CENTER | WS_GROUP
Control Type   Window Class  Window Style
────────────────────────────────────────────────────────────────────────────
CTEXT          static        SS_CENTER | WS_GROUP
RTEXT          static        SS_RIGHT  | WS_GROUP
ICON           static        SS_ICON
EDITTEXT       edit          ES_LEFT | WS_BORDER | WS_TABSTOP
SCROLLBAR      scrollbar     SBS_HORZ
LISTBOX        listbox       LBS_NOTIFY | WS_BORDER | WS_VSCROLL
COMBOBOX       combobox      CBS_SIMPLE | WS_TABSTOP


The RC resource compiler is the only program that understands this shorthand
notation. In addition to the window styles shown above, each of these
controls has the style:

WS_CHILD | WS_VISIBLE

For all these control types except EDITTEXT, SCROLLBAR, LISTBOX, and
COMBOBOX, the format of the control statement is:

control-type "text", nID, xPos, yPos, xWidth, yHeight, dwStyle

For EDITTEXT, SCROLLBAR, LISTBOX, and COMBOBOX, the format is:

control-type nID, xPos, yPos, xWidth, yHeight, dwStyle

which excludes the text field. In both statements, the dwStyle parameter is
optional.

In Chapter 6, I discussed rules for determining the width and height of
predefined child window controls. You might want to refer back to that
chapter for these rules, keeping in mind that sizes specified in dialog box
templates are always in terms of 1/4 the average character width and 1/8 the
character height.

The "style" field of the control statements is optional. It allows you to
include other window style identifiers. For instance, if you wanted to
create a check box consisting of text to the left of a square box, you could
use:

CHECKBOX "text", nID, xPos, yPos, xWidth, yHeight, BS_LEFTTEXT

While the shorthand notation for child window controls is convenient, it is
also incomplete. You can't create a child window edit control without a
border, for example.  For this reason, the RC resource compiler also
recognizes a generalized control statement that looks like this:

CONTROL "text", nID, "class", dwStyle, xPos, yPos, xWidth, yHeight

This statement allows you to create any type of child window control by
specifying the window class and the complete window style. For example,
instead of using:

PUSHBUTTON "OK", IDOK, 10, 20, 32, 14

you can use:

CONTROL "OK", IDOK, "button", WS_CHILD | WS_VISIBLE |
          BS_PUSHBUTTON | WS_TABSTOP, 10, 20, 32, 14

When the resource script is compiled, these two statements are encoded
identically in the .RES file and the .EXE file.

When you use CONTROL statements in a dialog box template, you don't need to
include the WS_CHILD and WS_VISIBLE styles. Windows includes these in the
window style when creating the child windows. The format of the CONTROL
statement also clarifies what the Windows dialog manager does when it
creates a dialog box. First, as I described earlier, it creates a popup
window whose parent is the window handle that was provided in the DialogBox
function. Then for each control in the dialog template, the dialog box
manager creates a child window. The parent of each of these controls is the
popup dialog box. The CONTROL statement shown above is translated into a
CreateWindow call that looks like this:

CreateWindow ("button", "OK",
     WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON,
     10 * cxChar / 4, 20 * cyChar / 8,
     32 * cxChar / 4, 14 * cyChar / 8,
     hDlg, nID, hInstance, NULL) ;

where cxChar and cyChar are the width and height of a system font character
in pixels. The hDlg parameter is returned from the CreateWindow call that
creates the dialog box window. The hInstance parameter is obtained from the
original DialogBox call.


A More Complex Dialog Box

The simple dialog box in ABOUT1 demonstrates the basics of getting a dialog
box up and running; now let's try something a little more complex. The
ABOUT2 program, shown in Figure 10-3, demonstrates how to manage controls
(in this case, radio buttons) within a dialog box procedure and also how to
paint on the client area of the dialog box.

 ABOUT2.MAK

#----------------------
# ABOUT2.MAK make file
#----------------------

about2.exe : about2.obj about2.def about2.res
     link about2, /align:16, NUL, /nod slibcew libw, about2
     rc about2.res

about2.obj : about2.c about2.h
     cl -c -Gsw -Ow -W2 -Zp about2.c

about2.res : about2.rc about2.h about2.ico
     rc -r about2.rc

 ABOUT2.C

/*------------------------------------------
   ABOUT2.C -- About Box Demo Program No. 2
               (c) Charles Petzold, 1990
  ------------------------------------------*/

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

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

short nCurrentColor  = IDD_BLACK,
      nCurrentFigure = IDD_RECT ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName [] = "About2" ;
     MSG         msg ;
     HWND        hwnd ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (hInstance, szAppName) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = szAppName ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "About Box Demo Program",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }
void PaintWindow (HWND hwnd, short nColor, short nFigure)
     {
     static DWORD dwColor [8] = { RGB (0,     0, 0), RGB (  0,   0, 255),
                                  RGB (0,   255, 0), RGB (  0, 255, 255),
                                  RGB (255,   0, 0), RGB (255,   0, 255),
                                  RGB (255, 255, 0), RGB (255, 255, 255) } ;
     HBRUSH       hBrush ;
     HDC          hdc ;
     RECT         rect ;

     hdc = GetDC (hwnd) ;
     GetClientRect (hwnd, &rect) ;
     hBrush = CreateSolidBrush (dwColor [nColor - IDD_BLACK]) ;
     hBrush = SelectObject (hdc, hBrush) ;

     if (nFigure == IDD_RECT)
          Rectangle (hdc, rect.left, rect.top, rect.right, rect.bottom) ;
     else
          Ellipse   (hdc, rect.left, rect.top, rect.right, rect.bottom) ;

     DeleteObject (SelectObject (hdc, hBrush)) ;
     ReleaseDC (hwnd, hdc) ;
     }

void PaintTheBlock (HWND hCtrl, short nColor, short nFigure)
     {
     InvalidateRect (hCtrl, NULL, TRUE) ;
     UpdateWindow (hCtrl) ;
     PaintWindow (hCtrl, nColor, nFigure) ;
     }

BOOL FAR PASCAL AboutDlgProc (HWND hDlg, WORD message, WORD wParam, LONG
lParam)
     {
     static HWND  hCtrlBlock ;
     static short nColor, nFigure ;

     switch (message)
          {
          case WM_INITDIALOG :
               nColor  = nCurrentColor ;
               nFigure = nCurrentFigure ;

               CheckRadioButton (hDlg, IDD_BLACK, IDD_WHITE, nColor) ;
               CheckRadioButton (hDlg, IDD_RECT,  IDD_ELL,   nFigure) ;

               hCtrlBlock = GetDlgItem (hDlg, IDD_PAINT) ;
               SetFocus (GetDlgItem (hDlg, nColor)) ;
               return FALSE ;
     switch (message)
          {
          case WM_CREATE :
               hInstance = ((LPCREATESTRUCT) lParam)->hInstance ;

               lpfnAboutDlgProc = MakeProcInstance (AboutDlgProc, hInstance)
;
               return 0 ;

          case WM_COMMAND :
               switch (wParam)
                    {
                    case IDM_ABOUT :
                         if (DialogBox (hInstance, "AboutBox", hwnd,
                                        lpfnAboutDlgProc))
                              InvalidateRect (hwnd, NULL, TRUE) ;
                         return 0 ;
                    }
               break ;

          case WM_PAINT :
               BeginPaint (hwnd, &ps) ;
               EndPaint (hwnd, &ps) ;
               PaintWindow (hwnd, nCurrentColor, nCurrentFigure) ;
               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 ABOUT2.RC

/*---------------------------
   ABOUT2.RC resource script
  ---------------------------*/

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

about2 ICON about2.ico

About2 MENU
     {
     POPUP "&Help"
          {
          MENUITEM "&About About2...",       IDM_ABOUT
          }
     }

#define TABGRP (WS_TABSTOP | WS_GROUP)

AboutBox DIALOG 20, 20, 140, 188
     STYLE WS_POPUP | WS_DLGFRAME
     {
     CTEXT       "About2"       -1,           0,  12, 140,   8
     ICON        "About2"       -1,           8,   8,   0,   0
     CTEXT       "About Box Demo Program" -1, 4,  36, 130,   8
     CTEXT       ""             IDD_PAINT,   68,  54,  60,  60
     GROUPBOX    "&Color"       -1,           4,  50,  54, 112
     RADIOBUTTON "&Black"       IDD_BLACK,    8,  60,  40,  12, TABGRP
     RADIOBUTTON "B&lue"        IDD_BLUE,     8,  72,  40,  12
     RADIOBUTTON "&Green"       IDD_GREEN,    8,  84,  40,  12
     RADIOBUTTON "Cya&n"        IDD_CYAN,     8,  96,  40,  12
     RADIOBUTTON "&Red"         IDD_RED,      8, 108,  40,  12
     RADIOBUTTON "&Magenta"     IDD_MAGENTA,  8, 120,  40,  12
     RADIOBUTTON "&Yellow"      IDD_YELLOW,   8, 132,  40,  12
     RADIOBUTTON "&White"       IDD_WHITE,    8, 144,  40,  12
     GROUPBOX    "&Figure"      -1,          68, 120,  60,  40, WS_GROUP
     RADIOBUTTON "Rec&tangle"   IDD_RECT,    72, 134,  50,  12, TABGRP
     RADIOBUTTON "&Ellipse"     IDD_ELL,     72, 146,  50,  12
     DEFPUSHBUTTON "OK"         IDOK,        20, 168,  40,  14, WS_GROUP
     PUSHBUTTON  "Cancel"       IDCANCEL,    80, 168,  40,  14, WS_GROUP
     }

 ABOUT2.H

/*----------------------
   ABOUT2.H header file
  ----------------------*/

#define IDM_ABOUT      1

#define IDD_BLACK     10
#define IDD_BLUE      11
#define IDD_GREEN     12
#define IDD_CYAN      13
#define IDD_RED       14
#define IDD_MAGENTA   15
#define IDD_YELLOW    16
#define IDD_WHITE     17
#define IDD_RECT      20
#define IDD_ELL       21

#define IDD_PAINT     30

 ABOUT2.ICO  -- Please refer to the book.

FIG 1003.EPB

 ABOUT2.DEF

;-----------------------------------
; ABOUT2.DEF module definition file
;-----------------------------------

NAME           ABOUT2

DESCRIPTION    'About Box Demo No. 2 (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc
               AboutDlgProc

The About box in ABOUT2 has two groups of radio buttons. One group is used
to select a color, and the other group is used to select either a rectangle
or an ellipse. The rectangle or ellipse is shown in the dialog box with the
interior colored with the current color selection. If you press the OK
button, the dialog box is ended, and the program's window procedure draws
the selected figure on its own client area. If you press Cancel, the client
area of the main window remains the same. The dialog box is shown in Figure
10-4 on the following page. Although the ABOUT2 dialog box uses the
predefined identifiers IDOK and IDCANCEL for the two push buttons, each of
the radio buttons has its own identifier beginning with the letters IDD ("ID
for dialog box control"). These identifiers are defined in ABOUT2.H.

  (Figure 10-4. may be found in the printed book.)


Working with Dialog Box Controls

In Chapter 6, you discovered that most child window controls send WM_COMMAND
messages to the parent window. (The exception is scroll bar controls.) You
also saw that the parent window can alter child window controls (for
instance, checking or unchecking radio buttons or check boxes) by sending
messages to the controls. You can similarly alter controls in a dialog box
procedure. If you have a series of radio buttons, for example, you can check
and uncheck the buttons by sending them messages. However, Windows also
provides several shortcuts when working with controls in dialog boxes. Let's
look at the way in which the dialog box procedure and the child window
controls communicate.

The dialog box template for ABOUT2 is shown in the ABOUT2.RC resource script
in Figure 10-3. The GROUPBOX control is simply a frame with a title (either
Color or Figure) that surrounds each of the two groups of radio buttons. The
eight radio buttons in the first group are mutually exclusive, as are the
two radio buttons in the second group.

When one of the radio buttons is clicked with the mouse (or when the
Spacebar is pressed while the radio button has the input focus), the child
window sends its parent a WM_COMMAND message with wParam set to the ID of
the control. The low word of lParam is the window handle of the control, and
the high word of lParam is a notification code. For a radio button, this
notification code is BN_CLICKED, or 0. The dialog box window procedure in
Windows then passes this WM_COMMAND message to the dialog box procedure
within ABOUT2.C. When the dialog box procedure receives a WM_COMMAND message
for one of the radio buttons, it turns on the check mark for that button and
turns off the check marks for all the other buttons in the group.

You may recall from Chapter 6 that checking and unchecking a button requires
that you send the child window control a BM_CHECK message. To turn on a
button check, you use:

SendMessage (hwndCtrl, BM_SETCHECK, 1, 0L) ;

To turn off the check, you use:

SendMessage (hwndCtrl, BM_SETCHECK, 0, 0L) ;

The hwndCtrl parameter is the window handle of the child window button
control.

But this method presents a little problem in the dialog box procedure,
because you don't know the window handles of all radio buttons. You know
only the one you're getting the message from. Fortunately, Windows provides
you with a function to obtain the window handle of a dialog box control
using the dialog box window handle and the control ID:

hwndCtrl = GetDlgItem (hDlg, nID) ;

(You can also obtain the ID value of a control from the window handle by
using this function:

nID = GetWindowWord (hwndCtrl, GWW_ID) ;

but this is rarely necessary.)

You'll notice in the ABOUT2.H header file shown in Figure 10-3 that the ID
values for the eight colors are sequential from IDD_BLACK to IDD_WHITE. This
arrangement helps in processing the WM_COMMAND messages from the radio
buttons. For a first attempt at checking and unchecking the radio buttons,
you might try something like the following in the dialog box procedure:

static short nColor ;
[other program lines]
case WM_COMMAND :
     switch (wParam)
          {
[other program lines]
          case IDD_BLACK :
          case IDD_RED :
          case IDD_GREEN :
          case IDD_YELLOW :
          case IDD_BLUE :
          case IDD_MAGENTA :
          case IDD_CYAN :
          case IDD_WHITE :
               nColor = wParam ;

               for (n = IDD_BLACK, n <= IDD_WHITE, n++)
                    SendMessage (GetDlgItem (hDlg, n),
                         BM_SETCHECK, n == wParam, 0L) ;
               return TRUE ;
[other program lines]

This approach works satisfactorily. You've saved the new color value in
nColor, and you've also set up a loop that cycles through all the ID values
for the eight colors. You obtain the window handle of each of these eight
radio button controls and use SendMessage to send each handle a BM_SETCHECK
message. The wParam value of this message is set to 1 only for the button
that sent the WM_COMMAND message to the dialog box window procedure.

The first shortcut is the special dialog box procedure SendDlgItemMessage:

SendDlgItemMessage (hDlg, nCtrlID, message, wParam, lParam) ;

It is equivalent to:

SendMessage (GetDlgItem (hDlg, nCtrlID), message, wParam, lParam) ;

Now the loop would look like this:

for (n = IDD_BLACK, n <= IDD_WHITE, n++)
     SendDlgItemMessage (hDlg, n, BM_SETCHECK, n == wParam, 0L) ;

That's a little better. But the real breakthrough comes when you discover
the CheckRadioButton function:

CheckRadioButton (hDlg, nIDFirst, nIDLast, nIDCheck) ;

This function turns off the checks on all radio button controls with IDs
from nIDFirst to nIDLast except for the radio button with an ID of nIDCheck,
which is checked. The IDs must be sequential. So we can get rid of the loop
entirely and use:

CheckRadioButton (hDlg, IDD_BLACK, IDD_WHITE, wParam) ;

That's how it's done in the dialog box procedure in ABOUT2.

A similar shortcut function is provided for working with check boxes. If you
create a CHECKBOX dialog window control, you can turn the check mark on and
off using the function:

CheckDlgButton (hDlg, nIDCheckbox, wCheck) ;

If wCheck is set to 1, the button is checked; if it's set to 0, the button
is unchecked. You can obtain the status of a check box in a dialog box
using:

wCheck = IsDlgButtonChecked (hDlg, nIDCheckbox) ;

You can either retain the current status of the check mark as a static
variable within the dialog box procedure, or you can do something like this
to toggle the button on a WM- _COMMAND message:

CheckDlgButton (hDlg, nIDCheckbox,
     !IsDlgButtonChecked (hDlg, nIDCheckbox)) ;

If you define a BS_AUTOCHECKBOX control, then you don't need to process the
WM_COMMAND message at all. You can simply obtain the current status of the
button using IsDlgButtonChecked before terminating the dialog box.


The OK and Cancel Buttons

ABOUT2 has two push buttons, labeled OK and Cancel. In the dialog box
template in ABOUT2.RC, the OK button has an ID of IDOK (defined in WINDOWS.H
as 1) and the Cancel button an ID of IDCANCEL (defined in WINDOWS.H as 2).
The OK button is the default:

DEFPUSHBUTTON "OK"     IDOK,     20, 168, 40, 14, WS_GROUP
PUSHBUTTON    "Cancel" IDCANCEL, 80, 168, 40, 14, WS_GROUP

This arrangement is normal for OK and Cancel buttons in dialog boxes; having
the OK button as the default helps out with the keyboard interface. Here's
how: Normally, you would end the dialog box by clicking one of these buttons
with the mouse or pressing the Spacebar when the desired button has the
input focus. However, the dialog box window procedure also generates a
WM_COMMAND message when the user presses Enter, regardless of which control
has the input focus. The value of wParam is set to the ID value of the
default push button in the dialog box unless another push button has the
input focus. In that case, wParam is set to the ID of the push button with
the input focus. If no push button in the dialog box is a default push
button, then Windows sends the dialog box procedure a WM_COMMAND message
with wParam equal to IDOK. If the user presses the Esc key or Ctrl-Break,
Windows sends the dialog box procedure a WM_COMMAND message with wParam
equal to IDCANCEL. So you don't have to add separate keyboard logic to the
dialog box procedure, because the keystrokes that normally terminate a
dialog box are translated by Windows into WM_COMMAND messages for these two
push buttons.

The AboutDlgProc function handles these two WM_COMMAND messages by calling
EndDialog:

switch (wParam)
     {
     case IDOK :
          nCurrentColor  = nColor ;
          nCurrentFigure = nFigure ;
          EndDialog (hDlg, TRUE) ;
          return TRUE ;

     case IDCANCEL :
          EndDialog (hDlg, FALSE) ;
          return TRUE ;

ABOUT2's window procedure uses the global variables nCurrentColor and
nCurrentFigure when drawing the rectangle or ellipse in the program's client
area. AboutDlgProc uses the static local variables nColor and nFigure when
drawing the figure within the dialog box.

Notice the different values in the second parameter of EndDialog. This is
the value that is passed back as the return value from the original
DialogBox function in WndProc:

case IDM_ABOUT :
     if (DialogBox (hInstance, "AboutBox", hwnd, lpfnAboutDlgProc))
          InvalidateRect (hwnd, NULL, TRUE) ;
     return 0 ;

If DialogBox returns TRUE (nonzero), meaning that the OK button was pressed,
then the WndProc client area needs to be updated with the new figure and
color. These were saved in the global variables nCurrentColor and
nCurrentFigure by AboutDlgProc when it received a WM_COMMAND message with
wParam equal to IDOK. If DialogBox returns FALSE, the main window continues
to use the original settings of nCurrentColor and nCurrentFigure.

TRUE and FALSE are commonly used in EndDialog calls to signal to the main
window procedure whether the user ended the dialog box with OK or Cancel.
However, the parameter to EndDialog is actually an int, and DialogBox
returns an int, so it's possible to return more information in this way than
simply TRUE or FALSE.


Tab Stops and Groups

In Chapter 6, we used window subclassing to add a facility to COLORS1 that
let us move from one scroll bar to another by pressing the Tab key. In a
dialog box, window subclassing is unnecessary: Windows does all the logic
for moving the input focus from one control to another. However, you have to
help out by using the WS_TABSTOP and WS_GROUP window styles in the dialog
box template. For all controls that you want to access using the Tab key,
specify WS_TABSTOP in the window style. If you refer back to the table on
page 415, you'll notice that many of the controls include WS_TABSTOP as a
default, while others do not. Generally the controls that do not include
WS_TABSTOP style (particularly the static controls) should not get the input
focus because they can't do anything with it. Unless you set the input focus
to a specific control in a dialog box during processing of the WM_INITDIALOG
message and return FALSE from the message, Windows sets the input focus to
the first control in the dialog box that has the WS_TABSTOP style.

The second keyboard interface that Windows adds to a dialog box involves the
cursor movement keys. This interface is of particular importance with radio
buttons. After you use the Tab key to move to the currently checked radio
button within a group, you need to use the cursor movement keys to change
the input focus from that radio button to other radio buttons within the
group. You accomplish this by using the WS_GROUP window style. For a
particular series of controls in the dialog box template, Windows will use
the cursor movement keys to shift the input focus from the first control
that has the WS_GROUP style up to (but not including) the next control that
has the WS_GROUP style. Windows will cycle from the last control in a dialog
box to the first control if necessary to find the end of the group.

By default, the controls LTEXT, CTEXT, RTEXT, and ICON include the WS_GROUP
style, which conveniently marks the end of a group. You often have to add
WS_GROUP styles to other types of controls.

Let's look at the dialog box template in ABOUT2.RC:

AboutBox DIALOG 20, 20, 140, 188
     STYLE WS_POPUP | WS_DLGFRAME
     {
     CTEXT       "About2"       -1,           0,  12, 140,   8
     ICON        "About2"       -1,           8,   8,   0,   0
     CTEXT       "About Box Demo Program" -1, 4,  36, 130,   8
     CTEXT       ""             IDD_PAINT,   68,  54,  60,  60
     GROUPBOX    "&Color"       -1,           4,  50,  54, 112
     RADIOBUTTON "&Black"       IDD_BLACK,    8,  60,  40,  12, TABGRP
     RADIOBUTTON "B&lue"        IDD_BLUE,     8,  72,  40,  12
     RADIOBUTTON "&Green"       IDD_GREEN,    8,  84,  40,  12
     RADIOBUTTON "Cya&n"        IDD_CYAN,     8,  96,  40,  12
     RADIOBUTTON "&Red"         IDD_RED,      8, 108,  40,  12
     RADIOBUTTON "&Magenta"     IDD_MAGENTA,  8, 120,  40,  12
     RADIOBUTTON "&Yellow"      IDD_YELLOW,   8, 132,  40,  12
     RADIOBUTTON "&White"       IDD_WHITE,    8, 144,  40,  12
     GROUPBOX    "&Figure"      -1,          68, 120,  60,  40, WS_GROUP
     RADIOBUTTON "Rec&tangle"   IDD_RECT,    72, 134,  50,  12, TABGRP
     RADIOBUTTON "&Ellipse"     IDD_ELL,     72, 146,  50,  12
     DEFPUSHBUTTON "OK"         IDOK,        20, 168,  40,  14, WS_GROUP
     PUSHBUTTON  "Cancel"       IDCANCEL,    80, 168,  40,  14, WS_GROUP
     }

To simplify the appearance of the template, an identifier is defined in
ABOUT2.RC that combines WS_TABSTOP and WS_GROUP:

#define TABGRP (WS_TABSTOP | WS_GROUP)

The four controls that have the WS_TABSTOP style are the first radio buttons
of each group (explicitly included) and the two push buttons (by default).
When you first invoke the dialog box, these are the four controls you can
move among using the Tab key.

Within each group of radio buttons, you use the cursor movement keys to
change the input focus and the check mark. For example, the first radio
button of the Color group (Black) and the group box labeled Figure have the
WS_GROUP style. This means that you can use the cursor movement keys to move
the focus from the Black radio button up to (but not including) the Figure
group box. Similarly, the first radio button of the Figure group (Rectangle)
and DEFPUSHBUTTON have the WS_GROUP style, so you can use the cursor
movement keys to move between the two radio buttons in this group: Rectangle
and  Ellipse. Both push buttons get a WS_GROUP style to prevent the cursor
movement keys from doing anything when the push buttons have the input
focus.

You'll notice when using ABOUT2 that the dialog box manager in Windows
performs some magic in the two groups of radio buttons. As expected, the
cursor movement keys within a group of radio buttons shift the input focus
and send a WM_COMMAND message to the dialog box procedure. But when you
change the checked radio button within the group, Windows also assigns the
newly checked radio button the WS_TABSTOP style. The next time you tab to
that group, Windows will set the input focus to the checked radio button.

An ampersand (&) causes the letter that follows to be underlined and adds
another keyboard interface. You can move the input focus to any of the radio
buttons by pressing the underlined letter. By pressing C (for the Color
group box) or F (for the Figure group box), you can move the input focus to
the currently checked radio button in that group.

Although programmers normally let the dialog box manager take care of all
this, Windows includes two functions that let you search for the next or
previous tab stop or group item. These functions are:

hwndCtrl = GetNextDlgTabItem (hDlg, hwndCtrl, bPrevious) ;

and:

hwndCtrl = GetNextDlgGroupItem (hDlg, hwndCtrl, bPrevious) ;

If bPrevious is TRUE, the functions return the previous tab stop or group
item; if FALSE, they return the next tab stop or group item.


Painting on the Dialog Box

ABOUT2 also does something relatively unusual: It paints on the dialog box.
Let's see how this works. Within the dialog box template in ABOUT2.RC, a
blank text control is defined with a position and size for the area we want
to paint:

CTEXT  ""  IDD_PAINT, 68, 54, 60, 60

This area is 15 characters wide and 7-1/2 characters high. Because this
control has no text, all that the window procedure for the "static" class
does is erase the background when the child window control has to be
repainted.

When the current color or figure selection changes or when the dialog box
itself gets a WM_PAINT message, the dialog box procedure calls
PaintTheBlock, which is a function in ABOUT2.C:

PaintTheBlock (hCtrlBlock, nColor, nFigure) ;

The window handle hCtrlBlock had been set during processing of the
WM_INITDIALOG message:

hCtrlBlock = GetDlgItem (hDlg, IDD_PAINT) ;

Here's the PaintTheBlock function:

void PaintTheBlock (HWND hCtrl, short nColor, short nFigure)
     {
     InvalidateRect (hCtrl, NULL, TRUE) ;
     UpdateWindow (hCtrl) ;
     PaintWindow (hCtrl, nColor, nFigure) ;
     }

This invalidates the child window control, updates it, and then calls
another function in ABOUT2 called PaintWindow.

The PaintWindow function obtains a device context handle for hCtrl and draws
the selected figure, filling it with a colored brush based on the selected
color. The size of the child window control is obtained from GetClientRect.
Although the dialog box template defines the size of the control in terms of
characters, GetClientRect obtains the dimensions in pixels. You can also use
the function MapDialogRect to convert the character coordinates in the
dialog box to pixel coordinates in the client area.

We're not really painting the dialog box's client area--we're actually
painting the client area of the child window control. Whenever the dialog
box gets a WM_PAINT message, the child window control is invalidated and
then updated to make it believe that its client area is now valid. We then
paint on top of it.


Using Other Functions with Dialog Boxes

Most functions that you can use with child windows you can also use with
controls in a dialog box. For instance, if you're feeling devious, you can
use MoveWindow to move the controls around the dialog box and have the user
chase them around with the mouse.

Sometimes you need to dynamically enable or disable certain controls in a
dialog box, depending on the settings of other controls. This call:

EnableWindow (hwndCtrl, bEnable) ;

enables the control where bEnable is TRUE (nonzero) and disables it where
bEnable is FALSE (0). When a control is disabled, it receives no keyboard or
mouse input. Don't disable a control that has the input focus.


Defining Your Own Controls

Although Windows assumes much of the responsibility for maintaining the
dialog box and child window controls, various methods let you slip some of
your own code into this process. We've already seen a method that allows you
to paint on the surface of a dialog box. You can also use window subclassing
(discussed in Chapter 6) to alter the operation of child window controls.

You can also define your own child window controls and use them in a dialog
box. For example, suppose you don't particularly care for the normal
rectangular push buttons and would prefer to create elliptical push buttons.
You can do this by registering a window class and using your own window
procedure to process messages for your customized child window. You then
specify this window class in a CONTROL statement in the dialog box template.
The ABOUT3 program, shown in Figure 10-5, does exactly that.

 ABOUT3.MAK

#----------------------
# ABOUT3.MAK make file
#----------------------

about3.exe : about3.obj about3.def about3.res
     link about3, /align:16, NUL, /nod slibcew libw, about3
     rc about3.res

about3.obj : about3.c about3.h
     cl -c -Gsw -Ow -W2 -Zp about3.c

about3.res : about3.rc about3.h about3.ico
     rc -r about3.rc

 ABOUT3.C

/*------------------------------------------
   ABOUT3.C -- About Box Demo Program No. 3
               (c) Charles Petzold, 1990
  ------------------------------------------*/

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

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;
long FAR PASCAL EllipPushWndProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName [] = "About3" ;
     MSG         msg ;
     HWND        hwnd ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;



          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (hInstance, szAppName) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = szAppName ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;

          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = EllipPushWndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = NULL ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = COLOR_WINDOW + 1 ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = "EllipPush" ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "About Box Demo Program",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

BOOL FAR PASCAL AboutDlgProc (HWND hDlg, WORD message, WORD wParam, LONG
lParam)
     {
     switch (message)
          {
          case WM_INITDIALOG :
               return TRUE ;
          case WM_COMMAND :
               switch (wParam)
                    {
                    case IDOK :
                         EndDialog (hDlg, 0) ;
                         return TRUE ;
                    }
               break ;
          }
     return FALSE ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static FARPROC lpfnAboutDlgProc ;
     static HANDLE  hInstance ;

     switch (message)
          {
          case WM_CREATE :
               hInstance = ((LPCREATESTRUCT) lParam)->hInstance ;

               lpfnAboutDlgProc = MakeProcInstance (AboutDlgProc, hInstance)
;
               return 0 ;

          case WM_COMMAND :
               switch (wParam)
                    {
                    case IDM_ABOUT :
                         DialogBox (hInstance, "AboutBox", hwnd,
                                        lpfnAboutDlgProc) ;
                         return 0 ;
                    }
               break ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

long FAR PASCAL EllipPushwndProc (HWND hwnd, WORD message,
                                  WORD wParam, LONG lParam)
     {
     char        szText [40] ;
     HBRUSH      hBrush ;
     HDC         hdc ;
     PAINTSTRUCT ps ;
     RECT        rect ;
     switch (message)
          {
          case WM_PAINT :
               GetClientRect (hwnd, &rect) ;
               GetWindowText (hwnd, szText, sizeof szText) ;

               hdc = BeginPaint (hwnd, &ps) ;

               hBrush = CreateSolidBrush (GetSysColor (COLOR_WINDOW)) ;
               hBrush = SelectObject (hdc, hBrush) ;
               SetBkColor (hdc, GetSysColor (COLOR_WINDOW)) ;
               SetTextColor (hdc, GetSysColor (COLOR_WINDOWTEXT)) ;

               Ellipse (hdc, rect.left, rect.top, rect.right, rect.bottom) ;
               DrawText (hdc, szText, -1, &rect,
                              DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;

               DeleteObject (SelectObject (hdc, hBrush)) ;

               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_KEYUP :
               if (wParam != VK_SPACE)
                    break ;
                                        // fall through
          case WM_LBUTTONUP :
               SendMessage (GetParent (hwnd), WM_COMMAND,
                    GetWindowWord (hwnd, GWW_ID), (LONG) hwnd) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 ABOUT3.RC

/*---------------------------
   ABOUT3.RC resource script
  ---------------------------*/

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

about3 ICON about3.ico
About3 MENU
     {
     POPUP "&Help"
          {
          MENUITEM "&About About3...",       IDM_ABOUT
          }
     }

#define TABGRP (WS_TABSTOP | WS_GROUP)

AboutBox DIALOG  20, 20, 160, 80
     STYLE WS_POPUP | WS_DLGFRAME
     {
     CTEXT   "About3"                         -1,  0, 12, 160,  8
     ICON    "About3"                         -1,  8,  8,   0,  0
     CTEXT   "About Box Demo Program"         -1,  0, 36, 160,  8
     CTEXT   "(c) Charles Petzold, 1990"      -1,  0, 48, 160,  8
     CONTROL "OK" IDOK, "EllipPush", TABGRP,      64, 60,  32, 14
     }

 ABOUT3.H

/*----------------------
   ABOUT3.H header file
  ----------------------*/

#define IDM_ABOUT      1

 ABOUT3.ICO  -- Please refer to the book.

FIG 10-05.epb

 ABOUT3.DEF

;-----------------------------------
; ABOUT3.DEF module definition file
;-----------------------------------

NAME           ABOUT3

DESCRIPTION    'About Box Demo No. 3 (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc
               AboutDlgProc
               EllipPushWndProc

The window class we'll be registering is called "EllipPush" ("elliptical
push button"). Rather than use a DEFPUSHBUTTON statement in the dialog box
template, we use a CONTROL statement that specifies this window class:

CONTROL "OK" IDOK, "EllipPush", TABGRP, 64, 60, 32, 14

The dialog box manager uses this window class in a CreateWindow call when
creating the child window control in the dialog box.

The ABOUT3.C program registers the "EllipPush" window class in WinMain:

wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc   = EllipPushWndProc ;
wndclass.cbClsExtra    = 0 ;
wndclass.cbWndExtra    = 0 ;
wndclass.hInstance     = hInstance ;
wndclass.hIcon         = NULL ;
wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = COLOR_WINDOW + 1 ;
wndclass.lpszMenuName  = NULL ;
wndclass.lpszClassName = "EllipPush" ;

RegisterClass (&wndclass) ;

The window class specifies that the window procedure is EllipPushWndProc,
which is also in ABOUT3.C.

The EllipPushWndProc window procedure processes only three messages:
WM_PAINT, WM_KEYUP, and WM_LBUTTONUP. During the WM_PAINT message, it
obtains the size of its window from GetClientRect and obtains the text that
appears in the push button from GetWindowText. It uses the Windows functions
Ellipse and DrawText to draw the ellipse and the text.

The processing of the WM_KEYUP and WM_LBUTTONUP messages is very simple:

case WM_KEYUP :
     if (wParam != VK_SPACE)
          break ;
                              // fall through
case WM_LBUTTONUP :
     SendMessage (GetParent (hwnd), WM_COMMAND,
          GetWindowWord (hwnd, GWW_ID), (LONG) hwnd) ;
     return 0 ;

The window procedure obtains the handle of its parent window (the dialog
box) using GetParent and sends a WM_COMMAND message with wParam equal to the
control's ID. The ID is obtained using GetWindowWord. The dialog box window
procedure then passes this message on to the dialog box procedure within
ABOUT3. The result is a customized push button, as shown in Figure 10-6. You
can use this same method to create other customized controls for dialog
boxes.

  (Figure 10-6. may be found in the printed book.)

Is that all there is to it? Well, not really. EllipPushWndProc is a
bare-bones version of the logic generally involved in maintaining a child
window control. For instance, the button doesn't flash like normal push
buttons. To invert the colors on the interior of the push button, the window
procedure would have to process WM_KEYDOWN (from the Spacebar) and
WM_LBUTTONDOWN messages. The window procedure should also capture the mouse
on a WM_LBUTTONDOWN message and release the mouse capture (and return the
button's interior color to normal) if the mouse is moved outside the child
window's client area while the button is still depressed. Only if the button
is released while the mouse is captured should the child window send a
WM_COMMAND message back to its parent.

EllipPushWndProc also does not process WM_ENABLE messages. As mentioned
above, a dialog box procedure can disable a window using the EnableWindow
function. The child window would then display gray rather than black text to
indicate that it has been disabled and cannot receive messages.

If the window procedure for a child window control needs to store data that
are different for each created window, then it can do so by using a positive
value of cbWndExtra in the window class structure. This reserves space in
the internal window structure that can be accessed by using SetWindowWord,
SetWindowLong, GetWindowWord, and GetWindowLong.



THE MESSAGE BOX

Let's take a breather here. We've been looking at ways to customize dialog
boxes. Now let's look at an alternative to dialog boxes, which is the
message box. We began using message boxes way back in Chapter 5, but we
haven't yet examined them in detail.

The message box is an appropriate and easy-to-use alternative to a dialog
box when you need a simple response from the user. The general syntax is:

nItem = MessageBox (hwndParent, lpszText, lpszCaption, nType) ;

The message box has a caption (the character string lpszCaption), one or
more lines of text (lpszText), one or more buttons, and (optionally) a
predefined icon. One of the buttons is a default. The nItem value returned
from MessageBox indicates the button that was pressed.

The hwndParent parameter is generally the handle to the window that creates
the message box. The input focus will be set to this window when the message
box is destroyed. If you don't have a window handle available or you don't
want the input focus to go to one of your windows, you can use NULL for the
handle. If you use a message box within a dialog box, use the dialog box
window handle (which we've been calling hDlg) for this parameter.

The lpszText parameter is a long pointer to NULL-terminated text that
appears in the body of the message box. Windows breaks this text into
several lines if necessary. You can also include tab characters in the text,
and you can define your own line breaks using carriage returns or linefeeds
or both. The lpszCaption string is generally the name of the application.

The nType parameter is a collection of flags joined by the C bitwise OR
operator. The first group of flags specifies the push buttons to appear at
the bottom of the message box: MB_OK (the default), MB_OKCANCEL, MB_YESNO,
MB_YESNOCANCEL, MB_RETRYCANCEL, and MB_ABORTRETRYIGNORE. As you can see,
these flags allow for a maximum of three buttons. The second group of flags
specifies which of the buttons is the default: MB_DEFBUTTON1 (the default),
MB_DEFBUTTON2, and MB_DEFBUTTON3.

The third group of flags specifies an icon to appear in the message box:
MB_ICONINFORMATION, MB_ICONEXCLAMATION, MB_ICONSTOP, and MB_ICONQUESTION.
There is no default. If you omit one of these flags, the message box has no
icon. You should use the information icon for a status message, the
exclamation point for a reminder, the question mark for a warning of the
consequences of an action, and the stop icon for a signal of serious
problems.

The fourth set of flags governs whether the message box is application
modal, in which case the user can switch to another application without
ending the message box, or system modal, which requires the user to end the
message box before doing anything else. The flags are MB_APPLMODAL (the
default) and MB_SYSTEMMODAL. Finally, you can use a fifth flag, MB_NOFOCUS,
which displays the message box but does not give it the input focus.

Depending on which button is pressed, the message box returns one of the
following identifiers: IDOK, IDCANCEL, IDYES, IDNO, IDRETRY, and IDABORT.

The Assertion Message Box

Although message boxes are customarily used to convey messages and ask
questions in finished programs, they are also helpful in debugging. For
instance, you may be familiar with the assert macro included in the ASSERT.H
header file with the Microsoft C Compiler. This macro is used for testing
various conditions during a program's execution. If the condition does not
hold, the macro displays the current source code filename and line number
and terminates the program. You can create a similar macro for Windows:

#ifndef NDEBUG

#define WinAssert(exp)\
               {\
               if (!(exp))\
                    {\
                    char szBuffer [40] ;\
                    sprintf (szBuffer, "File %s, Line %d",\
                         __FILE__, __LINE__) ;\
                    MessageBox (NULL, szBuffer,\
                         "Assertion Error",\
                         MB_OK | MB_ICONSTOP) ;\
                    }\
               }

#else

#define WinAssert(exp)

#endif

You can then make various assertions in your code. For instance, to be sure
that the value of hwnd in a certain section of your program is never NULL,
you can do this:

WinAssert (hwnd != NULL) ;

If hwnd is NULL when this statement is executed, the message box will be
displayed to alert you to the problem. A macro is used rather than a
function, because the predefined identifiers __FILE__ and __LINE__ must
equal the source code filename and line number where the assertion failed.
If you used a function, these identifiers would always be set to the
filename and line number where the function was located.

Unlike assert, the WinAssert macro shown above doesn't terminate the program
if the assertion fails. If you use a debugging terminal, you can use a
different version of this macro shown below. When you select the Abort
button, the FatalExit function is called to display a stack trace on the
debugging terminal.

if (IDABORT == MessageBox (NULL, szBuffer,
                           "Assertion Error",
                           MB_ABORTRETRYIGNORE | MB_ICONSTOP))
     FatalExit (-1) ;

Once you've finished debugging the program, you can compile with the
identifier NDEBUG, defined by using the compiler switch -D NDEBUG. The
WinAssert macro will then be defined as nothing.


Popup Information

Another handy use of a message box during program development is to provide
information to you while the program is executing. It would be ideal if you
could use a message box much as you use printf in C programs for MS-DOS,
with a formatting string and a variable number of arguments. And in fact,
you can create a function that lets you do this:

void OkMsgBox (char *szCaption, char *szFormat, ...)
     {
     char szBuffer [256] ;
     char *pArguments ;

     pArguments = (char *) &szFormat + sizeof szFormat ;
     vsprintf (szBuffer, szFormat, pArguments) ;
     MessageBox (NULL, szBuffer, szCaption, MB_OK) ;
     }

The vsprintf function is similar to sprintf except that it uses a pointer to
a series of arguments (pArguments) rather than the arguments themselves.
OkMsgBox sets pArguments to the arguments on the stack when OkMsgBox is
called. The first parameter to OkMsgBox is the message box caption, the
second parameter is a format string, and the third and subsequent parameters
are values to be displayed. Let's say you want a message box to appear every
time the window procedure gets a WM_SIZE message. Your code might look like
this:

case WM_SIZE :
     OkMsgBox ("WM_SIZE Message",
          "wParam = %04X, lParam = %04X-%04X",
          wParam, HIWORD (lParam), LOWORD (lParam)) ;
[other program lines]
     return 0 ;

This displays the values of wParam and lParam within the message box.



WORKING WITH FILES: POPPAD REVISITED

When we added a menu to POPPAD in Chapter 9, several menu options were left
unimplemented. We are now almost ready to add logic to POPPAD to open files,
read them in, and save the edited files to disk.

Working with files in Windows is no great joy. Although the standard dialog
box template to open a file is fairly simple, the dialog box procedure
itself is one of the most difficult you'll encounter. Before tackling that
function, let's investigate methods of file I/O in Windows programs.

The OpenFile Function Call

Windows includes a function to open a file and return an MS-DOS file handle:

hFile = OpenFile (lpszFileName, &of, wFunction) ;

The OpenFile funtion call returns a -1 if an error is encountered. For most
uses of OpenFile, a return value other than -1 will be an MS-DOS file handle
that you can use to read from and write to the file.

The lpszFileName parameter is a long (or far) pointer to the filename. A
disk drive and subdirectory are optional. The wFunction parameter tells
Windows what to do with this file (open it, create it, delete it, and so
forth). I'll describe this parameter in more detail shortly.

The &of parameter is a far pointer to a structure of type OFSTRUCT ("open
file structure"). You don't need to set any of the fields of this structure
before calling OpenFile: They are filled in by the first OpenFile function
call you make and are then used in subsequent OpenFile calls for the same
file. The OFSTRUCT fields are shown below:

Field            Data Type  Description
────────────────────────────────────────────────────────────────────────────
cBytes           BYTE       Length of structure in bytes
fFixedDisk       BYTE       Nonzero for fixed-disk file
nErrCode         WORD       MS-DOS error code
reserved[4]      BYTE       File date and time
szPathName[128]  BYTE       Fully qualified pathname and filename

If the OpenFile call is successful, the szPathName field is filled with the
fully qualified filename, including the current disk drive and subdirectory
path. Under current versions of MS-DOS, the 128 characters allowed for this
name are about 40 more than needed. A fully qualified filename has 2 bytes
for the drive letter and colon, up to 64 bytes for the directory path
starting with the initial backslash, another byte for the backslash
following the path, up to 12 bytes for the filename (8-character name,
period, and 3-character extension),  and a terminating 0.

The OpenFile function has three advantages over opening a file by other
means:

  ■   The lpszFileName parameter is assumed to contain characters from the
      ANSI character set. OpenFile does an AnsiToOem conversion on the name
      before trying to open the file. You would have to convert the filename
      yourself if you opened the file by other means.

  ■   If the file is not found in the current directory, OpenFile searches
      for the file on all directories listed in the MS-DOS environment PATH
      string.

  ■   Windows determines the fully qualified filename and inserts it in the
      szPathName field of the structure.

This last item is the most important feature of OpenFile. Generally, the
lpszFileName parameter you pass to OpenFile when first opening or creating a
file is only a filename. When you use OpenFile subsequently to open the same
file, Windows uses the fully qualified szPathName field of the OFSTRUCT
structure.

Here's why that's important: Although each program instance in Windows has a
current disk drive and subdirectory associated with it, the DlgDirList
function (which we'll use later in this chapter) can change the current
drive and subdirectory associated with a program instance. Let's say you use
some means other than OpenFile to obtain a filename (without a drive or
directory path) located in the current directory and that you successfully
open and close the file. The user then uses the dialog box to get a new
file. In the process, the dialog box calls DlgDirList to change the current
drive or subdirectory. The user then cancels the dialog box, and your
program tries to open the original file again. It's gone! Well, it's not
gone, but the current drive or subdirectory is different, so your program
can't find the file. You avoid this problem by using OpenFile.

The wFunction parameter of the OpenFile call comprises one or more flags
joined by the C bitwise OR operator. First, you use one of the following
four flags to open an existing file or create a new file:

────────────────────────────────────────────────────────────────────────────
OF_READ                           Opens an existing file for reading only

OF_WRITE                          Opens an existing file for writing only

OF_READWRITE                      Opens an existing file for reading and
                                  writing

OF_CREATE                         Creates a new file, or opens an existing
                                  file and truncates the size of the file
                                  to 0


Following the OpenFile call, the file is open and ready for reading and
writing. The value returned from OpenFile is the MS-DOS file handle (unless
this value is -1, in which case the file could not be opened or created).

If you prefer that the file be closed following the OpenFile call, you can
add the flag OF_EXIST. This flag is generally used with OF_READ, OF_WRITE,
or OF_READWRITE to see if the specified file exists. You can also use this
flag with OF_CREATE to create the file and immediately close it. The file
can be reopened later. When you use OF_EXIST, the value returned from
OpenFile is -1 if the file does not exist or (with OF_CREATE) if the file
could not be created. Any file handle returned from OpenFile with an
OF_EXIST flag should be ignored, because the file has been closed.

With OF_READ, OF_WRITE, or OF_READWRITE, you can also use the flag
OF_PROMPT. If Windows can't find the file in the current directory or in one
of the directories in the MS-DOS environment PATH string, this flag causes
Windows to display the infamous "Insert [filename] disk in drive A:" message
box. Windows users appreciate this message box about as much as MS-DOS users
like the "Abort, Retry, Ignore" message, so use this flag with discretion.
Also keep in mind that the OF_PROMPT message box has only an OK button,
which means that fixed-disk users must scrounge up a disk to put in drive A
before proceeding. If you must use the OF_PROMPT flag, also use the
OF_CANCEL flag, which adds a Cancel button to the message box. If the user
selects Cancel, the OpenFile function returns -1.

That's it for the flags to open a file for the first time. The file is open
when the OpenFile function returns, unless you've included the OF_EXIST flag
(in which case the file has been closed) or OpenFile returns -1 (in which
case an error occurred). You can now read from or write to this file and
then close it. How you perform these actions is discussed in the next
section.

When you want to reopen the file, you use the OF_READ, OF_WRITE,
OF_READWRITE, or OF_CREATE flag in combination with the OF_REOPEN flag. The
OF_REOPEN flag causes Windows to use the szPathName field in the OFSTRUCT
structure to obtain the original disk drive, subdirectory, and filename of
the file. Even if the current drive or subdirectory has changed since the
file was first opened, this flag ensures that Windows will use the same
drive and subdirectory as in the first OpenFile call. If you use OF_REOPEN
with OF_CREATE, the size of the file will be truncated to 0.

When you use the OF_READ flag to open a file for reading only, you can also
use the OF_VERIFY flag. This flag causes Windows to use the reserved field
in the OFSTRUCT structure to verify that the date and time of the file being
reopened are the same as those stored during the original OpenFile call.
This works only for files opened with OF_READ, because the value for the
file date and time is updated when a write-only or read-write file is
closed.

Also available are two OpenFile flags that do not open files. The OF_DELETE
flag deletes a file. If the file cannot be found in the current
subdirectory, OpenFile uses the MS-DOS environment PATH string to search for
a file to delete. Perhaps you're feeling cruel, in which case you can use
the OF_PROMPT flag with OF_DELETE so that Windows also gets the chance to
delete the file on the disk in drive A. OpenFile returns -1 if no file was
deleted. If the file has previously been opened with OpenFile and then
closed, you can use OF_DELETE in combination with OF_REOPEN.

The OF_PARSE flag does not open a file or even check for a file's existence.
This flag is used by itself to parse the filename and fill in the szPathName
field of the OFSTRUCT structure. OpenFile returns -1 if the filename is
invalid--say, if it uses illegal characters.


Two Methods of File I/O

The main rule of file I/O in Windows is this: Do not keep files open for
long periods of time. More specifically, that means you should not keep a
file open between messages. You should open or create a file, read it or
write to it in several large gulps, and then close it--all in the course of
processing a single message.

You have two options for using files that have been opened with the OpenFile
call:

  ■   Use normal C functions for file I/O. The file handle returned from
      OpenFile can be used directly with the "low-level" file I/O functions.
      The most important of these functions are open, read, lseek, close,
      create, write, and tell. We used the read and close functions in the
      HEAD program in Chapter 5.

      The problem with these functions is that you can't use far pointers
      with them unless your program is compact model or large model. As you
      know, compact model and large model are not recommended for Windows
      programs because their data segments must be fixed in memory. If you
      want to read part of a file into a global memory segment, you must
      first read the file into a local memory block and then transfer it to
      the global memory block.

      You can also use the normal C buffered file I/O functions such as
      fopen, fread, fwrite, and fclose. The MS-DOS file handle returned from
      OpenFile can be converted to a structure of type FILE using fdopen.
      The buffered file I/O functions are of less value in Windows than in
      other environments, because you need to read and write in large
      chunks, and buffering doesn't help unless you're reading small parts
      of a file.

  ■   Use file I/O functions included in Windows.  These go by the names of
      _lopen, _lclose, _lcreat, _llseek, _lread, and _lwrite.  The "l"
      prefix indicates that these functions accept far pointers for read and
      write buffers, thus allowing you to use them with global memory
      blocks. (These functions existed in Windows since version 1 but have
      only been documented beginning in Windows 3.)

This third method turns out to be the easiest when the file must be read
into or written from buffer areas accessible only with far pointers. (The
normal C low-level file I/O calls are preferable when you can use near
pointers.) Do not write your own assembly-language routines for interfacing
with the MS-DOS file I/O functions.

You'll probably use OpenFile to open and create files, but you can also use
_lopen and _lcreat. The syntax is:

hFile = _lopen (lpszPathName, iReadWrite) ;

The lpszPathName parameter is a filename with an optional drive and
subdirectory path. The iReadWrite parameter should be set to one of the
identifiers OF_READ, OF_WRITE, or OF_READWRITE. The hFile value returned
from _lopen is an MS-DOS file handle if the file is opened or -1 if the file
cannot be opened.

The _lcreat function looks similar to the _lopen call:

hFile = _lcreat (lpszPathName, iAttribute) ;

However, the second parameter is an MS-DOS file attribute. Use 0 for a
normal (nonhidden, nonsystem, read-write) file. If the file already exists,
the size is truncated to 0 and opened; if it doesn't exist, it is created
and opened. Like the _lopen call, _lcreat returns an MS-DOS file handle if
the function is successful or -1 if an error occurs.

After an _lopen or _lcreat call, the file pointer is set initially to the
beginning of the file. Normally, all reading and writing is sequential. The
file pointer is updated after each read or write. However, you can use
_llseek (MS-DOS Function 42H) to change the file pointer:

lPosition = _llseek (hFile, lPosition, iMethod) ;

The iMethod parameter should be set to one of the following values:

Value                             Purpose
────────────────────────────────────────────────────────────────────────────
0                                 Move the file pointer lPosition bytes
                                  from the beginning of the file

1                                 Move the file pointer lPosition bytes
                                  from the current position in the file

2                                 Move the file pointer lPosition bytes
                                  from the end of the file


The value of lPosition returned from _llseek is the new position of the file
pointer if the function is successful or -1L if an error occurs.

If you want to open an existing file and add data to it, you call:

_llseek (hFile, 0L, 2) ;

This moves the file pointer to the end of the file. You can also use _llseek
to determine the size of a file. You might want to define a function called
FileLength to do this:

long FileLength (int hFile)
     {
     long   lCurrentPos = _llseek (hFile, 0L, 1) ;
     long   lFileLength = _llseek (hFile, 0L, 2) ;

     _llseek (hFile, lCurrentPos, 0) ;

     return lFileLength ;
     }

FileLength saves the current position of the file pointer, moves the file
pointer to the end  of the file, and then restores the file pointer to its
original position.

To write to a file, use:

wBytesWritten = _lwrite (hFile, lpBuffer, wBytes) ;

The lpBuffer parameter is a far pointer to the data you want to write to the
file, and wBytes is the number of bytes to write. The file buffer cannot
extend past the end of a segment. The wBytesWritten value returned from the
function is the number of bytes that are actually written. This can be less
than wBytes if not enough disk space is available to write the entire
buffer. Normally, MS-DOS function calls allow you to write up to 65,535
bytes to a file, but _lwrite returns -1 to signal an error, so you'll want
to restrict yourself to 65,534 bytes or less.

The function to read from a file is similar:

wBytesRead = _lread (hFile, lpBuffer, wBytes) ;

The lpBuffer parameter is a far pointer to an area that receives the data
read from the file, and wBytes is the number of bytes to read. The
wBytesRead value returned can be less than wBytes if the end of the file is
encountered before wBytes are read. A return value of -1 signals an error.

Finally, to close a file, use:

_lclose (hFile) ;

When working with files and global memory blocks, you may also need to make
use of string functions that work with far pointers. Windows includes
functions called lstrlen, lstrcpy, lstrcat, and lstrcmp that are equivalent
to the normal C string functions strlen, strcpy, strcat, and strcmp, except
that they use far pointers. These functions are useful for moving data
between two global memory blocks or between a global memory block and a
local memory block. They are coded in assembly language and are thus much
faster than equivalent C code. The lstrcmp function is a "Windows version"
of the strcmp function: It is case sensitive for both the normal ASCII
character codes and the ANSI character codes, and it can accommodate strings
with multibyte character codes.


Dialog Boxes for Open and Save

With these preliminaries out of the way, we are now ready to create dialog
box templates and dialog box procedures to assist our Windows programs in
opening and saving files. The FILEDLG.C source code file, FILEDLG.H header
file, and FILEDLG.DLG dialog box template file, shown in Figure 10-7 on the
following pages, will be used in the POPPAD3 program in this chapter. You
can use these routines (or similar ones) in your own programs.

 FILEDLG.C

/*-----------------------------------------------
   FILEDLG.C -- Open and Close File Dialog Boxes
  -----------------------------------------------*/

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

BOOL FAR PASCAL FileOpenDlgProc (HWND, WORD, WORD, LONG) ;
BOOL FAR PASCAL FileSaveDlgProc (HWND, WORD, WORD, LONG) ;

LPSTR lstrchr  (LPSTR str, char ch) ;
LPSTR lstrrchr (LPSTR str, char ch) ;

static char      szDefExt   [5] ;
static char      szFileName [96] ;
static char      szFileSpec [16] ;
static POFSTRUCT pof ;
static WORD      wFileAttr, wStatus ;

int DoFileOpenDlg (HANDLE hInst, HWND hwnd, char *szFileSpecIn,
                   char *szDefExtIn, WORD wFileAttrIn,
                   char *szFileNameOut, POFSTRUCT pofIn)
     {
     FARPROC lpfnFileOpenDlgProc ;
     int     iReturn ;

     lstrcpy (szFileSpec, szFileSpecIn) ;
     lstrcpy (szDefExt,   szDefExtIn) ;
     wFileAttr = wFileAttrIn ;
     pof = pofIn ;

     lpfnFileOpenDlgProc = MakeProcInstance (FileOpenDlgProc, hInst) ;

     iReturn = DialogBox (hInst, "FileOpen", hwnd, lpfnFileOpenDlgProc) ;

     FreeProcInstance (lpfnFileOpenDlgProc) ;

     lstrcpy (szFileNameOut, szFileName) ;
     return iReturn ;
     }

int DoFileSaveDlg (HANDLE hInst, HWND hwnd, char *szFileSpecIn,
                   char *szDefExtIn, WORD *pwStatusOut,
                   char *szFileNameOut, POFSTRUCT pofIn)
     {
     FARPROC lpfnFileSaveDlgProc ;
     int     iReturn ;



     lstrcpy (szFileSpec, szFileSpecIn) ;
     lstrcpy (szDefExt,   szDefExtIn) ;
     pof = pofIn ;

     lpfnFileSaveDlgProc = MakeProcInstance (FileSaveDlgProc, hInst) ;

     iReturn = DialogBox (hInst, "FileSave", hwnd, lpfnFileSaveDlgProc) ;

     FreeProcInstance (lpfnFileSaveDlgProc) ;

     lstrcpy (szFileNameOut, szFileName) ;
     *pwStatusOut = wStatus ;
     return iReturn ;
     }

BOOL FAR PASCAL FileOpenDlgProc (HWND hDlg, WORD message,
                                 WORD wParam, LONG lParam)
     {
     char  cLastChar ;
     short nEditLen ;

     switch (message)
        {
        case WM_INITDIALOG :
           SendDlgItemMessage (hDlg, IDD_FNAME, EM_LIMITTEXT, 80, 0L) ;
           DlgDirList (hDlg, szFileSpec, IDD_FLIST, IDD_FPATH, wFileAttr) ;
           SetDlgItemText (hDlg, IDD_FNAME, szFileSpec) ;
           return TRUE ;

        case WM_COMMAND :
           switch (wParam)
              {
              case IDD_FLIST :
                 switch (HIWORD (lParam))
                    {
                    case LBN_SELCHANGE :
                       if (DlgDirSelect (hDlg, szFileName, IDD_FLIST))
                          lstrcat (szFileName, szFileSpec) ;
                       SetDlgItemText (hDlg, IDD_FNAME, szFileName) ;
                       return TRUE ;

                    case LBN_DBLCLK :
                       if (DlgDirSelect (hDlg, szFileName, IDD_FLIST))
                          {
                          lstrcat (szFileName, szFileSpec) ;
                          DlgDirList (hDlg, szFileName, IDD_FLIST,
IDD_FPATH,
                                                              wFileAttr) ;
                          SetDlgItemText (hDlg, IDD_FNAME, szFileSpec) ;
                          }
                       else
                          {
                          SetDlgItemText (hDlg, IDD_FNAME, szFileName) ;
                          SendMessage (hDlg, WM_COMMAND, IDOK, 0L) ;
                          }
                       return TRUE ;
                    }
                 break ;

              case IDD_FNAME :
                 if (HIWORD (lParam) == EN_CHANGE)
                    EnableWindow (GetDlgItem (hDlg, IDOK),
                       (BOOL) SendMessage (LOWORD (lParam),
                                             WM_GETTEXTLENGTH, 0, 0L)) ;
                 return TRUE ;

              case IDOK :
                 GetDlgItemText (hDlg, IDD_FNAME, szFileName, 80) ;

                 nEditLen  = lstrlen (szFileName) ;
                 cLastChar = *AnsiPrev (szFileName, szFileName + nEditLen) ;

                 if (cLastChar == '\\' || cLastChar == ':')
                    lstrcat (szFileName, szFileSpec) ;

                 if (lstrchr (szFileName, '*') || lstrchr (szFileName, '?'))
                    {
                    if (DlgDirList (hDlg, szFileName, IDD_FLIST,
                                          IDD_FPATH, wFileAttr))
                       {
                       lstrcpy (szFileSpec, szFileName) ;
                       SetDlgItemText (hDlg, IDD_FNAME, szFileSpec) ;
                       }
                    else
                       MessageBeep (0) ;

                    return TRUE ;
                    }

                 lstrcat (lstrcat (szFileName, "\\"), szFileSpec) ;

                 if (DlgDirList (hDlg, szFileName, IDD_FLIST,
                                                   IDD_FPATH, wFileAttr))
                    {
                    lstrcpy (szFileSpec, szFileName) ;
                    SetDlgItemText (hDlg, IDD_FNAME, szFileSpec) ;
                    return TRUE ;
                    }

                 szFileName [nEditLen] = '\0' ;
                 if (-1 == OpenFile (szFileName, pof, OF_READ | OF_EXIST))
                    {
                    lstrcat (szFileName, szDefExt) ;
                    if (-1 == OpenFile (szFileName, pof, OF_READ |
OF_EXIST))
                       {
                       MessageBeep (0) ;
                       return TRUE ;
                       }
                    }
                 lstrcpy (szFileName,
                          AnsiNext (lstrrchr (pof->szPathName, '\\'))) ;

                 OemToAnsi (szFileName, szFileName) ;
                 EndDialog (hDlg, TRUE) ;
                 return TRUE ;

              case IDCANCEL :
                 EndDialog (hDlg, FALSE) ;
                 return TRUE ;
              }
        }
     return FALSE ;
     }

BOOL FAR PASCAL FileSaveDlgProc (HWND hDlg, WORD message,
                                 WORD wParam, LONG lParam)
     {
     switch (message)
        {
        case WM_INITDIALOG :
           SendDlgItemMessage (hDlg, IDD_FNAME, EM_LIMITTEXT, 80, 0L) ;
           DlgDirList (hDlg, szFileSpec, 0, IDD_FPATH, 0) ;
           SetDlgItemText (hDlg, IDD_FNAME, szFileSpec) ;
           return TRUE ;

        case WM_COMMAND :
           switch (wParam)
              {
              case IDD_FNAME :
                 if (HIWORD (lParam) == EN_CHANGE)
                    EnableWindow (GetDlgItem (hDlg, IDOK),
                       (BOOL) SendMessage (LOWORD (lParam),
                                             WM_GETTEXTLENGTH, 0, 0L)) ;
                 return TRUE ;
              case IDOK :
                 GetDlgItemText (hDlg, IDD_FNAME, szFileName, 80) ;
                 if (-1 == OpenFile (szFileName, pof, OF_PARSE))
                    {
                    MessageBeep (0) ;
                    return TRUE ;
                    }

                 if (!lstrchr (AnsiNext (lstrrchr (pof->szPathName, '\\')),
                               '.'))
                    lstrcat (szFileName, szDefExt) ;

                 if (-1 != OpenFile (szFileName, pof, OF_WRITE | OF_EXIST))
                    wStatus = 1 ;

                 else if (-1 != OpenFile (szFileName, pof,
                                                  OF_CREATE | OF_EXIST))
                    wStatus = 0 ;

                 else
                    {
                    MessageBeep (0) ;
                    return TRUE ;
                    }

                 lstrcpy (szFileName,
                          AnsiNext (lstrrchr (pof->szPathName, '\\'))) ;

                 OemToAnsi (szFileName, szFileName) ;
                 EndDialog (hDlg, TRUE) ;
                 return TRUE ;

              case IDCANCEL :
                 EndDialog (hDlg, FALSE) ;
                 return TRUE ;
              }
        }
     return FALSE ;
     }

LPSTR lstrchr (LPSTR str, char ch)
     {
     while (*str)
          {
          if (ch == *str)
               return str ;

          str = AnsiNext (str) ;
          }
     return NULL ;
     }
LPSTR lstrrchr (LPSTR str, char ch)
     {
     LPSTR strl = str + lstrlen (str) ;

     do
          {
          if (ch == *strl)
               return strl ;

          strl = AnsiPrev (str, strl) ;
          }
     while (strl > str) ;

     return NULL ;
     }

 FILEDLG.H

/*-----------------------
   FILEDLG.H header file
  -----------------------*/

#define IDD_FNAME   0x10
#define IDD_FPATH   0x11
#define IDD_FLIST   0x12

 FILEDLG.DLG

/*--------------------------------
   FILEDLG.DLG dialog definitions
  --------------------------------*/

FileOpen DIALOG 10, 10, 148, 116
     STYLE WS_POPUP | WS_DLGFRAME
     {
     LTEXT   "Open File &Name:", -1,        2,  4,  76, 10
     EDITTEXT                 IDD_FNAME,    2, 18, 100, 12, ES_AUTOHSCROLL
     LTEXT          "&Files in", -1,        2, 40,  38, 10
     LTEXT          "",       IDD_FPATH,   44, 40,  98, 12
     LISTBOX                  IDD_FLIST,    2, 54,  70, 58, WS_TABSTOP |
WS_VSCROLL
     DEFPUSHBUTTON  "&Open",  IDOK,        88, 62,  50, 14, WS_GROUP
     PUSHBUTTON     "Cancel", IDCANCEL,    88, 86,  50, 14, WS_GROUP
     }
FileSave DIALOG 10, 10, 180, 54
     STYLE WS_POPUP | WS_DLGFRAME
     {
     LTEXT "Save File &Name As:", -1,       6,  4,  84, 12
     LTEXT          "",       IDD_FPATH,   90,  4,  78, 12
     EDITTEXT                 IDD_FNAME,    6, 20, 104, 12, ES_AUTOHSCROLL
     DEFPUSHBUTTON  "OK",     IDOK,       124, 20,  50, 14, WS_GROUP
     PUSHBUTTON     "Cancel", IDCANCEL,   124, 36,  50, 14, WS_GROUP
     }

FILEDLG.DLG contains two dialog box templates named "FileOpen" and
"FileSave." When displayed, these look very much like the dialog boxes used
in the programs that come with Windows, so the operation of the two dialog
boxes will be familiar to Windows users. FileOpen contains Open and Cancel
push buttons, and FileSave contains OK and Cancel push buttons. The static
text field with an ID of IDD_FPATH is used to display the current disk drive
and directory path. The edit field with the ID of IDD_FNAME allows a user to
type in a filename. The FileOpen dialog box also contains a list box that
displays all the files matching a particular file specification (we'll use
"*.TXT" with POPPAD3), all valid disk drive letters, and all child
subdirectories of the current drive and directory.

The FILEDLG.C file contains two functions named DoFileOpenDlg and
DoFileSaveDlg that a program can call to invoke the dialog boxes. These
functions are responsible for copying input parameters to variables within
FILEDLG.C, calling DialogBox, and returning information obtained from the
dialog box procedure to the program that called the FILEDLG.C functions. The
parameters to the DoFileOpenDlg and DoFileSaveDlg functions include a
default file specification (szFileSpecIn), a default filename extension
(szDefExtIn), and a pointer to a structure of type OFSTRUCT (pofIn). The
DoFileOpenDlgProc also requires a file attribute (wFileAttrIn) to be used
when listing files in the list box.

The DoFileOpenDlg and DoFileSaveDlg functions return a 1 if the user ends
the dialog box with Open or OK, and a 0 if the user ends with Cancel. If the
user ends with Open or OK, the OFSTRUCT structure passed to the functions
will contain the fully qualified filename of the selected file in the
szPathName field. The filename only (without a drive or directory) is copied
to the szFileNameOut character array. For DoFileSaveDlg, the pwStatusOut
parameter points to a word that is set to 1 if the file does exist and to 0
if it does not.

The actual dialog box procedures are FileOpenDlgProc and FileSaveDlgProc.
These are relatively complex because they must deal with the interaction
between the list box and the edit control and with the checking that must be
performed on filenames and subdirectories entered by the user. To help with
these areas, the dialog box procedures extensively use the Windows functions
DlgDirList and DlgDirSelect.


The DlgDirList and DlgDirSelect Functions

You'll recall from working with list boxes in Chapter 6 that you can fill a
list box with a list of files, subdirectories, and disk drives by sending
the list box a message:

SendMessage (hwndList, LB_DIR, wAttr, (LONG) lpszFileSpec) ;

The wAttr parameter is an MS-DOS file attribute (with some additional bits
to indicate the listing of subdirectories and disk drives), and lpszFileSpec
is a far pointer to a filename specification such as "*.*".

Although this list box message is quite convenient, within a dialog box you
can use an even more sophisticated function:

iStatus = DlgDirList (hDlg, lpszFileSpec, nIDList, nIDStatic, wAttr) ;

The nIDList parameter is the ID of the list box, and nIDStatic is the ID of
a static text field. DlgDirList parses the string pointed to by lpszFileSpec
to extract any disk drive or subdirectory information from it. It then
changes the current disk drive or subdirectory using  MS-DOS function calls.

DlgDirList sends an LB_DIR message to the list box indicated by nIDList to
fill it with filenames meeting the file specification and the file attribute
word. The current disk drive and subdirectory path are then displayed in the
static text field whose ID is nIDStatic. When DlgDirList returns, the string
pointed to by lpszFileSpec contains only the file specification without any
drive or subdirectory. The function returns nonzero if it is successful and
0 otherwise. A 0 value usually indicates that the string pointed to by
lpszFileSpec does not contain a valid drive or subdirectory.

For example, suppose that drive C has a directory named WINDOWS and that
your dialog box procedure contains this code:

char szFileSpec [] = "C:\\WINDOWS\\*.TXT" ;
[other program lines]
DlgDirList (hDlg, szFileSpec, IDD_FLIST, IDD_FPATH, 0x4010) ;

When DlgDirList returns, the current disk drive and subdirectory for the
instance of the program will have been changed to C:\WINDOWS. The list box
whose ID is IDD_FLIST will list files with the extension .TXT, all
subdirectories of the directory WINDOWS, and all valid disk drives. The
static text field whose ID is IDD_FPATH will display the text C:\WINDOWS.
The szFileSpec string array will contain "*.TXT". If either the nIDList or
nIDStatic parameter is 0, however, DlgDirList will assume that the list box
or static text field does not exist.

Both FileOpenDlgProc and FileSaveDlgProc use DlgDirList while processing the
WM_INITDIALOG message. (In FileSaveDlgProc, DlgDirList is called with the
nIDList parameter set to 0 because the dialog box does not contain a list
box.)

SetDlgItemText is used to set the text of the edit control to the text file
specification:

SetDlgItemText (hDlg, IDD_FNAME, szFileSpec) ;

This function does the same thing as the SetWindowText function or the
WM_SETTEXT message. You can use the companion function GetDlgItemText to
obtain the contents of the edit control.

FileOpenDlgProc also uses DlgDirSelect extensively. This function returns
the currently selected string from a list box:

bDirectory = DlgDirSelect (hDlg, lpszString, nIDList) ;

The nIDList parameter is the ID of the list box. The currently selected
string is copied to the character array pointed to by lpszString. If the
return value is TRUE (nonzero), meaning that the string is either a disk
drive or subdirectory name, DlgDirSelect removes the hyphens and brackets
that appear when disk-drive letters and the subdirectory names are displayed
in a list box. The function appends a colon to disk-drive letters and a
backslash to subdirectory names when copying them to the lpszString array.

If DlgDirSelect returns a disk drive or directory, you can then append the
default file specification ("*.TXT", for instance) to the string and use
that as the lpszFileSpec parameter to DlgDirList. DlgDirList will then
change the drive or directory and update the static text field. The file
specification pointed to by lpszFileSpec on return from DlgDirList can then
be transferred to the edit control. If DlgDirSelect returns FALSE (0),
meaning that lpszString contains a filename from the list box, then this
filename can be transferred to the edit field directly.


Getting Valid Filenames

The dialog box to open a file would be much simpler if it did not contain an
edit control. The edit control forces the dialog box procedure into doing
some filename parsing. Much of this logic occurs when FileOpenDlgProc
processes the WM_COMMAND message with wParam equal to IDOK. (When the user
double-clicks a list box filename, the dialog procedure transfers the name
to the edit box and then generates a WM_COMMAND message with wParam equal to
IDOK. This avoids repeating the parsing logic for a list box double-click
message.)

The dialog box procedure uses GetDlgItemText to obtain the string in the
edit control. The parsing logic begins with a check to determine if the last
character in this string is a backslash or colon. If it is, the user is
requesting that the drive or directory be changed, so the current file
specification must be appended to the string the user entered. If the
resultant filename string contains a global character (* or ?), the dialog
box procedure calls DlgDirList with the new specification, and the dialog
box procedure exits to wait for the next message.

If the character string entered by the user neither terminates with a
backslash or colon nor contains a global character, it could be either a
directory name or a filename. FileOpenDlgProc appends a backslash and the
current file specification to it. If DlgDirList doesn't report an error,
processing of the message is over. Otherwise, the entered text string is
probably a filename, in which case FileOpenDlgProc strips off the previously
appended file specification and calls OpenFile. If OpenFile does not find
the file, then the default extension is added, and OpenFile tries again. If
either one of these OpenFile calls is successful in opening the file for
reading, then the szPathName field of the OFSTRUCT structure is used to
obtain the filename without any drive or subdirectory, and the dialog box is
terminated. Otherwise, the dialog procedure beeps to indicate an error in
the filename.

The FILEDLG.C file contains alternate strchr and strrchr functions that
search for characters when parsing filename strings. These alternate
functions use AnsiNext and AnsiPrev to allow multibyte characters in the
filename strings.


The New Version of POPPAD

The new version of POPPAD that uses these two dialog boxes (called POPPAD3)
is shown in Figure 10-8.

 POPPAD3.MAK

#-----------------------
# POPPAD3.MAK make file
#-----------------------

poppad3.exe : poppad.obj  poppadf.obj poppadp0.obj \
              filedlg.obj poppad3.def poppad.res
     link poppad poppadf poppadp0 filedlg, poppad3.exe /align:16, \
          NUL, /nod slibcew libw, poppad3
     rc poppad.res  poppad3.exe

poppad.obj : poppad.c poppad.h
     cl -c -Gsw -Ow -W2 -Zp poppad.c

poppadf.obj : poppadf.c
     cl -c -Gsw -Ow -W2 -Zp poppadf.c

poppadp0.obj : poppadp0.c
     cl -c -Gsw -Ow -W2 -Zp poppadp0.c

filedlg.obj : filedlg.c filedlg.h
     cl -c -Gsw -Ow -W2 -Zp filedlg.c

poppad.res : poppad.rc poppad.h poppad.ico filedlg.dlg filedlg.h
     rc -r poppad.rc


 POPPAD.C

/*---------------------------------------
   POPPAD.C -- Popup Editor
               (c) Charles Petzold, 1990
  ---------------------------------------*/

#include <windows.h>
#include "poppad.h"
#define  EDITID 1

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

BOOL ReadFile  (HANDLE, HWND, HWND, POFSTRUCT, char *, BOOL) ;
BOOL WriteFile (HANDLE, HWND, HWND, POFSTRUCT, char *, BOOL) ;
BOOL PrintFile (HANDLE, HWND, HWND, char *) ;

LPSTR lstrrchr (LPSTR, char) ;

char szAppName  [] = "PopPad" ;
char szFileSpec [] = "*.TXT"  ;
char szUntitled [] = "(untitled)" ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     MSG      msg ;
     HWND     hwnd ;
     HANDLE   hAccel ;
     WNDCLASS wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (hInstance, szAppName) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = szAppName ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, NULL,
                          WS_OVERLAPPEDWINDOW,
                          GetSystemMetrics (SM_CXSCREEN) / 4,
                          GetSystemMetrics (SM_CYSCREEN) / 4,
                          GetSystemMetrics (SM_CXSCREEN) / 2,
                          GetSystemMetrics (SM_CYSCREEN) / 2,
                          NULL, NULL, hInstance, lpszCmdLine) ;

     ShowWindow (hwnd, nCmdShow) ;

     UpdateWindow (hwnd) ;

     hAccel = LoadAccelerators (hInstance, szAppName) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          if (!TranslateAccelerator (hwnd, hAccel, &msg))
               {
               TranslateMessage (&msg) ;
               DispatchMessage (&msg) ;
               }
          }
     return msg.wParam ;
     }

BOOL FAR PASCAL AboutDlgProc (HWND hDlg, WORD message, WORD wParam, LONG
lParam)
     {
     switch (message)
          {
          case WM_INITDIALOG :
               return TRUE ;

          case WM_COMMAND :
               switch (wParam)
                    {
                    case IDOK :
                         EndDialog (hDlg, 0) ;
                         return TRUE ;
                    }
               break ;
          }
     return FALSE ;
     }

void DoCaption (HWND hwnd, char *szFileName)
     {
     char szCaption [40] ;

     wsprintf (szCaption, "%s - %s", (LPSTR) szAppName,
               (LPSTR) (szFileName [0] ? szFileName : szUntitled)) ;

     SetWindowText (hwnd, szCaption) ;
     }
short AskAboutSave (HWND hwnd, char *szFileName)
     {
     char  szBuffer [40] ;
     short nReturn ;

     wsprintf (szBuffer, "Save current changes: %s",
               (LPSTR) (szFileName [0] ? szFileName : szUntitled)) ;

     if (IDYES == (nReturn = MessageBox (hwnd, szBuffer, szAppName,
                                    MB_YESNOCANCEL | MB_ICONQUESTION)))

          if (!SendMessage (hwnd, WM_COMMAND, IDM_SAVE, 0L))
               return IDCANCEL ;

     return nReturn ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static BOOL    bNeedSave = FALSE ;
     static char    szRealFileName [16] ;
     static FARPROC lpfnAboutDlgProc ;
     static HANDLE  hInst ;
     static HWND    hwndEdit ;
     char           szFileName [16] ;
     LONG           lSelect ;
     OFSTRUCT       of ;
     WORD           wEnable ;

     switch (message)
          {
          case WM_CREATE :
               hInst = ((LPCREATESTRUCT) lParam)->hInstance ;
               lpfnAboutDlgProc = MakeProcInstance (AboutDlgProc, hInst) ;

               hwndEdit = CreateWindow ("edit", NULL,
                         WS_CHILD | WS_VISIBLE | WS_HSCROLL | WS_VSCROLL |
                              WS_BORDER | ES_LEFT | ES_MULTILINE |
                              ES_AUTOHSCROLL | ES_AUTOVSCROLL,
                         0, 0, 0, 0,
                         hwnd, EDITID, hInst, NULL) ;

               SendMessage (hwndEdit, EM_LIMITTEXT, 32000, 0L) ;

               if (lstrlen (((LPCREATESTRUCT) lParam)->lpCreateParams))
                    {
                    OpenFile (((LPCREATESTRUCT) lParam)->lpCreateParams,
                                        &of, OF_PARSE) ;
                    lstrcpy (szFileName,
                              AnsiNext (lstrrchr (of.szPathName, '\\'))) ;
                    if (ReadFile (hInst, hwnd, hwndEdit, &of,
                              szFileName, FALSE))
                         lstrcpy (szRealFileName, szFileName) ;
                    }
               DoCaption (hwnd, szRealFileName) ;
               return 0 ;

          case WM_SETFOCUS :
               SetFocus (hwndEdit) ;
               return 0 ;

          case WM_SIZE :
               MoveWindow (hwndEdit, 0, 0, LOWORD (lParam),
                                           HIWORD (lParam), TRUE) ;
               return 0 ;

          case WM_INITMENUPOPUP :
               if (lParam == 1)
                    {
                    EnableMenuItem (wParam, IDM_UNDO,
                         SendMessage (hwndEdit, EM_CANUNDO, 0, 0L) ?
                              MF_ENABLED : MF_GRAYED) ;

                    EnableMenuItem (wParam, IDM_PASTE,
                         IsClipboardFormatAvailable (CF_TEXT) ?
                              MF_ENABLED : MF_GRAYED) ;

                    lSelect = SendMessage (hwndEdit, EM_GETSEL, 0, 0L) ;

                    if (HIWORD (lSelect) == LOWORD (lSelect))
                         wEnable = MF_GRAYED ;
                    else
                         wEnable = MF_ENABLED ;

                    EnableMenuItem (wParam, IDM_CUT,   wEnable) ;
                    EnableMenuItem (wParam, IDM_COPY,  wEnable) ;
                    EnableMenuItem (wParam, IDM_CLEAR, wEnable) ;
                    }
               return 0 ;

          case WM_COMMAND :
               if (LOWORD (lParam) && wParam == EDITID)
                    {
                    switch (HIWORD (lParam))
                         {
                         case EN_UPDATE :
                              bNeedSave = TRUE ;
                              return 0 ;
                         case EN_ERRSPACE :
                              MessageBox (hwnd, "Edit control out of
space.",
                                        szAppName, MB_OK | MB_ICONSTOP) ;
                              return 0 ;
                         }
                    break ;
                    }

               switch (wParam)
                    {
                    case IDM_NEW :
                         if (bNeedSave && IDCANCEL ==
                                   AskAboutSave (hwnd, szRealFileName))
                              return 0 ;

                         SetWindowText (hwndEdit, "\0") ;
                         szRealFileName [0] = '\0' ;
                         DoCaption (hwnd, szRealFileName) ;
                         bNeedSave = FALSE ;
                         return 0 ;

                    case IDM_OPEN :
                         if (bNeedSave && IDCANCEL ==
                                   AskAboutSave (hwnd, szRealFileName))
                              return 0 ;

                         if (ReadFile (hInst, hwnd, hwndEdit, &of,
                                        szFileName, TRUE))
                              {
                              lstrcpy (szRealFileName, szFileName) ;
                              DoCaption (hwnd, szRealFileName) ;
                              bNeedSave = FALSE ;
                              }

                         return 0 ;

                    case IDM_SAVE :
                         if (szRealFileName [0])
                              {
                              if (WriteFile (hInst, hwnd, hwndEdit, &of,
                                             szRealFileName, FALSE))
                                   {
                                   bNeedSave = FALSE ;
                                   return 1 ;
                                   }
                              return 0 ;
                              }
                                                  // fall through
                    case IDM_SAVEAS :
                         if (WriteFile (hInst, hwnd, hwndEdit, &of,
                                        szFileName, TRUE))
                              {
                              lstrcpy (szRealFileName, szFileName) ;
                              DoCaption (hwnd, szFileName) ;
                              bNeedSave = FALSE ;
                              return 1 ;
                              }
                         return 0 ;

                    case IDM_PRINT :
                         PrintFile (hInst, hwnd, hwndEdit,
                              szRealFileName [0] ? szRealFileName :
                                                   szUntitled) ;
                         return 0 ;

                    case IDM_EXIT :
                         SendMessage (hwnd, WM_CLOSE, 0, 0L) ;
                         return 0 ;

                    case IDM_ABOUT :
                         DialogBox (hInst, "AboutBox", hwnd,
                                   lpfnAboutDlgProc) ;
                         return 0 ;

                    case IDM_UNDO :
                         SendMessage (hwndEdit, WM_UNDO, 0, 0L) ;
                         return 0 ;

                    case IDM_CUT :
                         SendMessage (hwndEdit, WM_CUT, 0, 0L) ;
                         return 0 ;

                    case IDM_COPY :
                         SendMessage (hwndEdit, WM_COPY, 0, 0L) ;
                         return 0 ;

                    case IDM_PASTE :
                         SendMessage (hwndEdit, WM_PASTE, 0, 0L) ;
                         return 0 ;

                    case IDM_CLEAR :
                         SendMessage (hwndEdit, WM_CLEAR, 0, 0L) ;
                         return 0 ;

                    case IDM_SELALL :
                         SendMessage (hwndEdit, EM_SETSEL, 0,
                                        MAKELONG (0, 32767)) ;
                         return 0 ;
                    }
               break ;
          case WM_CLOSE :
               if (!bNeedSave || IDCANCEL !=
                         AskAboutSave (hwnd, szRealFileName))
                    DestroyWindow (hwnd) ;

               return 0 ;

          case WM_QUERYENDSESSION :
               if (!bNeedSave || IDCANCEL !=
                         AskAboutSave (hwnd, szRealFileName))
                    return 1L ;

               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 POPPADF.C

/*-----------------------------------
   POPPADF -- Popup Notepad File I/O
  -----------------------------------*/

#include <windows.h>
                                        // in FILEDLG.C

int DoFileOpenDlg (HANDLE, WORD, char *, char *, WORD,   char *, POFSTRUCT)
;
int DoFileSaveDlg (HANDLE, WORD, char *, char *, WORD *, char *, POFSTRUCT)
;

extern char szAppName  [] ;             // in POPPAD.C
extern char szFileSpec [] ;

long FileLength (HANDLE hFile)
     {
     long   lCurrentPos = _llseek (hFile, 0L, 1) ;
     long   lFileLength = _llseek (hFile, 0L, 2) ;

     _llseek (hFile, lCurrentPos, 0) ;

     return lFileLength ;
     }
void OkMessageBox (HWND hwnd, char *szString, char *szFileName)
     {
     char szBuffer [40] ;

     wsprintf (szBuffer, szString, (LPSTR) szFileName) ;

     MessageBox (hwnd, szBuffer, szAppName, MB_OK | MB_ICONEXCLAMATION) ;
     }

BOOL ReadFile (HANDLE hInstance, HWND hwnd, HWND hwndEdit, POFSTRUCT pof,
               char *szFileName, BOOL bAskName)
     {
     DWORD  dwLength ;
     HANDLE hFile, hTextBuffer ;
     LPSTR  lpTextBuffer ;

     if (bAskName)
          {
          if (!DoFileOpenDlg (hInstance, hwnd, szFileSpec, szFileSpec + 1,
                                        0x4010, szFileName, pof))
               return FALSE ;
          }

     if (-1 == (hFile = OpenFile (szFileName, pof, OF_READ | OF_REOPEN)))
          {
          OkMessageBox (hwnd, "Cannot open file %s", szFileName) ;
          return FALSE ;
          }

     if ((dwLength = FileLength (hFile)) >= 32000)
          {
          _lclose (hFile) ;
          OkMessageBox (hwnd, "File %s too large", szFileName) ;
          return FALSE ;
          }

     if (NULL == (hTextBuffer = GlobalAlloc (GHND, (DWORD) dwLength + 1)))
          {
          _lclose (hFile) ;
          OkMessageBox (hwnd, "Cannot allocate memory for %s", szFileName) ;
          return FALSE ;
          }

     lpTextBuffer = GlobalLock (hTextBuffer) ;
     _lread (hFile, lpTextBuffer, (WORD) dwLength) ;
     _lclose (hFile) ;
     lpTextBuffer [(WORD) dwLength] = '\0' ;

     SetWindowText (hwndEdit, lpTextBuffer) ;
     GlobalUnlock (hTextBuffer) ;
     GlobalFree (hTextBuffer) ;
     return TRUE ;
     }

BOOL WriteFile (HANDLE hInstance, HWND hwnd, HWND hwndEdit, POFSTRUCT pof,
                char *szFileName, BOOL bAskName)
     {
     char      szBuffer [40] ;
     HANDLE    hFile, hTextBuffer ;
     NPSTR     npTextBuffer ;
     WORD      wStatus, wLength ;

     if (bAskName)
          {
          if (!DoFileSaveDlg (hInstance, hwnd, szFileSpec, szFileSpec + 1,
                                   &wStatus, szFileName, pof))
               return FALSE ;

          if (wStatus == 1)
               {
               wsprintf (szBuffer, "Replace existing %s", (LPSTR)
szFileName) ;
               if (IDNO == MessageBox (hwnd, szBuffer, szAppName,
                                             MB_YESNO | MB_ICONQUESTION))
                    return FALSE ;
               }
          }
     else
          OpenFile (szFileName, pof, OF_PARSE) ;

     if (-1 == (hFile = OpenFile (szFileName, pof, OF_CREATE | OF_REOPEN)))
          {
          OkMessageBox (hwnd, "Cannot create file %s", szFileName) ;
          return FALSE ;
          }

     wLength = GetWindowTextLength (hwndEdit) ;
     hTextBuffer = (HANDLE) SendMessage (hwndEdit, EM_GETHANDLE, 0, 0L) ;
     npTextBuffer = LocalLock (hTextBuffer) ;

     if (wLength != _lwrite (hFile, npTextBuffer, wLength))
          {
          _lclose (hFile) ;
          OkMessageBox (hwnd, "Cannot write file %s to disk", szFileName) ;
          return FALSE ;
          }

     _lclose (hFile) ;
     LocalUnlock (hTextBuffer) ;

     return TRUE ;
     }

 POPPADP0.C

/*---------------------------------------------------------
   POPPADP0.C -- Popup Notepad Printing -- dummy functions
  ---------------------------------------------------------*/

#include <windows.h>

extern char szAppName [] ;              // in POPPAD.C

BOOL FAR PASCAL PrintDlgProc (HWND hDlg, WORD message,
                              WORD wParam, LONG lParam)
     {
     return FALSE ;
     }

BOOL FAR PASCAL AbortProc (HDC hPrinterDC, short nCode)
     {
     return FALSE ;
     }

BOOL PrintFile (HANDLE hInstance, HWND hwnd, HWND hwndEdit, char
*szFileName)
     {
     MessageBox (hwnd, "Printing not yet implemented", szAppName,
                       MB_OK | MB_ICONEXCLAMATION) ;
     return FALSE ;
     }

 POPPAD.RC

/*---------------------------
   POPPAD.RC resource script
  ---------------------------*/

#include <windows.h>
#include "poppad.h"
#include "filedlg.h"

PopPad ICON "poppad.ico"

PopPad MENU
     {
     POPUP "&File"
          {
          MENUITEM "&New",              IDM_NEW
          MENUITEM "&Open...",          IDM_OPEN
          MENUITEM "&Save",             IDM_SAVE
          MENUITEM "Save &As...",       IDM_SAVEAS
          MENUITEM SEPARATOR
          MENUITEM "&Print...",         IDM_PRINT
          MENUITEM SEPARATOR
          MENUITEM "E&xit",             IDM_EXIT
          }
     POPUP "&Edit"
          {
          MENUITEM "&Undo\tAlt+BkSp",   IDM_UNDO
          MENUITEM SEPARATOR
          MENUITEM "Cu&t\tShift+Del",   IDM_CUT
          MENUITEM "&Copy\tCtrl+Ins",   IDM_COPY
          MENUITEM "&Paste\tShift+Ins", IDM_PASTE
          MENUITEM "C&lear\tDel",       IDM_CLEAR
          MENUITEM SEPARATOR
          MENUITEM "&Select All",       IDM_SELALL
          }
     POPUP "&Help"
          {
          MENUITEM "&About PopPad...",  IDM_ABOUT
          }
     }

PopPad ACCELERATORS
     {
     VK_DELETE, IDM_CUT,   VIRTKEY, SHIFT
     VK_INSERT, IDM_COPY,  VIRTKEY, CONTROL
     VK_INSERT, IDM_PASTE, VIRTKEY, SHIFT
     VK_DELETE, IDM_CLEAR, VIRTKEY
     }

AboutBox DIALOG  20, 20, 160, 80
     STYLE WS_POPUP | WS_DLGFRAME
     {
     CTEXT "PopPad"                              -1,   0, 12, 160,  8
     ICON  "PopPad"                              -1,   8,  8,   0,  0
     CTEXT "Popup Editor for Microsoft Windows"  -1,   0, 36, 160,  8
     CTEXT "Copyright (c) Charles Petzold, 1990" -1,   0, 48, 160,  8
     DEFPUSHBUTTON "OK"                        IDOK,  64, 60,  32, 14,
WS_GROUP
     }

PrintDlgBox DIALOG 20, 20, 100, 76
     STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_VISIBLE
     CAPTION "PopPad"
     {
     CTEXT "Sending",                  -1,  0, 10, 100,  8
     CTEXT "",                  IDD_FNAME,  0, 20, 100,  8
     CTEXT "to print spooler.",        -1,  0, 30, 100,  8
     DEFPUSHBUTTON  "Cancel",    IDCANCEL, 34, 50,  32, 14, WS_GROUP
     }

rcinclude filedlg.dlg

 POPPAD.H

/*----------------------
   POPPAD.H header file
  ----------------------*/

#define IDM_NEW      1
#define IDM_OPEN     2
#define IDM_SAVE     3
#define IDM_SAVEAS   4

#define IDM_PRINT    5

#define IDM_EXIT     6
#define IDM_ABOUT    7

#define IDM_UNDO     8
#define IDM_CUT      9
#define IDM_COPY    10
#define IDM_PASTE   11
#define IDM_CLEAR   12
#define IDM_SELALL  13

 POPPAD.ICO  -- Please refer to the book.

FIG 1008.epb

 POPPAD3.DEF

;------------------------------------
; POPPAD3.DEF module definition file
;------------------------------------

NAME           POPPAD3

DESCRIPTION    'Popup Editor Version 3 (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc
               AboutDlgProc
               FileOpenDlgProc
               FileSaveDlgProc
               PrintDlgProc
               AbortProc

As you'll recall, the POPPAD series of programs uses a multiline edit
control to do all the editing. The POPPADF.C file serves as an intermediary
between the main POPPAD.C program and the functions in FILEDLG.C. The
ReadFile function in POPPADF.C calls DoFileOpenDlgProc and reads the file
into a global memory block. ReadFile is responsible for reporting if the
file is too large or if memory can't be allocated for the file. When the
file is read into a global memory block, it's transferred to the edit window
using SetWindowText.

WriteFile calls DoFileSaveDlgProc to obtain the name of the file. This
function is responsible for asking the user if it's acceptable to replace an
existing file by that name. WriteFile obtains the handle to the edit
control's buffer, locks it, and writes the file to the disk directly from
that buffer.

This doesn't leave the POPPAD.C module much to do in the way of file I/O.
But note these facts:

  ■   In WinMain, the lpszCmdLine address is used as the last field of the
      CreateWindow call. This string might contain a filename that was
      entered as a parameter to POPPAD3 when the program was executed.
      During processing of the WM_CREATE message in WndProc, this filename
      is passed to ReadFile with the bAskName parameter set to FALSE so that
      ReadFile won't call DoFileOpenDlgProc.

  ■   Much of the new logic in WndProc involves keeping track of changes to
      the text in the edit control. Whenever this text changes, the control
      sends an EN_UPDATE notification message to WndProc, which then sets
      bNeedSave to TRUE. When the user wants to open a new file or end the
      program, WndProc must check the bNeedSave variable. If it is TRUE,
      then the program calls AskAboutSave, which displays a message box that
      asks whether the user wants to save the current changes. When a file
      is saved, bNeedSave is set to FALSE.

  ■   POPPAD3's caption displays the name of the currently loaded file. If
      no filename is available (for instance, when the program is first
      executed), then the DoCaption function in POPPAD3 causes the character
      string "(untitled)" to be displayed.

      We are not yet finished with the POPPAD programs. In Chapter 15 some
      of these files (POPPAD.C, POPPADF.C, POPPAD.RC, POPPAD.H, and
      POPPAD.ICO) will be used in the final POPPAD program.  At that time a
      new file (POPPADP.C) will be substituted for POPPADP0.C to add logic
      to print files.



MODELESS DIALOG BOXES

At the beginning of this chapter, I explained that dialog boxes can be
either "modal" or "modeless." So far we've been looking at modal dialog
boxes, the more common of the two types. Modal dialog boxes (except for
system modal dialog boxes) allow the user to switch between the dialog box
and other programs. However, the user cannot switch to another window in the
program until the modal dialog box is destroyed. Modeless dialog boxes allow
the user to switch between the dialog box and the window that created it as
well as between the dialog box and other programs. The modeless dialog box
is thus more akin to the regular popup windows that your program might
create.

Modeless dialog boxes are preferred when the user would find it convenient
to keep the dialog box displayed for a while. For instance, the Windows
WRITE program uses modeless dialog boxes for the Find and Change dialogs.
If the Find dialog box were modal, the user would have to choose Find from
the menu, enter the string to be found, end the dialog box to return to the
document, and then repeat the entire process to search for another
occurrence of the same string. Allowing the user to switch between the
document and the dialog box is much more convenient.

As you've seen, modal dialog boxes are created using DialogBox. The function
returns only after the dialog box is destroyed. It returns the value
specified in the second parameter of the EndDialog call that was used within
the dialog box procedure to terminate the dialog box. Modeless dialog boxes
are created using CreateDialog. This function takes the same parameters as
DialogBox:

hDlgModeless = CreateDialog (hInstance, lpszTemplate, hwndParent,
                                   lpfnDialogProc) ;

The difference is that the CreateDialog function returns immediately with
the window handle of the dialog box. Normally, you store this window handle
in a global variable.

Although the use of the names DialogBox with modal dialog boxes and
CreateDialog with modeless dialog boxes may seem arbitrary, you can remember
which is which by keeping in mind that modeless dialog boxes are similar to
normal windows. CreateDialog should remind you of the CreateWindow function,
which creates normal windows.

Differences Between Modal and Modeless Dialog Boxes

Working with modeless dialog boxes is similar to working with modal dialog
boxes, but there are several important differences:

  ■   Modeless dialog boxes usually include a caption bar and a system menu
      box. The STYLE statement in the dialog box template for a modeless
      dialog box will look something like this:

           STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_VISIBLE

      The caption bar and system menu allow the user to move the modeless
      dialog box to another area of the display using either the mouse or
      the keyboard. You don't normally provide a caption bar and system menu
      with a modal dialog box, because the user can't do anything in the
      underlying window anyway.

  ■   Note that the WS_VISIBLE style is included in our sample STYLE
      statement. If you omit WS_VISIBLE, you must call ShowWindow after the
      CreateDialog call:

           hDlgModeless = CreateDialog (  . . .  ) ;
           ShowWindow (hDlgModeless, SW_SHOW) ;

      If you neither include WS_VISIBLE nor call ShowWindow, the modeless
      dialog box will not be displayed. In overlooking this fact,
      programmers who have mastered modal dialog boxes often experience
      difficulties when they first try to create a modeless dialog box.

  ■   Unlike messages to modal dialog boxes and message boxes, messages to
      modeless dialog boxes come through your program's message queue. The
      message queue must be altered to pass these messages to the dialog box
      window procedure. Here's how you do it: When you use CreateDialog to
      create a modeless dialog box, you should save the dialog box handle
      returned from the call in a global variable (for instance,
      hDlgModeless). Change your message loop to look like this:

           while (GetMessage (&msg, NULL, 0, 0))
                {
                if (hDlgModeless == 0 || !IsDialogMessage (hDlgModeless,
                &msg))
                     {
                     TranslateMessage (&msg) ;
                     DispatchMessage  (&msg) ;
                     }
                }

      If the message is intended for the modeless dialog box, then
      IsDialogMessage sends it to the dialog box window procedure and
      returns TRUE (nonzero); otherwise, it returns FALSE (0). The
      TranslateMessage and DispatchMessage functions should be called only
      if hDlgModeless is 0 or if the message is not for the dialog box.

      If you use keyboard accelerators for your program's window, then the
      message loop looks like this:

           while (GetMessage (&msg, NULL, 0, 0))
                {
                if (hDlgModeless == 0 || !IsDialogMessage (hDlgModeless,
                &msg))
                     {
                     if (!TranslateAccelerator (hwnd, hAccel, &msg))
                          {
                          TranslateMessage (&msg) ;
                          DispatchMessage  (&msg) ;
                          }
                     }
                }

      Because global variables are initialized to 0, hDlgModeless will be 0
      until the dialog box is created, thus ensuring that IsDialogMessage is
      not called with an invalid window handle. You must take the same
      precaution when you destroy the modeless dialog box as explained
      below.

      The hDlgModeless variable can also be used by other parts of the
      program as a test of the existence of the modeless dialog box. Other
      windows in the program can send messages to the dialog box while
      hDlgModeless is not equal to 0.

  ■   Use DestroyWindow rather than EndDialog to end a modeless dialog box.
      When you call DestroyWindow, set the hDlgModeless global variable to
      0.

The user customarily terminates a modeless dialog box by choosing Close from
the system menu. Although the Close option is enabled, the dialog box window
procedure within Windows does not process the WM_CLOSE message. You must do
this yourself in the dialog box procedure:

case WM_CLOSE :
     DestroyWindow (hDlg) ;
     hDlgModeless = 0 ;
     break ;

Note the difference between these two window handles: The hDlg parameter to
DestroyWindow is the parameter passed to the dialog box procedure;
hDlgModeless is the global variable returned from CreateDialog that you test
within the message loop.

You can also allow a user to close a modeless dialog box using push buttons.
Use the same logic as for the WM_CLOSE message. Any information that the
dialog box must "return" to the window that created it can be stored in
global variables.


The New COLORS Program

The COLORS1 program described in Chapter 6 created nine child windows to
display three scroll bars and six text items. At that time, the program was
one of the more complex we had developed. Converting COLORS1 to use a
modeless dialog box makes the program_ and particularly its WndProc
function--almost ridiculously simple. The revised COLORS2 program is shown
in Figure 10-9.

 COLORS2.MAK

#-----------------------
# COLORS2.MAK make file
#-----------------------

colors2.exe : colors2.obj colors2.def colors2.res
     link colors2, /align:16, NUL, /nod slibcew libw, colors2
     rc colors2.res

colors2.obj : colors2.c
     cl -c -Gsw -Ow -W2 -Zp colors2.c

colors2.res : colors2.rc
     rc -r colors2.rc

 COLORS2.C

/*--------------------------------------------------------
   COLORS2.C -- Version using Modeless Dialog Box Version
                (c) Charles Petzold, 1990
  --------------------------------------------------------*/

#include <windows.h>

long FAR PASCAL WndProc     (HWND, WORD, WORD, LONG) ;
BOOL FAR PASCAL ColorScrDlg (HWND, WORD, WORD, LONG) ;

HWND hDlgModeless ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName[] = "Colors2" ;
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;

     if (hPrevInstance)
          return FALSE ;

     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;



     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = NULL ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = CreateSolidBrush (0L) ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = szAppName ;

     RegisterClass (&wndclass) ;

     hwnd = CreateWindow (szAppName, "Color Scroll",
                          WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     hDlgModeless = CreateDialog (hInstance, "ColorScrDlg", hwnd,
                         MakeProcInstance (ColorScrDlg, hInstance)) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          if (hDlgModeless == 0 || !IsDialogMessage (hDlgModeless, &msg))
               {
               TranslateMessage (&msg) ;
               DispatchMessage  (&msg) ;
               }
          }
     return msg.wParam ;
     }

BOOL FAR PASCAL ColorScrDlg (HWND hDlg, WORD message, WORD wParam, LONG
lParam)
     {
     static short color [3] ;
     HWND         hwndParent, hCtrl ;
     short        nCtrlID, nIndex ;

     switch (message)
          {
          case WM_INITDIALOG :
               for (nCtrlID = 10 ; nCtrlID < 13 ; nCtrlID++)
                    {
                    hCtrl = GetDlgItem (hDlg, nCtrlID) ;
                    SetScrollRange (hCtrl, SB_CTL, 0, 255, FALSE) ;
                    SetScrollPos   (hCtrl, SB_CTL, 0, FALSE) ;
                    }
               return TRUE ;
          case WM_VSCROLL :
               hCtrl   = HIWORD (lParam) ;
               nCtrlID = GetWindowWord (hCtrl, GWW_ID) ;
               nIndex  = nCtrlID - 10 ;
               hwndParent = GetParent (hDlg) ;

               switch (wParam)
                    {
                    case SB_PAGEDOWN :
                         color [nIndex] += 15 ;        // fall through
                    case SB_LINEDOWN :
                         color [nIndex] = min (255, color [nIndex] + 1) ;

                         break ;
                    case SB_PAGEUP :
                         color [nIndex] -= 15 ;        // fall through
                    case SB_LINEUP :
                         color [nIndex] = max (0, color [nIndex] - 1) ;
                         break ;
                    case SB_TOP :
                         color [nIndex] = 0 ;
                         break ;
                    case SB_BOTTOM :
                         color [nIndex] = 255 ;
                         break ;
                    case SB_THUMBPOSITION :
                    case SB_THUMBTRACK :
                         color [nIndex] = LOWORD (lParam) ;
                         break ;
                    default :
                         return FALSE ;
                    }
               SetScrollPos  (hCtrl, SB_CTL,      color [nIndex], TRUE) ;
               SetDlgItemInt (hDlg,  nCtrlID + 3, color [nIndex], FALSE) ;

               DeleteObject (GetClassWord (hwndParent, GCW_HBRBACKGROUND)) ;
               SetClassWord (hwndParent, GCW_HBRBACKGROUND,
                    CreateSolidBrush (RGB (color [0], color [1], color
[2]))) ;

               InvalidateRect (hwndParent, NULL, TRUE) ;
               return TRUE ;
          }
     return FALSE ;
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     switch (message)
          {
          case WM_DESTROY :
               DeleteObject (GetClassWord (hwnd, GCW_HBRBACKGROUND)) ;
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 COLORS2.RC

/*----------------------------
   COLORS2.RC resource script
  ----------------------------*/

#include <windows.h>

#define SBS_VERT_TAB (SBS_VERT | WS_TABSTOP)

ColorScrDlg DIALOG  8, 16, 124, 132
     STYLE WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_VISIBLE
     CAPTION "Color Scroll Scrollbars"
     {
     CONTROL "&Red",   -1, "static",    SS_CENTER,    10,   4, 24,   8
     CONTROL "",       10, "scrollbar", SBS_VERT_TAB, 10,  16, 24, 100
     CONTROL "0",      13, "static",    SS_CENTER,    10, 120, 24,   8
     CONTROL "&Green", -1, "static",    SS_CENTER,    50,   4, 24,   8
     CONTROL "",       11, "scrollbar", SBS_VERT_TAB, 50,  16, 24, 100
     CONTROL "0",      14, "static",    SS_CENTER,    50, 120, 24,   8
     CONTROL "&Blue",  -1, "static",    SS_CENTER,    90,   4, 24,   8
     CONTROL "",       12, "scrollbar", SBS_VERT_TAB, 90,  16, 24, 100
     CONTROL "0",      15, "static",    SS_CENTER,    90, 120, 24,   8
     }

 COLORS2.DEF

;------------------------------------
; COLORS2.DEF module definition file
;------------------------------------

NAME           COLORS2

DESCRIPTION    'Color Scroll with Dialog Box (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc
               ColorScrDlg

Although the original COLORS1 program displayed scroll bars that were based
on the size of the window, the new version keeps them at a constant size
within the modeless dialog box, as shown in Figure 10-10.

  (Figure 10-10. may be found in the printed book.)

The dialog box template in COLORS2.RC uses CONTROL statements for all nine
child windows in the dialog box. The modeless dialog box is created in
COLORS2's WinMain function following the ShowWindow call for the program's
main window. Note that the window style for the main window includes
WS_CLIPCHILDREN, which allows the program to repaint the main window without
erasing the dialog box.

The dialog box window handle returned from CreateDialog is stored in the
global variable hDlgModeless and tested during the message loop, as
described above. In this program, however, it isn't  necessary to store the
handle in a global variable or to test the value before calling
IsDialogMessage. The message loop could have been written like this:

while (GetMessage (&msg, NULL, 0, 0))
     {
     if (!IsDialogMessage (hDlgModeless, &msg))
          {
          TranslateMessage (&msg) ;
          DispatchMessage  (&msg) ;
          }
     }

Because the dialog box is created before the program enters the message loop
and the dialog box is not destroyed until the program terminates, the value
of hDlgModeless will always be valid. I included the logic in case you want
to add some code to the dialog box window procedure to destroy the dialog
box:

case WM_CLOSE :
     DestroyWindow (hDlg) ;
     hDlgModeless = 0 ;
     break ;

In the original COLORS1 program, SetWindowText set the values of the three
numeric labels after converting the integers to text with itoa. The code
looked like this:

SetWindowText (hwndValue[n], itoa (color[n], szBuffer, 10)) ;

The value of n was the ID number of the current scroll bar being processed,
and hChValue was an array containing the window handles of the three static
text child windows for the numeric values of the colors.

The new version uses SetDlgItemInt to set each text field of each child
window to a number:

SetDlgItemInt (hDlg, nCtrlID + 3, color [nCtrlID], FALSE) ;

(Although SetDlgItemInt and its companion, GetDlgItemInt, are most often
used with edit controls, they can also be used to set the text field of
other controls, such as static text controls.) The nCtrlID variable is the
ID number of the scroll bar; adding 3 to the number converts it to the ID
for the corresponding numeric label. The third parameter is the color value.
Normally, the fourth parameter would be set to TRUE to indicate that numbers
greater than 32,767 should be displayed as negatives. For this program,
however, the values range from 0 to 255, so the fourth parameter has no
effect.

In the process of converting COLORS1 to COLORS2, we passed more and more of
the work to Windows. The earlier version called CreateWindow 10 times; the
new version calls CreateWindow once and CreateDialog once. But if you think
that we've reduced our CreateWindow calls to a minimum, get a load of this
next program.


HEXCALC: Window or Dialog Box?

Perhaps the epitome of lazy programming is the HEXCALC program, shown in
Figure 10-11 beginning on the following page. This program doesn't call
CreateWindow at all, never processes WM_PAINT messages, never obtains a
device context, and never processes mouse messages. Yet it manages to
incorporate a 10-function hexadecimal calculator with a full keyboard and
mouse interface in fewer than 150 lines of source code. The calculator is
shown in Figure 10-12 on page 485.

 HEXCALC.MAK

#-----------------------
# HEXCALC.MAK make file
#-----------------------

hexcalc.exe: hexcalc.obj hexcalc.def hexcalc.res
     link hexcalc, /align:16, NUL, /nod slibcew libw, hexcalc
     rc hexcalc.res

hexcalc.obj: hexcalc.c
     cl -c -Gsw -Ow -W2 -Zp hexcalc.c

hexcalc.res : hexcalc.rc hexcalc.ico
     rc -r hexcalc.rc

 HEXCALC.C

/*----------------------------------------
   HEXCALC.C -- Hexadecimal Calculator
                (c) Charles Petzold, 1990
  ----------------------------------------*/

#include <windows.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName [] = "HexCalc" ;
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;

     if (!hPrevInstance)
          {
          wndclass.style          = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc    = WndProc ;
          wndclass.cbClsExtra     = 0 ;
          wndclass.cbWndExtra     = DLGWINDOWEXTRA ;
          wndclass.hInstance      = hInstance ;
          wndclass.hIcon          = LoadIcon (hInstance, szAppName) ;



          wndclass.hCursor        = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground  = COLOR_WINDOW + 1 ;
          wndclass.lpszMenuName   = NULL ;
          wndclass.lpszClassName  = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateDialog (hInstance, szAppName, 0, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

void ShowNumber (HWND hwnd, DWORD dwNumber)
     {
     char szBuffer [20] ;

     SetDlgItemText (hwnd, VK_ESCAPE, strupr (ltoa (dwNumber, szBuffer,
16))) ;
     }

DWORD CalcIt (DWORD dwFirstNum, short nOperation, DWORD dwNum)
     {
     switch (nOperation)
          {
          case '=' : return dwNum ;
          case '+' : return dwFirstNum +  dwNum ;
          case '-' : return dwFirstNum -  dwNum ;
          case '*' : return dwFirstNum *  dwNum ;
          case '&' : return dwFirstNum &  dwNum ;
          case '|' : return dwFirstNum |  dwNum ;
          case '^' : return dwFirstNum ^  dwNum ;
          case '<' : return dwFirstNum << dwNum ;
          case '>' : return dwFirstNum >> dwNum ;
          case '/' : return dwNum ? dwFirstNum / dwNum : ULONG_MAX ;
          case '%' : return dwNum ? dwFirstNum % dwNum : ULONG_MAX ;
          default  : return 0L ;
          }
     }

long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static BOOL  bNewNumber = TRUE ;
     static DWORD dwNumber, dwFirstNum ;
     static short nOperation = '=' ;
     HWND         hButton ;

     switch (message)
          {
          case WM_KEYDOWN :                  // left arrow --> backspace
               if (wParam != VK_LEFT)
                    break ;
               wParam = VK_BACK ;
                                             // fall through
          case WM_CHAR :
               if ((wParam = toupper (wParam)) == VK_RETURN)
                    wParam = '=' ;

               if (hButton = GetDlgItem (hwnd, wParam))
                    {
                    SendMessage (hButton, BM_SETSTATE, 1, 0L) ;
                    SendMessage (hButton, BM_SETSTATE, 0, 0L) ;
                    }
               else
                    {
                    MessageBeep (0) ;
                    break ;
                    }
                                             // fall through
          case WM_COMMAND :
               SetFocus (hwnd) ;

               if (wParam == VK_BACK)                  // backspace
                    ShowNumber (hwnd, dwNumber /= 16) ;

               else if (wParam == VK_ESCAPE)           // escape
                    ShowNumber (hwnd, dwNumber = 0L) ;

               else if (isxdigit (wParam))             // hex digit
                    {
                    if (bNewNumber)
                         {
                         dwFirstNum = dwNumber ;
                         dwNumber = 0L ;
                         }
                    bNewNumber = FALSE ;

                    if (dwNumber <= ULONG_MAX >> 4)
                         ShowNumber (hwnd, dwNumber = 16 * dwNumber + wParam
-
                              (isdigit (wParam) ? '0' : 'A' - 10)) ;
                    else
                         MessageBeep (0) ;
                    }
               else                                    // operation
                    {
                    if (!bNewNumber)
                         ShowNumber (hwnd, dwNumber =
                              CalcIt (dwFirstNum, nOperation, dwNumber)) ;
                    bNewNumber = TRUE ;
                    nOperation = wParam ;
                    }
               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 HEXCALC.RC

/*----------------------------
   HEXCALC.RC resource script
  ----------------------------*/

#include <windows.h>

HexCalc ICON hexcalc.ico

HexCalc DIALOG 32768, 0, 102, 122
     STYLE WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX
     CLASS "HexCalc"
     CAPTION "Hex Calculator"
     {
     PUSHBUTTON "D",       68,  8,  24, 14, 14
     PUSHBUTTON "A",       65,  8,  40, 14, 14
     PUSHBUTTON "7",       55,  8,  56, 14, 14
     PUSHBUTTON "4",       52,  8,  72, 14, 14
     PUSHBUTTON "1",       49,  8,  88, 14, 14
     PUSHBUTTON "0",       48,  8, 104, 14, 14
     PUSHBUTTON "0",       27, 26,   4, 50, 14
     PUSHBUTTON "E",       69, 26,  24, 14, 14
     PUSHBUTTON "B",       66, 26,  40, 14, 14
     PUSHBUTTON "8",       56, 26,  56, 14, 14
     PUSHBUTTON "5",       53, 26,  72, 14, 14
     PUSHBUTTON "2",       50, 26,  88, 14, 14
     PUSHBUTTON "Back",     8, 26, 104, 32, 14
     PUSHBUTTON "C",       67, 44,  40, 14, 14
     PUSHBUTTON "F",       70, 44,  24, 14, 14
     PUSHBUTTON "9",       57, 44,  56, 14, 14
     PUSHBUTTON "6",       54, 44,  72, 14, 14
     PUSHBUTTON "3",       51, 44,  88, 14, 14
     PUSHBUTTON "+",       43, 62,  24, 14, 14
     PUSHBUTTON "-",       45, 62,  40, 14, 14
     PUSHBUTTON "*",       42, 62,  56, 14, 14
     PUSHBUTTON "/",       47, 62,  72, 14, 14
     PUSHBUTTON "%",       37, 62,  88, 14, 14
     PUSHBUTTON "Equals",  61, 62, 104, 32, 14
     PUSHBUTTON "&&",      38, 80,  24, 14, 14
     PUSHBUTTON "|",      124, 80,  40, 14, 14
     PUSHBUTTON "^",       94, 80,  56, 14, 14
     PUSHBUTTON "<",       60, 80,  72, 14, 14
     PUSHBUTTON ">",       62, 80,  88, 14, 14
     }

 HEXCALC.ICO  -- Please refer to the book.

FIG 1011.epb

 HEXCALC.DEF

;------------------------------------
; HEXCALC.DEF module definition file
;------------------------------------

NAME           HEXCALC

DESCRIPTION    'Hexadecimal Calculator (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

  (Figure 10-12. may be found in the printed book.)

HEXCALC is a normal infix notation calculator that uses C notation for the
operations. It works with unsigned 32-bit integers and does addition,
subtraction, multiplication, division, and remainders; bitwise AND, OR, and
exclusive OR operations; and left and right bit shifts. Division by 0 causes
the result to be set to FFFFFFFF.

You can use either the mouse or keyboard with HEXCALC. You begin by
"clicking in" or typing the first number (up to eight hexadecimal digits),
then the operation, and then the second number. You can then show the result
by clicking the Equals button or by pressing either the Equals key or Enter
key. To correct your entries, you use the Back button or the Backspace or
Left Arrow key. Click the "display" box or press the Esc key to clear the
current entry.

What's so strange about HEXCALC is that the window displayed on the screen
seems to be a hybrid of a normal overlapped window and a modeless dialog
box. On the one hand, all the messages to HEXCALC are processed in a
function called WndProc that appears to be a normal window procedure. The
function returns a long, it processes the WM_DESTROY message, and it calls
DefWindowProc just like a normal window procedure. On the other hand, the
window is created in WinMain with a call to CreateDialog using a dialog box
template from HEXCALC.RC. So is HEXCALC a normal overlapped window or a
modeless dialog box?

The simple answer is that a dialog box is a window. Normally, Windows uses
its own internal window procedure to process messages to a dialog box popup
window. Windows then passes these messages to a dialog box procedure within
the program that creates the dialog box. In HEXCALC we are forcing Windows
to use the dialog box template to create a popup window, but we're
processing messages to that window ourselves.

A closer look at HEXCALC.RC will reveal how this is done. The top of the
dialog box template looks like this:

HexCalc DIALOG 32768, 0, 102, 122
     STYLE WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX
     CLASS "HexCalc"
     CAPTION "Hex Calculator"

Notice the identifiers such as WS_OVERLAPPED and WS_MINIMIZEBOX, which we
might use to create a normal window using a CreateWindow call. The CLASS
statement is the crucial difference between this dialog box and the others
we've created so far. When we omitted this statement in previous dialog box
templates, Windows registered a window class for the dialog box and used its
own window procedure to process the dialog box messages. The inclusion of a
CLASS statement here tells Windows to send the messages else-
where--specifically, to the window procedure specified in the "HexCalc"
window class.

The "HexCalc" window class is registered in the WinMain function of HEXCALC,
just like a window class for a normal window. However, note this very
important difference: The cbWndExtra field of the WNDCLASS structure is set
to DLGWINDOWEXTRA. This is essential for dialog procedures that you register
yourself.

After registering the window class, WinMain calls CreateDialog:

hwnd = CreateDialog (hInstance, szAppName, 0, NULL) ;

The second parameter (the string "HexCalc") is the name of the dialog box
template. The third parameter, which is normally the window handle of the
parent window, is set to 0 because the window has no parent. The last
parameter, which is normally the address of the dialog procedure, isn't
required because Windows won't be processing the messages and hence can't
send them to a dialog procedure.

This CreateDialog call in conjunction with the dialog box template is
effectively translated by Windows into a CreateWindow call that does the
equivalent of this:

hwnd = CreateWindow ("HexCalc", "Hex Calculator",
          WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX
          CW_USEDEFAULT, CW_USEDEFAULT,
          102 * 4 / cxChar, 122 * 8 / cyChar,
          NULL, NULL, hInstance, NULL) ;

The xChar and yChar variables are the width and height of a system font
character.

We reap an enormous benefit from letting Windows make this CreateWindow
call: Windows will not stop at creating the 1 popup window but will also
call CreateWindow for all 29 child window push-button controls defined in
the dialog box template. All these controls send WM_COMMAND messages to the
window procedure of the parent window, which is none other than WndProc.
This is an excellent technique for creating a window that must contain a
collection of child windows.


Creatively Using Control IDs

HEXCALC contains no header file with identifiers for all the ID numbers of
the child window controls in the dialog box template. We can dispense with
this file because the  ID number for each of the push-button controls is set
to the ASCII code of the text that appears in the control. This means that
WndProc can treat WM_COMMAND messages and WM_CHAR messages in much the same
way. In each case, wParam is the ASCII code of the button.

Of course, a little massaging of the keyboard messages is necessary. WndProc
traps WM_KEYDOWN messages to translate the Left Arrow key to a Backspace
key. During processing of WM_CHAR messages, WndProc converts the character
code to uppercase and the Enter key to the ASCII code for the Equals key.

The validity of a WM_CHAR message is checked by calling GetDlgItem. If the
GetDlgItem function returns 0, then the keyboard character is not one of the
ID numbers defined in the dialog box template. If the character is one of
the IDs, however, the appropriate button is flashed by sending it a couple
of BM_SETSTATE messages:

if (hButton = GetDlgItem (hwnd, wParam))
     {
     SendMessage (hButton, BM_SETSTATE, 1, 0L) ;
     SendMessage (hButton, BM_SETSTATE, 0, 0L) ;
     }

This adds a nice touch to HEXCALC's keyboard interface, and with a minimum
of effort.

When WndProc processes WM_COMMAND messages, it always sets the input focus
to the parent window:

case WM_COMMAND :
     SetFocus (hwnd) ;

Otherwise, the input focus would be shifted to one of the buttons whenever
it was clicked with the mouse.



USING THE DIALOG UTILITY

When you sit down to create a dialog box template, you'll discover that it's
not quite as easy as it looks. Dialog boxes are an important part of your
program, and the controls should be clearly and logically organized. But the
process of placing and sizing these controls is mostly a matter of trial and
error. You'll save yourself a lot of time and frustration by using the
DIALOG program included with the Windows Software Development Kit. This
program allows you to use the mouse to place controls within a dialog box
frame, to move and resize them, and to give them various attributes.

Before you start haphazardly designing your dialog box in DIALOG, you should
spend a little time in preparation. Although you can create a header file
containing identifiers for the dialog box IDs, it's easier to create and
edit the header file in a normal text editor. You should also have a general
idea of how the controls will be arranged in the dialog box.

DIALOG can read .RES files (the binary compiled resource file, not the ASCII
.RC resource script) and header files. DIALOG will prompt for the name of
both a .RES file and a .H file when you choose Open from the File menu.
Alternatively, you can read in a header file by using Open from the Include
menu.

The names of all the dialog boxes currently stored in the .RES file are
displayed when you choose View Dialog from the File menu. You can then pick
one to edit. Thus, even if you begin by attempting to manually create a
dialog box template in a .RC file and then give up in frustration, you can
switch to DIALOG to get all the coordinates and sizes correct. Remember to
compile the resource script before running DIALOG, however; as noted above,
DIALOG cannot read .RC files, only compiled .RES files. Alternatively, you
can begin a new dialog box by choosing New Dialog from the Edit menu.

You can fill up the dialog box with controls by choosing from the Control
menu, which lists 13 basic types of controls such as Check Box, Radio
Button, and Push Button. You then click the mouse where you want the control
to be placed. If the control uses text, it will initially show only the
string "Text." You can select the control for editing merely by clicking on
it. You can stretch or size it by hooking the little boxes on the sides or
corners, and you can move it around by hooking the center.

The process of placing and sizing controls becomes a little easier if you
first choose Grid from the Options menu and change both numbers to 2, thus
forcing all dialog box coordinates and sizes to be in even numbers of units.
Otherwise, you'll continually be wondering whether a particular control in a
group is really one pixel or two pixels off or whether you've merely been
looking at the screen too long. Also, try to create the controls in the
order that you'll want them to appear in the dialog box template, because
this in turn governs the order of tab stops and groups. Don't worry
inordinately about getting it precisely right the first time through; as
you'll see below, you can always change the order later.

One by one, select each control in the dialog box, choose Styles from the
Edit menu, and enter the text of the control and the ID number in the Styles
dialog box. If you've read in a header file, you can use the defined names
rather than numbers for the IDs. You can also use the Styles dialog box to
change the window style of the control. For instance, if you created a
push-button control by choosing Push Button from the Control menu, you can
make it a default push button by checking the Def Push Button box.

The dialog box you're creating is initially shown with a simple frame. To
add a caption bar and a system menu box, select the dialog box itself with
the mouse, and then choose Styles from the Edit menu. As you've seen,
controls in a Windows dialog box are organized by "groups" and "tab stops."
You use the Tab key to move between controls marked as tab stops, and the
arrow keys to move between controls within a group.

You can reorder the controls and select groups and tab stops through the
Groups option of the Dialog menu, which displays a list of all the controls
you've created. You can move a control by selecting it with the mouse and
placing the horizontal black bar cursor in a new location. To define a
group, you mark the first and last control of the group. You would also
usually flag the first control of the group as a tab stop, but you can pick
another control as the tab stop if you would like the cursor to jump to the
middle of a group.

The best part of DIALOG is that you can try out these tab stops and groups
to see if they work right. Simply choose Test from the Dialog menu. You can
use both the keyboard and mouse to test how the input focus shifts between
controls. (Don't expect radio buttons to work, however; they require some
cooperation from a dialog box procedure.)

After you're done, you can save the file. DIALOG will actually save two
files: a new .RES binary file containing the new dialog boxes and any
changes to existing dialog boxes, and an ASCII file with the extension .DLG.
The .DLG file contains the human-readable (well, almost readable) dialog box
templates. All the controls will be expressed as CONTROL statements that
include window style identifiers from WINDOWS.H. If you used names from a
header file for the IDs, these names will be used in the .DLG file.

If you've changed the header file, you'll want to save that also. But
beware: DIALOG will strip comments from it. For that reason, it's better to
maintain the header file outside DIALOG and read it into DIALOG, rather than
to save it from DIALOG.

DIALOG can't read or alter the ASCII .RC resource script file; it can't read
.DLG files, either. DIALOG can read only the binary .RES file, which it
saves in both the .RES and .DLG formats. If you create a dialog box in
DIALOG and save it as MYPROG.RES and MYPROG.DLG (for instance), you should
later edit the MYPROG.RC resource script and include the line:

rcinclude myprog.dlg

This allows the RC.EXE resource compiler to add the contents of the
MYPROG.DLG file to the other resources included in MYPROG.RC. Do not use
#include for the .DLG file. The RC.EXE resource compiler interprets only
#define statements in any file included with #include. And if you started
out by creating a dialog box in the MYPROG.RC resource script file and then
edited it in DIALOG, you must also delete the original dialog box template
from MYPROG.RC. Otherwise, you'll have two definitions for the same dialog
box.

I have a confession to make. Although none of the dialog box templates in
this chapter appear to be output from DIALOG, I originally created all of
them in DIALOG. I later edited the .DLG files (and in many cases merged them
into the .RC files) for the sole purpose of making them readable and
presentable. DIALOG is almost essential when creating dialog boxes. Don't
waste your time doing it any other way.





PART IV  THE GRAPHICS DEVICE INTERFACE
────────────────────────────────────────────────────────────────────────────

Chapter 11  An Introduction to GDI
────────────────────────────────────────────────────────────────────────────

We have been using Graphics Device Interface (GDI) functions since Chapter
1, when we first started writing to the client area of our windows. Now it's
time for a more formal coverage of the subject. This chapter discusses the
preliminaries of GDI but stops short of drawing graphics, which is the
subject of Chapter 12. Chapter 13 covers bitmaps and metafiles, which are
means of storing graphical information; Chapter 14 discusses text and fonts;
and Chapter 15 deals with printing.

THE GDI PHILOSOPHY

Graphics in Windows are handled primarily by functions exported from the
GDI.EXE module (although some drawing functions actually have entry points
in the USER.EXE file). The GDI.EXE module calls routines in the various
driver files--a .DRV for the video display screen and possibly one or more
other .DRV driver files that control printers or plotters. The video driver
accesses the hardware of the video display. Different video display adapters
and printers require different driver files.

The GDI system is constructed so that Windows can determine from the driver
what the driver can handle itself and what it needs assistance with. For
instance, if the video hardware includes a graphics coprocessor that can
draw ellipses, then GDI can take advantage of that; otherwise, the GDI
module must itself calculate the points of the ellipse and pass the points
to the driver.

Because a large number of different display devices can be attached to the
IBM PC and compatibles, one of the primary goals of GDI is to support
device-independent graphics on output devices such as video displays,
printers, and plotters. Windows programs should be able to run without
problems on any graphics output device that Windows supports. GDI
accomplishes this goal by providing facilities to insulate your programs
from the particular characteristics of different output devices. In this way
it is like other device-independent graphics programming languages. But
where Windows GDI is different is in its strong support of pixel-level
manipulation.

The world of graphics output devices is divided into two broad groups:
raster devices and vector devices. Most PC output devices are raster
devices, which means that they represent images as a pattern of dots. This
category includes video display adapters, dot-matrix printers, and laser
printers. Vector devices, which draw images using lines, are generally
limited to plotters.

Although most video display adapters and printers are raster devices, most
graphics interface languages are based solely on vectors. This means that a
program using one of these graphics languages is a level of abstraction away
from the hardware. The output device is using pixels for a graphics
representation, but the program is not talking to the interface in terms of
pixels. While you can certainly use Windows GDI as a high-level vector
drawing system, you can also use it for relatively low-level pixel
manipulation.

In this respect, Windows GDI is to other graphics interface languages what C
is to other programming languages. C is well known for its high degree of
portability among different operating systems and environments. Yet C is
also well known for allowing a programmer to perform low-level system
functions that are often impossible in other high-level languages. Just as C
is sometimes thought of as a "high-level assembly language," you can think
of GDI as a high-level interface to the hardware of the graphics device.

As you've seen, by default Windows uses a coordinate system based on pixels.
Most other graphics languages use a "virtual" coordinate system with
horizontal and vertical axes that range (for instance) from 0 to 32,767.
Although some graphics languages don't let you use pixel coordinates,
Windows GDI lets you use either system (as well as additional coordinate
systems based on physical measurements). You can use a virtual coordinate
system and keep your program distanced from the hardware, or you can use the
device coordinate system and snuggle right up to the hardware.

Some programmers think that after you start working in terms of pixels,
you've abandoned device independence. We've already seen that this is not
necessarily true. The trick  is to use the pixels in a device-independent
fashion. This requires that the graphics interface language provide
facilities for a program to determine the hardware characteristics of the
device and make appropriate adjustments. For instance, we've frequently used
the pixel size of a standard system font character to space text on the
screen. This approach allows our programs to adjust to different display
adapters with different resolutions, text sizes, and aspect ratios. You'll
see other methods in this chapter for determining display sizes.

Windows can run on either a monochrome display or a color display. If you
choose, you can write a program without worrying very much about color. If
you use color in your program and the program later runs on a monochrome
display adapter, Windows will use a shade of gray to represent the color.
However, you can also determine from your program how many colors are
available on the particular display device and take best advantage of the
hardware.

Of course, just as you can write C programs that have subtle portability
problems when they run on other computers, you can also inadvertently let
device dependencies creep into your Windows programs. That's part of the
price of not being fully insulated from the hardware. We'll examine many of
the device-dependent traps in the next few chapters.

You should also be aware of the limitations of Windows GDI. GDI is not (at
this time) capable of doing everything you may want a graphics interface to
do. Although you can move graphics objects around the display, GDI is
generally a static display system with no real animation support. GDI
provides no direct support for three-dimensional representations or for
rotations of objects. For instance, when you draw an ellipse, the ellipse
axes must be parallel to the horizontal and vertical coordinates. Although
some graphics languages use floating-point numbers for virtual coordinates,
Windows--for performance reasons--always uses 16-bit signed integers.


THE DEVICE CONTEXT

When you want to draw on a graphics output device (such as the screen or a
printer), you must first obtain a handle to a device context (or DC). In
giving your program this handle, Windows is giving you permission to use the
device. You then include the handle as a parameter in the GDI functions to
identify to Windows the device you want to draw on.

The device context contains many current "attributes" that determine how the
GDI functions work on the device. These attributes allow the parameters to
the GDI functions to include only starting coordinates or sizes and not
everything else that Windows needs to display the object on the device. For
example, when you call TextOut, you need specify in the function only the
device context handle, the starting coordinates, the text, and the length of
the text. You don't need to specify the font, the color of the text, the
color of the background behind the text, and the intercharacter spacing,
because these attributes are  part of the device context. When you want to
change one of these attributes, you call a function that changes the
attribute in the device context. Subsequent TextOut calls use the changed
attribute.

Getting the Handle to the Device Context

Windows provides several methods for obtaining a device context handle. If
you obtain a device context handle while processing a message, you should
release it (or delete it) before exiting the window function. After you
release the handle, it is no longer valid.

The most common method for obtaining and then releasing a device context
handle involves using the BeginPaint and EndPaint calls when processing the
WM_PAINT message:

hdc = BeginPaint (hwnd, &ps) ;
[other program lines]
EndPaint (hwnd, &ps) ;

The variable ps is a structure of type PAINTSTRUCT. The hdc field of this
structure is the handle to the device context that BeginPaint returns. The
PAINTSTRUCT structure also contains a RECT (rectangle) structure named
rcPaint that contains a clipping rectangle indicating the invalid region of
the window's client area. With the device context handle obtained from
BeginPaint, you can draw only within this rectangle. The EndPaint call
validates this region.

Windows programs can also obtain a handle to a device context during
processing of messages other than WM_PAINT:

hdc = GetDC (hwnd) ;
[other program lines]
ReleaseDC (hwnd, hdc) ;

This device context applies to the client area of the window whose handle is
hwnd. The primary difference between the use of these calls and of the
BeginPaint and EndPaint combination is that you can draw on your entire
client area with the handle returned from GetDC. However, ReleaseDC doesn't
validate any possibly invalid regions of the client area.

A Windows program can also obtain a handle to a device context that applies
to the entire window and not only to the window's client area:

hdc = GetWindowDC (hwnd) ;
[other program lines]
ReleaseDC (hwnd, hdc) ;

This device context includes the window caption bar, menu, scroll bars, and
frame in addition to the client area. The GetWindowDC function is rarely
used. If you want to experiment with it, you should trap WM_NCPAINT
("nonclient paint") messages, which prevent Windows from drawing on the
nonclient area of the window.

The BeginPaint, GetDC, and GetWindowDC calls obtain a device context
associated with a particular window. You can also obtain a device context
for the entire display by calling CreateDC:

hdc = CreateDC (lpszDriver, lpszDevice, lpszOutput, lpData) ;
[other program lines]
DeleteDC (hdc) ;

In the BLOWUP1 program in Chapter 4 we used this function to obtain a device
context handle that allowed us to write outside our window's client area:

hdc = CreateDC ("DISPLAY", NULL, NULL, NULL) ;

Writing outside your windows is generally impolite, but it's convenient for
some unusual applications. (Although this fact is undocumented, you can also
retrieve a device context for the entire screen by calling GetDC with a NULL
parameter.)

In Chapter 15 we'll use the CreateDC function to obtain a handle to a
printer device context:

hdcPrinter = CreateDC ("IBMGRX", "IBM Graphics", "LPT1:", NULL) ;

Of course, we won't include the names of specific printers in our programs.
Programs can instead obtain this information from WIN.INI.

Sometimes you need only to obtain some information about a device context
and not to do any drawing. In these cases, you can obtain a handle to an
"information context" using CreateIC. The parameters are the same as for the
CreateDC function:

hdcInfo = CreateIC (lpszDriver, lpszDevice, lpszOutput, lpData) ;
[other program lines]
DeleteDC (hdcInfo) ;

You can't write to the device using this information context handle. We'll
use this function in the DEVCAPS1 program shown later in this chapter to
obtain an information context for the display and the printer.

In the GRAFMENU program in Chapter 9, we obtained a memory device context to
manipulate some bitmaps. A memory device context is always created to be
compatible with an existing device context:

hdcMem = CreateCompatibleDC (hdc) ;
[other program lines]
DeleteDC (hdcMem) ;

When you first obtain a memory device context, the display surface that it
represents contains exactly 1 pixel. We'll work more with memory device
contexts in Chapter 13.

In Chapter 13 we'll also work with "metafiles." A metafile is a collection
of GDI calls encoded in binary form. You can create a metafile by obtaining
a metafile device context:

hdcMeta = CreateMetaFile (lpszFilename) ;
[other program lines]
hmf = CloseMetaFile (hdcMeta) ;

During the time that the metafile device context is valid, any GDI calls you
make using hdcMeta become part of the metafile. When you call CloseMetaFile,
the device context handle becomes invalid. The function returns a handle to
the metafile (hmf).


Getting Device Context Information

A device context usually refers to a physical display device such as a video
display or a printer. Often, you need to obtain information about this
device, including the size of the display (in terms of both pixels and
physical dimensions) and its color capabilities. You can get this
information by calling the GetDeviceCaps ("get device capabilities")
function:

nValue = GetDeviceCaps (hdc, nIndex) ;

The nIndex parameter is 1 of 28 identifiers defined in WINDOWS.H. For
instance, the nIndex HORZRES causes GetDeviceCaps to return the width of the
device in pixels; a VERTRES parameter returns the height of the device in
pixels. If hdc is a handle to a screen device context, that's the same
information you can get from GetSystemMetrics. If hdc is a handle to a
printer device context, then GetDeviceCaps returns the height and width of
the printer display area in pixels.

You can also use GetDeviceCaps to determine the device's capabilities of
processing various types of graphics. This is unimportant for the video
display, but it becomes very important when working with printers. For
instance, most plotters can't draw bitmapped images--and GetDeviceCaps can
tell you that.





The DEVCAPS1 Program

The DEVCAPS1 program, shown in Figure 11-1, displays all the information
available from the GetDeviceCaps function for either the video display or
the selected printer. (A second version of this program, called DEVCAPS2,
will be presented in Chapter 15.) If you change the current printer using
the Windows Control Panel program, DEVCAPS1 updates the printer information.

 DEVCAPS1.MAK

#------------------------
# DEVCAPS1.MAK make file
#------------------------

devcaps1.exe : devcaps1.obj devcaps.obj devcaps1.def devcaps1.res
     link devcaps1 devcaps, /align:16, NUL, /nod slibcew libw, devcaps1
     rc devcaps1.res

devcaps1.obj : devcaps1.c devcaps1.h
     cl -c -Gsw -Ow -W2 -Zp devcaps1.c

devcaps.obj : devcaps.c
     cl -c -Gsw -Ow -W2 -Zp devcaps.c

devcaps1.res : devcaps1.rc devcaps1.h
     rc -r devcaps1.rc

 DEVCAPS1.C

/*------------------------------------------------------
   DEVCAPS1.C -- Displays Device Capability Information
                 (c) Charles Petzold, 1990
  ------------------------------------------------------*/

#include <windows.h>
#include <string.h>
#include "devcaps1.h"

void DoBasicInfo (HDC, HDC, short, short) ;       // in DEVCAPS.C
void DoOtherInfo (HDC, HDC, short, short) ;
void DoBitCodedCaps (HDC, HDC, short, short, short) ;

long FAR PASCAL WndProc (HWND, WORD, WORD, LONG) ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     static char szAppName[] = "DevCaps" ;
     HWND        hwnd ;
     MSG         msg ;
     WNDCLASS    wndclass ;



     if (!hPrevInstance)
          {
          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = WndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = szAppName ;
          wndclass.lpszClassName = szAppName ;

          RegisterClass (&wndclass) ;
          }

     hwnd = CreateWindow (szAppName, "Device Capabilities",
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     ShowWindow (hwnd, nCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
          {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
          }
     return msg.wParam ;
     }

HDC GetPrinterIC ()
     {
     char szPrinter [64] ;
     char *szDevice, *szDriver, *szOutput ;

     GetProfileString ("windows", "device", "", szPrinter, 64) ;

     if ((szDevice = strtok (szPrinter, "," )) &&
         (szDriver = strtok (NULL,      ", ")) &&
         (szOutput = strtok (NULL,      ", ")))

               return CreateIC (szDriver, szDevice, szOutput, NULL) ;

     return NULL ;
     }
long FAR PASCAL WndProc (HWND hwnd, WORD message, WORD wParam, LONG lParam)
     {
     static short cxChar, cyChar, nCurrentDevice = IDM_SCREEN,
                                  nCurrentInfo   = IDM_BASIC ;
     HDC          hdc, hdcInfo ;
     HMENU        hMenu ;
     PAINTSTRUCT  ps ;
     TEXTMETRIC   tm ;

     switch (message)
          {
          case WM_CREATE :
               hdc = GetDC (hwnd) ;
               SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;

               GetTextMetrics (hdc, &tm) ;
               cxChar = tm.tmAveCharWidth ;
               cyChar = tm.tmHeight + tm.tmExternalLeading ;
               ReleaseDC (hwnd, hdc) ;
               return 0 ;

          case WM_COMMAND :
               hMenu = GetMenu (hwnd) ;

               switch (wParam)
                    {
                    case IDM_SCREEN :
                    case IDM_PRINTER :
                         CheckMenuItem (hMenu, nCurrentDevice, MF_UNCHECKED)
;
                         nCurrentDevice = wParam ;
                         CheckMenuItem (hMenu, nCurrentDevice, MF_CHECKED) ;
                         InvalidateRect (hwnd, NULL, TRUE) ;
                         return 0 ;

                    case IDM_BASIC :
                    case IDM_OTHER :
                    case IDM_CURVE :
                    case IDM_LINE :
                    case IDM_POLY :
                    case IDM_TEXT :
                         CheckMenuItem (hMenu, nCurrentInfo, MF_UNCHECKED) ;
                         nCurrentInfo = wParam ;
                         CheckMenuItem (hMenu, nCurrentInfo, MF_CHECKED) ;
                         InvalidateRect (hwnd, NULL, TRUE) ;
                         return 0 ;
                    }
               break ;
          case WM_DEVMODECHANGE :
               InvalidateRect (hwnd, NULL, TRUE) ;
               return 0 ;

          case WM_PAINT :
               hdc = BeginPaint (hwnd, &ps) ;
               SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;

               if (nCurrentDevice == IDM_SCREEN)
                    hdcInfo = CreateIC ("DISPLAY", NULL, NULL, NULL) ;
               else
                    hdcInfo = GetPrinterIC () ;

               if (hdcInfo)
                    {
                    switch (nCurrentInfo)
                         {
                         case IDM_BASIC :
                              DoBasicInfo (hdc, hdcInfo, cxChar, cyChar) ;
                              break ;

                         case IDM_OTHER :
                              DoOtherInfo (hdc, hdcInfo, cxChar, cyChar) ;
                              break ;

                         case IDM_CURVE :
                         case IDM_LINE :
                         case IDM_POLY :
                         case IDM_TEXT :
                              DoBitCodedCaps (hdc, hdcInfo, cxChar, cyChar,
                                              nCurrentInfo - IDM_CURVE) ;
                              break ;
                         }

                    DeleteDC (hdcInfo) ;
                    }

               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
     }

 DEVCAPS.C

/*---------------------------------------------------------
   DEVCAPS.C -- Display routines for DEVCAPS1 and DEVCAPS2
                (c) Charles Petzold, 1990
  ---------------------------------------------------------*/

#include <windows.h>
#include <string.h>
#include <stdio.h>

typedef struct
     {
     short nMask ;
     char  *szMask ;
     char  *szDesc ;
     }
     BITS ;

void DoBasicInfo (HDC hdc, HDC hdcInfo, short cxChar, short cyChar)
     {
     static struct
          {
          short nIndex ;
          char  *szDesc ;
          }
          info [] =
          {
          HORZSIZE,      "HORZSIZE     Width in millimeters:",
          VERTSIZE,      "VERTSIZE     Height in millimeters:",
          HORZRES,       "HORZRES      Width in pixels:",
          VERTRES,       "VERTRES      Height in raster lines:",
          BITSPIXEL,     "BITSPIXEL    Color bits per pixel:",
          PLANES,        "PLANES       Number of color planes:",
          NUMBRUSHES,    "NUMBRUSHES   Number of device brushes:",
          NUMPENS,       "NUMPENS      Number of device pens:",
          NUMMARKERS,    "NUMMARKERS   Number of device markers:",
          NUMFONTS,      "NUMFONTS     Number of device fonts:",
          NUMCOLORS,     "NUMCOLORS    Number of device colors:",
          PDEVICESIZE,   "PDEVICESIZE  Size of device structure:",
          ASPECTX,       "ASPECTX      Relative width of pixel:",
          ASPECTY,       "ASPECTY      Relative height of pixel:",
          ASPECTXY,      "ASPECTXY     Relative diagonal of pixel:",
          LOGPIXELSX,    "LOGPIXELSX   Horizontal dots per inch:",
          LOGPIXELSY,    "LOGPIXELSY   Vertical dots per inch:",
          SIZEPALETTE,   "SIZEPALETTE  Number of palette entries:",
          NUMRESERVED,   "NUMRESERVED  Reserved palette entries:",
          COLORRES,      "COLORRES     Actual color resolution:"
          } ;
     char   szBuffer [80] ;
     short  i, nLine ;

     for (i = 0 ; i < sizeof info / sizeof info [0] ; i++)
          TextOut (hdc, cxChar, (i + 1) * cyChar, szBuffer,
               sprintf (szBuffer, "%-40s%8d", info[i].szDesc,
                    GetDeviceCaps (hdcInfo, info[i].nIndex))) ;
     }

void DoOtherInfo (HDC hdc, HDC hdcInfo, short cxChar, short cyChar)
     {
     static BITS clip [] =
          {
          CP_RECTANGLE,  "CP_RECTANGLE",     "Can clip to rectangle:"
          } ;

     static BITS raster [] =
          {
          RC_BITBLT,       "RC_BITBLT",       "Capable of simple BitBlt:",
          RC_BANDING,      "RC_BANDING",      "Requires banding support:",
          RC_SCALING,      "RC_SCALING",      "Requires scaling support:",
          RC_BITMAP64,     "RC_BITMAP64",     "Supports bitmaps >64K:",
          RC_GDI20_OUTPUT, "RC_GDI20_OUTPUT", "Has 2.0 output calls:",
          RC_DI_BITMAP,    "RC_DI_BITMAP",    "Supports DIB to memory:",
          RC_PALETTE,      "RC_PALETTE",      "Supports a palette:",
          RC_DIBTODEV,     "RC_DIBTODEV",     "Supports bitmap conversion:",
          RC_BIGFONT,      "RC_BIGFONT",      "Supports fonts >64K:",
          RC_STRETCHBLT,   "RC_STRETCHBLT",   "Supports StretchBlt:",
          RC_FLOODFILL,    "RC_FLOODFILL",    "Supports FloodFill:"
          } ;

     static char *szTech [] = { "DT_PLOTTER (Vector plotter)",
                                "DT_RASDISPLAY (Raster display)",
                                "DT_RASPRINTER (Raster printer)",
                                "DT_RASCAMERA (Raster camera)",
                                "DT_CHARSTREAM (Character-stream, PLP)",
                                "DT_METAFILE (Metafile, VDM)",
                                "DT_DISPFILE (Display-file)" } ;
     char        szBuffer [80] ;
     short       i ;

     TextOut (hdc, cxChar, cyChar, szBuffer,
          sprintf (szBuffer, "%-24s%04XH",
               "DRIVERVERSION:", GetDeviceCaps (hdcInfo, DRIVERVERSION))) ;

     TextOut (hdc, cxChar, 2 * cyChar, szBuffer,
          sprintf (szBuffer, "%-24s%-40s",
               "TECHNOLOGY:", szTech [GetDeviceCaps (hdcInfo, TECHNOLOGY)]))
;
     TextOut (hdc, cxChar, 4 * cyChar, szBuffer,
          sprintf (szBuffer, "CLIPCAPS (Clipping capabilities)")) ;

     for (i = 0 ; i < sizeof clip / sizeof clip [0] ; i++)
          TextOut (hdc, 9 * cxChar, (i + 6) * cyChar, szBuffer,
               sprintf (szBuffer, "%-16s%-28s %3s",
                    clip[i].szMask, clip[i].szDesc,
                    GetDeviceCaps (hdcInfo, CLIPCAPS) & clip[i].nMask ?
                         "Yes" : "No")) ;

     TextOut (hdc, cxChar, 8 * cyChar, szBuffer,
          sprintf (szBuffer, "RASTERCAPS (Raster capabilities)")) ;

     for (i = 0 ; i < sizeof raster / sizeof raster [0] ; i++)
          TextOut (hdc, 9 * cxChar, (i + 10) * cyChar, szBuffer,
               sprintf (szBuffer, "%-16s%-28s %3s",
                    raster[i].szMask, raster[i].szDesc,
                    GetDeviceCaps (hdcInfo, RASTERCAPS) & raster[i].nMask ?
                         "Yes" : "No")) ;
     }

void DoBitCodedCaps (HDC hdc, HDC hdcInfo, short cxChar, short cyChar,
                     short nType)
     {
     static BITS curves [] =
          {
          CC_CIRCLES,    "CC_CIRCLES",    "circles:",
          CC_PIE,        "CC_PIE",        "pie wedges:",
          CC_CHORD,      "CC_CHORD",      "chord arcs:",
          CC_ELLIPSES,   "CC_ELLIPSES",   "ellipses:",
          CC_WIDE,       "CC_WIDE",       "wide borders:",
          CC_STYLED,     "CC_STYLED",     "styled borders:",
          CC_WIDESTYLED, "CC_WIDESTYLED", "wide and styled borders:",
          CC_INTERIORS,  "CC_INTERIORS",  "interiors:"
          } ;

     static BITS lines [] =
          {
          LC_POLYLINE,   "LC_POLYLINE",   "polylines:",
          LC_MARKER,     "LC_MARKER",     "markers:",
          LC_POLYMARKER, "LC_POLYMARKER", "polymarkers",
          LC_WIDE,       "LC_WIDE",       "wide lines:",
          LC_STYLED,     "LC_STYLED",     "styled lines:",
          LC_WIDESTYLED, "LC_WIDESTYLED", "wide and styled lines:",
          LC_INTERIORS,  "LC_INTERIORS",  "interiors:"
          } ;
     static BITS poly [] =
          {
          PC_POLYGON,    "PC_POLYGON",    "alternate fill polygon:",
          PC_RECTANGLE,  "PC_RECTANGLE",  "rectangle:",
          PC_TRAPEZOID,  "PC_TRAPEZOID",  "winding number fill polygon:",
          PC_SCANLINE,   "PC_SCANLINE",   "scanlines:",
          PC_WIDE,       "PC_WIDE",       "wide borders:",
          PC_STYLED,     "PC_STYLED",     "styled borders:",
          PC_WIDESTYLED, "PC_WIDESTYLED", "wide and styled borders:",
          PC_INTERIORS,  "PC_INTERIORS",  "interiors:"
          } ;

     static BITS text [] =
          {
          TC_OP_CHARACTER, "TC_OP_CHARACTER", "character output precision:",
          TC_OP_STROKE,    "TC_OP_STROKE",    "stroke output precision:",
          TC_CP_STROKE,    "TC_CP_STROKE",    "stroke clip precision:",
          TC_CR_90,        "TC_CP_90",        "90-degree character
rotation:",
          TC_CR_ANY,       "TC_CR_ANY",       "any character rotation:",
          TC_SF_X_YINDEP,  "TC_SF_X_YINDEP",  "scaling independent of x and
y:",
          TC_SA_DOUBLE,    "TC_SA_DOUBLE",    "doubled character for
scaling:",
          TC_SA_INTEGER,   "TC_SA_INTEGER",   "integer multiples for
scaling:",
          TC_SA_CONTIN,    "TC_SA_CONTIN",    "any multiples for exact
scaling:",
          TC_EA_DOUBLE,    "TC_EA_DOUBLE",    "double-weight characters:",
          TC_IA_ABLE,      "TC_IA_ABLE",      "italicizing:",
          TC_UA_ABLE,      "TC_UA_ABLE",      "underlining:",
          TC_SO_ABLE,      "TC_SO_ABLE",      "strikeouts:",
          TC_RA_ABLE,      "TC_RA_ABLE",      "raster fonts:",
          TC_VA_ABLE,      "TC_VA_ABLE",      "vector fonts:"
          } ;

     static struct
          {
          short nIndex ;
          char  *szTitle ;
          BITS  (*pbits) [] ;
          short nSize ;
          }
          bitinfo [] =
          {
          CURVECAPS,  "CURVCAPS (Curve capabilities)",
                      (BITS (*)[]) curves, sizeof curves / sizeof curves
[0],
          LINECAPS,   "LINECAPS (Line capabilities)",
                      (BITS (*)[]) lines, sizeof lines / sizeof lines [0],
          POLYGONALCAPS, "POLYGONALCAPS (Polygonal capabilities)",
                      (BITS (*)[]) poly, sizeof poly / sizeof poly [0],
TEXTCAPS,   "TEXTCAPS (Text capabilities)",
                      (BITS (*)[]) text, sizeof text / sizeof text [0]
          } ;

     static char szBuffer [80] ;
     BITS        (*pbits) [] = bitinfo [nType].pbits ;
     short       nDevCaps = GetDeviceCaps (hdcInfo, bitinfo [nType].nIndex)
;
     short       i ;

     TextOut (hdc, cxChar, cyChar, bitinfo [nType].szTitle,
                    strlen (bitinfo [nType].szTitle)) ;

     for (i = 0 ; i < bitinfo [nType].nSize ; i++)
          TextOut (hdc, cxChar, (i + 3) * cyChar, szBuffer,
               sprintf (szBuffer, "%-16s %s %-32s %3s",
                    (*pbits)[i].szMask, "Can do", (*pbits)[i].szDesc,
                    nDevCaps & (*pbits)[i].nMask ? "Yes" : "No")) ;
     }

 DEVCAPS1.RC

/*-----------------------------
   DEVCAPS1.RC resource script
  -----------------------------*/

#include "devcaps1.h"

DevCaps MENU
     {
     POPUP "&Device"
          {
          MENUITEM "&Screen",                IDM_SCREEN, CHECKED
          MENUITEM "&Printer",               IDM_PRINTER
          }
     POPUP "&Capabilities"
          {
          MENUITEM "&Basic Information",     IDM_BASIC, CHECKED
          MENUITEM "&Other Information",     IDM_OTHER
          MENUITEM "&Curve Capabilities",    IDM_CURVE
          MENUITEM "&Line Capabilities",     IDM_LINE
          MENUITEM "&Polygonal Capabilities",IDM_POLY
          MENUITEM "&Text Capabilities",     IDM_TEXT
          }
     }

 DEVCAPS1.H

/*------------------------
   DEVCAPS1.H header file
  ------------------------*/

#define IDM_SCREEN  1
#define IDM_PRINTER 2

#define IDM_BASIC   3
#define IDM_OTHER   4
#define IDM_CURVE   5
#define IDM_LINE    6
#define IDM_POLY    7
#define IDM_TEXT    8

 DEVCAPS1.DEF

;-------------------------------------
; DEVCAPS1.DEF module definition file
;-------------------------------------

NAME           DEVCAPS1

DESCRIPTION    'Displays Device Capability Info (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        WndProc

The DEVCAPS1 Device menu lets you select either the screen or the printer.
Because DEVCAPS1 needs only to obtain information about this device, it gets
a handle to an information context (using the CreateIC function) rather than
to a device context. Getting an information context handle for the video
device is easy:

HIC = CreateIC ("DISPLAY", NULL, NULL, NULL) ;

However, an information context handle for the printer requires more complex
code:

HDC GetPrinterIC ()
     {
     char szPrinter [64] ;
     char *szDevice, *szDriver, *szOutput ;

     GetProfileString ("windows", "device", "", szPrinter, 64) ;

     if ((szDevice = strtok (szPrinter, ",")) &&
         (szDriver = strtok (NULL,      ", ")) &&
         (szOutput = strtok (NULL,      ", ")))
               return CreateIC (szDriver, szDevice, szOutput, NULL) ;

     return NULL ;
     }

The selected printer is listed in the [windows] section of the WIN.INI file
in the following format:

device=device name,driver filename,port

For an IBM Graphics printer connected to the LPT1 printer port, the WIN.INI
line is:

device=IBM Graphics,IBMGRX,LPT1:

IBM Graphics is the name of the printer, and IBMGRX.DRV is the name of the
driver file.

To get an information context (or device context) for the current printer,
you must first obtain the character string following device= in WIN.INI by
using the GetProfileString function. You must then parse this string into
the three components: the device name, the driver filename, and the port.
You can do this in various ways. I happened to use the C strtok function,
which is designed for parsing character strings separated by delimiters such
as commas and spaces. Note that the device name itself can have embedded
blanks.

If you'd like to use DEVCAPS1 to examine the device capabilities of other
printers, you can add printer driver files to your Windows subdirectory
using the Control Panel program and then select each of these printers, one
by one, as the current printer. Specify that the port the printer is
connected to is "NONE." An advantage of CreateIC over CreateDC is that
CreateDC returns a device context handle only if the printer is attached to
a port, whereas CreateIC doesn't care whether the printer is attached. To
get an idea of the range of devices you'll be dealing with, you might want
to obtain the GetDeviceCaps information for a few different types of
printers, such as a simple nongraphics printer (the "Generic/ Text Only"
printer), a sophisticated laser printer (the Apple LaserWriter Plus), and a
plotter (the Hewlett-Packard ColorPro). DEVCAPS1 intercepts the
WM_DEVMODECHANGE message that Control Panel sends to all applications to
signal that the current printer has been changed.

The Capabilities menu in DEVCAPS1 lets you display one of six screens that
show the GetDeviceCaps information. Much of the DEVCAPS1 code is dedicated
to formatting this information. When you choose the Basic Information option
from the Capabilities menu, the most important information is displayed,
including the size of the display, the number of pure colors it can display,
and the organization of display memory into color planes and color bits per
pixel. Figure 11-2 shows this basic information for a VGA; Figure 11-3 shows
the information for an Apple LaserWriter Plus.

When you choose the Other Information option from the Capabilities menu, the
program displays the type of device (usually "Raster device," "Raster
printer," or "Vector plotter") and gives some information that is crucial
for using printers, as you'll discover when you come to Chapter 15. Figure
11-4 shows this display for an Apple LaserWriter Plus. The RC_BITBLT
identifier indicates that this printer can accept bitmaps; text-only
printers and plotters cannot, however, which means that some GDI functions
won't work on them. The RC_BANDING identifier indicates that this IBM
printer, like many printers, requires "banding" support, which means that
the GDI module must print to the printer in segments, each occupying a small
section of the page. Again, we'll explore these issues further in Chapter
16.

  (Figure 11-2. may be found in the printed book.)

  (Figure 11-3. may be found in the printed book.)

  (Figure 11-4. may be found in the printed book.)

The other menu options in DEVCAPS display curve, line, polygon, and text
capabilities. The information displayed indicates the type of graphics and
text manipulation that the device driver can handle. However, this
information is much more important to Windows itself than to application
programs--if the driver lacks one of these capabilities and a program
demands it, then the GDI module must assume the task. For instance, you
don't have to check to see if your printer is capable of drawing ellipses
before you draw an ellipse.

The information that you can obtain from GetDeviceCaps using the CURVECAPS,
LINECAPS, POLYGONALCAPS, and TEXTCAPS parameters is encoded in bits in the
return value. Although the Programmer's Reference doesn't indicate it,
WINDOWS.H includes identifiers beginning with the letters CC, LC, PC, and TC
to help you mask out the bits you want.


The Size of the Device

The most important information that your Windows program can obtain about
the video device from GetDeviceCaps is the size of the display (in both
millimeters and pixels) and the display's pixel aspect ratio. These
dimensions can help in scaling images to be displayed. To give you some idea
of what these numbers look like, the table below presents information from
GetDeviceCaps for four common IBM video adapters: the Color/Graphics Adapter
(CGA), Enhanced Graphics Adapter (EGA), the Video Graphics Array (VGA), and
the 8514/A:

╓┌───────────────────────────┌────┌────┌────┌────────────────────────────────╖
GetDeviceCaps Index         CGA  EGA  VGA  8514/A
────────────────────────────────────────────────────────────────────────────
HORZSIZE (width in mm)      240  240  208  280
VERTSIZE (height in mm)     180  175  156  210
HORZRES (pixel width)       640  640  640  1024
GetDeviceCaps Index         CGA  EGA  VGA  8514/A
────────────────────────────────────────────────────────────────────────────
HORZRES (pixel width)       640  640  640  1024
VERTRES (pixel height)      200  350  480  760
ASPECTX (horizontal)        5    38   36   10
ASPECTY (vertical)          12   48   36   14
ASPECTXY (diagonal)         13   61   51   14
LOGPIXELSX (x pixels/inch)  96   96   96   120
LOGPIXELSY (y pixels/inch)  48   72   96   120


The HORZSIZE and VERTSIZE values are the width and height of the display
area in millimeters. Of course, the Windows driver doesn't really know the
size of the display you have attached to your video adapter. These
dimensions are based on standard display sizes for the adapters.

The HORZRES and VERTRES values are the width and height of the display area
in pixels. For a device context for a video display, these are the same
values as those returned from GetSystemMetrics.

The ASPECTX, ASPECTY, and ASPECTXY values are the relative width, height,
and diagonal size of each pixel. ASPECTXY equals the square root of the sum
of ASPECTX squared and ASPECTY squared.

The LOGPIXELSX and LOGPIXELSY values are the number of pixels per a
horizontal and a vertical "logical inch." A logical inch is not a real inch
(25.4 mm), as you can easily determine by performing a few calculations
using the HORZSIZE, VERTSIZE, HORZRES, and VERTRES values. These LOGPIXELSX
and LOGPIXELSY values require a little explanation. You may have noticed
that the WRITE program and some other Windows programs display a ruler that
isn't quite right: If you measure the ruler as displayed on an VGA, you'll
find that what it declares as 1 inch is really more like 1-1/2 inches. These
programs are using the LOGPIXELSX and LOGPIXELSY values for the ruler. If
WRITE used actual physical dimensions, normal 10-point or 12-point text
would be so small as to be nearly illegible. These logical dimensions in
effect blow up the display to allow an adequate size for displaying text.
When we start working with text and fonts in Chapter 14, we'll wrestle again
with this problem. It affects only video displays; for printers, all the
dimensions returned from GetDeviceCaps are consistent.


Finding Out About Color

During the discussion of bitmaps in Chapter 8, I noted the two ways in which
memory in a video display adapter can be organized for color. In some video
adapters, memory is organized into a number of color planes. Within a plane,
each bit corresponds to a single pixel and represents a particular primary
color (such as red, green, or blue). Other video adapters have a single
color plane, in which a number of adjacent bits represent the color of each
pixel.

GetDeviceCaps lets you determine the organization of memory in the video
adapter and the number of colors it can represent. This call returns the
number of color planes:

nPlanes = GetDeviceCaps (hdc, PLANES) ;

This call returns the number of color bits per pixel:

nBitsPixel = GetDeviceCaps (hdc, BITSPIXEL) ;

Most graphics display devices that are capable of color use either multiple
color planes or multiple color bits per pixel, but not both; in other words,
one of these calls will return a value of 1. The number of colors that can
be rendered on the video adapter can be calculated by the formula:

nColors = 1 << (nPlanes * nBitsPixel) ;

This value may or may not be the same as the number of colors obtainable
with the NUMCOLORS parameter:

nColors = GetDeviceCaps (hdc, NUMCOLORS) ;

These two numbers will be different for most plotters. For a plotter, both
the PLANES and BITSPIXEL values will equal 1, but the NUMCOLORS value will
reflect the number of colored pens that the plotter has. For monochrome
devices, GetDeviceCaps returns a 2 for the NUMCOLORS parameter.

The two values can also be different for video adapters that support
loadable color palettes under Windows 3 (such as the IBM 8514/A adapter).
The 8514/A has 1 plane and 8 bits per pixel, which means that 256 colors are
possible. GetDeviceCaps with the NUMCOLORS parameter returns the number of
colors reserved by Windows (20 in the case of the 8514/A). The remaining 236
colors can be set by a Windows program.

The number of colors returned from GetDeviceCaps is the number of pure
colors that the device can display. Windows can use dithering (which
involves a pixel pattern that combines pixels of different colors) to
represent colors in addition to the pure colors.

A color is usually represented by an unsigned long integer with 3 bytes, one
each for the intensity of red, green, and blue. (Chapters 5 and 6 discussed
this subject in greater detail.) You can determine the closest pure color of
a particular color value by calling GetNearestColor:

rgbPureColor = GetNearestColor (hdc, rgbColor) ;


The Device Context Attributes

As I noted above, Windows uses the device context to store "attributes" that
govern how the GDI functions operate on the display. For instance, when you
display some text using the TextOut function, you don't have to specify the
color of the text or the font. Windows uses the device context to obtain
this information.

When a program obtains a handle to a device context, Windows creates a
device context with default values for all the attributes. The device
context attributes are shown in the following table. A program can change or
obtain any of the attributes.

╓┌──────────────────┌───────────────────┌──────────────────────┌─────────────►
Device Context     Default             Function(s) to Change  Function(s) to
Attribute
─────────────────────────────────────────────────────────────────────────────
Mapping mode       MM_TEXT             SetMapMode             GetMapMode

Window origin      (0, 0)              SetWindowOrg           GetWindowOrg

OffsetWindowOrg

Viewport origin    (0, 0)              SetViewportOrg         GetViewportOrg

OffsetViewportOrg

Window extents     (1, 1)              SetWindowExt           GetWindowExt
Device Context     Default             Function(s) to Change  Function(s) to
Attribute
─────────────────────────────────────────────────────────────────────────────
Window extents     (1, 1)              SetWindowExt           GetWindowExt

SetMapMode

Viewport extents   (1, 1)              SetViewportExt         GetViewportExt

SetMapMode

Pen                BLACK_PE            SelectObject           SelectObject

Brush              WHITE_BRUSH         SelectObject           SelectObject

Font               SYSTEM_FONT         SelectObject           SelectObject

Bitmap             None                SelectObject           SelectObject

Current pen        (0, 0)              MoveTo
position
Device Context     Default             Function(s) to Change  Function(s) to
Attribute
─────────────────────────────────────────────────────────────────────────────
position

LineTo             GetCurrentPosition

Background mode    OPAQUE              SetBkMode              GetBkMode

Background color   White               SetBkColor             GetBkColor

Text color         Black               SetTextColor           GetTextColor

Drawing mode       R2_COPYPEN          SetROP2                GetROP2

Stretching mode    BLACKONWHITE        SetPolyFillMode        GetPolyFillMode

Polygon filling    ALTERNATE           SetPolyFillMode        GetPolyFillMode
mode

Intercharacter     0                   SetTextCharacterExtra  GetTextCharacte
Device Context     Default             Function(s) to Change  Function(s) to
Attribute
─────────────────────────────────────────────────────────────────────────────
Intercharacter     0                   SetTextCharacterExtra  GetTextCharacte
spacing

Brush origin       (0, 0) in s