How to handle exceptions when creating FileStream - delphi

I have a function like this, that I would like to refactor
function Myfunction(sUrl, sFile: String) : Boolean;
var
GetData : TFileStream;
begin
Result := False;
//if the line below fails, I get an unhandled exception
GetData := TFileStream.Create(sFile, fmOpenWrite or fmCreate);
try
try
IdHTTP.Get(sUrl, GetData);
Result := (IdHTTP.ResponseCode = 200);
except
on E: Exception do begin
MessageBox(0, PChar(E.message), 'Niðurhala skrá', MB_ICONERROR or MB_OK);
end;
end;
finally
GetData.Free;
end;
end;
Procedure SomeOtherCode;
Begin
//How can I best defend against the unhandled exception above
//unless the call to the function is packed in a try .. except block
//the code jumps skips the if statement an goes to next
//exception block on the stack
if MyFunction('http://domain.com/file.html', 'c:\folder\file.html') then
ShowMessage('Got the file')
else
ShowMessage('Error !');
End
end;
Question:
Please refer to the comment within the procedure SomeOtherCode above.
Best Regards

Just wrap the code where you want to trap exceptions in a try..except block:
function MyFunction(...): Boolean;
var
Stream: TFileStream;
begin
Result := False;
try
Stream := TFileStream.Create(...);
try
// more code
Result := ...
finally
Stream.Free;
end;
except
// handle exception
end
end;

The whole point about exception handling is two-fold:
finally is for resource cleanup; you see this often in business logic
except is for reacting on specific exception (and getting rid of state logic through function results and intermediate variables); you hardly see it in business logic
In your case:
Myfunction should not return a Boolean, not contain an except block, and not perform a MessageBox but just let the exceptions propagate.
SomeOtherCode should contain the except block and tell the user what went wrong.
Example:
procedure Myfunction(sUrl, sFile: String);
var
GetData: TFileStream;
begin
Result := False;
//if the line below fails, I get an unhandled exception
GetData := TFileStream.Create(sFile, fmOpenWrite or fmCreate);
try
IdHTTP.Get(sUrl, GetData);
if (IdHTTP.ResponseCode <> 200) <> then
raise Exception.CreateFmt('Download of %s failed, return code %d', [sURl, IdHTTP.ResponseCode]);
finally
GetData.Free;
end;
end;
procedure SomeOtherCode:
begin
try
MyFunction('http://domain.com/file.html', 'c:\folder\file.html');
except
on E: Exception do begin
MessageBox(0, PChar(E.message), 'Niðurhala skrá', MB_ICONERROR or MB_OK);
end;
end;
end;
Now the code is much cleaner:
no more UI in your business logic
one place where your except is being handled
all failures are handled equally (cannot create file, download failure)
Good luck with this.
--jeroen

If you want your function to show messages to the user and return false on any failure, code it as follows:
function Myfunction(sUrl, sFile: String) : Boolean;
var
GetData : TFileStream;
begin
Result := False;
try
//if the line below fails, I get an unhandled exception
GetData := TFileStream.Create(sFile, fmOpenWrite or fmCreate);
try
try
IdHTTP.Get(sUrl, GetData);
Result := (IdHTTP.ResponseCode = 200);
except
on E: Exception do begin
MessageBox(0, PChar(E.message), 'Niðurhala skrá', MB_ICONERROR or MB_OK);
end;
end;
finally
GetData.Free;
end;
except
// you can handle specific exceptions (like file creation errors) or any exception here
end;
end;
Warning
IMHO this design is mixing business logic (such as get a resource/file from the Internet and save it to a file) and user interface logic (such as showing messages to the user in case of errors).
In general, is a better approach to separate business from UI logic, because your code is reusable.
For example you might want to re-factor as this:
function DownloadToAFile(const sUrl, sFile: string): boolean;
var
GetData : TFileStream;
begin
GetData := TFileStream.Create(sFile, fmOpenWrite or fmCreate);
try
IdHTTP.Get(sUrl, GetData);
Result := (IdHTTP.ResponseCode = 200);
finally
GetData.Free;
end;
end;
function UIDownloadToAFile(const sUrl, sFile: string): boolean;
begin
try
Result := DownloadToAFile(sURL, sFile);
except
on E: EIDException do //IndyError
MessageBox(0, PChar(E.message), 'Internet Error', MB_ICONERROR or MB_OK);
on E: EFileCreateError do //just can't remember the extact class name for this error
MessageBox(0, PChar(E.message), 'File create Error', MB_ICONERROR or MB_OK);
end;
end;
procedure SomeOtherCode:
begin
if UIDownloadToAFile('http://domain.com/file.html', 'c:\folder\file.html') then
ShowMessage('Got the file')
else
ShowMessage('Error !');
end;
Tomorrow, if you're writing a service, or a DataSnap module, you're free to use the DownloadToAFile or maybe to write a new ServiceDownloadToAFile wich in turns writes errors to a log or windows events, or maybe send a email notifying the HostAdmin about it.

