WW WW WW PPPPPPPP JJ WW WW WW PP PP JJ WW WWWW WW PP PP JJ WW WW WW WW PPPPPPPP JJ WW WW WW WW PP JJ JJ WWWW WWWW PP JJ JJ WW WW PP JJJJJ ---------------------------------------------------------------- The Windows Programmer's Journal Volume 01 Copyright 1993 by Peter J. Davis Number 03 and Mike Wallace Mar 93 ---------------------------------------------------------------- A monthly forum for novice-advanced programmers to share ideas and concepts about programming in the Windows (tm) environment. Each issue is uploaded to the info systems listed below on the first of the month, but made available at the convenience of the sysops, so allow for a couple of days. You can get in touch with the editors via Internet or Bitnet at: HJ647C at GWUVM.BITNET or HJ647C at GWUVM.GWU.EDU CompuServe: 71141,2071 GEnie: P.DAVIS5 or you can send paper mail to: Windows Programmer's Journal 9436 Mirror Pond Dr. Fairfax, Va. 22032 We can also be reached by phone at: (703) 503-3165. The WPJ BBS can be reached at: (703) 503-3021. The WPJ BBS is currently 2400 Baud (8N1). We'll be going to 14,400 in the near future, we hope. LEGAL STUFF - Microsoft, MS-DOS, Microsoft Windows, Windows NT, Windows for Workgroups, Windows for Pen Computing, Win32, and Win32S are registered trademarks of Microsoft Corporation. - Turbo Pascal for Windows, Turbo C++ for Windows, and Borland C++ for Windows are registered trademarks of Borland International. - Other trademarks mentioned herein are the property of their respective owners. - WordPerfect is a registered trademark of WordPerfect Corporation. - WPJ is available from the WINSDK, WINADV and MSWIN32 forums on CompuServe, and the IBMPC, WINDOWS and BORLAND forums on GEnie. It is also available on America Online in the Programming library. On Internet, it's available on WSMR-SIMTEL20.ARMY.MIL and FTP.CICA.INDIANA.EDU. We upload it by the 1st of each month and it is usually available by the 3rd or 4th, depending on when the sysops receive it. - The Windows Programmer's Journal takes no responsibility for the content of the text within this document. All text is the property and responsibility of the individual authors. The Windows Programmer's Journal is solely a vehicle for allowing articles to be collected and distributed in a common and easy to share form. - No part of the Windows Programmer's Journal may be re-published or duplicated in part or whole, except in the complete and unmodified form of the Windows Programmer's Journal, without the express written permission of each individual author. The Windows Programmer's Journal may not be sold for profit without the express written permission of the Publishers, Peter Davis and Michael Wallace, and only then after they have obtained permission from the individual authors. Table of Contents Subject Page Author(s) ----------------------------------------------------------------- WPJ.INI ....................................... 4 Pete Davis Letters ....................................... 7 Readers Beginner's Column ............................. 10 Dave Campbell Install Program Part III ...................... 20 Pete Davis Home Cooking - C++ From Scratch ............... 22 Andrew Bradnan Creating and Using Owner Draw Buttons ......... 26 Todd Snoddy Hacker's Gash ................................. 30 Mike and Pete Special News .................................. 32 Mike Wallace Windows 3.1: Using Version Stamping Library ... 33 Alex Fedorov Book Review ................................... 36 Pete Davis Book Review ................................... 38 Mike Wallace Printing in Windows ........................... 40 Pete Davis Advanced C++ and Windows ...................... 45 Andrew Bradnan Trials and Tribulations Part 1 ................ 54 Jim Youngman Getting in Touch with Us ..................... 57 Pete & Mike Last Page .................................... 58 Mike Wallace Windows Programmer's Journal Staff: Publishers ......................... Pete Davis and Mike Wallace Editor-in-Chief .................... Pete Davis Managing Editor .................... Mike Wallace Contributing Editor ................ David Campbell Contributing Editor ................ Andrew Bradnan Contributing Writer ................ Dave Campbell Contributing Writer ................ Alex Federov Contributing Writer ................ Andrew Bradnan Contributing Writer ................ Jim Youngman Contributing Writer ................ Todd Snoddy WPJ.INI by Pete Davis Issue #3, wow... So far things have been going really well. The third issue is almost as big as the first and second put together! We've even got some extra articles for next month. We're getting a good response from the readers and the contributions are coming in. It seems like every day we get a letter from someone in another country. We've been getting mail from people in Romania, Czechoslovakia, Russia, Hong Kong, Australia, England, Germany, etc... In fact, the amount of mail we're getting is getting pretty high. We try to answer it all, but we don't always get a chance to. Also, some of the replies don't make it back. I've noticed this more often with certain Internet sights in the U.K. I don't know why that is, but the ones that have something like ac.uk in them don't seem to make it back. Sorry about that. The list goes on and on. Keep the mail coming, we love to get it. I mentioned in the last issue about readers suggesting different formats for the text. So far, the biggest response has been in favor of the WINHELP format and the plain text format. That way you can read it on- screen and, if you want, print it out. Speaking of printing it out, we've had some negative responses about the printing format. I have to take responsibility for that. I had the page length a bit too long and a lot of you were getting pages with three or four lines of text. I'll try not to let that happen again. As far as the Help format, because we can do bitmaps now, it would be nice if we could get someone who is artistically inclined (Not us, as you'll see in this first issue) to give us a hand with some of the graphics. You don't have to be a Renoir, just better than us. If you can draw stick figures, you probably qualify. So, this issue we'll be starting with the WINHELP format. We hope you like it. We're pretty pleased with the idea. Right now we're using minimal tools for getting it into the help format, but we're looking into getting some better ones later on. We'd like to thank the guys at America Online for giving us some time each month to get on and be able to stay in touch with our readers there. Mike will have more to say about all this in the Last Page. [See the "Special News" column - MW] This month I'm going to be doing my article on printing that I should have done last month. Sorry for the delay, but we've just been real busy. I'm also going to do the third article in the series on the install program. This article is basically going to give some insight into the data structure that we're going to use for storing data about each of the files for the install. This month David Campbell will be taking over the Beginners Column in C and we have Andrew Bradnan taking over the Beginners Column in C++. (We could still use someone to do the Beginner's Column in Pascal for Windows). - 4 - This brings me to another point. Beginner's Column might be a bit of a misnomer. Yes, they'll all be basic, but like anything else, as time goes on, the articles will progress. They will all be, eventually, an intermediate programmer's column. There's only so much that you can say about the basics and eventually you have to move on. We're starting a new feature this month called Hacker's Gash. Hacker's Gash is going to be a bunch of little tips and tricks that you can use in Windows. This is where the readers can really get in on the fun. Read through it, get an idea of what kinds of things we're doing and send in a list of your own tricks. Ah, good news, the BBS is finally up. Haven't replaced the hard drive, but I've done some serious work on it and did some pretty strenuous testing on it and it seems to be ok for now. I will need to replace it eventually, but we don't have the money right now for that. Besides being the first place to get the Windows Programmer's Journal, the WPJ BBS has the SIMTEL20 CD-ROM which has 600+ megs of public domain and shareware software. We will try to add a second CD-ROM in the near future and add some new disks. We'll probably try to add the CICA disk next, which has a very large library of Windows public domain and shareware software. The number's around here somewhere, but to make it easier, and as long as you're reading here, it's (703) 503-3021. (That's in the U.S. for you guys overseas.) Mike and I have debated doing this, but we're really getting to the point where we don't have much choice. Seeing as we're going into the Windows Help format, which will allow us to support graphics, we've decided that we're going to start allowing some advertisers in the magazine. We're going to keep the ads to a minimum, but, even though the magazine is free, there are some costs involved in putting it together and we really need some reimbursement for it. Right now, our Compuserve and Genie bills alone are costing us a fair amount. Running the BBS will cost us $25/month in phone bills alone, not to mention possible hardware maintenance. We're hoping to get a few advertisers to help offset some of those costs. The ads will be, most likely, from shareware authors. If you are a Windows shareware author and you're interested in advertising, get in touch with us and we'll discuss the costs, size, design, etc. One last thing before I wrap up. We've gotten a lot of comments about the format of the magazine. Most of these were in reference to the format we're going to distribute it in. If you have preferences about the layout or other stylistic concerns, we'd like to hear them. That's really about it for this month, I guess. We just wanted to thank all of you for reading and to keep the comments and suggestions coming. We'd also like to thank those of you who have submitted articles and we ask that you please keep the articles coming in. It would be nice if we got a few more submissions each month so we could get the magazine up to the size that we'd really like. I think 50 pages would be a good size, although, with something like this, I suppose, the bigger the better, so more than 50 pages would be fantastic. Well, I've said enough. Enjoy the magazine. - 5 - _Pete Davis P.S. I just read Mike's Last Page (He does it last, so I didn't see it when I was writing the WPJ.INI) You might want to read that and then come back to this, but I wanted to give my own thoughts on what Mike said regarding the WPJ as a source of information. We are not here to be a sole source of information, nor will we always be correct. We do our best to make sure that the information we give out is correct. The only people who check the submissions are Mike and myself. Neither one of us has a PhD in Windows Programming. We both make mistakes. The WPJ is going to have errors in it. We're going to give out completely wrong information at times. When that happens, we will try to correct ourselves by the next issue (usually in response to someone saying "You guys screwed up!"). What I'm saying is that there are a lot of sources out there. You need to check out as many as you can. Cross-reference the information between different books and magazines and when you see an inconsistency, that's probably a fault. Mike and I have several reasons for doing the WPJ. First of all, we felt beginners weren't being addressed well enough. Also, we wanted to cover as many Windows programming languages and environments as possible. (We're getting there, but slowly.) Most important, I think, we wanted to address Windows programming and Windows programming only. Windows is an enormous programming environment and there's tons of information to absorb. No one publication can even approach covering it all. I like to think that one of our advantages is that, unlike a lot of other programming magazines, we don't center on a topic each month. Other magazines might have, say, an issue on Custom Controls or an issue on Multi-media. Well, that's great, but what about those of us who don't work with Custom Controls or Multi-media? There are two issues that we don't care for. We try to keep a variety of topics each month so we can appeal to as many people as possible with each issue, but I digress. To wrap up, I just want to say, (this feels like deja vu, I'll check later, but I think I said this in the last issue also) don't count on us being right every time. If something we wrote about doesn't work, try it again. If it still doesn't work, we probably screwed up. Let us know, and we'll try to correct it by the next issue. I know, I'm getting repetitive. I know, I'm getting repetitive. Until the next issue, peace. - 6 - Letters Date: 10-Feb-93 07:04 EST From: Chris Newham [100027,16] Subj: Windows Programmer's Journal Hi, I would just like to say that I have enjoyed the two issues of WPJ so far. You can add at least 1 to your readership numbers as my friend and I download it once between us. The sections on DLLs & install progs are of particular interest to us as we have just finished work on a DLL and are now working on the install prog. A point that I think you might mention and give some consideration to for the install prog is this: If you are installing DLLs then you need to check if the file exists first (easy); if it exists you then need to check its version control information to determine if the copy you wish to install is newer (also quite easy); the difficult part comes when you have determined that you need to replace the existing DLL but the DLL is already in use by Windows. Windows will not let you delete or replace the file. Microsoft's install program works around this but we haven't yet worked out how; we think that it copies the new DLL to a temp dir or hides it somewhere then replaces it when Windows next starts up. It's an interesting little problem and has at present got us stumped. If you know how to solve it let's see it published. If we find a solution we will let you know. [We're working on this problem, too. - MW] I am very impressed by the quality of the journal and would like to see it remain in either text or Windows write format as my time on the PC is limited and so what I do is take a hard copy of the journal to work to read at lunch time so Winhelp format would not suit me. Keep up the good work Best wishes Chris. - 7 - Date: 12-Feb-93 17:29 EST From: Tammy Steele Subj: WPJ Hi Mike, I just downloaded my mail today. I generally only read it once a month. So, sorry for the delay in my comments about WPJ. I had a chance to skim the first issue and have just spent some time reading through the second issue. Of course I cannot make official "Microsoft" comments, but I can give you my opinion. Generally speaking, I think the WPJ will be another good place for us to point people to for information. One problem will be getting the Microsoft support engineers familiar with the content of the journal. Another problem is the accuracy of the articles. In order for Microsoft to point people to the articles, we need review the accuracy of them. We are pretty swamped now with too much to do, not enough people. I am sending mail to my group about the journal. So we'll see what happens. Specifically, I thought it was good that you discussed the feedback you received from the first journal. Especially the Linked List sample. If the sample were used for a large number of nodes, as I'm sure someone probably mentioned, it would not be a "cooperative" windows app because each globalalloc eats a selector and the selectors (8K of them) are shared between *all* windows apps and the Windows system itself. Also, it would be useful for samples to be updated to the current software although there is still value to having the code and comments (ie, the DLL article.) People who submit articles should check the MSKB and if they have areas that they are uncertain about, should post questions in the forum before they submit articles. For example, the DLL article talks about WEPS and says something like "the documentation says the WEP is called once...but I haven't seen it be called in Codeview in Windows 3.0." There is an article that discusses the reason why you can't see it be called in Codeview under Windows 3.0 and/or this question could have been easily answered in the DLL section on WINSDK. [This question is answered farther down in this column. - MW] I like the coding style and the sample makefiles (the makefiles are easy to read.) I'm sure you've had lots of comments, as you mentioned, about how to format this. I personally think you should offer it in helpfile format *and* text format so that people have the option. [editors note: This seems to be the most common opinion.] Overall, I see this journal as a really great place for people to compile information and share it. Thanks for all your work, Tammy - 8 - Editor's Note: We got a letter from Craig Derouen (CompuServe ID: 75136, 1261) of Seattle. I'll reprint his letter here and let him explain his BBS: I've looked at your 2 first issues of WPJ and am impressed. I run an extensive programmer's BBS out here in Seattle, primarily Windows code but also C, C++ and 8086 code for DOS. I carry both your issues. I am also in Fidonet and have all the listings for WPJ available for Frequest. Also I am a support BBS for WinTech,Windows/DOS,CUJ and PC Techniques Magazine. I have a LOT of code on line. Anyways here's the details: Cornerstone BBS (2400,9600 baud) (206) 362-4283 Fidonet 1:343/22 All magazine listings are available for first time callers; free downloads and some file requests are also available. --------------- Editor's Note: In our last issue, we had an article by Rod Haxton on DLLs. In the article, he mentioned that he had never seen WEP get called under CodeView in Windows 3.0. So, I posed the question to the WINSDK forum on CompuServe and got a response from Brian Scott of Microsoft. Thanks, Brian. Mike, If you implicitly link to a DLL in Windows 3.0, the WEP is called after the application has unloaded. Since Codeview stops debugging after the application terminates, it does not see the WEP get called. If you need to debug the WEP, you can load it using LoadLibrary() and free it using FreeLibrary(), instead of linking to the import library. In this case, you will see the WEP get called when you call FreeLibrary(). If you need to link to the DLL using an import library, then you can debug the WEP using something like WDEB386, putting OutputDebugString() statements in your WEP, or moving the code in the WEP into a cleanup function that you call just before the application terminates. Hope this helps, Brian Scott - Microsoft - 9 - Editor's Note: Pete and I started a beginner's column with the first issue, and Dave Campbell offered last month to take it over. Dave is a confirmed Windows hacker and so will be writing this column starting with this issue. We hope you like it. Questions should be directed to Dave. Means of reaching him are given at the end of his column. Beginner's Column By Dave Campbell My name is Dave Campbell and I write Windows software under the business name WynApse. I am going to be writing the beginner's column until I get buried in work (wouldn't that be too bad?) or I get usurped by someone who wants to do this more than I do. Up until recently, I have been a Borland programmer, so the tools I am most familiar with are the Borland C++ suite, 3.0 and 3.1, in particular. Pete and Mike's previous two columns have been Microsoft, and I am leaning that way myself right now, so I will continue that. My intention is to provide code and ideas that aren't tied to Borland or Microsoft, but could be used with either compiler. A major difference between the two is make files, so I will stay away from those as much as possible. My intention is to write code that is useable on 3.0/3.1 Windows. If enough questions come in for 3.1-specific applications, we will cover them. This leads directly into the request for requests. I would like to present code that is useful to all the readers, and the best way would be to be responsive to readers questions. Please send questions! I will list various ways of reaching me at the end of each article. Now on to the fun stuff...Mike and Pete have been working on a Hello World program for a few issues, and I am going to add some configuration to that code and demonstrate some basic uses for radio buttons and INI files. Radio Buttons A radio button is one of several control types available to Windows programmers inside dialog boxes. They work similar to the buttons on your car radio...only one may be pressed at a time, at least in this application. There are other controls available in the basic set, and we will look into them later. For now, let's look a little deeper into the declaration of a radio button. Radio button declarations are made in the .RC file, for example: CONTROL "Hello", IDM_HELLO, "BUTTON", BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 20, 15, 42, 12 We will use one more button in our dialog box, and that is a pushbutton. The ever-present OK button to be precise. The control is a graphical - 10 - representation of a 3D button with text written on it. By making a pushbutton an "OK button", we are really painting the word OK on the face of a 3D pushbutton. The button definition we are going to use is: CONTROL "OK", IDOK, "BUTTON", BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 29, 57, 24, 14 The explanation of the fields in these two declarations is covered later in this article. INI files INI files are used by Windows to control the environment under which Windows executes. Windows programs use INI files to manage the individual environmental changes available to users to customize the system to his needs. There are two ways to read a variable from an INI file, but only one way to write information into one: Reading: ReadPrivateProfileString ReadPrivateProfileInt Writing: WritePrivateProfileString This does seem clunky, but it's what we've got to work with. When we get to that point, I'll demonstrate that it's not that big of a problem. Windows handles the INI files very nicely, in that you will always get something for your efforts. In the Read calls, a default value is given, in case the variable does not exist in the INI file (or the INI file doesn't exist, for that matter). In the Write call, if the variable doesn't exist (or even if the INI file doesn't exist) prior to the call, it will when the call returns. This means that you needn't be concerned about the pre- existence of an INI file before trying to use one. Let's consider an INI file for our application, HELLO.INI: [Hello] Setup=0 This is the simplest form of an INI file: there is a single 'Application Name', [Hello], and a single 'Keyname', Setup, that is associated with it. The value for Setup is 0. During execution of our program, if we had a need for picking up the Setup value, we would simply use the line: nDflt = ReadPrivateProfileInt("Hello", "Setup", 0, "hello.ini"); - 11 - where nDflt is defined: int nDflt; This reads from hello.ini, looking for the 'Setup' keyword under the 'Hello' application name, and assigning the value equivalence listed there, or 0 for default, to the variable nDflt. If, however, the INI file was defined as: [Hello] Setup=Hello World you now cannot read the file with ReadPrivateProfileInt. Now the line to read the value is: char szDflt[25]; ReadPrivateProfileString("Hello", "Setup", szDflt, "Goodbye World", 13, "hello.ini"); This has some similarities, and some differences from the one above. The "Hello", "Setup", and "hello.ini" are self-explanatory, but the string read also adds other parameters. The string name to store the result into is listed as the third parameter, in this case szDflt. The fifth parameter is an integer variable declaring the maximum number of characters accepted from the INI file, and the fourth parameter, in this case "Goodbye World", is the default value. If this line were to be executed and the INI file not even exist, szDflt would contain the string "Goodbye World". This is also the case if the file exists but does not contain either [Hello] or Setup, or both. To change an INI file, the WritePrivateProfileString function must be called: char szDflt[25]; int nDflt; wsprintf(szDflt, "%d", nDflt); WritePrivateProfileString("Hello", "Setup", (LPSTR) szDflt, "hello.ini"); 'wsprintf' should be used in place of 'sprintf' because it is already in Windows, and will not cause the linking to another library. The downside is the need to cast the string as a LPSTR. wsprintf will build a string, in this case, containing the ASCII representation of the single integer nDflt. That string is then passed to the INI file using the syntax shown. If the Setup variable were a string, the variable to be inserted would be given in the WritePrivateProfileString call rather than using the intermediate wsprintf step. Whew, I'm glad that's over. - 12 - Dialog Box The dialog box we're going to display will have two radio buttons, and an OK button. Typically, the buttons to complete a dialog box are located either along the bottom, or if the dialog is very busy, they can be placed along the right edge. Of course, this has nothing to do with programming Windows. This is all aesthetics and being kind to users. Look at a thousand Windows applications, and you'll get used to seeing things a certain way. Users get used to them being that way, and you will lose some people just with your user interface, or lack thereof. The code I propose for the dialog box is: Hello DIALOG 63, 56, 83, 77 STYLE WS_POPUP | WS_CAPTION CAPTION "Hello Setup" FONT 8, "Helv" BEGIN CONTROL "Hello", IDM_HELLO, "BUTTON", BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 20, 15, 42, 12 CONTROL "Goodbye", IDM_GOODBYE, "BUTTON", BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 20, 28, 42, 12 CONTROL "OK", IDOK, "BUTTON", BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 29, 57, 24, 14 END This code can be produced either by hand with any text editor, or by graphically placing the objects with Borland's Resource Workshop or some similar tool. I usually start with the Resource Workshop, and make small tweeks by hand. Large changes are best done graphically to ensure the placement, and logical usefulness of the dialog box is alright. I am going to take a break right here and talk about coding style. This is something that is as peculiar to a person as their name. Everybody has their own, and I wouldn't think of trying to force my ideas on someone. But...(have you ever noticed there's always a big But around somewhere?), if you don't have your mind made up yet about those long Windows lines like the 100+ character ones above, please split them onto succeeding lines! Line-wrap on listings is UGLY!! OK, that's over, I've got it out of my system. But (another one), since I am writing this, you are going to have to put up with my style... I'm done now. That short dissertation was free, now back to the program. The field explanations are as follows: Hello DIALOG 63, 56, 83, 77 This line defines the name, "Hello" of the dialog, and the coordinates of the box relative to the client area of the parent window. The units are NOT pixels, and beyond that, I don't want to get into it now. Just play with it for now until you get it where you like it. The first two numbers are the x,y coordinates, and the second two are the width and height of the - 13 - dialog. STYLE WS_POPUP | WS_CAPTION The STYLE line defines the manner in which the dialog box is built. WS_POPUP is pretty standard for dialog boxes. There are real differences between POPUP windows and OVERLAPPED windows, the main one being the CAPTION on a POPUP is an option. Because we are going to call this as a modal dialog box, we are going to give it a caption bar to allow it to be moved. 'Modal' dialog boxes, as opposed to 'modeless', disable the parent window, and demand attention until closed. Without the caption bar, the box cannot be moved. CAPTION "Hello Setup" The CAPTION line declares the caption used with the WS_CAPTION style parameter. FONT 8, "Helv" This defines the default font used throughout the dialog box. You could pick any conceivable font here, but it is best to be kind and only use those shipped to Windows users, or you are going to get some nasty mail messages. BEGIN BEGIN..END or {..} define the body of the dialog box. CONTROL "Hello", IDM_HELLO, "BUTTON", BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 20, 15, 42, 12 CONTROL "Goodbye", IDM_GOODBYE, "BUTTON", BS_AUTORADIOBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 20, 28, 42, 12 The two radio button definitions following the declaration CONTROL are: - The text of the control, in our case "Hello" or "Goodbye" - The ID value to be returned to the parent window, IDM_HELLO or IDM_GOODBYE are defined in our ".H" file. - BUTTON declares the control class. Buttons are generally small child windows. - BS stands for BUTTON STYLE, and BS_AUTORADIOBUTTON declares the buttons will only be pressed one at a time, and the button is automatically checked. - WS_CHILD declares the dialog box as a child window. This means it resides within the boundaries of the parent window. - WS_VISIBLE applies to overlapped and popup windows. The initial condition - 14 - is visible. - WS_TABSTOP specifies that the user may step through the control sequence with the tab key. - 20, 28, 42, 12 are the coordinates and size of the control, similar to that of the dialog box itself. CONTROL "OK", IDOK, "BUTTON", BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 29, 57, 24, 14 The OK button declaration: everything should be self-explanatory except the BS_DEFPUSHBUTTON style. DEFPUSHBUTTON means that this is the default PUSHBUTTON. Pushbuttons are: OK, CANCEL, ABORT, RETRY, IGNORE, YES, NO, and HELP. Only one may be the default, and it has a dark border around it, so that if the Enter key is pressed, that is the one you get. IDOK is defined in windows.h, so we don't have to declare it. The user will get this dialog box on the screen, and select one of the radio buttons, or toggle them back and forth a few times, then select OK. The dialog box procedure should read the INI file and preset the proper radio button to show the user the current value. Upon selecting OK, the procedure will have to set the selected information into the INI file. That completes the dialog box procedure discussion. The code follows: BOOL FAR PASCAL HelloDlgProc (HWND hDlg, WORD message, WORD wParam, LONG lParam) { switch (message) { case WM_INITDIALOG : CheckRadioButton(hDlg, IDM_HELLO, IDM_GOODBYE, InitSettings); return TRUE; case WM_COMMAND : switch (wParam) { case IDM_HELLO : WritePrivateProfileString("Hello", "Setup", "1", "Hello.ini"); InitSettings = wParam; break; case IDM_GOODBYE : WritePrivateProfileString("Hello", "Setup", "0", "Hello.ini"); InitSettings = wParam; break; case IDOK : EndDialog(hDlg, wParam); - 15 - return TRUE; } break; } return FALSE; } /* HelloDlgProc */ Notice the WM_INITDIALOG call: CheckRadioButton(hDlg, IDM_HELLO, IDM_GOODBYE, InitSettings); An assumption is being made here that the variable InitSettings has been read into our program somewhere, and is set to (in our case) either IDM_HELLO or IDM_GOODBYE. CheckRadioButton uses the dialog box handle hDlg to identify a numerical sequence of buttons from IDM_HELLO to IDM_GOODBYE, and ensures that only InitSettings is set. More advanced Windows programmers will probably want to skip the case IDM_HELLO . case IDM_GOODBYE statements altogether, and wait until OK is pressed to check the state of the buttons. That is fine, and more streamlined, but for now, let's not leave anyone behind. I want all of us to learn this stuff. Let's change one more function, or else the whole idea of the dialog box selecting radio buttons is wasted. Let's read the INI file in WndProc, and change the text displayed based upon the Select value. This is going to take two steps, just like the two parts of the previous sentence: case WM_CREATE : InitSettings = GetPrivateProfileInt("Hello", "Setup", 1, "Hello.ini"); InitSettings = InitSettings ? IDM_HELLO : IDM_GOODBYE; hdc = GetDC(hWnd); InvalidateRect(hWnd, NULL, TRUE); ReleaseDC(hWnd, hdc); and: /*------------------------------------------------------------------------- fall through to WM_PAINT... -------------------------------------------------------------------------- */ case WM_PAINT : hdc = BeginPaint(hWnd,&ps); /* returns pointer to hdc - 16 - */ GetClientRect(hWnd, &rect); /* -1 tells the DrawText function to calculate length of string based on NULL-termination */ DrawText(hdc, (InitSettings == IDM_HELLO) ? "Hello Windows!" : "Goodbye Windows!", -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER); EndPaint(hWnd,&ps); return 0; The WM_CREATE case reads the INI file to find out which we want printed and sets up the 'InitSettings' variable used in the dialog box. Then we get a "handle to a device context" or hdc for our window. A device context is the area into which we "paint" text in a window. In a dialog box, we can use wsprint, but in a window, we have to "DrawText", and we draw it into a device context. A device context could be a window or a printer page, Windows doesn't care. At this point, we are getting the device context for our main window's client area. We then report to Windows that the entire area is invalid, which will set us up for a repaint. In handling the WM_PAINT case, we again need an hdc, and this time do it with a call to BeginPaint, passing a pointer to a structure variable of type PAINTSTRUCT. BeginPaint has Windows fill in the ps for us, and is then available for our use. We aren't going to use it, however. We call GetClientRect to get the dimensions of the client area into the rect structure. DrawText uses the hdc, the rect structure, and the InitSettings value to decide what to paint and where. Ultimately, either "Hello Windows!", or "Goodbye Windows!" is printed on a single line, centered horizontally and vertically: DT_SINGLELINE | DT_CENTER | DT_VCENTER. Notice the note above the DrawText line. Instead of telling Windows how many characters we are painting, let Windows do it for us! EndPaint closes the ps structure, and finishes our WM_PAINT case. The Setup dialog I almost forgot, we do need to get the dialog box onto the screen. I have added three lines to the hello.c file: hMenu = GetSystemMenu(hWndMain, FALSE); AppendMenu(hMenu, MF_SEPARATOR, 0, NULL); AppendMenu(hMenu, MF_STRING, IDM_SETUP, "Setup..."); These get a handle to the system menu of the window, and insert a menu - 17 - separator followed by the word "Setup...". When "Setup..." is chosen, the value IDM_SETUP is sent to our windows message loop: case WM_SYSCOMMAND : switch (wParam) { case IDM_SETUP : lpfnHelloDlgProc = MakeProcInstance(HelloDlgProc, hInst); DialogBox(hInst, "Hello", hWnd, lpfnHelloDlgProc); FreeProcInstance(lpfnHelloDlgProc); return 0; } break; This is handled as WM_SYSCOMMAND, because the system menu is the one used. We must get a long pointer to a function, lpfnHelloDlgProc, to use in the DialogBox function call. The parameter "Hello" in the call is the title of the dialog box we built. Because of the 'DialogBox' call, this dialog box will be modal, and control will not return to the main window until OK is pressed in the dialog box. Don't forget the classical Windows programmer's bug...any ideas?? Ok, since nobody is raising their hand...it's exporting the dialog box in the .def file. I have forgotten this so many times, I hesitate to admit it. If you don't list the dialog box in the .def file, it is guaranteed not to work in Windows 3.0, and will be extremely unreliable in 3.1. The next time you get an unreliable dialog box, remember the "classical Windows programmer's bug". The files are archived with the magazine. I have made one other excursion, and that is the dialog box code is in a separate file named HELLO.DLG. The file HELLO.RC contains the following line to include the .DLG file: rcinclude Hello.dlg This is pretty standard among Windows programmers, because it keeps the dialog box code in its own file. 3.0/3.1/Borland I just checked and found that I had produced great 3.1 code, but the thing wouldn't run under 3.0. If you are programming for the general public, you better keep at least a 'virgin' copy of 3.0 around on your hard disk for this sort of checking. Users tend to get a little touchy about that sort of thing. The fix is really easy for Microsoft people. The problem lies in the Resource Compilation stage. If you type 'RC -?' at the command line, you will see a '-30' switch listed. This is what will get you - 18 - 3.0+ executables. That is the way I have it in the make file. Borland 3.1 programmers have a couple more lines. In addition to the RC file change, you must also add the line: #define WINVER 0x0300 ahead of your include of windows.h in your hello.c file. Also you must change the wc.lpfnWndProc = WndProc; /* Name of proc to handle window */ line to be: wc.lpfnWndProc = (WNDPROC)WndProc; /* Name of proc to handle window */ I may have left off some stuff here, if so, let me know about it. We're all in this thing together. Please hang in there. If you are beyond the scope of this article, stick with me we are going to go places together. If you are way beyond this, write us an article. If you are bogged down, just compile it, and stare at the source. If you want help with something, send me a note. That's it for this time. Next month I plan on building an About box with live compile date inserted, and I'll discuss Icons and Menus. In coming issues my intention is to discuss File Open boxes, Help files, Dialog Boxes as main windows, Obscure Dialog Box uses, Timers, and debugging. Feel free to contact me in any of the ways below. I want to rat out the things other people are having questions about, not just what I think people want to hear. Dave Campbell WynApse PO Box 86247 Phoenix, AZ 85080-6247 (602)863-0411 CIS: 72251, 445 Phoenix ACM BBS (602) 970-0474 - WynApse SoftWare forum - 19 - Install Program Part III: The SETUP.INF File by Pete Davis Before I get started this month, there are a couple of things I wanted to talk about. First of all, because of some stuff coming up in the near future, I won't be able to do Part IV of the install program next month. It will, however, continue in May. Sorry about this, but Part IV is going to be a big one and I'm not going to have the time to do it yet. I'd also like to respond to Chris Newham's letter (found, oddly enough, in the Letters section) regarding installing DLLs which are currently active. I have to admit I haven't yet tried this, but plan, by Part IV to have a solution. I went through Microsoft's code for their install program and I couldn't find anything that seemed to make exceptions for DLLs. This leads me to believe that the solution is something fairly simple. If worst comes to worst, you could always just find out who's using it and shut them down. I doubt this is a very good way to do it, but it's just a thought. Like I said, I'm going to look into it and I can hopefully have a solution to it by Part IV. (I better have a solution by then, 'cause Part IV is going to handle copying the files over.) Ok, this one's going to be short and sweet. There's not much to it. We're using a SETUP.INF file which is going to tell us what the name of our application is, how big it is, what files have to be installed, what disks they're on, whether or not they're executables, etc. I had two options for doing this. I could have used the GetPrivateProfileString and GetPrivateProfileInt functions and make it into a .INI file, but I wanted to maintain Windows 2.0 compatibility. (Just kidding :-) Actually, one reason I didn't is because I didn't think of it until it was too late. Actually, there are some problems with that approach. The problem is that we're dealing with multiple files and they're going to have the same entry names. I'm sure you understand completely now, right? Ok, here's an example of a SETUP.INF file and then I'll explain it again. ; Semicolons, as is typical for these kinds of files, mean comments follow ; and the line is ignored. Application=My Application AppSize=1204558 DefaultDir=\MYAPP ; ; Now we'll have information about each file ; ; CompName = Name of file compressed on install disk ; UCompName = Name of the file when we uncompress it. ; FileType = 0 - EXE 1 - DLL 2 - Other (Other is the default) ; FileSize = Uncompressed file size ; AddDir = Sub-directory name if it's a sub-dir of the DefaultDir ; (i.e. AddDir=\DATA would mean the file is in \MYAPP\DATA CompName=MYAPP.EX0 UCompName=MYAPP.EXE FileType=0 - 20 - FileSize=10294 CompName=DATA1.DA0 UCompName=DATA1.DAt FileType=2 AddDir=\DATA Ok, that should be enough for a sample. Now our code is going to start a separate node in our linked list of files to install each time it hits a CompName= statement. This is harder to do with the GetPrivateProfile... functions. We could throw in a new section like [FILE1] for the first file, [FILE2] for the second, etc. But back to the topic, we're not doing it that way. I just wanted to give you ideas of how it could be done if you choose to do it that way. All right, well, that was all simple enough. Did I mention linked list in the last paragraph? Yup, that nasty phrase!!! Ok, it's pretty simple linked list. To make it easier, it's essentially a stack, so each time we get a new file, we just add it to the front of the list. The code is in the READSET.C file and the FILES.H file. I've commented it pretty heavily, so I won't go in depth here. It's all very simple. In Part IV, which will be in May, we're going to do a lot of the real work. Like I said, it's going to be a big one, and we're going to be tying in all the stuff from the first three parts. I might have to finish the entire thing in June, just because what's left is so big. I've tried to break this series up into easily recognizable parts, First I had the DDE with Program Manager, then I had the LZEXPAND.DLL part, and this month we had the READSET stuff. I feel like what's left all goes in the category of 'the rest of it', but there's so much, that I'll have to break it up into two hard do divide sections. I'll probably just do a few of the small things in each one, like the Progress Bar, creating directories, copying files, etc... Oh well, that's it for this month. Sorry it was so short and sorry I can't do it next month. If I can, I might try to do all of the rest in May to make up for not doing it at all in April. We'll see. Until next time...... - 21 - [Editor's Note: Last month, I started a Beginner's column on C++. Andrew Bradnan was kind enough to offer to take over the column, and will be writing it starting with this issue. Any questions you have can be directed to him. His CompuServe ID is given at the end of his column. - MW] Home Cooking - C++ from Scratch by Andrew Bradnan This month I'll be starting a new column WPJ will be doing every month to introduce people to C++ and Windows. I am going to try and keep this simple enough for a programmer new to Windows programming and C++. Your brain may melt if you are at this stage, but give it a try and it will sink in after a while. Feel free to send me some email on Compuserve [70204,63]. I'll answer any questions and put the good ones at the end of next month's article. I've read many books on C++ and I considered all but a few totally confusing. (I don't plan on adding my name to this list.) There is another rumor that C++ is really slow because it writes all sorts of code behind your back. This is kind of like complaining that C writes all kind of assembly behind your back. The compiler is supposed to write code for you. The great thing about C++ is that you can forget many of the details. I usually forget them all by myself. Instead of me rambling how great C++ is, let's find an example of exactly how we can forget some things. On purpose. The simplest place to start is constructors and destructors. Of all the confusing C++ terms (abstraction, encapsulation, modularity, hierarchy, typing, concurrency, and persistence), constructors fall into the abstraction and encapsulation category. More later. Since we almost always want to initialize our data structures (objects) to some known state, C++ will automatically call your own constructor every time. The converse is also true. When you delete an object, or it goes out of scope (falls out of a curly braces), you also want to free up memory, and set your variables to a used state. Where in the bleep do they dome from? You only have to do two things. First declare your member functions, and then define them. Piece of cake. For an example, Windows is kind enough to require us to create and destroy all sorts of stuff. There are over 63 calls to create something in the Windows API. All these objects (bitmaps, etc.) have to be closed, deleted, freed, or destroyed (pick your favorite synonym). Since we almost always trap the WM_CREATE message lets create a PAINTSTRUCT object. We'll call it a PAINT. If you want to put anything on the screen, you have to ask permission. Since Windows is kind enough to let many programs run at once, they have to share the screen. Kind of like kindergarten, and Windows is the teacher. To get permission you have to ask for a display context (think constructor). Your piece of the screen. A display context is provided when we call BeginPaint(...). Due to the limitations of DOS and/or Windows you can only have five DC in use at once. So after you are done you have to release the DC so that another program can draw in his corner of the sand box (think destructor). This is done with EndPaint(...). BeginPaint (...) also fills in a PAINTSTRUCT data structure. The only additional information filled in is whether you should - 22 - paint the background, and the smallest rectangle you have to paint. In my tradition, let's write some code that uses our PAINT object. This will clarify what advantages we'll gain and what member functions we are going to have to write. // // PAINT Object Test // Written by Andrew Bradnan (c) 1993 // Note: The only C++ code is in the WM_PAINT case statement and in PAINT.H #define STRICT #include #include "paint.h" char szAppName[] = "DC Test" ; long FAR PASCAL _export WndProc (HWND, UINT, UINT, LONG) ; int PASCAL WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow) { HWND hWnd ; MSG msg ; WNDCLASS wc ; if (!hPrevInstance) { wc.style = CS_HREDRAW | CS_VREDRAW ; wc.lpfnWndProc = WndProc ; wc.cbClsExtra = 0 ; wc.cbWndExtra = 0 ; wc.hInstance = hInstance ; wc.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wc.hCursor = LoadCursor (NULL, IDC_ARROW) ; wc.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wc.lpszMenuName = NULL ; wc.lpszClassName = szAppName ; RegisterClass (&wc) ; }; hWnd = CreateWindow (szAppName, "DC Test Program", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL); ShowWindow (hWnd, nCmdShow); UpdateWindow (hWnd); while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; }; return msg.wParam ; }; - 23 - long FAR PASCAL _export WndProc (HWND, UINT message, UINT wParam, LONG lParam) { switch (message) { case WM_PAINT: { PAINT Paint (hWnd); // PAINT constructor called here!! DrawText (Paint, "PAINT Test!!", -1, Paint, DT_SINGLELINE | DT_CENTER | DT_VCENTER); // BOTH cast operators called! }; // PAINT destructor called here!! return 1; // Everything Created OK case WM_DESTROY: PostQuitMessage (0); return 0; }; return DefWindowProc (hWnd, message, wParam, lParam) ; }; As you can see we have gone from four lines of code to two. Not too bad. The interesting part is that we no longer have to call BeginPaint or EndPaint, and we no longer care what those pesky PAINTSTRUCT members are. The compiler grabs it from the PAINT object for us, calling our declared explicit cast operators. There is also sort of an intentional bug. If you cover up only part of the client window and then bring the sample application back to the foreground, "PAINT Test!" doesn't draw in the middle of the client window. "PAINT Test!" will draw in the middle of the rectangle that was covered up. Windows is kind enough to let you do a little optimization should you want to. From the sample code above we can determine that we need a PAINT object with four member functions. One constructor, a destructor, a cast to an HDC, and a cast to a LPRECT. // // PAINT Object Header // Written by Andrew Bradnan (c) 1993 // #ifndef __PAINT_H #define __PAINT_H #ifndef __WINDOWS_H #include #endif class PAINT { public: PAINT (HWND p_hWnd) : hWnd (p_hWnd) {BeginPaint (hWnd, &ps); }; ~PAINT () { EndPaint (hWnd, &ps); }; operator HDC () { return ps.hdc; }; operator RECT FAR * () { return &ps.rcPaint; }; operator BOOL () { return ps.fErase; }; - 24 - protected: HWND hWnd; PAINTSTRUCT ps; }; #endif // __DC_H As you can see, strangely enough all the code is written in the header file. This is called inlining. The short story is that this allows the compiler to optimize your source code. It does this by replacing the function call with the code you have written. So our call to DrawText () really won't call two functions to get the parameters it will just reference the members in our PAINTSTRUCT ps. It is essentially like using a macro except you get all the type checking thrown in for free. Some statement will not inline but your compiler will let you know what these are. You will also note that the constructor looks a little weird. In the constructor, we can optionally tell the compiler how to initialize our members hWnd and ps. If we do not the compiler will create them, set all the members to zero, and then we would initialize the member within the curly braces. Obviously one more step than you want. To initialize hWnd we use the parameter passed in. Memory for hWnd is allocated and then filled with the parameter passed in. Which is exactly what we wanted. Space for ps is allocated, set to zero, and then we initialize it, using BeginPaint, in the function body. An extra step but it can't be avoided in this case. So the moral of the story is that C++ code can be quite easy to read when you are using the object. Writing the actual code is a little messier. Just remember you only have to get it right once. You can forget two function calls and three PAINTSTRUCT member names. You can even forget about PAINTSTRUCT. Readers familiar with some of the brand name applications frameworks may be wondering why I did not add DrawText() as a member function to our PAINT object. Doing this we could just remove the two cast operators and call DrawText with new parameters - DrawText (LPSTR szText, int cb, UINT fuFormat). The only problem is we have to learn more than we forget. Now you have two versions of DrawText(). This is supposed to be easier not more complicated. The second reason is that you can't DrawText a PAINT. English wise, this makes no sense. You can draw some text on a DC. That would make sense. You would also end up with every drawing function listed under the PAINT and/or DC object. Definitely confusing. Just look at Microsoft's AFX. Yuck! I have built the above example using BCW 3.1. With minimal changes it ought to work fine with MSC 7.0 and with Windows NT. Next month I'll start a little earlier so I can test it on all three platforms and send the appropriate make files to make your life easier. Again if you have any problems, questions, suggestions, or answers send me a note on Compuserve [70204,63]. Andrew Bradnan is president of Erudite Software, Inc. Their latest offering, which he wrote using C++, is called Noise for Windows, a sound utility for Windows 3.1. Feel free to contact him for information. - 25 - Creating And Using Owner Draw Buttons By Todd Snoddy Many of you may be wondering how some Windows programs display the fancy bitmapped buttons. Although these are not standard controls, Windows does provide the capability to utilize controls that are custom designed. Many times a properly displayed graphic can mean much more than a simple text word. I will try to explain how you can use your own custom buttons in your programs. My sample code is written in Turbo Pascal for Windows, although the techniques apply just the same to Turbo C++ for Windows. I use Borland's Object Windows Library in my examples, but if you use Microsoft Foundation Classes it should still be possible to understand the basic concepts. My code emulates some of the functions of Borland's BWCC.DLL, which many people use to enhance the visual appearance of their programs. This DLL provides an assortment of functions that can give your program that extra visual 3D look without too much work on your part. Unfortunately, the size of the DLL can be a big factor in deciding whether or not to use it, especially if you are writing a shareware program and want to keep its size down. You may also have your own reasons for not wanting to use the DLL, and this may cause you to look for other solutions. I will demonstrate how to use what is called owner drawn controls. My examples show how to simulate the BWCC style buttons from BWCC.DLL using owner drawn buttons. If you want to use owner drawn buttons in your programs, it should be rather straightforward to use my code "straight out of the box" or modify it to suit your needs. We'll start with the obvious question. What are owner drawn controls? An owner drawn control is a special type of control which is designed to allow the program to specify custom behavior. It is most often used for listboxes with graphics or for custom buttons. Whenever a user clicks on an owner drawn control or it changes state in some other way, like getting the focus, a special message is sent to the owning window of this control. This message is called WM_DRAWITEM, and its purpose is to let the window know that one of it's owner drawn controls needs attention. Along with this message, a pointer to an information structure is sent to the window. This structure contains information about what exactly happened with the control, and which control it was. The pointer to this structure is passed in lParam. In Pascal format, the structure looks like: TDrawItemStruct = record CtlType: Word; { Control type, can be odt_Button, odt_ComboBox, odt_ListBox, odt_Menu} CtlID: Word; { ID of Control } itemID: Word; { Not used for buttons. Has different meanings for other types of controls } itemAction: Word; { What happened. For buttons, tells if gained or - 26 - lost focus, or selected } itemState: Word; { What state control should be in after this drawing. Buttons only use ods_Disabled, ods_Focused, and ods_Selected } hwndItem: HWnd; { Window handle for the control } hDC: HDC; { Display context to be used for drawing the control } rcItem: TRect; { Defines clipping boundary rectangle for control } itemData: Longint; { Not used for buttons. Only used for listboxes and comboboxes } end; The owning window can examine this structure and determine what needs to be done with the control. By looking in the CtlType field, it will know what type of control the message is for. For a owner drawn button, this will be odt_Button. The CtlID field contains the ID of the control. The itemID field is not used for owner draw buttons. The itemAction field tells what happened with the control to cause this WM_DRAWITEM message. It can contain oda_DrawEntire, oda_Focus, or oda_Select. Only oda_Focus and oda_Select are relevant for owner drawn buttons. If oda_Focus is set, then the focus for the button changed, and you must check itemState to see whether or not the control gained or lost the focus. If oda_Select is set, the selection state of the button changed, and you must check itemState to know what the new state is. The itemState field specifies what state the control should be drawn in next. To check the values of itemAction and itemState, you must use the logical AND operation since they can contain more than one value. If (itemState AND ods_Focused) = TRUE, then the button has the focus. If (itemState AND ods_Selected) = TRUE, then the button is selected, or pushed. The hwndItem field specifies the window handle for the control. You can use this to send messages to the control's window procedure. The hDC field is the display context that should be used when drawing the control. The rcItem field defines the clipping rectangle for the control. It is used mainly with owner drawn menus. The itemData field is only used for listboxes and comboboxes. There are a couple of ways that your window procedure can process the WM_DRAWITEM message. It can either draw the control itself, or it can pass the message on to the window procedure of the control. This will use an object oriented technique and let the control draw itself instead of the main window procedure having to worry about how to draw each control. This is the technique that I used in my example code. The dialog window merely passes the WM_DRAWITEM message along to the control. The control reacts to this message by looking at the TDrawItemStruct record and determining what state it should draw, and then draws the button using StretchBlt. I originally wrote this to draw with BitBlt, but when the program was tested under the 1024 x 768 resolution while using large fonts, it became obvious that hardcoding the size of the bitmap didn't work properly when the dialog sizes were increased. This problem has basically two solutions. Either use StretchBlt to draw the bitmap button at a larger - 27 - than normal size, or have separate bitmaps depending on the resolution. Both of these methods have their pros and cons, and in the long run I decided to just use StretchBlt. You will notice a degradation in the quality of the bitmaps if you do run in the high resolutions and use the large fonts because StretchBlt can't do a perfect job scaling an image up. That's the basic idea for using owner drawn buttons. Things will probably be much clearer after actually looking at the source code. I'll briefly describe how to use it. My code is primarily designed to be used for owner drawn buttons in a dialog box, although there's no reason why you can't use them in a normal window. There are several steps that you will have to take to use this code in your own programs. 1. DESIGN YOUR DIALOG. When designing your dialog, you will need to create a standard button for each owner draw button in the dialog. Remember what the button ID is, as that's what you'll need to know to associate your owner draw control object to this button. You will need to set the size of the button to be 32 x 20, which is half the size of the actual bitmap for Borland's standard VGA bitmaps. I'm assuming that you are using Borland's Resource Workshop to design your dialog. After you have all of your buttons positioned where you want them and sized properly, bring up the control attributes dialog by double clicking on each control, and set the control type to owner draw button. After this, you won't be able to see the button anymore, but it will still be there. Save your dialog. 2. DESIGN THE BUTTON BITMAPS. You can use any Windows graphics program that can save in BMP format to design the actual bitmaps. The bitmaps will use the BWCC numbering scheme for their names. The numbering scheme is: normal = button ID + 1000, pressed = button ID + 3000, and focused = button ID + 5000. This means that if your button ID is 500, the bitmap number for the normal button without focus will 1500, for pressed 3500, and for focused it will be 5500. These are the names that you will give to the bitmaps when you save them. There is a shareware program written by N. Waltham called Buttons that will automate this task for you by creating all of the necessary bitmaps from one main bitmap. It automatically adds a 3D shadow effect and has some other useful features. This program is available on Compuserve in the BPASCAL forum, and the author can be contacted at 100013.3330@compuserve.com. Although you must register with the author, I don't mind sending copies of it via Internet email to interested users. 3. ADD NECESSARY CODE TO YOUR PROGRAM. You will need to add OwnDraw to your USES statement in your program. You will also need to make your dialog object a descendant of TOwnerDialog. The sample program shows an example of doing this. The last thing to do will be to use the NewButton method in your dialog's constructor for each owner draw button in that dialog. The NewButton method is called with the button ID, and a Boolean True or False depending on whether or not you want that button to be the - 28 - default button in the dialog. If this is True, then it will be the button drawn with focus when your dialog is initially created. That's all there is to it. When your dialog is displayed, the owner draw buttons will be displayed along with it. Of course, to get the buttons to do some useful function, you will need procedures in your dialog object to respond to each button selection. The sample program demonstrates this too. As you can see, using bitmapped buttons in your programs is not quite as difficult as it may at first look. When properly used, bitmaps can really make a big difference in the look of your program. I welcome any comments you may have, good or bad. I can be reached on Compuserve at 71044,1653, on America OnLine at TSnoddy, and on the Internet at tsnoddy@nyx.cs.du.edu. - 29 - Hacker's Gash by Mike Wallace and Pete Davis This is our first attempt at a tips/tricks column. If you couldn't figure out what the title meant, blame Pete. Here are three tricks we've come up with. Hope you like them. If you have any you want to share with our readers, send them in! Full credit will be given for all tips we publish, of course. 1) Menu bar on a dialog box: We spent a lot of time on this one, but the solution turned out to be a simple one. In the dialog box definition in either (a) the .RC file, or (b) a .DLG file you include in the .RC file, throw in the lines: STYLE WS_OVERLAPPEDWINDOW MENU where is a menu you have defined in the .RC file. The style WS_OVERLAPPEDWINDOW combines WS_OVERLAPPED, WS_CAPTION, WS_SYSMENU and WS_THICKFRAME, and is a standard parent window. 2) Highlighting a button on a dialog box when the cursor is over it: By highlight, I mean the button moves "in" slightly, and it's a cool effect that can look pretty impressive. Add the following declarations to the function that controls the dialog box: static BOOL ButtnDn; static int LastDown; static int IDNumber=0; int counter; Next, assume you have 5 buttons on your dialog box, and these buttons are identified by the constants IDB_BUTNx (where x is a number between 1 and 5), which are defined in the .H file for the .DLG file containing the definition for your dialog box. Also assume these five constants are defined sequentially, say, 101 to 105. The variable "hDlg" is the handle to the dialog box, and "message" is the message passed to the function by Windows (these are, respectively, the first and second parameters passed into a standard window-handling function). Add the following two cases inside the "switch(message) {}" structure: case WM_SETCURSOR: ButtnDn= TRUE; LastDown= IDNumber; IDNumber= GetDlgCtrlID(wParam); for(counter=IDB_BUTN1; counter<=IDB_BUTN5; counter++) if(counter==IDNumber) SendDlgItemMessage(hDlg, counter, BM_SETSTATE, TRUE, 0L); if(IDNumber != LastDown) SendDlgItemMessage(hDlg, LastDown, BM_SETSTATE, FALSE, 0L); - 30 - break; case WM_NCHITTEST: if (ButtnDn) { ButtnDn = FALSE; SendDlgItemMessage(hDlg, LastDown, BM_SETSTATE, FALSE, 0L); } break; 3) Using DlgDirList for a subdirectory: I recently tried using the DlgDirList function passing a pathname like "DATA\\*.DAT" to fill a list box with all the files in the DATA directory ending with .DAT (by the way, the double backslash ("\\") is needed because the backslash is a special character in a C string). Afterwards, I found out that if this function is successful, it changes the current working directory to whatever is specified in the string (here, the "DATA" directory). Afterwards, I tried changing the directory back to the original working directory, but if the function got called again, chaos ensued. I managed to get around this by including in the program the following line: #include The direct.h file has prototypes for the directory-manipulating functions, including the one needed here: chdir, which changes the current working directory. I replaced the call to DlgDirList with the following code: chdir("DATA"); DlgDirList(hDlg, "*.DAT", IDL_POPUP, IDL_DIRSTRING, 0); chdir(".."); The "hDlg" variable is the handle to the dialog box, "IDL_POPUP" is the dialog box ID value for the list box control, "IDL_DIRSTRING" is the dialog box ID value for a static text control that will be updated with the current path name, and 0 is the DOS file attribute used to retrieve a list of read/write files with no other attributes set. There are other values you can use for the DOS file attribute value to retrieve, for example, subdirectories and drives. This code simply changes to the DATA directory, gets a list of the .DAT files in that directory and puts that list in the IDL_POPUP list box and the path in the IDL_DIRSTRING static text control, and then changes back to the parent directory. You can use the DlgDirSelect function to retrieve a selected filename from the list box. - 31 - Special News by Mike Wallace We recently got a letter from Todd Snoddy offering to put WPJ on the America Online information system. We said "Sure", and a couple of days later got a phone call from a sysop for the board. He liked the magazine (all contributing authors can now pat themselves on the back) and asked how we would feel about holding an on-line conference on AO, giving readers a chance to talk directly to us and ask us any questions they have about the journal. Sounded good to us, so we agreed, and now hope to bring this to you AO subscribers soon. The details are still being worked out, so watch the AO Programming Library for updates. For the benefit of our readers without access to America Online, I'll try to write down what questions get asked and our answers and include them in our next issue. Thanks, Todd, and the great folks at America Online! - 32 - Windows 3.1: Using Version Stamping library By Alex Fedorov Windows 3.1 introduces the new resource type with ID number 16 called VS_FILE_INFO. This resource contains the symbolic data about the file version, its description, the company name and so on. Such information can be used to determine the file type/subtype and its version and can be used in installation programs. Resource data lives as a set of blocks; each block contains information of its own. The first one (with a fixed size) describes the TVS_FixedFileInfo structure. The fields of this structure contain the following information: file version, environment type and version, file type and subtype. There are several subblocks, which contains symbolic information with the following IDs (the table contains only those which must present): ID Contents CompanyName Company name FileDescription File description FileVersion File version InternalName Internal file name ProductName Product name ProductVersion Product version Resource of this type can be created with resource editor, such as Resource Workshop (version 1.02 or higher), or with resource compiler (RC or BRC). The latter needs the resource template. VS_FILE_INFO resource for resource compiler looks like the following: 1 VERSIONINFO LOADONCALL MOVEABLE FILEVERSION 3,10,0,61 PRODUCTVERSION 3,10,0,61 FILEFLAGSMASK VS_FF_DEBUG | VS_FF_PATCHED FILEFLAGS VS_FF_DEBUG FILEOS VOS__WINDOWS16 FILETYPE VFT_APP FILESUBTYPE VFT2_UNKNOWN BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904E4" BEGIN VALUE "CompanyName", "Microsoft Corporation\0" VALUE "FileDescription", "Windows Terminal application file\0" VALUE "FileVersion", "3.10.061\0" VALUE "InternalName", "Terminal\0" - 33 - VALUE "LegalCopyright","Copyright \251Microsoft Corp.1991\0" VALUE "OriginalFilename", "TERMINAL.EXE\0" VALUE "ProductName","Microsoft\256 Windows\231 Operating System\0" VALUE "ProductVersion", "3.10.061\0" END END END To get access to VS_FILE_INFO resource data you can use the functions from VER Dynamic Link Library included with Windows 3.1. The example below shows how to use some of those functions. {------------------------------------------------------ FileVer: An example of VER.DLL functions usage (c) Alex G. Fedorov, 1993 ------------------------------------------------------} Program FileVer; uses WinCrt,WinTypes,WinProcs,Ver,Strings; Const {Any file name here} FileName = 'C:\WINDOWS\SYSTEM\VGA.DRV'; Type PBuff = ^TBuff; TBuff = Array[0..127] of Char; Var Size : LongInt; hVer : LongInt; VerData : Array[0..463] of Byte; {Avg. resource size} Buff : PBuff; NameStr : Array[0..127] of Char; Block : Array[0..7] of Char; Procedure ShowString(Name : PChar); { Show the string with description } Begin { Create query string } StrCopy(NameStr,'\StringFileInfo\');StrCat(NameStr,Block); StrCat(NameStr,'\'); StrCat(NameStr,Name); { Query for string - 34 - } If VerQueryValue(@VerData,NameStr,Pointer(Buff),Word(Size)) { If string was found, show it } Then Writeln(Name,' ':20-StrLen(Name),Buff^) End; Begin { Get the resource size. hVer - Resource handle } Size := GetFileVersionInfoSize(FileName,hVer); { Get VS_VERSION_INFO block } GetFileVersionInfo(FileName,hVer,Size,@VerData); { Get the symbolic information block The default name for it: 040904E4 The result is in Buff } VerQueryValue(@VerData,'\VarFileInfo\Translation',Pointer(Bu ff),Word(Size)); { Translate the block name } wvsPrintf(Block,'%04X%04X',Buff^); If Size <> 0 Then Begin Writeln(^M^J'Version information for ',FileName); Writeln; { Query for the following IDs } ShowString('CompanyName'); ShowString('FileDescription'); ShowString('FileVersion'); ShowString('ProductName') End; Readln; DoneWinCrt End. - 35 - Book Review by Pete Davis Microsoft Windows 3.1 Programmer's Reference Library If you're a serious Windows programmer, you've probably got Microsoft's series of books on Windows programming. The series is broken up into 4 volumes and two extra books, as follows: - Volume 1: Overview This is, as it says, an overview. It covers a lot of different topics from how windows are managed, graphics, and how to write extensions for Control Panel and File Manager. - Volume 2: Functions This is an alphabetical listing of all the Windows 3.1 functions. It's a big one. - Volume 3: Messages, Structures and Macros Like the previous two, this one is exactly what it says it is. it's got some great information on all the structures, which comes in real handy. - Volume 4: Resources This one has a lot of information on file formats for a bunch of different Windows files (.GRP files, Meta Files, etc.) It's also got a bit of information on creating Help files, using assembly language in Windows, and more. - Programming Tools Not listed as a Volume. This is more of an additional Microsoft C reference. It covers a lot of the SDK tools, debugging with Codeview, data compressions and that kind of stuff. - Guide to Programming Also not listed as a Volume, but I would have made it one. It covers different types of resources like Icons, Dialog boxes, etc. Printing, DLLs, MDI, Fonts, Assembly and C, and more. Ok, so there's our list. I would say, all-in-all, this is probably the most definitive resource on programming for Windows and that anyone planning on doing a lot of Windows programming should get the whole series. It's a big ticket item. The least expensive book is $23 (US) and the most expensive is $39 (US). I suggest you get some other books and not limit yourself to just these. Although they cover most of the topics most programmer's will need, the books are lacking in some areas and there are some typos which have slowed this programmer down on more than one occasion. Every book has typos and that's why, as a serious programmer, your library should come from several sources so you can check the books against each other when you run into problems. - 36 - These books are, however, well worth the cost. There's a lot of stuff covered in these books that you won't find in other sources. The detail on different file formats, for example, I haven't seen in any other books. The structures list is very complete and is a very handy reference. There's also some good information on memory management and DLLs. What these books aren't These books aren't everything and, like I said, don't limit yourselves to just these. The Windows API bible (which Mike will be reviewing) has some great information too and it's a good one to check against Microsoft's Function reference when you get suspicious that a function isn't doing something it's supposed to do. Also, because it's Microsoft, you're not going to get the wealth of inside, under-the-hood, information like you get from Undocumented Windows (Andrew Schulman, David Maxey, Matt Pietrek. Addison-Wesley. See the review in WPJ Vol.1 No.1) Unfortunately, because Windows is such an enormous system and no single book can even begin to cover every aspect of it, we programmers have to get a lot of different books. If you're serious, though, you should consider setting aside the money to get this collection. - 37 - Book Review by Mike Wallace The Waite Group's Windows API Bible by James L. Conger Earlier in this issue Pete reviewed the Microsoft Windows 3.1 Programmer's Reference Library. Now it's my turn to review my Windows programming reference book of choice: The Waite Group's Windows API Bible by James Conger. Pardon my French, but this book kicks serious "lune". While the Microsoft books spread out everything across several books, the Waite Group threw it all into one book, making anything you want to look up easy to find. This book is a must-have for anyone wanting to learn how to program Windows. Here are the details: The book is divided into 30 chapters, each discussing a distinct area (e.g., creating windows, menus, windows messages, metafiles). Each chapter starts off with a discussion of its topic (usually several pages long), then a summary of the associated functions (a list of the function names with a one line purpose), and then a complete description of the same functions in the summary. Their descriptions are detailed and include suggested uses for the function, a great "see also" reference list (more complete than the Microsoft books), an explanation of all the parameters and the return value, and code (real programs) that shows how to use the function. It's my one stop for looking up a function. Plus, the inside front cover contains a list of all the API functions with a page number so I can quickly jump to the function I want to look up, followed by a list of messages grouped by category (e.g., button notification codes, windows messages). But wait, there's more...the book also includes a pullout reference card containing a list of all functions with the type declarations for all parameters and the return value, plus the same list of messages that's on the inside cover. The function list in the pullout card is organized the same way as the chapters, so that all the functions listed in the chapter on, say, sound functions, are grouped together in the pullout under the heading "Sound Functions." I couldn't ask for a better list. In the Microsoft books, you have to know the name of the function before you can look it up, and there have been many times when I didn't know what the function was called, but I knew what it did. This book lets me find the name of the function quickly, and then shows me how to use it. In the appendices, there is a discussion of useful macros from WINDOWS.H, mouse test hit codes, and a printout of WINDOWS.H, which has been very helpful to me on occasion. The book ends with a very complete index, making it extremely easy to look up related subjects. There are very few faults with this book, but there a few. One is the strange absence of the Windows API functions starting with the letter "N". I am not making this up. The Microsoft reference book on functions lists four functions starting with "N": NetBIOSCall, NotifyProc, NotifyRegister and NotifyUnRegister. None of these functions are in the API Bible. There - 38 - is also no discussion of OLE or True Type fonts, although the author writes (in the introduction) these will be covered in a separate volume currently under development. Also, the Microsoft volume on programming tools (those included with the SDK, such as Spy) covers an area not addressed by the API Bible. If you want to use the Microsoft SDK, the API Bible won't be of much help, but this isn't much of a fault, because Microsoft isn't the only producer of Windows SDKs, and you're going to get manuals for any SDK you buy, so why include a lot of information that some readers won't need to know? To me, the faults of this book do not detract from its overall usefulness. It has paid for itself many times over, and when compared to the Microsoft reference books, the price seems insignificant. Total price for the six books in the Microsoft Windows 3.1 Programmer's Reference Library: around $170-180. The Windows API Bible: $40. I recommend this book. You'll thank me for it. - 39 - Printing in Windows by Pete Davis Ok, I failed to get this article out last time and I've been feeling really guilty about it, so here it is. Writing this article hasn't been easy. Writing about printing reminds me of my first attempt at printing under Windows. It wasn't a very good one. Actually, printing under Windows isn't a big deal and if you're printing graphics, it's a lot easier than you'd think. My real problem with printing the first time was incomplete/incorrect documentation. I won't mention names, but I saw at least 3 examples of printing in different books on Windows programming and every one of them either misled me or had code that didn't work. Here's the deal. When you print, you MUST, and I mean MUST (I'm really serious about this), have an abort procedure. This is an absolute necessity. You MUST have it. You can't do without it. You'll have problems if you don't have an abort procedure, so write one. Are you starting to get the picture here? An abort procedure is NOT optional, you MUST have it. Now, you might be wondering why I'm emphasizing this point. Every book I read on printing made it sound like an option. I needed to print one page of text (and not much text at that) and didn't have a need for an abort procedure, so I didn't include one. I kept getting UAEs as soon as I tried to print. It even occurred to me that I might need the Abort procedure, and I specifically looked for a phrase which said something along those lines in every bit of documentation I had. Finally, 4 days later, I decided I'd just pop an abort procedure in. (I had tried just about everything else at that point, why not?) Of course, it worked, and I had wasted four days because I HAD TO HAVE AN ABORT PROCEDURE!!!! So, I'm assuming that, after reading this, none of you are going to spend four days trying to figure out why your print routine UAEs only to find that you forgot your abort procedure. If I hear of any of you doing this after reading this article, I'm going to come over and personally give you a whuppin (as we say down in Arkansas). If you haven't, at this point, realized that an abort procedure might possibly be a good idea to include in your print procedure, you fit into one of the following three categories: 1> You program on the cutting edge of Atari 2600 technology. (or some other cutting edge of obsolescence) 2> Blind and not able to read any of this anyway. 3> Just plain stupid There, so I have that off my chest, on to the meat of this article, which is how to print. It's real easy!!! The first thing we'll look at is our Abort procedure. (Did I mention that you need this part?) The abort procedure is real simple. It receives the device context of the printer and an error code. In our example, the - 40 - nCode is the error code and if it's non-zero, you have an error. Basically all your abort procedure has to do have a message loop. I don't do anything else with it, myself. It should look something like this. BOOL FAR PASCAL AbortProc(HDC hdcPrint, short nCode) { MSG msg; while(!bPrintAbort && PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { if(!hDlgPrint || !IsDialogMessage (hDlgPrint, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } } return !bPrintAbort; } The next thing on our agenda is the PrintDlgProc procedure. This is basically the dialog box procedure for our cancel printing dialog box. Although there is a cancel button in our dialog box, instead of checking specifically for the cancel button, our only concern is with a WM_COMMAND. (We only have one button, so what other WM_COMMANDS are they going to be sending?) If we get the WM_COMMAND, we basically abort the printout (by setting bPrintAbort = TRUE) and destroy our dialog box. Pretty darn simple. BOOL FAR PASCAL PrintDlgProc(HWND hDlg, WORD message, WORD wParam, LONG lParam) { if (message == WM_COMMAND) { bPrintAbort = TRUE; EnableWindow(GetParent(hDlg), TRUE); DestroyWindow(hDlg); hDlgPrint = 0; return TRUE; } return FALSE; } The next procedure is our printing procedure. There are a couple of neat things here that I'd like to talk about. First of all, there's the four lines involved with getting information about our printer driver. What we're doing here is just getting the default printer device. We get the information via a GetProfileString command. Under the section "windows" and then the line starting with "device". That's our printer. All the information goes into szPrinter which we then break into the Device, - 41 - Driver, and PortName using the strtok function. This basically breaks out our string into separate strings. The second parameter of strtok is the thing we want to break our string apart at. In this case, we want it broken up at the commas. Also, notice how we only pass szPrinter once. If we use NULL in the next two occurrences of strtok, it knows to continue using szPrinter and to continue from where we left off which, in this case, is at the last comma. GetProfileString("windows", "device", ",,,", szPrinter, 80); lpDevice = strtok(szPrinter, ","); lpDriver = strtok(NULL, ","); lpPortName = strtok(NULL, ","); Our next job is pretty simple, we just create a device context based on the information about our driver. /* Create the device context. */ if ((hdcPrint = CreateDC(lpDriver, lpDevice, lpPortName, NULL)) == NULL) { MessageBox(hWnd, "Could not assign printer.", "ERROR", MB_OK); return FALSE; } Here's another thing you don't really have to deal with in DOS. With Windows, you need to know where you are on the page. The reason is that you have to tell Windows when you're done with the current page. This means you need to know the size of a page and the size of the text on the page. Now, this can be a bit of a pain, but it also allows for a lot of interesting stuff. For example, you don't have to start from the top of the page and go down. You can print the bottom part of the page, and then print something in the middle of the page, and so on. Then you just tell Windows to eject the page. In our case, we're going to be assuming 1 page or less of text and not really deal with that. (As they say in school books, this is left as an exercise for the reader.) All we're going to do is use the text size to tell us where to print the next line of text. First we have to use CurYPos as our current Y position on the page. (I love descriptive variable names.) Then we need to find out how tall a single character is. That's done from getting a text metric for the printer and adding the character's height, plus the external leading, which is kind of like the blank space between lines. CurYPos = 1; GetTextMetrics(hdcPrint, &tm); yChar = tm.tmHeight + tm.tmExternalLeading; The next step is to create our dialog box and abort procedure. By the way, the abort procedure is not optional. lpfnPrintDlgProc = MakeProcInstance (PrintDlgProc, hInst); hDlgPrint = CreateDialog(hInst, "PrintDlgBox", hWnd, lpfnPrintDlgProc); - 42 - lpfnAbortProc = MakeProcInstance(AbortProc, hInst); bPrintAbort = FALSE; Now, the secret to printing is all in the Escape commands. (Actually, in Windows 3.1, they have commands you can use instead of Escape, but since we try to maintain our 3.0 backwards compatibility, we're going to use Escape.) The Escape is the heart of the printing routines. You use it to send different commands and/or data to the printer driver. The first one we'll send is our SETABORTPROC command. This sets up our all-important abort procedure. Escape(hdcPrint, SETABORTPROC, 0, (LPSTR) lpfnAbortProc, NULL); The next Escape we're going to send is our STARTDOC command. We're going to pass the name of our document and the length of the string that has the name of our document. Pretty straight-forward. Escape(hdcPrint, STARTDOC, strlen(szMsg), (LPSTR) szMsg, NULL); Since our example involves reading from a file, we're just going to read one line at a time from the file. We use a TextOut function to output our string to the printer driver. We need to give X and Y coordinates. In our case, the X is always 0 and the Y position is calculated from the GetTextMetric which we did above. We just add the text height to the current Y position after each line. while (fgets(CurLine, 132, inFile)!=NULL) { TextOut(hdcPrint, 0, CurYPos, CurLine, strlen(CurLine)-2); CurYPos += yChar; } When we're done, we need to send a NEWFRAME command via the Escape function. This basically means to advance to the next page. Now, if you're printing multiple pages, you need to do a NEWFRAME between each page. In our case, we're only allowing one pages, so we do it right at the end. Escape(hdcPrint, NEWFRAME, 0, NULL, NULL); The last thing to do is send an ENDDOC. This basically tells the printer driver we're done and it can begin printing the document. Escape(hdcPrint, ENDDOC, 0, NULL, NULL); After all that, we just close the file and delete the device context for our printer, then get a cup of coffee and a smoke and relax. fclose(inFile); DeleteDC(hdcPrint); Ok, so it's a little more complex than printing text under DOS, but the thing I didn't mention is the graphics. Now, I'm not going to go into great detail about it, because it is a little more complex, but not much. - 43 - Essentially, all you have to do is route all of your graphics to a printer device context instead of a screen device context. The complexity comes in when you're trying to figure out pages sizes and that kind of stuff, but for the most part, printing graphics is as easy as printing text. There are some things we didn't cover here. One of them is a thing called banding, which is a way of printing your page in parts called bands. This is particularly useful for dot-matrix and other non-postscript printers. Banding makes it a bit faster to do the printing. Some printer drivers have the banding built-in and I, personally, have never had to work in an environment where printing had to be particularly fast, so I've avoided banding. If someone feels that banding is of particular importance, they're more than welcome to write an article on it. That just about wraps up printing. It's really not all that complex, and as you can see, Windows gives you a lot of power as to how to handle it. There are all kinds of neat and nifty things you can do, like mixing graphics and text, which is a cinch. Try doing that in DOS. And last but not least, please, don't forget your abort procedure. - 44 - Advanced C++ and Windows By Andrew Bradnan Talking to the User Overview While Windows offers a great variety of ways to talk to the user, none of them are very easy. Even fewer are quick to implement. In this article we will look at some C++ classes to make things a little easier. Output Introduction Let's look at a few ways to talk to the user. The simplest way to tell the user something is to create a message box. This is one of the easiest things to do in Windows. Luckily, Windows does most of the work for us. We are going to make it even easier. With the classes we are going to create you will never have to remember what the parameters are, what order they go in, and exactly how they spelled all the constant values you can pass in and that MessageBox() passes back. We also need a good way to tell the user and ourselves about error messages (God forbid). Message Boxes If you are at all familiar with the MessageBox () call you are well aware that there are several different incantations. Each has its own purpose and can be used for several circumstances. Its function prototype looks like this: int MessageBox (HWND hwndParent, LPSTR lpszText, LPSTR lpszTitle, UINT fuStyle); You can pass it 18 different flags (or combinations) and it will return seven different flags to tell you what the user did. Not as ugly as CreateWindow() but not easy either. OKBOX A stream is an abstraction referring to the flow of data from a producer to a consumer. The first output object we are going to create is an OKBOX. Once again it does exactly what it sounds like. It displays a message box with one "OK" button. The OKBOX will allow us to use C++ stream conventions. Let's first look at how we would like to use an OKBOX so we can write the appropriate member functions. OKTEST.H // // OKBOX Test Header // Andrew Bradnan (c) 1992 // C++ Windows Version // #ifndef __OBJECTS_H #include #endif - 45 - class WINDOW : public BASEWINDOW { public: WINDOW (LPCSTR lpcszClass, HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow) : BASEWINDOW (lpszClass, hInstance, hPrevInstance, lpszCmdLine, nCmdShow) {}; BOOL OnCreate (CREATESTRUCT FAR *lpCreateStruct); }; OKTEST.CPP // // OKBOX Test Module // Andrew Bradnan (c) 1992 // C++ Windows Version // #include BOOL WINDOW::OnCreate:: (CREATESTRUCT FAR *lpCreateStruct) { OKBOX msg ("OK Test Caption"); msg << "User notification." return TRUE; }; int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow) { WINDOW Window ("OK Test", hInstance, hPrevInstance, lpszCmdLine, nCmdLine); return Window.MessageLoop (); }; We are using our WINDOW virtual function to trap the WM_CREATE message. Here we define msg as an OKBOX with the caption "OK Test Caption" When we want to send a message to the user we just stream some text to it. This will invoke the message box with the "User notification." It is much easier to read. Now that we have an easy way to send a message to the user or ourselves let's look in the "black box" to see how OKBOX works. OKBOX.H // // OKBOX Header // Andrew Bradnan (c) 1992 // C++ Windows Version // #ifndef __OKBOX_H #define __OKBOX_H #ifndef __WINSTR_H - 46 - #include #endif class OKBOX { public: OKBOX (LPSTR lpszCaption); int operator<< (STRING& strMessage); private: STRING strCaption; }; #endif // __OKBOX_H OKBOX.CPP // // OKBOX Module // Andrew Bradnan (c) 1992 // C++ Windows Version // #include OKBOX::OKBOX (LPSTR lpszCaption) : strCaption (lpszCaption) {}; int OKBOX::operator<< (STRING strMessage) { return MessageBox (NULL, strMessage, strCaption, MB_OK); }; Short and sweet. We have a constructor that takes a string which we will save as the caption for the message box. Our other function, operator<<() is called by the compiler when ever it sees an OKBOX object on the left of the << and a STRING (or an object that can construct a STRING like a LPSTR) object on the right side. ERRORS Unfortunately, errors are bound to occur. We will assume these are all going to be user errors. If we think a bit, an ERROR object is going to be coded almost exactly like an OKBOX. In fact, let's rewrite our OKBOX object so that we can take advantage of this. Quality if free, but we have to pay for it up front. So that we can reuse some of the code we will create an object call MSGBOX. MSGBOX will contain the code shared by the OKBOX and the ERROR object. MSGBOX.H // // MSGBOX Object Header - 47 - // Andrew Bradnan (c) 1992 // C++ Windows Version // #ifndef __MSGBOX_H #define __MSGBOX_H #ifndef __WINSTR_H #include #endif class MSGBOX { public: MSGBOX (LPSTR lpszCaption) : strCaption (lpszCaption), fuStyle (Style) {}; int operator<< (STRING& strMessage) { return MessageBox (NULL, strOutput, strCaption, fuStyle); }; private: STRING strCaption; UINT fuStyle; }; #endif // __MSGBOX_H The MSGBOX object only adds fuStyle so that different message boxes can be created. Now we can rewrite OKBOX to use the code in MSGBOX. OKBOX will be inherited from MSGBOX. Since we will inherit all the public member functions, we will only have to write a short constructor. OKBOX.H // // OKBOX Object Header // Andrew Bradnan (c) 1992 // C++ Windows Version // #ifndef __OKBOX_H #define __OKBOX_H class OKBOX : public MSGBOX { public: OKBOX (LPSTR lpszCaption) : MSGBOX (lpszCation, MB_OK) {}; }; #endif // __OKBOX_H The code for an ERROR object is exactly the same except for the value of fuStyle. - 48 - ERROR.H // // ERROR Object Header // Andrew Bradnan (c) 1992 // C++ Windows Version // #ifndef __ERROR_H #define __ERROR_H class ERROR : public MSGBOX { public: ERROR (LPSTR lpszCaption) : MSGBOX (lpszCation, MB_ICONSTOP | MB_SYSTEMMODAL | MB_OK) {}; }; #endif // __ERROR_H Great! Now we can notify the user using the OKBOX object and tell the user about errors using the ERROR object. Questions Your programs often need to ask the user a question. If this is a yes or no affair we can use a message box. Let's look at how we would like to use an object like this. Then we will write the member functions to fill out the class. QTEST.H // // QUESTION Test Header // Andrew Bradnan (c) 1992 // C++ Windows Version // #ifndef __OBJECTS_H #include #endif class WINDOW : public BASEWINDOW { public: WINDOW (LPCSTR lpcszClass, HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow) : BASEWINDOW (lpszClass, hInstance, hPrevInstance, lpszCmdLine, nCmdShow) {}; BOOL OnCreate (CREATESTRUCT FAR *lpCreateStruct); }; QTEST.CPP // - 49 - // QUESTION Test Header // Andrew Bradnan (c) 1992 // C++ Windows Version // #include BOOL OnCreate (CREATESTRUCT FAR * lpCreateStruct) { OKBOX ("Question Test"); if ((QUESTION) "Do you like the object?") msg << "Great, I hope your programming is easier." else msg >> "What the hell do you know!"; return TRUE; }; int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow) { WINDOW Window ("Question Test", hInstance, hPrevInstance, lpszCmdLine, nCmdLine); return Window.MessageLoop (); }; Once again we are trapping the WM_CREATE message and trying out our QUESTION object. The code is kind of sneaky. I know one thing, you can actually read C++ code if you write your classes carefully. I would hate to write those four lines of code in C. They certainly would not be as easy to read. Let's look at how the C++ compiler helps us. First of all, when the compiler sees that we would like to cast the string to a QUESTION it creates a temporary QUESTION object using our string. Then since the if statement really wants a BOOL the compiler will cast the QUESTION to a BOOL. It is in this explicit cast, operator BOOL (), that will make the message box call. The cast member function will "look" to see if the user hit the "Yes" or "No" button, and return TRUE or FALSE. If you didn't understand that, let me explain it another way. The C++ compiler did all the work. Seeing as how I'm trying to champion code reuse let's try and use the MSGBOX object. We are only going to have to write two member functions for this object. The constructor and the cast member function. QUESTION.H // // QUESTION Object Header // Andrew Bradnan (c) 1992 // C++ Windows Version // #ifndef __QUESTION_H - 50 - #define __QUESTION_H class QUESTION : public QUESTION { public: QUESTION (LPSTR lpszOutput) : MSGBOX ("Warning!", MB_QUESTION | MB_YESNO), strOutput (lpszOutput) {}; operator BOOL (void) { if (IDYES == operator<< (strOutput)) return TRUE; else return FALSE; }; private: STRING strOutput; }; #endif // QUESTION_H As you can see the only action really takes place in the explicit cast, operator BOOL (). It is here that we call the member function operator<< () to invoke the message box call. We didn't do too much but transform one call to the MSGBOX class. Since we declared it inline we won't even gain any function call overhead. The C++ compiler will put this ugly code right where our beautiful code exists right now. Pretty damn cool. Warnings We also might want to warn the user of impending doom. The File Manager uses this to confirm that you really want to overwrite a file. If works almost like a QUESTION object except it can return whether the user chose "Yes", "No" or "Cancel". WARNING.H // // WARNING Object Header // Andrew Bradnan (c) 1992 // C++ Windows Version // #ifndef __WARNING_H #define __WARNING_H class WARNING : public MSGBOX { public: WARNING (LPSTR lpszOutput) : MSGBOX ("Warning!", MB_EXCLAMATION | MB_YESNOCANCEL) {}; - 51 - operator BOOL (void) { BOOL fUserChoice; fUserChoice = operator<< (strOutput); if (fUserChoice == IDYES) return TRUE; else if (fUserChoice == IDNO) return FALSE; else // if (fUserChoice == IDCANCEL) return -1; }; private: STRING strOutput; }; #endif // WARNING_H Wondrous. Why, you ask, go through all this trouble just for a MessageBox() call? Well, there are plenty of reasons. Let's review them. First, who can remember all those flag variables? Not me. Does the caption go first, like I know it should (and always write)? No! No matter how many times I write it the correct way, Windows just will not learn. Second, the folks at Microsoft just love to change things. The MessageBox() call may gain new functionality. Why romp through all the code you have written to change it for the latest greatest MessageBoxEx()? It would be a pain and you surely would miss some of your old code. Now all you need to do is make a change to the MSGBOX class. Third, you can add all sorts of checking to the base class. This really doesn't apply in this case but in more complicated classes this will be important. We even gain some checking from the string class. "How?" you ask. There is nothing to screw up with constant strings. I'm afraid not, my friend. Once upon a time, I forgot to export one of my functions (good thing you have never done that) that used one of these classes. My data segment was pointing off to never never land and luckily my STRING's told me as much. They didn't even GP fault, just quietly told me that I had screwed up. Windows 3.1 has brought multimedia to our clutches so let's look at a real example of how powerful C++ inheritance can be. We will be updating MSGBOX. MSGBOX.H // // MSGBOX Object Module // Andrew Bradnan (c) 1992 // C++ Windows Version // #ifndef __MSGBOX_H #define __MSGBOX_H #ifndef __WINSTR_H - 52 - #include #endif class MSGBOX { public: MSGBOX (LPSTR lpszCaption); : strCaption (lpszCaption), fuStyle (Style) {}; int operator<< (STRING& strMessage); { MessageBeep (fuStyle); return MessageBox (NULL, strOutput, strCaption, fuStyle); }; private: STRING strCaption; UINT fuStyle; }; #endif // __MSGBOX_H You have now added multimedia to everywhere you use message boxes. All by changing one measly line. You're a fast worker. Your boss will probably give you a raise. We have just implemented some easy ways to have dialog with the user. They are by no means complicated, but just think of the complicated things you can make this easy, even fun to program. There are plenty of other nasty constants you can play with to create you own classes. Then you can rip those pages right out of the Windows Programmer's Reference. - 53 - The Trials and Tribulations of an Antipodean Abecedarian who uses Turbo Pascal to Program Windows Part 1 They Also Serve Who Only Stand and Wait Jim Youngman [CompuServe 100250,60] I live in Melbourne, Australia and I work in a one man Operations Research department. The programming that I do is in order to solve OR problems that I encounter. I am very much isolated in my job although I do my best to keep in touch with others through professional societies and computer user groups. e-mail has proved invaluable since I had a modem installed a short time ago. The problems I have struck are often simple in the end, but they can be confusing for a beginning Windows programmer such as myself. The documentation for the various programming languages does not tell you what you need to do; rather it tells you how to do something when you know what it is you need to do. Perhaps sharing some of these trivial trials and tribulations in a series of short articles might just help another beginner. I have the job of writing a user friendly front end to a mathematical programming package. The end users will be line managers with a background in mechanics rather than computers. This led me to decide that the best medium for them to use the final application would be Windows (we are standardized on IBM compatible machines). One of the first things I needed to do after setting up various screens using the Borland Resource Workshop was to find out how to run the mathematical programming package from within a Windows program. The package is DOS based and uses the Phar Lap DOS extender to run in 32 bit mode. How could I do this? I could not find any clues in the manuals, nor in the only book on TPW that I had at the time: Tom Swan's Turbo Pascal for Windows 3.0 Programming (1991) which is an excellent introduction to the subject. The books being no help, I then went on to search the Borland Pascal Help file. Eventually I tried a search on Load and soon discovered the function LoadModule. This was described as loading and executing a Windows application, but there was a cross-reference to WinExec. The description of this function did not specifically mention either Windows or DOS applications, so I tried it: procedure RunIPOPT; begin WinExec(hWindow,'c:\xp\ipopt', sw_Show) end; - 54 - This worked OK. However, the IPOPT program displays masses of mathematical detail on screen as it is running. I do not want to confuse the end users with this detail, so I want to eventually hide the window in which it runs and bring up a message box to inform the user when the program has run to completion. For the time being I will leave the window visible (sw_Show) so that I can monitor progress: procedure RunIPOPT; begin WinExec(hWindow,'c:\xp\ipopt',sw_Show); MessageBox(hWindow, 'Program Completed','IPOPT', mb_Ok) end; Surely this should work. But, no! The message box came up on screen before IPOPT had even begun to run. Again all my books failed me, as did the Help file too this time. Borland's Windows API guide had a description of the command, but there were still no examples that showed how it might be used in practice. I was stuck! All fairy tales have happy endings, but they have to begin with "Once upon a time ...". Once upon a time I was browsing in my favorite computer book store and discovered a copy of Neil Rubenking's Turbo Pascal for Windows Techniques (1992). A cursory look at the index found a reference to the WinExec function. I looked it up. It had exactly what I wanted. I bought a copy immediately. The book is another excellent reference that I now have on my shelf and reach for often. It seems that the crucial point I was missing was the need to create a loop to test whether the DOS program was still running. I adapted Neil Rubenking's code to produce the following procedure: procedure RunDos(hWindow: hWnd; CommandLine, CompletionMessage, CompletionTitle: PChar); var IH : word; M : TMsg; begin IH := WinExec(CommandLine, SW_Show); if IH <= 32 then MessageBox(hWindow, CommandLine, 'Failed to run'+chr(13)+chr(10)+'Execution Error'+ chr(13)+chr(10)+'Contact Jim Youngman for Help', mb_Ok + mb_IconHand) else begin repeat while PeekMessage(M, 0, 0, 0, PM_REMOVE) do - 55 - begin if M.Message = WM_QUIT then begin PostQuitMessage(M.wParam); Exit; end else begin TranslateMessage(M); DispatchMessage(M); end; end; {end do} until GetModuleUsage(IH) = 0; MessageBox(HWindow, CompletionMessage, CompletionTitle, mb_Ok + mb_IconInformation); end; {else} end; I lived happily ever after, at least until the next problem. - 56 - Getting in touch with us: Internet and Bitnet: HJ647C at GWUVM.GWU.EDU -or- HJ647C at GWUVM.BITNET (Pete) GEnie: P.DAVIS5 (Pete) CompuServe: 71141,2071 (Mike) WPJ BBS (703) 503-3021 (Mike and Pete) You can also send paper mail to: Windows Programmer's Journal 9436 Mirror Pond Drive Fairfax, VA 22032 U.S.A. In future issues we will be posting e-mail addresses of contributors and columnists who don't mind you knowing their addresses. We will also contact any writers from the first two issues and see if they want their mail addresses made available for you to respond to them. For now, send your comments to us and we'll forward them. - 57 - The Last Page by Mike Wallace Things have been hectic in the WPJ offices (i.e., our basement) - you've been sending us great articles and mail with lots of input (see the Letters column). It's been great - you seem to like what we're doing, and we're having a good time. This month a tips/tricks column (Hacker's Gash) makes its debut in WPJ (Official slogan: "The original party snack"). It was written by us, but we're always glad to hear from you people. If you have any tricks you want to share, let us know about them! We seem to be accomplishing our basic goal of helping people learn how to program in Windows, but this goal brings up an issue I want to write a bit about. In the Letters column, there is a letter from Tammy Steele, formerly of the SDK forum on CompuServe, and in her letter she discusses the accuracy of WPJ. If the magazine isn't accurate, the Microsoft sysops aren't going to recommend WPJ as a source of helpful info. When we started this effort, I didn't envision Microsoft referring people needing help our way. Don't ask me why, I guess it just never occurred to me, but it illustrated to me the point that the available sources of help on this matter shouldn't be exclusive - that is, no one has a monolopy on this field (nor should anyone), because no one source can be everything to everybody. I'm not going to tell you that WPJ can answer every question you ever had about programming in Windows, because different sources offer different things to their readers. We write about the topics we think the most people can benefit from, but in some ways we're much different than, say, "Windows/DOS Developer's Journal". Not to say we're better, just different. Don't limit yourself to just one source of information. If you have a specific question about Windows programming, you have several options: write us a letter and we'll try to answer it, or, if you have a ID on CompuServe, America Online, GEnie, etc., post it and see if you get a response. Microsoft also puts out a Developer CD full of articles (yes, I have this CD and it's a great source). What I'm leading up to is that the sysops for these services (and their members) help people learn this stuff, but sometimes a complete answer isn't possible (due to space constraints), so why not refer someone to WPJ if we have an article that answers the question? Much like I frequently refer to The Waite Group's Windows API Bible (see review in this issue), I also read some of the other Windows programming magazines. Sometimes they help, sometimes not. As long as WPJ is useful and offers something you can't get elsewhere, we'll stay around. A magazine shouldn't exist for its own sake, and judging from your response, we're succeeding in filling a niche. Pete and I didn't start this because we were bored; we saw an area we thought wasn't getting addressed, namely, help beginners learn the basics, while offering advanced articles for the people who have been around for a while. If you don't like what we're doing, send us a note. If we keep getting positive letters and don't hear anything to the contrary, we'll continue what we're doing. Tammy's concerns about accuracy are well-founded. We'll be the first ones to admit it if we make a mistake. I learn just as much from our contributing authors as you do, but if a mistake slips through, I want to know about it. With your help, we can make WPJ a helpful source of - 58 - info. 'Nuff said. - 59 -