Delphi (Seattle) - Closing a dynamically created modal form causes access violation - forms

Summary:
one form (Loan Form) dynamically creates a modal form called DatePickerForm (when user clicks a specific button).
After selecting a date in the DatePickerForm, the user clicks on that form's 'Close' button: (a BitBtn) - this is what causes an access violation error.
Details:
The purpose of the reusable modal DatePickerForm is to provide users with a consistent way of entering dates in special circumstances. It will be used in multiple other situations - that is, if I get it to work as planned.
Exact error text is: "Project ABCD.exe raised exception class $C0000005 with message 'access violation at 0x0060d0b1: read of address 0x00000000'."
The code compiles and the program works fine until step 4 below:
Run-time Process:
The user clicks on a button on the Loan form (works)
The modal form DatePickerForm is created (owner: Application), then shown. (works)
The user selects a date from the DatePicker control. (works)
The User clicks on the OK button (fails)
The DatePickerForm should close and we should return to the Loan form - but the error occurs instead.
The next step would be reading the date still on the DatePicker's form DatePicker control (the form still exists, it is just invisible at this point)
My questions:
A) Should this work or am I using dynamic form creation incorrectly?
B) Is there a better way to achieve this?
Any help will be appreciated.
John
DatePickerForm code (complete):
unit DatePicker_PopupForm;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.Buttons, Vcl.ComCtrls;
type
TfmDatePicker_Popup = class(TForm)
DTDatePicker: TDateTimePicker;
lblDatePrompt: TLabel;
btnOK: TBitBtn;
procedure btnOKClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
fmDatePicker_Popup: TfmDatePicker_Popup;
implementation
{$R *.dfm}
procedure TfmDatePicker_Popup.btnOKClick(Sender: TObject);
begin
fmDatePicker_Popup.CloseModal;
end;
end.
Loan form - partial code (complete code is roughly 9700 lines long)
unit LoanForm;
interface
uses
Winapi.Windows, ......, DatePicker_PopupForm;
...
implementation
...
procedure TfmLoan.btnSetDefaultClick(Sender: TObject);
begin
DatePickerForm := TfmDatePicker_Popup.Create(Application);
DatePickerForm.DTDatePicker.Date := GD_ProcessDate;
DatePickerForm.ShowModal;
dDefaultDate := DatePickerForm.DTDatePicker.Date;
end;
...
end.

The documentation says:
Do not call CloseModal in your application. CloseModal is used by the VCL when a modal form needs to be closed. CloseModal does not close the form by itself; it simply calls the registered close events and updates the ModalResult property.
So, do as it says. Close a modal form by setting the form's ModalResult property.
The easiest way to do that is to remove the button OnClick event handler. Instead set the button's ModalResult property in the designer.

It is clear from the error message that you are accessing a nil pointer. And the reason for that is because you are calling CloseModal() (which you should not be calling directly in the first place) on a global fmDatePicker_Popup object pointer that is not actually pointing at a valid Form object to begin with:
procedure TfmDatePicker_Popup.btnOKClick(Sender: TObject);
begin
fmDatePicker_Popup.CloseModal; // <-- fmDatePicker_Popup is not assigned!
end;
The reason fmDatePicker_Popup is nil is because in btnSetDefaultClick(), when you create your TfmDatePicker_Popup object, you are assigning it to a different DatePickerForm variable instead of the fmDatePicker_Popup variable:
procedure TfmLoan.btnSetDefaultClick(Sender: TObject);
begin
DatePickerForm := TfmDatePicker_Popup.Create(Application); // <--
...
end;
TfmDatePicker_Popup shouldn't be relying on any external pointers to itself at all. Since btnOKClick() is a member of the TfmDatePicker_Popup class, it should be using the implicit Self pointer instead:
procedure TfmDatePicker_Popup.btnOKClick(Sender: TObject);
begin
Self.CloseModal;
end;
Or simply:
procedure TfmDatePicker_Popup.btnOKClick(Sender: TObject);
begin
CloseModal;
end;
That being said, CloseModal() is the wrong thing to call anyway. It doesn't actually close the Form, it just triggers the Form's OnClose event. Per the ShowModal() documentation:
To close a modal form, set its ModalResult property to a nonzero value.
ShowModal() internally calls CloseModal() when it detects the ModalResult has become non-zero. If the OnClose event handler sets its Action parameter to caNone, the ModalResult is reset to 0 and the Form is not closed.
So use the Form's ModalResult property instead, like the documentation says to:
procedure TfmDatePicker_Popup.btnOKClick(Sender: TObject);
begin
Self.ModalResult := mrOk;
end;
Which can then be automated by removing the OnClick handler altogether and instead setting the button's ModalResult property to a non-zero value (or, in the case of TBitBtn, set its Kind property, which also sets its ModalResult). When a button on a modal Form is clicked, it assigns its own ModalResult to its parent Form's ModalResult before triggering its OnClick event.
And then, you should also change btnSetDefaultClick() to look more like this instead:
procedure TfmLoan.btnSetDefaultClick(Sender: TObject);
var
DatePickerForm: TfmDatePicker_Popup;
begin
DatePickerForm := TfmDatePicker_Popup.Create(nil);
try
DatePickerForm.DTDatePicker.Date := GD_ProcessDate;
if DatePickerForm.ShowModal = mrOk then
dDefaultDate := DatePickerForm.DTDatePicker.Date;
finally
DatePickerForm.Free;
end;
end;

