Pascal Newsletter #14
The full source code examples of this issue are available for download.
![]() |
![]() |
Pascal Newsletter #14 - 13-JAN-2001 INDEX 1. A FEW WORDS FROM THE EDITOR 2. KYLIX, WHEN??? 3. DRAWING CELLS IN A DBGRID 4. ERROR HANDLING IN DELPHI 5 (I) - EXCEPTIONS - WHERE DID THE ERROR OCCUR? - A LITTLE SIMPLIFICATION 5. GETTING THE USER NAME OF A WINDOWS SESSION 6. GETTING THE COMPUTER'S NETWORK NAME 7. GETTING THE DRIVE LETTER OF THE CD-ROM DRIVE UNIT 8. INVOKING THE DEFAULT MAILER PROGRAM TO SEND AN EMAIL 9. LINKS ________________________________________________________________________ 1. A FEW WORDS FROM THE EDITOR This publication can grow and improve thanks to the feedback it receives from the subscribers, and thus more subscribers will result in the better quality of this newsletter, and more valuable will be for all. For this reason I'd like to ask you to try to actively take part in this endeavor and share the existence of this newsletter among your friends, colleagues and acquaintances that might be interested in this publication. Regards, Ernesto De Spirito eds2004 @ latiumsoftware.com ________________________________________________________________________ JfControls Library. Multi-language. Multi-appearance. Skins. Privileges. More than 40 integrated and customizable components. Impressive GUI. Centralized resources administration. Multiple programming problems solved. For Delphi 3-7 and C++ Builder 3-6. http://www.jfactivesoft.com/ ________________________________________________________________________ 2. KYLIX, WHEN??? Delphi for Linux is now expected for the first quarter of this year, and apparently it'll be simultaneously released with Delphi 6 for Windows. At least, as a "by-product" of the delay Kylix will come with support Linux 2.4 kernel. The beta version that I mentioned in previous issues of this newsletter was given to the first people who joined the beta program, it is not freeware or shareware, it cannot be distributed, so it isn't available for download. I'm afraid you'll have to wait till the release of Kylix. It is quite likely that there will be a trial version as of other Borland software. The VCL will still be available in Delphi 6 for Windows, meant for Windows-only developments, but both Kylix and Delphi 6 for Windows will come with CLX (pronounced "clicks"), which is sort of a portable VCL that will hide OS specific implementation details. CLX will use QT, a cross-platform GUI library, enabling applications to run natively in both Windows and Linux with only a recompilation. Applications using the VCL without making Windows API calls will be easily converted to using CLX. CLX will be released under a dual license. Developers can purchase a commercial license or otherwise distribute their applications as GPL. Another particularity of CLX is that it'll be usable from any environment, being the purchase of Kylix not required (some command- line tools will be provided). The Borland Database Engine (BDE) won't be available for Linux, at least initially, but Midas will. Kylix will support MySql and Interbase, and probably other database vendors will provide datasets to access their databases by the time Kylix is on the market. Kylix will use GNOME and KDE, but in the first version it won't provide full support for either of the two. It is expected that it will improve its support for both desktops in a future version. ________________________________________________________________________ 3. DRAWING CELLS IN A DBGRID You can draw the contents of some cells in a DBGrid by assigning a procedure to handle its OnDrawColumnCell event. When DefaultDrawing is True, the DBGrid draws the cell before calling your OnDrawColumnCell event handler. If you intend to "hand-draw" all or many of the cells, you should set DefaultDrawing to False to avoid drawing the cells twice (once by the DBGrid and then by your procedure), and then you should draw all the cells yourself. Here is an example of how to do it: procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject; const Rect: TRect; DataCol: Integer; Column: TColumn; State: TGridDrawState); var X, Y, Index, RecNo: integer; DataSet: TDataset; Field: TField; CellText: string; begin with DBGrid1.Canvas do begin ... The first thing the procedure does is determine the background color, by checking if the cell has the focus, is selected, or is a fixed column. If it is a "normal" cell, I used the record number to display odd and even rows in different background colors: ... // Determine the background (and font) color with Brush do begin if gdFocused in State then begin Color := clHighlight; Font.Color := clHighlightText; end else if gdSelected in State then begin Color := clHighlight; Font.Color := clHighlightText; end else if gdFixed in State then begin Color := DBGrid1.FixedColor; end else begin if DataCol = 0 then Color := $AACCFF // Special color for the first column else begin // Determine the row number if DBGrid1.Datasource <> nil then begin DataSet := DBGrid1.Datasource.DataSet; if DataSet <> nil then begin RecNo := DataSet.RecNo; if RecNo = -1 then begin RecNo := DataSet.RecordCount + 1; if RecNo = 0 then RecNo := 1; end; end else RecNo := 1; end else RecNo := 1; if (RecNo And 1) = 0 then Color := $FFFFEE // Background color for odd rows else Color := $EEFFFF; // Background color for even rows end; end; end; ... Then I check if the column corresponds to a field, and if so I simply call DefaultDrawColumnCell to let this method draw the cell using the brush color and font properties I set before. You can see that in the case of the ItemsTotal field (I used the Orders.DB table that comes with the BDE), if its value is greater than 10,000 I display the value in red color and bold face: ... Field := Column.Field; if Field <> nil then begin // Make a special proviso for one field if Field.FieldName = 'ItemsTotal' then if Field.AsCurrency > 10000 then with Font do begin Color := $FF; Style := Style + [fsBold]; end; // Draw the cell by the default procedure DBGrid1.DefaultDrawColumnCell(Rect, DataCol, Column, State); end ... If the column does not correspond to a field then first of all I fill the cell with the background and then I check if it's a text column or a graphics column. In the first case, I provide the text, then determine the position where I should draw it inside the cell according to the alignment of the column, and finally draw it: ... else begin // Fill the cell with the background color FillRect(Rect); if DataCol = 7 then begin // You should supply the text for custom columns... CellText := 'Custom Text'; // Determine the position of the text case Column.Alignment of taRightJustify: X := Rect.Right - TextWidth(CellText) - 2; taCenter: X := (Rect.Right - Rect.Left - TextWidth(CellText)) div 2 + Rect.Left; else // taLeftJustify: X := Rect.Left + 2; end; // Draw the text TextOut(X, Rect.Top + 2, CellText); end ... Something similar happens with the graphic: ... else if DataCol = 8 then begin // ... or for example draw a graphics // Determine the position of the graphic inside the cell case Column.Alignment of taRightJustify: X := Rect.Right - 2 - 16; taCenter: X := (Rect.Right - Rect.Left - 16) div 2 + Rect.Left; else // taLeftJustify: X := Rect.Left + 2; end; Y := (Rect.Bottom - Rect.Top - 16) div 2 + Rect.Top + 1; if Table1.FieldByName('ItemsTotal').AsCurrency >= 10000 then Index := 0 else Index := 1; // Draw it ImageList1.Draw(DBGrid1.Canvas, X, Y, Index); end; end; if gdFocused in State then // Does the cell have the focus? DBGrid1.Canvas.DrawFocusRect(Rect); // Draw focus rectangle end; end; In the next issue I'll show you how to put a check box in a column for a boolean field. ________________________________________________________________________ 4. ERROR HANDLING IN DELPHI 5 (I) The purpose of this article is to show you how you can trap the errors that might occur in an application to display their corresponding messages, and since users never remember them, I'll show you how you can save the error messages in an error log. Also, with the help of some assembler code you'll see how to determine where the error happened. I want to apologize for including inline assembler, but I wanted to provide you with a useful unit you can use in your projects. Although I'm going to try to give some explanations, the inner workings of the unit I'm introducing here are difficult to understand, but you don't need to understand them to be able to use the unit in your projects. It's enough to have some knowledge of exceptions and learn roughly what the procedures in the unit do. You can get an idea by exploring the source code example you can find attached to the newsletter. EXCEPTIONS ========== Many errors that occur in an application are reported thru the mechanism of exceptions. You can trap the exceptions in a try..except..end block, as shown here: uses ErrHndlr; procedure TForm1.Button1Click(Sender: TObject); var StringList: TStringList; begin StringList := nil; try StringList := TStringList.Create; StringList.LoadFromFile('project1.dpe'); ShowMessage(StringList.Text); except on e: exception do begin ShowErrorBox(e.Message); SaveError(e.Message) end; end; StringList.Free; end; When an exception occurs inside a try block, the rest of the statements inside the block will be skipped and the execution flow will jump to the except part, which otherwise (if no exception occurs) would be omitted. In the above example, the method LoadFromFile will raise an exception when it attempts to open a file that doesn't exist, and therefore the ShowMessage procedure won't be called and the program will continue executing in the except block where we display an error message and we save it in the errors log by calling the ShowErrorBox and SaveError procedures respectively. We can define these procedures in a separate unit as follows: unit ErrHndlr; interface uses SysUtils; procedure ShowErrorBox(const ErrorMssg: string); procedure SaveError(ErrorMssg: string); implementation uses Forms, Classes, Windows; procedure ShowErrorBox(const ErrorMssg: string); begin Application.MessageBox(PChar(ErrorMssg), PChar(Application.Title), MB_ICONERROR); end; procedure SaveError(const ErrMessage: string); overload; var ErrorsLog: TFileStream; LogFile, ErrorMssg: String; begin ErrorsLog := nil; try // Determine the name of the log file LogFile := ChangeFileExt(Application.ExeName, '.err'); ErrorMssg := ErrMessage; // Open the log file if FileExists(LogFile) then begin ErrorsLog := TFileStream.Create(LogFile, fmOpenReadWrite + fmShareExclusive); // Go to the bottom of the file ErrorsLog.Position := ErrorsLog.Size; end else // It doesn't exist: create it. ErrorsLog := TFileStream.Create(LogFile, fmCreate); // Prepend the error message with the current date and time, and // append a CrLf pair (Carriage Return + Line Feed) to the end ErrorMssg := FormatDateTime('ddd dd-mmm-yyyy hh:nn:ss', Now) + ' - ' + ErrorMssg + #13#10; // Write to the file ErrorsLog.Write(Pointer(ErrorMssg)^, Length(ErrorMssg)); except end; // Close the file and free the stream ErrorsLog.Free; end; end. The code is simple. SaveError takes the string passed as parameter, for example 'Cannot open file ', and "formats" it prepending the current date and time to it, for example like this: Mon 08-Jan-2001 15:35:24 - Cannot open file project1.dpe and finally appends this line to the errors-log file. NOTE: We recommend that you change the code to determine the name of the log file because on Windows NT or Windows 2000 systems it might happen that the user doesn't have writing permissions for the directory where the application is located. Your application should read this value from an INI file or the Windows Registry. WHERE DID THE ERROR OCCUR? ========================== By examining the error log we can determine that an error occurred, which error it was, and when it happened, but so far we don't know where. If we had the address where the problem occurred then we could use AVFinder. For those who haven't heard of it, AVFinder is a useful freeware tool that helps you locate the procedure where an Access Violation (AV) occurred in a Delphi executable. You introduce the address that appears in the AV dialog box and AVFinder tells you which module, procedure and line number it belongs to. It can be downloaded from this address: http://www.planet-express.com/downloads/ For AVFinder to work you need to set the linker to generate a detailed map file: Project / Options... / Linker / Map file / Detailed. You should also have to compile your project with optimizations turned off for AVFinder to report the location of the AV (or the error) correctly: Project / Options... / Compiler / Code generation / Optimization (uncheck). Now, going back to our code, I overloaded SaveError with a new version that accepts an address as an extra parameter and include it (if it was supplied) when building the error line. procedure SaveError(const ErrMessage: string; Address: Pointer); overload; ... ErrorMssg := FormatDateTime('ddd dd-mmm-yyyy hh:nn:ss', Now) + ' - ' + ErrorMssg; if Address <> nil then ErrorMssg := ErrorMssg + ' @ ' + IntToHex(Integer(Address), 1); ErrorMssg := ErrorMssg + #13#10; ... Then if we modify our exception handling code to call SaveError passing it the address of the current procedure... procedure TForm1.Button1Click(Sender: TObject); ... try ... except on e: exception do begin ShowErrorBox(e.Message); SaveError(e.Message, @TForm1.Button1Click) end; end; ... ...the line saved to the error logs might look like this: Mon 08-Jan-2001 15:36:50 - Cannot open file project1.dpe @ 447484 It's ok, but now our code is not "generic", something that we can copy and paste without modifications. Moreover, if for example we changed the name of the form or the button, we would have to manually change the name of the event handler procedure in the SaveError line. It would be good if we had a way to determine the address of the current procedure more transparently. For this purpose I wrote a function named GetEIP that simply returns its return address, i.e. the point in the calling procedure where the program will resume its execution when the function returns. {$IFOPT W+} {$DEFINE STACKFRAMES_ON} {$W-} {$ENDIF} function GetEIP: Pointer; asm mov eax, [esp]; // Result := ESP^; // Return Address end; {$IFDEF STACKFRAMES_ON} {$W+} {$UNDEF STACKFRAMES_ON} {$ENDIF} I can't teach you assembler, but I'll try to explain you a little bit of what I did here. The CPU has a kind of "internal variables" which are called "registers", and they have names like for example EAX, EBX, ECX, EDX, ESI, EDI, EBP and ESP among others. Functions that return a 32-bit value (like a Pointer) should do so by placing the return value in the EAX register before returning to the caller. ESP is a special-purpose register and it's used to control the stack. When a function doesn't have a stack frame (we force it with the compiler directive $W-), immediately upon entering the function, ESP points to its return address, and in assembler we get the value a register is pointing to by enclosing the register within brackets ([ESP]). Now we can use a code like the following, which is "generic" (we can copy and paste it where we need it without having to change it): procedure TForm1.Button1Click(Sender: TObject); ... try ... except on e: exception do begin ShowErrorBox(e.Message); SaveError(e.Message, GetEIP); end; end; ... This would add a line like the following to the errors log: Mon 08-Jan-2001 15:38:47 - Cannot open file project1.dpe @ 44747C When you feed AVFinder with this address (for example 44747C), you'll see that it won't correspond to the beginning of the procedure (as it was when we passed @TForm1.Button1Click to SaveError), but now it corresponds to the SaveError line. This is more useful because in a procedure that has more than one try block, you can know in which of them the exception occurred. Thinking of simplifying things further, it would be nice if SaveError could automatically determine the address of the calling procedure or function, relieving us from having to pass it as a parameter. For this purpose I modified the first SaveError procedure so it just gets its return address and passes it to the new SaveError procedure: {$W+} // TURN STACKFRAMES ON procedure SaveError(const ErrMessage: string); overload; var Address: Pointer; begin asm // Address := %ReturnAddress%; mov edx, [ebp+4]; // EDX := (EBP+4)^; mov Address, edx; // Address := EDX; end; SaveError(ErrMessage, Address); end; When a procedure uses a stack frame, the return address is pointed by EBP+4 instead of ESP. I turned on stack frames at the beginning to make sure the procedure will use a stack frame. I used EDX instead of EAX for reasons of optimization (the EAX register is automatically being used to hold the ErrMessage parameter). Now we can call SaveError as before (without the Address parameter), and the address will be determined automatically: procedure TForm1.Button1Click(Sender: TObject); ... try ... except on e: exception do begin ShowErrorBox(e.Message); SaveError(e.Message); end; end; ... A LITTLE SIMPLIFICATION ======================= To simplify things a little bit more for the programmer, and yet provide even more information in the errors log, I wrote several overloaded versions of a procedure I named HandleError. For an exception handler, these procedures are useful: procedure HandleError(E: Exception); overload; var Address: Pointer; begin asm // Address := %ReturnAddress%; mov ecx, [ebp+4]; // ECX := (EBP+4)^; mov Address, ecx; // Address := ECX; end; HandleError(nil, E, Address); end; procedure HandleError(Sender: TObject; E: Exception); overload; var Address: Pointer; begin asm // Address := %ReturnAddress%; mov eax, [ebp+4]; // ECX := (EBP+4)^; mov Address, eax; // Address := ECX; end; HandleError(Sender, E, Address); end; As you can see, they are both wrappers for this procedure: procedure HandleError(Sender: TObject; E: Exception; Address: Pointer); overload; begin ShowErrorBox(E.Message); SaveError(Sender, E.ClassName + ': ' + E.Message, Address); end; It simply displays the message (calling ShowErrorBox), and then calls SaveError to save it: procedure SaveError(Sender: TObject; const ErrMessage: string; Address: Pointer); var ErrorMssg: string; begin ErrorMssg := ErrMessage; if Sender <> nil then begin ErrorMssg := ErrorMssg + ' ['; if Sender is TComponent then ErrorMssg := ErrorMssg + GetComponentFullName(TComponent(Sender)) else ErrorMssg := ErrorMssg + Sender.ClassName; ErrorMssg := ErrorMssg + ']'; end; SaveError(ErrorMssg, Address); end; Now, in your programs you can use a code like the following, which is simpler: procedure TForm1.Button1Click(Sender: TObject); ... try ... except on e: exception do HandleError(Sender, e); end; ... And the line saved to the errors log would contain more information: Mon 08-Jan-2001 15:38:47 - EFOpenError: Cannot open file project1.dpe [Form1.Button1] @ 44747C The good thing of passing the Sender parameter is that we can get a quick idea of where the error occurred without having to use AVFinder. - - - - - - - - - - - - - - - - - - - - - In the next issue I'll show you how to handle untrapped exceptions, API errors and custom errors. I hope you've found this article useful so far. If you have doubts or questions you can subscribe to our low-traffic mailing list. You can find more information here: http://www.latiumsoftware.com/en/forums.html ________________________________________________________________________ 5. GETTING THE USER NAME OF A WINDOWS SESSION If we need to know the name with which the user of the system has logged in to start his Windows session, we can use the Windows API function GetUserName. The next function encapsulates the call to this API to return the user name as a string. uses Windows; function GetLoginName: string; var buffer: array[0..255] of char; size: dword; begin size := 256; if GetUserName(buffer, size) then Result := buffer else Result := '' end; Sample call: procedure TForm1.Button1Click(Sender: TObject); begin ShowMessage(GetLoginName); end; ________________________________________________________________________ 6. GETTING THE COMPUTER'S NETWORK NAME If we want to know the name that identifies in a network the machine that is running our program, we can use the API Windows function GetComputerName that gives us the NetBIOS name of the local computer. The following function encapsulates the call to this API to return the machine name as a string: uses Windows; function GetComputerNetName: string; var buffer: array[0..255] of char; size: dword; begin size := 256; if GetComputerName(buffer, size) then Result := buffer else Result := '' end; Windows 2000 users can use the API GetComputerNameEx that apart from the NetBIOS name allows the retrieval of diverse DNS names. Sample call: procedure TForm1.Button1Click(Sender: TObject); begin ShowMessage(GetComputerNetName); end; ________________________________________________________________________ 7. GETTING THE DRIVE LETTER OF THE CD-ROM DRIVE UNIT To get the drive letter corresponding to the first CD-ROM drive in a system we'll make use of two API functions: GetLogicalDriveStrings and GetDriveType. With the first one we'll retrieve the list of logical drives in a buffer. This list is a sequence of null-terminated four-character-length strings (counting the null terminator), and it ends in a null character, for example: 'a:\'#0'b:\'#0'c:\'#0'd:\'#0'f:\'#0#0 With GetDriveType we can determine if a given drive is a CD-ROM drive by checking if the return value is the constant DRIVE_CDROM. The following function returns the first logical drive that corresponds to a CDROM drive. The function returns an empty string ('') if no CDROM drive was found: uses Windows; function GetFirstCdRomDrive: string; var r: LongWord; Drives: array[0..128] of char; pDrive: pchar; begin Result := ''; r := GetLogicalDriveStrings(sizeof(Drives), Drives); if r = 0 then exit; if r > sizeof(Drives) then raise Exception.Create(SysErrorMessage(ERROR_OUTOFMEMORY)); pDrive := Drives; // Point to the first drive while pDrive^ <> #0 do begin if GetDriveType(pDrive) = DRIVE_CDROM then begin Result := pDrive; exit; end; inc(pDrive, 4); // Point to the next drive end; end; Sample call: procedure TForm1.Button1Click(Sender: TObject); begin ShowMessage(GetFirstCdRomDrive); end; ________________________________________________________________________ 8. INVOKING THE DEFAULT MAILER PROGRAM TO SEND AN EMAIL You can invoke the "New Message" or "Compose Message" window of the default mailer program using the API function ShellExecute declared in the ShellApi unit, passing 'mailto:' as the third parameter (lpFile), as shown below: uses ShellAPI; procedure TForm1.Button1Click(Sender: TObject); begin ShellExecute(Self.Handle, nil, 'mailto:', nil, nil, SW_NORMAL); end; You can also add the email address of the recipient: procedure TForm1.Button1Click(Sender: TObject); begin ShellExecute(Self.Handle, nil, 'mailto:eds2004 @ latiumsoftware.com', nil, nil, SW_NORMAL); end; The subject line can be also be included: procedure TForm1.Button1Click(Sender: TObject); begin ShellExecute(Self.Handle, nil, 'mailto:eds2004 @ latiumsoftware.com?Subject=Test', nil, nil, SW_NORMAL); end; And even the body of the message: procedure TForm1.Button1Click(Sender: TObject); begin ShellExecute(Self.Handle, nil, 'mailto:eds2004 @ latiumsoftware.com' + '?Subject=Test&Body=Just testing the example', nil, nil, SW_NORMAL); end; NOTE: The mailto protocol doesn't support attachments. ________________________________________________________________________ 9. LINKS * JfControls. The most powerful integrated group of tools conceived for Delphi. New library of controls with totally innovative administration. http://www.jfactivesoft.com/ ________________________________________________________________________ YOU CAN HELP US We need your help to keep this newsletter going and growing. You can help by referring the newsletter to your colleagues: http://www.latiumsoftware.com/en/pascal/delphi-newsletter.php Or you can help by voting for us in some or all of these rankings to give more visibility to our web site and thus increase the number of subscriptions to this newsletter: http://www.sandbrooksoftware.com/cgi-bin/TopSite2/rankem.cgi?id=latium http://news.optimax.com/delphi/links/links.exe/click?id=70C517ECAE6E http://www.programmingpages.com/?r=latiumsoftwarecomenpascal http://www.top219.org/cgi-bin/vote.cgi?delphi&83 http://top100borland.com/in.php?who=20 http://top200.jazarsoft.com/delphi/rank.php3?id=latium http://213.65.224.200/cgi-bin/toplist.cgi/hits?Id=80 It's just a few seconds for you that REALLY mean a lot to us. ________________________________________________________________________ If you haven't received the full source code examples for this issue, you can get them from http://www.latiumsoftware.com/download/p0014.zip ________________________________________________________________________ This newsletter is provided "AS IS" without warranty of any kind. Its use implies the acceptance of our licensing terms and disclaimer of warranty you can read at http://www.latiumsoftware.com/en/legal.php where you will also find a note about legal trademarks. Articles are copyright of their respective authors and they are reproduced here with their permission. You can redistribute this newsletter as long as you do it in full (including copyright notices), without changes, and gratis. ________________________________________________________________________ Main page: http://www.latiumsoftware.com/en/pascal/delphi-newsletter.php Group home page: http://groups.yahoo.com/group/pascal-newsletter/ Subscribe/join: pascal-newsletter-subscribe@yahoogroups.com Unsubscribe/leave: pascal-newsletter-unsubscribe@yahoogroups.com Problems with your subscription? eds2004 @ latiumsoftware.com ________________________________________________________________________ Latium Software http://www.latiumsoftware.com/en/index.php Copyright (c) 2001 by Ernesto De Spirito. All rights reserved. ________________________________________________________________________ |
The full source code examples of this issue are available for download.
![]() |
Errors? Omissions? Comments? Please contact us!






