// 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;