Related

Delphi get (and destroy) an instance of a form

In an application, that uses a TPanel on a "main form", to display other forms, I need to be able not only to display those forms on the TPanel, but also to close and destroy them using controls (buttons) on the main form.
The goal is following - many buttons, each one displaying a specific form on one panel, on the main form. Then, one button, that kills any possible form, that is currently being displayed (= embedded on the panel). Plus, the same closing/killing action should be called every time, when some of the "opening" buttons are triggered, so if some form is being displayed on the panel, it should be replaced by the new form.
To display a form inside the TPanel, I use something like this:
procedure TMainForm.Button1Click(Sender: TObject);
begin
if not assigned(form4) then
form4:= TForm4.Create(Panel2);
form4.Parent:= Panel2;
form4.Show;
end;
Now, to close the window, using another button on the main form, I tried various methods, icluding a CloseWindow method, using handles or pointers etc. The most promissing way, was this:
procedure TMainForm.Button2Click(Sender: TObject);
begin
Panel2.controls[0].Free;
end;
It actually closes the form, but since FreeAndNil is not used, repeated click on the Button1, leads to a nasty series of exceptions, because the form (form4 in this case) has been freed, but the reference to it has not, so the assigned() method returns true and then I try to assign a value to something, that no longer exists with form4.Parent:= Panel2;. An onClose Action:= caFree on the embedded form, does not help either, because the onClose action is not being triggered at all... Placing FreeAndNil(Form4) in onDestroy event of the Form4, also leads to a series of exceptions starting with "Invalid pointer operation".
Using
procedure TMainForm.Button2Click(Sender: TObject);
begin
FreeAndNil(Panel2.controls[0]);
end;
leads to a following error: [dcc32 Error] Unit3.pas(47): E2197 Constant object cannot be passed as var parameter
So what is the correct way to get an instance of the embedded form, to be able to use FreeAndNil(...the form...)? I cannot address it by a name, mainly because I don't know what form is currently being displayed. I need to be able to find an instance of the form based on fact, that it belongs to the TPanel on the main form, and then completely destroy it, so that a second click on the Button1, will display it again, and another click on the Button2, will close and destroy it again.
The key to the solution is Vcl.Forms.TFormClass. From the documentation:
TFormClass is the metaclass for TForm. Its value is the class
reference for TForm or for one of its descendants.
And use it as follows:
First, ditch the FormN global variables from all units that define forms that you will be showing in the panel of the main form. You will not need them and deleting them will prevent you from doing mistakes.
Secondly, in the main form, add a private declaration, CurrentForm: TForm and a private procedure ShowForm
...
private
CurrentForm: TForm;
procedure ShowForm(aFormClass: TFormClass);
...
Because of the aFormClass: TFormClass argument, you can pass in any form type.
Write event handlers for the buttons that should create and show the forms in the panel, similar to this:
procedure TForm25.ShowFormAClick(Sender: TObject);
begin
ShowForm(TForm26);
end;
And write the ShowForm() method:
procedure TForm25.ShowForm(aFormClass: TFormClass);
begin
CurrentForm.Free;
CurrentForm := aFormClass.Create(self);
CurrentForm.Parent := Panel1;
CurrentForm.Show;
end;
Finally also write the event handler for the button that should hide whatever form is currently displayed:
procedure TForm25.ShowNothingClick(Sender: TObject);
begin
FreeAndNil(CurrentForm);
end;
What you're doing wrong
You're completely misunderstanding Delphi's default global form variables. E.g. in this case Form4: TForm4;. (And yet again, I lament the fact that Delphi clings to this horrendous design shortcut.)
Just because Delphi happens to generate this for you doesn't give it any special meaning. You cannot assume it in any way binds to a specific instance of TForm4. And you most certainly shouldn't assume its Assigned status gives any indication how many instances of the form exist. Indeed, as you've noticed, even when Assigned(Form4) = True it's possible the instance that Form4 was referring to has already been destroyed.
To gain a better understanding, experiment with the following:
{Add these to your main form}
FForm4a: TForm4;
FForm4b: TForm4;
FHelloWorld: TForm4;
{Try the following in a button click event
You should see 3 instances of TForm4.}
FForm4a := TForm4(Self);
FForm4b := TForm4(Self);
FHelloWorld := TForm4(Self); {You might not have realised, but the
identifier can be completely different
to the class name.}
FForm4a.Show();
FForm4b.Show();
FHelloWorld.Show();
{In another try}
FreeAndNil(FHelloWorld);
if not Assigned(FHelloWorld) then
ShowMessage('Code explicitly ensured the reference to the form was cleared.');
FForm4b.Free;
if Assigned(FForm4b) then
ShowMessage('If you do not clear the reference, the form variable *remains* assigned.');
{Finally close all TForm4 instances through the UI
(if your form settings allow it), and call the following.}
if Assigned(FForm4a) then
ShowMessage('Form4a is assigned.');
if Assigned(FForm4b) then
ShowMessage('Form4b is assigned.');
if Assigned(FHelloWorld) then
ShowMessage('HelloWorld is assigned.');
So how can you solve your problem?
For a start if you want to know about forms that are on Panel2, check Panel2! Don't waste time checking global variables that have nothing to do with what's on Panel2.
function TMainForm.DoesForm4Exist(): Boolean;
begin
Result := True;
for I := 0 to Panel2.ControlCount-1 do
begin
if (Panel2.Controls[I] is TForm4) then Exit;
end;
Result := False;
end;
You now have a reliable way to check if Panel2 currently has a TForm4 instance. And this should set you on the right path.
Improving the solution
You can make the above code generic so you can reuse it for your other forms. At the same time, you can return a reference to the form, so you interact with it programatically. E.g. to destroy it:
function TMainForm.FindForm(AFormClass: TFormClass): TForm;
begin
Result := nil;
for I := 0 to Panel2.ControlCount-1 do
begin
if (Panel2.Controls[I] is AFormClass) then
begin
Result := TForm(Panel2.Controls[I])
Exit;
end;
end;
end;
{Example using the code to find and destroy a TForm4 instance}
LForm := FindForm(TForm4);
if Assigned(LForm) then LForm.Free;
You can similarly make the form creation generic as per Tom's answer.
Your main problem is, you're trying to solve a problem that doesn't exist. Mainly, you don't need to keep track of the form that is potentially being displayed in the panel and hence you don't have to use FreeAndNil at all.
That is because you can ask the panel anytime what it has got, and since you know what form you are attempting to display when you click a button, you can specifically ask the panel if it has got that kind of specific form.
procedure DisplayFormInPanel(Panel: TPanel; FormClass: TFormClass);
begin
if (Panel.ControlCount > 0) and not (Panel.Controls[0] is FormClass) then
Panel.Controls[0].Free;
if Panel.ControlCount = 0 then
begin
with FormClass.Create(Panel) do begin
Parent := Panel;
Show;
end;
end;
end;
The above procedure gets rid of any possible form that is not of the kind you want to display. If there's already a form that is of the kind that is going to be displayed, then nothing happens - the form stays in the panel.
Call it like:
procedure TForm1.Button1Click(Sender: TObject);
begin
DisplayFormInPanel(Panel2, TForm4);
end;
And your button click handler that closes any form could be as simple as
if Panel.ControlCount > 0 then
Panel.Controls[0].Free;
Regarding the DCC error, there is no point in pursuing that because the reference in the controls array is not the same as the "form4" reference. Even if you could nil that reference, it wouldn't effect "form4" and hence would not help with testing for Assigned. And the reference in the controls array is going to be removed from the controls array as soon as the form is freed anyway.