One solution which is quite popular is to avoid 'success' or 'failure' return values completely. Instead of a function, use a procedure and handle failures using exceptions instead:
procedure Download(sUrl, sFile: String);
and then
try
Download ('http://domain.com/file.html', 'c:\folder\file.html');
ShowMessage('Got the file')
except
on E:Exxx do
begin
// handle exception
ShowMessage('Error !');
end
end;
This has also the effect that nobody can invoke the function and silently ignore the return value.

For some reason most people misuse except-finally combination. Correct sequence is
try
// allocate resource here
try
finally
// free resource here
end;
except
// handle exception here
end;
This lets you catch exceptions in constructor and destructor.

You should use only one try and get in this your all function code.

Related

What is real web-url for mORMot web-service?

Please help to understand about routing and web-url of web-service below.
type
TAirportService = class(TInterfacedObject, IAirportService)
public
procedure GetAirportDefinition(const AirPortID: integer; out Definition: TDTOAirportDefinition);
end;
procedure TAirportService.GetAirportDefinition(const AirPortID: integer;
out Definition: TDTOAirportDefinition);
begin
// create an object from static data
// (real application may use database and complex code to retrieve the values)
with Definition.Airport.Add do begin
Location := 'LAX';
Terminal := TRawUTF8DynArrayFrom(['terminalA', 'terminalB', 'terminalC']);
Gate := TRawUTF8DynArrayFrom(['gate1', 'gate2', 'gate3', 'gate4', 'gate5']);
BHS := 'Siemens';
DCS := 'Altiea';
end;
with Definition.Airline.Add do begin
CX := TRawUTF8DynArrayFrom(['B777', 'B737', 'A380', 'A320']);
QR := TRawUTF8DynArrayFrom(['A319', 'A380', 'B787']);
ET := '380';
SQ := 'A320';
end;
Definition.GroundHandler := TRawUTF8DynArrayFrom(['Swissport','SATS','Wings','TollData']);
end;
procedure StartWebService();
var
aModel: TSQLModel;
aDB: TSQLRestServer;
aServer: TSQLHttpServer;
begin
// set the logs level to only important events (reduce .log size)
TSQLLog.Family.Level := LOG_STACKTRACE+[sllInfo,sllServer];
// initialize the ORM data model
aModel := TSQLModel.Create([]);
try
// create a fast in-memory ORM server
aDB := TSQLRestServerFullMemory.Create(aModel,'test.json',false,false);
try
// register our TAirportServer implementation
// aDB.ServiceRegister(TServiceCalculator,[TypeInfo(ICalculatorXML)],sicShared);
aDB.ServiceRegister(TAirportService,[TypeInfo(IAirportService)],sicShared);
// launch the HTTP server
aServer := TSQLHttpServer.Create('8092', [aDB], '+', useHttpApiRegisteringURI);
try
aServer.AccessControlAllowOrigin := '*'; // allow cross-site AJAX queries
writeln('Background server is running'#10);
write('Press [Enter] to close the server.');
ConsoleWaitForEnterKey;
finally
aServer.Free;
end;
finally
aDB.Free;
end;
finally
aModel.Free;
end;
end;
I try to call follow web-urls:
http://localhost:8092/root/AirportService/GetAirportDefinition?AirPortID=1
http://localhost:8092/root/AirportService.GetAirportDefinition?AirPortID=1
http://localhost:8092/root/AirportService/GetAirportDefinition
http://localhost:8092/AirportService/GetAirportDefinition?AirPortID=1
http://localhost:8092/AirportService.GetAirportDefinition?AirPortID=1
http://localhost:8092/AirportService/GetAirportDefinition
but every time I get:
{
"errorCode":400,
"errorText":"Bad Request"
}
or Bad request
Where am I wrong?
A was wrong, really urls below works as needed:
http://localhost:8092/root/AirportService/GetAirportDefinition?AirPortID=1
http://localhost:8092/root/AirportService.GetAirportDefinition?AirPortID=1

How to properly start, working and finish a transaction?

I'm using MySQL, and I know that Nested Connection are not allowed - use "save points" for this - but I would like create a more generic code that could also be used with other DBMS.
So, I would like know how to properly start, working and finish a transaction in the code below?
Once ExampleDAO.Save() function could be used inside other function, like OtherExampleDAO.Save(), I need verify a transaction has been started before I try start a new one.
The lines with the verification if Assigned(dbTransaction) then always returns true, so how to properly verify if dbTransaction was instantiated?
function TExampleDAO.Save(const Example: TExample): Boolean;
var
dbxTransaction: TDBXTransaction;
begin
if Assigned(Example) then // prevents invalid object, like ExampleDAO.Save(nil);
begin
try
if (_connection.TransactionsSupported) AND
((not _connection.InTransaction) OR (_connection.MultipleTransactionsSupported)) then
begin
dbxTransaction := _connection.BeginTransaction(TDBXIsolations.ReadCommitted);
end;
try
// example
_sqlQuery.Close;
_sqlQuery.SQL.Clear;
_sqlQuery.SQL.Add('INSERT INTO example(a, b) '
+ 'VALUES(:a, :b)');
_sqlQuery.ParamByName('a').AsAnsiString := Example.A;
_sqlQuery.ParamByName('b').AsDateTime := Example.B;
_sqlQuery.ExecSQL(False);
// example info
_sqlQuery.Close;
_sqlQuery.SQL.Clear;
_sqlQuery.SQL.Add('INSERT INTO example_info(c, d) '
+ 'VALUES(:c, :d)');
_sqlQuery.ParamByName('c').AsInteger := Example.Info.C;
_sqlQuery.ParamByName('d').AsFloat := Example.Info.D;
_sqlQuery.ExecSQL(False);
if Assigned(dbxTransaction) then
_connection.CommitFreeAndNil(dbxTransaction);
Result := True;
except
on Exc:Exception do
begin
if Assigned(dbxTransaction) then
_connection.RollBackFreeAndNil(dbxTransaction);
raise Exc;
Result := False;
end;
end;
finally
if Assigned(dbxTransaction) then
FreeAndNil(dbxTransaction);
end;
end
else
begin
Result := False;
end;
end;
You need to properly initialize dbxTransaction to nil at the start of your function. Local variables in Delphi (on the Win32 platform, at least) are not initialized until a value is assigned to them, meaning that the content is unknown. Passing any value other than nil to Assigned will result in True. I recommend never testing a local variable's content on any platform until it has had a value assigned in your code.
Here's an example of how to make it work. (I've also removed the unnecessary assignment to Result in the exception block.)
function TExampleDAO.Salve(const Example: TExample): Boolean;
var
dbxTransaction: TDBXTransaction;
begin
dbxTransaction := nil; // Initialize the transaction variable here
if Assigned(Example) then // prevents invalid object, like ExampleDAO.Save(nil);
begin
try
if (_connection.TransactionsSupported) AND
((not _connection.InTransaction) OR (_connection.MultipleTransactionsSupported)) then
begin
dbxTransaction := _connection.BeginTransaction(TDBXIsolations.ReadCommitted);
end;
try
// example
_sqlQuery.Close;
_sqlQuery.SQL.Clear;
_sqlQuery.SQL.Add('INSERT INTO example(a, b) '
+ 'VALUES(:a, :b)');
_sqlQuery.ParamByName('a').AsAnsiString := Example.A;
_sqlQuery.ParamByName('b').AsDateTime := Example.B;
_sqlQuery.ExecSQL(False);
// example info
_sqlQuery.Close;
_sqlQuery.SQL.Clear;
_sqlQuery.SQL.Add('INSERT INTO example_info(c, d) '
+ 'VALUES(:c, :d)');
_sqlQuery.ParamByName('c').AsInteger := Example.Info.C;
_sqlQuery.ParamByName('d').AsFloat := Example.Info.D;
_sqlQuery.ExecSQL(False);
if Assigned(dbxTransaction) then
_connection.CommitFreeAndNil(dbxTransaction);
Result := True;
except
on Exc:Exception do
begin
if Assigned(dbxTransaction) then
_connection.RollBackFreeAndNil(dbxTransaction);
raise Exc;
end;
end;
finally
if Assigned(dbxTransaction) then
FreeAndNil(dbxTransaction);
end;
end
else
begin
Result := False;
end;
end;
As was noted by #SirRufo in the comments to your question, failing to pass Example as a parameter should probably raise an exception as well, which would mean that it could become a procedure instead of a function and Result would no longer apply at all.

