RandomClear icon

How to create custom error dialog

RandomClear | PRO | 11/25/13 12:21:05 PM UTC | 0 ⭐ | 1017 👁️ | Never ⏰ | []
Delphi |

9.46 KB

|

None

|

0 👍

/

0 👎

// See also: https://pastebin.com/fs1CwVfZ - "how to use VCL form as exception dialog"
// See also: https://pastebin.com/BFhcdsnh - "how to replace dialog icon"
// See also: https://pastebin.com/S5h3VB3J - "how to subclass without changing class name"
 
// You may be not satisfied by standard EurekaLog dialogs, so you may want to use your own dialog.
// Here is what you need to do: create class inheriting from abstract TBaseDialog class (EDialog unit), register it, and set ExceptionDialogType option. 
// The following sample shows 4 new dialog. 
//
// As you can see, the central method here is ShowModalInternal. It does all the work. 
// It's abstract and must be overwritten in child classes. 
// We use this method to show MessageBox, but you can do something else. 
// Like creating and showing VCL or FMX form.
//
// All other methods of TBaseDialog are virtual, but not abstract. They contain default behavior. 
// You can override them to alter behavior, but you don't have to. 
// Base dialog class contains large number of helpers (methods and properties). 
// All that dialog needs to do is to invoke these methods in right order. 
// Therefore any child class can use powerful tools to quickly build new dialog.
// Note: there is another abstract dialog class - TWinAPIDialog from EDialogWinAPI unit. 
// It's useful if you want to create new dialog based on direct WinAPI calls, 
// rather than using ready functions or frameworks (VCL/CLX/FMX).
// 
// Important note: dialog class is responsible for almost whole exception processing. 
// That's because "dialog" don't have to be visual. 
// Think about Win32 service, system log, WER (Windows Error Reporting), etc. 
// So, this is not always possible to distinguish between "error dialog" and "exception processing". 
// That's why these concepts are both controlled by single "dialog" class. 
// As we saw above, a major method for visual dialog is ShowModalInternal method. 
// But real entry point is Execute method. 
// You can see its default implementation in TBaseDialog.Execute.
 
uses
  EDialog, EClasses, ETypes;
 
type
  // "Empty" dialog that does nothing at all
  TNullDialog = class(TBaseDialog)
  protected
    procedure Beep; override;
    function ShowModalInternal: TResponse; override;
  public
    class function ThreadSafe: Boolean; override;
  end;
 
  // MessageBox dialog
  TMessageBoxDialog = class(TBaseDialog)
  protected
    function ShowModalInternal: TResponse; override;
    procedure Beep; override;
  public
    class function ThreadSafe: Boolean; override;
  end;
 
  // A variant of MessageBox with more detailed message (with call stack)
  TMessageBoxDetailedDialog = class(TMessageBoxDialog)
  protected
    function ExceptionMessage: String; override;
  end;
 
  // "Default" dialog - dialog that invokes standard dialog (non-EurekaLog)
  TRTLHandlerDialog = class(TBaseDialog)
  protected
    procedure Beep; override;
    function GetCallRTLExceptionEvent: Boolean; override;
    function ShowModalInternal: TResponse; override;
  end;
 
{ TNullDialog }
 
procedure TNullDialog.Beep;
begin
  // does nothing - no beep needed
end;
 
// Main method: do nothing, return success
function TNullDialog.ShowModalInternal: TResponse;
begin
  SetReproduceText(ReproduceText);
 
  Result.SendResult := srSent;
  Result.ErrorCode := ERROR_SUCCESS;
  Result.ErrorMessage := '';
end;
 
// Indicate that we can be called from any thread 
// (this should be False for VCL/CLX/FMX dialogs)
class function TNullDialog.ThreadSafe: Boolean;
begin
  Result := True;
end;
 
{ TMessageBoxDialog }
 
procedure TMessageBoxDialog.Beep;
begin
  // does nothing - beep is invoked by Windows.MessageBox in 
  // TMessageBoxDialog.ShowModalInternal
end;
 
// Main method 
function TMessageBoxDialog.ShowModalInternal: TResponse;
var
  Flags: Cardinal;
  Msg: String;
begin
  // Set default result
  Result.ErrorCode := ERROR_SUCCESS;
  Result.ErrorMessage := '';
  if SendErrorReportChecked then
    Result.SendResult := srSent
  else
    Result.SendResult := srCancelled;
 
  // Prepare message to show
  Msg := ExceptionMessage;
  if ShowSendErrorControl then
  begin
    Msg := Format(Options.CustomizedExpandedTexts[mtSend_AskSend], [Msg]);
    Flags := MB_YESNO;
  end
  else
    Flags := MB_OK;
  Flags := Flags or MB_ICONERROR or MB_TASKMODAL;
  if SendErrorReportChecked or (not ShowSendErrorControl) then
    Flags := Flags or MB_DEFBUTTON1
  else
    Flags := Flags or MB_DEFBUTTON2;
 
  // Call actual MessageBox and set result
  case MessageBox(Msg,
                  Options.CustomizedExpandedTexts[mtDialog_Caption],
                  Flags) of
    0: Result.ErrorCode := GetLastError;
    IDYes:
       Result.SendResult := srSent;
    IDNo:
       Result.SendResult := srCancelled;
  end;
 
  // Save error code/error message for failures
  if Result.ErrorCode <> ERROR_SUCCESS then
  begin
    Result.SendResult := srUnknownError;
    Result.ErrorMessage := SysErrorMessage(Result.ErrorCode);
  end
  else
    SetReproduceText(ReproduceText);
