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.
Note |
When most VCL components handle a Windows message, they call a second-level message handler (usually a dynamic method), instead of executing code directly in the message-response method. This approach makes it easier for you to customize the component in a derived class. Typically, a second-level handler will do its own work and then call any event handler the component user has assigned. So, you should always call inherited to let the component fire the event as expected. |
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.
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.
All these considerations are summarized by the following complete code for the TMdThousandEdit class:
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
Note |
Selecting a proper value for these sound properties is far from simple. Later in this chapter, I'll show you how to add a property editor to the component to simplify the operation. |
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:
type TMdActiveButton = class (TButton) protected procedure MouseEnter (var Msg: TMessage); message cm_mouseEnter; procedure MouseLeave (var Msg: TMessage); message cm_mouseLeave; end;
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.
Warning |
This is a rather advanced topic, so feel free to skip this section if you are new to writing Delphi components. Component messages are not documented in the Delphi help file, so I felt it was important to at least list them here. |
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:
Activation and input focus messages are sent to the component being activated or deactivated, receiving or losing the input focus:
cm_Activate |
Corresponds to the OnActivate event of forms and of the application |
cm_Deactivate |
Corresponds to OnDeactivate |
cm_Enter |
Corresponds to OnEnter |
cm_Exit |
Corresponds to OnExit |
cm_FocusChanged |
Sent whenever the focus changes between components of the same form (later, you'll see an example using this message) |
cm_GotFocus |
Declared but not used |
cm_LostFocus |
Declared but not used |
Messages sent to child components when a property changes:
cm_BiDiModeChanged |
cm_IconChanged |
cm_BorderChanged |
cm_ShowHintChanged |
cm_ColorChanged |
cm_ShowingChanged |
cm_Ctl3DChanged |
cm_SysFontChanged |
cm_CursorChanged |
cm_TabStopChanged |
cm_EnabledChanged |
cm_TextChanged |
cm_FontChanged |
cm_VisibleChanged |
Monitoring these messages can help track changes in a property. You might need to respond to these messages in a new component, but it's not likely.
Messages related to ParentXxx properties: cm_ParentFontChanged, cm_ParentColorChanged, cm_ParentCtl3DChanged, cm_ParentBiDiModeChanged, and cm_ParentShowHintChanged. These are similar to the messages in the previous group.
Notifications of changes in the Windows system: cm_SysColorChange, cm_WinIniChange,
cm_TimeChange, and cm_FontChange. Handling these messages is useful only in special components that need to keep track of system colors or fonts.
Mouse messages: cm_Drag is sent many times during dragging operations. cm_ MouseEnter and cm_MouseLeave are sent to the control when the cursor enters or leaves its surface, but they are sent by the Application object as low-priority messages. cm_ MouseWheel corresponds to wheel-based operations.
Application messages:
cm_AppKeyDown |
Sent to the Application object to let it determine whether a key corresponds to a menu shortcut |
cm_AppSysCommand |
Corresponds to the wm_SysCommand message |
cm_DialogHandle |
Sent in a DLL to retrieve the value of the DialogHandle property (used by some dialog boxes not built with Delphi) |
cm_InvokeHelp |
Sent by code in a DLL to call the InvokeHelp method |
cm_WindowHook |
Sent in a DLL to call the HookMainWindow and UnhookMainWindow methods |
You'll rarely need to use these messages. There is also a cm_HintShowPause message, which is never handled in VCL.
Delphi internal messages:
cm_CancelMode |
Terminates special operations, such as showing the pull-down list of a combo box |
cm_ControlChange |
Sent to each control before adding or removing a child control (handled by some common controls) |
cm_ControlListChange |
Sent to each control before adding or removing a child control (handled by the DBCtrlGrid component) |
cm_DesignHitTest |
Determines whether a mouse operation should go to the component or to the form designer |
cm_HintShow |
Sent to a control just before displaying its hint (only if the ShowHint property is True) |
cm_HitTest |
Sent to a control when a parent control is trying to locate a child control at a given mouse position (if any) |
cm_MenuChanged |
Sent after MDI or OLE menu-merging operations |
Messages related to special keys:
cm_ChildKey |
Sent to the parent control to handle some special keys (in Delphi, this message is handled only by DBCtrlGrid components) |
cm_DialogChar |
Sent to a control to determine whether a given input key is its accelerator character |
cm_DialogKey |
Handled by modal forms and controls that need to perform special actions |
Cm_IsShortCut |
Is currently not used (as most code simply calls IsShortCut), but it is intended to be used to identify if a shortcut is known to be supported by a form, through either the OnShortCut event, a menu item, or an action. |
cm_WantSpecialKey |
Handled by controls that interpret special keys in an unusual way (for example, using the Tab key for navigation, as some Grid components do) |
Messages for specific components:
cm_GetDataLink |
Used by DBCtrlGrid controls (and discussed in Chapter 17, "Writing Database Components") |
cm_TabFontChanged |
Used by TabbedNotebook components |
cm_ButtonPressed |
Used by SpeedButtons to notify other sibling SpeedButton components (to enforce radio-button behavior) |
cm_DeferLayout |
Used by DBGrid components |
OLE container messages: cm_DocWindowActivate, cm_IsToolControl, cm_Release,
cm_UIActivate, and cm_UIDeactivate.
Dock-related messages, including cm_DockClient, cm_DockNotification, cmFloat, and
cm_UndockClient.
Method-implementation messages, such as cm_RecreateWnd, called inside the RecreateWnd method of TControl; cm_Invalidate, called inside TControl.Invalidate; cm_Changed, called inside TControl.Changed; and cm_AllChildrenFlipped, called in the DoFlipChildren methods of TWinControl and TScrollingWinControl. In the similar group fall two action list–related messages, cm_ActionUpdate and cm_ActionExecute.
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:
General keyboard messages: cn_Char, cn_KeyUp, cn_KeyDown, cn_SysChar, and cn_ SysKeyDown
Special keyboard messages used only by list boxes with the lbs_WantKeyboardInput style:
cn_CharToItem and cn_VKeyToItem
Messages related to the owner-draw technique: cn_CompareItem, cn_DeleteItem, cn_ DrawItem, and cn_MeasureItem
Messages for scrolling, used only by scroll bar and track bar controls: cn_HScroll and cn_ VScroll
General notification messages, used by most controls: cn_Command, cn_Notify, and
cn_ParentNotify
Control color messages: cn_CtlColorBtn, cn_CtlColorDlg, cn_CtlColorEdit,
cn_CtlColorListbox, cn_CtlColorMsgbox, cn_CtlColorScrollbar, and cn_CtlColorStatic
Other control notifications are defined for common controls support (in the ComCtrls unit).
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.