Check Validity of a Zip File

When I try to check the validity of a zip file an exception is raised that the process can not access the file because it is being used by another process, yet the code in Open1.Click opens the zip file with no problem. Is there something wrong in Valid1Click?
procedure TForm1.Valid1Click(Sender: TObject);
{ Is the zip file valid. }
var
iZipFile: TZipFile;
iZipFilename: string;
iValid: Boolean;
begin
Screen.Cursor := crHourGlass;
try
{ Create the TZipFile Class }
iZipFile := TZipFile.Create;
try
if FileExists(ZipFilename1.Text) then
begin
iZipFilename := ZipFilename1.Text;
{ Open zip file for reading }
iZipFile.Open(iZipFilename, zmRead);
iValid := iZipFile.IsValid(iZipFilename);
if iValid then
MessageBox(0, 'The zip file is valid.', 'Check Zip File',
MB_ICONINFORMATION or MB_OK)
else
MessageBox(0, 'The zip file is NOT valid.', 'Check Zip File',
MB_ICONWARNING or MB_OK);
end
else
begin
MessageBox(0, 'The zip file does not exist.', 'Warning',
MB_ICONWARNING or MB_OK);
end;
{ Close the zip file }
iZipFile.Close;
finally
iZipFile.Free;
end;
finally
Screen.Cursor := crDefault;
end;
end;
procedure TForm1.Open1Click(Sender: TObject);
{ Open zip file. }
var
i: integer;
iZipFile: TZipFile;
iFilename: string;
iDateTime: TDateTime;
iCompressedSize: cardinal;
iUnCompressedSize: cardinal;
iCRC32: cardinal;
iCompressionMethod: word;
iFileComment: string;
iListItem: TlistItem;
begin
if OpenDialog1.Execute then
begin
if FileExists(OpenDialog1.FileName) then
begin
iZipFile := TZipFile.Create;
try
ListView1.Items.Clear;
ZipFilename1.Text := OpenDialog1.FileName;
try
iZipFile.Open(ZipFilename1.Text, zmReadWrite);
for i := 0 to iZipFile.FileCount - 1 do
begin
iFilename := iZipFile.FileNames[i];
iListItem := ListView1.Items.Add;
iListItem.Caption := iFilename;
iDateTime := FileDateToDateTime
(iZipFile.FileInfo[i].ModifiedDateTime);
iListItem.SubItems.Add(DateTimeToStr(iDateTime)); { 0 }
iCompressedSize := iZipFile.FileInfo[i].CompressedSize;
iListItem.SubItems.Add(FormatByteSize(iCompressedSize)); { 1 }
iUnCompressedSize := iZipFile.FileInfo[i].UncompressedSize;
iListItem.SubItems.Add(FormatByteSize(iUnCompressedSize)); { 2 }
iCRC32 := iZipFile.FileInfo[i].CRC32;
iListItem.SubItems.Add(IntToStr(iCRC32)); { 3 }
iCompressionMethod := iZipFile.FileInfo[i].CompressionMethod;
iListItem.SubItems.Add
(ZipCompressionToStr(iCompressionMethod)); { 4 }
iFileComment := iZipFile.Comment;
iListItem.SubItems.Add(iFileComment); { 5 }
end;
iZipFile.Close;
except
on E: Exception do
begin
ShowMessage(E.ClassName + #10#13 + E.Message);
end;
end;
finally
iZipFile.Free;
end;
end;
end;
You have these lines the wrong way round:
iZipFile.Open(iZipFilename, zmRead);
iValid := iZipFile.IsValid(iZipFilename);
The first line locks the file, and so the second line fails. You will have to call IsValid before calling Open.
Having said that, since you use zmRead, it should be possible for the call to IsValid to open the file again because the call to Open used fmOpenRead. So I suspect that there may be a bug in the ZIP file code, or the file stream code, in the version of Delphi that you are using. All the same, calling IsValid before Open is sure to work.
In fact, IsValid is a class method. You should call it like this:
iValid := TZipFile.IsValid(iZipFilename);
It comes to the same thing in the end, but it makes it clear to the reader of the code that the method call does not rely on the state of an instance.
In fact I personally would simply do away with the call to IsValid and go straight to calling Open. If that fails, I believe that a meaningful error message will be raised.
Update
Looks like you don't want to open the file at all, and just want to check its validity. In which case, you don't need an instance, you don't call the constructor, and you just use a single call to TZipFile.IsValid.
procedure TForm1.Valid1Click(Sender: TObject);
begin
Screen.Cursor := crHourGlass;
try
if FileExists(ZipFilename1.Text) then
begin
if TZipFile.IsValid(ZipFilename1.Text) then
...
finally
Screen.Cursor := crDefault;
end;
end;
As David Heffernan stated initially : it should be possible to use isValid after TZipFile::Open(filename, mode), since one would expect opening a file read only would not block it for others to read.
When using the Open-method based on a fileNAME (another one exists for reading f from a TFileStream), it internally creates a fileStream, and while it does specify fmOpenRead, it does not set a share mode on opening this stream.
See this blog post for an example on how this can be avoided by first creating a TFileStream yourself in which you specify the share mode explicitly :
https://www.digon.be/community/blog/TZipFile-problem-accessing-open-files-even-with-TZipMode-zmRead

Why is my code causing a I/O 104 error?

This program raises an I/O 104 error on EoF when first entering the while loop.
The purpose of the program is to look up if a username is already taken. The existing usernames are stored in a text file.
procedure TForm1.btnRegisterClick(Sender: TObject);
begin
sCUser := edtUserName.Text;
AssignFile(tNames, 'Names.txt');
begin
try
Reset(tNames);
except
ShowMessage('File not found');
Exit;
end;
end;
rewrite(tNames);
while not EoF(tNames) do // I get a I/O 104 Error here `
begin
Readln(tNames, sLine);
iPosComme := Pos(',', sLine);
sUser := Copy(sLine, 1, iPosComme - 1);
Delete(sLine, 1, iPosComme - 1);
if sCUser = sUser then begin
ShowMessage('Username taken');
end
else
begin
rewrite(tNames);
Writeln(tNames, sCUser + ',' + '0');
CloseFile(tNames);
end;
end;
end;
Remove the call to Rewrite()before Eof(). Even if you were not getting an IO error, your code would still fail because Rewrite() closes the file you opened with Reset() and then it creates a new bank file, so Eof() would always be True.
Update: error 104 is file not open for input, which means Reset() is not opening the file but is not raising an exception (which sounds like an RTL bug if Eof() is raising an exception, indicating that {I+} is active).
In any case, using AssignFile() and related routines is the old way to do file I/O. You should use newer techniques, like FileOpen() with FileRead(), TFileStream with TStreamReader, TStringList, etc...
Update: your loop logic is wrong. You are comparing only the first line. If it does not match the user, you are wiping out the file, writing the user to a new file, closing the file, and then continuing the loop. EoF() will then fail at that point. You need to rewrite your loop to the following:
procedure TForm1.btnRegisterClick(Sender: TObject
var
SCUser, sUser: String;
tNames: TextFile;
iPosComme: Integer;
Found: Boolean;
begin
sCUser := edtUserName.Text;
AssignFile(tNames,'Names.txt');
try
Reset(tNames);
except
ShowMessage('File not found');
Exit;
end;
try
Found := False;
while not EoF(tNames) do
begin
Readln(tNames,sLine);
iPosComme := Pos(',', sLine);
sUser := Copy(sLine ,1,iPosComme -1);
if sCUser = sUser then
begin
ShowMessage('Username taken') ;
Found := True;
Break;
end;
end;
if not Found then
Writeln(tNames,sCUser + ',0');
finally
CloseFile(tNames);
end;
end;
For the sake of completeness, this Version works for me, but it is hard to guess what the code is intended to do. Especially the while loop seems a bit displaced, since the file will contain exactly one line after the rewrite-case has ben hit once.
program wtf;
{$APPTYPE CONSOLE}
{$I+}
uses
SysUtils;
procedure Sample( sCUser : string);
var sUser, sLine : string;
iPosComme : Integer;
tnames : textfile;
begin
AssignFile(tNames,'Names.txt');
try
Reset(tNames);
except
Writeln('File not found');
Exit;
end;
while not EoF(tNames) do
begin
Readln(tNames,sLine);
iPosComme := Pos(',', sLine);
sUser := Copy(sLine ,1,iPosComme -1);
Delete( sLine,1, iPosComme -1);
if sCuser = sUser then begin
Writeln('Username taken') ;
end
else begin
Rewrite(tNames);
Writeln(tNames,sCUser + ',' + '0');
CloseFile(tNames);
Break; // file has been overwritten and closed
end;
end;
end;
begin
try
Sample('foobar');
except
on E: Exception do Writeln(E.ClassName, ': ', E.Message);
end;
end.
I wrote a version of this method that uses the newer TStreamReader and TStreamWriter classes.
This won't work with Delphi 7 of course, it's just to show how this could be done in newer versions of Delphi.
The code was heavily inspired by Remys answer.
procedure TForm1.btnRegisterClick(Sender: TObject);
var
Stream: TStream;
Reader: TStreamReader;
Writer: TStreamWriter;
Columns: TStringList;
UserName: string;
Found: Boolean;
FileName: string;
Encoding: TEncoding;
begin
FileName := ExpandFileName('Names.txt'); // An absolute path would be even better
UserName := edtUsername.Text;
Found := False;
Encoding := TEncoding.Default; // or another encoding, e.g. TEncoding.Unicode for Unicode
Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Reader := TStreamReader.Create(Stream, Encoding);
try
Columns := TStringList.Create;
try
Columns.Delimiter := ',';
Columns.StrictDelimiter := True; // or False, depending on the file format
while not Reader.EndOfStream do
begin
Columns.DelimitedText := Reader.ReadLine;
if Columns.Count > 0 then
begin
if AnsiSameStr(Columns[0], UserName) then // or AnsiSameText if UserName is not case-sensitive
begin
ShowMessage('Username taken') ;
Found := True;
Break;
end;
end;
end;
finally
Columns.Free;
end;
finally
Reader.Free;
end;
finally
Stream.Free;
end;
if not Found then
begin
Writer := TStreamWriter.Create(FileName, True, Encoding);
try
// Warning: This will cause problems when the file does not end with a new line
Writer.WriteLine(UserName + ',0');
finally
Writer.Free;
end;
end;
end;
If performance and memory usage are not a concern:
procedure TForm1.btnRegisterClick(Sender: TObject);
var
Rows: TStringList;
Columns: TStringList;
UserName: string;
Found: Boolean;
FileName: string;
Encoding: TEncoding;
Row: string;
begin
FileName := ExpandFileName('Names.txt'); // An absolute path would be even better
UserName := edtUsername.Text;
Found := False;
Encoding := TEncoding.Default; // or another encoding, e.g. TEncoding.Unicode for Unicode
Rows := TStringList.Create;
try
Rows.LoadFromFile(FileName, Encoding);
Columns := TStringList.Create;
try
Columns.Delimiter := ',';
Columns.StrictDelimiter := True; // or False, depending on the file format
for Row in Rows do
begin
Columns.DelimitedText := Row;
if Columns.Count > 0 then
begin
if AnsiSameStr(Columns[0], UserName) then // or AnsiSameText if UserName is not case-sensitive
begin
ShowMessage('Username taken') ;
Found := True;
Break;
end;
end;
end;
finally
Columns.Free;
end;
if not Found then
begin
Rows.Add(UserName + ',0');
Rows.SaveToFile(FileName, Encoding);
end;
finally
Rows.Free;
end;
end;
This solution can be adapted to Delphi 7 by removing the Encoding variable.
If it's part of a bigger database it should be stored in a real database management system rather than a text file.

delphi screen capture in global exception

I am working on a component, using Delphi 2006, the component retrieves system information and writes to file.
The requirement is such that I have to incorporate a global exception handler in the component, so when the exception occurs it will be caught and my custom message will be shown to the user.
procedure Tmy.GlobalExceptionHandlerThis(Sender : TObject; E : Exception );
begin
//catch the exception and show the message
TakeScreenShotAndSaveInapplicationFolder;
MessageDlg('Exception has Occured , Detail '+E.Message,mtError,[mbOK],0);
end;
This works fine but according to the requirement I have to capture the errorscreen shot (This is to find visually the form where the exception popped up)
So I did this, with take screenshot code from delphigeist.com:
procedure TakeScreenShotAndSaveInapplicationFolder;
var
thisBitmap: TBitmap;
sDate : string;
begin
DateSeparator :='_';
TimeSeparator:='_';
sDate :=DateTimeToStr(now);
thisBitmap := TBitmap.Create;
ScreenshotArea(thisBitmap, Screen.DesktopRect, True);
thisBitmap.SaveToFile(ExtractFilePath(Application.ExeName)+sDate+'.jpg');
FreeAndNil(thisBitmap);
end;
Problem:
When the exception occurs, I want to take the screen shot of the message also but with my code the this happens
Can anyone tell me how I can get the screen shot like this?
That is along the form get the message
MessageDlg('Exception has Occured, Detail ' + E.Message,mtError,[mbOK],0);
is modal, so after the message I can't take the screen shot. And before I can't also,
so when can I take the screen shot right when the exception message is displayed?
procedure Tmy.GlobalExceptionHandlerThis(Sender : TObject; E : Exception );
begin
//catch the exception and show the message
TakeScreenShotAndSaveInapplicationFolder;
MessageDlg('Exception has Occured , Detail '+E.Message,mtError,[mbOK],0);
TakeScreenShotAndSaveInapplicationFolder;
end;
Modify this message box (a wrapper around Windows.MessageBox), as follows:
{ TAwMessageBox }
type
TAwMessageBox = class(TObject)
private
FCaption: String;
FFlags: Cardinal;
FHookProc: TFarProc;
FText: String;
FWndHook: HHOOK;
function Execute: Integer;
procedure HookProc(var Message: THookMessage);
end;
function TAwMessageBox.Execute: Integer;
begin
try
try
FHookProc := MakeHookInstance(HookProc);
FWndHook := SetWindowsHookEx(WH_CALLWNDPROCRET, FHookProc, 0,
GetCurrentThreadID);
Result := Application.MessageBox(PChar(FText), PChar(FCaption), FFlags);
finally
if FWndHook <> 0 then
UnhookWindowsHookEx(FWndHook);
if FHookProc <> nil then
FreeHookInstance(FHookProc);
end;
except
Result := 0;
end;
end;
procedure TAwMessageBox.HookProc(var Message: THookMessage);
var
Data: PCWPRetStruct;
Title: array[0..255] of Char;
begin
with Message do
if nCode < 0 then
Result := CallNextHookEx(FWndHook, nCode, wParam, lParam)
else
Result := 0;
if Message.nCode = HC_ACTION then
begin
Data := PCWPRetStruct(Message.lParam);
if (Data.message = WM_ACTIVATE) and (LoWord(Data.wParam) = WA_INACTIVE) then
begin
ZeroMemory(#Title, SizeOf(Title));
GetWindowText(Data.hwnd, #Title, SizeOf(Title));
if String(Title) = FCaption then
begin
TakeScreenShotAndSaveInapplicationFolder;
UnhookWindowsHookEx(FWndHook);
FWndHook := 0;
FreeHookInstance(FHookProc);
FHookProc := nil;
end;
end;
end;
end;
function MsgBox(const Text: String; Flags: Cardinal;
const Caption: String): Integer;
begin
with TAwMessageBox.Create do
try
FCaption := Caption;
FFlags := Flags;
FText := Text;
Result := Execute;
finally
Free;
end;
end;
Testing code and screen shot:
procedure TForm1.ApplicationEvents1Exception(Sender: TObject; E: Exception);
begin
MsgBox('Exception has occured. Details:'#13#10#13#10 + E.Message,
MB_OK or MB_ICONERROR, 'Error');
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
raise Exception.Create('Test exception');
end;
Message dialogs (and exception dialogs) are modal so the first TakeScreenShotAndSaveInApplicationFolder call will execute before it displays and the second will not execute until after it has closed.
You could create your own message dialog which captures the screen as part of it's Show routine, but I'd suggest that you should look at what the customer wants, rather than what they have asked for and get a better solution.
From the sounds of it, they want to be able to see exactly what state the application was in at the time of the error. This means screengrab + error details, and I don't see why the error details need to be part of the screengrab specifically.
Why don't you look at using a 3rd party error logging system (MadExcept, JclDebug) and extend it to capture a screenshot of the application without the error message?
This would give you just as much information (more, due to the additional info that the exception logs can produce), without the headache of trying to screenshot error dialogs when they're raised.
Also, I'd question grabbing the entire desktop screen. It's prone to inadvertently grabbing sensitive information on background windows.
Reference Links:
Jcl - http://sourceforge.net/projects/jcl/
MadExcept - http://madshi.net/madExceptDescription.htm
Use your own custom form to show the error dialog and let that form control the screenshottaking.
i managed to get what i wanted, after going through #NGLN idea(answer above),and #Pieter B idea for taking screen shot by the form itself..
so i used the Open-Source-SynTaskDialog to display my exception message like this
procedure Tmy.GlobalExceptionHandlerThis(Sender : TObject; E : Exception );
begin
var Task: TTaskDialog;
begin
Task.Title:='Error message';
Task.Inst := 'An error/exception has occured';
Task.Content := 'the details are ...';
Task.Execute([],0,[],tiError ,tfiShield ,200);
end;
And inside the SynTaskDialog.pas i did this
procedure TTaskDialogForm.ButtonClick(Sender: TObject);
begin
TakeScreenShotAndSaveInapplicationFolder; {<--take the snap shot here..!!!}
if (Sender<>nil) and Sender.InheritsFrom(TSynButton) then
with TSynButton(Sender) do begin
self.Tag := Tag;
if Tag in [mrOk..mrNo] then
self.ModalResult := Tag;
Close;
end;
end;
this i did on button click as onshow the the snap shot was only half

Resources