Today you get into some of the more advanced aspects of Windows programming with Delphi. Specifically, you learn about
You have a lot to do today, so let's get right to it.
Not too long ago, you might have been able to get by with not providing context-sensitive help in your applications. In fact, for small applications you might have been able to get by without providing a help file at all. I wouldn't recommend that in today's market, though. Users are increasingly demanding when it comes to features. A help file is a no longer an option but a required component of a full-featured application.
Context-sensitive help means that when the user presses the F1 key, a particular page of the application's help file is displayed depending on the program's current context. Take the Delphi IDE, for example. Let's say you have the Project Options dialog box open and have the Application page displayed. When you press F1 (or press the Help button on the dialog box), WinHelp runs and displays the help topic for the Application page of the Project Options dialog box.
Likewise, if you press F1 when the Object Repository Options dialog box is on the screen, you get help for that particular dialog box. Context-sensitive help works with menu items as well. If you highlight a menu item and press F1, you are taken to the help file page regarding that menu item.
To implement context-sensitive help in your application, you must perform the following steps:
Let's examine these steps one at a time.
Writing help files is a chore. I don't exactly hate it, mind you, but it's not one of the tasks I look forward to. If you are fortunate, you h ave a documentation department that writes the help files for you.
NOTE: Even some limited-budget programmers have documentation departments that write help files for them. Frequently they refer to the doc writers as "Honey" or "Dear." For example, a programmer might say to her documentation writer, "Honey, when you get done with that help page would you put the kids to bed?"
Regardless of who writes the help file, there must be some coordination between the help-file writer and the programmer. The context identifiers in the help file must match those specified for the components in the program itself. Although this is not exactly a difficult task, coordination is still required so that everyone is on the same page (pun intended).
A Windows help file might be constructed from several individual files. The source for a Windows help file is called the topic file. The topic file is a rich text format (.rtf) file with lots of special codes that the help compiler understands. If your help file includes graphics, you might have one or more graphics files. Graphics files used in creating a help file include bitmap (.bmp), Windows metafile (.wmf), and some other specialized graphics files.
Finally, you have the help project file (.hpj). The project file contains a description of how the help compiler should go about merging the topic file, the graphics files, and any other specialized files the target help file needs. It also includes a [MAP] section that maps context ID numbers to particular help topics. After the project file is created, it is compiled with a help compiler such as the Microsoft Help Workshop (you can find it in the \Delphi 4\Help\Tools directory). The Help Workshop takes the help project file and compiles it to produce the final help file (.hlp).
TIP: Although you can write help files with any word processor that suppo rts .rtf files, I strongly recommend buying a help-authoring program. A good help-authoring package can take much of the frustration out of writing help files. Commercial help authoring tools include ForeHelp by Fore
Front, Inc. (http://www.ff.com) and RoboHelp by Blue Sky Software (http://www.blue-sky.com). I have used ForeHelp and, for the most part, I like the way it works. There are also some good shareware help authoring tools out there. One such shareware help authoring program is HelpScribble. You can check out HelpScribble at http://www.ping.be/jg/.
Regardless of what method you use to create the help file, you need to have a context number associated with each major topic in the help file. The context number is used by the Windows help system, WinHelp32.exe, to display a particular page in the help file.
For example, let's say you have an Options dialog box in your application. When the user presses F1 with the Options dialog box open, your program will pass the context ID for that dialog box to WinHelp. WinHelp will run and display the page in the help file that explains the application's Options dialog box. You don't need a context ID for every page in the help file, but you should have a help context ID for your main topic pages, dialog boxes, and other major components of your application.
Most components (forms, menu items, and controls) have a property called HelpContext. This property contains the context ID that will be passed to WinHelp if the user presses the F1 key when that component has focus. By default, the HelpContext property is set to 0. If the HelpContext property is 0 for a component, the component will inherit the HelpContext value of its parent window. This enables you to set the HelpContext for a form and then, no matter which component on the fo rm has input focus, the help context for the form will be used when F1 is pressed.
NOTE: You might have noticed that the SpeedButton and ToolButton components do not have a HelpContext property. Because these components are non-windowed controls, they can never gain input focus. Therefore, the F1 key cannot be used with speed buttons and toolbar buttons.
At a minimum, you should provide context-sensitive help for each form in your application (a form being a dialog box or secondary window). Ultimately, it is up to you to decide which items to provide context-sensitive help for. If you are going to err, you should probably err on the side of providing too much context-sensitive help (if there is such a thing) rather than not enough.
Implementing context-sensitive help in your Delphi applications is relatively easy. As I said earlier, the real work in adding context-sensitive help to your Delphi applications is in the writing of the help file. The rest is easy in comparison.
Regardless of how you implement context-sensitive help, you first have to tell Windows the name of the help file for your application. To do that, you assign the help filename to the HelpFile property of the Application class. You can do this in one of two ways. The easier way is at design time via the Project Options dialog box.
On Day 9, "Projects, the Code Editor, and the Code Explorer," you learned about the project options. I discussed the fact that the Application page of the Project Options dialog box has a field called Help File that is used to specify the name of the help file for the application. Simply type the name of your help file in this field. VCL will assign the help file name to the HelpFile property, and then the application will use that filename each time help is requested.
You can also set the name of the h elp file at runtime. This might be necessary if you enable your users to place the help file in a directory of their own choosing. You could store the location of the help file in the Windows Registry (the Registry is discussed later, in the section titled "Using the Registry") and then assign the help file's path and filename to the Application object's HelpFile property. For example, part of your OnCreate event handler might look like this:
var Filename : string; begin Filename := GetHelpFileName; { user-defined function } Application.HelpFile := Filename;
Although it's not very common, you can change the HelpFile property at different points in your program if you want. You might do this to switch between different help files, for example. After you have the help filename set up, you can go on to the actual implementation of the help system.
To add F1 support for forms and components, all you have to do is set the HelpContext property to the matching context ID in the help file; VCL takes it from there. Be sure that you have assigned a help filename to the HelpFile property of the Application class and that the help file contains valid context identifiers.
In addition to F1 key support, most applications have one or two menu items under the Help menu (where else?) that can be used to start WinHelp. There will usually be a menu item called Contents. Choosing this item will display the contents page for the help file. In addition to a Contents menu item, some applications have a Help menu item called Help Topics. Choosing this menu item displays the index for the help file. (The index is created as part of the help file creation process.)
To implement these and other Help items, you have to do some programming. (It's only one line of code in each case.) VCL provides a method called HelpCommand that can be used to display WinHelp in one of several modes. If you were to implement the Help|Contents menu item, the code would look like this:
procedure TForm1.Contents1Click(Sender: TObject); begin Application.HelpCommand(HELP_FINDER, 0); end;
The HelpCommand method calls WinHelp with the specified command. (See the Windows API help under WinHelp for a complete list of available commands.) In this case, WinHelp is invoked with a command of HELP_FINDER. This command tells WinHelp to display the contents page, as shown in Figure 14.1. The final parameter of the HelpCommand method is used to pass additional data to WinHelp. This parameter is not used with the HELP_FINDER command, so it is set to 0.
FIGURE 14.1. The ScratchPad Help Contents page.
NOTE: In order for WinHelp to show the Contents page, you must have a contents file for your help file. The contents file has an extension of .cnt and is a text file that describes how the contents page should be displayed. You can read more about the contents file in the Microsoft Help Workshop help. You can find the Help Workshop and its help file in the \Delphi 4\Help\Tools directory.
NOTE: A good index for your help file is invaluable. The quality of the help file--and the quality of the help file's index--are directly proportional to the amount of technical support you will have to provide. Don't overlook this fact.
Most of the time the two help implementations I just told you about are all you need for your application. At other times, however, you need to call WinHelp directly and with a specific context ID. For these times, VCL provides the HelpContext method. This method takes, as its single parameter, the context ID of the page you want to see when WinHelp runs. For example, let's say you have a help page with a context ID of 99. To run WinHelp and display th at specific page, you would do the following:
Application.HelpContext(99);
By supplying a specific context ID, you can cause WinHelp to display any page of your help file on demand. This is what VCL does for you when you specify the HelpContext property for a particular component.
Most of the time, setting the HelpContext properties for the forms or components for which you want to implement context-sensitive help is all you need to do. If, however, you need to call specific help pages in your application, you might consider defining constants for your help identifiers. Using named constants is much easier than trying to remember an integer value for a particular help context ID.
In the last section, I talked about using the HelpContext method to call WinHelp with a particular context ID. I used the example of a help context ID of 99. Rather than using the numerical value of the context identifier, you can use a constant like this:
Application.HelpContext(IDH_FILEOPEN);
Obviously, the string is easier to remember than its integer value equivalent. The context-sensitive help symbols can be kept in a separate include file that is added to your application where needed (using the {$I} compiler directive). Listing 14.1 shows a typical include file containing context-sensitive help identifiers.
const IDH_FILENEW = 1; IDH_FILEOPEN = 2; IDH_FILESAVE = 3; IDH_FILESAVEAS = 4; IDH_FILEPRINT = 5; IDH_FILEPRINTSETUP = 6; IDH_FILEEXIT = 7; IDH_EDITUNDO = 8; IDH_EDITCOPY = 9; IDH_EDITCUT = 10; IDH_EDITPASTE = 11; IDH_EDITSELECTALL = 12; IDH_EDITWORDWRAP = 13; IDH_HELPABOUT = 14;
Somewhere in your source file you can add a line that includes the help file header:
{$I HELP.INC}
Now you can use the name of the context ID rather than the actu al integer value.
How the help include file is created depends on the tools you are using to write your help files. Most help-authoring software includes an option to generate an include file of some kind that contains the named constants. The specific implementation depends on whether the help file is being written by you or by a coworker and what help-authoring software you are using. If you are not using help-authoring software, you can simply type the symbols in.
It's time to put your newly acquired knowledge into practice. In this section, you add context-sensitive help to the ScratchPad program. (You knew we'd come back to old ScratchPad, didn't you?)
The book's code contains a simple help file for the ScratchPad program. Copy the help file, Scratch.hlp, to your working directory so that Delphi can find it when you build the ScratchPad application.
Context-sensitive help should take about 10 minutes to implement. Here you go:
Application.HelpCommand(HELP_FINDER, 0);
Run the program and experiment with it. If you press F1 when the cursor is in the memo window, the Contents page of the help file will be displayed. If you highlight a menu item and press F1, help for that menu item is displayed. Also check out the Contents item on the main menu to see whether it works as expected.
TIP: If you compile and run the ScratchPad program for Day 14 that comes with the book's code, you will see that it has a feature not discussed here. The program has a Help Mode that can be used to get help on a specific speedbar button or menu item. To start the Help Mode, press the Help button on the speedbar. The cursor changes to the help cursor. Now select any menu item or speedbar button. The help topic for the item selected is displayed. The Help Mode turns off automatically after you choose an item. Browse the source code to see how the Help Mode is implemented. This feature also makes use of special message handling as discussed later in the chapter.
Context-sensitive help is no longer a luxury. Whether your users are members of the general public or your coworkers, they are still your users. They demand certain features and context-sensitive help is one of them. As easy as context-sensitive help is to implement in a Delphi application, there is no reason for it to be missing from your applications.
Even in the most well-designed program, events that are beyond the control of the programmer can go wrong. Users make mistakes. For example, a user might input a bad value into a data field or open a file of the wrong type for your application. Whatever the scenario , you should be prepared to handle these types of errors wherever and whenever possible. You can't anticipate your users' every move, but you can anticipate some of the more common blunders and handle them gracefully.
Exception handling is essentially a sophisticated form of error checking. Although any program can implement and use them, exceptions are of primary benefit to users of components and other Object Pascal classes. For example, if you are using a component and something nasty happens within the component, you need to know about it. A well-written component will raise an exception when something goes wrong. You can catch that exception and handle it however you want--maybe by terminating the program or by allowing your user to correct the problem and try again.
You might not write a lot of exception handling into your day-to-day code. Your primary use of exceptions will be in handling exceptions that VCL raises when things go wrong within a component. If you write components, you will almost certainly use exception handling more frequently.
NOTE: The use of exceptions in your programs should be reserved for extreme error conditions. You should raise an exception when the error condition is serious enough to make continued use of the program difficult. Most runtime errors can be handled with parameter checking, validation of user input, and other more traditional error-checking techniques.
A full discussion of exception handling could easily take an entire chapter, so I limit this discussion to how you can handle exceptions raised by VCL components.
Exception-handling syntax is not terribly complicated. The four exception-handling keywords are try, except, finally, and raise. The try, except, and finally keywords are used when handling exceptions, and the raise keyword is used to initiate an exception. Let's look at these in more detail.
try TryStatements except on TypeToCatch do begin ExceptStatements end; end;
The try keyword marks the beginning of the try block. The statements in TryStatements are executed. If any exceptions are raised during the execution of TryStatements, ExceptStatements are executed. If no exception is raised, ExceptStatements are ignored and program execution proceeds to the statement following the end statement. TypeToCatch is of the VCL exception handling classes. If the optional TypeToCatch is not specified, all exceptions are caught regardless of the type of exception raised.
try TryStatements finally FinallyStatements end;
The try keyword marks the beginning of the try block. The statements in TryStatements are executed. FinallyStatements will always be executed regardless of whether an exception occurs in TryStatements.
Before I attempt to explain the try and except keywords, let's look at a simple example. Listing 14.2 contains the File|Open event handler from the Picture Viewer program you created on Day 4, "The Delphi IDE Explored." The event handler has been modified to use exception handling. If you recall, this code attempts to load a picture file (.bmp, .wmf, or .ico). If the file chosen by the user is not a picture file, VCL raises an exception. If that happens, you need to catch the exception and display a message box telling the user that the file is not a picture file.
01: procedure TMainForm.Open1Click(Sender: TObject); 02: var 03: Child : TChild; 04: begin 05: if OpenPictureDialog.Execute then begin 06: Child := TChild.Create(Self); 07: with Child.Image.Picture do begin 08: try 09: LoadFromFile(OpenPictureDialog.FileName); 10: Child.ClientWidth := Width; 11: Child.ClientHeight := Height; 12: except 13: Child.Close; 14: Application.MessageBox( 15: `This file is not a Windows image file.', 16: `Picture Viewer Error', MB_ICONHAND or MB_OK); 17: Exit; 18: end;
19: end;
20: Child.Caption := 21: ExtractFileName(OpenPictureDialog.FileName); 22: Child.Show; 23: end; 24: end;
In this code, you see a try block (line 8) and an except block (line 12). The try block is used to define the code for which an exception might be raised. The try statement tells the compiler, "Try this and see if it works." If the code works, the except block is ignored and program execution continues. If any of the statements inside the try block raise an exception, the code within the except block is executed. The except block must immediately follow the try block.
It is important to realize that as soon as an exception is raised, program execution jumps immediately to the except block. In this example, the call to LoadFromFile on line 9 could raise an exception. If an exception is raised, program execution jumps to the first statement following the except keyword. In that case, lines 10 and 11 will never be executed.
As you can see, the except statement catches an exception that was raised somewhere
in the program. Most of the time, this means an exception was raised in VCL somewhere
(or in a third-party component library if you have other component libraries installed).
An exception is raised by using the raise keyword. The code that raises the exception
does so for a particular class. For example, a typical raise statement might look
like this:
if BadParameter then raise EInvalidArgument.Create(`A bad parameter was passed');
The raise statement raises an instance of an exception-handling class, EInvalidArgument in this case. When you write your own code, you can raise exceptions of the exception classes created by VCL or you can create your own exception classes. Let's say you have an error code 111 for a particular type of error and that you have an exception class named EMyException. In that case, you can write the raise statement like this:
raise EMyException.Create(`Error 111');
The compiler makes a copy of the object being raised and passes it on to the except statement immediately following the try block (I'll get back to that in a moment).
As I said, for starters your involvement with exceptions will likely be in catching VCL exceptions. If something goes wrong in a VCL component, VCL is going to raise an exception. If you do not handle the exception, VCL will handle it in the default manner. Usually this means that a message box will be displayed that describes the error that occurred. By catching these exceptions in your code, you can decide how they should be handled rather than accepting the default behavior.
Take a look at Listing 14.2 again. Starting at line 12, you see this code:
except Child.Close; Application.MessageBox( `This file is not a Windows image file.', `Picture Viewer Error', MB_ICONHAND or MB_OK); Exit; end;
The except keyword tells the compiler that you want to catch all exceptions of any type. Because no case statement follows the except statement, the code in the except block will be executed regardless of the type of exception raised. This is convenient if you don't know what type of exception a particular piece of code will raise or if you want to catch any and all exceptions regardless of the type. In the real world, though, you will probably need to be more specific in what exceptions you catch.
Let's go back to Listing 14.2 again. The code on line 9 might raise an exception if the file the user is trying to open is not a graphics file. If that happens, VCL will raise an EInvalidGraphic exception. You can catch that type of except ion only and let all others be handled in the default manner. The code looks like this:
except on EInvalidGraphic do begin Child.Close; Application.MessageBox( `This file is not a Windows image file.', `Picture Viewer Error', MB_ICONHAND or MB_OK); Exit; end; end;
Notice that this code is catching an EInvalidGraphic exception only. This is the preferred method of catching exceptions. Now any exceptions of this type will be caught by the except block and all other exception types will be handled in the default manner.
It is important to understand that when you catch an exception, code execution continues after executing the except block. One of the advantages to catching exceptions is that you can recover from the exception and continue program execution. Notice the Exit statement in the preceding code snippet. In this case, you don't want to continue code execution following the exception, so you exit the procedure after handling the exception.
NOTE: VCL will handle many types of exceptions automatically, but it cannot account for every possibility. An exception that is not handled is called an unhandled exception. If an unhandled exception occurs, Windows generates an error message and your application is terminated. Try to anticipate what exceptions might occur in your application and do your best to handle them.
You can catch multiple exception types in your except block. For example, say you have written code that calls some VCL methods and also calls some functions in your program that might raise an exception. If a VCL EInvalidGraphic exception occurs, you want to handle it. In addition, your own code might raise an exception of type EMyOwnException (a class you have written for handling exceptions). You want to handle that exception in a different manner. Given that scenario, you write the code like this:
try OneOfMyFu nctions; Image.Picture.LoadFromFile(`test.bmp'); AnotherOfMyFunctions; except on EInvalidGraphic do begin { do some stuff } end; on EMyOwnException do begin { do some stuff } end; end;
In this case you are preparing to catch either a VCL EInvalidGraphic exception or your EMyOwnException. You want to handle each type of exception in a specific manner, so you catch each type of exception independently.
NOTE: Creating your own exception handling class can be as simple as one line of code:type EMyOwnException = class(Exception);
Often you create you own exception classes simply to distinguish the type of exception. No additional code is required.
Note that in the preceding examples you are catching a VCL exception class, but you aren't actually doing anything with the class instance passed to the except block. In this case you don't really care what information the EInvalidGraphic class contains because it is enough to simply know that the exception occurred.
To use the specific instance of the exception class passed to the except block, declare a variable for the exception class in the do statement--for example,
try Image.Picture.LoadFromFile(`test.bmp'); except on E : EInvalidGraphic do begin { do something with E } end; end;
Here the variable E is declared as a pointer to the EInvalidGraphic class passed
to the except block. You can then use E to access properties or methods of the EInvalidGraphic
class. Note that the variable E is valid only within the block where it is declared.
You should check the online help for the specific type of VCL exception-
handling class you are interested in for more information about what properties and
methods are available for that class.
TIP: The VCL exception classes all have a Message property that contains a textua l description of the exception that occurred. You can use this property to display a message to the user:except on E : EInvalidGraphic do begin { do some stuff } MessageDlg(E.Message, mtError, [mbOk], 0); end; end;
Sometimes the VCL messages might not be descriptive enough for your liking. In those cases you can create your own error message, but often the Message property is sufficient.
The finally keyword defines a section of code that is called regardless of whether an exception occurs. In other words, the code in the finally section will be called if an exception is raised and will be called if no exception is raised. If you use a finally block, you cannot use except.
The primary reason to use finally is to ensure that all dynamically allocated memory is properly disposed of in the event an exception is raised--for example,
procedure TForm1.FormCreate(Sender: TObject); var Buffer : Pointer; begin Buffer := AllocMem(1024); try { Code here that might raise an exception. } finally FreeMem(Buffer); end; end;
Here, the memory allocated for Buffer will always be freed properly regardless of whether an exception is raised in the try block.
Your application can also handle exceptions at the application level. TApplication has an event called OnException that will occur any time an unhandled exception is raised in your application. By responding to this event, you can catch any exceptions raised by your applications.
NOTE: This event occurs when an unhandled exception is raised. Any exceptions you catch with try and except are handled; therefore, the OnException event will not occur for those exceptions.
You hook the OnException event just like yo u do when hooking the OnHint event like you did earlier:
procedure MyOnException(Sender : TObject; E : Exception);
procedure TForm1.MyOnException(Sender : TObject; E : Exception); begin{ Do whatever necessary to handle the exception here. }
end;
Application.OnException := MyOnException;
Now your MyOnException method will be called when an unhandled exception occurs.
CAUTION: If you do not handle an exception properly, you can lock up your program and possibly even crash Windows. It is usually best to let VCL and Windows handle exceptions unless you know exactly how to handle them.
The ShowException function can be used to display a message box that describes the error that occurred. This function is usually called by the default OnException handler, but you can use it in your applications as well. One of the parameters of the OnException event handler is a pointer to an Exception object (Exception is VCL's base exception-handling class). To display the error message box, pass the Exception object to the ShowException function:
procedure TForm1.MyOnException(Sender : TObject; E : Exception); begin { Do whatever necessary to handle the exception here { then display the error message. } Application.ShowException(E); end;
Now the error message box is displayed just as it would be if VCL were dealing with the unhandled exception.
As you can see, handling exceptions at th e application level is an advanced exception-handling technique and is not something you should attempt unless you know exactly what you're doing.
Simply put, debugging when using exception handling can be a bit of a pain. Each time an exception is raised, the debugger pauses program execution at the except block just as if a breakpoint were set on that line. If the except block is in your code, the execution point is displayed as it would be if you had stopped at a breakpoint. You can restart the program again by clicking the Run button, or you can step through your code.
NOTE: You might not be able to continue execution of your program after an exception is raised. Whether you can continue debugging depends on what went wrong in your program.
Sometimes the except block is in the VCL code. This will be the case for VCL exceptions that you don't handle in your code. In this circumstance, the CPU view will show the execution point.
Another aspect of debugging with exceptions is that the VCL exception message box is displayed even when you are handling the exception yourself. This can be confusing and somewhat annoying if you haven't encountered it before. To prevent the VCL message box from being displayed and the debugger from breaking on exceptions, go to the Language Exceptions page of the Debugger Options dialog box and uncheck the Stop on Delphi Exceptions check box at the top of the page. When Stop on Delphi Exceptions is off, the debugger will not pause program execution when a VCL exception is raised.
As with many other aspects of Delphi and VCL, learning exception handling takes
some time. Remember to use exception handling where necessary, but use traditional
error-handling techniques for the minor errors.
The book's code contains a program called EHTest. This program demonstrates raising and catching several kinds of exceptions. For this program to function properly, be sure that the Stop on Delphi Exceptions option is off as described earlier. You can download the book's code at http://www.mcp.com/info. Figure 14.2 shows the EHTest program running.
FIGURE 14.2. The EHTest program running.
Once upon a time, Windows programs used configuration files (.ini files) to store application-specific information. The master configuration file is, as you probably know, called WIN.INI. Applications could store system-wide information in WIN.INI or application-specific configuration data in a private .ini file. This approach has several advantages, but also some disadvantages.
Somewhere along the line someone smarter (presumably) than I decided that the new way to do things would be to use the Registry to store application-specific configuration information. Throughout the land, knaves everywhere bowed to the king and said, "The Registry is good." Whether good or bad, conventional wisdom has it that the use of .ini files is out and the use of the Registry is in.
The term Registry is short for the Windows Registration Database. The Registry contains a wide variety of information about your Windows configuration. Just about every option and every setting in your Windows setup is stored in the Registry. In addition to the system information stored in the Registry, you will find data specific to installed applications. The type of information stored for each application depends entirely on the application but can include information such as the last size and position of the window, a list of most recently used documents for the application, the last directory used when opening a file, and on and on. The possibilities are endless.
Windows 95 and NT come with a program called the Registry Editor (REGEDIT.EXE) that can be used to bro wse the Registry and to make changes to the entries in it.
CAUTION: Be very careful about making changes to the Registry! Registry changes can affect how individual programs operate and even how Windows itself operates.
Figure 14.3 shows the Registry Editor as it displays the Delphi Code Insight options.
FIGURE 14.3. The Windows Registry Editor.
As you can see from Figure 14.3, the Registry is hierarchical. You can approach the Registry exactly as you approach files and directories on a hard drive.
Each item in the Registry is called a key. A key can be likened to a directory on your hard drive. To access a particular key, you first have to open the key. After the key is open, you can write to or read from it. Take a look at Figure 14.3. The key that is being displayed is
MY COMPUTER\HKEY_CURRENT_USER\SOFTWARE\BORLAND\DELPHI\4.0\CODE INSIGHT
You can't see every branch of the Registry tree, but if you look at the status bar of the Registry Editor you can see the current key displayed. Also notice that the Delphi\4.0 key has several subkeys. You can create as many keys and subkeys as you want for your application.
An individual key can store data in its data items. Every key has a data item called (Default). The default value is not normally used because you will almost always create your own data items for a particular key. By looking at Figure 14.3, you can see that the Code Insight key has the following data items:
Auto Code Completions Auto Code Parameters Code Complete Delay Declaration Information Explorer Visible Scope Sort
If you've been paying attention, you will recognize that these data items correspond to the Code Insight options on the Code Insight page of the Environment Options dialog box. Each data item has an associated value. You can write to the Registry to change the data item or you can read the data item.
The Registry has the capability to store several different types of data in the data items. The primary types of data are the binary data, string, and integer data types. The binary data type can be used to store any kind of binary data. You can, for example, store an array of integers in a binary data item. You probably won't directly use the binary data type very often, if ever.
For most of your purposes, you will probably be concerned only with reading and writing strings or integer values. As you can see in Figure 14.3, even numerical data can be stored as strings. It's really up to you to decide how to store data in the Registry.
Up to this point, I have discussed what you can do with the Registry but not how you do it. Let's look at that next.
The Windows API provides several functions for Registry manipulation. These functions include RegCreateKey, RegOpenKey, RegQueryValue, RegSetValue, RegDeleteKey, and many more. Dealing with the Registry at the API level is a bit tedious. I am thankful (and you should be, too) that the folks at Borland thought to provide a VCL class called TRegistry that encapsulates Registry operations. This class provides everything you need to write to and read from the Registry. Before I get into how to use the TRegistry class, let's go over the properties and methods of this class.
TRegistry has just four properties. The CurrentKey property contains the value of the current key, which is an integer value that identifies the key. When you call a TRegistry method, that method acts on the current key. The CurrentKey property is set for you when you open a key. This property can be read, but reading it is of dubious value.
The RootKey and CurrentPath properties work together to build a text st ring to the current key. The CurrentPath property contains a text description of the current key's path excluding the RootKey value. Take this key, for example:
\HKEY_CURRENT_USER\Software\Borland\Delphi\4.0\Code Insight
In this case the root key, \HKEY_CURRENT_USER, comes from the RootKey property, and the value, Software\Borland\Delphi\4.0\Code Insight, comes from the CurrentPath property.
NOTE: By default, the RootKey has a value of \HKEY_CURRENT_USER. This is where you should store application-specific data, so it is not normally necessary to change the root key. If you need to change the root key, you can assign a new value to the RootKey property. Note that the root key types are not string values, but are special Windows-defined values. Other root key
values include HKEY_CLASSES_ROOT, HKEY_LOCAL_MACHINE, HKEY_USERS, HKEY_CURRENT_CONFIG, and HKEY_DYN_DATA.
The LazyWrite property determines how the application writes the data to the specific key. If LazyWrite is True, control is immediately returned to the application when you close the key. In other words, the writing of the key begins and then your application goes on its way. If LazyWrite is False, control will not return to the application until the writing of the key is completed. By default, LazyWrite is True, and you should leave it set to True unless you have some critical data that needs to be written before your application resumes operation.
The TRegistry class has several methods that you use to read from and write to the Registry. Table 14.1 lists the primary methods and their descriptions.
Method | Description |
CloseKey | Closes the key and writes the dat a to the key. You should close a key as soon as you are done with it, but you don't have to specifically call CloseKey because the TRegistry destructor will close the key for you. |
CreateKey | Creates a key but does not open it for use. Use the OpenKey method rather than CreateKey if you are going to create the key and begin writing data to it. |
DeleteKey | Deletes the specified key. You can specify any key to delete. To delete the current key, pass an empty string to DeleteKey. |
GetKeyNames | Returns all of the subkeys for the current key in a TStrings object. You can use this method if you need to iterate all the subkeys for a given key. |
GetValueNames | Returns the names of all of the data items for the current key. Use this method if you need to iterate the data items of a given key. |
KeyExists | Returns True if the key exists or False if it does not exist. You can use this method to check for the existence of a key before attempting to read the key. |
LoadKey | Loads a key previously stored on disk. See the Delphi online help for specific details. |
OpenKey | Opens the specified key. If the key does not exist, the value of the CanCreate parameter determines whether the key will be automatically created. Use this method rather than CreateKey if you are going to create the key and begin writing data to it because OpenKey creates the key and then opens it, whereas CreateKey just creates the key but does not open it. |
ReadBinaryData | Reads binary data from the specified data item. |
ReadBool | Reads a Boolean value from the specified data item. |
ReadDateTime | Reads a date and time value from the specified data item. The returned value is an instance of the TDateTime class. To retrieve just a date value, use ReadDate; to retrieve just a time value, use ReadTime. |
ReadFloat | Reads a floating-point value from the specified data item. |
ReadInteger | Reads an integer value from the specified data item. |
ReadString | Reads a string value from the specified data item. |
SaveKey | Saves a key to disk so that it can be loaded later with LoadKey. Generally speaking, you shouldn't store more than 2KB (2048 bytes) of data by using this method. |
ValueExists | Returns True if the specified data item exists. |
WriteBinaryData | Writes a binary data item to the specified key. Use this item to store arrays or other types of binary data. |
WriteBool | Writes a Boolean value to the specified data item. The value is converted to an integer and then stored in the data item. |
WriteDateTime | Writes a TDateTime obje ct to the specified data item. To store just a date object, use WriteDate. To store just a time object, use WriteTime. The TDateTime object is converted to a binary data type before being stored. |
WriteFloat | Writes a floating-point value to the specified data item after converting it to binary data. |
WriteInteger | Writes an integer value to the specified data item. |
WriteString | Writes a string to the specified data item. |
Although there are a lot of methods listed in Table 14.1, many of them perform the same operations--they just use different data types. When you know how to use one of these methods, you pretty much know how to use them all. Notice that several of these methods convert the value passed to binary data and then store it in the Registry.
Using TRegistry is fairly easy. Most of your interaction with the Registry will require just these four steps:
NOTE: Before you can use TRegistry, you must add the Registry unit to your form's uses list.
The following code illustrates these steps:
procedure TForm1.FormCreate(Sender: TObject); var Reg : TRegistry; KeyGood : Boolean; Top : Integer; Left : Integer; Width : Integer; Height : Integer; begin Reg := TRegistry.Create; try KeyGood := Reg.OpenKey( `Software\SAMS\Delphi 4 in 21 Days', False); if not KeyGood then begin Top := Reg.ReadInteger(`Top'); Left := Reg.ReadInteger(`Left'); Width := Reg.ReadInteger(`Width'); Height := Reg.ReadInteger(`Height'); SetBounds(Left, Top, Width, Height); end; finally Reg.Free; end; end;
This code opens a key and reads values for the top and left coordinates and the width and height of a form. It then calls the SetBounds function to move or size the window. Notice that the result of the OpenKey method is assigned to a Boolean variable. OpenKey returns True if the key was successfully opened and False if it was not. If the key was successfully opened, the individual data items are read. You should always check the return value of OpenKey if there is any doubt that opening the key might fail. Notice also that this code uses a try. . . finally block to ensure that the Reg variable is properly freed before the function returns.
NOTE: VCL will raise exceptions if reading data from or writing data to a key fails. If you attempt to read data from an unopened key, you will get an exception. Either be prepared to handle the exception or be sure to check the return value from OpenKey before reading or writing data items.
Finally, notice that the key is not specifically closed in the preceding code. If you neglect to close the key, the TRegistry destructor will close the key for you. In that case, the destructor will be called (and the key closed) as soon as the Reg object is deleted, so an explicit call to CloseKey is not necessary.
NOTE: The OpenKey function automatically prepends the value of the RootKey property (HKEY_CURRENT_USER by default) to the front of the string passed to OpenKey, so you don't have to include the root when opening a key.
I have the cart just slightly b efore the horse here because I am talking about reading from the Registry when you haven't yet written to it. No matter--writing to the Registry is just as simple:
procedure TForm1.FormDestroy(Sender: TObject); var Reg : TRegistry; begin Reg := TRegistry.Create; try Reg.OpenKey(
`Software\SAMS\Delphi 4 in 21 Days', True);
Reg.WriteInteger(`Top', Top); Reg.WriteInteger(`Left', Left); Reg.WriteInteger(`Width', Width); Reg.WriteInteger(`Height', Height); finally Reg.Free; end; end;
This code simply opens a key and writes the form's Top, Left, Width, and Height properties to the key using the WriteInteger method. Notice that the last parameter of the OpenKey method is True. This specifies that the key should be created if it does not yet exist. If you use this construct, you should never need to call the CreateKey method at all.
That's basically all there is to reading values from and writing values to the Registry. The other data reading and writing methods are just variations on the previous code snippet.
Listing 14.3 shows the main unit of a program called RegTest that uses the Registry to store application-specific data. This program stores several items in the Registry: the last size and position of the window; the window state (normal, minimized, or maximized); the last directory, last file, and last filter index used when opening a file with the File Open dialog box; and the date and time the program was last run. To clear out the Registry key created by the RegTest program, you can click the Delete Key button on the main form (see Figure 14.4, later in the chapter). This program is also included with the book's code.
unit RegTestU; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, Menus, StdCtrls, ExtCtrls, Registry; type TForm1 = class(TForm) Panel1: TPanel; Label1: TLabel; DeleteKey: TButton; Panel2: TPanel; Label2: TLabel; Label3: TLabel; TimeLabel: TLabel; DateLabel: TLabel; MainMenu: TMainMenu; File1: TMenuItem; FileOpen: TMenuItem; FileExit: TMenuItem; OpenDialog: TopenDialog; procedure FormCreate(Sender: TObject); procedure FileOpenClick(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure FileExitClick(Sender: TObject); procedure DeleteKeyClick(Sender: TObject); private { Private declarations } KeyDeleted : Boolean; public { Public declarations } end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.FormCreate(Sender: TObject); var Reg : TRegistry; KeyGood : Boolean; DT : TDateTime; Top : Integer; Left : Integer; Width : Integer; Height : Integer; begin { Initialize the KeyDeleted variable to False. This { variable is used if the user deletes the key from { the program. See the MainFormClose function. } KeyDeleted := False; { Create a TRegistry object to access the registry. } Reg := TRegistry.Create; try { Open the key. } KeyGood := Reg.OpenKey( `Software\SAMS\Delphi 4 in 21 Days', False); { See if the key is open. If not, then this is the first { time the program has been run so there's nothing to do. } { If the key is good then read all of the data items { pertinent to application startup. } if KeyGood then begin Top := Reg.ReadInteger(`Top');
Left := Reg.ReadInteger(`Left');
Width := Reg.ReadInteger(`Width'); Height := Reg.ReadInteger(`Height'); SetBounds(Left, Top, Width, Height); WindowState := TWindowState(Reg.ReadInteger(`WindowState')); { The TDateTime class is a handy item to have around { if you are doing date and time operations. } DT := Reg.ReadDate(`Date and Time'); DateLabel.Caption := DateToStr(DT); TimeLabel.Caption := TimeToStr(DT); en d; finally Reg.Free; end; end; procedure TForm1.FileOpenClick(Sender: TObject); var Reg : TRegistry; begin { This function displays the File Open dialog but { doesn't actually open a file. The last path, filter, { and filename are written to the registry when the { user presses OK. } { Create a TRegistry object to access the registry. } Reg := TRegistry.Create; try { Open the key. } Reg.OpenKey(`Software\SAMS\Delphi 4 in 21 Days', True); { Read the values. Check if the value exists first. } if Reg.ValueExists(`LastDir') then OpenDialog.InitialDir := Reg.ReadString(`LastDir'); if Reg.ValueExists(`LastFile') then OpenDialog.FileName := Reg.ReadString(`LastFile'); if Reg.ValueExists(`FilterIndex') then OpenDialog.FilterIndex := Reg.ReadInteger(`FilterIndex'); { Display the File Open dialog. If the user presses OK then { save the path, filename, and filter to the registry. } if OpenDialog.Execute then begin Reg.WriteString(`LastDir', ExtractFilePath(OpenDialog.FileName)); Reg.WriteString(`LastFile', ExtractFileName(OpenDialog.FileName)); Reg.WriteInteger (`FilterIndex', OpenDialog.FilterIndex); end; finally Reg.Free; end; end; procedure TForm1.FormDestroy(Sender: TObject); var Reg : TRegistry; begin { If the user pressed the button to the key then { we don't want to write out the information. } if KeyDeleted then Exit; { Create a TRegistry object to access the registry. } Reg := TRegistry.Create; try { Open the key. } Reg.OpenKey( `Software\SAMS\Delphi 4 in 21 Days', True); { Write out the values. } Reg.WriteInteger(`WindowState', Ord(WindowState)); if WindowState <> wsMaximized then begin Reg.WriteInteger(`Top', Top); Reg.WriteInteger(`Left', Left); Reg.WriteInteger(`Width', Width); Reg.WriteInteger(`Height', Height); end; Reg.WriteDate(`Date and Time', Now); finally Reg.Free; end; end; procedure TForm1.FileExitClick(Sender: TObject);
begin
Close; end; procedure TForm1.DeleteKeyClick(Sender: TObject); var Reg : TRegistry; begin { The user pressed the Key button so delete the key. } { Set a flag so that we don't re-create the key when { the program closes. } Reg := TRegistry.Create; try Reg.DeleteKey(`Software\SAMS'); KeyDeleted := True; finally Reg.Free; end; end; end.
By examining this listing and running the RegTest program, you can learn a lot about using the Registry in your applications. Figure 14.4 shows the RegTest program running; Figure 14.5 shows the Registry key that is created by the program.
FIGURE 14.4. The RegTest program running.
FIGURE 14.5. The Registry Editor showing the key created by the RegTest program.
On Day 11, "Delphi Tools and Options," I talked about Windows messages as part of the discussion of the WinSight program. For the most part, Delphi makes message handling easy through its use of events. As I have said, an event is usually generated in response to a Windows message being sent to an application. There are times, however, when you might want to handle messages yourself. There are two primary scenarios that require you to handle messages outside of the normal Delphi messaging system:
Handling messages on your own requires a few extra programming techniques; you learn about those techniques in this section.
How do Windows messages get sent? Some messages are sent by Windows itself to instruct the window to do something or to notify the window that something has happened. At other times, messages are sent by the programmer or, in the case of VCL, by the framework the programmer is using. Regardless of who is sending the messages, you can be guaranteed that a lot of messages are flying around at any given millisecond.
Basically, messages fall into two categories:
To illustrate the difference between these two types of messages, let's take a look at the standard edit control. The standard Windows edit control has almost 80 command messages and nearly 20 notification messages. Surprised? It's true, and don't forget that the edit control is just one control out of dozens of Windows controls.
NOTE: I lied to you back on Day 5, "The Visual Component Model," when I talked about events (well, not exactly). At that time I said that there are over 200 messages that Windows could send to an application, and that is essentially true. But when you consider messages specific to controls as well as messages that can be sent to main windows, the count goes to something like 700 messages. Wow! And that only includes command messages, not notification messages. You should have a new appreciation for VCL after reading this section.
A programmer writing a Windows program in C has to send a lot of messages to accomplish tasks. For example, to get the length of the currently selected text in an edit control requires the following code:
int start; int end; long result = SendMessage(hWndEdit, EM_GETSEL, 0, 0); start = LOWORD(result); end = HIWORD(result); int length = end - start;
This is how C programmers spend their days. Contrast that with the VCL way of doing things:
Length := Edit.SelLength;
Which do you prefer? Regardless of who is sending the mes sage, command messages are used heavily both by programmers and by Windows itself.
Notification messages, however, are sent only by Windows itself. Notification messages tell Windows that something has happened within a control. For example, the EN_CHANGE message is sent when the contents of an edit control have changed. VCL has an event for the Edit, MaskEdit, and Memo components called OnChange that is triggered in response to the EN_CHANGE notification message (as well as some others). Programmers using traditional Windows programming tools have to intercept these messages and deal with them as needed--which brings you to the next subject: message parameters.
Every Windows message has two parameters: the WPARAM (short for word parameter) and the LPARAM (short for long parameter).
NOTE: In 32-bit Windows, WPARAM and LPARAM are both 32-bit values. In 16-bit Windows, the WPARAM is a 16-bit value (a Word), and the LPARAM is a 32-bit value (a LongInt). That ends your history lesson on the names WPARAM and LPARAM. And now back to your program. . .
These two parameters could be likened to the parameters sent to a function. When a Windows message is received, the parameters are examined to obtain information specific to the message being sent. For example, the WM_LBUTTONDOWN message is a notification message that is sent when the left mouse button is clicked on a window. For a WM_LBUTTONDOWN message, the WPARAM contains a special code that tells which of the other mouse buttons were pressed and also what keys on the keyboard were pressed when the event occurred. The LPARAM contains the coordinates of the mouse cursor when the mouse button was clicked: the X position is contained in the low-order word and the Y position is contained in the high-order word.
To get at this information, the message must be cracked to reveal what's inside. The code for cracking a WM_LBU TTONDOWN message can look like this:
procedure MessageCracker(wParam, lParam : Integer); var Shift, X, Y : Integer; begin Shift := wParam; X := LOWORD(lParam); Y := HIWORD(lParam); { Code that does something with Shift, X, and Y. } end;
This is a fairly simple example, but it's indicative of what goes on each time a message is handled in a Windows application.
NOTE: Because the X and Y coordinates in the preceding example are contained in a single value (the lParam variable), they must be individually extracted. HIWORD and LOWORD are used for that purpose. In the C/C++ programming world, HIWORD and LOWORD are Windows macros. In Object Pascal, LOWORD is simply a type that is identical to a Word. HIWORD is a function that extracts the left-most 16 bits from a 32-bit variable. LOWORD, when used as you see in the preceding code snippet, extracts the right-most 16 bits.
You'll be relieved to know that VCL performs message cracking for you for all VCL events. For example, if you set up a message handler for the OnMouseDown event, Delphi generates a function in your code that looks like this:
procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin if Button = mbLeft then begin { Put your code here. } end; end;
As you can see, the event handler generated by Delphi contains all the information you need. VCL will crack the WPARAM and LPARAM for you and hand them to you in sensible pieces that you can deal with more easily. This same thing happens for every message for which VCL creates an event.
NOTE: VCL takes messaging and message cracking one step further in the case of the mouse button messages. Windows actually generates up to three different mouse down messages: one for the left button being pressed, one for the middle button (if one exists), and yet another when the right button is pressed. VCL handles all of these messages in a single event. The TMouseButton parameter of the OnMouseDown event tells you which of the three buttons was pressed.
With that primer on Windows messages under your belt, let's take a brief look at exactly how to send messages.
The Windows API provides two functions for sending messages: PostMessage and SendMessage. The PostMessage function posts the message to the Windows message queue and immediately returns. This function simply hands the message to Windows and goes on its way. PostMessage returns 1 if the function call succeeded or 0 if it did not. (About the only reason for PostMessage to fail is if the message is sent to an invalid window.)
The SendMessage function, however, sends the message to Windows and waits until the message is carried out before returning. The return value from SendMessage depends on the message being sent. Sometimes the biggest reason to use SendMessage over PostMessage is because you need the return value from a particular message.
NOTE: The differences between situations when you use PostMessage versus SendMessage are subtle. For example, due to timing issues within Windows, there are times when sending a message will fail to have the effect you expected. In those cases you'll have to use PostMessage. The reverse is also true: Sometimes PostMessage is wrong for a given set of circumstances and you'll have to use SendMessage. Although this is not something you really need to be concerned with right now, it is something to file away for future reference.
You can use both PostMessage and SendMessage in your Delphi applications. For example, to post yourself a message you would do something like this:
PostMessage(Handle, WM_QUIT, 0, 0);
The first parameter of both PostMessage and SendMessage is the window handle of the window to which you want to send the message. In this case you are sending the message to the main form (assuming this code was written in the main form's source code unit).
In addition to the Windows API functions, VCL provides a method called Perform that you can use to send messages to any VCL window. Perform bypasses the Windows messaging system and sends the message directly to the message-handling mechanism for a given window. The Perform equivalent of the preceding example is as follows:
Perform(WM_QUIT, 0, 0);
You can use any of these three functions to send messages to your application and to other windows within your application.
You've already learned about handling VCL events, but a short review can't hurt. When a particular component receives a Windows message, it looks up the message and checks to see whether there is an event handler assigned for that particular message. If an event handler has been assigned for that event, VCL calls the event handler. If no event handler is assigned, the message is handled in the default manner. You can handle any messages you like and ignore the rest.
What happens in your event handlers depends on a variety of factors: the particular message being handled, what your program does in response to the message, and whether you are modifying the incoming message or just using the event handler as notification that the event occurred. As you get further into Windows programming, you will see that there are literally hundreds of things you might do in response to events.
Most of the time, you will be using the event handler for notification that a particular event occurred. Take the OnMouseDown event, for example. If you handle the OnMouseDown event, you are simply asking to be notified when the user clicks the component with the mouse (remember that forms are components, too). You probably are not going to modify the message parameters in any way; you just want to know that th e event occurred. A great number of your event handlers will be used for notification purposes.
Sometimes, however, you want to change one or more of the message parameters before sending the message on its way. Let's say, for example, that you want to force the text in a particular edit control to uppercase. (You could set the CharCase property of the Edit component to ecUpperCase, of course, but let's stick with this example for now.) To change all keystrokes to uppercase, you could do something like this in your OnKeyPress event handler:
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char); begin if Key >= `a' then
Dec(Key, 32);
end;
In this way you are actually modifying some aspect of the message before it gets sent to VCL for processing. The parameters sent in the event handler are often passed by reference for exactly this reason.
NOTE: Delphi protects you a bit by passing message parameters that you should not change by value rather than by reference (by var parameter). This is also true of message parameters for which changing the value has no effect. Take the OnMouseDown event discussed earlier. This event is for notification only. There is no point in modifying the Button, Shift, X, or Y parameters, so all are passed by value.
Whether you change one or more of the parameters sent depends entirely on the message and what you intend to do with the message. Over time, you will likely run into situations where modification of a particular message is required to cause Windows to behave in a particular way. At other times, you will not modify the parameters at all, but will inspect them to determine what is happening with your application. Because the possibilities are so numerous, I'll have to leave further exploration of message parameters as an exercise for the reader.
There will undo ubtedly be some point when you need to respond to a Windows message for which VCL provides no event. When that time comes, you are going to want to know how to handle those messages, and that is what this section is about.
VCL provides events for the most commonly used Windows messages. Obviously, with over 700 Windows messages, VCL does not provide events for them all. The 80/20 theory says, among other things, that 20 percent of the people do 80 percent of the work. The same is probably true of Windows messages. VCL may only provide events for 20 percent of Windows messages, but they are the messages you will use 80 percent of the time. Still, there are plenty of Windows messages that VCL does not provide events for, and you need to know how to handle those messages when the time comes.
You'll be glad to know that you can handle just about any Windows message when you know how. After you have the basics down, each message is just a variation on the same theme. The mechanism that Delphi uses to handle messages not covered by VCL events is the message keyword. This keyword is used to associate a certain Windows message with a method in your code. When your window receives that message, the method is called. Hmmm. . . sounds like events, doesn't it? There are certainly some similarities.
Implementing message handling at this level is as easy as the following:
Here's an example of a method declaration for a method that handles the WM_ERASEBKGND message:
procedure WmEraseBkgnd(var Msg : TWMEraseBkgnd); message WM_ERASEBKGND;
Notice the message keyword at the end of the method declaration. The message keyword is followed by the Windows message that this method is designed to handle. Notice also that the method's parameter is a var TWMEraseBknd record. VCL has a message-cracking record for most Windows messages. The record name is the Windows message name preceded by T and minus the underscore.
As you can see, the Windows message WM_ERASEBKGND gets translated into the record named TWMEraseBkgnd. You sort of have to guess at the capitalization, but for the most part it makes perfect sense. This record gets passed along to the message handler (more on that in the next section). The method itself can be named anything you like, but the form you see here is traditional.
NOTE: The message-cracking records are defined in the VCL unit called Messages.pas. Browse the Messages unit if you need information on a particular message-cracking record.
To put this in perspective, you need to see the entire class declaration for a class that implements custom message handling. Listing 14.4 shows a typical main form class declaration for a class that uses custom message handling.
TMainForm = class(TForm) ShowTBoix: TButton; GroupBox1: TGroupBox; Hatched: TCheckBox; LetVCLHandle: TCheckBox; Instructions: TButton; procedure InstructionsClick(Sender: TObject); procedure ShowTBoixClick(Sender: TObject); procedure HatchedClick(Sender: TObject); private { Private declarations } procedure WmNCHitTest(var Msg : TWMNCHitTest); message WM_NCHITTEST; procedure WmEraseBkgnd(var Msg : TWMEraseBkgnd); message WM_ERASEBKGND; procedure WmGetMinMaxInfo(var Msg : TWMGetMinMaxInfo); message WM_GETMINMAXINFO; procedure MyMessage(var Msg : TMessage); message My_Message; public { Public declarations } end;
Notice in particular the method declarations in the private section. I put the message keyword on the line following the method declaration in order to keep the line length down. Normally you would put the message k eyword immediately following the method declaration. In the end, it doesn't matter to the compiler how you write the declarations, so suit yourself. Don't let the declaration for the WmMyMessage method throw you. This is the message handler for a user-defined message. I'll talk about user-defined messages a little later.
The message-handling method (or just message handler) is the method that is called whenever the message you are responding to is received by your application. The message handler has a single parameter: the message-cracker record I discussed earlier. The message handler for the WM_ERASEBKGND message would look like this:
procedure TMainForm.WmEraseBkgnd(var Msg: TWMEraseBkgnd); begin { Your message handling code here. } end;
As I said, the message-cracker record will contain all the parameters necessary to handle the message. The message-cracker record for the WM_ERASEBKGND message is as follows:
TWMEraseBkgnd = record
Msg: Cardinal; DC: HDC; Unused: Longint; Result: Longint; end;
All message-cracking records have two data members in common: Msg and Result. The Msg member contains the message that is being sent. This parameter is used by VCL and is not something you will be concerned with.
The Result data member, however, is important. This is used to set the return value for the message you are handling. The return value varies from message to message. For example, the return value from your message handler for WM_ERASEBKGND should return True (non-zero) if you erase the background prior to drawing, or False (zero) if you do not erase the background. (Check the Win32 API online help for the individual message you are processing to determine what to set the Result data member to.) Set the Result data member of the message-cracker record as needed:
procedure TMainForm.WmEraseBkgnd(var Msg: TWMEraseBkgnd); begin { Do some stuff. } Message.Result := 0; en d;
Any other data members of the message-cracking record will vary from message to message.
Sometimes you will need to call the default message handler for a particular message in addition to performing your own handling. In that case, you can call DefaultHandler. For example, you might want to paint on the background of your window in some circumstances but not in others. If you don't paint the background, you want VCL to paint the background in the default way. So, you would do this:
procedure TMainForm.WmEraseBkgnd(var Msg: TWMEraseBkgnd); begin if LetVCLHandle.Checked then begin DefaultHandler(Msg); Exit; end; { Do some other drawing. } Msg.Result := 1; end;
In other cases you will use DefaultHandler to perform some default processing for you. Whether you call DefaultHandler before you do your processing or afterward depends, again, on what you are trying to accomplish.
Besides the normal Windows messages, Windows enables you to create what is called a user-defined message.
New Term: A user-defined message is nothing more than a private message that you can send to yourself or to one of the other windows in your application.
Implementing and catching a user-defined message is nearly identical to handling a regular Windows message. The one exception is that you need to first define the message. You define a user-defined message like this:
const My_Message = WM_USER + 1;
This code declares user-defined message called My_Message.
NOTE: The symbol WM_USER is a special symbol that marks the beginning of the range of numbers that can be used for user-defined messages. You can use anything up to WM_USER + 31,743 for user-defined messages. I usually just pick something in the WM_USER + 100 range. The exact number you use is not important; just be sure that you do not accidentally use th e same value for two messages.
If you look back to Listing 14.4, you will notice the declaration for the user-defined message. After you define the message, you can declare the message handler as follows:
procedure MyMessage(var Msg : TMessage); message My_Message;
Notice that the message-cracker record passed is of the TMessage type. This is the generic message-cracker record. It is defined as follows:
TMessage = record Msg: Cardinal; WParam: Longint; LParam: Longint; Result: Longint); end;
NOTE: The actual declaration of TMessage looks slightly different, but the record shown here is the result.
When you send a user-defined message, you pass any parameters you want in the WParam and LParam members. For example, let's say you are sending a user-defined message indicating that error code 124 occurred on iteration 1019 of a processing loop. The call to Perform would look like this:
Res := MainForm.Perform(MyMessage, 124, 1019);
You could use either PostMessage or SendMessage to send the message, too, of course. For user-defined messages, you can probably use Perform for most of your messaging.
Okay, so the message is defined and sent. Now you need to write code to handle it. The message handler for the MYMESSAGE message might look like this:
procedure TMainForm.WmMyMessage(var Msg: TMessage); var
S : string;
begin S := Format(`Error #%d occurred on iteration number %d.', [Msg.WParam, Msg.LParam]); MessageDlg(`Error Message', mtError, [mbOk], 0); Msg.Result := 1; end;
The return value from Perform will be the value of the Result member of the TMessage record. It's up to you whether you send parameters and whether your message handler returns a result.
The book's code contains a program called MsgTest that illustrates the use of handling Windows messages not covered by VCL event s. It also illustrates the use of a user-defined message. This program illustrates the implementation of a Windows programming trick: The window can be dragged by clicking and dragging anywhere on the client area of the form.
You have covered a lot of ground today. You started with a look at context-sensitive
help and how to put it to use. Remember, context-sensitive help is not necessarily
easy, but it is something you should implement. After that, you looked at exception
handling and how you can use it to trap VCL exceptions. You have also gotten some
insights into the Windows Registry. The Registry is something that can and should
be used to store
application-specific data. Knowing how to use the Registry can help you to implement
those little features that make users smile. Finally, we ended with a discussion
on how to handle messages other than through the use of events. All in all, it was
a long day, but a rewarding one.
The Workshop contains quiz questions to help you solidify your understanding of the material covered and exercises to provide you with experience in using what you have learned. You can find the answers to the quiz questions in Appendix A, "Answers to the Quiz Questions."
DefaultHandler(Msg);
Wow, that was intense, wasn't it? But did you enjoy yourself? Are you starting to get the fever? By now I'll bet the wheels in your head are really turning. It's likely that you have already envisioned an application or two of your own. Maybe you have even begun work on an application. I hope you have because, as I have said many times, that is how you really learn. If you haven't yet developed an idea for an application, don't worry about it. It will come to you in good time.
This week includes a mixture of material. Early in the week, you found out the basics of building a Delphi application. You can drop components on a form all you want, but someday you have to write code. It can be daunting to branch out on your own. Delphi has lead you by the hand up to this point. But now it's time to leave the nest. You found out how to add your own functions and data members to your code. You also learned how to add resources such as bitmaps and sounds to your programs. This is good stuff. Before long you'll be doing all these things on your own like a pro.
This week's less-than-exciting material deals with more on Delphi projects and how to use the debugger. These chapters might not be flashy, but they still contain vital information that you need when developing applications. It's all about maximizing your time. Learning how to use the debugger takes a few hours or even a few days, but it will save you weeks of work in the long run. Trust me. If you know how to use the d ebugger, you can really get in there and find out what is going wrong when you encounter a bug in your program. If you don't have a good handle on the debugger, I urge you to go back and review Day 10. As I said, the debugger is not the most thrilling Delphi feature you learn about in this book, but it certainly is one of the most valuable. Learning how to effectively use your projects falls into this same category. Proper project management will save you time in the long run, too.
You learned a bit about graphics and multimedia programming this week. Graphics and sound can really add glitz to your programs. Don't overdo it, though, or your programs might turn your readers off. It's easy to get carried away with graphics and multimedia, so be careful to use these features only where needed.
Toward the end of the week, you learned about some features that can take an average program and turn it into a great program. Let's face it, window decorations such as status bars and toolbars cannot take a weak programming idea and transform it into a great program. No amount of gadgetry can do that. But if you have a good program to start with, adding bells and whistles can make your program stand out from the competition. There's nothing stopping you from adding these kinds of goodies to your applications because Delphi makes it easy.
You also learned about printing from a Delphi programming. Printing is, by nature, nonvisual. Delphi's great visual programming tools can't help you here. Still, Visual Component Library (VCL) makes printing much less frustrating than it would be with the straight API. When you begin printing, you will find that it is not all that difficult. Once again, experiment as much as you can. Playing around can be the best teacher. Don't worry about the boss--tell him or her that I said it was okay to play around. (That's play around not play a round. Don't blame me if you're caught playing golf on company time!)
Finally, you ended the week with some more in-d epth programming techniques. Implementing context-sensitive help is something you will have to do in today's competitive market. The good news is that Delphi makes it easy. The bad news is that there is no shortcut to writing good help files. Take my advice and get a good help authoring program. Such a program can take the frustration out of creating help files. If you write help files the hard way, you will probably get bogged down in the tedium and start skimping on the details. Using a good help authoring program helps you avoid that situation. Remember that the help file is as important as the program itself. After all, what good is a Maserati if you can't figure out how to get it out of first gear?
Using the Registry can put a real shine on your program, too. Oh, it won't make your program appear any different, but when you give the users options, you increase the value of your program. Storing these options in the Registry makes your job easier, too. There's no excuse for omitting features that users have come to expect. Sure, it takes a little time and you have to pay attention to detail, but there's no substitute for quality.
You ended the week with a discussion on message handling. This is a lower level of programming than you have encountered thus far. Handling messages is not something that you might have to do often, but when you must handle messages, knowing how will be more than half the battle. The section on message handling will benefit you down the road if not immediately.
© Copyright, Macmillan Computer Publishing. All rights reserved.