How to call another form using a panel in the main form?

I am coding in Delphi 10. I have two forms: FormPrincipal, which is the main form, and Formbanco the one I want to call.
In FormPrincipal I put a panel PanelCorpo and I want to call Formbanco and show it in the position of this panel.
I have tried two methods, but both did not work. See below:
1st) FormPrincpal calling Formbanco using Showmodal:
// TActionlist OnExecute event
procedure TFormPrincipal.AbreFormBancoExecute(Sender: TObject);
begin
try
Application.CreateForm(Tformbanco,Formbanco);
Formbanco.Parent := PanelCorpo;
Formbanco.Align := alclient;
Formbanco.Showmodal;
finally
Freeandnil(formbanco);
end;
end;
The behavior was: it opened the called form Formbanco properly, but frozen. Both forms did not allow to focus!
2nd) FormPrincpal calling Formbanco using Show:
// TActionlist OnExecute event
procedure TFormPrincipal.AbreFormBancoExecute(Sender: TObject);
begin
try
Application.CreateForm(Tformbanco,Formbanco);
Formbanco.Parent := PanelCorpo;
Formbanco.Align := alclient;
Formbanco.Show;
finally
Freeandnil(formbanco);
end;
end;
The behaviour was: it blinks very quickly the Formbanco, almost not visible, and continues in FormPrincipal. I can't access Formbanco!
I do appreciate help on this.
A modal form can't be a child. So the second attempt using Show is better. The mistake there is to destroy the form. Remember that Show is asynchronous, so you destroy the form as soon as you create it. Don't do that. You will need to destroy it somewhere else, in response to another event. You will know what that should be.
The function should look like this:
procedure TFormPrincipal.AbreFormBancoExecute(Sender: TObject);
begin
Formbanco := Tformbanco.Create(Self);
Formbanco.Parent := PanelCorpo;
Formbanco.Align := alclient;
Formbanco.BorderIcons := [];
Formbanco.BorderStyle := bsNone;
Formbanco.Show;
end;
If you use TForm as a panel, don't do it. Create TFrame and include it as every component on your FormPrincipal.
If you want to include the content of a form on an other (inside a panel, a tabcontrol, ...) you can only do if you put a tpanel (or tlayer) as top level parent on the second form. When you want to include it on an other form change it's Parent property (and it's alignment if necessary). You don't have to show your second form in this case (but of course create it) : the panel/layout is on first one and showed on it.

