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
eds2008 @ 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-2006 & 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:eds2008 @ 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:eds2008 @ 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:eds2008 @ 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.programmingpages.com/?r=latiumsoftwarecomenpascal
http://top100borland.com/in.php?who=20
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/en/file.php?id=p14
________________________________________________________________________
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? eds2008 @ latiumsoftware.com
________________________________________________________________________
Latium Software http://www.latiumsoftware.com/en/index.php
Copyright (c) 2001 by Ernesto De Spirito. All rights reserved.
________________________________________________________________________
|