
|
HomePage | About Us | HTML | DelpHi | Downloads | Credits | | BookMark This Site |
|
|
|
- 14 - Advanced Programming
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.
Implementing Context-S ensitive Help 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 the Help File 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.
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).
Context Identifiers and the HelpContext Property 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.
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 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.
Setting the Help File 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 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.
Adding F1 Key Support 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.
Adding Menu Support for the Help File 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); 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.
Context-Sensitive Help on Demand 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.
Using Help Include Files 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.
LISTING 14.1. HELP.INC. const 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.
Putting It to Work 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:
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.
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.
Checking Errors with Exception Handling 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.
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 Keywords: try, except, finally, and raise Exception-handling syntax is not terribly complicated. The four exception-handling keywords are try, except, finally, and raise. The try, efcept, 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 with except try 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 with finally try 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.
LISTING 14.2. AN EXCEPTION-HANDLING EXAMPLE. 01:
procedure TMainForm.Open1Click(Sender: TObject); 19:
end; 20:
Child.Caption := 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.
Raising Exceptions 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
if
BadParameter then 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).
More on except 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 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 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.
Multiple Exception Types 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 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 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 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-
Using finally 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); Here, the memory allocated for Buffer will always be freed properly regardless of whether an exception is raised in the try block.
Catching
Unhandled Exceptions at the 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.
You hook the OnException event just like yo u do when hooking the OnHint event like you did earlier:
Now your MyOnException method will be called when an unhandled exception occurs.
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); 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.
Debugging and Exception Handling 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.
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 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.
Using the Registry 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.
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.
Registry Keys 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
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 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.
Registry Data Types 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 TRegistry Class 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 Properties 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.
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.
TRegistry Methods 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.
TABLE 14.1. PRIMARY METHODS OF TRegistry.
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 Using TRegistry is fairly easy. Most of your interaction with the Registry will require just these four steps:
The following code illustrates these steps:
procedure
TForm1.FormCreate(Sender: TObject); 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.
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.
Writing to the Registry 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);
`Software\SAMS\Delphi 4 in 21 Days', True);
Reg.WriteInteger(`Top', Top); 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.
Using the Registry to Store Data 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.
LISTING 14.3. RegTestU.pas. unit
RegTestU;
Left := Reg.ReadInteger(`Left');
Width := Reg.ReadInteger(`Width'); begin
Close; 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.
Implementing
Specialized Message 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.
More on Windows Messages 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.
Message Types 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.
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; 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 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.
WPARAM, LPARAM, and Message Cracking Every Windows message has two parameters: the WPARAM (short for word parameter) and the LPARAM (short for long parameter).
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); This is a fairly simple example, but it's indicative of what goes on each time a message is handled in a Windows application.
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; 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.
With that primer on Windows messages under your belt, let's take a brief look at exactly how to send messages.
Sending Versus Posting 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.
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.
Handling Events 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);
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.
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.
Handling Other Windows Messages 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 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.
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.
LISTING 14.4. Message.h. TMainForm =
class(TForm) 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 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); 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; 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); 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); 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.
User-Defined Messages 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 This code declares user-defined message called My_Message.
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
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); S :
string; begin 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.
Summary 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
Workshop 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."
Q&A
Q uiz
Exercises
In Review
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.
|