SIGSEGV error when creating a new form

I have a main form with a TMainMenu and when I click on a submenu I have to show a new form. This is the code:
procedure TForm1.MenuItem12Click(Sender: TObject);
var Form2 : TForm2;
begin
Form2 := TForm2.Create(nil);
Form2.Show;
end;
And the in the Uses I put Unit2, which is the name of the 2nd form's unit. When I run the program, It correctly opens the Form 2. By the way, when I close the program I have a SIGSEGV error with Lazarus.
How could I avoid it? I have used this code too in other programs, but I had no problems. Both forms are setted on FormStyle := fsNormal;
Most likely causes is a problem in the OnFormClose event of Form2 -OR- a flaw in the destructor code of any objects on Form2.
The only problem with the code you show is that you leak the form. You create it with no owner, and nothing else destroys it.
The obvious way to deal with that is to own it:
Form2 := TForm2.Create(Self);
This may not fix your error but it is the only thing wrong with the code you showed.

How to I keep forms from handling hotkeys when they don't have the focus?

We have a Delphi XE2 application. Form1 has hotkey Ctrl+F, and Form2, which is active, without hotkeys. The user presses Ctrl+F on Form2 and Form1 processes the hotkey. It's totally incorrect because we see activity of the inactive form. How do I fix it?
I implement all short cut key handling using actions. If you do this then you can use the centralisation that actions, action lists, action managers etc. provide to enable and disable all actions based on whether or not a form is active.
Do that, for example, by setting the action list's State property on the OnActivate and OnDeactivate event handlers of the form:
procedure TMyForm.FormActivate(Sender: TObject);
begin
ActionList.State := asNormal;
end;
procedure TMyForm.FormDeactivate(Sender: TObject);
begin
ActionList.State := asSuspended;
end;
You could add a check for the active form into the hotkey handler, ie
if(Screen.ActiveForm <> Self)then Exit;
This is assuming the eventhandler is implemented by Form1 (which is Self then inside the handler).
Just overwrite the following function of the inactive window that catches the action:
function IsShortCut(var Message: TWMKey): Boolean; override;
function TMainForm.IsShortCut(var Message: TWMKey): Boolean;
begin
if MainForm.Active then
result := inherited IsShortCut( Message )
else
result := false;
end;

