One of the most common ways of customizing existing components is to add predefined behavior to their event handlers. Every time you need to attach the same event handler to components of different forms, you should consider adding the event code to a descendant class of the component. An obvious example is edit boxes that accept only numeric input. Instead of attaching a common OnChar event handler to each edit box, you can define a new component.
However, this component won't handle the event; events are for component users only. Instead, the component can either handle the Windows message directly or override a method, often called a second-level message handler. The former technique was commonly used in the past, but it makes a component specific to the Windows platform. To create a component that's portable to CLX and Linux—and, in the future, to the .NET architecture—you should avoid low-level Windows messages and instead override virtual methods of the base component and control classes.
In addition to portability, there are other reasons why overriding existing second-level handlers is generally a better approach than handling straight Windows messages. First, this technique is more sound from an object-oriented perspective. Instead of duplicating the message-response code from the base class and then customizing it, you're overriding a virtual method call that the VCL designers planned for you to override. Second, if someone needs to derive another class from one of your component classes, you should make it as easy for them to customize as possible, and overriding second-level handlers is less likely to induce errors (if only because you're writing less code). For example, I could have written the following numeric edit box control by handling the wm_Char system message:
type TMdNumEdit = class (TCustomEdit) public procedure WmChar (var Msg: TWmChar); message wm_Char;
However, the code is more portable if I override the KeyPress method, as I've done in the code of the next component. (In a later example I'll have to handle custom Windows messages, because there is no corresponding method to override.)
To customize an edit box component to restrict the input it will accept, all you need to do is override its KeyPress method, which is called when the component receives the wm_Char Windows message. Here is the code for the TMdNumEdit class:
type TMdNumEdit = class (TCustomEdit) private FInputError: TNotifyEvent; protected function GetValue: Integer; procedure SetValue (Value: Integer); procedure KeyPress(var Key: Char); override; public constructor Create (Owner: TComponent); override; published property OnInputError: TNotifyEvent read FInputError write FInputError; property Value: Integer read GetValue write SetValue default 0; property AutoSelect; property AutoSize; // and so on...
This component inherits from TCustomEdit instead of TEdit so that it can hide the Text property and surface the Integer Value property instead. Notice that you don't create a new field to store this value, because you can use the existing (but now unpublished) Text property. To do so, you convert the numeric value to and from a text string. The TCustomEdit class (actually, the Windows control it wraps) automatically paints the information from the Text property on the surface of the component:
function TMdNumEdit.GetValue: Integer; begin // set to 0 in case of error Result := StrToIntDef (Text, 0); end; procedure TMdNumEdit.SetValue (Value: Integer); begin Text := IntToStr (Value); end;
The most important method is the redefined KeyPress method, which filters out all the nonnumeric characters and fires a specific event in case of an error:
procedure TMdNumEdit.KeyPress (var Msg: TWmChar); begin if not (Key in ['0'..'9']) and not (Key = #8) then begin Key := #0; // pretend that nothing was pressed if Assigned (FInputError) then FInputError (Self); end else inherited; end;
This method checks each character as the user enters it, testing for numerals and the Backspace key (which has an ASCII value of 8). The user should be able to use Backspace in addition to the system keys (the arrow keys and Del), so you need to check for that value.
Now, place this component on a form, type something in the edit box, and see how it behaves. You might also want to attach a method to the OnInputError event to provide feedback to the user when an incorrect key is pressed.
A Numeric Edit with Thousands Separators
As a further extension of the example, when the user types large numbers (stored internally as floating point numbers, which compared to integers can be larger and have decimal digits) it would be nice for the thousands separators to automatically appear and update themselves as required by the input:
You can do this by overriding the internal Change method and formatting the number properly. There are only a couple of small problems to consider. The first is that to format the number you need to have a string containing a number, but the text in the edit box is not a numeric string Delphi recognizes, as it has thousands of separators and cannot be converted to a number directly. I've written a modified version of the StringToFloat function, called StringToFloatSkipping, to accomplish this conversion.
The second small problem is that if you modify the text in the edit box, the current position of the cursor will be lost. So, you need to save the original cursor position, reformat the number, and then reapply the cursor position—considering that if a separator has been added or removed, the cursor position should change accordingly.
type TMdThousandEdit = class (TMdNumEdit) public procedure Change; override; end; function StringToFloatSkipping (s: string): Extended; var s1: string; I: Integer; begin // remove non-numbers s1 := ''; for i := 1 to length (s) do if s[i] in ['0'..'9'] then s1 := s1 + s[i]; Result := StrToFloat (s1); end; procedure TMdThousandEdit.Change; var CursorPos, // original position of the cursor LengthDiff: Integer; // number of new separators (+ or -) begin if Assigned (Parent) then begin CursorPos := SelStart; LengthDiff := Length (Text); Text := FormatFloat ('#,###', StringToFloatSkipping (Text)); LengthDiff := Length (Text) - LengthDiff; // move the cursor to the proper position SelStart := CursorPos + LengthDiff; end; inherited; end;
The next component, TMdSoundButton, plays one sound when you press the button and another sound when you release it. The user specifies each sound by modifying two string properties that name the appropriate WAV files for the respective sounds. Once again, you need to intercept some system messages (wm_LButtonDown and wm_LButtonUp) or override the appropriate second-level handler.
Here is the code for the TMdSoundButton class, with the two protected methods and the two string properties that identify the sound files, mapped to private fields because you don't need to do anything special when the user changes those properties:
type TMdSoundButton = class(TButton) private FSoundUp, FSoundDown: string; protected procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override; procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override; published property SoundUp: string read FSoundUp write FSoundUp; property SoundDown: string read FSoundDown write FSoundDown; end;
Here is the code for one of the two methods:
uses MMSystem; procedure TMdSoundButton.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin inherited MouseDown (Button, Shift, X, Y); PlaySound (PChar (FSoundDown), 0, snd_Async); end;
Notice that you call the inherited version of the methods before you do anything else. For most second-level handlers, this is a good practice, because it ensures that you execute the standard behavior before you execute any custom behavior. Next, notice that you call the PlaySound Win32 API function to play the sound. You can use this function (defined in the MmSystem unit) to play either WAV files or system sounds, as the SoundB example demonstrates. Here is a textual description of the form of this sample program (from the DFM file):
object MdSoundButton1: TMdSoundButton Caption = 'Press' SoundUp = 'RestoreUp' SoundDown = 'RestoreDown' end
The Windows interface is evolving toward a new standard, including components that become highlighted as the mouse cursor moves over them. Delphi provides similar support in many of its built-in components. Mimicking this behavior for a button might seem a complex task to accomplish, but it is not. The development of a component can become much simpler once you know which virtual function to override or which message to hook onto.
The next component, the TMdActiveButton class, demonstrates this technique by handling some internal Delphi messages to accomplish its task in a simple way. (For information about where these internal Delphi messages come from, see the next section, "Component Messages and Notifications.") The ActiveButton component handles the cm_MouseEnter and cm_MouseExit internal Delphi messages, which are received when the mouse cursor enters or leaves the area corresponding to the component:
The code you write for these two methods can do whatever you want. For this example, I've decided to toggle the bold style of the button's font. You can see the effect of moving the mouse over one of these components in Figure 9.9.
procedure TMdActiveButton.MouseEnter (var Msg: TMessage); begin Font.Style := Font.Style + [fsBold]; end; procedure TMdActiveButton.MouseLeave (var Msg: TMessage); begin Font.Style := Font.Style - [fsBold]; end;
You can add other effects, including enlarging the font, making the button the default, or increasing the button's size a little. The best effects usually involve colors, but you must inherit from the TBitBtn class to have this support (TButton controls have a fixed color).
To build the ActiveButton component, I used two internal Delphi component messages, as indicated by their cm prefix. These messages can be quite interesting, as the example highlights, but they are almost completely undocumented by Borland. There is also a second group of internal Delphi messages, indicated as component notifications and distinguished by their cn prefix. I don't have enough space here to discuss each of them or provide a detailed analysis; browse the VCL source code if you want to learn more.
A Delphi component passes component messages to other components to indicate any change in its state that might affect those components. Most of these messages begin as Windows messages, but some of them are more complex, higher-level translations and not simple remappings. In addition, components send their own messages as well as forwarding those received from Windows. For example, changing a property value or some other characteristic of the component may necessitate telling one or more other components about the change.
You can group these messages into categories:
Component notification messages are those sent from a parent form or component to its children. These notifications correspond to messages sent by Windows to the parent control's window, but logically intended for the control. For example, interaction with controls such as buttons, edit boxes, or list boxes causes Windows to send a wm_Command message to the parent of the control. When a Delphi program receives these messages, it forwards the message to the control itself, as a notification. The Delphi control can handle the message and eventually fire an event. Similar dispatching operations take place for many other messages.
The connection between Windows messages and component notification ones is so tight that you'll often recognize the name of the Windows message from the name of the notification message, replacing the initial cn with wm. There are several distinct groups of component notification messages:
Other control notifications are defined for common controls support (in the ComCtrls unit).
An Example of Component Messages
As an example of the use of some component messages, I've written the CMNTest program. It has a form with three edit boxes and associated labels. The first message it handles, cm_ DialogKey, allows it to treat the Enter key as if it were a Tab key. The code of this method checks for the Enter key's code and sends the same message, but passes the vk_Tab key code. To halt further processing of the Enter key, you set the result of the message to 1:
procedure TForm1.CMDialogKey(var Message: TCMDialogKey); begin if (Message.CharCode = VK_RETURN) then begin Perform (CM_DialogKey, VK_TAB, 0); Message.Result := 1; end else inherited; end;
The second message, cm_DialogChar, monitors accelerator keys. This technique can be useful to provide custom shortcuts without defining an extra menu for them. Notice that while this code is correct for a component, in a normal application this can be achieved more easily by handling the form's OnShortCut event. In this case, you log the special keys in a label:
procedure TForm1.CMDialogChar(var Msg: TCMDialogChar); begin Label1.Caption := Label1.Caption + Char (Msg.CharCode); inherited; end;
Finally, the form handles the cm_FocusChanged message, to respond to focus changes without having to handle the OnEnter event of each of its components. Again, the action displays a description of the focused component:
procedure TForm1.CmFocusChanged(var Msg: TCmFocusChanged); begin Label5.Caption := 'Focus on ' + Msg.Sender.Name; end;
The advantage of this approach is that it works independently of the type and number of components you add to the form, and it does so without any special action on your part. Again, this is a trivial example for such an advanced topic, but if you add to this the code of the ActiveButton component, you have at least a few reasons to look into these special, undocumented messages. At times, writing the same code without their support can become extremely complex.
|Copyright © 2004-2021 "Delphi Sources" by BrokenByte Software. Delphi Programming Guide||