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