Delphi XE3 form Open and Close

Can someone help me with this:
I have form 1 and form 2
in form1 1 :use form2.
in form1 put a button with code Form2.Showmodal;
form2 is made invisible
form2 has one button:
form2.close = works but does not close just hides the form.
-form2.free - either access violation or closes and the form1 is frozen (taskmngr to kill it)
Form2. release - acccess viololation or closes..if I click the open button on form1 to reopen the form it give access violation..
Form2.close + onClose action :=cafree; - access violation..
Form2.closemodal - has no effect..
how can I dispose and reuse form2 which is shown as modal from form1 ?
thanks a bunch..it has to be something simple Im overlooking.
s
form2.close = works but does not close just hides the form.
Yes, it does close the form. That is what the default behavior of a closed form is - to hide itself. In the case of a modal form, Close() merely set's the form's ModalResult to a non-zero value, which causes ShowModal() to exit and close/hide the form.
form2.free - either access violation or closes and the form1 is frozen (taskmngr to kill it)
It is not safe to Free() a form from inside of an event handler belonging to the same form. The VCL still needs to access the form object after the event handler exits. To safely free the form, you have to use Release() instead, which signals the form to automatically free itself at a later time when it is safe to do so.
Form2. release - acccess viololation or closes..if I click the open button on form1 to reopen the form it give access violation..
The only way Release() can cause an AV is if you are calling it using an invalid form pointer. If re-opening a form causes an AV, then you have some serious bugs in your code.
Form2.close + onClose action :=cafree; - access violation..
caFree causes the form to call Release() on itself. See above.
Form2.closemodal - has no effect..
You are not supposed to call CloseModal() directly. Use Close() or set the ModalResult instead.
First, remove Form 2 from auto-creating.
Project > Options > Forms
Remove Form 2 from "Auto-create forms"
This makes sure that this form is not automatically created.
When you create an instance of it, do not refer to it by its name (such as Form2). Instead, create a temporary variable. If you want to show it in the modal state, do it something like this:
procedure Button1Click(Sender: TObject);
var
F: TForm2;
begin
F:= TForm2.Create(nil);
try
F.ShowModal;
finally
F.Free;
end;
end;
Don't refer to your form by any name you may have given it, such as Form2. If you instantiate it as another variable as demonstrated above (with F), then make sure all calls you make to it are through this variable. In fact, as long as you remove this form from the auto-created forms, you may completely remove the declaration to this form:
var
Form2: TForm2;
If you want it to show in a non-modal state, while the main form is still accessible, it has to be done quite differently. Let me know if that's what you need, and I'll adjust my answer.
//this script for showing Form through Button with position
//change position by changing left or top by changing 120 and 300
//in Delphi 10.3 and above
// add form2 unit name in main unit in implementation as uses
//example
//implementation
//uses main;
procedure Button1Click(Sender: TObject);
var
F: TForm2; // Desired Form for Calling or showing
begin
F:= TForm2.Create(nil);
try
F.Left :=left+120; //Left position of Desired Form
F.Top :=top+300; //Top position of Desired Form
F.ShowModal;
finally
F.Free;
end;
end;