Delphi Programming Guide
Delphi Programmer 

Menu  Table of contents

Part I - Foundations
  Chapter 1 – Delphi 7 and Its IDE
  Chapter 2 – The Delphi Programming Language
  Chapter 3 – The Run-Time Library
  Chapter 4 – Core Library classes
  Chapter 5 – Visual Controls
  Chapter 6 – Building the User Interface
  Chapter 7 – Working with Forms
Part II - Delphi Object-Oriented Architectures
  Chapter 8 – The Architecture of Delphi Applications
  Chapter 9 – Writing Delphi Components
  Chapter 10 – Libraries and Packages
  Chapter 11 – Modeling and OOP Programming (with ModelMaker)
  Chapter 12 – From COM to COM+
Part III - Delphi Database-Oriented Architectures
  Chapter 13 – Delphi's Database Architecture
  Chapter 14 – Client/Server with dbExpress
  Chapter 15 – Working with ADO
  Chapter 16 – Multitier DataSnap Applications
  Chapter 17 – Writing Database Components
  Chapter 18 – Reporting with Rave
Part IV - Delphi, the Internet, and a .NET Preview
  Chapter 19 – Internet Programming: Sockets and Indy
  Chapter 20 – Web Programming with WebBroker and WebSnap
  Chapter 21 – Web Programming with IntraWeb
  Chapter 22 – Using XML Technologies
  Chapter 23 – Web Services and SOAP
  Chapter 24 – The Microsoft .NET Architecture from the Delphi Perspective
  Chapter 25 – Delphi for .NET Preview: The Language and the RTL
       
  Appendix A – Extra Delphi Tools by the Author
  Appendix B – Extra Delphi Tools from Other Sources
  Appendix C – Free Companion Books on Delphi
       
  Index    
  List of Figures    
  List of tables    
  List of Listings    
  List of Sidebars  

 
Previous Section Next Section

Customizing Windows Controls

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.)

The Numeric Edit Box

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.

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 Sound Button

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.

Handling Internal Messages: The Active Button

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.


Figure 9.9:  An example of the use of the ActiveButton component
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).

Component Messages and Notifications

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.

Component Messages

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 Notifications

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).

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.


 
Previous Section Next Section


 


 

Delphi Sources


Copyright © 2004-2024 "Delphi Sources" by BrokenByte Software. Delphi Programming Guide
ร๐๓๏๏เ ยส๎ํ๒เ๊๒ๅ   Facebook   ั๑๛๋๊เ ํเ Twitter