将 InputComboBox 的 KeyPress 事件作为函数参数传递

Pass InputComboBox's KeyPress event as function parameter

我的 OS 是 Windows 10 64 位,我使用的是 Delphi 10.0 Seattle Update 1.

我有一个调用 InputBox 的函数,它包含一个 ComboBox,而不是一个 Edit。见下文:

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;

它工作正常并且完成了它需要的工作,但我想在其中添加其他内容。有时候,这个函数会用在需要屏蔽文字的地方,比如这里的车牌是这样的:AAA-0000(3个数字4个字母),所以,调用这个的时候,我想通过触发 OnKeyPress 时将调用的 procedure/function。

我的掩码验证程序有这个 header:

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

它不属于任何class,它被放置在你的日常utils单元中。

当我们需要使用它的时候,我们是这样使用的:

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

所以,我希望我的 InputComboBox 具有此功能,它可以包含我们作为参数传递的任何验证程序:

with Combo do
begin
  OnKeyPress := proValidaMascaraPlaca
end;

显然,这行不通,所以我尝试了我在这里看到的另一种方法。它包括一个模拟它的函数,就好像它是一个 procedure(Sender: TObject; var Key: Char) of object:

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

我的函数仍然没有过程参数,所以我正在修复这个例子中的那个:

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

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

我尝试了 nil 而不是 @Combo,但都失败了。他们确实编译了,但是当调用 proValidaMascaraPlaca() 时,它在 CharSender 上收到了一些奇怪的值,这导致了访问冲突。

我希望有人了解我正在尝试做的事情,或者已经 done/seen 这件事,并且知道我应该怎么做才能让它发挥作用。这是我一段时间以来一直在尝试做的事情,我觉得它会让我以一种全新的方式查看参数。

问题是用于 OnKeyPress 事件(与所有其他 VCL/FMX 事件一样)的类型被声明为 of object,因此它 需要 分配的处理程序有一个 Self 参数。当使用 class 方法时,该参数是隐含的,由编译器为您管理。但是,当使用带有 MakeMethod() 的独立 proValidaMascaraPlaca() 过程时,您分配给 TMethod.Data 字段的任何值都会在 Self 参数中传递,但 proValidaMascaraPlaca() 没有Self参数!这就是为什么您的 SenderKey 参数中出现垃圾的原因,因为调用堆栈已损坏。

要使这项工作正常,您需要添加一个 explicit Self 参数作为 proValidaMascaraPlaca() 的第一个参数,例如:

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

Self 然后将接收您在 MakeMethod()Data 参数中指定的任何值(在您的示例中为 @Combo),以及 SenderKey 将收到 Combo 发送给他们的任何值,正如预期的那样。

如果您不想编辑 proValidaMascaraPlaca() 本身(因为它在实用程序库中),您将必须创建一个单独的包装函数以传递给 MakeMethod(),然后包装器可以调用 proValidaMascaraPlaca() 忽略 Self,例如:

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;

一个更简单(最终更安全)的解决方案是从 TComboBox 派生一个新的 class,并覆盖虚拟 KeyPress() 方法,例如:

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