2004. szeptember 26., vasárnap

Creating a descendant of a component to enhance functionality


Problem/Question/Abstract:

Adding an accelerator key to a TPageControl

Answer:

This tip is an example of extending the functionality of a component by creating a descendant. While implicit to the discussion at hand, here's where the power of an object-oriented language such as Delphi lays. As you'll see in the code below, it doesn't take much to create new functionality of an object by creating a descendant. The point of this is that had I not been using an object-oriented language, I would have had to re-write the original code of the TPageControl, then add the extended functionality. Fortunately, the VCL, which is really an object hierarchy, allows me to transparently inherit and retain the ancestral functionality and concentrate on the new functionality. You gotta love it!

For those of you new to Delphi, an accelerator key is a key that is pressed in combination with the Alt key to execute a command. They're sometimes called keyboard shortcuts or hotkeys, and you'll typically see them in menus as the underlined letter of a menu item. For instance, the "F" in the File menu selection is an accelerator key for that item. So to open up the File menu, you'd press Alt-F.

Accelerator keys aren't limited to just menu items. In fact, for almost any Caption property or a Caption-like property (e.g. Radio Group items) of a component, you can define an accelerator key. All you need to do is place an "&AMP" before a letter to designate it as an accelerator key. This is useful with VCL components like a TRadioGroup's Items, which allow the user to quickly select the radio button choice with the touch of a key. However, not all VCL components will respond to accelerator keystrokes if you define them. TPageControl in Delphi 2.0, which replaces TTabbedNotebook, is one of those components. And with it, accelerator key functionality would be particularly useful.

The only method I know for implementing accelerator key functionality in a TPageControl is to create a new component. There's another way, but you have to create menu and define hotkeys for menu items with equivalent functionality (they'll turn your pages for you), and that's a pretty kludgy way of doing things. Besides, the code to accomplish what we want is actually very simple.

Below is the unit code for a descendant of TPageControl that adds accelerator key functionality. We'll discuss the particulars after the listing:

unit accel;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  ComCtrls;

type
  TAccelPageCtrl = class(TPageControl)
  private
    { Private declarations }
    procedure CMDialogChar(var Msg: TCMDialogChar); message CM_DIALOGCHAR;
  protected
    { Protected declarations }
  public
    { Public declarations }
  published
    { Published declarations }
  end;

procedure Register;

implementation

procedure TAccelPageCtrl.CMDialogChar(var Msg: TCMDialogChar);
var
  I: Integer;
  Okay: Boolean;
begin

  Okay := False;

  inherited; //call the inherited message handler.

  //Now with our own component, start at Page 1 (Item 0) and work to the end.
  for I := 0 to PageCount - 1 do
  begin
    //Is key pressed accelerator key in Caption?
    Okay := IsAccel(Msg.CharCode, Pages[I].Caption) and CanChange(I);
                //this is the fix
    //It is, so change the page and break out of the loop.
    if Okay then
    begin
      Msg.Result := 1; //you can set this to anything, but by convention it's 1
      ActivePage := Pages[I];
      Change;
      Break;
    end;
  end;

end;

procedure Register;
begin
  RegisterComponents('BD', [TAccelPageCtrl]);
end;

end.

As you can see from the above. all that's required to add accelerator key response is a simple message handler procedure. The message we're interested in is CM_DialogChar, a Delphi custom message type encapsulated by TCMDialogChar, which is a wrapper type for the Windows WM_SYSCHAR message. WM_SYSCHAR is the Windows message that is used to trap accelerator keys; you can find a good discussion of it in the online help. The most important thing to note is what happens when the TAccelPageCtrl component detects that a CM_DialogChar message has fired.

Take a look at the CMDialogChar procedure, and note that all that's going in the code is a simple for loop that starts at the first page of the descendant object and goes to the last page, unless the key that was pressed happened to be an accelerator key. We can easily determine if a key is an accelerator key with the IsAccel function, which takes the key code pressed and a string (we passed the Caption property of the current TabSheet). IsAccel searches through the string and looks for a matching accelerator key. If it finds one, it returns True. If so, we set the message result value and change the page of TAccelPageCtrl to the page where the accelerator was found by setting the ActivePage property and calling the inherited Change procedure from TPageControl.

I haven't used TPageControl since I created this component because of how easy TAccelPageCtrl makes switching from TabSheet to TabSheet. It's far easier to do a Alt-<key> combination than use the mouse when you're at the keyboard. Play around with this and you'll be convinced not to use the standard VCL TPageControl.

Nincsenek megjegyzések:

Megjegyzés küldése