Search code examples
delphieventsparametersparameter-passingdelphi-10-seattle

Pass InputComboBox's KeyPress event as function parameter


My OS is Windows 10 64bit, and I'm using Delphi 10.0 Seattle Update 1.

I have a function that calls an InputBox that, instead of an Edit, it contains a ComboBox. See below:

function funInputComboBox(const STRC_Label: String; out STRV_Result: String; const STRC_Items: String = ''; const STRC_LocateItem: String = ''): Boolean;

  function GetCharSize(Canvas: TCanvas): TPoint;
  var
    I: Integer;
    Buffer: array[0..51] of Char;
  begin
    for I := 0 to 25 do Buffer[I] := Chr(I + Ord('A'));
    for I := 0 to 25 do Buffer[I + 26] := Chr(I + Ord('a'));
    GetTextExtentPoint(Canvas.Handle, Buffer, 52, TSize(Result));
    Result.X := Result.X div 52;
  end;

var
  Form: TForm;
  Prompt: TLabel;
  Combo: TComboBox;
  DialogUnits: TPoint;
  ButtonTop, ButtonWidth, ButtonHeight: Integer;
begin
  Result := False;
  STRV_Result := '';

  Form := TForm.Create(Application);

  with Form do
  try
    Canvas.Font := Font;
    DialogUnits := GetCharSize(Canvas);
    BorderStyle := bsDialog;
    Caption := Application.Title;
    ClientWidth := MulDiv(180, DialogUnits.X, 4);
    Position := poScreenCenter;
    Prompt := TLabel.Create(Form);

    with Prompt do
    begin
      Parent := Form;
      Caption := STRC_Label;
      Left := MulDiv(8, DialogUnits.X, 4);
      Top := MulDiv(8, DialogUnits.Y, 8);
      Constraints.MaxWidth := MulDiv(164, DialogUnits.X, 4);
      WordWrap := True;
    end;

    Combo := TComboBox.Create(Form);

    with Combo do
    begin
      Parent := Form;
      Style := csDropDown;
      Items.Text := STRC_Items;
      ItemIndex := Items.IndexOf(STRC_LocateItem);
      Left := Prompt.Left;
      Top := Prompt.Top + Prompt.Height + 5;
      Width := MulDiv(164, DialogUnits.X, 4);
      CharCase := ecUpperCase;
      -- OnKeyPress := ?????
    end;

    ButtonTop := Combo.Top + Combo.Height + 15;
    ButtonWidth := MulDiv(50, DialogUnits.X, 4);
    ButtonHeight := MulDiv(14, DialogUnits.Y, 8);

    with TButton.Create(Form) do
    begin
      Parent := Form;
      Caption := 'OK';
      ModalResult := mrOk;
      Default := True;
      SetBounds(MulDiv(38, DialogUnits.X, 4), ButtonTop, ButtonWidth, ButtonHeight);
    end;

    with TButton.Create(Form) do
    begin
      Parent := Form;
      Caption := 'Cancelar';
      ModalResult := mrCancel;
      Cancel := True;
      SetBounds(MulDiv(92, DialogUnits.X, 4), Combo.Top + Combo.Height + 15, ButtonWidth, ButtonHeight);
      Form.ClientHeight := Top + Height + 13;
    end;

    Result := (ShowModal = mrOk);

    if Result then
      STRV_Result := Combo.Text;
  finally
    Form.Free;
  end;
end;

It works fine and does the job it needs to, but I want to add something else in there. Sometimes, this function will be used in places that need the text to be masked, for example, the car plates here looks like this: AAA-0000 (3 numbers and 4 letters), so, when I call this, I want to pass the procedure/function that will be called when OnKeyPress is triggered.

My mask validation procedure has this header:

procedure proValidaMascaraPlaca(Sender: TObject; var Key: Char);

It doesn't belong to any class, it's placed in your everyday utils unit.

When we need to use it, we do it likes this:

procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
begin
  proValidaMascaraPlaca(Sender, Key);
end;

So, I want my InputComboBox to have this feature, where it could have any validation procedure we pass as a parameter:

with Combo do
begin
  OnKeyPress := proValidaMascaraPlaca
end;

Obviously, this doesn't worked, so I tried another method I saw here. It consisted of having a function that would simulate it as if it was a procedure(Sender: TObject; var Key: Char) of object:

function MakeMethod(Data, Code: Pointer): TMethod;
begin
  Result.Data := Data;
  Result.Code := Code;
end;

My function still doesn't have the procedure parameter, so I'm fixing the one from this example:

TKeyPressEvent = procedure(Sender: TObject; var Key: Char) of object;

with Combo do
begin
  OnKeyPress := TKeyPressEvent(MakeMethod(@Combo, @proValidaMascaraPlaca));
end;

I tried nil instead of @Combo, but both failed. They did compile, but when proValidaMascaraPlaca() was called, it received some weird value on Char and Sender, which resulted in an Access Violation.

I hope that someone understands what I'm trying to do, or has already done/seen this, and knows what I should do to make it work. It's something I've been trying to do for some time now, that I fell like it will make me view parameters in a whole new way.


Solution

  • The problem is that the type used for the OnKeyPress event (as with all other VCL/FMX events) is declared as of object, so it requires the assigned handler to have a Self parameter. When using a class method, that parameter is implied, managed by the compiler for you. But, when using your standalone proValidaMascaraPlaca() procedure with MakeMethod(), whatever value you assign to the TMethod.Data field gets passed in the Self parameter, but proValidaMascaraPlaca() has no Self parameter! That is why you are getting garbage in your Sender and Key parameters, because the call stack is getting corrupted.

    To make this work, you need to add an explicit Self parameter as the 1st parameter to proValidaMascaraPlaca(), eg:

    procedure proValidaMascaraPlaca(Self: Pointer; Sender: TObject; var Key: Char);
    

    Self will then receive whatever value you specify in the Data parameter of MakeMethod() (@Combo in your example), and Sender and Key will receive whatever values the Combo sends to them, as expected.

    If you don't want to edit proValidaMascaraPlaca() itself (because it is in a utility library), you will have to create a separate wrapper function to pass to MakeMethod(), and then that wrapper can call proValidaMascaraPlaca() ignoring Self, eg:

    procedure MyComboKeyPress(Self: Pointer; Sender: TObject; var Key: Char);
    begin
      proValidaMascaraPlaca(Sender, Key);
    end;
    
    ...
    
    with Combo do
    begin
      ...
      Combo.OnKeyPress := TKeyPressEvent(MakeMethod(nil, @MyComboKeyPress));
    end;
    

    A much simpler (and ultimately safer) solution would be to derive a new class from TComboBox instead, and override the virtual KeyPress() method, eg:

    type
      TMyValidatingComboBox = class(TComboBox)
      protected
        procedure KeyPress(var Key: Char); override;
      end;
    
    procedure TMyValidatingComboBox.KeyPress(var Key: Char);
    begin
      inherited;
      proValidaMascaraPlaca(Self, Key);
    end;
    
    ...
    
    Combo := TMyValidatingComboBox.Create(Form);