0
votes

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:

  1. The user clicks on a button on the Loan form (works)
  2. The modal form DatePickerForm is created (owner: Application), then shown. (works)
  3. The user selects a date from the DatePicker control. (works)
  4. The User clicks on the OK button (fails)
  5. The DatePickerForm should close and we should return to the Loan form - but the error occurs instead.
  6. 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.
2

2 Answers

3
votes

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.

0
votes

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;