═══ 1. Title Page ═══ Welcome to EDM/2 - The Electronic OS/2 Developers' Magazine!. Portions Copyright (C)1993 by Steve Luzynski. Issue #1 - March 1993. (Press 'Forward' to page.) ═══ 2. Copyright Notice and other Legal Stuff ═══ EDM/2 Copyright (C)1993 by Steve Luzynski. This publication may be freely distributed in electronic form provided that all parts are present in their original unmodified form. A reasonable fee may be charged for the physical act of distribution; no fee may be charged for the publication itself. All articles are copyrighted by their authors. No part of any article may be reproduced without permission from the original author. Neither this publication nor Steve Luzynski is affiliated with International Business Machines Corporation. OS/2 is a registered trademark of International Business Machines Corporation. Other trademarks are property of their respective owners. Any mention of a product in this publication does not constitute an endorsement or affiliation unless specifically stated in the text. ═══ 3. From the Editor ═══ Hello, and welcome to the first issue of EDM/2, the Electronic Developer's Magazine! This project was the result of my own struggle to get information on programming OS/2, particularly in the case of writing Presentation Manager programs. What information there is available seems to be spread out amongst .INF files, books written for OS/2 1.3, and old magazine articles. My goal was to provide a single resource where developers, both new and old, could get information on topics ranging from "How do I create a window?" to "How do I write an IFS?" The tone of this magazine may be a bit irreverent at times - not all computer programmers are antisocial hermits who only leave their rooms to get more Doritos (that's just me). Consider yourself warned. (insert smile here) In later issues, I'll use this space to actually talk about topics related to developing for OS/2. This time, however, I have some administrative business to get off my chest... Contributions If you have an idea for an article, by all means contact me! Since this magazine is targeted at developers of all levels, no topic is too general or too esoteric to be appreciated by someone. You can generally assume basic familiarity with the C programming language - beyond that, use your judgement. Someone reading an article on writing device drivers will probably be more advanced than someone reading about resource files. One area I would like to see covered that hasn't been in this issue is the commercial developer's software that is available out there. Reviews of Zortech C++, IBM C Set/2, etc. would make nice inclusions. Money This magazine is free for the taking. All of the material is copyrighted by the person who wrote it - the authors retain all rights to the text as well as their code unless stated otherwise. I can't pay them for their efforts, so by leaving their work as theirs I leave open the possibility for paid reprints in traditional paper works. Since I really could in no way control the random distribution of this work, I'm not even attempting to charge for it. However, if you do feel a deep seated desire to pay someone for it, by all means feel free to send your donation: o to me. Your money will all be pushed back into the magazine in the form of storage space, Compuserve fees, etc. My address can be located by emailing me at the address listed at the end of this article. o to the Free Software Foundation. They are responsible for emx/gcc and a lot more really nice FREE packages available for a variety of machines. They also have administrative overhead just like any other organization. They can be reached at: Free Software Foundation, Inc. 675 Mass Ave Cambridge, MA 02139, USA Etc This articles presented in this magazine were written by people with a wide variety of expertise in the use of IPF (the Information Presentation Facility). Because of this, the style of the articles is different for each one - from a straightforward scrolling window to multilevel windows with custom icons and hotlinks to glossary items. As we all get better at this, eventually everything will look as nice as Gavin's article. (editorial smile) Please bear with me until then and let me know if you have any suggestions on style or format. But enough from me. Let's get to it. -Steve Luzynski, Editor. sal8@po.cwru.edu ═══ 4. This Month's Features ═══ o Getting Started with emx/gcc o The Making of MineSweeper o Palette Manager o Advanced GPI:Retained Segments and Transformations ═══ 4.1. Getting Started with emx/gcc ═══ Installing EMX A Free 'C' Compiler for OS/2 2.0. ═══ 4.1.1. Introduction ═══ (Note: This document was originally written by Brooke P. Anderson. It has been modified slightly to conform to the .INF format. The original may be obtained from the same source as emx itself.) Introduction This document describes a free GNU software-development system for OS/2 v2.0. It tells you how to acquire all of the necessary software, how to install it, how to start using it, and where to look for more information. The GNU software-development system includes a C and C++ compiler, a debugger, an assembler, a make utility (for automating the compilation of programs made of many source files), and a hypertext reader (for reading the documentation). The compiler generates full 32-bit, optimized code, and it supports all of the OS/2 API calls, so you software developers out there can use it for PM programming, manipulating semaphores, manipulating threads, using named pipes, etc. Optional packages include curses (a standard library for manipulating screenfuls of text and for moving the cursor), a collection of sample programs, and full source code for the system. GNU software is originally developed by the Free Software Foundation, an organization which produces a lot of free software for UNIX. After the Free Software Foundation releases the UNIX versions, people often port them to many other operation systems (such as OS/2). Despite the fact that the software is free, the UNIX community considers it a standard and often prefers it over other products because of its high quality. The compilers, for example, produce well-optimized code. Sometimes, there is more than one port of a GNU program. For OS/2, there are two different ports of the GNU compiler (called "gcc"). This document discusses only one of them (the EMX port) since the EMX port provides faster floating point routines and works with a debugger. This document deals with version 0.8f of the EMX port, which is the latest version. People frequently produce new versions of and enhancements for the GNU software and the ports based on it. IMPORTANT: I have not tested the software on FAT file systems. I think it will work, but I have not checked all of the file names to make sure they comply with the egregious 8.3 file-naming convention. The following topics may be selected by clicking on the individual topics of interest or by simply pressing the 'Forward' button on the bottom of the window. How to get emx How to install emx UNZ50x32.EXE WHLINF8F.ZIP GNUMK362.ZIP GNUDEV.ZIP, EMXDEV.ZIP, GPPDEV.ZIP, and OBJCDEV.ZIP The final steps Using info Using the compiler Using the debugger Using make Using the assembler Where to get more information Conclusions Optional packages Sources of distribution Distributing this document ═══ 4.1.1.1. How to get emx ═══ The full software-development system (and the various optional packages described later in this document) are available from a variety of sources. If you have access to Internet, you can get the files from anonymous-ftp sites. In the USA, the main anonymous-ftp site for OS/2 is ftp-os2.nmsu.edu. In Germany, the main anonymous-ftp site for OS/2 is rusinfo.rus.uni-stuttgart.de. Also, see the end of this document for a list of people who are willing to distribute the whole system through regular mail. You need to obtain the following files (the sizes of the files are listed next to the names): emxdev.zip (620k), gnudev.zip (873k), gppdev.zip (944k), gobjcdev.zip (426k), gnumk362.zip (255k), whlinf8f.zip (814k), and unz50x32.exe (114k). This file (emxst31.doc) is available in emxst31.zip (10k). On ftp-os2.nmsu.edu, these files are available in pub/os2/2.0/programming/emx-0.8f -- except for emxst31.zip, whlinf8f.zip, and the correct version of gnumk362.zip, which are temporarily in pub/uploads. On rusinfo.rus.uni-stuttgart.de, check in pub/os2/emx-0.8f. ═══ 4.1.1.2. How to install it ═══ The following subsections describe how to install the various pieces of the GNU software-development system. Go through the procedures step by step as described -- the order is important. Don't feel compelled to read through any of the readme files or other documentation spit out during the unarchiving process -- I think you will have a much easier time if you go through this document beforehand. ═══ 4.1.1.3. UNZ50X32.EXE ═══ You will need unzip v5.0 to unarchive the files. Earlier versions of unzip (including PKZip versions earlier than 1.9) will not work. To unarchive unz50x32.exe, you simply move it to a convenient directory and type "unz50x32". Add the name of the directory unzip is in to your path. For example, I have unzip in c:\apps\unzip, so I appended "c:\apps\unzip;" to the "SET PATH" statement in my config.sys file. Then reboot. Now, you can unarchive any zip archive by typing "unzip filename", where "filename.zip" is the name of the archive. ═══ 4.1.1.4. WHLINF8F.ZIP ═══ Copy whlinf8f.zip to any convenient directory and unarchive it. It will disgorge a program called "info.exe" (the hypertext reader), several auxiliary files, and a bunch of documentation files for itself, the compiler, the debugger, and make. ═══ 4.1.1.5. GNUMK362.ZIP ═══ Copy gnumk362.zip to any convenient directory and unarchive it. The archive will create the directory ".\make" and disgorge several files into it. In other words, if you unarchive gnumk362.zip in the directory "\apps", the process will create the directory "\apps\make". You can delete the info directory (the one in the make directory) as its contents are duplicated in whlinf8f.zip. Add the name of the directory make is in to your path statement in config.sys. (I have make in c:\apps\make, so I appended "c:\apps\make;" to the "SET PATH" statement in my config.sys.) ═══ 4.1.1.6. GNUDEV.ZIP, EMXDEV.ZIP, GPPDEV.ZIP, and OBJCDEV.ZIP ═══ These archives create the directory ".\emx" and a bunch of directories under that. Thus, unarchive the files from the directory in which you want this emx directory. (In other words, if you unarchive gnudev.zip in the directory "\apps", the process will create \apps\emx, \apps\emx\bin, \apps\emx\lib, and many other such directories.) ═══ 4.1.1.7. The final steps ═══ Now, you need to modify your config.sys file. The examples below are from my config.sys file, and I have the emx directory installed under c:\apps. Thus, my system has the directories "c:\apps\emx\dll", "c:\apps\emx\lib", "c:\apps\emx\include", "c:\apps\emx\bin", and so on -- you will need to modify the following examples so that the directories are specified correctly. First, you need to specify in your libpath the location of emx.dll and other dll files. In my config.sys, I have LIBPATH=.;C:\OS2\DLL;C:\OS2\MDOS;C:\; C:\OS2\APPS\DLL;c:\apps\emx\dll; Second, add the name of the emx\bin directory to your path statement in config.sys. (I appended "c:\apps\emx\bin;" to the "SET PATH" statement in my config.sys.) Third, you need to set a few environmental variables so that the compiler knows where to find and how to use various files: set C_INCLUDE_PATH=c:/apps/emx/include set LIBRARY_PATH=c:/apps/emx/lib set CPLUS_INCLUDE_PATH=C:/apps/emx/include.cpp;C:/apps/emx/include set PROTODIR=c:/apps/emx/include.cpp/gen set OBJC_INCLUDE_PATH=c:/apps/emx/include set TERM=mono set TERMCAP=c:/apps/emx/etc/termcap.dat IMPORTANT: you need to use forward slashes ("/") and not backward slashes ("\") when setting these environmental variables. However, do NOT use forward slashes in your libpath statement. Now, reboot your machine so that these definitions take effect. Then, go into an OS/2 window and type "cd \apps\emx\lib" (or whatever you would type to get to emx\lib), and type "omflib". This completes the installation process. ═══ 4.1.1.8. Using info ═══ Go into the directory in which info.exe resides. Type "info". You are now looking at a screen that has some information on the top half (information such as "Typing 'd' returns here, 'q' quits, '?' lists all info commands, 'h' gives a primer for first-timers . . .") and a list of subjects near the bottom (subjects such as "Info", "Make," "Gcc", "Gdb", etc.). Go ahead and type "h" for a tutorial. The most basic functions to remember are: type "q" to quit, type "?" to get a list of commands, hit the space bar or PgDn key to go down a page, press the Del key or the PgUp key to go up a page, press "n" to go to the next node (think of a node as being a collection of one or more pages dealing with a single topic), press "p" to go to the previous node, use the up and down arrow keys to highlight choices of new nodes to jump to, and press enter to jump to the highlighted node. Just play around with it a little while, and you will get the hang of it. Type "d" to get back to the main directory, use the down arrow to highlight "Gcc", press enter, press the down arrow to highlight "Contributors", press enter, scan through the pages of text by hitting the PgDn key a couple of times, type "?" to see a list of commands, type "p" a couple of times, etc. ═══ 4.1.1.9. Using the compiler ═══ To compile a C source file called "myprog.c", type "gcc -o myprog.exe myprog.c". The -o switch tells gcc that it should call the resulting executable "myprog.exe". To compile the C++ source file "myprog.cc", type "gcc -o myprog.exe myprog.cc -lgpp". C++ source files should have the extension ".cc". The -lgpp switch tells gcc to link the C++ libraries. You can also tell gcc to optimize your code by using the -O switch. There are two levels of optimization (-O and -O2, the highest being -O2). Thus, for the fastest-executing code (at the expense of time to compile and of size of the executable), you would type "gcc -O2 -o myprog.exe myprog.c" for myprog.c and "gcc -O2 -o myprog.exe myprog.cc -lgpp" for myprog.cc. Note: Specifying "-o myprog.exe" is important. If you don't specify ".exe" as the suffix of the output file name, the compiler will generate a UNIX-style executable which will not run under OS/2 even if you subsequently rename the file so that it has a .exe extension. ═══ 4.1.1.10. Using the debugger ═══ To debug a program, you need to compile it with the -g switch: "gcc -g -o myprog.exe myprog.c" for myprog.c and likewise for myprog.cc. After compiling, you then type "gdb myprog.exe" to start the debugger. Type "h" at the debugger prompt to get a list of help topics. gdb supports all sorts of breakpoints (including conditional ones), and you can watch variables, set variables to different values, etc. For example, to get help on running programs from within the gdb, you can type "help running". That will give you a list of commands such as run, kill, step, etc. You can get help on individual commands by typing, for example, "help step". The following is a sample compile and debug session. Go into the emx\test directory and type "gcc -g -o hello.exe hello.cc -lgpp" to compile hello.cc. Type "hello" to run it, to see if the compiler is functioning. The program will print "Hello, world!" on the screen. Now type "gdb hello.exe" to start gdb. At the prompt "(gdb)", type "list main" to list the function "main". Then type "break 5" to cause execution to stop at line 5 in the main function. Type "run" to start execution -- it will stop at line 5. Type "print argc" to see the value of the variable "argc". Type "step" to run line 5 then halt execution at the next line. Type "quit" to quit. To list a function (main(), say) that is longer than a screenful, type "list main"; then type "list" again to list the next screenful of main. ═══ 4.1.1.11. Using make ═══ Assume you have a program made of the following source files: "myprog1.c" and "myprog2.c". You might manually compile these files by typing "gcc -o myprog.exe myprog1.c myprog2.c". Of course, if you change only one of the files, typing such a command causes the recompiling of both source files. During development, this could be tiresome if the files take a long time to compile. A better way would be to type "gcc -c myprog1.c" which compiles myprog1.c into the object file "myprog1.o" (the -c switch tells gcc to make an object file), then to type "gcc -c myprog2.c" to generate myprog2.o, and finally to type "gcc -o myprog.exe myprog1.o myprog2.o". This way, if you change only myprog1.c, you can recompile it and relink it with myprog2.o to create myprog.exe (skipping the "gcc -c myprog2.c" step). This will be much faster if the source files take a long time to compile (or if you have a lot of source files). Of course, doing all this typing is tiresome, too. Also, if myprog1.c happens to depend on myprog1.h, and you change myprog1.h, you must recompile myprog1.c. Thus, you have to keep track of all the file dependencies in order to know, after changing one header file, which other files need to be recompiled. Fortunately, make takes care of all of this automatically. All you have to do is create one text file that describes the various dependencies and the various steps to compile the program. You name the text file "Makefile". From then on, whenever you type "make", make examines the Makefile, looks for files which have changed since the last compile, recompiles any files which depend on the changed files, and relinks everything into a new executable. For example, suppose that myprog.exe is made from myprog1.c and myprog2.c, that myprog1.c contains the lines "#include "myprog1.c"" and "#include "mainhead.h"", and that myprog2.c includes myprog2.h and mainhead.h. The Makefile describing all of this is myprog.exe: myprog1.o myprog2.o gcc -o myprog.exe myprog1.o myprog2.o myprog1.o: myprog1.c myprog1.h mainhead.h gcc -c myprog1.c myprog2.o: myprog2.c myprog2.h mainhead.h gcc -c myprog2.c The first line shows that myprog.exe depends on myprog1.o and myprog2.o. If either of those has changed since the last time make was invoked, make will relink them to create myprog.exe by giving the command under the first line. The fourth line shows that myprog1.o depends on myprog1.c, myprog1.h, and mainhead.h. If any of these three files have changed since the last time make was run, make will recompile myprog1.o by issuing the command on line five. It will also realize that myprog.o has changed, that myprog.exe depends on myprog.o, and will relink myprog.exe. If mainhead.h is changed, make will recompile and relink everything since myprog1.o needs to be changed, myprog2.o needs to be changed, and thus myprog.exe needs to be changed. The example above shows the general form of a Makefile. You give a target (like "myprog.exe" or "myprog1.o") followed by a colon, followed by a space, followed by a space-delimited list of files the target depends on. The next line specifies the action to be taken when any of the dependencies change: the first character MUST be a tab (not just a bunch of spaces used for indentation); then you type the command make should issue. A Makefile is just a list of such targets, dependencies, and actions. ═══ 4.1.1.12. Using the assembler ═══ The compiler provided with this system (gcc) can handle not only C and C++ code but assembly as well. It can also generate assembly language from C or C++ source. To assemble and link assembly-language programs, type "gcc -o myprog.exe myprog.s" where "myprog.exe" is the name of the executable and where "myprog.s" is the name of the source-code file. Assembly-language source-code files should have the extension ".s". To generate assembly from C code, type "gcc -S myprog.c" where "myprog.c" is the name of the C-source-code file -- the resulting assembly language will be put into the file "myprog.s". ═══ 4.1.1.13. Where to get more information ═══ The GNU software-development system does not come with documentation like that you would get with, for example, Borland C++. However, emxdev.doc (in the emx\doc directory) does contain a complete list of library functions, including a list of headers you need to include, what the functions do, and what parameters they accept. Also, since the C compiler is ANSI-C compliant (or at least close to it) and since the C++ compiler is close to AT&T-C++-2.0 compliant, you can use just about any reference manuals for ANSI C and AT&T C++ 2.0. I use the ones I got with an old version of Borland Turbo C++. If you don't have such manuals, you should be able to find something suitable in a bookstore. If you want a C reference manual, I recommend C: A REFERENCE MANUAL, by S. P. Harbison and G. L. Steele, Jr. (Prentice-Hall, 1991). If you are just learning C or C++, there is a large number of books to choose from, and you shouldn't have any trouble finding one that is suitable. For those of you developing applications that use the PM or that use special OS/2 functions, the system DOES support all of the OS/2 API functions, including ones for semaphores, PM programming, named pipes, threads, etc., and it supports Kbd, Mou, and Vio functions. See emxdev.doc (in the emx\doc directory) for a list of the supported functions. The documentation does not contain a manual on how to use these API calls -- you need an OS/2 programming book for that. For information on programming the PM, take a look at: o OS/2 2.0 Presentation Manager GPI: A Programming Guide to Text, Graphics, and Printing, by G. C. E. Winn (Van Norstrand Reinhold, 1992) o Learning to Program OS/2 2.0 Presentation Manager by Example: Putting the Pieces Together, by Stephen Knight (Van Norstrand Reinhold, 1992). o Programming the OS/2 Presentation Manager, by Charles Petzhold (Microsoft Press, 1989). The book by Petzhold was written for OS/2 1.3, but the information in it is still valid. You can also get the IBM redbooks (which are quite economical and of which there is a large assortment of titles). For using assembly language, the best choice would be a book about assembly-language programming in OS/2, supplemented perhaps with a book on programming the 80386 or 80486. Also, way back when you were unarchiving, you might have been itching to examine the various readme files and other documentation. Now is the time to do that to your heart's content. Browse through the files in the emx/doc directory and the information available from the hypertext reader. Additional sources of information include GEnie (the OS/2 category of the IBMPC bulletin board), Usenet news (the comp.os.os2 newsgroups), and CompuServe. These are places where you can exchange information with other people who use and program for OS/2. In particular, CompuServe is one of the official homes for the OS/2 developers' assistance program. If you are a member of the program, IBM will (for only $15) provide you with a CD that contains a beta version of the software development kit (including the C and C++ compiler and debugger and a full set of on-line documentation), a beta version of OS/2 (which contains enhancements such as Windows 3.1 compatibility), and many other goodies. For more information, type "go os2dap" on CompuServe or call 1-407-982-6408. The people at 1-800-3-ibm-os2 might also be able to provide more information. ═══ 4.1.1.14. Conclusions ═══ I wrote this to help people get started with a free -- yet powerful -- 32-bit software-development system for OS/2. For (at most) the price of a few books, you have a full programming system for C++, C, and assembly language. For the additional price of an OS/2 programming book, you have a bargain-basement SDK. If you find errors in this document, or if you have suggestions for its improvement, please let me know. My GEnie address is "BROOKE", and my Internet address is "brooke@hope.caltech.edu". ═══ 4.1.1.15. Optional Packages ═══ There are three optional packages you can get for this software-development system, packages which are not necessary but which can nevertheless be important. All of them are in the pub/os2/2.0/programming/emx-0.8f directory on ftp-os2.nmsu.edu. On rusinfo.rus.uni-stuttgart.de, they are probably available in a directory such as pub/os2/emx-0.8f. The first package contains curses. Curses is a library of functions that allow you to move the cursor around on the screen, manipulate screenfuls of text, and get input. You need the files "bsddev.zip" and "bsddoc.zip". The source code is available in "bsdsrc.zip". The second package contains full source code to the software-development system. Most people do not need the source code to everything, but the source code to the libraries (i.e., to all the functions) is sometimes useful. The source code to the C libraries is in "emxlib.zip", and the source to the C++ libraries is in "gccsrc.zip". install.doc (in the emx\doc directory) gives the names of all the other relevant archives. The third package is a collection of sample and test programs that you can use to test the compiler and to learn about various aspects of programming. These programs are in the file "emxtest.zip." ═══ 4.1.1.16. Sources of Distribution ═══ This document already described some places from which you can get the necessary archives: ftp-os2.nmsu.edu and rusinfo.rus.uni-stuttgart.de. However, some people don't have access to these sites or don't have modems fast enough to download megabytes of data in a reasonable amount of time. For these people, I am including the following list of people who are willing to distribute the whole system through regular mail. Keep in mind that people might change their prices, cease distributing the software, move, etc., so contact them first to get current details. Make sure you ask them if they have the latest version of the EMX port (which is currently version 0.8f). Brooke Anderson 1155 E. Del Mar #312 Pasadena, CA 91106 USA Phone: (818) 577-7555 GEnie: BROOKE Internet: brooke@hope.caltech.edu Cost: $18 in US; in other countries, shipping + US$10 Provides: the basic set of archives and bsddev.zip, bsddoc.zip, bsdsrc.zip, emxtest.zip, emxlib.zip, and gccsrc.zip. Juergen Egeling Werderstr. 41 7500 Karlsruhe Germany Phone: 0721-373842 FAX: 0721-373842 BITNET: ry90@dkauni2 Internet: ry90@ibm3090.rz.uni-karlsruhe.dbp.de X.400: S=ry90;OU=ibm3090;OU=rz;P=uni-karlsruhe;A=dbp;C=de Cost: disks + shipping + DM 25 Wey J. Ho Department of Physics Monash University Clayton VIC 3168 Australia Phone: +613-565-3615 (or Australia (03) 565 3615) Fax: +613-565-3637 (or Australia (03) 565 3637) Internet: sci240s@monu6.cc.monash.edu.au Cost: disks + shipping + AU$10 Doug Robison 1311 Webster Chillicothe, MO 64601 USA Phone: (816) 646-1085 GEnie: D.ROBISON Cost: disks + shipping + US$5 If you would like to get on this list and if you have access to Internet or an on-line service, just send me your name, a description of how people can contact you (including your e-mail address), how much money you want for the job (such as "$20", "disks + shipping + $30", or whatever you want to charge), and what you are offering for that price. It is helpful to have a list that includes people in various countries. ═══ 4.1.1.17. Distributing this Document ═══ I give permission to use, to distribute, and to copy this document freely. If you want to upload it to any bulletin-board or on-line service, please do so. I do update this document occasionally (and put the latest version on GEnie and ftp-os2.nmsu.edu), so you might want to make sure you have the latest version before distributing it. Brooke Anderson 1155 E. Del Mar #312 Pasadena, CA 91106 USA Phone: (818) 577-7555 GEnie: BROOKE Internet: brooke@hope.caltech.edu ═══ 4.2. The Making of MineSweeper ═══ The Making of MineSweeper ═══ 4.2.1. Introduction ═══ This document and its contents are copyright (C) 1993, by David Charlap. This document describes some of the design decisions and problems that I encountered when making my MineSweeper for OS/2 program. It assumes a basic knowledge of C and Presentation Manager. I decided to write an OS/2 version of MineSweeper for many various reasons. The main reason was to learn a bit more about programming in the OS/2 presentation manager environment. I also wanted an OS/2 version of this game, since loading the Windows environment is a slow procedure. Although there is already a version of MineSweeper for OS/2 available, I found it to be lacking features of the Microsoft game that I wanted. I chose to develop MineSweeper using the GCC/2 compiler, since I already had it installed on my computer at home. The code I have written should compile without change on IBM's C Set/2 compiler or on the EMX/GCC compiler or on any other OS/2-compatible compiler. ═══ 4.2.2. The game ═══ Before discussing the design, allow me to explain the basic concept of the game. MineSweeper is a game of logic, pitting the player against a clock. A grid of squares is presented, some of which contain mines. All the squares are covered at the game's outset. The object is to discover which covered squares contain mines under them using clues provided by the program. A player uncovers a square by clicking on it with the mouse. If the square contains a mine, the player loses and play stops. If it does not contain a mine, a number is displayed on the square which indicates the number of mines that are adjacent to that square. When all the squares that do not contain mines are uncovered, the player wins and play stops. To prevent mistakes, a player may place a flag on a covered square to indicate a suspected mine position. The computer will not allow a flagged square to be uncovered. The player may also place a question mark on a square to aid in visualizing the problem. The computer will allow a question-marked square to be uncovered. The game is timed. ═══ 4.2.3. The implementation ═══ While there are many ways to implement such a game, I chose to copy the implementation that Microsoft used for their game. Partly because I was already familiar with it, and partly because it is an intuitive approach to the game. ═══ 4.2.3.1. How the game should behave ═══ The game is played in a window whose size is directly proportional to the size of the game grid. When the grid's dimensions are changed, the window is re-sized to accomodate the new grid. The top of the window contains the current elapsed time, a count of the number of mines left to be flagged, and a button that may be clicked for a quick restart of the game. Below this is the game grid. Covered squares are drawn in a way to resemble buttons. A user uncovers a square by clicking on it with mouse button 1. When the button is depressed over a covered square, the square is drawn as a depressed button. If the mouse is dragged over the game grid, the "depressed" button will follow the pointer. When the button is released, the square beneath it will be uncovered. If an uncovered square has no adjacent mines, the program will automatically uncover all squares adjacent to it. Flags and question marks are placed by clicking on covered squares with mouse button 2. The marks are placed as soon as the button is pressed. As a shortcut, and an aid to solving, mouse button 3 is used to uncover all the squares around an uncovered square. When button 3 is clicked on an uncovered square, and there are sufficient flags surrounding the square to satisfy the number shown, all remaining squares will be uncovered. For two-button mouse users, pressing buttons 1 and 2 together (chord) will also cause this effect. When the shortcut button is pressed, the squares surrounding the pointer will be drawn in the depressed position. As the mouse is dragged, the 3x3 area of depressed squares will track the mouse. When button 3 (or either button 1 or 2, if the chord is used) is released, the shortcut process will begin. ═══ 4.2.3.2. How to code this ═══ Coding this game play was not simple. My first attempt involved creating separate buttons for each square on the playfield. This way, the user could just click on a button, and the program would receive a WM_COMMAND message indicating which button was clicked. This proved difficult for many reasons. First, it is hard to get a bitmap image on the face of a button. Although I have seen it done, I do not know how, and I did not want to take the time to learn. Second, and more important, buttons to not behave properly. Ordinary buttons revert to their "up" state if the mouse moves off of them while the mouse button is depressed, but there is no facility provided to then depress the adjacent button that the pointer is subsequently above. Also, facilities are not provided to have more than one button draw itself on the "down" state when one is clicked on. In other words, getting the depressed button images to follow the mouse as it is dragged across the playfield is difficult to do, if not impossible, using button windows. Instead, I decided to fake it. I would have multiple bitmaps in memory. One for each possible image that could be drawn on a grid square. I would then just draw the bitmaps on the squares. If the bitmaps are properly drawn, they should look just like buttons. For making the buttons depress, I would simply have additional bitmaps and draw them where appropriate. There are only a few downsides to this approach. First of all, bitmaps can not be stretched without introducing distortions. This means that whatever size I pick for them is going to be used throughout the game, regardless of user preferences. Second, bitmaps are difficult to re-color when loaded out of resource files, so the colors will not necessarily match the system-defined button colors. But these are minor concerns, I feel, given the difficulty of implementing the game in any other way. ═══ 4.2.3.2.1. The basic internal structure ═══ Many variables are needed to keep track of the game. I chose to make most of the critical game variables global, so that they can be accessed from any procedure without large amounts of parameter passing. While this may be poor programming practice, it makes a game like this much easier to write and understand, in my opinion. In addition to the expected variables, like the size of the grid and the number of mines, three arrays are used. The arrays are dynamically allocated and re-allocated whenever the game grid changes size. All three arrays correspond to the game grid, one element corresponding to each square. One array is boolean, containing the map of mines - TRUE if the square has a mine, FALSE otherwise. The second array contains the visible screen. Each element contains a number, indicating what the user sees at that coordinate. This array is initialized to values corresponding to a blank raised button, but will change as play progresses. This is used primarily for displaying the screen. The third array contains the count of mines adjacent to each square. Rather than compute the number each time a square is uncovered, the entire set is computed in advance. This way, it's a quick operation to get the correct number when a square is uncovered. Other game variables include an array of bitmap handles for the drawing procedures, flags to an end-of-game condition, and the current timer count. ═══ 4.2.3.2.2. Using bitmaps as buttons ═══ To implement the game grid, 18 bitmaps would be needed. I would need a blank square, squares with the numbers 1 through 8 on them, a "button" in the raised and depressed state, a "button" with a flag on it, a "button" with a question mark on it in the raised and depressed state, and bitmaps to indicate errors: an exploded mine, a misplaced flag, and an unflagged mine. All these bitmaps were drawn (on 16x16 16 color bitmaps) using the system-supplied icon editor. These were then all included in the resource file with lines like the following in mine.rc: BITMAP MINE_BLANK_UP MineBlankUp.BMP BITMAP MINE_BLANK_DOWN MineBlankDown.BMP BITMAP MINE_FLAG MineFlag.BMP BITMAP MINE_QUESTION_UP MineQuestionUp.BMP Where names like MINE_BLANK_UP are constants defined in the program's header file. Using bitmaps from a program's resources is fairly simple. First, a presentation space is required for drawing into. While a WinBeginPaint call will create a presentation space, I want to be able to draw bitmaps at times other than during WM_PAINT message processing. For this reason, I create a presentation space during processing of the WM_CREATE message and store its handle in a global variable. A presentation space may be created outside of paint message processing in the following way: hdc = WinOpenWindowDC(hwnd); sizl.cx = sizl.cy = 0; hps = GpiCreatePS(hab, hdc, &sizl, PU_PELS | GPIF_DEFAULT | GPIT_MICRO | GPIA_ASSOC ); Where hwnd is the window handle passed as part of the WM_CREATE message, sizl is a variable of type SIZEL, and hab is the anchor block created as part of the program's WinInitialize function. The value of hps returned is used for all painting operations throughout the program. It is also passed to WinBeginPaint during WM_PAINT message processing to prevent spurious presentation spaces from being created. Once the presentation space exists, the bitmaps are all loaded from the program's resources with GpiLoadBitmap calls during the WM_CREATE message processing: hbitmap=GpiLoadBitmap(hps, NULLHANDLE, ID, BOX_WIDTH, BOX_HEIGHT); Where ID is the resource-ID for the bitmap, as defined in the resource file, and BOX_WIDTH and BOX_HEIGHT are constants indicating the size of the bitmap. If the height and width of the bitmaps do not match the constants provided, they will be stretched to fit the dimensions requested. The bitmap handles returned in hbitmap are all stored so that they do not need to be loaded again. Once handles are available for the bitmaps, displaying them is simple, using the WinDrawBitmap call: WinDrawbitmap (hps, hbitmap, NULL, &ptl, CLR_NEUTRAL, CLR_BACKGROUND, DBM_NORMAL); Where hbitmap is the handle of the bitmap requested, and ptl is a POINTL structure containing the window-relative coordinate of the lower-left corner of the bitmap's target position. The only other thing to consider is cleaning up when the program terminates. Although presentation spaces and handles will be released back to the system when the WinTerminate call is placed, it is good practice to release them manually, just to be sure nothing is left behind by accident. (Bugs in new systems, like OS/2 can cause this to happen, so it's better safe than sorry.) I place all cleanup instructions in the WM_DESTROY message handler. The following statements will delete bitmap handles and presenation spaces: GpiDeleteBitmap(hbitmap); GpiDestroyPS(hps); Where hbitmap is a valid bitmap handle and hps is the presentation space created earlier. The GpiDeleteBitmap function should be called repeatedly, once for each bitmap handle used in the program. ═══ 4.2.3.2.3. Using the mouse ═══ Another significant problem of implementing the desired interface is that of getting a depressed "button" to track the mouse. For example, if the pointer is moved from one square to another while button 1 is pressed, the depressed button image on the screen should follow it. For reasons outlined previously, normal buttons won't provide this effect. Instead, using bitmaps, the entire effect must be simulated by handling appropriate messages. To properly implement button 1's behavior, we must handle the following messages: WM_BUTTON1DOWN, WM_BUTTON1UP, and WM_MOUSEMOVE. WM_BUTTON1DOWN is generated whenever mouse button 1 is pressed. This may be the left or right button, depending on whether OS/2 is configured for left-handed mouse operation or not. Since we want the left-right buttons to swap when in left-handed operating mode anyway, we can simply deal with button 1 and not concern ourselves with left or right. The WM_BUTTON1DOWN message provides the coordinates (window-relative) that the pointer is over when the button was pressed. These are delivered as the high and low words of the first message parameter and may be extracted with the following macros: x = SHORT1FROMMP(mp1); y = SHORT2FROMMP(mp1); Before taking action, we must be sure these coordinates are valid. Any time the button is pressed while the pointer is within the window's client area will cause a WM_BUTTON1DOWN message to be sent, and these coordiantes may not be over a segment of the game grid. ;p.If the coordinates are invalid, we simply break from the switch statement, and allow the WinDefWindowProc to handle the message. If the coordinate is valid, we then compute which grid square the coordinate is over. Since the grid is composed of same-sized rectangular regions, we have simply to subtract any margins and divide the coordinate by the width (or height) of a square to get the identity of the square. Once the square is identified, we re-draw it in its depressed form. In general, the depressed form of a square is identical to it's non-depressed form. The exceptions are the blank covered square and the question-mark covered square. These are depressable "buttons", and have different bitmaps for the depressed state and the raised state. A few other steps must also be taken, aside from drawing the depressed button. We must set a flag in a static variable indicating that the mouse button is down, so that the WM_MOUSEMOVE message handler will know the mouse button's position. We must also store the grid coordiante of the depressed button. Finally, we must capture the mouse. When the mouse is captured, the program will get all mouse messages, even if the pointer is over some other application's space. This is necessary, since the user may drag the pointer outside of our window and release the button there. If this happens, and the mouse isn't captured, the program will not receive the WM_BUTTON1UP message, and would think that the button is still down. The mouse is captured and released with the following functions: WinSetCapture (HWND_DESKTOP, hwnd); /* To capture the mouse */ WinSetCapture (HWND_DESKTOP, NULLHANDLE); /* To release the capture */ Where hwnd is the window handle provided to the message handler. The WM_BUTTON1UP message is returned when the user releases mouse button 1. Compared to the button down handler, this is simple. First, the capture on the mouse is released. Then, if the coordinate is over a covered-but-not-flagged square, we call the uncover function. If this is the first click of the game, then the timer is started. The WM_MOUSEMOVE message is sent to the application whenever the pointer position changes over the window. Normally, we will ignore these, passing them to the system's handler, WinDefWindowProc, but if button 1 is down, we want to take action first. First, we must check if the pointer's position has moved to another square. If it is over the same square as our saved position, then we do nothing. If it is not over the same square, then we have to draw the previous square in it's raised position, and draw the square it's now over in the depressed position and save the new coordinate. Managing the mouse button 2 is simpler, since flags and question marks are placed as soon as the button is depressed. When the WM_BUTTON2DOWN message is received and the pointer is over an appropriate square, the array element corresponding to the square is changed and the square is re-drawn. Managing the chord and button-3 sequences are similar to the button 1, except that nine boxes must now be drawn in the depressed state instead of one. This only gets a little ugly when the button 1 and button 2 messages must do double-duty to manage the chord sequence as well as their individual functions, but this is managed easilly with some well-placed if() statements. ═══ 4.2.3.2.4. Uncovering squares ═══ Uncoverig a square seems like a simple procedure, but introduces interesting problems. The concept is simple. The program checks if the square is uncoverable and aborts if it is not. It then checks if a mine is being uncovered and ends the game if it is. Then it does the actual uncovering, ending the game if the player wins. In actuality, it's more complicated than this. As a courtesey, the program should never allow the user to uncover a mine on the first click, so if a mine is uncovered on the first click, the program should move that mine elsewhere. Also, if a square has no mines adjacent to it, the program should automatically uncover all the surrounding squares. Step one is simple. Check if the square is covered and does not have a flag on it. If not, just return and do nothing. There should also be some sanity checking here, in case the program tries (in error) to uncover a square that isn't on the grid. For step 2, the program must check if the square has a mine under it. If so, then the program must check if it is the first click. Moving the mine is tricky - the program must place a new mine, delete the old mine, and adjust the adjacency counters so all the numbers will still read proper values. Finally, it must then loop back to the start and retry uncovering the square. If it's not the first click, the finding a mine is game over. The timer is stopped and the endgame flag is set, which effectively prevents further play, since all of the mouse button message handlers check if the game is over as a part of their processing. If the square does not have a mine, however, it must be uncovered. In which case, the count of adjacent mines is fetched. If there are one or more adjacent mines, the bitmap for that number is assigned to that square, and it is displayed. If the uncovered square has zero adjacent mines, all the adjacent squares must be uncovered. At first, I simply had the uncover procedure call itself recursively, but I found that very large amounts of empty space would cause stack overflow problems, so I had to use an iterative solution, instead. I have a separate procedure to handle this, now. To iteratively calculate which squares to uncover, the program loops through all the squares. If it finds a covered square that is adjacent to an uncovered square with no adjacent mines, it sets the square to its uncovered state without calling the uncover procedure. It loops through the grid repeatedly until no changes are made to the grid. The program maintains a count of covered squares at all times. The count is initialized to the number of squares in the grid and is decremented whenever a square is uncovered. When the count of covered squares equals the number of mines, a win situation is declared. When the player wins, the timer is stopped, the endgame flag is set, and (if one of the three standard games is chosen) the score is compared to the high score, and if the high score is beaten, the score is recorded and the player may enter his name. ═══ 4.2.3.2.5. Saving and restoring settings ═══ MineSweeper saves all critical game settings to an initialization file (MINE.INI) when it terminates and restores these settings from this file when restarting. The window's position, the high scores, and the game settings are among the items saved. This is all done using OS/2's profile management system. The profile management system is a set of PM calls (all beginning with Prf) that manage INI files. An INI file is opened by calling PrfOpenProfile, closed by calling PrfCloseProfile, and is read from and written to with other API calls. The calls I used, and the syntax for them are: hini = PrfOpenProfile (hab, "MINE.INI"); PrfCloseProfile(hini); PrfWriteProfileData(hini, pszApp, pszKey, &data, sizeof(data)); PrfWriteProfileString(hini, pszApp, pszKey, pszString); PrfQueryProfileData(hini, pszApp, pszKey, &buf, &buflen); PrfQueryProfileSize(hini, pszApp, pszKey, &buflen); PrfQueryProfileString(hini, pszApp, , pszKey, NULL, pszString, buflen); PrfOpenProfile takes two arguments. The first is the anchor block that is created when WinInitialize is called. The second is the file name of the INI file. A variable of type HINI is returned. This HINI variable is used to reference the INI file for reading and writing. Two preset HINI handles are also available: HINI_USERPROFILE and HINI_SYSTEMPROFILE may be used to access the user and system INI files, which are normally OS2.INI and OS2SYS.INI. I chose not to use these files, however, since software is required to delete entries from INI files. By keeping data in a separate initialization file, a user may erase all settings by simply deleting the MINE.INI file. PrfCloseProfile takes one argument: the HINI variable returned by the PrfOpenProfile call. It closes the profile. The user and system INI files are not closeable. Data in an ini file is referenced by two keys: and application name and a key name. These two keys, together, are used to reference arbitrary-sized blocks of data in the INI file. These blocks may be either strings or binary data. Almost all functions that read or write an INI file take the same first three paramters. The first being a valid HINI ini handle, the second being a string containing the application name, and the third being the key name. PrfWriteProfileData is used to write binary data to an INI file. It takes five parameters. The first three are the standard handle, app name and key name. The fourth is a pointer to the data, and the fifth is the length of the data. PrfWriteProfileString is used to write null-terminated string data to an INI file. It takes four paramters. The first three are the standard three, and the fourth is the string. PrfQueryProfileData is used to read binary data from an INI file. It takes five paramters. The first three are the standard three. The fourth parameter is a pointer to a buffer area to hold the data, and the fifth is a pointer to a long variable containing the size of the buffer. When the call completes, this variable will contain the actual number of bytes transferred. PrfQueryProfileString is used to read string data from an INI file. It takes six paramters. The first three are the standar three. The fourth is a default string that will be supplied if the key can not be found in the INI file; I leave this parameter as a NULL, indicating that I don't want a default string. The fifth paramter is a pointer to the buffer that will contain the string, and the sixth paramter is the maximum string length. The call will return the actual number of bytes transfered into the buffer. Finally, since I am dynamically allocating storage for these strings, I must find out the length of these strings before I actually read them in, in order to allocate a large enough buffer first. This is done with the PrfQueryProfileSize API call. PrfQueryProfileSize takes four parameters. The first three are the usual three, and the fourth is a pointer to a ULONG variable. This variable will contain the number of bytes that the referenced data block contains. ═══ 4.2.3.2.6. Sizing the window ═══ In MineSweeper, the mines are always the same size. And I do not want any margins around the minefield. This means that the window must be re-sized to fit the minefield. This is a two step process. First, the proper size must be calculated, then the window must be re-sized to fit. Calculating the window size is trivial, but not immediately obvious. Obviously, the width must be greater than the width of one row of squares, and the height must be greater than the height of one columns of squares. But this is not enough. The size of the window includes ALL of the window, including borders, menus, and title bars. So, the border width must by qyeried from the system and added to the width estimate. And the menu bar height, the title bar height, and the border height must also be queried and added to the height estimates. These values can be extracted with the WinQuerySysValue function: menuHeight = WinQuerySysValue(HWND_DESKTOP, SV_CYMENU); captionHeight = WinQuerySysValue(HWND_DESKTOP, SV_CYTITLEBAR); borderHeight = WinQuerySysValue(HWND_DESKTOP, SV_CYBORDER) * 2; borderWidth = WinQuerySysValue(HWND_DESKTOP, SV_CXBORDER) * 2; WinQuerySysValue takes two parameters. The first is a valid desktop handle, which (under OS/2 version 2.0) is always HWND_DESKTOP, and the second is a constant indicating which value to extract. The following were used: SV_CYMENU The minimum height for a menu bar. If the menu font is too large, a menu bar will actually become larger than this height. Additionally, if a menu bar is too long for the window, and wraps onto two or more lines, this value will only be the height of one line. In other words, it's not really very accurate. SV_CYTITLEBAR The height of a titlebar SV_CYBORDER The height of a thin border. Multiply this by two to get both borders. SV_CXBORDER The width of a thin border. Multiply this by two to get both borders. There are many other system values, but these are the only ones I needed. The actual width of the window is the width of the window's contents plus the border width. Unfortunately, the ambiguities of the menu bar's height do not make it that simple to generate the window's height. While there may be better ways to calculate the menu bar's actual height, I chose a quick-and-dirty approach. I first make a best guess of the window's height, by adding the system height values to the height of the playfield (consisting of the game grid and the score region). I then set the window to that size with the WinSetWindowPos command. After that I use the WinQueryWindowRect command to get the actual size of the menu bar. This works, because everything in OS/2 is a window - I get the window handle for the menu, and then query the window's size - giving me the size of the menu. Using this size, I re-calculate the height of the window and re-size it if it has changed. This all happens quickly, and no painting occurs during processing, so the user does not see the window change sizes. The window's size and position is set with the following call: WinSetWindowPos(hwnd, HWND_TOP, x, y, cx, cy, SWP_SIZE | SWP_MOVE | SWP_ZORDER | SWP_SHOW | SWP_ACTIVATE); Where hwnd is the window handle. The second parameter is for placing the window in the stack of open windows; HWND_TOP is a constant that tells the system to put this window above all the others. x, and y are coordinates for the lower-left hand corner of the window. cx, and cy are the horizontal and vertical sizes of the window. The last parameter is a set of flags. The ones presented here tell OS/2 to re-size the window, re-position it, change it's "stack" position, make it visible, and give it focus, respectively. The menu bar's size is fetched with the following calls: hwndMenu = WinWindowFromID(hwndFrame, FID_MENU); WinQueryWindowRect(hwndMenu, &rcl); Where hwndMenu is the window handle of the menu bar, hwndFrame is the window handle of the frame window that owns the menu bar, and rcl is a RECLT structure that contains the dimensions of the window. WinWindowFromID is an API call that returns the window handle of a child window. In this case, I want to know the handle of a menu-bar window, but I only know the handle of the frame window. So I use WinWindowFromID to extract the handle. It takes two parameters. The first is the handle of the parent window, and the second is the ID of the child. Menu bars that are created as part of a standard window always have an ID of FID_MENU. WinQueryWindowRect fills in a RECTL structure with the extents of a window. Since the coordinates are window-relative, the bottom and left extents are always 0 and 0, leaving the other two extents containing the width and height of the window. ═══ 4.3. The Unofficial Guide to the Palette Manager ═══ The Unofficial Guide to the Palette Manager ═══ 4.3.1. The Guide ═══ IMPORTANT NOTE: If you aren't going to read this whole article, at the very least please read about the FATAL ET4000 bug (search for FATAL ET4000 BUG if you're reading this as ASCII). Credit where it's due: When Windows 3.0 first came out with its Palette Manager in 256-color mode, I fell in love. Finally, a GUI that understood palettes and "did the right thing" when two programs tried to use the palette simultaneously. I was elated to find out that when OS/2 2.0 came out, it would also have a Palette Manager. My elation turned to disgust when I discovered the following paragraph in the READ.ME file for the OS/2 Programmer's Toolkit: Palette Manager functions are available, but no devices currently allow the physical palette to be changed. This means applications attempting to put customized colors into the palette will get the nearest colors from the default palette. As far as I'm concerned, this is tantamount to saying, "You can do anything you want ... as long as you don't want to do anything!" Fortunately, when the Service Pack came out later in 1992, the 32-bit XGA, Trident, and ET4000 drivers all included good Palette Manager support (though with a few bugs, one of them serious). Sadly, as of this writing, I believe that people with other 256-color displays (such as 8514/A's and clones) still don't have decent Palette Manager support. So what's Palette Manager, and why should you care? VGA-standard adapters can display any of 262,144 different colors. Unfortunately, you can't display them all at once; you can only see 256 at a time. Fortunately, you can pick any 256 of these colors at once (unlike the early CGA, where you had a choice of two sets of four fixed colors). The 256 colors that your adapter is showing at any time make up your physical palette. Under Presentation Manager, your program creates a logical palette. For example, you may want eight shades of red. You ask Palette Manager for eight colors, specifying the RGB values that you want. Palette Manager then gives you eight spaces in the physical palette and sets them to the RGB values you specified. If it runs out of space in the physical palette, then it looks at the colors it couldn't get for you and returns the colors in the physical palette that come closest to matching them. In either case, you don't have to worry about which spots in the physical palette you got; you just draw lines in color 3 (for instance), confident that they will be medium red. (At this point, some of you might say, "Couldn't you do this with a Logical Color Table?" I think the answer is yes [I've never used LCTs myself], but I think Palette Manager is easier to use and more flexible. Also, I'm sort of getting the impression that LCTs are being phased out.) There are other things you can do with Palette Manager. Broadly speaking, there are four different types of tasks you can carry out using Palette Manager: o Examining (but not changing) the physical palette o Creating a logical palette for drawing (e.g., using GpiBox) o Creating a logical palette for displaying a bitmap with special color needs o Creating and animating a logical palette In an ideal world, the third task would just be a special case of the second. However, bugs in the Palette Manager require you to do a few special things to get it to work, so I'll treat it separately. My program PHYSCOLO demonstrates the first task, SCALE demonstrates the second, GRAYBITM demonstrates the third, and ZOOM demonstrates the fourth. Although these programs do very different things, they have many similarities. They (and any other program that uses Palette Manager) carry out the following steps: 1. Make sure Palette Manager is available. 2. Create a PS (and DC) 3. Create the logical palette 4. Select the palette into the PS 5. Realize the palette 6. Use the palette 7. Clean up by deselecting the palette, destroying it, and destroying the PS. Let's look at how we carry out these tasks. At this point, I strongly suggest that you follow along with a copy of PHYSCOLO.C. Either print it out or open it up in another window. STEP ONE: Make sure Palette Manager is available. In the "main" part of PHYSCOLO, you'll find the following block: { LONG sColors; HDC hdcScr; hdcScr = GpiQueryDevice (WinGetPS (HWND_DESKTOP)); DevQueryCaps (hdcScr, CAPS_ADDITIONAL_GRAPHICS, 1, &hasPalMan); hasPalMan &= CAPS_PALETTE_MANAGER; ... } (There are two more lines in the actual listing that check how many colors are available on your computer. While all existing drivers with Palette Manager support have exactly 256 colors, this could change in the future.) After running this block, hasPalMan will be zero if the computer doesn't support Palette Manager. PHYSCOLO just keeps going, but it might be more appropriate to give an error message and exit. (See ZOOM, for instance.) So if hasPalMan is nonzero, are you okay? Not necessarily. Remember the OS/2 Toolkit READ.ME: OS/2 may report that you have Palette Manager support even if the device won't allow your physical palette to change. Unfortunately, I don't know any way to tell if you "really" have Palette Manager support; if anyone knows, I'd like to hear from them. STEP TWO: Create a PS (and DC) Here's the First Commandment of using Palette Manager: IF YOU'RE GOING TO USE PALETTES, !!DON'T!! USE A CACHED MICRO-PS! If someone had told me this, it would have saved me a couple of weeks. If you only learn two things from this article, learn this and the Fatal ET4000 bug (below). In other words, don't do the following when handling WM_PAINT: case WM_PAINT: { HPS hps; hps = WinBeginPaint (...); ... } Why? When you create a cached micro-PS, it "forgets" everything between WM_PAINT messages. While that's fine for simple graphics, it's not appropriate when you're using a palette, because the cached micro-PS will forget the palette! Instead, we create an uncached micro-PS. Looking at the "ClientWinProc" procedure in PHYSCOLO, we see the following lines: static HPS hps; static HDC hdc; (These are static, of course, since they have to be remembered between calls to ClientWinProc.) Later on, we see the rest of the lines for creating the PS: case WM_CREATE: { SIZEL sizl; sizl.cx = sizl.cy = 0; hdc = WinOpenWindowDC (hwnd); hps = GpiCreatePS (hab, hdc, &sizl, PU_PELS | GPIF_DEFAULT | GPIT_MICRO | GPIA_ASSOC); } STEP THREE: Create the Palette This step (and Step Five) will differ depending on which task you carry out (e.g., whether the logical palette is for examining the physical palette or for palette animation). In the case of PHYSCOLO, we do the following: static HPAL hpal; ... WM_CREATE: { ... ULONG tbl [MAX_PAL_SIZE]; INT j; for (j = 0; j < colorsToShow; j++) { tbl [j] = PC_EXPLICIT * 16777216 + j; } hpal = GpiCreatePalette (hab, 0L, LCOLF_CONSECRGB, colorsToShow, tbl); } Four things worth pointing out: 1. Ordinarily, it's a good idea to sort the palette, with the most important RGB values closer to the beginning of tbl. Here, since PHYSCOLO merely examines the physical palette, we don't sort entries. 2. The PC_EXPLICIT flag in each tbl entry means, "This number represents an entry in the physical palette, and NOT a color." So, for instance, PC_EXPLICIT * 16777216 + 3 means the third entry in the physical palette. 3. The LCOLF_CONSECRGB flag means that tbl is an array of RGB values, each of which is 4 bytes long (which is why we can "fake" it with ULONGs). Currently, you must use this flag for all calls to GpiCreatePalette (and your table must be an array of RGBs / ULONGs). 4. The 0L parameter is appropriate for PHYSCOLO, since all we're doing is examining the physical palette. Later, I'll try to convince you that it's appropriate for every other use of GpiCreatePalette, too. STEP FOUR: Select the Palette into the PS. This is easy: GpiSelectPalette (hps, hpal) STEP FIVE: Realize the Palette. You realize the palette when your program changes its logical palette or in response to OS/2's new WM_REALIZEPALETTE message. From what I've seen, WM_REALIZEPALETTE is sent to a Presentation Manager program when that program moves into the foreground, or when some other program changes an (unreserved) palette entry. (Reserved palette entries are ones used for palette animation; see below.) If your program doesn't start in the foreground, it should realize the logical palette before it first draws using that palette. Otherwise, the client area will be drawn in black. That's what the STATIC BOOL firstPaint = TRUE; ... if (hasPalMan && firstPaint { ... WinRealizePalette (hwnd, hps, &palSize); } stuff in the WM_PAINT case is for. The parameters to WinRealizePalette are straightforward, though I wish I understood why it's necessary to pass a pointer to the palette size rather than the palette size itself. The other time PHYSCOLO calls WinRealizePalette is when handling WM_REALIZEPALETTE. When PHYSCOLO receives a WM_REALIZEPALETTE message, it merely calls WinRealizePalette and ignores the return value. This is a little atypical, as I'll explain below. STEP SIX: Use the palette. This is easy. Looking at PHYSCOLO's listing, we see the line GpiSetColor (hps, j); What this means is, use the color of the j-th logical palette entry for your next drawing operation. That's all there is to it. STEP SEVEN: Clean up by deselecting the palette, destroying it, and destroying the PS. When you're completely finished with your palette, you should deselect it and destroy it. When you're completely finished with your PS, you should destroy it. Usually, this will be when your program is ending, which is why all these tasks are handled in the WM_DESTROY code for PHYSCOLO: if (hasPalMan) { GpiSelectPalette (hps, NULLHANDLE); GpiDestroyPalette (hpal); } GpiDestroyPS (hps); This should be completely straightforward. Be sure you get rid of every palette and PS when you're done with it. OTHER USES OF PALETTE MANAGER We've gone through PHYSCOLO and seen how to examine the physical palette. What changes do we have to make to create a custom logical palette for drawing, or for displaying a bitmap, or for animating a palette? The answer is, not many. Most of the changes are in Steps Three and Five. SCALE This program draws a grayscale, or a redscale, or a cyanscale, or .... See SCALE.DOC for details. If we consider how it uses the Palette Manager, SCALE isn't really all that different from PHYSCOLO. It does Steps One and Two (checking for the Palette Manager and creating the PS and DC) the same way that PHYSCOLO does. When we reach Step Three, things are slightly different. In SCALE, the entries in tbl represent RGB triples, and they take the form: tbl [j] = red * 65536 + green * 256 + blue * 1; where "red," "green," and "blue" are the RGB values for the color you desire in logical palette entry j. These values range from 0 (meaning none of that primary) to 255 (meaning the maximum amount of that primary). Here's an example: Bright cyan is formed by mixing the maximum amount of green with the maximum amount of blue. Numerically, this would be 255 * 256 + 255 * 1, or 65535. So, for example, if tbl[3] had a value of 65535, this would mean that the third entry in the logical palette would be bright cyan. (We don't use the PC_EXPLICIT flag since we want to create colors, rather than examine physical palette entries.) Note that I still use 0L in the call to GpiCreatePalette. Why? Well, there are two possible alternative options (which can even be OR'd together): LCOL_PURECOLOR. According to the online docs (in the OS/2 Toolkit), "If this option is set, only pure colors are used and no dithering is done." Well, I've never used this option and I've never seen any dithering. In my honest opinion, Palette Manager does the closest match instead of dithering, since dithering wouldn't work for narrow lines (or single pixels!), and since it'd be too computationally expensive for filled areas. Anyway, you can try this flag if you want; I don't use it and don't plan to. LCOL_OVERRIDE_DEFAULT_COLORS. OS/2 Presentation Manager sets aside about 20 colors for the user interface (eg, for the color of Window title bars). This means that you can't ever really get 256 colors. Unless you use this flag, that is. However, using this flag will screw up the look of the user interface. In my honest opinion, it's not worth it; sort your palette and live with the fact that you'll "only" get 236 distinct colors. Step Four is the same old "GpiSelectPalette (hps, hpal)." Step Five is half the same (in handling firstPaint), but half different. In the handling of WM_REALIZEPALETTE, we actually use the return value of WinRealizePalette: ULONG palSize = colorsToShow; if (WinRealizePalette (hwnd, hps, &palSize)) { WinInvalidateRect (hwnd, NULL, FALSE); } What does this do? Basically, when you call WinRealizePalette, it returns the number of palette entry requests that it could NOT meet. If you asked for a palette with 16 colors and it could only give you 13, the other 3 will be matched to the closest available colors, and WinRealizePalette will return 3. In this case, you should redraw your client area, and that's what the "WinInvalidateRect" call does. Step Six, using the palette, is much the same as it was for PHYSCOLO. Notice, however, that SCALE also has a routine to handle things if the machine doesn't even pay lip service to supporting Palette Manager. That's what the "GpiQueryColorIndex" line is about. On a system without Palette Manager, this returns the argument to GpiSetColor that will return the closest match to the RGB value you want. Finally, Step Seven is the same for SCALE as it was for PHYSCOLO. GRAYBITM This program creates a small bitmap with grayscale shapes and displays it on the screen (stretching it to fit the client area). It's pretty much the same as SCALE. There are a couple of differences: It creates the palette before creating a PS, and it uses the logical palette in two different PSes; the one that's used to create the bitmap (hpsMem in the "CreateBitmap" procedure), and the one that's used to display the bitmap (hps in "ClientWinProc"). These differences aren't really that important, though the latter shows that you can "share" palettes by sharing the HPAL. Notice that handling WM_PAINT is simplicity itself: We just call WinDrawBitmap. But why didn't we use GpiBitBlt? And why do the colors get screwed up when we resize (eg, maximize) the GRAYBITM window? We'll tackle these issues when we talk about the bugs in the Palette Manager. ZOOM This program does palette animation. What's that? Well, suppose you do the following: o Create a logical palette with space for four colors, with all four palette entries set to the window background color. o Draw four non-overlapping filled circles using each of the four colors, with the circle that uses color 0 on the left, circle 1 to the right of it, and so on. Okay, so at this point you've drawn four "invisible" circles. Now suppose you set the first entry in the logical palette to blue (or any color that isn't the background). Boom, a circle appears on the left. After a brief pause, change the first color in the logical palette back to the background, and change the second color to blue. Then change the second color to the background and the third color to blue. And so on. If you time these well, this makes it look like an animated sequence of a circle moving from left to right across your client area. But the thing worth pointing out is, you don't have to do any fancy drawing or undrawing in real-time; all you're doing is changing palette entries. Since you're only changing palette entries, this sort of animation can go very fast, once you've finished the complicated initial drawing. ZOOM does something similar: It draws 16 overlapping squares, creates a palette with spaces for 16 colors, and then animates. The result is a little like moving down a long corridor. (For a real cheap thrill, run TESTZOOM.CMD, CTRL-ESC to get the Window List, select all the Zoom sessions, and Tile them. If one of these windows has the focus, click on the desktop to "get rid" of it.) From a programmer's standpoint, ZOOM is yet another straightforward use of Palette Manager. It carries out Steps One and Two in the usual way (though it gives an error message and dies instead of just "trudging on" if it can't find Palette Manager -- since this program exists only to demonstrate palette animation, there's no point in having it continue.) In Step Three, things are slightly different: tbl [j] = PC_RESERVED * 16777216 + ... What's this PC_RESERVED flag? Well, as I said before, if Palette Manager can't give you all the colors you requested, it will use the closest match among the physical palette. (That's why we do a WinInvalidateRect if WinRealizePalette returns a nonzero value.) However, if you use the PC_RESERVED flag, Palette Manager will not match anything with that particular palette entry. This is important if the RGB values of the palette entry will be changing rapidly (the overhead of keeping the closest match up-to-date would be too great). Since changing RGB values rapidly is the heart of palette animation, you should always use the PC_RESERVED flag for palette animation. Step Four is still the same. Steps Five and Six are different. In fact, I'd say that Steps Five and Six are now intertwined, since the way you "use" a palette for animation is to constantly change it, and you have to realize the palette every time you change it. Hence: ULONG tmp = tbl [0]; ... tbl [j] = tbl [j + 1]; tbl [shadesToShow] = tmp; GpiAnimatePalette (hpal, LCOL_CONSECRGB, 0, shadesToShow, tbl); GpiAnimatePalette is, naturally enough, the function that actually animates (ie, changes the RGB values of) the logical palette. Its arguments are the handle to the palette to change, the ubiquitous LCOL_CONSECRGB, the first index to change, the number of indexes to change, and the table that holds the new RGB values. The call above changes all the palette entries; to change only entries 3 through 7, change the "0" above to "3", and the "shadesToShow" to "5". (The values in tbl would still be the same.) Finally, Step Seven is the same as usual. PALETTE MANAGER BUGS There are several reasons why this guide is unofficial rather than official. One reason is that it describes what I've discovered in the school of hard knocks, which is sometimes quite different from what the official documents say. Another reason is that I don't have any access to official IBM folks, so there may well be errors in this article. I've described how I've used Palette Manager to the best of my ability, but it could be that there are simpler, more elegant ways to carry out some of these tasks. I doubt it, but we'll never know until the IBM documentation improves. (Another reason that this is unofficial is that I have no compunction against slamming IBM when I think they deserve it.) Finally, one of the important reasons why this is an unofficial guide is that I'm going to tell you that the current incarnations of Palette Manager have some real bugs, including one that's very serious. Namely: THE FATAL ET4000 BUG (or, How To Hang Your Computer Using Palette Manager) This is the most important fact in this article; if you forget everything else, remember this. And warn anyone who uses your program about it! If you're running one of the 256-color ET4000 SuperVGA drivers from the Service Pack, AND IF a program changes the physical palette (either an OS/2 program that uses Palette Manager or a seamless WinOS/2 program), THEN !!DON'T MOVE AN ICON ON THE OS/2 DESKTOP!! IF YOU DO, YOUR COMPUTER WILL HANG! Your computer won't be safe until the physical palette has reverted to normal. This won't happen until after the program(s) that changed the palette are finished. And even then, it can take a while; one way to force the palette back is to start PHYSCOLO (or move it to the foreground) and then click on the desktop. The palette will revert to normal. TRIDENT USERS BEWARE! THIS BUG MAY ALSO OCCUR FOR YOU (I couldn't get verification by press time). Fortunately, this bug does NOT occur in the XGA drivers. Neither does it occur in the latest 256-color drivers from the OS/2 2.1 beta (version 6.479), so ET4000 users may want to switch to the beta. I hope the fact that the bug isn't in the beta means that IBM has fixed it, but I haven't heard anything official from them, so take care. Unfortunately, this isn't the only bug in Palette Manager. I know of three others: THE UNIVERSAL BUG: Start a program that uses Palette Manager to draw in its window (eg, SCALE or GRAYBITM). Drop down the system menu over the client area. Click elsewhere to get rid of the menu. The area under the menu won't redraw correctly. This bug occurs on every platform that's known to support Palette Manager, though it's less obvious on XGAs and 2.1 beta-using ET4000s than on Tridents and ET4000s with the Service Pack drivers. THE GPIBITBLT BUG: If you use GpiBitBlt to render a bitmap that uses a logical palette, it'll be drawn with the wrong colors on ET4000s and Tridents. Guaranteed. However, it'll be drawn fine on XGAs. THE WINDRAWBITMAP SCALING BUG: If you use WinDrawBitmap with parameters that require the bitmap to stretch or shrink, it'll be drawn with the wrong colors on ET4000s and Tridents. Again, though, it'll look fine on XGAs. I have sent bug reports from IBM on all of these, as well as sample programs that demonstrate the GpiBitBlt bug. However, I'm just one voice crying in the wilderness: If you want to see these bugs fixed, PLEASE get the file OS2PROB.TXT from the /pub/os2/2.0/info directory on ftp-os2.nmsu.edu, fill it out, and e-mail it to 76711.610@compuserve.com. Make sure that you only report ONE bug on each form; otherwise, you'll just get a note back from IBM asking you to resubmit your report on several forms. As long as I am the only person asking for these bugs to be fixed, they'll probably be a low priority for IBM. FINAL THOUGHTS Don't let these bugs get you down; Palette Manager is a fine subsystem that lets you accomplish a lot of great effects. My hat's off to IBM for giving us such a simple and elegant way to use palettes, and my hat's off to YOU for the fine Palette-Manager-using programs that I hope you'll be writing soon ;-). Raja Thiagarajan / sthiagar@bronze.ucs.indiana.edu / 2-21-93 ═══ 4.4. Advanced GPI:Retained Segments and Transformations ═══ Advanced GPI: Retained Segments and Transformations ═══ 4.4.1. Advanced GPI ═══ [The code presented in this article is contained in GPI.ZIP -Ed.] When you say "Graphic Program Interface" what first comes to mind is a set of about a dozen and a half system calls to draw lines, boxes, circles, and other kinds of graphic elements. But, I may ask you, did you know that by using OS/2 Presentation Manager, you could refresh your entire window, whatever you draw, with one call? Did you know that you can take graphic "segments" and do things like rotate them, translate them, and scale them, without actually having to write the code to do the matrix math yourself? Well, thats what we're going to be talking about. There is a whole host of functions that are built into the GPI of OS/2's presentation manager that perform these features, plus more. Unfortunately, these functions are not normally used by the common user, and thats why I'm going to be talking about them here. This is an introduction to the Advanced GPI functions that many of us don't usually think about. Just as an introduction, I'm assuming that you've programmed OS/2 presentation manager programs before. Therefore, I'm not going to go into the specifics of the calls such as WinCreateStdWindow, or any of the other Window functions, since this article mainly deals with Gpi functions. But, if you've never programmed PM before, I do think that you should still be able to follow this article through to the end, and by looking at the source code, which I hope is clear and straightforward, you should be able to pick up with what I'm talking about. So, I would also recommend that you have a copy of the source code around while you're reading this, since I'll be talking about distinct parts of the code, and to get a better view of whats going on, you should see the context of how the call is used. First, we're going to talk about presentation spaces. What exactly is a presentation space and how can we use it to help us? Very generally, a presentation space is the part of the system that takes your calls to the GPI functions, and generates the pictures that you see on the screen. In most cases, this mapping of graphic calls to displayed results is normally a pass-through operation, that is, the things that you do aren't retained in memory, other than the results that you see on the screen. This is the default behavior of the presentation space that is associated with the client window that would be created by a WinCreateStdWindow call, this type of presentation space is called the "Cached Micro-PS." But, there are other types of presentation spaces, the most important of these being the "Normal PS." What a Normal PS allows you to do is take the graphic calls that you're sending it, and remember them for later operations, like refreshing the window, or rotating your objects, or whatever you like. A Normal PS also supports correlation functions that will let you know if any of your graphic segments pass through a certain rectangle of the screen, that you define. This correlation is wonderful for user interface tasks, such as clicking on an object to select it. We'll see how to use these functions later in the article. The program that is developed here displays nine objects on the screen, each of which is its own segment in the presentation space. By clicking on an object, with either the right or left button, you can select an object. When an object is selected, you can use the '+' and '-' keys to rotate it, or drag with the right mouse button to translate it. The '=' key does the same thing as the '+' key, for ease of use. Since we know that the default Presentation Space create with the WinCreateStdWindow call isn't a Normal PS, we have to figure out how to create the PS that we really want. Looking through our list of the GPI functions, we might take notice of GpiCreatePS. HPS GpiCreatePS(HAB hab,HDC hdc,PSIZEL pszl,LONG loptions) "hab" is the Handle to our Anchor Block that we got when we did our first WinInitialize call. This should be readily available as a global variable to our program. HDC is the Handle of the Device Context that is associated with a window, or in layman's terms, a handle that describes what type of device we're working with. This is readily available by calling the WinOpenWindowDC function. So, all we need now are the pszl that defines the size of the presentation space that we want to create, and some options. Looking at the functional description of the call, we see that if we specify (0,0) as the size of the PS we're creating, a default size for our device will be used instead. So, we're left with some options, that describe what type of PS we're going to be creating. In our case, we want the Page Units to be pixels (since we're drawing on the screen), we want a Normal PS, and we want to asociate the PS with the hdc that we're dealing with, so we tell it to automatically do this for us. So, our final version of the call ends up being: sizel.cx=0; sizel.cy=0; hps=GpiCreatePS(hab, hdc, &sizel, PU_PELS | GPIF_DEFAULT | GPIT_NORMAL | GPIT_ASSOC); And, we can use this HPS throughout our program, as long as we follow a couple simple conventions, the main one being that we destroy it at the end of our program. Normal PS's take up a lot of memory, and you don't want to be using them where you don't have to. Use a normal PS only when you want to use the functions that go along with it. You'll quickly run out of memory if you use a normal PS for every window that you create. Looking at our WM_CREATE message in our client window procedure, we see the above call to GpiCreatePS, along with a couple other suspicious looking calls, the first of these being the GpiSetDrawingMode command. Before explaining the call, I'll explain a little more about the process that will be going on in the program. We know that we have now a Normal PS instead of a Cached Micro PS and we know that we can have retained graphics using this normal PS. Well, it would seem fairly obvious that there has to be some way of more readily organizing the information (graphics) that we're going to be retaining. What the Gpi can do for us is break our individual calls to Gpi primitives into larger blocks, each of which is a segment. Each segment has a "tag" that goes along with it. This "tag" is just a number that we assign to the segment so that we can remember which one is which. The segments also have numbers, and we'll order them sequentially. In our program, the segment number and the tag are the same number, for simplicity's sake. Note that to do the functions that we're doing, each segment must have its own unique non-zero tag. Along with the tag, a segment transform matrix is also stored. If you're not familiar with computer graphics and transform matrices, just think of this as something that will specify the position and rotation angle of the segment. It can also specify the scaling value of a segment, but we won't deal with those calls in this example program. The calls to the scaling routines are very similar to the ones for translation and rotation, so if you have a reference manual that describes these functions, you should be able to use them with no problem. So, we've got our segments. For this program, we're going to have nine segments. Two will be five pointed stars, one a square with rounded corners, one an oval, and the rest will be squares. They'll be arranged in a 3x3 grid on the screen. We also must mote that matrices in OS/2 are mainly composed of fixed point numbers instead of floating point. This allows the Gpi to not require a floting point coprocessor and still run at a resonable speed on slow computers. Thus, there is a macro called MAKEFIXED that takes two arguments, an integer portion and a fractional portion of the number. As a little background, to represent a number as a fixed point, call our number 'n'. You would use the following code: i=(INT)n; f=(n*65536)-((INT)n*65536); fixed=MAKEFIXED(i,f); So, back to talking about the GpiSetDrawingMode command. What this does is tell our presentaion space to do one of the following things: Draw only, draw and retain, or retain only. We're going to be using the retain only function, so that we can enter the segments into the presentation space, then draw them ourselves with other specialized calls later. So, we use the call GpiSetDrawingMode(hps,DM_RETAIN); To set the drawing mode. The other suspicious looking call is the GpiSetPickApertureSize call. I'll come back to this later. What it does is set the size of the rectangle that we'll be using as our correlation area. Now, the last thing that's there is the SetupSegments call. This is a procedure that was written to draw the nine segments, and set their initial segment transform matrices so that they're not rotated, and so that they're arranged in a 3x3 grid. Looking at this procedure, we see some calls that look somewhat unfamilar. GpiSetInitialSegmentAttrs. Oh, this must have to do something with the segment attributes. Yes, it does. Every segment has some flags that go along with it, this just tells the system "for every new segment that you make, set these options from now on" The options that are most commonly used are: 1. ATTR_VISIBLE The segment will be visible -- i.e. will be drawn 2. ATTR_DETECTABLE The segment will be detectable by the correlation functions 3. ATTR_CHAINED The segment will be in the segment chain. The segment chain is the list of segments that will be drawn by the GpiDrawChain call. So, in our case, we want all of our segments to be in the segment chain. Now, the second unusual looking call is the GpiScale call. What we're doing here is initializing the variable 'identity' so that it contains the identity matrix, so that we can set our viewing transformation to be the identity. What this means is that if we say our square is 30x30, it will come out as 30 pixels on a side, exactly. So, we call GpiSetViewingTransformMatrix with the appropriate arguments. The one that might look suspicious is the second one, the 9L. This specifies that there are nine elements in our matrix. This is because for two dimensional transformations, you deal with 3x3 matrices. The rest of the code is organized as follows: GpiOpenSegment(); GpiSetTag(); GpiSetColor(); GpiTranslate(); GpiSetSegmentTransformMatrix(); . . . Draw our segment . . . GpiSetModelTransformMatrix(); GpiCloseSegment(); What we're doing is keeping a counter of the segments that we've drawn, so that each segment will have a unique number. We open that segment, set its tag, and its color. The GpiTranslate call sets the matrix 'translate' to be matrix that will translate the image drawn from (0,0) to the point specified in ptlTranslate. Then, using the GpiSetSegmentTransformMatrix call, we set the matrix for the current segment, to be the translation matrix that we just computed. Along with the individual segment transform matrices for each segment, there is a model transform matrix which is applied to all segments drawn. When a segment is drawn, its transform matrix gets multiplied to the model transform matrix, resulting in an accumulation of the transforms. To get around this effect, at the end of each segment we set the model transform to be the identity. We then close the segment we're working with. So, after the SetupSegments call, we've got nine retained segments, each with its own individual transform matrix to put it in the right spot on the screen. The hard part is through. We've already drawn the innards of our segments, an operation that only has to happen once, and we've also set their initial values, something that only has to happen once. From here on out, its just the accumulation of the deltas that make up the other operations we can do on the segments. Looking at the WM_PAINT messaage, which is the next things that happens, we see that it is very short and concise. We erase the presentation space, set the forground mix to XOR mode, so that we can easily erase a segment once we've drawn it, then we call the infamous GpiDrawChain, which draws all the chained segments, along with applying their individual transform matrices. Note that the WinBeginPaint call in our WM_PAINT message is probably a little different than those that you've seen before. Because WinBeginPaint usually returned a hps, but we have a PS already, we have to tell it that, so the second argument to the call is our current hps. This way it doesn't get obliterated by the new PS that would have been created. The next message that I'll look at is the WM_BUTTON?DOWN messages, they're both the same, so I'll just talk about WM_BUTTON1DOWN. What we're going to be doing here is correlating the retained segments to see if we've clicked on one or not. To do this we have to do a couple of setup things first. Intuitively, we have to set the size and position of the rectangle that we're going to be correlating with, then do the actual correlation. Because the size of the correlation rectangle doesn't change throughout the program, I've placed it in the WM_CREATE message, so it only gets executed once. Take a look at the code, it sets the variable 'size' so that its 10 pixels square, then calls GpiSetPickApertureSize. This calls are fairly obvious, first the hps, then the options, then the pointer to the variable 'size'. The option PICKAP_REC tells the Gpi system that we want to use the value specified in the third argument, as opposed to using some default vaule. Now for the position of the pick aperture. Back to the WM_BUTTON1DOWN message, we see that the variable 'ptl' is set to be the current position of the mouse, and we call GpiSetPickAperturePosition. Its arguments are self explanatory. Next comes the neat call, the correlation. lNumHits=GpiCorrelateChain( HPS hps, LONG lType, POINTL pptlPick, LONG lMaxHits, LONG lMaxDepth, PLONG alSegTag); lType specifies what type of segments you want to correlate with. In our case, we want to do all segments, so we specify PICKSEL_ALL. The pptlPick argument is again the position of the pick aperture. lMaxHits and lMaxDepth together specify the maximum number of hits that will be recorded by this function. In my case, as in most cases, both of these arguments will have the value one, since we only are interested in one segment at a time. The PLONG alSegTag is an output array of segment number and tag value pairs. For example, on return, if we've hit a segment alSegTag[0] will be the segment number of the selected segment, and alSegTag[1] will be its tag vaule. The routine returns the number of segments hit, so in our case, this will be either zero or one, and we're only interested in the case where its nonzero. If the call returns nonzero, we set our variable 'selected' to be the index of the currently selected segment, and continue, otherwise we set 'selected' to be zero, to signify that nothing is currently selected. Now, when we get a WM_MOUSEMOVE message, and when button two is down, we do some more magic. What we want to accomplish here is to figure out how far the mouse was moved at each step, and then add that increment to the current position of the currently selected segment. The first thing that we do is draw the segment in XOR mode, which will erase it from the screen. We compute the position delta, and then use GpiTranslate to get the matrix that represents that translation. Using GpiSetSegmnetTransformMatrix with the TRANSFORM_ADD parameter, we then add this delta translation to the total translation. The TRANSFORM_ADD parameter tells the Gpi to postmultiply the matrix onto the current matrix. Because we want all rotations to come before all translations, we postmultiplt translations, and premultiply rotations. The last thing that we do here is draw the segment again, so that it appears in its new place. The GpiDrawSegment call draws just one segment, which is specified as the second argument to the call. The last part of our program that we havn't looked at yet is the WM_CHAR message. It too is fairly simple. It sets the rotation angle to be 3.0 degrees, then if the '-' key has been pressed, negates this value. It then premultiplies the right matrix onto the segment transform matrix with the GpiRotate and GpiSetSegmentTransformMatrix calls. Note that the parameter to GpiSetSegmentTransformMatrix is TRANSFORM_PREEMPT. I've described all the Gpi functions that allow you to create segments, and modify the segment transform matrices so that you can rotate and translate them. Hopefully I've given enough background so that you can write your own programs that use these advanced Gpi functions, along with the others that deal with retained segments and segment transform matrices. In most cases, these calls aren't as hard as they seem, and it is guaranteed that using these calls will both save you programming time, and increase the speed of your own code. Have fun! ═══ 5. Columns ═══ o Questions and Answers o Introduction to PM o Project Barrel ═══ 5.1. Questions and Answers ═══ Questions and Answers ═══ 5.1.1. Introduction ═══ Welcome to the first "Questions and Answers"! Each month, I collect various questions sent to me via email and try to answer each directly; the ones that I feel contribute the most to developers, whether in terms of information or as a nifty trick to tuck into your cap, get published in this column. To submit a question, send mail to my email address - os2man@panix.com - and be sure to grant permission to publish your question (those that forget will not be considered for publication). ═══ 5.1.1.1. "Smart icons" ═══ Stefan Gruendal (Stefan_Gruendel@wue.maus.de) writes: But to my question: How do I build my own "smart icons", i.e. bitmaps on pushbuttons, that optically "move into the screen"? I didn't find any way to achieve this with the Toolkit's Dialog Editor. But as mentioned above, I know there's a way. Starting with OS/2 2.0, a new button style was added - BS_ICON - which allows you to do what you are trying to accomplish. It works, as far as I know, only for pushbuttons, and the icon or bitmap is taken from the resource file, whose resource id is specified in the pushbutton text. For example: In FOO.H: #define IDBM_BUTTON 256 #define IDPB_BUTTON 257 In FOO.RC: BITMAP IDBM_BUTTON BUTTON.BMP In FOO.C: sprintf(achText,"#%d",IDBM_BUTTON); hwndButton=WinCreateWindow(hwndClient, WC_BUTTON, achText, WS_VISIBLE|BS_PUSHBUTTON|BS_ICON, 10, 10, 32, 32, hwndClient, HWND_TOP, IDPB_BUTTON, NULL, NULL); The bitmap is stretched or compressed to fill the button. ═══ 5.1.1.2. Finding yourself ═══ Raja Thiagarajan (sthiagar@bronze.ucs.indiana.edu) writes: Can a PM program tell if there's a previous instance of itself running? In Win3.x (but apparently NOT Win32), there's a hPrevInst handle; is there an OS/2 2.0 equivalent? Basically, I'm thinking in terms of a program that would try to borrow resources from a previous instance if a previous instance is running. (Specifically, if my palette animation program gets started twice, the second instance oughta share palettes with the first instance!) What you're really asking is two questions: 1. How can I determine if a previous instance of my application is already running? 2. How can I share resources between multiple instances of my application? To answer your first question, you need to enumerate all of the main windows present on the desktop, and figure out if any of them are yours. This is achieved using the following code: HWND queryAppInstance(PCHAR pchClassWanted) { HENUM heEnum; HWND hwndTop; HWND hwndClient; CHAR achClass[256]; heEnum=WinBeginEnumWindows(HWND_DESKTOP); hwndTop=WinGetNextWindow(heEnum); while (hwndTop!=NULLHANDLE) { hwndClient=WinWindowFromID(hwndTop,FID_CLIENT); if (hwndClient!=NULLHANDLE) { WinQueryClassName(hwndClient,sizeof(achClass),achClass); if (strcmp(achClass,pchClassWanted)==0) { WinEndEnumWindows(heEnum); return hwndClient; } /* endif */ } /* endif */ hwndTop=WinGetNextWindow(heEnum); } /* endwhile */ WinEndEnumWindows(heEnum); return NULLHANDLE; } To answer your second question, the only way that I know of to share resources is to place them in a DLL. The procedure to do this is as follows: o Create a dummy source file with an empty procedure in it. o Compile the source file and link as a DLL. o Add your resources to the DLL in the same manner as you would to an executable. Then, when your application starts, you simply call WinLoadLibrary (the preferred way) or DosLoadModule to load the DLL. These functions return a handle to the DLL which must then be used in any function which loads resources (e.g. GpiLoadBitmap, WinLoadPointer, etc.). Note that this procedure does not require knowing the window handle of any previous instance of your application because OS/2 implements DLLs in a shared fashion (which I suspect is one of the reasons they were created in the first place). All you need to know is the name of the DLL. This technique can also be used to share resources between different applications. ═══ 5.1.1.3. Device independence ═══ A reader who desires to remain anonymous writes: Generally: My understanding was that OS/2 would handle printing for me. That is to say that I wouldn't have to create separate printer drivers for every printer under the sun (or any for that matter). Since I am creating an image on the screen that is device independent (well, mostly anyway), is there an easy way to get printer output? PM achieves a level of device independence by defining a logical output space. This logical output space is then bound to a physical output space, which creates a mapping of logical characteristics to their physical counterparts. The logical and physical output spaces are referred to as the presentation space and the device context (HPS and HDC) and are bound to one another by using either the GpiAssociate function or by specifying GPIA_ASSOC to the GpiCreatePS function. The easiest way to accomplish what you desire is to organize your drawing code into one or more functions with a single entrypoint that accepts an HPS as a parameter. Then, when you want to draw to the screen, you can call WinGetPS/WinBeginPaint to get an HPS and call the function. When you want hardcopy, you call DevOpenDC to get an HDC and GpiCreatePS to get an HPS and call the function. Note that to get hardcopy, you need to perform some additional setup to get things to work properly. The two most important things are that you initialize the DEVOPENSTRUC structure properly before calling DevOpenDC and that you send the following escape codes (via DevEscape) at the following times: hdcPrn=DevOpenDC(...); hpsPrn=GpiCreatePS(...); DevEscape(...,DEVESC_STARTDOC,...); if (!doDraw(hpsPrn)) { DevEscape(...,DEVESC_ABORTDOC,...); } /* endif */ DevEscape(...,DEVESC_ENDDOC,...); GpiDestroyPS(hpsPrn); DevCloseDC(hdcPrn); I'm not sure because I can't seem to find my copy anywhere, but I belive that the book by Graham Winn (entitled something to the effect of "Building applications using the OS/2 Presentation Manager") dedicates a chapter to the nuances of printing. ═══ 5.2. Introduction to PM ═══ Introduction to PM Programming Part I ═══ 5.2.1. Overview ═══ Overview It can be quite a daunting task to sit down and learn to write Presentation Manager programs. There is so much ground to cover, but fear not - getting over the initial learning curve is the hardest part. Once you have a grasp of the basics, it is fairly easy to pick up on more advanced techniques. Here I intend to provide a starting point for C programmers to get into PM, so that you do have a solid grounding. Now unfortunately there are not many books on PM which explain not just the what, but the why. That is, not just presenting a syntax diagram and saying that (for example) WinBeginPaint is used when you want to paint a window, but why you need it, when to use it (and not WinGetPS ), and how to use it. I will endeavour to explain the why, so you understand exactly what you are doing, instead of blindly copying code from examples. You will see the code, and at each step we will discuss the important sections, so you can see just what each API does. You certainly don't have to remember every single API though (there are hundreds). As you have a need to do something, you will learn the new APIs required to achieve that. Please remember that, as this is an introductory article, it is not presented as being 100% technically correct and accurate. It is more to give the reader a practical feel for what is going on. Technical references for technical questions appear in the Bibliography. And remember, you can always read the headers! ═══ 5.2.2. Scope ═══ Scope I am assuming that you are a competent C programmer, and have a working knowledge of OS/2 from a user's perspective. The sample code here was produced with IBM's C Set/2, and is ANSI C. I do not assume anything but a familiarity with GUIs in general. ═══ 5.2.3. Trademarks etc. ═══ Trademarks etc. Please note that any trademarks referred to in this article remain the property of their respective companies. #include ═══ 5.2.4. Presentation Manager ═══ Presentation Manager Presentation Manager is the GUI which sits atop OS/2. It consists mainly of a series of DLLs which contain all the controls, APIs and other stuff which makes PM tick. The WPS is in effect a PM application itself, albeit with certain other special properties. One good thing about PM is that because the bulk of the code to handle the user interface is in DLLs, applications tend to be smaller, as they contain largely just the code to handle the processing of the information. This lets the programmer focus on the "guts" of the program, rather than being concerned with the user interface. Also, PM provides a consistent and intuitive interface for the user along standard design guidelines. Of course, as you will no doubt see, there is still a great deal to learn about PM! ═══ 5.2.4.1. The Event Driven Model ═══ The Event Driven Model GUIs are modelled on an Event Driven paradigm. This is better understood when contrasted with the sequential nature of conventional programs. Imagine the following ficticious code which might appear in the menu of a program: DisplayMenu(); while (!kbhit()) UpdateTime(); c=getch(); if (c==0x27) Quit(); You can see that in the while loop, the program is constantly polling the keyboard to check if a key has been pressed. This is a very inefficient practice, as the machine spends most of its time in that loop, doing nothing. In a multitasking environment, this waste of CPU time reduces system throughput, as the CPU could be doing something more useful. In OS/2 however, programs do not poll for input (whether from the mouse or keyboard). All input is managed by PM itself. This means that when the user clicks the mouse on a button for example, PM handles this event. It puts the event into a queue for that application, and the function which handles this event (the window procedure) only gets called when it has a message waiting for it. As a result, the program only acts when it needs to, rather than constantly checking if it actually has something to do. So rather than following sequentially through the program, PM programs send, receive and respond to events. This technique requires a shift in thinking, but with practice it will become clear. It is arguably a more intuitive model for programming, and it also promotes good technique. ═══ 5.2.4.1.1. Windows Everywhere ═══ Windows Everywhere A Window is simply a rectangular area on the screen. A window can have certain properties or flags, and PM is full of them. They may or may not have a frame or border, a titlebar, or a menu. You may only think of a window as being the Frame window of an application. However buttons are windows too. So are dialog boxes, list boxes, scrollbars - in fact each control in PM is a window. We will explore this further in a future article, but for now just take it that a window is rectangle. Don't worry too much if this sounds a bit vague - it will become clear once you start making your own windows. ═══ 5.2.4.1.2. Getting the Message ═══ Getting the Message The messages discussed in our talk of Events is simply a C struct, which looks like this: typedef struct _QMSG /* Queue Message */ { HWND hwnd; ULONG msg; MPARAM mp1; MPARAM mp2; ULONG time; POINTL ptl; ULONG reserved; } QMSG; This struct is a key element in PM. The fields are explained thus: ■ hwnd - Window Handle - unique reference for a given window. ■ msg - A system- or user-defined message ■ mp1 and mp2 - Message parameters (meaning dependent upon the message). Can be a number, a pointer, a handle, etc. ■ time - The time stamp when the event ocurred. ■ ptl - The (x,y) co-ordinates of the mouse. ■ reserved - Just what it says! All messages have a unique number, and the system messages are all #defined in the OS/2 headers. Some typical messages include: ┌──────────┬────────────────────────────────────────┐ │Message │Description │ ├──────────┼────────────────────────────────────────┤ │WM_CHAR │When the user presses a key │ ├──────────┼────────────────────────────────────────┤ │WM_CLOSE │if the user chooses to Exit the program │ ├──────────┼────────────────────────────────────────┤ │WM_PAINT │when an area of the screen needs to be │ │ │drawn │ └──────────┴────────────────────────────────────────┘ You will not normally deal with the QMSG structure directly - your window procedure will handle this, and deals only with the handle, the message and the two parameters (mp1 and mp2). ═══ 5.2.4.1.3. Having a Handle on Things ═══ Having a Handle on Things A handle is a unique number or reference for any given system object. The most common handles refer to windows. An application can have any number of windows, and handles are a convenient way to keep track of them. Handles are also used internally by PM to manage its own information on the system. ═══ 5.2.4.1.4. Window Procedures ═══ Window Procedures A Window Procedure is one of the most important parts of any PM application. Its job is to handle all of the messages that get sent to its window. It usually consists of a big switch statement, with a case for each message. You can handle as many or as few messages as you wish, because (fortunately!) one of the APIs in PM is a default window procedure. So any message you don't need or want to handle, you pass on to WinDefWindowProc, which handles it in the default manner. Well, enough of this banter - let's do something useful and make a PM program. ═══ 5.2.5. Your first PM Program ═══ Your first PM Program Let's start simply by getting a plain window on screen. I will show you the code, followed by explanations, with the full source at the end of this section. /* Definitions */ #define INCL_WINFRAMEMGR #define MYAPP "MyApp" #define ID_MYWINDOW 42 If you look at the file OS2.H, you will see a whole bunch of #ifdefs, and #includes. Because the header files are so large and numerous, and you don't always want to include everything since you may not use all of it, all you need do is #define which parts you need to include. There are certain things which are included by default, so here we only neet to define INCL_WINFRAMEMGR, for the definition of WM_ERASEBACKGROUND, as all the rest is already there. The most important header is PMWIN.H, so take some time to browse through it as it has some very important things in it. /* Includes */ #include Having defined which sections we need from the headers, all we need at the moment is to include OS2.H. /* Prototypes */ MRESULT EXPENTRY MyWinProc (HWND, ULONG, MPARAM, MPARAM); We prototype our functions, like all good ANSI programmers! /* Global Variables */ HAB hab; HWND hwndClient, hwndFrame; The HAB stands for a Handle to the Anchor Block. As you know, a handle is just a reference, but the anchor block is an internal data structure which PM manages. The origin of the name is a little obscure. Anyway, each instance of an application has an HAB. There are few times when you actually use it, usually only during the initialization of your program. For now, let's say it is like a handle to your application. Now we have two other handles, this time handles to windows. Notice that we have two handles. One for the client, one for the frame. This requires a little more detail. The Frame Window is the window from the edges of the resizeable frame border. It contains the border, titlebar, minimize and maximize buttons, system menu, and optionally a menu. The Client Window is the area of whitespace bordered by the frame and the menu. This is the space where information is displayed, and the user interacts with. Take a look at the System Editor for example. The frame window encompasses the entire application window. The client area is the area of white where the text appears (the edit window). Having a separate client window makes it easier to paint, scroll, and other such operations. /* main function */ void main (void) { HMQ hmq; QMSG qmsg; ULONG flCreate; hab = WinInitialize (0); if (!hab) return; Here we launch straight into the main function. First we declare some local variables. The HMQ is a handle to a message queue. When an application is created, it needs a queue to keep the pending messages in, as they await processing by the window procedure. The QMSG is the data structure we looked at earlier (see Messages). This is used in the main message loop below. The flCreate is used to keep the flags we need to create our window. The very first thing we do is call WinInitialize. This initializes certain things in PM and gives us an HAB. If we could not get a HAB, something really bad has gone wrong, so we exit immediately. hmq = WinCreateMsgQueue (hab, 0); Here we create our message queue with our HAB, and get a handle to it so we can refer to it later. WinRegisterClass( hab, MYAPP, (PFNWP) MyWinProc, CS_SIZEREDRAW, 0); Because each application behaves differently and has their own window procedure, PM needs to know certain things about your application. This is called registering. You give it your HAB, a unique string (called the class name) which identifies your program, a pointer to your window procedure (which is why you cast it) and any particular flags you may require. These flags are called Class Styles, and CS_SIZEREDRAW means we want our application to be redrawn if it is resized. The last parameter allows us to reserve some space within each window for instance-specific data, but we don't need this yet. ═══ 5.2.5.1. Creating the Window ═══ Creating the Window flCreate = FCF_TITLEBAR | FCF_SYSMENU | FCF_SIZEBORDER | FCF_MINMAX | FCF_TASKLIST | FCF_SHELLPOSITION ; hwndFrame = WinCreateStdWindow( HWND_DESKTOP, WS_VISIBLE, &flCreate, MYAPP, "Sample PM App", 0L, NULLHANDLE, ID_MYWINDOW, &hwndClient); First we set up a variable which has ORed together a bunch of flags (called Frame Control Flags) to tell PM what sort of window we want. They mean (in order) we want a titlebar, a system menu, a resizeable border, both minimize and maximize buttons, we want it to appear in the task list, and we don't care what it's initial size or position is (let the shell decide). Now comes the actual window creation. We specify the parent window (usually the desktop, as is here). WS_VISIBLE means we want it to be visible, pass the frame control flags, our class name (so it knows which window procedure to use), the title of the window, then any other special flags. NULLHANDLE tells it to look for Resources (such as icons, menus, dialogs, etc) in the executable itself. We pass the ID of the window (to distinguish it from other windows, and to associate resources with), and finally get a handle to our client window. I hope you're keeping up so far, because we're almost there! while (WinGetMsg (hab,&qmsg,(HWND)NULL,0,0)) WinDispatchMsg (hab,&qmsg); This is the most crucial part of the program, and is really what gives it the capability to respond to events. It is a constant loop which gets a message from the queue and dispatches it to the window procedure. WinGetMsg will keep going until it receives the WM_QUIT message. WinDispatchMsg actually gets PM to call your window procedure, which is why all window procedures have the same parameters (hwnd, msg, mp1, mp2). The other parameters in WinGetMsg allow you to do fancy things which we won't get into now. WinDestroyWindow(hwndFrame); WinDestroyMsgQueue(hmq); WinTerminate(hab); } This is just for cleanup - once the user exits the application, we clean up and release the resources we used. /* our window procedure */ MRESULT EXPENTRY MyWinProc (HWND hwnd, ULONG msg, MPARAM mp1, MPARAM mp2) { As I mentioned above, because PM calls your window procedure, you must have it declared exactly like this. EXPENTRY tells the compiler that this function will be in the list of exports (a list of " public" functions). switch (msg) { case WM_ERASEBACKGROUND: return (MRESULT) TRUE; break; Here we begin our amazing switch statement, which branches on the message. For each case, we have a Window Message. This first one is PM asking if we want it to erase the background for us. If we return true, it will do so. If we want to paint the background (or client area) ourselves, we would return False. case WM_CLOSE: if (WinMessageBox(HWND_DESKTOP,HWND_DESKTOP, "Are you sure you want to quit?","Sample", 0,MB_YESNO | MB_QUERY) == MBID_YES) WinPostMsg(hwnd,WM_QUIT,0L,0L); break; This case is somewhat special. WM_CLOSE is sent when the user presses Alt-F4, selects Close from the System menu, double clicks on the System menu, selects Close from the Window List, or selects File|Exit from the menu (if there is one). This gives us a chance to ask the user if they are sure they want to quit, and make sure there are no changes to any files they might wish to save. Here we use the WinMessageBox API, passing it the parent and owner, the message we wish to display, the title of the message box, an ID (used for providing Help), and some flags. The flags specify that we want Yes and No buttons, and a Query icon. The call will return MBID_YES if they pressed the Yes button, in which case we send our application the WM_QUIT message. This causes WinGetMsg (in main) to return FALSE, the while loop to terminate, and finally cleanup before exiting. default: return WinDefWindowProc (hwnd, msg, mp1, mp2); } return FALSE; } This part makes life easy for us. There are a myriad of other messages that our application receives, and WinDefWindowProc will handle them all for us. This is part of the beauty of the event-driven model - we grab only the messages which interest us, and let PM do the rest. This gives us a standard look and feel, with no extra work. Now we have written a bare-bones PM application, follow the instructions in the next section to run it. ═══ 5.2.5.2. Instructions ═══ Instructions You can extract the source code to a file. To do this, for each file, go to the code, press Ctrl-F. This will create a file called TEXT.TMP in the root directory of the current drive. Go to an OS/2 Window and copy that file to a working directory, renaming it to the respective name (which appears in the title). You should be able to compile and run the program straight away. Just issue the command: [C:\WORK\PM]nmake /f step01.mak And then run: [C:\WORK\PM]step01 You should see a plain window with a titlebar. Closing the window will confirm with a dialog. Once you have seen the program running, go back and re-read the section explaining the code, so you can see just what is happening. ═══ 5.2.6. Sample Code ═══ Sample Code [The code presented in this article is also available in 'intropm.zip'. -Ed.] ■ STEP01.C ■ STEP01.MAK ═══ 5.2.6.1. STEP01.C ═══ /* Definitions */ #define INCL_WINFRAMEMGR #define MYAPP "MyApp" #define ID_MYWINDOW 42 /* Includes */ #include /* Prototypes */ MRESULT EXPENTRY MyWinProc (HWND, ULONG, MPARAM, MPARAM); /* Global Variables */ HAB hab; HWND hwndClient, hwndFrame; /* main function */ void main (void) { HMQ hmq; QMSG qmsg; ULONG flCreate; hab = WinInitialize (0); if (!hab) return; hmq = WinCreateMsgQueue (hab, 0); WinRegisterClass( hab, MYAPP, (PFNWP) MyWinProc, CS_SIZEREDRAW, 0); flCreate = FCF_TITLEBAR | FCF_SYSMENU | FCF_SIZEBORDER | FCF_MINMAX | FCF_TASKLIST | FCF_SHELLPOSITION ; hwndFrame = WinCreateStdWindow( HWND_DESKTOP, WS_VISIBLE, &flCreate, MYAPP, "Sample PM App", 0L, NULLHANDLE, ID_MYWINDOW, &hwndClient); while (WinGetMsg (hab,&qmsg,(HWND)NULL,0,0)) WinDispatchMsg (hab,&qmsg); WinDestroyWindow(hwndFrame); WinDestroyMsgQueue(hmq); WinTerminate(hab); } /* our window procedure */ MRESULT EXPENTRY MyWinProc (HWND hwnd, ULONG msg, MPARAM mp1, MPARAM mp2) { switch (msg) { case WM_ERASEBACKGROUND: return (MRESULT) TRUE; break; case WM_CLOSE: if (WinMessageBox(HWND_DESKTOP,HWND_DESKTOP, "Are you sure you want to quit?","Sample", 0,MB_YESNO | MB_QUERY) == MBID_YES) WinPostMsg(hwnd,WM_QUIT,0L,0L); break; default: return WinDefWindowProc (hwnd, msg, mp1, mp2); } return FALSE; } ═══ 5.2.6.2. STEP01.MAK ═══ #-------------------------------------------------------------------- # # Makefile for STEP01.MAK # #-------------------------------------------------------------------- CC = icc LINK = link386 CFLAGS = /c /Ti LFLAGS = /pmtype:pm /debug ·c.obj: $(CC) $(CFLAGS) $*.c all: step01.exe step01.exe: step01.obj $(LINK) $(LFLAGS) step01,,,os2386,; ═══ 5.2.7. What Next? ═══ What Next? Now that you have a basic PM application running, in the next issue we will add a menu, some dialogs, and respond to some messages that do something useful. It may seem like a lot of code to just get a plain window to appear, but we will see how easy it is to add features next month. I welcome any feedback on this article (netmail preferred) - any comments or suggestions you may have, questions on this article, or things you would like to see in a future article. I hope you have learned something! ═══ 5.2.8. Bibliography ═══ Bibliography The following references were used in the preparation of this article: ■ OS/2 Version 2.0 - The Redbooks ■ OS/2 Version 2.0 Technical Library ═══ 5.3. Project Barrel ═══ Project Barrel ═══ 5.3.1. Project Ideas ═══ This column is a place where I intend to throw out ideas that I have come across, either by thinking of them myself or by listening around on the net. Some of these ideas are more practical than others; all of them should be worth at least thinking about. Feel free to attack anything you see here; similarly, feel free to send in your ideas, in as much or as little detail as you wish. Icon Management - A commercial application is available that does most of this now. However, Windows users typically don't have to pay for this level of utility and there's no reason we should either. Ideally, the program would allows for easy movement into and out of zipfiles (or a similar compressed storage archive) and allow for drag 'n drop attachment to programs. The ability to view more than the 100 or so the drives object can view would also be a plus. Network software - Let's face it, the software that comes with TCP/IP 1.2.1 is crap. Telnet only runs in 25 line mode, LaMail is a hideous ordeal to set up, RN doesn't even work. There's a lot of opportunity here to write one of those apps that people use every day of their lives. Games - OS/2 Klondike is nice, but how about something that really shows off what a 32 bit multithreaded program can do? For Presentation Manager, StarTrek springs to mind as a possibility, as does an RPG a'la Castle of the Winds. In fullscreen mode anything is possible. I would also like to see some multiplayer network games - I just saw a friend playing Bolo on the Mac and didn't see any reason why I shouldn't have something like it on my machine. Emacs - Yes, I actually like the beast (I did this entire magazine with Emacs). What I would like (and I have a vested interest in this) is an emacs major mode for editing .IPF files. One that would match tags (in the same way that c-mode matches parentheses) and provide for a consistent style with headers, comments, etc. Etc. - Ever look and see how HUGE the Windows areas are on most BBS's? There's no reason why there shouldn't be that much stuff out there for OS/2 - the two systems are similar in programming style and there's even an entire C compiler for OS/2 free for the asking. Anytime you see a Windows program you like, figure out what it is you like and port it! ═══ 6. Future Attractions ═══ Coming up in the future, we have: o Introduction to PM Part 2 o Writing Installable File Systems o Getting Started with IPF o And much more! ═══ 7. Contributors to this issue ═══ o Steve Luzynski o David Charlap o Larry Salomon o Raja Thiagarajan o Gavin R Baker o Steve Lacy ═══ 7.1. Steve Luzynski ═══ Steve Luzynski is the editor and creator of this magazine. He is currently a Computer Engineering student at Case Western Reserve University in Cleveland, OH, where he spends a lot of time being cold. Steve has yet to release any programs for OS/2 as a direct result of: 1) editing this magazine; and 2) having to waste time going to class when there are programs to write. Steve can by reached via e-mail at 'sal8@po.cwru.edu' or on Compuserve at 72677,2140. ═══ 7.2. David Charlap ═══ David Charlap is 23 years old and is a full-time student at New Jersey Institute Of Technology. He is pursuing a Masters degree in computer science. He learned OS/2 programming while working for a software development company and is currently writing small OS/2 applications for shareware consumption. MineSweeper is his first application that has been released to the general public. David may be reached by e-mail at: dic5340@hertz.njit.edu He may also be reached at the following address: David Charlap 8 Snow Ridge Denville, NJ 07834 USA ═══ 7.3. Larry Salomon ═══ Larry Salomon wrote his first Presentation Manager application for OS/2 version 1.1 in 1989. Since that time, he has written numerous VIO and PM applications, including the Scramble applet included with OS/2 and the I-Brow/Magnify/Screen Capture trio included with the IBM Professional Developers Kit CD-ROM currently being distributed by IBM. Currently, he works for International Masters Publishers in Stamford, Connecticut and resides in Bellerose, New York with his wife Lisa. ═══ 7.4. Raja Thiagarajan ═══ Raja Thiagarajan was born in Madras, India in 1965, but moved to Bloomington, Indiana by the age of two in order to get a 25-year headstart on writing "An Unofficial Guide to the Palette Manager." Raja has college degrees in Astrophysics and Computer Science, but will never understand NASA's priorities, the C preprocessor, Scheme's call/cc, or Prolog's cut operator. Raja would love to hear from you; send e-mail to sthiagar@bronze.ucs.indiana.edu ═══ 7.5. Gavin R Baker ═══ Gavin R Baker Gavin R Baker is the man behind ThinkSoft, a consulting firm based in Melbourne Australia which specialises in developing custom software. He has experience in Assembler, Pascal, C, C++, (and a lot of other languages), and has worked with Unix, DOS, Windows, OS/2, VMS and Pick operating systems. He is an active member of Team OS/2. When he isn't programming, he is also a musician, an actor, and wastes lots of time reading Net News. He can be contacted thusly: net: demogrb@lust.latrobe.edu.au bix: gbaker cis: 100026,270 ═══ 7.6. Steve Lacy ═══ Steve Lacy is a third year computer science student at Carnegie Mellon University, and is currently overoccupied with school, and his job as an undergraduate research programmer for the Digital Mapping Lab at CMU. You can reach Steve via e-mail at sl31@andrew.cmu.edu. He would be glad to hear any and all of your comments about the previous article. Steve also spends his free time collecting CD's, and unfortunately spends far too much money in the process. Remember, diversity is knowledge, and personal gratification overrides the need for food in many cases..... ═══ ═══ Graphical User Interface An interface usually comprising multiple overlapping windows, icons, pull-down menus, and the mouse as a pointing device. It is recognised that the GUI was invented by Xerox at PARC (Paolo Alto Research Centre) in the late 60's (??). ═══ ═══ Dynamic Link Library An executable module which can be shared between multiple applications, containing code and/or resources. It is loaded dynamically at run-time rather than being statically linked at compile time. This reduces memory requirements and allows code independance and code sharing. ═══ ═══ Multitasking Where the CPU shares itself between more than one task, by giving each task a slice of processing time. ═══ ═══ Workplace Shell The Workplace Shell is the Object Oriented shell for OS/2. ═══ ═══ Application Programming Interface API is a general term for a set of functions which provide a standard set of services to an application. In this article, we refer to the OS/2 APIs as the calls to OS/2 and PM which make up the system services in the kernel and the GUI. ═══ ═══ Parent The parent-child relationship establishes a hierarchy of windows in the system. Child windows are clipped to their parent windows; that is no child window will apear outside the boundary of its parent. ═══ ═══ Owner The owner of a window will receive messages from the windows it owns.