delphidialog

Delphi Access Violation at Address 00xxxxx


I'm learning Delphi, and I have a problem to show a dialog when I´m calling a private function which does the logic. It seems like an null pointer reference but I cannot find where it is.

image

Here is the code:

unit SandBox;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    AhojButton: TButton;
    procedure AhojButtonClick(Sender: TObject);

  private
    procedure ShowDialog(amount: Integer);

  public

  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.ShowDialog(amount: Integer);

  var td: TTaskDialog;
  var tb: TTaskDialogBaseButtonItem;
begin

  try
    td := TTaskDialog.Create(nil);
    tb := TTaskDialogBaseButtonItem.Create(nil);

    td.Caption := 'Warning';
    td.Text := 'Continue or Close?';
    td.MainIcon := tdiWarning;
    td.CommonButtons := [];

    tb := td.Buttons.Add;
    tb.Caption := 'Continue';
    tb.ModalResult := 100;

    tb := td.Buttons.Add;
    tb.Caption := 'Close';
    tb.ModalResult := 101;

    td.Execute;

    if td.ModalResult = 100 then
      ShowMessage('Continue')
    else if td.ModalResult = 101 then
      ShowMessage('Close');

  finally
    td.Free;
    tb.Free;
  end;

end;

procedure TForm1.AhojButtonClick(Sender: TObject);
begin
  ShowDialog(100);
end;

end.

I tried to instantiate and free both TTaskDialog and TTaskDialogBaseButtonItem.

This line throws the error:

tb := TTaskDialogBaseButtonItem.Create(nil);

Solution

  • You do indeed have a nil pointer dereference, and you can see the nil right in the line of code that you mentioned:

    tb := TTaskDialogBaseButtonItem.Create(nil);
                                           ^^^
    

    TTaskDialogBaseButtonItem is a TCollectionItem descendant. Its constructor takes a TCollection as input, but you are not passing one in. And although TCollectionItem itself does not require a TCollection, TTaskDialogBaseButtonItem does, as you can see in its constructor:

    constructor TTaskDialogBaseButtonItem.Create(Collection: TCollection);
    begin
      inherited;
      FCaption := '';
      FClient := TCustomtaskDialog(Collection.Owner); // <-- HERE
      FEnabled := True;
      FModalResult := ID + 100; // Avoid mrNone..mrYesToAll and IDOK..IDCONTINUE
    end;
    

    As you can see, the TCollection cannot be nil!

    If you compile your project with Debug DCUs enabled, and then run your code inside the debugger, then the debugger would have taken you right to this line of code when the AV occurs.

    So, to fix the error, you might think to pass in the dialog's Buttons collection, eg:

    tb := TTaskDialogBaseButtonItem.Create(td.Buttons);
    

    But this will throw a different error at runtime:

    Invalid property value

    This is because the Buttons collection actually expects TTaskDialogButtonItem, which is a descendant of TTaskDialogBaseButtonItem. So, change the Create call accordingly:

    tb := TTaskDialogButtonItem.Create(td.Buttons);
    

    Now, the code will run without crashing. However, your dialog will have a 3rd unwanted button in it!

    image

    Your code is also creating a memory leak, because you are assigning your tb variable to point at the button object that you create above, and then you re-assign tb to point at a different button object which you ask the dialog to create, but you never free the button that you create manually:

    tb := TTaskDialogButtonItem.Create(td.Buttons); // <-- leaked!
    ...
    tb := td.Buttons.Add;
    ...
    tb.Free; // <-- frees a button you don't own!
    

    You don't need to create an actual button object just to declare a variable that will point at button objects. Let the dialog create the actual button objects for you, your tb variable can merely point at them.

    Try this instead:

    procedure TForm1.ShowDialog(amount: Integer);
    var
      td: TTaskDialog;
      tb: TTaskDialogBaseButtonItem;
    begin
      td := TTaskDialog.Create(nil);
      try
        td.Caption := 'Warning';
        td.Text := 'Continue or Close?';
        td.MainIcon := tdiWarning;
        td.CommonButtons := [];
    
        tb := td.Buttons.Add;
        tb.Caption := 'Continue';
        tb.ModalResult := 100;
    
        tb := td.Buttons.Add;
        tb.Caption := 'Close';
        tb.ModalResult := 101;
    
        if td.Execute then
        begin
          if td.ModalResult = 100 then
            ShowMessage('Continue')
          else if td.ModalResult = 101 then
            ShowMessage('Close');
        end;
    
      finally
        td.Free;
      end;
    end;
    

    This will now show the desired dialog, will not crash, and will not leak memory:

    image