Given a TForm with a TListBox on it, the following works:
procedure TForm1.FormCreate(Sender: TObject);
procedure _WorkOnListBox;
begin
ListBox.Items.Append('Test');
end;
begin
_WorkOnListBox;
end;
As does the following:
procedure TForm1.DoWithoutListBoxEvents(AProc: TProc);
begin
ListBox.Items.BeginUpdate;
try
AProc;
finally
ListBox.Items.EndUpdate;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
DoWithoutListBoxEvents(procedure
begin
LayersListBox.Items.Append('Test');
end);
end;
But the following does not:
procedure TForm1.FormCreate(Sender: TObject);
procedure _WorkOnListBox;
begin
ListBox.Items.Append('Test');
end;
begin
DoWithoutListBoxEvents(_WorkOnListBox);
end;
I get an E2555 Cannot capture symbol '_WorkOnListBox'. Why? Is there any way to get the DoWithoutListBoxEvents to work without using an anonymous procedure? Although I think it looks elegant with it, I'm trying to stay FPC compatible.
DoWithoutEvents() takes a TProc as input:
type
TProc = procedure;
Only a standalone non-class procedure and an anonymous procedure can be assigned to a TProc. _WorkOnForm is neither of those, it is a local procedure instead. A local procedure has special compiler handling that ties it to its parent's stack frame. Thus, _WorkOnForm is not compatible with TProc.
Related
Running into a runtime error when I open a modal form (Form2) that, on create, calls another procedure that does somehting with a VCL component. The following program demonstrates the issue.
This is the code on the modal form:
procedure DoLabel;
begin
form2.Label1.Caption := 'A';
end;
procedure TForm2.FormCreate(Sender: TObject);
begin
DoLabel;
end;
This compiles well. However, the program crashes on opening the modal form. The debugger says: access violation at address xxxx. It is probably a basic error, but what am I doing wrong? And how to solve this?
You are using the global Form2 variable, which has not been assigned yet (or at all) while the TForm2 object is still being constructed.
You need to change your code to not rely on that variable during construction (and preferably remove it entirely, unless TForm2 is auto-created at startup, which it does not sound like it is).
For instance, pass the Form's Self pointer, or even its TLabel pointer, as an input parameter to DoLabel(), eg:
procedure DoLabel(ALabel: TLabel);
begin
ALabel.Caption := 'A';
end;
procedure DoFormStuff(AForm: TForm2);
begin
// use AForm as needed...
end;
procedure TForm2.FormCreate(Sender: TObject);
begin
DoLabel(Label1);
DoFormStuff(Self);
end;
Though, in this case, it would make more sense to have DoFormStuff(), and possible DoLabel() too, be a member of the TForm2 class instead of free functions:
procedure TForm2.FormCreate(Sender: TObject);
begin
DoLabel;
DoFormStuff;
end;
procedure TForm2.DoLabel;
begin
Label1.Caption := 'A';
end;
procedure TForm2.DoFormStuff;
begin
// use Self as needed...
end;
Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 2 years ago.
Improve this question
I have a TComboBox in Delphi 10.3. I have a Combobox with over 30 items. I need to code a different action for each item of the Combobox. At the moment I'm using if-else statements. As there are 30 different items the if statements are going to be way too long. Is there a quicker way to do this?
This entirely depends on your situation. It is almost impossible to answer your Q without knowing your precise scenario.
Nevertheless, here are a few ideas. Maybe they are relevant to your situation, maybe they are not.
Trivial parameterisation by index
In the best case scenario, your 30 actions can be parameterised. For instance, suppose the items of the combo box are
Show 1
Show 10
Show 100
Show 1000
...
which will display a message box with the given number. In this scenario, you don't need 30 different procedures (here each represented by a simple call to ShowMessage):
procedure TForm1.btnNextClick(Sender: TObject);
begin
case ComboBox1.ItemIndex of
0:
ShowMessage('1');
1:
ShowMessage('10');
2:
ShowMessage('100');
3:
ShowMessage('1000');
// ...
end;
end;
Instead, you should use only one procedure, but with a parameter:
procedure TForm1.btnNextClick(Sender: TObject);
begin
if ComboBox1.ItemIndex <> -1 then
ShowMessage(IntPower(10, ComboBox1.ItemIndex).ToString)
end;
Parameterisation by the associated object
If the action cannot be described by the item's index alone, you can use the object pointer associated with each item. Maybe it is enough to use it to store an integer:
procedure TForm1.FormCreate(Sender: TObject);
begin
ComboBox1.Items.BeginUpdate;
try
ComboBox1.Items.Clear;
ComboBox1.Items.AddObject('Show 51', TObject(51));
ComboBox1.Items.AddObject('Show 111', TObject(111));
ComboBox1.Items.AddObject('Show 856', TObject(856));
ComboBox1.Items.AddObject('Show 1000', TObject(1000));
finally
ComboBox1.Items.EndUpdate;
end;
end;
procedure TForm1.btnNextClick(Sender: TObject);
begin
if ComboBox1.ItemIndex <> -1 then
ShowMessage(Integer(ComboBox1.Items.Objects[ComboBox1.ItemIndex]).ToString);
end;
Otherwise, you can let it be a true pointer to some object with any amount of data (integers, strings, ...).
Unrelated procedures
The examples above all require that the procedures can be parameterised, i.e. replaced by a single procedure with a parameter. If this is not the case, if the procedures are completely unrelated, you need a different approach. But again, which approach is most suitable depends on your precise situation.
Here are a few examples.
Simple case statement
At design time, set the items to Play sound, Run Notepad, and Show Start Menu.
procedure PlaySound;
begin
MessageBeep(MB_ICONINFORMATION);
end;
procedure RunNotepad;
begin
ShellExecute(Form1.Handle, nil, 'notepad', nil, nil, SW_SHOWNORMAL)
end;
procedure ShowStartMenu;
begin
Form1.Perform(WM_SYSCOMMAND, SC_TASKLIST, 0)
end;
procedure TForm1.btnNextClick(Sender: TObject);
begin
case ComboBox1.ItemIndex of
0:
PlaySound;
1:
RunNotepad;
2:
ShowStartMenu;
end;
end;
Storing procedural pointers with the items
procedure PlaySound;
begin
MessageBeep(MB_ICONINFORMATION);
end;
procedure RunNotepad;
begin
ShellExecute(Form1.Handle, nil, 'notepad', nil, nil, SW_SHOWNORMAL)
end;
procedure ShowStartMenu;
begin
Form1.Perform(WM_SYSCOMMAND, SC_TASKLIST, 0)
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
ComboBox1.Items.BeginUpdate;
try
ComboBox1.Items.Clear;
ComboBox1.Items.AddObject('Play sound', TObject(#PlaySound));
ComboBox1.Items.AddObject('Run notepad', TObject(#RunNotepad));
ComboBox1.Items.AddObject('ShowStartMenu', TObject(#ShowStartMenu));
finally
ComboBox1.Items.EndUpdate;
end;
end;
procedure TForm1.btnNextClick(Sender: TObject);
begin
if ComboBox1.ItemIndex <> -1 then
TProcedure(ComboBox1.Items.Objects[ComboBox1.ItemIndex])();
end;
Benefit: no risk of confusing the indices; the actions are "attached" to the items.
Using a dictionary of commands
Maybe your application has a global set of commands, denoted by English words. Then you might want to use a dictionary to get the procedure associated with a word. This can be used for the combo box as well. At design time, let there be three items: beep, write, and start:
type
TForm1 = class(TForm)
ComboBox1: TComboBox;
btnNext: TButton;
procedure btnNextClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FCommands: TDictionary<string, TProcedure>;
public
end;
procedure PlaySound;
begin
MessageBeep(MB_ICONINFORMATION);
end;
procedure RunNotepad;
begin
ShellExecute(Form1.Handle, nil, 'notepad', nil, nil, SW_SHOWNORMAL)
end;
procedure ShowStartMenu;
begin
Form1.Perform(WM_SYSCOMMAND, SC_TASKLIST, 0)
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
FCommands := TDictionary<string, TProcedure>.Create;
FCommands.Add('beep', PlaySound);
FCommands.Add('write', RunNotepad);
FCommands.Add('start', ShowStartMenu);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FCommands.Free;
end;
procedure TForm1.btnNextClick(Sender: TObject);
var
Cmd: TProcedure;
begin
if
(ComboBox1.ItemIndex <> -1)
and
FCommands.TryGetValue(ComboBox1.Items[ComboBox1.ItemIndex], Cmd)
then
Cmd();
end;
I'm trying to free a component when i click it. So, i've written the simplest code i could imagine to achieve this: a procedure that frees it's sender. But on Delphi 7 (Tried on Delphi XE 10 and it worked with no errors) it sometimes throws an Access Violation or Abstract Error randomly. The easiest way to replicate this is to insert like 30 Buttons and assign an onclick procedure with the code below, then click them.
I've tried the two codes below, both on onclick:
procedure FreeMe(Sender: TObject);
begin
TButton(Sender).Free;
end;
or
procedure FreeMe(Sender: TObject);
begin
(Sender as TButton).Free;
end;
You need to delay the freeing until after the button's OnClick event handier has fully exited. It is important that the freeing happens when the object being freed is idle and not in the middle of processing anything.
One way to do that is to use PostMessage(), eg:
var
MyReleaseWnd: HWND;
procedure TMyMainForm.FormCreate(Sender: TObject);
begin
MyReleaseWnd := AllocateHWnd(MyReleaseWndProc);
end;
procedure TMyMainForm.FormDestroy(Sender: TObject);
begin
DeallocateHWnd(MyReleaseWnd);
end;
procedure TMyMainForm.MyReleaseWndProc(var Message: TMessage);
begin
if Message.Msg = CM_RELEASE then
TObject(Msg.LParam).Free
else
Message.Result := DefWindowProc(MyReleaseWnd, Message.Msg, Message.WParam, Message.LParam);
end;
procedure DelayFreeMe(Sender: TObject);
begin
PostMessage(MyReleaseWnd, CM_RELEASE, 0, LPARAM(Sender));
end;
Alternatively, in 10.2 Tokyo and later, you can use TThread.ForceQueue() instead:
procedure DelayFreeMe(Sender: TObject);
begin
TThread.ForceQueue(nil, Sender.Free);
end;
Either way, you can then do this:
procedure TSomeForm.ButtonClick(Sender: TObject);
begin
DelayFreeMe(Sender);
end;
I would like to know a way to verify if the focus of a component was activated by the mouse, if yes to execute a certain procedure in Delphi.
you can use enter and exit event :
procedure TForm1.Edit1Exit(Sender: TObject);
begin
myProcedureRun1;
end;
procedure TForm1.Edit1Enter(Sender: TObject);
begin
myProcedureRun2;
end;
I'm having EAccessViolation when I close the form of my application and I don't know how to deal with this, I have two units, here is the main unit relevant code:
unit MainUnit;
uses
.., myComponent1;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
begin
MyComponent1.doSomeWork(p1, p2, ..., pn);
end;
procedure TForm1.OnMyComponen1tEvent(sender: TObject; p: Integer);
begin
memo1.Lines.Add(message);
end;
end.
This unit uses another unit is a component class, in which i send a signal to memo1 to show the message, of course using the component event, it's something like:
unit myComponent;
type
TMyComponentEvent = procedure(sender: TObject; p: integer) of object;
type
TMyComponent = class(TComponent)
// Properties and events declaration
procedure TPThread.Execute;
begin
try
// Create and run some worker threads
// Wait for them to finish the job
// This is the last thing to do:
if Assigned(FOnMyComponentEvent) then
begin
FOnMyComponentEvent(Self, p);
end;
finally
//free ressources
end;
end;
procedure TMyComponent.DoSomeWork;
begin
TPThread.Create(p1, p2 ...);
end;
end.
When I close the form before the program finishes its job ( The threads are still working), i get that exception but sometimes, there is no exception raised. Well, when the exception is raised it indicates the line: memo1.Lines.Add(message);.
I don't know how to solve it, so how can I prevent the exception from happening?
Sounds like you are not setting the MyEvent event to nil when destroying the Form, eg
procedure TForm1.FormCreate(sender: TObject);
begin
OtherUnit.MyEvent := MyEvent;
end;
procedure TForm1.FormDestroy(sender: TObject);
begin
OtherUnit.MyEvent := nil;
end;