end;
 
// Can be called from any thread
class function TMessageBoxDialog.ThreadSafe: Boolean;
begin
  Result := True;
end;
 
{ TRTLHandlerDialog }
 
// Indicate desire to invoke RTL handler
function TRTLHandlerDialog.GetCallRTLExceptionEvent: Boolean;
begin
  Result := True;
end;
 
function TRTLHandlerDialog.ShowModalInternal: TResponse;
begin
  SetReproduceText(ReproduceText);
 
  Result.SendResult := srRestart;  // means "call RTL handler"
  Result.ErrorCode := ERROR_SUCCESS;
  Result.ErrorMessage := '';
end;
 
procedure TRTLHandlerDialog.Beep;
begin
  // Does nothing - transfer work to RTL handler
end;
 
{ TMessageBoxDetailedDialog }
 
// This one is a bit more complex - we want to add call stack to error message.
// However, default form is not very readable with variable-width fonts.
// That's why first we need a way to format call stack in another way.
 
type
  // Our new formatter
  TMessageBoxDetailedFormatter = class(TEurekaBaseStackFormatter)
  protected
    function GetItemText(const AIndex: Integer): String; override;
    function GetStrings: TStrings; override;
  end;
 
// Forms one line of call stack
function TMessageBoxDetailedFormatter.GetItemText(const AIndex: Integer): String;
var
  Cache: TEurekaDebugInfo;
  Info: PEurekaDebugInfo;
  ModuleName, UnitName, RoutineName, LineInfo: String;
begin
  Info := CallStack.GetItem(AIndex, Cache);
 
  ModuleName := ExtractFileName(Info^.Location.ModuleName);
  UnitName := Info^.Location.UnitName;
 
  if UnitName = ChangeFileExt(ModuleName, '') then
    UnitName := ''
  else
    UnitName := '.' + UnitName;
 
  RoutineName := CallStack.ComposeName
    (Info^.Location.ClassName, Info^.Location.ProcedureName);
  if RoutineName <> '' then
    RoutineName := '.' + RoutineName;
 
  if Info^.Location.LineNumber > 0 then
    LineInfo := Format(',%d[%d]', 
      [Info^.Location.LineNumber, Info^.Location.ProcOffsetLine]) 
  else
    LineInfo := '';
 
  Result := ModuleName + UnitName + RoutineName + LineInfo;
end;
 
// Formats entire call stack
function TMessageBoxDetailedFormatter.GetStrings: TStrings;
var
  ThreadID: Cardinal;
  I: Integer;
  Line: String;
  Stack: TEurekaBaseStackList;
begin
  if not Assigned(FStr) then
  begin
    FStr := TStringList.Create;
    FModified := True;
  end;
  if FModified then
  begin
    Stack := CallStack;
    CalculateLengths;
    FStr.BeginUpdate;
    try
      FStr.Clear;
      FStr.Capacity := Stack.Count;
 
      if Stack.Count > 0 then
      begin
        ThreadID := Stack.Items[0].ThreadID;
        for I := 0 to Stack.Count - 1 do
        begin
          if (Stack.Items[I].Location.Module <> 0) and
             (Stack.Items[I].Location.DebugDetail in [ddUnit..ddSourceCode]) and
             (Stack.Items[I].ThreadID = ThreadID) then
          begin
            Line := GetItemText(I);
            if (FStr.Count <= 0) or (FStr[FStr.Count - 1] <> Line) then
              FStr.Add(Line);
          end;
        end;
      end;
    finally
      FStr.EndUpdate;
    end;
    FModified := False;
  end;
  Result := FStr;
end;
 
// Append call stack to error message
function TMessageBoxDetailedDialog.ExceptionMessage: String;
const
  MaxLines = 15;
var
  Formatter: TMessageBoxDetailedFormatter;
  Stack: TEurekaBaseStackList;
begin
  {$WARNINGS OFF}
  // Abstract methods are intended here. 
  // It is like assert: they should not be called.
  Formatter := TMessageBoxDetailedFormatter.Create;
  {$WARNINGS ON}
  try
 
    if Assigned(CallStack) then
      Formatter.Assign(CallStack.Formatter);
    Formatter.CaptionHeader := '';
 
    Stack := nil;
    try
      if CallStack <> nil then
      begin
        Stack := TEurekaStackList.Create;
        Stack.Assign(CallStack);
        while Stack.Count > MaxLines do
          Stack.Delete(Stack.Count - 1);
      end;
      Result := inherited ExceptionMessage + sLineBreak + sLineBreak +
                CallStackToString(Stack, '', Formatter);
    finally
      FreeAndNil(Stack);
    end;
  finally
    FreeAndNil(Formatter);
  end;
end;
 
...
 
initialization
 
  RegisterDialogClass(TNullDialog);
  RegisterDialogClass(TMessageBoxDialog);
  RegisterDialogClass(TMessageBoxDetailedDialog);
  RegisterDialogClass(TRTLHandlerDialog);
 
end.
 
// Usage:
CurrentEurekaModuleOptions.ExceptionDialogType := TMessageBoxDetailedDialog.ClassName;

Comments